1
0

Spring AI 1.x to 2.0 migration: breaking-change reference and offline verification suite

Companion code for the ankurm.com guide. Verified on Spring AI 2.0.0, Spring Boot 4.1.0,
JDK 25.0.3. run.txt is unedited mvn test output: 5 tests, 0 failures.

Runs with NO API key, no network and no provider account. A canned ChatModel returns fixed
responses; ChatClient, advisors and chat memory all sit above the model, so everything that
changed is still exercised faithfully. TestChatModel records each Prompt it receives, which
lets the tests assert what Spring AI SENT rather than what a model replied.

docs/01  complete breaking-change reference: platform (Boot 4 mandatory, Jackson 2 to 3),
         artifact renames (spring-ai-advisors-vector-store -> spring-ai-vector-store-advisor,
         spring-ai-core split, OCI and Minimax removals, MCP transports moved into Spring AI),
         options builders replacing setters, the dropped .options property prefix, tool
         calling (internalToolExecutionEnabled and toolNames removed outright, not renamed),
         chat memory, structured output schema changes, and a migration order that works.

Tested here
  - Chat memory becomes stricter: 2.0 removes the remaining default-ID and builder-based
    configuration paths (ChatMemory.DEFAULT_CONVERSATION_ID and .conversationId() are both
    gone). Explicit per-request conversation ids - introduced during the 1.x line and already
    the recommended approach - become the only migration target.
  - CONVERSATION_ID survives as the metadata KEY. It and the removed DEFAULT_CONVERSATION_ID
    look interchangeable and are opposites; swapping them to clear a compile error restores
    exactly the shared-conversation behaviour that removing it was meant to take away.
  - PromptChatMemoryAdvisor is gone; MessageChatMemoryAdvisor replaces it, and the difference
    is not cosmetic (system-prompt text vs real Message objects, so token counts change).
  - A test asserts two conversation ids cannot see each other.
This commit is contained in:
2026-08-01 10:44:31 +05:30
commit cb777068a7
8 changed files with 606 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
target/
*.class
dep.txt
.idea/
*.iml
.vscode/
.DS_Store
# run.txt IS committed on purpose -- it is the evidence for the blog post

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Ankur Mhatre
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

70
README.md Normal file
View File

