Skip to main content

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 = match serde_json::from_str::<Value>(
459                                                &observation,
460                                            ) {
461                                                Ok(json_result) => json_result,
462                                                Err(e) => {
463                                                    tracing::warn!(
464                                                        "got error in parsing resp: {e:?}, raw: {observation}"
465                                                    );
466                                                    Value::String(observation.clone())
467                                                }
468                                            };
469
470                                            let delta = json!({
471                                                "content": null,
472                                                "parsed": parsed,
473                                                "tool_name": tool.name(),
474                                                "tool_call_id": tool_call_id
475                                            });
476
477                                            let _ = tx.send(Ok(StreamData::new(
478                                                json!({
479                                                    "id": chat_completion_id,
480                                                    "conversation_id": conversation_id,
481                                                    "object": "chat.completion.chunk",
482                                                    "created": created,
483                                                    "model": model,
484                                                    "choices": [{
485                                                        "index": 0,
486                                                        "delta": delta,
487                                                        "logprobs": null,
488                                                        "finish_reason": null
489                                                    }]
490                                                }),
491                                                None,
492                                                parsed.to_string(),
493                                            )));
494
495                                            tracing::debug!("observation: {observation}");
496
497                                            current_iteration_steps
498                                                .push((action.clone(), observation.clone()));
499                                            steps.push((action, observation));
500                                        }
501
502                                        if !accumulated_content.is_empty() {
503                                            if let Some(memory) = &memory {
504                                                let mut memory = memory.lock().await;
505                                                memory.add_ai_message(&accumulated_content);
506                                            }
507                                        }
508
509                                        if let Some(memory) = &memory {
510                                            let mut memory = memory.lock().await;
511                                            let mut tools_ai_message_seen: HashMap<String, ()> =
512                                                HashMap::default();
513
514                                            for (action, observation) in &current_iteration_steps {
515                                                match serde_json::from_str::<LogTools>(&action.log)
516                                                {
517                                                    Ok(LogTools { tool_id, tools }) => {
518                                                        if let Ok(tools_value) =
519                                                            serde_json::from_str::<Value>(&tools)
520                                                        {
521                                                            if tools_ai_message_seen
522                                                                .insert(tools, ())
523                                                                .is_none()
524                                                            {
525                                                                memory.add_message(
526                                                                    Message::new_ai_message("")
527                                                                        .with_tool_calls(
528                                                                            tools_value,
529                                                                        ),
530                                                                );
531                                                            }
532                                                            memory.add_message(
533                                                                Message::new_tool_message(
534                                                                    observation.clone(),
535                                                                    tool_id,
536                                                                ),
537                                                            );
538                                                        } else {
539                                                            tracing::warn!(
540                                                                "Failed to parse tools JSON: {}",
541                                                                tools
542                                                            );
543                                                        }
544                                                    }
545                                                    Err(e) => {
546                                                        tracing::warn!(
547                                                            "Failed to parse action log: {}",
548                                                            e
549                                                        );
550                                                    }
551                                                }
552                                            }
553                                        }
554
555                                        break;
556                                    }
557                                    AgentEvent::Finish(finish) => {
558                                        if let Some(memory) = &memory {
559                                            let mut memory = memory.lock().await;
560
561                                            if steps.is_empty()
562                                                && current_iteration_steps.is_empty()
563                                            {
564                                                memory.add_user_message(
565                                                    match &input_variables["input"] {
566                                                        Value::String(s) => s,
567                                                        x => x,
568                                                    },
569                                                );
570                                            }
571
572                                            if !accumulated_content.is_empty() {
573                                                memory.add_ai_message(&accumulated_content);
574                                            }
575
576                                            let mut tools_ai_message_seen: HashMap<String, ()> =
577                                                HashMap::default();
578                                            for (action, observation) in &steps {
579                                                match serde_json::from_str::<LogTools>(&action.log)
580                                                {
581                                                    Ok(LogTools { tool_id, tools }) => {
582                                                        if let Ok(tools_value) =
583                                                            serde_json::from_str::<Value>(&tools)
584                                                        {
585                                                            if tools_ai_message_seen
586                                                                .insert(tools, ())
587                                                                .is_none()
588                                                            {
589                                                                memory.add_message(
590                                                                    Message::new_ai_message("")
591                                                                        .with_tool_calls(
592                                                                            tools_value,
593                                                                        ),
594                                                                );
595                                                            }
596                                                            memory.add_message(
597                                                                Message::new_tool_message(
598                                                                    observation.clone(),
599                                                                    tool_id,
600                                                                ),
601                                                            );
602                                                        } else {
603                                                            tracing::warn!(
604                                                                "Failed to parse tools JSON: {tools}"
605                                                            );
606                                                        }
607                                                    }
608                                                    Err(e) => {
609                                                        tracing::warn!(
610                                                            "Failed to parse action log: {e}"
611                                                        );
612                                                    }
613                                                }
614                                            }
615                                            memory.add_ai_message(&finish.output);
616                                        }
617
618                                        let _ = tx.send(Ok(StreamData::new(
619                                            json!({
620                                                "id": chat_completion_id,
621                                                "conversation_id": conversation_id,
622                                                "object": "chat.completion.chunk",
623                                                "created": created,
624                                                "model": model,
625                                                "choices": [{
626                                                    "index": 0,
627                                                    "delta": {},
628                                                    "logprobs": null,
629                                                    "finish_reason": "stop"
630                                                }]
631                                            }),
632                                            None,
633                                            "stop",
634                                        )));
635                                        return;
636                                    }
637                                }
638                            }
639                        },
640                        Err(e) => {
641                            let _ = tx.send(Ok(StreamData::new(
642                                json!({
643                                    "id": chat_completion_id,
644                                    "conversation_id": conversation_id,
645                                    "object": "chat.completion.chunk",
646                                    "created": created,
647                                    "model": model,
648                                    "choices": [{
649                                        "index": 0,
650                                        "delta": {
651                                            "content": format!("Stream error: {e}")
652                                        },
653                                        "logprobs": null,
654                                        "finish_reason": "stop"
655                                    }]
656                                }),
657                                None,
658                                "",
659                            )));
660                            return;
661                        }
662                    }
663                }
664
665                if let Some(memory) = &memory {
666                    let memory = memory.lock().await;
667                    let messages = memory.messages();
668                    input_variables.insert("chat_history".to_string(), json!(messages));
669                }
670
671                // Check max iterations before continuing
672                if let Some(max_iterations) = max_iterations {
673                    if steps.len() >= max_iterations as usize {
674                        let _ = tx.send(Ok(StreamData::new(
675                            json!({
676                                "id": chat_completion_id,
677                                "conversation_id": conversation_id,
678                                "object": "chat.completion.chunk",
679                                "created": created,
680                                "model": model,
681                                "choices": [{
682                                    "index": 0,
683                                    "delta": {
684                                        "content": "Maximum iterations reached."
685                                    },
686                                    "logprobs": null,
687                                    "finish_reason": "length"
688                                }]
689                            }),
690                            None,
691                            "Maximum iterations reached.",
692                        )));
693                        return;
694                    }
695                }
696            }
697        });
698
699        Ok(Box::pin(UnboundedReceiverStream::new(rx)))
700    }
701}