mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-15 16:56:56 +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:
@@ -1,4 +1,5 @@
|
||||
import type { AgentStep } from '../../types/agent'
|
||||
import { useLanguage } from '../../contexts/LanguageContext'
|
||||
|
||||
interface AgentStepPanelProps {
|
||||
steps?: AgentStep[]
|
||||
@@ -13,21 +14,80 @@ const statusStyles: Record<AgentStep['status'], { dot: string; text: string }> =
|
||||
replanned: { dot: '#38bdf8', text: '#9bdcf7' },
|
||||
}
|
||||
|
||||
// Map raw backend tool names to friendly user-facing labels.
|
||||
// Backend emits `step.label` like `tool:get_positions` and we render that as
|
||||
// "📊 Checking your positions" instead of hiding it from the user.
|
||||
const toolLabels: Record<string, { zh: string; en: string; id: string }> = {
|
||||
// Read-only state
|
||||
get_positions: { zh: '📊 检查持仓', en: '📊 Checking positions', id: '📊 Memeriksa posisi' },
|
||||
get_balance: { zh: '💰 查余额', en: '💰 Reading balance', id: '💰 Membaca saldo' },
|
||||
get_trade_history: { zh: '📜 查交易历史', en: '📜 Reading trade history', id: '📜 Membaca riwayat' },
|
||||
get_decisions: { zh: '🤖 查 AI 决策记录', en: '🤖 Reading AI decisions', id: '🤖 Membaca keputusan AI' },
|
||||
get_strategies: { zh: '📋 查策略列表', en: '📋 Listing strategies', id: '📋 Daftar strategi' },
|
||||
get_candidate_coins: { zh: '🎯 查标的池', en: '🎯 Reading candidate pool', id: '🎯 Kandidat' },
|
||||
get_exchange_configs: { zh: '🔌 查交易所配置', en: '🔌 Reading exchanges', id: '🔌 Bursa' },
|
||||
get_model_configs: { zh: '🧠 查 AI 模型', en: '🧠 Reading AI models', id: '🧠 Model AI' },
|
||||
get_preferences: { zh: '⚙️ 查偏好', en: '⚙️ Reading preferences', id: '⚙️ Preferensi' },
|
||||
get_backend_logs: { zh: '🪵 查后台日志', en: '🪵 Reading logs', id: '🪵 Membaca log' },
|
||||
get_watchlist: { zh: '👁 查关注列表', en: '👁 Reading watchlist', id: '👁 Membaca watchlist' },
|
||||
|
||||
// Market data
|
||||
search_stock: { zh: '🔍 搜索股票', en: '🔍 Searching stocks', id: '🔍 Mencari saham' },
|
||||
get_market_price: { zh: '📈 查实时价格', en: '📈 Fetching price', id: '📈 Mengambil harga' },
|
||||
get_market_snapshot: { zh: '📈 查市场快照', en: '📈 Reading market snapshot', id: '📈 Snapshot pasar' },
|
||||
get_kline: { zh: '📈 查 K 线', en: '📈 Reading candlesticks', id: '📈 Membaca candlestick' },
|
||||
|
||||
// Mutating
|
||||
manage_trader: { zh: '🤖 管理 Trader', en: '🤖 Managing trader', id: '🤖 Mengelola trader' },
|
||||
manage_strategy: { zh: '📋 管理策略', en: '📋 Managing strategy', id: '📋 Mengelola strategi' },
|
||||
manage_exchange_config: { zh: '🔌 管理交易所', en: '🔌 Managing exchange', id: '🔌 Mengelola bursa' },
|
||||
manage_model_config: { zh: '🧠 管理 AI 模型', en: '🧠 Managing AI model', id: '🧠 Mengelola model' },
|
||||
manage_preferences: { zh: '⚙️ 更新偏好', en: '⚙️ Updating preferences', id: '⚙️ Memperbarui preferensi' },
|
||||
manage_watchlist: { zh: '👁 更新关注列表', en: '👁 Updating watchlist', id: '👁 Memperbarui watchlist' },
|
||||
execute_trade: { zh: '⚡ 准备下单', en: '⚡ Preparing trade', id: '⚡ Menyiapkan order' },
|
||||
}
|
||||
|
||||
function friendlyStepLabel(rawLabel: string, lang: 'zh' | 'en' | 'id'): string {
|
||||
const trimmed = rawLabel.trim()
|
||||
if (trimmed.toLowerCase().startsWith('tool:')) {
|
||||
const toolName = trimmed.slice(5).trim().toLowerCase()
|
||||
const entry = toolLabels[toolName]
|
||||
if (entry) return entry[lang]
|
||||
// Unknown tool — surface a generic but still informative label
|
||||
const generic = {
|
||||
zh: `🔧 调用 ${toolName}`,
|
||||
en: `🔧 Calling ${toolName}`,
|
||||
id: `🔧 Memanggil ${toolName}`,
|
||||
}
|
||||
return generic[lang]
|
||||
}
|
||||
return rawLabel
|
||||
}
|
||||
|
||||
export function AgentStepPanel({ steps, visible }: AgentStepPanelProps) {
|
||||
const { language } = useLanguage()
|
||||
const lang = (language === 'zh' || language === 'id' ? language : 'en') as
|
||||
| 'zh'
|
||||
| 'en'
|
||||
| 'id'
|
||||
|
||||
if (!visible || !steps || steps.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const sanitizedSteps = steps.filter((step) => {
|
||||
const label = step.label.trim().toLowerCase()
|
||||
// Drop only the internal-routing chatter (central_brain); keep tool steps —
|
||||
// they are exactly what the user wants to see ("agent is actually doing something").
|
||||
const visibleSteps = steps.filter((step) => {
|
||||
const detail = (step.detail || '').trim().toLowerCase()
|
||||
return !(label.startsWith('tool:') || detail === 'central_brain')
|
||||
return detail !== 'central_brain'
|
||||
})
|
||||
|
||||
if (sanitizedSteps.length === 0) {
|
||||
if (visibleSteps.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const liveRunHeading = lang === 'zh' ? 'AGENT 实时动作' : lang === 'id' ? 'AKSI AGENT' : 'LIVE RUN'
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -48,11 +108,12 @@ export function AgentStepPanel({ steps, visible }: AgentStepPanelProps) {
|
||||
marginBottom: 10,
|
||||
}}
|
||||
>
|
||||
Live Run
|
||||
{liveRunHeading}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{sanitizedSteps.map((step) => {
|
||||
{visibleSteps.map((step) => {
|
||||
const style = statusStyles[step.status]
|
||||
const label = friendlyStepLabel(step.label, lang)
|
||||
return (
|
||||
<div
|
||||
key={step.id}
|
||||
@@ -85,9 +146,9 @@ export function AgentStepPanel({ steps, visible }: AgentStepPanelProps) {
|
||||
fontWeight: step.status === 'running' ? 600 : 500,
|
||||
}}
|
||||
>
|
||||
{step.label}
|
||||
{label}
|
||||
</div>
|
||||
{step.detail && (
|
||||
{step.detail && step.detail.trim().toLowerCase() !== 'central_brain' && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: 11.5,
|
||||
|
||||
@@ -10,12 +10,12 @@ interface ChatMessagesProps {
|
||||
|
||||
function hasMeaningfulExecutionSteps(steps?: AgentStep[]) {
|
||||
if (!steps || steps.length === 0) return false
|
||||
// Tool steps (label "tool:get_positions" etc.) ARE meaningful — they're the
|
||||
// visible signal that the agent is actually doing something. Only drop the
|
||||
// internal routing chatter (central_brain) and pure-planning placeholders.
|
||||
return steps.some((step) => {
|
||||
const label = step.label.trim().toLowerCase()
|
||||
const detail = (step.detail || '').trim().toLowerCase()
|
||||
if (label.startsWith('tool:') || detail === 'central_brain') {
|
||||
return false
|
||||
}
|
||||
if (detail === 'central_brain') return false
|
||||
return step.status !== 'planning'
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user