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