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