#![allow(clippy::uninlined_format_args)]
use super::{add_salient_features_summary, extract_key_concepts, extract_key_steps};
use crate::embeddings::EmbeddingProvider;
use crate::episode::Episode;
use crate::semantic::summary::EpisodeSummary;
use crate::types::TaskOutcome;
use anyhow::Result;
use chrono::Utc;
#[derive(Debug, Clone)]
pub struct SemanticSummarizer {
#[allow(dead_code)]
min_summary_length: usize,
max_summary_length: usize,
max_key_steps: usize,
}
impl SemanticSummarizer {
#[must_use]
pub fn new() -> Self {
Self {
min_summary_length: 100,
max_summary_length: 200,
max_key_steps: 5,
}
}
#[must_use]
pub fn with_config(min_len: usize, max_len: usize, max_steps: usize) -> Self {
Self {
min_summary_length: min_len,
max_summary_length: max_len,
max_key_steps: max_steps,
}
}
#[allow(clippy::unused_async)]
pub async fn summarize_episode(&self, episode: &Episode) -> Result<EpisodeSummary> {
let key_concepts = extract_key_concepts(episode);
let key_steps = extract_key_steps(episode, self.max_key_steps);
let summary_text = self.generate_summary_text(episode);
Ok(EpisodeSummary {
episode_id: episode.episode_id,
summary_text,
key_concepts,
key_steps,
summary_embedding: None,
created_at: Utc::now(),
})
}
pub async fn summarize_with_embedding(
&self,
episode: &Episode,
embedding_provider: &impl EmbeddingProvider,
) -> Result<EpisodeSummary> {
let mut summary = self.summarize_episode(episode).await?;
let embedding = embedding_provider.embed_text(&summary.summary_text).await?;
summary.summary_embedding = Some(embedding);
Ok(summary)
}
#[must_use]
pub fn extract_key_concepts(&self, episode: &Episode) -> Vec<String> {
extract_key_concepts(episode)
}
#[must_use]
pub fn extract_key_steps(&self, episode: &Episode) -> Vec<String> {
extract_key_steps(episode, self.max_key_steps)
}
#[must_use]
pub fn generate_summary_text(&self, episode: &Episode) -> String {
let mut parts = Vec::new();
parts.push(format!(
"Task: {}.",
episode.task_description.trim_end_matches('.')
));
let mut context_parts = Vec::new();
if let Some(ref lang) = episode.context.language {
context_parts.push(format!("Language: {lang}"));
}
if let Some(ref framework) = episode.context.framework {
context_parts.push(format!("Framework: {framework}"));
}
if !episode.context.domain.is_empty() && episode.context.domain != "general" {
context_parts.push(format!("Domain: {}", episode.context.domain));
}
if !context_parts.is_empty() {
parts.push(format!("Context: {}.", context_parts.join(", ")));
}
if !episode.steps.is_empty() {
let total_steps = episode.steps.len();
let successful = episode.successful_steps_count();
let failed = episode.failed_steps_count();
let step_summary = if failed > 0 {
format!(
"Execution: {} steps ({} successful, {} failed)",
total_steps, successful, failed
)
} else {
format!("Execution: {} steps (all successful)", total_steps)
};
parts.push(format!("{step_summary}."));
let key_steps = extract_key_steps(episode, self.max_key_steps);
if !key_steps.is_empty() {
let steps_desc = key_steps.join("; ");
parts.push(format!("Key steps: {steps_desc}."));
}
}
if let Some(ref features) = episode.salient_features {
add_salient_features_summary(features, &mut parts);
}
if let Some(ref outcome) = episode.outcome {
let outcome_text = match outcome {
TaskOutcome::Success { verdict, artifacts } => {
if artifacts.is_empty() {
format!("Outcome: Success - {verdict}")
} else {
format!(
"Outcome: Success - {verdict}. Artifacts: {}",
artifacts.join(", ")
)
}
}
TaskOutcome::PartialSuccess {
verdict,
completed,
failed,
} => {
format!(
"Outcome: Partial success - {verdict}. Completed: {}. Failed: {}",
completed.join(", "),
failed.join(", ")
)
}
TaskOutcome::Failure {
reason,
error_details,
} => {
if let Some(details) = error_details {
format!("Outcome: Failure - {reason}. Details: {details}")
} else {
format!("Outcome: Failure - {reason}")
}
}
};
parts.push(format!("{outcome_text}."));
}
let mut summary = parts.join(" ");
let words: Vec<&str> = summary.split_whitespace().collect();
if words.len() > self.max_summary_length {
summary = words[..self.max_summary_length].join(" ");
summary.push_str("...");
}
summary
}
}
impl Default for SemanticSummarizer {
fn default() -> Self {
Self::new()
}
}