1use rig_core::{
10 completion::{
11 CompletionRequest, Document, ToolDefinition,
12 message::{CallId, Message, ToolChoice, ToolName, UserContent},
13 },
14 effect::{HandlerDescriptor, Outcome},
15 error::{ErrorKind, ErrorReport},
16 json_utils::to_canonical_string,
17 structured_output::{
18 AUGMENTATION_SEPARATOR, OUTPUT_TOOL_DESCRIPTION, output_tool_augmentation,
19 prompted_augmentation,
20 },
21 tool::{ToolExecutionError, ToolResult},
22 transcript::tool_result_output,
23};
24
25use crate::agent::{
26 Failure, MessageParts, OutputKind, OutputToolConfig,
27 content::parts::{ToolResultLimit, ToolResultStatus},
28};
29
30pub fn resolve_output(
36 mode: OutputKind,
37 has_schema: bool,
38 granted_tools: usize,
39 callable: bool,
40 provider_composes_native: bool,
41) -> OutputKind {
42 if !has_schema {
43 return OutputKind::Native;
44 }
45 match mode {
46 OutputKind::Native => OutputKind::Native,
47 OutputKind::Prompted => OutputKind::Prompted,
48 OutputKind::Tool => {
49 if callable {
50 OutputKind::Tool
51 } else {
52 OutputKind::Native
53 }
54 }
55 OutputKind::Auto => {
56 if granted_tools > 0 && callable && !provider_composes_native {
57 OutputKind::Tool
58 } else {
59 OutputKind::Native
60 }
61 }
62 }
63}
64
65pub struct RequestGraph<'a> {
68 pub preamble: Option<&'a str>,
70 pub utterances: Vec<&'a MessageParts>,
72 pub documents: Vec<Document>,
74 pub tools: Vec<&'a HandlerDescriptor>,
76 pub temperature: Option<f64>,
78 pub max_tokens: Option<u64>,
80 pub accept_unknown_finish_reasons: bool,
82 pub additional_params: Option<&'a serde_json::Value>,
84 pub options: &'a rig_core::completion::GenerationOptions,
86 pub provider_options: &'a rig_core::completion::ProviderOptions,
88 pub tool_choice: Option<&'a ToolChoice>,
90 pub output: OutputKind,
92 pub schema: Option<&'a serde_json::Value>,
94 pub output_tool: Option<&'a str>,
96 pub output_tool_config: Option<&'a OutputToolConfig>,
98}
99
100pub fn fold_request(
106 graph: &RequestGraph<'_>,
107) -> Result<CompletionRequest, crate::agent::content::parts::ContentError> {
108 let mut chat_history: Vec<Message> = Vec::with_capacity(graph.utterances.len() + 2);
109 let system = system_message(graph);
110 if let Some(content) = system {
111 chat_history.push(Message::System { content });
112 }
113 chat_history.extend(graph.utterances.iter().map(|parts| parts.to_message()));
114
115 let mut tools: Vec<ToolDefinition> = graph
116 .tools
117 .iter()
118 .filter_map(|descriptor| tool_definition(descriptor))
119 .collect();
120 if graph.output == OutputKind::Tool
121 && let (Some(name), Some(schema)) = (graph.output_tool, graph.schema)
122 {
123 tools.push(ToolDefinition {
124 name: ToolName::new(name)
125 .map_err(|_| crate::agent::content::parts::ContentError::Missing)?,
126 description: graph
127 .output_tool_config
128 .and_then(|config| config.description.as_deref())
129 .unwrap_or(OUTPUT_TOOL_DESCRIPTION)
130 .to_owned(),
131 parameters: schema.clone(),
132 });
133 }
134
135 let output_schema = match (graph.output, graph.schema) {
136 (OutputKind::Native, Some(schema)) => {
137 rig_core::schemars::Schema::try_from(schema.clone()).ok()
138 }
139 (OutputKind::Native, None)
140 | (OutputKind::Auto | OutputKind::Tool | OutputKind::Prompted, _) => None,
141 };
142
143 if chat_history.is_empty() {
144 return Err(crate::agent::content::parts::ContentError::Missing);
145 }
146 let mut request = CompletionRequest::from(chat_history);
149 request.documents = graph.documents.clone();
150 request.tools = tools;
151 request.temperature = graph.temperature;
152 request.max_tokens = graph.max_tokens;
153 request.tool_choice = graph.tool_choice.cloned();
154 request.additional_params = graph.additional_params.cloned();
155 request.options = graph.options.clone();
156 request.provider_options = graph.provider_options.clone();
157 request.output_schema = output_schema;
158 request.accept_unknown_finish_reasons = graph.accept_unknown_finish_reasons;
159 Ok(request)
160}
161
162fn system_message(graph: &RequestGraph<'_>) -> Option<String> {
165 let augmentation = match (graph.output, graph.output_tool, graph.schema) {
166 (OutputKind::Tool, Some(name), _) => graph
167 .output_tool_config
168 .is_none_or(|config| config.augment_preamble)
169 .then(|| output_tool_augmentation(name)),
170 (OutputKind::Prompted, _, Some(schema)) => {
171 Some(prompted_augmentation(&to_canonical_string(schema)))
172 }
173 (OutputKind::Tool, None, _)
174 | (OutputKind::Prompted, _, None)
175 | (OutputKind::Auto | OutputKind::Native, _, _) => None,
176 };
177 match (graph.preamble, augmentation) {
178 (None, None) => None,
179 (Some(preamble), None) => Some(preamble.to_owned()),
180 (None, Some(augmentation)) => Some(augmentation),
181 (Some(preamble), Some(augmentation)) => Some(format!(
182 "{preamble}{}{augmentation}",
183 AUGMENTATION_SEPARATOR
184 )),
185 }
186}
187
188pub(crate) fn tool_name(descriptor: &HandlerDescriptor) -> Option<&str> {
190 match &descriptor.family {
191 rig_core::effect::FamilyDescriptor::Tool { name, .. } => Some(name),
192 rig_core::effect::FamilyDescriptor::Completion { .. }
193 | rig_core::effect::FamilyDescriptor::Embed { .. }
194 | rig_core::effect::FamilyDescriptor::Rerank { .. }
195 | rig_core::effect::FamilyDescriptor::Memory { .. }
196 | rig_core::effect::FamilyDescriptor::Retrieve { .. }
197 | rig_core::effect::FamilyDescriptor::Custom { .. } => None,
198 }
199}
200
201pub fn tool_definition(descriptor: &HandlerDescriptor) -> Option<ToolDefinition> {
204 match &descriptor.family {
205 rig_core::effect::FamilyDescriptor::Tool {
206 name,
207 description,
208 parameters,
209 ..
210 } => Some(ToolDefinition {
211 name: ToolName::new(name.as_str()).ok()?,
212 description: description.clone(),
213 parameters: parameters.clone(),
214 }),
215 rig_core::effect::FamilyDescriptor::Completion { .. }
216 | rig_core::effect::FamilyDescriptor::Embed { .. }
217 | rig_core::effect::FamilyDescriptor::Rerank { .. }
218 | rig_core::effect::FamilyDescriptor::Memory { .. }
219 | rig_core::effect::FamilyDescriptor::Retrieve { .. }
220 | rig_core::effect::FamilyDescriptor::Custom { .. } => None,
221 }
222}
223
224pub fn tool_result_part(
229 id: CallId,
230 name: ToolName,
231 outcome: &Result<Outcome, ErrorReport>,
232) -> Result<(UserContent, ToolResultStatus), Failure> {
233 if let Some(failure) = tool_failure(outcome) {
234 return Err(failure);
235 }
236 let (result, status) = match outcome {
237 Ok(Outcome::ToolResult { result }) => (
238 result.clone(),
239 if result.is_success() {
240 ToolResultStatus::Ok
241 } else if result.is_refused() {
242 ToolResultStatus::Refused
243 } else if result.is_skipped() {
244 ToolResultStatus::Skipped
245 } else {
246 ToolResultStatus::Error
247 },
248 ),
249 Ok(other) => (
250 ToolResult::failed(ToolExecutionError::other(format!(
251 "the tool handler answered with a {} outcome",
252 other.family()
253 ))),
254 ToolResultStatus::WrongFamily,
255 ),
256 Err(report) if report.kind == ErrorKind::Denied => (
257 ToolResult::skipped(report.message.clone()),
258 ToolResultStatus::Denied,
259 ),
260 Err(report) => {
261 let result = ToolResult::failed(report.clone().into());
262 let status = if result.is_refused() {
263 ToolResultStatus::Refused
264 } else {
265 ToolResultStatus::Error
266 };
267 (result, status)
268 }
269 };
270 Ok((tool_result_output(id, name, &result), status))
271}
272
273pub fn limit_tool_result_text(text: &str, limit: &ToolResultLimit) -> Option<String> {
276 if text.len() <= limit.max_bytes {
277 return None;
278 }
279 let mut head = limit.max_bytes / 2;
280 while !text.is_char_boundary(head) {
281 head -= 1;
282 }
283 let mut tail = text.len() - (limit.max_bytes - limit.max_bytes / 2);
284 while !text.is_char_boundary(tail) {
285 tail += 1;
286 }
287 let omitted = tail - head;
288 let marker = limit.marker.replace("{omitted}", &omitted.to_string());
289 let head = text.get(..head).unwrap_or_default();
290 let tail = text.get(tail..).unwrap_or_default();
291 let mut cut = String::with_capacity(head.len() + marker.len() + tail.len());
292 cut.push_str(head);
293 cut.push_str(&marker);
294 cut.push_str(tail);
295 Some(cut)
296}
297
298#[must_use]
301pub fn tool_results_exceed(parts: &MessageParts, limit: &ToolResultLimit) -> bool {
302 let MessageParts::User { content } = parts else {
303 return false;
304 };
305 content.iter().any(|part| match part {
306 UserContent::ToolResult(result) => result.content.iter().any(|item| {
307 matches!(item, rig_core::message::ToolResultContent::Text(text) if text.text.len() > limit.max_bytes)
308 }),
309 _ => false,
310 })
311}
312
313pub fn limit_tool_results(parts: &mut MessageParts, limit: &ToolResultLimit) {
316 let MessageParts::User { content } = parts else {
317 return;
318 };
319 for part in content {
320 let UserContent::ToolResult(result) = part else {
321 continue;
322 };
323 for item in &mut result.content {
324 if let rig_core::message::ToolResultContent::Text(text) = item
325 && let Some(cut) = limit_tool_result_text(&text.text, limit)
326 {
327 text.text = cut;
328 }
329 }
330 }
331}
332
333pub fn tool_failure(outcome: &Result<Outcome, ErrorReport>) -> Option<Failure> {
337 match outcome {
338 Ok(_) => None,
339 Err(report) if report.kind == ErrorKind::Cancelled => {
340 Some(Failure::Cancelled(report.clone()))
341 }
342 Err(report)
343 if matches!(
344 report.kind,
345 ErrorKind::BusClosed | ErrorKind::HandlerUnavailable | ErrorKind::Divergence
346 ) =>
347 {
348 Some(Failure::Tool(report.clone()))
349 }
350 Err(_) => None,
351 }
352}
353
354pub fn retrieval_query(utterances: &[MessageParts]) -> String {
357 utterances
358 .iter()
359 .rev()
360 .find_map(|parts| parts.to_message().rag_text())
361 .unwrap_or_default()
362}
363
364#[cfg(test)]
365mod tests;