use super::output_strategy::{OutputStrategy, ValidationContext, ValidationResult};
use super::GenerationObjectConfig;
use crate::error::GenerateObjectError;
use crate::retry::RetryPolicy;
use ai_sdk_provider::language_model::{
CallOptions, CallWarning, Content, FinishReason, LanguageModel, Message, ResponseFormat,
ResponseMetadata, TextPart, Usage,
};
use futures::future::BoxFuture;
use serde_json::Value;
use std::future::IntoFuture;
use std::sync::Arc;
pub struct GenerateObjectBuilder<M, P, S> {
model: M,
prompt: P,
strategy: S,
config: GenerationObjectConfig,
}
impl GenerateObjectBuilder<(), (), ()> {
pub fn new() -> Self {
Self {
model: (),
prompt: (),
strategy: (),
config: GenerationObjectConfig::default(),
}
}
}
impl Default for GenerateObjectBuilder<(), (), ()> {
fn default() -> Self {
Self::new()
}
}
impl<M, P, S> GenerateObjectBuilder<M, P, S> {
pub fn schema_name(mut self, name: impl Into<String>) -> Self {
self.config.schema_name = Some(name.into());
self
}
pub fn schema_description(mut self, description: impl Into<String>) -> Self {
self.config.schema_description = Some(description.into());
self
}
pub fn temperature(mut self, temperature: f32) -> Self {
self.config.temperature = Some(temperature);
self
}
pub fn max_tokens(mut self, max_tokens: u32) -> Self {
self.config.max_tokens = Some(max_tokens);
self
}
pub fn retry_policy(mut self, retry_policy: RetryPolicy) -> Self {
self.config.retry_policy = retry_policy;
self
}
}
impl<P, S> GenerateObjectBuilder<(), P, S> {
pub fn model<Mod: LanguageModel + 'static>(
self,
model: Mod,
) -> GenerateObjectBuilder<Arc<dyn LanguageModel>, P, S> {
GenerateObjectBuilder {
model: Arc::new(model),
prompt: self.prompt,
strategy: self.strategy,
config: self.config,
}
}
}
impl<M, S> GenerateObjectBuilder<M, (), S> {
pub fn prompt(self, prompt: impl Into<String>) -> GenerateObjectBuilder<M, Vec<Message>, S> {
let text = prompt.into();
let messages = vec![Message::User {
content: vec![ai_sdk_provider::language_model::UserContentPart::Text { text }],
}];
GenerateObjectBuilder {
model: self.model,
prompt: messages,
strategy: self.strategy,
config: self.config,
}
}
pub fn messages(self, messages: Vec<Message>) -> GenerateObjectBuilder<M, Vec<Message>, S> {
GenerateObjectBuilder {
model: self.model,
prompt: messages,
strategy: self.strategy,
config: self.config,
}
}
}
impl<M, P> GenerateObjectBuilder<M, P, ()> {
pub fn output_strategy<Strat: OutputStrategy + 'static>(
self,
strategy: Strat,
) -> GenerateObjectBuilder<M, P, Arc<Strat>> {
GenerateObjectBuilder {
model: self.model,
prompt: self.prompt,
strategy: Arc::new(strategy),
config: self.config,
}
}
}
impl<S> GenerateObjectBuilder<Arc<dyn LanguageModel>, Vec<Message>, Arc<S>>
where
S: OutputStrategy + 'static,
{
pub async fn execute(self) -> Result<GenerateObjectResult<S::Result>, GenerateObjectError> {
let model = self.model;
let messages = self.prompt;
let strategy = self.strategy;
let config = self.config;
let schema = strategy.json_schema().await;
let options = CallOptions {
prompt: messages.clone().into(),
response_format: Some(ResponseFormat::Json {
schema: schema.clone(),
name: config.schema_name,
description: config.schema_description,
}),
temperature: config.temperature,
max_output_tokens: config.max_tokens,
tools: None, tool_choice: None,
..Default::default()
};
let response = config
.retry_policy
.retry(|| {
let model = model.clone();
let options = options.clone();
async move { model.do_generate(options).await }
})
.await?;
let text = extract_text_from_content(&response.content)
.ok_or(GenerateObjectError::NoTextContent)?;
let value: Value = serde_json::from_str(&text)?;
let context = ValidationContext {
text: text.clone(),
response: response.response.clone().map(|r| ResponseMetadata {
id: r.id,
timestamp: r.timestamp,
model_id: r.model_id,
}),
usage: response.usage.clone(),
};
let validation = strategy.validate_final_result(Some(value), context).await;
match validation {
ValidationResult::Success { value: result, .. } => Ok(GenerateObjectResult {
object: result,
usage: response.usage.clone(),
finish_reason: response.finish_reason,
warnings: response.warnings.clone(),
raw_response: response,
}),
ValidationResult::Failure { error, .. } => {
Err(GenerateObjectError::ValidationFailed(error.to_string()))
}
}
}
}
impl<S> IntoFuture for GenerateObjectBuilder<Arc<dyn LanguageModel>, Vec<Message>, Arc<S>>
where
S: OutputStrategy + 'static,
{
type Output = Result<GenerateObjectResult<S::Result>, GenerateObjectError>;
type IntoFuture = BoxFuture<'static, Self::Output>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(self.execute())
}
}
#[derive(Debug)]
pub struct GenerateObjectResult<R> {
pub object: R,
pub usage: Usage,
pub finish_reason: FinishReason,
pub warnings: Vec<CallWarning>,
pub raw_response: ai_sdk_provider::language_model::GenerateResponse,
}
pub fn generate_object() -> GenerateObjectBuilder<(), (), ()> {
GenerateObjectBuilder::new()
}
fn extract_text_from_content(content: &[Content]) -> Option<String> {
let mut text = String::new();
for item in content {
if let Content::Text(TextPart { text: t, .. }) = item {
text.push_str(t);
}
}
if text.is_empty() {
None
} else {
Some(text)
}
}
#[cfg(test)]
mod tests {
}