Skip to content

Commit 41e1fdf

Browse files
author
luk384090-cloud
committed
refactor(agentstudio): migrate AgentStudio sub-SDK to v2 package structure
- Move all modules from dashscope/agentstudio to dashscope/v2/agentstudio - Update all internal import paths to use the new package structure - Create shared exception base class dashscope._shared._errors.DashScopeException - Implement lazy-loading proxy wrapper for AgentStudio client for error conversion - Move validation functions validate_region and validate_workspace_id to shared module - Update client configuration to use new settings retrieval method - Modify SDK identity header generation logic to adapt to new architecture - Refactor transport layer identity to implement unified session ID management - Rewrite import paths for pagination and helper functions - Update documentation comments to reflect new architectural design changes
1 parent 94bdb3a commit 41e1fdf

41 files changed

Lines changed: 868 additions & 368 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎dashscope/_shared/_errors.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright (c) Alibaba, Inc. and its affiliates.
3+
"""The one exception base class both stacks inherit from.
4+
5+
``except dashscope.common.error.DashScopeException`` has to catch errors
6+
raised by the 1.x API classes *and* by the AgentStudio sub-SDK, which the
7+
2.0 client mounts directly. That only works if every subclass in both
8+
stacks derives from the same class object, so the base lives here rather
9+
than in either stack: copying it would silently split the hierarchy and
10+
let errors escape a caller's ``except`` clause.
11+
12+
``dashscope.common.error`` re-exports this name, so the 1.x import path
13+
keeps working and keeps the identical class.
14+
"""
15+
16+
17+
class DashScopeException(Exception):
18+
"""Base class for every error the dashscope SDK raises."""
19+
20+
21+
__all__ = ["DashScopeException"]

‎dashscope/_shared/_sdk_ident.py‎

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright (c) Alibaba, Inc. and its affiliates.
3+
"""SDK identification headers shared by the 1.x API and the 2.0 package.
4+
5+
``x-dashscope-sdk-client`` tells the backend which client and which
6+
functional area issued a request, and ``x-dashscope-sdk-session-id``
7+
groups every request one interpreter process makes.
8+
9+
The client segment is process-wide *mutable* state: entry points that
10+
embed the SDK overwrite it (the ``dashscope`` CLI marks itself as
11+
``python-cli`` via ``set_sdk_client``), and every caller in both stacks
12+
must see the same value. Duplicating it would silently drop the override
13+
for whichever stack held the copy, so it lives here -- one source of
14+
truth, same reason as the session id in ``_shared._sdk_session``.
15+
16+
The 2.0 transport builds its own header in
17+
``dashscope.v2.transport._ident`` because it appends a trailing
18+
generation marker (``aigc`` -> ``aigc2``) that this unmarked 1.x shape
19+
must not carry. Sub-SDKs that predate the marker -- AgentStudio, whose
20+
``agentstudio`` tag the backend already reports on -- keep using this
21+
module.
22+
23+
Only the stdlib is imported at module level; ``dashscope.version`` is
24+
pulled in lazily and defensively so this leaf stays below both stacks.
25+
"""
26+
27+
import os
28+
from typing import Dict
29+
30+
from dashscope._shared._sdk_session import sdk_session_id
31+
32+
_SDK_CLIENT = "python-sdk"
33+
34+
# Shared with the 2.0 transport so one process reports one
35+
# x-dashscope-sdk-session-id across both stacks (see _shared._sdk_session;
36+
# acli deliberately keeps its own, it is a one-way sync copy).
37+
_SDK_SESSION_ID = sdk_session_id()
38+
39+
__all__ = ["get_sdk_headers", "set_sdk_client"]
40+
41+
42+
def _sdk_version() -> str:
43+
try:
44+
from dashscope.version import __version__
45+
except Exception: # pragma: no cover - defensive import guard
46+
return "0.0.0"
47+
return __version__
48+
49+
50+
# Read once at import so the value is stable for the process lifetime.
51+
_VERSION = _sdk_version()
52+
53+
54+
def set_sdk_client(client: str) -> None:
55+
"""Override the client identifier in the x-dashscope-sdk-client header.
56+
57+
Process-wide, used by entry points that embed the SDK (e.g. the
58+
``dashscope`` CLI marks itself as ``python-cli``).
59+
"""
60+
global _SDK_CLIENT # pylint: disable=global-statement
61+
if client:
62+
_SDK_CLIENT = client
63+
64+
65+
def get_sdk_headers(module: str = "") -> Dict[str, str]:
66+
"""Structured SDK identification headers for backend statistics.
67+
68+
Set DASHSCOPE_DISABLE_SDK_HEADERS=1 to omit them.
69+
70+
Args:
71+
module: Optional module name (e.g. agentstudio, rl, app).
72+
"""
73+
if os.environ.get("DASHSCOPE_DISABLE_SDK_HEADERS"):
74+
return {}
75+
parts = [_SDK_CLIENT, _VERSION]
76+
if module:
77+
parts.append(module)
78+
return {
79+
"x-dashscope-sdk-client": "/".join(parts),
80+
"x-dashscope-sdk-session-id": _SDK_SESSION_ID,
81+
}

