use std::sync::Arc;
use agent_framework_core::client::EmbeddingClient;
use agent_framework_core::error::{Error, Result};
use agent_framework_core::types::{EmbeddingGenerationOptions, GeneratedEmbeddings};
use serde_json::{json, Map, Value};
use crate::DEFAULT_BASE_URL;
pub const DEFAULT_EMBEDDING_MODEL: &str = "mistral-embed";
#[derive(Clone)]
pub struct MistralEmbeddingClient {
inner: Arc<Inner>,
}
#[derive(Clone)]
struct Inner {
http: reqwest::Client,
api_key: String,
base_url: String,
model: String,
}
impl std::fmt::Debug for MistralEmbeddingClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MistralEmbeddingClient")
.field("base_url", &self.inner.base_url)
.field("model", &self.inner.model)
.finish_non_exhaustive()
}
}
impl MistralEmbeddingClient {
pub fn new(api_key: impl Into<String>, model: Option<String>) -> Self {
Self {
inner: Arc::new(Inner {
http: reqwest::Client::new(),
api_key: api_key.into(),
base_url: DEFAULT_BASE_URL.to_string(),
model: model.unwrap_or_else(|| DEFAULT_EMBEDDING_MODEL.to_string()),
}),
}
}
pub fn from_env(model: Option<String>) -> Result<Self> {
let key = std::env::var("MISTRAL_API_KEY")
.map_err(|_| Error::Configuration("MISTRAL_API_KEY is not set".into()))?;
let mut client = Self::new(key, model);
if let Ok(base) = std::env::var("MISTRAL_BASE_URL") {
client = client.with_base_url(base);
}
Ok(client)
}
pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
Arc::make_mut(&mut self.inner).base_url = base_url.into();
self
}
}
#[async_trait::async_trait]
impl EmbeddingClient for MistralEmbeddingClient {
async fn get_embeddings(
&self,
values: Vec<String>,
options: Option<EmbeddingGenerationOptions>,
) -> Result<GeneratedEmbeddings> {
let mut body = Map::new();
let model = options
.as_ref()
.and_then(|o| o.model.clone())
.unwrap_or_else(|| self.inner.model.clone());
body.insert("model".into(), json!(model));
body.insert("input".into(), json!(values));
if let Some(dimensions) = options.as_ref().and_then(|o| o.dimensions) {
body.insert("output_dimension".into(), json!(dimensions));
}
if let Some(dtype) = options
.as_ref()
.and_then(|o| o.additional_properties.get("output_dtype"))
{
body.insert("output_dtype".into(), dtype.clone());
}
let url = format!("{}/embeddings", self.inner.base_url.trim_end_matches('/'));
let resp = self
.inner
.http
.post(&url)
.bearer_auth(&self.inner.api_key)
.json(&Value::Object(body))
.send()
.await
.map_err(|e| Error::service(format!("request failed: {e}")))?;
if !resp.status().is_success() {
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
return Err(Error::service_status(
status.as_u16(),
format!("Mistral API error {status}: {text}"),
None,
));
}
let value: Value = resp
.json()
.await
.map_err(|e| Error::service(format!("invalid response json: {e}")))?;
agent_framework_openai::embeddings::parse_embeddings_response(&value)
}
fn model(&self) -> Option<&str> {
Some(&self.inner.model)
}
}