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
179 lines
6.1 KiB
Go
179 lines
6.1 KiB
Go
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
|
|
}
|