Skip to main content

lc_core/output_parsers/
json_parser.rs

1use async_trait::async_trait;
2use futures_util::Stream;
3use std::pin::Pin;
4
5use super::base::{BaseOutputParser, OutputParserError, OutputParserResult};
6use crate::language_models::LLMResult;
7use crate::runnables::{Runnable, RunnableConfig};
8use crate::structured_output::parser::PartialJsonParser;
9
10/// JSON 输出解析器
11///
12/// 将 LLM 输出的 JSON 字符串解析为 `serde_json::Value`。
13/// 支持:
14/// - 标准 JSON 解析
15/// - 从 Markdown 代码块中提取 JSON
16/// - 可选的部分 JSON 解析(用于流式场景)
17///
18/// 相当于 Python LangChain 的 `JsonOutputParser`。
19///
20/// # 示例
21/// ```ignore
22/// use langchainrust::output_parsers::JsonOutputParser;
23/// use serde_json::json;
24///
25/// let parser = JsonOutputParser::new();
26/// let result = parser.parse(r#"{"name": "Rust", "year": 2015}"#).await?;
27/// assert_eq!(result["name"], "Rust");
28/// ```
29pub struct JsonOutputParser {
30    /// 是否允许部分 JSON 解析(用于流式场景)
31    partial: bool,
32}
33
34impl JsonOutputParser {
35    /// 创建标准 JSON 输出解析器。
36    pub fn new() -> Self {
37        Self { partial: false }
38    }
39
40    /// 创建支持部分 JSON 解析的解析器
41    ///
42    /// 在流式场景中,LLM 可能输出不完整的 JSON,
43    /// 启用此选项后会尝试从中解析尽可能多的数据。
44    pub fn new_partial() -> Self {
45        Self { partial: true }
46    }
47
48    /// 从文本中提取 JSON 字符串
49    ///
50    /// 剥掉 Markdown 代码块 ```json ... ```、前导/尾随文本,返回真正的 JSON 值。
51    /// 比旧的 `find("```")` 匹配更稳:未闭合的围栏(只有开头没有结尾)也能正确剥离,
52    /// 且带围栏的完整 JSON 不会被误判为解析失败。
53    fn extract_json_str<'a>(&self, text: &'a str) -> OutputParserResult<&'a str> {
54        let json = PartialJsonParser::strip_markdown_fence(text);
55        if json.is_empty() {
56            // 没有 JSON 结构字符:交给调用方 serde 报错(避免返回空串被当成合法值)
57            Ok(text.trim())
58        } else {
59            Ok(json.trim())
60        }
61    }
62}
63
64impl Default for JsonOutputParser {
65    fn default() -> Self {
66        Self::new()
67    }
68}
69
70/// 取字符串前 `max_chars` 个字符用于错误预览。
71///
72/// 不能用字节截断:多字节 UTF-8 字符会被切在字符中间导致切片 panic
73/// (非法 CJK JSON 的错误路径曾按字节 200 截断而崩溃)。
74fn preview_slice(s: &str, max_chars: usize) -> &str {
75    match s.char_indices().nth(max_chars) {
76        // 第 max_chars 个字符的起始字节是安全边界,切到它即保留前 max_chars 个字符
77        Some((i, _)) => &s[..i],
78        None => s,
79    }
80}
81
82#[async_trait]
83impl BaseOutputParser<serde_json::Value> for JsonOutputParser {
84    async fn parse(&self, text: &str) -> OutputParserResult<serde_json::Value> {
85        let json_str = self.extract_json_str(text)?;
86
87        if self.partial {
88            self.parse_partial_json(json_str)
89        } else {
90            serde_json::from_str(json_str).map_err(|e| {
91                OutputParserError::JsonError(format!(
92                    "JSON parse failed (position {}:{}): {}, input: {}",
93                    e.line(),
94                    e.column(),
95                    e,
96                    preview_slice(json_str, 200)
97                ))
98            })
99        }
100    }
101
102    fn get_format_instructions(&self) -> String {
103        "请使用 JSON 格式输出,例如:{\"key\": \"value\"}。确保 JSON 是合法的。".to_string()
104    }
105}
106
107impl JsonOutputParser {
108    /// 尝试解析部分(不完整)JSON
109    ///
110    /// 在 LLM 流式输出场景中,逐步累积的 JSON 可能是不完整的。
111    /// 此方法尝试从中提取尽可能多的数据。
112    fn parse_partial_json(&self, text: &str) -> OutputParserResult<serde_json::Value> {
113        // 先尝试完整解析
114        if let Ok(value) = serde_json::from_str::<serde_json::Value>(text) {
115            return Ok(value);
116        }
117
118        // 尝试修复常见的不完整 JSON 模式
119        let repaired = self.repair_partial_json(text);
120        if let Ok(value) = serde_json::from_str::<serde_json::Value>(&repaired) {
121            return Ok(value);
122        }
123
124        Err(OutputParserError::JsonError(format!(
125            "partial JSON parse failed: {}",
126            preview_slice(text, 200)
127        )))
128    }
129
130    /// 修复不完整的 JSON 字符串
131    ///
132    /// 处理常见的不完整 JSON 格式,如:
133    /// - 末尾多余的逗号
134    /// - 不完整的字符串
135    /// - 不完整的对象/数组
136    fn repair_partial_json(&self, text: &str) -> String {
137        let mut repaired = text.trim().to_string();
138
139        // 处理以 `"` 结束的不完整字符串(去掉最后一个不完整的 token)
140        if let Some(stripped) = Self::strip_incomplete_token(&repaired) {
141            repaired = stripped;
142        }
143
144        // Scan tracking string state to correctly count braces/brackets
145        let mut in_string = false;
146        let mut escape_next = false;
147        let mut open_braces = 0usize;
148        let mut close_braces = 0usize;
149        let mut open_brackets = 0usize;
150        let mut close_brackets = 0usize;
151
152        for ch in repaired.chars() {
153            if escape_next {
154                escape_next = false;
155                continue;
156            }
157            if ch == '\\' && in_string {
158                escape_next = true;
159                continue;
160            }
161            if ch == '"' {
162                in_string = !in_string;
163                continue;
164            }
165            if !in_string {
166                match ch {
167                    '{' => open_braces += 1,
168                    '}' => close_braces += 1,
169                    '[' => open_brackets += 1,
170                    ']' => close_brackets += 1,
171                    _ => {}
172                }
173            }
174        }
175
176        // 补全括号
177        for _ in close_braces..open_braces {
178            repaired.push('}');
179        }
180
181        for _ in close_brackets..open_brackets {
182            repaired.push(']');
183        }
184
185        // 确保字符串以引号结束(如果开始了一个字符串)
186        // Scan forward (not backward) to find unclosed strings (M31)
187        let mut in_string = false;
188        let mut escape_next = false;
189        let mut last_open_quote_pos: Option<usize> = None;
190
191        for (i, ch) in repaired.char_indices() {
192            if escape_next {
193                escape_next = false;
194                continue;
195            }
196            if ch == '\\' && in_string {
197                escape_next = true;
198                continue;
199            }
200            if ch == '"' {
201                if in_string {
202                    in_string = false;
203                    last_open_quote_pos = None;
204                } else {
205                    in_string = true;
206                    last_open_quote_pos = Some(i);
207                }
208                continue;
209            }
210        }
211
212        // If we're still in a string at the end, truncate at the opening quote
213        // and close it, or just close it if the string value is partially complete
214        if in_string {
215            // Check if the unclosed string contains a newline (invalid in JSON strings)
216            if let Some(open_pos) = last_open_quote_pos {
217                let after_quote = &repaired[open_pos + 1..];
218                if after_quote.contains('\n') {
219                    // Truncate at the newline and close the string
220                    if let Some(pos) = repaired[open_pos + 1..].find('\n') {
221                        let newline_pos = pos + open_pos + 1;
222                        repaired.truncate(newline_pos);
223                        repaired.push('"');
224                    }
225                }
226            }
227        }
228
229        repaired
230    }
231
232    /// 去掉末尾的不完整 token
233    fn strip_incomplete_token(s: &str) -> Option<String> {
234        let trimmed = s.trim_end();
235
236        // If the string ends with an incomplete key or value token,
237        // try to find the last complete token boundary.
238        // Look for the last structural character (: , { [ }) and truncate after it.
239        let chars: Vec<char> = trimmed.chars().collect();
240        if chars.is_empty() {
241            return None;
242        }
243
244        // Scan backwards to find the last structural boundary
245        let mut i = chars.len();
246        while i > 0 {
247            i -= 1;
248            match chars[i] {
249                ',' | ':' | '{' | '[' | '}' | ']' => {
250                    // Found a structural character; truncate after it
251                    let truncate_at: usize = trimmed
252                        .char_indices()
253                        .nth(i + 1)
254                        .map(|(pos, _)| pos)
255                        .unwrap_or(trimmed.len());
256                    if truncate_at < s.len() {
257                        let result = trimmed[..truncate_at].to_string();
258                        if result != s.trim_end() {
259                            return Some(result);
260                        }
261                    }
262                    return None;
263                }
264                '"' => {
265                    // Check if this is a closing quote (even number of quotes before it)
266                    // If so, the JSON might be complete at this point
267                    return None;
268                }
269                _ => {}
270            }
271        }
272
273        None
274    }
275}
276
277#[async_trait]
278impl Runnable<LLMResult, serde_json::Value> for JsonOutputParser {
279    type Error = OutputParserError;
280
281    async fn invoke(
282        &self,
283        input: LLMResult,
284        _config: Option<RunnableConfig>,
285    ) -> Result<serde_json::Value, Self::Error> {
286        self.parse(&input.content).await
287    }
288
289    async fn stream(
290        &self,
291        input: LLMResult,
292        _config: Option<RunnableConfig>,
293    ) -> Result<
294        Pin<Box<dyn Stream<Item = Result<serde_json::Value, Self::Error>> + Send>>,
295        Self::Error,
296    > {
297        let result = self.parse(&input.content).await?;
298        let stream = futures_util::stream::once(async move { Ok(result) });
299        Ok(Box::pin(stream))
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    #[tokio::test]
308    async fn test_json_parser_standard_obj() {
309        let parser = JsonOutputParser::new();
310        let result = parser
311            .parse(r#"{"name": "Rust", "year": 2015}"#)
312            .await
313            .unwrap();
314        assert_eq!(result["name"], "Rust");
315        assert_eq!(result["year"], 2015);
316    }
317
318    #[tokio::test]
319    async fn test_json_parser_from_markdown_block() {
320        let parser = JsonOutputParser::new();
321        let input = "以下是结果:\n```json\n{\"status\": \"ok\"}\n```\n";
322        let result = parser.parse(input).await.unwrap();
323        assert_eq!(result["status"], "ok");
324    }
325
326    #[tokio::test]
327    async fn test_json_parser_array() {
328        let parser = JsonOutputParser::new();
329        let result = parser.parse("[1, 2, 3]").await.unwrap();
330        assert_eq!(result[0], 1);
331        assert_eq!(result[2], 3);
332    }
333
334    #[tokio::test]
335    async fn test_json_parser_from_markdown_block_unclosed_fence() {
336        // H4: 只有开头 ```json 没有结尾 ```(模型输出被截断)也要能剥掉围栏解析
337        let parser = JsonOutputParser::new();
338        let input = "以下是结果:\n```json\n{\"status\": \"ok\"}";
339        let result = parser.parse(input).await.unwrap();
340        assert_eq!(result["status"], "ok");
341    }
342
343    #[tokio::test]
344    async fn test_json_parser_from_prose_prefix() {
345        // H4: 模型先输出一句"结果是:"再给 JSON,也要剥掉前导文本
346        let parser = JsonOutputParser::new();
347        let input = "结果是:\n{\"a\": 1}\n以上";
348        let result = parser.parse(input).await.unwrap();
349        assert_eq!(result["a"], 1);
350    }
351
352    #[tokio::test]
353    async fn test_json_parser_invalid_json() {
354        let parser = JsonOutputParser::new();
355        let result = parser.parse("{invalid}").await;
356        assert!(result.is_err());
357    }
358
359    #[tokio::test]
360    async fn test_json_parser_format_instructions() {
361        let parser = JsonOutputParser::new();
362        let instructions = parser.get_format_instructions();
363        assert!(!instructions.is_empty());
364    }
365
366    #[tokio::test]
367    async fn test_json_parser_invoke_runnable() {
368        // Runnable 形态接收 LLMResult,取 content 字段解析
369        let parser = JsonOutputParser::new();
370        let result = parser
371            .invoke(
372                LLMResult {
373                    content: r#"{"key": "value"}"#.to_string(),
374                    ..Default::default()
375                },
376                None,
377            )
378            .await
379            .unwrap();
380        assert_eq!(result["key"], "value");
381    }
382
383    #[tokio::test]
384    async fn test_json_parser_partial_success() {
385        let parser = JsonOutputParser::new_partial();
386        // 完整 JSON,partial 模式也应该能解析
387        let result = parser.parse(r#"{"a": 1}"#).await.unwrap();
388        assert_eq!(result["a"], 1);
389    }
390
391    #[tokio::test]
392    async fn test_json_parser_invalid_cjk_over_200_bytes() {
393        // >200 字节的非法中文 JSON:错误路径若按字节 200 截断会切在多字节字符中间 panic,
394        // 修复后应返回 Err 而非崩溃
395        let parser = JsonOutputParser::new();
396        let long_cjk = "汉".repeat(200);
397        let bad = format!("{{\"名字\": {}", long_cjk);
398        let result = parser.parse(&bad).await;
399        assert!(result.is_err());
400    }
401
402    #[tokio::test]
403    async fn test_json_parser_partial_invalid_cjk_over_200_bytes() {
404        // partial 模式同样走错误预览截断,需同样不 panic
405        let parser = JsonOutputParser::new_partial();
406        let long_cjk = "汉".repeat(200);
407        let bad = format!("{{\"名字\": {}", long_cjk);
408        let result = parser.parse(&bad).await;
409        assert!(result.is_err());
410    }
411}