mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-06 20:41:14 +08:00
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:
@@ -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
86
agent/history.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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' },
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user