Skip to main content

ferrin_google/
tools.rs

1//! Factories for Google provider-executed tools.
2//!
3//! Each factory returns a [`ferrin_tool::Tool`] whose definition is a
4//! `ToolDefinition::Provider` with the `google.<tool>` id; the request
5//! preparation converts the arguments to the wire format.
6
7use ferrin_spec::JsonObject;
8use ferrin_spec::JsonValue;
9use ferrin_tool::Schema;
10use ferrin_tool::Tool;
11use serde::Serialize;
12use serde_json::json;
13
14use crate::prepare_tools::ids;
15
16fn args<T: Serialize>(value: &T) -> JsonObject {
17    match serde_json::to_value(value) {
18        Ok(JsonValue::Object(mut object)) => {
19            object.retain(|_, value| !value.is_null());
20            object
21        }
22        _ => JsonObject::new(),
23    }
24}
25
26fn executed(id: &str, args: JsonObject) -> Tool {
27    Tool::provider_executed(id, args)
28        .input_schema(Schema::any())
29        .build()
30}
31
32/// Search types of `google.google_search`.
33#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
34#[serde(rename_all = "camelCase")]
35pub struct SearchTypes {
36    /// Enable web search.
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub web_search: Option<JsonObject>,
39    /// Enable image search.
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub image_search: Option<JsonObject>,
42}
43
44/// Time range filter of `google.google_search`.
45#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
46#[serde(rename_all = "camelCase")]
47pub struct TimeRangeFilter {
48    /// Start time (RFC 3339).
49    pub start_time: String,
50    /// End time (RFC 3339).
51    pub end_time: String,
52}
53
54/// Arguments of `google.google_search`.
55#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
56#[serde(rename_all = "camelCase")]
57pub struct GoogleSearchArgs {
58    /// Search types.
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub search_types: Option<SearchTypes>,
61    /// Time range filter.
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub time_range_filter: Option<TimeRangeFilter>,
64}
65
66/// Arguments of `google.file_search`.
67#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
68#[serde(rename_all = "camelCase")]
69pub struct FileSearchArgs {
70    /// File search store names (`fileSearchStores/...`).
71    pub file_search_store_names: Vec<String>,
72    /// Number of chunks to retrieve.
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub top_k: Option<u32>,
75    /// Metadata filter expression.
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub metadata_filter: Option<String>,
78}
79
80/// Arguments of `google.vertex_rag_store`.
81#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
82#[serde(rename_all = "camelCase")]
83pub struct VertexRagStoreArgs {
84    /// RAG corpus resource name.
85    pub rag_corpus: String,
86    /// Number of results.
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub top_k: Option<u32>,
89}
90
91/// Provider-executed tool factories.
92#[derive(Debug, Clone, Copy, Default)]
93pub struct GoogleTools;
94
95impl GoogleTools {
96    /// Creates the factory namespace.
97    #[must_use]
98    pub fn new() -> Self {
99        Self
100    }
101
102    /// `google.google_search`: Google Search grounding.
103    #[must_use]
104    pub fn google_search(&self, config: GoogleSearchArgs) -> Tool {
105        executed(ids::GOOGLE_SEARCH, args(&config))
106    }
107
108    /// `google.enterprise_web_search` (Vertex AI).
109    #[must_use]
110    pub fn enterprise_web_search(&self) -> Tool {
111        executed(ids::ENTERPRISE_WEB_SEARCH, JsonObject::new())
112    }
113
114    /// `google.url_context`: fetch URLs mentioned in the prompt.
115    #[must_use]
116    pub fn url_context(&self) -> Tool {
117        executed(ids::URL_CONTEXT, JsonObject::new())
118    }
119
120    /// `google.code_execution`: server-side Python execution.
121    #[must_use]
122    pub fn code_execution(&self) -> Tool {
123        Tool::provider_executed(ids::CODE_EXECUTION, JsonObject::new())
124            .input_schema(Schema::from_json_schema(json!({
125                "type": "object",
126                "properties": {
127                    "language": {"type": "string", "description": "The programming language of the code."},
128                    "code": {"type": "string", "description": "The code to be executed."}
129                },
130                "required": ["language", "code"]
131            })))
132            .output_schema(Schema::from_json_schema(json!({
133                "type": "object",
134                "properties": {
135                    "outcome": {"type": "string", "description": "The outcome of the execution (e.g., \"OUTCOME_OK\")."},
136                    "output": {"type": "string", "description": "The output from the code execution."}
137                },
138                "required": ["outcome", "output"]
139            })))
140            .build()
141    }
142
143    /// `google.file_search`: retrieval from file search stores.
144    #[must_use]
145    pub fn file_search(&self, config: FileSearchArgs) -> Tool {
146        executed(ids::FILE_SEARCH, args(&config))
147    }
148
149    /// `google.vertex_rag_store` (Vertex AI).
150    #[must_use]
151    pub fn vertex_rag_store(&self, config: VertexRagStoreArgs) -> Tool {
152        executed(ids::VERTEX_RAG_STORE, args(&config))
153    }
154
155    /// `google.google_maps`: Google Maps grounding.
156    #[must_use]
157    pub fn google_maps(&self) -> Tool {
158        executed(ids::GOOGLE_MAPS, JsonObject::new())
159    }
160}