Add tool-calling module: @Tool, ToolCallingAdvisor, returnDirect, ToolContext, and Tool Search Advisor

Every test drives the real Spring AI advisor classes (ToolCallingAdvisor,
ToolSearchToolCallingAdvisor) against a hand-written ScriptedChatModel that queues
real ChatResponse objects instead of calling a live LLM -- confirmed viable because
ChatModel has exactly one abstract method, call(Prompt) (checked with javap).

Covers:
- The plain call/execute/recall loop (WeatherTools, an ordinary @Tool method)
- @Tool(returnDirect = true) skipping the second model round trip entirely
  (ServerStatusTools)
- ToolContext: excluded from the model-facing JSON schema (verified against the
  real generated schema), still delivered to the tool from caller-supplied data
  (UserContextTools)
- A 230-tool synthetic library across six fake domains, generated via
  FunctionToolCallback.builder(...) (LargeToolLibrary)
- ToolSearchToolCallingAdvisor + RegexToolIndex: one tool ("toolSearchTool")
  offered on the first call instead of 230, with real tool-count and
  character-footprint measurements taken off the actual outgoing prompts

Findings recorded in the module's Javadoc rather than silently worked around:
- ToolCallingAdvisor only engages when the request's Prompt carries
  ToolCallingChatOptions, built from ChatModel.getOptions().mutate() (not
  getDefaultOptions(), a separate default method the request-building path never
  calls) -- confirmed by disassembling ToolCallingAdvisor.adviseCall and
  DefaultChatClientUtils
- The Tool Search Advisor's own tool is named "toolSearchTool" (camelCase), not
  "tool_search_tool" -- confirmed via @Tool(name=...) in the decompiled class
- Its session ID comes from ChatClientRequest.context() (AdvisorSpec.param), not
  from ChatClient.toolContext(Map) -- confirmed by disassembling
  ToolSearchToolCallingAdvisor.initializeSession
- RegexToolIndex matches on verb/noun substrings, not semantic relevance -- a
  real captured search for "look up an invoice" returned 5 lookup_-named tools
  across three unrelated domains alongside the one actually wanted

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FtpJvZfg4nvLvtzgJTDWpB
This commit is contained in:
Claude
2026-09-23 17:03:41 +00:00
parent d73620e305
commit fff9f52116
33 changed files with 1256 additions and 0 deletions
@@ -0,0 +1,12 @@
# Plain ToolCallingAdvisor: two model calls for one weather question
model call count: 2
call 1 -- model requests a tool:
tool call: What's the weather in Boston?
call 2 -- advisor sends the tool result back to the model:
tool response: ToolResponse[id=call-1, name=current_weather, responseData={"city":"Boston","temperatureCelsius":14.5,"conditions":"Overcast","humidityPercent":71}]
final answer returned to the caller:
It's 14.5C and overcast in Boston right now, with 71% humidity.
@@ -0,0 +1,6 @@
# returnDirect = true: the model is called exactly once
model call count: 1
raw content returned to the caller (the tool's own JSON, unparaphrased):
{"service":"orders-api","status":"UP","activeConnections":214,"checkedAt":"2026-09-23T18:00:00Z"}
@@ -0,0 +1,16 @@
# ToolContext: excluded from the model-facing schema, still delivered to the tool
my_account input schema sent to the model:
{
"$schema" : "https://json-schema.org/draft/2020-12/schema",
"type" : "object",
"properties" : { },
"required" : [ ],
"additionalProperties" : false
}
model's tool call arguments (empty -- it was never told a userId parameter exists):
{}
final answer, using the real userId the CALLER supplied via ChatClient.toolContext():
You're on the GOLD tier with a balance of $4820.50.
@@ -0,0 +1,11 @@
# ToolSearchToolCallingAdvisor: one tool offered up front instead of the whole library
tools registered on the ChatClient: 230
tools OFFERED to the model on call 1 (before any search): 1
[toolSearchTool]
tools OFFERED to the model on call 2 (after it searched for "look up an invoice"): 6
[crm_lookup_support_ticket, finance_lookup_invoice, hr_lookup_employee_record, hr_lookup_open_requisition, hr_lookup_pto_balance, toolSearchTool]
final answer: Invoice INV-1001 was found in the finance system.
@@ -0,0 +1,10 @@
# Definition-text footprint: full library vs. one search-tool definition
tools in the library: 230
total characters of name + description + input schema, ALL 230 tools: 69958
characters of name + description + input schema, toolSearchTool ONLY: 463
reduction on the first call of a conversation: 99.3%
caveat: characters are not tokens, and this is the FIRST call only -- once the model
has searched, the tools it found are added back in for the rest of that conversation.
See ToolSearchProgressiveDisclosureTest / output/04 for what gets added back.
@@ -0,0 +1,16 @@
# Synthetic tool library: size and per-domain breakdown
total tools: 230
crm 40 tools
devops 38 tools
finance 40 tools
hr 36 tools
logistics 40 tools
security 36 tools
sample tool definitions:
hr_lookup_employee_record Look up a single employee record by ID in the hr system.
hr_list_employee_record List employee record records in the hr system, optionally filtered.
hr_create_employee_record Create a new employee record in the hr system.
hr_update_employee_record Update an existing employee record in the hr system.