ferrin_core/generate_text/
parse_tool_call.rs1use 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#[derive(Debug)]
22pub struct RepairRequest<'a> {
23 pub tool_call: &'a ToolCall,
25 pub tools: &'a ToolSet,
27 pub system: Option<&'a Instructions>,
29 pub messages: &'a [Message],
31 pub error: &'a Error,
34}
35
36impl RepairRequest<'_> {
37 #[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
46pub trait ToolCallRepair: Send + Sync {
52 fn repair<'a>(
54 &'a self,
55 request: RepairRequest<'a>,
56 ) -> BoxFuture<'a, Result<Option<ToolCall>, BoxError>>;
57}
58
59pub type RefineToolInputFn =
61 Arc<dyn Fn(JsonValue) -> BoxFuture<'static, Result<JsonValue, Error>> + Send + Sync>;
62
63#[derive(Clone, Default)]
65pub struct RefineToolInputs(HashMap<ToolName, RefineToolInputFn>);
66
67impl RefineToolInputs {
68 pub fn insert(&mut self, tool_name: impl Into<ToolName>, refine: RefineToolInputFn) {
70 self.0.insert(tool_name.into(), refine);
71 }
72
73 #[must_use]
75 pub fn get(&self, tool_name: &str) -> Option<&RefineToolInputFn> {
76 self.0.get(tool_name)
77 }
78
79 #[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
92pub(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
102pub(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, ctx.tools),
108 }
109}
110
111pub(crate) async fn try_parse(
113 call: &ToolCall,
114 ctx: &ParseContext<'_>,
115) -> Result<ParsedToolCall, Error> {
116 let error = match do_parse(call, ctx).await {
117 Ok(parsed) => return Ok(parsed),
118 Err(error) => error,
119 };
120 let Some(repair) = ctx.repair else {
121 return Err(error);
122 };
123 if !matches!(error, Error::NoSuchTool { .. } | Error::InvalidToolInput(_)) {
124 return Err(error);
125 }
126 let repaired = repair
127 .repair(RepairRequest {
128 tool_call: call,
129 tools: ctx.tools,
130 system: ctx.system,
131 messages: ctx.messages,
132 error: &error,
133 })
134 .await;
135 match repaired {
136 Err(cause) => Err(Error::ToolCallRepair {
137 original: Box::new(error),
138 cause,
139 }),
140 Ok(None) => Err(error),
141 Ok(Some(repaired_call)) => do_parse(&repaired_call, ctx).await,
142 }
143}
144
145async fn do_parse(call: &ToolCall, ctx: &ParseContext<'_>) -> Result<ParsedToolCall, Error> {
146 let Some(tool) = ctx.tools.get(call.tool_name.as_str()) else {
147 if call.provider_executed && call.dynamic {
148 let mut input = parse_raw_input(call)?;
149 if let Some(refine) = ctx.refine.get(call.tool_name.as_str()) {
150 input = refine(input).await?;
151 }
152 return Ok(ParsedToolCall {
153 tool_call_id: call.tool_call_id.clone(),
154 tool_name: call.tool_name.clone(),
155 input,
156 provider_executed: true,
157 dynamic: true,
158 invalid: false,
159 error: None,
160 title: None,
161 tool_metadata: None,
162 provider_metadata: call.provider_metadata.clone(),
163 });
164 }
165 return Err(Error::no_such_tool(
166 call.tool_name.clone(),
167 ctx.tools.names().cloned().collect(),
168 ));
169 };
170 if let Some(ToolChoice::Tool { tool_name }) = ctx.tool_choice
171 && *tool_name != call.tool_name
172 {
173 return Err(Error::ToolChoiceViolation {
174 expected: tool_name.clone(),
175 actual: call.tool_name.clone(),
176 });
177 }
178 let raw = parse_raw_input(call)?;
179 let mut input = if call.provider_executed || tool.kind().is_provider_executed() {
180 raw
181 } else {
182 tool.validate_input(&call.tool_name, raw).map_err(|error| {
183 Error::invalid_tool_input(call.tool_name.clone(), call.input.clone(), Box::new(error))
184 })?
185 };
186 if let Some(refine) = ctx.refine.get(call.tool_name.as_str()) {
187 input = refine(input).await?;
188 }
189 Ok(ParsedToolCall {
190 tool_call_id: call.tool_call_id.clone(),
191 tool_name: call.tool_name.clone(),
192 input,
193 provider_executed: call.provider_executed,
194 dynamic: call.dynamic || tool.kind().is_dynamic(),
195 invalid: false,
196 error: None,
197 title: tool.title().map(str::to_owned),
198 tool_metadata: tool.metadata().cloned(),
199 provider_metadata: call.provider_metadata.clone(),
200 })
201}
202
203fn parse_raw_input(call: &ToolCall) -> Result<JsonValue, Error> {
204 if call.input.trim().is_empty() {
205 return Ok(JsonValue::Object(serde_json::Map::new()));
206 }
207 ferrin_schema::json::parse(&call.input).map_err(|error| {
208 Error::invalid_tool_input(call.tool_name.clone(), call.input.clone(), Box::new(error))
209 })
210}
211
212fn invalid_call(call: &ToolCall, error: &Error, tools: &ToolSet) -> ParsedToolCall {
213 let input = serde_json::from_str::<JsonValue>(&call.input)
214 .unwrap_or_else(|_| JsonValue::String(call.input.clone()));
215 ParsedToolCall {
216 tool_call_id: call.tool_call_id.clone(),
217 tool_name: call.tool_name.clone(),
218 input,
219 provider_executed: call.provider_executed,
220 dynamic: true,
221 invalid: true,
222 error: Some(error.to_string()),
223 title: None,
224 tool_metadata: tools
225 .get(call.tool_name.as_str())
226 .and_then(|tool| tool.metadata().cloned()),
227 provider_metadata: call.provider_metadata.clone(),
228 }
229}
230
231pub(crate) fn check_tool_choice(
233 choice: Option<&ToolChoice>,
234 calls: &[ParsedToolCall],
235) -> Result<(), Error> {
236 match choice {
237 Some(ToolChoice::Required) if calls.is_empty() => {
238 Err(Error::ToolChoiceNotSatisfied { expected: None })
239 }
240 Some(ToolChoice::Tool { tool_name })
241 if !calls.iter().any(|call| call.tool_name == *tool_name) =>
242 {
243 Err(Error::ToolChoiceNotSatisfied {
244 expected: Some(tool_name.clone()),
245 })
246 }
247 _ => Ok(()),
248 }
249}