From e7fe4345e708a9919eeb5ee7f6ca5d26b64a7a88 Mon Sep 17 00:00:00 2001 From: guanxu <1510424541@qq.com> Date: Tue, 24 Mar 2026 15:32:49 +0800 Subject: [PATCH] feat: Improve model tool call - OpenAI and DashScope models support parallel tool calls - DashScope support native tools (web_extractor, code_interpreter) --- .../dashscope/DashScopeToolsHelper.java | 7 + .../dashscope/dto/DashScopeParameters.java | 62 ++++- .../dashscope/dto/DashScopeSearchOptions.java | 241 ++++++++++++++++++ .../formatter/openai/OpenAIChatFormatter.java | 7 + .../core/model/DashScopeHttpClient.java | 2 +- .../core/model/GenerateOptions.java | 29 +++ .../dto/DashScopeSearchOptionsTest.java | 100 ++++++++ .../core/model/DashScopeChatModelTest.java | 206 ++++++++++++++- .../core/model/OpenAIChatModelTest.java | 66 ++++- 9 files changed, 713 insertions(+), 7 deletions(-) create mode 100644 agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/dto/DashScopeSearchOptions.java create mode 100644 agentscope-core/src/test/java/io/agentscope/core/formatter/dashscope/dto/DashScopeSearchOptionsTest.java diff --git a/agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/DashScopeToolsHelper.java b/agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/DashScopeToolsHelper.java index 7a5a9eb57..cfa6d346d 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/DashScopeToolsHelper.java +++ b/agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/DashScopeToolsHelper.java @@ -96,6 +96,13 @@ public void applyOptions( if (presencePenalty != null) { params.setPresencePenalty(presencePenalty); } + + // Apply parallel tool calls + Boolean parallelToolCalls = + getOption(options, defaultOptions, GenerateOptions::getParallelToolCalls); + if (parallelToolCalls != null) { + params.setParallelToolCalls(parallelToolCalls); + } } /** diff --git a/agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/dto/DashScopeParameters.java b/agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/dto/DashScopeParameters.java index 520b9632b..1832c924d 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/dto/DashScopeParameters.java +++ b/agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/dto/DashScopeParameters.java @@ -60,13 +60,21 @@ public class DashScopeParameters { @JsonProperty("enable_thinking") private Boolean enableThinking; + /** Token budget for thinking. */ + @JsonProperty("thinking_budget") + private Integer thinkingBudget; + /** Enable search mode. */ @JsonProperty("enable_search") private Boolean enableSearch; - /** Token budget for thinking. */ - @JsonProperty("thinking_budget") - private Integer thinkingBudget; + /** Strategies for web search. */ + @JsonProperty("search_options") + private DashScopeSearchOptions searchOptions; + + /** Enable code interpreter mode. */ + @JsonProperty("enable_code_interpreter") + private Boolean enableCodeInterpreter; /** List of available tools. */ @JsonProperty("tools") @@ -79,6 +87,10 @@ public class DashScopeParameters { @JsonProperty("tool_choice") private Object toolChoice; + /** Whether to enable parallel tool calls. */ + @JsonProperty("parallel_tool_calls") + private Boolean parallelToolCalls; + /** Random seed for reproducibility. */ @JsonProperty("seed") private Integer seed; @@ -181,6 +193,22 @@ public void setEnableSearch(Boolean enableSearch) { this.enableSearch = enableSearch; } + public DashScopeSearchOptions getSearchOptions() { + return searchOptions; + } + + public void setSearchOptions(DashScopeSearchOptions searchOptions) { + this.searchOptions = searchOptions; + } + + public Boolean getEnableCodeInterpreter() { + return enableCodeInterpreter; + } + + public void setEnableCodeInterpreter(Boolean enableCodeInterpreter) { + this.enableCodeInterpreter = enableCodeInterpreter; + } + public List getTools() { return tools; } @@ -197,6 +225,14 @@ public void setToolChoice(Object toolChoice) { this.toolChoice = toolChoice; } + public Boolean getParallelToolCalls() { + return parallelToolCalls; + } + + public void setParallelToolCalls(Boolean parallelToolCalls) { + this.parallelToolCalls = parallelToolCalls; + } + public Integer getSeed() { return seed; } @@ -289,6 +325,21 @@ public Builder thinkingBudget(Integer thinkingBudget) { return this; } + public Builder enableSearch(Boolean enableSearch) { + params.setEnableSearch(enableSearch); + return this; + } + + public Builder searchOptions(DashScopeSearchOptions searchOptions) { + params.setSearchOptions(searchOptions); + return this; + } + + public Builder enableCodeInterpreter(Boolean enableCodeInterpreter) { + params.setEnableCodeInterpreter(enableCodeInterpreter); + return this; + } + public Builder tools(List tools) { params.setTools(tools); return this; @@ -299,6 +350,11 @@ public Builder toolChoice(Object toolChoice) { return this; } + public Builder parallelToolCalls(Boolean parallelToolCalls) { + params.setParallelToolCalls(parallelToolCalls); + return this; + } + public Builder seed(Integer seed) { params.setSeed(seed); return this; diff --git a/agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/dto/DashScopeSearchOptions.java b/agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/dto/DashScopeSearchOptions.java new file mode 100644 index 000000000..16ceb7659 --- /dev/null +++ b/agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/dto/DashScopeSearchOptions.java @@ -0,0 +1,241 @@ +/* + * Copyright 2024-2026 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 + * + * http://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 io.agentscope.core.formatter.dashscope.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * DashScope search options DTO. + * + *

This class represents the search strategy configuration in a DashScope API request. + * It is used to control internet search behavior when enable_search is true. + * + *

Example JSON: + *

{@code
+ * {
+ *   "enable_source": true,
+ *   "enable_citation": true,
+ *   "citation_format": "[]",
+ *   "forced_search": false,
+ *   "search_strategy": "turbo",
+ *   "enable_search_extension": false,
+ *   "prepend_search_result": false
+ * }
+ * }
+ */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class DashScopeSearchOptions { + + /** + * Whether to display searched information in the return results. + * Default value is false. + */ + @JsonProperty("enable_source") + private Boolean enableSource; + + /** + * Whether to enable citation markers like [1] or [ref_1]. + * Only effective when enable_source is true. + * Default value is false. + */ + @JsonProperty("enable_citation") + private Boolean enableCitation; + + /** + * The style of citation markers. + * Only effective when enable_citation is true. + * Default value is "[]". + * Optional values: "[]", "[ref_]". + */ + @JsonProperty("citation_format") + private String citationFormat; + + /** + * Whether to force enable search. + * Default value is false. + */ + @JsonProperty("forced_search") + private Boolean forcedSearch; + + /** + * The strategy for searching internet information. + * Default value is "turbo". + * Optional values: "turbo", "max", "agent", "agent_max". + */ + @JsonProperty("search_strategy") + private SearchStrategy searchStrategy; + + /** + * Whether to enable specific domain enhancement. + * Default value is false. + */ + @JsonProperty("enable_search_extension") + private Boolean enableSearchExtension; + + /** + * In streaming output with enable_source true, configures whether the first + * returned data packet contains only search source information. + * Default value is false. + * Note: Currently not supported in DashScope Java SDK according to docs, + * but included for completeness based on API spec. + */ + @JsonProperty("prepend_search_result") + private Boolean prependSearchResult; + + public DashScopeSearchOptions() {} + + public Boolean getEnableSource() { + return enableSource; + } + + public void setEnableSource(Boolean enableSource) { + this.enableSource = enableSource; + } + + public Boolean getEnableCitation() { + return enableCitation; + } + + public void setEnableCitation(Boolean enableCitation) { + this.enableCitation = enableCitation; + } + + public String getCitationFormat() { + return citationFormat; + } + + public void setCitationFormat(String citationFormat) { + this.citationFormat = citationFormat; + } + + public Boolean getForcedSearch() { + return forcedSearch; + } + + public void setForcedSearch(Boolean forcedSearch) { + this.forcedSearch = forcedSearch; + } + + public SearchStrategy getSearchStrategy() { + return searchStrategy; + } + + public void setSearchStrategy(SearchStrategy searchStrategy) { + this.searchStrategy = searchStrategy; + } + + public Boolean getEnableSearchExtension() { + return enableSearchExtension; + } + + public void setEnableSearchExtension(Boolean enableSearchExtension) { + this.enableSearchExtension = enableSearchExtension; + } + + public Boolean getPrependSearchResult() { + return prependSearchResult; + } + + public void setPrependSearchResult(Boolean prependSearchResult) { + this.prependSearchResult = prependSearchResult; + } + + public enum SearchStrategy { + /** + * Taking into account both responsiveness and search effect, it is suitable for most scenarios.(default) + */ + @JsonProperty("turbo") + TURBO, + + /** + * With a more comprehensive search strategy, call multi-source search engines for more exhaustive + * search results, but response times may be longer + */ + @JsonProperty("max") + MAX, + + /** + * Web search tools and large models can be called multiple times to achieve multiple rounds of information + * retrieval and content integration. + *

+ * This strategy only works with qwen3.5-plus, qwen3.5-plus-2026-02-15, qwen3.5-flash, qwen3.5-flash-2026-02-23, + * qwen3-max with qwen3-max-2026-01-23 (streaming only), qwen3-max-2026-01-23 non-thinking mode, qwen3-max-2025-09-23. + * When this strategy is enabled, only the search source (enable_source: true) is supported, + * and other web search options are not available. + */ + @JsonProperty("agent") + AGENT, + + /** + * Web extractor is supported on the basis of agent policies. + *

+ * This strategy is only applicable to qwen3.5-plus, qwen3.5-plus-2026-02-15, qwen3.5-flash, + * qwen3.5-flash-2026-02-23, and qwen3-max and qwen3-max-2026-01-23 thinking modes. + * When this strategy is enabled, only the search source (enable_source: true) is supported, + * and other web search options are not available. + */ + @JsonProperty("agent_max") + AGENT_MAX + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private final DashScopeSearchOptions searchOptions = new DashScopeSearchOptions(); + + public Builder enableSource(Boolean enableSource) { + searchOptions.setEnableSource(enableSource); + return this; + } + + public Builder enableCitation(Boolean enableCitation) { + searchOptions.setEnableCitation(enableCitation); + return this; + } + + public Builder citationFormat(String citationFormat) { + searchOptions.setCitationFormat(citationFormat); + return this; + } + + public Builder forcedSearch(Boolean forcedSearch) { + searchOptions.setForcedSearch(forcedSearch); + return this; + } + + public Builder searchStrategy(SearchStrategy searchStrategy) { + searchOptions.setSearchStrategy(searchStrategy); + return this; + } + + public Builder enableSearchExtension(Boolean enableSearchExtension) { + searchOptions.setEnableSearchExtension(enableSearchExtension); + return this; + } + + public Builder prependSearchResult(Boolean prependSearchResult) { + searchOptions.setPrependSearchResult(prependSearchResult); + return this; + } + + public DashScopeSearchOptions build() { + return searchOptions; + } + } +} diff --git a/agentscope-core/src/main/java/io/agentscope/core/formatter/openai/OpenAIChatFormatter.java b/agentscope-core/src/main/java/io/agentscope/core/formatter/openai/OpenAIChatFormatter.java index ffe9d657e..c83f5a2af 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/formatter/openai/OpenAIChatFormatter.java +++ b/agentscope-core/src/main/java/io/agentscope/core/formatter/openai/OpenAIChatFormatter.java @@ -112,6 +112,13 @@ public void applyOptions( request.setSeed(seed.intValue()); } + // Apply parallel tool calls + Boolean parallelToolCalls = + getOptionOrDefault(options, defaultOptions, GenerateOptions::getParallelToolCalls); + if (parallelToolCalls != null) { + request.setParallelToolCalls(parallelToolCalls); + } + // Apply additional body params (must be last to allow overriding) applyAdditionalBodyParams(request, defaultOptions); applyAdditionalBodyParams(request, options); diff --git a/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeHttpClient.java b/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeHttpClient.java index c554fff01..1790adccc 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeHttpClient.java +++ b/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeHttpClient.java @@ -484,7 +484,7 @@ public static PublicKeyResult fetchPublicKey( * @param publicKeyId the public key ID * @param publicKey the Base64-encoded public key */ - public static record PublicKeyResult(String publicKeyId, String publicKey) {} + public record PublicKeyResult(String publicKeyId, String publicKey) {} private Map buildHeaders( boolean streaming, Map additionalHeaders, EncryptionContext context) { diff --git a/agentscope-core/src/main/java/io/agentscope/core/model/GenerateOptions.java b/agentscope-core/src/main/java/io/agentscope/core/model/GenerateOptions.java index 90353947f..d71f8808e 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/model/GenerateOptions.java +++ b/agentscope-core/src/main/java/io/agentscope/core/model/GenerateOptions.java @@ -50,6 +50,7 @@ public class GenerateOptions { private final Integer topK; private final Long seed; private final Boolean cacheControl; + private final Boolean parallelToolCalls; private final Map additionalHeaders; private final Map additionalBodyParams; private final Map additionalQueryParams; @@ -78,6 +79,7 @@ private GenerateOptions(Builder builder) { this.topK = builder.topK; this.seed = builder.seed; this.cacheControl = builder.cacheControl; + this.parallelToolCalls = builder.parallelToolCalls; this.additionalHeaders = builder.additionalHeaders != null ? Collections.unmodifiableMap(new HashMap<>(builder.additionalHeaders)) @@ -320,6 +322,17 @@ public Boolean getCacheControl() { return cacheControl; } + /** + * Gets whether parallel tool calls are enabled. + * + *

When true, enable parallel function calling during tool use. + * + * @return true if parallel tool calls are enabled, false or null if not set + */ + public Boolean getParallelToolCalls() { + return parallelToolCalls; + } + /** * Gets the additional HTTP headers to include in API requests. * @@ -451,6 +464,10 @@ public static GenerateOptions mergeOptions(GenerateOptions primary, GenerateOpti builder.seed(primary.seed != null ? primary.seed : fallback.seed); builder.cacheControl( primary.cacheControl != null ? primary.cacheControl : fallback.cacheControl); + builder.parallelToolCalls( + primary.parallelToolCalls != null + ? primary.parallelToolCalls + : fallback.parallelToolCalls); // Merge map fields: fallback first, then override with primary mergeMaps(fallback.additionalHeaders, primary.additionalHeaders, builder::additionalHeader); @@ -502,6 +519,7 @@ public static class Builder { private Integer topK; private Long seed; private Boolean cacheControl; + private Boolean parallelToolCalls; private Map additionalHeaders; private Map additionalBodyParams; private Map additionalQueryParams; @@ -754,6 +772,17 @@ public Builder cacheControl(Boolean cacheControl) { return this; } + /** + * Sets whether to enable parallel function calling during tool use. + * + * @param parallelToolCalls true to enable parallel tool calls, false to disable + * @return this builder instance + */ + public Builder parallelToolCalls(Boolean parallelToolCalls) { + this.parallelToolCalls = parallelToolCalls; + return this; + } + /** * Adds an additional HTTP header to include in API requests. * diff --git a/agentscope-core/src/test/java/io/agentscope/core/formatter/dashscope/dto/DashScopeSearchOptionsTest.java b/agentscope-core/src/test/java/io/agentscope/core/formatter/dashscope/dto/DashScopeSearchOptionsTest.java new file mode 100644 index 000000000..c262ebd38 --- /dev/null +++ b/agentscope-core/src/test/java/io/agentscope/core/formatter/dashscope/dto/DashScopeSearchOptionsTest.java @@ -0,0 +1,100 @@ +/* + * Copyright 2024-2026 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 + * + * http://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 io.agentscope.core.formatter.dashscope.dto; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +/** + * Unit tests for the {@link DashScopeSearchOptions} class. + */ +class DashScopeSearchOptionsTest { + + @Test + void testConstructor() { + DashScopeSearchOptions searchOptions = new DashScopeSearchOptions(); + + assertNotNull(searchOptions); + assertNull(searchOptions.getEnableSource()); + assertNull(searchOptions.getEnableCitation()); + assertNull(searchOptions.getCitationFormat()); + assertNull(searchOptions.getForcedSearch()); + assertNull(searchOptions.getSearchStrategy()); + assertNull(searchOptions.getEnableSearchExtension()); + } + + @Test + void testGettersAndSetters() { + DashScopeSearchOptions searchOptions = new DashScopeSearchOptions(); + searchOptions.setEnableSource(true); + searchOptions.setEnableCitation(true); + searchOptions.setCitationFormat("[]"); + searchOptions.setForcedSearch(true); + searchOptions.setSearchStrategy(DashScopeSearchOptions.SearchStrategy.TURBO); + searchOptions.setEnableSearchExtension(true); + searchOptions.setPrependSearchResult(true); + + assertNotNull(searchOptions); + assertTrue(searchOptions.getEnableSource()); + assertTrue(searchOptions.getEnableCitation()); + assertEquals("[]", searchOptions.getCitationFormat()); + assertTrue(searchOptions.getForcedSearch()); + assertSame(DashScopeSearchOptions.SearchStrategy.TURBO, searchOptions.getSearchStrategy()); + assertTrue(searchOptions.getEnableSearchExtension()); + assertTrue(searchOptions.getPrependSearchResult()); + } + + @Test + void testBuilderDefault() { + DashScopeSearchOptions searchOptions = DashScopeSearchOptions.builder().build(); + + assertNotNull(searchOptions); + assertNull(searchOptions.getEnableSource()); + assertNull(searchOptions.getEnableCitation()); + assertNull(searchOptions.getCitationFormat()); + assertNull(searchOptions.getForcedSearch()); + assertNull(searchOptions.getSearchStrategy()); + assertNull(searchOptions.getEnableSearchExtension()); + } + + @Test + void testBuilderWithValues() { + DashScopeSearchOptions searchOptions = + DashScopeSearchOptions.builder() + .enableSource(true) + .enableCitation(true) + .citationFormat("[]") + .forcedSearch(true) + .searchStrategy(DashScopeSearchOptions.SearchStrategy.TURBO) + .enableSearchExtension(true) + .prependSearchResult(true) + .build(); + + assertNotNull(searchOptions); + assertTrue(searchOptions.getEnableSource()); + assertTrue(searchOptions.getEnableCitation()); + assertEquals("[]", searchOptions.getCitationFormat()); + assertTrue(searchOptions.getForcedSearch()); + assertSame(DashScopeSearchOptions.SearchStrategy.TURBO, searchOptions.getSearchStrategy()); + assertTrue(searchOptions.getEnableSearchExtension()); + assertTrue(searchOptions.getPrependSearchResult()); + } +} diff --git a/agentscope-core/src/test/java/io/agentscope/core/model/DashScopeChatModelTest.java b/agentscope-core/src/test/java/io/agentscope/core/model/DashScopeChatModelTest.java index f74ad57f5..25a3fef8c 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/model/DashScopeChatModelTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/model/DashScopeChatModelTest.java @@ -20,6 +20,7 @@ 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.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -27,11 +28,13 @@ import io.agentscope.core.formatter.dashscope.DashScopeMultiAgentFormatter; import io.agentscope.core.formatter.dashscope.dto.DashScopeParameters; import io.agentscope.core.formatter.dashscope.dto.DashScopeRequest; +import io.agentscope.core.formatter.dashscope.dto.DashScopeSearchOptions; import io.agentscope.core.message.Msg; import io.agentscope.core.message.MsgRole; import io.agentscope.core.message.TextBlock; import io.agentscope.core.model.test.ModelTestUtils; import io.agentscope.core.model.transport.OkHttpTransport; +import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; @@ -40,6 +43,7 @@ import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Tag; @@ -63,17 +67,31 @@ class DashScopeChatModelTest { private DashScopeChatModel model; private String mockApiKey; + private MockWebServer mockServer; @BeforeEach - void setUp() { + void setUp() throws IOException { mockApiKey = ModelTestUtils.createMockApiKey(); + mockServer = new MockWebServer(); + mockServer.start(); + String baseUrl = mockServer.url("/").toString().replaceAll("/$", ""); + // Create model with builder model = - DashScopeChatModel.builder().apiKey(mockApiKey).modelName("qwen-plus").stream(false) + DashScopeChatModel.builder() + .apiKey(mockApiKey) + .baseUrl(baseUrl) + .modelName("qwen-plus") + .stream(false) .build(); } + @AfterEach + void tearDown() throws IOException { + mockServer.shutdown(); + } + @Test @DisplayName("Should create model with valid configuration") void testBasicModelCreation() { @@ -957,6 +975,190 @@ void testCacheControlNotAppliedWhenDisabled() throws Exception { mockServer.shutdown(); } + @Test + @DisplayName("Should enable web_extractor tool when set search_strategy to agent_max") + void testEnableWebExtractorTool() throws Exception { + DashScopeSearchOptions searchOptions = + DashScopeSearchOptions.builder() + .searchStrategy(DashScopeSearchOptions.SearchStrategy.AGENT_MAX) + .build(); + DashScopeParameters parameters = + DashScopeParameters.builder().searchOptions(searchOptions).build(); + + mockServer.enqueue( + new MockResponse() + .setResponseCode(200) + .setBody( + """ + { + "request_id": "test", + "output": { + "choices": [] + } + } + """) + .setHeader("Content-Type", "application/json")); + + DashScopeChatModel chatModel = + DashScopeChatModel.builder().apiKey(mockApiKey).modelName("qwen-plus").stream(false) + .enableThinking(true) + .enableSearch(true) + .baseUrl(mockServer.url("/").toString().replaceAll("/$", "")) + .httpTransport(OkHttpTransport.builder().build()) + .defaultOptions( + GenerateOptions.builder() + .additionalBodyParam("search_options", searchOptions) + .build()) + .build(); + + chatModel + .doStream( + List.of( + Msg.builder() + .role(MsgRole.SYSTEM) + .content( + TextBlock.builder() + .text("You are helpful.") + .build()) + .build(), + Msg.builder() + .role(MsgRole.USER) + .content(TextBlock.builder().text("Hello").build()) + .build()), + List.of(), + GenerateOptions.builder().build()) + .blockLast(); + + RecordedRequest recorded = mockServer.takeRequest(); + String body = recorded.getBody().readUtf8(); + + assertNotNull(searchOptions); + assertNotNull(parameters); + assertSame( + DashScopeSearchOptions.SearchStrategy.AGENT_MAX, + parameters.getSearchOptions().getSearchStrategy()); + assertTrue( + body.contains("\"search_options\""), + "Request body should contain search_options" + body); + assertTrue( + body.contains("\"search_strategy\":\"agent_max\""), + "Request body should contain search_strategy with agent_max" + body); + } + + @Test + @DisplayName("Should enable code_interpreter tool when set enable_code_interpreter to true") + void testEnableCodeInterpreterTool() throws Exception { + DashScopeParameters parameters = + DashScopeParameters.builder().enableCodeInterpreter(true).build(); + + mockServer.enqueue( + new MockResponse() + .setResponseCode(200) + .setBody( + """ + { + "request_id": "test", + "output": { + "choices": [] + } + } + """) + .setHeader("Content-Type", "application/json")); + + DashScopeChatModel chatModel = + DashScopeChatModel.builder().apiKey(mockApiKey).modelName("qwen-plus").stream(false) + .baseUrl(mockServer.url("/").toString().replaceAll("/$", "")) + .httpTransport(OkHttpTransport.builder().build()) + .defaultOptions( + GenerateOptions.builder() + .additionalBodyParam("enable_code_interpreter", true) + .build()) + .build(); + + chatModel + .doStream( + List.of( + Msg.builder() + .role(MsgRole.SYSTEM) + .content( + TextBlock.builder() + .text("You are helpful.") + .build()) + .build(), + Msg.builder() + .role(MsgRole.USER) + .content(TextBlock.builder().text("Hello").build()) + .build()), + List.of(), + GenerateOptions.builder().build()) + .blockLast(); + + RecordedRequest recorded = mockServer.takeRequest(); + String body = recorded.getBody().readUtf8(); + + assertNotNull(parameters); + assertTrue(parameters.getEnableCodeInterpreter()); + assertTrue( + body.contains("\"enable_code_interpreter\":true"), + "Request body should contain enable_code_interpreter with true" + body); + mockServer.shutdown(); + } + + @Test + @DisplayName("Should enable parallel tool calls when set parallel_tool_calls to true") + void testEnableParallelToolCalls() throws Exception { + mockServer.enqueue( + new MockResponse() + .setResponseCode(200) + .setBody( + """ + { + "request_id": "test", + "output": { + "choices": [] + } + } + """) + .setHeader("Content-Type", "application/json")); + + ToolSchema tool1 = + ToolSchema.builder() + .name("get_weather") + .description("Get weather information") + .build(); + ToolSchema tool2 = + ToolSchema.builder().name("calculate").description("Perform calculations").build(); + + model.doStream( + List.of( + Msg.builder() + .role(MsgRole.SYSTEM) + .content( + TextBlock.builder() + .text("You are helpful.") + .build()) + .build(), + Msg.builder() + .role(MsgRole.USER) + .content( + TextBlock.builder() + .text( + "Get the weather of Shanghai and" + + " calculate 1+2+3.") + .build()) + .build()), + List.of(tool1, tool2), + GenerateOptions.builder().parallelToolCalls(true).build()) + .blockLast(); + + RecordedRequest recorded = mockServer.takeRequest(); + String body = recorded.getBody().readUtf8(); + + assertTrue( + body.contains("\"parallel_tool_calls\":true"), + "Request body should contain parallel_tool_calls with true" + body); + } + /** * Use reflection to invoke applyThinkingMode * diff --git a/agentscope-core/src/test/java/io/agentscope/core/model/OpenAIChatModelTest.java b/agentscope-core/src/test/java/io/agentscope/core/model/OpenAIChatModelTest.java index 617d77890..eab7c05b5 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/model/OpenAIChatModelTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/model/OpenAIChatModelTest.java @@ -228,7 +228,11 @@ void testApplyOptions() throws Exception { .build()); GenerateOptions options = - GenerateOptions.builder().temperature(0.7).maxTokens(1000).build(); + GenerateOptions.builder() + .temperature(0.7) + .maxTokens(1000) + .parallelToolCalls(true) + .build(); StepVerifier.create(model.stream(messages, null, options)) .assertNext(response -> assertNotNull(response)) @@ -240,6 +244,7 @@ void testApplyOptions() throws Exception { String body = request.getBody().readUtf8(); assertTrue(body.contains("\"temperature\":0.7")); assertTrue(body.contains("\"max_tokens\":1000")); + assertTrue(body.contains("\"parallel_tool_calls\":true")); } @Test @@ -466,4 +471,63 @@ void testBuildModelWithDefaultEndpointPath() throws Exception { || request.getPath().contains("/v1/chat/completions"), "Path should contain default endpoint path: " + request.getPath()); } + + @Test + @DisplayName("Should enable parallel tool calls when set parallel_tool_calls to true") + void testEnableParallelToolCalls() throws Exception { + String responseJson = + """ + { + "id": "chatcmpl-123", + "object": "chat.completion", + "created": 1677652280, + "model": "gpt-4", + "choices": [] + } + """; + + mockServer.enqueue( + new MockResponse() + .setBody(responseJson) + .setHeader("Content-Type", "application/json")); + + ToolSchema tool1 = + ToolSchema.builder() + .name("get_weather") + .description("Get weather information") + .build(); + ToolSchema tool2 = + ToolSchema.builder().name("calculate").description("Perform calculations").build(); + + model.doStream( + List.of( + Msg.builder() + .role(MsgRole.SYSTEM) + .content( + TextBlock.builder() + .text("You are helpful.") + .build()) + .build(), + Msg.builder() + .role(MsgRole.USER) + .content( + TextBlock.builder() + .text( + "Get the weather of Shanghai and" + + " calculate 1+2+3.") + .build()) + .build()), + List.of(tool1, tool2), + GenerateOptions.builder().parallelToolCalls(true).build()) + .blockLast(); + + RecordedRequest recorded = mockServer.takeRequest(); + String body = recorded.getBody().readUtf8(); + + assertTrue( + body.contains("\"parallel_tool_calls\":true"), + "Request body should contain parallel_tool_calls with true" + body); + + mockServer.shutdown(); + } }