Skip to main content

apollo/memory/
context_inject.rs

1//! Assemble Vellum-style context blocks before the user turn.
2
3use std::path::Path;
4use std::sync::Arc;
5
6use super::brief::{format_brief_xml, load_brief};
7use super::graph::GraphNode;
8use super::principal::{load_chat_aliases, resolve_history_chat_ids};
9use super::recall::build_supporting_recall;
10use super::surreal::SurrealMemory;
11use crate::memory::MemoryBackend;
12
13pub struct InjectConfig<'a> {
14    pub workspace: &'a Path,
15    pub principal_id: Option<&'a str>,
16    pub graph_recall_limit: usize,
17}
18
19pub async fn merged_history(
20    memory: &Arc<dyn MemoryBackend>,
21    chat_id: &str,
22    principal_id: Option<&str>,
23    per_chat_limit: usize,
24) -> anyhow::Result<Vec<(String, String)>> {
25    let Some(pid) = principal_id.filter(|s| !s.is_empty()) else {
26        return memory
27            .get_conversation_history(chat_id, per_chat_limit)
28            .await;
29    };
30
31    let aliases = load_chat_aliases(memory, pid).await;
32    let chat_ids = resolve_history_chat_ids(chat_id, pid, &aliases);
33    let mut merged: Vec<(String, String)> = Vec::new();
34
35    for cid in chat_ids {
36        let rows = memory
37            .get_conversation_history(&cid, per_chat_limit)
38            .await?;
39        for row in rows {
40            if !merged.contains(&row) {
41                merged.push(row);
42            }
43        }
44    }
45
46    if merged.len() > per_chat_limit {
47        merged = merged.split_off(merged.len().saturating_sub(per_chat_limit));
48    }
49    Ok(merged)
50}
51
52pub async fn personal_context_blocks(
53    memory: &Arc<dyn MemoryBackend>,
54    cfg: InjectConfig<'_>,
55    user_text: &str,
56) -> Vec<String> {
57    let mut blocks = Vec::new();
58
59    let (tc, ol) = load_brief(memory).await;
60    if let Some(b) = format_brief_xml(&tc, &ol) {
61        blocks.push(b);
62    }
63
64    if let Some(r) = build_supporting_recall(cfg.workspace, memory, user_text, 3).await {
65        blocks.push(r);
66    }
67
68    if let Some(surreal) = memory.as_any().downcast_ref::<SurrealMemory>() {
69        if let Ok(seed) = surreal
70            .graph_match_text(user_text, cfg.graph_recall_limit)
71            .await
72        {
73            let mut nodes: Vec<GraphNode> = seed;
74            if let Some(first) = nodes.first() {
75                if let Ok(neighbors) = surreal.graph_neighbors(&first.id, 3).await {
76                    for n in neighbors {
77                        if !nodes.iter().any(|x| x.id == n.id) {
78                            nodes.push(n);
79                        }
80                    }
81                }
82            }
83            if let Some(g) = surreal.format_graph_context(&nodes) {
84                blocks.push(g);
85            }
86        }
87    }
88
89    blocks
90}