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
90 lines
4.7 KiB
Java
90 lines
4.7 KiB
Java
package com.ankurm.interop;
|
|
|
|
import static org.assertj.core.api.Assertions.assertThat;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.regex.Matcher;
|
|
import java.util.regex.Pattern;
|
|
|
|
import org.junit.jupiter.api.Test;
|
|
import org.springframework.boot.WebApplicationType;
|
|
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
|
import org.springframework.context.annotation.Configuration;
|
|
import org.springframework.boot.builder.SpringApplicationBuilder;
|
|
import org.springframework.context.ConfigurableApplicationContext;
|
|
|
|
class BothStartersTest {
|
|
|
|
@Configuration
|
|
@EnableAutoConfiguration
|
|
static class BothApp { }
|
|
|
|
record Outcome(String result, List<String> beans) { }
|
|
|
|
static Outcome start(FakeOpenAi fake, String... extra) {
|
|
List<String> props = new ArrayList<>(List.of(
|
|
"spring.ai.openai.base-url=" + fake.url(), "spring.ai.openai.api-key=k",
|
|
"spring.ai.openai.chat.options.model=gpt-6.1-sol",
|
|
"spring.ai.mcp.client.enabled=false", "spring.ai.mcp.server.enabled=false",
|
|
"langchain4j.open-ai.chat-model.base-url=" + fake.url() + "/v1",
|
|
"langchain4j.open-ai.chat-model.api-key=k",
|
|
"langchain4j.open-ai.chat-model.model-name=gpt-6.1-sol"));
|
|
props.addAll(List.of(extra));
|
|
try (ConfigurableApplicationContext ctx = new SpringApplicationBuilder(BothApp.class)
|
|
.web(WebApplicationType.NONE).properties(props.toArray(String[]::new)).run()) {
|
|
List<String> beans = new ArrayList<>();
|
|
for (String n : ctx.getBeanDefinitionNames()) {
|
|
Class<?> c = null;
|
|
try {
|
|
c = ctx.getType(n);
|
|
} catch (Exception ignored) {
|
|
// not resolvable without creating it
|
|
}
|
|
if (c != null && (c.getName().endsWith("OpenAiChatModel") || c.getName().endsWith("ChatClient$Builder")))
|
|
beans.add(n + " -> " + c.getName());
|
|
}
|
|
beans.add("(count) beans of type dev.langchain4j.model.chat.ChatModel: " + ctx.getBeanNamesForType(dev.langchain4j.model.chat.ChatModel.class).length);
|
|
beans.add("(count) beans of type org.springframework.ai.chat.model.ChatModel: " + ctx.getBeanNamesForType(org.springframework.ai.chat.model.ChatModel.class).length);
|
|
return new Outcome("context started", beans);
|
|
} catch (Exception e) {
|
|
Throwable root = e;
|
|
while (root.getCause() != null) root = root.getCause();
|
|
String msg = root.getMessage();
|
|
if (root.getClass().getSimpleName().equals("BeanDefinitionOverrideException")) {
|
|
Matcher m = Pattern.compile("factoryBeanName=([\\w.$]+)").matcher(msg);
|
|
List<String> owners = new ArrayList<>();
|
|
while (m.find()) owners.add(m.group(1));
|
|
msg = "bean 'openAiChatModel' is defined twice, by " + String.join(" and by ", owners);
|
|
}
|
|
return new Outcome("FAILED: " + root.getClass().getSimpleName() + ": " + msg, List.of());
|
|
}
|
|
}
|
|
|
|
@Test
|
|
void bothStartersInOneApplication() throws Exception {
|
|
try (FakeOpenAi fake = new FakeOpenAi()) {
|
|
Outcome both = start(fake);
|
|
Outcome excluded = start(fake, "spring.autoconfigure.exclude=dev.langchain4j.openai.spring.OpenAiAutoConfiguration");
|
|
Outcome overriding = start(fake, "spring.main.allow-bean-definition-overriding=true");
|
|
try (var t = new Transcript("01-both-starters.txt", "Both starters in one Spring Boot 4.1.1 application")) {
|
|
t.line("Spring AI: spring-ai-starter-model-openai 2.0.1. LangChain4j: langchain4j-open-ai-spring-boot4-starter 1.22.0-beta32.");
|
|
t.blank();
|
|
t.line("1. Both starters, nothing else:");
|
|
t.line(" %s", both.result());
|
|
t.blank();
|
|
t.line("2. spring.autoconfigure.exclude=dev.langchain4j.openai.spring.OpenAiAutoConfiguration:");
|
|
t.line(" %s", excluded.result());
|
|
excluded.beans().forEach(b -> t.line(" %s", b.startsWith("(count)") ? b.substring(8) : "bean " + b));
|
|
t.blank();
|
|
t.line("3. spring.main.allow-bean-definition-overriding=true:");
|
|
t.line(" %s", overriding.result());
|
|
overriding.beans().forEach(b -> t.line(" %s", b.startsWith("(count)") ? b.substring(8) : "bean " + b));
|
|
}
|
|
assertThat(both.result()).startsWith("FAILED: BeanDefinitionOverrideException");
|
|
assertThat(excluded.result()).isEqualTo("context started");
|
|
assertThat(overriding.result()).isEqualTo("context started");
|
|
}
|
|
}
|
|
}
|