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.first() else {
63 return events;
64 };
65
66 let Some(delta) = choice.get("delta") else {
67 return events;
68 };
69
70 if let Some(_tool_calls) = delta.get("tool_calls").and_then(|tc| tc.as_array()) {
72 *has_tool_calls = true;
73 let tool_call_chunk = tool_call_acc.accumulate(delta);
74 events.push(AgentEventChunk::Delta(DeltaEvent::Action(tool_call_chunk)));
75 } else if let Some(content) = delta.get("content").and_then(|c| c.as_str()) {
76 if !content.is_empty() {
77 model_output.push_str(content);
78 events.push(AgentEventChunk::Delta(DeltaEvent::Content(
79 content.to_string(),
80 )));
81 }
82 }
83
84 if let Some(finish_reason) = choice.get("finish_reason").and_then(|f| f.as_str()) {
86 let final_event = Self::handle_finish_reason(
87 finish_reason,
88 tool_call_acc,
89 *has_tool_calls,
90 model_output,
91 );
92 events.push(AgentEventChunk::Final(final_event));
93 }
94
95 events
96 }
97
98 fn handle_finish_reason(
99 finish_reason: &str,
100 tool_call_acc: &mut ToolCallAccumulator,
101 has_tool_calls: bool,
102 model_output: &str,
103 ) -> AgentEvent {
104 match finish_reason {
105 "tool_calls" => {
106 let action = tool_call_acc.take_action();
107 AgentEvent::Action(vec![action])
108 }
109 "stop" => {
110 if has_tool_calls {
111 let action = tool_call_acc.take_action();
112 AgentEvent::Action(vec![action])
113 } else {
114 AgentEvent::Finish(AgentFinish {
115 output: model_output.to_string(),
116 })
117 }
118 }
119 _ => AgentEvent::Finish(AgentFinish {
120 output: model_output.to_string(),
121 }),
122 }
123 }
124}
125
126#[async_trait]
127impl Agent for OpenAIMcpAgent {
128 async fn plan(
129 &self,
130 intermediate_steps: &[(AgentAction, String)],
131 inputs: PromptArgs,
132 ) -> Result<AgentEvent, AgentError> {
133 let mut inputs = inputs.clone();
134 let scratchpad = self.construct_scratchpad(intermediate_steps)?;
135 inputs.insert("agent_scratchpad".to_string(), json!(scratchpad));
136 let output = self.chain.call(inputs).await?.generation;
137
138 match serde_json::from_str::<Vec<FunctionCallResponse>>(&output) {
139 Ok(tools) => {
140 let mut actions = Vec::with_capacity(tools.len());
141 for tool in tools {
142 let log = LogTools {
144 tool_id: tool.id,
145 tools: output.clone(),
146 };
147 actions.push(AgentAction {
148 tool: tool.function.name,
149 tool_input: tool.function.arguments,
150 log: serde_json::to_string(&log)?,
151 });
152 }
153 return Ok(AgentEvent::Action(actions));
154 }
155 Err(_) => return Ok(AgentEvent::Finish(AgentFinish { output })),
156 }
157 }
158
159 fn get_tools(&self) -> Vec<Arc<dyn Tool>> {
160 self.tools.clone()
161 }
162}
163
164#[async_trait]
165impl AgentExt for OpenAIMcpAgent {
166 async fn plan_with_steps(
167 &self,
168 steps: &[impl IntermediateStep],
169 inputs: PromptArgs,
170 ) -> Result<AgentEvent, AgentError> {
171 let mut inputs = inputs.clone();
172 let scratchpad = self.construct_scratchpad(steps)?;
173 inputs.insert("agent_scratchpad".to_string(), json!(scratchpad));
174 let output = self.chain.call(inputs).await?.generation;
175
176 match serde_json::from_str::<Vec<FunctionCallResponse>>(&output) {
177 Ok(tools) => {
178 let mut actions = Vec::with_capacity(tools.len());
179 for tool in tools {
180 let log = LogTools {
182 tool_id: tool.id,
183 tools: output.clone(),
184 };
185 actions.push(AgentAction {
186 tool: tool.function.name,
187 tool_input: tool.function.arguments,
188 log: serde_json::to_string(&log)?,
189 });
190 }
191 return Ok(AgentEvent::Action(actions));
192 }
193 Err(_) => return Ok(AgentEvent::Finish(AgentFinish { output })),
194 }
195 }
196
197 async fn plan_stream(
198 &self,
199 steps: &[impl IntermediateStep],
200 inputs: PromptArgs,
201 ) -> Result<AgentStream, AgentError> {
202 use async_stream::stream;
203 use futures_util::StreamExt;
204
205 let mut inputs = inputs.clone();
206 let scratchpad = self.construct_scratchpad(steps)?;
207 inputs.insert("agent_scratchpad".to_string(), json!(scratchpad));
208
209 let mut chain_stream = self.chain.stream(inputs).await?;
210 let mut model_output = String::new();
211 let mut tool_call_acc = ToolCallAccumulator::new();
212 let mut has_tool_calls = false;
213
214 let s = stream! {
215 while let Some(chunk_result) = chain_stream.next().await {
216 let chunk = match chunk_result {
217 Ok(chunk) => chunk,
218 Err(e) => {
219 yield Err(e);
220 return;
221 }
222 };
223
224 let events = Self::process_chunk_delta(&chunk.value, &mut model_output, &mut tool_call_acc, &mut has_tool_calls);
226
227 for event in events {
228 let is_final = matches!(event, AgentEventChunk::Final(_));
230 yield Ok(event);
231
232 if is_final {
233 return;
234 }
235 }
236 }
237
238 match has_tool_calls {
239 true => {
240 let action = tool_call_acc.take_action();
241 yield Ok(AgentEventChunk::Final(AgentEvent::Action(vec![action])))
242 },
243 false => yield Ok(AgentEventChunk::Final(AgentEvent::Finish(AgentFinish { output: model_output })))
244 }
245 };
246
247 Ok(Box::pin(s) as AgentStream)
248 }
249}
250
251struct ToolCallAccumulator {
252 name: Option<String>,
253 args: String,
254 id: Option<String>,
255}
256
257impl ToolCallAccumulator {
258 fn new() -> Self {
259 Self {
260 name: None,
261 args: String::new(),
262 id: None,
263 }
264 }
265
266 fn accumulate(&mut self, delta: &serde_json::Value) -> AgentAction {
267 let mut args_chunk = String::default();
268
269 if let Some(tool_call) = delta
270 .get("tool_calls")
271 .and_then(|v| v.as_array())
272 .and_then(|v| v.first())
273 {
274 if let Some(function) = tool_call.get("function") {
275 if let Some(name) = function.get("name").and_then(|n| n.as_str()) {
276 self.name = Some(name.to_string());
277 }
278 if let Some(args) = function.get("arguments").and_then(|a| a.as_str()) {
279 self.args.push_str(args);
280 args_chunk = args.to_string();
281 }
282 }
283 if let Some(id) = tool_call.get("id").and_then(|i| i.as_str()) {
284 if !id.is_empty() {
285 self.id = Some(id.to_string());
286 }
287 }
288 };
289
290 self.to_action_chunk(args_chunk)
291 }
292
293 fn take_action(&mut self) -> AgentAction {
294 let args = std::mem::take(&mut self.args);
295 self.to_action_chunk(args)
296 }
297
298 fn to_action_chunk(&self, args_chunk: String) -> AgentAction {
299 let function_call_response = serde_json::json!({
300 "id": self.id.clone(),
301 "type": "function",
302 "function": {
303 "name": self.name.clone(),
304 "arguments": args_chunk
305 }
306 });
307
308 let tools_array = serde_json::json!([function_call_response]);
310 let tools_output = serde_json::to_string(&tools_array).unwrap_or_else(|_| {
311 "[{{\"error\": \"Failed to serialize function call\"}}]".to_string()
312 });
313
314 let log_tools = LogTools {
315 tool_id: self.id.clone().unwrap_or_default(),
316 tools: tools_output,
317 };
318
319 let log_str = serde_json::to_string(&log_tools).unwrap_or_else(|_| {
320 format!(
322 "{{\"tool_id\": \"{}\", \"tools\": \"[]\"}}",
323 self.id.clone().unwrap_or_default()
324 )
325 });
326
327 AgentAction {
328 tool: self.name.clone().unwrap_or_default(),
329 tool_input: args_chunk,
330 log: log_str,
331 }
332 }
333}