use crate::types::common::{LettaId, Timestamp};
use bon::Builder;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MessageRole {
User,
Assistant,
System,
Tool,
Function,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum MessageContentVariant {
String(String),
Items(Vec<MessageContent>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum MessageContentItem {
Text {
text: String,
},
Image {
source: ImageContentSource,
},
ToolCall {
#[serde(flatten)]
tool_call: ToolCall,
},
ToolReturn {
#[serde(flatten)]
tool_return: ToolReturn,
},
Reasoning {
reasoning: String,
},
OmittedReasoning {
message: String,
},
RedactedReasoning {
redacted_chars: u32,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum MessageContent {
Text {
text: String,
},
Image {
source: ImageContentSource,
},
ToolCall {
#[serde(flatten)]
tool_call: ToolCall,
},
ToolReturn {
#[serde(flatten)]
tool_return: ToolReturn,
},
Reasoning {
reasoning: String,
},
OmittedReasoning {
message: String,
},
RedactedReasoning {
redacted_chars: u32,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImageUrl {
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ImageContentSource {
Base64 {
media_type: String,
data: String,
#[serde(skip_serializing_if = "Option::is_none")]
detail: Option<String>,
},
Letta {
id: String,
},
Url {
url: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall {
pub name: String,
pub arguments: String,
pub tool_call_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MessageToolCall {
pub id: String,
pub function: MessageToolCallFunction,
#[serde(rename = "type")]
pub tool_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MessageToolCallFunction {
pub name: String,
pub arguments: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MessageToolReturn {
pub status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub stdout: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stderr: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCallFunction {
pub name: String,
pub arguments: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolReturn {
pub status: ToolReturnStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub stdout: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stderr: Option<Vec<String>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ToolReturnStatus {
Success,
Error,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReasoningMessageSource {
ReasonerModel,
NonReasonerModel,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum HiddenReasoningMessageState {
Redacted,
Omitted,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<LettaId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub organization_id: Option<LettaId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub agent_id: Option<LettaId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
pub role: MessageRole,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<Vec<MessageContentItem>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<MessageToolCall>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub step_id: Option<LettaId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub otid: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_returns: Option<Vec<MessageToolReturn>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub group_id: Option<LettaId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sender_id: Option<LettaId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub batch_item_id: Option<LettaId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_by_id: Option<LettaId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_updated_by_id: Option<LettaId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_at: Option<Timestamp>,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_at: Option<Timestamp>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateMessageRequest {
pub role: MessageRole,
pub content: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCall>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ListMessagesParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub before: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub after: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_tool_calls: Option<bool>,
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn test_message_serialization() {
let message = Message {
id: Some(LettaId::from_str("message-550e8400-e29b-41d4-a716-446655440000").unwrap()),
organization_id: None,
agent_id: Some(
LettaId::from_str("agent-550e8400-e29b-41d4-a716-446655440001").unwrap(),
),
role: MessageRole::User,
content: Some(vec![MessageContentItem::Text {
text: "Hello, world!".to_string(),
}]),
name: None,
tool_calls: None,
tool_call_id: None,
model: None,
step_id: None,
otid: None,
tool_returns: None,
group_id: None,
sender_id: None,
batch_item_id: None,
created_by_id: None,
last_updated_by_id: None,
created_at: Some(chrono::Utc::now()),
updated_at: None,
};
let json = serde_json::to_string(&message).unwrap();
let deserialized: Message = serde_json::from_str(&json).unwrap();
assert_eq!(message.role, deserialized.role);
}
#[test]
fn test_message_content_variants() {
let text_content = MessageContentItem::Text {
text: "Hello".to_string(),
};
let json = serde_json::to_string(&text_content).unwrap();
assert!(json.contains("\"type\":\"text\""));
let tool_call = MessageContentItem::ToolCall {
tool_call: ToolCall {
name: "get_weather".to_string(),
arguments: r#"{"location": "Seattle"}"#.to_string(),
tool_call_id: "call-123".to_string(),
},
};
let json = serde_json::to_string(&tool_call).unwrap();
assert!(json.contains("\"type\":\"tool_call\""));
}
#[test]
fn test_message_role_serialization() {
assert_eq!(
serde_json::to_string(&MessageRole::User).unwrap(),
"\"user\""
);
assert_eq!(
serde_json::to_string(&MessageRole::Assistant).unwrap(),
"\"assistant\""
);
assert_eq!(
serde_json::to_string(&MessageRole::Tool).unwrap(),
"\"tool\""
);
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MessageType {
#[serde(rename = "system_message")]
SystemMessage,
#[serde(rename = "user_message")]
UserMessage,
#[serde(rename = "assistant_message")]
AssistantMessage,
#[serde(rename = "reasoning_message")]
ReasoningMessage,
#[serde(rename = "hidden_reasoning_message")]
HiddenReasoningMessage,
#[serde(rename = "tool_call_message")]
ToolCallMessage,
#[serde(rename = "tool_return_message")]
ToolReturnMessage,
#[serde(rename = "stop_reason")]
StopReason,
#[serde(rename = "usage_statistics")]
UsageStatistics,
#[serde(other)]
Other,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "message_type", rename_all = "snake_case")]
pub enum LettaMessageUnion {
SystemMessage(SystemMessage),
UserMessage(UserMessage),
AssistantMessage(AssistantMessage),
ReasoningMessage(ReasoningMessage),
HiddenReasoningMessage(HiddenReasoningMessage),
ToolCallMessage(ToolCallMessage),
ToolReturnMessage(ToolReturnMessage),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemMessage {
pub id: LettaId,
pub date: Timestamp,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub otid: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sender_id: Option<LettaId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub step_id: Option<LettaId>,
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserMessage {
pub id: LettaId,
pub date: Timestamp,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub otid: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sender_id: Option<LettaId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub step_id: Option<LettaId>,
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssistantMessage {
pub id: LettaId,
pub date: Timestamp,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub otid: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sender_id: Option<LettaId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub step_id: Option<LettaId>,
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReasoningMessage {
pub id: LettaId,
pub date: Timestamp,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub otid: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sender_id: Option<LettaId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub step_id: Option<LettaId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<ReasoningMessageSource>,
pub reasoning: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub signature: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HiddenReasoningMessage {
pub id: LettaId,
pub date: Timestamp,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub otid: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sender_id: Option<LettaId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub step_id: Option<LettaId>,
pub state: HiddenReasoningMessageState,
#[serde(skip_serializing_if = "Option::is_none")]
pub hidden_reasoning: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCallMessage {
pub id: LettaId,
pub date: Timestamp,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub otid: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sender_id: Option<LettaId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub step_id: Option<LettaId>,
pub tool_call: ToolCall,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolReturnMessage {
pub id: LettaId,
pub date: Timestamp,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub otid: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sender_id: Option<LettaId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub step_id: Option<LettaId>,
pub tool_return: String,
pub status: ToolReturnStatus,
pub tool_call_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub stdout: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stderr: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
pub struct MessageCreate {
pub role: MessageRole,
pub content: MessageCreateContent,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub otid: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sender_id: Option<LettaId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub batch_item_id: Option<LettaId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub group_id: Option<LettaId>,
}
impl Default for MessageCreate {
fn default() -> Self {
Self {
role: MessageRole::User,
content: MessageCreateContent::String(String::new()),
name: None,
otid: None,
sender_id: None,
batch_item_id: None,
group_id: None,
}
}
}
impl MessageCreate {
pub fn user(content: impl Into<String>) -> Self {
Self {
role: MessageRole::User,
content: MessageCreateContent::String(content.into()),
..Default::default()
}
}
pub fn assistant(content: impl Into<String>) -> Self {
Self {
role: MessageRole::Assistant,
content: MessageCreateContent::String(content.into()),
name: None,
otid: None,
sender_id: None,
batch_item_id: None,
group_id: None,
}
}
pub fn system(content: impl Into<String>) -> Self {
Self {
role: MessageRole::System,
content: MessageCreateContent::String(content.into()),
name: None,
otid: None,
sender_id: None,
batch_item_id: None,
group_id: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum MessageCreateContent {
String(String),
ContentParts(Vec<ContentPart>),
}
impl From<String> for MessageCreateContent {
fn from(s: String) -> Self {
Self::String(s)
}
}
impl From<&str> for MessageCreateContent {
fn from(s: &str) -> Self {
Self::String(s.to_string())
}
}
impl From<Vec<String>> for MessageCreateContent {
fn from(parts: Vec<String>) -> Self {
Self::ContentParts(
parts
.into_iter()
.map(|text| ContentPart::Text(TextContent { text }))
.collect(),
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentPart {
Text(TextContent),
Image(ImageContent),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TextContent {
pub text: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImageContent {
pub image_url: ImageUrl,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LettaResponse {
pub messages: Vec<LettaMessageUnion>,
pub stop_reason: LettaStopReason,
pub usage: LettaUsageStatistics,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StopReasonType {
EndTurn,
Error,
InvalidToolCall,
MaxSteps,
NoToolCall,
ToolRule,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LettaStopReason {
pub stop_reason: StopReasonType,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LettaUsageStatistics {
#[serde(skip_serializing_if = "Option::is_none")]
pub completion_tokens: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt_tokens: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub total_tokens: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub step_count: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub steps_messages: Option<Vec<Vec<Message>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub run_ids: Option<Vec<LettaId>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, Builder)]
pub struct CreateMessagesRequest {
pub messages: Vec<MessageCreate>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_steps: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub use_assistant_message: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub assistant_message_tool_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub assistant_message_tool_kwargs: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_return_message_types: Option<Vec<MessageType>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ListMessagesRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub after: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub before: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub group_id: Option<LettaId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub use_assistant_message: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub assistant_message_tool_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub assistant_message_tool_kwargs: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "message_type", rename_all = "snake_case")]
pub enum UpdateMessageRequest {
#[serde(rename = "system_message")]
SystemMessage(UpdateSystemMessage),
#[serde(rename = "user_message")]
UserMessage(UpdateUserMessage),
#[serde(rename = "reasoning_message")]
ReasoningMessage(UpdateReasoningMessage),
#[serde(rename = "assistant_message")]
AssistantMessage(UpdateAssistantMessage),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateSystemMessage {
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateUserMessage {
pub content: UpdateUserMessageContent,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum UpdateUserMessageContent {
String(String),
ContentParts(Vec<ContentPart>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateReasoningMessage {
pub reasoning: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateAssistantMessage {
pub content: UpdateAssistantMessageContent,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum UpdateAssistantMessageContent {
String(String),
ContentParts(Vec<ContentPart>),
}