feat: add chat history MCP tools
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
This commit is contained in:
@@ -187,6 +187,7 @@ func routes(logger *slog.Logger, cfg *config.Config, info buildinfo.Info, db *st
|
|||||||
Meals: tools.NewMealsTool(db),
|
Meals: tools.NewMealsTool(db),
|
||||||
CRM: tools.NewCRMTool(db),
|
CRM: tools.NewCRMTool(db),
|
||||||
Skills: tools.NewSkillsTool(db, activeProjects),
|
Skills: tools.NewSkillsTool(db, activeProjects),
|
||||||
|
ChatHistory: tools.NewChatHistoryTool(db, activeProjects),
|
||||||
}
|
}
|
||||||
|
|
||||||
mcpHandler, err := mcpserver.New(cfg.MCP, logger, toolSet, activeProjects.Clear)
|
mcpHandler, err := mcpserver.New(cfg.MCP, logger, toolSet, activeProjects.Clear)
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ type ToolSet struct {
|
|||||||
Meals *tools.MealsTool
|
Meals *tools.MealsTool
|
||||||
CRM *tools.CRMTool
|
CRM *tools.CRMTool
|
||||||
Skills *tools.SkillsTool
|
Skills *tools.SkillsTool
|
||||||
|
ChatHistory *tools.ChatHistoryTool
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(cfg config.MCPConfig, logger *slog.Logger, toolSet ToolSet, onSessionClosed func(string)) (http.Handler, error) {
|
func New(cfg config.MCPConfig, logger *slog.Logger, toolSet ToolSet, onSessionClosed func(string)) (http.Handler, error) {
|
||||||
@@ -54,6 +55,7 @@ func New(cfg config.MCPConfig, logger *slog.Logger, toolSet ToolSet, onSessionCl
|
|||||||
registerMealTools,
|
registerMealTools,
|
||||||
registerCRMTools,
|
registerCRMTools,
|
||||||
registerSkillTools,
|
registerSkillTools,
|
||||||
|
registerChatHistoryTools,
|
||||||
} {
|
} {
|
||||||
if err := register(server, logger, toolSet); err != nil {
|
if err := register(server, logger, toolSet); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -514,3 +516,31 @@ func registerSkillTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func registerChatHistoryTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet) error {
|
||||||
|
if err := addTool(server, logger, &mcp.Tool{
|
||||||
|
Name: "save_chat_history",
|
||||||
|
Description: "Save a chat session's message history for later retrieval. Stores messages with optional title, summary, channel, agent, and project metadata.",
|
||||||
|
}, toolSet.ChatHistory.SaveChatHistory); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := addTool(server, logger, &mcp.Tool{
|
||||||
|
Name: "get_chat_history",
|
||||||
|
Description: "Retrieve a saved chat history by its UUID or session_id. Returns the full message list.",
|
||||||
|
}, toolSet.ChatHistory.GetChatHistory); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := addTool(server, logger, &mcp.Tool{
|
||||||
|
Name: "list_chat_histories",
|
||||||
|
Description: "List saved chat histories with optional filters: project, channel, agent_id, session_id, or recent days.",
|
||||||
|
}, toolSet.ChatHistory.ListChatHistories); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := addTool(server, logger, &mcp.Tool{
|
||||||
|
Name: "delete_chat_history",
|
||||||
|
Description: "Permanently delete a saved chat history by id.",
|
||||||
|
}, toolSet.ChatHistory.DeleteChatHistory); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
189
internal/store/chat_histories.go
Normal file
189
internal/store/chat_histories.go
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
178
internal/tools/chat_history.go
Normal file
178
internal/tools/chat_history.go
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||||
|
|
||||||
|
"git.warky.dev/wdevs/amcs/internal/session"
|
||||||
|
"git.warky.dev/wdevs/amcs/internal/store"
|
||||||
|
ext "git.warky.dev/wdevs/amcs/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ChatHistoryTool struct {
|
||||||
|
store *store.DB
|
||||||
|
sessions *session.ActiveProjects
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewChatHistoryTool(db *store.DB, sessions *session.ActiveProjects) *ChatHistoryTool {
|
||||||
|
return &ChatHistoryTool{store: db, sessions: sessions}
|
||||||
|
}
|
||||||
|
|
||||||
|
// save_chat_history
|
||||||
|
|
||||||
|
type SaveChatHistoryInput struct {
|
||||||
|
SessionID string `json:"session_id" jsonschema:"unique identifier for the chat session (e.g. OpenClaw session id or thread id)"`
|
||||||
|
Title string `json:"title,omitempty" jsonschema:"optional human-readable title for this conversation"`
|
||||||
|
Channel string `json:"channel,omitempty" jsonschema:"optional channel name (e.g. telegram, discord, signal)"`
|
||||||
|
AgentID string `json:"agent_id,omitempty" jsonschema:"optional agent identifier (e.g. claude, codex, main)"`
|
||||||
|
Project string `json:"project,omitempty" jsonschema:"optional project name or id; falls back to active session project"`
|
||||||
|
Messages []ext.ChatMessage `json:"messages" jsonschema:"ordered list of messages in the conversation"`
|
||||||
|
Summary string `json:"summary,omitempty" jsonschema:"optional brief summary of the conversation"`
|
||||||
|
Metadata map[string]any `json:"metadata,omitempty" jsonschema:"optional arbitrary key-value metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SaveChatHistoryOutput struct {
|
||||||
|
ChatHistory ext.ChatHistory `json:"chat_history"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ChatHistoryTool) SaveChatHistory(ctx context.Context, req *mcp.CallToolRequest, in SaveChatHistoryInput) (*mcp.CallToolResult, SaveChatHistoryOutput, error) {
|
||||||
|
if strings.TrimSpace(in.SessionID) == "" {
|
||||||
|
return nil, SaveChatHistoryOutput{}, errRequiredField("session_id")
|
||||||
|
}
|
||||||
|
if len(in.Messages) == 0 {
|
||||||
|
return nil, SaveChatHistoryOutput{}, errRequiredField("messages")
|
||||||
|
}
|
||||||
|
|
||||||
|
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, SaveChatHistoryOutput{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
h := ext.ChatHistory{
|
||||||
|
SessionID: strings.TrimSpace(in.SessionID),
|
||||||
|
Title: strings.TrimSpace(in.Title),
|
||||||
|
Channel: strings.TrimSpace(in.Channel),
|
||||||
|
AgentID: strings.TrimSpace(in.AgentID),
|
||||||
|
Messages: in.Messages,
|
||||||
|
Summary: strings.TrimSpace(in.Summary),
|
||||||
|
Metadata: in.Metadata,
|
||||||
|
}
|
||||||
|
if h.Metadata == nil {
|
||||||
|
h.Metadata = map[string]any{}
|
||||||
|
}
|
||||||
|
if project != nil {
|
||||||
|
h.ProjectID = &project.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
saved, err := t.store.SaveChatHistory(ctx, h)
|
||||||
|
if err != nil {
|
||||||
|
return nil, SaveChatHistoryOutput{}, err
|
||||||
|
}
|
||||||
|
return nil, SaveChatHistoryOutput{ChatHistory: saved}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// get_chat_history
|
||||||
|
|
||||||
|
type GetChatHistoryInput struct {
|
||||||
|
ID string `json:"id,omitempty" jsonschema:"UUID of the saved chat history"`
|
||||||
|
SessionID string `json:"session_id,omitempty" jsonschema:"original session_id — returns the most recent history for that session"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetChatHistoryOutput struct {
|
||||||
|
ChatHistory *ext.ChatHistory `json:"chat_history"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ChatHistoryTool) GetChatHistory(ctx context.Context, _ *mcp.CallToolRequest, in GetChatHistoryInput) (*mcp.CallToolResult, GetChatHistoryOutput, error) {
|
||||||
|
if in.ID == "" && in.SessionID == "" {
|
||||||
|
return nil, GetChatHistoryOutput{}, errRequiredField("id or session_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
if in.ID != "" {
|
||||||
|
id, err := uuid.Parse(in.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, GetChatHistoryOutput{}, errInvalidField("id", "invalid id", "must be a valid UUID")
|
||||||
|
}
|
||||||
|
h, found, err := t.store.GetChatHistory(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, GetChatHistoryOutput{}, err
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return nil, GetChatHistoryOutput{}, nil
|
||||||
|
}
|
||||||
|
return nil, GetChatHistoryOutput{ChatHistory: &h}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
h, found, err := t.store.GetChatHistoryBySessionID(ctx, in.SessionID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, GetChatHistoryOutput{}, err
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return nil, GetChatHistoryOutput{}, nil
|
||||||
|
}
|
||||||
|
return nil, GetChatHistoryOutput{ChatHistory: &h}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// list_chat_histories
|
||||||
|
|
||||||
|
type ListChatHistoriesInput struct {
|
||||||
|
Project string `json:"project,omitempty" jsonschema:"filter by project name or id"`
|
||||||
|
Channel string `json:"channel,omitempty" jsonschema:"filter by channel (e.g. telegram, discord)"`
|
||||||
|
AgentID string `json:"agent_id,omitempty" jsonschema:"filter by agent id"`
|
||||||
|
SessionID string `json:"session_id,omitempty" jsonschema:"filter by session_id"`
|
||||||
|
Days int `json:"days,omitempty" jsonschema:"only include histories from the last N days"`
|
||||||
|
Limit int `json:"limit,omitempty" jsonschema:"maximum number of results to return (default 20, max 200)"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListChatHistoriesOutput struct {
|
||||||
|
ChatHistories []ext.ChatHistory `json:"chat_histories"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ChatHistoryTool) ListChatHistories(ctx context.Context, req *mcp.CallToolRequest, in ListChatHistoriesInput) (*mcp.CallToolResult, ListChatHistoriesOutput, error) {
|
||||||
|
filter := store.ListChatHistoriesFilter{
|
||||||
|
Channel: strings.TrimSpace(in.Channel),
|
||||||
|
AgentID: strings.TrimSpace(in.AgentID),
|
||||||
|
SessionID: strings.TrimSpace(in.SessionID),
|
||||||
|
Days: in.Days,
|
||||||
|
Limit: in.Limit,
|
||||||
|
}
|
||||||
|
|
||||||
|
if in.Project != "" {
|
||||||
|
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ListChatHistoriesOutput{}, err
|
||||||
|
}
|
||||||
|
if project != nil {
|
||||||
|
filter.ProjectID = &project.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
histories, err := t.store.ListChatHistories(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ListChatHistoriesOutput{}, err
|
||||||
|
}
|
||||||
|
if histories == nil {
|
||||||
|
histories = []ext.ChatHistory{}
|
||||||
|
}
|
||||||
|
return nil, ListChatHistoriesOutput{ChatHistories: histories}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete_chat_history
|
||||||
|
|
||||||
|
type DeleteChatHistoryInput struct {
|
||||||
|
ID uuid.UUID `json:"id" jsonschema:"UUID of the chat history to delete"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteChatHistoryOutput struct {
|
||||||
|
Deleted bool `json:"deleted"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ChatHistoryTool) DeleteChatHistory(ctx context.Context, _ *mcp.CallToolRequest, in DeleteChatHistoryInput) (*mcp.CallToolResult, DeleteChatHistoryOutput, error) {
|
||||||
|
deleted, err := t.store.DeleteChatHistory(ctx, in.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, DeleteChatHistoryOutput{}, err
|
||||||
|
}
|
||||||
|
return nil, DeleteChatHistoryOutput{Deleted: deleted}, nil
|
||||||
|
}
|
||||||
@@ -236,3 +236,24 @@ type AgentGuardrail struct {
|
|||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Chat Histories
|
||||||
|
|
||||||
|
type ChatMessage struct {
|
||||||
|
Role string `json:"role"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChatHistory struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
SessionID string `json:"session_id"`
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
Channel string `json:"channel,omitempty"`
|
||||||
|
AgentID string `json:"agent_id,omitempty"`
|
||||||
|
ProjectID *uuid.UUID `json:"project_id,omitempty"`
|
||||||
|
Messages []ChatMessage `json:"messages"`
|
||||||
|
Summary string `json:"summary,omitempty"`
|
||||||
|
Metadata map[string]any `json:"metadata"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|||||||
26
migrations/018_chat_histories.sql
Normal file
26
migrations/018_chat_histories.sql
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
-- Migration: 018_chat_histories
|
||||||
|
-- Adds a dedicated table for saving and retrieving agent chat histories.
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS chat_histories (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
session_id TEXT NOT NULL,
|
||||||
|
title TEXT,
|
||||||
|
channel TEXT,
|
||||||
|
agent_id TEXT,
|
||||||
|
project_id UUID REFERENCES projects(id) ON DELETE SET NULL,
|
||||||
|
messages JSONB NOT NULL DEFAULT '[]',
|
||||||
|
summary TEXT,
|
||||||
|
metadata JSONB NOT NULL DEFAULT '{}',
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_chat_histories_session_id ON chat_histories(session_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_chat_histories_project_id ON chat_histories(project_id) WHERE project_id IS NOT NULL;
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_chat_histories_channel ON chat_histories(channel) WHERE channel IS NOT NULL;
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_chat_histories_agent_id ON chat_histories(agent_id) WHERE agent_id IS NOT NULL;
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_chat_histories_created_at ON chat_histories(created_at DESC);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_chat_histories_fts
|
||||||
|
ON chat_histories
|
||||||
|
USING GIN (to_tsvector('simple', coalesce(title, '') || ' ' || coalesce(summary, '')));
|
||||||
Reference in New Issue
Block a user