mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-18 01:44:38 +08:00
feat(agent): make the assistant agentic - visible tools, LLM voice, full toolset
The agent felt like an artificial idiot because the LLM almost never spoke
for itself: 14+ Go paths injected fmt.Sprintf canned replies, the frontend
filtered out tool-progress events so users saw three dots for 10-20s, the
main prompt told the LLM "be a trading partner" AND "answer only what's
asked", and the planner sliced the toolset by inferred domain so a "BTC
dropped, how much am I losing?" question couldn't see positions and market
at the same time.
- agent/central_brain.go: shouldTrustDeterministicSkillReply now always
returns false. Successful mutations (trader/strategy/model/exchange
create/update/start/stop/delete) flow through reviewTaskCompletion so the
LLM sees the real outcome JSON and writes the user-facing prose. The
trade-confirmation regex path (handleTradeConfirmation) was already
outside this code path and is unaffected.
- agent/agent.go: rewrite the Behavior section of the main system prompt.
Replace the contradictory "answer only what's asked / don't upsell" with
"lead with the direct answer, then optionally one relevant follow-up
only when (a) open risk, (b) missing config, or (c) the next step is
obvious — e.g. created, want me to start it?". Explicitly authorize
chaining ("if the user says create and start, do both this turn") and
ban "please wait / I'll get back to you" language because there is no
background job to come back from.
- agent/tools.go: plannerToolsForText always returns the full 22-tool set
(new __all__ domain). The old per-domain trimming hid manage_trader from
market questions and execute_trade from anything that didn't look like
an explicit trade — cross-domain reasoning was structurally blocked. The
compact-vs-full strategy schema switch is preserved so mutation intents
still see the full config schema.
- web/src/components/agent/{AgentStepPanel,ChatMessages}.tsx: stop
filtering tool: steps. Map raw tool names to friendly labels with emoji
("get_positions" → "📊 检查持仓") in zh/en/id. Users now see what the
agent is doing in real time instead of silence. central_brain routing
chatter still gets dropped.
- agent/planner_tools_test.go: tests updated to assert the new
full-toolset behavior and the compact-vs-full strategy schema switch.
This commit is contained in:
@@ -43,10 +43,22 @@ var (
|
||||
// agentTools returns the tools available to the LLM for autonomous action.
|
||||
func agentTools() []mcp.Tool { return cachedTools }
|
||||
|
||||
// plannerToolsForText returns the tools the LLM can call on this turn.
|
||||
//
|
||||
// Historically this filtered tools to a "domain" inferred from the user's
|
||||
// text (asking about "market" hid trader tools, etc.). The intent was to
|
||||
// keep prompts small for older models, but it made cross-domain reasoning
|
||||
// structurally impossible — e.g. "BTC dropped, how much am I losing?" needs
|
||||
// BOTH market AND position tools. Modern LLMs handle 22-tool surfaces fine,
|
||||
// and the agent-feels-blind-and-useless symptom is worse than any prompt
|
||||
// bloat. We now always expose the full toolset.
|
||||
//
|
||||
// `compactStrategy` still trims the giant strategy schema for non-mutation
|
||||
// intents (it's a 117-line nested schema; only worth showing in full when
|
||||
// the user is actually editing strategy config).
|
||||
func plannerToolsForText(text string) []mcp.Tool {
|
||||
domain := plannerToolDomainForText(text)
|
||||
compactStrategy := !looksLikeStrategyMutationIntent(text)
|
||||
names := plannerToolNamesForDomain(domain)
|
||||
names := plannerToolNamesForDomain("__all__")
|
||||
return toolsByName(names, compactStrategy)
|
||||
}
|
||||
|
||||
@@ -80,7 +92,28 @@ func plannerToolDomainForText(text string) string {
|
||||
}
|
||||
|
||||
func plannerToolNamesForDomain(domain string) []string {
|
||||
// Full toolset — exposed in every turn so the LLM can cross-domain reason.
|
||||
// The `__all__` sentinel is the canonical "give me everything" entry; older
|
||||
// domain switches are kept for callers that explicitly request a subset.
|
||||
all := []string{
|
||||
// Account / lifecycle state
|
||||
"get_preferences", "manage_preferences",
|
||||
"get_decisions", "get_backend_logs",
|
||||
"get_exchange_configs", "manage_exchange_config",
|
||||
"get_model_configs", "manage_model_config",
|
||||
"get_strategies", "manage_strategy",
|
||||
"manage_trader",
|
||||
"get_balance", "get_positions", "get_trade_history",
|
||||
"get_candidate_coins",
|
||||
"get_watchlist", "manage_watchlist",
|
||||
// Trade execution
|
||||
"execute_trade",
|
||||
// Market data
|
||||
"get_market_snapshot", "get_market_price", "get_kline", "search_stock",
|
||||
}
|
||||
switch domain {
|
||||
case "__all__", "":
|
||||
return all
|
||||
case "market":
|
||||
return []string{"get_market_snapshot", "get_market_price", "get_kline", "search_stock"}
|
||||
case "account":
|
||||
@@ -96,16 +129,7 @@ func plannerToolNamesForDomain(domain string) []string {
|
||||
case "diagnosis":
|
||||
return []string{"get_decisions", "get_backend_logs", "get_model_configs", "get_exchange_configs", "get_strategies", "manage_trader"}
|
||||
default:
|
||||
return []string{
|
||||
"get_preferences", "manage_preferences",
|
||||
"get_decisions", "get_backend_logs",
|
||||
"get_exchange_configs", "manage_exchange_config",
|
||||
"get_model_configs", "manage_model_config",
|
||||
"get_strategies", "manage_strategy",
|
||||
"manage_trader",
|
||||
"get_balance", "get_positions", "get_trade_history",
|
||||
"get_market_snapshot", "get_market_price", "get_kline", "search_stock",
|
||||
}
|
||||
return all
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user