rmcp_agent/agent/
executor.rs

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