rig_core/
structured_output.rs1use crate::message::ToolChoice;
13
14pub const OUTPUT_TOOL_NAME: &str = "final_result";
16
17pub const OUTPUT_TOOL_DESCRIPTION: &str = "Call this tool exactly once with your final answer when you are done. Its arguments are the structured result and must satisfy the output schema.";
19
20pub const AUGMENTATION_SEPARATOR: &str = "\n\n";
22
23pub fn output_tool_augmentation(name: &str) -> String {
26 format!(
27 "When you have gathered enough information to answer, call the `{name}` tool exactly once with your final answer. Its arguments are the structured result and must satisfy the required schema. Do not return the final answer as plain text."
28 )
29}
30
31pub fn prompted_augmentation(schema: &str) -> String {
34 format!(
35 "Respond with ONLY a single JSON object that conforms to this JSON Schema. Do not include any prose, explanation, or markdown code fences.\n{schema}"
36 )
37}
38
39pub fn reprompt_text_answer(name: &str) -> String {
42 format!(
43 "Provide your final answer by calling the `{name}` tool with the structured result as its arguments, not as plain text."
44 )
45}
46
47pub fn reprompt_missing_fields(name: &str, missing: &[String]) -> String {
50 format!(
51 "The `{name}` arguments were missing required field(s): {}. Call `{name}` again with every required field.",
52 missing.join(", ")
53 )
54}
55
56pub fn output_tool_name(is_taken: impl Fn(&str) -> bool) -> String {
59 let mut name = OUTPUT_TOOL_NAME.to_owned();
60 let mut suffix = 1u32;
61 while is_taken(&name) {
62 name = format!("{OUTPUT_TOOL_NAME}_{suffix}");
63 suffix += 1;
64 }
65 name
66}
67
68pub fn output_tool_callable(choice: Option<&ToolChoice>, name: &str) -> bool {
71 match choice {
72 None | Some(ToolChoice::Auto | ToolChoice::Required) => true,
73 Some(ToolChoice::None) => false,
74 Some(ToolChoice::Specific { function_names }) => {
75 function_names.iter().any(|named| named == name)
76 }
77 }
78}
79
80pub fn missing_required_fields(
83 schema: &serde_json::Value,
84 arguments: &serde_json::Value,
85) -> Vec<String> {
86 let object = arguments.as_object();
87 schema
88 .get("required")
89 .and_then(serde_json::Value::as_array)
90 .into_iter()
91 .flatten()
92 .filter_map(serde_json::Value::as_str)
93 .filter(|name| object.is_none_or(|object| !object.contains_key(*name)))
94 .map(str::to_owned)
95 .collect()
96}
97
98pub fn text_satisfies_schema(schema: Option<&serde_json::Value>, text: &str) -> bool {
102 serde_json::from_str::<serde_json::Value>(text.trim())
103 .ok()
104 .is_some_and(|value| {
105 schema.is_none_or(|schema| missing_required_fields(schema, &value).is_empty())
106 })
107}