Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion agentrun/agent_runtime/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,9 @@ class AgentRuntimeMutableProps(BaseModel):


class AgentRuntimeImmutableProps(BaseModel):
pass
workspace_id: Optional[str] = None
"""Agent Runtime 所属的工作空间标识符;可选项,不填则使用默认工作空间
/ Workspace identifier the Agent Runtime belongs to; optional, defaults to the default workspace if not provided"""


class AgentRuntimeSystemProps(BaseModel):
Expand Down Expand Up @@ -329,6 +331,9 @@ class AgentRuntimeListInput(PageableInput):
"""系统标签过滤, 多个标签用逗号分隔"""
search_mode: Optional[str] = None
"""搜索模式"""
workspace_id: Optional[str] = None
"""按工作空间标识符过滤
/ Filter by workspace identifier"""


class AgentRuntimeEndpointCreateInput(
Expand Down
9 changes: 9 additions & 0 deletions agentrun/credential/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,9 @@ class CredentialMutableProps(BaseModel):
class CredentialImmutableProps(BaseModel):
credential_name: Optional[str] = None
"""凭证名称"""
workspace_id: Optional[str] = None
"""凭证所属的工作空间标识符;可选项,不填则使用默认工作空间
/ Workspace identifier the credential belongs to; optional, defaults to the default workspace if not provided"""


class CredentialSystemProps(CredentialConfigInner):
Expand Down Expand Up @@ -221,6 +224,9 @@ class CredentialListInput(PageableInput):
"""凭证来源类型(必填)"""
provider: Optional[str] = None
"""提供商"""
workspace_id: Optional[str] = None
"""按工作空间标识符过滤
/ Filter by workspace identifier"""


class CredentialListOutput(BaseModel):
Expand All @@ -232,6 +238,9 @@ class CredentialListOutput(BaseModel):
enabled: Optional[bool] = None
related_resource_count: Optional[int] = None
updated_at: Optional[str] = None
workspace_id: Optional[str] = None
"""凭证所属的工作空间标识符
/ Workspace identifier the credential belongs to"""

async def to_credential_async(self, config: Optional[Config] = None):
from .client import CredentialClient
Expand Down
12 changes: 12 additions & 0 deletions agentrun/knowledgebase/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,12 @@ class KnowledgeBaseImmutableProps(BaseModel):
"""知识库名称 / KnowledgeBase name"""
provider: Optional[Union[KnowledgeBaseProvider, str]] = None
"""提供商 / Provider"""
workspace_id: Optional[str] = None
"""知识库所属的 AgentRun 工作空间标识符;可选项,不填则使用默认工作空间。
注意:与 ``BailianProviderSettings.workspace_id`` 不同,后者指百炼侧的 workspace。
/ Workspace identifier the knowledge base belongs to in AgentRun; optional,
defaults to the default workspace if not provided. Distinct from
``BailianProviderSettings.workspace_id`` which refers to the Bailian-side workspace."""


class KnowledgeBaseSystemProps(BaseModel):
Expand Down Expand Up @@ -354,6 +360,9 @@ class KnowledgeBaseListInput(PageableInput):

provider: Optional[Union[KnowledgeBaseProvider, str]] = None
"""提供商 / Provider"""
workspace_id: Optional[str] = None
"""按 AgentRun 工作空间标识符过滤
/ Filter by AgentRun workspace identifier"""


class KnowledgeBaseListOutput(BaseModel):
Expand All @@ -377,6 +386,9 @@ class KnowledgeBaseListOutput(BaseModel):
"""创建时间 / Created at"""
last_updated_at: Optional[str] = None
"""最后更新时间 / Last updated at"""
workspace_id: Optional[str] = None
"""知识库所属的 AgentRun 工作空间标识符
/ AgentRun workspace identifier the knowledge base belongs to"""

async def to_knowledge_base_async(self, config: Optional[Config] = None):
"""转换为知识库对象(异步)/ Convert to KnowledgeBase object (async)
Expand Down
9 changes: 9 additions & 0 deletions agentrun/memory_collection/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ class MemoryCollectionImmutableProps(BaseModel):
"""Memory Collection 名称"""
type: Optional[str] = None
"""类型"""
workspace_id: Optional[str] = None
"""Memory Collection 所属的工作空间标识符;可选项,不填则使用默认工作空间
/ Workspace identifier the memory collection belongs to; optional, defaults to the default workspace if not provided"""


class MemoryCollectionSystemProps(BaseModel):
Expand Down Expand Up @@ -158,6 +161,9 @@ class MemoryCollectionListInput(PageableInput):
"""状态 / Status"""
type: Optional[str] = None
"""类型 / Type"""
workspace_id: Optional[str] = None
"""按工作空间标识符过滤
/ Filter by workspace identifier"""


class MemoryCollectionListOutput(BaseModel):
Expand All @@ -169,6 +175,9 @@ class MemoryCollectionListOutput(BaseModel):
type: Optional[str] = None
created_at: Optional[str] = None
last_updated_at: Optional[str] = None
workspace_id: Optional[str] = None
"""Memory Collection 所属的工作空间标识符
/ Workspace identifier the memory collection belongs to"""

async def to_memory_collection_async(self, config: Optional[Config] = None):
"""转换为完整的 MemoryCollection 对象(异步)"""
Expand Down
9 changes: 9 additions & 0 deletions agentrun/model/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,9 @@ class CommonModelMutableProps(BaseModel):

class CommonModelImmutableProps(BaseModel):
model_type: Optional[ModelType] = None
workspace_id: Optional[str] = None
"""模型资源所属的工作空间标识符;可选项,不填则使用默认工作空间
/ Workspace identifier the model resource belongs to; optional, defaults to the default workspace if not provided"""


class CommonModelSystemProps:
Expand Down Expand Up @@ -220,6 +223,9 @@ class ModelServiceUpdateInput(ModelServiceMutableProps):
class ModelServiceListInput(PageableInput):
model_type: Optional[ModelType] = None
provider: Optional[str] = None
workspace_id: Optional[str] = None
"""按工作空间标识符过滤
/ Filter by workspace identifier"""


class ModelProxyCreateInput(ModelProxyMutableProps, ModelProxyImmutableProps):
Expand All @@ -233,3 +239,6 @@ class ModelProxyUpdateInput(ModelProxyMutableProps):
class ModelProxyListInput(PageableInput):
proxy_mode: Optional[str] = None
status: Optional[Status] = None
workspace_id: Optional[str] = None
"""按工作空间标识符过滤
/ Filter by workspace identifier"""
3 changes: 3 additions & 0 deletions agentrun/sandbox/__template_async_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ class Template(BaseModel):
"""MCP 状态 / MCP State"""
allow_anonymous_manage: Optional[bool] = None
"""是否允许匿名管理 / Whether to allow anonymous management"""
workspace_id: Optional[str] = None
"""Template 所属的工作空间标识符
/ Workspace identifier the template belongs to"""
created_at: Optional[str] = None
"""创建时间 / Creation Time"""
last_updated_at: Optional[str] = None
Expand Down
6 changes: 6 additions & 0 deletions agentrun/sandbox/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,9 @@ class TemplateInput(BaseModel):
"""磁盘大小(GB) / Disk Size (GB)"""
allow_anonymous_manage: Optional[bool] = None
"""是否允许匿名管理 / Whether to allow anonymous management"""
workspace_id: Optional[str] = None
"""Template 所属的工作空间标识符;可选项,不填则使用默认工作空间
/ Workspace identifier the template belongs to; optional, defaults to the default workspace if not provided"""

@model_validator(mode="before")
@classmethod
Expand Down Expand Up @@ -392,3 +395,6 @@ class PageableInput(BaseModel):
page_size: Optional[int] = 10
"""每页大小 / Page Size"""
template_type: Optional[TemplateType] = None
workspace_id: Optional[str] = None
"""按工作空间标识符过滤
/ Filter by workspace identifier"""
11 changes: 9 additions & 2 deletions agentrun/sandbox/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ class Template(BaseModel):
"""MCP 状态 / MCP State"""
allow_anonymous_manage: Optional[bool] = None
"""是否允许匿名管理 / Whether to allow anonymous management"""
workspace_id: Optional[str] = None
"""Template 所属的工作空间标识符
/ Workspace identifier the template belongs to"""
created_at: Optional[str] = None
"""创建时间 / Creation Time"""
last_updated_at: Optional[str] = None
Expand All @@ -115,7 +118,9 @@ async def create_async(
)

@classmethod
def create(cls, input: TemplateInput, config: Optional[Config] = None):
def create(
cls, input: TemplateInput, config: Optional[Config] = None
):
return cls.__get_client(config=config).create_template(
input, config=config
)
Expand Down Expand Up @@ -167,7 +172,9 @@ async def get_by_name_async(
)

@classmethod
def get_by_name(cls, template_name: str, config: Optional[Config] = None):
def get_by_name(
cls, template_name: str, config: Optional[Config] = None
):
return cls.__get_client(config=config).get_template(
template_name=template_name, config=config
)
Expand Down
150 changes: 150 additions & 0 deletions tests/e2e/__test_workspace_id_async_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
"""
workspace_id 跨模块 E2E 测试 / Cross-module workspace_id E2E test

验证 SDK 在 create / get / list 接口上正确传递和回填 ``workspace_id``。
Verifies the SDK correctly passes and back-fills ``workspace_id`` across
create / get / list interfaces for resource modules.

环境变量 / Environment variables:
- ``AGENTRUN_TEST_WORKSPACE_ID``:用于本测试的工作空间 ID。未配置则跳过整个文件。
Workspace ID to use for this test; the entire file is skipped if not set.
"""

import os

import pytest

from agentrun.credential import (
Credential,
CredentialClient,
CredentialConfig,
CredentialCreateInput,
CredentialListInput,
)
from agentrun.sandbox import Template
from agentrun.sandbox.model import PageableInput, TemplateInput, TemplateType
from agentrun.utils.exception import ResourceNotExistError

WORKSPACE_ID = os.getenv("AGENTRUN_TEST_WORKSPACE_ID")

pytestmark = pytest.mark.skipif(
not WORKSPACE_ID,
reason=(
"AGENTRUN_TEST_WORKSPACE_ID not configured; skipping workspace_id E2E"
),
)


class TestWorkspaceId:
"""workspace_id 跨模块 E2E 测试"""

@pytest.fixture
def credential_name(self, unique_name: str) -> str:
return f"{unique_name}-ws-cred"

@pytest.fixture
def template_name(self, unique_name: str) -> str:
return f"{unique_name}-ws-tpl"

async def test_credential_with_workspace_id_async(
self, credential_name: str
):
"""凭证创建时指定 workspace_id,回读与列举均能拿到该 workspace_id"""
client = CredentialClient()
ws = WORKSPACE_ID # type: ignore[assignment]
assert ws is not None

cred: Credential | None = None
try:
# 1. 创建带 workspace_id 的凭证
cred = await Credential.create_async(
CredentialCreateInput(
credential_name=credential_name,
description="E2E workspace_id test",
credential_config=CredentialConfig.inbound_api_key(
"sk-test-ws-e2e"
),
workspace_id=ws,
)
)
assert cred.credential_name == credential_name
assert (
cred.workspace_id == ws
), f"create 返回的 workspace_id 不匹配: {cred.workspace_id!r}"

# 2. get 接口回读 workspace_id
cred_fetched = await client.get_async(
credential_name=credential_name
)
assert (
cred_fetched.workspace_id == ws
), f"get 返回的 workspace_id 不匹配: {cred_fetched.workspace_id!r}"

# 3. list 接口按 workspace_id 过滤,本次创建的资源应在结果中
list_results = await client.list_async(
CredentialListInput(workspace_id=ws)
)
names = [item.credential_name for item in list_results]
assert credential_name in names, (
f"list(workspace_id={ws!r}) 未返回刚创建的凭证"
f" {credential_name!r},"
f"实际返回 {names!r}"
)
# 列表项的 workspace_id 也应该是同一个
for item in list_results:
if item.credential_name == credential_name:
assert item.workspace_id == ws
finally:
if cred is not None:
try:
await cred.delete_async()
except ResourceNotExistError:
pass

async def test_template_with_workspace_id_async(self, template_name: str):
"""Sandbox Template 创建时指定 workspace_id,回读与列举均能拿到该 workspace_id"""
ws = WORKSPACE_ID # type: ignore[assignment]
assert ws is not None

template: Template | None = None
try:
# 1. 创建带 workspace_id 的 Template
template = await Template.create_async(
TemplateInput(
template_name=template_name,
template_type=TemplateType.CODE_INTERPRETER,
description="E2E workspace_id test",
cpu=2.0,
memory=4096,
disk_size=512,
sandbox_idle_timeout_in_seconds=600,
sandbox_ttlin_seconds=600,
workspace_id=ws,
)
)
assert template.template_name == template_name
assert (
template.workspace_id == ws
), f"create 返回的 workspace_id 不匹配: {template.workspace_id!r}"

# 2. get 接口回读 workspace_id
template_fetched = await Template.get_by_name_async(template_name)
assert (
template_fetched.workspace_id == ws
), f"get 返回的 workspace_id 不匹配: {template_fetched.workspace_id!r}"

# 3. list 接口按 workspace_id 过滤
list_results = await Template.list_templates_async(
PageableInput(workspace_id=ws, page_size=100)
)
names = [t.template_name for t in list_results or []]
assert template_name in names, (
f"list_templates(workspace_id={ws!r}) 未返回刚创建的"
f" Template {template_name!r},实际返回 {names!r}"
)
finally:
if template is not None:
try:
await Template.delete_by_name_async(template_name)
except ResourceNotExistError:
pass
8 changes: 7 additions & 1 deletion tests/e2e/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,18 @@

def auto_load_env():
folder = Path(__file__).parent
while folder != "/":
# 一直向上查找 .env 文件,直到根目录为止
# / Walk up to root looking for a .env file
while True:
dotfile = folder / ".env"
if dotfile.exists():
load_dotenv(dotfile)
print("load .env:", dotfile)
break
if folder.parent == folder:
# 已到根目录,未找到 .env,依赖外部环境变量
# / Reached the filesystem root with no .env found; rely on env vars
break
folder = folder.parent


Expand Down
Loading
Loading