use anyhow::{anyhow, Context, Result};
use async_trait::async_trait;
use log::{error, info};
use reqwest::{multipart, Client};
use serde::{Deserialize, Serialize};
use crate::{
apis::AnthropicApiEndpoints, constants::ANTHROPIC_FILES_API_URL, domain::AllmsError,
files::LLMFiles, utils::get_mime_type,
};
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct AnthropicFile {
pub id: Option<String>,
debug: bool,
api_key: String,
}
#[async_trait(?Send)]
impl LLMFiles for AnthropicFile {
fn new(id: Option<String>, api_key: &str) -> Self {
Self {
id,
debug: false,
api_key: api_key.to_string(),
}
}
fn debug(mut self) -> Self {
self.debug = true;
self
}
async fn upload(mut self, file_name: &str, file_bytes: Vec<u8>) -> Result<Self> {
let files_url = ANTHROPIC_FILES_API_URL.to_string();
let mime_type = get_mime_type(file_name)
.and_then(|mime_type| {
if mime_type != "application/pdf" {
None
} else {
Some(mime_type)
}
})
.ok_or_else(|| anyhow!("Only PDF files (application/pdf) are supported"))?;
let form = multipart::Form::new().part(
"file",
multipart::Part::bytes(file_bytes)
.file_name(file_name.to_string())
.mime_str(mime_type)
.context("Failed to set MIME type")?,
);
let client = Client::new();
let response = client
.post(files_url)
.header("x-api-key", self.api_key.clone())
.header(
"anthropic-version",
AnthropicApiEndpoints::messages_default().version(),
)
.header(
"anthropic-beta",
AnthropicApiEndpoints::files_default().version(),
)
.multipart(form)
.send()
.await?;
let response_status = response.status();
let response_text = response.text().await?;
if self.debug {
info!(
"[debug] Anthropic Files status API response: [{}] {:#?}",
&response_status, &response_text
);
}
let response_deser: AnthropicFileResp =
serde_json::from_str(&response_text).map_err(|error| {
let error = AllmsError {
crate_name: "allms".to_string(),
module: "files::anthropic".to_string(),
error_message: format!(
"Anthropic Files API response serialization error: {}",
error
),
error_detail: response_text,
};
error!("{:?}", error);
anyhow!("{:?}", error)
})?;
self.id = Some(response_deser.id);
Ok(self)
}
async fn delete(&self) -> Result<()> {
let files_url = if let Some(id) = self.id.as_ref() {
format!("{}/{}", &*ANTHROPIC_FILES_API_URL, id)
} else {
return Err(anyhow!("File ID is required to delete a file"));
};
let client = Client::new();
let response = client
.delete(files_url)
.header("x-api-key", self.api_key.clone())
.header(
"anthropic-version",
AnthropicApiEndpoints::messages_default().version(),
)
.header(
"anthropic-beta",
AnthropicApiEndpoints::files_default().version(),
)
.send()
.await?;
let response_status = response.status();
let response_text = response.text().await?;
if self.debug {
info!(
"[debug] Anthropic Files status API response: [{}] {:#?}",
&response_status, &response_text
);
}
serde_json::from_str::<AnthropicFileDeleteResp>(&response_text)
.map_err(|error| {
let error = AllmsError {
crate_name: "allms".to_string(),
module: "files::anthropic".to_string(),
error_message: format!(
"Anthropic Files Delete API response serialization error: {}",
error
),
error_detail: response_text,
};
error!("{:?}", error);
anyhow!("{:?}", error)
})
.map(|response| match response.result_type {
AnthropicDeleteResultType::FileDeleted => (),
})
}
fn get_id(&self) -> Option<&String> {
self.id.as_ref()
}
fn is_debug(&self) -> bool {
self.debug
}
}
#[derive(Deserialize, Serialize, Debug, Clone)]
struct AnthropicFileResp {
id: String,
created_at: String,
filename: String,
mime_type: String,
size_bytes: usize,
#[serde(rename = "type")]
file_type: AnthropicFileType,
downloadable: bool,
}
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
#[serde(rename_all = "snake_case")]
enum AnthropicFileType {
#[default]
File,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
struct AnthropicFileDeleteResp {
id: String,
#[serde(rename = "type")]
result_type: AnthropicDeleteResultType,
}
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
#[serde(rename_all = "snake_case")]
enum AnthropicDeleteResultType {
#[default]
FileDeleted,
}