fix: NOFXi Agent real AI integration + quality improvements

Critical fixes:
1. Agent now uses NOFX's MCP client for AI calls (real models, not placeholder)
   - Gets AI client from first configured trader's MCP config
   - Added AutoTrader.GetMCPClient() export
2. /analyze uses REAL market data (market.Get → EMA/MACD/RSI/BB/OI/funding)
   then sends to AI — no more hallucinated prices
3. AI chat enriches prompts with live market data when user mentions a coin
4. Fallback: if AI unavailable, shows raw formatted market data (still useful)

Router improvements:
- Better natural language detection for Chinese
- '该不该买' '走势' '趋势' '行情' → analyze intent
- '多少钱' '赚了' '亏了' → query intent
- Fewer messages falling through to generic chat

Web UI:
- Endpoints updated to /api/agent/* namespace
- nofxi.html served at agent root (localhost:8900)

Result: AI analysis now shows real BTC price, real RSI, real EMA — not fiction.
This commit is contained in:
shinchan-zhai
2026-03-23 00:11:24 +08:00
parent f095b6655b
commit fb456a3537
6 changed files with 94 additions and 14 deletions

View File

@@ -21,6 +21,7 @@ import (
"nofx/manager"
"nofx/market"
"nofx/mcp"
"nofx/store"
)
@@ -29,6 +30,7 @@ type Agent struct {
// NOFX core (injected)
traderManager *manager.TraderManager
store *store.Store
aiClient mcp.AIClient
// Agent components
config *Config
@@ -88,6 +90,11 @@ func New(
return a
}
// SetAIClient sets the MCP AI client for LLM calls.
func (a *Agent) SetAIClient(c mcp.AIClient) {
a.aiClient = c
}
// Start starts all agent services.
@@ -339,8 +346,18 @@ func (a *Agent) handleAnalyze(ctx context.Context, L string, intent Intent) (str
// Build rich analysis prompt with real data
prompt := buildAnalysisPrompt(symbol, md, L)
// For now, format the raw market data as analysis
return fmt.Sprintf(a.msg(L, "analysis_header"), strings.TrimSuffix(symbol, "USDT")) + "\n\n" + prompt, nil
// Use AI to analyze the real market data
if a.aiClient != nil {
resp, err := a.aiClient.CallWithMessages(a.msg(L, "system_prompt"), prompt)
if err == nil && resp != "" {
return fmt.Sprintf(a.msg(L, "analysis_header"), strings.TrimSuffix(symbol, "USDT")) + "\n\n" + resp, nil
}
a.logger.Error("AI analysis failed", "error", err)
}
// Fallback: format raw data directly
header := fmt.Sprintf(a.msg(L, "analysis_header"), strings.TrimSuffix(symbol, "USDT"))
return header + "\n\n" + market.Format(md), nil
}
func buildAnalysisPrompt(symbol string, md *market.Data, L string) string {
@@ -477,12 +494,38 @@ func (a *Agent) handleStrategyCmd(L string, intent Intent) string {
}
func (a *Agent) handleChat(ctx context.Context, L string, userID int64, text string) (string, error) {
// TODO: integrate with NOFX's MCP client for AI chat
// For now, route to the existing Telegram agent or return guidance
if L == "zh" {
return "🤖 收到。AI 对话功能正在接入 NOFX 的 AI 引擎。\n\n你可以试试:\n• `/analyze BTC` — 市场分析\n• `/positions` — 查看持仓\n• `/status` — 系统状态", nil
if a.aiClient == nil {
if L == "zh" {
return "🤖 AI 模型未配置。请在 Web UI (:8080) 中创建 Trader 并配置 AI 模型。\n\n命令仍然可用:\n• `/analyze BTC` — 市场分析\n• `/positions` — 查看持仓\n• `/status` — 系统状态", nil
}
return "🤖 AI model not configured. Create a Trader in Web UI (:8080) first.\n\nCommands still work:\n• `/analyze BTC`\n• `/positions`\n• `/status`", nil
}
return "🤖 Got it. AI chat is being integrated with NOFX's AI engine.\n\nTry:\n• `/analyze BTC` — market analysis\n• `/positions` — view positions\n• `/status` — system status", nil
systemPrompt := a.msg(L, "system_prompt")
// If user seems to be asking about a specific coin, enrich with real data
enrichment := ""
for _, sym := range []string{"BTC", "ETH", "SOL", "BNB", "XRP", "DOGE"} {
if strings.Contains(strings.ToUpper(text), sym) {
md, err := market.Get(sym + "USDT")
if err == nil {
enrichment += fmt.Sprintf("\n\n[Real-time %s data]\nPrice: $%.4f | 1h: %.2f%% | 4h: %.2f%% | RSI7: %.1f | EMA20: %.4f | MACD: %.6f | Funding: %.4f%%",
sym, md.CurrentPrice, md.PriceChange1h, md.PriceChange4h, md.CurrentRSI7, md.CurrentEMA20, md.CurrentMACD, md.FundingRate*100)
}
break
}
}
userPrompt := text
if enrichment != "" {
userPrompt = text + enrichment
}
resp, err := a.aiClient.CallWithMessages(systemPrompt, userPrompt)
if err != nil {
return "", fmt.Errorf("AI: %w", err)
}
return resp, nil
}
func (a *Agent) handleSignal(sig Signal) {

View File

@@ -38,11 +38,14 @@ func NewRouter() *Router {
regexp.MustCompile(`(?i)(做多|做空|买入|卖出|开仓|平仓)\s*(.+)`),
},
queryPatterns: []*regexp.Regexp{
regexp.MustCompile(`(?i)(position|balance|pnl|profit|loss|持仓|余额|盈亏|trader|交易员)`),
regexp.MustCompile(`(?i)(position|balance|pnl|profit|loss|持仓|余额|盈亏|trader|交易员|账户|仓位|资金)`),
regexp.MustCompile(`(?i)(多少钱|赚了|亏了|收益|回报)`),
},
analyzePatterns: []*regexp.Regexp{
regexp.MustCompile(`(?i)(analyze|analysis|分析|看看)\s+(.+)`),
regexp.MustCompile(`(?i)(what.*think|怎么看|你觉得)\s*(.+)?`),
regexp.MustCompile(`(?i)(analyze|analysis|分析|看看|研究)\s+(.+)`),
regexp.MustCompile(`(?i)(what.*think|怎么看|你觉得|走势|趋势|行情)\s*(.+)?`),
regexp.MustCompile(`(?i)(该不该|适合|能不能|要不要).*(买|卖|做多|做空|入场|开仓).*`),
regexp.MustCompile(`(?i)(should\s+i|is\s+it\s+good).*(buy|sell|long|short).*`),
},
}
}

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"log/slog"
"net/http"
"os"
"time"
)
@@ -76,9 +77,27 @@ func (w *WebHandler) StartStandalone(port int) {
// Ticker
mux.HandleFunc("/api/agent/ticker", handleTicker)
// Serve nofxi.html as the root page
mux.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(rw, r)
return
}
// Try web/nofxi.html relative to executable, then working dir
paths := []string{"web/nofxi.html", "../web/nofxi.html"}
for _, p := range paths {
if _, err := os.Stat(p); err == nil {
http.ServeFile(rw, r, p)
return
}
}
rw.Header().Set("Content-Type", "text/html")
rw.Write([]byte("<h1>NOFXi Agent</h1><p>API is running. Web UI not found.</p>"))
})
go func() {
addr := fmt.Sprintf(":%d", port)
w.logger.Info("NOFXi agent web API starting", "port", port)
w.logger.Info("NOFXi agent web starting", "port", port, "url", fmt.Sprintf("http://localhost:%d", port))
if err := http.ListenAndServe(addr, mux); err != nil {
w.logger.Error("agent web server error", "error", err)
}

10
main.go
View File

@@ -145,6 +145,16 @@ func main() {
// Start NOFXi Agent (proactive intelligence layer)
nofxiAgent := nofxiagent.New(traderManager, st, nil, slog.Default())
// Try to get an AI client from existing trader configs
for _, t := range traderManager.GetAllTraders() {
if mcpClient := t.GetMCPClient(); mcpClient != nil {
nofxiAgent.SetAIClient(mcpClient)
logger.Info("🧠 NOFXi Agent using AI model from trader", "trader", t.GetName())
break
}
}
nofxiAgent.Start()
defer nofxiAgent.Stop()

View File

@@ -588,6 +588,11 @@ func (at *AutoTrader) GetStore() *store.Store {
return at.store
}
// GetMCPClient returns the AI client for NOFXi agent integration.
func (at *AutoTrader) GetMCPClient() mcp.AIClient {
return at.mcpClient
}
// calculatePnLPercentage calculates P&L percentage (based on margin, automatically considers leverage)
// Return rate = Unrealized P&L / Margin x 100%
func calculatePnLPercentage(unrealizedPnl, marginUsed float64) float64 {

View File

@@ -291,7 +291,7 @@ checkSt(); setInterval(checkSt,30000);
// === Ticker Info Bar ===
async function loadInfoBar(){
try{
const r=await fetch(API+'/api/ticker?symbol=BTCUSDT');
const r=await fetch(API+'/api/agent/ticker?symbol=BTCUSDT');
const d=await r.json();
if(!d.lastPrice) return;
const bar=document.getElementById('infoBar');
@@ -337,7 +337,7 @@ async function send(text){
addM('user',text);
typEl.classList.add('on');sBtnEl.disabled=true;inpEl.focus();
try{
const r=await fetch(API+'/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:text,user_id:1,lang:lang})});
const r=await fetch(API+'/api/agent/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:text,user_id:1,lang:lang})});
const d=await r.json();
addM('bot',d.response||d.error||'No response');
}catch(e){addM('bot','⚠️ Error: '+e.message)}
@@ -380,7 +380,7 @@ function setTF(tf){
async function loadChart(){
const sym=document.getElementById('symSel').value;
try{
const[kr,tr]=await Promise.all([fetch(`${API}/api/klines?symbol=${sym}&interval=${curTF}&limit=300`),fetch(`${API}/api/ticker?symbol=${sym}`)]);
const[kr,tr]=await Promise.all([fetch(`${API}/api/agent/klines?symbol=${sym}&interval=${curTF}&limit=300`),fetch(`${API}/api/agent/ticker?symbol=${sym}`)]);
const klines=await kr.json(), ticker=await tr.json();
if(klines?.length){
cSeries.setData(klines.map(k=>({time:k.time,open:k.open,high:k.high,low:k.low,close:k.close})));