Skip to main content

codex_tools/
tool_search.rs

1use crate::JsonSchema;
2use crate::LoadableToolSpec;
3use crate::ResponsesApiNamespaceTool;
4use crate::ResponsesApiTool;
5use crate::ToolSearchSourceInfo;
6use crate::ToolSpec;
7use crate::default_namespace_description;
8
9#[derive(Clone, PartialEq)]
10pub struct ToolSearchEntry {
11    pub search_text: String,
12    pub output: LoadableToolSpec,
13}
14
15#[derive(Clone, PartialEq)]
16pub struct ToolSearchInfo {
17    pub entry: ToolSearchEntry,
18    pub source_info: Option<ToolSearchSourceInfo>,
19}
20
21impl ToolSearchInfo {
22    pub fn from_tool_spec(
23        spec: ToolSpec,
24        source_info: Option<ToolSearchSourceInfo>,
25    ) -> Option<Self> {
26        let search_text = default_tool_search_text(&spec);
27        Self::from_spec(search_text, spec, source_info)
28    }
29
30    pub fn from_spec(
31        search_text: String,
32        spec: ToolSpec,
33        source_info: Option<ToolSearchSourceInfo>,
34    ) -> Option<Self> {
35        let output = match spec {
36            ToolSpec::Function(mut tool) => {
37                tool.defer_loading = Some(true);
38                tool.output_schema = None;
39                LoadableToolSpec::Function(tool)
40            }
41            ToolSpec::Namespace(mut namespace) => {
42                if namespace.description.trim().is_empty() {
43                    namespace.description = default_namespace_description(&namespace.name);
44                }
45                for tool in &mut namespace.tools {
46                    let ResponsesApiNamespaceTool::Function(tool) = tool;
47                    tool.defer_loading = Some(true);
48                    tool.output_schema = None;
49                }
50                LoadableToolSpec::Namespace(namespace)
51            }
52            ToolSpec::ToolSearch { .. } | ToolSpec::WebSearch { .. } | ToolSpec::Freeform(_) => {
53                return None;
54            }
55        };
56
57        Some(Self {
58            entry: ToolSearchEntry {
59                search_text,
60                output,
61            },
62            source_info,
63        })
64    }
65}
66
67fn default_tool_search_text(spec: &ToolSpec) -> String {
68    let mut parts = Vec::new();
69
70    match spec {
71        ToolSpec::Function(tool) => append_function_search_text(tool, &mut parts),
72        ToolSpec::Namespace(namespace) => {
73            push_search_part(&mut parts, namespace.name.clone());
74            push_search_part(&mut parts, namespace.description.clone());
75            for tool in &namespace.tools {
76                let ResponsesApiNamespaceTool::Function(tool) = tool;
77                append_function_search_text(tool, &mut parts);
78            }
79        }
80        ToolSpec::ToolSearch { description, .. } => {
81            push_search_part(&mut parts, description.clone());
82        }
83        ToolSpec::WebSearch { .. } => {
84            push_search_part(&mut parts, "web search".to_string());
85        }
86        ToolSpec::Freeform(tool) => {
87            push_search_part(&mut parts, tool.name.clone());
88            push_search_part(&mut parts, tool.description.clone());
89            push_search_part(&mut parts, tool.format.syntax.clone());
90        }
91    }
92
93    parts.join(" ")
94}
95
96fn append_function_search_text(tool: &ResponsesApiTool, parts: &mut Vec<String>) {
97    push_search_part(parts, tool.name.clone());
98    push_search_part(parts, tool.name.replace('_', " "));
99    push_search_part(parts, tool.description.clone());
100    append_schema_search_text(&tool.parameters, parts);
101}
102
103fn append_schema_search_text(schema: &JsonSchema, parts: &mut Vec<String>) {
104    if let Some(description) = &schema.description {
105        push_search_part(parts, description.clone());
106    }
107    if let Some(properties) = &schema.properties {
108        for (name, schema) in properties {
109            push_search_part(parts, name.clone());
110            append_schema_search_text(schema, parts);
111        }
112    }
113    if let Some(items) = &schema.items {
114        append_schema_search_text(items, parts);
115    }
116    if let Some(variants) = &schema.any_of {
117        for variant in variants {
118            append_schema_search_text(variant, parts);
119        }
120    }
121}
122
123fn push_search_part(parts: &mut Vec<String>, part: String) {
124    let part = part.trim();
125    if !part.is_empty() {
126        parts.push(part.to_string());
127    }
128}
129
130#[cfg(test)]
131#[path = "tool_search_tests.rs"]
132mod tests;