Skip to main content

systemprompt_agent/services/
context.rs

1//! Reconstructing conversation history for a context into AI-ready messages,
2//! including decoding file parts and serializing artifacts as context.
3//!
4//! Copyright (c) systemprompt.io — Business Source License 1.1.
5//! See <https://systemprompt.io> for licensing details.
6
7use crate::services::a2a_server::processing::message::content::extract_message_content;
8use crate::services::shared::{AgentServiceError, Result};
9use systemprompt_models::text::truncate_with_ellipsis;
10use systemprompt_models::{AiMessage, MessageRole};
11
12use crate::models::a2a::Artifact;
13use crate::repository::task::TaskRepository;
14
15#[derive(Debug, Clone)]
16pub struct ContextService {
17    task_repo: TaskRepository,
18}
19
20impl ContextService {
21    #[must_use]
22    pub const fn new(task_repo: TaskRepository) -> Self {
23        Self { task_repo }
24    }
25
26    pub async fn load_conversation_history(
27        &self,
28        context_id: &systemprompt_identifiers::ContextId,
29    ) -> Result<Vec<AiMessage>> {
30        let tasks = self
31            .task_repo
32            .list_tasks_by_context(context_id)
33            .await
34            .map_err(|e| {
35                AgentServiceError::Internal(format!("Failed to load conversation history: {}", e))
36            })?;
37
38        let mut history_messages = Vec::new();
39
40        for task in tasks {
41            if let Some(task_history) = task.history {
42                for msg in task_history {
43                    let (text, parts) = extract_message_content(&msg);
44                    if text.is_empty() && parts.is_empty() {
45                        continue;
46                    }
47
48                    let role = match msg.role {
49                        crate::models::a2a::MessageRole::User => MessageRole::User,
50                        crate::models::a2a::MessageRole::Agent => MessageRole::Assistant,
51                    };
52
53                    history_messages.push(AiMessage {
54                        role,
55                        content: text,
56                        parts,
57                    });
58                }
59            }
60
61            if let Some(artifacts) = task.artifacts {
62                for artifact in artifacts {
63                    let artifact_content = Self::serialize_artifact_for_context(&artifact);
64                    history_messages.push(AiMessage {
65                        role: MessageRole::Assistant,
66                        content: artifact_content,
67                        parts: Vec::new(),
68                    });
69                }
70            }
71        }
72
73        Ok(history_messages)
74    }
75
76    fn serialize_artifact_for_context(artifact: &Artifact) -> String {
77        let artifact_name = artifact.title.as_deref().unwrap_or("unnamed");
78
79        let mut content = format!(
80            "[Artifact: {} (type: {}, id: {})]",
81            artifact_name, artifact.metadata.artifact_type, artifact.id
82        );
83
84        if let Some(description) = &artifact.description
85            && !description.is_empty()
86        {
87            let truncated = truncate_with_ellipsis(description, 300);
88            content.push_str(&format!("\n{truncated}"));
89        }
90
91        content
92    }
93}