conversation.go
package models
import (
"encoding/json"
"github.com/readysite/readysite/pkg/database"
)
// Conversation represents an AI chat session.
type Conversation struct {
database.Model
UserID string // User who owns this conversation
Title string // Conversation title
Context string // JSON: conversation context
}
// User returns the user who owns this conversation.
func (c *Conversation) User() (*User, error) {
return Users.Get(c.UserID)
}
// Messages returns all messages in this conversation.
func (c *Conversation) Messages() ([]*Message, error) {
return Messages.Search("WHERE ConversationID = ? ORDER BY CreatedAt", c.ID)
}
// LastMessage returns the most recent message.
func (c *Conversation) LastMessage() (*Message, error) {
return Messages.First("WHERE ConversationID = ? ORDER BY CreatedAt DESC", c.ID)
}
// MessageCount returns the number of messages in this conversation.
func (c *Conversation) MessageCount() int {
return Messages.Count("WHERE ConversationID = ?", c.ID)
}
// --- Message ---
// Message represents a chat message in a conversation.
type Message struct {
database.Model
ConversationID string // Conversation this message belongs to
Role string // "user", "assistant", "system"
Content string // Message text content
ToolCalls string // JSON: array of tool calls
Status string // "pending", "streaming", "complete", "error"
}
// IsPending returns true if the message is waiting for AI response.
func (m *Message) IsPending() bool {
return m.Status == "pending"
}
// IsStreaming returns true if the message is currently being streamed.
func (m *Message) IsStreaming() bool {
return m.Status == "streaming"
}
// Conversation returns the conversation this message belongs to.
func (m *Message) Conversation() (*Conversation, error) {
return Conversations.Get(m.ConversationID)
}
// Mutations returns all mutations triggered by this message.
func (m *Message) Mutations() ([]*Mutation, error) {
return Mutations.Search("WHERE MessageID = ? ORDER BY CreatedAt", m.ID)
}
// HasToolCalls returns true if this message has tool calls.
func (m *Message) HasToolCalls() bool {
return m.ToolCalls != "" && m.ToolCalls != "null" && m.ToolCalls != "[]"
}
// ToolCallCount returns the number of tool calls in this message.
func (m *Message) ToolCallCount() int {
if m.ToolCalls == "" {
return 0
}
var calls []map[string]any
if err := json.Unmarshal([]byte(m.ToolCalls), &calls); err != nil {
return 0
}
return len(calls)
}
// ToolCallItem represents a tool call for template rendering.
// This is a view-model projection of assist.ToolCall, excluding Arguments
// which isn't needed for display. Keep in sync with assist.ToolCall.
type ToolCallItem struct {
ID string
Name string
Result string
Error string
}
// ToolCallsList returns tool calls as a slice for template iteration.
func (m *Message) ToolCallsList() []ToolCallItem {
if m.ToolCalls == "" {
return nil
}
var calls []ToolCallItem
if err := json.Unmarshal([]byte(m.ToolCalls), &calls); err != nil {
return nil
}
return calls
}
// Files returns all files attached to this message.
func (m *Message) Files() ([]*File, error) {
mfs, err := MessageFiles.Search("WHERE MessageID = ?", m.ID)
if err != nil {
return nil, err
}
files := make([]*File, 0, len(mfs))
for _, mf := range mfs {
file, err := mf.File()
if err == nil && file != nil {
files = append(files, file)
}
}
return files, nil
}
// AttachFile attaches a file to this message.
func (m *Message) AttachFile(fileID string) error {
mf := &MessageFile{
MessageID: m.ID,
FileID: fileID,
}
_, err := MessageFiles.Insert(mf)
return err
}
// --- ConversationSummary ---
// ConversationSummary stores a compressed summary of older messages.
// This reduces token usage by replacing detailed message history with summaries.
type ConversationSummary struct {
database.Model
ConversationID string // Conversation this summary belongs to
StartMessageID string // First message included in summary
EndMessageID string // Last message included in summary
MessageCount int // Number of messages summarized
Summary string // Compressed summary text
KeyActions string // JSON array of key actions taken
}
// Conversation returns the conversation this summary belongs to.
func (s *ConversationSummary) Conversation() (*Conversation, error) {
return Conversations.Get(s.ConversationID)
}
// --- Mutation ---
// Mutation represents an AI action for undo/redo support.
type Mutation struct {
database.Model
MessageID string // Message that triggered this mutation
Action string // "create", "update", "delete"
EntityType string // "page", "endpoint", "collection", "document"
EntityID string // ID of the affected entity
BeforeState string // JSON: entity state before mutation
AfterState string // JSON: entity state after mutation
Undone bool // Whether this mutation has been undone
}
// Message returns the message that triggered this mutation.
func (m *Mutation) Message() (*Message, error) {
return Messages.Get(m.MessageID)
}