mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-07 04:50:57 +08:00
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:
@@ -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) {
|
||||
|
||||
@@ -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).*`),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
21
agent/web.go
21
agent/web.go
@@ -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
10
main.go
@@ -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()
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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})));
|
||||
|
||||
Reference in New Issue
Block a user