Add langchain4j-interop: Spring AI 2.0 and LangChain4j 1.22 side by side

Same order assistant in both frameworks against a local fake OpenAI
server: starter bean collision, wire differences, tool failure and loop
bounds, observability meters, MCP-first tools, and a compile check of
the 2025 article's snippets and Maven coordinates.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01G8ikz8xdWuTP5yun8DZ1hk
This commit is contained in:
Claude
2026-10-11 10:09:45 +00:00
parent d51ef56468
commit e79b75a105
35 changed files with 1330 additions and 0 deletions
@@ -0,0 +1,18 @@
import org.springframework.ai.chat.client.advisor.api.AdvisedRequest;
import org.springframework.ai.chat.client.advisor.api.AdvisedResponse;
import org.springframework.ai.chat.client.advisor.api.CallAroundAdvisor;
import org.springframework.ai.chat.client.advisor.api.CallAroundAdvisorChain;
// From the original TokenBudgetAdvisor.
class OldCallAroundAdvisor implements CallAroundAdvisor {
@Override
public AdvisedResponse aroundCall(AdvisedRequest request, CallAroundAdvisorChain chain) {
return chain.nextAroundCall(request);
}
@Override
public String getName() { return "x"; }
@Override
public int getOrder() { return 0; }
}
@@ -0,0 +1,32 @@
import java.util.List;
import org.springframework.ai.chat.client.ChatClient;
import reactor.core.publisher.Flux;
// The original ChatService, unchanged: this one is expected to still compile.
class OldChatService {
private final ChatClient chatClient;
OldChatService(ChatClient.Builder builder) {
this.chatClient = builder
.defaultSystem("You are a senior Java architect. Be precise and concise.")
.build();
}
public String ask(String userMessage) {
return chatClient.prompt().user(userMessage).call().content();
}
public Flux<String> stream(String userMessage) {
return chatClient.prompt().user(userMessage).stream().content();
}
public ArchitectureReview reviewCode(String codeSnippet) {
return chatClient.prompt()
.user(u -> u.text("Review this Java code for production readiness: {code}").param("code", codeSnippet))
.call()
.entity(ArchitectureReview.class);
}
public record ArchitectureReview(String verdict, List<String> issues, List<String> suggestions, int score) { }
}
@@ -0,0 +1,8 @@
import org.springframework.ai.document.Document;
// From the original RAGService: Document::getContent
class OldDocumentGetContent {
String text(Document d) {
return d.getContent();
}
}
@@ -0,0 +1,11 @@
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;
// From the original AgentConfig.
class OldLangChain4jBuilder {
interface Agent { String chat(String m); }
Agent agent(OpenAiChatModel model) {
return AiServices.builder(Agent.class).chatLanguageModel(model).build();
}
}
@@ -0,0 +1,8 @@
import org.springframework.ai.chat.messages.Message;
// From the original TokenBudgetAdvisor.estimateTokens: m.getContent()
class OldMessageGetContent {
int length(Message m) {
return m.getContent().length();
}
}
@@ -0,0 +1,6 @@
import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer;
// From the original ObservabilityConfig.
class OldMeterRegistryCustomizer {
MeterRegistryCustomizer<?> unused;
}
@@ -0,0 +1,8 @@
import org.springframework.ai.reader.pdf.config.PdfDocumentReaderConfig;
// From the original DocumentIngestionService.
class OldPdfConfig {
Object config() {
return PdfDocumentReaderConfig.builder().withPageTopMargin(0).build();
}
}
@@ -0,0 +1,8 @@
import org.springframework.ai.vectorstore.SearchRequest;
// From the original RAGService and SemanticCacheService.
class OldSearchRequest {
Object build(String question, int topK) {
return SearchRequest.query(question).withTopK(topK).withSimilarityThreshold(0.75);
}
}
@@ -0,0 +1,6 @@
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
// From the original DocumentIngestionService: this one is expected to still compile.
class OldTokenTextSplitter {
TokenTextSplitter splitter = new TokenTextSplitter(512, 50, 5, 10000, true);
}