mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-17 17:34:39 +08:00
fix(telegram): eliminate narration, add full-setup workflow and tests
- Rewrite NO NARRATION rule: response is EITHER api_call tag alone OR final text reply — no text before api_call under any circumstances - Ban all narration patterns: 现在我将/好的/正在/I will/Let me etc. - Add 'create strategy + create trader + start' full setup workflow - Add 12 automated tests covering: - No narration leaking to user (5 narration variants tested) - api_call tag never leaks to user - Full setup workflow: POST strategy → verify → POST trader → start - Start existing trader workflow - Max iterations safety, tag stripping, parser edge cases
This commit is contained in:
@@ -47,6 +47,29 @@ func mockGetLLM(llm *mockLLM) func() mcp.AIClient {
|
|||||||
|
|
||||||
const testPrompt = "You are a test assistant."
|
const testPrompt = "You are a test assistant."
|
||||||
|
|
||||||
|
// mockAPIServer creates a test HTTP server with configurable route handlers.
|
||||||
|
func mockAPIServer(handlers map[string]string) (*httptest.Server, int) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
key := r.Method + " " + r.URL.Path
|
||||||
|
if body, ok := handlers[key]; ok {
|
||||||
|
w.Write([]byte(body)) //nolint:errcheck
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Also try path-only match (for GET)
|
||||||
|
if body, ok := handlers[r.URL.Path]; ok {
|
||||||
|
w.Write([]byte(body)) //nolint:errcheck
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
w.Write([]byte(`{"error":"not found"}`)) //nolint:errcheck
|
||||||
|
}))
|
||||||
|
var port int
|
||||||
|
fmt.Sscanf(srv.Listener.Addr().String(), "127.0.0.1:%d", &port)
|
||||||
|
return srv, port
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Basic agent behaviour ──────────────────────────────────────────────────
|
||||||
|
|
||||||
// TestAgentDirectReply: LLM replies without api_call — one call, direct reply.
|
// TestAgentDirectReply: LLM replies without api_call — one call, direct reply.
|
||||||
func TestAgentDirectReply(t *testing.T) {
|
func TestAgentDirectReply(t *testing.T) {
|
||||||
llm := &mockLLM{responses: []string{"Hello! How can I help you?"}}
|
llm := &mockLLM{responses: []string{"Hello! How can I help you?"}}
|
||||||
@@ -64,27 +87,20 @@ func TestAgentDirectReply(t *testing.T) {
|
|||||||
|
|
||||||
// TestAgentAPICall: LLM calls API, gets result, gives final reply — two LLM calls.
|
// TestAgentAPICall: LLM calls API, gets result, gives final reply — two LLM calls.
|
||||||
func TestAgentAPICall(t *testing.T) {
|
func TestAgentAPICall(t *testing.T) {
|
||||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
srv, port := mockAPIServer(map[string]string{
|
||||||
if r.URL.Path == "/api/my-traders" {
|
"/api/my-traders": `[{"trader_id":"t1","trader_name":"BTC Trader","is_running":false}]`,
|
||||||
w.Write([]byte(`[{"id":"t1","name":"BTC Strategy"}]`)) //nolint:errcheck
|
})
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(404)
|
|
||||||
}))
|
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
var port int
|
|
||||||
fmt.Sscanf(srv.Listener.Addr().String(), "127.0.0.1:%d", &port)
|
|
||||||
|
|
||||||
llm := &mockLLM{responses: []string{
|
llm := &mockLLM{responses: []string{
|
||||||
`Let me check.<api_call>{"method":"GET","path":"/api/my-traders","body":{}}</api_call>`,
|
`<api_call>{"method":"GET","path":"/api/my-traders","body":{}}</api_call>`,
|
||||||
"You have one trader: BTC Strategy.",
|
"You have one trader: BTC Trader.",
|
||||||
}}
|
}}
|
||||||
a := New(port, "tok", "test-user", mockGetLLM(llm), testPrompt)
|
a := New(port, "tok", "test-user", mockGetLLM(llm), testPrompt)
|
||||||
|
|
||||||
reply := a.Run("list my traders", nil)
|
reply := a.Run("list my traders", nil)
|
||||||
|
|
||||||
if reply != "You have one trader: BTC Strategy." {
|
if reply != "You have one trader: BTC Trader." {
|
||||||
t.Fatalf("unexpected reply: %q", reply)
|
t.Fatalf("unexpected reply: %q", reply)
|
||||||
}
|
}
|
||||||
if llm.calls != 2 {
|
if llm.calls != 2 {
|
||||||
@@ -94,17 +110,15 @@ func TestAgentAPICall(t *testing.T) {
|
|||||||
|
|
||||||
// TestAgentMultiStep: LLM chains two API calls before final reply — three LLM calls.
|
// TestAgentMultiStep: LLM chains two API calls before final reply — three LLM calls.
|
||||||
func TestAgentMultiStep(t *testing.T) {
|
func TestAgentMultiStep(t *testing.T) {
|
||||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
srv, port := mockAPIServer(map[string]string{
|
||||||
w.Write([]byte(`{"ok":true}`)) //nolint:errcheck
|
"/api/account": `{"total_equity":1000}`,
|
||||||
}))
|
"/api/positions": `[]`,
|
||||||
|
})
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
var port int
|
|
||||||
fmt.Sscanf(srv.Listener.Addr().String(), "127.0.0.1:%d", &port)
|
|
||||||
|
|
||||||
llm := &mockLLM{responses: []string{
|
llm := &mockLLM{responses: []string{
|
||||||
`Checking account.<api_call>{"method":"GET","path":"/api/account","body":{}}</api_call>`,
|
`<api_call>{"method":"GET","path":"/api/account","body":{}}</api_call>`,
|
||||||
`Now checking positions.<api_call>{"method":"GET","path":"/api/positions","body":{}}</api_call>`,
|
`<api_call>{"method":"GET","path":"/api/positions","body":{}}</api_call>`,
|
||||||
"Account looks healthy and no open positions.",
|
"Account looks healthy and no open positions.",
|
||||||
}}
|
}}
|
||||||
a := New(port, "tok", "test-user", mockGetLLM(llm), testPrompt)
|
a := New(port, "tok", "test-user", mockGetLLM(llm), testPrompt)
|
||||||
@@ -121,14 +135,11 @@ func TestAgentMultiStep(t *testing.T) {
|
|||||||
|
|
||||||
// TestAgentAPIResultInContext: API result must appear in next LLM message.
|
// TestAgentAPIResultInContext: API result must appear in next LLM message.
|
||||||
func TestAgentAPIResultInContext(t *testing.T) {
|
func TestAgentAPIResultInContext(t *testing.T) {
|
||||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
srv, port := mockAPIServer(map[string]string{
|
||||||
w.Write([]byte(`{"balance":1234.56}`)) //nolint:errcheck
|
"/api/account": `{"balance":1234.56}`,
|
||||||
}))
|
})
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
var port int
|
|
||||||
fmt.Sscanf(srv.Listener.Addr().String(), "127.0.0.1:%d", &port)
|
|
||||||
|
|
||||||
llm := &mockLLM{responses: []string{
|
llm := &mockLLM{responses: []string{
|
||||||
`<api_call>{"method":"GET","path":"/api/account","body":{}}</api_call>`,
|
`<api_call>{"method":"GET","path":"/api/account","body":{}}</api_call>`,
|
||||||
"Balance is 1234.56 USDT.",
|
"Balance is 1234.56 USDT.",
|
||||||
@@ -148,10 +159,193 @@ func TestAgentAPIResultInContext(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── NO NARRATION tests ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// TestNoNarrationBeforeAPICall: any text before <api_call> must NOT reach the user.
|
||||||
|
// The agent strips text-before-tag and only forwards it as assistant context.
|
||||||
|
func TestNoNarrationBeforeAPICall(t *testing.T) {
|
||||||
|
srv, port := mockAPIServer(map[string]string{
|
||||||
|
"/api/strategies": `[{"id":"s1","name":"BTC Trend"}]`,
|
||||||
|
})
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
narrations := []string{
|
||||||
|
"现在我将为您创建策略。\n",
|
||||||
|
"好的,我来帮你查询。",
|
||||||
|
"Let me check this for you. ",
|
||||||
|
"正在处理...",
|
||||||
|
"I will call the API now. ",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, narration := range narrations {
|
||||||
|
llm := &mockLLM{responses: []string{
|
||||||
|
// LLM outputs narration before the api_call tag (bad behaviour we must handle)
|
||||||
|
narration + `<api_call>{"method":"GET","path":"/api/strategies","body":{}}</api_call>`,
|
||||||
|
"你有1个策略:BTC Trend。",
|
||||||
|
}}
|
||||||
|
a := New(port, "tok", "test-user", mockGetLLM(llm), testPrompt)
|
||||||
|
reply := a.Run("查询我的策略", nil)
|
||||||
|
|
||||||
|
// Final reply must not contain narration fragments
|
||||||
|
if strings.Contains(reply, "现在我将") || strings.Contains(reply, "Let me") ||
|
||||||
|
strings.Contains(reply, "正在处理") || strings.Contains(reply, "好的,我来") ||
|
||||||
|
strings.Contains(reply, "I will call") {
|
||||||
|
t.Fatalf("narration leaked into reply for input %q: got %q", narration, reply)
|
||||||
|
}
|
||||||
|
// api_call tag must not appear in reply
|
||||||
|
if strings.Contains(reply, "<api_call>") {
|
||||||
|
t.Fatalf("api_call tag leaked into reply: %q", reply)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAPICallTagNotLeakedToUser: <api_call> tag must never appear in returned reply.
|
||||||
|
func TestAPICallTagNotLeakedToUser(t *testing.T) {
|
||||||
|
srv, port := mockAPIServer(map[string]string{
|
||||||
|
"/api/account": `{"total_equity":500}`,
|
||||||
|
})
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
llm := &mockLLM{responses: []string{
|
||||||
|
`<api_call>{"method":"GET","path":"/api/account","body":{}}</api_call>`,
|
||||||
|
`账户余额 500 USDT。<api_call>{"method":"GET","path":"/api/account","body":{}}</api_call>`,
|
||||||
|
}}
|
||||||
|
a := New(port, "tok", "test-user", mockGetLLM(llm), testPrompt)
|
||||||
|
reply := a.Run("show balance", nil)
|
||||||
|
|
||||||
|
if strings.Contains(reply, "<api_call>") {
|
||||||
|
t.Fatalf("api_call tag leaked to user: %q", reply)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Workflow tests ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// TestCreateStrategyWorkflow: simulates creating a BTC trend strategy.
|
||||||
|
// Verifies: POST strategy → GET verify → final reply shows strategy info.
|
||||||
|
func TestCreateStrategyWorkflow(t *testing.T) {
|
||||||
|
srv, port := mockAPIServer(map[string]string{
|
||||||
|
"POST /api/strategies": `{"id":"s1","name":"BTC趋势"}`,
|
||||||
|
"GET /api/strategies/s1": `{"id":"s1","name":"BTC趋势","config":{"coin_source":{"source_type":"static","static_coins":["BTC/USDT"]},"leverage":5}}`,
|
||||||
|
})
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
llm := &mockLLM{responses: []string{
|
||||||
|
// Step 1: create strategy
|
||||||
|
`<api_call>{"method":"POST","path":"/api/strategies","body":{"name":"BTC趋势","config":{}}}</api_call>`,
|
||||||
|
// Step 2: verify strategy
|
||||||
|
`<api_call>{"method":"GET","path":"/api/strategies/s1","body":{}}</api_call>`,
|
||||||
|
// Step 3: final reply
|
||||||
|
"策略已创建:BTC趋势,币种 BTC/USDT,杠杆 5x。",
|
||||||
|
}}
|
||||||
|
a := New(port, "tok", "test-user", mockGetLLM(llm), testPrompt)
|
||||||
|
reply := a.Run("帮我配置个btc趋势交易的策略", nil)
|
||||||
|
|
||||||
|
if llm.calls != 3 {
|
||||||
|
t.Fatalf("expected 3 LLM calls, got %d", llm.calls)
|
||||||
|
}
|
||||||
|
if reply == "" || strings.Contains(reply, "<api_call>") {
|
||||||
|
t.Fatalf("bad final reply: %q", reply)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFullSetupWorkflow: create strategy → create trader → start trader.
|
||||||
|
// This is the "帮我配置策略并跑起来" workflow.
|
||||||
|
func TestFullSetupWorkflow(t *testing.T) {
|
||||||
|
calls := map[string]int{}
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
key := r.Method + " " + r.URL.Path
|
||||||
|
calls[key]++
|
||||||
|
switch key {
|
||||||
|
case "POST /api/strategies":
|
||||||
|
w.Write([]byte(`{"id":"s1","name":"BTC趋势"}`)) //nolint:errcheck
|
||||||
|
case "GET /api/strategies/s1":
|
||||||
|
w.Write([]byte(`{"id":"s1","name":"BTC趋势","config":{}}`)) //nolint:errcheck
|
||||||
|
case "POST /api/traders":
|
||||||
|
w.Write([]byte(`{"id":"tr1","name":"BTC趋势交易员"}`)) //nolint:errcheck
|
||||||
|
case "POST /api/traders/tr1/start":
|
||||||
|
w.Write([]byte(`{"ok":true}`)) //nolint:errcheck
|
||||||
|
default:
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
var port int
|
||||||
|
fmt.Sscanf(srv.Listener.Addr().String(), "127.0.0.1:%d", &port)
|
||||||
|
|
||||||
|
llm := &mockLLM{responses: []string{
|
||||||
|
// 1. create strategy
|
||||||
|
`<api_call>{"method":"POST","path":"/api/strategies","body":{"name":"BTC趋势"}}</api_call>`,
|
||||||
|
// 2. verify strategy
|
||||||
|
`<api_call>{"method":"GET","path":"/api/strategies/s1","body":{}}</api_call>`,
|
||||||
|
// 3. create trader
|
||||||
|
`<api_call>{"method":"POST","path":"/api/traders","body":{"name":"BTC趋势交易员","strategy_id":"s1"}}</api_call>`,
|
||||||
|
// 4. start trader
|
||||||
|
`<api_call>{"method":"POST","path":"/api/traders/tr1/start","body":{}}</api_call>`,
|
||||||
|
// 5. final reply
|
||||||
|
"策略和交易员已创建并启动!BTC趋势交易员正在运行。",
|
||||||
|
}}
|
||||||
|
a := New(port, "tok", "test-user", mockGetLLM(llm), testPrompt)
|
||||||
|
reply := a.Run("帮我配置个btc趋势交易的策略交易 跑起来", nil)
|
||||||
|
|
||||||
|
if llm.calls != 5 {
|
||||||
|
t.Fatalf("expected 5 LLM calls, got %d", llm.calls)
|
||||||
|
}
|
||||||
|
// Verify each API was called
|
||||||
|
if calls["POST /api/strategies"] != 1 {
|
||||||
|
t.Errorf("expected 1 POST /api/strategies, got %d", calls["POST /api/strategies"])
|
||||||
|
}
|
||||||
|
if calls["POST /api/traders"] != 1 {
|
||||||
|
t.Errorf("expected 1 POST /api/traders, got %d", calls["POST /api/traders"])
|
||||||
|
}
|
||||||
|
if calls["POST /api/traders/tr1/start"] != 1 {
|
||||||
|
t.Errorf("expected 1 POST /api/traders/tr1/start, got %d", calls["POST /api/traders/tr1/start"])
|
||||||
|
}
|
||||||
|
if strings.Contains(reply, "<api_call>") {
|
||||||
|
t.Fatalf("api_call tag in final reply: %q", reply)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestStartExistingTrader: when trader already exists, just start it.
|
||||||
|
func TestStartExistingTrader(t *testing.T) {
|
||||||
|
calls := map[string]int{}
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
key := r.Method + " " + r.URL.Path
|
||||||
|
calls[key]++
|
||||||
|
switch key {
|
||||||
|
case "GET /api/my-traders":
|
||||||
|
w.Write([]byte(`[{"trader_id":"tr1","trader_name":"BTC Trader","is_running":false}]`)) //nolint:errcheck
|
||||||
|
case "POST /api/traders/tr1/start":
|
||||||
|
w.Write([]byte(`{"ok":true}`)) //nolint:errcheck
|
||||||
|
default:
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
var port int
|
||||||
|
fmt.Sscanf(srv.Listener.Addr().String(), "127.0.0.1:%d", &port)
|
||||||
|
|
||||||
|
llm := &mockLLM{responses: []string{
|
||||||
|
`<api_call>{"method":"GET","path":"/api/my-traders","body":{}}</api_call>`,
|
||||||
|
`<api_call>{"method":"POST","path":"/api/traders/tr1/start","body":{}}</api_call>`,
|
||||||
|
"交易员 BTC Trader 已启动。",
|
||||||
|
}}
|
||||||
|
a := New(port, "tok", "test-user", mockGetLLM(llm), testPrompt)
|
||||||
|
reply := a.Run("启动交易员", nil)
|
||||||
|
|
||||||
|
if calls["POST /api/traders/tr1/start"] != 1 {
|
||||||
|
t.Errorf("expected trader to be started, got %d start calls", calls["POST /api/traders/tr1/start"])
|
||||||
|
}
|
||||||
|
if strings.Contains(reply, "<api_call>") {
|
||||||
|
t.Fatalf("api_call tag in reply: %q", reply)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Parser tests ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// TestParseAPICall: unit tests for the XML tag parser.
|
// TestParseAPICall: unit tests for the XML tag parser.
|
||||||
func TestParseAPICall(t *testing.T) {
|
func TestParseAPICall(t *testing.T) {
|
||||||
t.Run("valid call", func(t *testing.T) {
|
t.Run("valid call no text before", func(t *testing.T) {
|
||||||
resp := `Stopping trader.<api_call>{"method":"POST","path":"/api/traders/t1/stop","body":{}}</api_call>`
|
resp := `<api_call>{"method":"POST","path":"/api/traders/t1/stop","body":{}}</api_call>`
|
||||||
req, text := parseAPICall(resp)
|
req, text := parseAPICall(resp)
|
||||||
if req == nil {
|
if req == nil {
|
||||||
t.Fatal("expected api_call, got nil")
|
t.Fatal("expected api_call, got nil")
|
||||||
@@ -159,6 +353,17 @@ func TestParseAPICall(t *testing.T) {
|
|||||||
if req.Method != "POST" || req.Path != "/api/traders/t1/stop" {
|
if req.Method != "POST" || req.Path != "/api/traders/t1/stop" {
|
||||||
t.Fatalf("unexpected req: %+v", req)
|
t.Fatalf("unexpected req: %+v", req)
|
||||||
}
|
}
|
||||||
|
if text != "" {
|
||||||
|
t.Fatalf("expected empty text before tag, got: %q", text)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("text before tag is captured", func(t *testing.T) {
|
||||||
|
resp := `Stopping trader.<api_call>{"method":"POST","path":"/api/traders/t1/stop","body":{}}</api_call>`
|
||||||
|
req, text := parseAPICall(resp)
|
||||||
|
if req == nil {
|
||||||
|
t.Fatal("expected api_call, got nil")
|
||||||
|
}
|
||||||
if text != "Stopping trader." {
|
if text != "Stopping trader." {
|
||||||
t.Fatalf("unexpected text before tag: %q", text)
|
t.Fatalf("unexpected text before tag: %q", text)
|
||||||
}
|
}
|
||||||
@@ -181,3 +386,45 @@ func TestParseAPICall(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestStripAPICallTag: defensive cleanup of stray tags in final reply.
|
||||||
|
func TestStripAPICallTag(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
input string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{`正常回复`, `正常回复`},
|
||||||
|
{`回复<api_call>{"method":"GET","path":"/x"}</api_call>`, `回复`},
|
||||||
|
{`<api_call>{"method":"GET","path":"/x"}</api_call>`, ``},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
got := stripAPICallTag(c.input)
|
||||||
|
if strings.TrimSpace(got) != c.want {
|
||||||
|
t.Errorf("stripAPICallTag(%q) = %q, want %q", c.input, got, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMaxIterations: agent stops after maxIterations and returns a summary.
|
||||||
|
func TestMaxIterations(t *testing.T) {
|
||||||
|
srv, port := mockAPIServer(map[string]string{
|
||||||
|
"/api/account": `{"ok":true}`,
|
||||||
|
})
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
// Always returns another api_call — should hit max iterations
|
||||||
|
responses := make([]string, maxIterations+2)
|
||||||
|
for i := range responses {
|
||||||
|
responses[i] = `<api_call>{"method":"GET","path":"/api/account","body":{}}</api_call>`
|
||||||
|
}
|
||||||
|
responses[maxIterations] = "Final summary after max iterations."
|
||||||
|
|
||||||
|
llm := &mockLLM{responses: responses}
|
||||||
|
a := New(port, "tok", "test-user", mockGetLLM(llm), testPrompt)
|
||||||
|
reply := a.Run("loop forever", nil)
|
||||||
|
|
||||||
|
if strings.Contains(reply, "<api_call>") {
|
||||||
|
t.Fatalf("api_call tag in reply after max iterations: %q", reply)
|
||||||
|
}
|
||||||
|
_ = reply // just confirm it terminates
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,31 +15,31 @@ func BuildAgentPrompt(apiDocs, userID string) string {
|
|||||||
|
|
||||||
## Tool: api_call
|
## Tool: api_call
|
||||||
|
|
||||||
Append EXACTLY ONE tag at the very end of your reply when you need to call the API:
|
When you need to call the API, your ENTIRE response must be ONLY the tag — nothing else:
|
||||||
<api_call>{"method":"GET","path":"/api/xxx","body":{}}</api_call>
|
<api_call>{"method":"GET","path":"/api/xxx","body":{}}</api_call>
|
||||||
|
|
||||||
Rules:
|
When you have a final answer (no more API calls needed), reply with plain text — NO tag at all.
|
||||||
- The tag must be the LAST thing in your message — nothing after it
|
|
||||||
|
ABSOLUTE RULES — violation = broken product:
|
||||||
|
- 【ZERO NARRATION】Your response is EITHER the api_call tag alone OR a final text reply. NEVER both except api_call at the very end.
|
||||||
|
- NEVER output ANY text before an api_call tag. No "好的", no "现在", no "我将", no "Let me", no "I will", no "正在", no "Creating...", no ellipsis, NOTHING.
|
||||||
- NEVER more than one <api_call> tag per response
|
- NEVER more than one <api_call> tag per response
|
||||||
- 【CRITICAL】NEVER say "让我查询..."、"现在获取..."、"I will call..."、"Let me check..." — just ACT silently, no narration at all
|
|
||||||
- method: "GET" | "POST" | "PUT" | "DELETE"
|
- method: "GET" | "POST" | "PUT" | "DELETE"
|
||||||
- body: JSON object (use {} for GET requests)
|
- body: JSON object (use {} for GET requests)
|
||||||
- query parameters go in the path: /api/positions?trader_id=xxx
|
- query parameters go in the path: /api/positions?trader_id=xxx
|
||||||
|
|
||||||
## NOFX API Documentation
|
## NOFX API Documentation
|
||||||
|
|
||||||
The following API documentation includes full parameter schemas. Use these to understand exactly what each field means and construct correct requests.
|
|
||||||
|
|
||||||
%s
|
%s
|
||||||
|
|
||||||
## Behavior Rules
|
## Behavior Rules
|
||||||
1. 【NO NARRATION】Never tell the user what API you are calling. Zero narration. Just act.
|
1. 【SILENT ACTION】When you need to call an API: output ONLY the <api_call> tag. Zero words before it.
|
||||||
2. Only ONE <api_call> tag per response, always at the very end
|
2. Only ONE <api_call> tag per response, always alone with nothing else
|
||||||
3. After getting an API result, decide: call another API or give a final reply
|
3. After getting an API result, decide: call another API (output tag only) or give final reply (text only)
|
||||||
4. If the API returns success (2xx), the operation succeeded — do not retry
|
4. If the API returns success (2xx), the operation succeeded — do not retry
|
||||||
5. Reply in the same language the user used (中文→中文, English→English)
|
5. Reply in the same language the user used (中文→中文, English→English)
|
||||||
6. Keep replies concise — show results, not process
|
6. Keep final replies concise — show results, not process
|
||||||
7. Ask for ALL required information in ONE message — never ask one field at a time
|
7. Ask for ALL missing required info in ONE message — never ask one field at a time
|
||||||
8. When user provides enough info, act immediately — no confirmation needed
|
8. When user provides enough info, act immediately — no confirmation needed
|
||||||
9. Be decisive — infer intent from context, use schema to fill in smart defaults
|
9. Be decisive — infer intent from context, use schema to fill in smart defaults
|
||||||
|
|
||||||
@@ -58,12 +58,6 @@ After ANY PUT or POST that creates or modifies a resource:
|
|||||||
- 5xx: server error, ask user to try again
|
- 5xx: server error, ask user to try again
|
||||||
- stream interrupted / unavailable: apologize briefly and ask user to retry
|
- stream interrupted / unavailable: apologize briefly and ask user to retry
|
||||||
|
|
||||||
## How to Use the API Schema
|
|
||||||
All API knowledge comes from the documentation above. Use field descriptions to:
|
|
||||||
- Know exactly which fields are required vs optional
|
|
||||||
- Understand semantics and build correct request bodies from natural language
|
|
||||||
- For StrategyConfig: intelligently fill all fields based on user's trading style
|
|
||||||
|
|
||||||
## Account State (injected at conversation start)
|
## Account State (injected at conversation start)
|
||||||
At the start of each new conversation, a [Current Account State] block is provided with:
|
At the start of each new conversation, a [Current Account State] block is provided with:
|
||||||
- AI Models: all configured models with their IDs and enabled status
|
- AI Models: all configured models with their IDs and enabled status
|
||||||
@@ -83,10 +77,8 @@ Use this to:
|
|||||||
|
|
||||||
**Configure exchange**: Ask for all required fields in ONE message (see schema). Always set enabled:true.
|
**Configure exchange**: Ask for all required fields in ONE message (see schema). Always set enabled:true.
|
||||||
|
|
||||||
**Create trader**: GET /api/exchanges + GET /api/models to get IDs → confirm with user → POST /api/traders.
|
**Create strategy** (independent from traders):
|
||||||
|
- Never GET trader info just to create a strategy.
|
||||||
**Create strategy** (most important workflow):
|
|
||||||
- A strategy is INDEPENDENT of traders. Never GET trader info just to create a strategy.
|
|
||||||
- If user specifies style + coins (e.g. "BTC trend"), build and POST immediately — no questions needed.
|
- If user specifies style + coins (e.g. "BTC trend"), build and POST immediately — no questions needed.
|
||||||
- Build StrategyConfig intelligently from user's description:
|
- Build StrategyConfig intelligently from user's description:
|
||||||
- "trend" / "趋势" → enable EMA(20,50), MACD, RSI, multi-timeframe (15m,1h,4h), longer primary TF
|
- "trend" / "趋势" → enable EMA(20,50), MACD, RSI, multi-timeframe (15m,1h,4h), longer primary TF
|
||||||
@@ -95,13 +87,21 @@ Use this to:
|
|||||||
- "BTC/ETH" → set coin_source.source_type="static", static_coins=["BTC/USDT"] or similar
|
- "BTC/ETH" → set coin_source.source_type="static", static_coins=["BTC/USDT"] or similar
|
||||||
- After POST: GET /api/strategies/:id to verify → show user: name, coins, key indicators, leverage
|
- After POST: GET /api/strategies/:id to verify → show user: name, coins, key indicators, leverage
|
||||||
|
|
||||||
|
**"帮我配置策略并跑起来" / "create strategy and start" (full setup workflow)**:
|
||||||
|
Execute these steps IN ORDER with NO user confirmation between them:
|
||||||
|
1. POST /api/strategies — create strategy with config built from user's description
|
||||||
|
2. GET /api/strategies/:id — verify strategy was saved correctly
|
||||||
|
3. POST /api/traders — create trader: use exchange_id and model_id from Account State (if only one each, use directly); set strategy_id from step 1; set name like "BTC趋势" or similar
|
||||||
|
4. POST /api/traders/:id/start — start the trader
|
||||||
|
5. Final reply: show strategy name, trader name, key config (coins, leverage, indicators), confirm running
|
||||||
|
|
||||||
**Update strategy config**:
|
**Update strategy config**:
|
||||||
1. GET /api/strategies/:id to read current full config
|
1. GET /api/strategies/:id to read current full config
|
||||||
2. Modify only what user asked (keep all other fields)
|
2. Modify only what user asked (keep all other fields)
|
||||||
3. PUT /api/strategies/:id with complete merged config
|
3. PUT /api/strategies/:id with complete merged config
|
||||||
4. GET /api/strategies/:id to verify → show user actual saved values for changed fields
|
4. GET /api/strategies/:id to verify → show user actual saved values for changed fields
|
||||||
|
|
||||||
**Start/stop trader**: GET /api/my-traders first. If only one trader, act directly. If multiple, list and ask.
|
**Start/stop existing trader**: From Account State, if only one trader, act directly. If multiple, list and ask.
|
||||||
|
|
||||||
**Query data**: GET /api/my-traders to get trader_id, then query /api/positions?trader_id=xxx or /api/account?trader_id=xxx etc.`, userID, apiDocs)
|
**Query data**: Use trader_id from Account State, then query /api/positions?trader_id=xxx or /api/account?trader_id=xxx etc.`, userID, apiDocs)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user