mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-10 14:27:00 +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
|
brain *Brain
|
||||||
scheduler *Scheduler
|
scheduler *Scheduler
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
|
history *chatHistory
|
||||||
NotifyFunc func(userID int64, text string) error
|
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 {
|
func New(tm *manager.TraderManager, st *store.Store, cfg *Config, logger *slog.Logger) *Agent {
|
||||||
if cfg == nil { cfg = DefaultConfig() }
|
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 }
|
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" {
|
if text == "/status" {
|
||||||
return a.handleStatus(lang), nil
|
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
|
// EVERYTHING else → LLM with tools
|
||||||
return a.thinkAndAct(ctx, userID, lang, text)
|
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
|
userPrompt = text + "\n\n---\n[NOFXi System Context - real-time data for reference]\n" + enrichment
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call LLM
|
// Build messages with conversation history
|
||||||
resp, err := a.aiClient.CallWithMessages(systemPrompt, userPrompt)
|
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 {
|
if err != nil {
|
||||||
a.logger.Error("LLM call failed", "error", err)
|
a.logger.Error("LLM call failed", "error", err)
|
||||||
return a.noAIFallback(lang, text)
|
return a.noAIFallback(lang, text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Record assistant response in history
|
||||||
|
a.history.Add(userID, "assistant", resp)
|
||||||
|
|
||||||
return resp, nil
|
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: '/positions' },
|
||||||
{ label: '💰 余额', cmd: '/balance' },
|
{ label: '💰 余额', cmd: '/balance' },
|
||||||
{ label: '📋 Traders', cmd: '/traders' },
|
{ label: '📋 Traders', cmd: '/traders' },
|
||||||
|
{ label: '🧹 清除记忆', cmd: '/clear' },
|
||||||
{ label: '❓ 帮助', cmd: '/help' },
|
{ label: '❓ 帮助', cmd: '/help' },
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
@@ -246,6 +247,7 @@ export function AgentChatPage() {
|
|||||||
{ label: '💼 Positions', cmd: '/positions' },
|
{ label: '💼 Positions', cmd: '/positions' },
|
||||||
{ label: '💰 Balance', cmd: '/balance' },
|
{ label: '💰 Balance', cmd: '/balance' },
|
||||||
{ label: '📋 Traders', cmd: '/traders' },
|
{ label: '📋 Traders', cmd: '/traders' },
|
||||||
|
{ label: '🧹 Clear', cmd: '/clear' },
|
||||||
{ label: '❓ Help', cmd: '/help' },
|
{ label: '❓ Help', cmd: '/help' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user