feat(boba-tea-shop): improve stability of Milk Tea DemoBoba tea shop OpenAI#464
Conversation
Change-Id: I7db4d765607b007df037c1be79a6e2f3780b40ec
Change-Id: I73da48c7ee5527fcf59696dd4203c9c9a87356f9
…iness-sub-agent Change-Id: I22703fd67d8a9a90f5a5f8eb1ac51cfefa7ecea0
There was a problem hiding this comment.
Pull request overview
This PR enhances the boba-tea-shop example by adding OpenAI model provider support alongside the existing DashScope provider, fixing a thread switching issue in the HTTP transport layer, and optimizing MCP client initialization to be lazy-loaded with error recovery.
Key Changes:
- Adds configurable multi-provider support (DashScope and OpenAI) through new
AgentScopeModelConfigclasses - Refactors model initialization from inline code to Spring Bean injection for better modularity
- Implements lazy initialization of MCP client with double-checked locking pattern and error handling in business-sub-agent
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 15 comments.
Show a summary per file
| File | Description |
|---|---|
frontend/src/base/i18n/zh.ts |
Updates Chinese localization text for example prompts |
consult-sub-agent/src/main/resources/application.yml |
Adds base-url configuration support for DashScope |
consult-sub-agent/src/main/java/.../AgentScopeRunner.java |
Refactors to inject Model bean instead of inline instantiation |
consult-sub-agent/src/main/java/.../AgentScopeModelConfig.java |
New configuration class for multi-provider model setup |
business-sub-agent/src/main/resources/application.yml |
Adds base-url configuration support for DashScope |
business-sub-agent/src/main/java/.../AgentScopeRunner.java |
Refactors model injection and adds lazy MCP client initialization |
business-sub-agent/src/main/java/.../AgentScopeModelConfig.java |
New configuration class for multi-provider model setup |
agentscope-core/src/main/java/.../JdkHttpTransport.java |
Adds publishOn scheduler to fix thread switching issues in streaming |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "Failed to initialize MCP client: " | ||
| + e.getMessage() | ||
| + " , will try later."); |
There was a problem hiding this comment.
The error message construction uses string concatenation with the '+' operator. For better readability and following Java best practices, consider using String.format() or logger's built-in parameterization: logger.warn("Failed to initialize MCP client: {}, will try later.", e.getMessage())
| "Failed to initialize MCP client: " | |
| + e.getMessage() | |
| + " , will try later."); | |
| "Failed to initialize MCP client: {}, will try later.", | |
| e.getMessage()); |
| DashScopeChatModel.Builder builder = | ||
| DashScopeChatModel.builder() | ||
| .apiKey(dashscopeApiKey) | ||
| .modelName(dashscopeModelName) | ||
| .formatter(new DashScopeChatFormatter()); | ||
| if (dashscopeBaseUrl != null | ||
| && !dashscopeBaseUrl.isEmpty() | ||
| && !dashscopeBaseUrl.equals("-")) { | ||
| builder.baseUrl(dashscopeBaseUrl); | ||
| } | ||
| return builder.build(); |
There was a problem hiding this comment.
The DashScope model configuration is missing the .stream(true) and .enableThinking(true) options that were present in the previous implementation. These options were explicitly enabled in the original code and should be preserved to maintain the same functionality and behavior.
| private void initializeMcpOnce() { | ||
| if (!mcpInitialized) { | ||
| synchronized (this) { | ||
| if (!mcpInitialized) { | ||
| try { | ||
| NacosMcpServerManager mcpServerManager = | ||
| new NacosMcpServerManager(aiService); | ||
| NacosMcpClientWrapper mcpClientWrapper = | ||
| NacosMcpClientBuilder.create( | ||
| "business-mcp-server", mcpServerManager) | ||
| .build(); | ||
| toolkit.registerMcpClient(mcpClientWrapper).block(); | ||
| mcpInitialized = true; | ||
| } catch (Exception e) { | ||
| // Log the error and continue without MCP tools | ||
| logger.warn( | ||
| "Failed to initialize MCP client: " | ||
| + e.getMessage() | ||
| + " , will try later."); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
The method initializeMcpOnce() is missing documentation. While not public, this method implements a complex lazy initialization pattern with error handling. Adding a comment or Javadoc would help explain the double-checked locking pattern and the error recovery strategy of "will try later".
| /* | ||
| * Copyright 2024-2026 the original author or authors. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package io.agentscope.examples.bobatea.business.config; | ||
|
|
||
| import io.agentscope.core.formatter.dashscope.DashScopeChatFormatter; | ||
| import io.agentscope.core.formatter.openai.OpenAIChatFormatter; | ||
| import io.agentscope.core.model.DashScopeChatModel; | ||
| import io.agentscope.core.model.Model; | ||
| import io.agentscope.core.model.OpenAIChatModel; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
|
|
||
| /** | ||
| * AgentScope Model and Formatter Configuration | ||
| * Supports both DashScope and OpenAI model providers | ||
| */ | ||
| @Configuration | ||
| public class AgentScopeModelConfig { | ||
| private static final Logger logger = LoggerFactory.getLogger(AgentScopeModelConfig.class); | ||
|
|
||
| private static final String PROVIDER_DASHSCOPE = "dashscope"; | ||
| private static final String PROVIDER_OPENAI = "openai"; | ||
|
|
||
| @Value("${agentscope.model.provider}") | ||
| private String modelProvider; | ||
|
|
||
| @Value("${agentscope.dashscope.api-key}") | ||
| private String dashscopeApiKey; | ||
|
|
||
| @Value("${agentscope.dashscope.model-name:qwen-max}") | ||
| private String dashscopeModelName; | ||
|
|
||
| @Value("${agentscope.dashscope.base-url}") | ||
| private String dashscopeBaseUrl; | ||
|
|
||
| @Value("${agentscope.openai.api-key}") | ||
| private String openaiApiKey; | ||
|
|
||
| @Value("${agentscope.openai.model-name:gpt-5}") | ||
| private String openaiModelName; | ||
|
|
||
| @Value("${agentscope.openai.base-url}") | ||
| private String openaiBaseUrl; | ||
|
|
||
| @Bean | ||
| public Model model() { | ||
| if (PROVIDER_OPENAI.equalsIgnoreCase(modelProvider)) { | ||
| logger.info( | ||
| "Creating OpenAI Model with model: {}, baseUrl: {}", | ||
| openaiModelName, | ||
| openaiBaseUrl); | ||
| OpenAIChatModel.Builder builder = | ||
| OpenAIChatModel.builder() | ||
| .apiKey(openaiApiKey) | ||
| .modelName(openaiModelName) | ||
| .stream(true) | ||
| .formatter(new OpenAIChatFormatter()); | ||
| if (openaiBaseUrl != null && !openaiBaseUrl.isEmpty() && !openaiBaseUrl.equals("-")) { | ||
| builder.baseUrl(openaiBaseUrl); | ||
| } | ||
| return builder.build(); | ||
| } else { | ||
| logger.info( | ||
| "Creating DashScope Model with model: {}, baseUrl: {}", | ||
| dashscopeModelName, | ||
| dashscopeBaseUrl); | ||
| DashScopeChatModel.Builder builder = | ||
| DashScopeChatModel.builder() | ||
| .apiKey(dashscopeApiKey) | ||
| .modelName(dashscopeModelName) | ||
| .formatter(new DashScopeChatFormatter()); | ||
| if (dashscopeBaseUrl != null | ||
| && !dashscopeBaseUrl.isEmpty() | ||
| && !dashscopeBaseUrl.equals("-")) { | ||
| builder.baseUrl(dashscopeBaseUrl); | ||
| } | ||
| return builder.build(); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
There is significant code duplication between this class and the identical AgentScopeModelConfig class in consult-sub-agent. Consider extracting this configuration into a shared module or parent configuration class to follow the DRY (Don't Repeat Yourself) principle and reduce maintenance burden.
| toolkit.registerMcpClient(mcpClientWrapper).block(); | ||
| mcpInitialized = true; | ||
| } catch (Exception e) { | ||
| // Log the error and continue without MCP tools | ||
| logger.warn( | ||
| "Failed to initialize MCP client: " | ||
| + e.getMessage() | ||
| + " , will try later."); |
There was a problem hiding this comment.
The .block() call on a reactive operation could cause thread blocking issues, especially when called during initialization. Since this is wrapped in error handling and the flag prevents re-initialization, consider using .subscribe() with proper error handling instead, or document why synchronous blocking is necessary here. This is particularly important given that the PR description mentions fixing "unexpected thread switching" issues.
| toolkit.registerMcpClient(mcpClientWrapper).block(); | |
| mcpInitialized = true; | |
| } catch (Exception e) { | |
| // Log the error and continue without MCP tools | |
| logger.warn( | |
| "Failed to initialize MCP client: " | |
| + e.getMessage() | |
| + " , will try later."); | |
| toolkit.registerMcpClient(mcpClientWrapper) | |
| .doOnSuccess(ignored -> mcpInitialized = true) | |
| .doOnError( | |
| error -> | |
| logger.warn( | |
| "Failed to initialize MCP client: " | |
| + error.getMessage() | |
| + " , will try later.", | |
| error)) | |
| .subscribe(); | |
| } catch (Exception e) { | |
| // Log the error and continue without MCP tools | |
| logger.warn( | |
| "Failed to initialize MCP client: " | |
| + e.getMessage() | |
| + " , will try later.", | |
| e); |
| logger.warn( | ||
| "Failed to initialize MCP client: " | ||
| + e.getMessage() | ||
| + " , will try later."); |
There was a problem hiding this comment.
The error message states "will try later", but there's no retry mechanism implemented. Once mcpInitialized is checked and the exception is caught, the flag remains false but initializeMcpOnce() will be called again. However, if the initialization continues to fail, this could lead to repeated failed initialization attempts on every agent creation. Consider either implementing an actual retry mechanism with backoff, or updating the error message to accurately reflect the behavior (e.g., "will retry on next agent creation").
| + " , will try later."); | |
| + " , will retry on next agent creation."); |
| if (PROVIDER_OPENAI.equalsIgnoreCase(modelProvider)) { | ||
| logger.info( | ||
| "Creating OpenAI Model with model: {}, baseUrl: {}", | ||
| openaiModelName, | ||
| openaiBaseUrl); | ||
| OpenAIChatModel.Builder builder = | ||
| OpenAIChatModel.builder() | ||
| .apiKey(openaiApiKey) | ||
| .modelName(openaiModelName) | ||
| .stream(true) | ||
| .formatter(new OpenAIChatFormatter()); | ||
| if (openaiBaseUrl != null && !openaiBaseUrl.isEmpty() && !openaiBaseUrl.equals("-")) { | ||
| builder.baseUrl(openaiBaseUrl); | ||
| } | ||
| return builder.build(); |
There was a problem hiding this comment.
The API keys are being logged when creating the model instances. While the log statement only shows the model name and baseUrl, ensure that the apiKey fields are not accidentally exposed in any logging framework's debug or trace levels. Consider reviewing logging configurations to prevent sensitive credential leakage.
| @Bean | ||
| public Model model() { | ||
| if (PROVIDER_OPENAI.equalsIgnoreCase(modelProvider)) { | ||
| logger.info( | ||
| "Creating OpenAI Model with model: {}, baseUrl: {}", | ||
| openaiModelName, | ||
| openaiBaseUrl); | ||
| OpenAIChatModel.Builder builder = | ||
| OpenAIChatModel.builder() | ||
| .apiKey(openaiApiKey) | ||
| .modelName(openaiModelName) | ||
| .stream(true) | ||
| .formatter(new OpenAIChatFormatter()); | ||
| if (openaiBaseUrl != null && !openaiBaseUrl.isEmpty() && !openaiBaseUrl.equals("-")) { | ||
| builder.baseUrl(openaiBaseUrl); | ||
| } | ||
| return builder.build(); | ||
| } else { | ||
| logger.info( | ||
| "Creating DashScope Model with model: {}, baseUrl: {}", | ||
| dashscopeModelName, | ||
| dashscopeBaseUrl); | ||
| DashScopeChatModel.Builder builder = | ||
| DashScopeChatModel.builder() | ||
| .apiKey(dashscopeApiKey) | ||
| .modelName(dashscopeModelName) | ||
| .formatter(new DashScopeChatFormatter()); | ||
| if (dashscopeBaseUrl != null | ||
| && !dashscopeBaseUrl.isEmpty() | ||
| && !dashscopeBaseUrl.equals("-")) { | ||
| builder.baseUrl(dashscopeBaseUrl); | ||
| } | ||
| return builder.build(); | ||
| } | ||
| } |
There was a problem hiding this comment.
The model() method lacks input validation for the modelProvider field. If the configuration property agentscope.model.provider is not set or has an invalid value, the code will silently fall through to the DashScope provider without any warning. Consider adding validation and logging when an unknown provider is specified to help with debugging configuration issues.
| if (PROVIDER_OPENAI.equalsIgnoreCase(modelProvider)) { | ||
| logger.info( | ||
| "Creating OpenAI Model with model: {}, baseUrl: {}", | ||
| openaiModelName, | ||
| openaiBaseUrl); | ||
| OpenAIChatModel.Builder builder = | ||
| OpenAIChatModel.builder() | ||
| .apiKey(openaiApiKey) | ||
| .modelName(openaiModelName) | ||
| .stream(true) | ||
| .formatter(new OpenAIChatFormatter()); | ||
| if (openaiBaseUrl != null && !openaiBaseUrl.isEmpty() && !openaiBaseUrl.equals("-")) { | ||
| builder.baseUrl(openaiBaseUrl); | ||
| } | ||
| return builder.build(); |
There was a problem hiding this comment.
The API keys are being logged when creating the model instances. While the log statement only shows the model name and baseUrl, ensure that the apiKey fields are not accidentally exposed in any logging framework's debug or trace levels. Consider reviewing logging configurations to prevent sensitive credential leakage.
agentscope-core/src/main/java/io/agentscope/core/model/transport/JdkHttpTransport.java
Show resolved
Hide resolved
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…OpenAI (agentscope-ai#464) ## AgentScope-Java Version 1.0.7 ## Description feat(boba-tea-shop): add support for OpenAI models in sub-agent' fix(transport): resolve unexpected thread switching feat(boba-tea-shop): optimize MCP client initialization logic for business-sub-agent ## Checklist Please check the following items before code is ready to be reviewed. - [x] Code has been formatted with `mvn spotless:apply` - [x] All tests are passing (`mvn test`) - [x] Javadoc comments are complete and follow project conventions - [x] Related documentation has been updated (e.g. links, examples, etc.) - [x] Code is ready for review
#601) ## AgentScope-Java Version 1.0.8 ## Description Same as #464 ## Checklist Please check the following items before code is ready to be reviewed. - [x] Code has been formatted with `mvn spotless:apply` - [x] All tests are passing (`mvn test`) - [x] Javadoc comments are complete and follow project conventions - [x] Related documentation has been updated (e.g. links, examples, etc.) - [x] Code is ready for review
agentscope-ai#601) ## AgentScope-Java Version 1.0.8 ## Description Same as agentscope-ai#464 ## Checklist Please check the following items before code is ready to be reviewed. - [x] Code has been formatted with `mvn spotless:apply` - [x] All tests are passing (`mvn test`) - [x] Javadoc comments are complete and follow project conventions - [x] Related documentation has been updated (e.g. links, examples, etc.) - [x] Code is ready for review
AgentScope-Java Version
1.0.7
Description
feat(boba-tea-shop): add support for OpenAI models in sub-agent'
fix(transport): resolve unexpected thread switching
feat(boba-tea-shop): optimize MCP client initialization logic for business-sub-agent
Checklist
Please check the following items before code is ready to be reviewed.
mvn spotless:applymvn test)