From c7a4cab85522e2d977cfdf6dcdb9708fd4af22f7 Mon Sep 17 00:00:00 2001 From: rdehuyss Date: Wed, 10 Jun 2026 08:49:47 +0200 Subject: [PATCH] Update for latest Spring AI version --- base/build.gradle | 5 +- .../ai/javaclaw/JavaClawConfiguration.java | 10 +- .../FileSystemChatMemoryRepository.java | 11 +- .../DynamicToolDiscoveryConfiguration.java | 18 +- .../advisor/MessageChatMemoryAdvisor.java | 154 --------------- .../AppendableChatMemoryRepository.java | 11 -- .../chat/memory/MessageWindowChatMemory.java | 141 -------------- .../FileSystemChatMemoryRepositoryTest.java | 50 ----- ...DynamicToolDiscoveryConfigurationTest.java | 20 +- .../tools/search/LuceneToolSearcherTest.java | 41 ++-- .../memory/MessageWindowChatMemoryTest.java | 181 ------------------ gradle/libs.versions.toml | 12 +- .../AnthropticClaudeCodeConfiguration.java | 36 ++-- workspace/INFO.md | 2 +- 14 files changed, 69 insertions(+), 623 deletions(-) delete mode 100644 base/src/main/java/org/springframework/ai/chat/client/advisor/MessageChatMemoryAdvisor.java delete mode 100644 base/src/main/java/org/springframework/ai/chat/memory/AppendableChatMemoryRepository.java delete mode 100644 base/src/main/java/org/springframework/ai/chat/memory/MessageWindowChatMemory.java delete mode 100644 base/src/test/java/org/springframework/ai/chat/memory/MessageWindowChatMemoryTest.java diff --git a/base/build.gradle b/base/build.gradle index 147d72d0..7ff73c77 100644 --- a/base/build.gradle +++ b/base/build.gradle @@ -14,10 +14,11 @@ dependencies { implementation 'org.springframework.ai:spring-ai-client-chat' implementation 'org.springframework.ai:spring-ai-starter-mcp-client' + implementation 'org.springframework.ai:spring-ai-tool-search-advisor' + implementation 'org.springframework.ai:spring-ai-tool-search-tool' + implementation(libs.spring.ai.lucene) implementation(libs.spring.ai.agent.utils) - implementation(libs.spring.ai.tool.search) - implementation(libs.spring.ai.tool.search.lucene) runtimeOnly 'com.h2database:h2' testImplementation 'org.springframework.boot:spring-boot-starter-test' diff --git a/base/src/main/java/ai/javaclaw/JavaClawConfiguration.java b/base/src/main/java/ai/javaclaw/JavaClawConfiguration.java index f17c60c2..f97fcd71 100644 --- a/base/src/main/java/ai/javaclaw/JavaClawConfiguration.java +++ b/base/src/main/java/ai/javaclaw/JavaClawConfiguration.java @@ -10,11 +10,11 @@ import org.springaicommunity.agent.tools.FileSystemTools; import org.springaicommunity.agent.tools.SkillsTool; import org.springaicommunity.agent.tools.SmartWebFetchTool; -import org.springaicommunity.tool.search.ToolSearchToolCallAdvisor; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; -import org.springframework.ai.chat.client.advisor.ToolCallAdvisor; +import org.springframework.ai.chat.client.advisor.ToolCallingAdvisor; +import org.springframework.ai.chat.client.advisor.toolsearch.ToolSearchToolCallingAdvisor; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.memory.ChatMemoryRepository; import org.springframework.ai.chat.memory.MessageWindowChatMemory; @@ -70,7 +70,7 @@ public ChatClient.Builder chatClientBuilder(ObjectProvider chatModelP @DependsOn({"mcpHeaderCustomizer"}) public ChatClient chatClient(ChatClient.Builder chatClientBuilder, ChatMemory chatMemory, - ObjectProvider toolSearchToolCallAdvisorProvider, + ObjectProvider toolSearchToolCallAdvisorProvider, SyncMcpToolCallbackProvider mcpToolProvider, TaskManager taskManager, ConfigurationManager configurationManager, @@ -85,9 +85,9 @@ public ChatClient chatClient(ChatClient.Builder chatClientBuilder, String agentPrompt = agentMd.getContentAsString(StandardCharsets.UTF_8) + System.lineSeparator() + workspace.createRelative("INFO.md").getContentAsString(StandardCharsets.UTF_8) + System.lineSeparator(); - ToolCallAdvisor toolCallAdvisor = toolSearchToolCallAdvisorProvider.getIfAvailable(); + ToolCallingAdvisor toolCallAdvisor = toolSearchToolCallAdvisorProvider.getIfAvailable(); if (toolCallAdvisor == null) { - toolCallAdvisor = ToolCallAdvisor.builder().build(); + toolCallAdvisor = ToolCallingAdvisor.builder().build(); } chatClientBuilder diff --git a/base/src/main/java/ai/javaclaw/agent/memory/FileSystemChatMemoryRepository.java b/base/src/main/java/ai/javaclaw/agent/memory/FileSystemChatMemoryRepository.java index 9da13f89..e3b01fc8 100644 --- a/base/src/main/java/ai/javaclaw/agent/memory/FileSystemChatMemoryRepository.java +++ b/base/src/main/java/ai/javaclaw/agent/memory/FileSystemChatMemoryRepository.java @@ -2,7 +2,7 @@ import ai.javaclaw.files.YamlDocument; import ai.javaclaw.files.YamlParser; -import org.springframework.ai.chat.memory.AppendableChatMemoryRepository; +import org.springframework.ai.chat.memory.ChatMemoryRepository; import org.springframework.ai.chat.messages.Message; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.Resource; @@ -39,7 +39,7 @@ * */ @Component -public class FileSystemChatMemoryRepository implements AppendableChatMemoryRepository { +public class FileSystemChatMemoryRepository implements ChatMemoryRepository { private final Path conversationsDir; @@ -73,13 +73,6 @@ public List findByConversationId(String conversationId) { } } - @Override - public void appendAll(String conversationId, List messages) { - List existing = findByConversationId(conversationId); - List combined = Stream.concat(existing.stream(), messages.stream()).toList(); - saveAll(conversationId, combined); - } - @Override public void saveAll(String conversationId, List messages) { Path file = resolveFile(conversationId); diff --git a/base/src/main/java/ai/javaclaw/tools/search/DynamicToolDiscoveryConfiguration.java b/base/src/main/java/ai/javaclaw/tools/search/DynamicToolDiscoveryConfiguration.java index be12984b..9d8122b4 100644 --- a/base/src/main/java/ai/javaclaw/tools/search/DynamicToolDiscoveryConfiguration.java +++ b/base/src/main/java/ai/javaclaw/tools/search/DynamicToolDiscoveryConfiguration.java @@ -1,8 +1,8 @@ package ai.javaclaw.tools.search; -import org.springaicommunity.tool.search.ToolSearchToolCallAdvisor; -import org.springaicommunity.tool.search.ToolSearcher; -import org.springaicommunity.tool.searcher.LuceneToolSearcher; +import org.springframework.ai.chat.client.advisor.toolsearch.ToolSearchToolCallingAdvisor; +import org.springframework.ai.tool.toolsearch.ToolIndex; +import org.springframework.ai.tool.toolsearch.index.lucene.LuceneToolIndex; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; @@ -14,15 +14,15 @@ public class DynamicToolDiscoveryConfiguration { @Bean(destroyMethod = "close") - public ToolSearcher toolSearcher(DynamicToolDiscoveryProperties properties) { - return new LuceneToolSearcher(properties.luceneMinScoreThreshold()); + public LuceneToolIndex toolIndex(DynamicToolDiscoveryProperties properties) { + return new LuceneToolIndex(properties.luceneMinScoreThreshold()); } @Bean - public ToolSearchToolCallAdvisor toolSearchToolCallAdvisor(ToolSearcher toolSearcher, - DynamicToolDiscoveryProperties properties) { - return ToolSearchToolCallAdvisor.builder() - .toolSearcher(toolSearcher) + public ToolSearchToolCallingAdvisor toolSearchToolCallingAdvisor(ToolIndex toolIndex, + DynamicToolDiscoveryProperties properties) { + return ToolSearchToolCallingAdvisor.builder() + .toolIndex(toolIndex) .maxResults(properties.maxResults()) .build(); } diff --git a/base/src/main/java/org/springframework/ai/chat/client/advisor/MessageChatMemoryAdvisor.java b/base/src/main/java/org/springframework/ai/chat/client/advisor/MessageChatMemoryAdvisor.java deleted file mode 100644 index 75bb803a..00000000 --- a/base/src/main/java/org/springframework/ai/chat/client/advisor/MessageChatMemoryAdvisor.java +++ /dev/null @@ -1,154 +0,0 @@ -package org.springframework.ai.chat.client.advisor; - -import org.springframework.ai.chat.client.ChatClientMessageAggregator; -import org.springframework.ai.chat.client.ChatClientRequest; -import org.springframework.ai.chat.client.ChatClientResponse; -import org.springframework.ai.chat.client.advisor.api.Advisor; -import org.springframework.ai.chat.client.advisor.api.AdvisorChain; -import org.springframework.ai.chat.client.advisor.api.BaseAdvisor; -import org.springframework.ai.chat.client.advisor.api.BaseChatMemoryAdvisor; -import org.springframework.ai.chat.client.advisor.api.StreamAdvisorChain; -import org.springframework.ai.chat.memory.ChatMemory; -import org.springframework.ai.chat.messages.Message; -import org.springframework.ai.chat.messages.SystemMessage; -import org.springframework.util.Assert; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.core.scheduler.Scheduler; - -import java.util.*; - -/** - * A copy of Springs MessageChatMemoryAdvisor that does not add duplicate messages from memory if they are contained in the chatClientRequest.prompt().getInstructions() - */ -public final class MessageChatMemoryAdvisor implements BaseChatMemoryAdvisor { - - private final ChatMemory chatMemory; - - private final int order; - - private final Scheduler scheduler; - - private MessageChatMemoryAdvisor(ChatMemory chatMemory, int order, Scheduler scheduler) { - Assert.notNull(chatMemory, "chatMemory cannot be null"); - Assert.notNull(scheduler, "scheduler cannot be null"); - this.chatMemory = chatMemory; - this.order = order; - this.scheduler = scheduler; - } - - @Override - public int getOrder() { - return this.order; - } - - @Override - public Scheduler getScheduler() { - return this.scheduler; - } - - @Override - public ChatClientRequest before(ChatClientRequest chatClientRequest, AdvisorChain advisorChain) { - String conversationId = getConversationId(chatClientRequest.context()); - - List instructions = chatClientRequest.prompt().getInstructions(); - - // 1. Remove duplicated messages by means of LinkedHashSet - SequencedSet allMessages = new LinkedHashSet<>(this.chatMemory.get(conversationId)); - allMessages.addAll(instructions); - List processedMessages = new ArrayList<>(allMessages); - - // 2.1. Ensure system message, if present, appears first in the list. - for (int i = 0; i < processedMessages.size(); i++) { - if (processedMessages.get(i) instanceof SystemMessage) { - Message systemMessage = processedMessages.remove(i); - processedMessages.add(0, systemMessage); - break; - } - } - - // 3. Create a new request with the advised messages. - ChatClientRequest processedChatClientRequest = chatClientRequest.mutate() - .prompt(chatClientRequest.prompt().mutate().messages(processedMessages).build()) - .build(); - - // 4. Add the new user message to the conversation memory. - Message userMessage = processedChatClientRequest.prompt().getLastUserOrToolResponseMessage(); - this.chatMemory.add(conversationId, userMessage); - - return processedChatClientRequest; - } - - @Override - public ChatClientResponse after(ChatClientResponse chatClientResponse, AdvisorChain advisorChain) { - List assistantMessages = new ArrayList<>(); - if (chatClientResponse.chatResponse() != null) { - assistantMessages = chatClientResponse.chatResponse() - .getResults() - .stream() - .map(g -> (Message) g.getOutput()) - .toList(); - } - this.chatMemory.add(this.getConversationId(chatClientResponse.context()), - assistantMessages); - return chatClientResponse; - } - - @Override - public Flux adviseStream(ChatClientRequest chatClientRequest, - StreamAdvisorChain streamAdvisorChain) { - // Get the scheduler from BaseAdvisor - Scheduler scheduler = this.getScheduler(); - - // Process the request with the before method - return Mono.just(chatClientRequest) - .publishOn(scheduler) - .map(request -> this.before(request, streamAdvisorChain)) - .flatMapMany(streamAdvisorChain::nextStream) - .transform(flux -> new ChatClientMessageAggregator().aggregateChatClientResponse(flux, - response -> this.after(response, streamAdvisorChain))); - } - - public static Builder builder(ChatMemory chatMemory) { - return new Builder(chatMemory); - } - - public static final class Builder { - - private int order = Advisor.DEFAULT_CHAT_MEMORY_PRECEDENCE_ORDER; - - private Scheduler scheduler = BaseAdvisor.DEFAULT_SCHEDULER; - - private final ChatMemory chatMemory; - - private Builder(ChatMemory chatMemory) { - this.chatMemory = chatMemory; - } - - /** - * Set the order. - * - * @param order the order - * @return the builder - */ - public Builder order(int order) { - this.order = order; - return this; - } - - public Builder scheduler(Scheduler scheduler) { - this.scheduler = scheduler; - return this; - } - - /** - * Build the advisor. - * - * @return the advisor - */ - public MessageChatMemoryAdvisor build() { - return new MessageChatMemoryAdvisor(this.chatMemory, this.order, this.scheduler); - } - - } -} \ No newline at end of file diff --git a/base/src/main/java/org/springframework/ai/chat/memory/AppendableChatMemoryRepository.java b/base/src/main/java/org/springframework/ai/chat/memory/AppendableChatMemoryRepository.java deleted file mode 100644 index 97548040..00000000 --- a/base/src/main/java/org/springframework/ai/chat/memory/AppendableChatMemoryRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.springframework.ai.chat.memory; - -import org.springframework.ai.chat.messages.Message; - -import java.util.List; - -public interface AppendableChatMemoryRepository extends ChatMemoryRepository { - - void appendAll(String conversationId, List messages); - -} diff --git a/base/src/main/java/org/springframework/ai/chat/memory/MessageWindowChatMemory.java b/base/src/main/java/org/springframework/ai/chat/memory/MessageWindowChatMemory.java deleted file mode 100644 index e2d59fa6..00000000 --- a/base/src/main/java/org/springframework/ai/chat/memory/MessageWindowChatMemory.java +++ /dev/null @@ -1,141 +0,0 @@ -package org.springframework.ai.chat.memory; - -import org.springframework.ai.chat.messages.Message; -import org.springframework.ai.chat.messages.SystemMessage; -import org.springframework.util.Assert; - -import java.util.ArrayList; -import java.util.List; - -/** - * A copy of Spring's MessageWindowChatMemory that: - * - keeps the order of the messages (changed HashSet to LinkedHashSet) - * - returns a windowed view instead of all messages instead of only keeping the last x messages in the repository - *

- * See https://github.com/spring-projects/spring-ai/blob/019267f/spring-ai-model/src/main/java/org/springframework/ai/chat/memory/MessageWindowChatMemory.java - */ -public class MessageWindowChatMemory implements ChatMemory { - private static final int DEFAULT_MAX_MESSAGES = 20; - - private final AppendableChatMemoryRepository chatMemoryRepository; - - private final int maxMessages; - - private MessageWindowChatMemory(AppendableChatMemoryRepository chatMemoryRepository, int maxMessages) { - Assert.notNull(chatMemoryRepository, "chatMemoryRepository cannot be null"); - Assert.isTrue(maxMessages > 0, "maxMessages must be greater than 0"); - this.chatMemoryRepository = chatMemoryRepository; - this.maxMessages = maxMessages; - } - - @Override - public void add(String conversationId, List messages) { - Assert.hasText(conversationId, "conversationId cannot be null or empty"); - Assert.notNull(messages, "messages cannot be null"); - Assert.noNullElements(messages, "messages cannot contain null elements"); - - this.chatMemoryRepository.appendAll(conversationId, messages); - } - - @Override - public List get(String conversationId) { - Assert.hasText(conversationId, "conversationId cannot be null or empty"); - List allMessages = this.chatMemoryRepository.findByConversationId(conversationId); - return window(allMessages); - } - - @Override - public void clear(String conversationId) { - Assert.hasText(conversationId, "conversationId cannot be null or empty"); - this.chatMemoryRepository.deleteByConversationId(conversationId); - } - - private List window(List messages) { - if (messages.size() <= this.maxMessages) { - return messages; - } - - List systemMessages = messages.stream() - .filter(SystemMessage.class::isInstance) - .toList(); - List nonSystemMessages = messages.stream() - .filter(m -> !(m instanceof SystemMessage)) - .toList(); - - int maxNonSystem = Math.max(0, this.maxMessages - systemMessages.size()); - List windowedNonSystem = nonSystemMessages.subList( - Math.max(0, nonSystemMessages.size() - maxNonSystem), nonSystemMessages.size()); - - List result = new ArrayList<>(systemMessages); - result.addAll(windowedNonSystem); - return result; - } - - public static Builder builder() { - return new Builder(); - } - - public static final class Builder { - - private ChatMemoryRepository chatMemoryRepository = new InMemoryChatMemoryRepository(); - - private int maxMessages = DEFAULT_MAX_MESSAGES; - - private Builder() { - } - - public Builder chatMemoryRepository(ChatMemoryRepository chatMemoryRepository) { - this.chatMemoryRepository = chatMemoryRepository; - return this; - } - - public Builder maxMessages(int maxMessages) { - this.maxMessages = maxMessages; - return this; - } - - public MessageWindowChatMemory build() { - return new MessageWindowChatMemory(new DelegatingAppendableChatMemoryRepository(this.chatMemoryRepository), this.maxMessages); - } - - } - - private static class DelegatingAppendableChatMemoryRepository implements AppendableChatMemoryRepository { - - private final ChatMemoryRepository chatMemoryRepository; - - public DelegatingAppendableChatMemoryRepository(ChatMemoryRepository chatMemoryRepository) { - this.chatMemoryRepository = chatMemoryRepository; - } - - @Override - public List findConversationIds() { - return chatMemoryRepository.findConversationIds(); - } - - @Override - public List findByConversationId(String conversationId) { - return chatMemoryRepository.findByConversationId(conversationId); - } - - @Override - public void saveAll(String conversationId, List messages) { - chatMemoryRepository.saveAll(conversationId, messages); - } - - @Override - public void appendAll(String conversationId, List messages) { - if (chatMemoryRepository instanceof AppendableChatMemoryRepository appendableChatMemoryRepository) { - appendableChatMemoryRepository.appendAll(conversationId, messages); - } else { - List allMessages = new ArrayList<>(); - allMessages.addAll(findByConversationId(conversationId)); - allMessages.addAll(messages); - saveAll(conversationId, allMessages); - } - } - - @Override - public void deleteByConversationId(String conversationId) {chatMemoryRepository.deleteByConversationId(conversationId);} - } -} diff --git a/base/src/test/java/ai/javaclaw/agent/memory/FileSystemChatMemoryRepositoryTest.java b/base/src/test/java/ai/javaclaw/agent/memory/FileSystemChatMemoryRepositoryTest.java index 70fed583..50887d22 100644 --- a/base/src/test/java/ai/javaclaw/agent/memory/FileSystemChatMemoryRepositoryTest.java +++ b/base/src/test/java/ai/javaclaw/agent/memory/FileSystemChatMemoryRepositoryTest.java @@ -118,56 +118,6 @@ void savePreservesMessageOrder() { .containsExactly("Question 1", "Answer 1", "Question 2", "Answer 2"); } - @Test - void appendAllAddsMessagesToExistingConversation() { - repository.saveAll("web", List.of(new UserMessage("Hello!"))); - - repository.appendAll("web", List.of(new AssistantMessage("Hi there!"))); - - List loaded = repository.findByConversationId("web"); - assertThat(loaded).hasSize(2); - assertThat(loaded.get(0).getText()).isEqualTo("Hello!"); - assertThat(loaded.get(1).getText()).isEqualTo("Hi there!"); - } - - @Test - void appendAllCreatesFileWhenConversationDoesNotExist() { - repository.appendAll("web", List.of(new UserMessage("First message"))); - - List loaded = repository.findByConversationId("web"); - assertThat(loaded).hasSize(1); - assertThat(loaded.get(0).getText()).isEqualTo("First message"); - } - - @Test - void appendAllPreservesCreatedAt() throws IOException { - repository.saveAll("web", List.of(new UserMessage("First"))); - Path file = workspaceDir.resolve("conversations/chat-web.yaml"); - String originalCreatedAt = extractFrontmatterValue(Files.readString(file), "createdAt"); - - repository.appendAll("web", List.of(new AssistantMessage("Second"))); - - String createdAtAfterAppend = extractFrontmatterValue(Files.readString(file), "createdAt"); - assertThat(createdAtAfterAppend).isEqualTo(originalCreatedAt); - } - - @Test - void appendAllPreservesMessageOrder() { - repository.saveAll("web", List.of( - new UserMessage("Q1"), - new AssistantMessage("A1") - )); - - repository.appendAll("web", List.of( - new UserMessage("Q2"), - new AssistantMessage("A2") - )); - - List loaded = repository.findByConversationId("web"); - assertThat(loaded).extracting(Message::getText) - .containsExactly("Q1", "A1", "Q2", "A2"); - } - // ----------------------------------------------------------------------- // findByConversationId — missing file // ----------------------------------------------------------------------- diff --git a/base/src/test/java/ai/javaclaw/tools/search/DynamicToolDiscoveryConfigurationTest.java b/base/src/test/java/ai/javaclaw/tools/search/DynamicToolDiscoveryConfigurationTest.java index b78f67e1..f90cd08a 100644 --- a/base/src/test/java/ai/javaclaw/tools/search/DynamicToolDiscoveryConfigurationTest.java +++ b/base/src/test/java/ai/javaclaw/tools/search/DynamicToolDiscoveryConfigurationTest.java @@ -1,8 +1,8 @@ package ai.javaclaw.tools.search; import org.junit.jupiter.api.Test; -import org.springaicommunity.tool.search.ToolSearchToolCallAdvisor; -import org.springaicommunity.tool.search.ToolSearcher; +import org.springframework.ai.chat.client.advisor.toolsearch.ToolSearchToolCallingAdvisor; +import org.springframework.ai.tool.toolsearch.ToolIndex; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import static org.assertj.core.api.Assertions.assertThat; @@ -16,14 +16,14 @@ class DynamicToolDiscoveryConfigurationTest { void whenPropertyIsMissing_defaultsToEnabled() { contextRunner .run(context -> { - assertThat(context).hasSingleBean(ToolSearcher.class); - assertThat(context).hasSingleBean(ToolSearchToolCallAdvisor.class); + assertThat(context).hasSingleBean(ToolIndex.class); + assertThat(context).hasSingleBean(ToolSearchToolCallingAdvisor.class); assertThat(context.getBean(DynamicToolDiscoveryProperties.class).enabled()).isTrue(); }); } @Test - void whenEnabled_registersToolSearcherAndAdvisor() { + void whenEnabled_registersToolIndexAndAdvisor() { contextRunner .withPropertyValues( "javaclaw.tools.dynamic-discovery.enabled=true", @@ -31,19 +31,19 @@ void whenEnabled_registersToolSearcherAndAdvisor() { "javaclaw.tools.dynamic-discovery.lucene-min-score-threshold=0.0" ) .run(context -> { - assertThat(context).hasSingleBean(ToolSearcher.class); - assertThat(context).hasSingleBean(ToolSearchToolCallAdvisor.class); + assertThat(context).hasSingleBean(ToolIndex.class); + assertThat(context).hasSingleBean(ToolSearchToolCallingAdvisor.class); assertThat(context.getBean(DynamicToolDiscoveryProperties.class).enabled()).isTrue(); }); } @Test - void whenDisabled_doesNotRegisterToolSearcherOrAdvisor() { + void whenDisabled_doesNotRegisterToolIndexOrAdvisor() { contextRunner .withPropertyValues("javaclaw.tools.dynamic-discovery.enabled=false") .run(context -> { - assertThat(context).doesNotHaveBean(ToolSearcher.class); - assertThat(context).doesNotHaveBean(ToolSearchToolCallAdvisor.class); + assertThat(context).doesNotHaveBean(ToolIndex.class); + assertThat(context).doesNotHaveBean(ToolSearchToolCallingAdvisor.class); }); } } diff --git a/base/src/test/java/ai/javaclaw/tools/search/LuceneToolSearcherTest.java b/base/src/test/java/ai/javaclaw/tools/search/LuceneToolSearcherTest.java index 41913d7b..e6e4577d 100644 --- a/base/src/test/java/ai/javaclaw/tools/search/LuceneToolSearcherTest.java +++ b/base/src/test/java/ai/javaclaw/tools/search/LuceneToolSearcherTest.java @@ -1,9 +1,9 @@ package ai.javaclaw.tools.search; import org.junit.jupiter.api.Test; -import org.springaicommunity.tool.search.ToolReference; -import org.springaicommunity.tool.search.ToolSearchRequest; -import org.springaicommunity.tool.searcher.LuceneToolSearcher; +import org.springframework.ai.tool.toolsearch.ToolReference; +import org.springframework.ai.tool.toolsearch.ToolSearchRequest; +import org.springframework.ai.tool.toolsearch.index.lucene.LuceneToolIndex; import static org.assertj.core.api.Assertions.assertThat; @@ -11,16 +11,16 @@ class LuceneToolSearcherTest { @Test void returnsRelevantToolsForQuery() throws Exception { - try (LuceneToolSearcher searcher = new LuceneToolSearcher(0.0f)) { + try (LuceneToolIndex index = new LuceneToolIndex(0.0f)) { String sessionId = "s1"; - searcher.indexTool(sessionId, new ToolReference("fileSystem", null, - "Read, write, and edit local files in the workspace. Use for file operations, patches, and edits.")); - searcher.indexTool(sessionId, new ToolReference("webFetch", null, - "Fetch a URL and extract readable content from web pages. Use for scraping and summarization.")); - searcher.indexTool(sessionId, new ToolReference("shell", null, - "Execute shell commands to inspect the repository, run builds/tests, and automate development tasks.")); + index.indexTool(sessionId, ToolReference.builder().toolName("fileSystem") + .summary("Read, write, and edit local files in the workspace. Use for file operations, patches, and edits.").build()); + index.indexTool(sessionId, ToolReference.builder().toolName("webFetch") + .summary("Fetch a URL and extract readable content from web pages. Use for scraping and summarization.").build()); + index.indexTool(sessionId, ToolReference.builder().toolName("shell") + .summary("Execute shell commands to inspect the repository, run builds/tests, and automate development tasks.").build()); - var response = searcher.search(new ToolSearchRequest(sessionId, "edit a local file", 5, null)); + var response = index.search(new ToolSearchRequest(sessionId, "edit a local file", 5, null)); assertThat(response.toolReferences()).isNotEmpty(); assertThat(response.toolReferences().getFirst().toolName()).isEqualTo("fileSystem"); @@ -29,14 +29,14 @@ void returnsRelevantToolsForQuery() throws Exception { @Test void ranksMoreRelevantToolHigherBasedOnDescription() throws Exception { - try (LuceneToolSearcher searcher = new LuceneToolSearcher(0.0f)) { + try (LuceneToolIndex index = new LuceneToolIndex(0.0f)) { String sessionId = "s2"; - searcher.indexTool(sessionId, new ToolReference("webFetch", null, - "Fetch a URL and extract page contents. Good for reading articles when you already have a URL.")); - searcher.indexTool(sessionId, new ToolReference("braveSearch", null, - "Search the web by keyword query and return results. Use when you do not have a URL yet.")); + index.indexTool(sessionId, ToolReference.builder().toolName("webFetch") + .summary("Fetch a URL and extract page contents. Good for reading articles when you already have a URL.").build()); + index.indexTool(sessionId, ToolReference.builder().toolName("braveSearch") + .summary("Search the web by keyword query and return results. Use when you do not have a URL yet.").build()); - var response = searcher.search(new ToolSearchRequest(sessionId, "search the web for spring ai docs", 5, null)); + var response = index.search(new ToolSearchRequest(sessionId, "search the web for spring ai docs", 5, null)); assertThat(response.toolReferences()).isNotEmpty(); assertThat(response.toolReferences().getFirst().toolName()).isEqualTo("braveSearch"); @@ -45,13 +45,14 @@ void ranksMoreRelevantToolHigherBasedOnDescription() throws Exception { @Test void honorsMaxResults() throws Exception { - try (LuceneToolSearcher searcher = new LuceneToolSearcher(0.0f)) { + try (LuceneToolIndex index = new LuceneToolIndex(0.0f)) { String sessionId = "s3"; for (int i = 0; i < 10; i++) { - searcher.indexTool(sessionId, new ToolReference("tool-" + i, null, "tool number " + i + " for testing")); + index.indexTool(sessionId, ToolReference.builder().toolName("tool-" + i) + .summary("tool number " + i + " for testing").build()); } - var response = searcher.search(new ToolSearchRequest(sessionId, "tool testing", 3, null)); + var response = index.search(new ToolSearchRequest(sessionId, "tool testing", 3, null)); assertThat(response.toolReferences().size()).isLessThanOrEqualTo(3); } diff --git a/base/src/test/java/org/springframework/ai/chat/memory/MessageWindowChatMemoryTest.java b/base/src/test/java/org/springframework/ai/chat/memory/MessageWindowChatMemoryTest.java deleted file mode 100644 index d1aa18d0..00000000 --- a/base/src/test/java/org/springframework/ai/chat/memory/MessageWindowChatMemoryTest.java +++ /dev/null @@ -1,181 +0,0 @@ -package org.springframework.ai.chat.memory; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.ai.chat.messages.AssistantMessage; -import org.springframework.ai.chat.messages.Message; -import org.springframework.ai.chat.messages.SystemMessage; -import org.springframework.ai.chat.messages.UserMessage; - -import java.util.List; -import java.util.stream.IntStream; - -import static org.assertj.core.api.Assertions.assertThat; - -class MessageWindowChatMemoryTest { - - static final String CONVERSATION_ID = "test-conv"; - static final int MAX_MESSAGES = 5; - - InMemoryChatMemoryRepository repository; - MessageWindowChatMemory memory; - - @BeforeEach - void setUp() { - repository = new InMemoryChatMemoryRepository(); - memory = MessageWindowChatMemory.builder() - .chatMemoryRepository(repository) - .maxMessages(MAX_MESSAGES) - .build(); - } - - // ----------------------------------------------------------------------- - // add - // ----------------------------------------------------------------------- - - @Test - void addStoresAllMessagesInRepository() { - memory.add(CONVERSATION_ID, List.of(new UserMessage("msg1"), new AssistantMessage("msg2"))); - memory.add(CONVERSATION_ID, List.of(new UserMessage("msg3"))); - - List stored = repository.findByConversationId(CONVERSATION_ID); - assertThat(stored).hasSize(3); - assertThat(stored.get(0).getText()).isEqualTo("msg1"); - assertThat(stored.get(1).getText()).isEqualTo("msg2"); - assertThat(stored.get(2).getText()).isEqualTo("msg3"); - } - - @Test - void addDoesNotTrimRepositoryWhenExceedingMaxMessages() { - List messages = IntStream.rangeClosed(1, MAX_MESSAGES + 3) - .mapToObj(i -> (Message) new UserMessage("msg" + i)) - .toList(); - - memory.add(CONVERSATION_ID, messages); - - // repository contains ALL messages, not just last maxMessages - List stored = repository.findByConversationId(CONVERSATION_ID); - assertThat(stored).hasSize(MAX_MESSAGES + 3); - } - - // ----------------------------------------------------------------------- - // get – windowed view - // ----------------------------------------------------------------------- - - @Test - void getReturnsAllMessagesWhenBelowMaxMessages() { - memory.add(CONVERSATION_ID, List.of( - new UserMessage("hello"), - new AssistantMessage("hi") - )); - - List result = memory.get(CONVERSATION_ID); - - assertThat(result).hasSize(2); - assertThat(result.get(0).getText()).isEqualTo("hello"); - assertThat(result.get(1).getText()).isEqualTo("hi"); - } - - @Test - void getReturnsAllMessagesWhenExactlyMaxMessages() { - List messages = IntStream.rangeClosed(1, MAX_MESSAGES) - .mapToObj(i -> (Message) new UserMessage("msg" + i)) - .toList(); - memory.add(CONVERSATION_ID, messages); - - List result = memory.get(CONVERSATION_ID); - - assertThat(result).hasSize(MAX_MESSAGES); - } - - @Test - void getReturnsWindowedViewWhenExceedingMaxMessages() { - List messages = IntStream.rangeClosed(1, MAX_MESSAGES + 3) - .mapToObj(i -> (Message) new UserMessage("msg" + i)) - .toList(); - memory.add(CONVERSATION_ID, messages); - - List result = memory.get(CONVERSATION_ID); - - assertThat(result).hasSize(MAX_MESSAGES); - // should contain the LAST maxMessages messages - assertThat(result.get(0).getText()).isEqualTo("msg4"); - assertThat(result.get(result.size() - 1).getText()).isEqualTo("msg" + (MAX_MESSAGES + 3)); - } - - @Test - void getPreservesSystemMessageWhenTrimming() { - SystemMessage system = new SystemMessage("You are a helpful assistant."); - memory.add(CONVERSATION_ID, List.of(system)); - // add enough messages to exceed the window - List extras = IntStream.rangeClosed(1, MAX_MESSAGES + 2) - .mapToObj(i -> (Message) new UserMessage("msg" + i)) - .toList(); - memory.add(CONVERSATION_ID, extras); - - List result = memory.get(CONVERSATION_ID); - - // system message is always preserved - assertThat(result).hasSize(MAX_MESSAGES); - assertThat(result.get(0)).isInstanceOf(SystemMessage.class); - assertThat(result.get(0).getText()).isEqualTo("You are a helpful assistant."); - } - - @Test - void getWindowDoesNotMutateRepository() { - List messages = IntStream.rangeClosed(1, MAX_MESSAGES + 5) - .mapToObj(i -> (Message) new UserMessage("msg" + i)) - .toList(); - memory.add(CONVERSATION_ID, messages); - - // call get multiple times - memory.get(CONVERSATION_ID); - memory.get(CONVERSATION_ID); - - // repository still has all original messages - assertThat(repository.findByConversationId(CONVERSATION_ID)).hasSize(MAX_MESSAGES + 5); - } - - @Test - void getReturnsEmptyListForUnknownConversation() { - assertThat(memory.get("unknown")).isEmpty(); - } - - // ----------------------------------------------------------------------- - // clear - // ----------------------------------------------------------------------- - - @Test - void clearRemovesAllMessagesFromRepository() { - memory.add(CONVERSATION_ID, List.of(new UserMessage("hello"), new AssistantMessage("hi"))); - - memory.clear(CONVERSATION_ID); - - assertThat(memory.get(CONVERSATION_ID)).isEmpty(); - assertThat(repository.findByConversationId(CONVERSATION_ID)).isEmpty(); - } - - // ----------------------------------------------------------------------- - // default maxMessages - // ----------------------------------------------------------------------- - - @Test - void defaultMaxMessagesIs20() { - InMemoryChatMemoryRepository defaultRepo = new InMemoryChatMemoryRepository(); - MessageWindowChatMemory memoryWithDefaultMax = MessageWindowChatMemory.builder() - .chatMemoryRepository(defaultRepo) - .build(); - - List messages = IntStream.rangeClosed(1, 25) - .mapToObj(i -> (Message) new UserMessage("msg" + i)) - .toList(); - memoryWithDefaultMax.add(CONVERSATION_ID, messages); - - List result = memoryWithDefaultMax.get(CONVERSATION_ID); - assertThat(result).hasSize(20); - assertThat(result.get(0).getText()).isEqualTo("msg6"); - assertThat(result.get(19).getText()).isEqualTo("msg25"); - - assertThat(defaultRepo.findByConversationId(CONVERSATION_ID)).hasSize(25); - } -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 87dc3fe3..09a492b3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,22 +6,20 @@ commonmark = "0.28.0" [libraries] bom-spring-boot = { module = "org.springframework.boot:spring-boot-dependencies", version.ref = "spring-boot" } -bom-spring-ai = { module = "org.springframework.ai:spring-ai-bom", version = "2.0.0-M6" } +bom-spring-ai = { module = "org.springframework.ai:spring-ai-bom", version = "2.0.0-RC1" } bom-spring-modulith = { module = "org.springframework.modulith:spring-modulith-bom", version = "2.0.6" } - -spring-ai-agent-utils = { module = "org.springaicommunity:spring-ai-agent-utils", version = "0.7.0" } -spring-ai-tool-search = { module = "org.springaicommunity:tool-search-tool", version.ref = "tool-search" } -spring-ai-tool-search-lucene = { module = "org.springaicommunity:tool-searcher-lucene", version.ref = "tool-search" } +spring-ai-agent-utils = { module = "org.springaicommunity:spring-ai-agent-utils", version = "0.9.0" } +spring-ai-lucene = { module = "org.apache.lucene:lucene-core", version = "9.12.3" } netty-resolver-dns-native-macos = { module = "io.netty:netty-resolver-dns-native-macos", version = "4.2.12.Final" } -jobrunr-spring-starter = { module = "org.jobrunr:jobrunr-spring-boot-4-starter", version = "8.6.0"} +jobrunr-spring-starter = { module = "org.jobrunr:jobrunr-spring-boot-4-starter", version = "8.6.1" } pebble-spring-starter = { module = "io.pebbletemplates:pebble-spring-boot-starter", version = "4.1.1" } java-discord-api = { module = "net.dv8tion:JDA", version = "6.4.1" } playwright = { module = "com.microsoft.playwright:playwright", version = "1.52.0" } telegrambots-client = { module = "org.telegram:telegrambots-client", version.ref = "telegrambots" } telegrambots-springboot-longpolling-starter = { module = "org.telegram:telegrambots-springboot-longpolling-starter", version.ref = "telegrambots" } commonmark-java = { module = "org.commonmark:commonmark", version.ref = "commonmark" } -commonmark-ext-gfm-strikethrough = { module = "org.commonmark:commonmark-ext-gfm-strikethrough", version.ref = "commonmark" } +commonmark-ext-gfm-strikethrough = { module = "org.commonmark:commonmark-ext-gfm-strikethrough", version.ref = "commonmark" } [bundles] diff --git a/providers/anthropic/src/main/java/ai/javaclaw/providers/anthropic/AnthropticClaudeCodeConfiguration.java b/providers/anthropic/src/main/java/ai/javaclaw/providers/anthropic/AnthropticClaudeCodeConfiguration.java index b940da15..10697f18 100644 --- a/providers/anthropic/src/main/java/ai/javaclaw/providers/anthropic/AnthropticClaudeCodeConfiguration.java +++ b/providers/anthropic/src/main/java/ai/javaclaw/providers/anthropic/AnthropticClaudeCodeConfiguration.java @@ -1,18 +1,16 @@ package ai.javaclaw.providers.anthropic; import com.anthropic.client.AnthropicClient; -import com.anthropic.client.AnthropicClientAsync; -import com.anthropic.client.okhttp.AnthropicOkHttpClient; -import com.anthropic.client.okhttp.AnthropicOkHttpClientAsync; +import com.anthropic.client.AnthropicClientImpl; +import com.anthropic.core.ClientOptions; import io.micrometer.observation.ObservationRegistry; import org.springframework.ai.anthropic.AnthropicChatModel; import org.springframework.ai.anthropic.AnthropicChatOptions; +import org.springframework.ai.anthropic.http.okhttp.SpringAiAnthropicHttpClient; import org.springframework.ai.chat.observation.ChatModelObservationConvention; import org.springframework.ai.model.anthropic.autoconfigure.AnthropicChatProperties; import org.springframework.ai.model.anthropic.autoconfigure.AnthropicConnectionProperties; -import org.springframework.ai.model.tool.DefaultToolExecutionEligibilityPredicate; import org.springframework.ai.model.tool.ToolCallingManager; -import org.springframework.ai.model.tool.ToolExecutionEligibilityPredicate; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; @@ -28,20 +26,18 @@ public class AnthropticClaudeCodeConfiguration { public AnthropicChatModel anthropicChatModel(AnthropicConnectionProperties connectionProperties, AnthropicChatProperties chatProperties, ToolCallingManager toolCallingManager, ObjectProvider observationRegistry, - ObjectProvider observationConvention, - ObjectProvider anthropicToolExecutionEligibilityPredicate) { + ObjectProvider observationConvention) { AnthropicChatOptions options = getAnthropicChatOptions(connectionProperties, chatProperties); var backend = new AnthropicClaudeCodeBackend(); + var client = anthropicClient(options, backend); var chatModel = AnthropicChatModel.builder() - .anthropicClient(anthropicClient(options, backend)) - .anthropicClientAsync(anthropicClientAsync(options, backend)) + .anthropicClient(client) + .anthropicClientAsync(client.async()) .options(options) .toolCallingManager(toolCallingManager) .observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)) - .toolExecutionEligibilityPredicate(anthropicToolExecutionEligibilityPredicate - .getIfUnique(DefaultToolExecutionEligibilityPredicate::new)) .build(); observationConvention.ifAvailable(chatModel::setObservationConvention); @@ -61,19 +57,13 @@ private static AnthropicChatOptions getAnthropicChatOptions(AnthropicConnectionP } private static AnthropicClient anthropicClient(AnthropicChatOptions options, AnthropicClaudeCodeBackend backend) { - var clientBuilder = AnthropicOkHttpClient.builder().backend(backend); - if (options.getTimeout() != null) clientBuilder.timeout(options.getTimeout()); - if (options.getMaxRetries() != null) clientBuilder.maxRetries(options.getMaxRetries()); - if (options.getProxy() != null) clientBuilder.proxy(options.getProxy()); - return clientBuilder.build(); - } + var httpClientBuilder = SpringAiAnthropicHttpClient.builder().backend(backend); + if (options.getTimeout() != null) httpClientBuilder.timeout(options.getTimeout()); + if (options.getProxy() != null) httpClientBuilder.proxy(options.getProxy()); - private static AnthropicClientAsync anthropicClientAsync(AnthropicChatOptions options, AnthropicClaudeCodeBackend backend) { - var asyncClientBuilder = AnthropicOkHttpClientAsync.builder().backend(backend); - if (options.getTimeout() != null) asyncClientBuilder.timeout(options.getTimeout()); - if (options.getMaxRetries() != null) asyncClientBuilder.maxRetries(options.getMaxRetries()); - if (options.getProxy() != null) asyncClientBuilder.proxy(options.getProxy()); - return asyncClientBuilder.build(); + var clientOptionsBuilder = ClientOptions.builder().httpClient(httpClientBuilder.build()); + if (options.getMaxRetries() != null) clientOptionsBuilder.maxRetries(options.getMaxRetries()); + return new AnthropicClientImpl(clientOptionsBuilder.build()); } } \ No newline at end of file diff --git a/workspace/INFO.md b/workspace/INFO.md index 9c8ac050..692f974a 100644 --- a/workspace/INFO.md +++ b/workspace/INFO.md @@ -8,4 +8,4 @@ - recurring tasks `recurring/.md` ### Tool calling -You have access to various tools and skills. Try to use them as much as possible. \ No newline at end of file +You have access to the Tool Search Tool which allows you to find various tools and skills. Before saying you do not have access, always use the Tool Search Tool to find the relevant tool. \ No newline at end of file