rig_core/tool/
feedback.rs1use crate::message::{
13 self, AssistantContent, CallId, ToolCall, ToolName, ToolResultContent, UserContent,
14};
15use crate::tool::{ToolResult, Touches};
16
17pub fn tool_result_output(call: CallId, name: ToolName, result: &ToolResult) -> UserContent {
20 UserContent::ToolResult(message::ToolResult {
21 call,
22 name,
23 content: result.output().clone().into_content(),
24 is_error: !result.is_success(),
25 })
26}
27
28pub fn tool_result_message(call: CallId, name: ToolName, message: String) -> UserContent {
32 UserContent::ToolResult(message::ToolResult {
33 call,
34 name,
35 content: vec![ToolResultContent::text(message)],
36 is_error: true,
37 })
38}
39
40pub fn invalid_arguments_feedback(tool: &str, raw: &str) -> String {
44 format!(
45 "The arguments for tool `{tool}` are not a JSON object: {}. \
46 Call the tool again with a JSON object as its arguments.",
47 echoed(raw)
48 )
49}
50
51pub fn unknown_tool_feedback<'a>(name: &str, tools: impl IntoIterator<Item = &'a str>) -> String {
54 let mut tools: Vec<_> = tools.into_iter().collect();
55 tools.sort_unstable();
56 let available = if tools.is_empty() {
57 "You have no tools available. Answer without calling tools.".to_owned()
58 } else {
59 format!(
60 "Your tools are: `{}`. Call one by its exact name.",
61 tools.join("`, `")
62 )
63 };
64 format!("There is no tool named `{name}`. {available}")
65}
66
67pub fn output_limit_feedback(tool: &str, raw: &str) -> String {
74 format!(
75 "Not run: your reply reached the output limit before `{tool}`'s arguments ended. Send \
76 less in one call: split the work into smaller calls, such as writing part of a file \
77 and adding the rest with edits. The arguments began: {}",
78 echoed(raw)
79 )
80}
81
82pub const ECHOED_CHARS: usize = 200;
84
85fn echoed(raw: &str) -> String {
87 match raw.char_indices().nth(ECHOED_CHARS) {
88 Some((end, _)) => format!(
89 "{}… ({} more characters)",
90 raw.get(..end).unwrap_or_default(),
91 raw.len().saturating_sub(end)
92 ),
93 None => raw.to_owned(),
94 }
95}
96
97pub const TOOL_NOT_EXECUTED_DUE_TO_INVALID_PEER: &str =
100 "Tool not executed because another tool call in the same assistant turn was invalid.";
101
102pub fn invalid_call_feedback(
106 content: &[AssistantContent],
107 invalid: &CallId,
108 feedback: &str,
109) -> Vec<UserContent> {
110 content
111 .iter()
112 .filter_map(|part| match part {
113 AssistantContent::ToolCall(call) if &call.id == invalid => Some(tool_result_message(
114 call.id.clone(),
115 call.function.name.clone(),
116 feedback.to_owned(),
117 )),
118 AssistantContent::ToolCall(call) => Some(not_executed(call)),
119 _ => None,
120 })
121 .collect()
122}
123
124pub fn not_executed(call: &ToolCall) -> UserContent {
127 tool_result_message(
128 call.id.clone(),
129 call.function.name.clone(),
130 TOOL_NOT_EXECUTED_DUE_TO_INVALID_PEER.to_owned(),
131 )
132}
133
134pub fn arguments_refusal(parameters: &serde_json::Value, call: &ToolCall) -> Option<String> {
148 let name = call.function.name.as_str();
149 if let Some(raw) = &call.function.invalid_arguments {
150 return Some(invalid_arguments_feedback(name, raw));
151 }
152 let allows_more = parameters
153 .get("additionalProperties")
154 .is_some_and(|more| more != &serde_json::Value::Bool(false))
155 || parameters.get("patternProperties").is_some();
156 let declared = parameters
157 .get("properties")
158 .and_then(serde_json::Value::as_object)
159 .filter(|_| !allows_more);
160 fn quoted<'a>(names: impl Iterator<Item = &'a String>) -> String {
161 names
162 .map(|arg| format!("`{arg}`"))
163 .collect::<Vec<_>>()
164 .join(", ")
165 }
166 let unknown = quoted(
167 call.function
168 .arguments
169 .keys()
170 .filter(|arg| declared.is_some_and(|declared| !declared.contains_key(arg.as_str()))),
171 );
172 if let Some(declared) = declared.filter(|_| !unknown.is_empty()) {
173 let mut known = quoted(declared.keys());
174 if known.is_empty() {
175 known = "none".to_owned();
176 }
177 return Some(format!(
178 "`{name}` has no argument {unknown}. Its arguments are: {known}. Call it again with \
179 only those."
180 ));
181 }
182 const SHOWN: usize = 5;
183 let arguments = serde_json::Value::Object(call.function.arguments.clone());
184 let problems = crate::tool::validate::validate(parameters, &arguments).err()?;
185 let mut named: Vec<String> = problems
186 .iter()
187 .take(SHOWN)
188 .map(ToString::to_string)
189 .collect();
190 if let Some(more) = problems.len().checked_sub(SHOWN).filter(|more| *more > 0) {
191 named.push(format!("and {more} more"));
192 }
193 Some(format!(
194 "`{name}`'s arguments do not fit its schema: {}. Call it again with arguments that fit.",
195 named.join("; ")
196 ))
197}
198
199pub fn not_finished(why: &str, started: bool, footprint: &Touches) -> String {
218 let why = why.trim().trim_end_matches('.');
219 match (started, footprint.reads_only()) {
220 (false, _) => format!("Not run: {why}."),
221 (true, true) => format!("Stopped: {why}. It only read; run it again if you still need it."),
222 (true, false) => {
223 format!("Stopped: {why}. It may have partly run; check before running it again.")
224 }
225 }
226}
227
228#[cfg(test)]
229mod tests;