Watch
1
0
Fork
You've already forked souveraine
0
souveraine/src/server/conversation.rs

128 lines
4.4 KiB
Rust

#![allow(dead_code)] // WIP scaffolding not yet wired
//! Server Conversation Handler
//!
//! Simplified conversation flow for server mode — one turn without the full
//! tool loop. Used for lightweight server-side interactions (the SSE streaming
//! path uses `server::turn::run_turn` for the full tool loop).
//!
//! Currently not called by any active code path (May 2026); kept as a
//! lighter-weight alternative to `run_turn` for future use.
use std::sync::Arc;
use crate::bridge::bifrost::{
ChatCompletionRequest, ContentPart, ImageUrlSource, Message as BifrostMessage,
};
use crate::bridge::LlmProvider;
use crate::core::session::{ContentBlock, ConversationMessage, Session};
pub struct ServerConversation {
pub session: Session,
provider: Arc<dyn LlmProvider>,
model: String,
}
pub struct ServerTurnResult {
pub response_text: String,
}
impl ServerConversation {
pub fn new(agent_name: &str, provider: Arc<dyn LlmProvider>, model: String) -> Self {
Self {
session: Session::new(agent_name),
provider,
model,
}
}
/// Simple turn without full tool loop (for now)
pub async fn turn(&mut self, user_input: &str) -> anyhow::Result<ServerTurnResult> {
// Add user message
self.session
.add_message(ConversationMessage::user_text(user_input));
// Build messages — flatten text blocks; ignore tool blocks
// until the server tool loop lands.
let messages: Vec<BifrostMessage> = self
.session
.messages
.iter()
.map(|m| {
let role = match m.role {
crate::core::session::MessageRole::System => "system",
crate::core::session::MessageRole::User => "user",
crate::core::session::MessageRole::Assistant => "assistant",
crate::core::session::MessageRole::Tool => "tool",
};
let has_images = m
.blocks
.iter()
.any(|b| matches!(b, ContentBlock::Image { .. }));
if has_images {
let parts: Vec<ContentPart> = m
.blocks
.iter()
.filter_map(|b| match b {
ContentBlock::Text { text } => {
Some(ContentPart::Text { text: text.clone() })
}
ContentBlock::Image { media_type, data } => {
let url = format!("data:{media_type};base64,{data}");
Some(ContentPart::ImageUrl {
image_url: ImageUrlSource { url },
})
}
_ => None,
})
.collect();
let text_content = m
.blocks
.iter()
.filter_map(|b| match b {
ContentBlock::Text { text } => Some(text.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n");
BifrostMessage::multimodal_user(text_content, parts)
} else {
let content = m
.blocks
.iter()
.filter_map(|b| match b {
ContentBlock::Text { text } => Some(text.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n");
BifrostMessage::text(role, content)
}
})
.collect();
// Call the active provider
let req = ChatCompletionRequest {
model: self.model.clone(),
messages,
stream: Some(false),
max_tokens: None,
temperature: None,
tools: None,
};
let response = self.provider.chat_completion(req).await?;
let content = response.content.clone();
// Store assistant response
self.session
.add_message(ConversationMessage::assistant_text(&content));
Ok(ServerTurnResult {
response_text: content,
})
}
}