refactor(agent): replace XML api_call with native function calling

Migrate the Telegram bot agent from an XML tag hack (<api_call>) to
OpenAI-native function calling via CallWithRequestFull.

Key changes:
- mcp/interface.go: add parseMCPResponseFull to clientHooks interface
- mcp/client.go: route callWithRequestFull through hooks for overridability
- mcp/claude_client.go: override parseMCPResponseFull for Claude response
  format (tool_use blocks instead of choices[].message.tool_calls)
- telegram/agent/agent.go: rewrite Run() to use CallWithRequestFull;
  define api_request tool with JSON Schema; implement tool-call loop
  with role="tool" result messages; remove XML parsing entirely
- telegram/agent/apicall.go: remove parseAPICall (dead code)
- telegram/agent/prompt.go: simplify — remove XML format instructions,
  replace with concise api_request tool usage instructions
- telegram/agent/agent_test.go: rebuild all tests using LLMResponse
  objects; add TestNarrationStructurallyImpossible, TestOnChunkCalledWithFinalReply,
  TestToolCallIDPropagated; remove XML-specific tests

Architecture advantage: with native function calling, the LLM returns
EITHER ToolCalls OR Content — never both. Narration is now structurally
impossible at the protocol level, not just enforced by prompt rules.

All 11 agent tests pass. mcp package tests pass.
This commit is contained in:
tinkle-community
2026-03-08 17:10:07 +08:00
parent 5f47dd13db
commit 9fcf44af65
8 changed files with 472 additions and 320 deletions

View File

@@ -15,6 +15,11 @@ type AIClient interface {
// onChunk is called with the full accumulated text so far (not raw deltas).
// Returns the complete final text when done.
CallWithRequestStream(req *Request, onChunk func(string)) (string, error)
// CallWithRequestFull returns both text content and tool calls.
// Use this when the request includes Tools — the LLM may respond with
// either a plain text reply (LLMResponse.Content) or tool invocations
// (LLMResponse.ToolCalls), but not both.
CallWithRequestFull(req *Request) (*LLMResponse, error)
}
// clientHooks internal hook interface (for subclass to override specific steps)
@@ -30,5 +35,6 @@ type clientHooks interface {
setAuthHeader(reqHeaders http.Header)
marshalRequestBody(requestBody map[string]any) ([]byte, error)
parseMCPResponse(body []byte) (string, error)
parseMCPResponseFull(body []byte) (*LLMResponse, error)
isRetryableError(err error) bool
}