Adds save/get/list/delete tools for persisting and retrieving agent chat histories in AMCS. Changes: - migrations/018_chat_histories.sql: new chat_histories table with indexes on session_id, project_id, channel, agent_id, created_at, and FTS over title+summary - internal/types/extensions.go: ChatMessage and ChatHistory types - internal/store/chat_histories.go: SaveChatHistory, GetChatHistory, GetChatHistoryBySessionID, ListChatHistories, DeleteChatHistory - internal/tools/chat_history.go: ChatHistoryTool with four handlers (save_chat_history, get_chat_history, list_chat_histories, delete_chat_history) - internal/mcpserver/server.go: ChatHistory field in ToolSet, registerChatHistoryTools registration function - internal/app/app.go: wire ChatHistoryTool into ToolSet
190 lines
5.1 KiB
Go
190 lines
5.1 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
ext "git.warky.dev/wdevs/amcs/internal/types"
|
|
)
|
|
|
|
func (db *DB) SaveChatHistory(ctx context.Context, h ext.ChatHistory) (ext.ChatHistory, error) {
|
|
messages, err := json.Marshal(h.Messages)
|
|
if err != nil {
|
|
return ext.ChatHistory{}, fmt.Errorf("marshal messages: %w", err)
|
|
}
|
|
meta, err := json.Marshal(h.Metadata)
|
|
if err != nil {
|
|
return ext.ChatHistory{}, fmt.Errorf("marshal metadata: %w", err)
|
|
}
|
|
|
|
row := db.pool.QueryRow(ctx, `
|
|
insert into chat_histories
|
|
(session_id, title, channel, agent_id, project_id, messages, summary, metadata)
|
|
values ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
returning id, created_at, updated_at
|
|
`,
|
|
h.SessionID, nullStr(h.Title), nullStr(h.Channel), nullStr(h.AgentID),
|
|
h.ProjectID, messages, nullStr(h.Summary), meta,
|
|
)
|
|
|
|
created := h
|
|
if err := row.Scan(&created.ID, &created.CreatedAt, &created.UpdatedAt); err != nil {
|
|
return ext.ChatHistory{}, fmt.Errorf("insert chat history: %w", err)
|
|
}
|
|
return created, nil
|
|
}
|
|
|
|
func (db *DB) GetChatHistory(ctx context.Context, id uuid.UUID) (ext.ChatHistory, bool, error) {
|
|
row := db.pool.QueryRow(ctx, `
|
|
select id, session_id, title, channel, agent_id, project_id,
|
|
messages, summary, metadata, created_at, updated_at
|
|
from chat_histories where id = $1
|
|
`, id)
|
|
h, err := scanChatHistory(row)
|
|
if err == pgx.ErrNoRows {
|
|
return ext.ChatHistory{}, false, nil
|
|
}
|
|
if err != nil {
|
|
return ext.ChatHistory{}, false, fmt.Errorf("get chat history: %w", err)
|
|
}
|
|
return h, true, nil
|
|
}
|
|
|
|
func (db *DB) GetChatHistoryBySessionID(ctx context.Context, sessionID string) (ext.ChatHistory, bool, error) {
|
|
row := db.pool.QueryRow(ctx, `
|
|
select id, session_id, title, channel, agent_id, project_id,
|
|
messages, summary, metadata, created_at, updated_at
|
|
from chat_histories
|
|
where session_id = $1
|
|
order by created_at desc
|
|
limit 1
|
|
`, sessionID)
|
|
h, err := scanChatHistory(row)
|
|
if err == pgx.ErrNoRows {
|
|
return ext.ChatHistory{}, false, nil
|
|
}
|
|
if err != nil {
|
|
return ext.ChatHistory{}, false, fmt.Errorf("get chat history by session: %w", err)
|
|
}
|
|
return h, true, nil
|
|
}
|
|
|
|
type ListChatHistoriesFilter struct {
|
|
ProjectID *uuid.UUID
|
|
Channel string
|
|
AgentID string
|
|
SessionID string
|
|
Days int
|
|
Limit int
|
|
}
|
|
|
|
func (db *DB) ListChatHistories(ctx context.Context, f ListChatHistoriesFilter) ([]ext.ChatHistory, error) {
|
|
limit := f.Limit
|
|
if limit <= 0 {
|
|
limit = 20
|
|
}
|
|
if limit > 200 {
|
|
limit = 200
|
|
}
|
|
|
|
conditions := []string{}
|
|
args := []any{}
|
|
|
|
if f.ProjectID != nil {
|
|
args = append(args, *f.ProjectID)
|
|
conditions = append(conditions, fmt.Sprintf("project_id = $%d", len(args)))
|
|
}
|
|
if f.Channel != "" {
|
|
args = append(args, f.Channel)
|
|
conditions = append(conditions, fmt.Sprintf("channel = $%d", len(args)))
|
|
}
|
|
if f.AgentID != "" {
|
|
args = append(args, f.AgentID)
|
|
conditions = append(conditions, fmt.Sprintf("agent_id = $%d", len(args)))
|
|
}
|
|
if f.SessionID != "" {
|
|
args = append(args, f.SessionID)
|
|
conditions = append(conditions, fmt.Sprintf("session_id = $%d", len(args)))
|
|
}
|
|
if f.Days > 0 {
|
|
args = append(args, time.Now().UTC().AddDate(0, 0, -f.Days))
|
|
conditions = append(conditions, fmt.Sprintf("created_at >= $%d", len(args)))
|
|
}
|
|
|
|
q := `
|
|
select id, session_id, title, channel, agent_id, project_id,
|
|
messages, summary, metadata, created_at, updated_at
|
|
from chat_histories`
|
|
if len(conditions) > 0 {
|
|
q += " where " + strings.Join(conditions, " and ")
|
|
}
|
|
args = append(args, limit)
|
|
q += fmt.Sprintf(" order by created_at desc limit $%d", len(args))
|
|
|
|
rows, err := db.pool.Query(ctx, q, args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list chat histories: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var result []ext.ChatHistory
|
|
for rows.Next() {
|
|
h, err := scanChatHistory(rows)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("scan chat history: %w", err)
|
|
}
|
|
result = append(result, h)
|
|
}
|
|
return result, rows.Err()
|
|
}
|
|
|
|
func (db *DB) DeleteChatHistory(ctx context.Context, id uuid.UUID) (bool, error) {
|
|
tag, err := db.pool.Exec(ctx, `delete from chat_histories where id = $1`, id)
|
|
if err != nil {
|
|
return false, fmt.Errorf("delete chat history: %w", err)
|
|
}
|
|
return tag.RowsAffected() > 0, nil
|
|
}
|
|
|
|
type rowScanner interface {
|
|
Scan(dest ...any) error
|
|
}
|
|
|
|
func scanChatHistory(row rowScanner) (ext.ChatHistory, error) {
|
|
var h ext.ChatHistory
|
|
var title, channel, agentID, summary *string
|
|
var messagesJSON, metaJSON []byte
|
|
|
|
if err := row.Scan(
|
|
&h.ID, &h.SessionID, &title, &channel, &agentID, &h.ProjectID,
|
|
&messagesJSON, &summary, &metaJSON, &h.CreatedAt, &h.UpdatedAt,
|
|
); err != nil {
|
|
return ext.ChatHistory{}, err
|
|
}
|
|
|
|
h.Title = strVal(title)
|
|
h.Channel = strVal(channel)
|
|
h.AgentID = strVal(agentID)
|
|
h.Summary = strVal(summary)
|
|
|
|
if err := json.Unmarshal(messagesJSON, &h.Messages); err != nil {
|
|
return ext.ChatHistory{}, fmt.Errorf("unmarshal messages: %w", err)
|
|
}
|
|
if err := json.Unmarshal(metaJSON, &h.Metadata); err != nil {
|
|
return ext.ChatHistory{}, fmt.Errorf("unmarshal metadata: %w", err)
|
|
}
|
|
if h.Messages == nil {
|
|
h.Messages = []ext.ChatMessage{}
|
|
}
|
|
if h.Metadata == nil {
|
|
h.Metadata = map[string]any{}
|
|
}
|
|
return h, nil
|
|
}
|