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),
108 }
109}
110
111pub(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}