Files
amcs/internal/store/chat_histories.go
Hein 91239bcf4b
Some checks failed
CI / build-and-test (push) Failing after -31m12s
refactor(store,tools): migrate IDs from UUID to bigserial int64
All internal entity lookups now use bigserial primary keys (int64) while
GUIDs are retained for external/public identification. Updated store
functions (TouchProject, UpdateThoughtMetadata, AddThoughtAttachment) to
query by id instead of guid, added GetThoughtByID, changed semanticSearch
and all tool helpers to use *int64 project IDs, and updated retry/backfill
workers to use int64 thought IDs throughout.
2026-05-03 11:43:34 +02:00

203 lines
5.7 KiB
Go

package store
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/jackc/pgx/v5"
"git.warky.dev/wdevs/amcs/internal/generatedmodels"
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, guid, created_at, updated_at
`,
h.SessionID, nullStr(h.Title), nullStr(h.Channel), nullStr(h.AgentID),
h.ProjectID, messages, nullStr(h.Summary), meta,
)
created := h
var model generatedmodels.ModelPublicChatHistories
if err := row.Scan(&model.ID, &model.GUID, &model.CreatedAt, &model.UpdatedAt); err != nil {
return ext.ChatHistory{}, fmt.Errorf("insert chat history: %w", err)
}
created.ID = model.ID.Int64()
created.GUID = model.GUID.UUID()
created.CreatedAt = model.CreatedAt.Time()
created.UpdatedAt = model.UpdatedAt.Time()
return created, nil
}
func (db *DB) GetChatHistory(ctx context.Context, id int64) (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 *int64
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 int64) (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 model generatedmodels.ModelPublicChatHistories
if err := row.Scan(
&model.ID, &model.SessionID, &model.Title, &model.Channel, &model.AgentID, &model.ProjectID,
&model.Messages, &model.Summary, &model.Metadata, &model.CreatedAt, &model.UpdatedAt,
); err != nil {
return ext.ChatHistory{}, err
}
h := ext.ChatHistory{
ID: model.ID.Int64(),
GUID: model.GUID.UUID(),
SessionID: model.SessionID.String(),
Title: model.Title.String(),
Channel: model.Channel.String(),
AgentID: model.AgentID.String(),
Summary: model.Summary.String(),
CreatedAt: model.CreatedAt.Time(),
UpdatedAt: model.UpdatedAt.Time(),
}
if model.ProjectID.Valid {
id := model.ProjectID.Int64()
h.ProjectID = &id
}
if err := json.Unmarshal(model.Messages, &h.Messages); err != nil {
return ext.ChatHistory{}, fmt.Errorf("unmarshal messages: %w", err)
}
if err := json.Unmarshal(model.Metadata, &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
}