@@ -0,0 +1,70 @@
# Spring AI 1.x → 2.0 migration
Companion repository for **[Spring AI 1.x to 2.0: The Migration Guide](https://ankurm.com/spring-ai-1-to-2-migration-guide/)**
on [ankurm.com](https://ankurm.com).
Verified against **Spring AI 2.0.0**, **Spring Boot 4.1.0**, **JDK 25.0.3**. Full output in
[`run.txt`](run.txt).
---
## Quick start
**No API key, no network, no provider account.** A canned `ChatModel` returns fixed responses, which
is enough to exercise every API that changed — `ChatClient`, advisors and chat memory all sit
*above* the model.
```console
git clone https://ankurm.com/git.app/asmhatre/spring-ai-2-migration.git
cd spring-ai-2-migration
mvn test
```
5 tests, under a second.
---
## What is here
| | |
|---|---|
| [Breaking-change reference](docs/01-breaking-changes.md) | The complete list, organised by area, with the migration order that works |
| [`ChatMemoryMigrationTest`](src/test/java/com/ankurm/ai/ChatMemoryMigrationTest.java) | The chat-memory changes, asserted — including a test proving two conversations cannot see each other |
| [`TestChatModel`](src/main/java/com/ankurm/ai/TestChatModel.java) | The offline model. Records every `Prompt` it receives, so tests assert **what Spring AI sent**, not what a model replied |
---
## The five things most likely to bite
1. **Boot 4 is mandatory.** Spring AI 2.0 is built on the Boot 4 dependency model and *cannot* load
in a Boot 3 context. Migrate Boot first, as a separate step.
2. **Jackson 2 → 3** (`com.fasterxml.jackson``tools.jackson`), which changes default date format
and property order. This is the change least likely to be caught by a compiler and most likely to
break a downstream consumer.
3. **`internalToolExecutionEnabled` is removed** — not renamed. Per-model internal tool execution no
longer exists in any `ChatModel`, so if you relied on the model driving the tool loop, that
orchestration moves up into the advisor layer.
4. **Chat memory gets stricter.** 2.0 removes the remaining default-ID and builder-based
configuration paths — `ChatMemory.DEFAULT_CONVERSATION_ID` and `.conversationId()` are both
gone. Explicit per-request conversation IDs, introduced during the 1.x line, become the only
safe migration target. Leaning on the old default put every caller in one shared conversation.
5. **Structured-output schema changes are silent.** Optional Kotlin properties and
`@JsonProperty` without an explicit `required` are no longer in the JSON Schema `required` array,
which changes what the model returns without producing any error.
---
## A trap worth its own line
`ChatMemory.CONVERSATION_ID` still exists. `ChatMemory.DEFAULT_CONVERSATION_ID` does not.
They look like the same thing and are opposites: the survivor is the **metadata key** you use to
pass an id, the removed one was the **value** `"default"` used when you passed nothing. Swapping one
for the other to clear a compile error restores exactly the shared-conversation behaviour that
removing it was meant to take away.
---
## Licence
MIT. See [LICENSE](LICENSE).

182
docs/01-breaking-changes.md Normal file
View File

@@ -0,0 +1,182 @@
# Spring AI 1.x → 2.0: complete breaking-change reference
Compiled from the official upgrade notes and verified against `spring-ai-bom:2.0.0` on
Spring Boot 4.1.0 / JDK 25. Items marked **[tested]** are asserted in
[`ChatMemoryMigrationTest`](../src/test/java/com/ankurm/ai/ChatMemoryMigrationTest.java).
---
## 0. Platform — do this first, it gates everything
| | 1.x | 2.0 |
|---|---|---|
| Spring Boot | 3.x | **4.x only** |
| Jackson | 2 (`com.fasterxml.jackson`) | **3 (`tools.jackson`)** |
| Java | 17 | 17 minimum, **21+ recommended** |
**Spring AI 2.0 is built on the Boot 4 dependency model and cannot be loaded in a Boot 3 context.**
There is no partial upgrade: you migrate Boot 3 → 4 first, or not at all.
Spring AI is **not** managed by the Spring Boot BOM. Import its own:
```xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>2.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
```
The Jackson 2 → 3 move is the biggest hidden cost. It changes the default date format and property
order, so anything that snapshot-tests JSON, or persists serialized model output, needs full
integration testing rather than a compile check.
## 1. Artifact and module changes
| Change | Action |
|---|---|
| `spring-ai-core` ended at `1.0.0-M6` | The project is split into focused modules — `spring-ai-model`, `spring-ai-client-chat`, etc. Depend on what you use. |
| `spring-ai-advisors-vector-store`**`spring-ai-vector-store-advisor`** | Rename the dependency. |
| `spring-ai-spring-cloud-bindings` **removed** | Configure credentials with normal Boot property sources. |
| MCP transports `mcp-spring-webflux` / `mcp-spring-webmvc` moved out of the MCP Java SDK **into Spring AI** | Update coordinates; they are now Spring AI artifacts. |
| OCI GenAI **removed** from the main repository | Migrate to the separate OCI GenAI integration repo. |
| Minimax dedicated support **removed** | Use the Anthropic support, per Minimax's own recommendation. Minimax **embeddings are no longer supported at all.** |
## 2. Options: setters are gone, builders only
Every `setX(...)` on options classes is deprecated or removed:
```java
// Before
OpenAiChatOptions options = new OpenAiChatOptions();
options.setTemperature(0.7); // no longer compiles
// After
OpenAiChatOptions options = OpenAiChatOptions.builder()
.temperature(0.7)
.build();
```
Two more renames in the same area:
- `N()`**`n()`** in `*Options` builders and configuration properties, to match Java conventions.
- `spring.ai.ollama.chat.think-option`**`spring.ai.ollama.chat.think`**.
`ChatClient` no longer accepts a pre-built `ChatOptions` instance. Use the builder lambda form.
## 3. Configuration properties
- **The `.options` prefix is gone** for every non-chat model type — embedding, image, audio,
moderation, OCR. `spring.ai.openai.embedding.options.model` becomes
`spring.ai.openai.embedding.model`.
- Default values are **no longer duplicated** in `*Properties` classes; they live at the options
level. If you were reading a default off a `*Properties` bean, it may now be `null`.
- `spring.ai.<provider>.chat.internal-tool-execution-enabled`**removed** (see §4).
- The `streamToolCallResponses` properties on the tool-calling advisors — **removed** (see §4).
## 4. Tool calling — the largest semantic change
**`internalToolExecutionEnabled` has been removed** from `ToolCallingChatOptions` and every
provider-specific options class.
- `.internalToolExecutionEnabled(false)` no longer compiles.
- `.internalToolExecutionEnabled(true)` no longer compiles **and the behaviour it named no longer
exists** — per-model internal tool execution has been removed from all `ChatModel`
implementations.
This is not a rename. If you relied on the model driving the tool-call loop itself, that loop is
gone and the orchestration moves up into the advisor layer.
Also removed:
- **`streamToolCallResponses`** from `ToolCallingAdvisor.Builder`, `ToolCallAdvisor.Builder` and
`ToolSearchToolCallingAdvisor.Builder`. The upgrade notes are unusually direct about why: the
design flaw behind it could not be fixed without a breaking change to `ChatClientResponse`, so
the option was removed outright rather than patched.
- **`SpringBeanToolCallbackResolver`**, and with it the whole pattern of declaring bare
`Function` / `Supplier` / `Consumer` beans and referencing them by name.
- **`toolNames()`** from all chat options classes and from `ChatClient`.
Migrate name-based tool wiring to explicit `ToolCallback` instances.
## 5. Chat memory — **[tested]**
Three changes that interact, and the reason this repository exists:
| Change | Effect |
|---|---|
| `ChatMemory.DEFAULT_CONVERSATION_ID` **removed** | no more implicit `"default"` conversation |
| Conversation ID **no longer optional** for `MessageChatMemoryAdvisor` / `VectorStoreChatMemoryAdvisor` | must be supplied per request |
| `.conversationId(String)` **removed** from those builders | cannot be set at construction time |
| `PromptChatMemoryAdvisor` **removed** | use `MessageChatMemoryAdvisor` |
```java
// After — the id is a per-request advisor param
client.prompt()
.user("what is my name?")
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, "user-42"))
.call()
.content();
```
> **Do not "fix" the compile error by reaching for `ChatMemory.CONVERSATION_ID` as a value.**
> `CONVERSATION_ID` survives, but it is the *metadata key* used to pass an id. The removed
> `DEFAULT_CONVERSATION_ID` was the *value* `"default"`. Same prefix, opposite meaning — and
> substituting one for the other restores exactly the shared-conversation behaviour that removing it
> was meant to take away.
**What actually changed.** Explicit per-request conversation IDs are not new — they were introduced
during the 1.x line and were already the recommended approach. 2.0 removes the *alternatives*: the
default-ID constant and the builder-based configuration path. The explicit per-request form is now
the only way to do it, which makes it the migration target.
That matters because the default-ID path was easy to lean on. Omitting the id in 1.x compiled and
ran, with everything landing in a single shared `"default"` conversation — fine for a demo, wrong
for anything multi-user. Removing the fallback converts "I forgot" from silent behaviour into a
compile error. The test suite asserts that two conversation IDs cannot see each other.
`PromptChatMemoryAdvisor``MessageChatMemoryAdvisor` is also not cosmetic: the removed advisor
injected history into the **system prompt as text**, the replacement adds **real `Message`
objects**. Providers treat those differently, so expect prompt shape and token counts to change.
### Chat memory repositories
Messages read from a `ChatMemoryRepository` (JDBC, Cassandra, MongoDB, Neo4j, Redis) now carry a
**creation timestamp in metadata**. Consequence: a retrieved message is **not** `equals()` to an
otherwise-identical message constructed in code. Anything comparing messages by value, or keying a
`Set`/`Map` on them, changes behaviour.
## 6. Structured output / `BeanOutputConverter`
- Kotlin properties optional in their primary constructor (nullable or defaulted) are **no longer**
in the JSON Schema `required` array.
- Properties annotated `@JsonProperty(required = false)`**including** `@JsonProperty` without an
explicit `required` — are no longer treated as required.
- **`BeanOutputConverter.postProcessSchema(JsonNode)` removed.** Subclasses overriding it will not
compile.
The first two change the schema you send to the model, which can change model behaviour without any
error. If you rely on the model populating a field, mark it required explicitly.
## 7. `ModelOptionsUtils` and MCP
- Several `ModelOptionsUtils` members are **removed**, because model options no longer depend on
Jackson directly.
- MCP `elicit(…)` overloads on `McpAsyncRequestContext` / `McpSyncRequestContext` that took
`tools.jackson.core.type.TypeReference<T>` now take
**`org.springframework.core.ParameterizedTypeReference<T>`**.
## 8. Migration order that works
1. **Boot 3 → Boot 4** on its own, with Spring AI still on 1.1.x. Land it.
2. Bump the Spring AI BOM to 2.0.0 and fix compile errors: options builders, `toolNames()`,
`internalToolExecutionEnabled`, module renames.
3. Fix chat memory — every advisor call site needs a conversation ID. Grep for
`DEFAULT_CONVERSATION_ID` and for advisor construction.
4. Re-test structured output against the real model. Schema changes are silent.
5. Integration-test anything that serializes: Jackson 3 changes date format and property order.
Steps 4 and 5 are the ones that do not announce themselves. Everything before them is a compiler
error, which is the easy kind of breakage.

71
pom.xml Normal file
View File

@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Companion code for "Spring AI 1.x to 2.0 Migration Guide" (ankurm.com).
mvn test
Runs entirely offline. There is no API key, no network call and no model provider: a test
ChatModel returns canned responses, which is enough to exercise every API that changed.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.0</version>
<relativePath/>
</parent>
<groupId>com.ankurm.ai</groupId>
<artifactId>spring-ai-2-migration</artifactId>
<version>1.0.0</version>
<name>Spring AI 1.x to 2.0 migration</name>
<properties>
<java.version>25</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!--
Spring AI is NOT managed by the Spring Boot BOM. You import its own BOM, and you are
responsible for keeping the pair compatible: Spring AI 2.0 is built on the Boot 4
dependency model and CANNOT be loaded in a Boot 3 context.
-->
<spring-ai.version>2.0.0</spring-ai.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--
The chat client and its advisors. Note spring-ai-core is gone: it stopped at 1.0.0-M6
and the project is now split into focused modules.
-->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-client-chat</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

BIN
run.txt Normal file

Binary file not shown.

View File

@@ -0,0 +1,57 @@
package com.ankurm.ai;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.prompt.Prompt;
import java.util.ArrayList;
import java.util.List;
/**
* A {@link ChatModel} that never calls anything.
*
* <p>This exists so the whole repository runs with {@code mvn test} and no API key, no network and
* no provider account. Every API this guide covers -- {@code ChatClient}, advisors, chat memory,
* options -- sits <em>above</em> the model, so a canned model exercises all of it faithfully.
*
* <p>It also records every {@link Prompt} it receives, which is how the tests prove what an advisor
* actually did to the conversation before it reached the model. That is the interesting assertion:
* not "what did the model say", but "what did Spring AI send".
*
* <p>Implementing {@code ChatModel} in 2.0 requires exactly one method,
* {@code ChatResponse call(Prompt)}. Everything else on the interface is a default.
*/
public class TestChatModel implements ChatModel {
private final List<Prompt> received = new ArrayList<>();
private String nextReply = "ok";
@Override
public ChatResponse call(Prompt prompt) {
received.add(prompt);
return new ChatResponse(List.of(new Generation(new AssistantMessage(nextReply))));
}
public TestChatModel replyWith(String reply) {
this.nextReply = reply;
return this;
}
/** Every prompt this model was asked to complete, in order. */
public List<Prompt> received() {
return List.copyOf(received);
}
/** The messages of the most recent prompt -- i.e. what the advisors assembled. */
public List<Message> lastMessages() {
return received.isEmpty() ? List.of() : received.getLast().getInstructions();
}
public void reset() {
received.clear();
nextReply = "ok";
}
}

View File

@@ -0,0 +1,196 @@
package com.ankurm.ai;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import java.util.Arrays;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* <h2>Chat memory: the 2.0 change most likely to break silently</h2>
*
* <p>Three things changed together, and they interact:
*
* <ol>
* <li>{@code ChatMemory.DEFAULT_CONVERSATION_ID} (value {@code "default"}) has been
* <b>removed</b>.</li>
* <li>The conversation ID is <b>no longer optional</b> for {@code MessageChatMemoryAdvisor} and
* {@code VectorStoreChatMemoryAdvisor}.</li>
* <li>{@code .conversationId(String)} has been <b>removed</b> from those advisors' builders, so
* you can no longer set a default at construction time.</li>
* </ol>
*
* <p>Explicit per-request conversation IDs are not new &mdash; they were introduced during the 1.x
* line and were already the recommended approach. What 2.0 does is remove the <em>alternatives</em>:
* the default-ID constant and the builder-based configuration path are both gone, so the explicit
* per-request form is now the only way to do it.
*
* <p>That matters because the default-ID path was easy to lean on. Omitting the ID in 1.x worked,
* and everything landed in a single shared {@code "default"} conversation &mdash; fine for a demo,
* wrong for anything multi-user. Removing the fallback turns "I forgot" from a silent behaviour into
* a compile error.
*
* <p>{@code PromptChatMemoryAdvisor} has also been removed outright; {@code MessageChatMemoryAdvisor}
* is the replacement.
*/
class ChatMemoryMigrationTest {
private static void title(String s) {
System.out.println();
System.out.println("=".repeat(78));
System.out.println(s);
System.out.println("=".repeat(78));
}
private static void bullet(String f, Object... a) {
System.out.printf(" " + f + "%n", a);
}
@Test
void defaultConversationIdConstantIsGone() throws Exception {
title("1. ChatMemory.DEFAULT_CONVERSATION_ID has been removed");
List<String> constants = Arrays.stream(ChatMemory.class.getFields())
.map(java.lang.reflect.Field::getName)
.toList();
bullet("public constants on ChatMemory : %s", constants);
assertThat(constants)
.as("the 1.x fallback constant is gone")
.doesNotContain("DEFAULT_CONVERSATION_ID");
assertThat(constants)
.as("CONVERSATION_ID -- the metadata KEY -- remains, and is a different thing")
.contains("CONVERSATION_ID");
bullet("");
bullet("Note the survivor is CONVERSATION_ID, the metadata key you use to PASS an id.");
bullet("The removed one was DEFAULT_CONVERSATION_ID, the value 'default' used when you");
bullet("passed nothing. Same prefix, opposite meaning -- so 'fixing' the compile error by");
bullet("swapping one for the other restores exactly the shared-conversation behaviour");
bullet("that removing it was meant to take away.");
}
@Test
void promptChatMemoryAdvisorIsGone() {
title("2. PromptChatMemoryAdvisor has been removed");
assertThatThrownBy(() -> Class.forName(
"org.springframework.ai.chat.client.advisor.PromptChatMemoryAdvisor"))
.isInstanceOf(ClassNotFoundException.class);
bullet("PromptChatMemoryAdvisor : ClassNotFoundException (as expected in 2.0)");
bullet("MessageChatMemoryAdvisor: %s",
MessageChatMemoryAdvisor.class.getName());
bullet("");
bullet("Replacement is MessageChatMemoryAdvisor. The difference is not cosmetic: the");
bullet("removed one injected history into the SYSTEM PROMPT as text, the replacement");
bullet("adds it as real Message objects. Providers treat those differently, so expect");
bullet("your prompts -- and your token counts -- to change on migration.");
}
@Test
void conversationIdMustNowBeSuppliedPerRequest() {
title("3. The conversation ID is now required, per request");
TestChatModel model = new TestChatModel().replyWith("hello back");
ChatMemory memory = MessageWindowChatMemory.builder().build();
ChatClient client = ChatClient.builder(model)
// In 1.x you could call .conversationId("...") here. That builder method is gone.
.defaultAdvisors(MessageChatMemoryAdvisor.builder(memory).build())
.build();
bullet("turn 1 -- conversation 'user-42'");
client.prompt()
.user("my name is Ankur")
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, "user-42"))
.call()
.content();
bullet("turn 2 -- same conversation");
client.prompt()
.user("what is my name?")
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, "user-42"))
.call()
.content();
List<Message> sent = model.lastMessages();
bullet("messages the model received on turn 2 : %d", sent.size());
sent.forEach(m -> bullet(" %-9s %s", m.getMessageType(), m.getText()));
assertThat(sent)
.as("turn 2 carries turn 1's history")
.hasSizeGreaterThan(1);
assertThat(sent.stream().map(Message::getText))
.anyMatch(t -> t.contains("my name is Ankur"));
System.out.println();
System.out.println(">> The id travels as an advisor PARAM per request, not as builder state.");
System.out.println(">> This form already existed in 1.x; 2.0 removes the alternatives, so it");
System.out.println(">> is now the only option. That suits how the id is actually scoped: in a");
System.out.println(">> web app it is per user or per session, and therefore cannot sensibly");
System.out.println(">> be baked into a singleton ChatClient.");
}
@Test
void separateConversationsDoNotSeeEachOther() {
title("4. Proving isolation, now that explicit ids are the only path");
TestChatModel model = new TestChatModel();
ChatMemory memory = MessageWindowChatMemory.builder().build();
ChatClient client = ChatClient.builder(model)
.defaultAdvisors(MessageChatMemoryAdvisor.builder(memory).build())
.build();
client.prompt().user("secret for alice")
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, "alice")).call().content();
client.prompt().user("what did you hear?")
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, "bob")).call().content();
List<Message> bobSaw = model.lastMessages();
bobSaw.forEach(m -> bullet("bob's prompt: %-9s %s", m.getMessageType(), m.getText()));
assertThat(bobSaw.stream().map(Message::getText))
.as("bob must not see alice's history")
.noneMatch(t -> t.contains("secret for alice"));
System.out.println();
System.out.println(">> Passing distinct ids keeps the conversations separate -- and in 2.0");
System.out.println(">> there is no longer a way NOT to pass one. Under 1.x the same code");
System.out.println(">> with the ids omitted would have compiled, put alice and bob in the");
System.out.println(">> shared 'default' conversation, and failed this assertion silently.");
}
@Test
void memoryStoresAndReturnsMessages() {
title("5. Direct ChatMemory use, for the storage-layer migration");
ChatMemory memory = MessageWindowChatMemory.builder().build();
memory.add("conv-1", new UserMessage("first"));
memory.add("conv-1", new UserMessage("second"));
List<Message> stored = memory.get("conv-1");
stored.forEach(m -> bullet("stored: %s", m.getText()));
assertThat(stored).hasSize(2);
memory.clear("conv-1");
assertThat(memory.get("conv-1")).isEmpty();
bullet("after clear: %d messages", memory.get("conv-1").size());
System.out.println();
System.out.println(">> If you use a JDBC/Cassandra/Mongo/Neo4j ChatMemoryRepository, note a");
System.out.println(">> separate 2.0 change: retrieved messages now carry a creation");
System.out.println(">> timestamp in metadata, so a message read back is NOT equals() to an");
System.out.println(">> identical one built in code. Any test or cache keyed on Message");
System.out.println(">> equality, or storing Messages in a Set, will change behaviour.");
}
}