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