Skip to main content

recall_echo/
graph_bridge.rs

1//! Bridge between recall-echo and recall-graph.
2//!
3//! Provides graph ingestion for archived conversations.
4//! When pulse-null feature is enabled, also bridges LmProvider → LlmProvider.
5
6/// Ingest a conversation archive into the knowledge graph.
7///
8/// Non-blocking: logs warnings on failure but never fails the caller.
9/// Returns the ingestion report on success.
10pub async fn ingest_into_graph(
11    memory_dir: &std::path::Path,
12    archive_content: &str,
13    session_id: &str,
14    log_number: Option<u32>,
15) -> Result<crate::graph::types::IngestionReport, crate::error::RecallError> {
16    let graph_dir = memory_dir.join("graph");
17    if !graph_dir.exists() {
18        return Err(crate::error::RecallError::NotInitialized(
19            "graph/ not initialized \u{2014} run `graph init` first".into(),
20        ));
21    }
22
23    let gm = crate::graph::GraphMemory::open(&graph_dir).await?;
24
25    // No LLM provider in standalone mode — episodes only, no entity extraction
26    let report = gm
27        .ingest_archive(archive_content, session_id, log_number, None)
28        .await?;
29
30    eprintln!(
31        "recall-echo: graph ingested \u{2014} {} episodes, {} entities created, {} merged, {} skipped, {} relationships",
32        report.episodes_created,
33        report.entities_created,
34        report.entities_merged,
35        report.entities_skipped,
36        report.relationships_created,
37    );
38
39    if !report.errors.is_empty() {
40        eprintln!(
41            "recall-echo: graph ingestion had {} warnings",
42            report.errors.len()
43        );
44    }
45
46    Ok(report)
47}
48
49/// Ingest with an LLM provider for entity extraction.
50///
51/// When pulse-null feature is enabled, this bridges the LmProvider
52/// to recall-graph's LlmProvider for full entity/relationship extraction.
53#[cfg(feature = "pulse-null")]
54pub async fn ingest_into_graph_with_llm(
55    memory_dir: &std::path::Path,
56    archive_content: &str,
57    session_id: &str,
58    log_number: Option<u32>,
59    provider: Option<&dyn pulse_system_types::llm::LmProvider>,
60) -> Result<crate::graph::types::IngestionReport, crate::error::RecallError> {
61    let graph_dir = memory_dir.join("graph");
62    if !graph_dir.exists() {
63        return Err(crate::error::RecallError::NotInitialized(
64            "graph/ not initialized \u{2014} run `graph init` first".into(),
65        ));
66    }
67
68    let gm = crate::graph::GraphMemory::open(&graph_dir).await?;
69
70    let bridge = provider.map(GraphLlmBridge::new);
71    let llm_ref: Option<&dyn crate::graph::llm::LlmProvider> = bridge
72        .as_ref()
73        .map(|b| b as &dyn crate::graph::llm::LlmProvider);
74
75    let report = gm
76        .ingest_archive(archive_content, session_id, log_number, llm_ref)
77        .await?;
78
79    eprintln!(
80        "recall-echo: graph ingested \u{2014} {} episodes, {} entities created, {} merged, {} skipped, {} relationships",
81        report.episodes_created,
82        report.entities_created,
83        report.entities_merged,
84        report.entities_skipped,
85        report.relationships_created,
86    );
87
88    if !report.errors.is_empty() {
89        eprintln!(
90            "recall-echo: graph ingestion had {} warnings",
91            report.errors.len()
92        );
93    }
94
95    Ok(report)
96}
97
98/// Adapter that wraps an `pulse_system_types::LmProvider` to implement
99/// `crate::graph::LlmProvider`.
100#[cfg(feature = "pulse-null")]
101pub struct GraphLlmBridge<'a> {
102    provider: &'a dyn pulse_system_types::llm::LmProvider,
103}
104
105#[cfg(feature = "pulse-null")]
106impl<'a> GraphLlmBridge<'a> {
107    pub fn new(provider: &'a dyn pulse_system_types::llm::LmProvider) -> Self {
108        Self { provider }
109    }
110}
111
112#[cfg(feature = "pulse-null")]
113#[async_trait::async_trait]
114impl crate::graph::llm::LlmProvider for GraphLlmBridge<'_> {
115    async fn complete(
116        &self,
117        system_prompt: &str,
118        user_message: &str,
119        max_tokens: u32,
120    ) -> Result<String, crate::graph::error::GraphError> {
121        use pulse_system_types::llm::{Message, MessageContent, Role};
122
123        let messages = vec![Message {
124            role: Role::User,
125            content: MessageContent::Text(user_message.to_string()),
126            source: None,
127        }];
128
129        let response = self
130            .provider
131            .invoke(system_prompt, &messages, max_tokens, None)
132            .await
133            .map_err(|e| crate::graph::error::GraphError::Llm(e.to_string()))?;
134
135        Ok(response.text())
136    }
137}