From 0f67747b0c220072200df79900eb8d074d437136 Mon Sep 17 00:00:00 2001 From: shinchan-zhai Date: Mon, 23 Mar 2026 09:28:06 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20enforce=20tool-based=20data=20only=20?= =?UTF-8?q?=E2=80=94=20Agent=20can=20no=20longer=20fabricate=20prices?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - System prompt now has absolute rule: no tool result = no price - Pre-market overview must call search_stock for each major stock - Index futures explicitly marked as unsupported - Training data prices explicitly declared outdated and forbidden - Applied to both CN and EN prompts --- agent/agent.go | 40 ++++++++++++++++++++++++++-------------- agent/scheduler.go | 8 ++++++++ agent/web.go | 24 +++++++++++++++++++++++- main.go | 8 ++++---- 4 files changed, 61 insertions(+), 19 deletions(-) diff --git a/agent/agent.go b/agent/agent.go index 4c3a4fab..4e11ccdc 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -285,14 +285,20 @@ func (a *Agent) buildSystemPrompt(lang string) string { %s %s -## 数据说明 +## 数据说明(极其重要,违反即失职!) - 加密货币(BTC/ETH等):交易所实时数据,标注 [Real-time] -- A股/港股/美股:通过 search_stock 工具获取**实时行情**(新浪财经数据源) -- 美股支持**盘前盘后数据**:search_stock 返回的 quote 中 ext_price/ext_change_pct/ext_time 为盘前盘后价格 -- 当返回数据包含 is_ext_hours=true 时,**必须**向用户展示盘前/盘后价格 -- 外汇:没有实时数据,需要说明 -- 对于没有实时数据的标的,严禁编造具体价格 -- **有数据就给数据,没有才说没有。诚实但不过度声明。** +- A股/港股/美股:**必须调用 search_stock 工具**获取实时行情。不调工具就没有数据。 +- 美股盘前盘后:search_stock 返回的 quote 中 ext_price/ext_change_pct/ext_time +- 外汇/指数期货:当前没有数据源,如实告知 + +### 铁律:禁止编造任何价格! +- **你的训练数据中的价格全部过时,不可使用** +- **没有通过工具获取的价格 = 你不知道 = 不能说** +- 用户问多只股票的盘前数据?→ 对每只股票调用 search_stock 工具 +- 用户问"盘前概览"?→ 调用 search_stock 查主要股票(AAPL、TSLA、NVDA、MSFT、GOOGL、AMZN、META等),用真实数据回答 +- **绝对不允许**不调工具就给出具体价格数字(如 $421.85) +- 如果某只股票 search_stock 查不到数据,就说"暂时无法获取该股票数据" +- 指数期货(纳指、标普、道琼斯期货)我们目前没有数据源,直接说"暂不支持指数期货数据" ## 工具使用 你可以调用以下工具来执行操作: @@ -340,14 +346,20 @@ func (a *Agent) buildSystemPrompt(lang string) string { %s %s -## Data Notice +## Data Notice (CRITICAL — violating this is unacceptable!) - Crypto (BTC/ETH): Exchange real-time data, marked [Real-time] -- A-shares/HK/US stocks: Real-time quotes via search_stock tool (Sina Finance data) -- US stocks include **pre-market/after-hours data**: ext_price/ext_change_pct/ext_time in quote -- When is_ext_hours=true, you MUST show pre-market/after-hours prices to the user -- Forex: No real-time data — must inform user -- NEVER fabricate prices for assets without data -- Show data when you have it. Only disclaim when you don't. +- Stocks: You MUST call search_stock tool to get real-time quotes. No tool call = no data. +- US stocks pre/after-hours: ext_price/ext_change_pct/ext_time in search_stock results +- Forex/Index futures: No data source currently — tell user honestly + +### ABSOLUTE RULE: NEVER fabricate any price! +- Your training data prices are ALL outdated and MUST NOT be used +- No tool result = you don't know = you cannot state a price +- User asks multiple stocks? → Call search_stock for EACH one +- User asks "pre-market overview"? → Call search_stock for major stocks (AAPL, TSLA, NVDA, MSFT, GOOGL, AMZN, META etc.) and use real data +- NEVER output a specific price number (like $421.85) without a tool having returned it +- If search_stock fails for a stock, say "unable to fetch data for this stock" +- Index futures (NDX, SPX, DJI futures) — we have no data source, say "index futures not supported yet" ## Tools You can call these tools to take action: diff --git a/agent/scheduler.go b/agent/scheduler.go index c5570aea..ab31e004 100644 --- a/agent/scheduler.go +++ b/agent/scheduler.go @@ -24,6 +24,7 @@ func (s *Scheduler) Start(ctx context.Context) { defer ticker.Stop() lastReport := time.Time{} lastCheck := time.Time{} + lastCleanup := time.Time{} for { select { @@ -40,6 +41,13 @@ func (s *Scheduler) Start(ctx context.Context) { s.riskCheck() lastCheck = now } + // Clean stale chat history every hour (sessions idle > 24h) + if now.Sub(lastCleanup) > 1*time.Hour { + if s.agent.history != nil { + s.agent.history.CleanOld(24 * time.Hour) + } + lastCleanup = now + } } } }() diff --git a/agent/web.go b/agent/web.go index b20bad76..b630275d 100644 --- a/agent/web.go +++ b/agent/web.go @@ -7,9 +7,16 @@ import ( "io" "log/slog" "net/http" + "regexp" "time" ) +// validSymbolRe matches only alphanumeric trading symbols (e.g. BTCUSDT, ETH-USD). +var validSymbolRe = regexp.MustCompile(`^[A-Za-z0-9\-_]{1,20}$`) + +// validIntervalRe matches only valid kline intervals (e.g. 1m, 5m, 1h, 4h, 1d, 1w). +var validIntervalRe = regexp.MustCompile(`^[0-9]{1,2}[mhHdDwWM]$`) + // WebHandler provides HTTP endpoints for the NOFXi agent. type WebHandler struct { agent *Agent @@ -36,7 +43,8 @@ func (w *WebHandler) HandleChat(rw http.ResponseWriter, r *http.Request) { UserID int64 `json:"user_id"` Lang string `json:"lang"` } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + // Limit request body to 64KB to prevent abuse + if err := json.NewDecoder(io.LimitReader(r.Body, 64*1024)).Decode(&req); err != nil { writeJSON(rw, 400, map[string]string{"error": "invalid request"}) return } @@ -70,6 +78,15 @@ func (w *WebHandler) HandleKlines(rw http.ResponseWriter, r *http.Request) { interval := r.URL.Query().Get("interval") if interval == "" { interval = "1h" } + if !validSymbolRe.MatchString(symbol) { + writeJSON(rw, 400, map[string]string{"error": "invalid symbol"}) + return + } + if !validIntervalRe.MatchString(interval) { + writeJSON(rw, 400, map[string]string{"error": "invalid interval"}) + return + } + proxyBinance(rw, fmt.Sprintf("https://fapi.binance.com/fapi/v1/klines?symbol=%s&interval=%s&limit=300", symbol, interval)) } @@ -78,6 +95,11 @@ func (w *WebHandler) HandleTicker(rw http.ResponseWriter, r *http.Request) { symbol := r.URL.Query().Get("symbol") if symbol == "" { symbol = "BTCUSDT" } + if !validSymbolRe.MatchString(symbol) { + writeJSON(rw, 400, map[string]string{"error": "invalid symbol"}) + return + } + proxyBinance(rw, fmt.Sprintf("https://fapi.binance.com/fapi/v1/ticker/24hr?symbol=%s", symbol)) } diff --git a/main.go b/main.go index 5fb55c7b..bca465e7 100644 --- a/main.go +++ b/main.go @@ -30,10 +30,10 @@ func main() { // Initialize logger logger.Init(nil) - logger.Info("╔════════════════════════════════════════════════════════════╗") - logger.Info("║ 🚀 NOFXi - AI Trading Agent ║") - logger.Info("║ Powered by NOFX Engine ║") - logger.Info("╚════════════════════════════════════════════════════════════╝") + logger.Info("╔══════════════════════════════════════════════════╗") + logger.Info("║ 🚀 NOFXi - AI Trading Agent ║") + logger.Info("║ Powered by NOFX Engine ║") + logger.Info("╚══════════════════════════════════════════════════╝") // Initialize global configuration (loaded from .env) config.Init()