rmcp_agent/agent/executor.rs
1use std::collections::HashMap;
2use std::pin::Pin;
3use std::sync::Arc;
4
5use async_trait::async_trait;
6use chrono::Utc;
7use futures_util::{Stream, lock::Mutex};
8use langchain_rust::agent::AgentError;
9use langchain_rust::chain::{Chain, ChainError};
10use langchain_rust::language_models::GenerateResult;
11use langchain_rust::memory::SimpleMemory;
12use langchain_rust::prompt::PromptArgs;
13use langchain_rust::schemas::{AgentAction, AgentEvent, BaseMemory, LogTools, Message, StreamData};
14use langchain_rust::tools::Tool;
15use serde_json::{Value, json};
16use tokio_stream::wrappers::UnboundedReceiverStream;
17use uuid::Uuid;
18
19use crate::agent::extension::{AgentEventChunk, AgentExt, DeltaEvent};
20
21pub struct OpenAIMcpAgentExecutor<A>
22where
23 A: AgentExt,
24{
25 agent: Arc<A>,
26 max_iterations: Option<i32>,
27 break_if_error: bool,
28
29 pub model: String,
30 pub memory: Option<Arc<Mutex<dyn BaseMemory>>>,
31}
32
33impl<A: AgentExt> OpenAIMcpAgentExecutor<A> {
34 pub fn new(agent: Arc<A>, model: impl ToString) -> Self {
35 Self {
36 agent,
37 max_iterations: Some(10),
38 break_if_error: false,
39 memory: None,
40 model: model.to_string(),
41 }
42 }
43
44 pub fn with_max_iterations(mut self, max_iterations: i32) -> Self {
45 self.max_iterations = Some(max_iterations);
46 self
47 }
48
49 pub fn with_memory(mut self, memory: Arc<Mutex<dyn BaseMemory>>) -> Self {
50 self.memory = Some(memory);
51 self
52 }
53
54 pub fn with_break_if_error(mut self, break_if_error: bool) -> Self {
55 self.break_if_error = break_if_error;
56 self
57 }
58
59 fn get_name_to_tools(&self) -> HashMap<String, Arc<dyn Tool>> {
60 let mut name_to_tool = HashMap::new();
61 for tool in self.agent.get_tools().iter() {
62 tracing::debug!("Loading Tool: {}", tool.name());
63 name_to_tool.insert(tool.name().trim().replace(" ", "_"), tool.clone());
64 }
65 name_to_tool
66 }
67}
68
69#[async_trait]
70impl<A> Chain for OpenAIMcpAgentExecutor<A>
71where
72 A: AgentExt + 'static,
73{
74 async fn call(&self, input_variables: PromptArgs) -> Result<GenerateResult, ChainError> {
75 let mut input_variables = input_variables.clone();
76 let name_to_tools = self.get_name_to_tools();
77 let mut steps: Vec<(AgentAction, String)> = Vec::new();
78 tracing::debug!("steps: {steps:?}");
79 if let Some(memory) = &self.memory {
80 let memory = memory.lock().await;
81 input_variables.insert("chat_history".to_string(), json!(memory.messages()));
82 } else {
83 input_variables.insert(
84 "chat_history".to_string(),
85 json!(SimpleMemory::new().messages()),
86 );
87 }
88
89 loop {
90 let agent_event = self
91 .agent
92 .plan(&steps, input_variables.clone())
93 .await
94 .map_err(|e| ChainError::AgentError(format!("Error in agent planning: {e}")))?;
95
96 match agent_event {
97 AgentEvent::Action(actions) => {
98 for action in actions {
99 tracing::debug!("Action: {:?}", action.tool_input);
100 let tool = name_to_tools
101 .get(&action.tool.trim().replace(" ", "_"))
102 .ok_or_else(|| {
103 AgentError::ToolError(format!("Tool {} not found", action.tool))
104 })
105 .map_err(|e| ChainError::AgentError(e.to_string()))?;
106
107 let observation = match tool.call(&action.tool_input).await {
108 Ok(result) => result,
109 Err(err) => {
110 let error_msg = err.to_string();
111 tracing::info!("The tool return the following error: {error_msg}");
112 if self.break_if_error {
113 return Err(ChainError::AgentError(
114 AgentError::ToolError(error_msg).to_string(),
115 ));
116 } else {
117 format!("The tool return the following error: {error_msg}")
118 }
119 }
120 };
121
122 steps.push((action, observation));
123 }
124 }
125 AgentEvent::Finish(finish) => {
126 if let Some(memory) = &self.memory {
127 let mut memory = memory.lock().await;
128
129 memory.add_user_message(match &input_variables["input"] {
130 // Avoid adding extra quotes to the user input in the history.
131 Value::String(s) => s,
132 x => x, // This is the JSON encoded value.
133 });
134
135 let mut tools_ai_message_seen: HashMap<String, ()> = HashMap::default();
136 for (action, observation) in steps {
137 let LogTools { tool_id, tools } = serde_json::from_str(&action.log)?;
138 let tools_value = serde_json::from_str(&tools)?;
139 if tools_ai_message_seen.insert(tools, ()).is_none() {
140 memory.add_message(
141 Message::new_ai_message("").with_tool_calls(tools_value),
142 );
143 }
144 memory.add_message(Message::new_tool_message(observation, tool_id));
145 }
146
147 memory.add_ai_message(&finish.output);
148 }
149
150 return Ok(GenerateResult {
151 generation: finish.output,
152 ..Default::default()
153 });
154 }
155 }
156
157 if let Some(max_iterations) = self.max_iterations {
158 if steps.len() >= max_iterations as usize {
159 return Ok(GenerateResult {
160 generation: "Max iterations reached".to_string(),
161 ..Default::default()
162 });
163 }
164 }
165 }
166 }
167
168 async fn stream(
169 &self,
170 input_variables: PromptArgs,
171 ) -> Result<Pin<Box<dyn Stream<Item = Result<StreamData, ChainError>> + Send>>, ChainError>
172 {
173 let mut input_variables = input_variables.clone();
174 let name_to_tools = self.get_name_to_tools();
175 let mut steps: Vec<(AgentAction, String)> = Vec::new();
176
177 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
178
179 if let Some(memory) = &self.memory {
180 let memory = memory.lock().await;
181 input_variables.insert("chat_history".to_string(), json!(memory.messages()));
182 } else {
183 input_variables.insert(
184 "chat_history".to_string(),
185 json!(SimpleMemory::new().messages()),
186 );
187 }
188
189 let conversation_id = input_variables
190 .get("conversation_id")
191 .unwrap_or(&Value::String(Uuid::now_v7().to_string()))
192 .to_string();
193
194 let chat_completion_id = format!("chatcmpl-{}", Uuid::now_v7());
195 let model = self.model.clone();
196 let created = Utc::now().timestamp();
197
198 // Send initial chunk
199 let _ = tx.send(Ok(StreamData::new(
200 json!({
201 "id": chat_completion_id,
202 "conversation_id": conversation_id,
203 "object": "chat.completion.chunk",
204 "created": created,
205 "model": model,
206 "choices": [{
207 "index": 0,
208 "delta": {
209 "role": "assistant",
210 "content": null
211 },
212 "logprobs": null,
213 "finish_reason": null
214 }]
215 }),
216 None,
217 "",
218 )));
219
220 let agent = self.agent.clone();
221 let memory = self.memory.clone();
222 let max_iterations = self.max_iterations;
223 let break_if_error = self.break_if_error;
224
225 tokio::spawn(async move {
226 use futures_util::StreamExt;
227
228 loop {
229 let mut plan_stream = match agent.plan_stream(&steps, input_variables.clone()).await
230 {
231 Ok(stream) => stream,
232 Err(e) => {
233 let _ = tx.send(Ok(StreamData::new(
234 json!({
235 "id": chat_completion_id,
236 "conversation_id": conversation_id,
237 "object": "chat.completion.chunk",
238 "created": created,
239 "model": model,
240 "choices": [{
241 "index": 0,
242 "delta": {
243 "content": format!("Error: {e}")
244 },
245 "logprobs": null,
246 "finish_reason": "stop"
247 }]
248 }),
249 None,
250 "",
251 )));
252 return;
253 }
254 };
255
256 while let Some(chunk_result) = plan_stream.next().await {
257 match chunk_result {
258 Ok(chunk) => match chunk {
259 AgentEventChunk::Delta(event) => match event {
260 DeltaEvent::Content(content) => {
261 if !content.is_empty() {
262 let _ = tx.send(Ok(StreamData::new(
263 json!({
264 "id": chat_completion_id,
265 "conversation_id": conversation_id,
266 "object": "chat.completion.chunk",
267 "created": created,
268 "model": model,
269 "choices": [{
270 "index": 0,
271 "delta": {
272 "content": content
273 },
274 "logprobs": null,
275 "finish_reason": null
276 }]
277 }),
278 None,
279 content,
280 )));
281 }
282 }
283 DeltaEvent::Action(action) => {
284 // Generate a tool call ID for this partial action
285 let log: Value = serde_json::from_str(action.log.as_str())
286 .unwrap_or_default();
287
288 let tool_call_id = match log.get("tool_id") {
289 Some(id) => id
290 .as_str()
291 .unwrap_or_else(|| {
292 tracing::warn!("tool_id is not a string: {}", id);
293 ""
294 })
295 .to_string(),
296 None => {
297 let tmp_id = Uuid::now_v7().to_string();
298 tracing::error!(
299 "missing `tool_id` in action.log, tmp id: {tmp_id}"
300 );
301 println!(
302 "missing `tool_id` in action.log, tmp id: {tmp_id}"
303 );
304 tmp_id
305 }
306 };
307
308 // Create the tool call JSON structure for the partial action
309 let tool_call_json = json!({
310 "id": tool_call_id,
311 "conversation_id": conversation_id,
312 "type": "function",
313 "function": {
314 "name": action.tool,
315 "arguments": action.tool_input,
316 }
317 });
318
319 // Send the partial tool call information
320 let _ = tx.send(Ok(StreamData::new(
321 json!({
322 "id": chat_completion_id,
323 "conversation_id": conversation_id,
324 "object": "chat.completion.chunk",
325 "created": created,
326 "model": model,
327 "choices": [{
328 "index": 0,
329 "delta": {
330 "tool_calls": [tool_call_json]
331 },
332 "logprobs": null,
333 "finish_reason": null
334 }]
335 }),
336 None,
337 "",
338 )));
339 }
340 },
341 AgentEventChunk::Final(event) => {
342 tracing::debug!("got event: {event:?}");
343 match event {
344 AgentEvent::Action(actions) => {
345 for action in actions {
346 let tool = match name_to_tools
347 .get(&action.tool.trim().replace(" ", "_"))
348 {
349 Some(tool) => tool,
350 None => {
351 let error_msg =
352 format!("Tool {} not found", action.tool);
353 let _ = tx.send(Ok(StreamData::new(
354 json!({
355 "id": chat_completion_id,
356 "conversation_id": conversation_id,
357 "object": "chat.completion.chunk",
358 "created": created,
359 "model": model,
360 "choices": [{
361 "index": 0,
362 "delta": {
363 "content": error_msg
364 },
365 "logprobs": null,
366 "finish_reason": "stop"
367 }]
368 }),
369 None,
370 error_msg,
371 )));
372 return;
373 }
374 };
375
376 let log: Value = serde_json::from_str(&action.log)
377 .unwrap_or_default();
378
379 let tool_call_id = log
380 .get("tool_id")
381 .and_then(|v| v.as_str())
382 .map(|s| s.to_string())
383 .unwrap_or_else(|| Uuid::now_v7().to_string());
384
385 let tool_call_json = json!({
386 "id": tool_call_id,
387 "conversation_id": conversation_id,
388 "type": "function",
389 "function": {
390 "name": action.tool,
391 "arguments": action.tool_input,
392 }
393 });
394
395 let _ = tx.send(Ok(StreamData::new(
396 json!({
397 "id": chat_completion_id,
398 "conversation_id": conversation_id,
399 "object": "chat.completion.chunk",
400 "created": created,
401 "model": model,
402 "choices": [{
403 "index": 0,
404 "delta": {
405 "tool_calls": [tool_call_json]
406 },
407 "logprobs": null,
408 "finish_reason": ""
409 }]
410 }),
411 None,
412 "",
413 )));
414
415 let observation =
416 match tool.call(&action.tool_input).await {
417 Ok(result) => result,
418 Err(err) => {
419 let error_msg =
420 format!("Tool error: {err}");
421
422 if break_if_error {
423 let _ = tx.send(Ok(StreamData::new(
424 json!({
425 "id": chat_completion_id,
426 "conversation_id": conversation_id,
427 "object": "chat.completion.chunk",
428 "created": created,
429 "model": model,
430 "choices": [{
431 "index": 0,
432 "delta": {
433 "content": error_msg
434 },
435 "logprobs": null,
436 "finish_reason": "stop"
437 }]
438 }),
439 None,
440 error_msg,
441 )));
442 return;
443 } else {
444 error_msg
445 }
446 }
447 };
448
449 let parsed =
450 match serde_json::from_str::<Value>(&observation) {
451 Ok(json_result) => json_result,
452 Err(_) => Value::String(observation.clone()),
453 };
454
455 let delta = json!({
456 "content": null,
457 "parsed": parsed,
458 "tool_name": tool.name(),
459 "tool_call_id": tool_call_id
460 });
461
462 let _ = tx.send(Ok(StreamData::new(
463 json!({
464 "id": chat_completion_id,
465 "conversation_id": conversation_id,
466 "object": "chat.completion.chunk",
467 "created": created,
468 "model": model,
469 "choices": [{
470 "index": 0,
471 "delta": delta,
472 "logprobs": null,
473 "finish_reason": null
474 }]
475 }),
476 None,
477 parsed.to_string(),
478 )));
479
480 tracing::debug!("observation: {observation}");
481
482 steps.push((action, observation));
483 }
484 break;
485 }
486 AgentEvent::Finish(finish) => {
487 if let Some(memory) = &memory {
488 let mut memory = memory.lock().await;
489 memory.add_user_message(
490 match &input_variables["input"] {
491 Value::String(s) => s,
492 x => x,
493 },
494 );
495
496 let mut tools_ai_message_seen: HashMap<String, ()> =
497 HashMap::default();
498
499 for (action, observation) in &steps {
500 match serde_json::from_str::<LogTools>(&action.log)
501 {
502 Ok(LogTools { tool_id, tools }) => {
503 match serde_json::from_str::<Value>(&tools)
504 {
505 Ok(tools_value) => {
506 if tools_ai_message_seen
507 .insert(tools, ())
508 .is_none()
509 {
510 memory.add_message(
511 Message::new_ai_message("")
512 .with_tool_calls(
513 tools_value,
514 ),
515 );
516 }
517 memory.add_message(
518 Message::new_tool_message(
519 observation.clone(),
520 tool_id,
521 ),
522 );
523 }
524 Err(_) => {}
525 }
526 }
527 Err(_) => {}
528 }
529 }
530 memory.add_ai_message(&finish.output);
531 }
532
533 let _ = tx.send(Ok(StreamData::new(
534 json!({
535 "id": chat_completion_id,
536 "conversation_id": conversation_id,
537 "object": "chat.completion.chunk",
538 "created": created,
539 "model": model,
540 "choices": [{
541 "index": 0,
542 "delta": {},
543 "logprobs": null,
544 "finish_reason": "stop"
545 }]
546 }),
547 None,
548 "stop",
549 )));
550 return;
551 }
552 }
553 }
554 },
555 Err(e) => {
556 let _ = tx.send(Ok(StreamData::new(
557 json!({
558 "id": chat_completion_id,
559 "conversation_id": conversation_id,
560 "object": "chat.completion.chunk",
561 "created": created,
562 "model": model,
563 "choices": [{
564 "index": 0,
565 "delta": {
566 "content": format!("Stream error: {e}")
567 },
568 "logprobs": null,
569 "finish_reason": "stop"
570 }]
571 }),
572 None,
573 "",
574 )));
575 return;
576 }
577 }
578 }
579
580 // Check max iterations before continuing
581 if let Some(max_iterations) = max_iterations {
582 if steps.len() >= max_iterations as usize {
583 let _ = tx.send(Ok(StreamData::new(
584 json!({
585 "id": chat_completion_id,
586 "conversation_id": conversation_id,
587 "object": "chat.completion.chunk",
588 "created": created,
589 "model": model,
590 "choices": [{
591 "index": 0,
592 "delta": {
593 "content": "Maximum iterations reached."
594 },
595 "logprobs": null,
596 "finish_reason": "length"
597 }]
598 }),
599 None,
600 "Maximum iterations reached.",
601 )));
602 return;
603 }
604 }
605 }
606 });
607
608 Ok(Box::pin(UnboundedReceiverStream::new(rx)))
609 }
610}