Skip to content

Commit 9170897

Browse files
committed
feat(agent_runtime): support workspace_name in create/list (auto-resolve to workspace_id)
让用户在创建 / 查询 Agent Runtime 时可以直接填 workspace 名称, SDK 自动调用官方 ListWorkspaces 解析为 workspace_id 再下发, 无需用户手动查 ID。 模型变更(agentrun/agent_runtime/model.py): - AgentRuntimeImmutableProps: 新增 workspace_name(流入 CreateInput) - AgentRuntimeListInput: 新增 workspace_name / workspace_names 新增 agentrun/agent_runtime/_workspace.py: - resolve_workspace_id_by_name(_async) 精确名字匹配 + (ak, region, name) 缓存 - resolve_workspace_ids_by_names(_async) 批量名字 -> 逗号分隔 ID - 找不到抛 ResourceNotExistError,重名抛 ValueError, Tea ClientException/ServerException 转 SDK 内置 ClientError/ServerError client / runtime(async 模板 + codegen 同步生成): - AgentRuntimeClient.create / list:调底层 API 前自动解析 workspace_name(s) - 同时传 workspace_id+workspace_name(或复数版本)抛 ValueError - AgentRuntime.list_all:透传 workspace_name / workspace_names 示例:examples/quickstart_runtime.py - 演示通过镜像部署 AgentRuntime,并使用 workspace_name 选择工作空间 测试(tests/unittests/agent_runtime/test_workspace.py,新增 25 用例): - 精确匹配 / 缓存 / 空名 / 找不到 / 重名 / Tea 异常透传 - client.create 与 client.list 在 sync + async 路径下的解析与互斥校验 校验: - 全量 3469 单测通过 - agentrun.agent_runtime 总覆盖率 99%,_workspace.py 95% - mypy --config-file mypy.ini agentrun/agent_runtime/ 无 issue Signed-off-by: Sodawyx <sodawyx@126.com>
1 parent 942e8a3 commit 9170897

7 files changed

Lines changed: 837 additions & 10 deletions

File tree

agentrun/agent_runtime/__client_async_template.py

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@
1818
)
1919
from typing_extensions import Unpack
2020

