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