use async_trait::async_trait;
use serde::de::DeserializeOwned;
use crate::backend::ModelInfo;
use crate::backend::usage::{
GenerateResult, MaterializeFailure, MaterializeReport, MaterializeResult,
};
use crate::error::Result;
use crate::model::Instructor;
#[derive(Debug, Clone)]
pub struct MediaFile {
pub uri: String,
pub mime_type: String,
pub data: Option<String>,
}
impl MediaFile {
#[must_use]
pub fn new(uri: impl Into<String>, mime_type: impl Into<String>) -> Self {
Self {
uri: uri.into(),
mime_type: mime_type.into(),
data: None,
}
}
#[cfg(feature = "_client")]
#[must_use]
pub fn from_bytes(data: impl AsRef<[u8]>, mime_type: impl Into<String>) -> Self {
use base64::Engine;
let encoded = base64::engine::general_purpose::STANDARD.encode(data.as_ref());
Self {
uri: String::new(),
mime_type: mime_type.into(),
data: Some(encoded),
}
}
}
#[async_trait]
pub trait LLMClient {
async fn materialize<T>(&self, prompt: &str) -> Result<T>
where
T: Instructor + DeserializeOwned + Send + 'static;
async fn materialize_with_media<T>(&self, prompt: &str, media: &[MediaFile]) -> Result<T>
where
T: Instructor + DeserializeOwned + Send + 'static,
{
if media.is_empty() {
self.materialize(prompt).await
} else {
Err(crate::error::RStructorError::Unsupported(
"this client does not support media inputs".to_string(),
))
}
}
async fn materialize_with_metadata<T>(&self, prompt: &str) -> Result<MaterializeResult<T>>
where
T: Instructor + DeserializeOwned + Send + 'static;
async fn materialize_with_attempts<T>(
&self,
prompt: &str,
) -> std::result::Result<MaterializeReport<T>, MaterializeFailure>
where
T: Instructor + DeserializeOwned + Send + 'static,
{
self.materialize_with_metadata(prompt)
.await
.map(MaterializeReport::from_result)
.map_err(MaterializeFailure::from_error)
}
async fn materialize_with_media_and_attempts<T>(
&self,
prompt: &str,
media: &[MediaFile],
) -> std::result::Result<MaterializeReport<T>, MaterializeFailure>
where
T: Instructor + DeserializeOwned + Send + 'static,
{
if media.is_empty() {
self.materialize_with_attempts(prompt).await
} else {
self.materialize_with_media(prompt, media)
.await
.map(|data| MaterializeReport::from_result(MaterializeResult::from_data(data)))
.map_err(MaterializeFailure::from_error)
}
}
async fn generate(&self, prompt: &str) -> Result<String>;
async fn generate_with_media(&self, prompt: &str, media: &[MediaFile]) -> Result<String> {
if media.is_empty() {
self.generate(prompt).await
} else {
Err(crate::error::RStructorError::Unsupported(
"this client does not support media inputs".to_string(),
))
}
}
async fn generate_with_metadata(&self, prompt: &str) -> Result<GenerateResult>;
#[cfg(feature = "streaming")]
fn generate_stream<'a>(&'a self, prompt: &'a str) -> crate::backend::streaming::TextStream<'a>
where
Self: Sync,
{
Box::pin(async_stream::try_stream! {
let text = self.generate(prompt).await?;
yield text;
})
}
#[cfg(feature = "streaming")]
fn materialize_stream<'a, T>(
&'a self,
prompt: &'a str,
) -> crate::backend::streaming::ObjectStream<'a, T>
where
T: Instructor + DeserializeOwned + Send + 'static,
Self: Sync,
{
Box::pin(async_stream::try_stream! {
let value: T = self.materialize(prompt).await?;
yield crate::backend::streaming::StreamedObject::Complete(value);
})
}
#[cfg(feature = "streaming")]
fn materialize_iter<'a, T>(
&'a self,
_prompt: &'a str,
) -> crate::backend::streaming::ItemStream<'a, T>
where
T: Instructor + DeserializeOwned + Send + 'static,
Self: Sync,
{
Box::pin(futures_util::stream::once(async move {
Err::<T, crate::error::RStructorError>(crate::error::RStructorError::Unsupported(
"materialize_iter is not implemented for this client".to_string(),
))
}))
}
fn from_env() -> Result<Self>
where
Self: Sized;
async fn list_models(&self) -> Result<Vec<ModelInfo>>;
}