gglib_core/domain/
chat.rs

1//! Chat domain types.
2//!
3//! These types represent chat conversations and messages in the domain model,
4//! independent of any infrastructure concerns.
5//!
6//! [`ConversationSettings`] captures CLI/GUI session parameters (sampling,
7//! context, tools) so conversations can be faithfully resumed.
8
9use serde::{Deserialize, Serialize};
10
11use super::agent::messages::AgentMessage;
12use super::agent::messages::AssistantContent;
13use super::agent::tool_types::ToolCall;
14
15/// A chat conversation.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct Conversation {
18    pub id: i64,
19    pub title: String,
20    pub model_id: Option<i64>,
21    pub system_prompt: Option<String>,
22    /// Session parameters captured at creation for resume.
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub settings: Option<ConversationSettings>,
25    pub created_at: String,
26    pub updated_at: String,
27}
28
29/// A chat message within a conversation.
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct Message {
32    pub id: i64,
33    pub conversation_id: i64,
34    pub role: MessageRole,
35    pub content: String,
36    pub created_at: String,
37    /// Optional JSON metadata for tool usage, etc.
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub metadata: Option<serde_json::Value>,
40}
41
42impl Message {
43    /// Convert a persisted message back into an [`AgentMessage`] for resume.
44    ///
45    /// Tool call metadata is faithfully restored from the JSON `"tool_calls"` key
46    /// (assistant messages) or `"tool_call_id"` key (tool messages).
47    #[must_use]
48    pub fn to_agent_message(&self) -> AgentMessage {
49        match self.role {
50            MessageRole::System => AgentMessage::System {
51                content: self.content.clone(),
52            },
53            MessageRole::User => AgentMessage::User {
54                content: self.content.clone(),
55            },
56            MessageRole::Assistant => {
57                let tool_calls: Vec<ToolCall> = self
58                    .metadata
59                    .as_ref()
60                    .and_then(|m| m.get("tool_calls"))
61                    .and_then(|v| serde_json::from_value(v.clone()).ok())
62                    .unwrap_or_default();
63                AgentMessage::Assistant {
64                    content: AssistantContent {
65                        text: if self.content.is_empty() {
66                            None
67                        } else {
68                            Some(self.content.clone())
69                        },
70                        tool_calls,
71                    },
72                }
73            }
74            MessageRole::Tool => {
75                let tool_call_id = self
76                    .metadata
77                    .as_ref()
78                    .and_then(|m| m.get("tool_call_id"))
79                    .and_then(|v| v.as_str())
80                    .unwrap_or("")
81                    .to_string();
82                AgentMessage::Tool {
83                    tool_call_id,
84                    content: self.content.clone(),
85                }
86            }
87        }
88    }
89}
90
91/// The role of a message sender.
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
93#[serde(rename_all = "lowercase")]
94pub enum MessageRole {
95    System,
96    User,
97    Assistant,
98    Tool,
99}
100
101impl MessageRole {
102    /// Parse a role from a string.
103    #[must_use]
104    pub fn parse(s: &str) -> Option<Self> {
105        match s {
106            "system" => Some(Self::System),
107            "user" => Some(Self::User),
108            "assistant" => Some(Self::Assistant),
109            "tool" => Some(Self::Tool),
110            _ => None,
111        }
112    }
113
114    /// Convert role to string representation.
115    #[must_use]
116    pub const fn as_str(&self) -> &'static str {
117        match self {
118            Self::System => "system",
119            Self::User => "user",
120            Self::Assistant => "assistant",
121            Self::Tool => "tool",
122        }
123    }
124}
125
126impl std::fmt::Display for MessageRole {
127    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128        write!(f, "{}", self.as_str())
129    }
130}
131
132/// Data for creating a new conversation.
133#[derive(Debug, Clone)]
134pub struct NewConversation {
135    pub title: String,
136    pub model_id: Option<i64>,
137    pub system_prompt: Option<String>,
138    /// Session parameters to persist for resume.
139    pub settings: Option<ConversationSettings>,
140}
141
142/// Data for creating a new message.
143#[derive(Debug, Clone)]
144pub struct NewMessage {
145    pub conversation_id: i64,
146    pub role: MessageRole,
147    pub content: String,
148    /// Optional JSON metadata for tool usage, etc.
149    pub metadata: Option<serde_json::Value>,
150}
151
152/// Data for updating an existing conversation.
153#[derive(Debug, Clone, Default)]
154pub struct ConversationUpdate {
155    pub title: Option<String>,
156    /// Use `Some(Some(prompt))` to set, `Some(None)` to clear, `None` to leave unchanged.
157    pub system_prompt: Option<Option<String>>,
158    /// Use `Some(Some(settings))` to set, `Some(None)` to clear, `None` to leave unchanged.
159    pub settings: Option<Option<ConversationSettings>>,
160}
161
162/// Session parameters captured at conversation creation for resume.
163///
164/// Stores sampling, context, and tool configuration so a CLI or GUI session
165/// can be faithfully restored. Serialized as a JSON column in the database.
166#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
167pub struct ConversationSettings {
168    /// Model name or identifier used for this session.
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub model_name: Option<String>,
171    /// Sampling temperature (0.0–2.0).
172    #[serde(skip_serializing_if = "Option::is_none")]
173    pub temperature: Option<f32>,
174    /// Nucleus sampling threshold (0.0–1.0).
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub top_p: Option<f32>,
177    /// Top-K sampling limit.
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub top_k: Option<i32>,
180    /// Maximum tokens per response.
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub max_tokens: Option<u32>,
183    /// Repetition penalty.
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub repeat_penalty: Option<f32>,
186    /// Context window size (numeric or "max").
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub ctx_size: Option<String>,
189    /// Whether memory locking was enabled.
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub mlock: Option<bool>,
192    /// Tool allowlist (empty = all tools).
193    #[serde(default, skip_serializing_if = "Vec::is_empty")]
194    pub tools: Vec<String>,
195    /// Per-tool timeout in milliseconds.
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub tool_timeout_ms: Option<u64>,
198    /// Maximum parallel tool calls.
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub max_parallel: Option<usize>,
201    /// Maximum agent loop iterations.
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub max_iterations: Option<usize>,
204    /// Whether tools were disabled entirely.
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub no_tools: Option<bool>,
207}