use serde::{Deserialize, Serialize};
use crate::error::ProviderKind;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Role {
System,
User,
Assistant,
Tool,
}
impl Role {
pub fn as_str(&self) -> &'static str {
match self {
Role::System => "system",
Role::User => "user",
Role::Assistant => "assistant",
Role::Tool => "tool",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
pub name: String,
pub arguments: serde_json::Value,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ContentBlock {
#[serde(rename = "text")]
Text {
text: String,
},
#[serde(rename = "image")]
Image {
source: ImageSource,
},
#[serde(rename = "audio")]
Audio {
source: FileSource,
},
#[serde(rename = "video")]
Video {
source: FileSource,
},
#[serde(rename = "file")]
File {
source: FileSource,
},
}
impl ContentBlock {
pub fn text(text: impl Into<String>) -> Self {
Self::Text { text: text.into() }
}
pub fn image_url(url: impl Into<String>) -> Self {
Self::Image {
source: ImageSource::Url { url: url.into() },
}
}
pub fn image_base64(media_type: impl Into<String>, data: impl Into<String>) -> Self {
Self::Image {
source: ImageSource::Base64 {
media_type: media_type.into(),
data: data.into(),
},
}
}
pub fn audio_url(url: impl Into<String>) -> Self {
Self::Audio {
source: FileSource::Url { url: url.into() },
}
}
pub fn audio_base64(media_type: impl Into<String>, data: impl Into<String>) -> Self {
Self::Audio {
source: FileSource::Base64 {
media_type: media_type.into(),
data: data.into(),
},
}
}
pub fn video_url(url: impl Into<String>) -> Self {
Self::Video {
source: FileSource::Url { url: url.into() },
}
}
pub fn video_base64(media_type: impl Into<String>, data: impl Into<String>) -> Self {
Self::Video {
source: FileSource::Base64 {
media_type: media_type.into(),
data: data.into(),
},
}
}
pub fn file_url(url: impl Into<String>) -> Self {
Self::File {
source: FileSource::Url { url: url.into() },
}
}
pub fn file_base64(media_type: impl Into<String>, data: impl Into<String>) -> Self {
Self::File {
source: FileSource::Base64 {
media_type: media_type.into(),
data: data.into(),
},
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ImageSource {
#[serde(rename = "url")]
Url {
url: String,
},
#[serde(rename = "base64")]
Base64 {
media_type: String,
data: String,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum FileSource {
#[serde(rename = "url")]
Url {
url: String,
},
#[serde(rename = "base64")]
Base64 {
media_type: String,
data: String,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Message {
pub role: Role,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub content: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub content_blocks: Vec<ContentBlock>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tool_calls: Vec<ToolCall>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub tool_error: bool,
}
impl Message {
pub fn system(content: impl Into<String>) -> Self {
Self {
role: Role::System,
content: content.into(),
content_blocks: Vec::new(),
tool_calls: Vec::new(),
tool_call_id: None,
tool_error: false,
}
}
pub fn user(content: impl Into<String>) -> Self {
Self {
role: Role::User,
content: content.into(),
content_blocks: Vec::new(),
tool_calls: Vec::new(),
tool_call_id: None,
tool_error: false,
}
}
pub fn assistant(content: impl Into<String>) -> Self {
Self {
role: Role::Assistant,
content: content.into(),
content_blocks: Vec::new(),
tool_calls: Vec::new(),
tool_call_id: None,
tool_error: false,
}
}
pub fn assistant_with_tool_calls(
content: impl Into<String>,
tool_calls: Vec<ToolCall>,
) -> Self {
Self {
role: Role::Assistant,
content: content.into(),
content_blocks: Vec::new(),
tool_calls,
tool_call_id: None,
tool_error: false,
}
}
pub fn tool(content: impl Into<String>, tool_call_id: impl Into<String>) -> Self {
Self {
role: Role::Tool,
content: content.into(),
content_blocks: Vec::new(),
tool_calls: Vec::new(),
tool_call_id: Some(tool_call_id.into()),
tool_error: false,
}
}
pub fn tool_error(content: impl Into<String>, tool_call_id: impl Into<String>) -> Self {
Self {
role: Role::Tool,
content: content.into(),
content_blocks: Vec::new(),
tool_calls: Vec::new(),
tool_call_id: Some(tool_call_id.into()),
tool_error: true,
}
}
pub fn user_multimodal(content_blocks: Vec<ContentBlock>) -> Self {
Self {
role: Role::User,
content: String::new(),
content_blocks,
tool_calls: Vec::new(),
tool_call_id: None,
tool_error: false,
}
}
pub fn is_multimodal(&self) -> bool {
!self.content_blocks.is_empty()
}
pub fn has_tool_calls(&self) -> bool {
!self.tool_calls.is_empty()
}
pub fn text_content(&self) -> String {
if self.is_multimodal() {
self.content_blocks
.iter()
.filter_map(|block| match block {
ContentBlock::Text { text } => Some(text.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n")
} else {
self.content.clone()
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ConversationTurn {
pub user_message: Message,
pub assistant_message: Message,
pub tool_results: Vec<Message>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum StreamEvent {
TextDelta {
text: String,
},
ToolCall {
id: String,
name: String,
arguments: String,
},
ToolResult {
id: String,
result: String,
},
TurnComplete {
turn: ConversationTurn,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Prompt {
pub messages: Vec<Message>,
}
impl Prompt {
pub fn new(messages: Vec<Message>) -> Self {
Self { messages }
}
pub fn single(message: Message) -> Self {
Self {
messages: vec![message],
}
}
pub fn push_message(&mut self, message: Message) {
self.messages.push(message);
}
pub fn with_message(mut self, message: Message) -> Self {
self.push_message(message);
self
}
pub fn with_history(mut self, history: Vec<ConversationTurn>) -> Self {
for turn in history {
self.push_turn(turn);
}
self
}
pub fn push_turn(&mut self, turn: ConversationTurn) {
self.messages.push(turn.user_message);
self.messages.push(turn.assistant_message);
self.messages.extend(turn.tool_results);
}
pub fn system_message(&self) -> Option<&str> {
self.messages
.iter()
.find(|m| m.role == Role::System)
.map(|m| m.content.as_str())
}
pub fn conversation_messages(&self) -> Vec<&Message> {
self.messages
.iter()
.filter(|m| m.role != Role::System)
.collect()
}
pub fn is_multimodal(&self) -> bool {
self.messages.iter().any(|m| m.is_multimodal())
}
}
impl From<&Prompt> for Prompt {
fn from(prompt: &Prompt) -> Self {
prompt.clone()
}
}
impl From<Vec<Message>> for Prompt {
fn from(messages: Vec<Message>) -> Self {
Self::new(messages)
}
}
impl From<Message> for Prompt {
fn from(message: Message) -> Self {
Self::single(message)
}
}
impl From<&str> for Prompt {
fn from(text: &str) -> Self {
Prompt {
messages: vec![Message::user(text.to_string())],
}
}
}
impl From<String> for Prompt {
fn from(text: String) -> Self {
Prompt {
messages: vec![Message::user(text)],
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Usage {
pub prompt_tokens: Option<i32>,
pub completion_tokens: Option<i32>,
pub total_tokens: Option<i32>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Response {
pub messages: Vec<Message>,
pub usage: Option<Usage>,
pub model: String,
pub provider: ProviderKind,
pub finish_reason: Option<String>,
}
impl Response {
pub fn text(&self) -> String {
self.messages
.first()
.map(|m| m.text_content())
.unwrap_or_default()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StreamChunk {
pub content: String,
pub done: bool,
pub finish_reason: Option<String>,
pub usage: Option<Usage>,
}
#[derive(Debug, Clone)]
pub struct StructuredOutput<T> {
pub output: T,
pub response: Response,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ToolDefinition {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub input_schema: serde_json::Value,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn system_message_extraction() {
let prompt = Prompt::new(vec![Message::system("Be concise."), Message::user("Hello")]);
assert_eq!(prompt.system_message(), Some("Be concise."));
assert_eq!(prompt.conversation_messages().len(), 1);
}
#[test]
fn multimodal_text_content_concatenation() {
let msg = Message::user_multimodal(vec![
ContentBlock::text("First"),
ContentBlock::image_url("https://example.com/img.png"),
ContentBlock::text("Second"),
]);
assert!(msg.is_multimodal());
assert_eq!(msg.text_content(), "First\nSecond");
}
#[test]
fn prompt_from_conversions() {
let p1: Prompt = Message::user("hi").into();
assert_eq!(p1.messages.len(), 1);
let p2: Prompt = vec![Message::user("a"), Message::user("b")].into();
assert_eq!(p2.messages.len(), 2);
}
}