Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ description: Quick reference for Agent APIs (agentstudio-hosted Agent/Applicatio
### agentstudio (`from dashscope.agentstudio import Client, AsyncClient`)
- `Client(api_key=, workspace=, region=None, base_url=None, uid=None, timeout=None, max_retries=2)`; if base_url is not given, `workspace` (or env var `DASHSCOPE_WORKSPACE`) is required; base_url can also be set via `DASHSCOPE_AGENTSTUDIO_URL`/`AGENTSTUDIO_URL`; default region `cn-beijing`, timeout 600s.
- Resource entry points: `client.agents / .sessions / .environments / .files / .skills / .vaults / .deployments / .deployment_runs`.
- `agents.create(*, name, model, description=None, system_prompt=None, tools=None, mcp_servers=None, skills=None, metadata=None)` → `Agent`; `retrieve(agent_id, *, version=None)` (alias `get`); `update(agent_id, *, version, ...)` (version must equal the server's current version, otherwise HTTP 409); `archive(agent_id)`; `list(*, limit, page, include_archived)` → `CursorPage`; `list_versions(agent_id)`.
- `agents.create(*, name, model, description=None, system_prompt=None, tools=None, mcp_servers=None, skills=None, multiagent=None, metadata=None)` → `Agent`; `model` is a `ModelConfig(id=…, temperature=…, max_tokens=…, enable_thinking=…, thinking_budget=…)` (bare id string = `ModelConfig(id=…)`); `retrieve(agent_id, *, version=None)` (alias `get`); `update(agent_id, *, version, name, model=None, ...)` — version must equal the current version (HTTP 409), `name` is required (`AGENT_005`), and a sent `model` replaces the whole config (keep the rest via `agent.model.copy_with(...)`); `archive(agent_id)`; `list(*, limit, page, include_archived)` → `CursorPage`; `list_versions(agent_id)`.
- `sessions.create(*, agent, environment_id=None, title=None, resources=None, metadata=None)` → `Session`; also `retrieve/update(title, metadata)/list(*, limit, page, agent_id, statuses, created_at_gt/gte/lt/lte)/archive/delete`.
- `environments.create(*, name, config, description=None, scope="organization", metadata=None)`; `files.upload(file, *, mime_type=None, progress=None)` (file accepts a path/binary stream/(name, fileobj)); `skills.create(*, file_id=None, file=None, mime_type=None)`, `skills.versions.create(skill_id, ...)`.

Expand Down
2 changes: 2 additions & 0 deletions dashscope/agentstudio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
MemoryStore,
MemoryVersion,
Message,
ModelConfig,
PermissionPolicy,
SecurityAlert,
SecurityAlertList,
Expand Down Expand Up @@ -124,6 +125,7 @@
"MemoryStore",
"MemoryVersion",
"Message",
"ModelConfig",
"PermissionPolicy",
"SecurityAlert",
"SecurityAlertList",
Expand Down
17 changes: 13 additions & 4 deletions dashscope/agentstudio/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,18 @@ class Client:

Usage::

from dashscope.agentstudio import Client
from dashscope.agentstudio import Client, ModelConfig

client = Client(api_key="sk-xxx", workspace="my-ws")
agent = client.agents.create(name="demo", model="qwen-max")
agent = client.agents.create(
name="demo",
model=ModelConfig(
id="qwen-max",
temperature=0.7,
enable_thinking=True,
thinking_budget=2048,
),
)
session = client.sessions.create(agent=agent.id)
with client.sessions.events.stream(session.id) as stream:
for event in stream:
Expand Down Expand Up @@ -173,11 +181,12 @@ class AsyncClient:

Usage::

from dashscope.agentstudio import AsyncClient
from dashscope.agentstudio import AsyncClient, ModelConfig

client = AsyncClient(api_key="sk-xxx", workspace="my-ws")
agent = await client.agents.create(
name="demo", model="qwen-max",
name="demo",
model=ModelConfig(id="qwen-max", max_tokens=4096),
)
session = await client.sessions.create(agent=agent.id)
async with client.sessions.events.stream(session.id) as stream:
Expand Down
18 changes: 10 additions & 8 deletions dashscope/agentstudio/resources/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from __future__ import annotations

from typing import Any, Mapping, Optional, Sequence
from typing import Any, Mapping, Optional, Sequence, Union

from dashscope.agentstudio.pagination import (
AsyncCursorPage,
Expand All @@ -15,7 +15,7 @@
_coerce_agent,
_coerce_agent_version,
)
from dashscope.agentstudio.types import Agent, AgentVersion
from dashscope.agentstudio.types import Agent, AgentVersion, ModelConfig
from dashscope.agentstudio.types.params import (
AgentCreateParams,
AgentListParams,
Expand All @@ -36,7 +36,7 @@ def create(
self,
*,
name: str,
model: str,
model: Union[str, ModelConfig],
description: Optional[str] = None,
system_prompt: Optional[str] = None,
tools: Optional[Sequence[Mapping[str, Any]]] = None,
Expand Down Expand Up @@ -85,8 +85,8 @@ def update(
*,
version: int,
name: Optional[str] = None,
model: Optional[Union[str, ModelConfig]] = None,
description: Optional[str] = None,
model: Optional[str] = None,
system_prompt: Optional[str] = None,
tools: Optional[Sequence[Mapping[str, Any]]] = None,
mcp_servers: Optional[Sequence[Mapping[str, Any]]] = None,
Expand All @@ -99,12 +99,13 @@ def update(
``version`` must equal the server's current version;
the server rejects mismatches with HTTP 409.
Retrieve the agent first to obtain the current version.
``name`` is required too (``AGENT_005`` without it).
"""
body = AgentUpdateParams(
version=version,
name=name,
description=description,
model=model,
description=description,
system_prompt=system_prompt,
tools=tools,
mcp_servers=mcp_servers,
Expand Down Expand Up @@ -189,7 +190,7 @@ async def create(
self,
*,
name: str,
model: str,
model: Union[str, ModelConfig],
description: Optional[str] = None,
system_prompt: Optional[str] = None,
tools: Optional[Sequence[Mapping[str, Any]]] = None,
Expand Down Expand Up @@ -242,8 +243,8 @@ async def update(
*,
version: int,
name: Optional[str] = None,
model: Optional[Union[str, ModelConfig]] = None,
description: Optional[str] = None,
model: Optional[str] = None,
system_prompt: Optional[str] = None,
tools: Optional[Sequence[Mapping[str, Any]]] = None,
mcp_servers: Optional[Sequence[Mapping[str, Any]]] = None,
Expand All @@ -256,12 +257,13 @@ async def update(
``version`` must equal the server's current version;
the server rejects mismatches with HTTP 409.
Retrieve the agent first to obtain the current version.
``name`` is required too (``AGENT_005`` without it).
"""
body = AgentUpdateParams(
version=version,
name=name,
description=description,
model=model,
description=description,
system_prompt=system_prompt,
tools=tools,
mcp_servers=mcp_servers,
Expand Down
78 changes: 54 additions & 24 deletions dashscope/agentstudio/resources/session_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@
)


def _terminal_status(event: Any) -> bool:
"""True when a ``session_status`` event reports that the run is over."""
if getattr(event, "type", None) != SSEEventType.SESSION_STATUS:
return False
block = event.content[0] if event.content else None
data = getattr(block, "data", None) or {}
return data.get("session_status") in _TERMINAL_STATUSES


class SessionEvents:
"""Session event send / list / stream."""

Expand Down Expand Up @@ -125,9 +134,13 @@ def stream(
) -> "_TypedEventStream":
"""Open the SSE stream and return an iterator of typed events.

``event_deltas`` opts into incremental text streaming for the given
event types (``"message"`` and/or ``"reasoning"``; aliases
``"agent.message"`` / ``"agent.thinking"``).
``event_deltas`` opts into incremental streaming for the given event
types (``"message"`` and/or ``"reasoning"``; aliases ``"agent.message"``
/ ``"agent.thinking"``); anything else the server rejects. The stream is
read once, so ``text_deltas`` can be taken from it only; a second pass
raises rather than returning nothing. A chunk stream is not a reliable
source for final text: a reconnect replays only persisted events, so use
``text_stream``.
"""
params: Optional[Dict[str, Any]] = None
if event_deltas:
Expand All @@ -146,7 +159,13 @@ def stream(


class _TypedEventStream:
"""Wrap :class:`EventStream` so iterating yields :class:`ServerEvent`."""
"""Wrap :class:`EventStream` so iterating yields :class:`ServerEvent`.

A stream is read once: a second pass would silently yield nothing, so it
raises instead. Call ``sessions.events.stream()`` again to read the events.
"""

_passed = False

def __init__(self, stream: EventStream) -> None:
self._stream = stream
Expand Down Expand Up @@ -179,6 +198,15 @@ def __exit__(self, *exc: Any) -> None:
self.close()

def __iter__(self) -> Iterator[ServerEvent]:
if self._passed:
raise RuntimeError(
"this stream is single-pass and was already consumed; call "
"sessions.events.stream() again to read it again",
)
self._passed = True
return self._iter()

def _iter(self) -> Iterator[ServerEvent]:
for payload in self._stream:
yield _coerce_event(payload)

Expand All @@ -196,30 +224,27 @@ def text_stream(self):
text = getattr(block, "text", "")
if text:
yield text
elif getattr(event, "type", None) == SSEEventType.SESSION_STATUS:
block = event.content[0] if event.content else None
d = getattr(block, "data", None) or {}
if d.get("session_status") in _TERMINAL_STATUSES:
return
elif _terminal_status(event):
return

@property
def text_deltas(self):
"""Iterate over incremental text chunks from ``event_delta`` frames.

Requires the stream to be opened with ``event_deltas``; otherwise
yields nothing (use :attr:`text_stream` for terminal full text).
yields nothing (use :attr:`text_stream` for terminal full text, which is
also what a reconnect can rebuild — chunks are not replayed).
"""
return self._text_deltas()

def _text_deltas(self):
for event in self:
etype = getattr(event, "type", None)
if etype == "event_delta":
if getattr(event, "type", None) == "event_delta":
text = event.delta_text
if text:
yield text
elif etype == SSEEventType.SESSION_STATUS:
block = event.content[0] if event.content else None
d = getattr(block, "data", None) or {}
if d.get("session_status") in _TERMINAL_STATUSES:
return
elif _terminal_status(event):
return

def close(self) -> None:
self._stream.close()
Expand Down Expand Up @@ -331,7 +356,15 @@ async def __aenter__(self) -> "_AioTypedEventStream":
async def __aexit__(self, *exc: Any) -> None:
await self.aclose()

_passed = False

def __aiter__(self) -> AsyncIterator[ServerEvent]:
if self._passed:
raise RuntimeError(
"this stream is single-pass and was already consumed; call "
"sessions.events.stream() again to read it again",
)
self._passed = True
return self._aiter()

async def _aiter(self) -> AsyncIterator[ServerEvent]:
Expand All @@ -358,20 +391,17 @@ async def _text_stream(self):

@property
def text_deltas(self):
"""Async incremental text chunks from ``event_delta`` frames."""
return self._text_deltas()

async def _text_deltas(self):
async for event in self:
etype = getattr(event, "type", None)
if etype == "event_delta":
if getattr(event, "type", None) == "event_delta":
text = event.delta_text
if text:
yield text
elif etype == SSEEventType.SESSION_STATUS:
block = event.content[0] if event.content else None
d = getattr(block, "data", None) or {}
if d.get("session_status") in _TERMINAL_STATUSES:
return
elif _terminal_status(event):
return

async def aclose(self) -> None:
await self._stream.aclose()
1 change: 1 addition & 0 deletions dashscope/agentstudio/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
AgentVersion,
MultiAgentConfig,
MultiAgentRosterEntry,
ModelConfig,
Credential,
CredentialAuth,
Memory,
Expand Down
51 changes: 51 additions & 0 deletions dashscope/agentstudio/types/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,44 @@ def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)


class ModelConfig(BaseModel):
"""Model configuration of an agent (the ``model`` field).

Wire names as-is: ``id`` required, inference params optional (unset ones
are omitted so the server default applies), ranges validated server-side.
``name`` is console-only: the public API neither accepts nor returns it.
Read or write the wire dict with :meth:`to_dict`.
"""

_fields = (
"id",
"name",
"temperature",
"max_tokens",
"enable_thinking",
"thinking_budget",
)

def copy_with(self, **changes: Any) -> "ModelConfig":
"""Copy with ``changes`` applied (``None`` drops a field).

Update replaces the whole config, so this changes one knob without
clearing the rest. A name outside :attr:`_fields` has to be carried
by this config already, so a forward-compat field can be retuned and
a typo still cannot.
"""
unknown = set(changes) - set(self._fields) - set(self.to_dict())
if unknown:
raise ValueError(
"unknown model config field(s): " + ", ".join(sorted(unknown)),
)
payload = self.to_dict()
payload.update(changes)
return ModelConfig(
**{k: v for k, v in payload.items() if v is not None},
)


class Agent(BaseModel):
_fields = (
"id",
Expand All @@ -468,6 +506,9 @@ class Agent(BaseModel):
)

def __init__(self, **kwargs: Any) -> None:
model = kwargs.get("model")
if isinstance(model, Mapping):
kwargs["model"] = ModelConfig(**dict(model))
multiagent = kwargs.get("multiagent")
if isinstance(multiagent, Mapping):
kwargs["multiagent"] = MultiAgentConfig(**dict(multiagent))
Expand All @@ -478,6 +519,16 @@ def system_prompt(self) -> Optional[str]:
"""Alias: server field is ``system``, kept for SDK user convenience."""
return self.system

@property
def model_id(self) -> Optional[str]:
"""Convenience: the model id carried by ``model`` (None if unset)."""
model = self.model
if isinstance(model, ModelConfig):
return model.id
if isinstance(model, Mapping):
return model.get("id")
return model if isinstance(model, str) else None


class AgentVersion(BaseModel):
_fields = (
Expand Down
Loading
Loading