readysite / website / models / conversation.go
5.0 KB
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)
}
← Back