From 325e81c207ff338ae60760e5bb4ffcbd54c4b8b5 Mon Sep 17 00:00:00 2001 From: luk384090-cloud Date: Sat, 10 Oct 2026 10:37:22 +0800 Subject: [PATCH 1/2] feat(decision_model): Add decision model feature support - Add DecisionModel and AioDecisionModel classes for invoking decision model services - Implement both synchronous and asynchronous invocation methods - Add DecisionAnswer, DecisionModelResponse, and DecisionUsage data structures - Register DecisionModel-related classes in the main module - Update type definitions to include new decision model types - Add API endpoint routing configuration for decision models - Add decision model feature documentation - Provide usage example code for decision models - Add comprehensive unit tests to verify decision model functionality --- README.md | 1 + README_zh.md | 1 + dashscope/__init__.py | 3 + dashscope/__init__.pyi | 55 ++++ dashscope/decision_model/__init__.py | 18 ++ dashscope/decision_model/decision_model.py | 349 +++++++++++++++++++++ samples/test_decision_model.py | 67 ++++ tests/unit/mock_server.py | 4 + tests/unit/test_decision_model.py | 256 +++++++++++++++ 9 files changed, 754 insertions(+) create mode 100644 dashscope/decision_model/__init__.py create mode 100644 dashscope/decision_model/decision_model.py create mode 100644 samples/test_decision_model.py create mode 100644 tests/unit/test_decision_model.py diff --git a/README.md b/README.md index 12649cd9..5fc7c788 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,7 @@ The SDK ships with an interactive AI assistant, **DashScope SDK Expert**, built | Text Embedding | text-embedding-v4, text-embedding-v3 | `TextEmbedding` | | Multi-Modal Embedding | tongyi-embedding-vision-plus, qwen3-vl-embedding | `MultiModalEmbedding` | | Text ReRank | qwen3-rerank, gte-rerank-v2 | `TextReRank` | +| Decision Model | decision-model-preview | `DecisionModel` | | Image Generation | wan2.7-image-pro, qwen-image-2.0-pro | `ImageSynthesis` | | Video Generation | wan2.7-t2v, wan2.7-i2v, happyhorse-1.0-t2v/i2v | `VideoSynthesis` | | Speech Synthesis (TTS) | cosyvoice-v3.5-plus, cosyvoice-v1 | `SpeechSynthesizer`, `HttpSpeechSynthesizer` | diff --git a/README_zh.md b/README_zh.md index aadf223a..4efaf878 100644 --- a/README_zh.md +++ b/README_zh.md @@ -186,6 +186,7 @@ SDK 内置了交互式 AI 助手 **DashScope SDK Expert**,基于随包提供 | 文本向量 | text-embedding-v4、text-embedding-v3 | `TextEmbedding` | | 多模态向量 | tongyi-embedding-vision-plus、qwen3-vl-embedding | `MultiModalEmbedding` | | 文本重排 | qwen3-rerank、gte-rerank-v2 | `TextReRank` | +| 决策模型 | decision-model-preview | `DecisionModel` | | 图像生成 | wan2.7-image-pro、qwen-image-2.0-pro | `ImageSynthesis` | | 视频生成 | wan2.7-t2v、wan2.7-i2v、happyhorse-1.0-t2v/i2v | `VideoSynthesis` | | 语音合成(TTS) | cosyvoice-v3.5-plus、cosyvoice-v1 | `SpeechSynthesizer`、`HttpSpeechSynthesizer` | diff --git a/dashscope/__init__.py b/dashscope/__init__.py index 944a1576..e9f1c044 100644 --- a/dashscope/__init__.py +++ b/dashscope/__init__.py @@ -42,6 +42,7 @@ base_websocket_api_url, validate_workspace_id, ) +from dashscope.decision_model import AioDecisionModel, DecisionModel from dashscope.finetune.deployments import Deployments from dashscope.finetune.finetunes import FineTunes from dashscope.embeddings.batch_text_embedding import BatchTextEmbedding @@ -169,6 +170,8 @@ def set_region(region: str, workspace_id: str = None): "Application", "TextReRank", "AioTextReRank", + "DecisionModel", + "AioDecisionModel", "Assistants", "Threads", "Messages", diff --git a/dashscope/__init__.pyi b/dashscope/__init__.pyi index 8af2cc03..2fe7f73c 100644 --- a/dashscope/__init__.pyi +++ b/dashscope/__init__.pyi @@ -323,6 +323,61 @@ class AioTextReRank: **kwargs: Any, ) -> ReRankResponse: ... +# --------------------------------------------------------------------------- +# Decision Model +# --------------------------------------------------------------------------- +class DecisionUsage: + input_tokens: int + +class DecisionAnswer: + type: str + choice: str | None + noul: float | None + score: float | None + probabilities: Dict[str, float] | None + confidence: float | None + legend: Dict[str, str] | None + +class DecisionModelResponse: + model: str + request_id: str + status_code: int + answers: Dict[str, DecisionAnswer] | None + usage: DecisionUsage | None + latency_ms: float | None + code: str + message: str + +class DecisionModel: + class Models: + decision_model_preview: str + @classmethod + def call( + cls, + model: str, + state: Union[str, Dict[str, Any]], + questions: Dict[str, Dict[str, Any]], + api_key: str | None = None, + workspace: str | None = None, + extra_headers: Dict | None = None, + **kwargs: Any, + ) -> DecisionModelResponse: ... + +class AioDecisionModel: + class Models: + decision_model_preview: str + @classmethod + async def call( + cls, + model: str, + state: Union[str, Dict[str, Any]], + questions: Dict[str, Dict[str, Any]], + api_key: str | None = None, + workspace: str | None = None, + extra_headers: Dict | None = None, + **kwargs: Any, + ) -> DecisionModelResponse: ... + # --------------------------------------------------------------------------- # Image Synthesis # --------------------------------------------------------------------------- diff --git a/dashscope/decision_model/__init__.py b/dashscope/decision_model/__init__.py new file mode 100644 index 00000000..06cb6c84 --- /dev/null +++ b/dashscope/decision_model/__init__.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from dashscope.decision_model.decision_model import ( + AioDecisionModel, + DecisionAnswer, + DecisionModel, + DecisionModelResponse, + DecisionUsage, +) + +__all__ = [ + "DecisionModel", + "AioDecisionModel", + "DecisionAnswer", + "DecisionModelResponse", + "DecisionUsage", +] diff --git a/dashscope/decision_model/decision_model.py b/dashscope/decision_model/decision_model.py new file mode 100644 index 00000000..6b2f9e1f --- /dev/null +++ b/dashscope/decision_model/decision_model.py @@ -0,0 +1,349 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +import json +from dataclasses import dataclass +from http import HTTPStatus +from typing import Any, Dict, Optional, Union + +import aiohttp + +import dashscope +from dashscope.api_entities.aio_session import ( + get_shared_aio_session, + send_with_retry_async, +) +from dashscope.client.base_api import CreateMixin, _workspace_header +from dashscope.common.api_key import get_default_api_key +from dashscope.common.base_type import BaseObjectMixin +from dashscope.common.constants import ( + DEFAULT_REQUEST_TIMEOUT_SECONDS, + REQUEST_TIMEOUT_KEYWORD, +) +from dashscope.common.env import resolve_base_url +from dashscope.common.error import ( + InputRequired, + InvalidParameter, + ModelRequired, +) +from dashscope.common.logging import logger +from dashscope.common.utils import ( + default_headers, + get_api_module, + join_url, +) + +__all__ = [ + "DecisionModel", + "AioDecisionModel", + "DecisionAnswer", + "DecisionModelResponse", + "DecisionUsage", +] + +SYSTEMONE_PATH = "systemone" + + +@dataclass(init=False) +class DecisionUsage(BaseObjectMixin): + input_tokens: int + """Number of tokens in the request.""" + + def __init__(self, **kwargs): # pylint: disable=useless-parent-delegation + super().__init__(**kwargs) + + +@dataclass(init=False) +class DecisionAnswer(BaseObjectMixin): + type: str + """Answer type, same as the question type(choice/noul/score).""" + + choice: Optional[str] = None + """choice type: the selected option name.""" + + noul: Optional[float] = None + """noul type: P(yes), 0 means no and 1 means yes.""" + + score: Optional[float] = None + """score type: probability weighted expectation of the level index, + may fall between two levels(e.g. 1.43).""" + + probabilities: Optional[Dict[str, float]] = None + """Probability of each option/level, sums to 1.""" + + confidence: Optional[float] = None + """Confidence of the answer(choice/score types).""" + + legend: Optional[Dict[str, str]] = None + """score type: level index -> level description.""" + + def __init__(self, **kwargs): # pylint: disable=useless-parent-delegation + super().__init__(**kwargs) + + +@dataclass(init=False) +class DecisionModelResponse(BaseObjectMixin): + model: str = "" + """The model name echo.""" + + request_id: str = "" + """The request id.""" + + status_code: int = HTTPStatus.OK + """The http status code, 200 means success.""" + + answers: Optional[Dict[str, DecisionAnswer]] = None + """Answers keyed by the question id of the request.""" + + usage: Optional[DecisionUsage] = None + """Token usage.""" + + latency_ms: Optional[float] = None + """Server side latency in milliseconds.""" + + code: str = "" + """Error code when the request failed.""" + + message: str = "" + """Error message when the request failed.""" + + def __init__(self, **kwargs): + answers = kwargs.pop("answers", None) + usage = kwargs.pop("usage", None) + super().__init__(**kwargs) + if answers is not None: + self.answers = { + key: DecisionAnswer(**answer) + if isinstance(answer, dict) + else answer + for key, answer in answers.items() + } + if usage is not None: + self.usage = ( + DecisionUsage(**usage) if isinstance(usage, dict) else usage + ) + + +def _validate_parameters( + model: str, + state: Union[str, Dict[str, Any]], + questions: Dict[str, Dict[str, Any]], +) -> None: + if model is None or not model: + raise ModelRequired("Model is required!") + if state is None: + raise InputRequired("state is required!") + if questions is None or not questions: + raise InputRequired("questions is required!") + + +class DecisionModel(CreateMixin): + """API for decision models(TypeSafe System One). + + One forward pass returns classification, scoring and yes/no + judgement with probability distributions and confidence, without + generating text. + """ + + SUB_PATH = "" + + class Models: + decision_model_preview = "decision-model-preview" + + @classmethod + def call( # type: ignore[override] # pylint: disable=arguments-renamed + cls, + model: str, + state: Union[str, Dict[str, Any]], + questions: Dict[str, Dict[str, Any]], + api_key: str = None, + workspace: str = None, + extra_headers: Dict = None, + **kwargs, + ) -> DecisionModelResponse: + """Call decision model service. + + Args: + model (str): The requested model, + such as decision-model-preview. + state (Union[str, Dict[str, Any]]): The business state to + decide on: ticket text, conversation or a structured + object(objects are serialized before sending). + questions (Dict[str, Dict[str, Any]]): The question table. + Key is a caller defined question id, value is a question + object with the following fields: + + - type (str): ``choice`` (pick one of many), ``noul`` + (yes/no judgement) or ``score`` (ordered scale). + - instructions (str): The question description or + judgement criteria. + - criteria: ``choice``: mapping of option name to + option description(1-255 items); ``noul``: optional + ``{"true": ..., "false": ...}`` descriptions; + ``score``: level descriptions ordered from low to + high(2-10 levels). + + example:: + + { + "department": { + "type": "choice", + "instructions": "Which team should handle?", + "criteria": { + "billing": "payment and refund", + "technical": "product defect", + }, + }, + "escalate": { + "type": "noul", + "instructions": "Notify the on-call staff?", + }, + "severity": { + "type": "score", + "instructions": "How severe is the problem?", + "criteria": ["minor", "major", "critical"], + }, + } + api_key (str, optional): The DashScope api key, if None, + will get by default rule. Defaults to None. + workspace (str, optional): The DashScope workspace id. + Defaults to None. + extra_headers (Dict, optional): Extra http headers. + Defaults to None. + **kwargs: + request_timeout: set request timeout in seconds. + + Raises: + InputRequired: The state and questions are required. + ModelRequired: The model is required. + + Returns: + DecisionModelResponse: The decision result, check + ``status_code == 200`` before consuming ``answers``. + """ + _validate_parameters(model, state, questions) + if kwargs.pop("stream", None): + raise InvalidParameter( + "Decision model does not support stream output", + ) + data = { + "model": model, + "state": state, + "questions": questions, + } + if extra_headers is not None and extra_headers: + kwargs["headers"] = { + **kwargs.get("headers", {}), + **extra_headers, + } + base_address = kwargs.pop( + "base_address", + dashscope.base_compatible_api_url, + ) + response = super().call( + data=data, + path=SYSTEMONE_PATH, + base_address=base_address, + api_key=api_key, + flattened_output=True, + workspace=workspace, + **kwargs, + ) + return DecisionModelResponse(**response) + + +class AioDecisionModel: + """Async API for decision models(TypeSafe System One).""" + + Models = DecisionModel.Models + + @classmethod + async def call( + cls, + model: str, + state: Union[str, Dict[str, Any]], + questions: Dict[str, Dict[str, Any]], + api_key: str = None, + workspace: str = None, + extra_headers: Dict = None, + **kwargs, + ) -> DecisionModelResponse: + """Call decision model service asynchronously. + + Args: + model (str): The requested model, + such as decision-model-preview. + state (Union[str, Dict[str, Any]]): The business state to + decide on: ticket text, conversation or a structured + object(objects are serialized before sending). + questions (Dict[str, Dict[str, Any]]): The question table, + see :meth:`DecisionModel.call` for the schema. + api_key (str, optional): The DashScope api key, if None, + will get by default rule. Defaults to None. + workspace (str, optional): The DashScope workspace id. + Defaults to None. + extra_headers (Dict, optional): Extra http headers. + Defaults to None. + **kwargs: + request_timeout: set request timeout in seconds. + + Raises: + InputRequired: The state and questions are required. + ModelRequired: The model is required. + + Returns: + DecisionModelResponse: The decision result, check + ``status_code == 200`` before consuming ``answers``. + """ + _validate_parameters(model, state, questions) + if kwargs.pop("stream", None): + raise InvalidParameter( + "Decision model does not support stream output", + ) + data = { + "model": model, + "state": state, + "questions": questions, + } + base_url = kwargs.pop("base_address", None) + if base_url: + base_url = resolve_base_url(base_url, workspace) + else: + base_url = resolve_base_url( + dashscope.base_compatible_api_url, + workspace, + ) + url = join_url(base_url, SYSTEMONE_PATH) + timeout = kwargs.pop( + REQUEST_TIMEOUT_KEYWORD, + DEFAULT_REQUEST_TIMEOUT_SECONDS, + ) + if api_key is None: + api_key = get_default_api_key() + headers = { + "Content-Type": "application/json; charset=utf-8", + **_workspace_header(workspace), + **default_headers(api_key, module=get_api_module(cls.__module__)), + } + if extra_headers is not None and extra_headers: + headers = {**headers, **extra_headers} + body = json.dumps(data, ensure_ascii=False).encode("utf-8") + session = await get_shared_aio_session() + + async def _send() -> aiohttp.ClientResponse: + return await session.post( + url, + data=body, + headers=headers, + timeout=aiohttp.ClientTimeout(total=timeout), + ) + + logger.debug("Starting request: %s", url) + response = await send_with_retry_async(_send) + async with response: + if "application/json" in response.content_type: + json_content = await response.json() + else: + json_content = {"message": await response.text()} + json_content["status_code"] = response.status + return DecisionModelResponse(**json_content) diff --git a/samples/test_decision_model.py b/samples/test_decision_model.py new file mode 100644 index 00000000..e5dab425 --- /dev/null +++ b/samples/test_decision_model.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +import asyncio +import os + +from dashscope import AioDecisionModel, DecisionModel + +STATE = { + "ticket_id": "T-1001", + "content": "订单支付后超过 24 小时仍未到账,用户无法继续使用核心服务,要求立即处理。", +} + +QUESTIONS = { + "department": { + "type": "choice", + "instructions": "应该由哪个团队处理?", + "criteria": { + "billing": "支付、退款和账单问题", + "technical": "产品故障和集成问题", + }, + }, + "escalate": { + "type": "noul", + "instructions": "是否需要立即通知值班人员?", + }, + "severity": { + "type": "score", + "instructions": "这个问题有多严重?", + "criteria": [ + "轻微问题,不影响功能", + "部分功能受影响,但存在替代方案", + "核心功能不可用,没有替代方案", + "造成严重业务或安全影响", + ], + }, +} + + +def test_decision_model(): + response = DecisionModel.call( + model=os.getenv("MODEL_NAME", DecisionModel.Models.decision_model_preview), + state=STATE, + questions=QUESTIONS, + ) + + if response.status_code == 200: + for question_id, answer in response.answers.items(): + print(f"{question_id}: {answer}") + else: + print(f"request_id: {response.request_id}") + print(f"code: {response.code}") + print(f"message: {response.message}") + + +async def test_aio_decision_model(): + response = await AioDecisionModel.call( + model=os.getenv("MODEL_NAME", AioDecisionModel.Models.decision_model_preview), + state=STATE, + questions=QUESTIONS, + ) + print(f"response:\n{response}") + + +if __name__ == "__main__": + test_decision_model() + asyncio.run(test_aio_decision_model()) diff --git a/tests/unit/mock_server.py b/tests/unit/mock_server.py index 0713088c..e3129f9f 100644 --- a/tests/unit/mock_server.py +++ b/tests/unit/mock_server.py @@ -596,6 +596,10 @@ def __init__(self) -> None: "/api/v1/services/rerank/text-rerank/text-rerank", self.handle_post, ) + app.router.add_post( + "/compatible-mode/v1/systemone", + self.handle_post, + ) # fine-tune app.router.add_post("/api/v1/fine-tunes", self.handle_post) app.router.add_get("/api/v1/fine-tunes", self.handle_get) diff --git a/tests/unit/test_decision_model.py b/tests/unit/test_decision_model.py new file mode 100644 index 00000000..6a6f32a6 --- /dev/null +++ b/tests/unit/test_decision_model.py @@ -0,0 +1,256 @@ +# -*- coding: utf-8 -*- +from typing import Any + +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +import asyncio +import json + +import pytest + +import dashscope +from dashscope import AioDecisionModel, DecisionModel +from dashscope.common.error import ( + InputRequired, + InvalidParameter, + ModelRequired, +) +from tests.unit.mock_request_base import MockServerBase +from tests.unit.mock_server import MockServer + +RESPONSE_BODY = { + "model": "decision-model-preview", + "request_id": "7b986c65-b223-9341-b5f0-b988e27ecaac", + "answers": { + "department": { + "type": "choice", + "choice": "billing", + "confidence": 0.88, + "probabilities": { + "billing": 0.94, + "technical": 0.06, + }, + }, + "escalate": { + "type": "noul", + "noul": 0.99, + }, + "severity": { + "type": "score", + "score": 2.25, + "confidence": 0.91, + "legend": { + "0": "轻微问题,不影响功能", + "1": "部分功能受影响,但存在替代方案", + "2": "核心功能不可用,没有替代方案", + "3": "造成严重业务或安全影响", + }, + "probabilities": { + "0": 0, + "1": 0.01, + "2": 0.73, + "3": 0.26, + }, + }, + }, + "usage": { + "input_tokens": 125, + }, + "latency_ms": 52.9, +} + +STATE = { + "ticket_id": "T-1001", + "content": "订单支付后超过 24 小时仍未到账,用户无法继续使用核心服务,要求立即处理。", +} + +QUESTIONS: dict[str, dict[str, Any]] = { + "department": { + "type": "choice", + "instructions": "应该由哪个团队处理?", + "criteria": { + "billing": "支付、退款和账单问题", + "technical": "产品故障和集成问题", + }, + }, + "escalate": { + "type": "noul", + "instructions": "是否需要立即通知值班人员?", + }, + "severity": { + "type": "score", + "instructions": "这个问题有多严重?", + "criteria": [ + "轻微问题,不影响功能", + "部分功能受影响,但存在替代方案", + "核心功能不可用,没有替代方案", + "造成严重业务或安全影响", + ], + }, +} + + +class TestDecisionModel(MockServerBase): + @classmethod + def setup_class(cls): + super().setup_class() + cls.origin_base_compatible_api_url = dashscope.base_compatible_api_url + dashscope.base_compatible_api_url = ( + "http://localhost:8089/compatible-mode/v1" + ) + + @classmethod + def teardown_class(cls): + super().teardown_class() + dashscope.base_compatible_api_url = cls.origin_base_compatible_api_url + + def test_call(self, mock_server: MockServer): + mock_server.responses.put(json.dumps(RESPONSE_BODY)) + response = DecisionModel.call( + model=DecisionModel.Models.decision_model_preview, + state=STATE, + questions=QUESTIONS, + ) + req = mock_server.requests.get(block=True) + assert req["path"] == "/compatible-mode/v1/systemone" + assert req["body"] == { + "model": "decision-model-preview", + "state": STATE, + "questions": QUESTIONS, + } + assert response.status_code == 200 + assert response.request_id == RESPONSE_BODY["request_id"] + assert response.model == "decision-model-preview" + assert response.usage is not None + assert response.usage.input_tokens == 125 + assert response.latency_ms == 52.9 + + assert response.answers is not None + department = response.answers["department"] + assert department.type == "choice" + assert department.choice == "billing" + assert department.confidence == 0.88 + assert department.probabilities == { + "billing": 0.94, + "technical": 0.06, + } + + escalate = response.answers["escalate"] + assert escalate.type == "noul" + assert escalate.noul == 0.99 + + severity = response.answers["severity"] + assert severity.type == "score" + assert severity.score == 2.25 + assert severity.confidence == 0.91 + assert severity.legend is not None + assert severity.legend["2"] == "核心功能不可用,没有替代方案" + assert severity.probabilities is not None + assert severity.probabilities["2"] == 0.73 + + def test_call_with_text_state(self, mock_server: MockServer): + mock_server.responses.put(json.dumps(RESPONSE_BODY)) + state = "订单支付后超过 24 小时仍未到账" + response = DecisionModel.call( + model="decision-model-preview", + state=state, + questions=QUESTIONS, + ) + req = mock_server.requests.get(block=True) + assert req["body"]["state"] == state + assert response.status_code == 200 + + def test_call_error_response(self, mock_server: MockServer): + error_body = { + "status_code": 400, + "code": "InvalidParameter", + "message": "questions is invalid", + "request_id": "4cc9f235-6fe0-9d62-9b83-2e1f5e06f6b0", + } + mock_server.responses.put(json.dumps(error_body)) + response = DecisionModel.call( + model="decision-model-preview", + state=STATE, + questions=QUESTIONS, + ) + mock_server.requests.get(block=True) + assert response.status_code == 400 + assert response.code == "InvalidParameter" + assert response.message == "questions is invalid" + assert response.answers is None + + def test_aio_call(self, mock_server: MockServer): + mock_server.responses.put(json.dumps(RESPONSE_BODY)) + + response = asyncio.run( + AioDecisionModel.call( + model=AioDecisionModel.Models.decision_model_preview, + state=STATE, + questions=QUESTIONS, + ), + ) + + req = mock_server.requests.get(block=True) + assert req["path"] == "/compatible-mode/v1/systemone" + assert req["body"] == { + "model": "decision-model-preview", + "state": STATE, + "questions": QUESTIONS, + } + assert response.status_code == 200 + assert response.request_id == RESPONSE_BODY["request_id"] + assert response.usage is not None + assert response.usage.input_tokens == 125 + assert response.answers is not None + assert response.answers["department"].choice == "billing" + assert response.answers["escalate"].noul == 0.99 + assert response.answers["severity"].score == 2.25 + + def test_stream_not_supported(self): + with pytest.raises(InvalidParameter): + DecisionModel.call( + model="decision-model-preview", + state=STATE, + questions=QUESTIONS, + stream=True, + ) + with pytest.raises(InvalidParameter): + asyncio.run( + AioDecisionModel.call( + model="decision-model-preview", + state=STATE, + questions=QUESTIONS, + stream=True, + ), + ) + + def test_model_required(self): + with pytest.raises(ModelRequired): + DecisionModel.call( + model="", + state=STATE, + questions=QUESTIONS, + ) + + def test_state_required(self): + with pytest.raises(InputRequired): + DecisionModel.call( + model="decision-model-preview", + state=None, + questions=QUESTIONS, + ) + + def test_questions_required(self): + with pytest.raises(InputRequired): + DecisionModel.call( + model="decision-model-preview", + state=STATE, + questions=None, + ) + with pytest.raises(InputRequired): + DecisionModel.call( + model="decision-model-preview", + state=STATE, + questions={}, + ) From 284c6f9412f6a3ddfb07bf4924e3b38f898e1828 Mon Sep 17 00:00:00 2001 From: luk384090-cloud Date: Sat, 10 Oct 2026 14:49:16 +0800 Subject: [PATCH 2/2] refactor(decision): rename decision model module to decision module --- dashscope/__init__.py | 2 +- dashscope/{decision_model => decision}/__init__.py | 2 +- .../{decision_model/decision_model.py => decision/decision.py} | 0 samples/{test_decision_model.py => test_decision.py} | 0 tests/unit/{test_decision_model.py => test_decision.py} | 0 5 files changed, 2 insertions(+), 2 deletions(-) rename dashscope/{decision_model => decision}/__init__.py (85%) rename dashscope/{decision_model/decision_model.py => decision/decision.py} (100%) rename samples/{test_decision_model.py => test_decision.py} (100%) rename tests/unit/{test_decision_model.py => test_decision.py} (100%) diff --git a/dashscope/__init__.py b/dashscope/__init__.py index e9f1c044..f25c3584 100644 --- a/dashscope/__init__.py +++ b/dashscope/__init__.py @@ -42,7 +42,7 @@ base_websocket_api_url, validate_workspace_id, ) -from dashscope.decision_model import AioDecisionModel, DecisionModel +from dashscope.decision import AioDecisionModel, DecisionModel from dashscope.finetune.deployments import Deployments from dashscope.finetune.finetunes import FineTunes from dashscope.embeddings.batch_text_embedding import BatchTextEmbedding diff --git a/dashscope/decision_model/__init__.py b/dashscope/decision/__init__.py similarity index 85% rename from dashscope/decision_model/__init__.py rename to dashscope/decision/__init__.py index 06cb6c84..e8b74716 100644 --- a/dashscope/decision_model/__init__.py +++ b/dashscope/decision/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -from dashscope.decision_model.decision_model import ( +from dashscope.decision.decision import ( AioDecisionModel, DecisionAnswer, DecisionModel, diff --git a/dashscope/decision_model/decision_model.py b/dashscope/decision/decision.py similarity index 100% rename from dashscope/decision_model/decision_model.py rename to dashscope/decision/decision.py diff --git a/samples/test_decision_model.py b/samples/test_decision.py similarity index 100% rename from samples/test_decision_model.py rename to samples/test_decision.py diff --git a/tests/unit/test_decision_model.py b/tests/unit/test_decision.py similarity index 100% rename from tests/unit/test_decision_model.py rename to tests/unit/test_decision.py