Skip to main content

abu_agent/agent/
lloop.rs

1use abu_base::chat::{AssistantMessage, ChatMessage, ToolCall, ToolDefinition};
2use abu_provider::ChatProvide;
3use abu_tool::ToolCallResult;
4use crate::{middleware::MiddlewareFlow, AgentError};
5use crate::memory::Memory;
6use super::{Agent, AgentResult};
7
8use thiserrorctx::Context;
9use tracing::{debug, info, warn};
10
11pub enum AgentControl<T> {
12    Normal(T),
13    Break(String),
14}
15
16impl<T> AgentControl<T> {
17    pub fn unwrap(self) -> T {
18        match self {
19            Self::Break(_) => panic!("control is break!"),
20            Self::Normal(v) => v,
21        }
22    }
23}
24
25macro_rules! extract_agent_control {
26    ($control:ident) => {
27        match $control {
28            AgentControl::Break(s) => return Ok(AgentControl::Break(s)),
29            AgentControl::Normal(m) => m,
30        }
31    };
32}
33
34macro_rules! return_middleware_break {
35    ($flow:ident) => {
36        if let MiddlewareFlow::Break(s) = $flow {
37            return Ok(AgentControl::Break(s));
38        }
39    };
40}
41
42impl<C: ChatProvide, M: Memory> Agent<C, M> {
43    pub fn tool_list(&self) -> &[ToolDefinition] {
44        self.toolbox.tool_definitions()
45    }
46
47    pub fn system_prompt(&self) -> &str {
48        &self.context_builder.system_prompt
49    }
50
51    pub async fn run(&mut self, query: &str) -> AgentResult<AgentControl<String>> {
52        info!(query = %query, "🤖 Agent started with user query");
53        self.hooks.on_agent_start(query).await.context("agent start hook")?;
54        
55        // init context
56        let memories = self.search_memory(query).await.context("search memory")?;
57        let mut messages = self.build_context(query, memories).await.context("build context")?;
58
59        // agent loop
60        let mut final_result = None; 
61        for step in 0..self.config.max_iteration {
62            debug!(step, "🔄 Agent step begin");
63            self.hooks.on_step_start(step).await.with_context(|| format!("step {step} start"))?;
64
65            // chat with llm
66            let control = self.llm_chat(step, &messages, true).await
67                .with_context(|| format!("chat with llm in step {}", step))?;
68            let mut ai_message = extract_agent_control!(control);
69            messages.push(ai_message.clone().into());
70
71            info!(step, role = "AI", content = ai_message.content, "🗣️ LLM Text Response");
72            if !ai_message.tool_calls.is_empty() {
73                info!(step, count = ai_message.tool_calls.len(), "🛠️ LLM requested tool calls");
74            } else {
75                final_result = Some(ai_message.content);
76                break;
77            }
78
79            // tool calls
80            for tool_call in ai_message.tool_calls.iter_mut() {
81                info!(step, tool = %tool_call.name, id = %tool_call.id, args = %tool_call.arguments, "🚀 Executing tool");
82                // execute tools
83                let control = self.execute_tool(step, tool_call).await.context("execute tool")?;
84                let result = extract_agent_control!(control);
85
86                let tool_content = if result.is_error {
87                    info!(step, result = %result.context, "Tool execute failed!");
88                    format!("Tool execute failed for {}", result.context)
89                } else {
90                    info!(step, result = %result.context, "✅ Tool execution finished");
91                    format!("Tool execute success with output {}", result.context)
92                };
93
94                // insert tool response
95                messages.push(ChatMessage::tool(tool_content, tool_call.id.clone()));
96            }
97
98            debug!(step, "🔄 Agent step end");
99            self.hooks.on_step_end(step, &ai_message).await.with_context(|| format!("step {step} start"))?;
100        }
101
102        match final_result {
103            Some(final_result) => {
104                info!(output = final_result, "🛑 Finish task with final output");
105                self.add_memory(query, &final_result).await?;
106                Ok(AgentControl::Normal(final_result))
107            }
108            None => {
109                warn!("Agent reached max steps without termination");
110                self.hooks.on_agent_max_iteration().await.context("max iter hook")?;
111                Ok(AgentControl::Normal("Task do not finish yet".to_string()))
112            }
113        }
114    }
115
116    pub async fn chat(&mut self, query: &str) -> AgentResult<AgentControl<String>> {
117        info!(query = %query, "🤖 Agent started with user query");
118        
119        // init context
120        let memories = self.search_memory(query).await.context("search memory")?;
121        let messages = self.build_context(query, memories).await.context("build context")?;
122        
123        // chat with llm
124        let control = self.llm_chat(0, &messages, false).await.context("chat with llm")?;
125        let ai_message = extract_agent_control!(control);
126
127        info!(role = "AI", content = ai_message.content, "🗣️ LLM Text Response");
128        self.add_memory(query, &ai_message.content).await?;
129
130        Ok(AgentControl::Normal(ai_message.content))
131    }
132
133    async fn execute_tool(&mut self, step: usize, tool_call: &mut ToolCall) -> AgentResult<AgentControl<ToolCallResult>> {
134        let flow = self.middlewares
135            .intercept_tool_call(tool_call)
136            .await.context("intercept tool call")?;
137        return_middleware_break!(flow);
138        
139        self.hooks.on_tool_start(step, tool_call).await.context("tool start hook")?;
140
141        let mut result = self.toolbox.execute_tool(&tool_call.name, &tool_call.arguments).await.context("execute tool")?;
142
143        let flow = self.middlewares
144            .intercept_tool_result(&tool_call.name, &mut result)
145            .await.context("intercept tool result")?;
146        return_middleware_break!(flow);
147        
148        self.hooks.on_tool_end(step, &result).await.context("tool end hook")?;
149
150        if result.is_error {
151            self.hooks.on_tool_error(step, &result.context).await.context("tool error hook")?;
152        }
153        
154        Ok(AgentControl::Normal(result))
155    }
156
157    async fn add_memory(&mut self, user_input: &str, ai_response: &str) -> AgentResult<()> {
158        debug!(user_input = user_input, ai_response = ai_response, "add memory");
159        
160        self.memory
161            .add(user_input, ai_response).await
162            .map_err(|e| AgentError::Memory(Box::new(e)))
163            .context("add new memory")?;
164
165        self.hooks.on_memory_add(user_input, ai_response).await.context("memory add hook")?;
166
167        Ok(())
168    }
169
170    async fn search_memory(&mut self, query: &str) -> AgentResult<Vec<ChatMessage>> {
171        debug!(query = query, "search memory");
172        let memories: Vec<ChatMessage> = self.memory.search(query).await
173            .map_err(|e| AgentError::Memory(Box::new(e)))
174            .context("search memory")?;
175        self.hooks.on_memory_search(query, &memories).await.context("memory search hook")?;
176
177        Ok(memories)
178    }
179
180    async fn build_context(&mut self, query: &str, memories: Vec<ChatMessage>) -> AgentResult<Vec<ChatMessage>> {
181        debug!("build context");
182        let messages: Vec<ChatMessage> = self.context_builder.build(query, memories);
183        self.hooks.on_context_build(query, &messages).await.context("context build hook")?;
184
185        Ok(messages)
186    }
187
188    async fn llm_chat(&mut self, step: usize, messages: &[ChatMessage], with_tool: bool) -> AgentResult<AgentControl<AssistantMessage>> {
189        self.hooks.on_llm_start(step, messages).await.context("llm start hook")?;
190
191        let mut ai_message = if with_tool {
192            self.llm.chat(messages).await?.message
193        } else {
194            self.llm.chat_no_tools(messages).await?.message
195        };
196
197        let flow = self.middlewares
198            .intercept_llm_out(&mut ai_message).await
199            .context("intercept llm out")?;
200        return_middleware_break!(flow);
201
202        self.hooks.on_llm_end(step, &ai_message).await.context("llm end hook")?;
203
204        Ok(AgentControl::Normal(ai_message))
205    }
206}