From 979934da81aa2b9abc9be53da052818bf9de52d0 Mon Sep 17 00:00:00 2001 From: aias00 Date: Wed, 28 May 2025 22:13:10 +0800 Subject: [PATCH 1/4] [refactor]refactor es chat memory (#1001) * [refactor] modify memory dependencies * [refactor] modify memory dependencies * [refactor] modify elasticsearch chat memory * [refactor] modify elasticsearch chat memory * [refactor] modify elasticsearch chat memory * [refactor] modify chat memory starter --- .../pom.xml | 13 ++ ...sticsearchChatMemoryAutoConfiguration.java | 107 ++++++++++++++++ .../ElasticsearchChatMemoryProperties.java | 33 +++-- .../RedisChatMemoryAutoConfiguration.java | 2 - ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../ElasticsearchChatMemoryRepository.java | 117 ++++++++---------- .../ElasticsearchChatMemoryRepositoryIT.java | 23 ++-- .../src/main/resources/application.yml | 10 +- .../spring-ai-alibaba-starter-memory/pom.xml | 18 +-- 9 files changed, 218 insertions(+), 106 deletions(-) create mode 100644 auto-configurations/spring-ai-alibaba-autoconfigure-memory/src/main/java/com/alibaba/cloud/ai/autoconfigure/memory/ElasticsearchChatMemoryAutoConfiguration.java rename community/memories/spring-ai-alibaba-elasticsearch-memory/src/main/java/com/alibaba/cloud/ai/memory/elasticsearch/ElasticsearchConfig.java => auto-configurations/spring-ai-alibaba-autoconfigure-memory/src/main/java/com/alibaba/cloud/ai/autoconfigure/memory/ElasticsearchChatMemoryProperties.java (74%) diff --git a/auto-configurations/spring-ai-alibaba-autoconfigure-memory/pom.xml b/auto-configurations/spring-ai-alibaba-autoconfigure-memory/pom.xml index a01c46455f..9c932d94c4 100644 --- a/auto-configurations/spring-ai-alibaba-autoconfigure-memory/pom.xml +++ b/auto-configurations/spring-ai-alibaba-autoconfigure-memory/pom.xml @@ -98,6 +98,19 @@ true + + co.elastic.clients + elasticsearch-java + true + + + + com.alibaba.cloud.ai + spring-ai-alibaba-starter-memory-elasticsearch + ${revision} + true + + diff --git a/auto-configurations/spring-ai-alibaba-autoconfigure-memory/src/main/java/com/alibaba/cloud/ai/autoconfigure/memory/ElasticsearchChatMemoryAutoConfiguration.java b/auto-configurations/spring-ai-alibaba-autoconfigure-memory/src/main/java/com/alibaba/cloud/ai/autoconfigure/memory/ElasticsearchChatMemoryAutoConfiguration.java new file mode 100644 index 0000000000..b93474e20c --- /dev/null +++ b/auto-configurations/spring-ai-alibaba-autoconfigure-memory/src/main/java/com/alibaba/cloud/ai/autoconfigure/memory/ElasticsearchChatMemoryAutoConfiguration.java @@ -0,0 +1,107 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.ai.autoconfigure.memory; + +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.json.jackson.JacksonJsonpMapper; +import co.elastic.clients.transport.ElasticsearchTransport; +import co.elastic.clients.transport.rest_client.RestClientTransport; +import com.alibaba.cloud.ai.memory.elasticsearch.ElasticsearchChatMemoryRepository; +import org.apache.http.HttpHost; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.ssl.SSLContextBuilder; +import org.elasticsearch.client.RestClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +import javax.net.ssl.SSLContext; + +/** + * Auto-configuration for ElasticSearch chat memory repository. + */ +@ConditionalOnClass({ ElasticsearchChatMemoryRepository.class, ElasticsearchClient.class }) +@ConditionalOnProperty(prefix = "spring.ai.memory.elasticsearch", name = "enabled", havingValue = "true", + matchIfMissing = false) +@EnableConfigurationProperties(ElasticsearchChatMemoryProperties.class) +public class ElasticsearchChatMemoryAutoConfiguration { + + private static final Logger logger = LoggerFactory.getLogger(ElasticsearchChatMemoryAutoConfiguration.class); + + @Bean + @Qualifier("elasticsearchChatMemoryRepository") + @ConditionalOnMissingBean(name = "elasticsearchChatMemoryRepository") + ElasticsearchChatMemoryRepository elasticsearchChatMemoryRepository(ElasticsearchChatMemoryProperties properties) + throws Exception { + logger.info("Configuring elasticsearch chat memory repository"); + // Create HttpHosts for all nodes + HttpHost[] httpHosts; + if (!CollectionUtils.isEmpty(properties.getNodes())) { + httpHosts = properties.getNodes().stream().map(node -> { + String[] parts = node.split(":"); + return new HttpHost(parts[0], Integer.parseInt(parts[1]), properties.getScheme()); + }).toArray(HttpHost[]::new); + } + else { + // Fallback to single node configuration + httpHosts = new HttpHost[] { + new HttpHost(properties.getHost(), properties.getPort(), properties.getScheme()) }; + } + + var restClientBuilder = RestClient.builder(httpHosts); + + // Add authentication if credentials are provided + if (StringUtils.hasText(properties.getUsername()) && StringUtils.hasText(properties.getPassword())) { + CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials(AuthScope.ANY, + new UsernamePasswordCredentials(properties.getUsername(), properties.getPassword())); + + // Create SSL context if using HTTPS + if ("https".equalsIgnoreCase(properties.getScheme())) { + SSLContext sslContext = SSLContextBuilder.create() + .loadTrustMaterial(null, (chains, authType) -> true) + .build(); + + restClientBuilder.setHttpClientConfigCallback( + httpClientBuilder -> httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider) + .setSSLContext(sslContext) + .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE)); + } + else { + restClientBuilder.setHttpClientConfigCallback( + httpClientBuilder -> httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider)); + } + } + + // Create the transport and client + ElasticsearchTransport transport = new RestClientTransport(restClientBuilder.build(), new JacksonJsonpMapper()); + ElasticsearchClient elasticsearchClient = new ElasticsearchClient(transport); + return new ElasticsearchChatMemoryRepository(elasticsearchClient); + } + +} diff --git a/community/memories/spring-ai-alibaba-elasticsearch-memory/src/main/java/com/alibaba/cloud/ai/memory/elasticsearch/ElasticsearchConfig.java b/auto-configurations/spring-ai-alibaba-autoconfigure-memory/src/main/java/com/alibaba/cloud/ai/autoconfigure/memory/ElasticsearchChatMemoryProperties.java similarity index 74% rename from community/memories/spring-ai-alibaba-elasticsearch-memory/src/main/java/com/alibaba/cloud/ai/memory/elasticsearch/ElasticsearchConfig.java rename to auto-configurations/spring-ai-alibaba-autoconfigure-memory/src/main/java/com/alibaba/cloud/ai/autoconfigure/memory/ElasticsearchChatMemoryProperties.java index 0b2db6f23c..9c8256c902 100644 --- a/community/memories/spring-ai-alibaba-elasticsearch-memory/src/main/java/com/alibaba/cloud/ai/memory/elasticsearch/ElasticsearchConfig.java +++ b/auto-configurations/spring-ai-alibaba-autoconfigure-memory/src/main/java/com/alibaba/cloud/ai/autoconfigure/memory/ElasticsearchChatMemoryProperties.java @@ -13,19 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.alibaba.cloud.ai.memory.elasticsearch; + +package com.alibaba.cloud.ai.autoconfigure.memory; + +import org.springframework.boot.context.properties.ConfigurationProperties; import java.util.ArrayList; import java.util.List; /** - * Configuration class for Elasticsearch document reader. Contains all necessary settings - * to connect to and query Elasticsearch. - * - * @author brianxiadong - * @since 0.0.1 + * Configuration properties for ElasticSearch chat memory. */ -public class ElasticsearchConfig { +@ConfigurationProperties(prefix = "spring.ai.memory.elasticsearch") +public class ElasticsearchChatMemoryProperties { /** * Elasticsearch host URL @@ -72,12 +72,11 @@ public class ElasticsearchConfig { */ private String scheme = "http"; - // Getters and Setters public String getHost() { return host; } - public void setHost(String host) { + public void setHost(final String host) { this.host = host; } @@ -85,7 +84,7 @@ public int getPort() { return port; } - public void setPort(int port) { + public void setPort(final int port) { this.port = port; } @@ -93,7 +92,7 @@ public List getNodes() { return nodes; } - public void setNodes(List nodes) { + public void setNodes(final List nodes) { this.nodes = nodes; } @@ -101,7 +100,7 @@ public String getIndex() { return index; } - public void setIndex(String index) { + public void setIndex(final String index) { this.index = index; } @@ -109,7 +108,7 @@ public String getQueryField() { return queryField; } - public void setQueryField(String queryField) { + public void setQueryField(final String queryField) { this.queryField = queryField; } @@ -117,7 +116,7 @@ public String getUsername() { return username; } - public void setUsername(String username) { + public void setUsername(final String username) { this.username = username; } @@ -125,7 +124,7 @@ public String getPassword() { return password; } - public void setPassword(String password) { + public void setPassword(final String password) { this.password = password; } @@ -133,7 +132,7 @@ public int getMaxResults() { return maxResults; } - public void setMaxResults(int maxResults) { + public void setMaxResults(final int maxResults) { this.maxResults = maxResults; } @@ -141,7 +140,7 @@ public String getScheme() { return scheme; } - public void setScheme(String scheme) { + public void setScheme(final String scheme) { this.scheme = scheme; } diff --git a/auto-configurations/spring-ai-alibaba-autoconfigure-memory/src/main/java/com/alibaba/cloud/ai/autoconfigure/memory/RedisChatMemoryAutoConfiguration.java b/auto-configurations/spring-ai-alibaba-autoconfigure-memory/src/main/java/com/alibaba/cloud/ai/autoconfigure/memory/RedisChatMemoryAutoConfiguration.java index 6ffd67802e..941440055b 100644 --- a/auto-configurations/spring-ai-alibaba-autoconfigure-memory/src/main/java/com/alibaba/cloud/ai/autoconfigure/memory/RedisChatMemoryAutoConfiguration.java +++ b/auto-configurations/spring-ai-alibaba-autoconfigure-memory/src/main/java/com/alibaba/cloud/ai/autoconfigure/memory/RedisChatMemoryAutoConfiguration.java @@ -19,13 +19,11 @@ import com.alibaba.cloud.ai.memory.redis.RedisChatMemoryRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.ai.chat.memory.ChatMemoryRepository; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Primary; import redis.clients.jedis.Jedis; /** diff --git a/auto-configurations/spring-ai-alibaba-autoconfigure-memory/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/spring-ai-alibaba-autoconfigure-memory/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 3d6efa44b4..6dabf3608b 100644 --- a/auto-configurations/spring-ai-alibaba-autoconfigure-memory/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/auto-configurations/spring-ai-alibaba-autoconfigure-memory/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -20,3 +20,4 @@ com.alibaba.cloud.ai.autoconfigure.memory.OracleChatMemoryAutoConfiguration com.alibaba.cloud.ai.autoconfigure.memory.PostgresChatMemoryAutoConfiguration com.alibaba.cloud.ai.autoconfigure.memory.SqlServerChatMemoryAutoConfiguration com.alibaba.cloud.ai.autoconfigure.memory.RedisChatMemoryAutoConfiguration +com.alibaba.cloud.ai.autoconfigure.memory.ElasticsearchChatMemoryAutoConfiguration diff --git a/community/memories/spring-ai-alibaba-elasticsearch-memory/src/main/java/com/alibaba/cloud/ai/memory/elasticsearch/ElasticsearchChatMemoryRepository.java b/community/memories/spring-ai-alibaba-elasticsearch-memory/src/main/java/com/alibaba/cloud/ai/memory/elasticsearch/ElasticsearchChatMemoryRepository.java index 4983f2a6d6..4482c85de0 100644 --- a/community/memories/spring-ai-alibaba-elasticsearch-memory/src/main/java/com/alibaba/cloud/ai/memory/elasticsearch/ElasticsearchChatMemoryRepository.java +++ b/community/memories/spring-ai-alibaba-elasticsearch-memory/src/main/java/com/alibaba/cloud/ai/memory/elasticsearch/ElasticsearchChatMemoryRepository.java @@ -21,19 +21,8 @@ import co.elastic.clients.elasticsearch.core.DeleteByQueryResponse; import co.elastic.clients.elasticsearch.core.SearchResponse; import co.elastic.clients.elasticsearch.core.search.Hit; -import co.elastic.clients.json.jackson.JacksonJsonpMapper; -import co.elastic.clients.transport.ElasticsearchTransport; -import co.elastic.clients.transport.rest_client.RestClientTransport; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.http.HttpHost; -import org.apache.http.auth.AuthScope; -import org.apache.http.auth.UsernamePasswordCredentials; -import org.apache.http.client.CredentialsProvider; -import org.apache.http.conn.ssl.NoopHostnameVerifier; -import org.apache.http.impl.client.BasicCredentialsProvider; -import org.apache.http.ssl.SSLContextBuilder; -import org.elasticsearch.client.RestClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.chat.memory.ChatMemoryRepository; @@ -43,14 +32,8 @@ import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; -import javax.net.ssl.SSLContext; import java.io.IOException; -import java.security.KeyManagementException; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @@ -64,19 +47,18 @@ public class ElasticsearchChatMemoryRepository implements ChatMemoryRepository, private static final String INDEX_NAME = "chat_memory"; - private final ElasticsearchConfig config; + // private final ElasticsearchConfig config; private final ElasticsearchClient client; private final ObjectMapper objectMapper; - public ElasticsearchChatMemoryRepository(ElasticsearchConfig config) { - this.config = config; + public ElasticsearchChatMemoryRepository(ElasticsearchClient client) { this.objectMapper = new ObjectMapper(); // Configure Jackson to ignore unknown properties to handle schema changes this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); try { - this.client = createClient(); + this.client = client; createIndexIfNotExists(); } catch (Exception e) { @@ -106,50 +88,55 @@ public void recreateIndex() throws IOException { createIndex(); } - private ElasticsearchClient createClient() - throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException { - // Create HttpHosts for all nodes - HttpHost[] httpHosts; - if (!CollectionUtils.isEmpty(config.getNodes())) { - httpHosts = config.getNodes().stream().map(node -> { - String[] parts = node.split(":"); - return new HttpHost(parts[0], Integer.parseInt(parts[1]), config.getScheme()); - }).toArray(HttpHost[]::new); - } - else { - // Fallback to single node configuration - httpHosts = new HttpHost[] { new HttpHost(config.getHost(), config.getPort(), config.getScheme()) }; - } - - var restClientBuilder = RestClient.builder(httpHosts); - - // Add authentication if credentials are provided - if (StringUtils.hasText(config.getUsername()) && StringUtils.hasText(config.getPassword())) { - CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); - credentialsProvider.setCredentials(AuthScope.ANY, - new UsernamePasswordCredentials(config.getUsername(), config.getPassword())); - - // Create SSL context if using HTTPS - if ("https".equalsIgnoreCase(config.getScheme())) { - SSLContext sslContext = SSLContextBuilder.create() - .loadTrustMaterial(null, (chains, authType) -> true) - .build(); - - restClientBuilder.setHttpClientConfigCallback( - httpClientBuilder -> httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider) - .setSSLContext(sslContext) - .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE)); - } - else { - restClientBuilder.setHttpClientConfigCallback( - httpClientBuilder -> httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider)); - } - } - - // Create the transport and client - ElasticsearchTransport transport = new RestClientTransport(restClientBuilder.build(), new JacksonJsonpMapper()); - return new ElasticsearchClient(transport); - } + // private ElasticsearchClient createClient(ElasticsearchConfig config) + // throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException { + // // Create HttpHosts for all nodes + // HttpHost[] httpHosts; + // if (!CollectionUtils.isEmpty(config.getNodes())) { + // httpHosts = config.getNodes().stream().map(node -> { + // String[] parts = node.split(":"); + // return new HttpHost(parts[0], Integer.parseInt(parts[1]), config.getScheme()); + // }).toArray(HttpHost[]::new); + // } + // else { + // // Fallback to single node configuration + // httpHosts = new HttpHost[] { new HttpHost(config.getHost(), config.getPort(), + // config.getScheme()) }; + // } + // + // var restClientBuilder = RestClient.builder(httpHosts); + // + // // Add authentication if credentials are provided + // if (StringUtils.hasText(config.getUsername()) && + // StringUtils.hasText(config.getPassword())) { + // CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + // credentialsProvider.setCredentials(AuthScope.ANY, + // new UsernamePasswordCredentials(config.getUsername(), config.getPassword())); + // + // // Create SSL context if using HTTPS + // if ("https".equalsIgnoreCase(config.getScheme())) { + // SSLContext sslContext = SSLContextBuilder.create() + // .loadTrustMaterial(null, (chains, authType) -> true) + // .build(); + // + // restClientBuilder.setHttpClientConfigCallback( + // httpClientBuilder -> + // httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider) + // .setSSLContext(sslContext) + // .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE)); + // } + // else { + // restClientBuilder.setHttpClientConfigCallback( + // httpClientBuilder -> + // httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider)); + // } + // } + // + // // Create the transport and client + // ElasticsearchTransport transport = new + // RestClientTransport(restClientBuilder.build(), new JacksonJsonpMapper()); + // return new ElasticsearchClient(transport); + // } @Override public List findConversationIds() { diff --git a/community/memories/spring-ai-alibaba-elasticsearch-memory/src/test/java/com/alibaba/cloud/ai/memory/elasticsearch/ElasticsearchChatMemoryRepositoryIT.java b/community/memories/spring-ai-alibaba-elasticsearch-memory/src/test/java/com/alibaba/cloud/ai/memory/elasticsearch/ElasticsearchChatMemoryRepositoryIT.java index c71fd803d9..3276fcdc83 100644 --- a/community/memories/spring-ai-alibaba-elasticsearch-memory/src/test/java/com/alibaba/cloud/ai/memory/elasticsearch/ElasticsearchChatMemoryRepositoryIT.java +++ b/community/memories/spring-ai-alibaba-elasticsearch-memory/src/test/java/com/alibaba/cloud/ai/memory/elasticsearch/ElasticsearchChatMemoryRepositoryIT.java @@ -35,6 +35,12 @@ import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.utility.DockerImageName; +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.transport.rest_client.RestClientTransport; +import co.elastic.clients.json.jackson.JacksonJsonpMapper; +import org.elasticsearch.client.RestClient; +import org.elasticsearch.client.RestClientBuilder; +import org.apache.http.HttpHost; import java.util.List; import java.util.UUID; @@ -65,9 +71,9 @@ class ElasticsearchChatMemoryRepositoryIT { */ @DynamicPropertySource static void registerProperties(DynamicPropertyRegistry registry) { - registry.add("elasticsearch.host", elasticsearchContainer::getHost); - registry.add("elasticsearch.port", () -> elasticsearchContainer.getMappedPort(9200)); - registry.add("elasticsearch.scheme", () -> "http"); + registry.add("spring.ai.memory.elasticsearch.host", elasticsearchContainer::getHost); + registry.add("spring.ai.memory.elasticsearch.port", () -> elasticsearchContainer.getMappedPort(9200)); + registry.add("spring.ai.memory.elasticsearch.scheme", () -> "http"); } @Autowired @@ -279,11 +285,12 @@ static class TestConfiguration { @Bean ChatMemoryRepository chatMemoryRepository() { - ElasticsearchConfig config = new ElasticsearchConfig(); - config.setHost(elasticsearchContainer.getHost()); - config.setPort(elasticsearchContainer.getMappedPort(9200)); - config.setScheme("http"); - return new ElasticsearchChatMemoryRepository(config); + RestClientBuilder restClientBuilder = RestClient.builder( + new HttpHost(elasticsearchContainer.getHost(), elasticsearchContainer.getMappedPort(9200), "http")); + RestClient restClient = restClientBuilder.build(); + RestClientTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper()); + ElasticsearchClient client = new ElasticsearchClient(transport); + return new ElasticsearchChatMemoryRepository(client); } } diff --git a/spring-ai-alibaba-graph/spring-ai-alibaba-graph-example/src/main/resources/application.yml b/spring-ai-alibaba-graph/spring-ai-alibaba-graph-example/src/main/resources/application.yml index 1c261f4c66..b529850420 100644 --- a/spring-ai-alibaba-graph/spring-ai-alibaba-graph-example/src/main/resources/application.yml +++ b/spring-ai-alibaba-graph/spring-ai-alibaba-graph-example/src/main/resources/application.yml @@ -19,19 +19,19 @@ server: spring: application: - name: spring-ai-alibaba-helloworld + name: spring-ai-alibaba-graph-example ai: alibaba: toolcalling: weather: - enabled: true - api-key: ${WEATHER_API_KEY} + enabled: enable + api-key: 89c78da27b4d4fdfaa052020252704 dashscope: - api-key: ${DASH_SCOPE_API_KEY} + api-key: sk-b0ac526ee44f4bb7b6c61cbcaea14b15 openai: base-url: https://dashscope.aliyuncs.com/compatible-mode - api-key: ${AI_DASHSCOPE_API_KEY} + api-key: sk-b0ac526ee44f4bb7b6c61cbcaea14b15 chat: options: model: qwen-max-latest diff --git a/spring-ai-alibaba-spring-boot-starters/spring-ai-alibaba-starter-memory/pom.xml b/spring-ai-alibaba-spring-boot-starters/spring-ai-alibaba-starter-memory/pom.xml index 97f22fb6b2..ebb17f285d 100644 --- a/spring-ai-alibaba-spring-boot-starters/spring-ai-alibaba-starter-memory/pom.xml +++ b/spring-ai-alibaba-spring-boot-starters/spring-ai-alibaba-starter-memory/pom.xml @@ -48,11 +48,11 @@ ${revision} - - redis.clients - jedis - ${jedis.version} - + + + + + com.alibaba.cloud.ai @@ -60,10 +60,10 @@ ${revision} - - co.elastic.clients - elasticsearch-java - + + + + com.alibaba.cloud.ai From 3941921f4af865b7820db949dcc076413c53de73 Mon Sep 17 00:00:00 2001 From: aias00 Date: Thu, 29 May 2025 10:29:51 +0800 Subject: [PATCH 2/4] [fix] fix api key (#1020) --- .../src/main/resources/application.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-ai-alibaba-graph/spring-ai-alibaba-graph-example/src/main/resources/application.yml b/spring-ai-alibaba-graph/spring-ai-alibaba-graph-example/src/main/resources/application.yml index b529850420..2280395b32 100644 --- a/spring-ai-alibaba-graph/spring-ai-alibaba-graph-example/src/main/resources/application.yml +++ b/spring-ai-alibaba-graph/spring-ai-alibaba-graph-example/src/main/resources/application.yml @@ -26,12 +26,12 @@ spring: toolcalling: weather: enabled: enable - api-key: 89c78da27b4d4fdfaa052020252704 + api-key: ${WEATHER_API_KEY} dashscope: - api-key: sk-b0ac526ee44f4bb7b6c61cbcaea14b15 + api-key: ${DASH_SCOPE_API_KEY} openai: base-url: https://dashscope.aliyuncs.com/compatible-mode - api-key: sk-b0ac526ee44f4bb7b6c61cbcaea14b15 + api-key: ${AI_DASHSCOPE_API_KEY} chat: options: model: qwen-max-latest From 1bf4c4092ee5925227f3c715c751d48e1f470af1 Mon Sep 17 00:00:00 2001 From: Jack Xu Date: Thu, 29 May 2025 13:41:00 +0800 Subject: [PATCH 3/4] =?UTF-8?q?Fix=20Tool=20calls=20occur=20error=EF=BC=88?= =?UTF-8?q?NPE=EF=BC=89=20when=20using=20stream=20output=20=20(#1015)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes #1004 and #992 --- ...ashScopeAiStreamFunctionCallingHelper.java | 9 ++++--- .../cloud/ai/dashscope/api/DashScopeApi.java | 9 +++---- ...opeAiStreamFunctionCallingHelperTests.java | 25 ++++++++----------- .../spring-ai-alibaba-graph-core/pom.xml | 3 ++- .../graph/checkpoint/savers/MemorySaver.java | 12 ++++----- 5 files changed, 26 insertions(+), 32 deletions(-) diff --git a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/api/DashScopeAiStreamFunctionCallingHelper.java b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/api/DashScopeAiStreamFunctionCallingHelper.java index 560d5db950..fe956cb465 100644 --- a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/api/DashScopeAiStreamFunctionCallingHelper.java +++ b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/api/DashScopeAiStreamFunctionCallingHelper.java @@ -63,14 +63,15 @@ public ChatCompletionChunk merge(ChatCompletionChunk previous, ChatCompletionChu String id = (current.requestId() != null ? current.requestId() : previous.requestId()); TokenUsage usage = (current.usage() != null ? current.usage() : previous.usage()); - Choice previousChoice0 = previous.output() == null ? null : previous.output().choices().get(0); - Choice currentChoice0 = current.output() == null ? null : current.output().choices().get(0); + Choice previousChoice0 = previous.output() == null ? null + : CollectionUtils.isEmpty(previous.output().choices()) ? null : previous.output().choices().get(0); + Choice currentChoice0 = current.output() == null ? null + : CollectionUtils.isEmpty(current.output().choices()) ? null : current.output().choices().get(0); // compatibility of incremental_output false for streaming function call if (!incrementalOutput && isStreamingToolFunctionCall(current)) { if (!isStreamingToolFunctionCallFinish(current)) { - return new ChatCompletionChunk(id, new ChatCompletionOutput(null, List.of(new Choice(null, null))), - usage); + return new ChatCompletionChunk(id, new ChatCompletionOutput(null, List.of()), usage); } else { return new ChatCompletionChunk(id, new ChatCompletionOutput(null, List.of(currentChoice0)), usage); diff --git a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/api/DashScopeApi.java b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/api/DashScopeApi.java index de4adb574b..27a69fb0d7 100644 --- a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/api/DashScopeApi.java +++ b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/api/DashScopeApi.java @@ -65,9 +65,7 @@ import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.function.client.WebClient; -import static com.alibaba.cloud.ai.dashscope.common.DashScopeApiConstants.DEFAULT_BASE_URL; -import static com.alibaba.cloud.ai.dashscope.common.DashScopeApiConstants.DEFAULT_PARSER_NAME; -import static com.alibaba.cloud.ai.dashscope.common.DashScopeApiConstants.HEADER_WORK_SPACE_ID; +import static com.alibaba.cloud.ai.dashscope.common.DashScopeApiConstants.*; /** * @author nuocheng.lxm @@ -100,8 +98,6 @@ public class DashScopeApi { private final ResponseErrorHandler responseErrorHandler; - private DashScopeAiStreamFunctionCallingHelper chunkMerger = new DashScopeAiStreamFunctionCallingHelper(); - /** * Returns a builder pre-populated with the current configuration for mutation. */ @@ -1417,7 +1413,8 @@ public Flux chatCompletionStream(ChatCompletionRequest chat AtomicBoolean isInsideTool = new AtomicBoolean(false); boolean incrementalOutput = chatRequest.parameters() != null && chatRequest.parameters().incrementalOutput != null && chatRequest.parameters().incrementalOutput; - + DashScopeAiStreamFunctionCallingHelper chunkMerger = new DashScopeAiStreamFunctionCallingHelper( + incrementalOutput); String uri = "/api/v1/services/aigc/text-generation/generation"; if (chatRequest.multiModel()) { uri = "/api/v1/services/aigc/multimodal-generation/generation"; diff --git a/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/api/DashScopeAiStreamFunctionCallingHelperTests.java b/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/api/DashScopeAiStreamFunctionCallingHelperTests.java index 3c1b659990..2f0f5f1758 100644 --- a/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/api/DashScopeAiStreamFunctionCallingHelperTests.java +++ b/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/api/DashScopeAiStreamFunctionCallingHelperTests.java @@ -15,17 +15,8 @@ */ package com.alibaba.cloud.ai.dashscope.api; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - import java.util.List; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - import com.alibaba.cloud.ai.dashscope.api.DashScopeApi.ChatCompletionChunk; import com.alibaba.cloud.ai.dashscope.api.DashScopeApi.ChatCompletionFinishReason; import com.alibaba.cloud.ai.dashscope.api.DashScopeApi.ChatCompletionMessage; @@ -35,6 +26,10 @@ import com.alibaba.cloud.ai.dashscope.api.DashScopeApi.ChatCompletionOutput; import com.alibaba.cloud.ai.dashscope.api.DashScopeApi.ChatCompletionOutput.Choice; import com.alibaba.cloud.ai.dashscope.api.DashScopeApi.TokenUsage; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; /** * Tests for DashScopeAiStreamFunctionCallingHelper class functionality @@ -146,7 +141,8 @@ void testChunkToChatCompletion() { void testMergeWithNonIncrementalOutput() { // Test merging in non-incremental output mode ChatCompletionChunk previous = createChunkWithToolCall("request-1", "tool-1", "function-1", "{\"param"); - ChatCompletionChunk current = createChunkWithToolCall("request-1", "tool-1", "function-1", "\":\"value\"}"); + ChatCompletionChunk current = createChunkWithToolCall("request-1", "tool-1", "function-1", + "{\"param\":\"value\"}"); // Use helper with non-incremental output ChatCompletionChunk result = helper.merge(previous, current); @@ -156,16 +152,15 @@ void testMergeWithNonIncrementalOutput() { assertNotNull(result); assertEquals("request-1", result.requestId()); assertNotNull(result.output().choices()); - assertEquals(1, result.output().choices().size()); - assertNull(result.output().choices().get(0).message()); + assertEquals(0, result.output().choices().size()); } @Test void testMergeWithNonIncrementalOutputFinished() { // Test merging of finished tool calls in non-incremental output mode ChatCompletionChunk previous = createChunkWithToolCall("request-1", "tool-1", "function-1", "{\"param"); - ChatCompletionChunk current = createChunkWithToolCall("request-1", "tool-1", "function-1", "\":\"value\"}", - ChatCompletionFinishReason.TOOL_CALLS); + ChatCompletionChunk current = createChunkWithToolCall("request-1", "tool-1", "function-1", + "{\"param\":\"value\"}", ChatCompletionFinishReason.TOOL_CALLS); // Use helper with non-incremental output ChatCompletionChunk result = helper.merge(previous, current); @@ -182,7 +177,7 @@ void testMergeWithNonIncrementalOutputFinished() { assertEquals(1, toolCalls.size()); assertEquals("tool-1", toolCalls.get(0).id()); assertEquals("function-1", toolCalls.get(0).function().name()); - assertEquals("\":\"value\"}", toolCalls.get(0).function().arguments()); + assertEquals("{\"param\":\"value\"}", toolCalls.get(0).function().arguments()); } @Test diff --git a/spring-ai-alibaba-graph/spring-ai-alibaba-graph-core/pom.xml b/spring-ai-alibaba-graph/spring-ai-alibaba-graph-core/pom.xml index dcb3a50b0f..4993961d21 100644 --- a/spring-ai-alibaba-graph/spring-ai-alibaba-graph-core/pom.xml +++ b/spring-ai-alibaba-graph/spring-ai-alibaba-graph-core/pom.xml @@ -1,5 +1,6 @@ - +