1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
//! Core Agent implementation
use super::memory::AgentMemoryManager;
use super::{AgentConfig, ConversationMemory, ConversationMode, ToolExecutor};
use crate::error::RragResult;
#[cfg(feature = "rexis-llm-client")]
use rexis_llm::{ChatMessage, ChatResponse, Client};
use tracing::{debug, error, info};
/// Agent that can use tools and maintain conversation
pub struct Agent {
/// LLM client
llm_client: Client,
/// Tool executor
tool_executor: ToolExecutor,
/// Legacy conversation memory (for backward compatibility)
legacy_memory: ConversationMemory,
/// New persistent memory manager (optional)
memory_manager: Option<AgentMemoryManager>,
/// Agent configuration
config: AgentConfig,
}
impl Agent {
/// Create a new agent (legacy constructor - uses in-memory ConversationMemory)
pub fn new(
llm_client: Client,
tool_executor: ToolExecutor,
config: AgentConfig,
) -> RragResult<Self> {
let mut legacy_memory = ConversationMemory::with_max_length(config.max_conversation_length);
// Add system prompt
legacy_memory.add_message(ChatMessage::system(config.system_prompt.clone()));
Ok(Self {
llm_client,
tool_executor,
legacy_memory,
memory_manager: None,
config,
})
}
/// Create a new agent with persistent memory
pub fn new_with_memory(
llm_client: Client,
tool_executor: ToolExecutor,
memory_manager: AgentMemoryManager,
config: AgentConfig,
) -> RragResult<Self> {
let mut legacy_memory = ConversationMemory::with_max_length(config.max_conversation_length);
// Add system prompt to legacy memory (fallback)
legacy_memory.add_message(ChatMessage::system(config.system_prompt.clone()));
Ok(Self {
llm_client,
tool_executor,
legacy_memory,
memory_manager: Some(memory_manager),
config,
})
}
/// Run the agent with a user query
///
/// In stateless mode: Creates fresh conversation for each call
/// In stateful mode: Continues previous conversation
pub async fn run(&mut self, user_input: impl Into<String>) -> RragResult<String> {
let input = user_input.into();
info!(user_input = %input, "Agent received user input");
if self.config.verbose {
debug!(input = %input, "Processing user query");
}
// Prepare conversation based on mode and memory system
let mut conversation = match self.config.conversation_mode {
ConversationMode::Stateless => {
// Fresh conversation: system prompt + user message
vec![
ChatMessage::system(self.config.system_prompt.clone()),
ChatMessage::user(input.clone()),
]
}
ConversationMode::Stateful => {
// Use new memory system if available, otherwise legacy
if let Some(ref memory_manager) = self.memory_manager {
// Add user message to persistent memory
memory_manager
.add_conversation_message(ChatMessage::user(input.clone()))
.await?;
// Get full conversation history
memory_manager.get_conversation_messages().await?
} else {
// Legacy in-memory conversation
self.legacy_memory
.add_message(ChatMessage::user(input.clone()));
self.legacy_memory.to_messages()
}
}
};
// Agent loop: iterate until we get a final answer
for iteration in 1..=self.config.max_iterations {
debug!(
iteration,
max_iterations = self.config.max_iterations,
"Agent iteration"
);
// Call LLM with tools
let response = self.llm_step(&conversation).await?;
// Check for tool calls
if let Some(tool_calls) = &response.tool_calls {
if !tool_calls.is_empty() {
info!(
tool_count = tool_calls.len(),
tools = ?tool_calls.iter().map(|t| &t.function.name).collect::<Vec<_>>(),
"Agent requesting tool calls"
);
// Add assistant message with tool calls to conversation
let mut assistant_msg = ChatMessage::assistant(response.content.clone());
assistant_msg.tool_calls = Some(tool_calls.clone());
conversation.push(assistant_msg);
// Execute all tool calls
let tool_results = self.tool_executor.execute_tool_calls(tool_calls);
// Add tool results to conversation
for result in tool_results {
if let rexis_llm::MessageContent::Text(ref content) = result.content {
debug!(tool_result = %content, "Tool execution completed");
}
conversation.push(result);
}
// Continue loop to let LLM process results
continue;
}
}
// No tool calls - this is the final answer
info!(
response = %response.content,
iterations = iteration,
"Agent generated final answer"
);
// Update memory in stateful mode
if self.config.conversation_mode == ConversationMode::Stateful {
if let Some(ref memory_manager) = self.memory_manager {
// Persist to new memory system
memory_manager
.add_conversation_message(ChatMessage::assistant(response.content.clone()))
.await?;
} else {
// Legacy in-memory
self.legacy_memory
.add_message(ChatMessage::assistant(response.content.clone()));
}
}
return Ok(response.content);
}
// Exceeded max iterations
error!(
max_iterations = self.config.max_iterations,
"Agent exceeded maximum iterations without reaching final answer"
);
Err(crate::error::RragError::Agent {
agent_id: "default".to_string(),
message: format!(
"Agent exceeded maximum iterations ({})",
self.config.max_iterations
),
source: None,
})
}
/// Single LLM call with tools
async fn llm_step(&self, conversation: &[ChatMessage]) -> RragResult<ChatResponse> {
// Get tool definitions
let tools = self.tool_executor.registry().tool_definitions();
debug!(
tool_count = tools.len(),
message_count = conversation.len(),
"Calling LLM with tools"
);
// Call LLM
let response = self
.llm_client
.chat_completion_with_tools(conversation.to_vec(), tools)
.await?;
debug!(
content_length = response.content.len(),
has_tool_calls = response.tool_calls.is_some(),
tool_call_count = response.tool_calls.as_ref().map(|t| t.len()).unwrap_or(0),
"LLM response received"
);
Ok(response)
}
/// Reset conversation (clears history, keeps system prompt)
pub async fn reset(&mut self) -> RragResult<()> {
if let Some(ref memory_manager) = self.memory_manager {
memory_manager.clear_conversation().await?;
} else {
self.legacy_memory.clear();
}
Ok(())
}
/// Get conversation history (legacy - uses in-memory only)
pub fn get_conversation(&self) -> &[ChatMessage] {
self.legacy_memory.get_messages()
}
/// Get conversation history from persistent memory (async)
pub async fn get_conversation_async(&self) -> RragResult<Vec<ChatMessage>> {
if let Some(ref memory_manager) = self.memory_manager {
memory_manager.get_conversation_messages().await
} else {
Ok(self.legacy_memory.to_messages())
}
}
/// Get agent configuration
pub fn config(&self) -> &AgentConfig {
&self.config
}
/// Get mutable configuration
pub fn config_mut(&mut self) -> &mut AgentConfig {
&mut self.config
}
/// Get access to the memory manager (if using persistent memory)
pub fn memory(&self) -> Option<&AgentMemoryManager> {
self.memory_manager.as_ref()
}
/// Get mutable access to the memory manager (if using persistent memory)
pub fn memory_mut(&mut self) -> Option<&mut AgentMemoryManager> {
self.memory_manager.as_mut()
}
}