use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};
use uuid::Uuid;
pub const EMPTY_STATE_ASSET_ID: &str = "__empty_state_asset__";
pub const DEFAULT_THREAD_TITLE: &str = "New thread";
pub const DEFAULT_SIDE_CHAT_TITLE: &str = "New side chat";
fn new_thread_id() -> String {
let now = Utc::now();
let date_part = now.format("%Y%m%d-%H%M%S").to_string();
let uuid_part = Uuid::new_v4().to_string();
format!("{}-{}", date_part, &uuid_part[..8])
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThreadMetadata {
pub id: String,
pub title: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub image_hash: String,
pub pinned_at: Option<DateTime<Utc>>,
}
impl ThreadMetadata {
pub fn new(title: String, image_hash: String) -> Self {
let now = Utc::now();
Self {
id: new_thread_id(),
title,
created_at: now,
updated_at: now,
image_hash,
pinned_at: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SideChatMetadata {
pub id: String,
pub title: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl SideChatMetadata {
pub fn new(title: String) -> Self {
let now = Utc::now();
Self {
id: new_thread_id(),
title,
created_at: now,
updated_at: now,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkspaceMetadata {
pub id: String,
pub name: String,
pub created_at: DateTime<Utc>,
pub directories: Vec<String>,
pub threads: BTreeMap<String, ThreadMetadata>,
}
impl WorkspaceMetadata {
pub fn new(name: String, directories: Vec<String>) -> Self {
Self {
id: format!("workspace-{}", Uuid::new_v4()),
name,
created_at: Utc::now(),
directories,
threads: BTreeMap::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct MessageAttachment {
pub attachment_hash: String,
pub source_path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "role", rename_all = "lowercase")]
pub enum ThreadMessage {
User {
id: String,
content: String,
timestamp: DateTime<Utc>,
attachments: Vec<MessageAttachment>,
},
Assistant {
id: String,
content: String,
timestamp: DateTime<Utc>,
citations: Vec<CitationSource>,
tool_steps: Vec<ToolStep>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CitationSource {
pub title: String,
pub url: String,
pub summary: String,
#[serde(default)]
pub favicon_url: Option<String>,
#[serde(default)]
pub favicon_base64: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolStep {
pub id: String,
pub name: String,
pub status: String,
#[serde(default)]
pub args: serde_json::Value,
#[serde(default)]
pub message: Option<String>,
#[serde(default, rename = "startedAtMs")]
pub started_at_ms: Option<u64>,
#[serde(default, rename = "endedAtMs")]
pub ended_at_ms: Option<u64>,
}
impl ThreadMessage {
fn new_id() -> String {
format!("msg-{}", Uuid::new_v4())
}
pub fn is_valid_id(id: &str) -> bool {
id.strip_prefix("msg-")
.is_some_and(|uuid| Uuid::parse_str(uuid).is_ok())
}
pub fn user_with_attachments(content: String, attachments: Vec<MessageAttachment>) -> Self {
Self::User {
id: Self::new_id(),
content,
timestamp: Utc::now(),
attachments,
}
}
pub fn id(&self) -> &str {
match self {
Self::User { id, .. } | Self::Assistant { id, .. } => id,
}
}
pub fn content(&self) -> &str {
match self {
Self::User { content, .. } | Self::Assistant { content, .. } => content,
}
}
pub fn attachments(&self) -> &[MessageAttachment] {
match self {
Self::User { attachments, .. } => attachments,
Self::Assistant { .. } => &[],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OcrRegion {
pub text: String,
#[serde(default)]
pub bbox: Vec<Vec<i32>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OcrModelAnnotation {
#[serde(default)]
pub scanned_at: Option<DateTime<Utc>>,
#[serde(default)]
pub ocr_data: Vec<OcrRegion>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum OcrAnnotationEntry {
EmptyState(Vec<OcrRegion>),
Model(OcrModelAnnotation),
}
pub type OcrAnnotations = HashMap<String, OcrAnnotationEntry>;
pub fn default_ocr_annotations() -> OcrAnnotations {
HashMap::from([(
EMPTY_STATE_ASSET_ID.to_string(),
OcrAnnotationEntry::EmptyState(Vec::new()),
)])
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ContextWindow {
pub tokens_used: u32,
#[serde(default)]
pub compacted_at: Option<DateTime<Utc>>,
#[serde(default)]
pub compacted_context: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AttachmentManifestEntry {
pub attachment_hash: String,
pub display_name: String,
pub file_type: crate::cas::AttachmentFileType,
pub file_brief: Option<String>,
pub last_mention_at: DateTime<Utc>,
}
pub type AttachmentManifest = Vec<AttachmentManifestEntry>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThreadData {
pub metadata: ThreadMetadata,
#[serde(default)]
pub messages: Vec<ThreadMessage>,
#[serde(default = "default_ocr_annotations")]
pub ocr_data: OcrAnnotations,
#[serde(default)]
pub context_window: ContextWindow,
#[serde(default)]
pub reverse_image_search: Option<crate::cas::ReverseImageSearchCache>,
pub attachment_manifest: AttachmentManifest,
pub image_tone: Option<String>,
}
impl ThreadData {
pub fn new(metadata: ThreadMetadata, initial_attachment: AttachmentManifestEntry) -> Self {
Self {
metadata,
messages: Vec::new(),
ocr_data: default_ocr_annotations(),
context_window: ContextWindow::default(),
reverse_image_search: None,
attachment_manifest: vec![initial_attachment],
image_tone: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SideChatData {
pub metadata: SideChatMetadata,
#[serde(default)]
pub messages: Vec<ThreadMessage>,
#[serde(default)]
pub context_window: ContextWindow,
pub attachment_manifest: AttachmentManifest,
}
impl SideChatData {
pub fn new(metadata: SideChatMetadata, first_message: ThreadMessage) -> Self {
Self {
metadata,
messages: vec![first_message],
context_window: ContextWindow::default(),
attachment_manifest: Vec::new(),
}
}
}