‎dashscope/_shared/_sdk_session.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
The ``x-dashscope-sdk-session-id`` header lets server-side access logs
66
group every request one SDK session makes. A process can emit requests
77
through both stacks at once -- the 1.x API classes and the 2.0 transport
8-
-- and the 2.0 client even delegates some calls (rl, agentstudio)
9-
straight into 1.x. If each stack minted its own id, one logical session
10-
would split across several ids and the grouping breaks.
8+
-- and the 2.0 client still delegates some calls (rl) straight into 1.x.
9+
If each stack minted its own id, one logical session would split across
10+
several ids and the grouping breaks.
1111
1212
This leaf module is the one source of truth both import. It depends only
1313
on the stdlib so it can be pulled in from anywhere without dragging

‎dashscope/_shared/_validation.py‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright (c) Alibaba, Inc. and its affiliates.
3+
"""Host-label validation shared by the 1.x API and the 2.0 implementation.
4+
5+
``workspace_id`` and ``region`` are interpolated into the host of the MaaS
6+
endpoint, so both stacks must reject the same malformed values with the
7+
same rule. Keeping the check in one leaf module means a value that is
8+
safe for one stack is safe for the other, and neither stack has to import
9+
the other to get it.
10+
11+
Stdlib only, like the rest of ``dashscope._shared``.
12+
"""
13+
14+
import re
15+
16+
# workspace_id and region become DNS labels of the MaaS endpoint host, so
17+
# they must not contain characters (/, ?, #, @, :, whitespace, ...) that
18+
# could break out of the host and redirect requests elsewhere.
19+
# \A...\Z rather than ^...$: $ also matches before a trailing newline.
20+
_WORKSPACE_ID_PATTERN = re.compile(r"\A[A-Za-z0-9][A-Za-z0-9_-]{0,63}\Z")
21+
# RFC 1123 hostname label: 1-63 chars, no leading/trailing hyphen. Upper
22+
# case is allowed because DNS is case-insensitive and httpx lowercases the
23+
# host, so rejecting it would break working callers for no security gain.
24+
_REGION_PATTERN = re.compile(
25+
r"\A[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\Z",
26+
)
27+
28+
__all__ = ["validate_region", "validate_workspace_id"]
29+
30+
31+
def validate_workspace_id(workspace_id):
32+
"""Validate that workspace_id is safe to use as a URL subdomain.
33+
34+
Raises:
35+
ValueError: If workspace_id contains characters outside
36+
[A-Za-z0-9_-], is empty, or exceeds 64 characters.
37+
"""
38+
if not workspace_id or not _WORKSPACE_ID_PATTERN.fullmatch(
39+
workspace_id,
40+
):
41+
raise ValueError(
42+
f"Invalid workspace_id {workspace_id!r}: must match "
43+
"^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$",
44+
)
45+
return workspace_id
46+
47+
48+
def validate_region(region):
49+
"""Validate that region is safe to use as a URL subdomain.
50+
51+
Deliberately a hostname-label check and not a MAAS_REGIONS whitelist:
52+
rejecting an unknown-but-well-formed region would break callers as soon
53+
as a subpackage supports a region this list has not caught up with.
54+
55+
Raises:
56+
ValueError: If region is not a valid hostname label.
57+
"""
58+
if not region or not _REGION_PATTERN.fullmatch(region):
59+
raise ValueError(
60+
f"Invalid region {region!r}: must be a hostname label "
61+
"(1-63 chars of [A-Za-z0-9-], not starting or ending "
62+
"with '-')",
63+
)
64+
return region

