stasis-rs 0.1.0

Durable AI orchestration framework with runtime jobs, lineage, and memory integration
Documentation
use std::sync::Arc;

use genai::chat::{ChatMessage, ChatRequest, ChatResponse};
use tokio::sync::mpsc;

use crate::domain::errors::{Result, StasisError};
use crate::ports::outbound::ai_chat_client::AiChatClient;
use crate::ports::outbound::ai_chat_client::StreamDelta;

#[derive(Clone, Debug, Default)]
pub struct PromptExecutionContext {
    pub trace_id: Option<String>,
    pub correlation_id: Option<String>,
    pub policy_profile: Option<String>,
    pub model_hint: Option<String>,
}

#[derive(Clone, Debug)]
pub struct PromptExecutionRequest {
    pub system_prompt: Option<String>,
    pub user_prompt: String,
    pub context: PromptExecutionContext,
}

impl PromptExecutionRequest {
    pub fn from_user_prompt(prompt: impl Into<String>) -> Self {
        Self {
            system_prompt: None,
            user_prompt: prompt.into(),
            context: PromptExecutionContext::default(),
        }
    }

    pub fn with_system_prompt(mut self, prompt: impl Into<String>) -> Self {
        self.system_prompt = Some(prompt.into());
        self
    }

    pub fn with_context(mut self, context: PromptExecutionContext) -> Self {
        self.context = context;
        self
    }
}

#[derive(Clone, Debug)]
pub struct PromptExecutionResponse {
    pub text: String,
    pub metadata: PromptExecutionContext,
}

#[derive(Clone, Debug)]
pub struct PromptChatCompletion {
    pub response: ChatResponse,
    pub metadata: PromptExecutionContext,
}

#[derive(Clone)]
pub struct PromptExecutionPipeline {
    chat_client: Arc<dyn AiChatClient>,
}

impl PromptExecutionPipeline {
    pub fn new(chat_client: Arc<dyn AiChatClient>) -> Self {
        Self { chat_client }
    }

    pub async fn complete_chat(
        &self,
        request: ChatRequest,
        context: PromptExecutionContext,
    ) -> Result<PromptChatCompletion> {
        let response = self.chat_client.complete(request, None).await?;
        Ok(PromptChatCompletion {
            response,
            metadata: context,
        })
    }

    pub async fn complete_chat_stream(
        &self,
        request: ChatRequest,
        context: PromptExecutionContext,
        chunk_tx: Option<&mpsc::UnboundedSender<StreamDelta>>,
    ) -> Result<PromptChatCompletion> {
        let response = self
            .chat_client
            .complete_stream(request, None, chunk_tx)
            .await?;
        Ok(PromptChatCompletion {
            response,
            metadata: context,
        })
    }

    pub async fn execute(
        &self,
        request: PromptExecutionRequest,
    ) -> Result<PromptExecutionResponse> {
        let context = request.context.clone();
        let mut messages = Vec::with_capacity(2);
        if let Some(system_prompt) = request.system_prompt {
            messages.push(ChatMessage::system(system_prompt));
        }
        messages.push(ChatMessage::user(request.user_prompt));

        let chat_response = self
            .complete_chat(ChatRequest::new(messages), context.clone())
            .await?
            .response;

        let text = chat_response
            .into_first_text()
            .map(|value| value.trim().to_string())
            .filter(|value| !value.is_empty())
            .ok_or_else(|| StasisError::PortFailure("chat response was empty".to_string()))?;

        Ok(PromptExecutionResponse {
            text,
            metadata: context,
        })
    }
}