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    /// Método de extração usado: "bert+regex" ou "regex-only". None se skip-extraction.
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub extraction_method: Option<String>,
95    pub merged_into_memory_id: Option<i64>,
96    pub warnings: Vec<String>,
97    /// Timestamp Unix epoch seconds.
98    pub created_at: i64,
99    /// Timestamp RFC 3339 UTC string paralelo a `created_at` para parsers ISO 8601.
100    pub created_at_iso: String,
101    /// Tempo total de execução em milissegundos desde início do handler até serialização.
102    pub elapsed_ms: u64,
103}
104
105/// Item individual retornado pela consulta `recall`.
106///
107/// O campo `memory_type` é serializado como `"type"` no JSON para manter
108/// compatibilidade com clientes externos — o nome Rust usa `memory_type`
109/// para evitar conflito com a palavra reservada.
110///
111/// # Examples
112///
113/// ```
114/// use sqlite_graphrag::output::RecallItem;
115///
116/// let item = RecallItem {
117///     memory_id: 7,
118///     name: "nota-rust".into(),
119///     namespace: "global".into(),
120///     memory_type: "user".into(),
121///     description: "aprendizado de Rust".into(),
122///     snippet: "ownership e borrowing".into(),
123///     distance: 0.12,
124///     source: "direct".into(),
125/// };
126///
127/// let json = serde_json::to_string(&item).unwrap();
128/// // Campo Rust `memory_type` aparece como `"type"` no JSON.
129/// assert!(json.contains("\"type\":\"user\""));
130/// assert!(!json.contains("memory_type"));
131/// assert!(json.contains("\"distance\":0.12"));
132/// ```
133#[derive(Serialize, Clone)]
134pub struct RecallItem {
135    pub memory_id: i64,
136    pub name: String,
137    pub namespace: String,
138    #[serde(rename = "type")]
139    pub memory_type: String,
140    pub description: String,
141    pub snippet: String,
142    pub distance: f32,
143    pub source: String,
144}
145
146#[derive(Serialize)]
147pub struct RecallResponse {
148    pub query: String,
149    pub k: usize,
150    pub direct_matches: Vec<RecallItem>,
151    pub graph_matches: Vec<RecallItem>,
152    /// Alias agregado de `direct_matches` + `graph_matches` para contrato documentado em SKILL.md.
153    pub results: Vec<RecallItem>,
154    /// Tempo total de execução em milissegundos desde início do handler até serialização.
155    pub elapsed_ms: u64,
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use serde::Serialize;
162
163    #[derive(Serialize)]
164    struct Dummy {
165        val: u32,
166    }
167
168    // Tipo não-serializável para forçar erro de serialização JSON
169    struct NotSerializable;
170    impl Serialize for NotSerializable {
171        fn serialize<S: serde::Serializer>(&self, _: S) -> Result<S::Ok, S::Error> {
172            Err(serde::ser::Error::custom(
173                "falha intencional de serialização",
174            ))
175        }
176    }
177
178    #[test]
179    fn emit_json_retorna_ok_para_valor_valido() {
180        let v = Dummy { val: 42 };
181        assert!(emit_json(&v).is_ok());
182    }
183
184    #[test]
185    fn emit_json_retorna_erro_para_valor_nao_serializavel() {
186        let v = NotSerializable;
187        assert!(emit_json(&v).is_err());
188    }
189
190    #[test]
191    fn emit_json_compact_retorna_ok_para_valor_valido() {
192        let v = Dummy { val: 7 };
193        assert!(emit_json_compact(&v).is_ok());
194    }
195
196    #[test]
197    fn emit_json_compact_retorna_erro_para_valor_nao_serializavel() {
198        let v = NotSerializable;
199        assert!(emit_json_compact(&v).is_err());
200    }
201
202    #[test]
203    fn emit_text_nao_entra_em_panico() {
204        emit_text("mensagem de teste");
205    }
206
207    #[test]
208    fn emit_progress_nao_entra_em_panico() {
209        emit_progress("progresso de teste");
210    }
211
212    #[test]
213    fn remember_response_serializa_corretamente() {
214        let r = RememberResponse {
215            memory_id: 1,
216            name: "teste".to_string(),
217            namespace: "ns".to_string(),
218            action: "created".to_string(),
219            operation: "created".to_string(),
220            version: 1,
221            entities_persisted: 2,
222            relationships_persisted: 3,
223            chunks_created: 4,
224            extraction_method: None,
225            merged_into_memory_id: None,
226            warnings: vec!["aviso".to_string()],
227            created_at: 1776569715,
228            created_at_iso: "2026-04-19T03:34:15Z".to_string(),
229            elapsed_ms: 123,
230        };
231        let json = serde_json::to_string(&r).unwrap();
232        assert!(json.contains("memory_id"));
233        assert!(json.contains("aviso"));
234        assert!(json.contains("\"namespace\""));
235        assert!(json.contains("\"merged_into_memory_id\""));
236        assert!(json.contains("\"operation\""));
237        assert!(json.contains("\"created_at\""));
238        assert!(json.contains("\"created_at_iso\""));
239        assert!(json.contains("\"elapsed_ms\""));
240    }
241
242    #[test]
243    fn recall_item_serializa_campo_type_renomeado() {
244        let item = RecallItem {
245            memory_id: 10,
246            name: "entidade".to_string(),
247            namespace: "ns".to_string(),
248            memory_type: "entity".to_string(),
249            description: "desc".to_string(),
250            snippet: "trecho".to_string(),
251            distance: 0.5,
252            source: "db".to_string(),
253        };
254        let json = serde_json::to_string(&item).unwrap();
255        assert!(json.contains("\"type\""));
256        assert!(!json.contains("memory_type"));
257    }
258
259    #[test]
260    fn recall_response_serializa_com_listas() {
261        let resp = RecallResponse {
262            query: "busca".to_string(),
263            k: 10,
264            direct_matches: vec![],
265            graph_matches: vec![],
266            results: vec![],
267            elapsed_ms: 42,
268        };
269        let json = serde_json::to_string(&resp).unwrap();
270        assert!(json.contains("direct_matches"));
271        assert!(json.contains("graph_matches"));
272        assert!(json.contains("\"k\":"));
273        assert!(json.contains("\"results\""));
274        assert!(json.contains("\"elapsed_ms\""));
275    }
276
277    #[test]
278    fn output_format_default_eh_json() {
279        let fmt = OutputFormat::default();
280        assert!(matches!(fmt, OutputFormat::Json));
281    }
282
283    #[test]
284    fn output_format_variantes_existem() {
285        let _text = OutputFormat::Text;
286        let _md = OutputFormat::Markdown;
287        let _json = OutputFormat::Json;
288    }
289
290    #[test]
291    fn recall_item_clone_produz_valor_igual() {
292        let item = RecallItem {
293            memory_id: 99,
294            name: "clone".to_string(),
295            namespace: "ns".to_string(),
296            memory_type: "relation".to_string(),
297            description: "d".to_string(),
298            snippet: "s".to_string(),
299            distance: 0.1,
300            source: "src".to_string(),
301        };
302        let cloned = item.clone();
303        assert_eq!(cloned.memory_id, item.memory_id);
304        assert_eq!(cloned.name, item.name);
305    }
306}