1use crate::agent::provider::LlmProvider;
5use crate::agent::tools::{engine_tools, exec_tool, SYSTEM_PROMPT};
6use crate::agent::types::{Block, Msg, Role, Stop};
7use crate::db::Corpus;
8use serde::Serialize;
9use serde_json::{json, Value};
10use std::collections::HashMap;
11
12#[derive(Debug, Clone, Serialize)]
13pub struct ToolCallLog {
14 pub name: String,
15 pub input: Value,
16 pub result: String,
17 pub is_error: bool,
18}
19
20#[derive(Debug, Clone, Serialize)]
21pub struct AgentAnswer {
22 pub answer: String,
23 pub trace: Vec<ToolCallLog>,
24 pub steps: usize,
25}
26
27pub async fn run_agent(
28 provider: &dyn LlmProvider,
29 corpus: &Corpus,
30 question: &str,
31 max_steps: usize,
32) -> Result<AgentAnswer, String> {
33 let tools = engine_tools(&corpus.facet_names());
35 let mut trace: Vec<ToolCallLog> = Vec::new();
36 let mut last_program: Option<Value> = None;
39
40 let mut opening = question.to_string();
45 let linked = corpus.entity_link(question);
46 if !linked.is_empty() {
47 let ranked = corpus.search_ranked(&linked, 8);
51 let sample = ranked
52 .iter()
53 .enumerate()
54 .map(|(i, (_sid, _cov, cells))| {
55 let joined: String = cells.join(" · ").chars().take(240).collect();
56 format!(" [{}] {}", i + 1, joined)
57 })
58 .collect::<Vec<_>>()
59 .join("\n");
60 opening.push_str(&format!(
61 "\n\n[Retrieved evidence for your question (corpus tokens: {}; {} matching situations). \
62 ANSWER FROM these passages when the question is about what the documents say; for \
63 counting/comparing use the analytics programs.{}]",
64 linked.join(", "),
65 ranked.len(),
66 if sample.is_empty() { String::new() } else { format!("\n{sample}") }
67 ));
68 trace.push(ToolCallLog {
69 name: "entity_link".into(),
70 input: json!({ "question": question }),
71 result: json!({ "linked": linked, "passages": ranked.len() }).to_string(),
72 is_error: false,
73 });
74 }
75 let mut msgs: Vec<Msg> = vec![Msg::user_text(opening)];
76
77 let mut cache: HashMap<String, (String, bool)> = HashMap::new();
80 let mut last_sig = String::new();
81 let mut repeat = 0usize;
82
83 for step in 1..=max_steps {
84 let turn = provider.chat(SYSTEM_PROMPT, &msgs, &tools).await?;
85
86 let mut ablocks: Vec<Block> = Vec::new();
88 if !turn.text.trim().is_empty() {
89 ablocks.push(Block::Text(turn.text.clone()));
90 }
91 for (id, name, input) in &turn.tool_uses {
92 ablocks.push(Block::ToolUse { id: id.clone(), name: name.clone(), input: input.clone() });
93 }
94 if !ablocks.is_empty() {
95 msgs.push(Msg { role: Role::Assistant, blocks: ablocks });
96 }
97
98 if turn.tool_uses.is_empty() || turn.stop == Stop::EndTurn {
99 if !provider.synthesizes() && last_program.is_none() {
103 let anchor = crate::agent::workflow::compile_anchor(&linked, &[]);
104 let profile = crate::agent::profile::auto_profile(corpus, &anchor);
105 if let Some(ans) = crate::agent::synth::render_program(&profile) {
106 trace.push(ToolCallLog { name: "auto_profile".into(), input: json!({ "anchor": anchor }), result: profile.to_string(), is_error: false });
107 return Ok(AgentAnswer { answer: ans, trace, steps: step });
108 }
109 }
110 let answer = finalize(provider, &last_program, &turn.text);
111 return Ok(AgentAnswer { answer, trace, steps: step });
112 }
113
114 let mut rblocks: Vec<Block> = Vec::new();
116 for (id, name, input) in &turn.tool_uses {
117 let key = format!("{name}|{input}");
118 let (result, is_error) = cache
119 .entry(key)
120 .or_insert_with(|| {
121 if name == "run_workflow" {
124 crate::agent::workflow::execute(corpus, input, &linked)
125 } else {
126 exec_tool(corpus, name, input)
127 }
128 })
129 .clone();
130 if !is_error {
131 if let Ok(v) = serde_json::from_str::<Value>(&result) {
132 if v.get("program").is_some() {
133 last_program = Some(v);
134 }
135 }
136 }
137 trace.push(ToolCallLog { name: name.clone(), input: input.clone(), result: result.clone(), is_error });
138 rblocks.push(Block::ToolResult { id: id.clone(), content: result, is_error });
139 }
140
141 if !provider.synthesizes() {
144 if let Some(p) = &last_program {
145 if let Some(ans) = crate::agent::synth::render_program(p) {
146 return Ok(AgentAnswer { answer: ans, trace, steps: step });
147 }
148 }
149 }
150
151 let sig = turn.tool_uses.iter().map(|(_, n, i)| format!("{n}|{i}")).collect::<Vec<_>>().join(";");
154 if sig == last_sig {
155 repeat += 1;
156 if repeat >= 3 {
157 let ans = if last_program.is_some() || !provider.synthesizes() {
158 finalize(provider, &last_program, &turn.text)
159 } else if turn.text.trim().is_empty() {
160 "I could not converge on an answer from the corpus with the available evidence.".to_string()
161 } else {
162 sanitize_answer(&turn.text)
163 };
164 return Ok(AgentAnswer { answer: ans, trace, steps: step });
165 }
166 rblocks.push(Block::Text(
167 "You already ran these exact queries and the results are unchanged. Do NOT repeat tool \
168 calls — give your final answer now, grounded in the evidence above."
169 .into(),
170 ));
171 } else {
172 repeat = 0;
173 last_sig = sig;
174 }
175 msgs.push(Msg { role: Role::User, blocks: rblocks });
176 }
177
178 if let Some(p) = &last_program {
180 if let Some(ans) = crate::agent::synth::render_program(p) {
181 return Ok(AgentAnswer { answer: ans, trace, steps: max_steps });
182 }
183 }
184 Err(format!("agent exceeded max_steps ({max_steps}) without a final answer"))
185}
186
187fn finalize(provider: &dyn LlmProvider, last_program: &Option<Value>, model_text: &str) -> String {
191 if !provider.synthesizes() {
192 if let Some(p) = last_program {
193 if let Some(rendered) = crate::agent::synth::render_program(p) {
194 return rendered;
195 }
196 }
197 }
198 sanitize_answer(model_text)
199}
200
201fn sanitize_answer(text: &str) -> String {
204 for w in 2..=10 {
205 let chars: Vec<char> = text.chars().collect();
206 let mut run_start = 0usize;
207 let mut run_len = 1usize;
208 for i in w..chars.len() {
209 if chars[i] == chars[i - w] {
210 run_len += 1;
211 if run_len >= w * 6 {
212 let cutoff: usize = text.chars().take(run_start).map(|c| c.len_utf8()).sum();
214 return text[..cutoff].trim_end_matches(|c: char| c.is_whitespace() || c == ',' || c == ';').to_string();
215 }
216 } else {
217 run_start = i - w + 1;
218 run_len = 1;
219 }
220 }
221 }
222 text.to_string()
223}