mod auto_functions;
use auto_functions::DEFAULT_MAX_FUNCTION_CALL_LOOPS;
use crate::GenaiError;
use crate::client::Client;
use crate::function_calling::ToolService;
use std::sync::Arc;
use std::time::Duration;
use tracing::debug;
use crate::{
AgentConfig, Content, DeepResearchConfig, EnvironmentSpec, FunctionCallingMode,
FunctionDeclaration, GenerationConfig, ImageConfig, InteractionInput, InteractionRequest,
InteractionResponse, ResponseFormat, ResponseFormatSpec, ServiceTier, SpeechConfig, Step,
StreamEvent, ThinkingLevel, ThinkingSummaries, Tool as InternalTool, ToolChoice, VideoConfig,
WebhookConfig,
};
use futures_util::{StreamExt, stream::BoxStream};
pub struct InteractionBuilder<'a> {
client: &'a Client,
model: Option<String>,
agent: Option<String>,
agent_config: Option<AgentConfig>,
history: Vec<Step>,
current_message: Option<String>,
content_input: Option<Vec<Content>>,
previous_interaction_id: Option<String>,
tools: Option<Vec<InternalTool>>,
response_modalities: Option<Vec<String>>,
response_format: Option<ResponseFormatSpec>,
generation_config: Option<GenerationConfig>,
speech_configs: Option<Vec<SpeechConfig>>,
background: Option<bool>,
store: Option<bool>,
system_instruction: Option<String>,
service_tier: Option<ServiceTier>,
cached_content: Option<String>,
webhook_config: Option<WebhookConfig>,
environment: Option<EnvironmentSpec>,
max_function_call_loops: usize,
tool_service: Option<Arc<dyn ToolService>>,
timeout: Option<Duration>,
}
impl std::fmt::Debug for InteractionBuilder<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("InteractionBuilder")
.field("model", &self.model)
.field("agent", &self.agent)
.field("agent_config", &self.agent_config)
.field("history", &self.history)
.field("current_message", &self.current_message)
.field("content_input", &self.content_input)
.field("previous_interaction_id", &self.previous_interaction_id)
.field("tools", &self.tools)
.field("response_modalities", &self.response_modalities)
.field("response_format", &self.response_format)
.field("generation_config", &self.generation_config)
.field("speech_configs", &self.speech_configs)
.field("webhook_config", &self.webhook_config)
.field("environment", &self.environment)
.field("background", &self.background)
.field("store", &self.store)
.field("system_instruction", &self.system_instruction)
.field("max_function_call_loops", &self.max_function_call_loops)
.field("tool_service", &self.tool_service.as_ref().map(|_| "..."))
.field("timeout", &self.timeout)
.finish()
}
}
impl<'a> InteractionBuilder<'a> {
pub(crate) fn new(client: &'a Client) -> Self {
Self {
client,
model: None,
agent: None,
agent_config: None,
history: Vec::new(),
current_message: None,
content_input: None,
previous_interaction_id: None,
tools: None,
response_modalities: None,
response_format: None,
generation_config: None,
speech_configs: None,
background: None,
store: None,
system_instruction: None,
service_tier: None,
cached_content: None,
webhook_config: None,
environment: None,
max_function_call_loops: DEFAULT_MAX_FUNCTION_CALL_LOOPS,
tool_service: None,
timeout: None,
}
}
fn validate(&self) -> Result<(), GenaiError> {
if self.store == Some(false) && self.previous_interaction_id.is_some() {
return Err(GenaiError::InvalidInput(
"Chained interactions require storage. \
Cannot use with_previous_interaction() with with_store_disabled(). \
Solution: Remove .with_store_disabled() to enable storage, \
or remove .with_previous_interaction() if this is a new conversation."
.to_string(),
));
}
if self.store == Some(false) && self.background == Some(true) {
return Err(GenaiError::InvalidInput(
"Background execution requires storage. \
Cannot use with_background(true) with with_store_disabled(). \
Solution: Remove .with_store_disabled() to enable storage, \
or set .with_background(false)."
.to_string(),
));
}
Ok(())
}
fn validate_for_auto_functions(&self) -> Result<(), GenaiError> {
self.validate()?;
if self.store == Some(false) {
return Err(GenaiError::InvalidInput(
"create_with_auto_functions() requires storage to maintain conversation context \
across multiple function execution rounds. \
Solution: Remove .with_store_disabled() to enable storage, \
or use create() for single-turn function handling."
.to_string(),
));
}
Ok(())
}
#[must_use]
pub fn with_previous_interaction(mut self, id: impl Into<String>) -> Self {
self.previous_interaction_id = Some(id.into());
self
}
#[must_use]
pub fn with_store_disabled(mut self) -> Self {
self.store = Some(false);
self
}
#[must_use]
pub fn with_background(mut self, background: bool) -> Self {
self.background = Some(background);
self
}
#[must_use]
pub fn with_model(mut self, model: impl Into<String>) -> Self {
self.model = Some(model.into());
self
}
#[must_use]
pub fn with_agent(mut self, agent: impl Into<String>) -> Self {
self.agent = Some(agent.into());
self
}
#[must_use]
pub fn with_agent_config(mut self, config: impl Into<AgentConfig>) -> Self {
self.agent_config = Some(config.into());
self
}
#[must_use]
pub fn with_deep_research_config(mut self, thinking_summaries: ThinkingSummaries) -> Self {
self.agent_config = Some(
DeepResearchConfig::new()
.with_thinking_summaries(thinking_summaries)
.into(),
);
self
}
#[must_use]
pub fn with_input(mut self, input: InteractionInput) -> Self {
match input {
InteractionInput::Text(text) => {
self.current_message = Some(text);
}
InteractionInput::Content(content) => {
self.content_input = Some(content);
}
InteractionInput::Steps(steps) => {
self.history = steps;
}
}
self
}
#[must_use]
pub fn with_text(mut self, text: impl Into<String>) -> Self {
self.current_message = Some(text.into());
self
}
#[must_use]
pub fn with_system_instruction(mut self, instruction: impl Into<String>) -> Self {
self.system_instruction = Some(instruction.into());
self
}
#[must_use]
pub fn with_content(mut self, content: Vec<Content>) -> Self {
self.content_input = Some(content);
self
}
#[must_use]
pub fn with_history(mut self, steps: Vec<Step>) -> Self {
self.history = steps;
self
}
#[must_use]
pub fn conversation(self) -> ConversationBuilder<'a> {
ConversationBuilder {
parent: self,
steps: Vec::new(),
}
}
fn push_tool(&mut self, tool: InternalTool) {
self.tools.get_or_insert_with(Vec::new).push(tool);
}
#[must_use]
pub fn add_tool(mut self, tool: impl Into<InternalTool>) -> Self {
self.push_tool(tool.into());
self
}
#[must_use]
pub fn set_tools(mut self, tools: Vec<InternalTool>) -> Self {
self.tools = Some(tools);
self
}
#[must_use]
pub fn add_function(mut self, function: FunctionDeclaration) -> Self {
self.push_tool(function.into_tool());
self
}
#[must_use]
pub fn add_functions(mut self, functions: Vec<FunctionDeclaration>) -> Self {
for func in functions {
self.push_tool(func.into_tool());
}
self
}
#[must_use]
pub fn with_tool_service(mut self, service: Arc<dyn ToolService>) -> Self {
self.tool_service = Some(service);
self
}
#[must_use]
pub fn with_google_search(mut self) -> Self {
self.push_tool(InternalTool::GoogleSearch { search_types: None });
self
}
#[must_use]
pub fn with_google_maps(mut self) -> Self {
self.push_tool(InternalTool::GoogleMaps {
latitude: None,
longitude: None,
enable_widget: None,
});
self
}
#[must_use]
pub fn with_code_execution(mut self) -> Self {
self.push_tool(InternalTool::CodeExecution);
self
}
#[must_use]
pub fn with_url_context(mut self) -> Self {
self.push_tool(InternalTool::UrlContext);
self
}
#[must_use]
pub fn with_response_modalities(mut self, modalities: Vec<String>) -> Self {
self.response_modalities = Some(modalities.into_iter().map(|m| m.to_lowercase()).collect());
self
}
#[must_use]
pub fn with_image_output(self) -> Self {
self.with_response_modalities(vec!["image".to_string()])
}
#[must_use]
pub fn with_audio_output(self) -> Self {
self.with_response_modalities(vec!["audio".to_string()])
}
#[must_use]
pub fn with_speech_config(mut self, config: SpeechConfig) -> Self {
self.speech_configs = Some(vec![config]);
self
}
#[must_use]
pub fn with_speech_configs(mut self, configs: Vec<SpeechConfig>) -> Self {
self.speech_configs = Some(configs);
self
}
#[must_use]
pub fn add_speech_config(mut self, config: SpeechConfig) -> Self {
self.speech_configs
.get_or_insert_with(Vec::new)
.push(config);
self
}
#[must_use]
pub fn with_image_config(mut self, config: ImageConfig) -> Self {
let gen_config = self
.generation_config
.get_or_insert_with(GenerationConfig::default);
gen_config.image_config = Some(config);
self
}
#[must_use]
pub fn with_video_config(mut self, config: VideoConfig) -> Self {
let gen_config = self
.generation_config
.get_or_insert_with(GenerationConfig::default);
gen_config.video_config = Some(config);
self
}
#[must_use]
pub fn with_video_output(self) -> Self {
self.with_response_modalities(vec!["video".to_string()])
}
#[must_use]
pub fn with_webhook_config(mut self, config: WebhookConfig) -> Self {
self.webhook_config = Some(config);
self
}
#[must_use]
pub fn with_environment(mut self, environment: impl Into<EnvironmentSpec>) -> Self {
self.environment = Some(environment.into());
self
}
#[must_use]
pub fn with_voice(self, voice: impl Into<String>) -> Self {
self.with_speech_config(SpeechConfig::with_voice_and_language(voice, "en-US"))
}
#[must_use]
pub fn with_response_format(mut self, format: impl Into<ResponseFormat>) -> Self {
self.response_format = Some(ResponseFormatSpec::Single(format.into()));
self
}
#[must_use]
pub fn with_response_formats(mut self, formats: Vec<ResponseFormat>) -> Self {
self.response_format = Some(ResponseFormatSpec::List(formats));
self
}
#[must_use]
pub fn with_generation_config(mut self, config: GenerationConfig) -> Self {
self.generation_config = Some(config);
self
}
#[must_use]
pub fn with_thinking_level(mut self, level: ThinkingLevel) -> Self {
let config = self
.generation_config
.get_or_insert_with(GenerationConfig::default);
config.thinking_level = Some(level);
self
}
#[must_use]
pub fn with_thinking_summaries(mut self, summaries: ThinkingSummaries) -> Self {
let config = self
.generation_config
.get_or_insert_with(GenerationConfig::default);
config.thinking_summaries = Some(summaries);
self
}
#[must_use]
pub fn with_seed(mut self, seed: i64) -> Self {
let config = self
.generation_config
.get_or_insert_with(GenerationConfig::default);
config.seed = Some(seed);
self
}
#[must_use]
pub fn with_stop_sequences(mut self, sequences: Vec<String>) -> Self {
let config = self
.generation_config
.get_or_insert_with(GenerationConfig::default);
config.stop_sequences = Some(sequences);
self
}
#[must_use]
pub fn with_function_calling_mode(mut self, mode: FunctionCallingMode) -> Self {
let config = self
.generation_config
.get_or_insert_with(GenerationConfig::default);
config.tool_choice = Some(ToolChoice::Mode(mode));
self
}
#[must_use]
pub fn with_tool_choice(mut self, tool_choice: ToolChoice) -> Self {
let config = self
.generation_config
.get_or_insert_with(GenerationConfig::default);
config.tool_choice = Some(tool_choice);
self
}
#[must_use]
pub fn with_allowed_tools(mut self, tool_names: Vec<String>) -> Self {
let config = self
.generation_config
.get_or_insert_with(GenerationConfig::default);
let mode = match config.tool_choice.take() {
Some(ToolChoice::Mode(mode)) => Some(mode),
Some(ToolChoice::AllowedTools(allowed)) => allowed.mode,
_ => None,
};
config.tool_choice = Some(ToolChoice::allowed_tools(mode, tool_names));
self
}
#[must_use]
pub fn with_service_tier(mut self, tier: ServiceTier) -> Self {
self.service_tier = Some(tier);
self
}
#[must_use]
pub fn with_cached_content(mut self, cached_content: impl Into<String>) -> Self {
self.cached_content = Some(cached_content.into());
self
}
#[must_use]
pub fn with_presence_penalty(mut self, penalty: f32) -> Self {
let config = self
.generation_config
.get_or_insert_with(GenerationConfig::default);
config.presence_penalty = Some(penalty);
self
}
#[must_use]
pub fn with_frequency_penalty(mut self, penalty: f32) -> Self {
let config = self
.generation_config
.get_or_insert_with(GenerationConfig::default);
config.frequency_penalty = Some(penalty);
self
}
#[must_use]
pub fn with_store_enabled(mut self) -> Self {
self.store = Some(true);
self
}
#[must_use]
pub fn with_max_function_call_loops(mut self, max_loops: usize) -> Self {
if max_loops == 0 {
tracing::warn!(
"max_function_call_loops set to 0 - auto function calling will immediately fail \
if the model returns any function calls. Consider using create() instead of \
create_with_auto_functions() if you don't want automatic function execution."
);
}
self.max_function_call_loops = max_loops;
self
}
#[must_use]
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub async fn create(self) -> Result<InteractionResponse, GenaiError> {
let client = self.client;
let timeout = self.timeout;
let request = self.build()?;
let future = client.execute(request);
match timeout {
Some(duration) => tokio::time::timeout(duration, future).await.map_err(|_| {
debug!("Request timed out after {:?}", duration);
GenaiError::Timeout(duration)
})?,
None => future.await,
}
}
pub fn create_stream(self) -> BoxStream<'a, Result<StreamEvent, GenaiError>> {
let client = self.client;
let timeout = self.timeout;
Box::pin(async_stream::try_stream! {
let mut request = self.build()?;
request.stream = Some(true);
let mut stream = client.execute_stream(request);
loop {
let next_chunk = stream.next();
let result = match timeout {
Some(duration) => {
match tokio::time::timeout(duration, next_chunk).await {
Ok(Some(result)) => Some(result),
Ok(None) => None,
Err(_) => {
debug!("Stream chunk timed out after {:?}", duration);
Err(GenaiError::Timeout(duration))?;
unreachable!()
}
}
}
None => next_chunk.await,
};
match result {
Some(Ok(event)) => yield event,
Some(Err(e)) => Err(e)?,
None => break,
}
}
})
}
pub fn build(self) -> Result<InteractionRequest, GenaiError> {
self.validate()?;
if self.content_input.is_some() && !self.history.is_empty() {
return Err(GenaiError::InvalidInput(
"Content input (with_content()) cannot be combined with with_history(). \
For multimodal multi-turn conversations, wrap the content in \
Step::user_input(...) and include it in the history instead."
.to_string(),
));
}
if self.agent_config.is_some() && self.agent.is_none() {
return Err(GenaiError::InvalidInput(
"with_agent_config() requires with_agent(). \
Agent config is ignored when using with_model()."
.to_string(),
));
}
let input = if let Some(mut content) = self.content_input {
if let Some(text) = self.current_message {
content.insert(0, Content::text(text));
}
InteractionInput::Content(content)
} else {
match (self.history.is_empty(), self.current_message) {
(true, None) => {
return Err(GenaiError::InvalidInput(
"Input is required for interaction".to_string(),
));
}
(true, Some(msg)) => InteractionInput::Text(msg),
(false, None) => InteractionInput::Steps(self.history),
(false, Some(msg)) => {
let mut steps = self.history;
steps.push(Step::user_text(msg));
InteractionInput::Steps(steps)
}
}
};
match (&self.model, &self.agent) {
(None, None) => {
return Err(GenaiError::InvalidInput(
"Either model or agent must be specified".to_string(),
));
}
(Some(model), Some(agent)) => {
return Err(GenaiError::InvalidInput(format!(
"Cannot specify both model ('{}') and agent ('{}') - use one or the other",
model, agent
)));
}
_ => {} }
let generation_config = match (self.generation_config, self.speech_configs) {
(Some(mut config), Some(speech)) => {
config.speech_config = Some(speech);
Some(config)
}
(None, Some(speech)) => Some(GenerationConfig {
speech_config: Some(speech),
..Default::default()
}),
(config, None) => config,
};
Ok(InteractionRequest {
model: self.model,
agent: self.agent,
agent_config: self.agent_config,
input,
previous_interaction_id: self.previous_interaction_id,
tools: self.tools,
response_modalities: self.response_modalities,
response_format: self.response_format,
generation_config,
stream: None, background: self.background,
store: self.store,
system_instruction: self.system_instruction,
service_tier: self.service_tier,
cached_content: self.cached_content,
webhook_config: self.webhook_config,
environment: self.environment,
})
}
}
pub struct ConversationBuilder<'a> {
parent: InteractionBuilder<'a>,
steps: Vec<Step>,
}
impl<'a> ConversationBuilder<'a> {
#[must_use]
pub fn user(mut self, content: impl Into<crate::TurnContent>) -> Self {
self.steps.push(match content.into() {
crate::TurnContent::Text(text) => Step::user_text(text),
crate::TurnContent::Parts(parts) => Step::user_input(parts),
});
self
}
#[must_use]
pub fn model(mut self, content: impl Into<crate::TurnContent>) -> Self {
self.steps.push(match content.into() {
crate::TurnContent::Text(text) => Step::model_text(text),
crate::TurnContent::Parts(parts) => Step::model_output(parts),
});
self
}
#[must_use]
pub fn turn(self, role: crate::Role, content: impl Into<crate::TurnContent>) -> Self {
match role {
crate::Role::Model => self.model(content),
_ => self.user(content),
}
}
#[must_use]
pub fn done(self) -> InteractionBuilder<'a> {
let mut parent = self.parent;
parent.history = self.steps;
parent
}
}
#[cfg(test)]
mod tests;