use std::path::PathBuf;
use bytes::Bytes;
use ferrin_spec::ApprovalId;
use ferrin_spec::MediaType;
use ferrin_spec::ProviderOptions;
use ferrin_spec::ProviderReference;
use ferrin_spec::ToolCallId;
use ferrin_spec::language_model::prompt::CustomPart;
use ferrin_spec::language_model::prompt::ReasoningPart;
use ferrin_spec::language_model::prompt::TextPart;
use ferrin_spec::language_model::prompt::ToolCallPart;
use ferrin_spec::language_model::prompt::ToolResultPart;
use serde::Deserialize;
use serde::Serialize;
use url::Url;
use crate::file_source::FileSource;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
#[non_exhaustive]
pub enum UserPart {
Text(TextPart),
Image(ImagePart),
File(FilePart),
}
impl UserPart {
#[must_use]
pub fn text(text: impl Into<String>) -> Self {
Self::Text(TextPart::new(text))
}
#[must_use]
pub fn image(source: impl Into<FileSource>) -> Self {
Self::Image(ImagePart::new(source))
}
#[must_use]
pub fn image_bytes(data: impl Into<Bytes>) -> Self {
Self::image(FileSource::bytes(data))
}
#[must_use]
pub fn image_base64(data: impl Into<String>) -> Self {
Self::image(FileSource::base64(data))
}
#[must_use]
pub fn image_url(url: Url) -> Self {
Self::image(FileSource::url(url))
}
#[must_use]
pub fn file(source: impl Into<FileSource>, media_type: impl Into<MediaType>) -> Self {
Self::File(FilePart::new(source, media_type))
}
#[must_use]
pub fn file_bytes(data: impl Into<Bytes>, media_type: impl Into<MediaType>) -> Self {
Self::file(FileSource::bytes(data), media_type)
}
#[must_use]
pub fn file_url(url: Url, media_type: impl Into<MediaType>) -> Self {
Self::file(FileSource::url(url), media_type)
}
#[must_use]
pub fn file_reference(reference: ProviderReference, media_type: impl Into<MediaType>) -> Self {
Self::file(FileSource::Reference { reference }, media_type)
}
#[must_use]
pub fn file_text(text: impl Into<String>, media_type: impl Into<MediaType>) -> Self {
Self::file(FileSource::text(text), media_type)
}
#[must_use]
pub fn file_path(path: impl Into<PathBuf>, media_type: impl Into<MediaType>) -> Self {
Self::file(FileSource::path(path), media_type)
}
#[must_use]
pub fn with_filename(mut self, filename: impl Into<String>) -> Self {
if let Self::File(file) = &mut self {
file.filename = Some(filename.into());
}
self
}
#[must_use]
pub fn with_provider_options(mut self, options: ProviderOptions) -> Self {
match &mut self {
Self::Text(part) => part.provider_options = Some(options),
Self::Image(part) => part.provider_options = Some(options),
Self::File(part) => part.provider_options = Some(options),
}
self
}
#[must_use]
pub fn as_text(&self) -> Option<&str> {
match self {
Self::Text(part) => Some(&part.text),
Self::Image(_) | Self::File(_) => None,
}
}
}
impl From<TextPart> for UserPart {
fn from(part: TextPart) -> Self {
Self::Text(part)
}
}
impl From<ImagePart> for UserPart {
fn from(part: ImagePart) -> Self {
Self::Image(part)
}
}
impl From<FilePart> for UserPart {
fn from(part: FilePart) -> Self {
Self::File(part)
}
}
impl From<&str> for UserPart {
fn from(text: &str) -> Self {
Self::text(text)
}
}
impl From<String> for UserPart {
fn from(text: String) -> Self {
Self::text(text)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ImagePart {
pub image: FileSource,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub media_type: Option<MediaType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_options: Option<ProviderOptions>,
}
impl ImagePart {
#[must_use]
pub fn new(image: impl Into<FileSource>) -> Self {
Self {
image: image.into(),
media_type: None,
provider_options: None,
}
}
#[must_use]
pub fn with_media_type(mut self, media_type: impl Into<MediaType>) -> Self {
self.media_type = Some(media_type.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FilePart {
pub data: FileSource,
pub media_type: MediaType,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub filename: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_options: Option<ProviderOptions>,
}
impl FilePart {
#[must_use]
pub fn new(data: impl Into<FileSource>, media_type: impl Into<MediaType>) -> Self {
Self {
data: data.into(),
media_type: media_type.into(),
filename: None,
provider_options: None,
}
}
#[must_use]
pub fn with_filename(mut self, filename: impl Into<String>) -> Self {
self.filename = Some(filename.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ReasoningFilePart {
pub data: FileSource,
pub media_type: MediaType,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_options: Option<ProviderOptions>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolApprovalRequest {
pub approval_id: ApprovalId,
pub tool_call_id: ToolCallId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub is_automatic: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signature: Option<String>,
}
impl ToolApprovalRequest {
#[must_use]
pub fn new(approval_id: impl Into<ApprovalId>, tool_call_id: impl Into<ToolCallId>) -> Self {
Self {
approval_id: approval_id.into(),
tool_call_id: tool_call_id.into(),
reason: None,
is_automatic: false,
signature: None,
}
}
#[must_use]
pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
self.reason = Some(reason.into());
self
}
#[must_use]
pub fn automatic(mut self) -> Self {
self.is_automatic = true;
self
}
#[must_use]
pub fn with_signature(mut self, signature: impl Into<String>) -> Self {
self.signature = Some(signature.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolApprovalResponse {
pub approval_id: ApprovalId,
pub approved: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub provider_executed: bool,
}
impl ToolApprovalResponse {
#[must_use]
pub fn approved(approval_id: impl Into<ApprovalId>) -> Self {
Self {
approval_id: approval_id.into(),
approved: true,
reason: None,
provider_executed: false,
}
}
#[must_use]
pub fn denied(approval_id: impl Into<ApprovalId>) -> Self {
Self {
approval_id: approval_id.into(),
approved: false,
reason: None,
provider_executed: false,
}
}
#[must_use]
pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
self.reason = Some(reason.into());
self
}
#[must_use]
pub fn provider_executed(mut self) -> Self {
self.provider_executed = true;
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
#[non_exhaustive]
pub enum AssistantPart {
Text(TextPart),
Custom(CustomPart),
File(FilePart),
Reasoning(ReasoningPart),
ReasoningFile(ReasoningFilePart),
ToolCall(ToolCallPart),
ToolResult(ToolResultPart),
ToolApprovalRequest(ToolApprovalRequest),
}
impl AssistantPart {
#[must_use]
pub fn text(text: impl Into<String>) -> Self {
Self::Text(TextPart::new(text))
}
#[must_use]
pub fn reasoning(text: impl Into<String>) -> Self {
Self::Reasoning(ReasoningPart::new(text))
}
#[must_use]
pub fn as_text(&self) -> Option<&str> {
match self {
Self::Text(part) => Some(&part.text),
_ => None,
}
}
#[must_use]
pub fn as_tool_call(&self) -> Option<&ToolCallPart> {
match self {
Self::ToolCall(part) => Some(part),
_ => None,
}
}
#[must_use]
pub fn as_tool_approval_request(&self) -> Option<&ToolApprovalRequest> {
match self {
Self::ToolApprovalRequest(part) => Some(part),
_ => None,
}
}
}
impl From<TextPart> for AssistantPart {
fn from(part: TextPart) -> Self {
Self::Text(part)
}
}
impl From<CustomPart> for AssistantPart {
fn from(part: CustomPart) -> Self {
Self::Custom(part)
}
}
impl From<FilePart> for AssistantPart {
fn from(part: FilePart) -> Self {
Self::File(part)
}
}
impl From<ReasoningPart> for AssistantPart {
fn from(part: ReasoningPart) -> Self {
Self::Reasoning(part)
}
}
impl From<ReasoningFilePart> for AssistantPart {
fn from(part: ReasoningFilePart) -> Self {
Self::ReasoningFile(part)
}
}
impl From<ToolCallPart> for AssistantPart {
fn from(part: ToolCallPart) -> Self {
Self::ToolCall(part)
}
}
impl From<ToolResultPart> for AssistantPart {
fn from(part: ToolResultPart) -> Self {
Self::ToolResult(part)
}
}
impl From<ToolApprovalRequest> for AssistantPart {
fn from(part: ToolApprovalRequest) -> Self {
Self::ToolApprovalRequest(part)
}
}
impl From<&str> for AssistantPart {
fn from(text: &str) -> Self {
Self::text(text)
}
}
impl From<String> for AssistantPart {
fn from(text: String) -> Self {
Self::text(text)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
#[non_exhaustive]
pub enum ToolPart {
ToolResult(ToolResultPart),
ToolApprovalResponse(ToolApprovalResponse),
}
impl ToolPart {
#[must_use]
pub fn as_tool_result(&self) -> Option<&ToolResultPart> {
match self {
Self::ToolResult(part) => Some(part),
Self::ToolApprovalResponse(_) => None,
}
}
#[must_use]
pub fn as_tool_approval_response(&self) -> Option<&ToolApprovalResponse> {
match self {
Self::ToolApprovalResponse(part) => Some(part),
Self::ToolResult(_) => None,
}
}
}
impl From<ToolResultPart> for ToolPart {
fn from(part: ToolResultPart) -> Self {
Self::ToolResult(part)
}
}
impl From<ToolApprovalResponse> for ToolPart {
fn from(part: ToolApprovalResponse) -> Self {
Self::ToolApprovalResponse(part)
}
}