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