diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9e2b506 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,36 @@ +ARG BASE_IMAGE=console.e97-dev07.inspirecloud.io/codex/codex:0.1 + +FROM ${BASE_IMAGE} AS build + +USER root +WORKDIR /opt/veadk-java +RUN apt-get update \ + && apt-get install -y --no-install-recommends openjdk-17-jdk-headless maven \ + && rm -rf /var/lib/apt/lists/* +COPY . . +RUN --mount=type=cache,target=/root/.m2 mvn -q -DskipTests -Dmaven.javadoc.skip=true install \ + && mvn -q -pl example dependency:copy-dependencies -DoutputDirectory=target/dependency + +FROM ${BASE_IMAGE} + +USER root +RUN apt-get update \ + && apt-get install -y --no-install-recommends openjdk-17-jre-headless \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /opt/veadk-java +COPY --from=build /opt/veadk-java/core/target/classes core/target/classes +COPY --from=build /opt/veadk-java/example/target/classes example/target/classes +COPY --from=build /opt/veadk-java/example/target/dependency example/target/dependency +COPY entrypoint.sh /app/entrypoint.sh +RUN mkdir -p /opt/application \ + && ln -sf /app/entrypoint.sh /opt/application/run.sh \ + && chmod 755 /app/entrypoint.sh \ + && chown -R codex:codex /opt/veadk-java /app/entrypoint.sh /opt/application + +EXPOSE 8000 + +USER codex +HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ + CMD python3 -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/list-apps', timeout=4)" || exit 1 +ENTRYPOINT ["/opt/application/run.sh"] diff --git a/README.md b/README.md index 42354b7..60842f5 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,36 @@ Start command: - Access URL: `http://localhost:8000` +### RunCode Sandbox Example + +`RunCodeAgent` exposes a separate `run_code_agent` application and registers only the +`run_code` tool. In AgentKit Runtime, bind a Sandbox tool to the Runtime so the platform injects +`AGENTKIT_TOOL_ID` and mounts its IAM role credential file. + +For local or public-cloud execution, configure long-lived credentials explicitly: + +```bash +export AGENTKIT_TOOL_ID="" +export VOLCENGINE_ACCESS_KEY="" +export VOLCENGINE_SECRET_KEY="" +``` + +For hybrid cloud, the Runtime normally injects these values automatically: + +```text +AGENTKIT_TOOL_ID +AGENTKIT_TOOL_HOST +AGENTKIT_TOOL_REGION +FAAS_IAM_ROLE_CREDENTIAL_PATH +``` + +Hybrid-cloud tool hosts on port `8711` (or under `.vestack.cloud`) default to HTTP. Set +`AGENTKIT_TOOL_SCHEME` explicitly if the environment uses a different scheme. The IAM credential +file is read for every tool call so refreshed STS credentials are used. + +After starting the ADK web server, create a session for `run_code_agent` and ask it to execute +code, for example: `Use Python and the run_code tool to calculate the 100th Fibonacci number.` + ### Run in IDE - Import the Maven multi-module project using IntelliJ IDEA or Eclipse. - Directly run the `main` method of `AgentCliRunner` or `AdkWeb`. diff --git a/core/src/main/java/com/volcengine/veadk/integration/agentkit/AgentKitCredential.java b/core/src/main/java/com/volcengine/veadk/integration/agentkit/AgentKitCredential.java new file mode 100644 index 0000000..1802eb7 --- /dev/null +++ b/core/src/main/java/com/volcengine/veadk/integration/agentkit/AgentKitCredential.java @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.volcengine.veadk.integration.agentkit; + +import com.fasterxml.jackson.databind.JsonNode; +import com.volcengine.veadk.utils.JSONUtil; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import org.apache.commons.lang3.StringUtils; + +/** Credentials used to sign AgentKit tool requests. */ +public record AgentKitCredential(String accessKey, String secretKey, String sessionToken) { + + private static final String ACCESS_KEY_ENV = "VOLCENGINE_ACCESS_KEY"; + private static final String SECRET_KEY_ENV = "VOLCENGINE_SECRET_KEY"; + private static final String SESSION_TOKEN_ENV = "VOLCENGINE_SESSION_TOKEN"; + private static final List CREDENTIAL_PATH_ENVS = + List.of( + "FAAS_IAM_ROLE_CREDENTIAL_PATH", + "BYTEFAAS_IAM_ROLE_CREDENTIAL_PATH", + "RUNTIME_IAM_ROLE_CREDENTIAL_PATH"); + private static final List DEFAULT_CREDENTIAL_PATHS = + List.of( + Path.of("/var/run/secrets/iam/credential"), + Path.of("/var/run/secrets/faas/iam_role_credential"), + Path.of("/app/.faas/iam_role_credential")); + + public static AgentKitCredential load() { + String accessKey = System.getenv(ACCESS_KEY_ENV); + String secretKey = System.getenv(SECRET_KEY_ENV); + if (StringUtils.isNotBlank(accessKey) && StringUtils.isNotBlank(secretKey)) { + return new AgentKitCredential(accessKey, secretKey, System.getenv(SESSION_TOKEN_ENV)); + } + + Path credentialPath = findCredentialPath(); + if (credentialPath == null) { + throw new IllegalStateException( + "AgentKit credentials are missing. Configure VOLCENGINE_ACCESS_KEY and " + + "VOLCENGINE_SECRET_KEY, or mount a FaaS IAM role credential file."); + } + return load(credentialPath); + } + + static AgentKitCredential load(Path credentialPath) { + try { + JsonNode credential = JSONUtil.parseJson(Files.readString(credentialPath)); + String accessKey = requiredText(credential, "access_key_id", credentialPath); + String secretKey = requiredText(credential, "secret_access_key", credentialPath); + String sessionToken = credential.path("session_token").asText(null); + return new AgentKitCredential(accessKey, secretKey, sessionToken); + } catch (IOException e) { + throw new IllegalStateException( + "Failed to read AgentKit credential file: " + credentialPath, e); + } + } + + private static Path findCredentialPath() { + for (String envName : CREDENTIAL_PATH_ENVS) { + String configuredPath = System.getenv(envName); + if (StringUtils.isNotBlank(configuredPath)) { + Path path = Path.of(configuredPath); + if (Files.isRegularFile(path)) { + return path; + } + } + } + return DEFAULT_CREDENTIAL_PATHS.stream() + .filter(Files::isRegularFile) + .findFirst() + .orElse(null); + } + + private static String requiredText(JsonNode credential, String field, Path credentialPath) { + String value = credential.path(field).asText(null); + if (StringUtils.isBlank(value)) { + throw new IllegalStateException( + "Credential field '" + field + "' is missing in " + credentialPath); + } + return value; + } +} diff --git a/core/src/main/java/com/volcengine/veadk/integration/agentkit/AgentKitWrapper.java b/core/src/main/java/com/volcengine/veadk/integration/agentkit/AgentKitWrapper.java index 7803d70..d764876 100644 --- a/core/src/main/java/com/volcengine/veadk/integration/agentkit/AgentKitWrapper.java +++ b/core/src/main/java/com/volcengine/veadk/integration/agentkit/AgentKitWrapper.java @@ -30,23 +30,24 @@ public class AgentKitWrapper extends BaseServiceImpl { private static final String ACTION_INVOKE_TOOL = "InvokeTool"; - private static final ServiceInfo SERVICE_INFO = - new ServiceInfo( - new HashMap() { - { - put(Const.CONNECTION_TIMEOUT, 5000); - put(Const.SOCKET_TIMEOUT, 30000); // Sandbox might be slow - put(Const.Scheme, "https"); - put( - Const.Header, - new ArrayList
() { - { - add(new BasicHeader("Accept", "application/json")); - } - }); - put(Const.Credentials, new Credentials("cn-beijing", "agentkit")); - } - }); + private static ServiceInfo createServiceInfo(String scheme) { + return new ServiceInfo( + new HashMap() { + { + put(Const.CONNECTION_TIMEOUT, 5000); + put(Const.SOCKET_TIMEOUT, 30000); // Sandbox might be slow + put(Const.Scheme, scheme); + put( + Const.Header, + new ArrayList
() { + { + add(new BasicHeader("Accept", "application/json")); + } + }); + put(Const.Credentials, new Credentials("cn-beijing", "agentkit")); + } + }); + } private static final Map API_INFO_LIST = new HashMap() { @@ -77,9 +78,17 @@ public class AgentKitWrapper extends BaseServiceImpl { new BasicNameValuePair("Version", "2025-10-30")); public AgentKitWrapper(String host, String region, String ak, String sk) { - super(SERVICE_INFO, API_INFO_LIST); + this("https", host, region, ak, sk, null); + } + + public AgentKitWrapper( + String scheme, String host, String region, String ak, String sk, String sessionToken) { + super(createServiceInfo(scheme), API_INFO_LIST); this.setAccessKey(ak); this.setSecretKey(sk); + if (sessionToken != null && !sessionToken.isBlank()) { + this.setSessionToken(sessionToken); + } this.setHost(host); this.getServiceInfo().setHost(host); this.setRegion(region); @@ -109,24 +118,28 @@ public String runCode( RawResponse response = json(ACTION_INVOKE_TOOL, INVOKETOOL_PARAMS, bodyStr); if (response.getCode() != SdkError.SUCCESS.getNumber()) { log.error( - "InvokeTool request:{}, raw response:{}", - bodyStr, - response.getException().getMessage()); + "AgentKit InvokeTool failed: toolId={}, operationType=RunCode," + + " errorCode={}", + toolId, + response.getCode()); throw response.getException(); } - log.debug( - "InvokeTool request:{}, raw response:{}", - bodyStr, - JSONUtil.parseJson(response.getData())); // Parse response to get "Result" JsonNode rootNode = JSONUtil.parseJson(response.getData()); + String requestId = rootNode.path("ResponseMetadata").path("RequestId").asText(""); + log.debug( + "AgentKit InvokeTool succeeded: toolId={}, operationType=RunCode, requestId={}", + toolId, + requestId); JsonNode resultNode = rootNode.path("Result").path("Result"); if (!resultNode.isMissingNode()) { return resultNode.asText(); } - return rootNode.toString(); + throw new IllegalStateException( + "AgentKit InvokeTool response is missing Result.Result, requestId=" + + requestId); } catch (Exception e) { throw new RuntimeException("Failed to run code via AgentKit", e); } diff --git a/core/src/main/java/com/volcengine/veadk/model/ArkLlm.java b/core/src/main/java/com/volcengine/veadk/model/ArkLlm.java index c9d8196..646b482 100644 --- a/core/src/main/java/com/volcengine/veadk/model/ArkLlm.java +++ b/core/src/main/java/com/volcengine/veadk/model/ArkLlm.java @@ -70,6 +70,7 @@ public final class ArkLlm extends BaseLlm { .build(); private final ArkService arkService; + private final OpenAiCompatibleChatService compatibleChatService; private ChatCompletionRequest.ChatCompletionRequestThinking thinking = null; public ArkLlm(String modelName) { @@ -79,7 +80,15 @@ public ArkLlm(String modelName) { public ArkLlm(String modelName, String thinking) { super(modelName); Objects.requireNonNull(modelName, "modelName must be set."); - this.arkService = ArkService.builder().apiKey(EnvUtil.getAgentApiKey()).build(); + String apiBase = EnvUtil.getAgentApiBase(); + if (StringUtils.isNotBlank(apiBase)) { + this.arkService = null; + this.compatibleChatService = + new OpenAiCompatibleChatService(apiBase, EnvUtil.getAgentApiKey()); + } else { + this.arkService = ArkService.builder().apiKey(EnvUtil.getAgentApiKey()).build(); + this.compatibleChatService = null; + } if (StringUtils.isNotBlank(thinking)) { this.thinking = new ChatCompletionRequest.ChatCompletionRequestThinking(thinking); } @@ -103,11 +112,18 @@ public Flowable generateContent(LlmRequest llmRequest, boolean stre } else { log.debug("Sending generateContent request to model {}", arkRequest.getModel()); // Handle non-streaming response - return Flowable.fromCallable(() -> arkService.createChatCompletion(arkRequest)) + return Flowable.fromCallable(() -> createChatCompletion(arkRequest)) .map(this::toLlmResponse); } } + private ChatCompletionResult createChatCompletion(ChatCompletionRequest arkRequest) { + if (compatibleChatService != null) { + return compatibleChatService.createChatCompletion(arkRequest); + } + return arkService.createChatCompletion(arkRequest); + } + /** * Handle streaming content generation * @param arkRequest The Ark completion request @@ -116,7 +132,9 @@ public Flowable generateContent(LlmRequest llmRequest, boolean stre private Flowable generateContentStreaming(ChatCompletionRequest arkRequest) { // Get streaming response from Ark service io.reactivex.Flowable streamResponse = - arkService.streamChatCompletion(arkRequest); + compatibleChatService != null + ? compatibleChatService.streamChatCompletion(arkRequest) + : arkService.streamChatCompletion(arkRequest); return Flowable.defer( () -> { diff --git a/core/src/main/java/com/volcengine/veadk/model/OpenAiCompatibleChatService.java b/core/src/main/java/com/volcengine/veadk/model/OpenAiCompatibleChatService.java new file mode 100644 index 0000000..1411e99 --- /dev/null +++ b/core/src/main/java/com/volcengine/veadk/model/OpenAiCompatibleChatService.java @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.volcengine.veadk.model; + +import com.volcengine.ark.runtime.model.completion.chat.ChatCompletionChunk; +import com.volcengine.ark.runtime.model.completion.chat.ChatCompletionRequest; +import com.volcengine.ark.runtime.model.completion.chat.ChatCompletionResult; +import com.volcengine.ark.runtime.service.ArkBaseService; +import com.volcengine.ark.runtime.service.ArkService; +import io.reactivex.Single; +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.Retrofit; +import retrofit2.http.Body; +import retrofit2.http.POST; +import retrofit2.http.Streaming; + +/** + * Chat Completions transport for OpenAI-compatible API bases. + * + *

The Ark Java SDK version used by VeADK hard-codes {@code /api/v3/chat/completions}. AgentKit + * ModelCenter, like the OpenAI client used by VeADK Python, expects the configured API base to be + * combined with the relative {@code chat/completions} resource path. + */ +final class OpenAiCompatibleChatService { + + private final ChatApi api; + + OpenAiCompatibleChatService(String apiBase, String apiKey) { + Retrofit retrofit = + ArkService.defaultRetrofit( + ArkService.defaultApiKeyClient(apiKey, ArkBaseService.DEFAULT_TIMEOUT), + ArkService.defaultObjectMapper(), + normalizeApiBase(apiBase), + null); + this.api = retrofit.create(ChatApi.class); + } + + ChatCompletionResult createChatCompletion(ChatCompletionRequest request) { + return ArkService.execute(api.createChatCompletion(request)); + } + + io.reactivex.Flowable streamChatCompletion(ChatCompletionRequest request) { + request.setStream(true); + return ArkService.stream( + api.createChatCompletionStream(request), ChatCompletionChunk.class); + } + + static String normalizeApiBase(String apiBase) { + String normalized = apiBase.trim(); + return normalized.endsWith("/") ? normalized : normalized + "/"; + } + + private interface ChatApi { + + @POST("chat/completions") + Single createChatCompletion(@Body ChatCompletionRequest request); + + @Streaming + @POST("chat/completions") + Call createChatCompletionStream(@Body ChatCompletionRequest request); + } +} diff --git a/core/src/main/java/com/volcengine/veadk/tools/sandbox/RunCodeTool.java b/core/src/main/java/com/volcengine/veadk/tools/sandbox/RunCodeTool.java index 5c6deea..d817919 100644 --- a/core/src/main/java/com/volcengine/veadk/tools/sandbox/RunCodeTool.java +++ b/core/src/main/java/com/volcengine/veadk/tools/sandbox/RunCodeTool.java @@ -6,6 +6,7 @@ import com.google.common.collect.ImmutableMap; import com.google.genai.types.FunctionDeclaration; import com.google.genai.types.Schema; +import com.volcengine.veadk.integration.agentkit.AgentKitCredential; import com.volcengine.veadk.integration.agentkit.AgentKitWrapper; import com.volcengine.veadk.utils.EnvUtil; import io.reactivex.rxjava3.core.Single; @@ -22,7 +23,6 @@ public class RunCodeTool extends BaseTool { private static final Logger logger = LoggerFactory.getLogger(RunCodeTool.class); - private final AgentKitWrapper agentKitWrapper; public RunCodeTool() { super( @@ -31,12 +31,6 @@ public RunCodeTool() { + " directly, compile and execute via Python; write sources and object files to" + " /tmp.", false); - this.agentKitWrapper = - new AgentKitWrapper( - EnvUtil.getAgentKitHost(), - EnvUtil.getAgentKitRegion(), - EnvUtil.getAccessKey(), - EnvUtil.getSecretKey()); } @Override @@ -102,6 +96,15 @@ private Map execute( try { String toolId = EnvUtil.getAgentKitToolId(); + AgentKitCredential credential = AgentKitCredential.load(); + AgentKitWrapper agentKitWrapper = + new AgentKitWrapper( + EnvUtil.getAgentKitScheme(), + EnvUtil.getAgentKitHost(), + EnvUtil.getAgentKitRegion(), + credential.accessKey(), + credential.secretKey(), + credential.sessionToken()); String output = agentKitWrapper.runCode(toolId, sessionId, code, language, timeout); return ImmutableMap.of("result", output); } catch (Exception e) { diff --git a/core/src/main/java/com/volcengine/veadk/utils/EnvUtil.java b/core/src/main/java/com/volcengine/veadk/utils/EnvUtil.java index 9b08309..6c80ef7 100644 --- a/core/src/main/java/com/volcengine/veadk/utils/EnvUtil.java +++ b/core/src/main/java/com/volcengine/veadk/utils/EnvUtil.java @@ -27,8 +27,10 @@ public class EnvUtil { private static final String TLS_REGION = "OBSERVABILITY_OPENTELEMETRY_TLS_REGION"; private static final String VIKINGMEM_MEMORY_TYPE = "DATABASE_VIKINGMEM_MEMORY_TYPE"; private static final String MODEL_AGENT_API_KEY = "MODEL_AGENT_API_KEY"; + private static final String MODEL_AGENT_API_BASE = "MODEL_AGENT_API_BASE"; private static final String TOOL_CODE_SANDBOX_URL = "TOOL_CODE_SANDBOX_URL"; private static final String AGENTKIT_TOOL_ID = "AGENTKIT_TOOL_ID"; + private static final String AGENTKIT_TOOL_SCHEME = "AGENTKIT_TOOL_SCHEME"; private static final String AGENTKIT_TOOL_SERVICE = "AGENTKIT_TOOL_SERVICE_CODE"; private static final String AGENTKIT_TOOL_REGION = "AGENTKIT_TOOL_REGION"; private static final String AGENTKIT_TOOL_HOST = "AGENTKIT_TOOL_HOST"; @@ -39,6 +41,7 @@ public class EnvUtil { private static final String DEFAULT_VIKING_MEMORY_TYPE = "sys_event_v1"; private static final String DEFAULT_AGENTKIT_SERVICE = "agentkit"; private static final String DEFAULT_AGENTKIT_REGION = "cn-beijing"; + private static final String DEFAULT_AGENTKIT_SCHEME = "https"; private EnvUtil() {} @@ -68,6 +71,18 @@ public static String getAgentKitHost() { return host; } + public static String getAgentKitScheme() { + String scheme = System.getenv(AGENTKIT_TOOL_SCHEME); + if (StringUtils.isNotBlank(scheme)) { + return scheme; + } + String host = System.getenv(AGENTKIT_TOOL_HOST); + return StringUtils.isNotBlank(host) + && (host.endsWith(":8711") || host.endsWith(".vestack.cloud")) + ? "http" + : DEFAULT_AGENTKIT_SCHEME; + } + public static String getAgentApiKey() { String apiKey = System.getenv(MODEL_AGENT_API_KEY); if (StringUtils.isBlank(apiKey)) { @@ -76,6 +91,10 @@ public static String getAgentApiKey() { return apiKey; } + public static String getAgentApiBase() { + return System.getenv(MODEL_AGENT_API_BASE); + } + public static String getAccessKey() { String accessKey = System.getenv(VOLCENGINE_ACCESS_KEY); if (StringUtils.isBlank(accessKey)) { diff --git a/core/src/test/java/com/volcengine/veadk/integration/agentkit/AgentKitCredentialTest.java b/core/src/test/java/com/volcengine/veadk/integration/agentkit/AgentKitCredentialTest.java new file mode 100644 index 0000000..63b5088 --- /dev/null +++ b/core/src/test/java/com/volcengine/veadk/integration/agentkit/AgentKitCredentialTest.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.volcengine.veadk.integration.agentkit; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class AgentKitCredentialTest { + + @TempDir Path tempDir; + + @Test + void loadCredentialFile() throws Exception { + Path credentialFile = tempDir.resolve("credential"); + Files.writeString( + credentialFile, + """ + { + "access_key_id": "test-ak", + "secret_access_key": "test-sk", + "session_token": "test-token" + } + """); + + AgentKitCredential credential = AgentKitCredential.load(credentialFile); + + assertThat(credential.accessKey()).isEqualTo("test-ak"); + assertThat(credential.secretKey()).isEqualTo("test-sk"); + assertThat(credential.sessionToken()).isEqualTo("test-token"); + } +} diff --git a/core/src/test/java/com/volcengine/veadk/model/OpenAiCompatibleChatServiceTest.java b/core/src/test/java/com/volcengine/veadk/model/OpenAiCompatibleChatServiceTest.java new file mode 100644 index 0000000..0a626fb --- /dev/null +++ b/core/src/test/java/com/volcengine/veadk/model/OpenAiCompatibleChatServiceTest.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.volcengine.veadk.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class OpenAiCompatibleChatServiceTest { + + @Test + void normalizeApiBase_addsTrailingSlash() { + assertEquals( + "http://modelcenter.modelcenter:6789/", + OpenAiCompatibleChatService.normalizeApiBase( + "http://modelcenter.modelcenter:6789")); + } + + @Test + void normalizeApiBase_preservesPathAndTrailingSlash() { + assertEquals( + "https://ark.cn-beijing.volces.com/api/v3/", + OpenAiCompatibleChatService.normalizeApiBase( + " https://ark.cn-beijing.volces.com/api/v3/ ")); + } +} diff --git a/core/src/test/java/com/volcengine/veadk/tools/sandbox/RunCodeToolTest.java b/core/src/test/java/com/volcengine/veadk/tools/sandbox/RunCodeToolTest.java index 2ab8c4d..1558243 100644 --- a/core/src/test/java/com/volcengine/veadk/tools/sandbox/RunCodeToolTest.java +++ b/core/src/test/java/com/volcengine/veadk/tools/sandbox/RunCodeToolTest.java @@ -17,9 +17,12 @@ import java.util.Map; import java.util.Optional; import org.junit.jupiter.api.Test; +import org.junitpioneer.jupiter.SetEnvironmentVariable; import org.mockito.MockedConstruction; import org.mockito.MockedStatic; +@SetEnvironmentVariable(key = "VOLCENGINE_ACCESS_KEY", value = "ak") +@SetEnvironmentVariable(key = "VOLCENGINE_SECRET_KEY", value = "sk") class RunCodeToolTest { @Test @@ -117,6 +120,7 @@ private void mockEnv(MockedStatic envUtilMock) { envUtilMock.when(EnvUtil::getAgentKitService).thenReturn("agentkit"); envUtilMock.when(EnvUtil::getAgentKitRegion).thenReturn("cn-beijing"); envUtilMock.when(EnvUtil::getAgentKitHost).thenReturn("host.com"); + envUtilMock.when(EnvUtil::getAgentKitScheme).thenReturn("https"); envUtilMock.when(EnvUtil::getAccessKey).thenReturn("ak"); envUtilMock.when(EnvUtil::getSecretKey).thenReturn("sk"); } diff --git a/core/src/test/java/com/volcengine/veadk/utils/EnvUtilTest.java b/core/src/test/java/com/volcengine/veadk/utils/EnvUtilTest.java index 60abced..c8ac916 100644 --- a/core/src/test/java/com/volcengine/veadk/utils/EnvUtilTest.java +++ b/core/src/test/java/com/volcengine/veadk/utils/EnvUtilTest.java @@ -8,6 +8,19 @@ import org.junitpioneer.jupiter.SetEnvironmentVariable; class EnvUtilTest { + @Test + @SetEnvironmentVariable(key = "AGENTKIT_TOOL_HOST", value = "agentkit-tool:8711") + @ClearEnvironmentVariable(key = "AGENTKIT_TOOL_SCHEME") + void getAgentKitScheme_withPlatformHost_shouldDefaultToHttp() { + assertThat(EnvUtil.getAgentKitScheme()).isEqualTo("http"); + } + + @Test + @SetEnvironmentVariable(key = "AGENTKIT_TOOL_HOST", value = "agentkit-tool:8711") + @SetEnvironmentVariable(key = "AGENTKIT_TOOL_SCHEME", value = "https") + void getAgentKitScheme_withExplicitScheme_shouldUseConfiguredValue() { + assertThat(EnvUtil.getAgentKitScheme()).isEqualTo("https"); + } @Test @SetEnvironmentVariable(key = "MODEL_AGENT_API_KEY", value = "test_api_key") @@ -21,6 +34,18 @@ void getAgentApiKey_withMissingEnv_shouldThrowException() { assertThatThrownBy(EnvUtil::getAgentApiKey).isInstanceOf(IllegalStateException.class); } + @Test + @SetEnvironmentVariable(key = "MODEL_AGENT_API_BASE", value = "http://modelcenter:6789") + void getAgentApiBase() { + assertThat(EnvUtil.getAgentApiBase()).isEqualTo("http://modelcenter:6789"); + } + + @Test + @ClearEnvironmentVariable(key = "MODEL_AGENT_API_BASE") + void getAgentApiBase_withMissingEnv_shouldReturnNull() { + assertThat(EnvUtil.getAgentApiBase()).isNull(); + } + @Test @SetEnvironmentVariable(key = "VOLCENGINE_ACCESS_KEY", value = "test_access_key") void getAccessKey() { diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 0000000..e74edfe --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,9 @@ +#!/bin/sh +set -eu + +exec java \ + -cp 'core/target/classes:example/target/classes:example/target/dependency/*' \ + com.volcengine.veadk.example.AgentKitWeb \ + --adk.agents.source-dir=example/target \ + --server.address=0.0.0.0 \ + --server.port=8000 diff --git a/example/src/main/java/com/volcengine/veadk/example/ArkAgent.java b/example/src/main/java/com/volcengine/veadk/example/ArkAgent.java index a095445..c7cb078 100644 --- a/example/src/main/java/com/volcengine/veadk/example/ArkAgent.java +++ b/example/src/main/java/com/volcengine/veadk/example/ArkAgent.java @@ -30,11 +30,20 @@ public class ArkAgent { + private static final String appName = "ark_agent"; + private static final String defaultModelId = "doubao-seed-1-8-251228"; + private static final String modelId = resolveModelId(); + public static BaseAgent ROOT_AGENT = initAgent(); + // public static BaseAgent ROOT_AGENT = initAgentWithVeTools(); - private static final String appName = "ark_agent"; - private static final String modelId = "doubao-seed-1-8-251228"; + private static String resolveModelId() { + String configuredModel = System.getenv("MODEL_AGENT_NAME"); + return configuredModel == null || configuredModel.isBlank() + ? defaultModelId + : configuredModel; + } private static BaseAgent initAgent() { return LlmAgent.builder() @@ -45,11 +54,15 @@ private static BaseAgent initAgent() { Answer user questions to the best of your knowledge. 1. use the 'getCurrentTime' tool to query the city’s current time. 2. use the 'getWeather' tool to query the city’s current weather. + 3. use the 'run_code' tool for calculations and programming tasks when the + user asks to execute code. Do not claim execution succeeded unless the + tool returns a successful result. """) .model(new ArkLlm(modelId)) .tools( FunctionTool.create(ArkAgent.class, "getCurrentTime"), - FunctionTool.create(ArkAgent.class, "getWeather")) + FunctionTool.create(ArkAgent.class, "getWeather"), + new RunCodeTool()) .build(); } diff --git a/example/src/main/java/com/volcengine/veadk/example/RunCodeAgent.java b/example/src/main/java/com/volcengine/veadk/example/RunCodeAgent.java new file mode 100644 index 0000000..fdc92f6 --- /dev/null +++ b/example/src/main/java/com/volcengine/veadk/example/RunCodeAgent.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.volcengine.veadk.example; + +import com.google.adk.agents.BaseAgent; +import com.google.adk.agents.LlmAgent; +import com.volcengine.veadk.model.ArkLlm; +import com.volcengine.veadk.tools.sandbox.RunCodeTool; + +/** AgentKit example that executes generated Python code in the bound Sandbox tool. */ +public final class RunCodeAgent { + + private static final String DEFAULT_MODEL_ID = "doubao-seed-1-8-251228"; + private static final String MODEL_ID = resolveModelId(); + + public static final BaseAgent ROOT_AGENT = createAgent(); + + private RunCodeAgent() {} + + private static BaseAgent createAgent() { + return LlmAgent.builder() + .name("run_code_agent") + .description("A Python coding assistant backed by the AgentKit Sandbox.") + .instruction( + """ + You are a Python coding assistant. You must use the run_code tool to solve + calculation and programming tasks. Prefer Python standard libraries, show + the executed result, and do not claim success unless the tool returns it. + """) + .model(new ArkLlm(MODEL_ID)) + .tools(new RunCodeTool()) + .build(); + } + + private static String resolveModelId() { + String configuredModel = System.getenv("MODEL_AGENT_NAME"); + return configuredModel == null || configuredModel.isBlank() + ? DEFAULT_MODEL_ID + : configuredModel; + } +}