Skip to main content

alchemy_llm/utils/
json_parse.rs

1//! Partial JSON parsing for streaming tool calls.
2//!
3//! During streaming, tool call arguments arrive incrementally. These utilities
4//! attempt to parse partial JSON, returning empty objects on failure.
5
6use serde_json::Value;
7
8/// Parse potentially incomplete JSON from streaming tool calls.
9///
10/// During streaming, tool call arguments arrive incrementally. This function
11/// attempts to parse the partial JSON, returning an empty object if parsing fails.
12///
13/// # Strategy
14///
15/// 1. Try parsing as complete JSON
16/// 2. Try adding various closing brackets/braces
17/// 3. Fall back to empty object
18///
19/// # Example
20///
21/// ```
22/// use alchemy_llm::utils::json_parse::parse_streaming_json;
23/// use serde_json::json;
24///
25/// let partial = r#"{"name": "test", "value": 42"#;
26/// let result = parse_streaming_json(partial);
27/// assert_eq!(result, json!({"name": "test", "value": 42}));
28///
29/// // Complete JSON works too
30/// let complete = r#"{"name": "test"}"#;
31/// let result = parse_streaming_json(complete);
32/// assert_eq!(result, json!({"name": "test"}));
33/// ```
34pub fn parse_streaming_json(s: &str) -> Value {
35    // Try complete parse first
36    if let Ok(v) = serde_json::from_str(s) {
37        return v;
38    }
39
40    // Try common completions
41    let completions = ["}", "}}", "\"}", "\"}}", "null}", "null}}", "]}", "]}}"];
42
43    for suffix in completions {
44        let attempt = format!("{}{}", s, suffix);
45        if let Ok(v) = serde_json::from_str(&attempt) {
46            return v;
47        }
48    }
49
50    // Fall back to empty object
51    Value::Object(Default::default())
52}
53
54/// More sophisticated partial JSON parsing using bracket tracking.
55///
56/// Analyzes the input to determine exactly which brackets and braces
57/// need to be closed, preserving the nesting order for accurate parsing
58/// of complex nested structures.
59///
60/// # Example
61///
62/// ```
63/// use alchemy_llm::utils::json_parse::parse_streaming_json_smart;
64/// use serde_json::json;
65///
66/// let partial = r#"{"items": [1, 2, 3"#;
67/// let result = parse_streaming_json_smart(partial);
68/// assert_eq!(result, json!({"items": [1, 2, 3]}));
69/// ```
70pub fn parse_streaming_json_smart(s: &str) -> Value {
71    // Try complete parse first
72    if let Ok(v) = serde_json::from_str(s) {
73        return v;
74    }
75
76    // Track the nesting stack to preserve order
77    let mut stack: Vec<char> = Vec::new();
78    let mut in_string = false;
79    let mut escape_next = false;
80
81    for c in s.chars() {
82        if escape_next {
83            escape_next = false;
84            continue;
85        }
86
87        match c {
88            '\\' if in_string => escape_next = true,
89            '"' => in_string = !in_string,
90            '{' if !in_string => stack.push('{'),
91            '}' if !in_string => {
92                if stack.last() == Some(&'{') {
93                    stack.pop();
94                }
95            }
96            '[' if !in_string => stack.push('['),
97            ']' if !in_string => {
98                if stack.last() == Some(&'[') {
99                    stack.pop();
100                }
101            }
102            _ => {}
103        }
104    }
105
106    // Build completion string in reverse order of the stack
107    let mut completion = String::new();
108
109    // Close any unclosed string
110    if in_string {
111        completion.push('"');
112    }
113
114    // Close brackets/braces in reverse order (LIFO)
115    for opener in stack.iter().rev() {
116        match opener {
117            '{' => completion.push('}'),
118            '[' => completion.push(']'),
119            _ => {}
120        }
121    }
122
123    let attempt = format!("{}{}", s, completion);
124    serde_json::from_str(&attempt).unwrap_or_else(|_| Value::Object(Default::default()))
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use serde_json::json;
131
132    #[test]
133    fn test_complete_json() {
134        let result = parse_streaming_json(r#"{"name": "test"}"#);
135        assert_eq!(result, json!({"name": "test"}));
136    }
137
138    #[test]
139    fn test_missing_closing_brace() {
140        let result = parse_streaming_json(r#"{"name": "test""#);
141        assert_eq!(result, json!({"name": "test"}));
142    }
143
144    #[test]
145    fn test_nested_missing_braces() {
146        let result = parse_streaming_json(r#"{"outer": {"inner": 1"#);
147        assert_eq!(result, json!({"outer": {"inner": 1}}));
148    }
149
150    #[test]
151    fn test_empty_input() {
152        let result = parse_streaming_json("");
153        assert_eq!(result, json!({}));
154    }
155
156    #[test]
157    fn test_with_number_value() {
158        let result = parse_streaming_json(r#"{"count": 42"#);
159        assert_eq!(result, json!({"count": 42}));
160    }
161
162    #[test]
163    fn test_with_null_value() {
164        let result = parse_streaming_json(r#"{"value": null}"#);
165        assert_eq!(result, json!({"value": null}));
166    }
167
168    #[test]
169    fn test_incomplete_string_value() {
170        // When string is incomplete, basic parser may not handle it
171        let result = parse_streaming_json(r#"{"name": "test"#);
172        // Should return empty object since string isn't complete
173        assert!(result.is_object());
174    }
175
176    #[test]
177    fn test_smart_parser_complete() {
178        let result = parse_streaming_json_smart(r#"{"name": "test"}"#);
179        assert_eq!(result, json!({"name": "test"}));
180    }
181
182    #[test]
183    fn test_smart_parser_array() {
184        let result = parse_streaming_json_smart(r#"{"items": [1, 2, 3"#);
185        assert_eq!(result, json!({"items": [1, 2, 3]}));
186    }
187
188    #[test]
189    fn test_smart_parser_nested() {
190        let result = parse_streaming_json_smart(r#"{"a": {"b": {"c": 1"#);
191        assert_eq!(result, json!({"a": {"b": {"c": 1}}}));
192    }
193
194    #[test]
195    fn test_smart_parser_mixed() {
196        let result = parse_streaming_json_smart(r#"{"list": [{"id": 1}, {"id": 2"#);
197        assert_eq!(result, json!({"list": [{"id": 1}, {"id": 2}]}));
198    }
199
200    #[test]
201    fn test_smart_parser_unclosed_string() {
202        let result = parse_streaming_json_smart(r#"{"name": "test"#);
203        // Smart parser can close the string
204        assert_eq!(result, json!({"name": "test"}));
205    }
206
207    #[test]
208    fn test_smart_parser_escaped_quotes() {
209        let result = parse_streaming_json_smart(r#"{"text": "hello \"world\""}"#);
210        assert_eq!(result, json!({"text": "hello \"world\""}));
211    }
212
213    #[test]
214    fn test_smart_parser_deeply_nested() {
215        let result = parse_streaming_json_smart(r#"{"a": [{"b": [{"c": 1"#);
216        assert_eq!(result, json!({"a": [{"b": [{"c": 1}]}]}));
217    }
218
219    #[test]
220    fn test_smart_parser_empty() {
221        let result = parse_streaming_json_smart("");
222        assert_eq!(result, json!({}));
223    }
224
225    #[test]
226    fn test_smart_parser_just_brace() {
227        let result = parse_streaming_json_smart("{");
228        assert_eq!(result, json!({}));
229    }
230
231    #[test]
232    fn test_smart_parser_partial_key() {
233        // Partial key won't parse even with smart parser
234        let result = parse_streaming_json_smart(r#"{"na"#);
235        // Should fall back to empty object
236        assert!(result.is_object());
237    }
238}