use serde::{Deserialize, Serialize};
use crate::json::JsonObject;
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TextContent {
pub text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub text_signature: Option<String>,
}
impl TextContent {
#[must_use]
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
text_signature: None,
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThinkingContent {
pub thinking: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thinking_signature: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub redacted: Option<bool>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ImageContent {
pub data: String,
pub mime_type: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolCall {
pub id: String,
pub name: String,
#[serde(default)]
pub arguments: JsonObject,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thought_signature: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub namespace: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub arguments_error: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum UserContent {
#[serde(rename = "text")]
Text(TextContent),
#[serde(rename = "image")]
Image(ImageContent),
}
impl UserContent {
#[must_use]
pub fn text(text: impl Into<String>) -> Self {
Self::Text(TextContent::new(text))
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum AssistantContent {
#[serde(rename = "text")]
Text(TextContent),
#[serde(rename = "thinking")]
Thinking(ThinkingContent),
#[serde(rename = "toolCall")]
ToolCall(ToolCall),
}
impl AssistantContent {
#[must_use]
pub fn text(text: impl Into<String>) -> Self {
Self::Text(TextContent::new(text))
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum UserTextField {
Text(String),
Blocks(Vec<TypedText>),
}
impl Default for UserTextField {
fn default() -> Self {
Self::Text(String::new())
}
}
impl UserTextField {
#[must_use]
pub fn text(&self, separator: &str) -> String {
match self {
Self::Text(text) => text.clone(),
Self::Blocks(blocks) => blocks
.iter()
.map(|TypedText::Text(block)| block.text.as_str())
.collect::<Vec<_>>()
.join(separator),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum TypedText {
#[serde(rename = "text")]
Text(TextContent),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum UserContentField {
Text(String),
Blocks(Vec<UserContent>),
}
impl Default for UserContentField {
fn default() -> Self {
Self::Blocks(Vec::new())
}
}
impl UserContentField {
#[must_use]
pub fn into_blocks(self) -> Vec<UserContent> {
match self {
Self::Text(text) => vec![UserContent::text(text)],
Self::Blocks(blocks) => blocks,
}
}
#[must_use]
pub fn text(&self, separator: &str) -> String {
match self {
Self::Text(text) => text.clone(),
Self::Blocks(blocks) => blocks
.iter()
.filter_map(|block| match block {
UserContent::Text(text) => Some(text.text.as_str()),
UserContent::Image(_) => None,
})
.collect::<Vec<_>>()
.join(separator),
}
}
}
impl From<String> for UserContentField {
fn from(text: String) -> Self {
Self::Text(text)
}
}
impl From<&str> for UserContentField {
fn from(text: &str) -> Self {
Self::Text(text.to_owned())
}
}
impl From<Vec<UserContent>> for UserContentField {
fn from(blocks: Vec<UserContent>) -> Self {
Self::Blocks(blocks)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn blocks_use_pi_tags_and_names() {
let tool = AssistantContent::ToolCall(ToolCall {
id: "call_1".into(),
name: "read".into(),
arguments: json!({"path": "a"})
.as_object()
.cloned()
.unwrap_or_default(),
..ToolCall::default()
});
assert_eq!(
serde_json::to_value(&tool).ok(),
Some(json!({"type":"toolCall","id":"call_1","name":"read","arguments":{"path":"a"}}))
);
let image = UserContent::Image(ImageContent {
data: "AA==".into(),
mime_type: "image/png".into(),
});
assert_eq!(
serde_json::to_value(&image).ok(),
Some(json!({"type":"image","data":"AA==","mimeType":"image/png"}))
);
}
#[test]
fn string_content_stays_a_string() {
let field: UserContentField = serde_json::from_value(json!("hi")).unwrap_or_default();
assert_eq!(field, UserContentField::Text("hi".into()));
assert_eq!(serde_json::to_value(&field).ok(), Some(json!("hi")));
let blocks: UserContentField =
serde_json::from_value(json!([{"type":"text","text":"a"}])).unwrap_or_default();
assert_eq!(blocks.text(" "), "a");
}
}