1use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12use crate::delta::DeltaBatch;
13
14pub fn safe_json_loads(text: &str) -> Result<serde_json::Value, String> {
16 let mut text = text.trim().to_string();
17
18 if text.starts_with("```json") {
20 text = text[7..].trim().to_string();
21 } else if text.starts_with("```") {
22 text = text[3..].trim().to_string();
23 }
24 if text.ends_with("```") {
25 text = text[..text.len() - 3].trim().to_string();
26 }
27
28 match serde_json::from_str::<serde_json::Value>(&text) {
29 Ok(val) => {
30 if val.is_object() {
31 Ok(val)
32 } else {
33 Err("Expected a JSON object from LLM.".to_string())
34 }
35 }
36 Err(e) => {
37 let open_braces = text.chars().filter(|&c| c == '{').count();
39 let close_braces = text.chars().filter(|&c| c == '}').count();
40 if open_braces > close_braces || text.trim_end().ends_with('"') {
41 Err(format!(
42 "LLM response appears to be truncated JSON. Original error: {e}"
43 ))
44 } else {
45 Err(format!("LLM response is not valid JSON: {e}"))
46 }
47 }
48 }
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct AgentResponse {
54 pub content: String,
55 #[serde(skip_serializing_if = "Option::is_none")]
56 pub reasoning: Option<String>,
57 #[serde(default)]
58 pub tool_calls: Vec<serde_json::Value>,
59}
60
61impl AgentResponse {
62 pub fn new(content: &str) -> Self {
64 Self {
65 content: content.to_string(),
66 reasoning: None,
67 tool_calls: Vec::new(),
68 }
69 }
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct BulletTag {
75 pub id: String,
76 pub tag: String,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct ReflectorOutput {
82 pub reasoning: String,
83 pub error_identification: String,
84 pub root_cause_analysis: String,
85 pub correct_approach: String,
86 pub key_insight: String,
87 pub bullet_tags: Vec<BulletTag>,
88 #[serde(default)]
89 pub raw: HashMap<String, serde_json::Value>,
90}
91
92impl ReflectorOutput {
93 pub fn from_json(data: &serde_json::Value) -> Self {
95 let mut bullet_tags = Vec::new();
96 if let Some(tags_arr) = data.get("bullet_tags").and_then(|v| v.as_array()) {
97 for item in tags_arr {
98 if let (Some(id), Some(tag)) = (
99 item.get("id").and_then(|v| v.as_str()),
100 item.get("tag").and_then(|v| v.as_str()),
101 ) {
102 bullet_tags.push(BulletTag {
103 id: id.to_string(),
104 tag: tag.to_lowercase(),
105 });
106 }
107 }
108 }
109
110 let raw = data
111 .as_object()
112 .map(|obj| obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
113 .unwrap_or_default();
114
115 Self {
116 reasoning: data
117 .get("reasoning")
118 .and_then(|v| v.as_str())
119 .unwrap_or("")
120 .to_string(),
121 error_identification: data
122 .get("error_identification")
123 .and_then(|v| v.as_str())
124 .unwrap_or("")
125 .to_string(),
126 root_cause_analysis: data
127 .get("root_cause_analysis")
128 .and_then(|v| v.as_str())
129 .unwrap_or("")
130 .to_string(),
131 correct_approach: data
132 .get("correct_approach")
133 .and_then(|v| v.as_str())
134 .unwrap_or("")
135 .to_string(),
136 key_insight: data
137 .get("key_insight")
138 .and_then(|v| v.as_str())
139 .unwrap_or("")
140 .to_string(),
141 bullet_tags,
142 raw,
143 }
144 }
145}
146
147#[derive(Debug, Clone)]
149pub struct CuratorOutput {
150 pub delta: DeltaBatch,
151 pub raw: HashMap<String, serde_json::Value>,
152}
153
154impl CuratorOutput {
155 pub fn from_json(data: &serde_json::Value) -> Self {
157 let delta = DeltaBatch::from_json(data);
158 let raw = data
159 .as_object()
160 .map(|obj| obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
161 .unwrap_or_default();
162
163 Self { delta, raw }
164 }
165}
166
167#[cfg(test)]
168#[path = "roles_tests.rs"]
169mod tests;