1use lc_shared::json_repair::{parse_tolerant_json, JsonRepairError};
12use serde::de::DeserializeOwned;
13
14#[derive(Debug, thiserror::Error)]
16pub enum LlmJsonParseError {
17 #[error("JSON repair failed: {0}")]
19 RepairFailed(String),
20
21 #[error("Deserialization failed: {details}")]
23 DeserializationFailed { details: String },
24
25 #[error("All {attempts} retry attempts failed")]
27 RetryExhausted { attempts: usize },
28}
29
30impl From<JsonRepairError> for LlmJsonParseError {
31 fn from(e: JsonRepairError) -> Self {
32 match e {
33 JsonRepairError::RepairFailed(msg) => LlmJsonParseError::RepairFailed(msg),
34 JsonRepairError::DeserializationFailed { details } => {
35 LlmJsonParseError::DeserializationFailed { details }
36 }
37 }
38 }
39}
40
41pub fn parse_llm_json<T: DeserializeOwned>(raw: &str) -> Result<T, LlmJsonParseError> {
52 parse_tolerant_json::<T>(raw).map_err(Into::into)
53}
54
55pub async fn parse_llm_json_with_retry<T, F, Fut>(
62 raw: &str,
63 max_retries: usize,
64 retry_callback: F,
65) -> Result<T, LlmJsonParseError>
66where
67 T: DeserializeOwned,
68 F: Fn(&str, &str) -> Fut,
69 Fut: std::future::Future<Output = Result<String, String>>,
70{
71 let mut current_raw = raw.to_string();
72
73 for attempt in 0..=max_retries {
74 match parse_llm_json::<T>(¤t_raw) {
75 Ok(value) => return Ok(value),
76 Err(e) if attempt < max_retries => {
77 let error_msg = e.to_string();
78 let corrected = retry_callback(¤t_raw, &error_msg)
79 .await
80 .map_err(|_| LlmJsonParseError::RetryExhausted {
81 attempts: attempt + 1,
82 })?;
83 current_raw = corrected;
84 }
85 Err(_) => {
86 return Err(LlmJsonParseError::RetryExhausted {
87 attempts: attempt + 1,
88 });
89 }
90 }
91 }
92
93 Err(LlmJsonParseError::RetryExhausted {
94 attempts: max_retries + 1,
95 })
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use serde::Deserialize;
102
103 #[derive(Debug, Deserialize, PartialEq)]
104 struct TestStruct {
105 name: String,
106 value: i32,
107 }
108
109 #[test]
110 fn test_parse_valid_json() {
111 let raw = r#"{"name": "test", "value": 42}"#;
112 let result: TestStruct = parse_llm_json(raw).unwrap();
113 assert_eq!(result.name, "test");
114 assert_eq!(result.value, 42);
115 }
116
117 #[test]
118 fn test_parse_json_with_code_fence() {
119 let raw = "```json\n{\"name\": \"test\", \"value\": 42}\n```";
120 let result: TestStruct = parse_llm_json(raw).unwrap();
121 assert_eq!(result.name, "test");
122 }
123
124 #[test]
125 fn test_parse_json_with_trailing_comma() {
126 let raw = r#"{"name": "test", "value": 42,}"#;
127 let result: TestStruct = parse_llm_json(raw).unwrap();
128 assert_eq!(result.name, "test");
129 assert_eq!(result.value, 42);
130 }
131
132 #[test]
133 fn test_parse_json_with_surrounding_text() {
134 let raw = "Here is the result: {\"name\": \"test\", \"value\": 42} done.";
135 let result: TestStruct = parse_llm_json(raw).unwrap();
136 assert_eq!(result.name, "test");
137 }
138
139 #[test]
140 fn test_parse_json_with_trailing_garbage() {
141 let raw = r#"{"name": "test", "value": 42} and some extra text"#;
142 let result: TestStruct = parse_llm_json(raw).unwrap();
143 assert_eq!(result.name, "test");
144 }
145
146 #[test]
147 fn test_parse_json_with_unescaped_inner_quotes() {
148 let raw = r#"{"name": "He said "hi" and left", "value": 1}"#;
149 let result: TestStruct = parse_llm_json(raw).unwrap();
150 assert_eq!(result.name, "He said \"hi\" and left");
151 assert_eq!(result.value, 1);
152 }
153
154 #[test]
155 fn test_parse_json_array_with_trailing_comma() {
156 let raw = r#"[{"name": "a", "value": 1}, {"name": "b", "value": 2},]"#;
157 let result: Vec<TestStruct> = parse_llm_json(raw).unwrap();
158 assert_eq!(result.len(), 2);
159 }
160
161 #[test]
162 fn test_parse_empty_text_fails() {
163 let result: Result<TestStruct, _> = parse_llm_json("");
164 assert!(result.is_err());
165 }
166
167 #[test]
168 fn test_parse_no_json_content_fails() {
169 let result: Result<TestStruct, _> = parse_llm_json("just some plain text");
170 assert!(result.is_err());
171 }
172
173 #[test]
174 fn test_parse_json_no_closing_fence() {
175 let raw = "```json\n{\"name\": \"test\", \"value\": 42}";
176 let result: TestStruct = parse_llm_json(raw).unwrap();
177 assert_eq!(result.name, "test");
178 }
179
180 #[test]
181 fn test_error_display() {
182 let err = LlmJsonParseError::RepairFailed("no json".to_string());
183 assert!(err.to_string().contains("no json"));
184
185 let err = LlmJsonParseError::RetryExhausted { attempts: 3 };
186 assert!(err.to_string().contains("3"));
187 }
188
189 #[tokio::test]
190 async fn test_parse_with_retry_succeeds_on_first_try() {
191 let raw = r#"{"name": "test", "value": 42}"#;
192 let result: TestStruct = parse_llm_json_with_retry(raw, 2, |_raw, _err| async {
193 Ok("should not be called".to_string())
194 })
195 .await
196 .unwrap();
197 assert_eq!(result.name, "test");
198 }
199
200 #[tokio::test]
201 async fn test_parse_with_retry_succeeds_on_second_try() {
202 let bad_raw = "not json at all";
203 let good_raw = r#"{"name": "retry", "value": 7}"#;
204 let result: TestStruct =
205 parse_llm_json_with_retry(bad_raw, 2, |_raw, _err| async { Ok(good_raw.to_string()) })
206 .await
207 .unwrap();
208 assert_eq!(result.name, "retry");
209 assert_eq!(result.value, 7);
210 }
211
212 #[tokio::test]
213 async fn test_parse_with_retry_fails_all_attempts() {
214 let bad_raw = "not json";
215 let result: Result<TestStruct, _> =
216 parse_llm_json_with_retry(bad_raw, 1, |_raw, _err| async {
217 Ok("still not json".to_string())
218 })
219 .await;
220 assert!(result.is_err());
221 }
222}