Skip to content

Commit ff97389

Browse files
Merge pull request #217 from dashscope/dev/agent-model-config
feat: agent model config add maxTokens/temperature parameters
2 parents 4d3e950 + dbf5224 commit ff97389

11 files changed

Lines changed: 651 additions & 49 deletions

File tree

‎dashscope/acli/examples/dashscope-sdk-expert/.acli/skills/agent.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ description: Quick reference for Agent APIs (agentstudio-hosted Agent/Applicatio
1313
### agentstudio (`from dashscope.agentstudio import Client, AsyncClient`)
1414
- `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.
1515
- Resource entry points: `client.agents / .sessions / .environments / .files / .skills / .vaults / .deployments / .deployment_runs`.
16-
- `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)`.
16+
- `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)`.
1717
- `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`.
1818
- `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, ...)`.
1919

‎dashscope/agentstudio/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
MemoryStore,
5656
MemoryVersion,
5757
Message,
58+
ModelConfig,
5859
PermissionPolicy,
5960
SecurityAlert,
6061
SecurityAlertList,
@@ -124,6 +125,7 @@
124125
"MemoryStore",
125126
"MemoryVersion",
126127
"Message",
128+
"ModelConfig",
127129
"PermissionPolicy",
128130
"SecurityAlert",
129131
"SecurityAlertList",

‎dashscope/agentstudio/client.py‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,18 @@ class Client:
100100
101101
Usage::
102102
103-
from dashscope.agentstudio import Client
103+
from dashscope.agentstudio import Client, ModelConfig
104104
105105
client = Client(api_key="sk-xxx", workspace="my-ws")
106-
agent = client.agents.create(name="demo", model="qwen-max")
106+
agent = client.agents.create(
107+
name="demo",
108+
model=ModelConfig(
109+
id="qwen-max",
110+
temperature=0.7,
111+
enable_thinking=True,
112+
thinking_budget=2048,
113+
),
114+
)
107115
session = client.sessions.create(agent=agent.id)
108116
with client.sessions.events.stream(session.id) as stream:
109117
for event in stream:
@@ -173,11 +181,12 @@ class AsyncClient:
173181
174182
Usage::
175183
176-
from dashscope.agentstudio import AsyncClient
184+
from dashscope.agentstudio import AsyncClient, ModelConfig
177185
178186
client = AsyncClient(api_key="sk-xxx", workspace="my-ws")
179187
agent = await client.agents.create(
180-
name="demo", model="qwen-max",
188+
name="demo",
189+
model=ModelConfig(id="qwen-max", max_tokens=4096),
181190
)
182191
session = await client.sessions.create(agent=agent.id)
183192
async with client.sessions.events.stream(session.id) as stream:

‎dashscope/agentstudio/resources/agents.py‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from __future__ import annotations
66

7-
from typing import Any, Mapping, Optional, Sequence
7+
from typing import Any, Mapping, Optional, Sequence, Union
88

99
from dashscope.agentstudio.pagination import (
1010
AsyncCursorPage,
@@ -15,7 +15,7 @@
1515
_coerce_agent,
1616
_coerce_agent_version,
1717
)
18-
from dashscope.agentstudio.types import Agent, AgentVersion
18+
from dashscope.agentstudio.types import Agent, AgentVersion, ModelConfig
1919
from dashscope.agentstudio.types.params import (
2020
AgentCreateParams,
2121
AgentListParams,
@@ -36,7 +36,7 @@ def create(
3636
self,
3737
*,
3838
name: str,
39-
model: str,
39+
model: Union[str, ModelConfig],
4040
description: Optional[str] = None,
4141
system_prompt: Optional[str] = None,
4242
tools: Optional[Sequence[Mapping[str, Any]]] = None,
@@ -85,8 +85,8 @@ def update(
8585
*,
8686
version: int,
8787
name: Optional[str] = None,
88+
model: Optional[Union[str, ModelConfig]] = None,
8889
description: Optional[str] = None,
89-
model: Optional[str] = None,
9090
system_prompt: Optional[str] = None,
9191
tools: Optional[Sequence[Mapping[str, Any]]] = None,
9292
mcp_servers: Optional[Sequence[Mapping[str, Any]]] = None,
@@ -99,12 +99,13 @@ def update(
9999
``version`` must equal the server's current version;
100100
the server rejects mismatches with HTTP 409.
101101
Retrieve the agent first to obtain the current version.
102+
``name`` is required too (``AGENT_005`` without it).
102103
"""
103104
body = AgentUpdateParams(
104105
version=version,
105106
name=name,
106-
description=description,
107107
model=model,
108+
description=description,
108109
system_prompt=system_prompt,
109110
tools=tools,
110111
mcp_servers=mcp_servers,
@@ -189,7 +190,7 @@ async def create(
189190
self,
190191
*,
191192
name: str,
192-
model: str,
193+
model: Union[str, ModelConfig],
193194
description: Optional[str] = None,
194195
system_prompt: Optional[str] = None,
195196
tools: Optional[Sequence[Mapping[str, Any]]] = None,
@@ -242,8 +243,8 @@ async def update(
242243
*,
243244
version: int,
244245
name: Optional[str] = None,
246+
model: Optional[Union[str, ModelConfig]] = None,
245247
description: Optional[str] = None,
246-
model: Optional[str] = None,
247248
system_prompt: Optional[str] = None,
248249
tools: Optional[Sequence[Mapping[str, Any]]] = None,
249250
mcp_servers: Optional[Sequence[Mapping[str, Any]]] = None,
@@ -256,12 +257,13 @@ async def update(
256257
``version`` must equal the server's current version;
257258
the server rejects mismatches with HTTP 409.
258259
Retrieve the agent first to obtain the current version.
260+
``name`` is required too (``AGENT_005`` without it).
259261
"""
260262
body = AgentUpdateParams(
261263
version=version,
262264
name=name,
263-
description=description,
264265
model=model,
266+
description=description,
265267
system_prompt=system_prompt,
266268
tools=tools,
267269
mcp_servers=mcp_servers,

‎dashscope/agentstudio/resources/session_events.py‎

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,15 @@
4545
)
4646

4747

48+
def _terminal_status(event: Any) -> bool:
49+
"""True when a ``session_status`` event reports that the run is over."""
50+
if getattr(event, "type", None) != SSEEventType.SESSION_STATUS:
51+
return False
52+
block = event.content[0] if event.content else None
53+
data = getattr(block, "data", None) or {}
54+
return data.get("session_status") in _TERMINAL_STATUSES
55+
56+
4857
class SessionEvents:
4958
"""Session event send / list / stream."""
5059

@@ -125,9 +134,13 @@ def stream(
125134
) -> "_TypedEventStream":
126135
"""Open the SSE stream and return an iterator of typed events.
127136
128-
``event_deltas`` opts into incremental text streaming for the given
129-
event types (``"message"`` and/or ``"reasoning"``; aliases
130-
``"agent.message"`` / ``"agent.thinking"``).
137+
``event_deltas`` opts into incremental streaming for the given event
138+
types (``"message"`` and/or ``"reasoning"``; aliases ``"agent.message"``
139+
/ ``"agent.thinking"``); anything else the server rejects. The stream is
140+
read once, so ``text_deltas`` can be taken from it only; a second pass
141+
raises rather than returning nothing. A chunk stream is not a reliable
142+
source for final text: a reconnect replays only persisted events, so use
143+
``text_stream``.
131144
"""
132145
params: Optional[Dict[str, Any]] = None
133146
if event_deltas:
@@ -146,7 +159,13 @@ def stream(
146159

147160

148161
class _TypedEventStream:
149-
"""Wrap :class:`EventStream` so iterating yields :class:`ServerEvent`."""
162+
"""Wrap :class:`EventStream` so iterating yields :class:`ServerEvent`.
163+
164+
A stream is read once: a second pass would silently yield nothing, so it
165+
raises instead. Call ``sessions.events.stream()`` again to read the events.
166+
"""
167+
168+
_passed = False
150169

151170
def __init__(self, stream: EventStream) -> None:
152171
self._stream = stream
@@ -179,6 +198,15 @@ def __exit__(self, *exc: Any) -> None:
179198
self.close()
180199

181200
def __iter__(self) -> Iterator[ServerEvent]:
201+
if self._passed:
202+
raise RuntimeError(
203+
"this stream is single-pass and was already consumed; call "
204+
"sessions.events.stream() again to read it again",
205+
)
206+
self._passed = True
207+
return self._iter()
208+
209+
def _iter(self) -> Iterator[ServerEvent]:
182210
for payload in self._stream:
183211
yield _coerce_event(payload)
184212

@@ -196,30 +224,27 @@ def text_stream(self):
196224
text = getattr(block, "text", "")
197225
if text:
198226
yield text
199-
elif getattr(event, "type", None) == SSEEventType.SESSION_STATUS:
200-
block = event.content[0] if event.content else None
201-
d = getattr(block, "data", None) or {}
202-
if d.get("session_status") in _TERMINAL_STATUSES:
203-
return
227+
elif _terminal_status(event):
228+
return
204229

205230
@property
206231
def text_deltas(self):
207232
"""Iterate over incremental text chunks from ``event_delta`` frames.
208233
209234
Requires the stream to be opened with ``event_deltas``; otherwise
210-
yields nothing (use :attr:`text_stream` for terminal full text).
235+
yields nothing (use :attr:`text_stream` for terminal full text, which is
236+
also what a reconnect can rebuild — chunks are not replayed).
211237
"""
238+
return self._text_deltas()
239+
240+
def _text_deltas(self):
212241
for event in self:
213-
etype = getattr(event, "type", None)
214-
if etype == "event_delta":
242+
if getattr(event, "type", None) == "event_delta":
215243
text = event.delta_text
216244
if text:
217245
yield text
218-
elif etype == SSEEventType.SESSION_STATUS:
219-
block = event.content[0] if event.content else None
220-
d = getattr(block, "data", None) or {}
221-
if d.get("session_status") in _TERMINAL_STATUSES:
222-
return
246+
elif _terminal_status(event):
247+
return
223248

224249
def close(self) -> None:
225250
self._stream.close()
@@ -331,7 +356,15 @@ async def __aenter__(self) -> "_AioTypedEventStream":
331356
async def __aexit__(self, *exc: Any) -> None:
332357
await self.aclose()
333358

359+
_passed = False
360+
334361
def __aiter__(self) -> AsyncIterator[ServerEvent]:
362+
if self._passed:
363+
raise RuntimeError(
364+
"this stream is single-pass and was already consumed; call "
365+
"sessions.events.stream() again to read it again",
366+
)
367+
self._passed = True
335368
return self._aiter()
336369

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

359392
@property
360393
def text_deltas(self):
394+
"""Async incremental text chunks from ``event_delta`` frames."""
361395
return self._text_deltas()
362396

363397
async def _text_deltas(self):
364398
async for event in self:
365-
etype = getattr(event, "type", None)
366-
if etype == "event_delta":
399+
if getattr(event, "type", None) == "event_delta":
367400
text = event.delta_text
368401
if text:
369402
yield text
370-
elif etype == SSEEventType.SESSION_STATUS:
371-
block = event.content[0] if event.content else None
372-
d = getattr(block, "data", None) or {}
373-
if d.get("session_status") in _TERMINAL_STATUSES:
374-
return
403+
elif _terminal_status(event):
404+
return
375405

376406
async def aclose(self) -> None:
377407
await self._stream.aclose()

‎dashscope/agentstudio/types/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
AgentVersion,
3131
MultiAgentConfig,
3232
MultiAgentRosterEntry,
33+
ModelConfig,
3334
Credential,
3435
CredentialAuth,
3536
Memory,

‎dashscope/agentstudio/types/models.py‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,44 @@ def __init__(self, **kwargs: Any) -> None:
446446
super().__init__(**kwargs)
447447

448448

449+
class ModelConfig(BaseModel):
450+
"""Model configuration of an agent (the ``model`` field).
451+
452+
Wire names as-is: ``id`` required, inference params optional (unset ones
453+
are omitted so the server default applies), ranges validated server-side.
454+
``name`` is console-only: the public API neither accepts nor returns it.
455+
Read or write the wire dict with :meth:`to_dict`.
456+
"""
457+
458+
_fields = (
459+
"id",
460+
"name",
461+
"temperature",
462+
"max_tokens",
463+
"enable_thinking",
464+
"thinking_budget",
465+
)
466+
467+
def copy_with(self, **changes: Any) -> "ModelConfig":
468+
"""Copy with ``changes`` applied (``None`` drops a field).
469+
470+
Update replaces the whole config, so this changes one knob without
471+
clearing the rest. A name outside :attr:`_fields` has to be carried
472+
by this config already, so a forward-compat field can be retuned and
473+
a typo still cannot.
474+
"""
475+
unknown = set(changes) - set(self._fields) - set(self.to_dict())
476+
if unknown:
477+
raise ValueError(
478+
"unknown model config field(s): " + ", ".join(sorted(unknown)),
479+
)
480+
payload = self.to_dict()
481+
payload.update(changes)
482+
return ModelConfig(
483+
**{k: v for k, v in payload.items() if v is not None},
484+
)
485+
486+
449487
class Agent(BaseModel):
450488
_fields = (
451489
"id",
@@ -468,6 +506,9 @@ class Agent(BaseModel):
468506
)
469507

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

522+
@property
523+
def model_id(self) -> Optional[str]:
524+
"""Convenience: the model id carried by ``model`` (None if unset)."""
525+
model = self.model
526+
if isinstance(model, ModelConfig):
527+
return model.id
528+
if isinstance(model, Mapping):
529+
return model.get("id")
530+
return model if isinstance(model, str) else None
531+
481532

482533
class AgentVersion(BaseModel):
483534
_fields = (

0 commit comments

Comments
 (0)