Add ai-services and spring-boot-ai-service modules: prompt templates, per-id chat memory, tools, RAG, the @MemoryId trap, the message-window probe, and @AiService on Spring Boot 4.1.1, each with a captured transcript

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01G8ikz8xdWuTP5yun8DZ1hk
This commit is contained in:
2026-10-11 07:48:34 +00:00
co-authored by Claude Sonnet 5.5
commit f87573fcb8
22 changed files with 670 additions and 0 deletions
@@ -0,0 +1,6 @@
Spring Boot version: 4.1.1
Bean of the interface type exists: [bootApp.Greeter]
Messages sent to the model:
SYSTEM: You greet people briefly.
USER: Greet Ankur
Reply: Reply built from 2 message(s).
+52
View File
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.ankurm.langchain4j</groupId>
<artifactId>langchain4j-demo</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>spring-boot-ai-service</artifactId>
<properties>
<spring-boot.version>4.1.1</spring-boot.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.ankurm.langchain4j</groupId>
<artifactId>ai-services</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-spring-boot-starter</artifactId>
<version>${langchain4j.beta.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,35 @@
package com.ankurm.lc4j.boot;
import com.ankurm.lc4j.aiservices.ScriptedChatModel;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.service.spring.AiService;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.SystemMessage;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class BootApp {
/** Stand-in for an OpenAI or Ollama model bean. @AiService finds the single ChatModel in the context. */
@Bean
ScriptedChatModel chatModel() {
return new ScriptedChatModel(r -> AiMessage.from("Reply built from " + r.messages().size() + " message(s)."));
}
@Bean
dev.langchain4j.memory.ChatMemory chatMemory() {
return MessageWindowChatMemory.withMaxMessages(10);
}
/** The whole declaration. No @Bean method, no builder: the scanner creates and registers it. */
@AiService
public interface Greeter {
@SystemMessage("You greet people briefly.")
@UserMessage("Greet {{it}}")
String greet(String name);
}
}
@@ -0,0 +1,42 @@
package com.ankurm.lc4j.boot;
import com.ankurm.lc4j.aiservices.ScriptedChatModel;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
class BootAiServiceTest {
@Autowired
ApplicationContext context;
@Autowired
BootApp.Greeter greeter;
@Autowired
ScriptedChatModel model;
@Test
void aiServiceInterfaceIsRegisteredAsABeanAndWorks() throws IOException {
String reply = greeter.greet("Ankur");
StringBuilder sb = new StringBuilder();
sb.append("Spring Boot version: ").append(org.springframework.boot.SpringBootVersion.getVersion()).append('\n');
sb.append("Bean of the interface type exists: ").append(context.getBeansOfType(BootApp.Greeter.class).keySet()).append('\n');
sb.append("Messages sent to the model:\n").append(ScriptedChatModel.render(model.lastRequest().messages()));
sb.append("Reply: ").append(reply).append('\n');
Files.createDirectories(Path.of("output"));
Files.writeString(Path.of("output/01-boot-context.txt"), sb.toString());
assertThat(reply).startsWith("Reply built from");
assertThat(context.getBeansOfType(BootApp.Greeter.class)).hasSize(1);
}
}