diff --git a/cookbook/new-zh/sandbox/sandbox.md b/cookbook/new-zh/sandbox/sandbox.md index 6ddc6e39..264a9796 100644 --- a/cookbook/new-zh/sandbox/sandbox.md +++ b/cookbook/new-zh/sandbox/sandbox.md @@ -37,7 +37,7 @@ pip install agentscope-runtime 为了确保完整的沙箱体验并启用所有功能,请按照以下步骤从我们的仓库拉取并标记必要的 Docker 镜像: > **镜像来源:阿里云容器镜像服务** -> +> > 所有Docker镜像都托管在阿里云容器镜像服务(ACR)上,以在全球范围内实现可获取和可靠性。镜像从ACR拉取后使用标准名称重命名,以与AgentScope Runtime无缝集成。 ```bash @@ -179,14 +179,14 @@ public class Main { ```java try (Sandbox sandbox = sandboxService.connect("sessionId", "userId", BaseSandbox.class)){ - System.out.println(sandbox.listTools("")); - if(sandbox instanceof BaseSandbox baseSandbox) { - String pythonResult = baseSandbox.runIpythonCell("print('Hello from the sandbox!')"); + System.out.println(sandbox.listTools("")); + if(sandbox instanceof BaseSandbox baseSandbox) { +String pythonResult = baseSandbox.runIpythonCell("print('Hello from the sandbox!')"); System.out.println("Sandbox execution result: " + pythonResult); - String shellResult = baseSandbox.runShellCommand("echo Hello, World!"); +String shellResult = baseSandbox.runShellCommand("echo Hello, World!"); System.out.println("Shell command result: " + shellResult); } -} + } ``` * **GUI 沙箱 (GUI Sandbox)**: 提供**可视化桌面环境**,可执行鼠标、键盘以及屏幕相关操作。 @@ -195,19 +195,19 @@ try (Sandbox sandbox = sandboxService.connect("sessionId", "userId", BaseSandbox ```json try (Sandbox sandbox = sandboxService.connect("sessionId", "userId", GuiSandbox.class)){ - Gson gson = new Gson(); - String tools = gson.toJson(sandbox.listTools("")); - System.out.println("Available tools: "); - System.out.println(tools); - - if(sandbox instanceof GuiSandbox guiSandbox) { - String desktopUrl = guiSandbox.getDesktopUrl(); - System.out.println("GUI Desktop URL: " + desktopUrl); - String cursorPosition = guiSandbox.computerUse("get_cursor_position"); - System.out.println("Cursor Position: " + cursorPosition); - String screenShot = guiSandbox.computerUse("get_screenshot"); - System.out.println("Screenshot (base64): " + screenShot); - } +Gson gson = new Gson(); +String tools = gson.toJson(sandbox.listTools("")); +System.out.println("Available tools: "); +System.out.println(tools); + +if(sandbox instanceof GuiSandbox guiSandbox) { +String desktopUrl = guiSandbox.getDesktopUrl(); +System.out.println("GUI Desktop URL: " + desktopUrl); +String cursorPosition = guiSandbox.computerUse("get_cursor_position"); +System.out.println("Cursor Position: " + cursorPosition); +String screenShot = guiSandbox.computerUse("get_screenshot"); +System.out.println("Screenshot (base64): " + screenShot); +} } ``` @@ -217,18 +217,18 @@ try (Sandbox sandbox = sandboxService.connect("sessionId", "userId", GuiSandbox. ```java try (Sandbox sandbox = sandboxService.connect("sessionId", "userId", FilesystemSandbox.class)){ - Gson gson = new Gson(); - String tools = gson.toJson(sandbox.listTools("")); +Gson gson = new Gson(); +String tools = gson.toJson(sandbox.listTools("")); System.out.println("Available tools: "); System.out.println(tools); if(sandbox instanceof FilesystemSandbox filesystemSandbox) { - String desktopUrl = filesystemSandbox.getDesktopUrl(); +String desktopUrl = filesystemSandbox.getDesktopUrl(); System.out.println("GUI Desktop URL: " + desktopUrl); - String cursorPosition = filesystemSandbox.createDirectory("test"); +String cursorPosition = filesystemSandbox.createDirectory("test"); System.out.println("Created directory 'test' at: " + cursorPosition); } -} + } ``` * **浏览器沙箱(Browser Sandbox)**: 基于 GUI 的沙箱,可进行浏览器操作。 @@ -237,31 +237,31 @@ try (Sandbox sandbox = sandboxService.connect("sessionId", "userId", FilesystemS ```java try (Sandbox sandbox = sandboxService.connect("sessionId", "userId", BrowserSandbox.class)){ - Gson gson = new Gson(); - String tools = gson.toJson(sandbox.listTools("")); +Gson gson = new Gson(); +String tools = gson.toJson(sandbox.listTools("")); System.out.println("Available tools: "); System.out.println(tools); if(sandbox instanceof BrowserSandbox browserSandbox) { - String desktopUrl = browserSandbox.getDesktopUrl(); +String desktopUrl = browserSandbox.getDesktopUrl(); System.out.println("GUI Desktop URL: " + desktopUrl); - String navigateResult = browserSandbox.navigate("https://cn.bing.com"); +String navigateResult = browserSandbox.navigate("https://cn.bing.com"); System.out.println("Navigate Result: " + navigateResult); } -} + } ``` * **TrainingSandbox**:训练评估沙箱,详情请参考:[训练用沙箱](training_sandbox.md)。 ```java try (Sandbox sandbox = sandboxService.connect("sessionId", "userId", APPWorldSandbox.class)){ - if(sandbox instanceof APPWorldSandbox appWorldSandbox){ - String profileList = appWorldSandbox.getEnvProfile("appworld","train",null); + if(sandbox instanceof APPWorldSandbox appWorldSandbox){ +String profileList = appWorldSandbox.getEnvProfile("appworld","train",null); System.out.println("Profile List: " + profileList); } else { - System.err.println("Failed to connect to TrainingSandbox."); + System.err.println("Failed to connect to TrainingSandbox."); } -} + } ``` > 更多沙箱类型正在开发中,敬请期待! @@ -273,26 +273,38 @@ MCP(模型上下文协议)是一个标准化协议,使AI应用程序能够 沙箱支持通过`add_mcp_servers`方法集成MCP服务器。添加后,您可以使用`list_tools`发现可用工具并使用`call_tool`执行它们。 ```java -try { - String mcpServerConfig = """ - { - "mcpServers": { - "time": { - "command": "uvx", - "args": [ - "mcp-server-time", - "--local-timezone=America/New_York" - ] - } +try (Sandbox sandbox = sandboxService.connect("sessionId", "userId", BaseSandbox.class)) { +String mcpServerConfig = """ + { + "mcpServers": { + "time": { + "command": "uvx", + "args": [ + "mcp-server-time", + "--local-timezone=America/New_York" + ] } } - """; - List mcpTools = ToolkitInit.getMcpTools( - mcpServerConfig, - SandboxType.BASE, - sandboxService.getManagerApi()); + } + """; + +Gson gson = new Gson(); +Type mcpServerType = new TypeToken>() { +}.getType(); +Map serverConfigMap = gson.fromJson(mcpServerConfig, mcpServerType); + +// 将MCP服务器添加到沙箱 + sandbox.addMcpServers(serverConfigMap); + +// 列出所有可用工具(现在包括MCP工具) +String tools = gson.toJson(sandbox.listTools("")); + System.out.println("Available tools: "); + System.out.println(tools); - System.out.println("MCP Tools: " + mcpTools); +// 使用MCP服务器提供的时间工具 +String result = sandbox.callTool("get_current_time", Map.of("timezone", "America/New_York")); + System.out.println("Tool call result: "); + System.out.println(result); } ``` @@ -303,7 +315,7 @@ try { > * 多个客户端共享同一沙箱环境 > * 在资源受限的本地机器上开发,同时在高性能服务器上执行 > * K8s 集群部署沙盒服务 -> +> > 有关sandbox-server的更高级用法,请参阅[工具沙箱高级用法](sandbox_advanced.md)了解详细说明。 您可以在本地机器或不同机器上启动沙箱服务器,以便于远程访问。您可以先启动一个runtime,作为远程沙箱管理器 @@ -370,55 +382,68 @@ public class Main { ### 使用沙箱服务添加MCP服务器 -```{code-cell} -from agentscope_runtime.engine.services.sandbox import SandboxService - -async def main(): - sandbox_service = SandboxService() - await sandbox_service.start() - - session_id = "session_mcp" - user_id = "user_mcp" - - sandboxes = sandbox_service.connect( - session_id=session_id, - user_id=user_id, - sandbox_types=["base"], - ) +```java +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import io.agentscope.runtime.engine.services.sandbox.SandboxService; +import io.agentscope.runtime.sandbox.box.BaseSandbox; +import io.agentscope.runtime.sandbox.box.Sandbox; +import io.agentscope.runtime.sandbox.manager.SandboxManager; +import io.agentscope.runtime.sandbox.manager.client.config.BaseClientConfig; +import io.agentscope.runtime.sandbox.manager.client.config.DockerClientConfig; +import io.agentscope.runtime.sandbox.manager.model.ManagerConfig; - sandbox = sandboxes[0] +import java.lang.reflect.Type; +import java.util.Map; - mcp_server_configs = { - "mcpServers": { - "time": { - "command": "uvx", - "args": [ - "mcp-server-time", - "--local-timezone=America/New_York", - ], - }, - }, - } +public class Main { + public static void main(String[] args) { + BaseClientConfig clientConfig = DockerClientConfig.builder().build(); + ManagerConfig managerConfig = ManagerConfig.builder() + .containerDeployment(clientConfig) + .build(); + SandboxService sandboxService = new SandboxService( + new SandboxManager(managerConfig) + ); + sandboxService.start(); - # 将MCP服务器添加到沙箱 - sandbox.add_mcp_servers(server_configs=mcp_server_configs) + try (Sandbox sandbox = sandboxService.connect("sessionId", "userId", BaseSandbox.class)) { + String mcpServerConfig = """ + { + "mcpServers": { + "time": { + "command": "uvx", + "args": [ + "mcp-server-time", + "--local-timezone=America/New_York" + ] + } + } + } + """; - # 列出所有可用工具(现在包括MCP工具) - print(sandbox.list_tools()) + Gson gson = new Gson(); + Type mcpServerType = new TypeToken>() { + }.getType(); + Map serverConfigMap = gson.fromJson(mcpServerConfig, mcpServerType); - # 使用MCP服务器提供的时间工具 - print( - sandbox.call_tool( - "get_current_time", - arguments={ - "timezone": "America/New_York", - }, - ), - ) +// 将MCP服务器添加到沙箱 + sandbox.addMcpServers(serverConfigMap); - await sandbox_service.stop() +// 列出所有可用工具(现在包括MCP工具) + String tools = gson.toJson(sandbox.listTools("")); + System.out.println("Available tools: "); + System.out.println(tools); -await main() +// 使用MCP服务器提供的时间工具 + String result = sandbox.callTool("get_current_time", Map.of("timezone", "America/New_York")); + System.out.println("Tool call result: "); + System.out.println(result); + } catch (Exception e) { + e.printStackTrace(); + } + } +} ``` ### 使用沙箱服务连接远程沙箱 @@ -436,8 +461,8 @@ public class Main { public static void main(String[] args) { // 创建并启动沙箱服务 ManagerConfig managerConfig = ManagerConfig.builder() - .baseUrl("http://remote-host:port") - .build(); + .baseUrl("http://remote-host:port") + .build(); SandboxService sandboxService = new SandboxService( new SandboxManager(managerConfig) ); diff --git a/core/src/main/java/io/agentscope/runtime/adapters/AgentHandler.java b/core/src/main/java/io/agentscope/runtime/adapters/AgentHandler.java index 04c0e8cd..c82d3e02 100644 --- a/core/src/main/java/io/agentscope/runtime/adapters/AgentHandler.java +++ b/core/src/main/java/io/agentscope/runtime/adapters/AgentHandler.java @@ -16,6 +16,7 @@ package io.agentscope.runtime.adapters; import io.agentscope.runtime.engine.schemas.AgentRequest; +import io.agentscope.runtime.engine.services.sandbox.SandboxService; import reactor.core.publisher.Flux; /** @@ -35,6 +36,7 @@ * */ public interface AgentHandler { + SandboxService getSandboxService(); String getName(); diff --git a/core/src/main/java/io/agentscope/runtime/adapters/agentscope/AgentScopeAgentHandler.java b/core/src/main/java/io/agentscope/runtime/adapters/agentscope/AgentScopeAgentHandler.java index 58445668..59dbc5e9 100644 --- a/core/src/main/java/io/agentscope/runtime/adapters/agentscope/AgentScopeAgentHandler.java +++ b/core/src/main/java/io/agentscope/runtime/adapters/agentscope/AgentScopeAgentHandler.java @@ -73,6 +73,10 @@ protected AgentScopeAgentHandler() { this.messageAdapter = new AgentScopeMessageAdapter(); } + public SandboxService getSandboxService() { + return sandboxService; + } + public void setSessionHistoryService(SessionHistoryService sessionHistoryService) { this.sessionHistoryService = sessionHistoryService; } diff --git a/core/src/main/java/io/agentscope/runtime/engine/Runner.java b/core/src/main/java/io/agentscope/runtime/engine/Runner.java index f653c3d0..291339f7 100644 --- a/core/src/main/java/io/agentscope/runtime/engine/Runner.java +++ b/core/src/main/java/io/agentscope/runtime/engine/Runner.java @@ -274,10 +274,8 @@ public AgentHandler getAgent() { return adapter; } - /** FIXME - */ public SandboxManager getSandboxManager() { - return null; + return adapter.getSandboxService().getManagerApi(); } } diff --git a/examples/browser_use_fullstack_runtime/backend/src/main/java/io/agentscope/browser/agent/AgentscopeBrowserUseAgent.java b/examples/browser_use_fullstack_runtime/backend/src/main/java/io/agentscope/browser/agent/AgentscopeBrowserUseAgent.java index 4701a15a..ae050d55 100755 --- a/examples/browser_use_fullstack_runtime/backend/src/main/java/io/agentscope/browser/agent/AgentscopeBrowserUseAgent.java +++ b/examples/browser_use_fullstack_runtime/backend/src/main/java/io/agentscope/browser/agent/AgentscopeBrowserUseAgent.java @@ -141,9 +141,6 @@ public Flux streamQuery(AgentRequest request, Object messages) { if (sandboxService != null) { try { Sandbox sandbox = connect(sessionId, userId); - - // Register Python code execution tool (matching Python: execute_python_code) - toolkit.registerTool(ToolkitInit.RunPythonCodeTool(sandbox)); // Register browser navigation tool toolkit.registerTool(ToolkitInit.BrowserNavigateTool(sandbox)); diff --git a/examples/browser_use_fullstack_runtime/frontend/package-lock.json b/examples/browser_use_fullstack_runtime/frontend/package-lock.json index 0679651b..cad942fa 100755 --- a/examples/browser_use_fullstack_runtime/frontend/package-lock.json +++ b/examples/browser_use_fullstack_runtime/frontend/package-lock.json @@ -18202,23 +18202,6 @@ } } }, - "node_modules/tailwindcss/node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/tapable": { "version": "2.3.0", "resolved": "https://registry.npmmirror.com/tapable/-/tapable-2.3.0.tgz", diff --git a/examples/browser_use_fullstack_runtime/frontend/src/App.tsx b/examples/browser_use_fullstack_runtime/frontend/src/App.tsx index 35dc3603..3d845d29 100755 --- a/examples/browser_use_fullstack_runtime/frontend/src/App.tsx +++ b/examples/browser_use_fullstack_runtime/frontend/src/App.tsx @@ -91,20 +91,43 @@ const App: React.FC = () => { const data = await response.json(); console.log(data); if (data.baseUrl && data.runtimeToken) { - // Replace /fastapi with /vnc/vnc_lite.html and append password param const baseVncPath = data.baseUrl.replace("/fastapi", "/vnc/vnc_lite.html"); - // URL-encode password and append to URL params + console.log(baseVncPath); const encodedPassword = encodeURIComponent(data.runtimeToken); const vncUrl = `${baseVncPath}?password=${encodedPassword}`; + + await retryUntilVncReady(vncUrl); setVncUrl(vncUrl); } } + async function retryUntilVncReady(url: string, maxRetries = 5, delay = 1000) { + for (let i = 0; i < maxRetries; i++) { + try { + const response = await fetch(url, { method: "HEAD" }); + if (response.ok) { + console.log(`VNC service ready after ${i + 1} attempt(s)`); + return true; + } + } catch (error) { + console.log(`VNC not ready, retrying... (${i + 1}/${maxRetries})`); + } + + if (i < maxRetries - 1) { + await new Promise(resolve => setTimeout(resolve, delay)); + } + } + + console.warn("VNC service may not be fully ready, proceeding anyway"); + return false; + } + + const handleSend = async (message: string) => { - await getVncInfo(); if (message.trim() === "") { return; } + const newMessage = { message, sender: "user", @@ -121,10 +144,15 @@ const App: React.FC = () => { setMessages(newMessages); + getVncInfo().catch(error => { + console.error("Failed to initialize VNC:", error); + }); + setIsTyping(true); await processMessageToChatGPT(newMessages); }; + async function processMessageToChatGPT(chatMessages: ChatMessage) { let apiMessages = chatMessages .map((messageObject) => { @@ -185,10 +213,14 @@ const App: React.FC = () => { accumulatedMessage = lines.pop() || ""; for (const line of lines) { - if (line.trim() === "") continue; + const trimmed = line.trim(); + if (!trimmed || !trimmed.startsWith("data:")) continue; + + const payload = trimmed.replace(/^(data:\s*)+/, ""); + if (!payload || payload === "[DONE]") continue; try { - const parsed = JSON.parse(line.split("data: ")[1]); + const parsed = JSON.parse(payload); const delta = parsed.choices[0]?.delta || {}; const content = delta.content || ""; const messageType = delta.messageType; @@ -233,11 +265,9 @@ const App: React.FC = () => { useEffect(() => { if (listRef.current) { const container = listRef.current; - // 检查用户是否在底部附近(50px 范围内) - const isNearBottom = + const isNearBottom = container.scrollHeight - container.scrollTop - container.clientHeight < 50; - // 只有在底部附近时才自动滚动到底部 if (isNearBottom) { container.scrollTop = container.scrollHeight; } diff --git a/web/src/main/java/io/agentscope/runtime/LocalDeployManager.java b/web/src/main/java/io/agentscope/runtime/LocalDeployManager.java index 4c72c429..574b6bce 100644 --- a/web/src/main/java/io/agentscope/runtime/LocalDeployManager.java +++ b/web/src/main/java/io/agentscope/runtime/LocalDeployManager.java @@ -132,7 +132,7 @@ public static class LocalDeployerManagerBuilder { private String endpointName; private String host; private int port = 8080; - private List protocols = List.of(Protocol.A2A); + private List protocols = List.of(Protocol.A2A, Protocol.ResponseAPI); private List protocolConfigs = List.of(); public LocalDeployerManagerBuilder endpointName(String endpointName) { diff --git a/web/src/main/java/io/agentscope/runtime/protocol/Protocol.java b/web/src/main/java/io/agentscope/runtime/protocol/Protocol.java index f06ad08f..99380123 100644 --- a/web/src/main/java/io/agentscope/runtime/protocol/Protocol.java +++ b/web/src/main/java/io/agentscope/runtime/protocol/Protocol.java @@ -17,5 +17,6 @@ package io.agentscope.runtime.protocol; public enum Protocol { - A2A + A2A, + ResponseAPI } diff --git a/web/src/main/java/io/agentscope/runtime/protocol/responseapi/ResponseApiHandler.java b/web/src/main/java/io/agentscope/runtime/protocol/responseapi/ResponseApiHandler.java new file mode 100644 index 00000000..4f52df6e --- /dev/null +++ b/web/src/main/java/io/agentscope/runtime/protocol/responseapi/ResponseApiHandler.java @@ -0,0 +1,401 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.agentscope.runtime.protocol.responseapi; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.openai.models.realtime.*; +import com.openai.models.realtime.RealtimeResponse.Status; +import io.agentscope.runtime.engine.Runner; +import io.agentscope.runtime.engine.schemas.*; +import io.agentscope.runtime.protocol.responseapi.model.*; +import io.agentscope.runtime.protocol.responseapi.model.Error; +import reactor.core.publisher.Flux; + +import java.time.Instant; +import java.util.*; +import java.util.logging.Logger; + +/** + * Handler for OpenAI Responses API requests. + */ +public class ResponseApiHandler { + + private static final Logger logger = Logger.getLogger(ResponseApiHandler.class.getName()); + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private final Runner runner; + + public ResponseApiHandler(Runner runner, ResponseApiProtocolConfig config) { + this.runner = runner; + // Config is available for future use (e.g., timeout configuration) + } + + /** + * Handle chat completion request and return streaming response. + * + * @param requestBody the request body containing messages + * @return Flux of ResponseStreamEvent + */ + public Flux handleStreamingResponse(ResponseApiRequest requestBody) { + try { + Object input = requestBody.getInput(); + if (input == null) { + return Flux.just(createErrorEvent("No input provided", "invalid_request_error")); + } + + AgentRequest agentRequest = buildAgentRequest(requestBody); + + Flux eventFlux = runner.streamQuery(agentRequest); + + return handleStreamingFlux(eventFlux); + } catch (Exception e) { + logger.severe("Error handling chat completion: " + e.getMessage()); + return Flux.just(createErrorEvent("Error: " + e.getMessage(), "internal_error")); + } + } + + public Flux handleStreamingFlux(Flux eventFlux) { + StreamState streamState = new StreamState(); + return Flux.concat( + Flux.just(RealtimeServerEvent.ofResponseCreated( + ResponseCreatedEvent.builder() + .eventId(UUID.randomUUID().toString()) + .response( + RealtimeResponse.builder() + .id(streamState.responseId) + .status(Status.IN_PROGRESS) + .build() + ) + .build() + ) + ), + eventFlux.flatMap(event -> { + if (event instanceof Message || event instanceof Content) { + return convertMessageToStreamEvent(event, streamState); + } else { + return Flux.empty(); + } + }).onErrorResume(e -> { + logger.severe("Streaming error: " + e.getMessage()); + return Flux.just(createErrorEvent("Streaming error: " + e.getMessage(), "internal_error")); + }) + .concatWith(Flux.just( + RealtimeServerEvent.ofResponseDone( + ResponseDoneEvent.builder() + .eventId(UUID.randomUUID().toString()) + .response( + RealtimeResponse.builder() + .id(streamState.responseId) + .status(Status.COMPLETED) + .build() + ) + .build() + ) + )) + ); + + } + + /** + * Handle non-streaming request and return aggregated response. + */ + public ResponseApiResponse handleNonStreamingResponse(ResponseApiRequest requestBody) { + ResponseApiResponse response = new ResponseApiResponse(); + String responseId = "resp_" + UUID.randomUUID(); + Integer created = Math.toIntExact(Instant.now().getEpochSecond()); + + response.setId(responseId); + response.setObject("response"); + response.setCreatedAt(created); + response.setModel(requestBody.getModel()); + response.setBackground(requestBody.getBackground()); + response.setParallelToolCalls(requestBody.getParallelToolCalls()); + response.setPreviousResponseId(requestBody.getPreviousResponseId()); + response.setInstructions(requestBody.getInstructions()); + response.setMaxOutputTokens(requestBody.getMaxOutputTokens()); + response.setMaxToolCalls(requestBody.getMaxToolCalls()); + response.setMetadata(requestBody.getMetadata()); + response.setPrompt(requestBody.getPrompt()); + response.setPromptCacheKey(requestBody.getPromptCacheKey()); + response.setPromptCacheRetention(requestBody.getPromptCacheRetention()); + response.setReasoning(requestBody.getReasoning()); + response.setSafetyIdentifier(requestBody.getSafetyIdentifier()); + response.setToolChoice(requestBody.getToolChoice()); + if (requestBody.getTopP() != null) { + response.setTopP(requestBody.getTopP()); + } + response.setTruncation(requestBody.getTruncation()); + + try { + Object input = requestBody.getInput(); + if (input == null) { + Error error = new Error(); + error.setMessage("No input provided"); + error.setCode("invalid_request_error"); + response.setStatus("failed"); + response.setError(error); + return response; + } + + AgentRequest agentRequest = buildAgentRequest(requestBody); + + List messages = runner.streamQuery(agentRequest) + .collectList() + .block(); + + if (messages == null) { + messages = List.of(); + } + + List outputItems = buildOutputItems(messages); + response.setOutput(outputItems); + response.setStatus("completed"); + response.setUsage(buildUsagePlaceholder()); + + } catch (Exception e) { + logger.severe("Error handling non-streaming response: " + e.getMessage()); + Error error = new Error(); + error.setMessage(e.getMessage()); + error.setCode("internal_error"); + response.setStatus("failed"); + response.setError(error); + } + return response; + } + + private String getUserId(ResponseApiRequest requestBody) { + if (requestBody.getMetadata() != null && requestBody.getMetadata().containsKey("userId")) { + return String.valueOf(requestBody.getMetadata().get("userId")); + } + return "default_user"; + } + + private String getSessionId(ResponseApiRequest requestBody) { + if (requestBody.getConversation() != null && requestBody.getConversation().getId() != null) { + return requestBody.getConversation().getId(); + } + if (requestBody.getMetadata() != null && requestBody.getMetadata().containsKey("sessionId")) { + return String.valueOf(requestBody.getMetadata().get("sessionId")); + } + return "default_session"; + } + + private AgentRequest buildAgentRequest(ResponseApiRequest requestBody) { + String inputText = getTextFromMessageParts(requestBody.getInput()); + AgentRequest agentRequest = new AgentRequest(); + Message agentMessage = new Message(); + agentMessage.setType(MessageType.MESSAGE); + agentMessage.setRole(Role.USER); + TextContent tc = new TextContent(); + tc.setText(inputText); + agentMessage.setContent(List.of(tc)); + agentRequest.setUserId(getUserId(requestBody)); + agentRequest.setSessionId(getSessionId(requestBody)); + agentRequest.setInput(List.of(agentMessage)); + return agentRequest; + } + + private String getTextFromMessageParts(Object input) { + StringBuilder inputTextBuilder = new StringBuilder(); + if(input instanceof String inputText){ + inputTextBuilder.append(inputText); + } else if (input instanceof List inputList) { + for (Object item : inputList) { + if (item instanceof String text) { + if (!text.trim().isBlank()) { + inputTextBuilder.append(text.trim()); + } + } + } + } + return inputTextBuilder.toString().trim(); + } + + private List buildOutputItems(List events) { + List output = new ArrayList<>(); + OutputMessage item = new OutputMessage(); + item.setType("message"); + item.setId(UUID.randomUUID().toString()); + item.setStatus("completed"); + item.setRole("assistant"); + item.setContent(buildContentPayload(events)); + output.add(item); + return output; + } + + private List buildContentPayload(List events) { + List payload = new ArrayList<>(); + StringBuilder accumulatedOutput = new StringBuilder(); + for (Event output : events) { + if (output instanceof Content) { + // Todo: only process text content for now, need to handle other content types later + if (output instanceof TextContent text) { + String content = text.getText(); + accumulatedOutput.append(content); + logger.info("Appended content chunk (" + content.length() + " chars), total so far: " + + accumulatedOutput.length()); + } + } + // Todo: need to know whether the blocking mode should also handle tool calls and responses + else if (output instanceof Message message) { + if (message.getType().equals("mcp_call")) { + for (Content content : message.getContent()) { + if (content instanceof DataContent dataContent) { + if (dataContent.getData() == null || !dataContent.getData().containsKey("name") || dataContent.getData().get("name").toString().isEmpty()) { + continue; + } + String toolName = dataContent.getData().get("name").toString(); + String arguments = dataContent.getData().get("arguments").toString(); + String callId = dataContent.getData().get("call_id").toString(); + String textContent = "Calling tool " + toolName + " with arguments: " + arguments + " (call ID: " + callId + ")"; + Map metaData = new HashMap<>(); + metaData.put("type", "toolCall"); + accumulatedOutput.append(textContent); + // Todo: Still need to know the exact token usage for tool call + } + } + } else if (message.getType().equals("mcp_approval_response")) { + for (Content content : message.getContent()) { + if (content instanceof DataContent dataContent) { + if (dataContent.getData() == null || !dataContent.getData().containsKey("name") || dataContent.getData().get("name").toString().isEmpty()) { + continue; + } + String toolResult = dataContent.getData().get("output").toString(); + String toolName = dataContent.getData().get("name").toString(); + String callId = dataContent.getData().get("call_id").toString(); + String textContent = "Tool " + toolName + " returned result: " + toolResult + " (call ID: " + callId + ")"; + Map metaData = new HashMap<>(); + metaData.put("type", "toolResponse"); + accumulatedOutput.append(textContent); + // Todo: Still need to know the exact token usage for tool call + } + } + } + } + } + ResponseContent responseContent = new ResponseContent(); + responseContent.setType("output_text"); + responseContent.setText(accumulatedOutput.toString()); + payload.add(responseContent); + + return payload; + } + + private ResponseUsage buildUsagePlaceholder() { + // Placeholder: hook actual token accounting if available + return new ResponseUsage(); + } + + private Flux convertMessageToStreamEvent(Event event, StreamState streamState) { + List events = new ArrayList<>(); + + if (event instanceof Content) { + if (event instanceof TextContent textContent) { + String text = textContent.getText(); + ResponseTextDeltaEvent deltaEvent = ResponseTextDeltaEvent.builder() + .eventId(UUID.randomUUID().toString()) + .responseId(streamState.responseId) + .itemId(UUID.randomUUID().toString()) + .outputIndex(streamState.outputIndex) + .contentIndex(streamState.contentIndex) + .delta(text) + .build(); + streamState.incrementContentIndex(); + + events.add(RealtimeServerEvent.ofResponseOutputTextDelta(deltaEvent)); + } + } + else if(event instanceof Message message){ + if (message.getType().equals("mcp_call")) { + for (Content content : message.getContent()) { + if (content instanceof DataContent dataContent) { + if (dataContent.getData() == null || !dataContent.getData().containsKey("name") || dataContent.getData().get("name").toString().isEmpty()) { + continue; + } + String arguments = dataContent.getData().get("arguments").toString(); + String callId = dataContent.getData().get("call_id").toString(); + + streamState.incrementOutputIndex(); + ResponseFunctionCallArgumentsDeltaEvent deltaEvent = ResponseFunctionCallArgumentsDeltaEvent.builder() + .eventId(UUID.randomUUID().toString()) + .responseId(streamState.responseId) + .itemId(UUID.randomUUID().toString()) + .outputIndex(streamState.outputIndex) + .delta(arguments) + .callId(callId) + .build(); + + events.add(RealtimeServerEvent.ofResponseFunctionCallArgumentsDelta(deltaEvent)); + // Todo: Still need to know the exact token usage for tool call + } + } + } else if (message.getType().equals("mcp_approval_response")) { + for (Content content : message.getContent()) { + if (content instanceof DataContent dataContent) { + if (dataContent.getData() == null || !dataContent.getData().containsKey("name") || dataContent.getData().get("name").toString().isEmpty()) { + continue; + } + String toolResult = dataContent.getData().get("output").toString(); + String callId = dataContent.getData().get("call_id").toString(); + + ResponseFunctionCallArgumentsDoneEvent deltaEvent = ResponseFunctionCallArgumentsDoneEvent.builder() + .eventId(UUID.randomUUID().toString()) + .responseId(streamState.responseId) + .itemId(UUID.randomUUID().toString()) + .outputIndex(streamState.outputIndex) + .arguments(toolResult) + .callId(callId) + .build(); + + streamState.incrementOutputIndex(); + events.add(RealtimeServerEvent.ofResponseFunctionCallArgumentsDone(deltaEvent)); + // Todo: Still need to know the exact token usage for tool call + } + } + } + } + + return Flux.fromIterable(events); + } + + private RealtimeServerEvent createErrorEvent(String errorMessage, String type) { + RealtimeErrorEvent error = RealtimeErrorEvent.builder() + .eventId(UUID.randomUUID().toString()) + .error(RealtimeError.builder() + .message(errorMessage) + .type(type) + .build()) + .build(); + return RealtimeServerEvent.ofError(error); + } + + class StreamState { + private int outputIndex = 0; + private int contentIndex = 0; + private String responseId = "resp_" + UUID.randomUUID(); + + public void incrementOutputIndex() { + this.outputIndex++; + this.contentIndex = 0; // Reset content index for new output + } + + public void incrementContentIndex() { + this.contentIndex++; + } + } +} + diff --git a/web/src/main/java/io/agentscope/runtime/protocol/responseapi/ResponseApiHandlerConfiguration.java b/web/src/main/java/io/agentscope/runtime/protocol/responseapi/ResponseApiHandlerConfiguration.java new file mode 100644 index 00000000..4c4caa93 --- /dev/null +++ b/web/src/main/java/io/agentscope/runtime/protocol/responseapi/ResponseApiHandlerConfiguration.java @@ -0,0 +1,52 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.agentscope.runtime.protocol.responseapi; + +import io.agentscope.runtime.engine.Runner; +import io.agentscope.runtime.protocol.ProtocolConfig; +import org.springframework.beans.factory.ObjectProvider; + +public class ResponseApiHandlerConfiguration { + + private static volatile ResponseApiHandlerConfiguration INSTANCE; + + private final ResponseApiHandler responseApiHandler; + + public ResponseApiHandlerConfiguration(Runner runner, ResponseApiProtocolConfig responseApiProtocolConfig) { + this.responseApiHandler = new ResponseApiHandler(runner, responseApiProtocolConfig); + } + + public static ResponseApiHandlerConfiguration getInstance(Runner runner, + ObjectProvider protocolConfigs) { + ResponseApiHandlerConfiguration inst = INSTANCE; + if (inst == null) { + synchronized (ResponseApiHandlerConfiguration.class) { + if (INSTANCE == null) { + ResponseApiProtocolConfig responseApiProtocolConfig = ResponseApiProtocolConfigUtils.getConfigIfAbsent(protocolConfigs); + INSTANCE = new ResponseApiHandlerConfiguration(runner, responseApiProtocolConfig); + } + inst = INSTANCE; + } + } + return inst; + } + + public ResponseApiHandler responseApiHandler() { + return this.responseApiHandler; + } +} + diff --git a/web/src/main/java/io/agentscope/runtime/protocol/responseapi/ResponseApiProtocolConfig.java b/web/src/main/java/io/agentscope/runtime/protocol/responseapi/ResponseApiProtocolConfig.java new file mode 100644 index 00000000..9566877f --- /dev/null +++ b/web/src/main/java/io/agentscope/runtime/protocol/responseapi/ResponseApiProtocolConfig.java @@ -0,0 +1,56 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.agentscope.runtime.protocol.responseapi; + +import io.agentscope.runtime.protocol.Protocol; +import io.agentscope.runtime.protocol.ProtocolConfig; + +/** + * {@link ProtocolConfig} implementation for ResponseAPI protocol. + */ +public class ResponseApiProtocolConfig implements ProtocolConfig { + + private final int completionTimeoutSeconds; + + public ResponseApiProtocolConfig(int completionTimeoutSeconds) { + this.completionTimeoutSeconds = completionTimeoutSeconds; + } + + public int getCompletionTimeoutSeconds() { + return completionTimeoutSeconds; + } + + @Override + public Protocol type() { + return Protocol.ResponseAPI; + } + + public static class Builder { + + protected int completionTimeoutSeconds = 60; + + public Builder completionTimeoutSeconds(int completionTimeoutSeconds) { + this.completionTimeoutSeconds = completionTimeoutSeconds; + return this; + } + + public ResponseApiProtocolConfig build() { + return new ResponseApiProtocolConfig(completionTimeoutSeconds); + } + } +} + diff --git a/web/src/main/java/io/agentscope/runtime/protocol/responseapi/ResponseApiProtocolConfigUtils.java b/web/src/main/java/io/agentscope/runtime/protocol/responseapi/ResponseApiProtocolConfigUtils.java new file mode 100644 index 00000000..b8bef584 --- /dev/null +++ b/web/src/main/java/io/agentscope/runtime/protocol/responseapi/ResponseApiProtocolConfigUtils.java @@ -0,0 +1,42 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.agentscope.runtime.protocol.responseapi; + +import io.agentscope.runtime.protocol.Protocol; +import io.agentscope.runtime.protocol.ProtocolConfig; +import org.springframework.beans.factory.ObjectProvider; + +/** + * Utils for {@link ResponseApiProtocolConfig}. + */ +public class ResponseApiProtocolConfigUtils { + + /** + * Get ResponseAPI protocol configuration from the provided configurations, returning a default one if absent + * + * @param protocolConfigs the provider of protocol configurations to search from + * @return the first found ResponseAPI protocol configuration, or a newly built default one if none found + */ + public static ResponseApiProtocolConfig getConfigIfAbsent(ObjectProvider protocolConfigs) { + return protocolConfigs.stream() + .filter(protocolConfig -> Protocol.ResponseAPI.equals(protocolConfig.type())) + .filter(protocolConfig -> ResponseApiProtocolConfig.class.isAssignableFrom(protocolConfig.getClass())) + .map(protocolConfig -> (ResponseApiProtocolConfig) protocolConfig).findFirst() + .orElse(new ResponseApiProtocolConfig.Builder().build()); + } +} + diff --git a/web/src/main/java/io/agentscope/runtime/protocol/responseapi/controller/ResponseApiController.java b/web/src/main/java/io/agentscope/runtime/protocol/responseapi/controller/ResponseApiController.java new file mode 100644 index 00000000..4f2f5d28 --- /dev/null +++ b/web/src/main/java/io/agentscope/runtime/protocol/responseapi/controller/ResponseApiController.java @@ -0,0 +1,65 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.agentscope.runtime.protocol.responseapi.controller; + +import io.agentscope.runtime.engine.Runner; +import io.agentscope.runtime.protocol.ProtocolConfig; +import io.agentscope.runtime.protocol.responseapi.ResponseApiHandler; +import io.agentscope.runtime.protocol.responseapi.ResponseApiHandlerConfiguration; +import io.agentscope.runtime.protocol.responseapi.model.ResponseApiRequest; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; + +import java.util.logging.Logger; + +/** + * REST controller for OpenAI Responses API protocol endpoints. + */ +@RestController +@RequestMapping("/compatible-mode/v1") +public class ResponseApiController { + + private static final Logger logger = Logger.getLogger(ResponseApiController.class.getName()); + + private final ResponseApiHandler responseApiHandler; + + public ResponseApiController(Runner runner, ObjectProvider protocolConfigs) { + this.responseApiHandler = ResponseApiHandlerConfiguration.getInstance(runner, protocolConfigs).responseApiHandler(); + } + + /** + * Stream chat completions endpoint (OpenAI-compatible Responses API) + */ + @PostMapping(value = "/responses", + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.TEXT_EVENT_STREAM_VALUE}) + @ResponseBody + public Object streamChatCompletions(@RequestBody ResponseApiRequest request) { + logger.info("Received OpenAI Responses API chat completion request"); + + // Check if streaming is requested (default to true for Responses API) + Boolean stream = request.getStream(); + + if (Boolean.TRUE.equals(stream)) { + return responseApiHandler.handleStreamingResponse(request); + } else { + logger.info("Non-streaming request received, returning aggregated response"); + return responseApiHandler.handleNonStreamingResponse(request); + } + } +} diff --git a/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/Conversation.java b/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/Conversation.java new file mode 100644 index 00000000..efd661b0 --- /dev/null +++ b/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/Conversation.java @@ -0,0 +1,69 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.agentscope.runtime.protocol.responseapi.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.util.List; +import java.util.Map; + +/** + * Minimal conversation object for Responses API. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class Conversation { + + private String id; + + private List input_items; + + private List output_items; + + private Map metadata; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public List getInput_items() { + return input_items; + } + + public void setInput_items(List input_items) { + this.input_items = input_items; + } + + public List getOutput_items() { + return output_items; + } + + public void setOutput_items(List output_items) { + this.output_items = output_items; + } + + public Map getMetadata() { + return metadata; + } + + public void setMetadata(Map metadata) { + this.metadata = metadata; + } +} diff --git a/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/Error.java b/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/Error.java new file mode 100644 index 00000000..17cc345e --- /dev/null +++ b/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/Error.java @@ -0,0 +1,41 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.agentscope.runtime.protocol.responseapi.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class Error { + private String message; + private String code; + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/IncompleteDetail.java b/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/IncompleteDetail.java new file mode 100644 index 00000000..d1c12995 --- /dev/null +++ b/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/IncompleteDetail.java @@ -0,0 +1,32 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.agentscope.runtime.protocol.responseapi.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class IncompleteDetail { + private String reason; + + public String getReason() { + return reason; + } + + public void setReason(String reason) { + this.reason = reason; + } +} diff --git a/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/InputTokenDetail.java b/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/InputTokenDetail.java new file mode 100644 index 00000000..6ba25b56 --- /dev/null +++ b/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/InputTokenDetail.java @@ -0,0 +1,34 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.agentscope.runtime.protocol.responseapi.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class InputTokenDetail { + @JsonProperty("cached_tokens") + private Integer cachedTokens; + + public Integer getCachedTokens() { + return cachedTokens; + } + + public void setCachedTokens(Integer cachedTokens) { + this.cachedTokens = cachedTokens; + } +} diff --git a/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/OutputMessage.java b/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/OutputMessage.java new file mode 100644 index 00000000..e6f32d84 --- /dev/null +++ b/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/OutputMessage.java @@ -0,0 +1,70 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.agentscope.runtime.protocol.responseapi.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class OutputMessage { + private String id; + private String role; + private String status; + private String type; + private List content; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public List getContent() { + return content; + } + + public void setContent(List content) { + this.content = content; + } +} diff --git a/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/OutputTokenDetail.java b/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/OutputTokenDetail.java new file mode 100644 index 00000000..cda9466a --- /dev/null +++ b/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/OutputTokenDetail.java @@ -0,0 +1,34 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.agentscope.runtime.protocol.responseapi.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class OutputTokenDetail { + @JsonProperty("reasoning_tokens") + private Integer reasoningTokens; + + public Integer getReasoningTokens() { + return reasoningTokens; + } + + public void setReasoningTokens(Integer reasoningTokens) { + this.reasoningTokens = reasoningTokens; + } +} diff --git a/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/ResponseApiRequest.java b/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/ResponseApiRequest.java new file mode 100644 index 00000000..5b19bb3e --- /dev/null +++ b/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/ResponseApiRequest.java @@ -0,0 +1,313 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.agentscope.runtime.protocol.responseapi.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; +import java.util.Map; + +/** + * POJO for OpenAI Responses API request body. + * */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class ResponseApiRequest { + + private List include; + + private Object input; + + private String instructions; + + @JsonProperty("max_output_tokens") + private Integer maxOutputTokens; + + @JsonProperty("max_tool_calls") + private Integer maxToolCalls; + + private Map metadata; + + private String model; + + @JsonProperty("parallel_tool_calls") + private Boolean parallelToolCalls; + + @JsonProperty("previous_response_id") + private String previousResponseId; + + private ResponsePrompt prompt; + + @JsonProperty("prompt_cache_key") + private String promptCacheKey; + + @JsonProperty("prompt_cache_retention") + private String promptCacheRetention; + + private ResponseReasoning reasoning; + + @JsonProperty("safety_identifier") + private String safetyIdentifier; + + @JsonProperty("service_tier") + private String serviceTier = "auto"; + + private Boolean store = Boolean.TRUE; + + private Boolean stream = Boolean.FALSE; + + @JsonProperty("stream_options") + private Object streamOptions; + + private float temperature = 1.0f; + + private Object text; + + @JsonProperty("tool_choice") + private Object toolChoice; + + private List tools; + + @JsonProperty("top_logprobs") + private Integer topLogprobs; + + @JsonProperty("top_p") + private Integer topP; + + private String truncation = "disabled"; + + public Boolean getBackground() { + return background; + } + + public void setBackground(Boolean background) { + this.background = background; + } + + private Boolean background; + + private Conversation conversation; + + public Conversation getConversation() { + return conversation; + } + + public void setConversation(Conversation conversation) { + this.conversation = conversation; + } + + public List getInclude() { + return include; + } + + public void setInclude(List include) { + this.include = include; + } + + public Object getInput() { + return input; + } + + public void setInput(Object input) { + this.input = input; + } + + public String getInstructions() { + return instructions; + } + + public void setInstructions(String instructions) { + this.instructions = instructions; + } + + public Integer getMaxOutputTokens() { + return maxOutputTokens; + } + + public void setMaxOutputTokens(Integer maxOutputTokens) { + this.maxOutputTokens = maxOutputTokens; + } + + public Integer getMaxToolCalls() { + return maxToolCalls; + } + + public void setMaxToolCalls(Integer maxToolCalls) { + this.maxToolCalls = maxToolCalls; + } + + public Map getMetadata() { + return metadata; + } + + public void setMetadata(Map metadata) { + this.metadata = metadata; + } + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + + public Boolean getParallelToolCalls() { + return parallelToolCalls; + } + + public void setParallelToolCalls(Boolean parallelToolCalls) { + this.parallelToolCalls = parallelToolCalls; + } + + public String getPreviousResponseId() { + return previousResponseId; + } + + public void setPreviousResponseId(String previousResponseId) { + this.previousResponseId = previousResponseId; + } + + public ResponsePrompt getPrompt() { + return prompt; + } + + public void setPrompt(ResponsePrompt prompt) { + this.prompt = prompt; + } + + public String getPromptCacheKey() { + return promptCacheKey; + } + + public void setPromptCacheKey(String promptCacheKey) { + this.promptCacheKey = promptCacheKey; + } + + public String getPromptCacheRetention() { + return promptCacheRetention; + } + + public void setPromptCacheRetention(String promptCacheRetention) { + this.promptCacheRetention = promptCacheRetention; + } + + public ResponseReasoning getReasoning() { + return reasoning; + } + + public void setReasoning(ResponseReasoning reasoning) { + this.reasoning = reasoning; + } + + public String getSafetyIdentifier() { + return safetyIdentifier; + } + + public void setSafetyIdentifier(String safetyIdentifier) { + this.safetyIdentifier = safetyIdentifier; + } + + public String getServiceTier() { + return serviceTier; + } + + public void setServiceTier(String serviceTier) { + this.serviceTier = serviceTier; + } + + public Boolean getStore() { + return store; + } + + public void setStore(Boolean store) { + this.store = store; + } + + public Boolean getStream() { + return stream; + } + + public void setStream(Boolean stream) { + this.stream = stream; + } + + public Object getStreamOptions() { + return streamOptions; + } + + public void setStreamOptions(Object streamOptions) { + this.streamOptions = streamOptions; + } + + public float getTemperature() { + return temperature; + } + + public void setTemperature(float temperature) { + this.temperature = temperature; + } + + public Object getText() { + return text; + } + + public void setText(Object text) { + this.text = text; + } + + public Object getToolChoice() { + return toolChoice; + } + + public void setToolChoice(Object toolChoice) { + this.toolChoice = toolChoice; + } + + public List getTools() { + return tools; + } + + public void setTools(List tools) { + this.tools = tools; + } + + public Integer getTopLogprobs() { + return topLogprobs; + } + + public void setTopLogprobs(Integer topLogprobs) { + this.topLogprobs = topLogprobs; + } + + public Integer getTopP() { + return topP; + } + + public void setTopP(Integer topP) { + this.topP = topP; + } + + public String getTruncation() { + return truncation; + } + + public void setTruncation(String truncation) { + this.truncation = truncation; + } +} + diff --git a/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/ResponseApiResponse.java b/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/ResponseApiResponse.java new file mode 100644 index 00000000..7af6566b --- /dev/null +++ b/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/ResponseApiResponse.java @@ -0,0 +1,333 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.agentscope.runtime.protocol.responseapi.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; +import java.util.Map; + +/** + * POJO for Responses API response object. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class ResponseApiResponse { + + private Boolean background; + + private Conversation conversation; + + @JsonProperty("created_at") + private Integer createdAt; + + private Error error; + + private String id; + + @JsonProperty("incomplete_details") + private IncompleteDetail incompleteDetail; + + private Object instructions; + + @JsonProperty("max_output_tokens") + private Integer maxOutputTokens; + + @JsonProperty("max_tool_calls") + private Integer maxToolCalls; + + private Map metadata; + + private String model; + + private String object; + + private List output; + + @JsonProperty("parallel_tool_calls") + private Boolean parallelToolCalls; + + @JsonProperty("previous_response_id") + private String previousResponseId; + + private ResponsePrompt prompt; + + @JsonProperty("prompt_cache_key") + private String promptCacheKey; + + @JsonProperty("prompt_cache_retention") + private String promptCacheRetention; + + private ResponseReasoning reasoning; + + @JsonProperty("safety_identifier") + private String safetyIdentifier; + + private String status; + + private Integer temperature; + + private ResponseText text; + + @JsonProperty("tool_choice") + private Object toolChoice; + + private List tools; + + @JsonProperty("top_logprobs") + private Integer topLogprobs; + + @JsonProperty("top_p") + private Integer topP; + + private String truncation; + + private ResponseUsage usage; + + public Boolean getBackground() { + return background; + } + + public void setBackground(Boolean background) { + this.background = background; + } + + public Conversation getConversation() { + return conversation; + } + + public void setConversation(Conversation conversation) { + this.conversation = conversation; + } + + public Integer getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Integer createdAt) { + this.createdAt = createdAt; + } + + public Error getError() { + return error; + } + + public void setError(Error error) { + this.error = error; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public IncompleteDetail getIncompleteDetail() { + return incompleteDetail; + } + + public void setIncompleteDetail(IncompleteDetail incompleteDetail) { + this.incompleteDetail = incompleteDetail; + } + + public Object getInstructions() { + return instructions; + } + + public void setInstructions(Object instructions) { + this.instructions = instructions; + } + + public Integer getMaxOutputTokens() { + return maxOutputTokens; + } + + public void setMaxOutputTokens(Integer maxOutputTokens) { + this.maxOutputTokens = maxOutputTokens; + } + + public Integer getMaxToolCalls() { + return maxToolCalls; + } + + public void setMaxToolCalls(Integer maxToolCalls) { + this.maxToolCalls = maxToolCalls; + } + + public Map getMetadata() { + return metadata; + } + + public void setMetadata(Map metadata) { + this.metadata = metadata; + } + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + + public String getObject() { + return object; + } + + public void setObject(String object) { + this.object = object; + } + + public List getOutput() { + return output; + } + + public void setOutput(List output) { + this.output = output; + } + + public Boolean getParallelToolCalls() { + return parallelToolCalls; + } + + public void setParallelToolCalls(Boolean parallelToolCalls) { + this.parallelToolCalls = parallelToolCalls; + } + + public String getPreviousResponseId() { + return previousResponseId; + } + + public void setPreviousResponseId(String previousResponseId) { + this.previousResponseId = previousResponseId; + } + + public ResponsePrompt getPrompt() { + return prompt; + } + + public void setPrompt(ResponsePrompt prompt) { + this.prompt = prompt; + } + + public String getPromptCacheKey() { + return promptCacheKey; + } + + public void setPromptCacheKey(String promptCacheKey) { + this.promptCacheKey = promptCacheKey; + } + + public String getPromptCacheRetention() { + return promptCacheRetention; + } + + public void setPromptCacheRetention(String promptCacheRetention) { + this.promptCacheRetention = promptCacheRetention; + } + + public ResponseReasoning getReasoning() { + return reasoning; + } + + public void setReasoning(ResponseReasoning reasoning) { + this.reasoning = reasoning; + } + + public String getSafetyIdentifier() { + return safetyIdentifier; + } + + public void setSafetyIdentifier(String safetyIdentifier) { + this.safetyIdentifier = safetyIdentifier; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public Integer getTemperature() { + return temperature; + } + + public void setTemperature(Integer temperature) { + this.temperature = temperature; + } + + public ResponseText getText() { + return text; + } + + public void setText(ResponseText text) { + this.text = text; + } + + public Object getToolChoice() { + return toolChoice; + } + + public void setToolChoice(Object toolChoice) { + this.toolChoice = toolChoice; + } + + public List getTools() { + return tools; + } + + public void setTools(List tools) { + this.tools = tools; + } + + public Integer getTopLogprobs() { + return topLogprobs; + } + + public void setTopLogprobs(Integer topLogprobs) { + this.topLogprobs = topLogprobs; + } + + public Integer getTopP() { + return topP; + } + + public void setTopP(Integer topP) { + this.topP = topP; + } + + public String getTruncation() { + return truncation; + } + + public void setTruncation(String truncation) { + this.truncation = truncation; + } + + public ResponseUsage getUsage() { + return usage; + } + + public void setUsage(ResponseUsage usage) { + this.usage = usage; + } +} + diff --git a/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/ResponseContent.java b/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/ResponseContent.java new file mode 100644 index 00000000..60bdccc6 --- /dev/null +++ b/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/ResponseContent.java @@ -0,0 +1,53 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.agentscope.runtime.protocol.responseapi.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.util.ArrayList; +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class ResponseContent { + private String text; + private String type = "output_text"; + private List annotations = new ArrayList<>(); + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public List getAnnotations() { + return annotations; + } + + public void setAnnotations(List annotations) { + this.annotations = annotations; + } +} diff --git a/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/ResponsePrompt.java b/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/ResponsePrompt.java new file mode 100644 index 00000000..3c41dc43 --- /dev/null +++ b/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/ResponsePrompt.java @@ -0,0 +1,52 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.agentscope.runtime.protocol.responseapi.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.util.Map; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class ResponsePrompt { + private String id; + private Map variables; + private String version; + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public Map getVariables() { + return variables; + } + + public void setVariables(Map variables) { + this.variables = variables; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } +} diff --git a/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/ResponseReasoning.java b/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/ResponseReasoning.java new file mode 100644 index 00000000..1d5e816b --- /dev/null +++ b/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/ResponseReasoning.java @@ -0,0 +1,42 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.agentscope.runtime.protocol.responseapi.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class ResponseReasoning { + private String effort; + + private String summary; + + public String getEffort() { + return effort; + } + + public void setEffort(String effort) { + this.effort = effort; + } + + public String getSummary() { + return summary; + } + + public void setSummary(String summary) { + this.summary = summary; + } +} diff --git a/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/ResponseText.java b/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/ResponseText.java new file mode 100644 index 00000000..07a8b6f6 --- /dev/null +++ b/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/ResponseText.java @@ -0,0 +1,26 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.agentscope.runtime.protocol.responseapi.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class ResponseText { + private String verbosity; + + +} diff --git a/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/ResponseUsage.java b/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/ResponseUsage.java new file mode 100644 index 00000000..e8c71451 --- /dev/null +++ b/web/src/main/java/io/agentscope/runtime/protocol/responseapi/model/ResponseUsage.java @@ -0,0 +1,84 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.agentscope.runtime.protocol.responseapi.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Map; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ResponseUsage { + + @JsonProperty("input_tokens") + private Integer inputTokens; + + @JsonProperty("output_tokens") + private Integer outputTokens; + + @JsonProperty("total_tokens") + private Integer totalTokens; + + @JsonProperty("input_tokens_details") + private Map inputTokensDetails; + + @JsonProperty("output_tokens_details") + private Map outputTokensDetails; + + public Integer getInputTokens() { + return inputTokens; + } + + public void setInputTokens(Integer inputTokens) { + this.inputTokens = inputTokens; + } + + public Integer getOutputTokens() { + return outputTokens; + } + + public void setOutputTokens(Integer outputTokens) { + this.outputTokens = outputTokens; + } + + public Integer getTotalTokens() { + return totalTokens; + } + + public void setTotalTokens(Integer totalTokens) { + this.totalTokens = totalTokens; + } + + public Map getInputTokensDetails() { + return inputTokensDetails; + } + + public void setInputTokensDetails(Map inputTokensDetails) { + this.inputTokensDetails = inputTokensDetails; + } + + public Map getOutputTokensDetails() { + return outputTokensDetails; + } + + public void setOutputTokensDetails(Map outputTokensDetails) { + this.outputTokensDetails = outputTokensDetails; + } +} +