blob: 4ec7222be8bd393d5c574e35f1c07d948a367631 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use crate::{Error, Result};
use base64::{Engine, engine::general_purpose::STANDARD};
use serde_json::Value;
use std::{borrow::Cow, collections::BTreeMap};
/// Provider options are opaque extensions until the selected protocol decodes
/// them. They never authorize network, filesystem or tool execution capabilities.
pub type Extensions = Option<BTreeMap<String, Value>>;
#[derive(Debug, Clone)]
pub enum Message {
System {
text: String,
extensions: Extensions,
},
User {
parts: Vec<UserPart>,
extensions: Extensions,
},
Assistant {
parts: Vec<AssistantPart>,
extensions: Extensions,
},
Tool {
results: Vec<ToolResult>,
extensions: Extensions,
},
}
#[derive(Debug, Clone)]
pub enum UserPart {
Text {
text: String,
extensions: Extensions,
},
File(File),
Custom {
kind: String,
extensions: Extensions,
},
}
impl UserPart {
pub fn extensions(&self) -> &Extensions {
match self {
Self::Text { extensions, .. } | Self::Custom { extensions, .. } => extensions,
Self::File(file) => &file.extensions,
}
}
}
#[derive(Debug, Clone)]
pub enum AssistantPart {
Text {
text: String,
extensions: Extensions,
},
Reasoning {
text: String,
extensions: Extensions,
},
ToolCall(ToolCall),
ToolResult(ToolResult),
Custom {
kind: String,
extensions: Extensions,
},
}
impl AssistantPart {
pub fn extensions(&self) -> &Extensions {
match self {
Self::Text { extensions, .. }
| Self::Reasoning { extensions, .. }
| Self::Custom { extensions, .. } => extensions,
Self::ToolCall(call) => &call.extensions,
Self::ToolResult(result) => &result.extensions,
}
}
}
#[derive(Debug, Clone)]
pub struct ToolCall {
pub id: String,
pub name: String,
pub input: Value,
pub provider_executed: bool,
pub extensions: Extensions,
}
#[derive(Debug, Clone)]
pub struct ToolResult {
pub id: String,
pub name: String,
pub output: ToolOutput,
pub extensions: Extensions,
}
#[derive(Debug, Clone)]
pub struct ToolOutput {
pub value: ToolValue,
pub extensions: Extensions,
}
#[derive(Debug, Clone)]
pub enum ToolValue {
Text(String),
Json(Value),
ErrorText(String),
ErrorJson(Value),
Denied { reason: Option<String> },
Content(Vec<UserPart>),
}
#[derive(Debug, Clone)]
pub struct File {
pub media_type: String,
pub filename: Option<String>,
pub source: FileSource,
pub extensions: Extensions,
}
#[derive(Debug, Clone)]
pub enum FileSource {
Inline(InlineData),
Url(String),
Text(String),
Reference(BTreeMap<String, Value>),
}
#[derive(Debug, Clone)]
pub enum InlineData {
Bytes(Vec<u8>),
Base64(String),
}
impl InlineData {
pub fn bytes(&self) -> Result<Cow<'_, [u8]>> {
match self {
Self::Bytes(bytes) => Ok(Cow::Borrowed(bytes)),
Self::Base64(text) => {
STANDARD
.decode(text)
.map(Cow::Owned)
.map_err(|_| Error::InvalidArgument {
field: "base64 media data".into(),
value: Value::Null,
})
}
}
}
pub fn base64(&self) -> Cow<'_, str> {
match self {
Self::Bytes(bytes) => Cow::Owned(STANDARD.encode(bytes)),
Self::Base64(text) => Cow::Borrowed(text),
}
}
}
impl File {
pub fn reference(&self, provider: &str) -> Result<&Value> {
let FileSource::Reference(references) = &self.source else {
return Err(Error::InvalidArgument {
field: "provider media reference".into(),
value: Value::Null,
});
};
references
.get(provider)
.filter(|v| !v.is_null())
.ok_or_else(|| Error::MissingReference {
provider: provider.into(),
reference: serde_json::to_value(references).expect("reference values are JSON"),
})
}
pub fn resolved_media_type(&self) -> Result<Cow<'_, str>> {
if self.media_type.contains('/') && !self.media_type.ends_with("/*") {
return Ok(Cow::Borrowed(&self.media_type));
}
let top = self.media_type.split('/').next().unwrap_or_default();
if let FileSource::Inline(data) = &self.source {
let bytes = data.bytes()?;
let detected = match top {
"image" if bytes.starts_with(b"\x89PNG") => Some("image/png"),
"image" if bytes.starts_with(&[0xff, 0xd8, 0xff]) => Some("image/jpeg"),
"image" if bytes.starts_with(b"GIF8") => Some("image/gif"),
"image" if bytes.starts_with(b"RIFF") && bytes.get(8..12) == Some(b"WEBP") => {
Some("image/webp")
}
"audio" if bytes.starts_with(b"RIFF") && bytes.get(8..12) == Some(b"WAVE") => {
Some("audio/wav")
}
"audio"
if bytes.starts_with(b"ID3")
|| (bytes.first() == Some(&0xff)
&& bytes.get(1).is_some_and(|v| v & 0xe0 == 0xe0)) =>
{
Some("audio/mpeg")
}
"audio" if bytes.starts_with(b"OggS") => Some("audio/ogg"),
"audio" if bytes.starts_with(b"fLaC") => Some("audio/flac"),
"application" if bytes.starts_with(b"%PDF-") => Some("application/pdf"),
"video" if bytes.get(4..8) == Some(b"ftyp") => Some("video/mp4"),
_ => None,
};
if let Some(media_type) = detected {
return Ok(Cow::Borrowed(media_type));
}
}
Err(Error::unsupported(format!(
"file of media type \"{}\" must specify subtype since {}",
self.media_type,
if matches!(self.source, FileSource::Inline(_)) {
"it could not be auto-detected"
} else {
"it is not passed as inline bytes"
}
)))
}
}