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