jdaugherty opened a new pull request, #16599:
URL: https://github.com/apache/grails-core/pull/16599

   Target: `8.1.x`. Adds a Spring dependency (`spring-ai-bom` 2.0.1), so the 
longer review policy for Spring dependency changes applies.
   
   ## Why a Grails module rather than "just add Spring AI"
   
   Spring AI is already a Spring Boot integration, and a Grails 8 application 
can add a provider starter today and inject a `ChatClient.Builder` into a 
service. This PR is about the places where that stops being true in a Grails 
application, and about the conventions Grails adds that Spring AI leaves to the 
host framework.
   
   1. **Streaming did not work from a controller.** Every Spring AI example 
streams a `Flux` from `stream()`. Grails actions go through 
`UrlMappingsInfoHandlerAdapter`, which handles a map, a `ModelAndView` or null; 
a returned `Flux` rendered nothing. Spring MVC's return value handlers are not 
involved.
   2. **Unit tests could not use it.** Spring AI ships no stub `ChatModel`, and 
`GrailsApplicationBuilder` loads only `org.grails.*` auto-configurations in 
unit tests, so a service injecting `ChatClient.Builder` failed under 
`ServiceUnitTest`. Integration tests needed a real API key to boot.
   3. **Version management landed on each application.** Spring AI's BOM 
manages only its own artifacts, and its Boot baseline moves every minor (2.0 is 
Boot 4.0/4.1, 2.1 is already Boot 4.2). Importing the BOM into `grails-bom` 
makes "which Spring AI goes with my Grails" a project decision, and 
`validateDependencyVersions` and `buildHealth` police the transitives once.
   4. **Defaults suited to a web application.** Spring AI retries a failed call 
ten times with backoff up to three minutes, and logs neither prompts nor 
completions. Grails environments let the plugin ship three retries everywhere 
and prompt logging in development only, which Spring profiles do not express as 
naturally.
   
   The streaming support is general and lives in grails-async, not grails-ai: 
any Reactive Streams `Publisher` returned from an action streams as server-sent 
events, and grails-ai only teaches it to render chat responses.
   
   The plugin stays provider-neutral. Each Spring AI provider is its own module 
with its own SDK, auto-configuration and credentials, and every OpenAI model 
kind auto-configures by default, so bundling one provider would force every 
other application to turn it off per model kind. Forge picks the provider at 
creation time instead.
   
   ## What is included
   
   - **grails-async**: `PublisherActionResultTransformer` streams a returned 
`Publisher` through Spring MVC's `DeferredResult` lifecycle (same timeouts and 
thread release as a returned `Promise`), item by item with back-pressure. 
`grails.async.web.StreamItemRenderer` SPI with text and Jackson defaults; 
Spring `ServerSentEvent` honoured for ids, names and retry; a terminal 
`complete` event so `EventSource` clients do not reconnect; failures reported 
as an `error` event on the committed response. Settings 
`grails.async.streaming.enabled` and `grails.async.streaming.timeout`, 
documented in the properties reference.
   - **grails-ai**: the chat client and the auto-configuration every provider 
shares, `plugin.yml` defaults, and a renderer that turns streamed 
`ChatResponse`s into text. Thin by design.
   - **grails-testing-support-ai**: `AiUnitTest` trait with a scripted chat 
model, a deterministic embedding model and an in-memory vector store, wired 
through a Spock global extension like the JSON views trait; in the test 
environment the stubs become the primary models and `spring.ai.model.chat`, 
`spring.ai.model.embedding` and `spring.ai.vectorstore.type` default to `none`, 
so no key and no database are needed. `grails.ai.stub.enabled=false` opts back 
into the real provider.
   - **Forge**: an `AI` category with `grails-ai`, `spring-ai-openai`, 
`spring-ai-anthropic` and `spring-ai-ollama`. Provider features write key 
placeholders such as `${OPENAI_API_KEY}`, never values.
   - **Docs**: a "Using AI in Your Application" chapter (installation, every 
model provider with its starter and Spring AI reference link, configuration, 
chat client, streaming, testing), kept distinct from and cross-linked with the 
Agent Skills section; a "Streaming Responses" page in the async chapter; what's 
new entries; feature lists on the command line pages. Spring AI links use a 
version attribute derived from the BOM, so they follow the pinned line.
   - **grails-test-examples/ai**: functional tests against a running server 
with the Ollama starter stubbed out.
   
   ## Behaviour change for existing applications
   
   An action that already returned a `Publisher` used to render its view; it 
now streams. Set `grails.async.streaming.enabled: false` to restore the old 
behaviour. No other existing behaviour changes.
   
   ## Dependencies and licensing
   
   `spring-ai-bom` 2.0.1 is imported as a platform. StringTemplate and the 
ANTLR 3 runtime, pulled in by Spring AI's prompt templates, declare "The BSD 
License" with the antlr.org 3-clause text, so `SbomPlugin` maps them to 
BSD-3-Clause like the existing jline entries.
   
   ## Verification
   
   | Suite | Result |
   |---|---|
   | grails-async | 82 passed |
   | grails-ai | 15 passed |
   | grails-testing-support-ai | 44 passed |
   | grails-test-examples/ai, unit and functional over Tomcat | 14 passed |
   | Forge `GrailsAiSpec` | 10 passed |
   | codeStyle (modules and Forge), validateDependencyVersions, 
validateBomProperties, RAT, repository conventions, guide build | clean |
   
   The functional tests cover emission from a timer thread after the container 
thread returned, JSON and `ServerSentEvent` items, the `error` and `complete` 
events, a chat answer through the stub synchronously and streamed, the 
`plugin.yml` defaults reaching Spring AI's `SpringAiRetryProperties`, and the 
Ollama provider backing off without a server. The full aggregate gate was left 
to CI.
   
   ## Follow-ups, not in this PR
   
   Data-source bridges so pgvector and JDBC chat memory find the GORM data 
source; a GORM-backed chat memory repository; constraints to JSON schema for 
tool parameters and structured output; `@Tool` discovery on services with the 
security context in `ToolContext`; GSP prompt templates; MCP exposure of 
services. Open question for reviewers: keep the plugin provider-neutral and add 
a startup warning when no `ChatModel` bean exists, or bundle the OpenAI starter.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to