From 10272192ce4d9971202dcdf27ec3029e0d8a1361 Mon Sep 17 00:00:00 2001 From: foleydang Date: Wed, 1 Jul 2026 16:25:58 +0800 Subject: [PATCH 1/4] feat: add AgentStudio SDK for managed agents - Sync-first API with typed models and CursorPage auto-pagination - Resources: agents, sessions, environments, files, skills, vaults, credentials - AsyncClient via client.async() - Region parameter, SSE event parsing - Sample and E2E tests --- samples/AgentStudioSimple.java | 86 ++ .../agentstudio/AgentStudioClient.java | 144 +++ .../agentstudio/AgentStudioConstants.java | 143 +++ .../agentstudio/message/ClientEvents.java | 194 ++++ .../agentstudio/message/ContentBlock.java | 160 +++ .../agentstudio/message/Message.java | 74 ++ .../dashscope/agentstudio/model/Agent.java | 66 ++ .../model/AgentStudioDeletionStatus.java | 21 + .../agentstudio/model/AgentStudioFile.java | 35 + .../agentstudio/model/AgentVersion.java | 56 + .../dashscope/agentstudio/model/Configs.java | 73 ++ .../agentstudio/model/Credential.java | 75 ++ .../agentstudio/model/Environment.java | 85 ++ .../dashscope/agentstudio/model/Session.java | 137 +++ .../dashscope/agentstudio/model/Skill.java | 56 + .../agentstudio/model/SkillVersion.java | 42 + .../dashscope/agentstudio/model/Vault.java | 33 + .../agentstudio/pagination/CursorPage.java | 65 ++ .../agentstudio/param/AgentCreateParam.java | 59 ++ .../agentstudio/param/AgentListParam.java | 32 + .../agentstudio/param/AgentUpdateParam.java | 78 ++ .../param/CredentialCreateParam.java | 35 + .../param/CredentialListParam.java | 32 + .../param/CredentialUpdateParam.java | 36 + .../param/EnvironmentCreateParam.java | 67 ++ .../param/EnvironmentListParam.java | 32 + .../param/EnvironmentUpdateParam.java | 44 + .../agentstudio/param/FileListParam.java | 36 + .../agentstudio/param/SessionCreateParam.java | 39 + .../param/SessionEventListParam.java | 45 + .../param/SessionEventSendParam.java | 30 + .../agentstudio/param/SessionListParam.java | 47 + .../agentstudio/param/SessionUpdateParam.java | 32 + .../agentstudio/param/SkillCreateParam.java | 28 + .../agentstudio/param/SkillListParam.java | 32 + .../agentstudio/param/VaultCreateParam.java | 31 + .../agentstudio/param/VaultListParam.java | 34 + .../agentstudio/param/VaultUpdateParam.java | 32 + .../resource/AgentStudioEventStream.java | 249 +++++ .../agentstudio/resource/Agents.java | 193 ++++ .../agentstudio/resource/AsyncHelper.java | 64 ++ .../agentstudio/resource/Environments.java | 170 +++ .../dashscope/agentstudio/resource/Files.java | 284 +++++ .../agentstudio/resource/SessionEvents.java | 176 ++++ .../agentstudio/resource/Sessions.java | 178 ++++ .../agentstudio/resource/Skills.java | 268 +++++ .../agentstudio/resource/Vaults.java | 343 +++++++ .../alibaba/dashscope/utils/JsonUtils.java | 2 + .../alibaba/dashscope/TestAgentStudio.java | 968 ++++++++++++++++++ src/test/resources/agentstudio.json | 310 ++++++ 50 files changed, 5551 insertions(+) create mode 100644 samples/AgentStudioSimple.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/AgentStudioClient.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/AgentStudioConstants.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/message/ClientEvents.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/message/ContentBlock.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/message/Message.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/model/Agent.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/model/AgentStudioDeletionStatus.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/model/AgentStudioFile.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/model/AgentVersion.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/model/Configs.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/model/Credential.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/model/Environment.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/model/Session.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/model/Skill.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/model/SkillVersion.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/model/Vault.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/pagination/CursorPage.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/param/AgentCreateParam.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/param/AgentListParam.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/param/AgentUpdateParam.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/param/CredentialCreateParam.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/param/CredentialListParam.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/param/CredentialUpdateParam.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/param/EnvironmentCreateParam.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/param/EnvironmentListParam.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/param/EnvironmentUpdateParam.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/param/FileListParam.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/param/SessionCreateParam.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/param/SessionEventListParam.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/param/SessionEventSendParam.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/param/SessionListParam.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/param/SessionUpdateParam.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/param/SkillCreateParam.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/param/SkillListParam.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/param/VaultCreateParam.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/param/VaultListParam.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/param/VaultUpdateParam.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/resource/AgentStudioEventStream.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/resource/Agents.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/resource/AsyncHelper.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/resource/Environments.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/resource/Files.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/resource/SessionEvents.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/resource/Sessions.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/resource/Skills.java create mode 100644 src/main/java/com/alibaba/dashscope/agentstudio/resource/Vaults.java create mode 100644 src/test/java/com/alibaba/dashscope/TestAgentStudio.java create mode 100644 src/test/resources/agentstudio.json diff --git a/samples/AgentStudioSimple.java b/samples/AgentStudioSimple.java new file mode 100644 index 0000000..6f8ad3e --- /dev/null +++ b/samples/AgentStudioSimple.java @@ -0,0 +1,86 @@ +import com.alibaba.dashscope.agentstudio.AgentStudioClient; +import com.alibaba.dashscope.agentstudio.message.ClientEvents; +import com.alibaba.dashscope.agentstudio.message.ContentBlock; +import com.alibaba.dashscope.agentstudio.message.Message; +import com.alibaba.dashscope.agentstudio.model.Agent; +import com.alibaba.dashscope.agentstudio.model.Session; +import com.alibaba.dashscope.agentstudio.param.AgentCreateParam; +import com.alibaba.dashscope.agentstudio.param.SessionCreateParam; +import com.alibaba.dashscope.agentstudio.resource.AgentStudioEventStream; +import com.google.gson.JsonObject; +import java.util.Collections; + +/** + * AgentStudio quick start: create agent, session, send message, stream reply. + * + *

Prerequisites: set DASHSCOPE_API_KEY env var, and either DASHSCOPE_WORKSPACE env var or pass + * workspace to the constructor. + * + *

+ * export DASHSCOPE_API_KEY=sk-xxx
+ * export DASHSCOPE_WORKSPACE=ws_xxxxxxxxxxxx
+ * 
+ */ +public class AgentStudioSimple { + + public static void main(String[] args) throws Exception { + // Option 1: apiKey + workspace (production) + AgentStudioClient client = new AgentStudioClient("sk-xxx", "ws_xxxxxxxxxxxx"); + + // Option 2: all from env vars (DASHSCOPE_API_KEY + DASHSCOPE_WORKSPACE) + // AgentStudioClient client = new AgentStudioClient(); + + // Option 3: custom base URL + // AgentStudioClient client = AgentStudioClient.builder() + // .apiKey("sk-xxx") + // .baseUrl("https://your-custom-host/api/v1/agentstudio") + // .build(); + + // 1. Create Agent + Agent agent = + client + .agents() + .create( + AgentCreateParam.builder() + .name("demo-agent") + .model("qwen-plus") + .systemPrompt("你是一个简洁的助手。") + .build()); + System.out.println("Agent: " + agent.getId()); + + // 2. Create Session + Session session = + client.sessions().create(SessionCreateParam.builder().agent(agent.getId()).build()); + System.out.println("Session: " + session.getId()); + + // 3. Send message + client + .sessions() + .events() + .send(session.getId(), Collections.singletonList(ClientEvents.userMessage("你好"))); + + // 4. Stream reply + try (AgentStudioEventStream stream = client.sessions().events().stream(session.getId())) { + for (Message event : stream) { + if ("message".equals(event.getType()) && event.getContent() != null) { + for (ContentBlock block : event.getContent()) { + if (block instanceof ContentBlock.Text) { + System.out.print(((ContentBlock.Text) block).getText()); + } + } + } else if ("session_status".equals(event.getType())) { + JsonObject stopReason = event.getStopReason(); + if (stopReason != null) { + System.out.println("\nstop_reason: " + stopReason.get("type")); + } + break; + } + } + } + + // Cleanup + client.sessions().delete(session.getId()); + client.agents().archive(agent.getId()); + client.close(); + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/AgentStudioClient.java b/src/main/java/com/alibaba/dashscope/agentstudio/AgentStudioClient.java new file mode 100644 index 0000000..98bad68 --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/AgentStudioClient.java @@ -0,0 +1,144 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio; + +import com.alibaba.dashscope.agentstudio.resource.Agents; +import com.alibaba.dashscope.agentstudio.resource.Environments; +import com.alibaba.dashscope.agentstudio.resource.Files; +import com.alibaba.dashscope.agentstudio.resource.Sessions; +import com.alibaba.dashscope.agentstudio.resource.Skills; +import com.alibaba.dashscope.agentstudio.resource.Vaults; +import com.alibaba.dashscope.protocol.ConnectionOptions; +import java.io.Closeable; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +public class AgentStudioClient implements Closeable { + private final Agents agents; + private final Sessions sessions; + private final Environments environments; + private final Skills skills; + private final Vaults vaults; + private final Files files; + private final String baseUrl; + + public AgentStudioClient() { + this(null, null, null, null, null); + } + + public AgentStudioClient(String workspace) { + this(null, null, workspace, null, null); + } + + public AgentStudioClient(String apiKey, String workspace) { + this(apiKey, null, workspace, null, null); + } + + public AgentStudioClient( + String apiKey, + String baseUrl, + String workspace, + String region, + ConnectionOptions connectionOptions) { + this.baseUrl = resolveBaseUrl(baseUrl, workspace, region); + this.agents = new Agents(this.baseUrl, connectionOptions, apiKey); + this.sessions = new Sessions(this.baseUrl, connectionOptions, apiKey); + this.environments = new Environments(this.baseUrl, connectionOptions, apiKey); + this.files = new Files(this.baseUrl, connectionOptions, apiKey); + this.skills = new Skills(this.baseUrl, connectionOptions, apiKey, this.files); + this.vaults = new Vaults(this.baseUrl, connectionOptions, apiKey); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String apiKey; + private String baseUrl; + private String workspace; + private String region; + private ConnectionOptions connectionOptions; + + public Builder apiKey(String apiKey) { + this.apiKey = apiKey; + return this; + } + + public Builder baseUrl(String baseUrl) { + this.baseUrl = baseUrl; + return this; + } + + public Builder workspace(String workspace) { + this.workspace = workspace; + return this; + } + + public Builder region(String region) { + this.region = region; + return this; + } + + public Builder connectionOptions(ConnectionOptions connectionOptions) { + this.connectionOptions = connectionOptions; + return this; + } + + public AgentStudioClient build() { + return new AgentStudioClient(apiKey, baseUrl, workspace, region, connectionOptions); + } + } + + public Agents agents() { + return agents; + } + + public Sessions sessions() { + return sessions; + } + + public Environments environments() { + return environments; + } + + public Skills skills() { + return skills; + } + + public Vaults vaults() { + return vaults; + } + + public Files files() { + return files; + } + + public String getBaseUrl() { + return baseUrl; + } + + public CompletableFuture async(Supplier supplier) { + return CompletableFuture.supplyAsync(supplier); + } + + @Override + public void close() { + sessions.events().close(); + skills.close(); + files.close(); + } + + private static String resolveBaseUrl(String explicitUrl, String workspace, String region) { + if (explicitUrl != null && !explicitUrl.isEmpty()) { + return explicitUrl; + } + String envUrl = System.getenv(AgentStudioConstants.ENV_BASE_URL); + if (envUrl == null || envUrl.isEmpty()) { + envUrl = System.getenv(AgentStudioConstants.ENV_BASE_URL_ALT); + } + if (envUrl != null && !envUrl.isEmpty()) { + return envUrl; + } + return AgentStudioConstants.resolveBaseUrl(workspace, region); + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/AgentStudioConstants.java b/src/main/java/com/alibaba/dashscope/agentstudio/AgentStudioConstants.java new file mode 100644 index 0000000..ec1008a --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/AgentStudioConstants.java @@ -0,0 +1,143 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio; + +import com.alibaba.dashscope.base.HalfDuplexParamBase; +import com.alibaba.dashscope.protocol.GeneralServiceOption; +import com.alibaba.dashscope.protocol.HttpMethod; +import com.alibaba.dashscope.protocol.Protocol; +import com.alibaba.dashscope.protocol.StreamingMode; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; + +public final class AgentStudioConstants { + public static final String BASE_URL_TEMPLATE = + "https://%s.%s.maas.aliyuncs.com/api/v1/agentstudio"; + public static final String DEFAULT_REGION = "cn-beijing"; + public static final int DEFAULT_TIMEOUT_MS = 600_000; + public static final int DEFAULT_CONNECT_TIMEOUT_MS = 10_000; + public static final String ENV_BASE_URL = "DASHSCOPE_AGENTSTUDIO_URL"; + public static final String ENV_BASE_URL_ALT = "AGENTSTUDIO_URL"; + public static final String ENV_WORKSPACE = "DASHSCOPE_WORKSPACE"; + public static final String ENV_REGION = "DASHSCOPE_API_REGION"; + + private AgentStudioConstants() {} + + public static final class SSEEventType { + public static final String MESSAGE = "message"; + public static final String INTERRUPT = "interrupt"; + public static final String TOOL_CONFIRMATION = "tool_confirmation"; + public static final String FUNCTION_CALL_OUTPUT = "function_call_output"; + public static final String TOOL_CALL_OUTPUT = "tool_call_output"; + public static final String DEFINE_OUTCOME = "define_outcome"; + public static final String FUNCTION_CALL = "function_call"; + public static final String TOOL_CALL = "tool_call"; + public static final String REASONING = "reasoning"; + public static final String MCP_CALL = "mcp_call"; + public static final String MCP_CALL_OUTPUT = "mcp_call_output"; + public static final String SESSION_STATUS = "session_status"; + public static final String ERROR = "error"; + public static final String SESSION_UPDATED = "session_updated"; + public static final String OUTCOME_EVALUATION = "outcome_evaluation"; + + private SSEEventType() {} + } + + public static final class MessageRole { + public static final String USER = "user"; + public static final String ASSISTANT = "assistant"; + public static final String TOOL = "tool"; + + private MessageRole() {} + } + + public static final class BlockType { + public static final String TEXT = "text"; + public static final String IMAGE = "image"; + public static final String AUDIO = "audio"; + public static final String DATA = "data"; + public static final String FILE = "file"; + public static final String REFUSAL = "refusal"; + public static final String ERROR = "error"; + + private BlockType() {} + } + + public static final class SessionStatusValue { + public static final String IDLE = "idle"; + public static final String RUNNING = "running"; + public static final String RESCHEDULING = "rescheduling"; + public static final String TERMINATED = "terminated"; + + private SessionStatusValue() {} + } + + public static String resolveBaseUrl(String workspace, String region) { + String ws = workspace; + if (ws == null || ws.isEmpty()) { + ws = System.getenv(ENV_WORKSPACE); + } + if (ws == null || ws.isEmpty()) { + throw new IllegalArgumentException( + "workspace is required. Pass it to the constructor, " + + "set DASHSCOPE_WORKSPACE env var, or set DASHSCOPE_AGENTSTUDIO_URL."); + } + String r = region; + if (r == null || r.isEmpty()) { + r = System.getenv(ENV_REGION); + } + if (r == null || r.isEmpty()) { + r = DEFAULT_REGION; + } + return String.format(BASE_URL_TEMPLATE, ws, r); + } + + /** + * Build a {@link GeneralServiceOption} for an agentstudio endpoint. When {@code baseUrl} is + * non-null it overrides the global default; otherwise the request falls back to {@code + * Constants.baseHttpApiUrl} via {@code GeneralApi}. + */ + public static GeneralServiceOption newServiceOption( + HttpMethod method, String path, String baseUrl) { + GeneralServiceOption opt = + GeneralServiceOption.builder() + .protocol(Protocol.HTTP) + .httpMethod(method) + .streamingMode(StreamingMode.OUT) + .path(path) + .build(); + if (baseUrl != null) { + opt.setBaseHttpUrl(baseUrl); + } + return opt; + } + + /** + * Stamp the client's instance apiKey onto a param if the caller didn't set one. The global + * fallback chain in {@link com.alibaba.dashscope.utils.ApiKey#getApiKey} still applies when + * {@code apiKey} is null. + */ + public static

P withApiKey(String apiKey, P param) { + if (apiKey != null && param.getApiKey() == null) { + param.setApiKey(apiKey); + } + return param; + } + + /** + * Append {@code key=value} (URL-encoded) to {@code sb} if value is non-null. Joins with {@literal + * &}. + */ + public static void appendParam(StringBuilder sb, String key, Object value) { + if (value == null) { + return; + } + if (sb.length() > 0) { + sb.append("&"); + } + try { + sb.append(key).append("=").append(URLEncoder.encode(value.toString(), "UTF-8")); + } catch (UnsupportedEncodingException e) { + sb.append(key).append("=").append(value); + } + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/message/ClientEvents.java b/src/main/java/com/alibaba/dashscope/agentstudio/message/ClientEvents.java new file mode 100644 index 0000000..249edbb --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/message/ClientEvents.java @@ -0,0 +1,194 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.message; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import java.util.List; +import java.util.Map; + +public final class ClientEvents { + + private ClientEvents() {} + + public static JsonObject userMessage(String text) { + return userMessage(text, null, null); + } + + public static JsonObject userMessage(List blocks) { + return userMessage(blocks, null, null); + } + + public static JsonObject userMessage(String text, String sessionThreadId) { + return userMessage(text, sessionThreadId, null); + } + + public static JsonObject userMessage( + String text, String sessionThreadId, Map metadata) { + JsonArray content = new JsonArray(); + JsonObject textBlock = new JsonObject(); + textBlock.addProperty("type", "text"); + textBlock.addProperty("text", text); + content.add(textBlock); + return buildUserMessage(content, sessionThreadId, metadata); + } + + public static JsonObject userMessage( + List blocks, String sessionThreadId, Map metadata) { + JsonArray content = new JsonArray(); + for (JsonObject block : blocks) { + content.add(block); + } + return buildUserMessage(content, sessionThreadId, metadata); + } + + private static JsonObject buildUserMessage( + JsonArray content, String sessionThreadId, Map metadata) { + JsonObject event = new JsonObject(); + event.addProperty("type", "message"); + event.addProperty("role", "user"); + event.add("content", content); + if (sessionThreadId != null) { + event.addProperty("session_thread_id", sessionThreadId); + } + if (metadata != null && !metadata.isEmpty()) { + JsonObject meta = new JsonObject(); + for (Map.Entry e : metadata.entrySet()) { + meta.addProperty(e.getKey(), e.getValue()); + } + event.add("metadata", meta); + } + return event; + } + + public static JsonObject userInterrupt() { + return userInterrupt(null); + } + + public static JsonObject userInterrupt(String sessionThreadId) { + JsonObject event = new JsonObject(); + event.addProperty("type", "interrupt"); + event.addProperty("role", "user"); + if (sessionThreadId != null) { + event.addProperty("session_thread_id", sessionThreadId); + } + return event; + } + + public static JsonObject userToolConfirmation(String toolUseId, String result) { + return userToolConfirmation(toolUseId, result, null, null); + } + + public static JsonObject userToolConfirmation( + String toolUseId, String result, String denyMessage) { + return userToolConfirmation(toolUseId, result, denyMessage, null); + } + + public static JsonObject userToolConfirmation( + String toolUseId, String result, String denyMessage, String sessionThreadId) { + if (!"allow".equals(result) && !"deny".equals(result)) { + throw new IllegalArgumentException("tool_confirmation result must be 'allow' or 'deny'"); + } + JsonObject data = new JsonObject(); + data.addProperty("call_id", toolUseId); + data.addProperty("result", result); + if (denyMessage != null && "deny".equals(result)) { + data.addProperty("deny_message", denyMessage); + } + JsonObject dataBlock = new JsonObject(); + dataBlock.addProperty("type", "data"); + dataBlock.add("data", data); + JsonArray content = new JsonArray(); + content.add(dataBlock); + JsonObject event = new JsonObject(); + event.addProperty("type", "tool_confirmation"); + event.addProperty("role", "user"); + event.add("content", content); + if (sessionThreadId != null) { + event.addProperty("session_thread_id", sessionThreadId); + } + return event; + } + + public static JsonObject userCustomToolResult( + String customToolUseId, String resultContent, boolean isError) { + return userCustomToolResult(customToolUseId, resultContent, isError, null); + } + + public static JsonObject userCustomToolResult( + String customToolUseId, String resultContent, boolean isError, String sessionThreadId) { + JsonObject data = new JsonObject(); + data.addProperty("call_id", customToolUseId); + data.addProperty("output", resultContent); + JsonObject dataBlock = new JsonObject(); + dataBlock.addProperty("type", "data"); + dataBlock.add("data", data); + JsonArray content = new JsonArray(); + content.add(dataBlock); + JsonObject event = new JsonObject(); + event.addProperty("type", "function_call_output"); + event.addProperty("role", "tool"); + event.add("content", content); + event.addProperty("is_error", isError); + if (sessionThreadId != null) { + event.addProperty("session_thread_id", sessionThreadId); + } + return event; + } + + public static JsonObject userToolResult(String toolUseId, String resultContent, boolean isError) { + return userToolResult(toolUseId, resultContent, isError, null); + } + + public static JsonObject userToolResult( + String toolUseId, String resultContent, boolean isError, String sessionThreadId) { + JsonObject data = new JsonObject(); + data.addProperty("call_id", toolUseId); + data.addProperty("output", resultContent); + JsonObject dataBlock = new JsonObject(); + dataBlock.addProperty("type", "data"); + dataBlock.add("data", data); + JsonArray content = new JsonArray(); + content.add(dataBlock); + JsonObject event = new JsonObject(); + event.addProperty("type", "tool_call_output"); + event.addProperty("role", "tool"); + event.add("content", content); + event.addProperty("is_error", isError); + if (sessionThreadId != null) { + event.addProperty("session_thread_id", sessionThreadId); + } + return event; + } + + public static JsonObject userDefineOutcome( + String description, String rubric, Integer maxIterations) { + return userDefineOutcome(description, rubric, maxIterations, null); + } + + public static JsonObject userDefineOutcome( + String description, String rubric, Integer maxIterations, String sessionThreadId) { + JsonObject data = new JsonObject(); + if (description != null) { + data.addProperty("description", description); + } + if (rubric != null) { + data.addProperty("rubric", rubric); + } + if (maxIterations != null) { + data.addProperty("max_iterations", maxIterations); + } + JsonObject dataBlock = new JsonObject(); + dataBlock.addProperty("type", "data"); + dataBlock.add("data", data); + JsonArray content = new JsonArray(); + content.add(dataBlock); + JsonObject event = new JsonObject(); + event.addProperty("type", "define_outcome"); + event.addProperty("role", "user"); + event.add("content", content); + if (sessionThreadId != null) { + event.addProperty("session_thread_id", sessionThreadId); + } + return event; + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/message/ContentBlock.java b/src/main/java/com/alibaba/dashscope/agentstudio/message/ContentBlock.java new file mode 100644 index 0000000..23abff6 --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/message/ContentBlock.java @@ -0,0 +1,160 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.message; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.annotations.SerializedName; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +public abstract class ContentBlock { + @SerializedName("type") + private String type; + + @Data + @EqualsAndHashCode(callSuper = true) + public static class Text extends ContentBlock { + @SerializedName("text") + private String text; + + @SerializedName("citations") + private List citations; + } + + @Data + public static class Citation { + @SerializedName("url") + private String url; + + @SerializedName("title") + private String title; + + @SerializedName("cited_text") + private String citedText; + } + + @Data + @EqualsAndHashCode(callSuper = true) + public static class Image extends ContentBlock { + @SerializedName("image_url") + private String imageUrl; + + @SerializedName("file_id") + private String fileId; + + @SerializedName("image_data") + private String imageData; + + @SerializedName("media_type") + private String mediaType; + } + + @Data + @EqualsAndHashCode(callSuper = true) + public static class Audio extends ContentBlock { + @SerializedName("data") + private String data; + + @SerializedName("format") + private String format; + + @SerializedName("file_id") + private String fileId; + } + + @Data + @EqualsAndHashCode(callSuper = true) + public static class DataContent extends ContentBlock { + @SerializedName("data") + private JsonObject data; + + @SerializedName("name") + private String name; + + @SerializedName("title") + private String title; + + @SerializedName("context") + private String context; + } + + @Data + @EqualsAndHashCode(callSuper = true) + public static class File extends ContentBlock { + @SerializedName("file_url") + private String fileUrl; + + @SerializedName("file_id") + private String fileId; + + @SerializedName("file_data") + private String fileData; + + @SerializedName("media_type") + private String mediaType; + + @SerializedName("filename") + private String filename; + } + + @Data + @EqualsAndHashCode(callSuper = true) + public static class Refusal extends ContentBlock { + @SerializedName("refusal") + private String refusal; + } + + @Data + @EqualsAndHashCode(callSuper = true) + public static class Error extends ContentBlock { + @SerializedName("error_code") + private String errorCode; + + @SerializedName("message") + private String message; + } + + public static class Deserializer implements JsonDeserializer { + private static final Map> REGISTRY = new HashMap<>(); + private static final Gson PLAIN_GSON = new GsonBuilder().create(); + + static { + REGISTRY.put("text", Text.class); + REGISTRY.put("image", Image.class); + REGISTRY.put("audio", Audio.class); + REGISTRY.put("data", DataContent.class); + REGISTRY.put("file", File.class); + REGISTRY.put("refusal", Refusal.class); + REGISTRY.put("error", Error.class); + } + + @Override + public ContentBlock deserialize( + JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + if (!json.isJsonObject()) { + return null; + } + JsonObject obj = json.getAsJsonObject(); + String type = null; + if (obj.has("type") && obj.get("type").isJsonPrimitive()) { + type = obj.get("type").getAsString(); + } + Class clazz = type != null ? REGISTRY.get(type) : null; + if (clazz == null) { + clazz = DataContent.class; + } + return PLAIN_GSON.fromJson(obj, clazz); + } + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/message/Message.java b/src/main/java/com/alibaba/dashscope/agentstudio/message/Message.java new file mode 100644 index 0000000..2db5eac --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/message/Message.java @@ -0,0 +1,74 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.message; + +import com.alibaba.dashscope.agentstudio.model.Session; +import com.alibaba.dashscope.utils.JsonUtils; +import com.google.gson.JsonObject; +import com.google.gson.annotations.SerializedName; +import java.util.List; +import java.util.Map; +import lombok.Data; + +@Data +public class Message { + @SerializedName("object") + private String object; + + @SerializedName("status") + private String status; + + @SerializedName("id") + private String id; + + @SerializedName("type") + private String type; + + @SerializedName("role") + private String role; + + @SerializedName("content") + private List content; + + @SerializedName("metadata") + private Map metadata; + + @SerializedName("is_error") + private Boolean isError; + + @SerializedName("created_at") + private String createdAt; + + @SerializedName("sequence_number") + private Long sequenceNumber; + + @SerializedName("session_thread_id") + private String sessionThreadId; + + @SerializedName("code") + private String code; + + @SerializedName("message") + private String message; + + /** + * Extract stop_reason from a session_status event's data block. + * + *

The SSE session_status idle event carries stop_reason in its data: {@code + * {"stop_reason":{"type":"end_turn"},"session_status":"idle"}}. This helper parses it out so + * callers don't need to manually dig through DataContent blocks. + * + * @return stop_reason (has type, eventIds), or null if not a session_status event or absent + */ + public Session.StopReason getStopReason() { + if (!"session_status".equals(type) || content == null) return null; + for (ContentBlock block : content) { + if (block instanceof ContentBlock.DataContent) { + JsonObject data = ((ContentBlock.DataContent) block).getData(); + if (data != null && data.has("stop_reason") && !data.get("stop_reason").isJsonNull()) { + return JsonUtils.fromJson(data.get("stop_reason"), Session.StopReason.class); + } + } + } + return null; + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/model/Agent.java b/src/main/java/com/alibaba/dashscope/agentstudio/model/Agent.java new file mode 100644 index 0000000..fc05b9f --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/model/Agent.java @@ -0,0 +1,66 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.model; + +import com.alibaba.dashscope.agentstudio.model.Configs.McpServerConfig; +import com.alibaba.dashscope.agentstudio.model.Configs.ModelConfig; +import com.alibaba.dashscope.agentstudio.model.Configs.SkillConfig; +import com.alibaba.dashscope.agentstudio.model.Configs.ToolConfig; +import com.alibaba.dashscope.common.FlattenResultBase; +import com.google.gson.annotations.SerializedName; +import java.util.List; +import java.util.Map; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class Agent extends FlattenResultBase { + @SerializedName("id") + private String id; + + @SerializedName("type") + private String type; + + @SerializedName("name") + private String name; + + @SerializedName("description") + private String description; + + @SerializedName("model") + private ModelConfig model; + + @SerializedName("system") + private String system; + + @SerializedName("tools") + private List tools; + + @SerializedName("skills") + private List skills; + + @SerializedName("mcp_servers") + private List mcpServers; + + @SerializedName("version") + private Integer version; + + @SerializedName("metadata") + private Map metadata; + + @SerializedName("workspace_id") + private String workspaceId; + + @SerializedName("archived_at") + private String archivedAt; + + @SerializedName("created_at") + private String createdAt; + + @SerializedName("updated_at") + private String updatedAt; + + public String getSystemPrompt() { + return system; + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/model/AgentStudioDeletionStatus.java b/src/main/java/com/alibaba/dashscope/agentstudio/model/AgentStudioDeletionStatus.java new file mode 100644 index 0000000..1e33668 --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/model/AgentStudioDeletionStatus.java @@ -0,0 +1,21 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.model; + +import com.alibaba.dashscope.common.FlattenResultBase; +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class AgentStudioDeletionStatus extends FlattenResultBase { + @SerializedName("id") + private String id; + + @SerializedName("type") + private String type; + + public boolean isDeleted() { + return type != null && type.contains("deleted"); + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/model/AgentStudioFile.java b/src/main/java/com/alibaba/dashscope/agentstudio/model/AgentStudioFile.java new file mode 100644 index 0000000..1d570c2 --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/model/AgentStudioFile.java @@ -0,0 +1,35 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.model; + +import com.alibaba.dashscope.common.FlattenResultBase; +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class AgentStudioFile extends FlattenResultBase { + @SerializedName("id") + private String id; + + @SerializedName("type") + private String type; + + @SerializedName("filename") + private String filename; + + @SerializedName("downloadable") + private Boolean downloadable; + + @SerializedName("mime_type") + private String mimeType; + + @SerializedName("size_bytes") + private Long sizeBytes; + + @SerializedName("status") + private String status; + + @SerializedName("created_at") + private String createdAt; +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/model/AgentVersion.java b/src/main/java/com/alibaba/dashscope/agentstudio/model/AgentVersion.java new file mode 100644 index 0000000..c388a43 --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/model/AgentVersion.java @@ -0,0 +1,56 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.model; + +import com.alibaba.dashscope.agentstudio.model.Configs.McpServerConfig; +import com.alibaba.dashscope.agentstudio.model.Configs.ModelConfig; +import com.alibaba.dashscope.agentstudio.model.Configs.SkillConfig; +import com.alibaba.dashscope.agentstudio.model.Configs.ToolConfig; +import com.alibaba.dashscope.common.FlattenResultBase; +import com.google.gson.annotations.SerializedName; +import java.util.List; +import java.util.Map; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class AgentVersion extends FlattenResultBase { + @SerializedName("agent_id") + private String agentId; + + @SerializedName("version") + private Integer version; + + @SerializedName("config") + private AgentVersionConfig config; + + @SerializedName("created_at") + private String createdAt; + + @Data + public static class AgentVersionConfig { + @SerializedName("name") + private String name; + + @SerializedName("description") + private String description; + + @SerializedName("model") + private ModelConfig model; + + @SerializedName("system") + private String system; + + @SerializedName("tools") + private List tools; + + @SerializedName("skills") + private List skills; + + @SerializedName("mcp_servers") + private List mcpServers; + + @SerializedName("metadata") + private Map metadata; + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/model/Configs.java b/src/main/java/com/alibaba/dashscope/agentstudio/model/Configs.java new file mode 100644 index 0000000..7c515c1 --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/model/Configs.java @@ -0,0 +1,73 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.model; + +import com.google.gson.annotations.SerializedName; +import java.util.List; +import lombok.Data; + +public final class Configs { + private Configs() {} + + @Data + public static class ModelConfig { + @SerializedName("id") + private String id; + + @SerializedName("name") + private String name; + } + + @Data + public static class ToolConfig { + @SerializedName("type") + private String type; + + @SerializedName("mcp_server_name") + private String mcpServerName; + + @SerializedName("default_config") + private DefaultConfig defaultConfig; + + @SerializedName("configs") + private List configs; + + @Data + public static class DefaultConfig { + @SerializedName("enabled") + private Boolean enabled; + } + + @Data + public static class PerToolConfig { + @SerializedName("name") + private String name; + + @SerializedName("enabled") + private Boolean enabled; + } + } + + @Data + public static class SkillConfig { + @SerializedName("type") + private String type; + + @SerializedName("skill_id") + private String skillId; + + @SerializedName("version") + private String version; + } + + @Data + public static class McpServerConfig { + @SerializedName("type") + private String type; + + @SerializedName("name") + private String name; + + @SerializedName("url") + private String url; + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/model/Credential.java b/src/main/java/com/alibaba/dashscope/agentstudio/model/Credential.java new file mode 100644 index 0000000..a15968a --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/model/Credential.java @@ -0,0 +1,75 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.model; + +import com.alibaba.dashscope.common.FlattenResultBase; +import com.google.gson.annotations.SerializedName; +import java.util.Map; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class Credential extends FlattenResultBase { + @SerializedName("id") + private String id; + + @SerializedName("type") + private String type; + + @SerializedName("vault_id") + private String vaultId; + + @SerializedName("display_name") + private String displayName; + + @SerializedName("auth") + private CredentialAuth auth; + + @SerializedName("metadata") + private Map metadata; + + @SerializedName("archived_at") + private String archivedAt; + + @SerializedName("created_at") + private String createdAt; + + @SerializedName("updated_at") + private String updatedAt; + + @Data + public static class Networking { + @SerializedName("type") + private String type; + } + + @Data + public static class CredentialAuth { + @SerializedName("type") + private String type; + + @SerializedName("token") + private String token; + + @SerializedName("secret_name") + private String secretName; + + @SerializedName("secret_value") + private String secretValue; + + @SerializedName("mcp_server_url") + private String mcpServerUrl; + + @SerializedName("access_token") + private String accessToken; + + @SerializedName("expires_at") + private String expiresAt; + + @SerializedName("refresh") + private String refresh; + + @SerializedName("networking") + private Networking networking; + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/model/Environment.java b/src/main/java/com/alibaba/dashscope/agentstudio/model/Environment.java new file mode 100644 index 0000000..c8e0547 --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/model/Environment.java @@ -0,0 +1,85 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.model; + +import com.alibaba.dashscope.common.FlattenResultBase; +import com.google.gson.annotations.SerializedName; +import java.util.List; +import java.util.Map; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class Environment extends FlattenResultBase { + @SerializedName("id") + private String id; + + @SerializedName("type") + private String type; + + @SerializedName("name") + private String name; + + @SerializedName("description") + private String description; + + @SerializedName("status") + private String status; + + @SerializedName("config") + private Config config; + + @SerializedName("metadata") + private Map metadata; + + @SerializedName("scope") + private String scope; + + @SerializedName("archived_at") + private String archivedAt; + + @SerializedName("created_at") + private String createdAt; + + @SerializedName("updated_at") + private String updatedAt; + + @Data + public static class Config { + @SerializedName("type") + private String type; + + @SerializedName("networking") + private Networking networking; + + @SerializedName("packages") + private Packages packages; + + @Data + public static class Networking { + @SerializedName("type") + private String type; + } + + @Data + public static class Packages { + @SerializedName("apt") + private List apt; + + @SerializedName("gem") + private List gem; + + @SerializedName("pip") + private List pip; + + @SerializedName("cargo") + private List cargo; + + @SerializedName("go") + private List go; + + @SerializedName("npm") + private List npm; + } + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/model/Session.java b/src/main/java/com/alibaba/dashscope/agentstudio/model/Session.java new file mode 100644 index 0000000..b645cff --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/model/Session.java @@ -0,0 +1,137 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.model; + +import com.alibaba.dashscope.agentstudio.model.Configs.ModelConfig; +import com.alibaba.dashscope.agentstudio.model.Configs.ToolConfig; +import com.alibaba.dashscope.common.FlattenResultBase; +import com.google.gson.JsonObject; +import com.google.gson.annotations.SerializedName; +import java.util.List; +import java.util.Map; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class Session extends FlattenResultBase { + @SerializedName("id") + private String id; + + @SerializedName("type") + private String type; + + @SerializedName("title") + private String title; + + @SerializedName("agent") + private SessionAgent agent; + + @SerializedName("environment_id") + private String environmentId; + + @SerializedName("status") + private String status; + + @SerializedName("stop_reason") + private StopReason stopReason; + + @SerializedName("resources") + private List resources; + + @SerializedName("metadata") + private Map metadata; + + @SerializedName("vault_ids") + private List vaultIds; + + @SerializedName("stats") + private Stats stats; + + @SerializedName("usage") + private Usage usage; + + @SerializedName("archived_at") + private String archivedAt; + + @SerializedName("created_at") + private String createdAt; + + @SerializedName("updated_at") + private String updatedAt; + + public String getAgentId() { + if (agent != null) return agent.getId(); + return null; + } + + public Integer getAgentVersion() { + if (agent != null) return agent.getVersion(); + return null; + } + + @Data + public static class SessionAgent { + @SerializedName("id") + private String id; + + @SerializedName("type") + private String type; + + @SerializedName("version") + private Integer version; + + @SerializedName("name") + private String name; + + @SerializedName("description") + private String description; + + @SerializedName("model") + private ModelConfig model; + + @SerializedName("system") + private String system; + + @SerializedName("tools") + private List tools; + } + + @Data + public static class StopReason { + @SerializedName("type") + private String type; + + @SerializedName("event_ids") + private List eventIds; + } + + @Data + public static class Usage { + @SerializedName("input_tokens") + private Long inputTokens; + + @SerializedName("output_tokens") + private Long outputTokens; + + @SerializedName("cache_creation_input_tokens") + private Long cacheCreationInputTokens; + + @SerializedName("cache_read_input_tokens") + private Long cacheReadInputTokens; + + @SerializedName("cache_creation") + private Long cacheCreation; + + @SerializedName("speed") + private Double speed; + } + + @Data + public static class Stats { + @SerializedName("active_seconds") + private Double activeSeconds; + + @SerializedName("duration_seconds") + private Double durationSeconds; + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/model/Skill.java b/src/main/java/com/alibaba/dashscope/agentstudio/model/Skill.java new file mode 100644 index 0000000..ea6b930 --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/model/Skill.java @@ -0,0 +1,56 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.model; + +import com.alibaba.dashscope.common.FlattenResultBase; +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class Skill extends FlattenResultBase { + @SerializedName("id") + private String id; + + @SerializedName("type") + private String type; + + @SerializedName("name") + private String name; + + @SerializedName("description") + private String description; + + @SerializedName("source") + private String source; + + @SerializedName("status") + private String status; + + @SerializedName("latest_version") + private String latestVersion; + + @SerializedName("version") + private String version; + + @SerializedName("file_id") + private String fileId; + + @SerializedName("scope") + private Scope scope; + + @SerializedName("created_at") + private String createdAt; + + @SerializedName("updated_at") + private String updatedAt; + + @Data + public static class Scope { + @SerializedName("type") + private String type; + + @SerializedName("id") + private String id; + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/model/SkillVersion.java b/src/main/java/com/alibaba/dashscope/agentstudio/model/SkillVersion.java new file mode 100644 index 0000000..f0c4d1f --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/model/SkillVersion.java @@ -0,0 +1,42 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.model; + +import com.alibaba.dashscope.common.FlattenResultBase; +import com.google.gson.JsonElement; +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class SkillVersion extends FlattenResultBase { + @SerializedName("id") + private String id; + + @SerializedName("type") + private String type; + + @SerializedName("skill_id") + private String skillId; + + @SerializedName("name") + private String name; + + @SerializedName("description") + private String description; + + @SerializedName("version") + private String version; + + @SerializedName("status") + private String status; + + @SerializedName("additional_properties") + private JsonElement additionalProperties; + + @SerializedName("created_at") + private String createdAt; + + @SerializedName("updated_at") + private String updatedAt; +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/model/Vault.java b/src/main/java/com/alibaba/dashscope/agentstudio/model/Vault.java new file mode 100644 index 0000000..0157f40 --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/model/Vault.java @@ -0,0 +1,33 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.model; + +import com.alibaba.dashscope.common.FlattenResultBase; +import com.google.gson.annotations.SerializedName; +import java.util.Map; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class Vault extends FlattenResultBase { + @SerializedName("id") + private String id; + + @SerializedName("type") + private String type; + + @SerializedName("display_name") + private String displayName; + + @SerializedName("metadata") + private Map metadata; + + @SerializedName("archived_at") + private String archivedAt; + + @SerializedName("created_at") + private String createdAt; + + @SerializedName("updated_at") + private String updatedAt; +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/pagination/CursorPage.java b/src/main/java/com/alibaba/dashscope/agentstudio/pagination/CursorPage.java new file mode 100644 index 0000000..d09c5bd --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/pagination/CursorPage.java @@ -0,0 +1,65 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.pagination; + +import com.alibaba.dashscope.common.FlattenResultBase; +import com.google.gson.annotations.SerializedName; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class CursorPage extends FlattenResultBase implements Iterable { + @SerializedName("data") + private List data; + + @SerializedName("next_page") + private String nextPage; + + @EqualsAndHashCode.Exclude + private transient Function>> fetchNext; + + public boolean hasNext() { + return nextPage != null && fetchNext != null; + } + + public CompletableFuture> getNext() { + if (!hasNext()) { + return CompletableFuture.completedFuture(null); + } + return fetchNext.apply(nextPage); + } + + @Override + public Iterator iterator() { + return new Iterator() { + private CursorPage currentPage = CursorPage.this; + private int index = 0; + + @Override + public boolean hasNext() { + while (true) { + if (currentPage.data != null && index < currentPage.data.size()) { + return true; + } + if (!currentPage.hasNext()) { + return false; + } + currentPage = currentPage.getNext().join(); + if (currentPage == null) return false; + index = 0; + } + } + + @Override + public T next() { + if (!hasNext()) throw new NoSuchElementException(); + return currentPage.data.get(index++); + } + }; + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/param/AgentCreateParam.java b/src/main/java/com/alibaba/dashscope/agentstudio/param/AgentCreateParam.java new file mode 100644 index 0000000..867e6cf --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/param/AgentCreateParam.java @@ -0,0 +1,59 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.param; + +import com.alibaba.dashscope.agentstudio.model.Configs.McpServerConfig; +import com.alibaba.dashscope.agentstudio.model.Configs.SkillConfig; +import com.alibaba.dashscope.agentstudio.model.Configs.ToolConfig; +import com.alibaba.dashscope.base.FlattenHalfDuplexParamBase; +import com.alibaba.dashscope.utils.JsonUtils; +import com.google.gson.JsonObject; +import java.util.List; +import java.util.Map; +import lombok.Builder.Default; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NonNull; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@EqualsAndHashCode(callSuper = true) +public class AgentCreateParam extends FlattenHalfDuplexParamBase { + @NonNull private String name; + @NonNull private String model; + @Default private String description = null; + @Default private String systemPrompt = null; + @Default private List tools = null; + @Default private List mcpServers = null; + @Default private List skills = null; + @Default private Map metadata = null; + + @Override + public JsonObject getHttpBody() { + JsonObject body = new JsonObject(); + body.addProperty("name", name); + JsonObject modelObj = new JsonObject(); + modelObj.addProperty("id", model); + body.add("model", modelObj); + if (description != null) { + body.addProperty("description", description); + } + if (systemPrompt != null) { + body.addProperty("system", systemPrompt); + } + if (tools != null) { + body.add("tools", JsonUtils.toJsonElement(tools)); + } + if (mcpServers != null) { + body.add("mcp_servers", JsonUtils.toJsonElement(mcpServers)); + } + if (skills != null) { + body.add("skills", JsonUtils.toJsonElement(skills)); + } + if (metadata != null && !metadata.isEmpty()) { + body.add("metadata", JsonUtils.toJsonElement(metadata)); + } + addExtraBody(body); + return body; + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/param/AgentListParam.java b/src/main/java/com/alibaba/dashscope/agentstudio/param/AgentListParam.java new file mode 100644 index 0000000..a6652a4 --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/param/AgentListParam.java @@ -0,0 +1,32 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.param; + +import com.alibaba.dashscope.agentstudio.AgentStudioConstants; +import com.alibaba.dashscope.base.FlattenHalfDuplexParamBase; +import com.google.gson.JsonObject; +import lombok.Builder.Default; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@EqualsAndHashCode(callSuper = true) +public class AgentListParam extends FlattenHalfDuplexParamBase { + @Default private Integer limit = null; + @Default private String page = null; + @Default private Boolean includeArchived = null; + + @Override + public JsonObject getHttpBody() { + return new JsonObject(); + } + + public String toQueryString() { + StringBuilder sb = new StringBuilder(); + AgentStudioConstants.appendParam(sb, "limit", limit); + AgentStudioConstants.appendParam(sb, "page", page); + AgentStudioConstants.appendParam(sb, "include_archived", includeArchived); + return sb.toString(); + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/param/AgentUpdateParam.java b/src/main/java/com/alibaba/dashscope/agentstudio/param/AgentUpdateParam.java new file mode 100644 index 0000000..fdc89af --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/param/AgentUpdateParam.java @@ -0,0 +1,78 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.param; + +import com.alibaba.dashscope.agentstudio.model.Configs.McpServerConfig; +import com.alibaba.dashscope.agentstudio.model.Configs.SkillConfig; +import com.alibaba.dashscope.agentstudio.model.Configs.ToolConfig; +import com.alibaba.dashscope.base.FlattenHalfDuplexParamBase; +import com.alibaba.dashscope.exception.InputRequiredException; +import com.alibaba.dashscope.utils.JsonUtils; +import com.google.gson.JsonObject; +import java.util.List; +import java.util.Map; +import lombok.Builder.Default; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@EqualsAndHashCode(callSuper = true) +public class AgentUpdateParam extends FlattenHalfDuplexParamBase { + /** Required: must equal the server's current (latest) version, or the update fails with 409. */ + @Default private Integer version = null; + + @Default private String name = null; + @Default private String model = null; + @Default private String description = null; + @Default private String systemPrompt = null; + @Default private List tools = null; + @Default private List mcpServers = null; + @Default private List skills = null; + @Default private Map metadata = null; + + @Override + public JsonObject getHttpBody() { + JsonObject body = new JsonObject(); + if (version != null) { + body.addProperty("version", version); + } + if (name != null) { + body.addProperty("name", name); + } + if (model != null) { + JsonObject modelObj = new JsonObject(); + modelObj.addProperty("id", model); + body.add("model", modelObj); + } + if (description != null) { + body.addProperty("description", description); + } + if (systemPrompt != null) { + body.addProperty("system", systemPrompt); + } + if (tools != null) { + body.add("tools", JsonUtils.toJsonElement(tools)); + } + if (mcpServers != null) { + body.add("mcp_servers", JsonUtils.toJsonElement(mcpServers)); + } + if (skills != null) { + body.add("skills", JsonUtils.toJsonElement(skills)); + } + if (metadata != null && !metadata.isEmpty()) { + body.add("metadata", JsonUtils.toJsonElement(metadata)); + } + addExtraBody(body); + return body; + } + + @Override + public void validate() throws InputRequiredException { + if (version == null) { + throw new InputRequiredException( + "version is required for update. Call agents().retrieve(agentId) first to obtain the" + + " current version (server rejects updates that don't match the latest version)."); + } + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/param/CredentialCreateParam.java b/src/main/java/com/alibaba/dashscope/agentstudio/param/CredentialCreateParam.java new file mode 100644 index 0000000..a75f1b2 --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/param/CredentialCreateParam.java @@ -0,0 +1,35 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.param; + +import com.alibaba.dashscope.base.FlattenHalfDuplexParamBase; +import com.alibaba.dashscope.utils.JsonUtils; +import com.google.gson.JsonObject; +import java.util.Map; +import lombok.Builder.Default; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NonNull; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@EqualsAndHashCode(callSuper = true) +public class CredentialCreateParam extends FlattenHalfDuplexParamBase { + @NonNull private JsonObject auth; + @Default private String displayName = null; + @Default private Map metadata = null; + + @Override + public JsonObject getHttpBody() { + JsonObject body = new JsonObject(); + body.add("auth", auth); + if (displayName != null) { + body.addProperty("display_name", displayName); + } + if (metadata != null && !metadata.isEmpty()) { + body.add("metadata", JsonUtils.toJsonElement(metadata)); + } + addExtraBody(body); + return body; + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/param/CredentialListParam.java b/src/main/java/com/alibaba/dashscope/agentstudio/param/CredentialListParam.java new file mode 100644 index 0000000..6ac1081 --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/param/CredentialListParam.java @@ -0,0 +1,32 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.param; + +import com.alibaba.dashscope.agentstudio.AgentStudioConstants; +import com.alibaba.dashscope.base.FlattenHalfDuplexParamBase; +import com.google.gson.JsonObject; +import lombok.Builder.Default; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@EqualsAndHashCode(callSuper = true) +public class CredentialListParam extends FlattenHalfDuplexParamBase { + @Default private Integer limit = null; + @Default private String page = null; + @Default private Boolean includeArchived = null; + + @Override + public JsonObject getHttpBody() { + return new JsonObject(); + } + + public String toQueryString() { + StringBuilder sb = new StringBuilder(); + AgentStudioConstants.appendParam(sb, "limit", limit); + AgentStudioConstants.appendParam(sb, "page", page); + AgentStudioConstants.appendParam(sb, "include_archived", includeArchived); + return sb.toString(); + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/param/CredentialUpdateParam.java b/src/main/java/com/alibaba/dashscope/agentstudio/param/CredentialUpdateParam.java new file mode 100644 index 0000000..ccb3a02 --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/param/CredentialUpdateParam.java @@ -0,0 +1,36 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.param; + +import com.alibaba.dashscope.base.FlattenHalfDuplexParamBase; +import com.alibaba.dashscope.utils.JsonUtils; +import com.google.gson.JsonObject; +import java.util.Map; +import lombok.Builder.Default; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@EqualsAndHashCode(callSuper = true) +public class CredentialUpdateParam extends FlattenHalfDuplexParamBase { + @Default private JsonObject auth = null; + @Default private String displayName = null; + @Default private Map metadata = null; + + @Override + public JsonObject getHttpBody() { + JsonObject body = new JsonObject(); + if (auth != null) { + body.add("auth", auth); + } + if (displayName != null) { + body.addProperty("display_name", displayName); + } + if (metadata != null) { + body.add("metadata", JsonUtils.toJsonElement(metadata)); + } + addExtraBody(body); + return body; + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/param/EnvironmentCreateParam.java b/src/main/java/com/alibaba/dashscope/agentstudio/param/EnvironmentCreateParam.java new file mode 100644 index 0000000..d7e0303 --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/param/EnvironmentCreateParam.java @@ -0,0 +1,67 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.param; + +import com.alibaba.dashscope.base.FlattenHalfDuplexParamBase; +import com.alibaba.dashscope.utils.JsonUtils; +import com.google.gson.JsonObject; +import java.util.Map; +import lombok.Builder.Default; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NonNull; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@EqualsAndHashCode(callSuper = true) +public class EnvironmentCreateParam extends FlattenHalfDuplexParamBase { + @NonNull private String name; + + /** + * Environment runtime configuration (open structure, aligned with server-side {@code + * JSONObject}). + * + *

Known keys: + * + *

    + *
  • {@code type} (String) — hosting type, e.g. "cloud" + *
  • {@code networking} (Map) — e.g. {"type": "unrestricted"} + *
  • {@code packages} (Map<String, List<String>>) — e.g. {"pip": ["numpy"]} + *
+ * + *

Example: + * + *

{@code
+   * Map config = new HashMap<>();
+   * config.put("type", "cloud");
+   * config.put("networking", Collections.singletonMap("type", "unrestricted"));
+   *
+   * Map> packages = new HashMap<>();
+   * packages.put("pip", Arrays.asList("numpy", "pandas"));
+   * config.put("packages", packages);
+   * }
+ */ + @NonNull private Map config; + + @Default private String description = null; + @Default private String scope = null; + @Default private Map metadata = null; + + @Override + public JsonObject getHttpBody() { + JsonObject body = new JsonObject(); + body.addProperty("name", name); + body.add("config", JsonUtils.toJsonElement(config)); + if (description != null) { + body.addProperty("description", description); + } + if (scope != null) { + body.addProperty("scope", scope); + } + if (metadata != null && !metadata.isEmpty()) { + body.add("metadata", JsonUtils.toJsonElement(metadata)); + } + addExtraBody(body); + return body; + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/param/EnvironmentListParam.java b/src/main/java/com/alibaba/dashscope/agentstudio/param/EnvironmentListParam.java new file mode 100644 index 0000000..e94a98a --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/param/EnvironmentListParam.java @@ -0,0 +1,32 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.param; + +import com.alibaba.dashscope.agentstudio.AgentStudioConstants; +import com.alibaba.dashscope.base.FlattenHalfDuplexParamBase; +import com.google.gson.JsonObject; +import lombok.Builder.Default; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@EqualsAndHashCode(callSuper = true) +public class EnvironmentListParam extends FlattenHalfDuplexParamBase { + @Default private Integer limit = null; + @Default private String page = null; + @Default private Boolean includeArchived = null; + + @Override + public JsonObject getHttpBody() { + return new JsonObject(); + } + + public String toQueryString() { + StringBuilder sb = new StringBuilder(); + AgentStudioConstants.appendParam(sb, "limit", limit); + AgentStudioConstants.appendParam(sb, "page", page); + AgentStudioConstants.appendParam(sb, "include_archived", includeArchived); + return sb.toString(); + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/param/EnvironmentUpdateParam.java b/src/main/java/com/alibaba/dashscope/agentstudio/param/EnvironmentUpdateParam.java new file mode 100644 index 0000000..7079c9b --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/param/EnvironmentUpdateParam.java @@ -0,0 +1,44 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.param; + +import com.alibaba.dashscope.base.FlattenHalfDuplexParamBase; +import com.alibaba.dashscope.utils.JsonUtils; +import com.google.gson.JsonObject; +import java.util.Map; +import lombok.Builder.Default; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@EqualsAndHashCode(callSuper = true) +public class EnvironmentUpdateParam extends FlattenHalfDuplexParamBase { + @Default private String name = null; + @Default private String description = null; + @Default private Map config = null; + @Default private String scope = null; + @Default private Map metadata = null; + + @Override + public JsonObject getHttpBody() { + JsonObject body = new JsonObject(); + if (name != null) { + body.addProperty("name", name); + } + if (description != null) { + body.addProperty("description", description); + } + if (config != null) { + body.add("config", JsonUtils.toJsonElement(config)); + } + if (scope != null) { + body.addProperty("scope", scope); + } + if (metadata != null) { + body.add("metadata", JsonUtils.toJsonElement(metadata)); + } + addExtraBody(body); + return body; + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/param/FileListParam.java b/src/main/java/com/alibaba/dashscope/agentstudio/param/FileListParam.java new file mode 100644 index 0000000..64d4935 --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/param/FileListParam.java @@ -0,0 +1,36 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.param; + +import com.alibaba.dashscope.agentstudio.AgentStudioConstants; +import com.alibaba.dashscope.base.FlattenHalfDuplexParamBase; +import com.google.gson.JsonObject; +import lombok.Builder.Default; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@EqualsAndHashCode(callSuper = true) +public class FileListParam extends FlattenHalfDuplexParamBase { + @Default private Integer limit = null; + @Default private String page = null; + @Default private String scopeId = null; + @Default private String afterId = null; + @Default private String beforeId = null; + + @Override + public JsonObject getHttpBody() { + return new JsonObject(); + } + + public String toQueryString() { + StringBuilder sb = new StringBuilder(); + AgentStudioConstants.appendParam(sb, "limit", limit); + AgentStudioConstants.appendParam(sb, "page", page); + AgentStudioConstants.appendParam(sb, "scope_id", scopeId); + AgentStudioConstants.appendParam(sb, "after_id", afterId); + AgentStudioConstants.appendParam(sb, "before_id", beforeId); + return sb.toString(); + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionCreateParam.java b/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionCreateParam.java new file mode 100644 index 0000000..520994a --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionCreateParam.java @@ -0,0 +1,39 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.param; + +import com.alibaba.dashscope.base.FlattenHalfDuplexParamBase; +import com.alibaba.dashscope.utils.JsonUtils; +import com.google.gson.JsonObject; +import java.util.Map; +import lombok.Builder.Default; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NonNull; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@EqualsAndHashCode(callSuper = true) +public class SessionCreateParam extends FlattenHalfDuplexParamBase { + @NonNull private String agent; + @Default private String environmentId = null; + @Default private String title = null; + @Default private Map metadata = null; + + @Override + public JsonObject getHttpBody() { + JsonObject body = new JsonObject(); + body.addProperty("agent", agent); + if (environmentId != null) { + body.addProperty("environment_id", environmentId); + } + if (title != null) { + body.addProperty("title", title); + } + if (metadata != null && !metadata.isEmpty()) { + body.add("metadata", JsonUtils.toJsonElement(metadata)); + } + addExtraBody(body); + return body; + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionEventListParam.java b/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionEventListParam.java new file mode 100644 index 0000000..218e328 --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionEventListParam.java @@ -0,0 +1,45 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.param; + +import com.alibaba.dashscope.agentstudio.AgentStudioConstants; +import com.alibaba.dashscope.base.FlattenHalfDuplexParamBase; +import com.google.gson.JsonObject; +import java.util.List; +import lombok.Builder.Default; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@EqualsAndHashCode(callSuper = true) +public class SessionEventListParam extends FlattenHalfDuplexParamBase { + @Default private List types = null; + @Default private String createdAtGt = null; + @Default private String createdAtGte = null; + @Default private String createdAtLt = null; + @Default private String createdAtLte = null; + @Default private Integer limit = null; + @Default private String order = null; + @Default private String page = null; + + @Override + public JsonObject getHttpBody() { + return new JsonObject(); + } + + public String toQueryString() { + StringBuilder sb = new StringBuilder(); + if (types != null && !types.isEmpty()) { + AgentStudioConstants.appendParam(sb, "types", String.join(",", types)); + } + AgentStudioConstants.appendParam(sb, "created_at[gt]", createdAtGt); + AgentStudioConstants.appendParam(sb, "created_at[gte]", createdAtGte); + AgentStudioConstants.appendParam(sb, "created_at[lt]", createdAtLt); + AgentStudioConstants.appendParam(sb, "created_at[lte]", createdAtLte); + AgentStudioConstants.appendParam(sb, "limit", limit); + AgentStudioConstants.appendParam(sb, "order", order); + AgentStudioConstants.appendParam(sb, "page", page); + return sb.toString(); + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionEventSendParam.java b/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionEventSendParam.java new file mode 100644 index 0000000..bf08c15 --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionEventSendParam.java @@ -0,0 +1,30 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.param; + +import com.alibaba.dashscope.base.FlattenHalfDuplexParamBase; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import java.util.List; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NonNull; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@EqualsAndHashCode(callSuper = true) +public class SessionEventSendParam extends FlattenHalfDuplexParamBase { + @NonNull private List input; + + @Override + public JsonObject getHttpBody() { + JsonObject body = new JsonObject(); + JsonArray arr = new JsonArray(); + for (JsonObject event : input) { + arr.add(event); + } + body.add("input", arr); + addExtraBody(body); + return body; + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionListParam.java b/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionListParam.java new file mode 100644 index 0000000..251285e --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionListParam.java @@ -0,0 +1,47 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.param; + +import com.alibaba.dashscope.agentstudio.AgentStudioConstants; +import com.alibaba.dashscope.base.FlattenHalfDuplexParamBase; +import com.google.gson.JsonObject; +import java.util.List; +import lombok.Builder.Default; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@EqualsAndHashCode(callSuper = true) +public class SessionListParam extends FlattenHalfDuplexParamBase { + @Default private Integer limit = null; + @Default private String page = null; + @Default private String agentId = null; + @Default private List statuses = null; + @Default private String createdAtGt = null; + @Default private String createdAtGte = null; + @Default private String createdAtLt = null; + @Default private String createdAtLte = null; + + @Override + public JsonObject getHttpBody() { + return new JsonObject(); + } + + public String toQueryString() { + StringBuilder sb = new StringBuilder(); + AgentStudioConstants.appendParam(sb, "limit", limit); + AgentStudioConstants.appendParam(sb, "page", page); + AgentStudioConstants.appendParam(sb, "agent_id", agentId); + if (statuses != null) { + for (String s : statuses) { + AgentStudioConstants.appendParam(sb, "statuses[]", s); + } + } + AgentStudioConstants.appendParam(sb, "created_at[gt]", createdAtGt); + AgentStudioConstants.appendParam(sb, "created_at[gte]", createdAtGte); + AgentStudioConstants.appendParam(sb, "created_at[lt]", createdAtLt); + AgentStudioConstants.appendParam(sb, "created_at[lte]", createdAtLte); + return sb.toString(); + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionUpdateParam.java b/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionUpdateParam.java new file mode 100644 index 0000000..73c9e5a --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionUpdateParam.java @@ -0,0 +1,32 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.param; + +import com.alibaba.dashscope.base.FlattenHalfDuplexParamBase; +import com.alibaba.dashscope.utils.JsonUtils; +import com.google.gson.JsonObject; +import java.util.Map; +import lombok.Builder.Default; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@EqualsAndHashCode(callSuper = true) +public class SessionUpdateParam extends FlattenHalfDuplexParamBase { + @Default private String title = null; + @Default private Map metadata = null; + + @Override + public JsonObject getHttpBody() { + JsonObject body = new JsonObject(); + if (title != null) { + body.addProperty("title", title); + } + if (metadata != null) { + body.add("metadata", JsonUtils.toJsonElement(metadata)); + } + addExtraBody(body); + return body; + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/param/SkillCreateParam.java b/src/main/java/com/alibaba/dashscope/agentstudio/param/SkillCreateParam.java new file mode 100644 index 0000000..1ab1c4f --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/param/SkillCreateParam.java @@ -0,0 +1,28 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.param; + +import com.alibaba.dashscope.base.FlattenHalfDuplexParamBase; +import com.google.gson.JsonObject; +import lombok.Builder.Default; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@EqualsAndHashCode(callSuper = true) +public class SkillCreateParam extends FlattenHalfDuplexParamBase { + @Default private String fileId = null; + @Default private String file = null; + @Default private String mimeType = null; + + @Override + public JsonObject getHttpBody() { + JsonObject body = new JsonObject(); + if (fileId != null) { + body.addProperty("file_id", fileId); + } + addExtraBody(body); + return body; + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/param/SkillListParam.java b/src/main/java/com/alibaba/dashscope/agentstudio/param/SkillListParam.java new file mode 100644 index 0000000..b32a9af --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/param/SkillListParam.java @@ -0,0 +1,32 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.param; + +import com.alibaba.dashscope.agentstudio.AgentStudioConstants; +import com.alibaba.dashscope.base.FlattenHalfDuplexParamBase; +import com.google.gson.JsonObject; +import lombok.Builder.Default; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@EqualsAndHashCode(callSuper = true) +public class SkillListParam extends FlattenHalfDuplexParamBase { + @Default private String source = null; + @Default private Integer limit = null; + @Default private String page = null; + + @Override + public JsonObject getHttpBody() { + return new JsonObject(); + } + + public String toQueryString() { + StringBuilder sb = new StringBuilder(); + AgentStudioConstants.appendParam(sb, "source", source); + AgentStudioConstants.appendParam(sb, "limit", limit); + AgentStudioConstants.appendParam(sb, "page", page); + return sb.toString(); + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/param/VaultCreateParam.java b/src/main/java/com/alibaba/dashscope/agentstudio/param/VaultCreateParam.java new file mode 100644 index 0000000..aa925cf --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/param/VaultCreateParam.java @@ -0,0 +1,31 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.param; + +import com.alibaba.dashscope.base.FlattenHalfDuplexParamBase; +import com.alibaba.dashscope.utils.JsonUtils; +import com.google.gson.JsonObject; +import java.util.Map; +import lombok.Builder.Default; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NonNull; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@EqualsAndHashCode(callSuper = true) +public class VaultCreateParam extends FlattenHalfDuplexParamBase { + @NonNull private String displayName; + @Default private Map metadata = null; + + @Override + public JsonObject getHttpBody() { + JsonObject body = new JsonObject(); + body.addProperty("display_name", displayName); + if (metadata != null && !metadata.isEmpty()) { + body.add("metadata", JsonUtils.toJsonElement(metadata)); + } + addExtraBody(body); + return body; + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/param/VaultListParam.java b/src/main/java/com/alibaba/dashscope/agentstudio/param/VaultListParam.java new file mode 100644 index 0000000..7f92b11 --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/param/VaultListParam.java @@ -0,0 +1,34 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.param; + +import com.alibaba.dashscope.agentstudio.AgentStudioConstants; +import com.alibaba.dashscope.base.FlattenHalfDuplexParamBase; +import com.google.gson.JsonObject; +import lombok.Builder.Default; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@EqualsAndHashCode(callSuper = true) +public class VaultListParam extends FlattenHalfDuplexParamBase { + @Default private Integer limit = null; + @Default private String page = null; + @Default private Boolean includeArchived = null; + @Default private String keyword = null; + + @Override + public JsonObject getHttpBody() { + return new JsonObject(); + } + + public String toQueryString() { + StringBuilder sb = new StringBuilder(); + AgentStudioConstants.appendParam(sb, "limit", limit); + AgentStudioConstants.appendParam(sb, "page", page); + AgentStudioConstants.appendParam(sb, "include_archived", includeArchived); + AgentStudioConstants.appendParam(sb, "keyword", keyword); + return sb.toString(); + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/param/VaultUpdateParam.java b/src/main/java/com/alibaba/dashscope/agentstudio/param/VaultUpdateParam.java new file mode 100644 index 0000000..b880b50 --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/param/VaultUpdateParam.java @@ -0,0 +1,32 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.param; + +import com.alibaba.dashscope.base.FlattenHalfDuplexParamBase; +import com.alibaba.dashscope.utils.JsonUtils; +import com.google.gson.JsonObject; +import java.util.Map; +import lombok.Builder.Default; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@EqualsAndHashCode(callSuper = true) +public class VaultUpdateParam extends FlattenHalfDuplexParamBase { + @Default private String displayName = null; + @Default private Map metadata = null; + + @Override + public JsonObject getHttpBody() { + JsonObject body = new JsonObject(); + if (displayName != null) { + body.addProperty("display_name", displayName); + } + if (metadata != null) { + body.add("metadata", JsonUtils.toJsonElement(metadata)); + } + addExtraBody(body); + return body; + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/resource/AgentStudioEventStream.java b/src/main/java/com/alibaba/dashscope/agentstudio/resource/AgentStudioEventStream.java new file mode 100644 index 0000000..56c83e0 --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/resource/AgentStudioEventStream.java @@ -0,0 +1,249 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.resource; + +import com.alibaba.dashscope.agentstudio.message.ContentBlock; +import com.alibaba.dashscope.agentstudio.message.Message; +import com.alibaba.dashscope.common.Status; +import com.alibaba.dashscope.exception.ApiException; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import java.io.Closeable; +import java.io.IOException; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import lombok.extern.slf4j.Slf4j; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import okhttp3.sse.EventSource; +import okhttp3.sse.EventSourceListener; +import okhttp3.sse.EventSources; + +@Slf4j +public class AgentStudioEventStream implements Iterable, Closeable { + private static final Message POISON = new Message(); + private static final Gson GSON = + new GsonBuilder() + .registerTypeAdapter(ContentBlock.class, new ContentBlock.Deserializer()) + .create(); + + private final BlockingQueue queue = new LinkedBlockingQueue<>(); + private final AtomicBoolean closed = new AtomicBoolean(false); + private final OkHttpClient client; + private final EventSource eventSource; + private final long timeoutMs; + + public AgentStudioEventStream(OkHttpClient client, Request request, long timeoutMs) { + this.client = client; + this.timeoutMs = timeoutMs; + EventSource.Factory factory = EventSources.createFactory(client); + this.eventSource = + factory.newEventSource( + request, + new EventSourceListener() { + @Override + public void onEvent(EventSource es, String id, String type, String data) { + if (closed.get()) return; + if (data == null || data.isEmpty() || "{}".equals(data.trim())) { + return; + } + try { + Message msg = GSON.fromJson(data, Message.class); + if (msg != null && msg.getType() != null) { + queue.put(msg); + } + } catch (Exception e) { + queue.offer(e); + } + } + + @Override + public void onClosed(EventSource es) { + queue.offer(POISON); + } + + @Override + public void onFailure(EventSource es, Throwable t, Response response) { + if (closed.get()) return; + ApiException wrapped = wrapFailure(t, response); + if (wrapped != null) { + queue.offer(wrapped); + } else { + queue.offer(POISON); + } + } + }); + } + + /** Convert OkHttp's onFailure into an ApiException that preserves HTTP status and body. */ + private static ApiException wrapFailure(Throwable t, Response response) { + if (response == null) { + if (t != null) { + return new ApiException(t instanceof Exception ? (Exception) t : new RuntimeException(t)); + } + return null; + } + int code = response.code(); + String body = ""; + try (ResponseBody rb = response.body()) { + if (rb != null) { + body = rb.string(); + } + } catch (IOException e) { + log.debug("Failed to read SSE failure response body", e); + } + Status status = + Status.builder() + .statusCode(code) + .code(code >= 500 ? "server_error" : code == 401 ? "auth_error" : "http_error") + .message(body.isEmpty() ? "HTTP " + code : body) + .build(); + return new ApiException(status, t); + } + + @Override + public Iterator iterator() { + return new Iterator() { + private Message next; + + @Override + public boolean hasNext() { + if (next != null) return true; + if (closed.get()) return false; + try { + Object item = queue.poll(timeoutMs, TimeUnit.MILLISECONDS); + if (item == null) { + // Differentiate timeout from real end-of-stream: POISON is real EOF, null is timeout. + throw new ApiException( + Status.builder() + .statusCode(-1) + .code("stream_timeout") + .message("No event received within " + timeoutMs + "ms") + .build()); + } + if (item == POISON) { + return false; + } + if (item instanceof Throwable) { + throw new ApiException((Throwable) item); + } + next = (Message) item; + return true; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + } + + @Override + public Message next() { + if (!hasNext()) throw new NoSuchElementException(); + Message result = next; + next = null; + return result; + } + }; + } + + public TextStream textStream() { + return new TextStream(this); + } + + @Override + public void close() { + if (closed.compareAndSet(false, true)) { + eventSource.cancel(); + // Each stream owns its own OkHttpClient (constructed in SessionEvents.stream), so it is safe + // to release both the dispatcher and the connection pool here without affecting sibling + // streams. + client.dispatcher().executorService().shutdown(); + client.connectionPool().evictAll(); + } + } + + public boolean isClosed() { + return closed.get(); + } + + public static class TextStream implements Iterable, Closeable { + private final AgentStudioEventStream source; + + TextStream(AgentStudioEventStream source) { + this.source = source; + } + + @Override + public Iterator iterator() { + final Iterator msgIter = source.iterator(); + return new Iterator() { + private String next; + private boolean done; + + @Override + public boolean hasNext() { + if (next != null) return true; + if (done) return false; + while (msgIter.hasNext()) { + Message msg = msgIter.next(); + String type = msg.getType(); + if ("session_status".equals(type)) { + String status = extractSessionStatus(msg); + if ("idle".equals(status) + || "terminated".equals(status) + || "rescheduling".equals(status)) { + done = true; + return false; + } + } + if ("message".equals(type) && "assistant".equals(msg.getRole())) { + if (msg.getContent() != null) { + for (ContentBlock block : msg.getContent()) { + if (block instanceof ContentBlock.Text) { + String text = ((ContentBlock.Text) block).getText(); + if (text != null && !text.isEmpty()) { + next = text; + return true; + } + } + } + } + } + } + done = true; + return false; + } + + @Override + public String next() { + if (!hasNext()) throw new NoSuchElementException(); + String result = next; + next = null; + return result; + } + }; + } + + private static String extractSessionStatus(Message msg) { + if (msg.getContent() != null && !msg.getContent().isEmpty()) { + ContentBlock block = msg.getContent().get(0); + if (block instanceof ContentBlock.DataContent) { + ContentBlock.DataContent dataBlock = (ContentBlock.DataContent) block; + if (dataBlock.getData() != null && dataBlock.getData().has("session_status")) { + return dataBlock.getData().get("session_status").getAsString(); + } + } + } + return null; + } + + @Override + public void close() { + source.close(); + } + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/resource/Agents.java b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Agents.java new file mode 100644 index 0000000..123c241 --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Agents.java @@ -0,0 +1,193 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.resource; + +import com.alibaba.dashscope.agentstudio.AgentStudioConstants; +import com.alibaba.dashscope.agentstudio.model.Agent; +import com.alibaba.dashscope.agentstudio.model.AgentVersion; +import com.alibaba.dashscope.agentstudio.pagination.CursorPage; +import com.alibaba.dashscope.agentstudio.param.AgentCreateParam; +import com.alibaba.dashscope.agentstudio.param.AgentListParam; +import com.alibaba.dashscope.agentstudio.param.AgentUpdateParam; +import com.alibaba.dashscope.api.GeneralApi; +import com.alibaba.dashscope.base.HalfDuplexParamBase; +import com.alibaba.dashscope.common.FlattenResultBase; +import com.alibaba.dashscope.common.GeneralGetParam; +import com.alibaba.dashscope.exception.InputRequiredException; +import com.alibaba.dashscope.protocol.ConnectionOptions; +import com.alibaba.dashscope.protocol.GeneralServiceOption; +import com.alibaba.dashscope.protocol.HttpMethod; +import com.alibaba.dashscope.utils.StringUtils; +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public final class Agents { + private final GeneralApi api; + private final String baseUrl; + private final String apiKey; + + public Agents(String baseUrl, ConnectionOptions connectionOptions, String apiKey) { + this.baseUrl = baseUrl; + this.apiKey = apiKey; + this.api = connectionOptions != null ? new GeneralApi<>(connectionOptions) : new GeneralApi<>(); + } + + public Agent create(AgentCreateParam param) { + return AsyncHelper.joinAndUnwrap(createAsync(param)); + } + + public Agent retrieve(String agentId) { + return retrieve(agentId, null, null, null); + } + + public Agent retrieve(String agentId, Integer version) { + return retrieve(agentId, version, null, null); + } + + public Agent retrieve(String agentId, String apiKey, Map headers) { + return retrieve(agentId, null, apiKey, headers); + } + + public Agent retrieve( + String agentId, Integer version, String apiKey, Map headers) { + return AsyncHelper.joinAndUnwrap(retrieveAsync(agentId, version, apiKey, headers)); + } + + public Agent update(String agentId, AgentUpdateParam param) { + return AsyncHelper.joinAndUnwrap(updateAsync(agentId, param)); + } + + public CursorPage list(AgentListParam param) { + return AsyncHelper.joinAndUnwrap(listAsync(param)); + } + + public Agent archive(String agentId) { + return AsyncHelper.joinAndUnwrap(archiveAsync(agentId)); + } + + public CursorPage listVersions(String agentId, AgentListParam param) { + return AsyncHelper.joinAndUnwrap(listVersionsAsync(agentId, param)); + } + + public CompletableFuture createAsync(AgentCreateParam param) { + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption(HttpMethod.POST, "agents", baseUrl); + return AsyncHelper.callAsync(api, AgentStudioConstants.withApiKey(apiKey, param), opt) + .thenApply(r -> FlattenResultBase.fromDashScopeResult(r, Agent.class)); + } + + public CompletableFuture retrieveAsync(String agentId) { + return retrieveAsync(agentId, null, null, null); + } + + public CompletableFuture retrieveAsync(String agentId, Integer version) { + return retrieveAsync(agentId, version, null, null); + } + + public CompletableFuture retrieveAsync( + String agentId, String apiKey, Map headers) { + return retrieveAsync(agentId, null, apiKey, headers); + } + + public CompletableFuture retrieveAsync( + String agentId, Integer version, String apiKey, Map headers) { + if (agentId == null || agentId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("agentId is required!")); + } + String path = StringUtils.format("agents/%s", agentId); + if (version != null) { + path += "?version=" + version; + } + GeneralServiceOption opt = AgentStudioConstants.newServiceOption(HttpMethod.GET, path, baseUrl); + String resolvedKey = apiKey != null ? apiKey : this.apiKey; + return AsyncHelper.callAsync( + api, + GeneralGetParam.builder() + .apiKey(resolvedKey) + .headers(headers != null ? headers : new HashMap<>()) + .build(), + opt) + .thenApply(r -> FlattenResultBase.fromDashScopeResult(r, Agent.class)); + } + + public CompletableFuture updateAsync(String agentId, AgentUpdateParam param) { + if (agentId == null || agentId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("agentId is required!")); + } + try { + param.validate(); + } catch (InputRequiredException e) { + return AsyncHelper.failedFuture(e); + } + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption( + HttpMethod.POST, StringUtils.format("agents/%s", agentId), baseUrl); + return AsyncHelper.callAsync(api, AgentStudioConstants.withApiKey(apiKey, param), opt) + .thenApply(r -> FlattenResultBase.fromDashScopeResult(r, Agent.class)); + } + + public CompletableFuture> listAsync(AgentListParam param) { + String query = param.toQueryString(); + String path = query.isEmpty() ? "agents" : "agents?" + query; + GeneralServiceOption opt = AgentStudioConstants.newServiceOption(HttpMethod.GET, path, baseUrl); + return AsyncHelper.callAsync( + api, GeneralGetParam.builder().apiKey(apiKey).headers(new HashMap<>()).build(), opt) + .thenApply( + r -> { + Type type = new TypeToken>() {}.getType(); + CursorPage page = FlattenResultBase.fromDashScopeResult(r, type); + page.setFetchNext( + cursor -> + listAsync( + AgentListParam.builder() + .limit(param.getLimit()) + .includeArchived(param.getIncludeArchived()) + .page(cursor) + .build())); + return page; + }); + } + + public CompletableFuture archiveAsync(String agentId) { + if (agentId == null || agentId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("agentId is required!")); + } + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption( + HttpMethod.POST, StringUtils.format("agents/%s/archive", agentId), baseUrl); + return AsyncHelper.callAsync( + api, AgentStudioConstants.withApiKey(apiKey, AgentUpdateParam.builder().build()), opt) + .thenApply(r -> FlattenResultBase.fromDashScopeResult(r, Agent.class)); + } + + public CompletableFuture> listVersionsAsync( + String agentId, AgentListParam param) { + if (agentId == null || agentId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("agentId is required!")); + } + String query = param != null ? param.toQueryString() : ""; + String path = StringUtils.format("agents/%s/versions", agentId); + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption( + HttpMethod.GET, query.isEmpty() ? path : path + "?" + query, baseUrl); + return AsyncHelper.callAsync( + api, GeneralGetParam.builder().apiKey(apiKey).headers(new HashMap<>()).build(), opt) + .thenApply( + r -> { + Type type = new TypeToken>() {}.getType(); + CursorPage page = FlattenResultBase.fromDashScopeResult(r, type); + page.setFetchNext( + cursor -> + listVersionsAsync( + agentId, + AgentListParam.builder() + .limit(param != null ? param.getLimit() : null) + .includeArchived(param != null ? param.getIncludeArchived() : null) + .page(cursor) + .build())); + return page; + }); + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/resource/AsyncHelper.java b/src/main/java/com/alibaba/dashscope/agentstudio/resource/AsyncHelper.java new file mode 100644 index 0000000..a8c836c --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/resource/AsyncHelper.java @@ -0,0 +1,64 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.resource; + +import com.alibaba.dashscope.api.GeneralApi; +import com.alibaba.dashscope.base.HalfDuplexParamBase; +import com.alibaba.dashscope.common.DashScopeResult; +import com.alibaba.dashscope.common.ResultCallback; +import com.alibaba.dashscope.exception.ApiException; +import com.alibaba.dashscope.protocol.ServiceOption; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +final class AsyncHelper { + + private AsyncHelper() {} + + static CompletableFuture callAsync( + GeneralApi api, HalfDuplexParamBase param, ServiceOption opt) { + CompletableFuture future = new CompletableFuture<>(); + try { + api.call( + param, + opt, + new ResultCallback() { + @Override + public void onEvent(DashScopeResult result) { + future.complete(result); + } + + @Override + public void onComplete() {} + + @Override + public void onError(Exception e) { + future.completeExceptionally(e); + } + }); + } catch (Exception e) { + future.completeExceptionally(e); + } + return future; + } + + static CompletableFuture failedFuture(Throwable ex) { + CompletableFuture f = new CompletableFuture<>(); + f.completeExceptionally(ex); + return f; + } + + static T joinAndUnwrap(CompletableFuture future) { + try { + return future.join(); + } catch (CompletionException e) { + Throwable cause = e.getCause(); + if (cause instanceof ApiException) { + throw (ApiException) cause; + } + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } + throw new ApiException(cause); + } + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/resource/Environments.java b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Environments.java new file mode 100644 index 0000000..175f2b5 --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Environments.java @@ -0,0 +1,170 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.resource; + +import com.alibaba.dashscope.agentstudio.AgentStudioConstants; +import com.alibaba.dashscope.agentstudio.model.AgentStudioDeletionStatus; +import com.alibaba.dashscope.agentstudio.model.Environment; +import com.alibaba.dashscope.agentstudio.pagination.CursorPage; +import com.alibaba.dashscope.agentstudio.param.EnvironmentCreateParam; +import com.alibaba.dashscope.agentstudio.param.EnvironmentListParam; +import com.alibaba.dashscope.agentstudio.param.EnvironmentUpdateParam; +import com.alibaba.dashscope.api.GeneralApi; +import com.alibaba.dashscope.base.HalfDuplexParamBase; +import com.alibaba.dashscope.common.FlattenResultBase; +import com.alibaba.dashscope.common.GeneralGetParam; +import com.alibaba.dashscope.exception.InputRequiredException; +import com.alibaba.dashscope.protocol.ConnectionOptions; +import com.alibaba.dashscope.protocol.GeneralServiceOption; +import com.alibaba.dashscope.protocol.HttpMethod; +import com.alibaba.dashscope.utils.StringUtils; +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public final class Environments { + private final GeneralApi api; + private final String baseUrl; + private final String apiKey; + + public Environments(String baseUrl, ConnectionOptions connectionOptions, String apiKey) { + this.baseUrl = baseUrl; + this.apiKey = apiKey; + this.api = connectionOptions != null ? new GeneralApi<>(connectionOptions) : new GeneralApi<>(); + } + + public Environment create(EnvironmentCreateParam param) { + return AsyncHelper.joinAndUnwrap(createAsync(param)); + } + + public Environment retrieve(String environmentId) { + return retrieve(environmentId, null, null); + } + + public Environment retrieve(String environmentId, String apiKey, Map headers) { + return AsyncHelper.joinAndUnwrap(retrieveAsync(environmentId, apiKey, headers)); + } + + public Environment update(String environmentId, EnvironmentUpdateParam param) { + return AsyncHelper.joinAndUnwrap(updateAsync(environmentId, param)); + } + + public CursorPage list(EnvironmentListParam param) { + return AsyncHelper.joinAndUnwrap(listAsync(param)); + } + + public Environment archive(String environmentId) { + return AsyncHelper.joinAndUnwrap(archiveAsync(environmentId)); + } + + public AgentStudioDeletionStatus delete(String environmentId) { + return delete(environmentId, null, null); + } + + public AgentStudioDeletionStatus delete( + String environmentId, String apiKey, Map headers) { + return AsyncHelper.joinAndUnwrap(deleteAsync(environmentId, apiKey, headers)); + } + + public CompletableFuture createAsync(EnvironmentCreateParam param) { + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption(HttpMethod.POST, "environments", baseUrl); + return AsyncHelper.callAsync(api, AgentStudioConstants.withApiKey(apiKey, param), opt) + .thenApply(r -> FlattenResultBase.fromDashScopeResult(r, Environment.class)); + } + + public CompletableFuture retrieveAsync(String environmentId) { + return retrieveAsync(environmentId, null, null); + } + + public CompletableFuture retrieveAsync( + String environmentId, String apiKey, Map headers) { + if (environmentId == null || environmentId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("environmentId is required!")); + } + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption( + HttpMethod.GET, StringUtils.format("environments/%s", environmentId), baseUrl); + String resolvedKey = apiKey != null ? apiKey : this.apiKey; + return AsyncHelper.callAsync( + api, + GeneralGetParam.builder() + .apiKey(resolvedKey) + .headers(headers != null ? headers : new HashMap<>()) + .build(), + opt) + .thenApply(r -> FlattenResultBase.fromDashScopeResult(r, Environment.class)); + } + + public CompletableFuture updateAsync( + String environmentId, EnvironmentUpdateParam param) { + if (environmentId == null || environmentId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("environmentId is required!")); + } + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption( + HttpMethod.POST, StringUtils.format("environments/%s", environmentId), baseUrl); + return AsyncHelper.callAsync(api, AgentStudioConstants.withApiKey(apiKey, param), opt) + .thenApply(r -> FlattenResultBase.fromDashScopeResult(r, Environment.class)); + } + + public CompletableFuture> listAsync(EnvironmentListParam param) { + String query = param.toQueryString(); + String path = query.isEmpty() ? "environments" : "environments?" + query; + GeneralServiceOption opt = AgentStudioConstants.newServiceOption(HttpMethod.GET, path, baseUrl); + return AsyncHelper.callAsync( + api, GeneralGetParam.builder().apiKey(apiKey).headers(new HashMap<>()).build(), opt) + .thenApply( + r -> { + Type type = new TypeToken>() {}.getType(); + CursorPage page = FlattenResultBase.fromDashScopeResult(r, type); + page.setFetchNext( + cursor -> + listAsync( + EnvironmentListParam.builder() + .limit(param.getLimit()) + .includeArchived(param.getIncludeArchived()) + .page(cursor) + .build())); + return page; + }); + } + + public CompletableFuture archiveAsync(String environmentId) { + if (environmentId == null || environmentId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("environmentId is required!")); + } + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption( + HttpMethod.POST, StringUtils.format("environments/%s/archive", environmentId), baseUrl); + return AsyncHelper.callAsync( + api, + AgentStudioConstants.withApiKey(apiKey, EnvironmentUpdateParam.builder().build()), + opt) + .thenApply(r -> FlattenResultBase.fromDashScopeResult(r, Environment.class)); + } + + public CompletableFuture deleteAsync(String environmentId) { + return deleteAsync(environmentId, null, null); + } + + public CompletableFuture deleteAsync( + String environmentId, String apiKey, Map headers) { + if (environmentId == null || environmentId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("environmentId is required!")); + } + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption( + HttpMethod.DELETE, StringUtils.format("environments/%s", environmentId), baseUrl); + String resolvedKey = apiKey != null ? apiKey : this.apiKey; + return AsyncHelper.callAsync( + api, + GeneralGetParam.builder() + .apiKey(resolvedKey) + .headers(headers != null ? headers : new HashMap<>()) + .build(), + opt) + .thenApply(r -> FlattenResultBase.fromDashScopeResult(r, AgentStudioDeletionStatus.class)); + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/resource/Files.java b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Files.java new file mode 100644 index 0000000..79d7c9e --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Files.java @@ -0,0 +1,284 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.resource; + +import com.alibaba.dashscope.agentstudio.AgentStudioConstants; +import com.alibaba.dashscope.agentstudio.model.AgentStudioDeletionStatus; +import com.alibaba.dashscope.agentstudio.model.AgentStudioFile; +import com.alibaba.dashscope.agentstudio.pagination.CursorPage; +import com.alibaba.dashscope.agentstudio.param.FileListParam; +import com.alibaba.dashscope.api.GeneralApi; +import com.alibaba.dashscope.base.HalfDuplexParamBase; +import com.alibaba.dashscope.common.FlattenResultBase; +import com.alibaba.dashscope.common.GeneralGetParam; +import com.alibaba.dashscope.common.Status; +import com.alibaba.dashscope.exception.ApiException; +import com.alibaba.dashscope.exception.InputRequiredException; +import com.alibaba.dashscope.protocol.ConnectionOptions; +import com.alibaba.dashscope.protocol.GeneralServiceOption; +import com.alibaba.dashscope.protocol.HttpMethod; +import com.alibaba.dashscope.utils.ApiKey; +import com.alibaba.dashscope.utils.JsonUtils; +import com.alibaba.dashscope.utils.StringUtils; +import com.google.gson.reflect.TypeToken; +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +public final class Files implements Closeable { + private final GeneralApi api; + private final String baseUrl; + private final String apiKey; + private final OkHttpClient uploadClient; + + public Files(String baseUrl, ConnectionOptions connectionOptions, String apiKey) { + this.baseUrl = baseUrl; + this.apiKey = apiKey; + this.api = connectionOptions != null ? new GeneralApi<>(connectionOptions) : new GeneralApi<>(); + this.uploadClient = + new OkHttpClient.Builder() + .connectTimeout(AgentStudioConstants.DEFAULT_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS) + .readTimeout(AgentStudioConstants.DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS) + .writeTimeout(AgentStudioConstants.DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS) + .build(); + } + + public AgentStudioFile upload(String filePath, String mimeType) { + return AsyncHelper.joinAndUnwrap(uploadAsync(filePath, mimeType)); + } + + public AgentStudioFile upload(String filename, InputStream inputStream, String mimeType) { + return AsyncHelper.joinAndUnwrap(uploadAsync(filename, inputStream, mimeType)); + } + + public AgentStudioFile retrieve(String fileId) { + return retrieve(fileId, null, null); + } + + public AgentStudioFile retrieve(String fileId, String apiKey, Map headers) { + return AsyncHelper.joinAndUnwrap(retrieveAsync(fileId, apiKey, headers)); + } + + public CursorPage list(FileListParam param) { + return AsyncHelper.joinAndUnwrap(listAsync(param)); + } + + public AgentStudioDeletionStatus delete(String fileId) { + return delete(fileId, null, null); + } + + public AgentStudioDeletionStatus delete( + String fileId, String apiKey, Map headers) { + return AsyncHelper.joinAndUnwrap(deleteAsync(fileId, apiKey, headers)); + } + + public CompletableFuture uploadAsync(String filePath, String mimeType) { + if (filePath == null || filePath.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("filePath is required!")); + } + File file = new File(filePath); + if (!file.exists() || !file.isFile()) { + return AsyncHelper.failedFuture(new InputRequiredException("file not found: " + filePath)); + } + String mt = mimeType != null ? mimeType : guessContentType(filePath); + RequestBody body = RequestBody.create(MediaType.parse(mt), file); + return uploadRequestAsync(file.getName(), body); + } + + public CompletableFuture uploadAsync( + String filename, InputStream inputStream, String mimeType) { + if (filename == null || filename.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("filename is required!")); + } + if (inputStream == null) { + return AsyncHelper.failedFuture(new InputRequiredException("inputStream is required!")); + } + String mt = mimeType != null ? mimeType : "application/octet-stream"; + RequestBody body = streamingBody(MediaType.parse(mt), inputStream); + return uploadRequestAsync(filename, body); + } + + public CompletableFuture retrieveAsync(String fileId) { + return retrieveAsync(fileId, null, null); + } + + public CompletableFuture retrieveAsync( + String fileId, String apiKey, Map headers) { + if (fileId == null || fileId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("fileId is required!")); + } + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption( + HttpMethod.GET, StringUtils.format("files/%s", fileId), baseUrl); + String resolvedKey = apiKey != null ? apiKey : this.apiKey; + return AsyncHelper.callAsync( + api, + GeneralGetParam.builder() + .apiKey(resolvedKey) + .headers(headers != null ? headers : new HashMap<>()) + .build(), + opt) + .thenApply(r -> FlattenResultBase.fromDashScopeResult(r, AgentStudioFile.class)); + } + + public CompletableFuture> listAsync(FileListParam param) { + String query = param.toQueryString(); + String path = query.isEmpty() ? "files" : "files?" + query; + GeneralServiceOption opt = AgentStudioConstants.newServiceOption(HttpMethod.GET, path, baseUrl); + return AsyncHelper.callAsync( + api, GeneralGetParam.builder().apiKey(apiKey).headers(new HashMap<>()).build(), opt) + .thenApply( + r -> { + Type type = new TypeToken>() {}.getType(); + CursorPage page = FlattenResultBase.fromDashScopeResult(r, type); + page.setFetchNext( + cursor -> + listAsync( + FileListParam.builder() + .limit(param.getLimit()) + .scopeId(param.getScopeId()) + .afterId(param.getAfterId()) + .beforeId(param.getBeforeId()) + .page(cursor) + .build())); + return page; + }); + } + + public CompletableFuture deleteAsync(String fileId) { + return deleteAsync(fileId, null, null); + } + + public CompletableFuture deleteAsync( + String fileId, String apiKey, Map headers) { + if (fileId == null || fileId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("fileId is required!")); + } + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption( + HttpMethod.DELETE, StringUtils.format("files/%s", fileId), baseUrl); + String resolvedKey = apiKey != null ? apiKey : this.apiKey; + return AsyncHelper.callAsync( + api, + GeneralGetParam.builder() + .apiKey(resolvedKey) + .headers(headers != null ? headers : new HashMap<>()) + .build(), + opt) + .thenApply(r -> FlattenResultBase.fromDashScopeResult(r, AgentStudioDeletionStatus.class)); + } + + private CompletableFuture uploadRequestAsync( + String filename, RequestBody fileBody) { + String key; + try { + key = ApiKey.getApiKey(this.apiKey); + } catch (Exception e) { + return AsyncHelper.failedFuture(e); + } + MultipartBody multipart = + new MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart("file", filename, fileBody) + .build(); + String resolvedBase = resolveUploadBaseUrl(); + String url = resolvedBase + "/files"; + Request request = + new Request.Builder() + .url(url) + .header("Authorization", "Bearer " + key) + .post(multipart) + .build(); + + CompletableFuture future = new CompletableFuture<>(); + uploadClient + .newCall(request) + .enqueue( + new Callback() { + @Override + public void onFailure(Call call, IOException e) { + future.completeExceptionally(new ApiException(e)); + } + + @Override + public void onResponse(Call call, Response response) { + try (Response r = response) { + String body = r.body() != null ? r.body().string() : ""; + if (!r.isSuccessful()) { + future.completeExceptionally( + new ApiException( + Status.builder().statusCode(r.code()).message(body).build())); + return; + } + future.complete( + JsonUtils.fromJson(body.isEmpty() ? "{}" : body, AgentStudioFile.class)); + } catch (Exception e) { + future.completeExceptionally(new ApiException(e)); + } + } + }); + return future; + } + + @Override + public void close() { + uploadClient.dispatcher().executorService().shutdown(); + uploadClient.connectionPool().evictAll(); + } + + private String resolveUploadBaseUrl() { + if (baseUrl != null && !baseUrl.isEmpty()) { + return baseUrl; + } + String envUrl = System.getenv(AgentStudioConstants.ENV_BASE_URL); + if (envUrl == null || envUrl.isEmpty()) { + envUrl = System.getenv(AgentStudioConstants.ENV_BASE_URL_ALT); + } + if (envUrl != null && !envUrl.isEmpty()) { + return envUrl; + } + return AgentStudioConstants.resolveBaseUrl(null, null); + } + + private static String guessContentType(String filePath) { + String lower = filePath.toLowerCase(); + if (lower.endsWith(".zip")) return "application/zip"; + if (lower.endsWith(".pdf")) return "application/pdf"; + if (lower.endsWith(".png")) return "image/png"; + if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg"; + if (lower.endsWith(".gif")) return "image/gif"; + if (lower.endsWith(".txt")) return "text/plain"; + if (lower.endsWith(".json")) return "application/json"; + return "application/octet-stream"; + } + + private static RequestBody streamingBody(MediaType mediaType, InputStream inputStream) { + return new RequestBody() { + @Override + public MediaType contentType() { + return mediaType; + } + + @Override + public void writeTo(okio.BufferedSink sink) throws java.io.IOException { + try (InputStream is = inputStream; + okio.Source source = okio.Okio.source(is)) { + sink.writeAll(source); + } + } + }; + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/resource/SessionEvents.java b/src/main/java/com/alibaba/dashscope/agentstudio/resource/SessionEvents.java new file mode 100644 index 0000000..ba4d3a6 --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/resource/SessionEvents.java @@ -0,0 +1,176 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.resource; + +import com.alibaba.dashscope.agentstudio.AgentStudioConstants; +import com.alibaba.dashscope.agentstudio.message.Message; +import com.alibaba.dashscope.agentstudio.pagination.CursorPage; +import com.alibaba.dashscope.agentstudio.param.SessionEventListParam; +import com.alibaba.dashscope.agentstudio.param.SessionEventSendParam; +import com.alibaba.dashscope.api.GeneralApi; +import com.alibaba.dashscope.base.HalfDuplexParamBase; +import com.alibaba.dashscope.common.FlattenResultBase; +import com.alibaba.dashscope.common.GeneralGetParam; +import com.alibaba.dashscope.exception.InputRequiredException; +import com.alibaba.dashscope.exception.NoApiKeyException; +import com.alibaba.dashscope.protocol.ConnectionOptions; +import com.alibaba.dashscope.protocol.GeneralServiceOption; +import com.alibaba.dashscope.protocol.HttpMethod; +import com.alibaba.dashscope.utils.ApiKey; +import com.alibaba.dashscope.utils.Constants; +import com.alibaba.dashscope.utils.StringUtils; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.reflect.TypeToken; +import java.io.Closeable; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import okhttp3.ConnectionPool; +import okhttp3.Dispatcher; +import okhttp3.OkHttpClient; +import okhttp3.Request; + +public final class SessionEvents implements Closeable { + private final GeneralApi api; + private final String baseUrl; + private final String apiKey; + private final OkHttpClient streamClientTemplate; + + public SessionEvents(String baseUrl, ConnectionOptions connectionOptions, String apiKey) { + this.baseUrl = baseUrl; + this.apiKey = apiKey; + this.api = connectionOptions != null ? new GeneralApi<>(connectionOptions) : new GeneralApi<>(); + this.streamClientTemplate = + new OkHttpClient.Builder() + .connectTimeout(AgentStudioConstants.DEFAULT_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS) + .readTimeout(AgentStudioConstants.DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS) + .writeTimeout(AgentStudioConstants.DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS) + .build(); + } + + private String resolveStreamBaseUrl() { + if (baseUrl != null && !baseUrl.isEmpty()) { + return baseUrl; + } + String envUrl = System.getenv(AgentStudioConstants.ENV_BASE_URL); + if (envUrl == null || envUrl.isEmpty()) { + envUrl = System.getenv(AgentStudioConstants.ENV_BASE_URL_ALT); + } + if (envUrl != null && !envUrl.isEmpty()) { + return envUrl; + } + return Constants.baseHttpApiUrl; + } + + public JsonObject send(String sessionId, List events) { + return AsyncHelper.joinAndUnwrap(sendAsync(sessionId, events)); + } + + public CursorPage list(String sessionId, SessionEventListParam param) { + return AsyncHelper.joinAndUnwrap(listAsync(sessionId, param)); + } + + public CompletableFuture sendAsync(String sessionId, List events) { + if (sessionId == null || sessionId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("sessionId is required!")); + } + if (events == null || events.isEmpty()) { + return AsyncHelper.failedFuture( + new IllegalArgumentException("events must contain at least 1 entry")); + } + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption( + HttpMethod.POST, StringUtils.format("sessions/%s/events", sessionId), baseUrl); + SessionEventSendParam param = SessionEventSendParam.builder().input(events).build(); + return AsyncHelper.callAsync(api, AgentStudioConstants.withApiKey(apiKey, param), opt) + .thenApply( + result -> { + Object output = result.getOutput(); + if (output instanceof JsonElement && ((JsonElement) output).isJsonObject()) { + return ((JsonElement) output).getAsJsonObject(); + } + return new JsonObject(); + }); + } + + public CompletableFuture> listAsync( + String sessionId, SessionEventListParam param) { + if (sessionId == null || sessionId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("sessionId is required!")); + } + String query = param.toQueryString(); + String path = StringUtils.format("sessions/%s/events", sessionId); + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption( + HttpMethod.GET, query.isEmpty() ? path : path + "?" + query, baseUrl); + return AsyncHelper.callAsync( + api, GeneralGetParam.builder().apiKey(apiKey).headers(new HashMap<>()).build(), opt) + .thenApply( + r -> { + Type type = new TypeToken>() {}.getType(); + CursorPage page = FlattenResultBase.fromDashScopeResult(r, type); + page.setFetchNext( + cursor -> + listAsync( + sessionId, + SessionEventListParam.builder() + .types(param.getTypes()) + .createdAtGt(param.getCreatedAtGt()) + .createdAtGte(param.getCreatedAtGte()) + .createdAtLt(param.getCreatedAtLt()) + .createdAtLte(param.getCreatedAtLte()) + .limit(param.getLimit()) + .order(param.getOrder()) + .page(cursor) + .build())); + return page; + }); + } + + public AgentStudioEventStream stream(String sessionId) { + return stream(sessionId, AgentStudioConstants.DEFAULT_TIMEOUT_MS); + } + + public AgentStudioEventStream stream(String sessionId, long timeoutMs) { + String url = resolveStreamBaseUrl(); + if (!url.endsWith("/")) { + url += "/"; + } + url += StringUtils.format("sessions/%s/events/stream", sessionId); + + String resolvedKey = resolveApiKey(); + OkHttpClient client = + streamClientTemplate + .newBuilder() + .dispatcher(new Dispatcher()) + .connectionPool(new ConnectionPool()) + .readTimeout(timeoutMs, TimeUnit.MILLISECONDS) + .build(); + + Request request = + new Request.Builder() + .url(url) + .header("Authorization", "Bearer " + resolvedKey) + .header("Accept", "text/event-stream") + .get() + .build(); + + return new AgentStudioEventStream(client, request, timeoutMs); + } + + @Override + public void close() { + streamClientTemplate.dispatcher().executorService().shutdown(); + streamClientTemplate.connectionPool().evictAll(); + } + + private String resolveApiKey() { + try { + return ApiKey.getApiKey(this.apiKey); + } catch (NoApiKeyException e) { + throw new IllegalStateException("No API key found. Set DASHSCOPE_API_KEY or pass apiKey.", e); + } + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/resource/Sessions.java b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Sessions.java new file mode 100644 index 0000000..5b504c7 --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Sessions.java @@ -0,0 +1,178 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.resource; + +import com.alibaba.dashscope.agentstudio.AgentStudioConstants; +import com.alibaba.dashscope.agentstudio.model.AgentStudioDeletionStatus; +import com.alibaba.dashscope.agentstudio.model.Session; +import com.alibaba.dashscope.agentstudio.pagination.CursorPage; +import com.alibaba.dashscope.agentstudio.param.SessionCreateParam; +import com.alibaba.dashscope.agentstudio.param.SessionListParam; +import com.alibaba.dashscope.agentstudio.param.SessionUpdateParam; +import com.alibaba.dashscope.api.GeneralApi; +import com.alibaba.dashscope.base.HalfDuplexParamBase; +import com.alibaba.dashscope.common.FlattenResultBase; +import com.alibaba.dashscope.common.GeneralGetParam; +import com.alibaba.dashscope.exception.InputRequiredException; +import com.alibaba.dashscope.protocol.ConnectionOptions; +import com.alibaba.dashscope.protocol.GeneralServiceOption; +import com.alibaba.dashscope.protocol.HttpMethod; +import com.alibaba.dashscope.utils.StringUtils; +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public final class Sessions { + private final GeneralApi api; + private final String baseUrl; + private final String apiKey; + private final SessionEvents events; + + public Sessions(String baseUrl, ConnectionOptions connectionOptions, String apiKey) { + this.baseUrl = baseUrl; + this.apiKey = apiKey; + this.api = connectionOptions != null ? new GeneralApi<>(connectionOptions) : new GeneralApi<>(); + this.events = new SessionEvents(baseUrl, connectionOptions, apiKey); + } + + public SessionEvents events() { + return events; + } + + public Session create(SessionCreateParam param) { + return AsyncHelper.joinAndUnwrap(createAsync(param)); + } + + public Session retrieve(String sessionId) { + return retrieve(sessionId, null, null); + } + + public Session retrieve(String sessionId, String apiKey, Map headers) { + return AsyncHelper.joinAndUnwrap(retrieveAsync(sessionId, apiKey, headers)); + } + + public Session update(String sessionId, SessionUpdateParam param) { + return AsyncHelper.joinAndUnwrap(updateAsync(sessionId, param)); + } + + public CursorPage list(SessionListParam param) { + return AsyncHelper.joinAndUnwrap(listAsync(param)); + } + + public Session archive(String sessionId) { + return AsyncHelper.joinAndUnwrap(archiveAsync(sessionId)); + } + + public AgentStudioDeletionStatus delete(String sessionId) { + return delete(sessionId, null, null); + } + + public AgentStudioDeletionStatus delete( + String sessionId, String apiKey, Map headers) { + return AsyncHelper.joinAndUnwrap(deleteAsync(sessionId, apiKey, headers)); + } + + public CompletableFuture createAsync(SessionCreateParam param) { + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption(HttpMethod.POST, "sessions", baseUrl); + return AsyncHelper.callAsync(api, AgentStudioConstants.withApiKey(apiKey, param), opt) + .thenApply(r -> FlattenResultBase.fromDashScopeResult(r, Session.class)); + } + + public CompletableFuture retrieveAsync(String sessionId) { + return retrieveAsync(sessionId, null, null); + } + + public CompletableFuture retrieveAsync( + String sessionId, String apiKey, Map headers) { + if (sessionId == null || sessionId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("sessionId is required!")); + } + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption( + HttpMethod.GET, StringUtils.format("sessions/%s", sessionId), baseUrl); + String resolvedKey = apiKey != null ? apiKey : this.apiKey; + return AsyncHelper.callAsync( + api, + GeneralGetParam.builder() + .apiKey(resolvedKey) + .headers(headers != null ? headers : new HashMap<>()) + .build(), + opt) + .thenApply(r -> FlattenResultBase.fromDashScopeResult(r, Session.class)); + } + + public CompletableFuture updateAsync(String sessionId, SessionUpdateParam param) { + if (sessionId == null || sessionId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("sessionId is required!")); + } + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption( + HttpMethod.POST, StringUtils.format("sessions/%s", sessionId), baseUrl); + return AsyncHelper.callAsync(api, AgentStudioConstants.withApiKey(apiKey, param), opt) + .thenApply(r -> FlattenResultBase.fromDashScopeResult(r, Session.class)); + } + + public CompletableFuture> listAsync(SessionListParam param) { + String query = param.toQueryString(); + String path = query.isEmpty() ? "sessions" : "sessions?" + query; + GeneralServiceOption opt = AgentStudioConstants.newServiceOption(HttpMethod.GET, path, baseUrl); + return AsyncHelper.callAsync( + api, GeneralGetParam.builder().apiKey(apiKey).headers(new HashMap<>()).build(), opt) + .thenApply( + r -> { + Type type = new TypeToken>() {}.getType(); + CursorPage page = FlattenResultBase.fromDashScopeResult(r, type); + page.setFetchNext( + cursor -> + listAsync( + SessionListParam.builder() + .limit(param.getLimit()) + .agentId(param.getAgentId()) + .statuses(param.getStatuses()) + .createdAtGt(param.getCreatedAtGt()) + .createdAtGte(param.getCreatedAtGte()) + .createdAtLt(param.getCreatedAtLt()) + .createdAtLte(param.getCreatedAtLte()) + .page(cursor) + .build())); + return page; + }); + } + + public CompletableFuture archiveAsync(String sessionId) { + if (sessionId == null || sessionId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("sessionId is required!")); + } + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption( + HttpMethod.POST, StringUtils.format("sessions/%s/archive", sessionId), baseUrl); + return AsyncHelper.callAsync( + api, AgentStudioConstants.withApiKey(apiKey, SessionUpdateParam.builder().build()), opt) + .thenApply(r -> FlattenResultBase.fromDashScopeResult(r, Session.class)); + } + + public CompletableFuture deleteAsync(String sessionId) { + return deleteAsync(sessionId, null, null); + } + + public CompletableFuture deleteAsync( + String sessionId, String apiKey, Map headers) { + if (sessionId == null || sessionId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("sessionId is required!")); + } + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption( + HttpMethod.DELETE, StringUtils.format("sessions/%s", sessionId), baseUrl); + String resolvedKey = apiKey != null ? apiKey : this.apiKey; + return AsyncHelper.callAsync( + api, + GeneralGetParam.builder() + .apiKey(resolvedKey) + .headers(headers != null ? headers : new HashMap<>()) + .build(), + opt) + .thenApply(r -> FlattenResultBase.fromDashScopeResult(r, AgentStudioDeletionStatus.class)); + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/resource/Skills.java b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Skills.java new file mode 100644 index 0000000..d5e21c2 --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Skills.java @@ -0,0 +1,268 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.resource; + +import com.alibaba.dashscope.agentstudio.AgentStudioConstants; +import com.alibaba.dashscope.agentstudio.model.AgentStudioDeletionStatus; +import com.alibaba.dashscope.agentstudio.model.Skill; +import com.alibaba.dashscope.agentstudio.model.SkillVersion; +import com.alibaba.dashscope.agentstudio.pagination.CursorPage; +import com.alibaba.dashscope.agentstudio.param.SkillCreateParam; +import com.alibaba.dashscope.agentstudio.param.SkillListParam; +import com.alibaba.dashscope.api.GeneralApi; +import com.alibaba.dashscope.base.HalfDuplexParamBase; +import com.alibaba.dashscope.common.FlattenResultBase; +import com.alibaba.dashscope.common.GeneralGetParam; +import com.alibaba.dashscope.exception.InputRequiredException; +import com.alibaba.dashscope.protocol.ConnectionOptions; +import com.alibaba.dashscope.protocol.GeneralServiceOption; +import com.alibaba.dashscope.protocol.HttpMethod; +import com.alibaba.dashscope.utils.StringUtils; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.reflect.TypeToken; +import java.io.Closeable; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public final class Skills implements Closeable { + private final GeneralApi api; + private final String baseUrl; + private final String apiKey; + private final Files files; + + public Skills(String baseUrl, ConnectionOptions connectionOptions, String apiKey, Files files) { + this.baseUrl = baseUrl; + this.apiKey = apiKey; + this.api = connectionOptions != null ? new GeneralApi<>(connectionOptions) : new GeneralApi<>(); + this.files = files; + } + + private CompletableFuture resolveFileId(SkillCreateParam param) { + if (param.getFileId() != null) { + return CompletableFuture.completedFuture(param.getFileId()); + } + if (param.getFile() != null) { + if (files == null) { + return AsyncHelper.failedFuture( + new InputRequiredException( + "File upload requires a configured Skills instance (use AgentStudioClient)")); + } + return files.uploadAsync(param.getFile(), param.getMimeType()).thenApply(f -> f.getId()); + } + return AsyncHelper.failedFuture( + new InputRequiredException("Either fileId or file must be provided")); + } + + public Skill create(SkillCreateParam param) { + return AsyncHelper.joinAndUnwrap(createAsync(param)); + } + + public Skill retrieve(String skillId) { + return retrieve(skillId, null, null); + } + + public Skill retrieve(String skillId, String apiKey, Map headers) { + return AsyncHelper.joinAndUnwrap(retrieveAsync(skillId, apiKey, headers)); + } + + public CursorPage list(SkillListParam param) { + return AsyncHelper.joinAndUnwrap(listAsync(param)); + } + + public AgentStudioDeletionStatus delete(String skillId) { + return delete(skillId, null, null); + } + + public AgentStudioDeletionStatus delete( + String skillId, String apiKey, Map headers) { + return AsyncHelper.joinAndUnwrap(deleteAsync(skillId, apiKey, headers)); + } + + public SkillVersion createVersion(String skillId, SkillCreateParam param) { + return AsyncHelper.joinAndUnwrap(createVersionAsync(skillId, param)); + } + + public CursorPage listVersions(String skillId, SkillListParam param) { + return AsyncHelper.joinAndUnwrap(listVersionsAsync(skillId, param)); + } + + public SkillVersion retrieveVersion(String skillId, String version) { + return AsyncHelper.joinAndUnwrap(retrieveVersionAsync(skillId, version)); + } + + public JsonObject downloadVersion(String skillId, String version) { + return AsyncHelper.joinAndUnwrap(downloadVersionAsync(skillId, version)); + } + + public CompletableFuture createAsync(SkillCreateParam param) { + return resolveFileId(param) + .thenCompose( + fileId -> { + param.setFileId(fileId); + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption(HttpMethod.POST, "skills", baseUrl); + return AsyncHelper.callAsync( + api, AgentStudioConstants.withApiKey(apiKey, param), opt); + }) + .thenApply(r -> FlattenResultBase.fromDashScopeResult(r, Skill.class)); + } + + public CompletableFuture retrieveAsync(String skillId) { + return retrieveAsync(skillId, null, null); + } + + public CompletableFuture retrieveAsync( + String skillId, String apiKey, Map headers) { + if (skillId == null || skillId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("skillId is required!")); + } + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption( + HttpMethod.GET, StringUtils.format("skills/%s", skillId), baseUrl); + String resolvedKey = apiKey != null ? apiKey : this.apiKey; + return AsyncHelper.callAsync( + api, + GeneralGetParam.builder() + .apiKey(resolvedKey) + .headers(headers != null ? headers : new HashMap<>()) + .build(), + opt) + .thenApply(r -> FlattenResultBase.fromDashScopeResult(r, Skill.class)); + } + + public CompletableFuture> listAsync(SkillListParam param) { + String query = param.toQueryString(); + String path = query.isEmpty() ? "skills" : "skills?" + query; + GeneralServiceOption opt = AgentStudioConstants.newServiceOption(HttpMethod.GET, path, baseUrl); + return AsyncHelper.callAsync( + api, GeneralGetParam.builder().apiKey(apiKey).headers(new HashMap<>()).build(), opt) + .thenApply( + r -> { + Type type = new TypeToken>() {}.getType(); + CursorPage page = FlattenResultBase.fromDashScopeResult(r, type); + page.setFetchNext( + cursor -> + listAsync( + SkillListParam.builder() + .source(param.getSource()) + .limit(param.getLimit()) + .page(cursor) + .build())); + return page; + }); + } + + public CompletableFuture deleteAsync(String skillId) { + return deleteAsync(skillId, null, null); + } + + public CompletableFuture deleteAsync( + String skillId, String apiKey, Map headers) { + if (skillId == null || skillId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("skillId is required!")); + } + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption( + HttpMethod.DELETE, StringUtils.format("skills/%s", skillId), baseUrl); + String resolvedKey = apiKey != null ? apiKey : this.apiKey; + return AsyncHelper.callAsync( + api, + GeneralGetParam.builder() + .apiKey(resolvedKey) + .headers(headers != null ? headers : new HashMap<>()) + .build(), + opt) + .thenApply(r -> FlattenResultBase.fromDashScopeResult(r, AgentStudioDeletionStatus.class)); + } + + public CompletableFuture createVersionAsync( + String skillId, SkillCreateParam param) { + if (skillId == null || skillId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("skillId is required!")); + } + return resolveFileId(param) + .thenCompose( + fileId -> { + param.setFileId(fileId); + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption( + HttpMethod.POST, StringUtils.format("skills/%s/versions", skillId), baseUrl); + return AsyncHelper.callAsync( + api, AgentStudioConstants.withApiKey(apiKey, param), opt); + }) + .thenApply(r -> FlattenResultBase.fromDashScopeResult(r, SkillVersion.class)); + } + + public CompletableFuture> listVersionsAsync( + String skillId, SkillListParam param) { + if (skillId == null || skillId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("skillId is required!")); + } + String query = param != null ? param.toQueryString() : ""; + String path = StringUtils.format("skills/%s/versions", skillId); + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption( + HttpMethod.GET, query.isEmpty() ? path : path + "?" + query, baseUrl); + return AsyncHelper.callAsync( + api, GeneralGetParam.builder().apiKey(apiKey).headers(new HashMap<>()).build(), opt) + .thenApply( + r -> { + Type type = new TypeToken>() {}.getType(); + CursorPage page = FlattenResultBase.fromDashScopeResult(r, type); + page.setFetchNext( + cursor -> + listVersionsAsync( + skillId, + SkillListParam.builder() + .source(param != null ? param.getSource() : null) + .limit(param != null ? param.getLimit() : null) + .page(cursor) + .build())); + return page; + }); + } + + public CompletableFuture retrieveVersionAsync(String skillId, String version) { + if (skillId == null || skillId.isEmpty() || version == null || version.isEmpty()) { + return AsyncHelper.failedFuture( + new InputRequiredException("skillId and version are required!")); + } + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption( + HttpMethod.GET, StringUtils.format("skills/%s/versions/%s", skillId, version), baseUrl); + return AsyncHelper.callAsync( + api, GeneralGetParam.builder().apiKey(apiKey).headers(new HashMap<>()).build(), opt) + .thenApply(r -> FlattenResultBase.fromDashScopeResult(r, SkillVersion.class)); + } + + public CompletableFuture downloadVersionAsync(String skillId, String version) { + if (skillId == null || skillId.isEmpty() || version == null || version.isEmpty()) { + return AsyncHelper.failedFuture( + new InputRequiredException("skillId and version are required!")); + } + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption( + HttpMethod.GET, + StringUtils.format("skills/%s/versions/%s/content", skillId, version), + baseUrl); + return AsyncHelper.callAsync( + api, GeneralGetParam.builder().apiKey(apiKey).headers(new HashMap<>()).build(), opt) + .thenApply( + result -> { + Object output = result.getOutput(); + if (output instanceof JsonElement && ((JsonElement) output).isJsonObject()) { + return ((JsonElement) output).getAsJsonObject(); + } + return new JsonObject(); + }); + } + + @Override + public void close() { + if (files != null) { + files.close(); + } + } +} diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/resource/Vaults.java b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Vaults.java new file mode 100644 index 0000000..47eb13f --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Vaults.java @@ -0,0 +1,343 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.agentstudio.resource; + +import com.alibaba.dashscope.agentstudio.AgentStudioConstants; +import com.alibaba.dashscope.agentstudio.model.AgentStudioDeletionStatus; +import com.alibaba.dashscope.agentstudio.model.Credential; +import com.alibaba.dashscope.agentstudio.model.Vault; +import com.alibaba.dashscope.agentstudio.pagination.CursorPage; +import com.alibaba.dashscope.agentstudio.param.CredentialCreateParam; +import com.alibaba.dashscope.agentstudio.param.CredentialListParam; +import com.alibaba.dashscope.agentstudio.param.CredentialUpdateParam; +import com.alibaba.dashscope.agentstudio.param.VaultCreateParam; +import com.alibaba.dashscope.agentstudio.param.VaultListParam; +import com.alibaba.dashscope.agentstudio.param.VaultUpdateParam; +import com.alibaba.dashscope.api.GeneralApi; +import com.alibaba.dashscope.base.HalfDuplexParamBase; +import com.alibaba.dashscope.common.FlattenResultBase; +import com.alibaba.dashscope.common.GeneralGetParam; +import com.alibaba.dashscope.exception.InputRequiredException; +import com.alibaba.dashscope.protocol.ConnectionOptions; +import com.alibaba.dashscope.protocol.GeneralServiceOption; +import com.alibaba.dashscope.protocol.HttpMethod; +import com.alibaba.dashscope.utils.StringUtils; +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public final class Vaults { + private final GeneralApi api; + private final String baseUrl; + private final String apiKey; + private final Credentials credentials; + + public Vaults(String baseUrl, ConnectionOptions connectionOptions, String apiKey) { + this.baseUrl = baseUrl; + this.apiKey = apiKey; + this.api = connectionOptions != null ? new GeneralApi<>(connectionOptions) : new GeneralApi<>(); + this.credentials = new Credentials(); + } + + public Credentials credentials() { + return credentials; + } + + public Vault create(VaultCreateParam param) { + return AsyncHelper.joinAndUnwrap(createAsync(param)); + } + + public Vault retrieve(String vaultId) { + return retrieve(vaultId, null, null); + } + + public Vault retrieve(String vaultId, String apiKey, Map headers) { + return AsyncHelper.joinAndUnwrap(retrieveAsync(vaultId, apiKey, headers)); + } + + public Vault update(String vaultId, VaultUpdateParam param) { + return AsyncHelper.joinAndUnwrap(updateAsync(vaultId, param)); + } + + public CursorPage list(VaultListParam param) { + return AsyncHelper.joinAndUnwrap(listAsync(param)); + } + + public Vault archive(String vaultId) { + return AsyncHelper.joinAndUnwrap(archiveAsync(vaultId)); + } + + public AgentStudioDeletionStatus delete(String vaultId) { + return delete(vaultId, null, null); + } + + public AgentStudioDeletionStatus delete( + String vaultId, String apiKey, Map headers) { + return AsyncHelper.joinAndUnwrap(deleteAsync(vaultId, apiKey, headers)); + } + + public CompletableFuture createAsync(VaultCreateParam param) { + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption(HttpMethod.POST, "vaults", baseUrl); + return AsyncHelper.callAsync(api, AgentStudioConstants.withApiKey(apiKey, param), opt) + .thenApply(r -> FlattenResultBase.fromDashScopeResult(r, Vault.class)); + } + + public CompletableFuture retrieveAsync(String vaultId) { + return retrieveAsync(vaultId, null, null); + } + + public CompletableFuture retrieveAsync( + String vaultId, String apiKey, Map headers) { + if (vaultId == null || vaultId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("vaultId is required!")); + } + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption( + HttpMethod.GET, StringUtils.format("vaults/%s", vaultId), baseUrl); + String resolvedKey = apiKey != null ? apiKey : this.apiKey; + return AsyncHelper.callAsync( + api, + GeneralGetParam.builder() + .apiKey(resolvedKey) + .headers(headers != null ? headers : new HashMap<>()) + .build(), + opt) + .thenApply(r -> FlattenResultBase.fromDashScopeResult(r, Vault.class)); + } + + public CompletableFuture updateAsync(String vaultId, VaultUpdateParam param) { + if (vaultId == null || vaultId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("vaultId is required!")); + } + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption( + HttpMethod.POST, StringUtils.format("vaults/%s", vaultId), baseUrl); + return AsyncHelper.callAsync(api, AgentStudioConstants.withApiKey(apiKey, param), opt) + .thenApply(r -> FlattenResultBase.fromDashScopeResult(r, Vault.class)); + } + + public CompletableFuture> listAsync(VaultListParam param) { + String query = param.toQueryString(); + String path = query.isEmpty() ? "vaults" : "vaults?" + query; + GeneralServiceOption opt = AgentStudioConstants.newServiceOption(HttpMethod.GET, path, baseUrl); + return AsyncHelper.callAsync( + api, GeneralGetParam.builder().apiKey(apiKey).headers(new HashMap<>()).build(), opt) + .thenApply( + r -> { + Type type = new TypeToken>() {}.getType(); + CursorPage page = FlattenResultBase.fromDashScopeResult(r, type); + page.setFetchNext( + cursor -> + listAsync( + VaultListParam.builder() + .limit(param.getLimit()) + .includeArchived(param.getIncludeArchived()) + .keyword(param.getKeyword()) + .page(cursor) + .build())); + return page; + }); + } + + public CompletableFuture archiveAsync(String vaultId) { + if (vaultId == null || vaultId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("vaultId is required!")); + } + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption( + HttpMethod.POST, StringUtils.format("vaults/%s/archive", vaultId), baseUrl); + return AsyncHelper.callAsync( + api, AgentStudioConstants.withApiKey(apiKey, VaultUpdateParam.builder().build()), opt) + .thenApply(r -> FlattenResultBase.fromDashScopeResult(r, Vault.class)); + } + + public CompletableFuture deleteAsync(String vaultId) { + return deleteAsync(vaultId, null, null); + } + + public CompletableFuture deleteAsync( + String vaultId, String apiKey, Map headers) { + if (vaultId == null || vaultId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("vaultId is required!")); + } + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption( + HttpMethod.DELETE, StringUtils.format("vaults/%s", vaultId), baseUrl); + String resolvedKey = apiKey != null ? apiKey : this.apiKey; + return AsyncHelper.callAsync( + api, + GeneralGetParam.builder() + .apiKey(resolvedKey) + .headers(headers != null ? headers : new HashMap<>()) + .build(), + opt) + .thenApply(r -> FlattenResultBase.fromDashScopeResult(r, AgentStudioDeletionStatus.class)); + } + + public final class Credentials { + + public Credential create(String vaultId, CredentialCreateParam param) { + return AsyncHelper.joinAndUnwrap(createAsync(vaultId, param)); + } + + public Credential retrieve(String vaultId, String credentialId) { + return retrieve(vaultId, credentialId, null, null); + } + + public Credential retrieve( + String vaultId, String credentialId, String apiKey, Map headers) { + return AsyncHelper.joinAndUnwrap(retrieveAsync(vaultId, credentialId, apiKey, headers)); + } + + public Credential update(String vaultId, String credentialId, CredentialUpdateParam param) { + return AsyncHelper.joinAndUnwrap(updateAsync(vaultId, credentialId, param)); + } + + public CursorPage list(String vaultId, CredentialListParam param) { + return AsyncHelper.joinAndUnwrap(listAsync(vaultId, param)); + } + + public Credential archive(String vaultId, String credentialId) { + return AsyncHelper.joinAndUnwrap(archiveAsync(vaultId, credentialId)); + } + + public AgentStudioDeletionStatus delete(String vaultId, String credentialId) { + return delete(vaultId, credentialId, null, null); + } + + public AgentStudioDeletionStatus delete( + String vaultId, String credentialId, String apiKey, Map headers) { + return AsyncHelper.joinAndUnwrap(deleteAsync(vaultId, credentialId, apiKey, headers)); + } + + public CompletableFuture createAsync(String vaultId, CredentialCreateParam param) { + if (vaultId == null || vaultId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("vaultId is required!")); + } + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption( + HttpMethod.POST, StringUtils.format("vaults/%s/credentials", vaultId), baseUrl); + return AsyncHelper.callAsync(api, AgentStudioConstants.withApiKey(apiKey, param), opt) + .thenApply(r -> FlattenResultBase.fromDashScopeResult(r, Credential.class)); + } + + public CompletableFuture retrieveAsync(String vaultId, String credentialId) { + return retrieveAsync(vaultId, credentialId, null, null); + } + + public CompletableFuture retrieveAsync( + String vaultId, String credentialId, String apiKey, Map headers) { + if (vaultId == null || vaultId.isEmpty() || credentialId == null || credentialId.isEmpty()) { + return AsyncHelper.failedFuture( + new InputRequiredException("vaultId and credentialId are required!")); + } + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption( + HttpMethod.GET, + StringUtils.format("vaults/%s/credentials/%s", vaultId, credentialId), + baseUrl); + String resolvedKey = apiKey != null ? apiKey : Vaults.this.apiKey; + return AsyncHelper.callAsync( + api, + GeneralGetParam.builder() + .apiKey(resolvedKey) + .headers(headers != null ? headers : new HashMap<>()) + .build(), + opt) + .thenApply(r -> FlattenResultBase.fromDashScopeResult(r, Credential.class)); + } + + public CompletableFuture updateAsync( + String vaultId, String credentialId, CredentialUpdateParam param) { + if (vaultId == null || vaultId.isEmpty() || credentialId == null || credentialId.isEmpty()) { + return AsyncHelper.failedFuture( + new InputRequiredException("vaultId and credentialId are required!")); + } + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption( + HttpMethod.POST, + StringUtils.format("vaults/%s/credentials/%s", vaultId, credentialId), + baseUrl); + return AsyncHelper.callAsync(api, AgentStudioConstants.withApiKey(apiKey, param), opt) + .thenApply(r -> FlattenResultBase.fromDashScopeResult(r, Credential.class)); + } + + public CompletableFuture> listAsync( + String vaultId, CredentialListParam param) { + if (vaultId == null || vaultId.isEmpty()) { + return AsyncHelper.failedFuture(new InputRequiredException("vaultId is required!")); + } + String query = param.toQueryString(); + String path = StringUtils.format("vaults/%s/credentials", vaultId); + if (!query.isEmpty()) { + path = path + "?" + query; + } + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption(HttpMethod.GET, path, baseUrl); + return AsyncHelper.callAsync( + api, GeneralGetParam.builder().apiKey(apiKey).headers(new HashMap<>()).build(), opt) + .thenApply( + r -> { + Type type = new TypeToken>() {}.getType(); + CursorPage page = FlattenResultBase.fromDashScopeResult(r, type); + page.setFetchNext( + cursor -> + listAsync( + vaultId, + CredentialListParam.builder() + .limit(param.getLimit()) + .includeArchived(param.getIncludeArchived()) + .page(cursor) + .build())); + return page; + }); + } + + public CompletableFuture archiveAsync(String vaultId, String credentialId) { + if (vaultId == null || vaultId.isEmpty() || credentialId == null || credentialId.isEmpty()) { + return AsyncHelper.failedFuture( + new InputRequiredException("vaultId and credentialId are required!")); + } + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption( + HttpMethod.POST, + StringUtils.format("vaults/%s/credentials/%s/archive", vaultId, credentialId), + baseUrl); + return AsyncHelper.callAsync( + api, + AgentStudioConstants.withApiKey(apiKey, CredentialUpdateParam.builder().build()), + opt) + .thenApply(r -> FlattenResultBase.fromDashScopeResult(r, Credential.class)); + } + + public CompletableFuture deleteAsync( + String vaultId, String credentialId) { + return deleteAsync(vaultId, credentialId, null, null); + } + + public CompletableFuture deleteAsync( + String vaultId, String credentialId, String apiKey, Map headers) { + if (vaultId == null || vaultId.isEmpty() || credentialId == null || credentialId.isEmpty()) { + return AsyncHelper.failedFuture( + new InputRequiredException("vaultId and credentialId are required!")); + } + GeneralServiceOption opt = + AgentStudioConstants.newServiceOption( + HttpMethod.DELETE, + StringUtils.format("vaults/%s/credentials/%s", vaultId, credentialId), + baseUrl); + String resolvedKey = apiKey != null ? apiKey : Vaults.this.apiKey; + return AsyncHelper.callAsync( + api, + GeneralGetParam.builder() + .apiKey(resolvedKey) + .headers(headers != null ? headers : new HashMap<>()) + .build(), + opt) + .thenApply( + r -> FlattenResultBase.fromDashScopeResult(r, AgentStudioDeletionStatus.class)); + } + } +} diff --git a/src/main/java/com/alibaba/dashscope/utils/JsonUtils.java b/src/main/java/com/alibaba/dashscope/utils/JsonUtils.java index 8452bee..3740be6 100644 --- a/src/main/java/com/alibaba/dashscope/utils/JsonUtils.java +++ b/src/main/java/com/alibaba/dashscope/utils/JsonUtils.java @@ -1,6 +1,7 @@ // Copyright (c) Alibaba, Inc. and its affiliates. package com.alibaba.dashscope.utils; +import com.alibaba.dashscope.agentstudio.message.ContentBlock; import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversationMessage; import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversationMessageAdapter; import com.alibaba.dashscope.common.Message; @@ -49,6 +50,7 @@ MultiModalConversationMessage.class, new MultiModalConversationMessageAdapter()) .registerTypeAdapter(AnnotationBase.class, new AnnotationDeserializer()) .registerTypeAdapter(StepDetailBase.class, new StepDetailDeserializer()) .registerTypeAdapter(ToolCallBase.class, new ToolCallGsonDeserializer()) + .registerTypeAdapter(ContentBlock.class, new ContentBlock.Deserializer()) .addSerializationExclusionStrategy(new AnnotationExclusionStrategy()) .setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) .disableHtmlEscaping() diff --git a/src/test/java/com/alibaba/dashscope/TestAgentStudio.java b/src/test/java/com/alibaba/dashscope/TestAgentStudio.java new file mode 100644 index 0000000..0eb6251 --- /dev/null +++ b/src/test/java/com/alibaba/dashscope/TestAgentStudio.java @@ -0,0 +1,968 @@ +package com.alibaba.dashscope; + +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.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.alibaba.dashscope.agentstudio.AgentStudioClient; +import com.alibaba.dashscope.agentstudio.message.ClientEvents; +import com.alibaba.dashscope.agentstudio.message.ContentBlock; +import com.alibaba.dashscope.agentstudio.message.Message; +import com.alibaba.dashscope.agentstudio.model.Agent; +import com.alibaba.dashscope.agentstudio.model.AgentStudioDeletionStatus; +import com.alibaba.dashscope.agentstudio.model.AgentStudioFile; +import com.alibaba.dashscope.agentstudio.model.AgentVersion; +import com.alibaba.dashscope.agentstudio.model.Credential; +import com.alibaba.dashscope.agentstudio.model.Environment; +import com.alibaba.dashscope.agentstudio.model.Session; +import com.alibaba.dashscope.agentstudio.model.Skill; +import com.alibaba.dashscope.agentstudio.model.SkillVersion; +import com.alibaba.dashscope.agentstudio.model.Vault; +import com.alibaba.dashscope.agentstudio.pagination.CursorPage; +import com.alibaba.dashscope.agentstudio.param.AgentCreateParam; +import com.alibaba.dashscope.agentstudio.param.AgentListParam; +import com.alibaba.dashscope.agentstudio.param.AgentUpdateParam; +import com.alibaba.dashscope.agentstudio.param.CredentialCreateParam; +import com.alibaba.dashscope.agentstudio.param.CredentialListParam; +import com.alibaba.dashscope.agentstudio.param.CredentialUpdateParam; +import com.alibaba.dashscope.agentstudio.param.EnvironmentCreateParam; +import com.alibaba.dashscope.agentstudio.param.EnvironmentListParam; +import com.alibaba.dashscope.agentstudio.param.FileListParam; +import com.alibaba.dashscope.agentstudio.param.SessionCreateParam; +import com.alibaba.dashscope.agentstudio.param.SessionEventListParam; +import com.alibaba.dashscope.agentstudio.param.SessionListParam; +import com.alibaba.dashscope.agentstudio.param.SessionUpdateParam; +import com.alibaba.dashscope.agentstudio.param.SkillCreateParam; +import com.alibaba.dashscope.agentstudio.param.SkillListParam; +import com.alibaba.dashscope.agentstudio.param.VaultCreateParam; +import com.alibaba.dashscope.agentstudio.param.VaultListParam; +import com.alibaba.dashscope.agentstudio.param.VaultUpdateParam; +import com.alibaba.dashscope.agentstudio.resource.Agents; +import com.alibaba.dashscope.agentstudio.resource.Environments; +import com.alibaba.dashscope.agentstudio.resource.Sessions; +import com.alibaba.dashscope.agentstudio.resource.Skills; +import com.alibaba.dashscope.agentstudio.resource.Vaults; +import com.alibaba.dashscope.exception.ApiException; +import com.alibaba.dashscope.utils.Constants; +import com.alibaba.dashscope.utils.JsonUtils; +import com.google.gson.JsonObject; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; + +@Execution(ExecutionMode.SAME_THREAD) +public class TestAgentStudio { + private static MockWebServer mockServer; + private static JsonObject fixtures; + + @BeforeAll + public static void before() throws IOException { + mockServer = new MockWebServer(); + mockServer.start(); + Constants.baseHttpApiUrl = String.format("http://127.0.0.1:%s/api/v1/", mockServer.getPort()); + Constants.apiKey = "test-key"; + byte[] content = Files.readAllBytes(Paths.get("./src/test/resources/agentstudio.json")); + fixtures = JsonUtils.parse(new String(content, StandardCharsets.UTF_8)); + } + + @AfterAll + public static void after() throws IOException { + mockServer.close(); + } + + private void enqueue(String fixtureKey) { + JsonObject rsp = fixtures.get(fixtureKey).getAsJsonObject(); + mockServer.enqueue(TestUtils.createMockResponse(JsonUtils.toJson(rsp), 200)); + } + + // ======================== Agents ======================== + + @Test + public void testAgentCreate() throws Exception { + enqueue("agent_response"); + Agent agent = + new Agents(null, null, null) + .create(AgentCreateParam.builder().name("test-agent").model("qwen-max").build()); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("POST", req.getMethod()); + assertTrue(req.getPath().endsWith("/agents")); + assertEquals("agent_xyz", agent.getId()); + assertEquals("test-agent", agent.getName()); + assertEquals(Integer.valueOf(2), agent.getVersion()); + assertEquals("You are a helpful assistant.", agent.getSystemPrompt()); + assertEquals("qwen-max", agent.getModel().getId()); + } + + @Test + public void testAgentCreateHttpBody() { + JsonObject body = + AgentCreateParam.builder() + .name("my-agent") + .model("qwen-plus") + .description("desc") + .systemPrompt("be helpful") + .build() + .getHttpBody(); + assertEquals("my-agent", body.get("name").getAsString()); + assertEquals("qwen-plus", body.getAsJsonObject("model").get("id").getAsString()); + assertEquals("desc", body.get("description").getAsString()); + assertEquals("be helpful", body.get("system").getAsString()); + } + + @Test + public void testAgentRetrieve() throws Exception { + enqueue("agent_response"); + Agent agent = new Agents(null, null, null).retrieve("agent_xyz"); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("GET", req.getMethod()); + assertTrue(req.getPath().contains("/agents/agent_xyz")); + assertEquals("A test agent", agent.getDescription()); + assertEquals("ws_001", agent.getWorkspaceId()); + } + + @Test + public void testAgentUpdate() throws Exception { + enqueue("agent_response"); + Agent agent = + new Agents(null, null, null) + .update( + "agent_xyz", AgentUpdateParam.builder().version(2).name("updated-agent").build()); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("POST", req.getMethod()); + assertTrue(req.getPath().contains("/agents/agent_xyz")); + assertEquals("agent_xyz", agent.getId()); + } + + @Test + public void testAgentUpdateRequiresVersion() { + assertThrows( + ApiException.class, + () -> + new Agents(null, null, null) + .update("agent_xyz", AgentUpdateParam.builder().name("no-version").build())); + } + + @Test + public void testAgentUpdateRequiresAgentId() { + assertThrows( + ApiException.class, + () -> + new Agents(null, null, null) + .update("", AgentUpdateParam.builder().version(1).name("x").build())); + } + + @Test + public void testAgentUpdateHttpBody() { + JsonObject body = + AgentUpdateParam.builder() + .version(3) + .model("qwen-turbo") + .systemPrompt("new prompt") + .build() + .getHttpBody(); + assertEquals(3, body.get("version").getAsInt()); + assertEquals("qwen-turbo", body.getAsJsonObject("model").get("id").getAsString()); + assertEquals("new prompt", body.get("system").getAsString()); + } + + @Test + public void testAgentList() throws Exception { + enqueue("agent_list_response"); + CursorPage page = + new Agents(null, null, null).list(AgentListParam.builder().limit(20).build()); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("GET", req.getMethod()); + assertTrue(req.getPath().contains("limit=20")); + assertEquals(2, page.getData().size()); + assertEquals("agent_001", page.getData().get(0).getId()); + assertEquals("cursor_agent", page.getNextPage()); + } + + @Test + public void testAgentListWithIncludeArchived() { + String qs = AgentListParam.builder().limit(10).includeArchived(true).build().toQueryString(); + assertTrue(qs.contains("limit=10")); + assertTrue(qs.contains("include_archived=true")); + } + + @Test + public void testAgentListVersions() throws Exception { + enqueue("agent_version_response"); + CursorPage page = + new Agents(null, null, null).listVersions("agent_xyz", AgentListParam.builder().build()); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("GET", req.getMethod()); + assertTrue(req.getPath().contains("/agents/agent_xyz/versions")); + assertEquals(2, page.getData().size()); + assertEquals("agent_xyz", page.getData().get(0).getAgentId()); + assertEquals(Integer.valueOf(1), page.getData().get(0).getVersion()); + assertNull(page.getNextPage()); + } + + @Test + public void testAgentModelDeserialization() throws Exception { + enqueue("agent_response"); + Agent agent = new Agents(null, null, null).retrieve("agent_xyz"); + mockServer.takeRequest(); + assertEquals("agent", agent.getType()); + assertEquals("test-agent", agent.getName()); + assertEquals(Integer.valueOf(2), agent.getVersion()); + assertEquals("You are a helpful assistant.", agent.getSystem()); + assertNotNull(agent.getTools()); + assertEquals(1, agent.getTools().size()); + assertNotNull(agent.getMetadata()); + assertEquals("test", agent.getMetadata().get("env")); + assertEquals("2025-01-01T00:00:00Z", agent.getCreatedAt()); + } + + // ======================== Sessions ======================== + + @Test + public void testSessionCreate() throws Exception { + enqueue("session_response"); + Session session = + new Sessions(null, null, null) + .create(SessionCreateParam.builder().agent("agent_xyz").title("Test Session").build()); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("POST", req.getMethod()); + assertTrue(req.getPath().endsWith("/sessions")); + assertEquals("sess_abc123", session.getId()); + assertEquals("idle", session.getStatus()); + assertEquals("agent_xyz", session.getAgentId()); + assertNotNull(session.getStopReason()); + assertEquals("end_turn", session.getStopReason().getType()); + assertNotNull(session.getUsage()); + assertEquals(100L, session.getUsage().getInputTokens().longValue()); + assertEquals(42.5, session.getUsage().getSpeed(), 0.01); + } + + @Test + public void testSessionRetrieve() throws Exception { + enqueue("session_response"); + Session session = new Sessions(null, null, null).retrieve("sess_abc123"); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("GET", req.getMethod()); + assertTrue(req.getPath().contains("/sessions/sess_abc123")); + assertEquals("sess_abc123", session.getId()); + } + + @Test + public void testSessionUpdate() throws Exception { + enqueue("session_response"); + Session session = + new Sessions(null, null, null) + .update("sess_abc123", SessionUpdateParam.builder().title("Updated").build()); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("POST", req.getMethod()); + assertTrue(req.getPath().contains("/sessions/sess_abc123")); + } + + @Test + public void testSessionList() throws Exception { + enqueue("session_list_response"); + CursorPage page = + new Sessions(null, null, null) + .list( + SessionListParam.builder() + .limit(10) + .agentId("agent_xyz") + .statuses(Arrays.asList("idle", "running")) + .build()); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("GET", req.getMethod()); + String path = req.getPath(); + assertTrue(path.contains("limit=10")); + assertTrue(path.contains("agent_id=agent_xyz")); + assertTrue(path.contains("statuses[]=idle")); + assertEquals(2, page.getData().size()); + assertEquals("cursor_abc", page.getNextPage()); + assertTrue(page.hasNext()); + } + + @Test + public void testSessionDelete() throws Exception { + enqueue("delete_response"); + AgentStudioDeletionStatus status = new Sessions(null, null, null).delete("sess_abc123"); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("DELETE", req.getMethod()); + assertEquals("sess_abc123", status.getId()); + assertTrue(status.isDeleted()); + } + + @Test + public void testSessionArchive() throws Exception { + enqueue("session_response"); + Session session = new Sessions(null, null, null).archive("sess_abc123"); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("POST", req.getMethod()); + assertTrue(req.getPath().contains("/sessions/sess_abc123/archive")); + } + + @Test + public void testSessionRetrieveRequiresId() { + assertThrows(ApiException.class, () -> new Sessions(null, null, null).retrieve(null)); + } + + @Test + public void testSessionListParamQueryString() { + String qs = + SessionListParam.builder() + .limit(5) + .page("cursor_abc") + .agentId("agent_1") + .statuses(Arrays.asList("idle", "running")) + .createdAtGt("2025-01-01") + .build() + .toQueryString(); + assertTrue(qs.contains("limit=5")); + assertTrue(qs.contains("page=cursor_abc")); + assertTrue(qs.contains("agent_id=agent_1")); + assertTrue(qs.contains("statuses[]=idle")); + assertTrue(qs.contains("created_at[gt]=2025-01-01")); + } + + @Test + public void testSessionListParamEmptyQuery() { + assertEquals("", SessionListParam.builder().build().toQueryString()); + } + + @Test + public void testSessionModelDeserialization() { + String json = JsonUtils.toJson(fixtures.get("session_response").getAsJsonObject()); + Session session = JsonUtils.fromJson(json, Session.class); + assertEquals("sess_abc123", session.getId()); + assertEquals("Test Session", session.getTitle()); + assertNotNull(session.getAgent()); + assertEquals("agent_xyz", session.getAgent().getId()); + assertEquals("test-agent", session.getAgent().getName()); + assertEquals(Integer.valueOf(1), session.getAgentVersion()); + assertEquals("env_001", session.getEnvironmentId()); + assertNotNull(session.getVaultIds()); + assertTrue(session.getVaultIds().isEmpty()); + } + + @Test + public void testSessionEventList() throws Exception { + enqueue("event_list_response"); + CursorPage page = + new Sessions(null, null, null) + .events() + .list("sess_abc123", SessionEventListParam.builder().limit(10).build()); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("GET", req.getMethod()); + assertTrue(req.getPath().contains("/sessions/sess_abc123/events")); + assertEquals(2, page.getData().size()); + Message msg = page.getData().get(0); + assertEquals("evt_1", msg.getId()); + assertEquals("user", msg.getRole()); + assertEquals("event", msg.getObject()); + assertEquals("completed", msg.getStatus()); + assertEquals(Long.valueOf(1), msg.getSequenceNumber()); + assertTrue(msg.getContent().get(0) instanceof ContentBlock.Text); + assertEquals("hello", ((ContentBlock.Text) msg.getContent().get(0)).getText()); + } + + // ======================== Environments ======================== + + @Test + public void testEnvironmentCreate() throws Exception { + enqueue("environment_response"); + Environment env = + new Environments(null, null, null) + .create( + EnvironmentCreateParam.builder() + .name("Test Environment") + .config(Collections.singletonMap("type", "cloud")) + .build()); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("POST", req.getMethod()); + assertTrue(req.getPath().endsWith("/environments")); + assertEquals("env_001", env.getId()); + assertEquals("Test Environment", env.getName()); + } + + @Test + public void testEnvironmentRetrieve() throws Exception { + enqueue("environment_response"); + Environment env = new Environments(null, null, null).retrieve("env_001"); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("GET", req.getMethod()); + assertTrue(req.getPath().contains("/environments/env_001")); + assertEquals("env_001", env.getId()); + } + + @Test + public void testEnvironmentList() throws Exception { + enqueue("environment_list_response"); + CursorPage page = + new Environments(null, null, null).list(EnvironmentListParam.builder().limit(20).build()); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("GET", req.getMethod()); + assertEquals(2, page.getData().size()); + assertNull(page.getNextPage()); + } + + @Test + public void testEnvironmentDelete() throws Exception { + enqueue("delete_response"); + AgentStudioDeletionStatus status = new Environments(null, null, null).delete("env_001"); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("DELETE", req.getMethod()); + assertTrue(req.getPath().contains("/environments/env_001")); + assertTrue(status.isDeleted()); + } + + // ======================== Skills ======================== + + @Test + public void testSkillCreate() throws Exception { + enqueue("skill_response"); + Skill skill = + new Skills(null, null, null, null) + .create(SkillCreateParam.builder().fileId("file_001").build()); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("POST", req.getMethod()); + assertTrue(req.getPath().endsWith("/skills")); + assertEquals("skill_001", skill.getId()); + assertEquals("bash_tool", skill.getName()); + } + + @Test + public void testSkillRetrieve() throws Exception { + enqueue("skill_response"); + Skill skill = new Skills(null, null, null, null).retrieve("skill_001"); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("GET", req.getMethod()); + assertTrue(req.getPath().contains("/skills/skill_001")); + assertEquals("skill_001", skill.getId()); + } + + @Test + public void testSkillList() throws Exception { + enqueue("skill_list_response"); + CursorPage page = + new Skills(null, null, null, null).list(SkillListParam.builder().build()); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("GET", req.getMethod()); + assertEquals(1, page.getData().size()); + } + + @Test + public void testSkillDelete() throws Exception { + enqueue("delete_response"); + AgentStudioDeletionStatus status = new Skills(null, null, null, null).delete("skill_001"); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("DELETE", req.getMethod()); + assertTrue(status.isDeleted()); + } + + @Test + public void testSkillVersionCreate() throws Exception { + enqueue("skill_version_response"); + SkillVersion sv = + new Skills(null, null, null, null) + .createVersion("skill_001", SkillCreateParam.builder().fileId("file_002").build()); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("POST", req.getMethod()); + assertTrue(req.getPath().contains("/skills/skill_001/versions")); + assertEquals("sv_001", sv.getId()); + } + + @Test + public void testSkillVersionList() throws Exception { + enqueue("skill_list_response"); + CursorPage page = + new Skills(null, null, null, null) + .listVersions("skill_001", SkillListParam.builder().build()); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("GET", req.getMethod()); + assertTrue(req.getPath().contains("/skills/skill_001/versions")); + } + + // ======================== ContentBlock ======================== + + @Test + public void testContentBlockText() { + ContentBlock block = + JsonUtils.fromJson( + "{\"type\":\"text\",\"text\":\"hello world\"," + + "\"citations\":[{\"url\":\"http://example.com\"}]}", + ContentBlock.class); + assertTrue(block instanceof ContentBlock.Text); + ContentBlock.Text text = (ContentBlock.Text) block; + assertEquals("hello world", text.getText()); + assertEquals(1, text.getCitations().size()); + } + + @Test + public void testContentBlockImage() { + ContentBlock block = + JsonUtils.fromJson( + "{\"type\":\"image\",\"image_url\":\"http://img.png\"," + + "\"file_id\":\"f1\",\"media_type\":\"image/png\"}", + ContentBlock.class); + assertTrue(block instanceof ContentBlock.Image); + ContentBlock.Image img = (ContentBlock.Image) block; + assertEquals("http://img.png", img.getImageUrl()); + assertEquals("f1", img.getFileId()); + } + + @Test + public void testContentBlockData() { + ContentBlock block = + JsonUtils.fromJson( + "{\"type\":\"data\",\"data\":{\"key\":\"val\"}," + "\"name\":\"test\"}", + ContentBlock.class); + assertTrue(block instanceof ContentBlock.DataContent); + assertEquals("val", ((ContentBlock.DataContent) block).getData().get("key").getAsString()); + } + + @Test + public void testContentBlockError() { + ContentBlock block = + JsonUtils.fromJson( + "{\"type\":\"error\",\"error_code\":\"E001\"," + "\"message\":\"something failed\"}", + ContentBlock.class); + assertTrue(block instanceof ContentBlock.Error); + assertEquals("E001", ((ContentBlock.Error) block).getErrorCode()); + } + + @Test + public void testContentBlockRefusal() { + ContentBlock block = + JsonUtils.fromJson( + "{\"type\":\"refusal\"," + "\"refusal\":\"I cannot do that\"}", ContentBlock.class); + assertTrue(block instanceof ContentBlock.Refusal); + assertEquals("I cannot do that", ((ContentBlock.Refusal) block).getRefusal()); + } + + @Test + public void testContentBlockUnknownFallsBackToData() { + ContentBlock block = + JsonUtils.fromJson( + "{\"type\":\"unknown_future_type\",\"data\":{\"x\":1}}", ContentBlock.class); + assertTrue(block instanceof ContentBlock.DataContent); + } + + // ======================== Vaults ======================== + + @Test + public void testVaultCreate() throws Exception { + enqueue("vault_response"); + Vault vault = + new Vaults(null, null, null) + .create(VaultCreateParam.builder().displayName("Test Vault").build()); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("POST", req.getMethod()); + assertTrue(req.getPath().endsWith("/vaults")); + assertEquals("vault_001", vault.getId()); + assertEquals("Test Vault", vault.getDisplayName()); + assertEquals("vault", vault.getType()); + } + + @Test + public void testVaultRetrieve() throws Exception { + enqueue("vault_response"); + Vault vault = new Vaults(null, null, null).retrieve("vault_001"); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("GET", req.getMethod()); + assertTrue(req.getPath().contains("/vaults/vault_001")); + assertEquals("vault_001", vault.getId()); + assertNotNull(vault.getMetadata()); + assertEquals("security", vault.getMetadata().get("team")); + } + + @Test + public void testVaultUpdate() throws Exception { + enqueue("vault_response"); + Vault vault = + new Vaults(null, null, null) + .update("vault_001", VaultUpdateParam.builder().displayName("Updated Vault").build()); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("POST", req.getMethod()); + assertTrue(req.getPath().contains("/vaults/vault_001")); + assertEquals("vault_001", vault.getId()); + } + + @Test + public void testVaultList() throws Exception { + enqueue("vault_list_response"); + CursorPage page = + new Vaults(null, null, null).list(VaultListParam.builder().limit(20).build()); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("GET", req.getMethod()); + assertTrue(req.getPath().contains("limit=20")); + assertEquals(2, page.getData().size()); + assertEquals("vault_001", page.getData().get(0).getId()); + assertEquals("cursor_vault", page.getNextPage()); + assertTrue(page.hasNext()); + } + + @Test + public void testVaultDelete() throws Exception { + enqueue("delete_response"); + AgentStudioDeletionStatus status = new Vaults(null, null, null).delete("vault_001"); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("DELETE", req.getMethod()); + assertTrue(req.getPath().contains("/vaults/vault_001")); + assertTrue(status.isDeleted()); + } + + @Test + public void testVaultArchive() throws Exception { + enqueue("vault_response"); + Vault vault = new Vaults(null, null, null).archive("vault_001"); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("POST", req.getMethod()); + assertTrue(req.getPath().contains("/vaults/vault_001/archive")); + } + + @Test + public void testVaultRetrieveRequiresId() { + assertThrows(ApiException.class, () -> new Vaults(null, null, null).retrieve(null)); + } + + // ======================== Credentials ======================== + + @Test + public void testCredentialCreate() throws Exception { + enqueue("credential_response"); + JsonObject auth = new JsonObject(); + auth.addProperty("type", "environment_variable"); + auth.addProperty("secret_name", "DASHSCOPE_API_KEY"); + auth.addProperty("secret_value", "sk-test"); + Credential cred = + new Vaults(null, null, null) + .credentials() + .create( + "vault_001", + CredentialCreateParam.builder().auth(auth).displayName("Test Credential").build()); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("POST", req.getMethod()); + assertTrue(req.getPath().endsWith("/vaults/vault_001/credentials")); + assertEquals("cred_001", cred.getId()); + assertEquals("vault_001", cred.getVaultId()); + assertEquals("Test Credential", cred.getDisplayName()); + } + + @Test + public void testCredentialRetrieve() throws Exception { + enqueue("credential_response"); + Credential cred = new Vaults(null, null, null).credentials().retrieve("vault_001", "cred_001"); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("GET", req.getMethod()); + assertTrue(req.getPath().contains("/vaults/vault_001/credentials/cred_001")); + assertEquals("cred_001", cred.getId()); + assertNotNull(cred.getAuth()); + assertEquals("environment_variable", cred.getAuth().getType()); + } + + @Test + public void testCredentialUpdate() throws Exception { + enqueue("credential_response"); + Credential cred = + new Vaults(null, null, null) + .credentials() + .update( + "vault_001", + "cred_001", + CredentialUpdateParam.builder().displayName("Updated Cred").build()); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("POST", req.getMethod()); + assertTrue(req.getPath().contains("/vaults/vault_001/credentials/cred_001")); + } + + @Test + public void testCredentialList() throws Exception { + enqueue("credential_list_response"); + CursorPage page = + new Vaults(null, null, null) + .credentials() + .list("vault_001", CredentialListParam.builder().build()); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("GET", req.getMethod()); + assertTrue(req.getPath().contains("/vaults/vault_001/credentials")); + assertEquals(2, page.getData().size()); + assertEquals("cred_001", page.getData().get(0).getId()); + assertNull(page.getNextPage()); + } + + @Test + public void testCredentialDelete() throws Exception { + enqueue("delete_response"); + AgentStudioDeletionStatus status = + new Vaults(null, null, null).credentials().delete("vault_001", "cred_001"); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("DELETE", req.getMethod()); + assertTrue(req.getPath().contains("/vaults/vault_001/credentials/cred_001")); + assertTrue(status.isDeleted()); + } + + @Test + public void testCredentialArchive() throws Exception { + enqueue("credential_response"); + Credential cred = new Vaults(null, null, null).credentials().archive("vault_001", "cred_001"); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("POST", req.getMethod()); + assertTrue(req.getPath().contains("/vaults/vault_001/credentials/cred_001/archive")); + } + + @Test + public void testCredentialCreateRequiresVaultId() { + JsonObject auth = new JsonObject(); + auth.addProperty("type", "environment_variable"); + auth.addProperty("secret_name", "TEST_KEY"); + auth.addProperty("secret_value", "test-val"); + assertThrows( + ApiException.class, + () -> + new Vaults(null, null, null) + .credentials() + .create(null, CredentialCreateParam.builder().auth(auth).build())); + } + + @Test + public void testCredentialRetrieveRequiresIds() { + assertThrows( + ApiException.class, + () -> new Vaults(null, null, null).credentials().retrieve("vault_001", null)); + } + + @Test + public void testVaultListWithKeyword() { + String qs = VaultListParam.builder().keyword("my-vault").limit(10).build().toQueryString(); + assertTrue(qs.contains("keyword=my-vault")); + assertTrue(qs.contains("limit=10")); + } + + // ======================== Files ======================== + + @Test + public void testFileUpload() throws Exception { + String baseUrl = String.format("http://127.0.0.1:%s/api/v1/agentstudio", mockServer.getPort()); + com.alibaba.dashscope.agentstudio.resource.Files filesResource = + new com.alibaba.dashscope.agentstudio.resource.Files(baseUrl, null, null); + try { + JsonObject rsp = fixtures.get("file_response").getAsJsonObject(); + mockServer.enqueue(TestUtils.createMockResponse(JsonUtils.toJson(rsp), 200)); + java.io.File tempFile = java.io.File.createTempFile("test-upload", ".txt"); + tempFile.deleteOnExit(); + java.nio.file.Files.write(tempFile.toPath(), "hello".getBytes(StandardCharsets.UTF_8)); + AgentStudioFile file = filesResource.upload(tempFile.getAbsolutePath(), "text/plain"); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("POST", req.getMethod()); + assertTrue(req.getPath().endsWith("/files")); + assertTrue(req.getHeader("Content-Type").startsWith("multipart/form-data")); + assertEquals("file_001", file.getId()); + assertEquals("test.pdf", file.getFilename()); + assertEquals("application/pdf", file.getMimeType()); + assertEquals(Long.valueOf(12345), file.getSizeBytes()); + } finally { + filesResource.close(); + } + } + + @Test + public void testFileUploadInputStream() throws Exception { + String baseUrl = String.format("http://127.0.0.1:%s/api/v1/agentstudio", mockServer.getPort()); + com.alibaba.dashscope.agentstudio.resource.Files filesResource = + new com.alibaba.dashscope.agentstudio.resource.Files(baseUrl, null, null); + try { + JsonObject rsp = fixtures.get("file_response").getAsJsonObject(); + mockServer.enqueue(TestUtils.createMockResponse(JsonUtils.toJson(rsp), 200)); + final boolean[] closed = {false}; + InputStream is = + new ByteArrayInputStream("test content".getBytes(StandardCharsets.UTF_8)) { + @Override + public void close() throws IOException { + closed[0] = true; + super.close(); + } + }; + AgentStudioFile file = filesResource.upload("test.txt", is, "text/plain"); + mockServer.takeRequest(); + assertEquals("file_001", file.getId()); + assertTrue(closed[0], "InputStream should be closed after upload"); + } finally { + filesResource.close(); + } + } + + @Test + public void testFileRetrieve() throws Exception { + enqueue("file_response"); + com.alibaba.dashscope.agentstudio.resource.Files filesResource = + new com.alibaba.dashscope.agentstudio.resource.Files(null, null, null); + AgentStudioFile file = filesResource.retrieve("file_001"); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("GET", req.getMethod()); + assertTrue(req.getPath().contains("/files/file_001")); + assertEquals("file_001", file.getId()); + assertEquals("completed", file.getStatus()); + } + + @Test + public void testFileList() throws Exception { + enqueue("file_list_response"); + com.alibaba.dashscope.agentstudio.resource.Files filesResource = + new com.alibaba.dashscope.agentstudio.resource.Files(null, null, null); + CursorPage page = + filesResource.list(FileListParam.builder().limit(20).build()); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("GET", req.getMethod()); + assertTrue(req.getPath().contains("limit=20")); + assertEquals(2, page.getData().size()); + assertEquals("file_001", page.getData().get(0).getId()); + assertEquals("cursor_file", page.getNextPage()); + } + + @Test + public void testFileDelete() throws Exception { + enqueue("delete_response"); + com.alibaba.dashscope.agentstudio.resource.Files filesResource = + new com.alibaba.dashscope.agentstudio.resource.Files(null, null, null); + AgentStudioDeletionStatus status = filesResource.delete("file_001"); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("DELETE", req.getMethod()); + assertTrue(req.getPath().contains("/files/file_001")); + assertTrue(status.isDeleted()); + } + + @Test + public void testFileUploadNullFilePath() { + com.alibaba.dashscope.agentstudio.resource.Files filesResource = + new com.alibaba.dashscope.agentstudio.resource.Files(null, null, null); + assertThrows(ApiException.class, () -> filesResource.upload(null, null)); + } + + @Test + public void testFileUploadNullInputStream() { + com.alibaba.dashscope.agentstudio.resource.Files filesResource = + new com.alibaba.dashscope.agentstudio.resource.Files(null, null, null); + assertThrows( + ApiException.class, () -> filesResource.upload("test.txt", (InputStream) null, null)); + } + + @Test + public void testFileRetrieveRequiresId() { + com.alibaba.dashscope.agentstudio.resource.Files filesResource = + new com.alibaba.dashscope.agentstudio.resource.Files(null, null, null); + assertThrows(ApiException.class, () -> filesResource.retrieve(null)); + } + + @Test + public void testFileDeleteRequiresId() { + com.alibaba.dashscope.agentstudio.resource.Files filesResource = + new com.alibaba.dashscope.agentstudio.resource.Files(null, null, null); + assertThrows(ApiException.class, () -> filesResource.delete(null)); + } + + // ======================== AgentStudioClient ======================== + + @Test + public void testAgentStudioClientBuilder() { + String baseUrl = String.format("http://127.0.0.1:%s/api/v1/agentstudio", mockServer.getPort()); + AgentStudioClient client = + AgentStudioClient.builder().apiKey("test-key").baseUrl(baseUrl).build(); + try { + assertNotNull(client.agents()); + assertNotNull(client.sessions()); + assertNotNull(client.environments()); + assertNotNull(client.skills()); + assertNotNull(client.vaults()); + assertNotNull(client.files()); + assertEquals(baseUrl, client.getBaseUrl()); + } finally { + client.close(); + } + } + + @Test + public void testAgentStudioClientClose() { + String baseUrl = String.format("http://127.0.0.1:%s/api/v1/agentstudio", mockServer.getPort()); + AgentStudioClient client = + AgentStudioClient.builder().apiKey("test-key").baseUrl(baseUrl).build(); + client.close(); + } + + // ======================== SessionEvents.send() ======================== + + @Test + public void testSessionEventSend() throws Exception { + enqueue("session_event_send_response"); + JsonObject event = ClientEvents.userMessage("hello"); + JsonObject result = + new Sessions(null, null, null) + .events() + .send("sess_abc123", Collections.singletonList(event)); + RecordedRequest req = mockServer.takeRequest(); + assertEquals("POST", req.getMethod()); + assertTrue(req.getPath().contains("/sessions/sess_abc123/events")); + assertNotNull(result); + } + + @Test + public void testSessionEventSendRequiresSessionId() { + assertThrows( + ApiException.class, + () -> + new Sessions(null, null, null) + .events() + .send(null, Collections.singletonList(new JsonObject()))); + } + + @Test + public void testSessionEventSendRequiresEvents() { + assertThrows( + IllegalArgumentException.class, + () -> new Sessions(null, null, null).events().send("sess_abc123", Collections.emptyList())); + } + + // ======================== URL Encoding ======================== + + @Test + public void testUrlEncodingInQueryParams() { + String qs = + SessionListParam.builder().page("cursor with spaces&special=chars").build().toQueryString(); + assertTrue(qs.contains("page=cursor+with+spaces%26special%3Dchars")); + } + + @Test + public void testUrlEncodingInSessionEventListParam() { + String qs = + SessionEventListParam.builder() + .createdAtGt("2025-01-01T00:00:00+08:00") + .build() + .toQueryString(); + assertTrue(qs.contains("created_at[gt]=2025-01-01T00%3A00%3A00%2B08%3A00")); + } + + // ======================== EnvironmentCreateParam scope type ======================== + + @Test + public void testEnvironmentCreateWithScopeAsString() { + JsonObject body = + EnvironmentCreateParam.builder() + .name("env-test") + .config(Collections.singletonMap("type", "cloud")) + .scope("organization") + .build() + .getHttpBody(); + assertEquals("organization", body.get("scope").getAsString()); + assertTrue(body.get("scope").isJsonPrimitive()); + } +} diff --git a/src/test/resources/agentstudio.json b/src/test/resources/agentstudio.json new file mode 100644 index 0000000..47b00b7 --- /dev/null +++ b/src/test/resources/agentstudio.json @@ -0,0 +1,310 @@ +{ + "agent_response": { + "id": "agent_xyz", + "type": "agent", + "name": "test-agent", + "description": "A test agent", + "model": {"id": "qwen-max"}, + "system": "You are a helpful assistant.", + "tools": [{"type": "bash_tool"}], + "skills": [], + "mcp_servers": [], + "version": 2, + "metadata": {"env": "test"}, + "workspace_id": "ws_001", + "archived_at": null, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T01:00:00Z", + "request_id": "req-010" + }, + "agent_list_response": { + "data": [ + { + "id": "agent_001", + "type": "agent", + "name": "agent-one", + "version": 1, + "created_at": "2025-01-01T00:00:00Z" + }, + { + "id": "agent_002", + "type": "agent", + "name": "agent-two", + "version": 3, + "created_at": "2025-01-02T00:00:00Z" + } + ], + "next_page": "cursor_agent", + "request_id": "req-011" + }, + "agent_version_response": { + "data": [ + { + "agent_id": "agent_xyz", + "version": 1, + "config": {"name": "test-agent", "model": {"id": "qwen-max"}}, + "created_at": "2025-01-01T00:00:00Z" + }, + { + "agent_id": "agent_xyz", + "version": 2, + "config": {"name": "test-agent-v2", "model": {"id": "qwen-plus"}}, + "created_at": "2025-01-02T00:00:00Z" + } + ], + "next_page": null, + "request_id": "req-012" + }, + "session_response": { + "id": "sess_abc123", + "type": "session", + "title": "Test Session", + "agent": { + "id": "agent_xyz", + "type": "agent", + "version": 1, + "name": "test-agent", + "description": null, + "model": {"id": "qwen-max"}, + "system": "You are a test agent.", + "tools": [] + }, + "environment_id": "env_001", + "status": "idle", + "stop_reason": { + "type": "end_turn", + "event_ids": ["evt_1", "evt_2"] + }, + "usage": { + "input_tokens": 100, + "output_tokens": 50, + "cache_creation": null, + "cache_read_input_tokens": 5, + "speed": 42.5 + }, + "vault_ids": [], + "metadata": {"env": "test"}, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T01:00:00Z", + "request_id": "req-001" + }, + "session_list_response": { + "data": [ + { + "id": "sess_1", + "status": "idle", + "agent": {"id": "agent_xyz", "type": "agent", "version": 1}, + "created_at": "2025-01-01T00:00:00Z" + }, + { + "id": "sess_2", + "status": "running", + "agent": {"id": "agent_xyz", "type": "agent", "version": 1}, + "created_at": "2025-01-02T00:00:00Z" + } + ], + "next_page": "cursor_abc", + "request_id": "req-002" + }, + "environment_response": { + "id": "env_001", + "type": "environment", + "name": "Test Environment", + "description": "A test env", + "status": "active", + "config": { + "type": "sandbox", + "networking": {"type": "public"}, + "packages": {"pip": ["requests", "numpy"]} + }, + "scope": "organization", + "metadata": {"team": "infra"}, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T01:00:00Z", + "request_id": "req-003" + }, + "environment_list_response": { + "data": [ + { + "id": "env_001", + "name": "Env1", + "status": "active" + }, + { + "id": "env_002", + "name": "Env2", + "status": "archived" + } + ], + "next_page": null, + "request_id": "req-004" + }, + "skill_response": { + "id": "skill_001", + "type": "skill", + "name": "bash_tool", + "description": "Execute bash commands", + "source": "customer", + "status": "active", + "latest_version": "1.0", + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T01:00:00Z", + "request_id": "req-005" + }, + "skill_list_response": { + "data": [ + { + "id": "skill_001", + "type": "skill", + "name": "bash_tool", + "description": "Execute bash commands", + "source": "customer", + "status": "active", + "latest_version": "1.0", + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T01:00:00Z" + } + ], + "next_page": null, + "request_id": "req-006" + }, + "skill_version_response": { + "id": "sv_001", + "skill_id": "skill_001", + "version": "1", + "status": "active", + "created_at": "2025-01-01T00:00:00Z", + "request_id": "req-007" + }, + "delete_response": { + "id": "sess_abc123", + "type": "session_deleted", + "request_id": "req-008" + }, + "vault_response": { + "id": "vault_001", + "type": "vault", + "display_name": "Test Vault", + "metadata": {"team": "security"}, + "archived_at": null, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T01:00:00Z", + "request_id": "req-020" + }, + "vault_list_response": { + "data": [ + { + "id": "vault_001", + "type": "vault", + "display_name": "Vault One", + "created_at": "2025-01-01T00:00:00Z" + }, + { + "id": "vault_002", + "type": "vault", + "display_name": "Vault Two", + "created_at": "2025-01-02T00:00:00Z" + } + ], + "next_page": "cursor_vault", + "request_id": "req-021" + }, + "credential_response": { + "id": "cred_001", + "type": "vault_credential", + "vault_id": "vault_001", + "display_name": "Test Credential", + "auth": {"type": "environment_variable", "secret_name": "DASHSCOPE_API_KEY"}, + "metadata": {"service": "openai"}, + "archived_at": null, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T01:00:00Z", + "request_id": "req-022" + }, + "credential_list_response": { + "data": [ + { + "id": "cred_001", + "type": "vault_credential", + "vault_id": "vault_001", + "display_name": "Cred One", + "created_at": "2025-01-01T00:00:00Z" + }, + { + "id": "cred_002", + "type": "vault_credential", + "vault_id": "vault_001", + "display_name": "Cred Two", + "created_at": "2025-01-02T00:00:00Z" + } + ], + "next_page": null, + "request_id": "req-023" + }, + "file_response": { + "id": "file_001", + "type": "file", + "filename": "test.pdf", + "downloadable": true, + "mime_type": "application/pdf", + "size_bytes": 12345, + "status": "completed", + "created_at": "2025-01-01T00:00:00Z" + }, + "file_list_response": { + "data": [ + { + "id": "file_001", + "type": "file", + "filename": "test.pdf", + "mime_type": "application/pdf", + "size_bytes": 12345, + "status": "completed", + "created_at": "2025-01-01T00:00:00Z" + }, + { + "id": "file_002", + "type": "file", + "filename": "data.json", + "mime_type": "application/json", + "size_bytes": 256, + "status": "completed", + "created_at": "2025-01-02T00:00:00Z" + } + ], + "next_page": "cursor_file", + "request_id": "req-030" + }, + "session_event_send_response": { + "id": "evt_send_001", + "type": "event", + "status": "completed", + "request_id": "req-031" + }, + "event_list_response": { + "data": [ + { + "id": "evt_1", + "object": "event", + "type": "message", + "status": "completed", + "role": "user", + "content": [{"type": "text", "text": "hello"}], + "sequence_number": 1, + "created_at": "2025-01-01T00:00:00Z" + }, + { + "id": "evt_2", + "object": "event", + "type": "message", + "status": "completed", + "role": "assistant", + "content": [{"type": "text", "text": "hi there"}], + "sequence_number": 2, + "created_at": "2025-01-01T00:00:01Z" + } + ], + "next_page": null, + "request_id": "req-009" + } +} From aba4fa2089aa9ee9ec97e8f23b8fd7d58a6fd9f3 Mon Sep 17 00:00:00 2001 From: foleydang Date: Wed, 1 Jul 2026 16:46:51 +0800 Subject: [PATCH 2/4] fix: harden AgentStudio SDK against NPEs and resource leaks - AgentStudioEventStream.extractSessionStatus: scan all content blocks instead of only the first; add isJsonNull guard - CursorPage.hasNext: guard against null currentPage and null out on exhaustion to prevent NPE on subsequent calls - Files.uploadAsync: close InputStream on failure path; mark streaming RequestBody as one-shot so OkHttp won't retry after writeTo closes the stream - SessionEvents.stream: drop per-call dispatcher/connectionPool overrides so the shared template owns the resources; AgentStudioEventStream.close no longer shuts down the shared pool (which previously killed sibling streams) - Skills.resolveFileId, Agents.updateAsync, plus 13 sibling create/update/ list methods across Vaults/Environments/Sessions/Agents/Skills: return InputRequiredException on null param instead of letting it NPE inside withApiKey/toQueryString Co-Authored-By: Claude Opus 4.7 --- .../agentstudio/pagination/CursorPage.java | 8 ++++++- .../resource/AgentStudioEventStream.java | 22 ++++++++----------- .../agentstudio/resource/Agents.java | 9 ++++++++ .../agentstudio/resource/Environments.java | 9 ++++++++ .../dashscope/agentstudio/resource/Files.java | 18 ++++++++++++++- .../agentstudio/resource/SessionEvents.java | 9 +------- .../agentstudio/resource/Sessions.java | 9 ++++++++ .../agentstudio/resource/Skills.java | 6 +++++ .../agentstudio/resource/Vaults.java | 18 +++++++++++++++ 9 files changed, 85 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/pagination/CursorPage.java b/src/main/java/com/alibaba/dashscope/agentstudio/pagination/CursorPage.java index d09c5bd..4e875ad 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/pagination/CursorPage.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/pagination/CursorPage.java @@ -42,15 +42,21 @@ public Iterator iterator() { @Override public boolean hasNext() { + if (currentPage == null) { + return false; + } while (true) { if (currentPage.data != null && index < currentPage.data.size()) { return true; } if (!currentPage.hasNext()) { + currentPage = null; return false; } currentPage = currentPage.getNext().join(); - if (currentPage == null) return false; + if (currentPage == null) { + return false; + } index = 0; } } diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/resource/AgentStudioEventStream.java b/src/main/java/com/alibaba/dashscope/agentstudio/resource/AgentStudioEventStream.java index 56c83e0..8b75d6f 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/resource/AgentStudioEventStream.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/resource/AgentStudioEventStream.java @@ -34,12 +34,10 @@ public class AgentStudioEventStream implements Iterable, Closeable { private final BlockingQueue queue = new LinkedBlockingQueue<>(); private final AtomicBoolean closed = new AtomicBoolean(false); - private final OkHttpClient client; private final EventSource eventSource; private final long timeoutMs; public AgentStudioEventStream(OkHttpClient client, Request request, long timeoutMs) { - this.client = client; this.timeoutMs = timeoutMs; EventSource.Factory factory = EventSources.createFactory(client); this.eventSource = @@ -158,11 +156,6 @@ public TextStream textStream() { public void close() { if (closed.compareAndSet(false, true)) { eventSource.cancel(); - // Each stream owns its own OkHttpClient (constructed in SessionEvents.stream), so it is safe - // to release both the dispatcher and the connection pool here without affecting sibling - // streams. - client.dispatcher().executorService().shutdown(); - client.connectionPool().evictAll(); } } @@ -229,12 +222,15 @@ public String next() { } private static String extractSessionStatus(Message msg) { - if (msg.getContent() != null && !msg.getContent().isEmpty()) { - ContentBlock block = msg.getContent().get(0); - if (block instanceof ContentBlock.DataContent) { - ContentBlock.DataContent dataBlock = (ContentBlock.DataContent) block; - if (dataBlock.getData() != null && dataBlock.getData().has("session_status")) { - return dataBlock.getData().get("session_status").getAsString(); + if (msg.getContent() != null) { + for (ContentBlock block : msg.getContent()) { + if (block instanceof ContentBlock.DataContent) { + ContentBlock.DataContent dataBlock = (ContentBlock.DataContent) block; + if (dataBlock.getData() != null + && dataBlock.getData().has("session_status") + && !dataBlock.getData().get("session_status").isJsonNull()) { + return dataBlock.getData().get("session_status").getAsString(); + } } } } diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/resource/Agents.java b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Agents.java index 123c241..63c1a5e 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/resource/Agents.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Agents.java @@ -72,6 +72,9 @@ public CursorPage listVersions(String agentId, AgentListParam para } public CompletableFuture createAsync(AgentCreateParam param) { + if (param == null) { + return AsyncHelper.failedFuture(new InputRequiredException("param is required!")); + } GeneralServiceOption opt = AgentStudioConstants.newServiceOption(HttpMethod.POST, "agents", baseUrl); return AsyncHelper.callAsync(api, AgentStudioConstants.withApiKey(apiKey, param), opt) @@ -116,6 +119,9 @@ public CompletableFuture updateAsync(String agentId, AgentUpdateParam par if (agentId == null || agentId.isEmpty()) { return AsyncHelper.failedFuture(new InputRequiredException("agentId is required!")); } + if (param == null) { + return AsyncHelper.failedFuture(new InputRequiredException("param is required!")); + } try { param.validate(); } catch (InputRequiredException e) { @@ -129,6 +135,9 @@ public CompletableFuture updateAsync(String agentId, AgentUpdateParam par } public CompletableFuture> listAsync(AgentListParam param) { + if (param == null) { + return AsyncHelper.failedFuture(new InputRequiredException("param is required!")); + } String query = param.toQueryString(); String path = query.isEmpty() ? "agents" : "agents?" + query; GeneralServiceOption opt = AgentStudioConstants.newServiceOption(HttpMethod.GET, path, baseUrl); diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/resource/Environments.java b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Environments.java index 175f2b5..ec7e3f4 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/resource/Environments.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Environments.java @@ -68,6 +68,9 @@ public AgentStudioDeletionStatus delete( } public CompletableFuture createAsync(EnvironmentCreateParam param) { + if (param == null) { + return AsyncHelper.failedFuture(new InputRequiredException("param is required!")); + } GeneralServiceOption opt = AgentStudioConstants.newServiceOption(HttpMethod.POST, "environments", baseUrl); return AsyncHelper.callAsync(api, AgentStudioConstants.withApiKey(apiKey, param), opt) @@ -102,6 +105,9 @@ public CompletableFuture updateAsync( if (environmentId == null || environmentId.isEmpty()) { return AsyncHelper.failedFuture(new InputRequiredException("environmentId is required!")); } + if (param == null) { + return AsyncHelper.failedFuture(new InputRequiredException("param is required!")); + } GeneralServiceOption opt = AgentStudioConstants.newServiceOption( HttpMethod.POST, StringUtils.format("environments/%s", environmentId), baseUrl); @@ -110,6 +116,9 @@ public CompletableFuture updateAsync( } public CompletableFuture> listAsync(EnvironmentListParam param) { + if (param == null) { + return AsyncHelper.failedFuture(new InputRequiredException("param is required!")); + } String query = param.toQueryString(); String path = query.isEmpty() ? "environments" : "environments?" + query; GeneralServiceOption opt = AgentStudioConstants.newServiceOption(HttpMethod.GET, path, baseUrl); diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/resource/Files.java b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Files.java index 79d7c9e..2ce6b8a 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/resource/Files.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Files.java @@ -108,7 +108,18 @@ public CompletableFuture uploadAsync( } String mt = mimeType != null ? mimeType : "application/octet-stream"; RequestBody body = streamingBody(MediaType.parse(mt), inputStream); - return uploadRequestAsync(filename, body); + CompletableFuture future = uploadRequestAsync(filename, body); + future.whenComplete( + (r, t) -> { + if (t != null) { + try { + inputStream.close(); + } catch (IOException e) { + // ignore + } + } + }); + return future; } public CompletableFuture retrieveAsync(String fileId) { @@ -272,6 +283,11 @@ public MediaType contentType() { return mediaType; } + @Override + public boolean isOneShot() { + return true; + } + @Override public void writeTo(okio.BufferedSink sink) throws java.io.IOException { try (InputStream is = inputStream; diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/resource/SessionEvents.java b/src/main/java/com/alibaba/dashscope/agentstudio/resource/SessionEvents.java index ba4d3a6..335c953 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/resource/SessionEvents.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/resource/SessionEvents.java @@ -27,8 +27,6 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; -import okhttp3.ConnectionPool; -import okhttp3.Dispatcher; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -142,12 +140,7 @@ public AgentStudioEventStream stream(String sessionId, long timeoutMs) { String resolvedKey = resolveApiKey(); OkHttpClient client = - streamClientTemplate - .newBuilder() - .dispatcher(new Dispatcher()) - .connectionPool(new ConnectionPool()) - .readTimeout(timeoutMs, TimeUnit.MILLISECONDS) - .build(); + streamClientTemplate.newBuilder().readTimeout(timeoutMs, TimeUnit.MILLISECONDS).build(); Request request = new Request.Builder() diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/resource/Sessions.java b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Sessions.java index 5b504c7..9434459 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/resource/Sessions.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Sessions.java @@ -74,6 +74,9 @@ public AgentStudioDeletionStatus delete( } public CompletableFuture createAsync(SessionCreateParam param) { + if (param == null) { + return AsyncHelper.failedFuture(new InputRequiredException("param is required!")); + } GeneralServiceOption opt = AgentStudioConstants.newServiceOption(HttpMethod.POST, "sessions", baseUrl); return AsyncHelper.callAsync(api, AgentStudioConstants.withApiKey(apiKey, param), opt) @@ -107,6 +110,9 @@ public CompletableFuture updateAsync(String sessionId, SessionUpdatePar if (sessionId == null || sessionId.isEmpty()) { return AsyncHelper.failedFuture(new InputRequiredException("sessionId is required!")); } + if (param == null) { + return AsyncHelper.failedFuture(new InputRequiredException("param is required!")); + } GeneralServiceOption opt = AgentStudioConstants.newServiceOption( HttpMethod.POST, StringUtils.format("sessions/%s", sessionId), baseUrl); @@ -115,6 +121,9 @@ public CompletableFuture updateAsync(String sessionId, SessionUpdatePar } public CompletableFuture> listAsync(SessionListParam param) { + if (param == null) { + return AsyncHelper.failedFuture(new InputRequiredException("param is required!")); + } String query = param.toQueryString(); String path = query.isEmpty() ? "sessions" : "sessions?" + query; GeneralServiceOption opt = AgentStudioConstants.newServiceOption(HttpMethod.GET, path, baseUrl); diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/resource/Skills.java b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Skills.java index d5e21c2..2eee7c0 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/resource/Skills.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Skills.java @@ -40,6 +40,9 @@ public Skills(String baseUrl, ConnectionOptions connectionOptions, String apiKey } private CompletableFuture resolveFileId(SkillCreateParam param) { + if (param == null) { + return AsyncHelper.failedFuture(new InputRequiredException("param is required!")); + } if (param.getFileId() != null) { return CompletableFuture.completedFuture(param.getFileId()); } @@ -133,6 +136,9 @@ public CompletableFuture retrieveAsync( } public CompletableFuture> listAsync(SkillListParam param) { + if (param == null) { + return AsyncHelper.failedFuture(new InputRequiredException("param is required!")); + } String query = param.toQueryString(); String path = query.isEmpty() ? "skills" : "skills?" + query; GeneralServiceOption opt = AgentStudioConstants.newServiceOption(HttpMethod.GET, path, baseUrl); diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/resource/Vaults.java b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Vaults.java index 47eb13f..a8c7008 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/resource/Vaults.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Vaults.java @@ -78,6 +78,9 @@ public AgentStudioDeletionStatus delete( } public CompletableFuture createAsync(VaultCreateParam param) { + if (param == null) { + return AsyncHelper.failedFuture(new InputRequiredException("param is required!")); + } GeneralServiceOption opt = AgentStudioConstants.newServiceOption(HttpMethod.POST, "vaults", baseUrl); return AsyncHelper.callAsync(api, AgentStudioConstants.withApiKey(apiKey, param), opt) @@ -111,6 +114,9 @@ public CompletableFuture updateAsync(String vaultId, VaultUpdateParam par if (vaultId == null || vaultId.isEmpty()) { return AsyncHelper.failedFuture(new InputRequiredException("vaultId is required!")); } + if (param == null) { + return AsyncHelper.failedFuture(new InputRequiredException("param is required!")); + } GeneralServiceOption opt = AgentStudioConstants.newServiceOption( HttpMethod.POST, StringUtils.format("vaults/%s", vaultId), baseUrl); @@ -119,6 +125,9 @@ public CompletableFuture updateAsync(String vaultId, VaultUpdateParam par } public CompletableFuture> listAsync(VaultListParam param) { + if (param == null) { + return AsyncHelper.failedFuture(new InputRequiredException("param is required!")); + } String query = param.toQueryString(); String path = query.isEmpty() ? "vaults" : "vaults?" + query; GeneralServiceOption opt = AgentStudioConstants.newServiceOption(HttpMethod.GET, path, baseUrl); @@ -216,6 +225,9 @@ public CompletableFuture createAsync(String vaultId, CredentialCreat if (vaultId == null || vaultId.isEmpty()) { return AsyncHelper.failedFuture(new InputRequiredException("vaultId is required!")); } + if (param == null) { + return AsyncHelper.failedFuture(new InputRequiredException("param is required!")); + } GeneralServiceOption opt = AgentStudioConstants.newServiceOption( HttpMethod.POST, StringUtils.format("vaults/%s/credentials", vaultId), baseUrl); @@ -255,6 +267,9 @@ public CompletableFuture updateAsync( return AsyncHelper.failedFuture( new InputRequiredException("vaultId and credentialId are required!")); } + if (param == null) { + return AsyncHelper.failedFuture(new InputRequiredException("param is required!")); + } GeneralServiceOption opt = AgentStudioConstants.newServiceOption( HttpMethod.POST, @@ -269,6 +284,9 @@ public CompletableFuture> listAsync( if (vaultId == null || vaultId.isEmpty()) { return AsyncHelper.failedFuture(new InputRequiredException("vaultId is required!")); } + if (param == null) { + return AsyncHelper.failedFuture(new InputRequiredException("param is required!")); + } String query = param.toQueryString(); String path = StringUtils.format("vaults/%s/credentials", vaultId); if (!query.isEmpty()) { From bf83d62fb4c819a19078705356985db7cd278cad Mon Sep 17 00:00:00 2001 From: foleydang Date: Wed, 1 Jul 2026 19:24:26 +0800 Subject: [PATCH 3/4] fix: align SDK params with server API and fix sample stop_reason type - Remove afterId/beforeId from FileListParam (server only accepts page) - Remove keyword from VaultListParam (server does not accept it) - Add resources field to SessionCreateParam for file mounting - Fix AgentStudioSimple sample: getStopReason() returns StopReason, not JsonObject Co-Authored-By: Claude Opus 4.6 --- samples/AgentStudioSimple.java | 5 ++--- .../alibaba/dashscope/agentstudio/param/FileListParam.java | 4 ---- .../dashscope/agentstudio/param/SessionCreateParam.java | 5 +++++ .../alibaba/dashscope/agentstudio/param/VaultListParam.java | 2 -- .../com/alibaba/dashscope/agentstudio/resource/Files.java | 2 -- .../com/alibaba/dashscope/agentstudio/resource/Vaults.java | 1 - src/test/java/com/alibaba/dashscope/TestAgentStudio.java | 6 +++--- 7 files changed, 10 insertions(+), 15 deletions(-) diff --git a/samples/AgentStudioSimple.java b/samples/AgentStudioSimple.java index 6f8ad3e..25a243b 100644 --- a/samples/AgentStudioSimple.java +++ b/samples/AgentStudioSimple.java @@ -7,7 +7,6 @@ import com.alibaba.dashscope.agentstudio.param.AgentCreateParam; import com.alibaba.dashscope.agentstudio.param.SessionCreateParam; import com.alibaba.dashscope.agentstudio.resource.AgentStudioEventStream; -import com.google.gson.JsonObject; import java.util.Collections; /** @@ -69,9 +68,9 @@ public static void main(String[] args) throws Exception { } } } else if ("session_status".equals(event.getType())) { - JsonObject stopReason = event.getStopReason(); + Session.StopReason stopReason = event.getStopReason(); if (stopReason != null) { - System.out.println("\nstop_reason: " + stopReason.get("type")); + System.out.println("\nstop_reason: " + stopReason.getType()); } break; } diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/param/FileListParam.java b/src/main/java/com/alibaba/dashscope/agentstudio/param/FileListParam.java index 64d4935..ed675f2 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/param/FileListParam.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/param/FileListParam.java @@ -16,8 +16,6 @@ public class FileListParam extends FlattenHalfDuplexParamBase { @Default private Integer limit = null; @Default private String page = null; @Default private String scopeId = null; - @Default private String afterId = null; - @Default private String beforeId = null; @Override public JsonObject getHttpBody() { @@ -29,8 +27,6 @@ public String toQueryString() { AgentStudioConstants.appendParam(sb, "limit", limit); AgentStudioConstants.appendParam(sb, "page", page); AgentStudioConstants.appendParam(sb, "scope_id", scopeId); - AgentStudioConstants.appendParam(sb, "after_id", afterId); - AgentStudioConstants.appendParam(sb, "before_id", beforeId); return sb.toString(); } } diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionCreateParam.java b/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionCreateParam.java index 520994a..3de391d 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionCreateParam.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/param/SessionCreateParam.java @@ -4,6 +4,7 @@ import com.alibaba.dashscope.base.FlattenHalfDuplexParamBase; import com.alibaba.dashscope.utils.JsonUtils; import com.google.gson.JsonObject; +import java.util.List; import java.util.Map; import lombok.Builder.Default; import lombok.Data; @@ -18,6 +19,7 @@ public class SessionCreateParam extends FlattenHalfDuplexParamBase { @NonNull private String agent; @Default private String environmentId = null; @Default private String title = null; + @Default private List> resources = null; @Default private Map metadata = null; @Override @@ -30,6 +32,9 @@ public JsonObject getHttpBody() { if (title != null) { body.addProperty("title", title); } + if (resources != null && !resources.isEmpty()) { + body.add("resources", JsonUtils.toJsonElement(resources)); + } if (metadata != null && !metadata.isEmpty()) { body.add("metadata", JsonUtils.toJsonElement(metadata)); } diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/param/VaultListParam.java b/src/main/java/com/alibaba/dashscope/agentstudio/param/VaultListParam.java index 7f92b11..1254ce2 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/param/VaultListParam.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/param/VaultListParam.java @@ -16,7 +16,6 @@ public class VaultListParam extends FlattenHalfDuplexParamBase { @Default private Integer limit = null; @Default private String page = null; @Default private Boolean includeArchived = null; - @Default private String keyword = null; @Override public JsonObject getHttpBody() { @@ -28,7 +27,6 @@ public String toQueryString() { AgentStudioConstants.appendParam(sb, "limit", limit); AgentStudioConstants.appendParam(sb, "page", page); AgentStudioConstants.appendParam(sb, "include_archived", includeArchived); - AgentStudioConstants.appendParam(sb, "keyword", keyword); return sb.toString(); } } diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/resource/Files.java b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Files.java index 2ce6b8a..811f14d 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/resource/Files.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Files.java @@ -161,8 +161,6 @@ public CompletableFuture> listAsync(FileListParam pa FileListParam.builder() .limit(param.getLimit()) .scopeId(param.getScopeId()) - .afterId(param.getAfterId()) - .beforeId(param.getBeforeId()) .page(cursor) .build())); return page; diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/resource/Vaults.java b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Vaults.java index a8c7008..03ab2e7 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/resource/Vaults.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Vaults.java @@ -143,7 +143,6 @@ public CompletableFuture> listAsync(VaultListParam param) { VaultListParam.builder() .limit(param.getLimit()) .includeArchived(param.getIncludeArchived()) - .keyword(param.getKeyword()) .page(cursor) .build())); return page; diff --git a/src/test/java/com/alibaba/dashscope/TestAgentStudio.java b/src/test/java/com/alibaba/dashscope/TestAgentStudio.java index 0eb6251..69a820f 100644 --- a/src/test/java/com/alibaba/dashscope/TestAgentStudio.java +++ b/src/test/java/com/alibaba/dashscope/TestAgentStudio.java @@ -743,9 +743,9 @@ public void testCredentialRetrieveRequiresIds() { } @Test - public void testVaultListWithKeyword() { - String qs = VaultListParam.builder().keyword("my-vault").limit(10).build().toQueryString(); - assertTrue(qs.contains("keyword=my-vault")); + public void testVaultListWithIncludeArchived() { + String qs = VaultListParam.builder().includeArchived(true).limit(10).build().toQueryString(); + assertTrue(qs.contains("include_archived=true")); assertTrue(qs.contains("limit=10")); } From b9a89db0cc417a6d2ff9031be4befbdaf2bce185 Mon Sep 17 00:00:00 2001 From: foleydang Date: Wed, 1 Jul 2026 20:24:30 +0800 Subject: [PATCH 4/4] fix: add null check for list param in Files and SessionEvents Files.listAsync and SessionEvents.listAsync called param.toQueryString() without checking if param was null, causing NPEs. All other listAsync methods already had this guard. Co-Authored-By: Claude Opus 4.6 --- .../java/com/alibaba/dashscope/agentstudio/resource/Files.java | 3 +++ .../alibaba/dashscope/agentstudio/resource/SessionEvents.java | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/resource/Files.java b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Files.java index 811f14d..8397a0e 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/resource/Files.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Files.java @@ -146,6 +146,9 @@ public CompletableFuture retrieveAsync( } public CompletableFuture> listAsync(FileListParam param) { + if (param == null) { + return AsyncHelper.failedFuture(new InputRequiredException("param is required!")); + } String query = param.toQueryString(); String path = query.isEmpty() ? "files" : "files?" + query; GeneralServiceOption opt = AgentStudioConstants.newServiceOption(HttpMethod.GET, path, baseUrl); diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/resource/SessionEvents.java b/src/main/java/com/alibaba/dashscope/agentstudio/resource/SessionEvents.java index 335c953..cb3efef 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/resource/SessionEvents.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/resource/SessionEvents.java @@ -98,6 +98,9 @@ public CompletableFuture> listAsync( if (sessionId == null || sessionId.isEmpty()) { return AsyncHelper.failedFuture(new InputRequiredException("sessionId is required!")); } + if (param == null) { + return AsyncHelper.failedFuture(new InputRequiredException("param is required!")); + } String query = param.toQueryString(); String path = StringUtils.format("sessions/%s/events", sessionId); GeneralServiceOption opt =