From c55800ab68d5f274a690c99d1619a697cff9354e Mon Sep 17 00:00:00 2001 From: shinchan-zhai Date: Mon, 23 Mar 2026 02:13:26 +0800 Subject: [PATCH] feat: add multi-turn conversation memory to Agent - Add chatHistory store (in-memory, per user, last 20 messages) - Agent now passes full conversation context to LLM via CallWithRequest - Records both user messages and assistant responses - Add /clear command to reset conversation history - Add 'Clear' quick action button in frontend - Supports natural multi-turn conversations (follow-up questions, context) --- agent/agent.go | 33 +++++++++++-- agent/history.go | 86 +++++++++++++++++++++++++++++++++ web/src/pages/AgentChatPage.tsx | 2 + 3 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 agent/history.go diff --git a/agent/agent.go b/agent/agent.go index 53cacfe8..d71cb728 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -29,6 +29,7 @@ type Agent struct { brain *Brain scheduler *Scheduler logger *slog.Logger + history *chatHistory NotifyFunc func(userID int64, text string) error } @@ -50,7 +51,7 @@ func DefaultConfig() *Config { func New(tm *manager.TraderManager, st *store.Store, cfg *Config, logger *slog.Logger) *Agent { if cfg == nil { cfg = DefaultConfig() } - return &Agent{traderManager: tm, store: st, config: cfg, logger: logger} + return &Agent{traderManager: tm, store: st, config: cfg, logger: logger, history: newChatHistory(20)} } func (a *Agent) SetAIClient(c mcp.AIClient) { a.aiClient = c } @@ -124,6 +125,13 @@ func (a *Agent) HandleMessage(ctx context.Context, userID int64, text string) (s if text == "/status" { return a.handleStatus(lang), nil } + if text == "/clear" { + a.history.Clear(userID) + if lang == "zh" { + return "🧹 对话记忆已清除。", nil + } + return "🧹 Conversation history cleared.", nil + } // EVERYTHING else → LLM with tools return a.thinkAndAct(ctx, userID, lang, text) @@ -147,13 +155,32 @@ func (a *Agent) thinkAndAct(ctx context.Context, userID int64, lang, text string userPrompt = text + "\n\n---\n[NOFXi System Context - real-time data for reference]\n" + enrichment } - // Call LLM - resp, err := a.aiClient.CallWithMessages(systemPrompt, userPrompt) + // Build messages with conversation history + messages := []mcp.Message{mcp.NewSystemMessage(systemPrompt)} + + // Add conversation history (up to last N messages) + history := a.history.Get(userID) + for _, msg := range history { + messages = append(messages, mcp.NewMessage(msg.Role, msg.Content)) + } + + // Add current user message + messages = append(messages, mcp.NewUserMessage(userPrompt)) + + // Record user message in history + a.history.Add(userID, "user", text) + + // Call LLM with full conversation context + req := &mcp.Request{Messages: messages} + resp, err := a.aiClient.CallWithRequest(req) if err != nil { a.logger.Error("LLM call failed", "error", err) return a.noAIFallback(lang, text) } + // Record assistant response in history + a.history.Add(userID, "assistant", resp) + return resp, nil } diff --git a/agent/history.go b/agent/history.go new file mode 100644 index 00000000..a0e5b2d5 --- /dev/null +++ b/agent/history.go @@ -0,0 +1,86 @@ +package agent + +import ( + "sync" + "time" +) + +// chatMessage represents a single message in conversation history. +type chatMessage struct { + Role string `json:"role"` // "user" or "assistant" + Content string `json:"content"` + Timestamp time.Time `json:"timestamp"` +} + +// chatHistory stores conversation history per user. +type chatHistory struct { + mu sync.RWMutex + sessions map[int64][]chatMessage + maxTurns int // max messages per user (user+assistant pairs) +} + +func newChatHistory(maxTurns int) *chatHistory { + if maxTurns <= 0 { + maxTurns = 20 // default: keep last 20 messages (10 turns) + } + return &chatHistory{ + sessions: make(map[int64][]chatMessage), + maxTurns: maxTurns, + } +} + +// Add appends a message to the user's history. +func (h *chatHistory) Add(userID int64, role, content string) { + h.mu.Lock() + defer h.mu.Unlock() + + h.sessions[userID] = append(h.sessions[userID], chatMessage{ + Role: role, + Content: content, + Timestamp: time.Now(), + }) + + // Trim to maxTurns + msgs := h.sessions[userID] + if len(msgs) > h.maxTurns { + h.sessions[userID] = msgs[len(msgs)-h.maxTurns:] + } +} + +// Get returns the conversation history for a user. +func (h *chatHistory) Get(userID int64) []chatMessage { + h.mu.RLock() + defer h.mu.RUnlock() + + msgs := h.sessions[userID] + if msgs == nil { + return nil + } + // Return a copy + result := make([]chatMessage, len(msgs)) + copy(result, msgs) + return result +} + +// Clear resets conversation history for a user. +func (h *chatHistory) Clear(userID int64) { + h.mu.Lock() + defer h.mu.Unlock() + delete(h.sessions, userID) +} + +// CleanOld removes sessions older than the given duration. +func (h *chatHistory) CleanOld(maxAge time.Duration) { + h.mu.Lock() + defer h.mu.Unlock() + + now := time.Now() + for uid, msgs := range h.sessions { + if len(msgs) > 0 { + lastMsg := msgs[len(msgs)-1] + if now.Sub(lastMsg.Timestamp) > maxAge { + delete(h.sessions, uid) + } + } + } +} diff --git a/web/src/pages/AgentChatPage.tsx b/web/src/pages/AgentChatPage.tsx index 1b0912fb..e5aa0fbc 100644 --- a/web/src/pages/AgentChatPage.tsx +++ b/web/src/pages/AgentChatPage.tsx @@ -238,6 +238,7 @@ export function AgentChatPage() { { label: '💼 持仓', cmd: '/positions' }, { label: '💰 余额', cmd: '/balance' }, { label: '📋 Traders', cmd: '/traders' }, + { label: '🧹 清除记忆', cmd: '/clear' }, { label: '❓ 帮助', cmd: '/help' }, ] : [ @@ -246,6 +247,7 @@ export function AgentChatPage() { { label: '💼 Positions', cmd: '/positions' }, { label: '💰 Balance', cmd: '/balance' }, { label: '📋 Traders', cmd: '/traders' }, + { label: '🧹 Clear', cmd: '/clear' }, { label: '❓ Help', cmd: '/help' }, ]