Skip to content

test(eval): add the harness and Gradle task for LLM tool-selection evals - #264

Open
adityamparikh wants to merge 1 commit into
apache:mainfrom
adityamparikh:test/eval-harness
Open

adityamparikh wants to merge 1 commit into
apache:mainfrom
adityamparikh:test/eval-harness

Conversation

@adityamparikh

@adityamparikh adityamparikh commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Part of #256. Adds the harness for the opt-in LLM tool-selection evals and the ./gradlew eval task. The first scenario is #265.

Stacked on #269 (./gradlew build runs buildSrc's tests) and #266 (google-java-format): the first two commits here are those PRs, and only the top commit (test(eval): add the harness and Gradle task for LLM tool-selection evals) is new.

How it works

A Spring AI ChatClient on OpenAI, set up as an MCP client of solr-mcp, gets natural-language prompts and calls the server's tools. An advisor records every tool call so a scenario can check which tools were called and with what arguments.

  • ./gradlew eval runs tests tagged llm-eval. Every other Test task excludes that tag, so ./gradlew build never calls a model. eval is never up to date, doesn't finalize jacocoTestReport (which would pull in test), and passes the bootJar path as solr.mcp.jar.
  • EvalConfiguration is a @TestConfiguration with @EnableAutoConfiguration, so Main's component scan never picks it up. Auto-configuration provides everything else:
    • Model: spring-ai-starter-model-openai with Spring AI's defaults (gpt-5-mini, key from OPENAI_API_KEY). Nothing is configured.
    • MCP client: spring-ai-starter-mcp-client starts the built jar over STDIO. The connection is in application-integration-test.properties: ${java.home}/bin/java, -jar ${solr.mcp.jar}, and SOLR_URL=${solr.url} from TestcontainersConfiguration. The profile also sets spring.ai.mcp.server.enabled=false, so auto-configuration doesn't start an MCP server on the test JVM's stdin and stdout.
    • ChatClient bean: the server's instructions (defaultSystem), its tools (defaultTools), and ToolCallAuditAdvisor (defaultAdvisors).
    • Data: an ApplicationRunner loads mydata/devnexus-2026.json (116 sessions) into devnexus through the server's own tools.
  • ToolCallAuditAdvisor observes the tool-calling loop from inside, as the Spring AI reference describes under Observing the tool-calling loop. Ordered after ToolCallingAdvisor, it runs on every iteration, and each iteration's request carries the turn so far. When the model answers without asking for another tool, that request holds every tool call and the ToolResponseMessage that answered it. The advisor records them as spring-ai-typesafe JevJudgeInput.ToolCall (name, arguments, result), and ToolCallAuditAdvisor.toolCalls(response) reads them back. No tool is wrapped.
  • ToolCallAuditAdvisorTest (in build) runs a real ChatClient tool loop over a scripted ChatModel and checks what the advisor recorded.

The three new dependencies are test-only. The shipped classpath, the SBOM (141 components) and the generated LICENSE are unaffected.

Verification

🤖 Generated with Claude Code

First part of apache#256. `./gradlew eval` runs tests tagged `llm-eval`; every
other Test task excludes the tag, so `./gradlew build` never calls a model.

The eval context is Spring Boot auto-configuration, set up by properties:

- spring-ai-starter-model-openai is the agent, on Spring AI's defaults
  with OPENAI_API_KEY; nothing is configured.
- spring-ai-starter-mcp-client starts the built jar over STDIO against a
  Testcontainers Solr (stdio connection in
  application-integration-test.properties).
- The ChatClient bean gets the server's instructions, its tools and
  ToolCallAuditAdvisor, which observes the tool-calling loop from inside
  (ordered after ToolCallingAdvisor, as the Spring AI reference describes
  under "Observing the tool-calling loop"). When the model gives its
  answer, that iteration's request holds every tool call and its
  ToolResponseMessage; the advisor records them as spring-ai-typesafe
  JevJudgeInput.ToolCall (name, arguments, result).

An ApplicationRunner loads the DevNexus 2026 sessions through the
server's own tools. ToolCallAuditAdvisorTest runs a real ChatClient tool
loop over a scripted model in ./gradlew build. The first scenario follows
in the next change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Aditya Parikh <aditya.m.parikh@gmail.com>
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant