Skip to main content

sqlite_graphrag/
output.rs

1use crate::errors::AppError;
2use serde::Serialize;
3
4#[derive(Debug, Clone, Copy, clap::ValueEnum, Default)]
5pub enum OutputFormat {
6    #[default]
7    Json,
8    Text,
9    Markdown,
10}
11
12#[derive(Debug, Clone, Copy, clap::ValueEnum, Default)]
13pub enum JsonOutputFormat {
14    #[default]
15    Json,
16}
17
18pub fn emit_json<T: Serialize>(value: &T) -> Result<(), AppError> {
19    let json = serde_json::to_string_pretty(value)?;
20    println!("{json}");
21    Ok(())
22}
23
24pub fn emit_json_compact<T: Serialize>(value: &T) -> Result<(), AppError> {
25    let json = serde_json::to_string(value)?;
26    println!("{json}");
27    Ok(())
28}
29
30pub fn emit_text(msg: &str) {
31    println!("{msg}");
32}
33
34pub fn emit_progress(msg: &str) {
35    tracing::info!(message = msg);
36}
37
38/// Emite mensagem de progresso bilíngue respeitando `--lang` ou `SQLITE_GRAPHRAG_LANG`.
39/// Uso: `output::emit_progress_i18n("Computing embedding...", "Calculando embedding...")`.
40pub fn emit_progress_i18n(en: &str, pt: &str) {
41    use crate::i18n::{current, Language};
42    match current() {
43        Language::English => tracing::info!(message = en),
44        Language::Portugues => tracing::info!(message = pt),
45    }
46}
47
48/// Payload JSON emitido pelo subcomando `remember`.
49///
50/// Todos os campos são obrigatórios no contrato JSON (ver `docs/schemas/remember.schema.json`).
51/// `operation` é alias de `action` para compatibilidade com clientes que usam o campo antigo.
52///
53/// # Examples
54///
55/// ```
56/// use sqlite_graphrag::output::RememberResponse;
57///
58/// let resp = RememberResponse {
59///     memory_id: 1,
60///     name: "nota-inicial".into(),
61///     namespace: "global".into(),
62///     action: "created".into(),
63///     operation: "created".into(),
64///     version: 1,
65///     entities_persisted: 0,
66///     relationships_persisted: 0,
67///     chunks_created: 1,
68///     merged_into_memory_id: None,
69///     warnings: vec![],
70///     created_at: 1_700_000_000,
71///     created_at_iso: "2023-11-14T22:13:20Z".into(),
72///     elapsed_ms: 42,
73/// };
74///
75/// let json = serde_json::to_string(&resp).unwrap();
76/// assert!(json.contains("\"memory_id\":1"));
77/// assert!(json.contains("\"elapsed_ms\":42"));
78/// assert!(json.contains("\"merged_into_memory_id\":null"));
79/// ```
80#[derive(Serialize)]
81pub struct RememberResponse {
82    pub memory_id: i64,
83    pub name: String,
84    pub namespace: String,
85    pub action: String,
86    /// Alias semântico de `action` para compatibilidade com contrato documentado em SKILL.md e AGENT_PROTOCOL.md.
87    pub operation: String,
88    pub version: i64,
89    pub entities_persisted: usize,
90    pub relationships_persisted: usize,
91    pub chunks_created: usize,
92    pub merged_into_memory_id: Option<i64>,
93    pub warnings: Vec<String>,
94    /// Timestamp Unix epoch seconds.
95    pub created_at: i64,
96    /// Timestamp RFC 3339 UTC string paralelo a `created_at` para parsers ISO 8601.
97    pub created_at_iso: String,
98    /// Tempo total de execução em milissegundos desde início do handler até serialização.
99    pub elapsed_ms: u64,
100}
101
102/// Item individual retornado pela consulta `recall`.
103///
104/// O campo `memory_type` é serializado como `"type"` no JSON para manter
105/// compatibilidade com clientes externos — o nome Rust usa `memory_type`
106/// para evitar conflito com a palavra reservada.
107///
108/// # Examples
109///
110/// ```
111/// use sqlite_graphrag::output::RecallItem;
112///
113/// let item = RecallItem {
114///     memory_id: 7,
115///     name: "nota-rust".into(),
116///     namespace: "global".into(),
117///     memory_type: "user".into(),
118///     description: "aprendizado de Rust".into(),
119///     snippet: "ownership e borrowing".into(),
120///     distance: 0.12,
121///     source: "direct".into(),
122/// };
123///
124/// let json = serde_json::to_string(&item).unwrap();
125/// // Campo Rust `memory_type` aparece como `"type"` no JSON.
126/// assert!(json.contains("\"type\":\"user\""));
127/// assert!(!json.contains("memory_type"));
128/// assert!(json.contains("\"distance\":0.12"));
129/// ```
130#[derive(Serialize, Clone)]
131pub struct RecallItem {
132    pub memory_id: i64,
133    pub name: String,
134    pub namespace: String,
135    #[serde(rename = "type")]
136    pub memory_type: String,
137    pub description: String,
138    pub snippet: String,
139    pub distance: f32,
140    pub source: String,
141}
142
143#[derive(Serialize)]
144pub struct RecallResponse {
145    pub query: String,
146    pub k: usize,
147    pub direct_matches: Vec<RecallItem>,
148    pub graph_matches: Vec<RecallItem>,
149    /// Alias agregado de `direct_matches` + `graph_matches` para contrato documentado em SKILL.md.
150    pub results: Vec<RecallItem>,
151    /// Tempo total de execução em milissegundos desde início do handler até serialização.
152    pub elapsed_ms: u64,
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158    use serde::Serialize;
159
160    #[derive(Serialize)]
161    struct Dummy {
162        val: u32,
163    }
164
165    // Tipo não-serializável para forçar erro de serialização JSON
166    struct NotSerializable;
167    impl Serialize for NotSerializable {
168        fn serialize<S: serde::Serializer>(&self, _: S) -> Result<S::Ok, S::Error> {
169            Err(serde::ser::Error::custom(
170                "falha intencional de serialização",
171            ))
172        }
173    }
174
175    #[test]
176    fn emit_json_retorna_ok_para_valor_valido() {
177        let v = Dummy { val: 42 };
178        assert!(emit_json(&v).is_ok());
179    }
180
181    #[test]
182    fn emit_json_retorna_erro_para_valor_nao_serializavel() {
183        let v = NotSerializable;
184        assert!(emit_json(&v).is_err());
185    }
186
187    #[test]
188    fn emit_json_compact_retorna_ok_para_valor_valido() {
189        let v = Dummy { val: 7 };
190        assert!(emit_json_compact(&v).is_ok());
191    }
192
193    #[test]
194    fn emit_json_compact_retorna_erro_para_valor_nao_serializavel() {
195        let v = NotSerializable;
196        assert!(emit_json_compact(&v).is_err());
197    }
198
199    #[test]
200    fn emit_text_nao_entra_em_panico() {
201        emit_text("mensagem de teste");
202    }
203
204    #[test]
205    fn emit_progress_nao_entra_em_panico() {
206        emit_progress("progresso de teste");
207    }
208
209    #[test]
210    fn remember_response_serializa_corretamente() {
211        let r = RememberResponse {
212            memory_id: 1,
213            name: "teste".to_string(),
214            namespace: "ns".to_string(),
215            action: "created".to_string(),
216            operation: "created".to_string(),
217            version: 1,
218            entities_persisted: 2,
219            relationships_persisted: 3,
220            chunks_created: 4,
221            merged_into_memory_id: None,
222            warnings: vec!["aviso".to_string()],
223            created_at: 1776569715,
224            created_at_iso: "2026-04-19T03:34:15Z".to_string(),
225            elapsed_ms: 123,
226        };
227        let json = serde_json::to_string(&r).unwrap();
228        assert!(json.contains("memory_id"));
229        assert!(json.contains("aviso"));
230        assert!(json.contains("\"namespace\""));
231        assert!(json.contains("\"merged_into_memory_id\""));
232        assert!(json.contains("\"operation\""));
233        assert!(json.contains("\"created_at\""));
234        assert!(json.contains("\"created_at_iso\""));
235        assert!(json.contains("\"elapsed_ms\""));
236    }
237
238    #[test]
239    fn recall_item_serializa_campo_type_renomeado() {
240        let item = RecallItem {
241            memory_id: 10,
242            name: "entidade".to_string(),
243            namespace: "ns".to_string(),
244            memory_type: "entity".to_string(),
245            description: "desc".to_string(),
246            snippet: "trecho".to_string(),
247            distance: 0.5,
248            source: "db".to_string(),
249        };
250        let json = serde_json::to_string(&item).unwrap();
251        assert!(json.contains("\"type\""));
252        assert!(!json.contains("memory_type"));
253    }
254
255    #[test]
256    fn recall_response_serializa_com_listas() {
257        let resp = RecallResponse {
258            query: "busca".to_string(),
259            k: 10,
260            direct_matches: vec![],
261            graph_matches: vec![],
262            results: vec![],
263            elapsed_ms: 42,
264        };
265        let json = serde_json::to_string(&resp).unwrap();
266        assert!(json.contains("direct_matches"));
267        assert!(json.contains("graph_matches"));
268        assert!(json.contains("\"k\":"));
269        assert!(json.contains("\"results\""));
270        assert!(json.contains("\"elapsed_ms\""));
271    }
272
273    #[test]
274    fn output_format_default_eh_json() {
275        let fmt = OutputFormat::default();
276        assert!(matches!(fmt, OutputFormat::Json));
277    }
278
279    #[test]
280    fn output_format_variantes_existem() {
281        let _text = OutputFormat::Text;
282        let _md = OutputFormat::Markdown;
283        let _json = OutputFormat::Json;
284    }
285
286    #[test]
287    fn recall_item_clone_produz_valor_igual() {
288        let item = RecallItem {
289            memory_id: 99,
290            name: "clone".to_string(),
291            namespace: "ns".to_string(),
292            memory_type: "relation".to_string(),
293            description: "d".to_string(),
294            snippet: "s".to_string(),
295            distance: 0.1,
296            source: "src".to_string(),
297        };
298        let cloned = item.clone();
299        assert_eq!(cloned.memory_id, item.memory_id);
300        assert_eq!(cloned.name, item.name);
301    }
302}