praxis_persist/models/
db_message.rs

1use serde::{Deserialize, Serialize};
2use chrono::{DateTime, Utc};
3use praxis_llm::types::FunctionCall;
4
5/// Database-agnostic message model
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct DBMessage {
8    pub id: String,
9    pub thread_id: String,
10    pub user_id: String,
11    pub role: MessageRole,
12    pub message_type: MessageType,
13    pub content: String,
14    pub tool_call_id: Option<String>,
15    pub tool_name: Option<String>,
16    pub arguments: Option<serde_json::Value>,
17    pub reasoning_id: Option<String>,
18    pub created_at: DateTime<Utc>,
19    pub duration_ms: Option<u64>,
20}
21
22impl Default for DBMessage {
23    fn default() -> Self {
24        Self {
25            id: uuid::Uuid::new_v4().to_string(),
26            thread_id: String::new(),
27            user_id: String::new(),
28            role: MessageRole::Assistant,
29            message_type: MessageType::Message,
30            content: String::new(),
31            tool_call_id: None,
32            tool_name: None,
33            arguments: None,
34            reasoning_id: None,
35            created_at: Utc::now(),
36            duration_ms: None,
37        }
38    }
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
42#[serde(rename_all = "lowercase")]
43pub enum MessageRole {
44    User,
45    Assistant,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
49#[serde(rename_all = "snake_case")]
50pub enum MessageType {
51    Message,
52    Reasoning,
53    ToolCall,
54    ToolResult,
55}
56
57// Conversion: DBMessage → praxis_llm::Message
58impl TryFrom<DBMessage> for praxis_llm::Message {
59    type Error = anyhow::Error;
60    
61    fn try_from(msg: DBMessage) -> Result<Self, Self::Error> {
62        match (msg.role, msg.message_type) {
63            (MessageRole::User, MessageType::Message) => {
64                Ok(praxis_llm::Message::Human {
65                    content: praxis_llm::Content::text(msg.content),
66                    name: None,
67                })
68            },
69            (MessageRole::Assistant, MessageType::Message) => {
70                Ok(praxis_llm::Message::AI {
71                    content: Some(praxis_llm::Content::text(msg.content)),
72                    tool_calls: None,
73                    name: None,
74                })
75            },
76            (MessageRole::Assistant, MessageType::ToolCall) => {
77                // Tool call - construct ToolCall struct
78                if let (Some(tool_call_id), Some(tool_name), Some(arguments)) = 
79                    (msg.tool_call_id, msg.tool_name, msg.arguments) {
80                    Ok(praxis_llm::Message::AI {
81                        content: None,
82                        tool_calls: Some(vec![praxis_llm::ToolCall {
83                            id: tool_call_id,
84                            tool_type: "function".to_string(),
85                            function: FunctionCall {
86                                name: tool_name,
87                                arguments: serde_json::to_string(&arguments)
88                                    .unwrap_or_else(|_| "{}".to_string()),
89                            },
90                        }]),
91                        name: None,
92                    })
93                } else {
94                    Err(anyhow::anyhow!("Invalid tool call message: missing required fields"))
95                }
96            },
97            (_, MessageType::ToolResult) => {
98                // Tool result
99                if let Some(tool_call_id) = msg.tool_call_id {
100                    Ok(praxis_llm::Message::Tool {
101                        tool_call_id,
102                        content: praxis_llm::Content::text(msg.content),
103                    })
104                } else {
105                    Err(anyhow::anyhow!("Invalid tool result message: missing tool_call_id"))
106                }
107            },
108            // Skip reasoning messages (not sent to LLM)
109            (_, MessageType::Reasoning) => {
110                Err(anyhow::anyhow!("Reasoning messages are not converted to LLM messages"))
111            },
112            // Handle other combinations that shouldn't happen
113            _ => {
114                Err(anyhow::anyhow!("Invalid message role/type combination"))
115            },
116        }
117    }
118}
119