codex_tools/
tool_search.rs1use 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;