diff --git a/backend/app/agent/agent_model.py b/backend/app/agent/agent_model.py index 1b1fe860b..00b49d0b5 100644 --- a/backend/app/agent/agent_model.py +++ b/backend/app/agent/agent_model.py @@ -24,6 +24,7 @@ from app.agent.listen_chat_agent import ListenChatAgent, logger from app.model.chat import AgentModelConfig, Chat +from app.model.model_platform import patch_bedrock_cloud_config from app.service.task import ActionCreateAgentData, Agents, get_task_lock from app.utils.event_loop_utils import _schedule_async_task @@ -80,6 +81,14 @@ def agent_model( for attr in config_attrs: effective_config[attr] = getattr(options, attr) extra_params = options.extra_params or {} + # Cloud mode: inject default Bedrock region and adjust URL for proxy. + if ( + effective_config.get("model_platform") == "aws-bedrock-converse" + and options.is_cloud() + ): + effective_config["api_url"], extra_params = patch_bedrock_cloud_config( + effective_config["api_url"], extra_params + ) init_param_keys = { "api_version", "azure_ad_token", @@ -89,6 +98,10 @@ def agent_model( "client", "async_client", "azure_deployment_name", + "region_name", + "aws_access_key_id", + "aws_secret_access_key", + "aws_session_token", } init_params = {} @@ -112,6 +125,26 @@ def agent_model( else: model_config[k] = v + # Auto-inject prompt caching based on model platform + try: + model_platform_enum = ModelPlatformType( + effective_config["model_platform"].lower() + ) + if model_platform_enum in { + ModelPlatformType.ANTHROPIC, + ModelPlatformType.AWS_BEDROCK_CONVERSE, + }: + model_config.setdefault("cache_control", "1h") + elif model_platform_enum == ModelPlatformType.OPENAI: + model_config.setdefault( + "prompt_cache_key", str(options.project_id) + ) + except (ValueError, AttributeError): + logging.error( + f"Invalid model platform: {effective_config['model_platform']}", + exc_info=True, + ) + if agent_name == Agents.task_agent: model_config["stream"] = True if agent_name == Agents.browser_agent: diff --git a/backend/app/agent/factory/mcp.py b/backend/app/agent/factory/mcp.py index 4f1dc86ab..9b92c90df 100644 --- a/backend/app/agent/factory/mcp.py +++ b/backend/app/agent/factory/mcp.py @@ -21,6 +21,7 @@ from app.agent.toolkit.mcp_search_toolkit import McpSearchToolkit from app.agent.tools import get_mcp_tools from app.model.chat import Chat +from app.model.model_platform import patch_bedrock_cloud_config from app.service.task import ActionCreateAgentData, Agents, get_task_lock @@ -73,6 +74,17 @@ async def mcp_agent(options: Chat): ) ) ) + extra_params = { + k: v + for k, v in (options.extra_params or {}).items() + if k not in ["model_platform", "model_type", "api_key", "url"] + } + api_url = options.api_url + if options.model_platform == "aws-bedrock-converse" and options.is_cloud(): + api_url, extra_params = patch_bedrock_cloud_config( + api_url, extra_params + ) + return ListenChatAgent( options.project_id, Agents.mcp_agent, @@ -81,7 +93,7 @@ async def mcp_agent(options: Chat): model_platform=options.model_platform, model_type=options.model_type, api_key=options.api_key, - url=options.api_url, + url=api_url, model_config_dict=( { "user": str(options.project_id), @@ -90,11 +102,7 @@ async def mcp_agent(options: Chat): else None ), timeout=600, # 10 minutes - **{ - k: v - for k, v in (options.extra_params or {}).items() - if k not in ["model_platform", "model_type", "api_key", "url"] - }, + **extra_params, ), # output_language=options.language, tools=tools, diff --git a/backend/app/model/chat.py b/backend/app/model/chat.py index 8f13a5aa4..1b9b25a8c 100644 --- a/backend/app/model/chat.py +++ b/backend/app/model/chat.py @@ -116,7 +116,7 @@ def get_uvx_env(self) -> dict[str, str]: ) def is_cloud(self): - return self.api_url is not None and "44.247.171.124" in self.api_url + return self.api_url is not None and "eigent-proxy" in self.api_url def file_save_path(self, path: str | None = None): email = re.sub(r'[\\/*?:"<>|\s]', "_", self.email.split("@")[0]).strip( diff --git a/backend/app/model/model_platform.py b/backend/app/model/model_platform.py index 3ee760c71..b9a42fc97 100644 --- a/backend/app/model/model_platform.py +++ b/backend/app/model/model_platform.py @@ -24,6 +24,22 @@ "llama.cpp": "openai-compatible-model", } +# Bedrock Converse requires a region during model initialization. +BEDROCK_CONVERSE_REGION: Final[str] = "us-west-2" + + +def patch_bedrock_cloud_config( + api_url: str, extra_params: dict +) -> tuple[str, dict]: + """Patch API URL and extra_params for Bedrock Converse in cloud mode. + + Appends '/bedrock' to the proxy URL and defaults the region. + Returns the updated (api_url, extra_params). + """ + extra_params = dict(extra_params) + extra_params.setdefault("region_name", BEDROCK_CONVERSE_REGION) + return api_url + "/bedrock", extra_params + def normalize_model_platform(platform: str) -> str: """Normalize provider aliases to supported model platform names.""" diff --git a/backend/app/utils/single_agent_worker.py b/backend/app/utils/single_agent_worker.py index acd4b6ba4..d276326fd 100644 --- a/backend/app/utils/single_agent_worker.py +++ b/backend/app/utils/single_agent_worker.py @@ -12,10 +12,15 @@ # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +import asyncio import datetime import logging +from collections.abc import Awaitable, Callable -from camel.agents.chat_agent import AsyncStreamingChatAgentResponse +from camel.agents.chat_agent import ( + AsyncStreamingChatAgentResponse, + ChatAgentResponse, +) from camel.societies.workforce.prompts import PROCESS_TASK_PROMPT from camel.societies.workforce.single_agent_worker import ( SingleAgentWorker as BaseSingleAgentWorker, @@ -67,7 +72,13 @@ def __init__( self.worker = worker # change type hint async def _process_task( - self, task: Task, dependencies: list[Task], stream_callback=None + self, + task: Task, + dependencies: list[Task], + stream_callback: Callable[ + ["ChatAgentResponse"], Awaitable[None] | None + ] + | None = None, ) -> TaskState: r"""Processes a task with its dependencies using an efficient agent management system. @@ -146,6 +157,10 @@ async def _process_task( async for chunk in response: chunk_count += 1 last_chunk = chunk + if stream_callback: + maybe = stream_callback(chunk) + if asyncio.iscoroutine(maybe): + await maybe if chunk.msg and chunk.msg.content: accumulated_content += chunk.msg.content logger.info( @@ -186,6 +201,10 @@ async def _process_task( last_chunk = None async for chunk in response: last_chunk = chunk + if stream_callback: + maybe = stream_callback(chunk) + if asyncio.iscoroutine(maybe): + await maybe if chunk.msg: if chunk.msg.content: accumulated_content += chunk.msg.content diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 5a0724ace..948a46977 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -6,7 +6,7 @@ readme = "README.md" requires-python = ">=3.11,<3.12" dependencies = [ "pip>=23.0", - "camel-ai[eigent]==0.2.90a6", + "camel-ai[eigent]==0.2.90", "fastapi>=0.115.12", "fastapi-babel>=1.0.0", "uvicorn[standard]>=0.34.2", diff --git a/backend/tests/app/component/test_model_validation.py b/backend/tests/app/component/test_model_validation.py index a8c65d5f8..4dfa1de57 100644 --- a/backend/tests/app/component/test_model_validation.py +++ b/backend/tests/app/component/test_model_validation.py @@ -215,6 +215,25 @@ def test_create_agent_invalid_model_platform(): create_agent(model_platform=None, model_type="GPT_4O_MINI") +@pytest.mark.unit +@patch("app.component.model_validation.ModelFactory.create") +@patch("app.component.model_validation.ChatAgent") +def test_create_agent_hardcodes_bedrock_converse_region( + mock_chat_agent, mock_model_factory +): + """Test Bedrock Converse validation always uses the hardcoded region.""" + mock_model_factory.return_value = MagicMock() + mock_chat_agent.return_value = MagicMock() + + create_agent( + model_platform="aws-bedrock-converse", + model_type="anthropic.claude-3-5-sonnet", + api_key="test_key", + ) + + assert mock_model_factory.call_args.kwargs["region_name"] == "us-west-2" + + @pytest.mark.unit def test_validation_missing_model_type(): """Test validation with missing model type.""" diff --git a/backend/uv.lock b/backend/uv.lock index 8fbada652..7feab5347 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = "==3.11.*" resolution-markers = [ "sys_platform == 'win32'", @@ -242,7 +242,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "aiofiles", specifier = ">=24.1.0" }, - { name = "camel-ai", extras = ["eigent"], specifier = "==0.2.90a6" }, + { name = "camel-ai", extras = ["eigent"], specifier = "==0.2.90" }, { name = "debugpy", specifier = ">=1.8.17" }, { name = "fastapi", specifier = ">=0.115.12" }, { name = "fastapi-babel", specifier = ">=1.0.0" }, @@ -313,7 +313,7 @@ wheels = [ [[package]] name = "camel-ai" -version = "0.2.90a6" +version = "0.2.90" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "astor" }, @@ -331,9 +331,9 @@ dependencies = [ { name = "tiktoken" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f5/52/5113d146328de8cb538f0616d0e7c65395dc52c6147a7b74d901978108da/camel_ai-0.2.90a6.tar.gz", hash = "sha256:9f1f537e49de690b03b4c110ace2ac17afa41663b108f65a0c0e609ef3b4eb27", size = 1206365, upload-time = "2026-03-13T14:49:21.5Z" } +sdist = { url = "https://files.pythonhosted.org/packages/26/b3/da958646c69b42cfafd3fc081b77bb19bb625773766da6ffa2d77c9c66e0/camel_ai-0.2.90.tar.gz", hash = "sha256:43f11673390cc8d4451d6b1bb2913ddef6131f411f47da02d16a3989c8096d02", size = 1212904, upload-time = "2026-03-22T08:37:04.611Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/a2/fa855f44589640b8ec1a3c21767d4fdf23ab5918a9aa2a73f21d1f7c2e3b/camel_ai-0.2.90a6-py3-none-any.whl", hash = "sha256:5ccc9af05559af51e421eb9ac1f48b30950117c061dd62dd67cb3e5b13f42d89", size = 1690382, upload-time = "2026-03-13T14:49:18.718Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e2/7005080797edcc760dcf7695ece29f97e18189ea2c56cd7514592801f0c9/camel_ai-0.2.90-py3-none-any.whl", hash = "sha256:9998c434779a1a847d9ccddce1c069f22fb9667b19ba06a2452c479882169082", size = 1700705, upload-time = "2026-03-22T08:37:02.259Z" }, ] [package.optional-dependencies] diff --git a/src/lib/llm.ts b/src/lib/llm.ts index 65f4340e6..e06105bb0 100644 --- a/src/lib/llm.ts +++ b/src/lib/llm.ts @@ -136,9 +136,43 @@ export const INIT_PROVODERS: Provider[] = [ id: 'aws-bedrock', name: 'AWS Bedrock', apiKey: '', - apiHost: '', + apiHost: 'https://bedrock-mantle.us-east-1.api.aws/v1', description: 'AWS Bedrock model configuration.', - hostPlaceHolder: 'e.g. https://bedrock-runtime.{{region}}.amazonaws.com', + is_valid: false, + model_type: '', + }, + { + id: 'aws-bedrock-converse', + name: 'AWS Bedrock Converse', + apiKey: '', + apiHost: 'https://bedrock-runtime.us-east-1.amazonaws.com', + description: + 'AWS Bedrock Converse model configuration. Auth: API Key (Bearer Token), or Access Key ID + Secret Access Key (+Session Token).', + externalConfig: [ + { + key: 'region_name', + name: 'Region', + value: 'us-east-1', + }, + { + key: 'aws_access_key_id', + name: 'Access Key ID', + value: '', + secret: true, + }, + { + key: 'aws_secret_access_key', + name: 'Secret Access Key', + value: '', + secret: true, + }, + { + key: 'aws_session_token', + name: 'Session Token (Optional)', + value: '', + secret: true, + }, + ], is_valid: false, model_type: '', }, diff --git a/src/pages/Agents/Models.tsx b/src/pages/Agents/Models.tsx index 31a2dbacc..8e2c12859 100644 --- a/src/pages/Agents/Models.tsx +++ b/src/pages/Agents/Models.tsx @@ -119,12 +119,15 @@ export default function SettingModels() { } = useAuthStore(); const _navigate = useNavigate(); const { t } = useTranslation(); - const getValidateMessage = (res: any) => - res?.message ?? - res?.detail?.message ?? - res?.detail?.error?.message ?? - res?.error?.message ?? - t('setting.validate-failed'); + const getValidateMessage = (res: any): string => { + const msg = + res?.message ?? + res?.detail?.message ?? + res?.detail?.error?.message ?? + res?.error?.message ?? + t('setting.validate-failed'); + return typeof msg === 'string' ? msg : JSON.stringify(msg); + }; const [items, _setItems] = useState( INIT_PROVODERS.filter((p) => p.id !== 'local') ); @@ -144,6 +147,7 @@ export default function SettingModels() { const [showApiKey, setShowApiKey] = useState(() => INIT_PROVODERS.filter((p) => p.id !== 'local').map(() => false) ); + const [showSecret, setShowSecret] = useState>({}); const [loading, setLoading] = useState(null); const [errors, setErrors] = useState< { @@ -507,7 +511,7 @@ export default function SettingModels() { form[idx]; let hasError = false; const newErrors = [...errors]; - if (items[idx].id !== 'local') { + if (items[idx].id !== 'local' && items[idx].id !== 'aws-bedrock-converse') { if (!apiKey || apiKey.trim() === '') { newErrors[idx].apiKey = t('setting.api-key-can-not-be-empty'); hasError = true; @@ -544,7 +548,7 @@ export default function SettingModels() { const res = await fetchPost('/model/validate', { model_platform: item.id, model_type: form[idx].model_type, - api_key: form[idx].apiKey, + api_key: form[idx].apiKey || null, url: form[idx].apiHost, extra_params: external, }); @@ -1068,6 +1072,7 @@ export default function SettingModels() { grok: PROVIDER_AVATAR_URLS.grok, mistral: PROVIDER_AVATAR_URLS.mistral, 'aws-bedrock': bedrockImage, + 'aws-bedrock-converse': bedrockImage, azure: azureImage, 'openai-compatible-model': openaiImage, // Use OpenAI icon as fallback // Local models @@ -1474,8 +1479,32 @@ export default function SettingModels() { + ) : ( + + ) + ) : undefined + } + onBackIconClick={ + ec.secret + ? () => + setShowSecret((prev) => ({ + ...prev, + [`${idx}-${ecIdx}`]: !prev[`${idx}-${ecIdx}`], + })) + : undefined + } value={ec.value} onChange={(e) => { const v = e.target.value; diff --git a/src/store/chatStore.ts b/src/store/chatStore.ts index 3c404ea82..88a22c3ab 100644 --- a/src/store/chatStore.ts +++ b/src/store/chatStore.ts @@ -652,7 +652,7 @@ const chatStore = (initial?: Partial) => model_platform: cloud_model_type.includes('gpt') ? 'openai' : cloud_model_type.includes('claude') - ? 'aws-bedrock' + ? 'aws-bedrock-converse' : cloud_model_type.includes('gemini') ? 'gemini' : 'openai-compatible-model', diff --git a/src/types/index.ts b/src/types/index.ts index feda06ebd..82eeea590 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -16,6 +16,8 @@ type externalConfig = { key: string; name: string; value: string; + placeholder?: string; + secret?: boolean; options?: { label: string; value: string;