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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "uv_build"

[project]
name = "agentarts-sdk"
version = "0.1.3"
version = "0.1.3dev2026070701"
description = "Huawei Cloud AgentArts SDK - Build, deploy and manage AI agents with cloud capabilities"
readme = "README.md"
license = {file = "LICENSE"}
Expand Down
4 changes: 2 additions & 2 deletions src/agentarts/sdk/memory/async_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ async def _ensure_initialized(self) -> None:

if self._pending_session_id is None:
session_config = SessionCreateRequest(actor_id=self.actor_id)
session_info = await self._async_data_plane.create_memory_session(self.space_id, session_config.to_dict())
session_info = await self._async_data_plane.create_memory_session(self.space_id, session_config)
self.session_id = session_info.id
if not self.session_id:
msg = f"Failed to create session: {session_info}"
Expand Down Expand Up @@ -191,7 +191,7 @@ async def get_message(self, message_id: str) -> MessageInfo:
"""Get a specific message - identical to sync version."""
await self._ensure_initialized()
logger.info(f"Getting message: {message_id}")
return await self._async_data_plane.get_message(self.space_id, self.session_id, message_id)
return await self._async_data_plane.get_message(message_id, self.space_id, self.session_id)

async def search_memories(
self,
Expand Down
4 changes: 2 additions & 2 deletions src/agentarts/sdk/memory/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ def __init__(
# If session_id is not provided, call backend API to create a new session
if session_id is None:
session_config = SessionCreateRequest(actor_id=actor_id)
session_info = self._data_plane.create_memory_session(space_id, session_config.to_dict())
session_info = self._data_plane.create_memory_session(space_id, session_config)
self.session_id = session_info.id
if not self.session_id:
msg = f"Failed to create session: {session_info}"
Expand Down Expand Up @@ -333,7 +333,7 @@ def get_message(self, message_id: str) -> MessageInfo:
Requires HUAWEICLOUD_SDK_MEMORY_API_KEY environment variable to be set
"""
logger.info(f"Getting message: {message_id}")
return self._data_plane.get_message(self.space_id, self.session_id, message_id)
return self._data_plane.get_message(message_id, self.space_id, self.session_id)

def search_memories(
self,
Expand Down
11 changes: 11 additions & 0 deletions src/agentarts/sdk/service/http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
``iter_lines()`` or ``iter_bytes()`` to consume the body incrementally.
"""

import logging
from collections.abc import Iterator
from dataclasses import dataclass, field
from enum import Enum
Expand All @@ -24,6 +25,8 @@
from agentarts.sdk.utils.signer import SDKSigner
from agentarts.sdk.utils.signer_v11 import V11Signer

logger = logging.getLogger(__name__)

_STREAM_CONTENT_TYPES = {"text/event-stream", "application/x-ndjson"}


Expand Down Expand Up @@ -262,6 +265,14 @@ def _sign_request_v11(self, method: str, full_url: str, **kwargs) -> dict:
headers=headers
)

logger.debug(
"V11 request: %s %s params=%s content-type=%s",
method,
full_url,
query_params,
headers.get("Content-Type"),
)

if "headers" not in kwargs or kwargs["headers"] is None:
kwargs["headers"] = {}
kwargs["headers"] = headers
Expand Down
19 changes: 16 additions & 3 deletions src/agentarts/sdk/service/runtime_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ def create_agent(
agent_gateway_id: str | None = None,
invoke_config: dict | None = None,
observability_config: dict | None = None,
storage_config: dict | None = None,
tags_config: list[dict] | None = None,
**extra: Any,
) -> dict[str, Any]:
Expand All @@ -284,6 +285,7 @@ def create_agent(
agent_gateway_id: ID of the agent gateway to attach.
invoke_config: Invocation-related configuration.
observability_config: Observability (tracing, metrics) configuration.
storage_config: Storage (e.g. SFS Turbo) configuration.
tags_config: Tags as list of {"key": "K", "value": "V"} dicts.
**extra: Additional fields forwarded to the API.

Expand Down Expand Up @@ -312,6 +314,8 @@ def create_agent(
payload["invoke_config"] = invoke_config
if observability_config is not None:
payload["observability"] = observability_config
if storage_config is not None:
payload["storage_config"] = storage_config
if tags_config is not None:
payload["tags"] = tags_config

Expand All @@ -329,6 +333,7 @@ def update_agent(
agent_gateway_id: str | None = None,
invoke_config: dict | None = None,
observability_config: dict | None = None,
storage_config: dict | None = None,
tags_config: list[dict] | None = None,
**extra: Any,
) -> dict[str, Any]:
Expand All @@ -345,6 +350,7 @@ def update_agent(
agent_gateway_id: ID of the agent gateway to attach.
invoke_config: Invocation-related configuration.
observability_config: Observability (tracing, metrics) configuration.
storage_config: Storage (e.g. SFS Turbo) configuration.
tags_config: Tags as list of {"key": "K", "value": "V"} dicts.
**extra: Additional fields forwarded to the API.

Expand All @@ -371,6 +377,8 @@ def update_agent(
payload["invoke_config"] = invoke_config
if observability_config is not None:
payload["observability"] = observability_config
if storage_config is not None:
payload["storage_config"] = storage_config
if tags_config is not None:
payload["tags"] = tags_config

Expand All @@ -389,6 +397,7 @@ def create_or_update_agent(
agent_gateway_id: str | None = None,
invoke_config: dict | None = None,
observability_config: dict | None = None,
storage_config: dict | None = None,
tags_config: list[dict] | None = None,
**extra: Any,
) -> dict[str, Any]:
Expand All @@ -410,6 +419,7 @@ def create_or_update_agent(
agent_gateway_id: ID of the agent gateway to attach.
invoke_config: Invocation-related configuration.
observability_config: Observability (tracing, metrics) configuration.
storage_config: Storage (e.g. SFS Turbo) configuration.
tags_config: Tags as list of {"key": "K", "value": "V"} dicts.
**extra: Additional fields forwarded to the API.

Expand All @@ -432,6 +442,7 @@ def create_or_update_agent(
agent_gateway_id=agent_gateway_id,
invoke_config=invoke_config,
observability_config=observability_config,
storage_config=storage_config,
tags_config=tags_config,
**extra,
)
Expand All @@ -448,6 +459,7 @@ def create_or_update_agent(
agent_gateway_id=agent_gateway_id,
invoke_config=invoke_config,
observability_config=observability_config,
storage_config=storage_config,
tags_config=tags_config,
**extra,
)
Expand Down Expand Up @@ -742,7 +754,8 @@ def exec_command(
bearer_token: Optional bearer token for authentication.
endpoint: Optional endpoint name.
user_id: Optional user ID for OAuth2 outbound credentials.
timeout: Request timeout in seconds.
timeout: Command execution timeout in seconds (sent in the request body).
The HTTP client timeout is set to timeout + 60s to allow for overhead.

Returns:
dict for normal mode, Iterator[str] for chunked mode (ndjson lines).
Expand All @@ -765,14 +778,14 @@ def exec_command(
if endpoint:
params["endpoint"] = endpoint

payload = {"command": command}
payload = {"command": command, "timeout": timeout}
result = self._data(
"POST",
path,
json=payload,
params=params if params else None,
headers=headers,
timeout=timeout,
timeout=timeout + 60,
)

if not result.success:
Expand Down
23 changes: 21 additions & 2 deletions src/agentarts/sdk/utils/signer_v11.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import hashlib
import hmac
import logging
from datetime import datetime, timezone
from urllib.parse import quote, unquote

Expand All @@ -12,6 +13,8 @@
DATE_FORMAT = "%Y%m%dT%H%M%SZ"
ALGORITHM = "V11-HMAC-SHA256"

logger = logging.getLogger(__name__)


class V11Signer:
"""V11-HMAC-SHA256 signer for Huawei Cloud API requests."""
Expand Down Expand Up @@ -85,9 +88,13 @@ def _canonical_query_string(self, query_params: dict | None) -> str:
if isinstance(value, list):
sorted_values = sorted(str(v) for v in value)
for v in sorted_values:
arr.append(f"{ke}={self._urlencode(v)}")
# Values keep '/' unencoded: the data-plane gateway decodes
# the wire query and treats '/' as safe when re-canonicalising,
# so the signature must match that form (e.g. upload's `path`
# value like /home/user/test.txt). Keys never contain '/'.
arr.append(f"{ke}={quote(str(v), safe='~/')}")
else:
arr.append(f"{ke}={self._urlencode(str(value))}")
arr.append(f"{ke}={quote(str(value), safe='~/')}")
return "&".join(arr)

def _canonical_headers(self, headers: dict[str, str], signed_headers: list[str]) -> str:
Expand Down Expand Up @@ -175,6 +182,18 @@ def sign(
signature = self._sign_string_to_sign(real_use_secret, string_to_sign)
auth_value = self._get_auth_header_value(signed_headers, signature)

logger.debug(
"V11 sign: method=%s path=%s query=%s signed_headers=%s\n"
"canonical_request:\n%s\nstring_to_sign:\n%s\nauthorization=%s",
method,
path,
self._canonical_query_string(query_params),
";".join(signed_headers),
canonical_request,
string_to_sign,
auth_value,
)

headers["Authorization"] = auth_value

return headers
Expand Down
2 changes: 1 addition & 1 deletion src/agentarts/toolkit/cli/runtime/exec_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
console = Console()

DEFAULT_TIMEOUT = 60
MAX_TIMEOUT = 300
MAX_TIMEOUT = 3600


def validate_timeout(timeout: int) -> int:
Expand Down
3 changes: 3 additions & 0 deletions src/agentarts/toolkit/operations/runtime/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
AgentArtsConfigList,
BaseConfig,
SWRConfig,
detect_arch,
)
from agentarts.toolkit.utils.swr_org import generate_swr_org_name

Expand Down Expand Up @@ -230,13 +231,15 @@ def add_agent(
agent_config = AgentArtsConfig.from_dict(existing_dict)
else:
detected_platform = detect_platform()
detected_arch = detect_arch()
agent_config = AgentArtsConfig(
base=BaseConfig(
name=name,
entrypoint=entrypoint,
region=region,
dependency_file=dependency_file,
platform=detected_platform,
arch=detected_arch,
),
swr_config=SWRConfig(
organization=swr_organization,
Expand Down
18 changes: 18 additions & 0 deletions src/agentarts/toolkit/operations/runtime/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ def create_agentarts_runtime(
network_config = None
identity_config = None
observability_config = None
storage_config = None
env_vars = None
tags_config = None
execution_agency_name = None
Expand Down Expand Up @@ -111,6 +112,22 @@ def create_agentarts_runtime(
if runtime_cfg.observability:
observability_config = runtime_cfg.observability.to_dict()

if runtime_cfg.storage_config:
sc = runtime_cfg.storage_config.to_dict()
# to_dict() returns {"sfs_turbo": [{...}]} (list) or {} (unconfigured).
# Only enforce required fields when the user explicitly opted
# into SFS Turbo by providing sfs_turbo_id. An unset/all-null
# storage_config (e.g. from `agentarts init` / `agentarts config`)
# is treated as "not configured" so deploy is never blocked.
st_list = sc.get("sfs_turbo") or []
if st_list:
st = st_list[0]
if st.get("sfs_turbo_id"):
if not st.get("mount_path"):
msg = "storage_config.sfs_turbo.mount_path is required when sfs_turbo_id is set"
raise ValueError(msg)
storage_config = sc

if runtime_cfg.environment_variables:
env_vars = [{"key": kv.key, "value": kv.value} for kv in runtime_cfg.environment_variables if kv.value]

Expand Down Expand Up @@ -144,6 +161,7 @@ def create_agentarts_runtime(
network_config=network_config,
identity_config=identity_config,
observability_config=observability_config,
storage_config=storage_config,
env_vars=env_vars,
tags_config=tags_config,
execution_agency_name=execution_agency_name,
Expand Down
4 changes: 2 additions & 2 deletions src/agentarts/toolkit/operations/runtime/exec_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from agentarts.toolkit.utils.common import echo_error

DEFAULT_TIMEOUT = 60
MAX_TIMEOUT = 300
MAX_TIMEOUT = 3600


def exec_runtime_command(
Expand Down Expand Up @@ -38,7 +38,7 @@ def exec_runtime_command(
endpoint: Optional endpoint name
skip_ssl_verification: Skip SSL certificate verification
user_id: Optional user ID for OAuth2 outbound credentials
timeout: Request timeout in seconds (default: 60, max: 300)
timeout: Request timeout in seconds (default: 60, max: 3600)

Returns:
dict for normal mode, Iterator[str] for chunked mode
Expand Down
8 changes: 8 additions & 0 deletions src/agentarts/toolkit/operations/runtime/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,16 @@ def create_config_file(

artifact_source:
url: null # Auto-generated during deploy from swr_config
swr_instance_id: null # UUID format, optional
commands: []

storage_config:
sfs_turbo:
sfs_turbo_id: null # UUID format, required when using SFS Turbo
sfs_path: null
mount_path: null # required when using SFS Turbo
read_only: false

environment_variables:{env_vars_yaml}

tags: []
Expand Down
Loading
Loading