Skip to main content

rmcp_agent/agent/
core.rs

1use std::sync::Arc;
2
3use async_trait::async_trait;
4use langchain_rust::agent::{Agent, AgentError};
5use langchain_rust::chain::Chain;
6use langchain_rust::prompt::{HumanMessagePromptTemplate, MessageFormatterStruct, PromptArgs};
7use langchain_rust::schemas::{
8    AgentAction, AgentEvent, AgentFinish, FunctionCallResponse, LogTools, Message,
9};
10use langchain_rust::tools::Tool;
11use langchain_rust::{
12    fmt_message, fmt_placeholder, fmt_template, message_formatter, template_jinja2,
13};
14use serde_json::json;
15
16use crate::agent::extension::{AgentEventChunk, AgentExt, AgentStream, DeltaEvent};
17use crate::agent::intermediate::IntermediateStep;
18
19pub struct OpenAIMcpAgent {
20    pub chain: Box<dyn Chain>,
21    pub tools: Vec<Arc<dyn Tool>>,
22}
23
24impl OpenAIMcpAgent {
25    pub(crate) fn create_prompt(prefix: &str) -> MessageFormatterStruct {
26        let message = Message::new_system_message(prefix);
27        let template = HumanMessagePromptTemplate::new(template_jinja2!("{{input}}", "input"));
28
29        message_formatter![
30            fmt_message!(message),
31            fmt_template!(template),
32            fmt_placeholder!("chat_history"),
33            fmt_placeholder!("agent_scratchpad")
34        ]
35    }
36
37    pub fn construct_scratchpad(
38        &self,
39        intermediate_steps: &[impl IntermediateStep],
40    ) -> Result<Vec<Message>, AgentError> {
41        let mut thoughts: Vec<Message> = vec![];
42
43        const MAX_STEPS: usize = 5;
44        const SUMMARY_THRESHOLD: usize = 10;
45
46        if intermediate_steps.len() > SUMMARY_THRESHOLD {
47            let summary_msg = self.create_summary_message(
48                &intermediate_steps[..intermediate_steps.len() - MAX_STEPS],
49            )?;
50            thoughts.push(summary_msg);
51
52            for step in &intermediate_steps[intermediate_steps.len() - MAX_STEPS..] {
53                step.append_to_conversation(&mut thoughts)?;
54            }
55        } else {
56            for step in intermediate_steps {
57                step.append_to_conversation(&mut thoughts)?;
58            }
59        }
60
61        Ok(thoughts)
62    }
63
64    fn create_summary_message(
65        &self,
66        old_steps: &[impl IntermediateStep],
67    ) -> Result<Message, AgentError> {
68        let summary = format!(
69            "Previous {} steps summary: [Summarized execution history with {} actions completed]",
70            old_steps.len(),
71            old_steps.len()
72        );
73
74        Ok(Message::new_system_message(&summary))
75    }
76
77    fn process_chunk_delta(
78        chunk: &serde_json::Value,
79        model_output: &mut String,
80        tool_call_acc: &mut ToolCallAccumulator,
81        has_tool_calls: &mut bool,
82    ) -> Vec<AgentEventChunk> {
83        let mut events = Vec::new();
84
85        let Some(choices) = chunk.get("choices").and_then(|c| c.as_array()) else {
86            return events;
87        };
88
89        let Some(choice) = choices.first() else {
90            return events;
91        };
92
93        let Some(delta) = choice.get("delta") else {
94            return events;
95        };
96
97        // Handle tool calls
98        if let Some(content) = delta.get("content").and_then(|c| c.as_str()) {
99            if !content.is_empty() {
100                model_output.push_str(content);
101                events.push(AgentEventChunk::Delta(DeltaEvent::Content(
102                    content.to_string(),
103                )));
104            }
105        } else if let Some(_tool_calls) = delta.get("tool_calls").and_then(|tc| tc.as_array()) {
106            *has_tool_calls = true;
107            let tool_call_chunk = tool_call_acc.accumulate(delta);
108            events.push(AgentEventChunk::Delta(DeltaEvent::Action(tool_call_chunk)));
109        }
110
111        // Handle finish reason
112        if let Some(finish_reason) = choice.get("finish_reason").and_then(|f| f.as_str()) {
113            let final_event = Self::handle_finish_reason(
114                finish_reason,
115                tool_call_acc,
116                *has_tool_calls,
117                model_output,
118            );
119            events.push(AgentEventChunk::Final(final_event));
120        }
121
122        events
123    }
124
125    fn handle_finish_reason(
126        finish_reason: &str,
127        tool_call_acc: &mut ToolCallAccumulator,
128        has_tool_calls: bool,
129        model_output: &str,
130    ) -> AgentEvent {
131        match finish_reason {
132            "tool_calls" => {
133                let action = tool_call_acc.take_action();
134                AgentEvent::Action(vec![action])
135            }
136            "stop" => {
137                if has_tool_calls {
138                    let action = tool_call_acc.take_action();
139                    AgentEvent::Action(vec![action])
140                } else {
141                    AgentEvent::Finish(AgentFinish {
142                        output: model_output.to_string(),
143                    })
144                }
145            }
146            _ => AgentEvent::Finish(AgentFinish {
147                output: model_output.to_string(),
148            }),
149        }
150    }
151}
152
153#[async_trait]
154impl Agent for OpenAIMcpAgent {
155    async fn plan(
156        &self,
157        intermediate_steps: &[(AgentAction, String)],
158        inputs: PromptArgs,
159    ) -> Result<AgentEvent, AgentError> {
160        let mut inputs = inputs.clone();
161        let scratchpad = self.construct_scratchpad(intermediate_steps)?;
162        inputs.insert("agent_scratchpad".to_string(), json!(scratchpad));
163        let output = self.chain.call(inputs).await?.generation;
164
165        match serde_json::from_str::<Vec<FunctionCallResponse>>(&output) {
166            Ok(tools) => {
167                let mut actions = Vec::with_capacity(tools.len());
168                for tool in tools {
169                    //Log tools will be send as log
170                    let log = LogTools {
171                        tool_id: tool.id,
172                        tools: output.clone(),
173                    };
174                    actions.push(AgentAction {
175                        tool: tool.function.name,
176                        tool_input: tool.function.arguments,
177                        log: serde_json::to_string(&log)?,
178                    });
179                }
180                return Ok(AgentEvent::Action(actions));
181            }
182            Err(_) => return Ok(AgentEvent::Finish(AgentFinish { output })),
183        }
184    }
185
186    fn get_tools(&self) -> Vec<Arc<dyn Tool>> {
187        self.tools.clone()
188    }
189}
190
191#[async_trait]
192impl AgentExt for OpenAIMcpAgent {
193    async fn plan_with_steps(
194        &self,
195        steps: &[impl IntermediateStep],
196        inputs: PromptArgs,
197    ) -> Result<AgentEvent, AgentError> {
198        let mut inputs = inputs.clone();
199        let scratchpad = self.construct_scratchpad(steps)?;
200        inputs.insert("agent_scratchpad".to_string(), json!(scratchpad));
201        let output = self.chain.call(inputs).await?.generation;
202
203        match serde_json::from_str::<Vec<FunctionCallResponse>>(&output) {
204            Ok(tools) => {
205                let mut actions = Vec::with_capacity(tools.len());
206                for tool in tools {
207                    // Log tools will be sent as log
208                    let log = LogTools {
209                        tool_id: tool.id,
210                        tools: output.clone(),
211                    };
212                    actions.push(AgentAction {
213                        tool: tool.function.name,
214                        tool_input: tool.function.arguments,
215                        log: serde_json::to_string(&log)?,
216                    });
217                }
218                return Ok(AgentEvent::Action(actions));
219            }
220            Err(_) => return Ok(AgentEvent::Finish(AgentFinish { output })),
221        }
222    }
223
224    async fn plan_stream(
225        &self,
226        steps: &[impl IntermediateStep],
227        inputs: PromptArgs,
228    ) -> Result<AgentStream, AgentError> {
229        use async_stream::stream;
230        use futures_util::StreamExt;
231
232        let mut inputs = inputs.clone();
233        let scratchpad = self.construct_scratchpad(steps)?;
234        inputs.insert("agent_scratchpad".to_string(), json!(scratchpad));
235
236        let mut chain_stream = self.chain.stream(inputs).await?;
237        let mut model_output = String::new();
238        let mut tool_call_acc = ToolCallAccumulator::new();
239        let mut has_tool_calls = false;
240
241        let s = stream! {
242            while let Some(chunk_result) = chain_stream.next().await {
243                let chunk = match chunk_result {
244                    Ok(chunk) => chunk,
245                    Err(e) => {
246                        yield Err(e);
247                        return;
248                    }
249                };
250
251                // Process chunk and get events
252                let events = Self::process_chunk_delta(&chunk.value, &mut model_output, &mut tool_call_acc, &mut has_tool_calls);
253
254                for event in events {
255                    // Check if this is a final event that should end the stream
256                    let is_final = matches!(event, AgentEventChunk::Final(_));
257                    yield Ok(event);
258
259                    if is_final {
260                        return;
261                    }
262                }
263            }
264
265            match has_tool_calls {
266                true => {
267                    let action = tool_call_acc.take_action();
268                    yield Ok(AgentEventChunk::Final(AgentEvent::Action(vec![action])))
269                },
270                false => yield Ok(AgentEventChunk::Final(AgentEvent::Finish(AgentFinish { output: model_output })))
271            }
272        };
273
274        Ok(Box::pin(s) as AgentStream)
275    }
276}
277
278struct ToolCallAccumulator {
279    name: Option<String>,
280    args: String,
281    id: Option<String>,
282}
283
284impl ToolCallAccumulator {
285    fn new() -> Self {
286        Self {
287            name: None,
288            args: String::new(),
289            id: None,
290        }
291    }
292
293    fn accumulate(&mut self, delta: &serde_json::Value) -> AgentAction {
294        let mut args_chunk = String::default();
295
296        if let Some(tool_call) = delta
297            .get("tool_calls")
298            .and_then(|v| v.as_array())
299            .and_then(|v| v.first())
300        {
301            if let Some(function) = tool_call.get("function") {
302                if let Some(name) = function.get("name").and_then(|n| n.as_str()) {
303                    self.name = Some(name.to_string());
304                }
305                if let Some(args) = function.get("arguments").and_then(|a| a.as_str()) {
306                    self.args.push_str(args);
307                    args_chunk = args.to_string();
308                }
309            }
310            if let Some(id) = tool_call.get("id").and_then(|i| i.as_str()) {
311                if !id.is_empty() {
312                    self.id = Some(id.to_string());
313                }
314            }
315        };
316
317        self.to_action_chunk(args_chunk)
318    }
319
320    fn take_action(&mut self) -> AgentAction {
321        let args = std::mem::take(&mut self.args);
322        self.to_action_chunk(args)
323    }
324
325    fn to_action_chunk(&self, args_chunk: String) -> AgentAction {
326        let processed_args = if args_chunk.trim().is_empty() {
327            "{}".to_string()
328        } else {
329            args_chunk
330        };
331
332        let function_call_response = serde_json::json!({
333            "id": self.id.clone(),
334            "type": "function",
335            "function": {
336                "name": self.name.clone(),
337                "arguments": processed_args
338            }
339        });
340
341        // Construct tool call array (consistent with non-streaming method)
342        let tools_array = serde_json::json!([function_call_response]);
343        let tools_output = serde_json::to_string(&tools_array).unwrap_or_else(|_| {
344            "[{{\"error\": \"Failed to serialize function call\"}}]".to_string()
345        });
346
347        let log_tools = LogTools {
348            tool_id: self.id.clone().unwrap_or_default(),
349            tools: tools_output,
350        };
351
352        let log_str = serde_json::to_string(&log_tools).unwrap_or_else(|_| {
353            // If serialization fails, return a simple format
354            format!(
355                "{{\"tool_id\": \"{}\", \"tools\": \"[]\"}}",
356                self.id.clone().unwrap_or_default()
357            )
358        });
359
360        AgentAction {
361            tool: self.name.clone().unwrap_or_default(),
362            tool_input: processed_args,
363            log: log_str,
364        }
365    }
366}