Skip to main content

gemini_rust/interactions/
builder.rs

1use std::sync::Arc;
2use tracing::{instrument, Span};
3
4use crate::client::{Error as ClientError, GeminiClient};
5use crate::interactions::model::*;
6use crate::interactions::stream::InteractionStream;
7
8/// Fluent builder for constructing and executing interaction requests.
9#[derive(Clone)]
10pub struct InteractionBuilder {
11    client: Arc<GeminiClient>,
12    model: Option<String>,
13    agent: Option<String>,
14    input: Option<InteractionInput>,
15    system_instruction: Option<String>,
16    tools: Vec<InteractionTool>,
17    response_format: Option<ResponseFormat>,
18    store: Option<bool>,
19    background: bool,
20    generation_config: Option<InteractionGenerationConfig>,
21    agent_config: Option<AgentConfig>,
22    previous_interaction_id: Option<String>,
23    environment: Option<EnvironmentConfigOrString>,
24    cached_content: Option<String>,
25    response_modalities: Vec<ResponseModality>,
26    service_tier: Option<ServiceTier>,
27    webhook_config: Option<WebhookConfig>,
28}
29
30impl InteractionBuilder {
31    pub(crate) fn new(client: Arc<GeminiClient>) -> Self {
32        Self {
33            client,
34            model: None,
35            agent: None,
36            input: None,
37            system_instruction: None,
38            tools: Vec::new(),
39            response_format: None,
40            store: None,
41            background: false,
42            generation_config: None,
43            agent_config: None,
44            previous_interaction_id: None,
45            environment: None,
46            cached_content: None,
47            response_modalities: Vec::new(),
48            service_tier: None,
49            webhook_config: None,
50        }
51    }
52
53    // ===== Model / Agent =====
54
55    /// Set the model (mutually exclusive with agent).
56    pub fn with_model(mut self, model: impl Into<String>) -> Self {
57        self.model = Some(model.into());
58        self.agent = None;
59        self
60    }
61
62    /// Set the agent (mutually exclusive with model).
63    pub fn with_agent(mut self, agent: impl Into<String>) -> Self {
64        self.agent = Some(agent.into());
65        self.model = None;
66        self
67    }
68
69    // ===== Input =====
70
71    /// Set input from anything that implements `Into<InteractionInput>`.
72    pub fn with_input(mut self, input: impl Into<InteractionInput>) -> Self {
73        self.input = Some(input.into());
74        self
75    }
76
77    /// Set input as plain text.
78    pub fn with_text(mut self, text: impl Into<String>) -> Self {
79        self.input = Some(InteractionInput::Text(text.into()));
80        self
81    }
82
83    /// Set input as a content array (multimodal).
84    pub fn with_content_input(mut self, content: Vec<InteractionContent>) -> Self {
85        self.input = Some(InteractionInput::ContentArray(content));
86        self
87    }
88
89    /// Set input as a step array (stateless multi-turn).
90    pub fn with_step_input(mut self, steps: Vec<Step>) -> Self {
91        self.input = Some(InteractionInput::StepArray(steps));
92        self
93    }
94
95    /// Add image input.
96    pub fn with_image(mut self, data: impl Into<String>, mime_type: ImageMimeType) -> Self {
97        let content = InteractionContent::image(data, mime_type);
98        self.input = match self.input {
99            Some(InteractionInput::ContentArray(mut arr)) => {
100                arr.push(content);
101                Some(InteractionInput::ContentArray(arr))
102            }
103            _ => Some(InteractionInput::ContentArray(vec![content])),
104        };
105        self
106    }
107
108    /// Add image input from a URI.
109    pub fn with_image_uri(mut self, uri: impl Into<String>, mime_type: ImageMimeType) -> Self {
110        let content = InteractionContent::image_uri(uri, mime_type);
111        self.input = match self.input {
112            Some(InteractionInput::ContentArray(mut arr)) => {
113                arr.push(content);
114                Some(InteractionInput::ContentArray(arr))
115            }
116            _ => Some(InteractionInput::ContentArray(vec![content])),
117        };
118        self
119    }
120
121    /// Add audio input.
122    pub fn with_audio(mut self, data: impl Into<String>, mime_type: AudioMimeType) -> Self {
123        let content = InteractionContent::audio(data, mime_type);
124        self.input = match self.input {
125            Some(InteractionInput::ContentArray(mut arr)) => {
126                arr.push(content);
127                Some(InteractionInput::ContentArray(arr))
128            }
129            _ => Some(InteractionInput::ContentArray(vec![content])),
130        };
131        self
132    }
133
134    /// Add video input from a URI.
135    pub fn with_video(mut self, uri: impl Into<String>) -> Self {
136        let content = InteractionContent::video_uri(uri);
137        self.input = match self.input {
138            Some(InteractionInput::ContentArray(mut arr)) => {
139                arr.push(content);
140                Some(InteractionInput::ContentArray(arr))
141            }
142            _ => Some(InteractionInput::ContentArray(vec![content])),
143        };
144        self
145    }
146
147    /// Add document input.
148    pub fn with_document(mut self, data: impl Into<String>, mime_type: DocumentMimeType) -> Self {
149        let content = InteractionContent::document(data, mime_type);
150        self.input = match self.input {
151            Some(InteractionInput::ContentArray(mut arr)) => {
152                arr.push(content);
153                Some(InteractionInput::ContentArray(arr))
154            }
155            _ => Some(InteractionInput::ContentArray(vec![content])),
156        };
157        self
158    }
159
160    // ===== System Instruction =====
161
162    /// Set the system instruction.
163    pub fn with_system_instruction(mut self, instruction: impl Into<String>) -> Self {
164        self.system_instruction = Some(instruction.into());
165        self
166    }
167
168    // ===== Tools =====
169
170    /// Add a tool.
171    pub fn with_tool(mut self, tool: InteractionTool) -> Self {
172        self.tools.push(tool);
173        self
174    }
175
176    /// Add tools.
177    pub fn with_tools(mut self, tools: Vec<InteractionTool>) -> Self {
178        self.tools.extend(tools);
179        self
180    }
181
182    /// Add a function declaration tool.
183    pub fn with_function(
184        mut self,
185        name: impl Into<String>,
186        description: impl Into<String>,
187        parameters: serde_json::Value,
188    ) -> Self {
189        self.tools
190            .push(InteractionTool::function(name, description, parameters));
191        self
192    }
193
194    /// Enable Google Search.
195    pub fn with_google_search(mut self) -> Self {
196        self.tools.push(InteractionTool::google_search());
197        self
198    }
199
200    /// Enable code execution.
201    pub fn with_code_execution(mut self) -> Self {
202        self.tools.push(InteractionTool::code_execution());
203        self
204    }
205
206    /// Enable URL context.
207    pub fn with_url_context(mut self) -> Self {
208        self.tools.push(InteractionTool::url_context());
209        self
210    }
211
212    /// Enable Google Maps.
213    pub fn with_google_maps(mut self) -> Self {
214        self.tools.push(InteractionTool::google_maps());
215        self
216    }
217
218    /// Enable file search.
219    pub fn with_file_search(mut self, store_names: Vec<String>) -> Self {
220        self.tools.push(InteractionTool::file_search(store_names));
221        self
222    }
223
224    /// Enable MCP Server.
225    pub fn with_mcp_server(mut self, name: impl Into<String>, url: impl Into<String>) -> Self {
226        self.tools.push(InteractionTool::mcp_server(name, url));
227        self
228    }
229
230    /// Enable computer use.
231    pub fn with_computer_use(mut self, environment: ComputerUseEnvironment) -> Self {
232        self.tools.push(InteractionTool::computer_use(environment));
233        self
234    }
235
236    /// Enable retrieval tool.
237    pub fn with_retrieval(mut self, retrieval_types: Vec<RetrievalType>) -> Self {
238        self.tools.push(InteractionTool::retrieval(retrieval_types));
239        self
240    }
241
242    // ===== Response Format =====
243
244    /// Set the response format.
245    pub fn with_response_format(mut self, format: ResponseFormat) -> Self {
246        self.response_format = Some(format);
247        self
248    }
249
250    /// Set JSON structured output with a schema.
251    pub fn with_json_schema(mut self, schema: serde_json::Value) -> Self {
252        self.response_format = Some(ResponseFormat::json_schema(schema));
253        self
254    }
255
256    // ===== Generation Config =====
257
258    pub fn with_temperature(mut self, temperature: f64) -> Self {
259        self.generation_config
260            .get_or_insert_with(Default::default)
261            .temperature = Some(temperature);
262        self
263    }
264
265    pub fn with_top_p(mut self, top_p: f64) -> Self {
266        self.generation_config
267            .get_or_insert_with(Default::default)
268            .top_p = Some(top_p);
269        self
270    }
271
272    pub fn with_seed(mut self, seed: i64) -> Self {
273        self.generation_config
274            .get_or_insert_with(Default::default)
275            .seed = Some(seed);
276        self
277    }
278
279    pub fn with_stop_sequences(mut self, sequences: Vec<String>) -> Self {
280        self.generation_config
281            .get_or_insert_with(Default::default)
282            .stop_sequences = sequences;
283        self
284    }
285
286    pub fn with_max_output_tokens(mut self, tokens: i64) -> Self {
287        self.generation_config
288            .get_or_insert_with(Default::default)
289            .max_output_tokens = Some(tokens);
290        self
291    }
292
293    pub fn with_thinking_level(mut self, level: InteractionThinkingLevel) -> Self {
294        self.generation_config
295            .get_or_insert_with(Default::default)
296            .thinking_level = Some(level);
297        self
298    }
299
300    pub fn with_thinking_summaries(mut self, summaries: ThinkingSummaries) -> Self {
301        self.generation_config
302            .get_or_insert_with(Default::default)
303            .thinking_summaries = Some(summaries);
304        self
305    }
306
307    pub fn with_presence_penalty(mut self, penalty: f64) -> Self {
308        self.generation_config
309            .get_or_insert_with(Default::default)
310            .presence_penalty = Some(penalty);
311        self
312    }
313
314    pub fn with_frequency_penalty(mut self, penalty: f64) -> Self {
315        self.generation_config
316            .get_or_insert_with(Default::default)
317            .frequency_penalty = Some(penalty);
318        self
319    }
320
321    pub fn with_tool_choice(mut self, choice: ToolChoiceConfig) -> Self {
322        self.generation_config
323            .get_or_insert_with(Default::default)
324            .tool_choice = Some(choice);
325        self
326    }
327
328    pub fn with_speech_config(mut self, config: InteractionSpeechConfig) -> Self {
329        self.generation_config
330            .get_or_insert_with(Default::default)
331            .speech_config
332            .push(config);
333        self
334    }
335
336    pub fn with_video_config(mut self, config: VideoConfig) -> Self {
337        self.generation_config
338            .get_or_insert_with(Default::default)
339            .video_config = Some(config);
340        self
341    }
342
343    /// Set the full generation config.
344    pub fn with_generation_config(mut self, config: InteractionGenerationConfig) -> Self {
345        self.generation_config = Some(config);
346        self
347    }
348
349    // ===== Agent Config =====
350
351    /// Set the agent config.
352    pub fn with_agent_config(mut self, config: AgentConfig) -> Self {
353        self.agent_config = Some(config);
354        self
355    }
356
357    // ===== Interaction Options =====
358
359    /// Set `previous_interaction_id` for server-side state management.
360    pub fn with_previous_interaction(mut self, id: impl Into<String>) -> Self {
361        self.previous_interaction_id = Some(id.into());
362        self
363    }
364
365    /// Set `store` flag. Use `store(false)` for stateless mode.
366    pub fn with_store(mut self, store: bool) -> Self {
367        self.store = Some(store);
368        self
369    }
370
371    /// Enable background execution.
372    pub fn with_background(mut self) -> Self {
373        self.background = true;
374        self
375    }
376
377    /// Set the environment configuration.
378    pub fn with_environment(mut self, env: EnvironmentConfig) -> Self {
379        self.environment = Some(EnvironmentConfigOrString::Config(env));
380        self
381    }
382
383    /// Set the environment by ID (reuse an existing environment).
384    pub fn with_environment_id(mut self, env_id: impl Into<String>) -> Self {
385        self.environment = Some(EnvironmentConfigOrString::Id(env_id.into()));
386        self
387    }
388
389    /// Set cached content.
390    pub fn with_cached_content(mut self, cached_content: impl Into<String>) -> Self {
391        self.cached_content = Some(cached_content.into());
392        self
393    }
394
395    /// Set response modalities.
396    pub fn with_response_modalities(mut self, modalities: Vec<ResponseModality>) -> Self {
397        self.response_modalities = modalities;
398        self
399    }
400
401    /// Set the service tier.
402    pub fn with_service_tier(mut self, tier: ServiceTier) -> Self {
403        self.service_tier = Some(tier);
404        self
405    }
406
407    /// Set the webhook configuration.
408    pub fn with_webhook_config(mut self, config: WebhookConfig) -> Self {
409        self.webhook_config = Some(config);
410        self
411    }
412
413    // ===== Build & Execute =====
414
415    /// Build the request.
416    pub fn build(self) -> Result<CreateInteractionRequest, ClientError> {
417        let input = self.input.ok_or_else(|| ClientError::InvalidResourceName {
418            name: "input is required for interaction".to_string(),
419        })?;
420
421        let model = if self.model.is_none() && self.agent.is_none() {
422            Some(
423                self.client
424                    .model
425                    .as_str()
426                    .trim_start_matches("models/")
427                    .to_string(),
428            )
429        } else {
430            self.model
431        };
432
433        Ok(CreateInteractionRequest {
434            model,
435            agent: self.agent,
436            input,
437            system_instruction: self.system_instruction,
438            tools: self.tools,
439            response_format: self.response_format,
440            stream: None,
441            store: self.store,
442            background: if self.background { Some(true) } else { None },
443            generation_config: self.generation_config,
444            agent_config: self.agent_config,
445            previous_interaction_id: self.previous_interaction_id,
446            environment: self.environment,
447            cached_content: self.cached_content,
448            response_modalities: self.response_modalities,
449            service_tier: self.service_tier,
450            webhook_config: self.webhook_config,
451        })
452    }
453
454    /// Execute the interaction (non-streaming).
455    #[instrument(skip_all, fields(
456        model = self.model.as_deref().unwrap_or(""),
457        agent = self.agent.as_deref().unwrap_or(""),
458        tools.count = self.tools.len(),
459        system.instruction.present = self.system_instruction.is_some(),
460        background = self.background,
461        previous.interaction.present = self.previous_interaction_id.is_some(),
462        status.code,
463        usage.total_tokens,
464    ))]
465    pub async fn execute(self) -> Result<Interaction, ClientError> {
466        let client = self.client.clone();
467        let request = self.build()?;
468        let response = client.create_interaction(request).await?;
469
470        Span::current().record("status.code", response.status.as_ref());
471
472        if let Some(usage) = &response.usage {
473            Span::current().record("usage.total_tokens", usage.total_tokens);
474        }
475
476        Ok(response)
477    }
478
479    /// Execute the interaction (streaming).
480    #[instrument(skip_all, fields(
481        model = self.model.as_deref().unwrap_or(""),
482        agent = self.agent.as_deref().unwrap_or(""),
483        tools.count = self.tools.len(),
484    ))]
485    pub async fn execute_stream(self) -> Result<InteractionStream, ClientError> {
486        let client = self.client.clone();
487        let request = self.build()?;
488        client.create_interaction_stream(request).await
489    }
490}