diff --git a/agentscope-extensions/agentscope-extensions-chat-completions-web/src/main/java/io/agentscope/core/chat/completions/builder/ChatCompletionsResponseBuilder.java b/agentscope-extensions/agentscope-extensions-chat-completions-web/src/main/java/io/agentscope/core/chat/completions/builder/ChatCompletionsResponseBuilder.java index 0d2ac4701..0f3c89735 100644 --- a/agentscope-extensions/agentscope-extensions-chat-completions-web/src/main/java/io/agentscope/core/chat/completions/builder/ChatCompletionsResponseBuilder.java +++ b/agentscope-extensions/agentscope-extensions-chat-completions-web/src/main/java/io/agentscope/core/chat/completions/builder/ChatCompletionsResponseBuilder.java @@ -23,6 +23,7 @@ import io.agentscope.core.chat.completions.model.ChatMessage; import io.agentscope.core.chat.completions.model.ToolCall; import io.agentscope.core.message.ContentBlock; +import io.agentscope.core.message.GenerateReason; import io.agentscope.core.message.Msg; import io.agentscope.core.message.MsgRole; import io.agentscope.core.message.TextBlock; @@ -99,9 +100,13 @@ public ChatCompletionsResponse buildResponse( ChatMessage message = convertMsgToChatMessage(reply); choice.setMessage(message); - // Set finish_reason based on whether there are tool calls - if (message.getToolCalls() != null && !message.getToolCalls().isEmpty()) { + // Set finish_reason based on GenerateReason or tool calls + GenerateReason generateReason = reply != null ? reply.getGenerateReason() : null; + if (generateReason == GenerateReason.TOOL_SUSPENDED + || (message.getToolCalls() != null && !message.getToolCalls().isEmpty())) { choice.setFinishReason("tool_calls"); + } else if (generateReason == GenerateReason.MAX_ITERATIONS) { + choice.setFinishReason("length"); } else { choice.setFinishReason("stop"); } diff --git a/agentscope-extensions/agentscope-extensions-chat-completions-web/src/main/java/io/agentscope/core/chat/completions/converter/OpenAIToolConverter.java b/agentscope-extensions/agentscope-extensions-chat-completions-web/src/main/java/io/agentscope/core/chat/completions/converter/OpenAIToolConverter.java new file mode 100644 index 000000000..5f8013b8b --- /dev/null +++ b/agentscope-extensions/agentscope-extensions-chat-completions-web/src/main/java/io/agentscope/core/chat/completions/converter/OpenAIToolConverter.java @@ -0,0 +1,129 @@ +/* + * 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.chat.completions.converter; + +import io.agentscope.core.chat.completions.model.OpenAITool; +import io.agentscope.core.chat.completions.model.OpenAIToolFunction; +import io.agentscope.core.model.ToolSchema; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Converter for converting OpenAI tool format to AgentScope ToolSchema. + * + *

This converter handles the transformation from OpenAI's tool format (used in Chat Completions + * API requests) to AgentScope's internal ToolSchema format. Tools converted by this converter are + * intended to be registered as schema-only tools, which will trigger tool suspension when called. + */ +public class OpenAIToolConverter { + + private static final Logger log = LoggerFactory.getLogger(OpenAIToolConverter.class); + + /** + * Converts a list of OpenAI tools to AgentScope ToolSchemas. + * + *

Only tools with type "function" are converted. Other tool types are skipped with a warning. + * + * @param tools The list of OpenAI tools to convert (may be null or empty) + * @return A list of converted ToolSchema objects; returns an empty list if input is null or + * empty + */ + public List convertToToolSchemas(List tools) { + if (tools == null || tools.isEmpty()) { + return List.of(); + } + + List schemas = new ArrayList<>(); + + for (OpenAITool tool : tools) { + if (tool == null) { + log.warn("Skipping null tool in conversion"); + continue; + } + + // Only support function type tools for now + if (!"function".equals(tool.getType())) { + log.warn( + "Skipping tool with unsupported type: {}. Only 'function' type is" + + " supported", + tool.getType()); + continue; + } + + OpenAIToolFunction function = tool.getFunction(); + if (function == null) { + log.warn("Skipping tool with null function definition"); + continue; + } + + String name = function.getName(); + String description = function.getDescription(); + Map parameters = function.getParameters(); + + // Validate required fields + if (name == null || name.isBlank()) { + log.warn("Skipping tool with null or empty name"); + continue; + } + + if (description == null || description.isBlank()) { + log.warn("Skipping tool '{}' with null or empty description", name); + // Use empty string as fallback for description + description = ""; + } + + try { + ToolSchema.Builder schemaBuilder = + ToolSchema.builder().name(name).description(description); + + if (parameters != null) { + schemaBuilder.parameters(parameters); + } + + if (function.getStrict() != null) { + schemaBuilder.strict(function.getStrict()); + } + + ToolSchema schema = schemaBuilder.build(); + schemas.add(schema); + log.debug("Converted OpenAI tool to ToolSchema: {}", name); + + } catch (Exception e) { + log.error("Failed to convert tool '{}' to ToolSchema: {}", name, e.getMessage(), e); + } + } + + return schemas; + } + + /** + * Converts a single OpenAI tool to a ToolSchema. + * + * @param tool The OpenAI tool to convert + * @return The converted ToolSchema, or null if conversion fails + */ + public ToolSchema convertToToolSchema(OpenAITool tool) { + if (tool == null) { + return null; + } + + List schemas = convertToToolSchemas(List.of(tool)); + return schemas.isEmpty() ? null : schemas.get(0); + } +} diff --git a/agentscope-extensions/agentscope-extensions-chat-completions-web/src/main/java/io/agentscope/core/chat/completions/model/ChatCompletionsRequest.java b/agentscope-extensions/agentscope-extensions-chat-completions-web/src/main/java/io/agentscope/core/chat/completions/model/ChatCompletionsRequest.java index e942ca663..77661dfdb 100644 --- a/agentscope-extensions/agentscope-extensions-chat-completions-web/src/main/java/io/agentscope/core/chat/completions/model/ChatCompletionsRequest.java +++ b/agentscope-extensions/agentscope-extensions-chat-completions-web/src/main/java/io/agentscope/core/chat/completions/model/ChatCompletionsRequest.java @@ -71,6 +71,15 @@ public class ChatCompletionsRequest { /** Whether to stream responses via Server-Sent Events (SSE). Optional, defaults to false. */ private Boolean stream; + /** + * A list of tools the model may call. Currently, only functions are supported as a tool. + * + *

When tools are provided, they are registered as schema-only tools. When the agent decides + * to call a tool, execution is suspended and the tool call is returned to the client for + * external execution. + */ + private List tools; + public String getModel() { return model; } @@ -94,4 +103,12 @@ public Boolean getStream() { public void setStream(Boolean stream) { this.stream = stream; } + + public List getTools() { + return tools; + } + + public void setTools(List tools) { + this.tools = tools; + } } diff --git a/agentscope-extensions/agentscope-extensions-chat-completions-web/src/main/java/io/agentscope/core/chat/completions/model/OpenAITool.java b/agentscope-extensions/agentscope-extensions-chat-completions-web/src/main/java/io/agentscope/core/chat/completions/model/OpenAITool.java new file mode 100644 index 000000000..1dd16b0cc --- /dev/null +++ b/agentscope-extensions/agentscope-extensions-chat-completions-web/src/main/java/io/agentscope/core/chat/completions/model/OpenAITool.java @@ -0,0 +1,82 @@ +/* + * 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.chat.completions.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * OpenAI tool definition for Chat Completions API requests. + * + *

This class represents a tool that can be called by the model, following OpenAI's format. + * + *

Example: + *

{@code
+ * {
+ *   "type": "function",
+ *   "function": {
+ *     "name": "get_weather",
+ *     "description": "Get the current weather",
+ *     "parameters": {
+ *       "type": "object",
+ *       "properties": {
+ *         "location": {"type": "string"}
+ *       },
+ *       "required": ["location"]
+ *     }
+ *   }
+ * }
+ * }
+ */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class OpenAITool { + + /** Tool type, always "function" for now. */ + @JsonProperty("type") + private String type = "function"; + + /** The function definition. */ + @JsonProperty("function") + private OpenAIToolFunction function; + + /** Default constructor for deserialization. */ + public OpenAITool() {} + + /** + * Creates a new OpenAITool with the specified function. + * + * @param function The function definition + */ + public OpenAITool(OpenAIToolFunction function) { + this.function = function; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public OpenAIToolFunction getFunction() { + return function; + } + + public void setFunction(OpenAIToolFunction function) { + this.function = function; + } +} diff --git a/agentscope-extensions/agentscope-extensions-chat-completions-web/src/main/java/io/agentscope/core/chat/completions/model/OpenAIToolFunction.java b/agentscope-extensions/agentscope-extensions-chat-completions-web/src/main/java/io/agentscope/core/chat/completions/model/OpenAIToolFunction.java new file mode 100644 index 000000000..a0ac995c1 --- /dev/null +++ b/agentscope-extensions/agentscope-extensions-chat-completions-web/src/main/java/io/agentscope/core/chat/completions/model/OpenAIToolFunction.java @@ -0,0 +1,95 @@ +/* + * 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.chat.completions.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; + +/** + * OpenAI tool function definition for Chat Completions API requests. + * + *

This class represents the function definition in a tool, following OpenAI's format. + * + *

Example: + *

{@code
+ * {
+ *   "name": "get_weather",
+ *   "description": "Get the current weather",
+ *   "parameters": {
+ *     "type": "object",
+ *     "properties": {
+ *       "location": {"type": "string"}
+ *     },
+ *     "required": ["location"]
+ *   }
+ * }
+ * }
+ */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class OpenAIToolFunction { + + /** The name of the function. */ + @JsonProperty("name") + private String name; + + /** The description of the function. */ + @JsonProperty("description") + private String description; + + /** The JSON Schema for the function parameters. */ + @JsonProperty("parameters") + private Map parameters; + + /** Whether to enable strict mode for schema validation. */ + @JsonProperty("strict") + private Boolean strict; + + /** Default constructor for deserialization. */ + public OpenAIToolFunction() {} + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Map getParameters() { + return parameters; + } + + public void setParameters(Map parameters) { + this.parameters = parameters; + } + + public Boolean getStrict() { + return strict; + } + + public void setStrict(Boolean strict) { + this.strict = strict; + } +} diff --git a/agentscope-extensions/agentscope-extensions-chat-completions-web/src/test/java/io/agentscope/core/chat/completions/converter/OpenAIToolConverterTest.java b/agentscope-extensions/agentscope-extensions-chat-completions-web/src/test/java/io/agentscope/core/chat/completions/converter/OpenAIToolConverterTest.java new file mode 100644 index 000000000..7756b120d --- /dev/null +++ b/agentscope-extensions/agentscope-extensions-chat-completions-web/src/test/java/io/agentscope/core/chat/completions/converter/OpenAIToolConverterTest.java @@ -0,0 +1,296 @@ +/* + * 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.chat.completions.converter; + +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.assertTrue; + +import io.agentscope.core.chat.completions.model.OpenAITool; +import io.agentscope.core.chat.completions.model.OpenAIToolFunction; +import io.agentscope.core.model.ToolSchema; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link OpenAIToolConverter}. + */ +@DisplayName("OpenAIToolConverter Tests") +class OpenAIToolConverterTest { + + private OpenAIToolConverter converter; + + @BeforeEach + void setUp() { + converter = new OpenAIToolConverter(); + } + + @Nested + @DisplayName("Convert To ToolSchemas Tests") + class ConvertToToolSchemasTests { + + @Test + @DisplayName("Should convert valid function tool to ToolSchema") + void shouldConvertValidFunctionToolToToolSchema() { + OpenAIToolFunction function = new OpenAIToolFunction(); + function.setName("get_weather"); + function.setDescription("Get weather for a location"); + function.setParameters( + Map.of( + "type", + "object", + "properties", + Map.of("location", Map.of("type", "string")))); + + OpenAITool tool = new OpenAITool(function); + + List schemas = converter.convertToToolSchemas(List.of(tool)); + + assertEquals(1, schemas.size()); + ToolSchema schema = schemas.get(0); + assertEquals("get_weather", schema.getName()); + assertEquals("Get weather for a location", schema.getDescription()); + assertNotNull(schema.getParameters()); + } + + @Test + @DisplayName("Should return empty list for null input") + void shouldReturnEmptyListForNullInput() { + List schemas = converter.convertToToolSchemas(null); + + assertTrue(schemas.isEmpty()); + } + + @Test + @DisplayName("Should return empty list for empty input") + void shouldReturnEmptyListForEmptyInput() { + List schemas = converter.convertToToolSchemas(List.of()); + + assertTrue(schemas.isEmpty()); + } + + @Test + @DisplayName("Should skip null tools in list") + void shouldSkipNullToolsInList() { + OpenAIToolFunction function = new OpenAIToolFunction(); + function.setName("valid_tool"); + function.setDescription("Valid"); + OpenAITool validTool = new OpenAITool(function); + + List tools = new ArrayList<>(); + tools.add(validTool); + tools.add(null); + + List schemas = converter.convertToToolSchemas(tools); + + assertEquals(1, schemas.size()); + assertEquals("valid_tool", schemas.get(0).getName()); + } + + @Test + @DisplayName("Should skip non-function type tools") + void shouldSkipNonFunctionTypeTools() { + OpenAITool tool = new OpenAITool(); + tool.setType("code_interpreter"); // Not supported + + List schemas = converter.convertToToolSchemas(List.of(tool)); + + assertTrue(schemas.isEmpty()); + } + + @Test + @DisplayName("Should skip tools with null function") + void shouldSkipToolsWithNullFunction() { + OpenAITool tool = new OpenAITool(); + tool.setType("function"); + tool.setFunction(null); + + List schemas = converter.convertToToolSchemas(List.of(tool)); + + assertTrue(schemas.isEmpty()); + } + + @Test + @DisplayName("Should skip tools with null or empty name") + void shouldSkipToolsWithNullOrEmptyName() { + OpenAIToolFunction function1 = new OpenAIToolFunction(); + function1.setName(null); + function1.setDescription("Test"); + OpenAITool tool1 = new OpenAITool(function1); + + OpenAIToolFunction function2 = new OpenAIToolFunction(); + function2.setName(""); + function2.setDescription("Test"); + OpenAITool tool2 = new OpenAITool(function2); + + OpenAIToolFunction function3 = new OpenAIToolFunction(); + function3.setName(" "); + function3.setDescription("Test"); + OpenAITool tool3 = new OpenAITool(function3); + + List schemas = converter.convertToToolSchemas(List.of(tool1, tool2, tool3)); + + assertTrue(schemas.isEmpty()); + } + + @Test + @DisplayName("Should use empty string for null or empty description") + void shouldUseEmptyStringForNullOrEmptyDescription() { + OpenAIToolFunction function1 = new OpenAIToolFunction(); + function1.setName("tool1"); + function1.setDescription(null); + OpenAITool tool1 = new OpenAITool(function1); + + OpenAIToolFunction function2 = new OpenAIToolFunction(); + function2.setName("tool2"); + function2.setDescription(""); + OpenAITool tool2 = new OpenAITool(function2); + + List schemas = converter.convertToToolSchemas(List.of(tool1, tool2)); + + assertEquals(2, schemas.size()); + assertEquals("", schemas.get(0).getDescription()); + assertEquals("", schemas.get(1).getDescription()); + } + + @Test + @DisplayName("Should handle tools with null parameters") + void shouldHandleToolsWithNullParameters() { + OpenAIToolFunction function = new OpenAIToolFunction(); + function.setName("no_params"); + function.setDescription("No parameters"); + function.setParameters(null); + + OpenAITool tool = new OpenAITool(function); + + List schemas = converter.convertToToolSchemas(List.of(tool)); + + assertEquals(1, schemas.size()); + // ToolSchema converts null parameters to empty map + assertNotNull(schemas.get(0).getParameters()); + assertTrue(schemas.get(0).getParameters().isEmpty()); + } + + @Test + @DisplayName("Should preserve strict parameter") + void shouldPreserveStrictParameter() { + OpenAIToolFunction function = new OpenAIToolFunction(); + function.setName("strict_tool"); + function.setDescription("Strict tool"); + function.setStrict(true); + + OpenAITool tool = new OpenAITool(function); + + List schemas = converter.convertToToolSchemas(List.of(tool)); + + assertEquals(1, schemas.size()); + assertTrue(schemas.get(0).getStrict()); + } + + @Test + @DisplayName("Should convert multiple tools") + void shouldConvertMultipleTools() { + OpenAIToolFunction function1 = new OpenAIToolFunction(); + function1.setName("tool1"); + function1.setDescription("Tool 1"); + OpenAITool tool1 = new OpenAITool(function1); + + OpenAIToolFunction function2 = new OpenAIToolFunction(); + function2.setName("tool2"); + function2.setDescription("Tool 2"); + OpenAITool tool2 = new OpenAITool(function2); + + List schemas = converter.convertToToolSchemas(List.of(tool1, tool2)); + + assertEquals(2, schemas.size()); + assertEquals("tool1", schemas.get(0).getName()); + assertEquals("tool2", schemas.get(1).getName()); + } + + @Test + @DisplayName("Should handle complex parameters") + void shouldHandleComplexParameters() { + Map properties = new HashMap<>(); + properties.put("location", Map.of("type", "string", "description", "City name")); + properties.put( + "unit", Map.of("type", "string", "enum", List.of("celsius", "fahrenheit"))); + + Map parameters = new HashMap<>(); + parameters.put("type", "object"); + parameters.put("properties", properties); + parameters.put("required", List.of("location")); + + OpenAIToolFunction function = new OpenAIToolFunction(); + function.setName("get_weather"); + function.setDescription("Get weather"); + function.setParameters(parameters); + + OpenAITool tool = new OpenAITool(function); + + List schemas = converter.convertToToolSchemas(List.of(tool)); + + assertEquals(1, schemas.size()); + assertNotNull(schemas.get(0).getParameters()); + assertEquals(parameters, schemas.get(0).getParameters()); + } + } + + @Nested + @DisplayName("Convert To ToolSchema Tests") + class ConvertToToolSchemaTests { + + @Test + @DisplayName("Should convert single tool to ToolSchema") + void shouldConvertSingleToolToToolSchema() { + OpenAIToolFunction function = new OpenAIToolFunction(); + function.setName("single_tool"); + function.setDescription("Single tool"); + + OpenAITool tool = new OpenAITool(function); + + ToolSchema schema = converter.convertToToolSchema(tool); + + assertNotNull(schema); + assertEquals("single_tool", schema.getName()); + } + + @Test + @DisplayName("Should return null for null input") + void shouldReturnNullForNullInput() { + ToolSchema schema = converter.convertToToolSchema(null); + + assertNull(schema); + } + + @Test + @DisplayName("Should return null for invalid tool") + void shouldReturnNullForInvalidTool() { + OpenAITool tool = new OpenAITool(); + tool.setType("code_interpreter"); // Invalid type + + ToolSchema schema = converter.convertToToolSchema(tool); + + assertNull(schema); + } + } +} diff --git a/agentscope-extensions/agentscope-extensions-chat-completions-web/src/test/java/io/agentscope/core/chat/completions/model/ChatCompletionsRequestTest.java b/agentscope-extensions/agentscope-extensions-chat-completions-web/src/test/java/io/agentscope/core/chat/completions/model/ChatCompletionsRequestTest.java index 15c8ab017..e614db10f 100644 --- a/agentscope-extensions/agentscope-extensions-chat-completions-web/src/test/java/io/agentscope/core/chat/completions/model/ChatCompletionsRequestTest.java +++ b/agentscope-extensions/agentscope-extensions-chat-completions-web/src/test/java/io/agentscope/core/chat/completions/model/ChatCompletionsRequestTest.java @@ -173,6 +173,47 @@ void shouldHandleNullStream() { } } + @Nested + @DisplayName("Tools Tests") + class ToolsTests { + + @Test + @DisplayName("Should set and get tools") + void shouldSetAndGetTools() { + ChatCompletionsRequest request = new ChatCompletionsRequest(); + io.agentscope.core.chat.completions.model.OpenAIToolFunction function = + new io.agentscope.core.chat.completions.model.OpenAIToolFunction(); + function.setName("get_weather"); + function.setDescription("Get weather"); + io.agentscope.core.chat.completions.model.OpenAITool tool = + new io.agentscope.core.chat.completions.model.OpenAITool(function); + + request.setTools(List.of(tool)); + + assertNotNull(request.getTools()); + assertEquals(1, request.getTools().size()); + assertEquals("get_weather", request.getTools().get(0).getFunction().getName()); + } + + @Test + @DisplayName("Should handle null tools") + void shouldHandleNullTools() { + ChatCompletionsRequest request = new ChatCompletionsRequest(); + + assertNull(request.getTools()); + } + + @Test + @DisplayName("Should handle empty tools list") + void shouldHandleEmptyToolsList() { + ChatCompletionsRequest request = new ChatCompletionsRequest(); + request.setTools(new ArrayList<>()); + + assertNotNull(request.getTools()); + assertTrue(request.getTools().isEmpty()); + } + } + @Nested @DisplayName("Complete Request Tests") class CompleteRequestTests { @@ -192,5 +233,28 @@ void shouldBuildCompleteRequest() { assertEquals(2, request.getMessages().size()); assertFalse(request.getStream()); } + + @Test + @DisplayName("Should build complete request with tools") + void shouldBuildCompleteRequestWithTools() { + ChatCompletionsRequest request = new ChatCompletionsRequest(); + request.setModel("gpt-4"); + request.setMessages(List.of(new ChatMessage("user", "Hello"))); + request.setStream(false); + + io.agentscope.core.chat.completions.model.OpenAIToolFunction function = + new io.agentscope.core.chat.completions.model.OpenAIToolFunction(); + function.setName("get_weather"); + function.setDescription("Get weather"); + io.agentscope.core.chat.completions.model.OpenAITool tool = + new io.agentscope.core.chat.completions.model.OpenAITool(function); + request.setTools(List.of(tool)); + + assertEquals("gpt-4", request.getModel()); + assertEquals(1, request.getMessages().size()); + assertFalse(request.getStream()); + assertNotNull(request.getTools()); + assertEquals(1, request.getTools().size()); + } } } diff --git a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-chat-completions-web-starter/src/main/java/io/agentscope/spring/boot/chat/config/ChatCompletionsWebAutoConfiguration.java b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-chat-completions-web-starter/src/main/java/io/agentscope/spring/boot/chat/config/ChatCompletionsWebAutoConfiguration.java index d21d9f040..7c26bba66 100644 --- a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-chat-completions-web-starter/src/main/java/io/agentscope/spring/boot/chat/config/ChatCompletionsWebAutoConfiguration.java +++ b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-chat-completions-web-starter/src/main/java/io/agentscope/spring/boot/chat/config/ChatCompletionsWebAutoConfiguration.java @@ -18,6 +18,7 @@ import io.agentscope.core.ReActAgent; import io.agentscope.core.chat.completions.builder.ChatCompletionsResponseBuilder; import io.agentscope.core.chat.completions.converter.ChatMessageConverter; +import io.agentscope.core.chat.completions.converter.OpenAIToolConverter; import io.agentscope.core.chat.completions.streaming.ChatCompletionsStreamingAdapter; import io.agentscope.spring.boot.chat.service.ChatCompletionsStreamingService; import io.agentscope.spring.boot.chat.web.ChatCompletionsController; @@ -71,6 +72,17 @@ public ChatMessageConverter chatMessageConverter() { return new ChatMessageConverter(); } + /** + * Create the OpenAI tool converter bean. + * + * @return A new {@link OpenAIToolConverter} instance for converting OpenAI tools to ToolSchema + */ + @Bean + @ConditionalOnMissingBean + public OpenAIToolConverter openAIToolConverter() { + return new OpenAIToolConverter(); + } + /** * Create the response builder bean. * @@ -121,6 +133,7 @@ public ChatCompletionsStreamingService chatCompletionsStreamingService( * @param messageConverter Converter for HTTP DTOs to framework messages * @param responseBuilder Builder for response objects * @param streamingService Service for streaming responses + * @param toolConverter Converter for OpenAI tools to ToolSchema * @return The configured ChatCompletionsController bean */ @Bean @@ -129,8 +142,9 @@ public ChatCompletionsController chatCompletionsController( ObjectProvider agentProvider, ChatMessageConverter messageConverter, ChatCompletionsResponseBuilder responseBuilder, - ChatCompletionsStreamingService streamingService) { + ChatCompletionsStreamingService streamingService, + OpenAIToolConverter toolConverter) { return new ChatCompletionsController( - agentProvider, messageConverter, responseBuilder, streamingService); + agentProvider, messageConverter, responseBuilder, streamingService, toolConverter); } } diff --git a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-chat-completions-web-starter/src/main/java/io/agentscope/spring/boot/chat/web/ChatCompletionsController.java b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-chat-completions-web-starter/src/main/java/io/agentscope/spring/boot/chat/web/ChatCompletionsController.java index 711a8ddaa..d3bc85027 100644 --- a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-chat-completions-web-starter/src/main/java/io/agentscope/spring/boot/chat/web/ChatCompletionsController.java +++ b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-chat-completions-web-starter/src/main/java/io/agentscope/spring/boot/chat/web/ChatCompletionsController.java @@ -18,6 +18,7 @@ import io.agentscope.core.ReActAgent; import io.agentscope.core.chat.completions.builder.ChatCompletionsResponseBuilder; import io.agentscope.core.chat.completions.converter.ChatMessageConverter; +import io.agentscope.core.chat.completions.converter.OpenAIToolConverter; import io.agentscope.core.chat.completions.model.ChatCompletionsRequest; import io.agentscope.core.chat.completions.model.ChatCompletionsResponse; import io.agentscope.core.message.Msg; @@ -83,6 +84,7 @@ public class ChatCompletionsController { private final ChatMessageConverter messageConverter; private final ChatCompletionsResponseBuilder responseBuilder; private final ChatCompletionsStreamingService streamingService; + private final OpenAIToolConverter toolConverter; /** * Constructs a new ChatCompletionsController. @@ -91,16 +93,19 @@ public class ChatCompletionsController { * @param messageConverter Converter for HTTP DTOs to framework messages * @param responseBuilder Builder for response objects * @param streamingService Service for streaming responses + * @param toolConverter Converter for OpenAI tools to ToolSchema */ public ChatCompletionsController( ObjectProvider agentProvider, ChatMessageConverter messageConverter, ChatCompletionsResponseBuilder responseBuilder, - ChatCompletionsStreamingService streamingService) { + ChatCompletionsStreamingService streamingService, + OpenAIToolConverter toolConverter) { this.agentProvider = agentProvider; this.messageConverter = messageConverter; this.responseBuilder = responseBuilder; this.streamingService = streamingService; + this.toolConverter = toolConverter; } /** @@ -147,6 +152,18 @@ public Object createCompletion(@Valid @RequestBody ChatCompletionsRequest reques "Failed to create ReActAgent: agentProvider returned null")); } + // Register schema-only tools from request if provided + if (request.getTools() != null && !request.getTools().isEmpty()) { + var toolSchemas = toolConverter.convertToToolSchemas(request.getTools()); + if (!toolSchemas.isEmpty()) { + agent.getToolkit().registerSchemas(toolSchemas); + log.debug( + "Registered {} schema-only tools from request: requestId={}", + toolSchemas.size(), + requestId); + } + } + // Convert all messages from the request List messages = messageConverter.convertMessages(request.getMessages()); if (messages.isEmpty()) { @@ -230,6 +247,18 @@ public Flux> createCompletionStream( "Failed to create ReActAgent: agentProvider returned null")); } + // Register schema-only tools from request if provided + if (request.getTools() != null && !request.getTools().isEmpty()) { + var toolSchemas = toolConverter.convertToToolSchemas(request.getTools()); + if (!toolSchemas.isEmpty()) { + agent.getToolkit().registerSchemas(toolSchemas); + log.debug( + "Registered {} schema-only tools from request: requestId={}", + toolSchemas.size(), + requestId); + } + } + // Convert all messages from the request List messages = messageConverter.convertMessages(request.getMessages()); if (messages.isEmpty()) { diff --git a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-chat-completions-web-starter/src/test/java/io/agentscope/spring/boot/chat/web/ChatCompletionsControllerTest.java b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-chat-completions-web-starter/src/test/java/io/agentscope/spring/boot/chat/web/ChatCompletionsControllerTest.java index 93b20a786..d60b2d8cc 100644 --- a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-chat-completions-web-starter/src/test/java/io/agentscope/spring/boot/chat/web/ChatCompletionsControllerTest.java +++ b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-chat-completions-web-starter/src/test/java/io/agentscope/spring/boot/chat/web/ChatCompletionsControllerTest.java @@ -27,6 +27,7 @@ import io.agentscope.core.ReActAgent; import io.agentscope.core.chat.completions.builder.ChatCompletionsResponseBuilder; import io.agentscope.core.chat.completions.converter.ChatMessageConverter; +import io.agentscope.core.chat.completions.converter.OpenAIToolConverter; import io.agentscope.core.chat.completions.model.ChatCompletionsRequest; import io.agentscope.core.chat.completions.model.ChatCompletionsResponse; import io.agentscope.core.chat.completions.model.ChatMessage; @@ -61,6 +62,7 @@ class ChatCompletionsControllerTest { private ChatMessageConverter messageConverter; private ChatCompletionsResponseBuilder responseBuilder; private ChatCompletionsStreamingService streamingService; + private OpenAIToolConverter toolConverter; private ReActAgent mockAgent; @SuppressWarnings("unchecked") @@ -70,6 +72,7 @@ void setUp() { messageConverter = mock(ChatMessageConverter.class); responseBuilder = mock(ChatCompletionsResponseBuilder.class); streamingService = mock(ChatCompletionsStreamingService.class); + toolConverter = mock(OpenAIToolConverter.class); mockAgent = mock(ReActAgent.class); // Default: agentProvider returns mockAgent @@ -77,7 +80,11 @@ void setUp() { controller = new ChatCompletionsController( - agentProvider, messageConverter, responseBuilder, streamingService); + agentProvider, + messageConverter, + responseBuilder, + streamingService, + toolConverter); } @Nested