use std::sync::Arc;
use anyhow::Result;
use async_trait::async_trait;
use tiktoken_rs::cl100k_base;
use chrono::Utc;
use praxis_llm::{ChatClient, Message, Content};
use praxis_persist::{PersistenceClient, DBMessage};
use crate::strategy::{ContextStrategy, ContextWindow};
use crate::templates::{DEFAULT_SYSTEM_PROMPT_TEMPLATE, DEFAULT_SUMMARIZATION_PROMPT};
pub struct DefaultContextStrategy {
max_tokens: usize,
llm_client: Arc<dyn ChatClient>,
system_prompt_template: String,
summarization_template: String,
}
impl DefaultContextStrategy {
pub fn new(
max_tokens: usize,
llm_client: Arc<dyn ChatClient>,
) -> Self {
Self {
max_tokens,
llm_client,
system_prompt_template: DEFAULT_SYSTEM_PROMPT_TEMPLATE.to_string(),
summarization_template: DEFAULT_SUMMARIZATION_PROMPT.to_string(),
}
}
pub fn with_templates(
max_tokens: usize,
llm_client: Arc<dyn ChatClient>,
system_prompt_template: String,
summarization_template: String,
) -> Self {
Self {
max_tokens,
llm_client,
system_prompt_template,
summarization_template,
}
}
fn count_tokens(&self, messages: &[DBMessage]) -> Result<usize> {
let bpe = cl100k_base().map_err(|e| anyhow::anyhow!("Tokenizer error: {}", e))?;
let mut total_tokens = 0;
for msg in messages {
let tokens = bpe.encode_with_special_tokens(&msg.content);
total_tokens += tokens.len();
}
Ok(total_tokens)
}
fn build_conversation_text(messages: &[DBMessage]) -> String {
messages.iter()
.map(|m| {
let role = match m.role {
praxis_persist::MessageRole::User => "User",
praxis_persist::MessageRole::Assistant => "Assistant",
};
format!("{}: {}", role, m.content)
})
.collect::<Vec<_>>()
.join("\n")
}
async fn generate_summary(&self, messages: &[DBMessage], previous_summary: Option<&str>) -> Result<String> {
let conversation = Self::build_conversation_text(messages);
let previous_summary_text = previous_summary.unwrap_or("Não temos resumo ainda.");
let summary_prompt = self.summarization_template
.replace("<previous_summary>", previous_summary_text)
.replace("<conversation>", &conversation);
let request = praxis_llm::ChatRequest::new(
"gpt-4o-mini".to_string(),
vec![Message::Human {
content: Content::text(summary_prompt),
name: None,
}],
);
let response = self.llm_client.chat(request).await?;
let summary = response.content.unwrap_or_else(|| "Summary generation failed".to_string());
Ok(summary)
}
fn build_system_prompt(&self, summary: Option<&str>) -> String {
let summary_text = summary.unwrap_or("Não temos resumo ainda.");
self.system_prompt_template.replace("<summary>", summary_text)
}
}
#[async_trait]
impl ContextStrategy for DefaultContextStrategy {
async fn get_context_window(
&self,
thread_id: &str,
persist_client: Arc<dyn PersistenceClient>,
) -> Result<ContextWindow> {
let thread = persist_client.get_thread(thread_id).await?
.ok_or_else(|| anyhow::anyhow!("Thread {} not found - should be created before sending messages", thread_id))?;
let messages_to_evaluate = persist_client
.get_messages_after(thread_id, thread.last_summary_update)
.await?;
let existing_summary = thread.summary.as_ref().map(|s| s.text.as_str());
if messages_to_evaluate.is_empty() {
return Ok(ContextWindow {
system_prompt: self.build_system_prompt(existing_summary),
messages: vec![],
});
}
let current_window_tokens = self.count_tokens(&messages_to_evaluate)?;
if current_window_tokens > self.max_tokens {
let messages_clone = messages_to_evaluate.clone();
let previous_summary = existing_summary.map(|s| s.to_string());
let persist_client_clone = Arc::clone(&persist_client);
let thread_id_owned = thread_id.to_string();
let strategy = Self {
max_tokens: self.max_tokens,
llm_client: self.llm_client.clone(),
system_prompt_template: self.system_prompt_template.clone(),
summarization_template: self.summarization_template.clone(),
};
tokio::spawn(async move {
if let Ok(summary_text) = strategy
.generate_summary(&messages_clone, previous_summary.as_deref())
.await {
let summary_time = Utc::now();
let _ = persist_client_clone.save_thread_summary(
&thread_id_owned,
summary_text,
summary_time
).await;
}
});
}
let system_prompt = self.build_system_prompt(existing_summary);
let llm_messages = messages_to_evaluate
.into_iter()
.filter_map(|msg| msg.try_into().ok())
.collect();
Ok(ContextWindow {
system_prompt,
messages: llm_messages,
})
}
}