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)
This commit is contained in:
shinchan-zhai
2026-03-23 02:13:26 +08:00
parent a21479a358
commit c55800ab68
3 changed files with 118 additions and 3 deletions

View File

@@ -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
}

86
agent/history.go Normal file
View File

@@ -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)
}
}
}
}

View File

@@ -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' },
]