21+
from agentrun.agent_runtime._workspace import (
22+
resolve_workspace_id_by_name,
23+
resolve_workspace_id_by_name_async,
24+
resolve_workspace_ids_by_names,
25+
resolve_workspace_ids_by_names_async,
26+
)
2127
from agentrun.agent_runtime.api.data import InvokeArgs
2228
from agentrun.agent_runtime.model import (
2329
AgentRuntimeArtifact,
@@ -68,11 +74,26 @@ async def create_async(
6874
AgentRuntime: 创建的 Agent Runtime 对象 / Created Agent Runtime object
6975
7076
Raises:
71-
ValueError: 当既未提供代码配置也未提供容器配置时 / When neither code nor container configuration is provided
77+
ValueError: 当既未提供代码配置也未提供容器配置时;或同时传入
78+
workspace_id 与 workspace_name / When neither code nor container
79+
configuration is provided, or when workspace_id and workspace_name
80+
are both set
7281
ResourceAlreadyExistError: 资源已存在 / Resource already exists
73-
ResourceNotExistError: 资源不存在 / Resource does not exist
82+
ResourceNotExistError: 资源不存在;或 workspace_name 在该账号下未找到
83+
/ Resource does not exist, or no workspace matches workspace_name
7484
HTTPError: HTTP 请求错误 / HTTP request error
7585
"""
86+
if input.workspace_id and input.workspace_name:
87+
raise ValueError(
88+
"workspace_id and workspace_name are mutually exclusive; please"
89+
" only set one of them."
90+
)
91+
if input.workspace_name:
92+
input.workspace_id = await resolve_workspace_id_by_name_async(
93+
input.workspace_name, config
94+
)
95+
input.workspace_name = None
96+
7697
if input.network_configuration is None:
7798
input.network_configuration = NetworkConfig()
7899

@@ -198,12 +219,41 @@ async def list_async(
198219
List[AgentRuntime]: Agent Runtime 对象列表 / List of Agent Runtime objects
199220
200221
Raises:
222+
ValueError: 同时传入 workspace_id 与 workspace_name,或同时传入
223+
workspace_ids 与 workspace_names / When workspace_id and
224+
workspace_name (or workspace_ids and workspace_names) are
225+
both set
226+
ResourceNotExistError: workspace_name(s) 在该账号下未找到
227+
/ No workspace matches the given workspace_name(s)
201228
HTTPError: HTTP 请求错误 / HTTP request error
202229
"""
203230
try:
204231
if input is None:
205232
input = AgentRuntimeListInput()
206233

234+
if input.workspace_id and input.workspace_name:
235+
raise ValueError(
236+
"workspace_id and workspace_name are mutually exclusive;"
237+
" please only set one of them."
238+
)
239+
if input.workspace_ids and input.workspace_names:
240+
raise ValueError(
241+
"workspace_ids and workspace_names are mutually exclusive;"
242+
" please only set one of them."
243+
)
244+
if input.workspace_name:
245+
input.workspace_id = await resolve_workspace_id_by_name_async(
246+
input.workspace_name, config
247+
)
248+
input.workspace_name = None
249+
if input.workspace_names:
250+
input.workspace_ids = (
251+
await resolve_workspace_ids_by_names_async(
252+
input.workspace_names, config
253+
)
254+
)
255+
input.workspace_names = None
256+
207257
results = await self.__control_api.list_agent_runtimes_async(
208258
ListAgentRuntimesRequest().from_map(input.model_dump()),
209259
config=config,

agentrun/agent_runtime/__runtime_async_template.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ async def create_async(
7777
ResourceAlreadyExistError: 资源已存在 / Resource already exists
7878
HTTPError: HTTP 请求错误 / HTTP request error
7979
"""
80-
return await cls.__get_client(config=config).create_async(input, config=config)
80+
return await cls.__get_client(config=config).create_async(
81+
input, config=config
82+
)
8183

8284
@classmethod
8385
async def delete_by_id_async(cls, id: str, config: Optional[Config] = None):
@@ -155,7 +157,9 @@ async def get_by_id_async(cls, id: str, config: Optional[Config] = None):
155157
ResourceNotExistError: 资源不存在 / Resource does not exist
156158
HTTPError: HTTP 请求错误 / HTTP request error
157159
"""
158-
return await cls.__get_client(config=config).get_async(id, config=config)
160+
return await cls.__get_client(config=config).get_async(
161+
id, config=config
162+
)
159163

160164
@classmethod
161165
async def _list_page_async(
@@ -179,6 +183,8 @@ async def list_all_async(
179183
status: Optional[str] = None,
180184
workspace_id: Optional[str] = None,
181185
workspace_ids: Optional[str] = None,
186+
workspace_name: Optional[str] = None,
187+
workspace_names: Optional[str] = None,
182188
config: Optional[Config] = None,
183189
) -> List["AgentRuntime"]:
184190
return await cls._list_all_async(
@@ -190,6 +196,8 @@ async def list_all_async(
190196
status=status,
191197
workspace_id=workspace_id,
192198
workspace_ids=workspace_ids,
199+
workspace_name=workspace_name,
200+
workspace_names=workspace_names,
193201
)
194202

195203
@classmethod
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
"""Workspace 名称解析助手 / Workspace Name Resolution Helper
2+
3+
提供 ``workspace_name -> workspace_id`` 的解析能力,供 ``AgentRuntimeClient``
4+
在 ``create`` / ``list`` 等场景下自动转换用户传入的工作空间名称。
5+
6+
The official AgentRun API only accepts ``workspace_id``. The SDK exposes a
7+
convenience field ``workspace_name``; this module wraps ``list_workspaces``
8+
to look the id up by name (exact match, with a simple in-memory cache).
9+
"""
10+
11+
from typing import Dict, List, Optional, Tuple
12+
13+
from alibabacloud_agentrun20250910.models import (
14+
ListWorkspacesRequest,
15+
Workspace,
16+
)
17+
from alibabacloud_tea_openapi.exceptions._client import ClientException
18+
from alibabacloud_tea_openapi.exceptions._server import ServerException
19+
import pydash
20+
21+
from agentrun.utils.config import Config
22+
from agentrun.utils.control_api import ControlAPI
23+
from agentrun.utils.exception import (
24+
ClientError,
25+
ResourceNotExistError,
26+
ServerError,
27+
)
28+
29+
# Cache key 为 (access_key_id, region_id, name),避免不同账号/地域串号。
30+
# Value 为解析得到的 workspace_id。
31+
_RESOLVE_CACHE: Dict[Tuple[str, str, str], str] = {}
32+
33+
34+
def _cache_key(cfg: Config, name: str) -> Tuple[str, str, str]:
35+
return (
36+
cfg.get_access_key_id() or "",
37+
cfg.get_region_id() or "",
38+
name,
39+
)
40+
41+
42+
def _pick_exact_match(
43+
workspaces: List[Workspace], name: str
44+
) -> Optional[Workspace]:
45+
matches = [w for w in workspaces if w.name == name]
46+
if len(matches) > 1:
47+
raise ValueError(
48+
f"Workspace name {name!r} is ambiguous: matched"
49+
f" {len(matches)} workspaces; please use workspace_id instead."
50+
)
51+
return matches[0] if matches else None
52+
53+
54+
def _raise_for_tea_exception(e: Exception) -> None:
55+
if isinstance(e, ClientException):
56+
raise ClientError(
57+
e.status_code,
58+
pydash.get(e, "data.message", pydash.get(e, "message", "")),
59+
pydash.get(e, "data.requestId", ""),
60+
pydash.get(e, "data.code", ""),
61+
) from e
62+
if isinstance(e, ServerException):
63+
raise ServerError(
64+
e.status_code,
65+
pydash.get(e, "data.message", pydash.get(e, "message", "")),
66+
pydash.get(e, "data.requestId", ""),
67+
pydash.get(e, "data.code", ""),
68+
) from e
69+
70+
71+
class _WorkspaceResolver(ControlAPI):
72+
"""轻量封装:复用 ControlAPI 拿底层 AgentRun client。"""
73+
74+
def resolve(self, name: str, config: Optional[Config] = None) -> str:
75+
if not name:
76+
raise ValueError("workspace_name must be non-empty")
77+
78+
cfg = Config.with_configs(self.config, config)
79+
cache_key = _cache_key(cfg, name)
80+
if cache_key in _RESOLVE_CACHE:
81+
return _RESOLVE_CACHE[cache_key]
82+
83+
ws = self._lookup_sync(name, config)
84+
if ws is None:
85+
raise ResourceNotExistError("Workspace", name)
86+
87+
assert ws.workspace_id is not None
88+
_RESOLVE_CACHE[cache_key] = ws.workspace_id
89+
return ws.workspace_id
90+
91+
async def resolve_async(
92+
self, name: str, config: Optional[Config] = None
93+
) -> str:
94+
if not name:
95+
raise ValueError("workspace_name must be non-empty")
96+
97+
cfg = Config.with_configs(self.config, config)
98+
cache_key = _cache_key(cfg, name)
99+
if cache_key in _RESOLVE_CACHE:
100+
return _RESOLVE_CACHE[cache_key]
101+
102+
ws = await self._lookup_async(name, config)
103+
if ws is None:
104+
raise ResourceNotExistError("Workspace", name)
105+
106+
assert ws.workspace_id is not None
107+
_RESOLVE_CACHE[cache_key] = ws.workspace_id
108+
return ws.workspace_id
109+
110+
# --- internal -----------------------------------------------------------
111+
112+
def _lookup_sync(
113+
self, name: str, config: Optional[Config] = None
114+
) -> Optional[Workspace]:
115+
client = self._get_client(config)
116+
try:
117+
response = client.list_workspaces(
118+
ListWorkspacesRequest(name=name, page_size="50")
119+
)
120+
except (ClientException, ServerException) as e:
121+
_raise_for_tea_exception(e)
122+
raise
123+
workspaces = (
124+
getattr(getattr(response.body, "data", None), "workspaces", None)
125+
or []
126+
)
127+
return _pick_exact_match(workspaces, name)
128+
129+
async def _lookup_async(
130+
self, name: str, config: Optional[Config] = None
131+
) -> Optional[Workspace]:
132+
client = self._get_client(config)
133+
try:
134+
response = await client.list_workspaces_async(
135+
ListWorkspacesRequest(name=name, page_size="50")
136+
)
137+
except (ClientException, ServerException) as e:
138+
_raise_for_tea_exception(e)
139+
raise
140+
workspaces = (
141+
getattr(getattr(response.body, "data", None), "workspaces", None)
142+
or []
143+
)
144+
return _pick_exact_match(workspaces, name)
145+
146+
147+
def resolve_workspace_id_by_name(
148+
name: str, config: Optional[Config] = None
149+
) -> str:
150+
"""同步:根据 workspace name 解析出 workspace_id。
151+
152+
Raises:
153+
ValueError: ``name`` 为空,或在该账号下存在重名 workspace。
154+
ResourceNotExistError: 该账号下未找到同名 workspace。
155+
"""
156+
157+
return _WorkspaceResolver(config).resolve(name, config)
158+
159+
160+
async def resolve_workspace_id_by_name_async(
161+
name: str, config: Optional[Config] = None
162+
) -> str:
163+
"""异步:根据 workspace name 解析出 workspace_id。"""
164+
165+
return await _WorkspaceResolver(config).resolve_async(name, config)
166+
167+
168+
def resolve_workspace_ids_by_names(
169+
names: str, config: Optional[Config] = None
170+
) -> str:
171+
"""同步:将逗号分隔的多个 workspace 名称解析为逗号分隔的 workspace_id 列表。"""
172+
173+
return ",".join(
174+
resolve_workspace_id_by_name(n.strip(), config)
175+
for n in names.split(",")
176+
if n.strip()
177+
)
178+
179+
180+
async def resolve_workspace_ids_by_names_async(
181+
names: str, config: Optional[Config] = None
182+
) -> str:
183+
"""异步:将逗号分隔的多个 workspace 名称解析为逗号分隔的 workspace_id 列表。"""
184+
185+
out: List[str] = []
186+
for n in names.split(","):
187+
n = n.strip()
188+
if not n:
189+
continue
190+
out.append(await resolve_workspace_id_by_name_async(n, config))
191+
return ",".join(out)
192+
193+
194+
def _clear_cache_for_tests() -> None:
195+
"""仅供单测使用:清空内部解析缓存。"""
196+
197+
_RESOLVE_CACHE.clear()
198+
199+
200+
__all__ = [
201+
"resolve_workspace_id_by_name",
202+
"resolve_workspace_id_by_name_async",
203+
"resolve_workspace_ids_by_names",
204+
"resolve_workspace_ids_by_names_async",
205+
]

0 commit comments

Comments
 (0)