Skip to main content

ferrin_core/generate_text/
parse_tool_call.rs

1//! Parsing, validation and repair of model tool calls.
2
3use std::collections::HashMap;
4use std::fmt;
5use std::sync::Arc;
6
7use ferrin_message::Message;
8use ferrin_spec::BoxFuture;
9use ferrin_spec::JsonValue;
10use ferrin_spec::ToolCall;
11use ferrin_spec::ToolChoice;
12use ferrin_spec::ToolName;
13use ferrin_tool::ToolSet;
14
15use super::ParsedToolCall;
16use crate::error::BoxError;
17use crate::error::Error;
18use crate::prompt::Instructions;
19
20/// Information handed to a [`ToolCallRepair`].
21#[derive(Debug)]
22pub struct RepairRequest<'a> {
23    /// The tool call as issued by the model.
24    pub tool_call: &'a ToolCall,
25    /// The tool set of the call.
26    pub tools: &'a ToolSet,
27    /// System instructions of the call.
28    pub system: Option<&'a Instructions>,
29    /// Messages sent to the model in this step.
30    pub messages: &'a [Message],
31    /// The error that triggered the repair ([`Error::NoSuchTool`] or
32    /// [`Error::InvalidToolInput`]).
33    pub error: &'a Error,
34}
35
36impl RepairRequest<'_> {
37    /// JSON schema of `tool_name`'s input, when the tool exists.
38    #[must_use]
39    pub fn input_schema(&self, tool_name: &str) -> Option<&JsonValue> {
40        self.tools
41            .get(tool_name)
42            .map(|tool| tool.input_schema().json_schema())
43    }
44}
45
46/// Repairs tool calls the model got wrong (unknown tool or invalid input).
47///
48/// Return `Ok(None)` to give up (the original error is kept), `Ok(Some(call))`
49/// to re-parse the repaired call, or `Err` to fail with
50/// [`Error::ToolCallRepair`].
51pub trait ToolCallRepair: Send + Sync {
52    /// Attempts a repair.
53    fn repair<'a>(
54        &'a self,
55        request: RepairRequest<'a>,
56    ) -> BoxFuture<'a, Result<Option<ToolCall>, BoxError>>;
57}
58
59/// Rewrites a validated tool input before execution.
60pub type RefineToolInputFn =
61    Arc<dyn Fn(JsonValue) -> BoxFuture<'static, Result<JsonValue, Error>> + Send + Sync>;
62
63/// Refinement functions by tool name.
64#[derive(Clone, Default)]
65pub struct RefineToolInputs(HashMap<ToolName, RefineToolInputFn>);
66
67impl RefineToolInputs {
68    /// Registers `refine` for `tool_name`.
69    pub fn insert(&mut self, tool_name: impl Into<ToolName>, refine: RefineToolInputFn) {
70        self.0.insert(tool_name.into(), refine);
71    }
72
73    /// Looks up the function for `tool_name`.
74    #[must_use]
75    pub fn get(&self, tool_name: &str) -> Option<&RefineToolInputFn> {
76        self.0.get(tool_name)
77    }
78
79    /// Returns `true` when no function is registered.
80    #[must_use]
81    pub fn is_empty(&self) -> bool {
82        self.0.is_empty()
83    }
84}
85
86impl fmt::Debug for RefineToolInputs {
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        f.debug_set().entries(self.0.keys()).finish()
89    }
90}
91
92/// Inputs of [`parse_tool_call`].
93pub(crate) struct ParseContext<'a> {
94    pub(crate) tools: &'a ToolSet,
95    pub(crate) tool_choice: Option<&'a ToolChoice>,
96    pub(crate) repair: Option<&'a dyn ToolCallRepair>,
97    pub(crate) refine: &'a RefineToolInputs,
98    pub(crate) system: Option<&'a Instructions>,
99    pub(crate) messages: &'a [Message],
100}
101
102/// Parses a model tool call. Failures never propagate: the call is marked
103/// `invalid` (and `dynamic`) with the error message attached.
104pub(crate) async fn parse_tool_call(call: &ToolCall, ctx: &ParseContext<'_>) -> ParsedToolCall {
105    match try_parse(call, ctx).await {
106        Ok(parsed) => parsed,
107        Err(error) => invalid_call(call, &error),
108    }
109}
110
111/// Parses a model tool call, returning the error on failure.
112pub(crate) async fn try_parse(
113    call: &ToolCall,
114    ctx: &ParseContext<'_>,
115) -> Result<ParsedToolCall, Error> {
116    if ctx.tools.is_empty() {
117        if call.provider_executed && call.dynamic {
118            let input = parse_raw_input(call)?;
119            return Ok(ParsedToolCall {
120                tool_call_id: call.tool_call_id.clone(),
121                tool_name: call.tool_name.clone(),
122                input,
123                provider_executed: true,
124                dynamic: true,
125                invalid: false,
126                error: None,
127                title: None,
128                provider_metadata: call.provider_metadata.clone(),
129            });
130        }
131        return Err(Error::no_such_tool(call.tool_name.clone(), Vec::new()));
132    }
133    let error = match do_parse(call, ctx).await {
134        Ok(parsed) => return Ok(parsed),
135        Err(error) => error,
136    };
137    let Some(repair) = ctx.repair else {
138        return Err(error);
139    };
140    if !matches!(error, Error::NoSuchTool { .. } | Error::InvalidToolInput(_)) {
141        return Err(error);
142    }
143    let repaired = repair
144        .repair(RepairRequest {
145            tool_call: call,
146            tools: ctx.tools,
147            system: ctx.system,
148            messages: ctx.messages,
149            error: &error,
150        })
151        .await;
152    match repaired {
153        Err(cause) => Err(Error::ToolCallRepair {
154            original: Box::new(error),
155            cause,
156        }),
157        Ok(None) => Err(error),
158        Ok(Some(repaired_call)) => do_parse(&repaired_call, ctx).await,
159    }
160}
161
162async fn do_parse(call: &ToolCall, ctx: &ParseContext<'_>) -> Result<ParsedToolCall, Error> {
163    let Some(tool) = ctx.tools.get(call.tool_name.as_str()) else {
164        return Err(Error::no_such_tool(
165            call.tool_name.clone(),
166            ctx.tools.names().cloned().collect(),
167        ));
168    };
169    if let Some(ToolChoice::Tool { tool_name }) = ctx.tool_choice
170        && *tool_name != call.tool_name
171    {
172        return Err(Error::ToolChoiceViolation {
173            expected: tool_name.clone(),
174            actual: call.tool_name.clone(),
175        });
176    }
177    let raw = parse_raw_input(call)?;
178    let mut input = if call.provider_executed || tool.kind().is_provider_executed() {
179        raw
180    } else {
181        tool.validate_input(&call.tool_name, raw).map_err(|error| {
182            Error::invalid_tool_input(call.tool_name.clone(), call.input.clone(), Box::new(error))
183        })?
184    };
185    if let Some(refine) = ctx.refine.get(call.tool_name.as_str()) {
186        input = refine(input).await?;
187    }
188    Ok(ParsedToolCall {
189        tool_call_id: call.tool_call_id.clone(),
190        tool_name: call.tool_name.clone(),
191        input,
192        provider_executed: call.provider_executed,
193        dynamic: call.dynamic || tool.kind().is_dynamic(),
194        invalid: false,
195        error: None,
196        title: tool.title().map(str::to_owned),
197        provider_metadata: call.provider_metadata.clone(),
198    })
199}
200
201fn parse_raw_input(call: &ToolCall) -> Result<JsonValue, Error> {
202    if call.input.trim().is_empty() {
203        return Ok(JsonValue::Object(serde_json::Map::new()));
204    }
205    ferrin_schema::json::parse(&call.input).map_err(|error| {
206        Error::invalid_tool_input(call.tool_name.clone(), call.input.clone(), Box::new(error))
207    })
208}
209
210fn invalid_call(call: &ToolCall, error: &Error) -> ParsedToolCall {
211    let input = serde_json::from_str::<JsonValue>(&call.input)
212        .unwrap_or_else(|_| JsonValue::String(call.input.clone()));
213    ParsedToolCall {
214        tool_call_id: call.tool_call_id.clone(),
215        tool_name: call.tool_name.clone(),
216        input,
217        provider_executed: call.provider_executed,
218        dynamic: true,
219        invalid: true,
220        error: Some(error.to_string()),
221        title: None,
222        provider_metadata: call.provider_metadata.clone(),
223    }
224}