‎dashscope/agentstudio/__init__.py‎

Lines changed: 49 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -1,164 +1,58 @@
11
# -*- coding: utf-8 -*-
22
# Copyright (c) Alibaba, Inc. and its affiliates.
3-
"""AgentStudio sub-SDK for the dashscope Python client.
3+
"""Compatibility shim; the implementation moved to ``dashscope.v2.agentstudio``.
44
5-
The ``agentstudio`` package wraps the AgentStudio managed-agent HTTP
6-
API. All resource classes are accessed through a client instance::
5+
The AgentStudio sub-SDK is owned by the 2.0 package. This module keeps the
6+
historical import path working: every public name is re-exported, and every
7+
submodule is aliased in ``sys.modules`` so deep imports such as
8+
``from dashscope.agentstudio.resources.agents import Agents`` resolve to the
9+
very same module object -- and therefore the very same classes -- as the
10+
``dashscope.v2.agentstudio`` path.
711
8-
from dashscope.agentstudio import Client
9-
from dashscope.agentstudio.types import (
10-
user_message, user_tool_result,
11-
)
12-
13-
Example
14-
-------
15-
>>> from dashscope.agentstudio import Client
16-
>>> from dashscope.agentstudio.types import user_message
17-
>>> client = Client(api_key="sk-xxx")
18-
>>> agent = client.agents.create(name="demo", model="qwen-plus")
19-
>>> session = client.sessions.create(agent=agent.id)
20-
>>> client.sessions.events.send(session.id, [user_message("Hello!")])
21-
>>> with client.sessions.events.stream(session.id) as stream:
22-
... for event in stream:
23-
... print(event.type, event.to_dict())
24-
... if event.type == "session_status":
25-
... break
12+
Import from ``dashscope.v2.agentstudio`` in new code.
2613
"""
2714

28-
# yapf: disable
15+
import pkgutil
16+
import sys
2917

30-
from .client import Client, AsyncClient
31-
from .transport import APIResponse, RequestOptions
32-
from . import types
33-
from .exceptions import (
34-
AgentStudioError,
35-
APIConnectionError,
36-
APIStatusError,
37-
APITimeoutError,
38-
AuthenticationError,
39-
ConflictError,
40-
InternalServerError,
41-
InvalidRequestError,
42-
NotFoundError,
43-
OverloadedError,
44-
PermissionDeniedError,
45-
RateLimitError,
46-
StreamClosedError,
47-
StreamError,
48-
)
49-
from .pagination import (
50-
AsyncCursorPage,
51-
CursorPage,
52-
)
53-
from .types import (
54-
Memory,
55-
MemoryStore,
56-
MemoryVersion,
57-
Message,
58-
PermissionPolicy,
59-
SecurityAlert,
60-
SecurityAlertList,
61-
SecurityOverview,
62-
ServerEvent,
63-
SessionResource,
64-
StopReason,
65-
user_define_outcome,
66-
user_interrupt,
67-
user_message,
68-
user_tool_approval_response,
69-
user_custom_tool_result,
70-
user_tool_result,
71-
)
72-
from .types.params import (
73-
AgentCreateParams,
74-
AgentUpdateParams,
75-
SessionCreateParams,
76-
SessionUpdateParams,
77-
EnvironmentCreateParams,
78-
EnvironmentUpdateParams,
79-
DeploymentCreateParams,
80-
DeploymentUpdateParams,
81-
SessionEventSendParams,
82-
WebhookEndpointCreateParams,
83-
WebhookEndpointUpdateParams,
84-
WebhookEventListParams,
85-
)
86-
from .constants import (
87-
PermissionPolicyType,
88-
SSEEventType,
89-
SessionStatus,
90-
StopReasonType,
91-
WebhookDeliveryStatus,
92-
WebhookDisabledReason,
93-
WebhookEventType,
94-
WebhookStatus,
18+
# Importing the package runs its own eager imports, so every submodule is
19+
# already in sys.modules by the time the walk below starts.
20+
from dashscope.v2 import agentstudio as _impl
21+
22+
# pylint: disable-next=wildcard-import,unused-wildcard-import
23+
from dashscope.v2.agentstudio import * # noqa: F401,F403
24+
25+
# The wildcard only carries the names in the implementation's __all__, which
26+
# lists classes and helpers but not its submodules. Naming them here keeps
27+
# ``from dashscope.agentstudio import exceptions`` resolvable statically; at
28+
# runtime they are already imported by the package's own eager imports.
29+
# pylint: disable-next=unused-import
30+
from dashscope.v2.agentstudio import ( # noqa: F401
31+
client,
32+
constants,
33+
exceptions,
34+
pagination,
35+
resources,
36+
streaming,
37+
transport,
38+
types,
9539
)
9640

97-
__all__ = [
98-
# client
99-
"Client",
100-
"AsyncClient",
101-
# base types
102-
"RequestOptions",
103-
"APIResponse",
104-
# exceptions
105-
"AgentStudioError",
106-
"APIConnectionError",
107-
"APIStatusError",
108-
"APITimeoutError",
109-
"AuthenticationError",
110-
"ConflictError",
111-
"InternalServerError",
112-
"InvalidRequestError",
113-
"NotFoundError",
114-
"OverloadedError",
115-
"PermissionDeniedError",
116-
"RateLimitError",
117-
"StreamError",
118-
"StreamClosedError",
119-
# pagination
120-
"CursorPage",
121-
"AsyncCursorPage",
122-
# unified message type
123-
"Memory",
124-
"MemoryStore",
125-
"MemoryVersion",
126-
"Message",
127-
"PermissionPolicy",
128-
"SecurityAlert",
129-
"SecurityAlertList",
130-
"SecurityOverview",
131-
"ServerEvent",
132-
"SessionResource",
133-
"StopReason",
134-
# client event helpers (re-exported for convenience)
135-
"user_message",
136-
"user_interrupt",
137-
"user_tool_approval_response",
138-
"user_custom_tool_result",
139-
"user_tool_result",
140-
"user_define_outcome",
141-
# typing module
142-
"types",
143-
# params (commonly used request-body types)
144-
"AgentCreateParams",
145-
"AgentUpdateParams",
146-
"SessionCreateParams",
147-
"SessionUpdateParams",
148-
"EnvironmentCreateParams",
149-
"EnvironmentUpdateParams",
150-
"DeploymentCreateParams",
151-
"DeploymentUpdateParams",
152-
"SessionEventSendParams",
153-
"WebhookEndpointCreateParams",
154-
"WebhookEndpointUpdateParams",
155-
"WebhookEventListParams",
156-
"WebhookStatus",
157-
"WebhookDisabledReason",
158-
"WebhookDeliveryStatus",
159-
"WebhookEventType",
160-
"SSEEventType",
161-
"SessionStatus",
162-
"StopReasonType",
163-
"PermissionPolicyType",
164-
]
41+
_V2_PACKAGE = _impl.__name__
42+
43+
__all__ = list(_impl.__all__)
44+
45+
# Alias each submodule under the old dotted path. Registering in
46+
# sys.modules alone is not enough: the import system skips a module it
47+
# finds there, so the attribute on the parent would never be set and
48+
# ``dashscope.agentstudio.resources`` would raise AttributeError.
49+
for _info in pkgutil.walk_packages(_impl.__path__, prefix=_V2_PACKAGE + "."):
50+
_module = sys.modules.get(_info.name)
51+
if _module is None: # pragma: no cover - walk_packages imports as it goes
52+
continue
53+
_alias = __name__ + _info.name[len(_V2_PACKAGE) :]
54+
sys.modules.setdefault(_alias, _module)
55+
_parent_name, _, _leaf = _alias.rpartition(".")
56+
_parent = sys.modules.get(_parent_name)
57+
if _parent is not None:
58+
setattr(_parent, _leaf, _module)

0 commit comments

Comments
 (0)