use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
#[serde(default)]
pub struct ProviderMetadata {
pub model_id: Option<String>,
pub request_id: Option<String>,
pub timestamp: Option<String>,
#[serde(flatten)]
pub extra: Value,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
pub enum PartState {
#[serde(rename = "streaming")]
Streaming,
#[serde(rename = "done")]
#[default]
Done,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum UIMessagePart {
Text {
text: String,
#[serde(default)]
state: Option<PartState>,
#[serde(default)]
provider_metadata: Option<ProviderMetadata>,
},
Reasoning {
text: String,
#[serde(default)]
state: Option<PartState>,
#[serde(default)]
provider_metadata: Option<ProviderMetadata>,
},
File {
#[serde(rename = "mediaType")]
media_type: String,
url: String,
#[serde(default)]
filename: Option<String>,
#[serde(default)]
provider_metadata: Option<ProviderMetadata>,
},
#[serde(rename = "tool-call")]
ToolCall {
#[serde(rename = "toolCallId")]
tool_call_id: String,
#[serde(rename = "toolName")]
tool_name: String,
args: Value,
#[serde(default)]
provider_metadata: Option<ProviderMetadata>,
},
#[serde(rename = "tool-result")]
ToolResult {
#[serde(rename = "toolCallId")]
tool_call_id: String,
#[serde(rename = "toolName", default)]
tool_name: Option<String>,
result: Value,
},
#[serde(rename = "dynamic-tool")]
DynamicTool {
#[serde(rename = "toolName")]
tool_name: String,
#[serde(rename = "toolCallId")]
tool_call_id: String,
#[serde(default)]
title: Option<String>,
#[serde(rename = "providerExecuted", default)]
provider_executed: bool,
state: DynamicToolState,
#[serde(rename = "callProviderMetadata", default)]
call_provider_metadata: Option<ProviderMetadata>,
#[serde(default)]
preliminary: bool,
},
#[serde(rename = "source-url")]
SourceUrl {
#[serde(rename = "sourceId")]
source_id: String,
url: String,
#[serde(default)]
title: Option<String>,
#[serde(default)]
provider_metadata: Option<ProviderMetadata>,
},
#[serde(rename = "source-document")]
SourceDocument {
#[serde(rename = "sourceId")]
source_id: String,
#[serde(rename = "mediaType")]
media_type: String,
title: String,
#[serde(default)]
filename: Option<String>,
#[serde(default)]
provider_metadata: Option<ProviderMetadata>,
},
#[serde(rename = "step-start")]
StepStart,
#[serde(rename = "data")]
Data {
#[serde(rename = "type")]
data_type: String,
#[serde(default)]
id: Option<String>,
data: Value,
},
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "state", rename_all = "kebab-case")]
pub enum DynamicToolState {
InputStreaming {
#[serde(default)]
input: Option<Value>,
},
InputAvailable {
input: Value,
},
OutputAvailable {
input: Value,
output: Value,
},
OutputError {
#[serde(default)]
input: Option<Value>,
#[serde(rename = "errorText")]
error_text: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MediaType {
Image,
Audio,
Video,
Document,
Other,
}
impl UIMessagePart {
pub fn as_text(&self) -> Option<&str> {
match self {
UIMessagePart::Text { text, .. } => Some(text),
_ => None,
}
}
pub fn is_text(&self) -> bool {
matches!(self, UIMessagePart::Text { .. })
}
pub fn is_reasoning(&self) -> bool {
matches!(self, UIMessagePart::Reasoning { .. })
}
pub fn is_tool_call(&self) -> bool {
matches!(self, UIMessagePart::ToolCall { .. } | UIMessagePart::DynamicTool { .. })
}
pub fn is_tool_result(&self) -> bool {
matches!(self, UIMessagePart::ToolResult { .. })
}
pub fn as_file(&self) -> Option<(&str, &str, Option<&String>)> {
match self {
UIMessagePart::File {
media_type,
url,
filename,
..
} => Some((media_type, url, filename.as_ref())),
_ => None,
}
}
pub fn state(&self) -> Option<PartState> {
match self {
UIMessagePart::Text { state, .. } | UIMessagePart::Reasoning { state, .. } => *state,
_ => None,
}
}
pub fn media_type_kind(&self) -> Option<MediaType> {
match self {
UIMessagePart::File { media_type, .. }
| UIMessagePart::SourceDocument { media_type, .. } => {
if media_type.starts_with("image/") {
Some(MediaType::Image)
} else if media_type.starts_with("audio/") {
Some(MediaType::Audio)
} else if media_type.starts_with("video/") {
Some(MediaType::Video)
} else if matches!(
media_type.as_str(),
"application/pdf"
| "application/msword"
| "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
| "text/plain"
| "text/csv"
| "application/json"
) {
Some(MediaType::Document)
} else {
Some(MediaType::Other)
}
}
_ => None,
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct UIMessage {
pub id: String,
pub role: String,
#[serde(default)]
pub metadata: Option<Value>,
pub parts: Vec<UIMessagePart>,
}
impl UIMessage {
pub fn text(&self) -> String {
self.parts
.iter()
.filter_map(|p| p.as_text())
.collect::<Vec<_>>()
.join("")
}
pub fn is_user(&self) -> bool {
self.role == "user"
}
pub fn is_assistant(&self) -> bool {
self.role == "assistant"
}
pub fn is_system(&self) -> bool {
self.role == "system"
}
pub fn get_parts_by_type<F>(&self, predicate: F) -> Vec<&UIMessagePart>
where
F: Fn(&UIMessagePart) -> bool,
{
self.parts.iter().filter(|p| predicate(p)).collect()
}
pub fn has_streaming_content(&self) -> bool {
self.parts.iter().any(|p| p.state() == Some(PartState::Streaming))
}
pub fn has_tool_calls(&self) -> bool {
self.parts.iter().any(|p| p.is_tool_call())
}
pub fn has_tool_results(&self) -> bool {
self.parts.iter().any(|p| p.is_tool_result())
}
pub fn has_files(&self) -> bool {
self.parts.iter().any(|p| p.as_file().is_some())
}
}