use anyhow::{Result, anyhow};
use rig::OneOrMany;
use rig::message::{
AssistantContent, DocumentSourceKind, Image, ImageMediaType, Message, ToolCall, ToolFunction,
ToolResult, ToolResultContent, UserContent,
};
use serde_json::Value;
use crate::message::{DynamicToolState, UIMessage, UIMessagePart};
pub fn extract_prompt_and_history(messages: &[UIMessage]) -> Result<(Message, Vec<Message>)> {
let messages = convert_messages(messages)?;
let (prompt, history) = messages
.split_last()
.ok_or_else(|| anyhow!("Message list is empty"))?;
Ok((prompt.clone(), history.to_vec()))
}
pub fn convert_messages(messages: &[UIMessage]) -> Result<Vec<Message>> {
messages.iter().map(convert_message).collect()
}
pub fn convert_message(msg: &UIMessage) -> Result<Message> {
match msg.role.as_str() {
"user" | "system" => {
let contents: Vec<_> = msg.parts.iter().filter_map(to_user_content).collect();
Ok(Message::User {
content: OneOrMany::many(contents)
.unwrap_or_else(|_| OneOrMany::one(UserContent::text(""))),
})
}
"assistant" => {
let contents: Vec<_> = msg.parts.iter().filter_map(to_assistant_content).collect();
Ok(Message::Assistant {
id: None,
content: OneOrMany::many(contents)
.unwrap_or_else(|_| OneOrMany::one(AssistantContent::text(""))),
})
}
role => Err(anyhow!("Unsupported message role: {}", role)),
}
}
fn to_user_content(part: &UIMessagePart) -> Option<UserContent> {
match part {
UIMessagePart::Text(p) => Some(UserContent::text(p.text.clone())),
UIMessagePart::ToolResult(p) => Some(UserContent::ToolResult(ToolResult {
id: p.tool_call_id.clone(),
call_id: Some(p.tool_call_id.clone()),
content: OneOrMany::one(ToolResultContent::text(json_to_string(&p.result))),
})),
UIMessagePart::DynamicTool(p) => {
match &p.state {
DynamicToolState::OutputAvailable { input: _, output } => {
Some(UserContent::ToolResult(ToolResult {
id: p.tool_call_id.clone(),
call_id: Some(p.tool_call_id.clone()),
content: OneOrMany::one(ToolResultContent::text(json_to_string(output))),
}))
}
DynamicToolState::OutputError {
input: _,
error_text,
} => {
Some(UserContent::ToolResult(ToolResult {
id: p.tool_call_id.clone(),
call_id: Some(p.tool_call_id.clone()),
content: OneOrMany::one(ToolResultContent::text(error_text.clone())),
}))
}
_ => None, }
}
UIMessagePart::File(p) => {
if p.media_type.starts_with("image/") {
Some(UserContent::Image(Image {
data: DocumentSourceKind::Url(p.url.clone()),
media_type: parse_image_media_type(&p.media_type),
detail: None,
additional_params: None,
}))
} else {
None
}
}
_ => None,
}
}
fn to_assistant_content(part: &UIMessagePart) -> Option<AssistantContent> {
match part {
UIMessagePart::Text(p) => Some(AssistantContent::text(p.text.clone())),
UIMessagePart::ToolCall(p) => Some(AssistantContent::ToolCall(
ToolCall::new(
p.tool_call_id.clone(),
ToolFunction {
name: p.tool_name.clone(),
arguments: p.args.clone(),
},
)
.with_call_id(p.tool_call_id.clone()),
)),
UIMessagePart::DynamicTool(p) => {
match &p.state {
DynamicToolState::InputAvailable { input }
| DynamicToolState::OutputAvailable { input, .. } => {
Some(AssistantContent::ToolCall(
ToolCall::new(
p.tool_call_id.clone(),
ToolFunction {
name: p.tool_name.clone(),
arguments: input.clone(),
},
)
.with_call_id(p.tool_call_id.clone()),
))
}
DynamicToolState::InputStreaming { input } => {
input.as_ref().map(|i| {
AssistantContent::ToolCall(
ToolCall::new(
p.tool_call_id.clone(),
ToolFunction {
name: p.tool_name.clone(),
arguments: i.clone(),
},
)
.with_call_id(p.tool_call_id.clone()),
)
})
}
_ => None, }
}
_ => None,
}
}
fn json_to_string(value: &Value) -> String {
match value {
Value::String(s) => s.clone(),
_ => serde_json::to_string(value).unwrap_or_default(),
}
}
fn parse_image_media_type(media_type: &str) -> Option<ImageMediaType> {
match media_type {
"image/jpeg" | "image/jpg" => Some(ImageMediaType::JPEG),
"image/png" => Some(ImageMediaType::PNG),
"image/gif" => Some(ImageMediaType::GIF),
"image/webp" => Some(ImageMediaType::WEBP),
"image/heic" => Some(ImageMediaType::HEIC),
"image/heif" => Some(ImageMediaType::HEIF),
"image/svg+xml" => Some(ImageMediaType::SVG),
_ => None,
}
}