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