use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Role {
System,
User,
Assistant,
Tool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ChatMessage {
pub role: Role,
pub content: Option<String>,
pub content_parts: Option<Vec<serde_json::Value>>,
pub tool_calls: Option<Vec<ToolCall>>,
pub tool_call_id: Option<String>,
pub name: Option<String>,
pub metadata: std::collections::BTreeMap<String, String>,
}
impl serde::Serialize for ChatMessage {
fn serialize<S: serde::Serializer>(&self, ser: S) -> std::result::Result<S::Ok, S::Error> {
use serde::ser::SerializeMap;
let mut m = ser.serialize_map(None)?;
m.serialize_entry("role", &self.role)?;
if let Some(parts) = &self.content_parts {
m.serialize_entry("content", parts)?;
} else if let Some(c) = &self.content {
m.serialize_entry("content", c)?;
}
if let Some(tc) = &self.tool_calls {
m.serialize_entry("tool_calls", tc)?;
}
if let Some(id) = &self.tool_call_id {
m.serialize_entry("tool_call_id", id)?;
}
if let Some(n) = &self.name {
m.serialize_entry("name", n)?;
}
m.end()
}
}
impl<'de> serde::Deserialize<'de> for ChatMessage {
fn deserialize<D: serde::Deserializer<'de>>(de: D) -> std::result::Result<Self, D::Error> {
#[derive(Deserialize)]
struct Raw {
role: Role,
#[serde(default)]
content: Option<serde_json::Value>,
#[serde(default)]
tool_calls: Option<Vec<ToolCall>>,
#[serde(default)]
tool_call_id: Option<String>,
#[serde(default)]
name: Option<String>,
}
let raw = Raw::deserialize(de)?;
let (content, content_parts) = match raw.content {
Some(serde_json::Value::String(s)) => (Some(s), None),
Some(serde_json::Value::Array(a)) => (None, Some(a)),
Some(serde_json::Value::Null) | None => (None, None),
Some(other) => (Some(other.to_string()), None),
};
Ok(ChatMessage {
role: raw.role,
content,
content_parts,
tool_calls: raw.tool_calls,
tool_call_id: raw.tool_call_id,
name: raw.name,
metadata: Default::default(),
})
}
}
impl ChatMessage {
pub fn system(content: impl Into<String>) -> Self {
Self::text(Role::System, content)
}
pub fn user_with_images(text: impl Into<String>, image_urls: &[String]) -> Self {
let mut parts = vec![serde_json::json!({"type": "text", "text": text.into()})];
for url in image_urls {
parts.push(serde_json::json!({"type": "image_url", "image_url": {"url": url}}));
}
ChatMessage {
role: Role::User,
content: None,
content_parts: Some(parts),
tool_calls: None,
tool_call_id: None,
name: None,
metadata: Default::default(),
}
}
pub fn user(content: impl Into<String>) -> Self {
Self::text(Role::User, content)
}
pub fn assistant(content: impl Into<String>) -> Self {
Self::text(Role::Assistant, content)
}
pub fn tool_result(
tool_call_id: impl Into<String>,
name: impl Into<String>,
content: impl Into<String>,
) -> Self {
ChatMessage {
role: Role::Tool,
content: Some(content.into()),
content_parts: None,
tool_calls: None,
tool_call_id: Some(tool_call_id.into()),
name: Some(name.into()),
metadata: Default::default(),
}
}
pub fn tool_result_with_image(
tool_call_id: impl Into<String>,
name: impl Into<String>,
notice: impl Into<String>,
data_url: impl Into<String>,
) -> Self {
ChatMessage {
role: Role::Tool,
content: None,
content_parts: Some(vec![
serde_json::json!({"type": "text", "text": notice.into()}),
serde_json::json!({"type": "image_url", "image_url": {"url": data_url.into()}}),
]),
tool_calls: None,
tool_call_id: Some(tool_call_id.into()),
name: Some(name.into()),
metadata: Default::default(),
}
}
fn text(role: Role, content: impl Into<String>) -> Self {
ChatMessage {
role,
content: Some(content.into()),
content_parts: None,
tool_calls: None,
tool_call_id: None,
name: None,
metadata: Default::default(),
}
}
pub fn tool_calls(&self) -> &[ToolCall] {
self.tool_calls.as_deref().unwrap_or(&[])
}
pub fn with_meta(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn with_metas(mut self, pairs: &[(String, String)]) -> Self {
for (k, v) in pairs {
self.metadata.insert(k.clone(), v.clone());
}
self
}
}
pub const TOOL_ERROR_METADATA_KEY: &str = "sc.tool_error";
pub const TOOL_OUTCOME_UNKNOWN_METADATA_KEY: &str = "sc.tool_outcome_unknown";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolOutcome {
KnownSuccess,
KnownError,
Unknown,
}
pub fn mark_tool_error(message: &mut ChatMessage) {
message.metadata.remove(TOOL_OUTCOME_UNKNOWN_METADATA_KEY);
message
.metadata
.insert(TOOL_ERROR_METADATA_KEY.to_string(), "true".to_string());
}
pub fn mark_tool_outcome_unknown(message: &mut ChatMessage) {
message.metadata.remove(TOOL_ERROR_METADATA_KEY);
message.metadata.insert(
TOOL_OUTCOME_UNKNOWN_METADATA_KEY.to_string(),
"true".to_string(),
);
}
pub fn is_tool_error(message: &ChatMessage) -> bool {
message
.metadata
.get(TOOL_ERROR_METADATA_KEY)
.map(String::as_str)
== Some("true")
}
pub fn tool_outcome(message: &ChatMessage) -> ToolOutcome {
if is_tool_error(message) {
ToolOutcome::KnownError
} else if message
.metadata
.get(TOOL_OUTCOME_UNKNOWN_METADATA_KEY)
.map(String::as_str)
== Some("true")
{
ToolOutcome::Unknown
} else {
ToolOutcome::KnownSuccess
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
#[serde(rename = "type", default = "default_tool_type")]
pub kind: String,
pub function: FunctionCall,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FunctionCall {
pub name: String,
pub arguments: String,
}
impl FunctionCall {
pub fn parsed_arguments(&self) -> serde_json::Result<serde_json::Value> {
let trimmed = self.arguments.trim();
if trimmed.is_empty() {
return Ok(serde_json::Value::Object(Default::default()));
}
serde_json::from_str(trimmed)
}
}
fn default_tool_type() -> String {
"function".to_string()
}