Skip to main content

tool_parser/parsers/
cohere.rs

1//! Cohere Command model tool call parser
2//!
3//! Parses tool calls from `<|START_ACTION|>...<|END_ACTION|>` blocks.
4//! Supports both CMD3 and CMD4 formats.
5//!
6//! # Format
7//!
8//! Cohere models output tool calls in the following format:
9//! ```text
10//! <|START_RESPONSE|>Let me help with that.<|END_RESPONSE|>
11//! <|START_ACTION|>
12//! {"tool_name": "search", "parameters": {"query": "rust programming"}}
13//! <|END_ACTION|>
14//! ```
15//!
16//! Or for multiple tool calls:
17//! ```text
18//! <|START_ACTION|>
19//! [
20//!   {"tool_name": "search", "parameters": {"query": "rust"}},
21//!   {"tool_name": "get_weather", "parameters": {"city": "Paris"}}
22//! ]
23//! <|END_ACTION|>
24//! ```
25//!
26//! # Field Mapping
27//! - `tool_name` → `name`
28//! - `parameters` → `arguments`
29
30use async_trait::async_trait;
31use openai_protocol::common::Tool;
32use serde_json::Value;
33
34use crate::{
35    errors::{ParserError, ParserResult},
36    parsers::helpers,
37    partial_json::PartialJson,
38    traits::ToolParser,
39    types::{FunctionCall, StreamingParseResult, ToolCall, ToolCallItem},
40};
41
42const START_ACTION: &str = "<|START_ACTION|>";
43const END_ACTION: &str = "<|END_ACTION|>";
44const START_RESPONSE: &str = "<|START_RESPONSE|>";
45const END_RESPONSE: &str = "<|END_RESPONSE|>";
46const START_TEXT: &str = "<|START_TEXT|>";
47const END_TEXT: &str = "<|END_TEXT|>";
48
49/// State machine for Cohere tool parsing
50#[derive(Debug, Clone, Copy, PartialEq)]
51enum ParseState {
52    /// Looking for START_ACTION marker
53    Text,
54    /// Inside an action block, parsing JSON
55    InAction,
56}
57
58/// Cohere Command model tool call parser
59///
60/// Handles the Cohere-specific format:
61/// `<|START_ACTION|>{"tool_name": "func", "parameters": {...}}<|END_ACTION|>`
62pub struct CohereParser {
63    /// Current parsing state
64    state: ParseState,
65
66    /// Parser for handling incomplete JSON during streaming
67    partial_json: PartialJson,
68
69    /// Buffer for accumulating incomplete patterns across chunks
70    buffer: String,
71
72    /// Stores complete tool call info (name and arguments) for each tool being parsed
73    prev_tool_call_arr: Vec<Value>,
74
75    /// Index of currently streaming tool call (-1 means no active tool)
76    current_tool_id: i32,
77
78    /// Flag for whether current tool's name has been sent to client
79    current_tool_name_sent: bool,
80
81    /// Tracks raw JSON string content streamed to client for each tool's arguments
82    streamed_args_for_tool: Vec<String>,
83}
84
85impl CohereParser {
86    /// Create a new Cohere parser
87    pub fn new() -> Self {
88        Self {
89            state: ParseState::Text,
90            partial_json: PartialJson::default(),
91            buffer: String::new(),
92            prev_tool_call_arr: Vec::new(),
93            current_tool_id: -1,
94            current_tool_name_sent: false,
95            streamed_args_for_tool: Vec::new(),
96        }
97    }
98
99    /// Clean text by removing response markers
100    fn clean_text(text: &str) -> String {
101        text.replace(START_RESPONSE, "")
102            .replace(END_RESPONSE, "")
103            .replace(START_TEXT, "")
104            .replace(END_TEXT, "")
105    }
106
107    /// Convert a Cohere tool call JSON object to our ToolCall format
108    fn convert_tool_call(json_str: &str) -> ParserResult<Vec<ToolCall>> {
109        let value: Value = serde_json::from_str(json_str.trim())
110            .map_err(|e| ParserError::ParsingFailed(format!("Invalid JSON: {e}")))?;
111
112        let tools = match value {
113            Value::Array(arr) => arr,
114            single => vec![single],
115        };
116
117        tools
118            .into_iter()
119            .filter_map(|tool| {
120                // Cohere uses "tool_name" instead of "name"
121                let name = tool
122                    .get("tool_name")
123                    .and_then(|v| v.as_str())
124                    .or_else(|| tool.get("name").and_then(|v| v.as_str()))?;
125
126                // Cohere uses "parameters" instead of "arguments"
127                let parameters = tool
128                    .get("parameters")
129                    .or_else(|| tool.get("arguments"))
130                    .map(|v| v.to_string())
131                    .unwrap_or_else(|| "{}".to_string());
132
133                Some(Ok(ToolCall {
134                    function: FunctionCall {
135                        name: name.to_string(),
136                        arguments: parameters,
137                    },
138                }))
139            })
140            .collect()
141    }
142
143    /// Extract JSON between START_ACTION and END_ACTION (skip markers inside strings).
144    fn extract_action_json(text: &str) -> Option<(usize, &str, usize)> {
145        let start_idx = text.find(START_ACTION)?;
146        let json_start = start_idx + START_ACTION.len();
147        let end_offset = Self::find_end_action_outside_strings(&text[json_start..])?;
148        let json_str = &text[json_start..json_start + end_offset];
149        Some((
150            start_idx,
151            json_str,
152            json_start + end_offset + END_ACTION.len(),
153        ))
154    }
155
156    /// Offset of END_ACTION in `s`, ignoring occurrences inside JSON strings.
157    /// Incomplete / unclosed quotes return None so streaming can wait for more
158    /// input (no find/rfind fallback — that breaks mid-string streams and can
159    /// swallow a following action).
160    fn find_end_action_outside_strings(s: &str) -> Option<usize> {
161        let mut in_string = false;
162        let mut escape = false;
163        let bytes = s.as_bytes();
164        let needle = END_ACTION.as_bytes();
165        let mut i = 0;
166        while i + needle.len() <= bytes.len() {
167            if escape {
168                escape = false;
169                i += 1;
170                continue;
171            }
172            let b = bytes[i];
173            if in_string {
174                if b == b'\\' {
175                    escape = true;
176                    i += 1;
177                    continue;
178                }
179                if b == b'"' {
180                    in_string = false;
181                }
182                i += 1;
183                continue;
184            }
185            if b == b'"' {
186                in_string = true;
187                i += 1;
188                continue;
189            }
190            if Some(&b) == needle.first() && bytes[i..].starts_with(needle) {
191                return Some(i);
192            }
193            i += 1;
194        }
195        None
196    }
197}
198
199impl Default for CohereParser {
200    fn default() -> Self {
201        Self::new()
202    }
203}
204
205#[async_trait]
206impl ToolParser for CohereParser {
207    async fn parse_complete(&self, text: &str) -> ParserResult<(String, Vec<ToolCall>)> {
208        // Check if text contains Cohere format
209        if !self.has_tool_markers(text) {
210            let cleaned = Self::clean_text(text);
211            return Ok((cleaned.trim().to_string(), vec![]));
212        }
213
214        let mut normal_text = String::new();
215        let mut tool_calls = Vec::new();
216        let mut remaining = text;
217
218        while let Some((start_idx, json_str, end_idx)) = Self::extract_action_json(remaining) {
219            // Text before action
220            normal_text.push_str(&remaining[..start_idx]);
221
222            // Parse tool calls from this action block
223            match Self::convert_tool_call(json_str) {
224                Ok(calls) => tool_calls.extend(calls),
225                Err(e) => {
226                    tracing::debug!("Failed to parse Cohere tool call: {}", e);
227                }
228            }
229
230            remaining = &remaining[end_idx..];
231        }
232
233        // Append any remaining text after last action block
234        normal_text.push_str(remaining);
235
236        // Clean up response markers
237        let cleaned_text = Self::clean_text(&normal_text);
238
239        Ok((cleaned_text.trim().to_string(), tool_calls))
240    }
241
242    async fn parse_incremental(
243        &mut self,
244        chunk: &str,
245        tools: &[Tool],
246    ) -> ParserResult<StreamingParseResult> {
247        self.buffer.push_str(chunk);
248
249        match self.state {
250            ParseState::Text => {
251                // Check for START_ACTION marker
252                let start_pos = self.buffer.find(START_ACTION);
253                if let Some(pos) = start_pos {
254                    // Emit text before the action as normal text
255                    let text_before = Self::clean_text(&self.buffer[..pos]);
256
257                    // Switch to InAction state and keep only content after START_ACTION
258                    self.state = ParseState::InAction;
259                    self.buffer.drain(..pos + START_ACTION.len());
260
261                    return Ok(StreamingParseResult {
262                        normal_text: text_before,
263                        calls: vec![],
264                    });
265                }
266
267                // Check for partial START_ACTION
268                if helpers::ends_with_partial_token(&self.buffer, START_ACTION).is_some() {
269                    // Keep buffering
270                    return Ok(StreamingParseResult::default());
271                }
272
273                // No action starting, emit cleaned text
274                let cleaned = Self::clean_text(&self.buffer);
275                self.buffer.clear();
276                Ok(StreamingParseResult {
277                    normal_text: cleaned,
278                    calls: vec![],
279                })
280            }
281
282            ParseState::InAction => {
283                // Check if we have END_ACTION
284                if let Some(pos) = Self::find_end_action_outside_strings(&self.buffer) {
285                    // We have complete JSON - extract it before modifying buffer
286                    let json_content = self.buffer[..pos].to_string();
287
288                    // Build tool indices
289                    let tool_indices = helpers::get_tool_indices(tools);
290
291                    // Create a temporary buffer for the helper (it expects to manage buffer state)
292                    let mut temp_buffer = String::new();
293
294                    // Use helper for streaming - pass JSON directly with offset 0
295                    let result = helpers::handle_json_tool_streaming(
296                        &json_content,
297                        0,
298                        &mut self.partial_json,
299                        &tool_indices,
300                        &mut temp_buffer,
301                        &mut self.current_tool_id,
302                        &mut self.current_tool_name_sent,
303                        &mut self.streamed_args_for_tool,
304                        &mut self.prev_tool_call_arr,
305                    )?;
306
307                    // Move past END_ACTION and switch back to Text state
308                    self.buffer.drain(..pos + END_ACTION.len());
309                    self.state = ParseState::Text;
310
311                    return Ok(result);
312                }
313
314                // Partial JSON - buffer and wait for END_ACTION
315                // Unlike formats without end markers, we can't stream partial JSON safely
316                // because we don't know if the JSON is complete until we see END_ACTION
317                Ok(StreamingParseResult::default())
318            }
319        }
320    }
321
322    fn has_tool_markers(&self, text: &str) -> bool {
323        text.contains(START_ACTION) || text.contains(END_ACTION)
324    }
325
326    fn get_unstreamed_tool_args(&self) -> Option<Vec<ToolCallItem>> {
327        helpers::get_unstreamed_args(&self.prev_tool_call_arr, &self.streamed_args_for_tool)
328    }
329
330    fn reset(&mut self) {
331        self.state = ParseState::Text;
332        helpers::reset_parser_state(
333            &mut self.buffer,
334            &mut self.prev_tool_call_arr,
335            &mut self.current_tool_id,
336            &mut self.current_tool_name_sent,
337            &mut self.streamed_args_for_tool,
338        );
339    }
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345
346    #[tokio::test]
347    async fn test_single_tool_call() {
348        let parser = CohereParser::new();
349        let input = r#"<|START_RESPONSE|>Let me search for that.<|END_RESPONSE|>
350<|START_ACTION|>
351{"tool_name": "search", "parameters": {"query": "rust programming"}}
352<|END_ACTION|>"#;
353
354        let (normal_text, tools) = parser.parse_complete(input).await.unwrap();
355        assert_eq!(tools.len(), 1);
356        assert_eq!(normal_text, "Let me search for that.");
357        assert_eq!(tools[0].function.name, "search");
358
359        let args: Value = serde_json::from_str(&tools[0].function.arguments).unwrap();
360        assert_eq!(args["query"], "rust programming");
361    }
362
363    #[tokio::test]
364    async fn test_multiple_tool_calls_array() {
365        let parser = CohereParser::new();
366        let input = r#"<|START_ACTION|>
367[
368  {"tool_name": "search", "parameters": {"query": "rust"}},
369  {"tool_name": "get_weather", "parameters": {"city": "Paris"}}
370]
371<|END_ACTION|>"#;
372
373        let (_, tools) = parser.parse_complete(input).await.unwrap();
374        assert_eq!(tools.len(), 2);
375        assert_eq!(tools[0].function.name, "search");
376        assert_eq!(tools[1].function.name, "get_weather");
377    }
378
379    #[tokio::test]
380    async fn test_no_tool_calls() {
381        let parser = CohereParser::new();
382        let input = "<|START_RESPONSE|>Hello, how can I help?<|END_RESPONSE|>";
383
384        let (normal_text, tools) = parser.parse_complete(input).await.unwrap();
385        assert_eq!(tools.len(), 0);
386        assert_eq!(normal_text, "Hello, how can I help?");
387    }
388
389    #[tokio::test]
390    async fn test_has_tool_markers() {
391        let parser = CohereParser::new();
392
393        assert!(parser.has_tool_markers("<|START_ACTION|>"));
394        assert!(parser.has_tool_markers("<|END_ACTION|>"));
395        assert!(parser.has_tool_markers("Some text <|START_ACTION|> more"));
396        assert!(!parser.has_tool_markers("Just plain text"));
397        assert!(!parser.has_tool_markers("[TOOL_CALLS]")); // Mistral format
398    }
399
400    #[tokio::test]
401    async fn test_empty_parameters() {
402        let parser = CohereParser::new();
403        let input = r#"<|START_ACTION|>{"tool_name": "ping"}<|END_ACTION|>"#;
404
405        let (_, tools) = parser.parse_complete(input).await.unwrap();
406        assert_eq!(tools.len(), 1);
407        assert_eq!(tools[0].function.name, "ping");
408        assert_eq!(tools[0].function.arguments, "{}");
409    }
410
411    #[tokio::test]
412    async fn test_nested_json() {
413        let parser = CohereParser::new();
414        let input = r#"<|START_ACTION|>
415{"tool_name": "process", "parameters": {"config": {"nested": {"value": [1, 2, 3]}}}}
416<|END_ACTION|>"#;
417
418        let (_, tools) = parser.parse_complete(input).await.unwrap();
419        assert_eq!(tools.len(), 1);
420
421        let args: Value = serde_json::from_str(&tools[0].function.arguments).unwrap();
422        assert_eq!(
423            args["config"]["nested"]["value"],
424            serde_json::json!([1, 2, 3])
425        );
426    }
427
428    #[tokio::test]
429    async fn test_text_markers_cleaned() {
430        let parser = CohereParser::new();
431        let input = r#"<|START_TEXT|>Some intro<|END_TEXT|>
432<|START_ACTION|>{"tool_name": "test", "parameters": {}}<|END_ACTION|>
433<|START_TEXT|>Conclusion<|END_TEXT|>"#;
434
435        let (normal_text, tools) = parser.parse_complete(input).await.unwrap();
436        assert_eq!(tools.len(), 1);
437        assert!(normal_text.contains("Some intro"));
438        assert!(normal_text.contains("Conclusion"));
439        assert!(!normal_text.contains("<|START_TEXT|>"));
440        assert!(!normal_text.contains("<|END_TEXT|>"));
441    }
442
443    #[tokio::test]
444    async fn test_malformed_json() {
445        let parser = CohereParser::new();
446        let input = r#"<|START_ACTION|>{"tool_name": invalid}<|END_ACTION|>"#;
447
448        let (_, tools) = parser.parse_complete(input).await.unwrap();
449        // Should gracefully handle malformed JSON
450        assert_eq!(tools.len(), 0);
451    }
452}