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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The DashScope Python SDK provides a comprehensive interface to [Alibaba Cloud Mo

## What is New

**v1.27.0 ships an interactive AI assistant — [DashScope SDK Expert](#ai-assistant-dashscope-sdk-expert).** Run `dashscope` with no arguments (or ask directly, e.g. `dashscope "how do I stream Generation output"`) to get SDK/API answers, runnable examples, CLI usage, and error diagnosis right in your terminal. Guidance is drawn from per-domain quick-reference skills (text, multimodal, speech, retrieval, fine-tuning, agent, cli) built on the SDK's public interfaces — parameters, outputs, and error codes — so you can ask instead of reading the docs. Type `/help` inside the assistant to view available commands.
**v1.27.0 ships an interactive AI assistant — [DashScope SDK Expert](#ai-assistant-dashscope-sdk-expert).** Run `dashscope` with no arguments (or ask directly with `dashscope expert "how do I stream Generation output"`) to get SDK/API answers, runnable examples, CLI usage, and error diagnosis right in your terminal. Guidance is drawn from per-domain quick-reference skills (text, multimodal, speech, retrieval, fine-tuning, agent, cli) built on the SDK's public interfaces — parameters, outputs, and error codes — so you can ask instead of reading the docs. Type `/help` inside the assistant to view available commands.

## Installation
To install the DashScope Python SDK, simply run:
Expand Down Expand Up @@ -175,7 +175,7 @@ A complete runnable example is available in [`samples/set_region_example.py`](sa
The SDK ships with an interactive AI assistant, **DashScope SDK Expert**, built on the bundled Agentic CLI (`dashscope/acli`) framework. For DashScope SDK/CLI users it is the recommended way to get development consultation and AI coding help — answering SDK/API questions, generating runnable examples, showing CLI usage, and diagnosing errors, right in your terminal.

- Run `dashscope` with no arguments to start the assistant. On first run it offers to install the SDK Expert knowledge pack (per-domain quick-reference skills: text, multimodal, speech, retrieval, fine-tuning, agent, cli), so guidance comes from the SDK's public interfaces — parameters, outputs, error codes — without reading the source
- Ask it instead of reading docs — e.g. `dashscope "how do I stream Generation output"` or `dashscope "CLI command to cancel a fine-tuning job"`. Type `/help` inside the assistant to list available commands (`/setup`, `/skill`, `/stats`, ...); classic SDK subcommands still work, and unrecognized commands are routed to the assistant
- Ask it instead of reading docs — e.g. `dashscope expert "how do I stream Generation output"` or `dashscope expert "CLI command to cancel a fine-tuning job"`, each of which asks once and exits. Type `/help` inside the assistant to list available commands (`/setup`, `/skill`, `/stats`, ...); classic SDK subcommands still work, plain `dashscope expert` enters the assistant interactively, and `dashscope expert --help` shows all the forms
- Full walkthrough: [DashScope SDK Expert guide](https://help.aliyun.com/en/model-studio/dashscope-sdk-expert)

## Supported Models
Expand Down
4 changes: 2 additions & 2 deletions README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ DashScope Python SDK 提供了访问[阿里云百炼(Model Studio)](https://

## 最新动态

**v1.27.0 内置交互式 AI 助手 —— [DashScope SDK Expert](#ai-助手dashscope-sdk-expert)。** 直接运行 `dashscope`(不带任何参数),或直接提问(如 `dashscope "如何流式输出 Generation 结果"`),即可在终端中获得 SDK/API 答疑、可运行示例、CLI 用法和错误诊断。助手基于按领域划分的速查技能(文本、多模态、语音、检索、微调、Agent、CLI),这些技能构建在 SDK 的公开接口——参数、输出、错误码——之上,让你直接提问,无需翻文档。在助手内输入 `/help` 可查看可用命令。
**v1.27.0 内置交互式 AI 助手 —— [DashScope SDK Expert](#ai-助手dashscope-sdk-expert)。** 直接运行 `dashscope`(不带任何参数),或用 `dashscope expert "如何流式输出 Generation 结果"` 直接提问,即可在终端中获得 SDK/API 答疑、可运行示例、CLI 用法和错误诊断。助手基于按领域划分的速查技能(文本、多模态、语音、检索、微调、Agent、CLI),这些技能构建在 SDK 的公开接口——参数、输出、错误码——之上,让你直接提问,无需翻文档。在助手内输入 `/help` 可查看可用命令。

## 安装

Expand Down Expand Up @@ -174,7 +174,7 @@ print(response)
SDK 内置了交互式 AI 助手 **DashScope SDK Expert**,基于随包提供的 Agentic CLI(`dashscope/acli`)框架构建。对于 DashScope SDK/CLI 用户,它是获取开发咨询和 AI 编码帮助的推荐方式——直接在终端中解答 SDK/API 问题、生成可运行示例、展示 CLI 用法、诊断错误。

- 直接运行 `dashscope`(不带参数)即可启动助手。首次运行时会提示安装 SDK Expert 知识包(按领域划分的速查技能:文本、多模态、语音、检索、微调、Agent、CLI),使助手的指导来自 SDK 的公开接口——参数、输出、错误码——而无需阅读源码
- 直接提问代替翻文档——如 `dashscope "如何流式输出 Generation 结果"` 或 `dashscope "取消微调任务的 CLI 命令"`。在助手内输入 `/help` 可列出可用命令(`/setup`、`/skill`、`/stats` 等);经典 SDK 子命令依然可用,无法识别的命令会自动转给助手处理
- 直接提问代替翻文档——如 `dashscope expert "如何流式输出 Generation 结果"` 或 `dashscope expert "取消微调任务的 CLI 命令"`,均为一次性提问后退出。在助手内输入 `/help` 可列出可用命令(`/setup`、`/skill`、`/stats` 等);经典 SDK 子命令依然可用,单独执行 `dashscope expert` 进入交互式助手,`dashscope expert --help` 查看全部形式
- 完整使用指南:[DashScope SDK Expert 文档](https://help.aliyun.com/zh/model-studio/dashscope-sdk-expert)

## 支持的模型
Expand Down
2 changes: 1 addition & 1 deletion dashscope/acli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import uuid

__version__ = "0.6.6"
__version__ = "0.6.7"

# Per-process identifier sent as x-dashscope-sdk-session-id so the
# backend can group multi-turn requests from one CLI run.
Expand Down
90 changes: 79 additions & 11 deletions dashscope/acli/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
UserSupplement,
normalize_for_model,
sanitize,
sanitize_text,
text_of,
tool_result_for_display,
tool_result_for_history,
Expand All @@ -66,14 +67,22 @@ def _classify_outcome(successes: int, failures: int) -> str:
def _tool_message(tool_call: ToolCall, result: str) -> dict:
"""Build the history entry answering ``tool_call``.

The restore note must be attached here, at truncation time: later
Redaction happens here, ahead of truncation. ``save_session`` already ran
``sanitize`` over the on-disk copy, so before this a key read out of the
environment was scrubbed from the session file and still handed to the
model in full — the persistence path was protecting the wrong reader.
Scrubbing after truncation would not work either: ``truncate_head_tail``
cuts on a character budget and can split a token in half, leaving a
fragment that matches no pattern.

The restore note must be attached here for the same reason: later
shrinking skips a result that already carries an omission marker, so the
largest results would end up with no way to be read back.
"""
return {
"role": "tool",
"content": tool_result_for_history(
result,
sanitize_text(result),
note=restore_note(tool_call.name, tool_call.arguments),
),
"name": tool_call.name,
Expand Down Expand Up @@ -154,7 +163,10 @@ def _budget_burned_slowly(error: BaseException) -> bool:
18. **Verify code changes before reporting done.** Run the tests that cover
what you touched; if nothing covers it, add a focused test for the new
behaviour and run that. Report the command and its pass/fail result —
never claim a change works without having executed it
never claim a change works without having executed it. When the output
transforms or joins data (ETL, CSV/JSON, migrations), also assert every
cross-reference resolves — schema-valid output with dangling IDs still
fails

Reply style:
- **Concise**. No filler like "let me see / let me help you / I'll analyze
Expand Down Expand Up @@ -245,6 +257,20 @@ def __init__(
)
except ValueError:
self.converge_hard_ratio = 0.85
# Silence nudge: one reminder per turn after N tool calls have gone by
# without any prose reaching the user. Mode-independent on purpose —
# the shape this exists for is an interactive turn of *distinct*
# run_command calls, which every existing backstop misses:
# StagnationTracker's read-only classifier counts run_command as
# mutating and so resets its streak, its repeat counters need
# byte-identical calls, and _convergence_section is oneshot-only.
# Env: ACLI_SILENT_TOOL_NUDGE (0 disables).
try:
self.silent_tool_nudge = int(
os.environ.get("ACLI_SILENT_TOOL_NUDGE", "6"),
)
except ValueError:
self.silent_tool_nudge = 6
# Hard acceptance gate (oneshot only): run the project's test command
# before accepting a final answer, and send the raw output back when
# it fails. Built here, ahead of the policy text below, so the prompt
Expand Down Expand Up @@ -354,6 +380,10 @@ def __init__(
self.turn_subagents = 0
self.turn_mcp_calls = 0
self.turn_skills = 0
# Consecutive tool calls that streamed no prose, and whether the
# silence nudge has already been spent this turn.
self._silent_tool_calls = 0
self._silent_nudge_injected = False
# Explicit /skill invocations to count on the next turn
self._pending_skill_names: list[str] = []

Expand Down Expand Up @@ -567,6 +597,33 @@ def _convergence_section(self, loop_index: int) -> str:
self.converge_hard_ratio,
)

def _silence_section(self) -> str:
"""Inject one reminder when tool calls stop producing prose.

Spent at most once per turn. Repeating it every iteration teaches the
model to skim past it, which is the failure mode prose instructions
already have — the point of a structural backstop is that it fires on
a count rather than on the model's willingness to re-read advice.

Deliberately not gated on ``oneshot``: the user watching a spinner for
five minutes with no text is the interactive case, and the oneshot
runs already have the convergence and read-only-cap sections.
"""
if self._silent_nudge_injected or not self.silent_tool_nudge:
return ""
if self._silent_tool_calls < self.silent_tool_nudge:
return ""
self._silent_nudge_injected = True
return (
"\n## ⏸ You have gone quiet\n"
f"{self._silent_tool_calls} tool calls in a row and not one word "
"of it reached the user.\n"
"Before the next call, say in one or two sentences what you are "
"doing and what you have found so far. If the calls were "
"collecting information to answer a question you can already "
"answer, answer it instead of calling again.\n"
)

async def _recall_memory(self, user_input) -> str:
"""Search for relevant profile info and format as context.

Expand Down Expand Up @@ -649,6 +706,8 @@ async def _run_stream_body(self, user_input) -> AsyncIterator[str]:
self.turn_tool_calls = 0
self.turn_subagents = 0
self.turn_mcp_calls = 0
self._silent_tool_calls = 0
self._silent_nudge_injected = False
try:
active = get_skill_manager().active_packages(user_input_text)
names = [p.name for p in active] + self._pending_skill_names
Expand Down Expand Up @@ -725,26 +784,28 @@ async def _run_stream_body(self, user_input) -> AsyncIterator[str]:
f"messages → {len(self.messages)}[/dim]",
)

# Reflection / stagnation / convergence hints are volatile, so
# they ride at the tail of the request instead of being folded
# into the system message. Rewriting messages_with_system[0] on
# every iteration changed the whole prompt prefix byte-for-byte and
# defeated provider-side prompt caching, where cached input costs
# roughly a tenth of uncached. The system message and the
# accumulated history below it stay append-only and byte-stable.
# Reflection / stagnation / convergence / silence hints are
# volatile, so they ride at the tail of the request instead of
# being folded into the system message. Rewriting
# messages_with_system[0] on every iteration changed the whole
# prompt prefix byte-for-byte and defeated provider-side prompt
# caching, where cached input costs roughly a tenth of uncached.
# The system message and the accumulated history below it stay
# append-only and byte-stable.
#
# The hint has to be a user message, not a second system message:
# the Anthropic converter keeps only the last system message, so a
# second one would replace the real prompt outright, and the Tongyi
# converter only reads messages[0] as system.
#
# All three sections are rebuilt every iteration because tracker
# All four sections are rebuilt every iteration because tracker
# state changes as tools run; each returns "" when its condition
# is not met, so an empty hint costs no message at all.
hint = (
self._reflection_section()
+ self._stagnation_section()
+ self._convergence_section(_loop_i)
+ self._silence_section()
).strip()
request_messages = (
[*messages_with_system, {"role": "user", "content": hint}]
Expand Down Expand Up @@ -901,6 +962,13 @@ async def _run_stream_body(self, user_input) -> AsyncIterator[str]:
tool_calls = [tc for tc in tool_calls if tc.name]

self.turn_tool_calls += len(tool_calls)
# Silence is per-iteration, not cumulative over the turn: an
# iteration that streamed prose did talk to the user, so the run
# of unheard calls starts over from the next one.
if full_content.strip():
self._silent_tool_calls = 0
else:
self._silent_tool_calls += len(tool_calls)
for tc in tool_calls:
if tc.name in ("delegate", "subagent_invoke"):
self.turn_subagents += 1
Expand Down
10 changes: 10 additions & 0 deletions dashscope/acli/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,16 @@ def main():

_mcp_server_main()
return
else:
# Only a bare `acli` and the explicit -c/--command one-shot enter
# the agent — the same contract the dashscope CLI enforces. An
# unrecognized token used to fall through and start a chat, which
# discarded the whole command line: `acli auth whoami` opened the
# agent instead of reporting that no such command exists. Exit 2
# matches the usage-error code typer/click use for the same case.
print(f"Error: No such command '{arg}'.")
print("Try 'acli --help' for help.")
sys.exit(2)
config = Config.load()
_apply_cli_overrides(config, protocol_override, max_turns_override)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,21 @@ with client.sessions.events.stream(session.id) as stream:
| Error code/exception | HTTP status | Meaning | Handling |
| --- | --- | --- | --- |
| `InputRequired` / `ModelRequired` / `InvalidInput` | raised locally | missing required params such as app_id/prompt/model | add params per the exception message; Application failures do not raise — check status_code |
| `invalid_request_error` (InvalidRequestError) | 400 | invalid agentstudio request parameters | check parameter names/types, e.g. empty events raises a local ValueError |
| `invalid_request_error` / `request_too_large` (InvalidRequestError) | 400/413 | invalid agentstudio request parameters, or payload too large | check parameter names/types, e.g. empty events raises a local ValueError |
| `authentication_error` (AuthenticationError) | 401 | api_key missing or invalid | check api_key or DASHSCOPE_API_KEY |
| `permission_denied_error` (PermissionDeniedError) | 403 | no access to the resource | check workspace/uid and resource ownership |
| `permission_denied_error` / `permission_error` (PermissionDeniedError) | 403 | no access to the resource | check workspace/uid and resource ownership |
| `not_found_error` (NotFoundError) | 404 | agent/session and other resources do not exist | verify the id and region/workspace are correct |
| `conflict_error` (ConflictError) | 409 | version in agents.update does not match the server | retrieve the latest version first, then update |
| `rate_limit_error` (RateLimitError) | 429 | rate limited | retry with backoff (client default max_retries=2) |
| `api_error` (InternalServerError) / `overloaded_error` (OverloadedError) | 500/502/504 / 503 | server error/overloaded | troubleshoot with `request_id` or retry |
| `rate_limit_error` / `billing_error` (RateLimitError) | 429 | rate limited or out of quota | retry with backoff (client default max_retries=2) |
| `api_error` / `timeout_error` (InternalServerError), `overloaded_error` (OverloadedError) | 500/502/504 / 503 | server error/overloaded | troubleshoot with `request_id` or retry |
| `APITimeoutError` / `APIConnectionError` | no response | connection/read/write timeout | increase timeout, check network, then retry |
| `StreamError` / `StreamClosedError` | mid-stream | SSE protocol error/stream closed | call `events.stream` again to reopen the stream |

Match on the exception class, not on `.code`: `.code` echoes the server's raw
string, so a 403 may arrive as either `permission_denied_error` or
`permission_error`, and it is normalised to `api_error` when the server omits
it. The class is always the one listed above.

## Java SDK

Java SDK v2.22.23 entry points:
Expand Down
Loading
Loading