Skip to content

Commit cf72a08

Browse files
humbertoyustaMarcMcIntosh
authored andcommitted
fix: more robust json parse for follow ups and locate
1 parent e5235ee commit cf72a08

File tree

4 files changed

+82
-3
lines changed

4 files changed

+82
-3
lines changed

refact-agent/engine/src/agentic/generate_follow_up_message.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ use std::sync::Arc;
22
use serde::Deserialize;
33
use tokio::sync::{RwLock as ARwLock, Mutex as AMutex};
44

5+
use crate::custom_error::MapErrToString;
56
use crate::global_context::GlobalContext;
67
use crate::at_commands::at_commands::AtCommandsContext;
78
use crate::subchat::subchat_single;
89
use crate::call_validation::{ChatContent, ChatMessage};
10+
use crate::json_utils;
911

1012
const PROMPT: &str = r#"
1113
Your task is to do two things for a conversation between a user and an assistant:
@@ -117,6 +119,7 @@ pub async fn generate_follow_up_message(
117119

118120
tracing::info!("follow-up model says {:?}", response);
119121

120-
let response: FollowUpResponse = serde_json::from_str(&response).map_err(|e| e.to_string())?;
122+
let response: FollowUpResponse = json_utils::extract_json_object(&response)
123+
.map_err_with_prefix("Failed to parse json:")?;
121124
Ok(response)
122125
}

refact-agent/engine/src/json_utils.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
use crate::custom_error::MapErrToString;
2+
3+
pub fn extract_json_object<T: for<'de> serde::Deserialize<'de>>(text: &str) -> Result<T, String> {
4+
let start = text.find('{').ok_or_else(|| "No opening brace '{' found".to_string())?;
5+
let end = text.rfind('}').ok_or_else(|| "No closing brace '}' found".to_string())?;
6+
7+
if end <= start {
8+
return Err("Closing brace appears before opening brace".to_string());
9+
}
10+
11+
serde_json::from_str(&text[start..=end]).map_err_to_string()
12+
}
13+
14+
#[cfg(test)]
15+
mod tests {
16+
use super::*;
17+
use serde_json::json;
18+
use serde::Deserialize;
19+
use indexmap::IndexMap;
20+
21+
#[test]
22+
fn test_extract_json_clean_input() {
23+
let input = r#"{"key": "value", "number": 42}"#;
24+
let result: Result<serde_json::Value, _> = extract_json_object(input);
25+
assert!(result.is_ok());
26+
assert_eq!(result.unwrap(), json!({"key": "value", "number": 42}));
27+
}
28+
29+
#[test]
30+
fn test_extract_json_with_text_before_after() {
31+
let input = "Some text before\n {\"key\": \"value\",\n \"number\": 42}\n\n and text after";
32+
let result: Result<serde_json::Value, _> = extract_json_object(input);
33+
assert!(result.is_ok());
34+
assert_eq!(result.unwrap(), json!({"key": "value", "number": 42}));
35+
}
36+
37+
#[test]
38+
fn test_extract_json_nested() {
39+
let input = r#"LLM response: {"FOUND": {"file1.rs": "symbol1,symbol2"}, "SIMILAR": {"file2.rs": "symbol3"}}"#;
40+
let result: Result<IndexMap<String, IndexMap<String, String>>, _> = extract_json_object(input);
41+
assert!(result.is_ok());
42+
43+
let map = result.unwrap();
44+
assert_eq!(map.len(), 2);
45+
assert_eq!(map.get("FOUND").unwrap().get("file1.rs").unwrap(), "symbol1,symbol2");
46+
assert_eq!(map.get("SIMILAR").unwrap().get("file2.rs").unwrap(), "symbol3");
47+
}
48+
49+
#[derive(Deserialize, Debug, PartialEq)]
50+
struct FollowUpResponse {
51+
pub follow_ups: Vec<String>,
52+
pub topic_changed: bool,
53+
}
54+
55+
#[test]
56+
fn test_follow_up_response_format() {
57+
let input = r#"
58+
Here are the follow up questions:
59+
```json
60+
{
61+
"follow_ups": ["How?", "More examples", "Thank you"],
62+
"topic_changed": false
63+
}
64+
```
65+
"#;
66+
67+
let result: Result<FollowUpResponse, _> = extract_json_object(input);
68+
69+
assert!(result.is_ok());
70+
let response = result.unwrap();
71+
72+
assert_eq!(response.follow_ups, vec!["How?", "More examples", "Thank you"]);
73+
assert_eq!(response.topic_changed, false);
74+
}
75+
}

refact-agent/engine/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ mod telemetry;
2828
mod global_context;
2929
mod background_tasks;
3030
mod yaml_configs;
31+
mod json_utils;
3132

3233
mod file_filter;
3334
mod files_in_workspace;

refact-agent/engine/src/tools/tool_locate_search.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ async fn find_relevant_files_with_search(
219219
crate::tools::tool_relevant_files::update_usage_from_message(&mut usage, &last_message);
220220
assert!(last_message.role == "assistant");
221221

222-
let assistant_output1 = serde_json::from_str::<IndexMap<String, serde_json::Value>>(last_message.content.content_text_only().as_str()).map_err(|e| {
222+
let assistant_output1 = crate::json_utils::extract_json_object::<IndexMap<String, serde_json::Value>>(last_message.content.content_text_only().as_str()).map_err(|e| {
223223
tracing::warn!("\n{}\nUnable to parse JSON: {:?}", last_message.content.content_text_only(), e);
224224
format!("Unable to parse JSON: {:?}", e)
225225
})?;
@@ -229,7 +229,7 @@ async fn find_relevant_files_with_search(
229229
return Ok((results, usage, serde_json::to_string_pretty(&assistant_output1).unwrap(), cd_instruction));
230230
}
231231

232-
let assistant_output2 = serde_json::from_str::<IndexMap<String, IndexMap<String, String>>>(last_message.content.content_text_only().as_str()).map_err(|e| {
232+
let assistant_output2 = crate::json_utils::extract_json_object::<IndexMap<String, IndexMap<String, String>>>(last_message.content.content_text_only().as_str()).map_err(|e| {
233233
tracing::warn!("\n{}\nUnable to parse JSON: {:?}", last_message.content.content_text_only(), e);
234234
format!("Unable to parse JSON: {:?}", e)
235235
})?;

0 commit comments

Comments
 (0)