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