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 &current_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}