Skip to main content

rig_core/
structured_output.rs

1//! Model-facing structured-output policy shared by the agent runtimes: the
2//! synthetic output tool's name and description, the instructions and reprompts
3//! the model sees, and the required-field check on an answer.
4//!
5//! ```
6//! use rig_core::structured_output::{output_tool_augmentation, output_tool_name};
7//! let name = output_tool_name(|taken| taken == "final_result");
8//! assert_eq!(name, "final_result_1");
9//! assert!(output_tool_augmentation(&name).contains("`final_result_1`"));
10//! ```
11
12use crate::message::ToolChoice;
13
14/// The output tool's default name.
15pub const OUTPUT_TOOL_NAME: &str = "final_result";
16
17/// The output tool's default description.
18pub 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
20/// The separator between the preamble and an augmentation.
21pub const AUGMENTATION_SEPARATOR: &str = "\n\n";
22
23/// Appended to the preamble when the answer is asked for through the output
24/// tool named `name`.
25pub 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
31/// Appended to the preamble when the answer is asked for as prompted JSON;
32/// `schema` is the schema's canonical rendering.
33pub 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
39/// The reprompt when the model answered as text instead of calling the output
40/// tool named `name`.
41pub 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
47/// The reprompt when the output tool named `name` was called without the
48/// `missing` required fields.
49pub 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
56/// The output tool's name for a run: the default, numbered from 1 while
57/// `is_taken` reports a collision (`final_result`, `final_result_1`, ...).
58pub 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
68/// Whether the tool choice permits calling the output tool named `name`.
69/// No choice, `Auto`, `Required`, and a `Specific` set naming it permit it.
70pub 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
80/// The top-level required fields of an object schema that `arguments` lacks,
81/// in the schema's order. A non-object argument lacks every one.
82pub 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
98/// Whether `text` parses as JSON with every top-level required field of
99/// `schema`. Without a schema any JSON passes. Field types and other schema
100/// constraints are not checked.
101pub 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}