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 ¤t_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}