Skip to content

Commit 4790a57

Browse files
authored
Merge pull request #102 from Serverless-Devs/feature/agentrun-sdk-model-alignment-2026-05
feat(agent_runtime): align SDK model with agentrun-20250910 & support workspace_name
2 parents 6569d3f + f9b5e67 commit 4790a57

9 files changed

Lines changed: 1742 additions & 30 deletions

File tree

agentrun/agent_runtime/__client_async_template.py

Lines changed: 61 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,29 @@ 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 is not None:
92+
cfg = Config.with_configs(self.config, config)
93+
resolved_id = await resolve_workspace_id_by_name_async(
94+
input.workspace_name, cfg
95+
)
96+
input = input.model_copy(
97+
update={"workspace_id": resolved_id, "workspace_name": None}
98+
)
99+
76100
if input.network_configuration is None:
77101
input.network_configuration = NetworkConfig()
78102

@@ -198,12 +222,47 @@ async def list_async(
198222
List[AgentRuntime]: Agent Runtime 对象列表 / List of Agent Runtime objects
199223
200224
Raises:
225+
ValueError: 同时传入 workspace_id 与 workspace_name,或同时传入
226+
workspace_ids 与 workspace_names / When workspace_id and
227+
workspace_name (or workspace_ids and workspace_names) are
228+
both set
229+
ResourceNotExistError: workspace_name(s) 在该账号下未找到
230+
/ No workspace matches the given workspace_name(s)
201231
HTTPError: HTTP 请求错误 / HTTP request error
202232
"""
203233
try:
204234
if input is None:
205235
input = AgentRuntimeListInput()
206236

237+
if input.workspace_id and input.workspace_name:
238+
raise ValueError(
239+
"workspace_id and workspace_name are mutually exclusive;"
240+
" please only set one of them."
241+
)
242+
if input.workspace_ids and input.workspace_names:
243+
raise ValueError(
244+
"workspace_ids and workspace_names are mutually exclusive;"
245+
" please only set one of them."
246+
)
247+
if input.workspace_name is not None or input.workspace_names:
248+
cfg = Config.with_configs(self.config, config)
249+
update: dict = {}
250+
if input.workspace_name is not None:
251+
update["workspace_id"] = (
252+
await resolve_workspace_id_by_name_async(
253+
input.workspace_name, cfg
254+
)
255+
)
256+
update["workspace_name"] = None
257+
if input.workspace_names:
258+
update["workspace_ids"] = (
259+
await resolve_workspace_ids_by_names_async(
260+
input.workspace_names, cfg
261+
)
262+
)
263+
update["workspace_names"] = None
264+
input = input.model_copy(update=update)
265+
207266
results = await self.__control_api.list_agent_runtimes_async(
208267
ListAgentRuntimesRequest().from_map(input.model_dump()),
209268
config=config,

agentrun/agent_runtime/__runtime_async_template.py

Lines changed: 18 additions & 4 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(
@@ -174,16 +178,26 @@ async def list_all_async(
174178
cls,
175179
*,
176180
agent_runtime_name: Optional[str] = None,
177-
tags: Optional[str] = None,
181+
system_tags: Optional[str] = None,
178182
search_mode: Optional[str] = None,
183+
status: Optional[str] = None,
184+
workspace_id: Optional[str] = None,
185+
workspace_ids: Optional[str] = None,
186+
workspace_name: Optional[str] = None,
187+
workspace_names: Optional[str] = None,
179188
config: Optional[Config] = None,
180189
) -> List["AgentRuntime"]:
181190
return await cls._list_all_async(
182191
lambda ar: ar.agent_runtime_id or "",
183192
config=config,
184193
agent_runtime_name=agent_runtime_name,
185-
tags=tags,
194+
system_tags=system_tags,
186195
search_mode=search_mode,
196+
status=status,
197+
workspace_id=workspace_id,
198+
workspace_ids=workspace_ids,
199+
workspace_name=workspace_name,
200+
workspace_names=workspace_names,
187201
)
188202

189203
@classmethod
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
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+
# 翻页参数:ListWorkspaces 的 name= 参数在服务端可能是 prefix/fuzzy 匹配,
34+
# 单页 50 条不足以覆盖海量同前缀场景,因此累积所有页再做 exact match。
35+
_PAGE_SIZE = 50
36+
# 安全上限:避免上游异常导致死循环;20 页 × 50 条 = 1000 个候选,
37+
# 同名 / 同前缀 workspace 远超该值的概率极低。
38+
_MAX_PAGES = 20
39+
40+
41+
def _cache_key(cfg: Config, name: str) -> Tuple[str, str, str]:
42+
return (
43+
cfg.get_access_key_id() or "",
44+
cfg.get_region_id() or "",
45+
name,
46+
)
47+
48+
49+
def _pick_exact_match(
50+
workspaces: List[Workspace], name: str
51+
) -> Optional[Workspace]:
52+
matches = [w for w in workspaces if w.name == name]
53+
if len(matches) > 1:
54+
raise ValueError(
55+
f"Workspace name {name!r} is ambiguous: matched"
56+
f" {len(matches)} workspaces; please use workspace_id instead."
57+
)
58+
return matches[0] if matches else None
59+
60+
61+
def _raise_for_tea_exception(e: Exception) -> None:
62+
if isinstance(e, ClientException):
63+
raise ClientError(
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+
if isinstance(e, ServerException):
70+
raise ServerError(
71+
e.status_code,
72+
pydash.get(e, "data.message", pydash.get(e, "message", "")),
73+
pydash.get(e, "data.requestId", ""),
74+
pydash.get(e, "data.code", ""),
75+
) from e
76+
77+
78+
class _WorkspaceResolver(ControlAPI):
79+
"""轻量封装:复用 ControlAPI 拿底层 AgentRun client。"""
80+
81+
def resolve(self, name: str, config: Optional[Config] = None) -> str:
82+
if not name:
83+
raise ValueError("workspace_name must be non-empty")
84+
85+
cfg = Config.with_configs(self.config, config)
86+
cache_key = _cache_key(cfg, name)
87+
if cache_key in _RESOLVE_CACHE:
88+
return _RESOLVE_CACHE[cache_key]
89+
90+
ws = self._lookup_sync(name, config)
91+
if ws is None:
92+
raise ResourceNotExistError("Workspace", name)
93+
94+
assert ws.workspace_id is not None
95+
_RESOLVE_CACHE[cache_key] = ws.workspace_id
96+
return ws.workspace_id
97+
98+
async def resolve_async(
99+
self, name: str, config: Optional[Config] = None
100+
) -> str:
101+
if not name:
102+
raise ValueError("workspace_name must be non-empty")
103+
104+
cfg = Config.with_configs(self.config, config)
105+
cache_key = _cache_key(cfg, name)
106+
if cache_key in _RESOLVE_CACHE:
107+
return _RESOLVE_CACHE[cache_key]
108+
109+
ws = await self._lookup_async(name, config)
110+
if ws is None:
111+
raise ResourceNotExistError("Workspace", name)
112+
113+
assert ws.workspace_id is not None
114+
_RESOLVE_CACHE[cache_key] = ws.workspace_id
115+
return ws.workspace_id
116+
117+
# --- internal -----------------------------------------------------------
118+
119+
def _lookup_sync(
120+
self, name: str, config: Optional[Config] = None
121+
) -> Optional[Workspace]:
122+
client = self._get_client(config)
123+
accumulated: List[Workspace] = []
124+
page_number = 1
125+
try:
126+
while page_number <= _MAX_PAGES:
127+
response = client.list_workspaces(
128+
ListWorkspacesRequest(
129+
name=name,
130+
page_size=str(_PAGE_SIZE),
131+
page_number=str(page_number),
132+
)
133+
)
134+
workspaces = (
135+
getattr(
136+
getattr(response.body, "data", None),
137+
"workspaces",
138+
None,
139+
)
140+
or []
141+
)
142+
if not workspaces:
143+
break
144+
accumulated.extend(workspaces)
145+
if len(workspaces) < _PAGE_SIZE:
146+
break
147+
page_number += 1
148+
except (ClientException, ServerException) as e:
149+
_raise_for_tea_exception(e)
150+
raise
151+
return _pick_exact_match(accumulated, name)
152+
153+
async def _lookup_async(
154+
self, name: str, config: Optional[Config] = None
155+
) -> Optional[Workspace]:
156+
client = self._get_client(config)
157+
accumulated: List[Workspace] = []
158+
page_number = 1
159+
try:
160+
while page_number <= _MAX_PAGES:
161+
response = await client.list_workspaces_async(
162+
ListWorkspacesRequest(
163+
name=name,
164+
page_size=str(_PAGE_SIZE),
165+
page_number=str(page_number),
166+
)
167+
)
168+
workspaces = (
169+
getattr(
170+
getattr(response.body, "data", None),
171+
"workspaces",
172+
None,
173+
)
174+
or []
175+
)
176+
if not workspaces:
177+
break
178+
accumulated.extend(workspaces)
179+
if len(workspaces) < _PAGE_SIZE:
180+
break
181+
page_number += 1
182+
except (ClientException, ServerException) as e:
183+
_raise_for_tea_exception(e)
184+
raise
185+
return _pick_exact_match(accumulated, name)
186+
187+
188+
def resolve_workspace_id_by_name(
189+
name: str, config: Optional[Config] = None
190+
) -> str:
191+
"""同步:根据 workspace name 解析出 workspace_id。
192+
193+
Raises:
194+
ValueError: ``name`` 为空,或在该账号下存在重名 workspace。
195+
ResourceNotExistError: 该账号下未找到同名 workspace。
196+
"""
197+
198+
return _WorkspaceResolver(config).resolve(name, config)
199+
200+
201+
async def resolve_workspace_id_by_name_async(
202+
name: str, config: Optional[Config] = None
203+
) -> str:
204+
"""异步:根据 workspace name 解析出 workspace_id。"""
205+
206+
return await _WorkspaceResolver(config).resolve_async(name, config)
207+
208+
209+
def resolve_workspace_ids_by_names(
210+
names: str, config: Optional[Config] = None
211+
) -> str:
212+
"""同步:将逗号分隔的多个 workspace 名称解析为逗号分隔的 workspace_id 列表。"""
213+
214+
return ",".join(
215+
resolve_workspace_id_by_name(n.strip(), config)
216+
for n in names.split(",")
217+
if n.strip()
218+
)
219+
220+
221+
async def resolve_workspace_ids_by_names_async(
222+
names: str, config: Optional[Config] = None
223+
) -> str:
224+
"""异步:将逗号分隔的多个 workspace 名称解析为逗号分隔的 workspace_id 列表。"""
225+
226+
out: List[str] = []
227+
for n in names.split(","):
228+
n = n.strip()
229+
if not n:
230+
continue
231+
out.append(await resolve_workspace_id_by_name_async(n, config))
232+
return ",".join(out)
233+
234+
235+
def _clear_cache_for_tests() -> None:
236+
"""仅供单测使用:清空内部解析缓存。"""
237+
238+
_RESOLVE_CACHE.clear()
239+
240+
241+
__all__ = [
242+
"resolve_workspace_id_by_name",
243+
"resolve_workspace_id_by_name_async",
244+
"resolve_workspace_ids_by_names",
245+
"resolve_workspace_ids_by_names_async",
246+
]

0 commit comments

Comments
 (0)