From 36ea72d8e878f513fc73ea78a429f27d0dd34bbc Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 15 Jun 2026 10:58:41 +0800 Subject: [PATCH 1/9] feat(cli): expand CLI commands and harden compatibility --- dashscope/__init__.py | 10 + dashscope/cli/__init__.py | 245 +++++- dashscope/cli/agentic_rl.py | 70 +- dashscope/cli/application.py | 82 ++ dashscope/cli/assistant_files.py | 121 +++ dashscope/cli/assistants.py | 208 +++++ dashscope/cli/code_generation.py | 76 ++ dashscope/cli/common.py | 36 +- dashscope/cli/deployments.py | 7 + dashscope/cli/embeddings.py | 79 ++ dashscope/cli/files.py | 32 +- dashscope/cli/fine_tunes.py | 8 + dashscope/cli/generation.py | 15 +- dashscope/cli/image_generation.py | 80 ++ dashscope/cli/image_synthesis.py | 62 ++ dashscope/cli/messages.py | 224 +++++ dashscope/cli/models.py | 54 ++ dashscope/cli/multimodal_conversation.py | 94 +++ dashscope/cli/multimodal_embedding.py | 116 +++ dashscope/cli/oss.py | 3 +- dashscope/cli/rerank.py | 68 ++ dashscope/cli/runs.py | 286 +++++++ dashscope/cli/speech_synthesis.py | 68 ++ dashscope/cli/steps.py | 91 ++ dashscope/cli/threads.py | 188 +++++ dashscope/cli/tokenization.py | 54 ++ dashscope/cli/transcription.py | 92 ++ dashscope/cli/understanding.py | 60 ++ dashscope/cli/video_synthesis.py | 174 ++++ setup.cfg | 7 +- tests/unit/test_cli_application.py | 78 ++ tests/unit/test_cli_assistant_files.py | 192 +++++ tests/unit/test_cli_assistants.py | 282 +++++++ tests/unit/test_cli_code_generation.py | 65 ++ tests/unit/test_cli_embeddings.py | 66 ++ tests/unit/test_cli_image_generation.py | 77 ++ tests/unit/test_cli_image_synthesis.py | 66 ++ tests/unit/test_cli_main.py | 796 ++++++++++++++++++ tests/unit/test_cli_messages.py | 315 +++++++ tests/unit/test_cli_models.py | 66 ++ .../unit/test_cli_multimodal_conversation.py | 87 ++ tests/unit/test_cli_multimodal_embedding.py | 83 ++ tests/unit/test_cli_rerank.py | 65 ++ tests/unit/test_cli_runs.py | 398 +++++++++ tests/unit/test_cli_speech_synthesis.py | 71 ++ tests/unit/test_cli_steps.py | 111 +++ tests/unit/test_cli_threads.py | 229 +++++ tests/unit/test_cli_tokenization.py | 51 ++ tests/unit/test_cli_transcription.py | 79 ++ tests/unit/test_cli_understanding.py | 55 ++ tests/unit/test_cli_video_synthesis.py | 227 +++++ 51 files changed, 6110 insertions(+), 59 deletions(-) create mode 100644 dashscope/cli/application.py create mode 100644 dashscope/cli/assistant_files.py create mode 100644 dashscope/cli/assistants.py create mode 100644 dashscope/cli/code_generation.py create mode 100644 dashscope/cli/embeddings.py create mode 100644 dashscope/cli/image_generation.py create mode 100644 dashscope/cli/image_synthesis.py create mode 100644 dashscope/cli/messages.py create mode 100644 dashscope/cli/models.py create mode 100644 dashscope/cli/multimodal_conversation.py create mode 100644 dashscope/cli/multimodal_embedding.py create mode 100644 dashscope/cli/rerank.py create mode 100644 dashscope/cli/runs.py create mode 100644 dashscope/cli/speech_synthesis.py create mode 100644 dashscope/cli/steps.py create mode 100644 dashscope/cli/threads.py create mode 100644 dashscope/cli/tokenization.py create mode 100644 dashscope/cli/transcription.py create mode 100644 dashscope/cli/understanding.py create mode 100644 dashscope/cli/video_synthesis.py create mode 100644 tests/unit/test_cli_application.py create mode 100644 tests/unit/test_cli_assistant_files.py create mode 100644 tests/unit/test_cli_assistants.py create mode 100644 tests/unit/test_cli_code_generation.py create mode 100644 tests/unit/test_cli_embeddings.py create mode 100644 tests/unit/test_cli_image_generation.py create mode 100644 tests/unit/test_cli_image_synthesis.py create mode 100644 tests/unit/test_cli_main.py create mode 100644 tests/unit/test_cli_messages.py create mode 100644 tests/unit/test_cli_models.py create mode 100644 tests/unit/test_cli_multimodal_conversation.py create mode 100644 tests/unit/test_cli_multimodal_embedding.py create mode 100644 tests/unit/test_cli_rerank.py create mode 100644 tests/unit/test_cli_runs.py create mode 100644 tests/unit/test_cli_speech_synthesis.py create mode 100644 tests/unit/test_cli_steps.py create mode 100644 tests/unit/test_cli_threads.py create mode 100644 tests/unit/test_cli_tokenization.py create mode 100644 tests/unit/test_cli_transcription.py create mode 100644 tests/unit/test_cli_understanding.py create mode 100644 tests/unit/test_cli_video_synthesis.py diff --git a/dashscope/__init__.py b/dashscope/__init__.py index 637d5de..f7f351b 100644 --- a/dashscope/__init__.py +++ b/dashscope/__init__.py @@ -1,9 +1,19 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. +# pylint: disable=wrong-import-position import logging +import warnings from logging import NullHandler +# Suppress urllib3 NotOpenSSLWarning on systems with LibreSSL before any SDK +# submodule imports urllib3. +warnings.filterwarnings( + "ignore", + message=".*urllib3.*only supports OpenSSL.*", + category=Warning, +) + from dashscope.aigc.code_generation import CodeGeneration from dashscope.aigc.conversation import Conversation, History, HistoryItem from dashscope.aigc.generation import AioGeneration, Generation diff --git a/dashscope/cli/__init__.py b/dashscope/cli/__init__.py index 00fd3a9..fd89013 100644 --- a/dashscope/cli/__init__.py +++ b/dashscope/cli/__init__.py @@ -7,6 +7,7 @@ """ import sys import warnings +from typing import Optional # Suppress urllib3 NotOpenSSLWarning on systems with LibreSSL warnings.filterwarnings( @@ -18,12 +19,34 @@ import typer # noqa: E402 import dashscope # noqa: E402 +from dashscope.cli.common import err_console # noqa: E402 +from dashscope.common.error import AuthenticationError # noqa: E402 from dashscope.cli import ( # noqa: E402 + application, + assistant_files, + assistants, + code_generation, deployments, + embeddings, files, fine_tunes, generation, + image_generation, + image_synthesis, + messages, + models, + multimodal_conversation, + multimodal_embedding, oss, + rerank, + runs, + speech_synthesis, + steps, + threads, + tokenization, + transcription, + understanding, + video_synthesis, ) @@ -69,6 +92,56 @@ "--page_size": "--page-size", } +_TOP_LEVEL_COMMANDS = { + "generation", + "ft", + "fine-tunes", + "files", + "deployments", + "oss", + "rerank", + "embeddings", + "tokenization", + "models", + "understanding", + "application", + "code-generation", + "image-synthesis", + "video-synthesis", + "image-generation", + "multimodal-conversation", + "multimodal-embedding", + "transcription", + "speech-synthesis", + "assistants", + "assistant-files", + "threads", + "messages", + "runs", + "steps", + "rl", + "agentic-rl", +} + + +_COMMANDS_WITH_LOCAL_API_KEY = {"oss", "rl", "agentic-rl"} +_LEGACY_COMMANDS_WITH_SIZE_OPTION = { + "files.list", + "fine_tunes.list", + "deployments.list", +} + + +def _translate_param_name(arg): + """Translate legacy underscore option names to Typer hyphen names.""" + if arg in _PARAM_MAP: + return _PARAM_MAP[arg] + if arg.startswith("--") and "=" in arg: + option_name, option_value = arg.split("=", 1) + if option_name in _PARAM_MAP: + return f"{_PARAM_MAP[option_name]}={option_value}" + return arg + def _translate_legacy_args(argv): """Translate legacy argparse command format to Typer format. @@ -81,59 +154,95 @@ def _translate_legacy_args(argv): if len(argv) < 2: return argv - new_argv = [argv[0]] # Keep program name - i = 1 - - # Check if first arg is a legacy command - if i < len(argv) and argv[i] in _COMMAND_MAP: - # Split "fine_tunes.call" into ["fine-tunes", "call"] - new_cmd = _COMMAND_MAP[argv[i]].split() - new_argv.extend(new_cmd) - i += 1 + is_legacy_command = argv[1] in _COMMAND_MAP + if not is_legacy_command: + return [argv[0]] + [_translate_param_name(arg) for arg in argv[1:]] - # Process remaining args + command_args = _COMMAND_MAP[argv[1]].split() + top_level_args = [] + translated_args = [] + i = 2 while i < len(argv): arg = argv[i] - - # Translate parameter names - if arg in _PARAM_MAP: - new_argv.append(_PARAM_MAP[arg]) - elif arg.startswith("--") and "=" in arg: - opt, val = arg.split("=", 1) - if opt in _PARAM_MAP: - new_argv.append(f"{_PARAM_MAP[opt]}={val}") + translated_arg = _translate_param_name(arg) + if argv[1] in _LEGACY_COMMANDS_WITH_SIZE_OPTION: + if translated_arg == "--page-size": + translated_arg = "--size" + elif translated_arg.startswith("--page-size="): + translated_arg = translated_arg.replace( + "--page-size=", + "--size=", + 1, + ) + + if translated_arg == "--api-key": + top_level_args.append("--api-key") + if i + 1 < len(argv): + top_level_args.append(argv[i + 1]) + i += 2 else: - new_argv.append(arg) - else: - new_argv.append(arg) + i += 1 + continue + if translated_arg.startswith("--api-key="): + top_level_args.append(translated_arg) + i += 1 + continue + + translated_args.append(translated_arg) i += 1 - return new_argv + return [argv[0]] + top_level_args + command_args + translated_args + + +def _exit_missing_api_key_value(option_name): + err_console.print( + f"[red]Error:[/red] Option '{option_name}' requires an argument.", + ) + sys.exit(2) def _extract_global_api_key(argv): """Extract global -k/--api-key from argv and set dashscope.api_key. - Returns modified argv with api-key args removed. + For backward compatibility, global api-key can appear before or after most + command names. Commands that define their own local api-key option are left + untouched once their command name has been reached. """ new_argv = [] i = 0 + current_command = None while i < len(argv): arg = argv[i] - # Check for -k or --api-key + if i > 0 and current_command is None and arg in _TOP_LEVEL_COMMANDS: + current_command = arg + + should_parse_global_api_key = ( + current_command not in _COMMANDS_WITH_LOCAL_API_KEY + ) + if arg in ("-k", "--api-key"): - # Next arg should be the key value - if i + 1 < len(argv): - dashscope.api_key = argv[i + 1] - i += 2 # Skip both -k and the value + if i + 1 >= len(argv): + _exit_missing_api_key_value(arg) + next_arg = argv[i + 1] + if not next_arg or next_arg.startswith("-"): + _exit_missing_api_key_value(arg) + if should_parse_global_api_key: + if next_arg in _TOP_LEVEL_COMMANDS: + _exit_missing_api_key_value(arg) + dashscope.api_key = next_arg + i += 2 + continue + + if arg.startswith("--api-key="): + api_key = arg.split("=", 1)[1] + if not api_key: + _exit_missing_api_key_value("--api-key") + if should_parse_global_api_key: + dashscope.api_key = api_key + i += 1 continue - elif arg.startswith("--api-key="): - # Handle --api-key=value format - dashscope.api_key = arg.split("=", 1)[1] - i += 1 - continue new_argv.append(arg) i += 1 @@ -153,6 +262,21 @@ def _extract_global_api_key(argv): rich_markup_mode="rich", ) + +@app.callback() +def callback( + api_key: Optional[str] = typer.Option( + None, + "-k", + "--api-key", + help="DashScope API Key.", + ), +): + """Configure global CLI options.""" + if api_key: + dashscope.api_key = api_key + + # Register sub-command groups app.add_typer(generation.app) app.add_typer(fine_tunes.app, name="ft") @@ -160,6 +284,26 @@ def _extract_global_api_key(argv): app.add_typer(files.app) app.add_typer(deployments.app) app.add_typer(oss.app) +app.add_typer(rerank.app) +app.add_typer(embeddings.app) +app.add_typer(tokenization.app) +app.add_typer(models.app) +app.add_typer(understanding.app) +app.add_typer(application.app) +app.add_typer(code_generation.app) +app.add_typer(image_synthesis.app) +app.add_typer(video_synthesis.app) +app.add_typer(image_generation.app) +app.add_typer(multimodal_conversation.app) +app.add_typer(multimodal_embedding.app) +app.add_typer(transcription.app) +app.add_typer(speech_synthesis.app) +app.add_typer(assistants.app) +app.add_typer(assistant_files.app) +app.add_typer(threads.app) +app.add_typer(messages.app) +app.add_typer(runs.app) +app.add_typer(steps.app) def _register_rl_app(): @@ -176,10 +320,22 @@ def _register_rl_app(): name="rl", help="๐Ÿš€ Agentic RL fine-tuning commands", ) - except ImportError: - pass - except Exception: - pass + app.add_typer( + rl_app, + name="agentic-rl", + help="๐Ÿš€ Agentic RL fine-tuning commands", + hidden=True, + ) + except ImportError as exception: + err_console.print( + "[yellow]Warning:[/yellow] Failed to register rl command: " + f"{exception}", + ) + except Exception as exception: + err_console.print( + "[yellow]Warning:[/yellow] Failed to register rl command: " + f"{exception}", + ) _register_rl_app() @@ -187,13 +343,18 @@ def _register_rl_app(): def main(): """Entry point for the ``dashscope`` console script.""" - # Extract global api-key parameter FIRST - argv = _extract_global_api_key(sys.argv) + # Translate legacy command format first so legacy --api_key can be treated + # as the global --api-key option. + argv = _translate_legacy_args(sys.argv) - # Then translate legacy command format - argv = _translate_legacy_args(argv) + # Extract global api-key parameter after legacy argument normalization. + argv = _extract_global_api_key(argv) # Update sys.argv for Typer sys.argv = argv - app() + try: + app() + except AuthenticationError as exception: + err_console.print(f"[red]Error:[/red] {exception}") + sys.exit(1) diff --git a/dashscope/cli/agentic_rl.py b/dashscope/cli/agentic_rl.py index 0af67f8..ece3953 100644 --- a/dashscope/cli/agentic_rl.py +++ b/dashscope/cli/agentic_rl.py @@ -24,6 +24,7 @@ ) from dashscope.finetune.reinforcement.common.errors import OutputError from dashscope.finetune.customize_types import FineTune +from dashscope.cli.common import error, normalize_local_path_or_url app = typer.Typer( @@ -35,6 +36,10 @@ ) console = Console() err_console = Console(stderr=True) +SUPPORTED_OUTPUT_FORMATS = {"table", "json", "yaml"} +SUPPORTED_FUNCTION_TYPES = { + function_type.name for function_type in FunctionType +} @app.callback() @@ -64,16 +69,43 @@ def _root_cause(e: Exception) -> Exception: # ================= Configuration & Utility Functions ================= +def validate_output_format(fmt: str) -> str: + """Validate and normalize output format.""" + output_format = fmt.lower() + if output_format not in SUPPORTED_OUTPUT_FORMATS: + err_console.print( + "[red]Error:[/red] Invalid output format. " + "Expected one of: table, json, yaml.", + ) + raise typer.Exit(2) + return output_format + + +def validate_function_type(func_type: str) -> str: + """Validate and normalize function type.""" + function_type = func_type.upper() + if function_type not in SUPPORTED_FUNCTION_TYPES: + expected_types = ", ".join(sorted(SUPPORTED_FUNCTION_TYPES)) + err_console.print( + f"[red]Error:[/red] Invalid function type. " + f"Expected one of: {expected_types}.", + ) + raise typer.Exit(2) + return function_type + + def format_output(data: Any, fmt: str = "table") -> None: """Unified output formatter: table | json | yaml""" + output_format = validate_output_format(fmt) + if hasattr(data, "model_dump"): data = data.model_dump() elif hasattr(data, "__dict__"): data = data.__dict__ - if fmt == "json": + if output_format == "json": print(json.dumps(data, indent=2, ensure_ascii=False)) - elif fmt == "yaml": + elif output_format == "yaml": console.print( yaml.dump(data, default_flow_style=False, allow_unicode=True), ) @@ -188,6 +220,7 @@ async def _register_fc_async( raise typer.Exit(1) +@app.command("register-functions", hidden=True) @app.command("register_functions") def register_fc( rollout_classpaths: Optional[List[str]] = typer.Option( @@ -227,6 +260,7 @@ def register_fc( - rollout_classpath - reward_classpaths """ + output_format = validate_output_format(output_format) result = asyncio.run( _register_fc_async( rollout_classpaths=rollout_classpaths or [], @@ -263,6 +297,7 @@ async def _test_fc_async( raise typer.Exit(1) +@app.command("test-functions", hidden=True) @app.command("test_functions") def test_fc( instance_id: str = typer.Argument( @@ -296,7 +331,14 @@ def test_fc( ): """๐Ÿงช Test a registered Rollout/Reward function instance with custom input data.""" - input_dict = load_json_input(input_data) + output_format = validate_output_format(output_format) + func_type = validate_function_type(func_type) + try: + input_dict = load_json_input(input_data) + except ValueError as exception: + err_console.print(f"[red]Error:[/red] {exception}") + raise typer.Exit(1) + result = asyncio.run( _test_fc_async( instance_id=instance_id, @@ -333,6 +375,7 @@ async def _upload_data_async( raise typer.Exit(1) +@app.command("upload-data", hidden=True) @app.command("upload_data") def upload_data( training_files: List[str] = typer.Option( @@ -358,6 +401,17 @@ def upload_data( ): """๐Ÿ“ฆ Upload training/validation datasets to the platform, returns file IDs""" + output_format = validate_output_format(output_format) + training_files = [ + normalize_local_path_or_url(training_file, "--training-files") + for training_file in training_files + ] + if validation_files: + validation_files = [ + normalize_local_path_or_url(validation_file, "--validation-files") + for validation_file in validation_files + ] + result = asyncio.run( _upload_data_async( training_files=training_files, @@ -442,7 +496,12 @@ def run( - reward_classpaths (at least one) - training_files (at least one) """ + output_format = validate_output_format(output_format) _apply_verbose(verbose) + if config and not config.exists(): + error(f"--config file {config} does not exist") + if config and not config.is_file(): + error(f"--config path {config} is not a file") # Prepare workflow parameters run_kwargs = { @@ -495,7 +554,7 @@ def run( fmt=output_format, ) - except (ValueError, Exception) as e: + except Exception as e: root = _root_cause(e) label = ( "Validation error" @@ -528,6 +587,7 @@ def get( output_format: str = typer.Option("table", "--output-format", "-o"), ): """๐Ÿ“Š Query the current status and metadata of a specific job""" + output_format = validate_output_format(output_format) try: result = AgenticRL.get(job_id=job_id, api_key=api_key or "") @@ -594,6 +654,7 @@ def logs( output_format: str = typer.Option("table", "--output-format", "-o"), ): """๐Ÿ“œ Fetch job execution logs (supports pagination)""" + output_format = validate_output_format(output_format) try: result = AgenticRL.logs( job_id=job_id, @@ -631,6 +692,7 @@ def list_jobs( output_format: str = typer.Option("table", "--output-format", "-o"), ): """๐Ÿ“‹ List historical fine-tuning jobs with pagination""" + output_format = validate_output_format(output_format) try: result = AgenticRL.list( page_no=page, diff --git a/dashscope/cli/application.py b/dashscope/cli/application.py new file mode 100644 index 0000000..78b6a32 --- /dev/null +++ b/dashscope/cli/application.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +"""``application`` sub-command group.""" +import json +from typing import List, Optional + +import typer + +import dashscope +from dashscope.cli.common import console, ensure_ok, handle_sdk_error + +app = typer.Typer( + name="application", + help="Application completion commands", + add_completion=False, + invoke_without_command=True, +) + + +@app.callback() +def callback(ctx: typer.Context): + """Show help if no subcommand is provided.""" + if ctx.invoked_subcommand is None: + typer.echo(ctx.get_help()) + + +@app.command("create") +@handle_sdk_error("Application request failed") +def create( + app_id: str = typer.Option(..., "-a", "--app-id", help="The application id"), + prompt: str = typer.Option(..., "-p", "--prompt", help="Input prompt"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), + session_id: Optional[str] = typer.Option( + None, + "--session-id", + help="Session id for multiple rounds call", + ), + doc_tag_codes: Optional[List[str]] = typer.Option( + None, + "--doc-tag-code", + help="Document tag code. Repeat this option for multiple codes.", + ), + doc_reference_type: Optional[str] = typer.Option( + None, + "--doc-reference-type", + help="Document reference type, such as simple or indexed", + ), + has_thoughts: bool = typer.Option( + False, + "--has-thoughts", + help="Return rag or plugin process details", + ), + temperature: Optional[float] = typer.Option( + None, + "--temperature", + help="Sampling temperature", + ), + top_p: Optional[float] = typer.Option(None, "--top-p", help="Top-p value"), + top_k: Optional[int] = typer.Option(None, "--top-k", help="Top-k value"), +): + """Call application completion API.""" + response = dashscope.Application.call( + app_id=app_id, + prompt=prompt, + workspace=workspace, + session_id=session_id, + doc_tag_codes=doc_tag_codes, + doc_reference_type=doc_reference_type, + has_thoughts=has_thoughts, + temperature=temperature, + top_p=top_p, + top_k=top_k, + ) + output = ensure_ok(response) + console.print_json(json.dumps(output, ensure_ascii=False)) + usage = getattr(response, "usage", None) + if usage: + console.print_json(json.dumps(usage, ensure_ascii=False)) diff --git a/dashscope/cli/assistant_files.py b/dashscope/cli/assistant_files.py new file mode 100644 index 0000000..a8eb253 --- /dev/null +++ b/dashscope/cli/assistant_files.py @@ -0,0 +1,121 @@ +# -*- coding: utf-8 -*- +"""``assistant-files`` sub-command group.""" +import json +from typing import Optional + +import typer + +from dashscope.assistants.files import Files +from dashscope.cli.common import console, handle_sdk_error + +app = typer.Typer( + name="assistant-files", + help="Assistant file management commands", + add_completion=False, + invoke_without_command=True, +) + + +@app.callback() +def callback(ctx: typer.Context): + """Show help if no subcommand is provided.""" + if ctx.invoked_subcommand is None: + typer.echo(ctx.get_help()) + + +@app.command("create") +@handle_sdk_error("Create assistant file failed") +def create( + assistant_id: str = typer.Argument(..., help="The assistant id"), + file_id: str = typer.Option(..., "--file-id", help="The file id"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """Create an assistant file.""" + response = Files.create( + assistant_id, + file_id=file_id, + workspace=workspace, + ) + console.print_json( + json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + ) + + +@app.command("get") +@handle_sdk_error("Retrieve assistant file failed") +def get_file( + assistant_id: str = typer.Argument(..., help="The assistant id"), + file_id: str = typer.Argument(..., help="The file id"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """Retrieve an assistant file.""" + response = Files.retrieve( + file_id, + assistant_id=assistant_id, + workspace=workspace, + ) + console.print_json( + json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + ) + + +@app.command("delete") +@handle_sdk_error("Delete assistant file failed") +def delete_file( + assistant_id: str = typer.Argument(..., help="The assistant id"), + file_id: str = typer.Argument(..., help="The file id"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """Delete an assistant file.""" + response = Files.delete( + file_id, + assistant_id=assistant_id, + workspace=workspace, + ) + console.print_json( + json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + ) + + +@app.command("list") +@handle_sdk_error("List assistant files failed") +def list_files( + assistant_id: str = typer.Argument(..., help="The assistant id"), + limit: Optional[int] = typer.Option(None, "--limit", help="Maximum number of files"), + order: Optional[str] = typer.Option(None, "--order", help="Sort order by created_at"), + after: Optional[str] = typer.Option(None, "--after", help="Cursor after file id"), + before: Optional[str] = typer.Option(None, "--before", help="Cursor before file id"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """List assistant files.""" + response = Files.list( + assistant_id, + limit=limit, + order=order, + after=after, + before=before, + workspace=workspace, + ) + console.print_json( + json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + ) diff --git a/dashscope/cli/assistants.py b/dashscope/cli/assistants.py new file mode 100644 index 0000000..79bade1 --- /dev/null +++ b/dashscope/cli/assistants.py @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- +"""``assistants`` sub-command group.""" +import json +from typing import List, Optional + +import typer + +import dashscope +from dashscope.cli.common import console, error, handle_sdk_error + +app = typer.Typer( + name="assistants", + help="Assistant management commands", + add_completion=False, + invoke_without_command=True, +) + + +@app.callback() +def callback(ctx: typer.Context): + """Show help if no subcommand is provided.""" + if ctx.invoked_subcommand is None: + typer.echo(ctx.get_help()) + + +def _parse_json_object(value: Optional[str], option_name: str): + if value is None: + return None + try: + parsed_value = json.loads(value) + except json.JSONDecodeError as exception: + error(f"Invalid {option_name} JSON: {exception.msg}") + if not isinstance(parsed_value, dict): + error(f"{option_name} must be a JSON object") + return parsed_value + + +def _parse_json_array(value: Optional[str], option_name: str): + if value is None: + return None + try: + parsed_value = json.loads(value) + except json.JSONDecodeError as exception: + error(f"Invalid {option_name} JSON: {exception.msg}") + if not isinstance(parsed_value, list): + error(f"{option_name} must be a JSON array") + return parsed_value + + +@app.command("create") +@handle_sdk_error("Create assistant failed") +def create( + model: str = typer.Option(..., "-m", "--model", help="The model to use"), + name: Optional[str] = typer.Option(None, "--name", help="Assistant name"), + description: Optional[str] = typer.Option(None, "--description", help="Assistant description"), + instructions: Optional[str] = typer.Option(None, "--instructions", help="Assistant instructions"), + tools: Optional[str] = typer.Option(None, "--tools", help="Tools as a JSON array string"), + file_ids: Optional[List[str]] = typer.Option( + None, + "--file-id", + help="File id, can be used multiple times", + ), + metadata: Optional[str] = typer.Option(None, "--metadata", help="Metadata as a JSON object string"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), + top_p: Optional[float] = typer.Option(None, "--top-p", help="Top-p sampling"), + top_k: Optional[int] = typer.Option(None, "--top-k", help="Top-k sampling"), + temperature: Optional[float] = typer.Option(None, "--temperature", help="Sampling temperature"), + max_tokens: Optional[int] = typer.Option(None, "--max-tokens", help="Maximum output tokens"), +): + """Create an assistant.""" + response = dashscope.Assistants.create( + model=model, + name=name, + description=description, + instructions=instructions, + tools=_parse_json_array(tools, "tools"), + file_ids=file_ids or [], + metadata=_parse_json_object(metadata, "metadata"), + workspace=workspace, + top_p=top_p, + top_k=top_k, + temperature=temperature, + max_tokens=max_tokens, + ) + console.print_json( + json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + ) + + +@app.command("list") +@handle_sdk_error("List assistants failed") +def list_assistants( + limit: Optional[int] = typer.Option(None, "--limit", help="Maximum number of assistants"), + order: Optional[str] = typer.Option(None, "--order", help="Sort order by created_at"), + after: Optional[str] = typer.Option(None, "--after", help="Cursor after assistant id"), + before: Optional[str] = typer.Option(None, "--before", help="Cursor before assistant id"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """List assistants.""" + response = dashscope.Assistants.list( + limit=limit, + order=order, + after=after, + before=before, + workspace=workspace, + ) + console.print_json( + json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + ) + + +@app.command("get") +@handle_sdk_error("Retrieve assistant failed") +def get_assistant( + assistant_id: str = typer.Argument(..., help="The assistant id"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """Retrieve an assistant.""" + response = dashscope.Assistants.retrieve( + assistant_id, + workspace=workspace, + ) + console.print_json( + json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + ) + + +@app.command("update") +@handle_sdk_error("Update assistant failed") +def update_assistant( + assistant_id: str = typer.Argument(..., help="The assistant id"), + model: Optional[str] = typer.Option(None, "-m", "--model", help="The model to use"), + name: Optional[str] = typer.Option(None, "--name", help="Assistant name"), + description: Optional[str] = typer.Option(None, "--description", help="Assistant description"), + instructions: Optional[str] = typer.Option(None, "--instructions", help="Assistant instructions"), + tools: Optional[str] = typer.Option(None, "--tools", help="Tools as a JSON array string"), + file_ids: Optional[List[str]] = typer.Option( + None, + "--file-id", + help="File id, can be used multiple times", + ), + metadata: Optional[str] = typer.Option(None, "--metadata", help="Metadata as a JSON object string"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), + top_p: Optional[float] = typer.Option(None, "--top-p", help="Top-p sampling"), + top_k: Optional[int] = typer.Option(None, "--top-k", help="Top-k sampling"), + temperature: Optional[float] = typer.Option(None, "--temperature", help="Sampling temperature"), + max_tokens: Optional[int] = typer.Option(None, "--max-tokens", help="Maximum output tokens"), +): + """Update an assistant.""" + response = dashscope.Assistants.update( + assistant_id, + model=model, + name=name, + description=description, + instructions=instructions, + tools=_parse_json_array(tools, "tools"), + file_ids=file_ids or [], + metadata=_parse_json_object(metadata, "metadata"), + workspace=workspace, + top_p=top_p, + top_k=top_k, + temperature=temperature, + max_tokens=max_tokens, + ) + console.print_json( + json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + ) + + +@app.command("delete") +@handle_sdk_error("Delete assistant failed") +def delete_assistant( + assistant_id: str = typer.Argument(..., help="The assistant id"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """Delete an assistant.""" + response = dashscope.Assistants.delete( + assistant_id, + workspace=workspace, + ) + console.print_json( + json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + ) diff --git a/dashscope/cli/code_generation.py b/dashscope/cli/code_generation.py new file mode 100644 index 0000000..b6353e6 --- /dev/null +++ b/dashscope/cli/code_generation.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +"""``code-generation`` sub-command group.""" +import json +from typing import Optional + +import typer + +import dashscope +from dashscope.aigc.code_generation import ( + AttachmentRoleMessageParam, + UserRoleMessageParam, +) +from dashscope.cli.common import console, ensure_ok, error, handle_sdk_error + +app = typer.Typer( + name="code-generation", + help="Code generation commands", + add_completion=False, + invoke_without_command=True, +) + + +@app.callback() +def callback(ctx: typer.Context): + """Show help if no subcommand is provided.""" + if ctx.invoked_subcommand is None: + typer.echo(ctx.get_help()) + + +@app.command("create") +@handle_sdk_error("Code generation request failed") +def create( + model: str = typer.Option(..., "-m", "--model", help="The model to call"), + scene: str = typer.Option(..., "-s", "--scene", help="Code generation scene"), + content: str = typer.Option( + ..., + "-c", + "--content", + help="User message content", + ), + attachment_meta: Optional[str] = typer.Option( + None, + "--attachment-meta", + help="Attachment meta as a JSON object string", + ), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), + n: int = typer.Option(1, "-n", "--n", help="The number of output results"), +): + """Call code generation API.""" + messages = [UserRoleMessageParam(content=content)] + if attachment_meta is not None: + try: + meta = json.loads(attachment_meta) + except json.JSONDecodeError as exception: + error(f"Invalid attachment meta JSON: {exception.msg}") + if not isinstance(meta, dict): + error("Attachment meta must be a JSON object") + messages.append(AttachmentRoleMessageParam(meta=meta)) + + response = dashscope.CodeGeneration.call( + model=model, + scene=scene, + message=messages, + workspace=workspace, + n=n, + ) + output = ensure_ok(response) + console.print_json(json.dumps(output, ensure_ascii=False)) + usage = getattr(response, "usage", None) + if usage: + console.print_json(json.dumps(usage, ensure_ascii=False)) diff --git a/dashscope/cli/common.py b/dashscope/cli/common.py index c8788fd..b4e7e66 100644 --- a/dashscope/cli/common.py +++ b/dashscope/cli/common.py @@ -1,13 +1,17 @@ # -*- coding: utf-8 -*- """Shared utilities, constants, and helpers for the dashscope CLI.""" import logging +import os +from functools import wraps from http import HTTPStatus -from typing import NoReturn +from typing import Callable, NoReturn, TypeVar +from urllib.parse import urlparse import typer from rich.console import Console logger = logging.getLogger("dashscope.cli") +CommandFunction = TypeVar("CommandFunction", bound=Callable) # --------------------------------------------------------------------------- # Constants @@ -64,3 +68,33 @@ def error(message: str, exit_code: int = 1) -> NoReturn: """Print an error message in red and exit.""" err_console.print(f"[red]Error:[/red] {message}") raise typer.Exit(exit_code) + + +def handle_sdk_error(action: str): + """Convert unexpected SDK exceptions into friendly CLI errors.""" + + def decorator(command_function: CommandFunction) -> CommandFunction: + @wraps(command_function) + def wrapper(*args, **kwargs): + try: + return command_function(*args, **kwargs) + except typer.Exit: + raise + except Exception as exception: + error(f"{action}: {exception}") + + return wrapper # type: ignore[return-value] + + return decorator + + +def normalize_local_path_or_url(value: str, option_name: str) -> str: + """Return expanded local path or URL, failing early for missing files.""" + parsed_value = urlparse(value) + if parsed_value.scheme: + return value + + file_path = os.path.expanduser(value) + if not os.path.exists(file_path): + error(f"{option_name} file {file_path} does not exist") + return file_path diff --git a/dashscope/cli/deployments.py b/dashscope/cli/deployments.py index a6d574f..d17bdd0 100644 --- a/dashscope/cli/deployments.py +++ b/dashscope/cli/deployments.py @@ -12,6 +12,7 @@ console, err_console, ensure_ok, + handle_sdk_error, logger, success, ) @@ -87,6 +88,7 @@ def _print_deployments(output): @app.command("create") +@handle_sdk_error("Create deployment failed") def create( model: str = typer.Option(..., "-m", "--model", help="The model ID"), suffix: Optional[str] = typer.Option( @@ -116,6 +118,7 @@ def create( # Backward compatibility alias @app.command("call", hidden=True) +@handle_sdk_error("Create deployment failed") def call( model: str = typer.Option(..., "-m", "--model", help="The model ID"), suffix: Optional[str] = typer.Option( @@ -140,6 +143,7 @@ def call( @app.command("get") +@handle_sdk_error("Retrieve deployment failed") def get( deployed_model: str = typer.Argument(..., help="The deployed model name"), ): @@ -154,6 +158,7 @@ def get( @app.command("list") +@handle_sdk_error("List deployments failed") def list_deployments( page: int = typer.Option(1, "-p", "--page", help="Page number"), size: int = typer.Option(10, "-s", "--size", help="Page size"), @@ -168,6 +173,7 @@ def list_deployments( @app.command("scale") +@handle_sdk_error("Scale deployment failed") def scale( deployed_model: str = typer.Argument( ..., @@ -194,6 +200,7 @@ def scale( @app.command("delete") +@handle_sdk_error("Delete deployment failed") def delete( deployed_model: str = typer.Argument(..., help="The deployed model name"), ): diff --git a/dashscope/cli/embeddings.py b/dashscope/cli/embeddings.py new file mode 100644 index 0000000..ae61cca --- /dev/null +++ b/dashscope/cli/embeddings.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +"""``embeddings`` sub-command group.""" +import json +from typing import List, Optional + +import typer + +import dashscope +from dashscope.cli.common import console, ensure_ok, handle_sdk_error + +app = typer.Typer( + name="embeddings", + help="Text embedding commands", + add_completion=False, + invoke_without_command=True, +) + + +@app.callback() +def callback(ctx: typer.Context): + """Show help if no subcommand is provided.""" + if ctx.invoked_subcommand is None: + typer.echo(ctx.get_help()) + + +@app.command("create") +@handle_sdk_error("Text embedding request failed") +def create( + model: str = typer.Option(..., "-m", "--model", help="The model to call"), + input_texts: List[str] = typer.Option( + ..., + "-i", + "--input", + help="Input text. Repeat this option for multiple texts.", + ), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), + text_type: Optional[str] = typer.Option( + None, + "-t", + "--text-type", + help="Text type, such as query or document", + ), + dimension: Optional[int] = typer.Option( + None, + "--dimension", + help="Output vector dimension", + ), + output_type: Optional[str] = typer.Option( + None, + "--output-type", + help="Output format, such as dense, sparse, or dense&sparse", + ), + instruct: Optional[str] = typer.Option( + None, + "--instruct", + help="Instruction to guide model understanding", + ), +): + """Call text embedding API.""" + embedding_input = input_texts[0] if len(input_texts) == 1 else input_texts + response = dashscope.TextEmbedding.call( + model=model, + input=embedding_input, + workspace=workspace, + text_type=text_type, + dimension=dimension, + output_type=output_type, + instruct=instruct, + ) + output = ensure_ok(response) + console.print_json(json.dumps(output, ensure_ascii=False)) + usage = getattr(response, "usage", None) + if usage: + console.print_json(json.dumps(usage, ensure_ascii=False)) diff --git a/dashscope/cli/files.py b/dashscope/cli/files.py index a63258e..50f03ea 100644 --- a/dashscope/cli/files.py +++ b/dashscope/cli/files.py @@ -1,13 +1,20 @@ # -*- coding: utf-8 -*- """``files`` sub-command group.""" import json +import os from typing import Optional import typer import dashscope from dashscope.common.constants import FilePurpose -from dashscope.cli.common import console, ensure_ok, success +from dashscope.cli.common import ( + console, + ensure_ok, + error, + handle_sdk_error, + success, +) app = typer.Typer( name="files", @@ -25,6 +32,7 @@ def callback(ctx: typer.Context): @app.command("upload") +@handle_sdk_error("Upload file failed") def upload( file: str = typer.Option( ..., @@ -52,18 +60,26 @@ def upload( ), ): """Upload a file.""" - rsp = dashscope.Files.upload( - file_path=file, - purpose=purpose, - description=description, # type: ignore[arg-type] - base_address=base_url, - ) + file_path = os.path.expanduser(file) + if not os.path.exists(file_path): + error(f"File {file_path} does not exist") + + try: + rsp = dashscope.Files.upload( + file_path=file_path, + purpose=purpose, + description=description, # type: ignore[arg-type] + base_address=base_url, + ) + except Exception as exception: + error(str(exception)) output = ensure_ok(rsp) file_id = output["uploaded_files"][0]["file_id"] success(f"Upload success, file id: {file_id}") @app.command("get") +@handle_sdk_error("Retrieve file failed") def get( file_id: str = typer.Argument(..., help="The file ID"), base_url: Optional[str] = typer.Option( @@ -83,6 +99,7 @@ def get( @app.command("list") +@handle_sdk_error("List files failed") def list_files( page: int = typer.Option(1, "-p", "--page", help="Page number"), size: int = typer.Option(10, "-s", "--size", help="Page size"), @@ -107,6 +124,7 @@ def list_files( @app.command("delete") +@handle_sdk_error("Delete file failed") def delete( file_id: str = typer.Argument(..., help="The file ID"), base_url: Optional[str] = typer.Option( diff --git a/dashscope/cli/fine_tunes.py b/dashscope/cli/fine_tunes.py index ba11175..cdd44df 100644 --- a/dashscope/cli/fine_tunes.py +++ b/dashscope/cli/fine_tunes.py @@ -13,6 +13,7 @@ console, err_console, ensure_ok, + handle_sdk_error, logger, print_failed_message, success, @@ -132,6 +133,7 @@ def _dump_logs(job_id: str): @app.command("create") +@handle_sdk_error("Create fine-tune job failed") def create( training_file_ids: List[str] = typer.Option( ..., @@ -207,6 +209,7 @@ def create( # Backward compatibility alias @app.command("call", hidden=True) +@handle_sdk_error("Create fine-tune job failed") def call( training_file_ids: List[str] = typer.Option( ..., @@ -270,6 +273,7 @@ def call( @app.command("get") +@handle_sdk_error("Retrieve fine-tune job failed") def get( job_id: str = typer.Argument(..., help="The fine-tune job id"), ): @@ -290,6 +294,7 @@ def get( @app.command("list") +@handle_sdk_error("List fine-tune jobs failed") def list_jobs( page: int = typer.Option(1, "-p", "--page", help="Page number"), size: int = typer.Option(10, "-s", "--size", help="Page size"), @@ -312,6 +317,7 @@ def list_jobs( @app.command("stream") +@handle_sdk_error("Stream fine-tune events failed") def stream( job_id: str = typer.Argument(..., help="The fine-tune job id"), ): @@ -320,6 +326,7 @@ def stream( @app.command("cancel") +@handle_sdk_error("Cancel fine-tune job failed") def cancel( job_id: str = typer.Argument(..., help="The fine-tune job id"), ): @@ -329,6 +336,7 @@ def cancel( @app.command("delete") +@handle_sdk_error("Delete fine-tune job failed") def delete( job_id: str = typer.Argument(..., help="The fine-tune job id"), ): diff --git a/dashscope/cli/generation.py b/dashscope/cli/generation.py index ee76f34..2e02a32 100644 --- a/dashscope/cli/generation.py +++ b/dashscope/cli/generation.py @@ -6,7 +6,11 @@ import typer from dashscope.aigc import Generation -from dashscope.cli.common import console, print_failed_message +from dashscope.cli.common import ( + console, + handle_sdk_error, + print_failed_message, +) app = typer.Typer( name="generation", @@ -24,6 +28,7 @@ def callback(ctx: typer.Context): @app.command("create") +@handle_sdk_error("Generation request failed") def create( prompt: str = typer.Option(..., "-p", "--prompt", help="Input prompt"), model: str = typer.Option(..., "-m", "--model", help="The model to call"), @@ -46,13 +51,17 @@ def create( for rsp in response: if rsp.status_code == HTTPStatus.OK: console.print(rsp.output) - console.print(rsp.usage) + usage = getattr(rsp, "usage", None) + if usage: + console.print(usage) else: print_failed_message(rsp) else: if response.status_code == HTTPStatus.OK: console.print(response.output) - console.print(response.usage) + usage = getattr(response, "usage", None) + if usage: + console.print(usage) else: print_failed_message(response) raise typer.Exit(1) diff --git a/dashscope/cli/image_generation.py b/dashscope/cli/image_generation.py new file mode 100644 index 0000000..85b47f0 --- /dev/null +++ b/dashscope/cli/image_generation.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +"""``image-generation`` sub-command group.""" +import json +from typing import List, Optional + +import typer + +from dashscope.aigc.image_generation import ImageGeneration +from dashscope.api_entities.dashscope_response import Message, Role +from dashscope.cli.common import ( + console, + ensure_ok, + handle_sdk_error, + normalize_local_path_or_url, +) + +app = typer.Typer( + name="image-generation", + help="Image generation commands", + add_completion=False, + invoke_without_command=True, +) + + +@app.callback() +def callback(ctx: typer.Context): + """Show help if no subcommand is provided.""" + if ctx.invoked_subcommand is None: + typer.echo(ctx.get_help()) + + +@app.command("create") +@handle_sdk_error("Image generation request failed") +def create( + model: str = typer.Option(..., "-m", "--model", help="The model to call"), + text: str = typer.Option(..., "-t", "--text", help="User text prompt"), + images: Optional[List[str]] = typer.Option( + None, + "--image", + help="Reference image URL or local file path, can be used multiple times", + ), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), + size: Optional[str] = typer.Option( + None, "--size", help="Output image size" + ), + n: Optional[int] = typer.Option( + None, "-n", "--n", help="Number of images" + ), + max_images: Optional[int] = typer.Option( + None, + "--max-images", + help="Maximum number of images", + ), +): + """Call image generation API.""" + content = [{"text": text}] + if images: + content.extend( + {"image": normalize_local_path_or_url(image, "--image")} + for image in images + ) + + response = ImageGeneration.call( + model=model, + messages=[Message(role=Role.USER, content=content)], + workspace=workspace, + size=size, + n=n, + max_images=max_images, + ) + output = ensure_ok(response) + console.print_json(json.dumps(output, ensure_ascii=False)) + usage = getattr(response, "usage", None) + if usage: + console.print_json(json.dumps(usage, ensure_ascii=False)) diff --git a/dashscope/cli/image_synthesis.py b/dashscope/cli/image_synthesis.py new file mode 100644 index 0000000..03be0de --- /dev/null +++ b/dashscope/cli/image_synthesis.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +"""``image-synthesis`` sub-command group.""" +import json +from typing import Optional + +import typer + +import dashscope +from dashscope.cli.common import console, ensure_ok, handle_sdk_error + +app = typer.Typer( + name="image-synthesis", + help="Image synthesis commands", + add_completion=False, + invoke_without_command=True, +) + + +@app.callback() +def callback(ctx: typer.Context): + """Show help if no subcommand is provided.""" + if ctx.invoked_subcommand is None: + typer.echo(ctx.get_help()) + + +@app.command("create") +@handle_sdk_error("Image synthesis request failed") +def create( + model: str = typer.Option(..., "-m", "--model", help="The model to call"), + prompt: str = typer.Option(..., "-p", "--prompt", help="Input prompt"), + negative_prompt: Optional[str] = typer.Option( + None, + "--negative-prompt", + help="Negative prompt", + ), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), + n: Optional[int] = typer.Option(None, "-n", "--n", help="Number of images"), + size: Optional[str] = typer.Option( + None, + "--size", + help="Output image size, such as 1024*1024", + ), +): + """Call image synthesis API.""" + response = dashscope.ImageSynthesis.call( + model=model, + prompt=prompt, + negative_prompt=negative_prompt, + workspace=workspace, + n=n, + size=size, + ) + output = ensure_ok(response) + console.print_json(json.dumps(output, ensure_ascii=False)) + usage = getattr(response, "usage", None) + if usage: + console.print_json(json.dumps(usage, ensure_ascii=False)) diff --git a/dashscope/cli/messages.py b/dashscope/cli/messages.py new file mode 100644 index 0000000..d65a65b --- /dev/null +++ b/dashscope/cli/messages.py @@ -0,0 +1,224 @@ +# -*- coding: utf-8 -*- +"""``messages`` sub-command group.""" +import json +from typing import List, Optional + +import typer + +import dashscope +from dashscope.cli.common import console, error, handle_sdk_error +from dashscope.threads.messages.files import Files as MessageFiles + +app = typer.Typer( + name="messages", + help="Thread message management commands", + add_completion=False, + invoke_without_command=True, +) +files_app = typer.Typer( + name="files", + help="Thread message file management commands", + add_completion=False, + invoke_without_command=True, +) +app.add_typer(files_app, name="files") + + +@app.callback() +def callback(ctx: typer.Context): + """Show help if no subcommand is provided.""" + if ctx.invoked_subcommand is None: + typer.echo(ctx.get_help()) + + +@files_app.callback() +def files_callback(ctx: typer.Context): + """Show help if no message file subcommand is provided.""" + if ctx.invoked_subcommand is None: + typer.echo(ctx.get_help()) + + +def _parse_json_object(value: Optional[str], option_name: str): + if value is None: + return None + try: + parsed_value = json.loads(value) + except json.JSONDecodeError as exception: + error(f"Invalid {option_name} JSON: {exception.msg}") + if not isinstance(parsed_value, dict): + error(f"{option_name} must be a JSON object") + return parsed_value + + +@app.command("create") +@handle_sdk_error("Create message failed") +def create( + thread_id: str = typer.Argument(..., help="The thread id"), + content: str = typer.Option(..., "-c", "--content", help="Message content"), + role: str = typer.Option("user", "--role", help="Message role"), + file_ids: Optional[List[str]] = typer.Option( + None, + "--file-id", + help="File id, can be used multiple times", + ), + metadata: Optional[str] = typer.Option( + None, + "--metadata", + help="Metadata as a JSON object string", + ), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """Create a thread message.""" + response = dashscope.Messages.create( + thread_id, + content=content, + role=role, + file_ids=file_ids or [], + metadata=_parse_json_object(metadata, "metadata"), + workspace=workspace, + ) + console.print_json( + json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + ) + + +@app.command("list") +@handle_sdk_error("List messages failed") +def list_messages( + thread_id: str = typer.Argument(..., help="The thread id"), + limit: Optional[int] = typer.Option(None, "--limit", help="Maximum number of messages"), + order: Optional[str] = typer.Option(None, "--order", help="Sort order by created_at"), + after: Optional[str] = typer.Option(None, "--after", help="Cursor after message id"), + before: Optional[str] = typer.Option(None, "--before", help="Cursor before message id"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """List thread messages.""" + response = dashscope.Messages.list( + thread_id, + limit=limit, + order=order, + after=after, + before=before, + workspace=workspace, + ) + console.print_json( + json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + ) + + +@app.command("get") +@handle_sdk_error("Retrieve message failed") +def get_message( + thread_id: str = typer.Argument(..., help="The thread id"), + message_id: str = typer.Argument(..., help="The message id"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """Retrieve a thread message.""" + response = dashscope.Messages.retrieve( + message_id, + thread_id=thread_id, + workspace=workspace, + ) + console.print_json( + json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + ) + + +@app.command("update") +@handle_sdk_error("Update message failed") +def update_message( + thread_id: str = typer.Argument(..., help="The thread id"), + message_id: str = typer.Argument(..., help="The message id"), + metadata: Optional[str] = typer.Option( + None, + "--metadata", + help="Metadata as a JSON object string", + ), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """Update a thread message.""" + response = dashscope.Messages.update( + message_id, + thread_id=thread_id, + metadata=_parse_json_object(metadata, "metadata"), + workspace=workspace, + ) + console.print_json( + json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + ) + + +@files_app.command("list") +@handle_sdk_error("List message files failed") +def list_message_files( + thread_id: str = typer.Argument(..., help="The thread id"), + message_id: str = typer.Argument(..., help="The message id"), + limit: Optional[int] = typer.Option(None, "--limit", help="Maximum number of files"), + order: Optional[str] = typer.Option(None, "--order", help="Sort order by created_at"), + after: Optional[str] = typer.Option(None, "--after", help="Cursor after file id"), + before: Optional[str] = typer.Option(None, "--before", help="Cursor before file id"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """List thread message files.""" + response = MessageFiles.list( + message_id, + thread_id=thread_id, + limit=limit, + order=order, + after=after, + before=before, + workspace=workspace, + ) + console.print_json( + json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + ) + + +@files_app.command("get") +@handle_sdk_error("Retrieve message file failed") +def get_message_file( + thread_id: str = typer.Argument(..., help="The thread id"), + message_id: str = typer.Argument(..., help="The message id"), + file_id: str = typer.Argument(..., help="The file id"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """Retrieve a thread message file.""" + response = MessageFiles.retrieve( + file_id, + thread_id=thread_id, + message_id=message_id, + workspace=workspace, + ) + console.print_json( + json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + ) diff --git a/dashscope/cli/models.py b/dashscope/cli/models.py new file mode 100644 index 0000000..dc0dc74 --- /dev/null +++ b/dashscope/cli/models.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +"""``models`` sub-command group.""" +import json + +import typer + +import dashscope +from dashscope.cli.common import console, ensure_ok, handle_sdk_error + +app = typer.Typer( + name="models", + help="Model management commands", + add_completion=False, + invoke_without_command=True, +) + + +@app.callback() +def callback(ctx: typer.Context): + """Show help if no subcommand is provided.""" + if ctx.invoked_subcommand is None: + typer.echo(ctx.get_help()) + + +@app.command("list") +@handle_sdk_error("List models failed") +def list_models( + page: int = typer.Option(1, "-p", "--page", help="Page number"), + page_size: int = typer.Option( + 10, + "-s", + "--page-size", + help="Items per page", + ), +): + """List available models.""" + response = dashscope.Models.list(page=page, page_size=page_size) + output = ensure_ok(response) + if output: + console.print_json(json.dumps(output, ensure_ascii=False)) + else: + console.print("There are no models.") + + +@app.command("get") +@handle_sdk_error("Retrieve model failed") +def get_model(name: str = typer.Argument(..., help="The model name")): + """Get model information.""" + response = dashscope.Models.get(name=name) + output = ensure_ok(response) + if output: + console.print_json(json.dumps(output, ensure_ascii=False)) + else: + console.print("There is no model.") diff --git a/dashscope/cli/multimodal_conversation.py b/dashscope/cli/multimodal_conversation.py new file mode 100644 index 0000000..3f4f8ff --- /dev/null +++ b/dashscope/cli/multimodal_conversation.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +"""``multimodal-conversation`` sub-command group.""" +import json +from typing import List, Optional + +import typer + +import dashscope +from dashscope.cli.common import ( + console, + ensure_ok, + handle_sdk_error, + normalize_local_path_or_url, +) + +app = typer.Typer( + name="multimodal-conversation", + help="Multimodal conversation commands", + add_completion=False, + invoke_without_command=True, +) + + +@app.callback() +def callback(ctx: typer.Context): + """Show help if no subcommand is provided.""" + if ctx.invoked_subcommand is None: + typer.echo(ctx.get_help()) + + +@app.command("create") +@handle_sdk_error("Multimodal conversation request failed") +def create( + model: str = typer.Option(..., "-m", "--model", help="The model to call"), + text: str = typer.Option(..., "-t", "--text", help="User text content"), + images: Optional[List[str]] = typer.Option( + None, + "--image", + help="Image URL or local file path, can be used multiple times", + ), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), + result_format: Optional[str] = typer.Option( + None, + "--result-format", + help="Result format, such as message or text", + ), + temperature: Optional[float] = typer.Option( + None, + "--temperature", + help="Sampling temperature", + ), + top_p: Optional[float] = typer.Option( + None, "--top-p", help="Top-p sampling" + ), + top_k: Optional[int] = typer.Option( + None, "--top-k", help="Top-k sampling" + ), + max_tokens: Optional[int] = typer.Option( + None, + "--max-tokens", + help="Maximum output tokens", + ), + seed: Optional[int] = typer.Option(None, "--seed", help="Random seed"), +): + """Call multimodal conversation API.""" + content = [] + if images: + content.extend( + {"image": normalize_local_path_or_url(image, "--image")} + for image in images + ) + content.append({"text": text}) + + response = dashscope.MultiModalConversation.call( + model=model, + messages=[{"role": "user", "content": content}], + workspace=workspace, + result_format=result_format, + temperature=temperature, + top_p=top_p, + top_k=top_k, + max_tokens=max_tokens, + seed=seed, + ) + output = ensure_ok(response) + console.print_json(json.dumps(output, ensure_ascii=False)) + usage = getattr(response, "usage", None) + if usage: + console.print_json(json.dumps(usage, ensure_ascii=False)) diff --git a/dashscope/cli/multimodal_embedding.py b/dashscope/cli/multimodal_embedding.py new file mode 100644 index 0000000..c2ce430 --- /dev/null +++ b/dashscope/cli/multimodal_embedding.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +"""``multimodal-embedding`` sub-command group.""" +import json +from typing import List, Optional + +import typer + +import dashscope +from dashscope.cli.common import ( + console, + ensure_ok, + error, + handle_sdk_error, + normalize_local_path_or_url, +) + +app = typer.Typer( + name="multimodal-embedding", + help="Multimodal embedding commands", + add_completion=False, + invoke_without_command=True, +) + + +@app.callback() +def callback(ctx: typer.Context): + """Show help if no subcommand is provided.""" + if ctx.invoked_subcommand is None: + typer.echo(ctx.get_help()) + + +@app.command("create") +@handle_sdk_error("Multimodal embedding request failed") +def create( + model: str = typer.Option(..., "-m", "--model", help="The model to call"), + texts: Optional[List[str]] = typer.Option( + None, + "--text", + help="Text input, can be used multiple times", + ), + images: Optional[List[str]] = typer.Option( + None, + "--image", + help="Image URL or local file path, can be used multiple times", + ), + audios: Optional[List[str]] = typer.Option( + None, + "--audio", + help="Audio URL or local file path, can be used multiple times", + ), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), + dimension: Optional[int] = typer.Option( + None, "--dimension", help="Output vector dimension" + ), + output_type: Optional[str] = typer.Option( + None, "--output-type", help="Output vector format" + ), + fps: Optional[float] = typer.Option( + None, "--fps", help="Video frame extraction ratio" + ), + instruct: Optional[str] = typer.Option( + None, "--instruct", help="Task instruction" + ), + enable_fusion: Optional[bool] = typer.Option( + None, + "--enable-fusion/--disable-fusion", + help="Whether to fuse all contents into one vector", + ), + res_level: Optional[int] = typer.Option( + None, "--res-level", help="Resolution tier" + ), + max_video_frames: Optional[int] = typer.Option( + None, + "--max-video-frames", + help="Max video sampling frames", + ), +): + """Call multimodal embedding API.""" + embedding_input = [] + if texts: + embedding_input.extend({"text": text} for text in texts) + if images: + embedding_input.extend( + {"image": normalize_local_path_or_url(image, "--image")} + for image in images + ) + if audios: + embedding_input.extend( + {"audio": normalize_local_path_or_url(audio, "--audio")} + for audio in audios + ) + if not embedding_input: + error("At least one --text, --image or --audio is required") + + response = dashscope.MultiModalEmbedding.call( + model=model, + input=embedding_input, + workspace=workspace, + dimension=dimension, + output_type=output_type, + fps=fps, + instruct=instruct, + enable_fusion=enable_fusion, + res_level=res_level, + max_video_frames=max_video_frames, + ) + output = ensure_ok(response) + console.print_json(json.dumps(output, ensure_ascii=False)) + usage = getattr(response, "usage", None) + if usage: + console.print_json(json.dumps(usage, ensure_ascii=False)) diff --git a/dashscope/cli/oss.py b/dashscope/cli/oss.py index cd3cca1..80bb815 100644 --- a/dashscope/cli/oss.py +++ b/dashscope/cli/oss.py @@ -7,7 +7,7 @@ import dashscope from dashscope.utils.oss_utils import OssUtils -from dashscope.cli.common import console, error, success +from dashscope.cli.common import console, error, handle_sdk_error, success app = typer.Typer( name="oss", @@ -25,6 +25,7 @@ def callback(ctx: typer.Context): @app.command("upload") +@handle_sdk_error("OSS upload failed") def upload( file: str = typer.Option( ..., diff --git a/dashscope/cli/rerank.py b/dashscope/cli/rerank.py new file mode 100644 index 0000000..65fd109 --- /dev/null +++ b/dashscope/cli/rerank.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +"""``rerank`` sub-command group.""" +import json +from typing import List, Optional + +import typer + +import dashscope +from dashscope.cli.common import console, ensure_ok, handle_sdk_error + +app = typer.Typer( + name="rerank", + help="Text rerank commands", + add_completion=False, + invoke_without_command=True, +) + + +@app.callback() +def callback(ctx: typer.Context): + """Show help if no subcommand is provided.""" + if ctx.invoked_subcommand is None: + typer.echo(ctx.get_help()) + + +@app.command("create") +@handle_sdk_error("Text rerank request failed") +def create( + model: str = typer.Option(..., "-m", "--model", help="The model to call"), + query: str = typer.Option(..., "-q", "--query", help="The query text"), + documents: List[str] = typer.Option( + ..., + "-d", + "--document", + help="Document text to rank. Repeat this option for multiple documents.", + ), + return_documents: Optional[bool] = typer.Option( + None, + "--return-documents/--no-return-documents", + help="Whether to return original documents in results", + ), + top_n: Optional[int] = typer.Option( + None, + "-n", + "--top-n", + help="How many documents to return", + ), + instruct: Optional[str] = typer.Option( + None, + "-i", + "--instruct", + help="Instruction to guide ranking strategy", + ), +): + """Call text rerank API.""" + response = dashscope.TextReRank.call( + model=model, + query=query, + documents=documents, + return_documents=return_documents, + top_n=top_n, + instruct=instruct, + ) + output = ensure_ok(response) + console.print_json(json.dumps(output, ensure_ascii=False)) + usage = getattr(response, "usage", None) + if usage: + console.print_json(json.dumps(usage, ensure_ascii=False)) diff --git a/dashscope/cli/runs.py b/dashscope/cli/runs.py new file mode 100644 index 0000000..e4411af --- /dev/null +++ b/dashscope/cli/runs.py @@ -0,0 +1,286 @@ +# -*- coding: utf-8 -*- +"""``runs`` sub-command group.""" +import json +from typing import Optional + +import typer + +import dashscope +from dashscope.cli.common import console, error, handle_sdk_error + +app = typer.Typer( + name="runs", + help="Thread run management commands", + add_completion=False, + invoke_without_command=True, +) + + +@app.callback() +def callback(ctx: typer.Context): + """Show help if no subcommand is provided.""" + if ctx.invoked_subcommand is None: + typer.echo(ctx.get_help()) + + +def _parse_json_object(value: Optional[str], option_name: str): + if value is None: + return None + try: + parsed_value = json.loads(value) + except json.JSONDecodeError as exception: + error(f"Invalid {option_name} JSON: {exception.msg}") + if not isinstance(parsed_value, dict): + error(f"{option_name} must be a JSON object") + return parsed_value + + +def _parse_json_array(value: Optional[str], option_name: str): + if value is None: + return None + try: + parsed_value = json.loads(value) + except json.JSONDecodeError as exception: + error(f"Invalid {option_name} JSON: {exception.msg}") + if not isinstance(parsed_value, list): + error(f"{option_name} must be a JSON array") + return parsed_value + + +@app.command("create") +@handle_sdk_error("Create run failed") +def create( + thread_id: str = typer.Argument(..., help="The thread id"), + assistant_id: str = typer.Option( + ..., + "--assistant-id", + help="The assistant id to run", + ), + model: Optional[str] = typer.Option(None, "-m", "--model", help="The model to use"), + instructions: Optional[str] = typer.Option(None, "--instructions", help="Run instructions"), + additional_instructions: Optional[str] = typer.Option( + None, + "--additional-instructions", + help="Additional instructions appended for this run", + ), + tools: Optional[str] = typer.Option(None, "--tools", help="Tools as a JSON array string"), + metadata: Optional[str] = typer.Option(None, "--metadata", help="Metadata as a JSON object string"), + extra_body: Optional[str] = typer.Option(None, "--extra-body", help="Extra body as a JSON object string"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), + top_p: Optional[float] = typer.Option(None, "--top-p", help="Top-p sampling"), + top_k: Optional[int] = typer.Option(None, "--top-k", help="Top-k sampling"), + temperature: Optional[float] = typer.Option(None, "--temperature", help="Sampling temperature"), + max_tokens: Optional[int] = typer.Option(None, "--max-tokens", help="Maximum output tokens"), +): + """Create a run.""" + response = dashscope.Runs.create( + thread_id, + assistant_id=assistant_id, + model=model, + instructions=instructions, + additional_instructions=additional_instructions, + tools=_parse_json_array(tools, "tools"), + metadata=_parse_json_object(metadata, "metadata"), + extra_body=_parse_json_object(extra_body, "extra_body"), + workspace=workspace, + top_p=top_p, + top_k=top_k, + temperature=temperature, + max_tokens=max_tokens, + ) + console.print_json( + json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + ) + + +@app.command("get") +@handle_sdk_error("Retrieve run failed") +def get_run( + run_id: str = typer.Argument(..., help="The run id"), + thread_id: str = typer.Option( + ..., + "--thread-id", + help="The thread id", + ), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """Retrieve a run.""" + response = dashscope.Runs.retrieve( + run_id, + thread_id=thread_id, + workspace=workspace, + ) + console.print_json( + json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + ) + + +@app.command("list") +@handle_sdk_error("List runs failed") +def list_runs( + thread_id: str = typer.Argument(..., help="The thread id"), + limit: Optional[int] = typer.Option(None, "--limit", help="Maximum number of runs"), + order: Optional[str] = typer.Option(None, "--order", help="Sort order by created_at"), + after: Optional[str] = typer.Option(None, "--after", help="Cursor after run id"), + before: Optional[str] = typer.Option(None, "--before", help="Cursor before run id"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """List runs of a thread.""" + response = dashscope.Runs.list( + thread_id, + limit=limit, + order=order, + after=after, + before=before, + workspace=workspace, + ) + console.print_json( + json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + ) + + +@app.command("submit-tool-outputs") +@handle_sdk_error("Submit tool outputs failed") +def submit_tool_outputs( + run_id: str = typer.Argument(..., help="The run id"), + thread_id: str = typer.Option( + ..., + "--thread-id", + help="The thread id", + ), + tool_outputs: str = typer.Option( + ..., + "--tool-outputs", + help="Tool outputs as a JSON array string", + ), + extra_body: Optional[str] = typer.Option( + None, + "--extra-body", + help="Extra body as a JSON object string", + ), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """Submit tool outputs to a run.""" + response = dashscope.Runs.submit_tool_outputs( + run_id, + thread_id=thread_id, + tool_outputs=_parse_json_array(tool_outputs, "tool_outputs"), + extra_body=_parse_json_object(extra_body, "extra_body"), + workspace=workspace, + ) + console.print_json( + json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + ) + + +@app.command("wait") +@handle_sdk_error("Wait run failed") +def wait_run( + run_id: str = typer.Argument(..., help="The run id"), + thread_id: str = typer.Option( + ..., + "--thread-id", + help="The thread id", + ), + timeout_seconds: float = typer.Option( + float("inf"), + "--timeout-seconds", + help="Maximum seconds to wait", + ), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """Wait for a run to reach a terminal status.""" + response = dashscope.Runs.wait( + run_id, + thread_id=thread_id, + timeout_seconds=timeout_seconds, + workspace=workspace, + ) + console.print_json( + json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + ) + + +@app.command("update") +@handle_sdk_error("Update run failed") +def update_run( + run_id: str = typer.Argument(..., help="The run id"), + thread_id: str = typer.Option( + ..., + "--thread-id", + help="The thread id", + ), + metadata: Optional[str] = typer.Option( + None, + "--metadata", + help="Metadata as a JSON object string", + ), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """Update a run.""" + response = dashscope.Runs.update( + run_id, + thread_id=thread_id, + metadata=_parse_json_object(metadata, "metadata"), + workspace=workspace, + ) + console.print_json( + json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + ) + + +@app.command("cancel") +@handle_sdk_error("Cancel run failed") +def cancel_run( + run_id: str = typer.Argument(..., help="The run id"), + thread_id: str = typer.Option( + ..., + "--thread-id", + help="The thread id", + ), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """Cancel a run.""" + response = dashscope.Runs.cancel( + run_id, + thread_id=thread_id, + workspace=workspace, + ) + console.print_json( + json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + ) diff --git a/dashscope/cli/speech_synthesis.py b/dashscope/cli/speech_synthesis.py new file mode 100644 index 0000000..cedac39 --- /dev/null +++ b/dashscope/cli/speech_synthesis.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +"""``speech-synthesis`` sub-command group.""" +import json +from typing import Optional + +import typer + +import dashscope +from dashscope.cli.common import console, handle_sdk_error + +app = typer.Typer( + name="speech-synthesis", + help="Speech synthesis commands", + add_completion=False, + invoke_without_command=True, +) + + +@app.callback() +def callback(ctx: typer.Context): + """Show help if no subcommand is provided.""" + if ctx.invoked_subcommand is None: + typer.echo(ctx.get_help()) + + +@app.command("create") +@handle_sdk_error("Speech synthesis request failed") +def create( + model: str = typer.Option(..., "-m", "--model", help="The model to call"), + text: str = typer.Option(..., "-t", "--text", help="Text to synthesize"), + voice: str = typer.Option(..., "--voice", help="Voice name"), + audio_format: str = typer.Option( + "wav", + "--audio-format", + help="Audio format, such as wav, pcm or mp3", + ), + sample_rate: int = typer.Option(24000, "--sample-rate", help="Audio sample rate"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), + url: Optional[str] = typer.Option(None, "--url", help="Custom HTTP base URL"), + volume: Optional[int] = typer.Option(None, "--volume", help="Volume value"), + rate: Optional[int] = typer.Option(None, "--rate", help="Speech rate value"), + pitch: Optional[int] = typer.Option(None, "--pitch", help="Pitch value"), +): + """Call HTTP speech synthesis API.""" + result = dashscope.HttpSpeechSynthesizer.call( + model=model, + text=text, + voice=voice, + audio_format=audio_format, + sample_rate=sample_rate, + workspace=workspace, + url=url, + volume=volume, + rate=rate, + pitch=pitch, + ) + output = { + "audio_url": result.audio_url, + "audio_id": result.audio_id, + "expires_at": result.expires_at, + "sentences": result.sentences, + } + console.print_json(json.dumps(output, ensure_ascii=False)) diff --git a/dashscope/cli/steps.py b/dashscope/cli/steps.py new file mode 100644 index 0000000..478c2e6 --- /dev/null +++ b/dashscope/cli/steps.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +"""``steps`` sub-command group.""" +import json +from typing import Optional + +import typer + +import dashscope +from dashscope.cli.common import console, handle_sdk_error + +app = typer.Typer( + name="steps", + help="Run step management commands", + add_completion=False, + invoke_without_command=True, +) + + +@app.callback() +def callback(ctx: typer.Context): + """Show help if no subcommand is provided.""" + if ctx.invoked_subcommand is None: + typer.echo(ctx.get_help()) + + +@app.command("list") +@handle_sdk_error("List run steps failed") +def list_steps( + run_id: str = typer.Argument(..., help="The run id"), + thread_id: str = typer.Option( + ..., + "--thread-id", + help="The thread id", + ), + limit: Optional[int] = typer.Option(None, "--limit", help="Maximum number of steps"), + order: Optional[str] = typer.Option(None, "--order", help="Sort order by created_at"), + after: Optional[str] = typer.Option(None, "--after", help="Cursor after step id"), + before: Optional[str] = typer.Option(None, "--before", help="Cursor before step id"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """List run steps.""" + response = dashscope.Steps.list( + run_id, + thread_id=thread_id, + limit=limit, + order=order, + after=after, + before=before, + workspace=workspace, + ) + console.print_json( + json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + ) + + +@app.command("get") +@handle_sdk_error("Retrieve run step failed") +def get_step( + step_id: str = typer.Argument(..., help="The step id"), + thread_id: str = typer.Option( + ..., + "--thread-id", + help="The thread id", + ), + run_id: str = typer.Option( + ..., + "--run-id", + help="The run id", + ), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """Retrieve a run step.""" + response = dashscope.Steps.retrieve( + step_id, + thread_id=thread_id, + run_id=run_id, + workspace=workspace, + ) + console.print_json( + json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + ) diff --git a/dashscope/cli/threads.py b/dashscope/cli/threads.py new file mode 100644 index 0000000..c846298 --- /dev/null +++ b/dashscope/cli/threads.py @@ -0,0 +1,188 @@ +# -*- coding: utf-8 -*- +"""``threads`` sub-command group.""" +import json +from typing import Optional + +import typer + +import dashscope +from dashscope.cli.common import console, error, handle_sdk_error + +app = typer.Typer( + name="threads", + help="Thread management commands", + add_completion=False, + invoke_without_command=True, +) + + +@app.callback() +def callback(ctx: typer.Context): + """Show help if no subcommand is provided.""" + if ctx.invoked_subcommand is None: + typer.echo(ctx.get_help()) + + +def _parse_json_object(value: Optional[str], option_name: str): + if value is None: + return None + try: + parsed_value = json.loads(value) + except json.JSONDecodeError as exception: + error(f"Invalid {option_name} JSON: {exception.msg}") + if not isinstance(parsed_value, dict): + error(f"{option_name} must be a JSON object") + return parsed_value + + +def _parse_json_array(value: Optional[str], option_name: str): + if value is None: + return None + try: + parsed_value = json.loads(value) + except json.JSONDecodeError as exception: + error(f"Invalid {option_name} JSON: {exception.msg}") + if not isinstance(parsed_value, list): + error(f"{option_name} must be a JSON array") + return parsed_value + + +@app.command("create") +@handle_sdk_error("Create thread failed") +def create( + messages: Optional[str] = typer.Option( + None, + "--messages", + help="Initial messages as a JSON array string", + ), + metadata: Optional[str] = typer.Option( + None, + "--metadata", + help="Metadata as a JSON object string", + ), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """Create a thread.""" + response = dashscope.Threads.create( + messages=_parse_json_array(messages, "messages"), + metadata=_parse_json_object(metadata, "metadata"), + workspace=workspace, + ) + console.print_json( + json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + ) + + +@app.command("update") +@handle_sdk_error("Update thread failed") +def update_thread( + thread_id: str = typer.Argument(..., help="The thread id"), + metadata: Optional[str] = typer.Option( + None, + "--metadata", + help="Metadata as a JSON object string", + ), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """Update a thread.""" + response = dashscope.Threads.update( + thread_id, + metadata=_parse_json_object(metadata, "metadata"), + workspace=workspace, + ) + console.print_json( + json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + ) + + +@app.command("get") +@handle_sdk_error("Retrieve thread failed") +def get_thread( + thread_id: str = typer.Argument(..., help="The thread id"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """Retrieve a thread.""" + response = dashscope.Threads.retrieve( + thread_id, + workspace=workspace, + ) + console.print_json( + json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + ) + + +@app.command("delete") +@handle_sdk_error("Delete thread failed") +def delete_thread( + thread_id: str = typer.Argument(..., help="The thread id"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """Delete a thread.""" + response = dashscope.Threads.delete( + thread_id, + workspace=workspace, + ) + console.print_json( + json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + ) + + +@app.command("create-and-run") +@handle_sdk_error("Create thread and run failed") +def create_and_run( + assistant_id: str = typer.Option(..., "--assistant-id", help="The assistant id"), + thread: Optional[str] = typer.Option(None, "--thread", help="Thread as a JSON object string"), + model: Optional[str] = typer.Option(None, "-m", "--model", help="The model to use"), + instructions: Optional[str] = typer.Option(None, "--instructions", help="Run instructions"), + additional_instructions: Optional[str] = typer.Option( + None, + "--additional-instructions", + help="Additional run instructions", + ), + tools: Optional[str] = typer.Option(None, "--tools", help="Tools as a JSON array string"), + metadata: Optional[str] = typer.Option( + None, + "--metadata", + help="Metadata as a JSON object string", + ), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """Create a thread and run it.""" + response = dashscope.Threads.create_and_run( + assistant_id=assistant_id, + thread=_parse_json_object(thread, "thread"), + model=model, + instructions=instructions, + additional_instructions=additional_instructions, + tools=_parse_json_array(tools, "tools"), + metadata=_parse_json_object(metadata, "metadata"), + workspace=workspace, + ) + console.print_json( + json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + ) diff --git a/dashscope/cli/tokenization.py b/dashscope/cli/tokenization.py new file mode 100644 index 0000000..a869d01 --- /dev/null +++ b/dashscope/cli/tokenization.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +"""``tokenization`` sub-command group.""" +import json +from typing import Optional + +import typer + +import dashscope +from dashscope.cli.common import console, ensure_ok, handle_sdk_error + +app = typer.Typer( + name="tokenization", + help="Tokenization commands", + add_completion=False, + invoke_without_command=True, +) + + +@app.callback() +def callback(ctx: typer.Context): + """Show help if no subcommand is provided.""" + if ctx.invoked_subcommand is None: + typer.echo(ctx.get_help()) + + +@app.command("create") +@handle_sdk_error("Tokenization request failed") +def create( + model: str = typer.Option(..., "-m", "--model", help="The model to call"), + prompt: str = typer.Option(..., "-p", "--prompt", help="Input prompt"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), + enable_search: bool = typer.Option( + False, + "--enable-search", + help="Enable search for supported qwen models", + ), +): + """Call tokenization API.""" + response = dashscope.Tokenization.call( + model=model, + prompt=prompt, + workspace=workspace, + enable_search=enable_search, + ) + output = ensure_ok(response) + console.print_json(json.dumps(output, ensure_ascii=False)) + usage = getattr(response, "usage", None) + if usage: + console.print_json(json.dumps(usage, ensure_ascii=False)) diff --git a/dashscope/cli/transcription.py b/dashscope/cli/transcription.py new file mode 100644 index 0000000..9425591 --- /dev/null +++ b/dashscope/cli/transcription.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +"""``transcription`` sub-command group.""" +import json +from typing import List, Optional + +import typer + +import dashscope +from dashscope.cli.common import console, ensure_ok, handle_sdk_error + +app = typer.Typer( + name="transcription", + help="Speech transcription commands", + add_completion=False, + invoke_without_command=True, +) + + +@app.callback() +def callback(ctx: typer.Context): + """Show help if no subcommand is provided.""" + if ctx.invoked_subcommand is None: + typer.echo(ctx.get_help()) + + +@app.command("create") +@handle_sdk_error("Transcription request failed") +def create( + model: str = typer.Option(..., "-m", "--model", help="The model to call"), + file_urls: List[str] = typer.Option( + ..., + "--file-url", + help="Audio file URL, can be used multiple times", + ), + phrase_id: Optional[str] = typer.Option(None, "--phrase-id", help="Phrase id"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), + channel_ids: Optional[List[int]] = typer.Option( + None, + "--channel-id", + help="Selected channel id, can be used multiple times", + ), + disfluency_removal_enabled: bool = typer.Option( + False, + "--disfluency-removal-enabled", + help="Whether to remove disfluency words", + ), + diarization_enabled: bool = typer.Option( + False, + "--diarization-enabled", + help="Whether to enable speaker diarization", + ), + speaker_count: Optional[int] = typer.Option(None, "--speaker-count", help="Speaker count"), + timestamp_alignment_enabled: bool = typer.Option( + False, + "--timestamp-alignment-enabled", + help="Whether to enable timestamp alignment", + ), + special_word_filter: Optional[str] = typer.Option( + None, + "--special-word-filter", + help="Special word filter", + ), + audio_event_detection_enabled: bool = typer.Option( + False, + "--audio-event-detection-enabled", + help="Whether to enable audio event detection", + ), +): + """Call speech transcription API.""" + response = dashscope.Transcription.call( + model=model, + file_urls=file_urls, + phrase_id=phrase_id, + workspace=workspace, + channel_id=channel_ids, + disfluency_removal_enabled=disfluency_removal_enabled, + diarization_enabled=diarization_enabled, + speaker_count=speaker_count, + timestamp_alignment_enabled=timestamp_alignment_enabled, + special_word_filter=special_word_filter, + audio_event_detection_enabled=audio_event_detection_enabled, + ) + output = ensure_ok(response) + console.print_json(json.dumps(output, ensure_ascii=False)) + usage = getattr(response, "usage", None) + if usage: + console.print_json(json.dumps(usage, ensure_ascii=False)) diff --git a/dashscope/cli/understanding.py b/dashscope/cli/understanding.py new file mode 100644 index 0000000..f7c2dd8 --- /dev/null +++ b/dashscope/cli/understanding.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +"""``understanding`` sub-command group.""" +import json +from typing import Optional + +import typer + +import dashscope +from dashscope.cli.common import console, ensure_ok, handle_sdk_error + +app = typer.Typer( + name="understanding", + help="Natural language understanding commands", + add_completion=False, + invoke_without_command=True, +) + + +@app.callback() +def callback(ctx: typer.Context): + """Show help if no subcommand is provided.""" + if ctx.invoked_subcommand is None: + typer.echo(ctx.get_help()) + + +@app.command("create") +@handle_sdk_error("Understanding request failed") +def create( + model: str = typer.Option(..., "-m", "--model", help="The model to call"), + sentence: str = typer.Option( + ..., + "-s", + "--sentence", + help="The sentence to process", + ), + labels: str = typer.Option( + ..., + "-l", + "--labels", + help="Labels separated by Chinese commas", + ), + task: Optional[str] = typer.Option( + None, + "-t", + "--task", + help="Task type, such as extraction or classification", + ), +): + """Call natural language understanding API.""" + response = dashscope.Understanding.call( + model=model, + sentence=sentence, + labels=labels, + task=task, + ) + output = ensure_ok(response) + console.print_json(json.dumps(output, ensure_ascii=False)) + usage = getattr(response, "usage", None) + if usage: + console.print_json(json.dumps(usage, ensure_ascii=False)) diff --git a/dashscope/cli/video_synthesis.py b/dashscope/cli/video_synthesis.py new file mode 100644 index 0000000..7f7150d --- /dev/null +++ b/dashscope/cli/video_synthesis.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- +"""``video-synthesis`` sub-command group.""" +import json +from typing import Optional + +import typer + +import dashscope +from dashscope.cli.common import console, ensure_ok, handle_sdk_error, success + +app = typer.Typer( + name="video-synthesis", + help="Video synthesis commands", + add_completion=False, + invoke_without_command=True, +) + + +@app.callback() +def callback(ctx: typer.Context): + """Show help if no subcommand is provided.""" + if ctx.invoked_subcommand is None: + typer.echo(ctx.get_help()) + + +@app.command("create") +@handle_sdk_error("Video synthesis request failed") +def create( + model: str = typer.Option(..., "-m", "--model", help="The model to call"), + prompt: Optional[str] = typer.Option(None, "-p", "--prompt", help="Input prompt"), + negative_prompt: Optional[str] = typer.Option( + None, + "--negative-prompt", + help="Negative prompt", + ), + img_url: Optional[str] = typer.Option(None, "--img-url", help="Input image URL"), + first_frame_url: Optional[str] = typer.Option( + None, + "--first-frame-url", + help="First frame image URL", + ), + last_frame_url: Optional[str] = typer.Option( + None, + "--last-frame-url", + help="Last frame image URL", + ), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), + size: Optional[str] = typer.Option(None, "--size", help="Output video size"), + duration: Optional[int] = typer.Option(None, "--duration", help="Video duration"), + seed: Optional[int] = typer.Option(None, "--seed", help="Random seed"), + prompt_extend: Optional[bool] = typer.Option( + None, + "--prompt-extend/--no-prompt-extend", + help="Whether to extend prompt automatically", + ), + watermark: Optional[bool] = typer.Option( + None, + "--watermark/--no-watermark", + help="Whether to add watermark", + ), + resolution: Optional[str] = typer.Option(None, "--resolution", help="Output resolution"), + ratio: Optional[str] = typer.Option(None, "--ratio", help="Aspect ratio"), +): + """Call video synthesis API.""" + response = dashscope.VideoSynthesis.call( + model=model, + prompt=prompt, + negative_prompt=negative_prompt, + img_url=img_url, + first_frame_url=first_frame_url, + last_frame_url=last_frame_url, + workspace=workspace, + size=size, + duration=duration, + seed=seed, + prompt_extend=prompt_extend, + watermark=watermark, + resolution=resolution, + ratio=ratio, + ) + output = ensure_ok(response) + console.print_json(json.dumps(output, ensure_ascii=False)) + usage = getattr(response, "usage", None) + if usage: + console.print_json(json.dumps(usage, ensure_ascii=False)) + + +@app.command("fetch") +@handle_sdk_error("Fetch video synthesis task failed") +def fetch( + task_id: str = typer.Argument(..., help="The video synthesis task id"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """Fetch video synthesis task status or result.""" + response = dashscope.VideoSynthesis.fetch(task_id, workspace=workspace) + output = ensure_ok(response) + console.print_json(json.dumps(output, ensure_ascii=False)) + + +@app.command("wait") +@handle_sdk_error("Wait video synthesis task failed") +def wait( + task_id: str = typer.Argument(..., help="The video synthesis task id"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """Wait for a video synthesis task to complete.""" + response = dashscope.VideoSynthesis.wait(task_id, workspace=workspace) + output = ensure_ok(response) + console.print_json(json.dumps(output, ensure_ascii=False)) + + +@app.command("cancel") +@handle_sdk_error("Cancel video synthesis task failed") +def cancel( + task_id: str = typer.Argument(..., help="The video synthesis task id"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """Cancel a pending video synthesis task.""" + ensure_ok(dashscope.VideoSynthesis.cancel(task_id, workspace=workspace)) + success(f"Cancel video synthesis task: {task_id} success") + + +@app.command("list") +@handle_sdk_error("List video synthesis tasks failed") +def list_tasks( + start_time: Optional[str] = typer.Option(None, "--start-time", help="Task start time"), + end_time: Optional[str] = typer.Option(None, "--end-time", help="Task end time"), + model_name: Optional[str] = typer.Option(None, "--model-name", help="Model name"), + api_key_id: Optional[str] = typer.Option(None, "--api-key-id", help="API key id"), + region: Optional[str] = typer.Option(None, "--region", help="Service region"), + status: Optional[str] = typer.Option(None, "--status", help="Task status"), + page: int = typer.Option(1, "-p", "--page", help="Page number"), + size: int = typer.Option(10, "-s", "--size", help="Page size"), + workspace: Optional[str] = typer.Option( + None, + "-w", + "--workspace", + help="The DashScope workspace id", + ), +): + """List video synthesis tasks.""" + response = dashscope.VideoSynthesis.list( + start_time=start_time, + end_time=end_time, + model_name=model_name, + api_key_id=api_key_id, + region=region, + status=status, + page_no=page, + page_size=size, + workspace=workspace, + ) + output = ensure_ok(response) + console.print_json(json.dumps(output, ensure_ascii=False)) diff --git a/setup.cfg b/setup.cfg index 7489038..e49415d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1 +1,6 @@ -[bdist_wheel] \ No newline at end of file +[bdist_wheel] + +[flake8] +extend-ignore = E203 +per-file-ignores = + dashscope/__init__.py:E402 \ No newline at end of file diff --git a/tests/unit/test_cli_application.py b/tests/unit/test_cli_application.py new file mode 100644 index 0000000..7aa3f36 --- /dev/null +++ b/tests/unit/test_cli_application.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from types import SimpleNamespace + +from typer.testing import CliRunner + +from dashscope.cli import application + + +class TestCliApplication: + def test_create(self, monkeypatch): + captured_request = {} + + def mock_call(**kwargs): + captured_request.update(kwargs) + return SimpleNamespace( + status_code=200, + output={ + "text": "APIๆŽฅๅฃ่ฏดๆ˜Žไธญ๏ผŒ้€š่ฟ‡parameters็š„topPๅฑžๆ€ง่ฎพ็ฝฎใ€‚", + "finish_reason": "stop", + }, + usage={ + "models": [ + { + "model_id": "qwen-turbo", + "input_tokens": 10, + "output_tokens": 8, + }, + ], + }, + ) + + monkeypatch.setattr(application.dashscope.Application, "call", mock_call) + + result = CliRunner().invoke( + application.app, + [ + "create", + "--app-id", + "app-1234", + "--prompt", + "APIๆŽฅๅฃ่ฏดๆ˜Žไธญ, TopPๅ‚ๆ•ฐ่ฏฅๅฆ‚ไฝ•ไผ ้€’?", + "--workspace", + "ws-1234", + "--session-id", + "session-1234", + "--doc-tag-code", + "tag-1", + "--doc-tag-code", + "tag-2", + "--doc-reference-type", + "simple", + "--has-thoughts", + "--temperature", + "1.0", + "--top-p", + "0.2", + "--top-k", + "50", + ], + ) + + assert result.exit_code == 0 + assert captured_request == { + "app_id": "app-1234", + "prompt": "APIๆŽฅๅฃ่ฏดๆ˜Žไธญ, TopPๅ‚ๆ•ฐ่ฏฅๅฆ‚ไฝ•ไผ ้€’?", + "workspace": "ws-1234", + "session_id": "session-1234", + "doc_tag_codes": ["tag-1", "tag-2"], + "doc_reference_type": "simple", + "has_thoughts": True, + "temperature": 1.0, + "top_p": 0.2, + "top_k": 50, + } + assert "finish_reason" in result.output + assert "input_tokens" in result.output diff --git a/tests/unit/test_cli_assistant_files.py b/tests/unit/test_cli_assistant_files.py new file mode 100644 index 0000000..7bc26bb --- /dev/null +++ b/tests/unit/test_cli_assistant_files.py @@ -0,0 +1,192 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from types import SimpleNamespace + +from typer.testing import CliRunner + +from dashscope.cli import assistant_files + + +class TestCliAssistantFiles: + def test_create(self, monkeypatch): + captured_assistant_id = None + captured_request = {} + + def mock_create(assistant_id, **kwargs): + nonlocal captured_assistant_id + captured_assistant_id = assistant_id + captured_request.update(kwargs) + return SimpleNamespace( + id=kwargs["file_id"], + object="assistant.file", + created_at=111111, + assistant_id=assistant_id, + ) + + monkeypatch.setattr( + assistant_files.Files, + "create", + mock_create, + ) + + result = CliRunner().invoke( + assistant_files.app, + [ + "create", + "asst-1234", + "--file-id", + "file-1234", + "--workspace", + "workspace-id", + ], + ) + + assert result.exit_code == 0 + assert captured_assistant_id == "asst-1234" + assert captured_request == { + "file_id": "file-1234", + "workspace": "workspace-id", + } + assert "file-1234" in result.output + assert "asst-1234" in result.output + + def test_get(self, monkeypatch): + captured_file_id = None + captured_request = {} + + def mock_retrieve(file_id, **kwargs): + nonlocal captured_file_id + captured_file_id = file_id + captured_request.update(kwargs) + return SimpleNamespace( + id=file_id, + object="assistant.file", + created_at=111111, + assistant_id=kwargs["assistant_id"], + ) + + monkeypatch.setattr( + assistant_files.Files, + "retrieve", + mock_retrieve, + ) + + result = CliRunner().invoke( + assistant_files.app, + [ + "get", + "asst-1234", + "file-1234", + "--workspace", + "workspace-id", + ], + ) + + assert result.exit_code == 0 + assert captured_file_id == "file-1234" + assert captured_request == { + "assistant_id": "asst-1234", + "workspace": "workspace-id", + } + assert "file-1234" in result.output + assert "asst-1234" in result.output + + def test_delete(self, monkeypatch): + captured_file_id = None + captured_request = {} + + def mock_delete(file_id, **kwargs): + nonlocal captured_file_id + captured_file_id = file_id + captured_request.update(kwargs) + return SimpleNamespace( + id=file_id, + object="assistant.file", + created_at=111111, + deleted=True, + ) + + monkeypatch.setattr( + assistant_files.Files, + "delete", + mock_delete, + ) + + result = CliRunner().invoke( + assistant_files.app, + [ + "delete", + "asst-1234", + "file-1234", + "--workspace", + "workspace-id", + ], + ) + + assert result.exit_code == 0 + assert captured_file_id == "file-1234" + assert captured_request == { + "assistant_id": "asst-1234", + "workspace": "workspace-id", + } + assert "file-1234" in result.output + assert "true" in result.output + + def test_list(self, monkeypatch): + captured_assistant_id = None + captured_request = {} + + def mock_list(assistant_id, **kwargs): + nonlocal captured_assistant_id + captured_assistant_id = assistant_id + captured_request.update(kwargs) + return SimpleNamespace( + first_id="file-1234", + last_id="file-5678", + has_more=False, + data=[ + SimpleNamespace( + id="file-1234", + object="assistant.file", + created_at=111111, + assistant_id=assistant_id, + ) + ], + ) + + monkeypatch.setattr( + assistant_files.Files, + "list", + mock_list, + ) + + result = CliRunner().invoke( + assistant_files.app, + [ + "list", + "asst-1234", + "--limit", + "10", + "--order", + "asc", + "--after", + "file-0001", + "--before", + "file-9999", + "--workspace", + "workspace-id", + ], + ) + + assert result.exit_code == 0 + assert captured_assistant_id == "asst-1234" + assert captured_request == { + "limit": 10, + "order": "asc", + "after": "file-0001", + "before": "file-9999", + "workspace": "workspace-id", + } + assert "file-1234" in result.output + assert "asst-1234" in result.output diff --git a/tests/unit/test_cli_assistants.py b/tests/unit/test_cli_assistants.py new file mode 100644 index 0000000..642dd5f --- /dev/null +++ b/tests/unit/test_cli_assistants.py @@ -0,0 +1,282 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from types import SimpleNamespace + +from typer.testing import CliRunner + +from dashscope.cli import assistants + + +class TestCliAssistants: + def test_create(self, monkeypatch): + captured_request = {} + + def mock_create(**kwargs): + captured_request.update(kwargs) + return SimpleNamespace( + id="asst-1234", + object="assistant", + model=kwargs["model"], + name=kwargs["name"], + description=kwargs["description"], + instructions=kwargs["instructions"], + tools=kwargs["tools"], + file_ids=kwargs["file_ids"], + metadata=kwargs["metadata"], + ) + + monkeypatch.setattr( + assistants.dashscope.Assistants, + "create", + mock_create, + ) + + result = CliRunner().invoke( + assistants.app, + [ + "create", + "--model", + "qwen-max", + "--name", + "smart helper", + "--description", + "A tool helper.", + "--instructions", + "You are a helpful assistant.", + "--tools", + '[{"type":"search"},{"type":"wanx"}]', + "--file-id", + "file-1", + "--file-id", + "file-2", + "--metadata", + '{"key":"value"}', + "--workspace", + "workspace-id", + "--top-p", + "0.9", + "--top-k", + "50", + "--temperature", + "0.8", + "--max-tokens", + "1024", + ], + ) + + assert result.exit_code == 0 + assert captured_request == { + "model": "qwen-max", + "name": "smart helper", + "description": "A tool helper.", + "instructions": "You are a helpful assistant.", + "tools": [{"type": "search"}, {"type": "wanx"}], + "file_ids": ["file-1", "file-2"], + "metadata": {"key": "value"}, + "workspace": "workspace-id", + "top_p": 0.9, + "top_k": 50, + "temperature": 0.8, + "max_tokens": 1024, + } + assert "asst-1234" in result.output + assert "smart helper" in result.output + + def test_get(self, monkeypatch): + captured_assistant_id = None + captured_request = {} + + def mock_retrieve(assistant_id, **kwargs): + nonlocal captured_assistant_id + captured_assistant_id = assistant_id + captured_request.update(kwargs) + return SimpleNamespace( + id="asst-1234", + object="assistant", + model="qwen-max", + name="smart helper", + instructions="You are a helpful assistant.", + ) + + monkeypatch.setattr( + assistants.dashscope.Assistants, + "retrieve", + mock_retrieve, + ) + + result = CliRunner().invoke( + assistants.app, + [ + "get", + "asst-1234", + "--workspace", + "workspace-id", + ], + ) + + assert result.exit_code == 0 + assert captured_assistant_id == "asst-1234" + assert captured_request == {"workspace": "workspace-id"} + assert "asst-1234" in result.output + assert "qwen-max" in result.output + + def test_delete(self, monkeypatch): + captured_assistant_id = None + captured_request = {} + + def mock_delete(assistant_id, **kwargs): + nonlocal captured_assistant_id + captured_assistant_id = assistant_id + captured_request.update(kwargs) + return SimpleNamespace( + id=assistant_id, + object="assistant.deleted", + deleted=True, + ) + + monkeypatch.setattr( + assistants.dashscope.Assistants, + "delete", + mock_delete, + ) + + result = CliRunner().invoke( + assistants.app, + [ + "delete", + "asst-1234", + "--workspace", + "workspace-id", + ], + ) + + assert result.exit_code == 0 + assert captured_assistant_id == "asst-1234" + assert captured_request == {"workspace": "workspace-id"} + assert "assistant.deleted" in result.output + assert "true" in result.output + + def test_update(self, monkeypatch): + captured_assistant_id = None + captured_request = {} + + def mock_update(assistant_id, **kwargs): + nonlocal captured_assistant_id + captured_assistant_id = assistant_id + captured_request.update(kwargs) + return SimpleNamespace( + id=assistant_id, + object="assistant", + model=kwargs["model"], + name=kwargs["name"], + description=kwargs["description"], + instructions=kwargs["instructions"], + tools=kwargs["tools"], + file_ids=kwargs["file_ids"], + metadata=kwargs["metadata"], + ) + + monkeypatch.setattr( + assistants.dashscope.Assistants, + "update", + mock_update, + ) + + result = CliRunner().invoke( + assistants.app, + [ + "update", + "asst-1234", + "--model", + "qwen-max", + "--name", + "smart helper", + "--description", + "Updated description.", + "--instructions", + "You are a helpful assistant.", + "--tools", + '[{"type":"search"}]', + "--file-id", + "file-1", + "--metadata", + '{"key":"value"}', + "--workspace", + "workspace-id", + "--top-p", + "0.9", + "--top-k", + "50", + "--temperature", + "0.8", + "--max-tokens", + "1024", + ], + ) + + assert result.exit_code == 0 + assert captured_assistant_id == "asst-1234" + assert captured_request == { + "model": "qwen-max", + "name": "smart helper", + "description": "Updated description.", + "instructions": "You are a helpful assistant.", + "tools": [{"type": "search"}], + "file_ids": ["file-1"], + "metadata": {"key": "value"}, + "workspace": "workspace-id", + "top_p": 0.9, + "top_k": 50, + "temperature": 0.8, + "max_tokens": 1024, + } + assert "asst-1234" in result.output + assert "Updated description" in result.output + + def test_list(self, monkeypatch): + captured_request = {} + + def mock_list(**kwargs): + captured_request.update(kwargs) + return SimpleNamespace( + object="list", + data=[SimpleNamespace(id="asst-1234", model="qwen-max")], + first_id="asst-1234", + last_id="asst-1234", + has_more=False, + ) + + monkeypatch.setattr( + assistants.dashscope.Assistants, + "list", + mock_list, + ) + + result = CliRunner().invoke( + assistants.app, + [ + "list", + "--limit", + "10", + "--order", + "asc", + "--after", + "asst-0001", + "--before", + "asst-9999", + "--workspace", + "workspace-id", + ], + ) + + assert result.exit_code == 0 + assert captured_request == { + "limit": 10, + "order": "asc", + "after": "asst-0001", + "before": "asst-9999", + "workspace": "workspace-id", + } + assert "asst-1234" in result.output + assert "qwen-max" in result.output diff --git a/tests/unit/test_cli_code_generation.py b/tests/unit/test_cli_code_generation.py new file mode 100644 index 0000000..c421e5e --- /dev/null +++ b/tests/unit/test_cli_code_generation.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from types import SimpleNamespace + +from typer.testing import CliRunner + +from dashscope.cli import code_generation + + +class TestCliCodeGeneration: + def test_create(self, monkeypatch): + captured_request = {} + + def mock_call(**kwargs): + captured_request.update(kwargs) + return SimpleNamespace( + status_code=200, + output={ + "choices": [ + { + "finish_reason": "stop", + "content": "def file_size(path): pass", + }, + ], + }, + usage={"input_tokens": 10, "output_tokens": 8}, + ) + + monkeypatch.setattr( + code_generation.dashscope.CodeGeneration, + "call", + mock_call, + ) + + result = CliRunner().invoke( + code_generation.app, + [ + "create", + "--model", + "tongyi-lingma-v1", + "--scene", + "nl2code", + "--content", + "่ฎก็ฎ—็ป™ๅฎš่ทฏๅพ„ไธ‹ๆ‰€ๆœ‰ๆ–‡ไปถ็š„ๆ€ปๅคงๅฐ", + "--attachment-meta", + '{"language":"python"}', + "--workspace", + "workspace-id", + "--n", + "1", + ], + ) + + assert result.exit_code == 0 + assert captured_request["model"] == "tongyi-lingma-v1" + assert captured_request["scene"] == "nl2code" + assert captured_request["workspace"] == "workspace-id" + assert captured_request["n"] == 1 + assert captured_request["message"][0]["role"] == "user" + assert captured_request["message"][0]["content"] == "่ฎก็ฎ—็ป™ๅฎš่ทฏๅพ„ไธ‹ๆ‰€ๆœ‰ๆ–‡ไปถ็š„ๆ€ปๅคงๅฐ" + assert captured_request["message"][1]["role"] == "attachment" + assert captured_request["message"][1]["meta"] == {"language": "python"} + assert "file_size" in result.output + assert "input_tokens" in result.output diff --git a/tests/unit/test_cli_embeddings.py b/tests/unit/test_cli_embeddings.py new file mode 100644 index 0000000..48d21db --- /dev/null +++ b/tests/unit/test_cli_embeddings.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from types import SimpleNamespace + +from typer.testing import CliRunner + +from dashscope.cli import embeddings + + +class TestCliEmbeddings: + def test_create(self, monkeypatch): + captured_request = {} + + def mock_call(**kwargs): + captured_request.update(kwargs) + return SimpleNamespace( + status_code=200, + output={ + "embeddings": [ + { + "text_index": 0, + "embedding": [0.1, 0.2], + }, + ], + }, + usage={"total_tokens": 2}, + ) + + monkeypatch.setattr(embeddings.dashscope.TextEmbedding, "call", mock_call) + + result = CliRunner().invoke( + embeddings.app, + [ + "create", + "--model", + "text-embedding-v3", + "--input", + "hello", + "--input", + "world", + "--workspace", + "workspace-id", + "--text-type", + "query", + "--dimension", + "1024", + "--output-type", + "dense", + "--instruct", + "Represent the query for retrieval.", + ], + ) + + assert result.exit_code == 0 + assert captured_request == { + "model": "text-embedding-v3", + "input": ["hello", "world"], + "workspace": "workspace-id", + "text_type": "query", + "dimension": 1024, + "output_type": "dense", + "instruct": "Represent the query for retrieval.", + } + assert "embeddings" in result.output + assert "total_tokens" in result.output diff --git a/tests/unit/test_cli_image_generation.py b/tests/unit/test_cli_image_generation.py new file mode 100644 index 0000000..29bd8b8 --- /dev/null +++ b/tests/unit/test_cli_image_generation.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from types import SimpleNamespace + +from typer.testing import CliRunner + +from dashscope.cli import image_generation + + +class TestCliImageGeneration: + def test_create(self, monkeypatch): + captured_request = {} + + def mock_call(**kwargs): + captured_request.update(kwargs) + return SimpleNamespace( + status_code=200, + output={ + "choices": [ + { + "message": { + "content": [ + {"image": "https://example.com/generated.png"}, + ], + }, + }, + ], + }, + usage={"input_tokens": 10, "output_tokens": 8}, + ) + + monkeypatch.setattr( + image_generation.ImageGeneration, + "call", + mock_call, + ) + + result = CliRunner().invoke( + image_generation.app, + [ + "create", + "--model", + "wan2.6-image", + "--text", + "ๅ‚่€ƒๅ›พ็š„้ฃŽๆ ผ็”Ÿๆˆ็•ช่Œ„็‚’่›‹", + "--image", + "https://example.com/reference-1.png", + "--image", + "https://example.com/reference-2.png", + "--workspace", + "workspace-id", + "--size", + "1024*1024", + "--n", + "1", + "--max-images", + "3", + ], + ) + + assert result.exit_code == 0 + assert captured_request["model"] == "wan2.6-image" + assert captured_request["workspace"] == "workspace-id" + assert captured_request["size"] == "1024*1024" + assert captured_request["n"] == 1 + assert captured_request["max_images"] == 3 + assert len(captured_request["messages"]) == 1 + message = captured_request["messages"][0] + assert message["role"] == "user" + assert message["content"] == [ + {"text": "ๅ‚่€ƒๅ›พ็š„้ฃŽๆ ผ็”Ÿๆˆ็•ช่Œ„็‚’่›‹"}, + {"image": "https://example.com/reference-1.png"}, + {"image": "https://example.com/reference-2.png"}, + ] + assert "generated.png" in result.output + assert "input_tokens" in result.output diff --git a/tests/unit/test_cli_image_synthesis.py b/tests/unit/test_cli_image_synthesis.py new file mode 100644 index 0000000..2afbb17 --- /dev/null +++ b/tests/unit/test_cli_image_synthesis.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from types import SimpleNamespace + +from typer.testing import CliRunner + +from dashscope.cli import image_synthesis + + +class TestCliImageSynthesis: + def test_create(self, monkeypatch): + captured_request = {} + + def mock_call(**kwargs): + captured_request.update(kwargs) + return SimpleNamespace( + status_code=200, + output={ + "task_id": "task-1234", + "task_status": "SUCCEEDED", + "results": [ + { + "url": "https://example.com/image.png", + }, + ], + }, + usage={"image_count": 1}, + ) + + monkeypatch.setattr( + image_synthesis.dashscope.ImageSynthesis, + "call", + mock_call, + ) + + result = CliRunner().invoke( + image_synthesis.app, + [ + "create", + "--model", + "wanx2.1-t2i-turbo", + "--prompt", + "ไธ€้—ดๆœ‰็€็ฒพ่‡ด็ช—ๆˆท็š„่Šฑๅบ—", + "--negative-prompt", + "ไฝŽๆธ…ๆ™ฐๅบฆ", + "--workspace", + "workspace-id", + "--n", + "1", + "--size", + "1024*1024", + ], + ) + + assert result.exit_code == 0 + assert captured_request == { + "model": "wanx2.1-t2i-turbo", + "prompt": "ไธ€้—ดๆœ‰็€็ฒพ่‡ด็ช—ๆˆท็š„่Šฑๅบ—", + "negative_prompt": "ไฝŽๆธ…ๆ™ฐๅบฆ", + "workspace": "workspace-id", + "n": 1, + "size": "1024*1024", + } + assert "task-1234" in result.output + assert "image_count" in result.output diff --git a/tests/unit/test_cli_main.py b/tests/unit/test_cli_main.py new file mode 100644 index 0000000..f536bfa --- /dev/null +++ b/tests/unit/test_cli_main.py @@ -0,0 +1,796 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +import os +import subprocess +import sys +from types import SimpleNamespace + +import pytest +from typer.testing import CliRunner + +import dashscope +from dashscope.cli import agentic_rl +from dashscope.cli import app as cli_app +from dashscope.cli import main as cli_main +from dashscope.common.error import AuthenticationError + + +class TestCliMain: + def test_main_prints_authentication_error_without_traceback( + self, monkeypatch, capsys + ): + def mock_list(**kwargs): + raise AuthenticationError("No api key provided.") + + monkeypatch.setattr( + "dashscope.cli.models.dashscope.Models.list", + mock_list, + ) + monkeypatch.setattr(sys, "argv", ["dashscope", "models", "list"]) + + with pytest.raises(SystemExit) as exception_info: + cli_main() + + captured_output = capsys.readouterr() + combined_output = captured_output.out + captured_output.err + + assert exception_info.value.code == 1 + assert "No api key provided." in combined_output + assert "Traceback" not in combined_output + + def test_top_level_help_shows_global_api_key(self): + result = CliRunner().invoke(cli_app, ["--help"]) + + assert result.exit_code == 0 + assert "--api-key" in result.output + + def test_python_module_help_suppresses_urllib3_warning(self): + result = subprocess.run( + [sys.executable, "-m", "dashscope.cli", "--help"], + check=False, + capture_output=True, + text=True, + ) + combined_output = result.stdout + result.stderr + + assert result.returncode == 0 + assert "NotOpenSSLWarning" not in combined_output + + def test_rl_test_functions_invalid_input_without_traceback(self): + result = CliRunner().invoke( + agentic_rl.app, + [ + "test-functions", + "ins-1234", + "--type", + "ROLLOUT", + "--input", + "not-json", + ], + ) + + assert result.exit_code == 1 + assert "Invalid input" in result.output + assert "Traceback" not in result.output + + def test_rl_register_functions_hyphen_alias_help(self): + result = CliRunner().invoke( + agentic_rl.app, + ["register-functions", "--help"], + ) + + assert result.exit_code == 0 + assert "rollout-classpaths" in result.output + + def test_rl_test_functions_hyphen_alias_help(self): + result = CliRunner().invoke( + agentic_rl.app, + ["test-functions", "--help"], + ) + + assert result.exit_code == 0 + assert "--input" in result.output + + def test_rl_upload_data_hyphen_alias_help(self): + result = CliRunner().invoke( + agentic_rl.app, + ["upload-data", "--help"], + ) + + assert result.exit_code == 0 + assert "training-files" in result.output + + def test_missing_global_api_key_value_does_not_consume_command( + self, monkeypatch, capsys + ): + monkeypatch.setattr( + sys, "argv", ["dashscope", "--api-key", "models", "list"] + ) + + with pytest.raises(SystemExit) as exception_info: + cli_main() + + captured_output = capsys.readouterr() + combined_output = captured_output.out + captured_output.err + + assert exception_info.value.code == 2 + assert "requires an argument" in combined_output + assert "No such command 'list'" not in combined_output + + def test_missing_global_api_key_value_does_not_consume_option( + self, monkeypatch, capsys + ): + monkeypatch.setattr(sys, "argv", ["dashscope", "--api-key", "--help"]) + + with pytest.raises(SystemExit) as exception_info: + cli_main() + + captured_output = capsys.readouterr() + combined_output = captured_output.out + captured_output.err + + assert exception_info.value.code == 2 + assert "requires an argument" in combined_output + assert "Usage:" not in combined_output + + def test_empty_global_api_key_value_exits_before_request( + self, monkeypatch, capsys + ): + monkeypatch.setattr(dashscope, "api_key", "existing-key") + monkeypatch.setattr( + sys, "argv", ["dashscope", "--api-key=", "models", "list"] + ) + + with pytest.raises(SystemExit) as exception_info: + cli_main() + + captured_output = capsys.readouterr() + combined_output = captured_output.out + captured_output.err + + assert exception_info.value.code == 2 + assert dashscope.api_key == "existing-key" + assert "requires an argument" in combined_output + assert "InvalidApiKey" not in combined_output + + def test_global_api_key_after_command_is_supported(self, monkeypatch): + captured_request = {} + + def mock_list(**kwargs): + captured_request["api_key"] = dashscope.api_key + captured_request.update(kwargs) + return SimpleNamespace(status_code=200, output={"models": []}) + + monkeypatch.setattr( + "dashscope.cli.models.dashscope.Models.list", mock_list + ) + monkeypatch.setattr(dashscope, "api_key", None) + monkeypatch.setattr( + sys, + "argv", + ["dashscope", "models", "--api-key", "command-key", "list"], + ) + + with pytest.raises(SystemExit) as exception_info: + cli_main() + + assert exception_info.value.code == 0 + assert captured_request == { + "api_key": "command-key", + "page": 1, + "page_size": 10, + } + + def test_global_api_key_equals_after_command_is_supported( + self, monkeypatch + ): + captured_request = {} + + def mock_list(**kwargs): + captured_request["api_key"] = dashscope.api_key + captured_request.update(kwargs) + return SimpleNamespace(status_code=200, output={"models": []}) + + monkeypatch.setattr( + "dashscope.cli.models.dashscope.Models.list", mock_list + ) + monkeypatch.setattr(dashscope, "api_key", None) + monkeypatch.setattr( + sys, + "argv", + ["dashscope", "models", "--api-key=command-key", "list"], + ) + + with pytest.raises(SystemExit) as exception_info: + cli_main() + + assert exception_info.value.code == 0 + assert captured_request == { + "api_key": "command-key", + "page": 1, + "page_size": 10, + } + + def test_agentic_rl_hidden_alias_help(self): + result = CliRunner().invoke(cli_app, ["agentic-rl", "--help"]) + + assert result.exit_code == 0 + assert "register_functions" in result.output + + def test_subcommand_api_key_option_is_not_consumed_by_global_parser( + self, monkeypatch + ): + captured_request = {} + + def mock_upload(model, file_path, api_key, base_address=None): + captured_request["model"] = model + captured_request["file_path"] = file_path + captured_request["api_key"] = api_key + captured_request["base_address"] = base_address + return "oss://uploaded", None + + monkeypatch.setattr( + "dashscope.cli.oss.os.path.exists", lambda path: True + ) + monkeypatch.setattr("dashscope.cli.oss.OssUtils.upload", mock_upload) + monkeypatch.setattr(dashscope, "api_key", "global-key") + monkeypatch.setattr( + sys, + "argv", + [ + "dashscope", + "oss", + "upload", + "--api-key", + "local-key", + "--file", + "~/file.txt", + "--model", + "qwen-vl", + ], + ) + + with pytest.raises(SystemExit) as exception_info: + cli_main() + + assert exception_info.value.code == 0 + assert captured_request == { + "model": "qwen-vl", + "file_path": os.path.expanduser("~/file.txt"), + "api_key": "local-key", + "base_address": None, + } + + def test_subcommand_api_key_missing_value_is_handled_by_subcommand_parser( + self, monkeypatch, capsys + ): + monkeypatch.setattr( + sys, + "argv", + [ + "dashscope", + "oss", + "upload", + "--api-key", + "--file", + "~/file.txt", + "--model", + "qwen-vl", + ], + ) + + with pytest.raises(SystemExit) as exception_info: + cli_main() + + captured_output = capsys.readouterr() + combined_output = captured_output.out + captured_output.err + + assert exception_info.value.code == 2 + assert "requires an argument" in combined_output + + def test_legacy_api_key_missing_value_exits_before_request( + self, monkeypatch, capsys + ): + monkeypatch.setattr( + sys, + "argv", + [ + "dashscope", + "generation.call", + "--api_key", + "-m", + "qwen-turbo", + "-p", + "hi", + ], + ) + + with pytest.raises(SystemExit) as exception_info: + cli_main() + + captured_output = capsys.readouterr() + combined_output = captured_output.out + captured_output.err + + assert exception_info.value.code == 2 + assert "requires an argument" in combined_output + + def test_legacy_api_key_option_is_extracted_after_translation( + self, monkeypatch + ): + monkeypatch.setattr(dashscope, "api_key", None) + captured_request = {} + + def mock_call(model, prompt, stream=False): + captured_request["api_key"] = dashscope.api_key + captured_request["model"] = model + captured_request["prompt"] = prompt + captured_request["stream"] = stream + return SimpleNamespace( + status_code=200, + output={"text": "ok"}, + usage={"input_tokens": 1}, + ) + + monkeypatch.setattr( + "dashscope.cli.generation.Generation.call", + mock_call, + ) + monkeypatch.setattr( + sys, + "argv", + [ + "dashscope", + "generation.call", + "--api_key", + "legacy-key", + "-m", + "qwen-turbo", + "-p", + "hi", + ], + ) + + with pytest.raises(SystemExit) as exception_info: + cli_main() + + assert exception_info.value.code == 0 + assert captured_request == { + "api_key": "legacy-key", + "model": "qwen-turbo", + "prompt": "hi", + "stream": False, + } + + def test_legacy_list_page_size_maps_to_size_option(self, monkeypatch): + captured_requests = {} + + def mock_files_list(**kwargs): + captured_requests["files"] = kwargs + return SimpleNamespace(status_code=200, output={}) + + def mock_fine_tunes_list(**kwargs): + captured_requests["fine_tunes"] = kwargs + return SimpleNamespace(status_code=200, output={"jobs": []}) + + def mock_deployments_list(**kwargs): + captured_requests["deployments"] = kwargs + return SimpleNamespace(status_code=200, output={"deployments": []}) + + monkeypatch.setattr( + "dashscope.cli.files.dashscope.Files.list", mock_files_list + ) + monkeypatch.setattr( + "dashscope.cli.fine_tunes.dashscope.FineTunes.list", + mock_fine_tunes_list, + ) + monkeypatch.setattr( + "dashscope.cli.deployments.dashscope.Deployments.list", + mock_deployments_list, + ) + + cases = [ + ("files.list", "files"), + ("fine_tunes.list", "fine_tunes"), + ("deployments.list", "deployments"), + ] + for legacy_command, request_key in cases: + monkeypatch.setattr( + sys, + "argv", + ["dashscope", legacy_command, "--page_size", "20"], + ) + + with pytest.raises(SystemExit) as exception_info: + cli_main() + + assert exception_info.value.code == 0 + assert captured_requests[request_key]["page_size"] == 20 + + def test_legacy_list_page_size_equals_maps_to_size_option( + self, monkeypatch + ): + captured_request = {} + + def mock_files_list(**kwargs): + captured_request.update(kwargs) + return SimpleNamespace(status_code=200, output={}) + + monkeypatch.setattr( + "dashscope.cli.files.dashscope.Files.list", mock_files_list + ) + monkeypatch.setattr( + sys, "argv", ["dashscope", "files.list", "--page_size=30"] + ) + + with pytest.raises(SystemExit) as exception_info: + cli_main() + + assert exception_info.value.code == 0 + assert captured_request["page_size"] == 30 + + def test_files_upload_missing_file_exits_without_traceback( + self, monkeypatch, capsys + ): + monkeypatch.setattr( + "dashscope.cli.files.os.path.exists", lambda path: False + ) + monkeypatch.setattr( + sys, + "argv", + ["dashscope", "files", "upload", "--file", "~/missing.jsonl"], + ) + + with pytest.raises(SystemExit) as exception_info: + cli_main() + + captured_output = capsys.readouterr() + combined_output = captured_output.out + captured_output.err + + assert exception_info.value.code == 1 + assert "does not exist" in combined_output + assert "Traceback" not in combined_output + + def test_files_upload_sdk_exception_exits_without_traceback( + self, monkeypatch, capsys + ): + def mock_upload(**kwargs): + raise RuntimeError("upload failed") + + monkeypatch.setattr( + "dashscope.cli.files.os.path.exists", lambda path: True + ) + monkeypatch.setattr( + "dashscope.cli.files.dashscope.Files.upload", mock_upload + ) + monkeypatch.setattr( + sys, + "argv", + ["dashscope", "files", "upload", "--file", "~/data.jsonl"], + ) + + with pytest.raises(SystemExit) as exception_info: + cli_main() + + captured_output = capsys.readouterr() + combined_output = captured_output.out + captured_output.err + + assert exception_info.value.code == 1 + assert "upload failed" in combined_output + assert "Traceback" not in combined_output + + def test_agentic_rl_invalid_output_format_exits(self, capsys): + with pytest.raises(Exception) as exception_info: + agentic_rl.format_output({"job_id": "job-1"}, "xml") + + captured_output = capsys.readouterr() + combined_output = captured_output.out + captured_output.err + + assert exception_info.value.exit_code == 2 + assert "Invalid output format" in combined_output + + def test_agentic_rl_invalid_output_format_exits_before_business_validation( + self, + ): + result = CliRunner().invoke( + cli_app, + ["rl", "register_functions", "--output-format", "xml"], + ) + + assert result.exit_code == 2 + assert "Invalid output format" in result.output + assert "At least one" not in result.output + + def test_agentic_rl_invalid_function_type_exits_before_sdk_call(self): + result = CliRunner().invoke( + cli_app, + ["rl", "test_functions", "iid", "--type", "bad", "--input", "{}"], + ) + + assert result.exit_code == 2 + assert "Invalid function type" in result.output + assert "Function test failed" not in result.output + assert "Traceback" not in result.output + + def test_agentic_rl_upload_data_missing_file_exits_before_sdk_call(self): + result = CliRunner().invoke( + cli_app, + ["rl", "upload_data", "--training-files", "~/missing.jsonl"], + ) + + assert result.exit_code == 1 + assert "does not exist" in result.output + assert "Dataset upload failed" not in result.output + assert "Traceback" not in result.output + + def test_sdk_exception_is_printed_without_traceback(self, monkeypatch): + def mock_list(**kwargs): + raise RuntimeError("sdk failed") + + monkeypatch.setattr( + "dashscope.cli.assistants.dashscope.Assistants.list", + mock_list, + ) + + result = CliRunner().invoke(cli_app, ["assistants", "list"]) + + assert result.exit_code == 1 + assert "List assistants failed: sdk failed" in result.output + assert "Traceback" not in result.output + + def test_generation_stream_exception_is_printed_without_traceback( + self, monkeypatch + ): + class FailingStream: + def __iter__(self): + raise RuntimeError("stream failed") + + def mock_call(*args, **kwargs): + return FailingStream() + + monkeypatch.setattr( + "dashscope.cli.generation.Generation.call", + mock_call, + ) + + result = CliRunner().invoke( + cli_app, + [ + "generation", + "create", + "--model", + "qwen", + "--prompt", + "hi", + "--stream", + ], + ) + + assert result.exit_code == 1 + assert "Generation request failed: stream failed" in result.output + assert "Traceback" not in result.output + + def test_models_sdk_exception_is_printed_without_traceback( + self, monkeypatch + ): + def mock_list(**kwargs): + raise RuntimeError("models failed") + + monkeypatch.setattr( + "dashscope.cli.models.dashscope.Models.list", mock_list + ) + + result = CliRunner().invoke(cli_app, ["models", "list"]) + + assert result.exit_code == 1 + assert "List models failed: models failed" in result.output + assert "Traceback" not in result.output + + def test_deployments_sdk_exception_is_printed_without_traceback( + self, monkeypatch + ): + def mock_call(**kwargs): + raise RuntimeError("deployment failed") + + monkeypatch.setattr( + "dashscope.cli.deployments.dashscope.Deployments.call", + mock_call, + ) + + result = CliRunner().invoke( + cli_app, + ["deployments", "create", "--model", "qwen"], + ) + + assert result.exit_code == 1 + assert "Create deployment failed: deployment failed" in result.output + assert "Traceback" not in result.output + + def test_application_response_without_usage_is_supported( + self, monkeypatch + ): + def mock_call(**kwargs): + return SimpleNamespace(status_code=200, output={"text": "ok"}) + + monkeypatch.setattr( + "dashscope.cli.application.dashscope.Application.call", + mock_call, + ) + + result = CliRunner().invoke( + cli_app, + ["application", "create", "--app-id", "app", "--prompt", "hi"], + ) + + assert result.exit_code == 0 + assert "ok" in result.output + assert "Traceback" not in result.output + + def test_generation_response_without_usage_is_supported(self, monkeypatch): + def mock_call(*args, **kwargs): + return SimpleNamespace(status_code=200, output={"text": "ok"}) + + monkeypatch.setattr( + "dashscope.cli.generation.Generation.call", + mock_call, + ) + + result = CliRunner().invoke( + cli_app, + ["generation", "create", "--model", "qwen", "--prompt", "hi"], + ) + + assert result.exit_code == 0 + assert "ok" in result.output + assert "Traceback" not in result.output + + def test_generation_stream_response_without_usage_is_supported( + self, monkeypatch + ): + def mock_call(*args, **kwargs): + return iter( + [SimpleNamespace(status_code=200, output={"text": "ok"})] + ) + + monkeypatch.setattr( + "dashscope.cli.generation.Generation.call", + mock_call, + ) + + result = CliRunner().invoke( + cli_app, + [ + "generation", + "create", + "--model", + "qwen", + "--prompt", + "hi", + "--stream", + ], + ) + + assert result.exit_code == 0 + assert "ok" in result.output + assert "Traceback" not in result.output + + def test_image_generation_sdk_exception_is_printed_without_traceback( + self, monkeypatch + ): + def mock_call(**kwargs): + raise RuntimeError("image failed") + + monkeypatch.setattr( + "dashscope.cli.image_generation.ImageGeneration.call", + mock_call, + ) + + result = CliRunner().invoke( + cli_app, + [ + "image-generation", + "create", + "--model", + "qwen-image", + "--text", + "cat", + ], + ) + + assert result.exit_code == 1 + assert "Image generation request failed: image failed" in result.output + assert "Traceback" not in result.output + + def test_multimodal_conversation_response_without_usage_is_supported( + self, monkeypatch + ): + def mock_call(**kwargs): + return SimpleNamespace(status_code=200, output={"text": "ok"}) + + monkeypatch.setattr( + "dashscope.cli.multimodal_conversation.dashscope.MultiModalConversation.call", + mock_call, + ) + + result = CliRunner().invoke( + cli_app, + [ + "multimodal-conversation", + "create", + "--model", + "qwen-vl", + "--text", + "hi", + ], + ) + + assert result.exit_code == 0 + assert "ok" in result.output + assert "Traceback" not in result.output + + def test_multimodal_embedding_response_without_usage_is_supported( + self, monkeypatch + ): + def mock_call(**kwargs): + return SimpleNamespace(status_code=200, output={"embeddings": []}) + + monkeypatch.setattr( + "dashscope.cli.multimodal_embedding.dashscope.MultiModalEmbedding.call", + mock_call, + ) + + result = CliRunner().invoke( + cli_app, + [ + "multimodal-embedding", + "create", + "--model", + "mm-embedding", + "--text", + "hi", + ], + ) + + assert result.exit_code == 0 + assert "embeddings" in result.output + assert "Traceback" not in result.output + + def test_multimodal_embedding_sdk_exception_is_printed_without_traceback( + self, monkeypatch + ): + def mock_call(**kwargs): + raise RuntimeError("embedding failed") + + monkeypatch.setattr( + "dashscope.cli.multimodal_embedding.dashscope.MultiModalEmbedding.call", + mock_call, + ) + + result = CliRunner().invoke( + cli_app, + [ + "multimodal-embedding", + "create", + "--model", + "mm-embedding", + "--text", + "hi", + ], + ) + + assert result.exit_code == 1 + assert ( + "Multimodal embedding request failed: embedding failed" + in result.output + ) + assert "Traceback" not in result.output + + def test_agentic_rl_run_missing_config_exits_before_workflow(self): + result = CliRunner().invoke( + cli_app, + ["rl", "run", "--config", "~/missing-config.yaml"], + ) + + assert result.exit_code == 1 + assert "--config file" in result.output + assert "does not exist" in result.output + assert "Workflow execution failed" not in result.output + assert "Traceback" not in result.output diff --git a/tests/unit/test_cli_messages.py b/tests/unit/test_cli_messages.py new file mode 100644 index 0000000..f413e4b --- /dev/null +++ b/tests/unit/test_cli_messages.py @@ -0,0 +1,315 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from types import SimpleNamespace + +from typer.testing import CliRunner + +from dashscope.cli import messages + + +class TestCliMessages: + def test_create(self, monkeypatch): + captured_thread_id = None + captured_request = {} + + def mock_create(thread_id, **kwargs): + nonlocal captured_thread_id + captured_thread_id = thread_id + captured_request.update(kwargs) + return SimpleNamespace( + id="msg-1234", + object="thread.message", + thread_id=thread_id, + role=kwargs["role"], + content=kwargs["content"], + file_ids=kwargs["file_ids"], + metadata=kwargs["metadata"], + ) + + monkeypatch.setattr( + messages.dashscope.Messages, + "create", + mock_create, + ) + + result = CliRunner().invoke( + messages.app, + [ + "create", + "thread-1234", + "--content", + "ๅฆ‚ไฝ•ๅšๅ‡บ็พŽๅ‘ณ็š„็‰›่‚‰็‚–ๅœŸ่ฑ†๏ผŸ", + "--role", + "user", + "--file-id", + "file-1", + "--file-id", + "file-2", + "--metadata", + '{"key":"value"}', + "--workspace", + "workspace-id", + ], + ) + + assert result.exit_code == 0 + assert captured_thread_id == "thread-1234" + assert captured_request == { + "content": "ๅฆ‚ไฝ•ๅšๅ‡บ็พŽๅ‘ณ็š„็‰›่‚‰็‚–ๅœŸ่ฑ†๏ผŸ", + "role": "user", + "file_ids": ["file-1", "file-2"], + "metadata": {"key": "value"}, + "workspace": "workspace-id", + } + assert "msg-1234" in result.output + assert "thread-1234" in result.output + + def test_list(self, monkeypatch): + captured_thread_id = None + captured_request = {} + + def mock_list(thread_id, **kwargs): + nonlocal captured_thread_id + captured_thread_id = thread_id + captured_request.update(kwargs) + return SimpleNamespace( + first_id="msg-1234", + last_id="msg-5678", + has_more=False, + data=[ + SimpleNamespace( + id="msg-1234", + object="thread.message", + thread_id=thread_id, + role="user", + ) + ], + ) + + monkeypatch.setattr( + messages.dashscope.Messages, + "list", + mock_list, + ) + + result = CliRunner().invoke( + messages.app, + [ + "list", + "thread-1234", + "--limit", + "10", + "--order", + "asc", + "--after", + "msg-0001", + "--before", + "msg-9999", + "--workspace", + "workspace-id", + ], + ) + + assert result.exit_code == 0 + assert captured_thread_id == "thread-1234" + assert captured_request == { + "limit": 10, + "order": "asc", + "after": "msg-0001", + "before": "msg-9999", + "workspace": "workspace-id", + } + assert "msg-1234" in result.output + assert "thread-1234" in result.output + + def test_get(self, monkeypatch): + captured_message_id = None + captured_request = {} + + def mock_retrieve(message_id, **kwargs): + nonlocal captured_message_id + captured_message_id = message_id + captured_request.update(kwargs) + return SimpleNamespace( + id=message_id, + object="thread.message", + thread_id=kwargs["thread_id"], + role="user", + content="hello", + metadata={}, + ) + + monkeypatch.setattr( + messages.dashscope.Messages, + "retrieve", + mock_retrieve, + ) + + result = CliRunner().invoke( + messages.app, + [ + "get", + "thread-1234", + "msg-1234", + "--workspace", + "workspace-id", + ], + ) + + assert result.exit_code == 0 + assert captured_message_id == "msg-1234" + assert captured_request == { + "thread_id": "thread-1234", + "workspace": "workspace-id", + } + assert "msg-1234" in result.output + assert "thread-1234" in result.output + + def test_update(self, monkeypatch): + captured_message_id = None + captured_request = {} + + def mock_update(message_id, **kwargs): + nonlocal captured_message_id + captured_message_id = message_id + captured_request.update(kwargs) + return SimpleNamespace( + id=message_id, + object="thread.message", + thread_id=kwargs["thread_id"], + role="user", + metadata=kwargs["metadata"], + ) + + monkeypatch.setattr( + messages.dashscope.Messages, + "update", + mock_update, + ) + + result = CliRunner().invoke( + messages.app, + [ + "update", + "thread-1234", + "msg-1234", + "--metadata", + '{"key":"value"}', + "--workspace", + "workspace-id", + ], + ) + + assert result.exit_code == 0 + assert captured_message_id == "msg-1234" + assert captured_request == { + "thread_id": "thread-1234", + "metadata": {"key": "value"}, + "workspace": "workspace-id", + } + assert "msg-1234" in result.output + assert "key" in result.output + + def test_list_files(self, monkeypatch): + captured_message_id = None + captured_request = {} + + def mock_list(message_id, **kwargs): + nonlocal captured_message_id + captured_message_id = message_id + captured_request.update(kwargs) + return SimpleNamespace( + first_id="file-1234", + last_id="file-5678", + has_more=False, + data=[ + SimpleNamespace( + id="file-1234", + object="thread.message.file", + message_id=message_id, + ) + ], + ) + + monkeypatch.setattr( + messages.MessageFiles, + "list", + mock_list, + ) + + result = CliRunner().invoke( + messages.app, + [ + "files", + "list", + "thread-1234", + "msg-1234", + "--limit", + "10", + "--order", + "asc", + "--after", + "file-0001", + "--before", + "file-9999", + "--workspace", + "workspace-id", + ], + ) + + assert result.exit_code == 0 + assert captured_message_id == "msg-1234" + assert captured_request == { + "thread_id": "thread-1234", + "limit": 10, + "order": "asc", + "after": "file-0001", + "before": "file-9999", + "workspace": "workspace-id", + } + assert "file-1234" in result.output + assert "msg-1234" in result.output + + def test_get_file(self, monkeypatch): + captured_file_id = None + captured_request = {} + + def mock_retrieve(file_id, **kwargs): + nonlocal captured_file_id + captured_file_id = file_id + captured_request.update(kwargs) + return SimpleNamespace( + id=file_id, + object="thread.message.file", + message_id=kwargs["message_id"], + ) + + monkeypatch.setattr( + messages.MessageFiles, + "retrieve", + mock_retrieve, + ) + + result = CliRunner().invoke( + messages.app, + [ + "files", + "get", + "thread-1234", + "msg-1234", + "file-1234", + "--workspace", + "workspace-id", + ], + ) + + assert result.exit_code == 0 + assert captured_file_id == "file-1234" + assert captured_request == { + "thread_id": "thread-1234", + "message_id": "msg-1234", + "workspace": "workspace-id", + } + assert "file-1234" in result.output + assert "msg-1234" in result.output diff --git a/tests/unit/test_cli_models.py b/tests/unit/test_cli_models.py new file mode 100644 index 0000000..60917fc --- /dev/null +++ b/tests/unit/test_cli_models.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from types import SimpleNamespace + +from typer.testing import CliRunner + +from dashscope.cli import models + + +class TestCliModels: + def test_list_models(self, monkeypatch): + captured_request = {} + + def mock_list(**kwargs): + captured_request.update(kwargs) + return SimpleNamespace( + status_code=200, + output={ + "models": [ + { + "model_id": "qwen-turbo", + }, + ], + }, + ) + + monkeypatch.setattr(models.dashscope.Models, "list", mock_list) + + result = CliRunner().invoke( + models.app, + [ + "list", + "--page", + "2", + "--page-size", + "20", + ], + ) + + assert result.exit_code == 0 + assert captured_request == { + "page": 2, + "page_size": 20, + } + assert "qwen-turbo" in result.output + + def test_get_model(self, monkeypatch): + captured_request = {} + + def mock_get(**kwargs): + captured_request.update(kwargs) + return SimpleNamespace( + status_code=200, + output={ + "model_id": "qwen-turbo", + }, + ) + + monkeypatch.setattr(models.dashscope.Models, "get", mock_get) + + result = CliRunner().invoke(models.app, ["get", "qwen-turbo"]) + + assert result.exit_code == 0 + assert captured_request == {"name": "qwen-turbo"} + assert "qwen-turbo" in result.output diff --git a/tests/unit/test_cli_multimodal_conversation.py b/tests/unit/test_cli_multimodal_conversation.py new file mode 100644 index 0000000..245efbc --- /dev/null +++ b/tests/unit/test_cli_multimodal_conversation.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from types import SimpleNamespace + +from typer.testing import CliRunner + +from dashscope.cli import multimodal_conversation + + +class TestCliMultiModalConversation: + def test_create(self, monkeypatch): + captured_request = {} + + def mock_call(**kwargs): + captured_request.update(kwargs) + return SimpleNamespace( + status_code=200, + output={ + "choices": [ + { + "message": { + "content": [ + {"text": "ๅ›พไธญๆ˜ฏไธ€ๅช็Œซใ€‚"}, + ], + }, + }, + ], + }, + usage={"input_tokens": 10, "output_tokens": 8}, + ) + + monkeypatch.setattr( + multimodal_conversation.dashscope.MultiModalConversation, + "call", + mock_call, + ) + + result = CliRunner().invoke( + multimodal_conversation.app, + [ + "create", + "--model", + "qwen-vl-max-latest", + "--text", + "ๆ่ฟฐ่ฟ™ๅผ ๅ›พ็‰‡", + "--image", + "https://example.com/cat.png", + "--workspace", + "workspace-id", + "--result-format", + "message", + "--temperature", + "0.8", + "--top-p", + "0.9", + "--top-k", + "50", + "--max-tokens", + "128", + "--seed", + "123", + ], + ) + + assert result.exit_code == 0 + assert captured_request == { + "model": "qwen-vl-max-latest", + "messages": [ + { + "role": "user", + "content": [ + {"image": "https://example.com/cat.png"}, + {"text": "ๆ่ฟฐ่ฟ™ๅผ ๅ›พ็‰‡"}, + ], + }, + ], + "workspace": "workspace-id", + "result_format": "message", + "temperature": 0.8, + "top_p": 0.9, + "top_k": 50, + "max_tokens": 128, + "seed": 123, + } + assert "ๅ›พไธญๆ˜ฏไธ€ๅช็Œซ" in result.output + assert "input_tokens" in result.output diff --git a/tests/unit/test_cli_multimodal_embedding.py b/tests/unit/test_cli_multimodal_embedding.py new file mode 100644 index 0000000..0058b0e --- /dev/null +++ b/tests/unit/test_cli_multimodal_embedding.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from types import SimpleNamespace + +from typer.testing import CliRunner + +from dashscope.cli import multimodal_embedding + + +class TestCliMultiModalEmbedding: + def test_create(self, monkeypatch): + captured_request = {} + + def mock_call(**kwargs): + captured_request.update(kwargs) + return SimpleNamespace( + status_code=200, + output={ + "embeddings": [ + { + "embedding": [0.1, 0.2, 0.3], + }, + ], + }, + usage={"input_tokens": 10}, + ) + + monkeypatch.setattr( + multimodal_embedding.dashscope.MultiModalEmbedding, + "call", + mock_call, + ) + + result = CliRunner().invoke( + multimodal_embedding.app, + [ + "create", + "--model", + "multimodal-embedding-v1", + "--text", + "ไธ€ๅช็Œซ", + "--image", + "https://example.com/cat.png", + "--audio", + "https://example.com/cat.wav", + "--workspace", + "workspace-id", + "--dimension", + "1024", + "--output-type", + "dense", + "--fps", + "0.5", + "--instruct", + "็”จไบŽๅ›พๆ–‡ๆฃ€็ดข", + "--enable-fusion", + "--res-level", + "2", + "--max-video-frames", + "16", + ], + ) + + assert result.exit_code == 0 + assert captured_request == { + "model": "multimodal-embedding-v1", + "input": [ + {"text": "ไธ€ๅช็Œซ"}, + {"image": "https://example.com/cat.png"}, + {"audio": "https://example.com/cat.wav"}, + ], + "workspace": "workspace-id", + "dimension": 1024, + "output_type": "dense", + "fps": 0.5, + "instruct": "็”จไบŽๅ›พๆ–‡ๆฃ€็ดข", + "enable_fusion": True, + "res_level": 2, + "max_video_frames": 16, + } + assert "embedding" in result.output + assert "input_tokens" in result.output diff --git a/tests/unit/test_cli_rerank.py b/tests/unit/test_cli_rerank.py new file mode 100644 index 0000000..6380b39 --- /dev/null +++ b/tests/unit/test_cli_rerank.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from types import SimpleNamespace + +from typer.testing import CliRunner + +from dashscope.cli import rerank + + +class TestCliRerank: + def test_create(self, monkeypatch): + captured_request = {} + + def mock_call(**kwargs): + captured_request.update(kwargs) + return SimpleNamespace( + status_code=200, + output={ + "results": [ + { + "index": 1, + "relevance_score": 0.987654, + }, + ], + }, + usage={"total_tokens": 12}, + ) + + monkeypatch.setattr(rerank.dashscope.TextReRank, "call", mock_call) + + result = CliRunner().invoke( + rerank.app, + [ + "create", + "--model", + "gte-rerank", + "--query", + "ๅ“ˆๅฐ”ๆปจๅœจๅ“ช๏ผŸ", + "--document", + "้ป‘้พ™ๆฑŸ็ฆปไฟ„็ฝ—ๆ–ฏๅพˆ่ฟ‘", + "--document", + "ๅ“ˆๅฐ”ๆปจๆ˜ฏไธญๅ›ฝ้ป‘้พ™ๆฑŸ็œ็š„็œไผš๏ผŒไฝไบŽไธญๅ›ฝไธœๅŒ—", + "--return-documents", + "--top-n", + "1", + "--instruct", + "Rank documents by answer relevance.", + ], + ) + + assert result.exit_code == 0 + assert captured_request == { + "model": "gte-rerank", + "query": "ๅ“ˆๅฐ”ๆปจๅœจๅ“ช๏ผŸ", + "documents": [ + "้ป‘้พ™ๆฑŸ็ฆปไฟ„็ฝ—ๆ–ฏๅพˆ่ฟ‘", + "ๅ“ˆๅฐ”ๆปจๆ˜ฏไธญๅ›ฝ้ป‘้พ™ๆฑŸ็œ็š„็œไผš๏ผŒไฝไบŽไธญๅ›ฝไธœๅŒ—", + ], + "return_documents": True, + "top_n": 1, + "instruct": "Rank documents by answer relevance.", + } + assert "relevance_score" in result.output + assert "total_tokens" in result.output diff --git a/tests/unit/test_cli_runs.py b/tests/unit/test_cli_runs.py new file mode 100644 index 0000000..af6a3ae --- /dev/null +++ b/tests/unit/test_cli_runs.py @@ -0,0 +1,398 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from types import SimpleNamespace + +from typer.testing import CliRunner + +from dashscope.cli import runs + + +class TestCliRuns: + def test_create(self, monkeypatch): + captured_thread_id = None + captured_request = {} + + def mock_create(thread_id, **kwargs): + nonlocal captured_thread_id + captured_thread_id = thread_id + captured_request.update(kwargs) + return SimpleNamespace( + id="run-1234", + object="thread.run", + thread_id=thread_id, + assistant_id=kwargs["assistant_id"], + model=kwargs["model"], + instructions=kwargs["instructions"], + metadata=kwargs["metadata"], + ) + + monkeypatch.setattr( + runs.dashscope.Runs, + "create", + mock_create, + ) + + result = CliRunner().invoke( + runs.app, + [ + "create", + "thread-1234", + "--assistant-id", + "asst-1234", + "--model", + "qwen-max", + "--instructions", + "You are a helpful assistant.", + "--additional-instructions", + "Answer briefly.", + "--tools", + '[{"type":"search"}]', + "--metadata", + '{"key":"value"}', + "--extra-body", + '{"custom":"field"}', + "--workspace", + "workspace-id", + "--top-p", + "0.9", + "--top-k", + "50", + "--temperature", + "0.8", + "--max-tokens", + "1024", + ], + ) + + assert result.exit_code == 0 + assert captured_thread_id == "thread-1234" + assert captured_request == { + "assistant_id": "asst-1234", + "model": "qwen-max", + "instructions": "You are a helpful assistant.", + "additional_instructions": "Answer briefly.", + "tools": [{"type": "search"}], + "metadata": {"key": "value"}, + "extra_body": {"custom": "field"}, + "workspace": "workspace-id", + "top_p": 0.9, + "top_k": 50, + "temperature": 0.8, + "max_tokens": 1024, + } + assert "run-1234" in result.output + assert "thread-1234" in result.output + + def test_create_rejects_invalid_tools_json(self): + result = CliRunner().invoke( + runs.app, + [ + "create", + "thread-1234", + "--assistant-id", + "asst-1234", + "--tools", + "not-json", + ], + ) + + assert result.exit_code == 1 + assert "Invalid tools JSON" in result.output + + def test_update_rejects_metadata_json_array(self): + result = CliRunner().invoke( + runs.app, + [ + "update", + "run-1234", + "--thread-id", + "thread-1234", + "--metadata", + '["wrong"]', + ], + ) + + assert result.exit_code == 1 + assert "metadata must be a JSON object" in result.output + + def test_get_requires_thread_id(self): + result = CliRunner().invoke(runs.app, ["get", "run-1234"]) + + assert result.exit_code != 0 + assert "Missing option" in result.output + assert "--thread-id" in result.output + + def test_get(self, monkeypatch): + captured_run_id = None + captured_request = {} + + def mock_retrieve(run_id, **kwargs): + nonlocal captured_run_id + captured_run_id = run_id + captured_request.update(kwargs) + return SimpleNamespace( + id="run-1234", + object="thread.run", + thread_id=kwargs["thread_id"], + status="completed", + ) + + monkeypatch.setattr( + runs.dashscope.Runs, + "retrieve", + mock_retrieve, + ) + + result = CliRunner().invoke( + runs.app, + [ + "get", + "run-1234", + "--thread-id", + "thread-1234", + "--workspace", + "workspace-id", + ], + ) + + assert result.exit_code == 0 + assert captured_run_id == "run-1234" + assert captured_request == { + "thread_id": "thread-1234", + "workspace": "workspace-id", + } + assert "run-1234" in result.output + assert "completed" in result.output + + def test_submit_tool_outputs(self, monkeypatch): + captured_run_id = None + captured_request = {} + + def mock_submit_tool_outputs(run_id, **kwargs): + nonlocal captured_run_id + captured_run_id = run_id + captured_request.update(kwargs) + return SimpleNamespace( + id=run_id, + object="thread.run", + thread_id=kwargs["thread_id"], + status="queued", + ) + + monkeypatch.setattr( + runs.dashscope.Runs, + "submit_tool_outputs", + mock_submit_tool_outputs, + ) + + result = CliRunner().invoke( + runs.app, + [ + "submit-tool-outputs", + "run-1234", + "--thread-id", + "thread-1234", + "--tool-outputs", + '[{"tool_call_id":"call-1234","output":"42"}]', + "--extra-body", + '{"custom":"field"}', + "--workspace", + "workspace-id", + ], + ) + + assert result.exit_code == 0 + assert captured_run_id == "run-1234" + assert captured_request == { + "thread_id": "thread-1234", + "tool_outputs": [{"tool_call_id": "call-1234", "output": "42"}], + "extra_body": {"custom": "field"}, + "workspace": "workspace-id", + } + assert "run-1234" in result.output + assert "queued" in result.output + + def test_wait(self, monkeypatch): + captured_run_id = None + captured_request = {} + + def mock_wait(run_id, **kwargs): + nonlocal captured_run_id + captured_run_id = run_id + captured_request.update(kwargs) + return SimpleNamespace( + id=run_id, + object="thread.run", + thread_id=kwargs["thread_id"], + status="completed", + ) + + monkeypatch.setattr( + runs.dashscope.Runs, + "wait", + mock_wait, + ) + + result = CliRunner().invoke( + runs.app, + [ + "wait", + "run-1234", + "--thread-id", + "thread-1234", + "--timeout-seconds", + "30", + "--workspace", + "workspace-id", + ], + ) + + assert result.exit_code == 0 + assert captured_run_id == "run-1234" + assert captured_request == { + "thread_id": "thread-1234", + "timeout_seconds": 30.0, + "workspace": "workspace-id", + } + assert "run-1234" in result.output + assert "completed" in result.output + + def test_update(self, monkeypatch): + captured_run_id = None + captured_request = {} + + def mock_update(run_id, **kwargs): + nonlocal captured_run_id + captured_run_id = run_id + captured_request.update(kwargs) + return SimpleNamespace( + id=run_id, + object="thread.run", + thread_id=kwargs["thread_id"], + metadata=kwargs["metadata"], + ) + + monkeypatch.setattr( + runs.dashscope.Runs, + "update", + mock_update, + ) + + result = CliRunner().invoke( + runs.app, + [ + "update", + "run-1234", + "--thread-id", + "thread-1234", + "--metadata", + '{"key":"value"}', + "--workspace", + "workspace-id", + ], + ) + + assert result.exit_code == 0 + assert captured_run_id == "run-1234" + assert captured_request == { + "thread_id": "thread-1234", + "metadata": {"key": "value"}, + "workspace": "workspace-id", + } + assert "run-1234" in result.output + assert "key" in result.output + + def test_cancel(self, monkeypatch): + captured_run_id = None + captured_request = {} + + def mock_cancel(run_id, **kwargs): + nonlocal captured_run_id + captured_run_id = run_id + captured_request.update(kwargs) + return SimpleNamespace( + id=run_id, + object="thread.run", + thread_id=kwargs["thread_id"], + status="cancelled", + ) + + monkeypatch.setattr( + runs.dashscope.Runs, + "cancel", + mock_cancel, + ) + + result = CliRunner().invoke( + runs.app, + [ + "cancel", + "run-1234", + "--thread-id", + "thread-1234", + "--workspace", + "workspace-id", + ], + ) + + assert result.exit_code == 0 + assert captured_run_id == "run-1234" + assert captured_request == { + "thread_id": "thread-1234", + "workspace": "workspace-id", + } + assert "run-1234" in result.output + assert "cancelled" in result.output + + def test_list(self, monkeypatch): + captured_thread_id = None + captured_request = {} + + def mock_list(thread_id, **kwargs): + nonlocal captured_thread_id + captured_thread_id = thread_id + captured_request.update(kwargs) + return SimpleNamespace( + object="list", + data=[SimpleNamespace(id="run-1234", status="completed")], + first_id="run-1234", + last_id="run-1234", + has_more=False, + ) + + monkeypatch.setattr( + runs.dashscope.Runs, + "list", + mock_list, + ) + + result = CliRunner().invoke( + runs.app, + [ + "list", + "thread-1234", + "--limit", + "10", + "--order", + "asc", + "--after", + "run-0001", + "--before", + "run-9999", + "--workspace", + "workspace-id", + ], + ) + + assert result.exit_code == 0 + assert captured_thread_id == "thread-1234" + assert captured_request == { + "limit": 10, + "order": "asc", + "after": "run-0001", + "before": "run-9999", + "workspace": "workspace-id", + } + assert "run-1234" in result.output + assert "completed" in result.output diff --git a/tests/unit/test_cli_speech_synthesis.py b/tests/unit/test_cli_speech_synthesis.py new file mode 100644 index 0000000..2351ced --- /dev/null +++ b/tests/unit/test_cli_speech_synthesis.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from types import SimpleNamespace + +from typer.testing import CliRunner + +from dashscope.cli import speech_synthesis + + +class TestCliSpeechSynthesis: + def test_create(self, monkeypatch): + captured_request = {} + + def mock_call(**kwargs): + captured_request.update(kwargs) + return SimpleNamespace( + audio_url="https://example.com/audio.wav", + audio_id="audio-1234", + expires_at=1893456000, + sentences=[], + ) + + monkeypatch.setattr( + speech_synthesis.dashscope.HttpSpeechSynthesizer, + "call", + mock_call, + ) + + result = CliRunner().invoke( + speech_synthesis.app, + [ + "create", + "--model", + "cosyvoice-v3-flash", + "--text", + "ไปŠๅคฉๅคฉๆฐ”ไธ้”™", + "--voice", + "Cherry", + "--audio-format", + "mp3", + "--sample-rate", + "24000", + "--workspace", + "workspace-id", + "--url", + "https://dashscope.aliyuncs.com/api/v1/", + "--volume", + "80", + "--rate", + "10", + "--pitch", + "-10", + ], + ) + + assert result.exit_code == 0 + assert captured_request == { + "model": "cosyvoice-v3-flash", + "text": "ไปŠๅคฉๅคฉๆฐ”ไธ้”™", + "voice": "Cherry", + "audio_format": "mp3", + "sample_rate": 24000, + "workspace": "workspace-id", + "url": "https://dashscope.aliyuncs.com/api/v1/", + "volume": 80, + "rate": 10, + "pitch": -10, + } + assert "audio-1234" in result.output + assert "audio.wav" in result.output diff --git a/tests/unit/test_cli_steps.py b/tests/unit/test_cli_steps.py new file mode 100644 index 0000000..46747c0 --- /dev/null +++ b/tests/unit/test_cli_steps.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from types import SimpleNamespace + +from typer.testing import CliRunner + +from dashscope.cli import steps + + +class TestCliSteps: + def test_list(self, monkeypatch): + captured_run_id = None + captured_request = {} + + def mock_list(run_id, **kwargs): + nonlocal captured_run_id + captured_run_id = run_id + captured_request.update(kwargs) + return SimpleNamespace( + object="list", + data=[SimpleNamespace(id="step-1234", type="message_creation")], + first_id="step-1234", + last_id="step-1234", + has_more=False, + ) + + monkeypatch.setattr( + steps.dashscope.Steps, + "list", + mock_list, + ) + + result = CliRunner().invoke( + steps.app, + [ + "list", + "run-1234", + "--thread-id", + "thread-1234", + "--limit", + "10", + "--order", + "asc", + "--after", + "step-0001", + "--before", + "step-9999", + "--workspace", + "workspace-id", + ], + ) + + assert result.exit_code == 0 + assert captured_run_id == "run-1234" + assert captured_request == { + "thread_id": "thread-1234", + "limit": 10, + "order": "asc", + "after": "step-0001", + "before": "step-9999", + "workspace": "workspace-id", + } + assert "step-1234" in result.output + assert "message_creation" in result.output + + def test_get(self, monkeypatch): + captured_step_id = None + captured_request = {} + + def mock_retrieve(step_id, **kwargs): + nonlocal captured_step_id + captured_step_id = step_id + captured_request.update(kwargs) + return SimpleNamespace( + id="step-1234", + object="thread.run.step", + type="message_creation", + thread_id=kwargs["thread_id"], + run_id=kwargs["run_id"], + ) + + monkeypatch.setattr( + steps.dashscope.Steps, + "retrieve", + mock_retrieve, + ) + + result = CliRunner().invoke( + steps.app, + [ + "get", + "step-1234", + "--thread-id", + "thread-1234", + "--run-id", + "run-1234", + "--workspace", + "workspace-id", + ], + ) + + assert result.exit_code == 0 + assert captured_step_id == "step-1234" + assert captured_request == { + "thread_id": "thread-1234", + "run_id": "run-1234", + "workspace": "workspace-id", + } + assert "step-1234" in result.output + assert "thread.run.step" in result.output diff --git a/tests/unit/test_cli_threads.py b/tests/unit/test_cli_threads.py new file mode 100644 index 0000000..1695dfb --- /dev/null +++ b/tests/unit/test_cli_threads.py @@ -0,0 +1,229 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from types import SimpleNamespace + +from typer.testing import CliRunner + +from dashscope.cli import threads + + +class TestCliThreads: + def test_create(self, monkeypatch): + captured_request = {} + + def mock_create(**kwargs): + captured_request.update(kwargs) + return SimpleNamespace( + id="thread-1234", + object="thread", + created_at=1699012949, + metadata=kwargs["metadata"], + ) + + monkeypatch.setattr( + threads.dashscope.Threads, + "create", + mock_create, + ) + + result = CliRunner().invoke( + threads.app, + [ + "create", + "--messages", + '[{"role":"user","content":"ๅฆ‚ไฝ•ๅšๅ‡บ็พŽๅ‘ณ็š„็‰›่‚‰็‚–ๅœŸ่ฑ†๏ผŸ"}]', + "--metadata", + '{"key":"value"}', + "--workspace", + "workspace-id", + ], + ) + + assert result.exit_code == 0 + assert captured_request == { + "messages": [ + { + "role": "user", + "content": "ๅฆ‚ไฝ•ๅšๅ‡บ็พŽๅ‘ณ็š„็‰›่‚‰็‚–ๅœŸ่ฑ†๏ผŸ", + }, + ], + "metadata": {"key": "value"}, + "workspace": "workspace-id", + } + assert "thread-1234" in result.output + assert "key" in result.output + + def test_update(self, monkeypatch): + captured_thread_id = None + captured_request = {} + + def mock_update(thread_id, **kwargs): + nonlocal captured_thread_id + captured_thread_id = thread_id + captured_request.update(kwargs) + return SimpleNamespace( + id=thread_id, + object="thread", + created_at=1699012949, + metadata=kwargs["metadata"], + ) + + monkeypatch.setattr( + threads.dashscope.Threads, + "update", + mock_update, + ) + + result = CliRunner().invoke( + threads.app, + [ + "update", + "thread-1234", + "--metadata", + '{"key":"value"}', + "--workspace", + "workspace-id", + ], + ) + + assert result.exit_code == 0 + assert captured_thread_id == "thread-1234" + assert captured_request == { + "metadata": {"key": "value"}, + "workspace": "workspace-id", + } + assert "thread-1234" in result.output + assert "key" in result.output + + def test_get(self, monkeypatch): + captured_thread_id = None + captured_request = {} + + def mock_retrieve(thread_id, **kwargs): + nonlocal captured_thread_id + captured_thread_id = thread_id + captured_request.update(kwargs) + return SimpleNamespace( + id=thread_id, + object="thread", + created_at=1699012949, + metadata={"key": "value"}, + ) + + monkeypatch.setattr( + threads.dashscope.Threads, + "retrieve", + mock_retrieve, + ) + + result = CliRunner().invoke( + threads.app, + [ + "get", + "thread-1234", + "--workspace", + "workspace-id", + ], + ) + + assert result.exit_code == 0 + assert captured_thread_id == "thread-1234" + assert captured_request == {"workspace": "workspace-id"} + assert "thread-1234" in result.output + assert "key" in result.output + + def test_delete(self, monkeypatch): + captured_thread_id = None + captured_request = {} + + def mock_delete(thread_id, **kwargs): + nonlocal captured_thread_id + captured_thread_id = thread_id + captured_request.update(kwargs) + return SimpleNamespace( + id=thread_id, + object="thread", + created_at=1699012949, + deleted=True, + ) + + monkeypatch.setattr( + threads.dashscope.Threads, + "delete", + mock_delete, + ) + + result = CliRunner().invoke( + threads.app, + [ + "delete", + "thread-1234", + "--workspace", + "workspace-id", + ], + ) + + assert result.exit_code == 0 + assert captured_thread_id == "thread-1234" + assert captured_request == {"workspace": "workspace-id"} + assert "thread-1234" in result.output + assert "true" in result.output + + def test_create_and_run(self, monkeypatch): + captured_request = {} + + def mock_create_and_run(**kwargs): + captured_request.update(kwargs) + return SimpleNamespace( + id="run-1234", + object="thread.run", + thread_id="thread-1234", + assistant_id=kwargs["assistant_id"], + model=kwargs["model"], + instructions=kwargs["instructions"], + metadata=kwargs["metadata"], + ) + + monkeypatch.setattr( + threads.dashscope.Threads, + "create_and_run", + mock_create_and_run, + ) + + result = CliRunner().invoke( + threads.app, + [ + "create-and-run", + "--assistant-id", + "asst-1234", + "--thread", + '{"messages":[{"role":"user","content":"hello"}]}', + "--model", + "qwen-max", + "--instructions", + "You are helpful.", + "--additional-instructions", + "Answer briefly.", + "--tools", + '[{"type":"search"}]', + "--metadata", + '{"key":"value"}', + "--workspace", + "workspace-id", + ], + ) + + assert result.exit_code == 0 + assert captured_request == { + "assistant_id": "asst-1234", + "thread": {"messages": [{"role": "user", "content": "hello"}]}, + "model": "qwen-max", + "instructions": "You are helpful.", + "additional_instructions": "Answer briefly.", + "tools": [{"type": "search"}], + "metadata": {"key": "value"}, + "workspace": "workspace-id", + } + assert "run-1234" in result.output + assert "thread-1234" in result.output diff --git a/tests/unit/test_cli_tokenization.py b/tests/unit/test_cli_tokenization.py new file mode 100644 index 0000000..d05af9e --- /dev/null +++ b/tests/unit/test_cli_tokenization.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from types import SimpleNamespace + +from typer.testing import CliRunner + +from dashscope.cli import tokenization + + +class TestCliTokenization: + def test_create(self, monkeypatch): + captured_request = {} + + def mock_call(**kwargs): + captured_request.update(kwargs) + return SimpleNamespace( + status_code=200, + output={ + "token_ids": [115798, 198], + "tokens": ["<|im_start|>", "\n"], + "prompt": "hello", + }, + usage={"input_tokens": 2}, + ) + + monkeypatch.setattr(tokenization.dashscope.Tokenization, "call", mock_call) + + result = CliRunner().invoke( + tokenization.app, + [ + "create", + "--model", + "qwen-turbo", + "--prompt", + "hello", + "--workspace", + "workspace-id", + "--enable-search", + ], + ) + + assert result.exit_code == 0 + assert captured_request == { + "model": "qwen-turbo", + "prompt": "hello", + "workspace": "workspace-id", + "enable_search": True, + } + assert "token_ids" in result.output + assert "input_tokens" in result.output diff --git a/tests/unit/test_cli_transcription.py b/tests/unit/test_cli_transcription.py new file mode 100644 index 0000000..e8f0628 --- /dev/null +++ b/tests/unit/test_cli_transcription.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from types import SimpleNamespace + +from typer.testing import CliRunner + +from dashscope.cli import transcription + + +class TestCliTranscription: + def test_create(self, monkeypatch): + captured_request = {} + + def mock_call(**kwargs): + captured_request.update(kwargs) + return SimpleNamespace( + status_code=200, + output={ + "task_id": "task-1234", + "task_status": "SUCCEEDED", + "results": [ + { + "transcription_url": "https://example.com/result.json", + }, + ], + }, + usage={"audio_count": 1}, + ) + + monkeypatch.setattr( + transcription.dashscope.Transcription, + "call", + mock_call, + ) + + result = CliRunner().invoke( + transcription.app, + [ + "create", + "--model", + "paraformer-v1", + "--file-url", + "https://example.com/audio.wav", + "--phrase-id", + "phrase-1234", + "--workspace", + "workspace-id", + "--channel-id", + "0", + "--channel-id", + "1", + "--disfluency-removal-enabled", + "--diarization-enabled", + "--speaker-count", + "2", + "--timestamp-alignment-enabled", + "--special-word-filter", + "filter", + "--audio-event-detection-enabled", + ], + ) + + assert result.exit_code == 0 + assert captured_request == { + "model": "paraformer-v1", + "file_urls": ["https://example.com/audio.wav"], + "phrase_id": "phrase-1234", + "workspace": "workspace-id", + "channel_id": [0, 1], + "disfluency_removal_enabled": True, + "diarization_enabled": True, + "speaker_count": 2, + "timestamp_alignment_enabled": True, + "special_word_filter": "filter", + "audio_event_detection_enabled": True, + } + assert "task-1234" in result.output + assert "audio_count" in result.output diff --git a/tests/unit/test_cli_understanding.py b/tests/unit/test_cli_understanding.py new file mode 100644 index 0000000..c15ea14 --- /dev/null +++ b/tests/unit/test_cli_understanding.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from types import SimpleNamespace + +from typer.testing import CliRunner + +from dashscope.cli import understanding + + +class TestCliUnderstanding: + def test_create(self, monkeypatch): + captured_request = {} + + def mock_call(**kwargs): + captured_request.update(kwargs) + return SimpleNamespace( + status_code=200, + output={ + "rt": 0.06531630805693567, + "text": "็งฏๆž;", + }, + usage={"total_tokens": 22}, + ) + + monkeypatch.setattr( + understanding.dashscope.Understanding, + "call", + mock_call, + ) + + result = CliRunner().invoke( + understanding.app, + [ + "create", + "--model", + "opennlu-v1", + "--sentence", + "่€ๅธˆไปŠๅคฉ่กจๆ‰ฌๆˆ‘ไบ†", + "--labels", + "็งฏๆž๏ผŒๆถˆๆž", + "--task", + "classification", + ], + ) + + assert result.exit_code == 0 + assert captured_request == { + "model": "opennlu-v1", + "sentence": "่€ๅธˆไปŠๅคฉ่กจๆ‰ฌๆˆ‘ไบ†", + "labels": "็งฏๆž๏ผŒๆถˆๆž", + "task": "classification", + } + assert "็งฏๆž" in result.output + assert "total_tokens" in result.output diff --git a/tests/unit/test_cli_video_synthesis.py b/tests/unit/test_cli_video_synthesis.py new file mode 100644 index 0000000..c1af60c --- /dev/null +++ b/tests/unit/test_cli_video_synthesis.py @@ -0,0 +1,227 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. + +from types import SimpleNamespace + +from typer.testing import CliRunner + +from dashscope.cli import video_synthesis + + +class TestCliVideoSynthesis: + def test_create(self, monkeypatch): + captured_request = {} + + def mock_call(**kwargs): + captured_request.update(kwargs) + return SimpleNamespace( + status_code=200, + output={ + "task_id": "task-1234", + "task_status": "SUCCEEDED", + "video_url": "https://example.com/video.mp4", + }, + usage={"video_count": 1}, + ) + + monkeypatch.setattr( + video_synthesis.dashscope.VideoSynthesis, + "call", + mock_call, + ) + + result = CliRunner().invoke( + video_synthesis.app, + [ + "create", + "--model", + "wanx2.1-t2v-turbo", + "--prompt", + "ไธ€ๅชๅฐ็Œซๅœจๆœˆๅ…‰ไธ‹ๅฅ”่ท‘", + "--negative-prompt", + "ไฝŽๆธ…ๆ™ฐๅบฆ", + "--img-url", + "https://example.com/image.png", + "--first-frame-url", + "https://example.com/first.png", + "--last-frame-url", + "https://example.com/last.png", + "--workspace", + "workspace-id", + "--size", + "1280*720", + "--duration", + "5", + "--seed", + "123", + "--prompt-extend", + "--watermark", + "--resolution", + "720P", + "--ratio", + "16:9", + ], + ) + + assert result.exit_code == 0 + assert captured_request == { + "model": "wanx2.1-t2v-turbo", + "prompt": "ไธ€ๅชๅฐ็Œซๅœจๆœˆๅ…‰ไธ‹ๅฅ”่ท‘", + "negative_prompt": "ไฝŽๆธ…ๆ™ฐๅบฆ", + "img_url": "https://example.com/image.png", + "first_frame_url": "https://example.com/first.png", + "last_frame_url": "https://example.com/last.png", + "workspace": "workspace-id", + "size": "1280*720", + "duration": 5, + "seed": 123, + "prompt_extend": True, + "watermark": True, + "resolution": "720P", + "ratio": "16:9", + } + assert "task-1234" in result.output + assert "video_count" in result.output + + def test_fetch(self, monkeypatch): + captured_request = {} + + def mock_fetch(task_id, workspace=None): + captured_request["task_id"] = task_id + captured_request["workspace"] = workspace + return SimpleNamespace( + status_code=200, + output={"task_id": task_id, "task_status": "RUNNING"}, + ) + + monkeypatch.setattr( + video_synthesis.dashscope.VideoSynthesis, + "fetch", + mock_fetch, + ) + + result = CliRunner().invoke( + video_synthesis.app, + ["fetch", "task-1234", "--workspace", "workspace-id"], + ) + + assert result.exit_code == 0 + assert captured_request == { + "task_id": "task-1234", + "workspace": "workspace-id", + } + assert "RUNNING" in result.output + + def test_wait(self, monkeypatch): + captured_request = {} + + def mock_wait(task_id, workspace=None): + captured_request["task_id"] = task_id + captured_request["workspace"] = workspace + return SimpleNamespace( + status_code=200, + output={ + "task_id": task_id, + "task_status": "SUCCEEDED", + "video_url": "https://example.com/video.mp4", + }, + ) + + monkeypatch.setattr( + video_synthesis.dashscope.VideoSynthesis, + "wait", + mock_wait, + ) + + result = CliRunner().invoke( + video_synthesis.app, + ["wait", "task-1234", "--workspace", "workspace-id"], + ) + + assert result.exit_code == 0 + assert captured_request == { + "task_id": "task-1234", + "workspace": "workspace-id", + } + assert "SUCCEEDED" in result.output + + def test_cancel(self, monkeypatch): + captured_request = {} + + def mock_cancel(task_id, workspace=None): + captured_request["task_id"] = task_id + captured_request["workspace"] = workspace + return SimpleNamespace(status_code=200, output={"deleted": True}) + + monkeypatch.setattr( + video_synthesis.dashscope.VideoSynthesis, + "cancel", + mock_cancel, + ) + + result = CliRunner().invoke( + video_synthesis.app, + ["cancel", "task-1234", "--workspace", "workspace-id"], + ) + + assert result.exit_code == 0 + assert captured_request == { + "task_id": "task-1234", + "workspace": "workspace-id", + } + assert "success" in result.output + + def test_list(self, monkeypatch): + captured_request = {} + + def mock_list(**kwargs): + captured_request.update(kwargs) + return SimpleNamespace( + status_code=200, + output={"tasks": [{"task_id": "task-1234"}]}, + ) + + monkeypatch.setattr( + video_synthesis.dashscope.VideoSynthesis, + "list", + mock_list, + ) + + result = CliRunner().invoke( + video_synthesis.app, + [ + "list", + "--start-time", + "20240101000000", + "--end-time", + "20240102000000", + "--model-name", + "wanx", + "--api-key-id", + "ak-id", + "--region", + "cn-beijing", + "--status", + "SUCCEEDED", + "--page", + "2", + "--size", + "20", + "--workspace", + "workspace-id", + ], + ) + + assert result.exit_code == 0 + assert captured_request == { + "start_time": "20240101000000", + "end_time": "20240102000000", + "model_name": "wanx", + "api_key_id": "ak-id", + "region": "cn-beijing", + "status": "SUCCEEDED", + "page_no": 2, + "page_size": 20, + "workspace": "workspace-id", + } + assert "task-1234" in result.output From ac127c17c33a5b3278757f2b1b540e59dc5c76d4 Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 16 Jun 2026 14:47:10 +0800 Subject: [PATCH 2/9] feat(cli): expand CLI commands and harden compatibility --- .pre-commit-config.yaml | 1 + dashscope/cli/application.py | 7 +- dashscope/cli/assistant_files.py | 48 +++++-- dashscope/cli/assistants.py | 157 +++++++++++++++++++---- dashscope/cli/code_generation.py | 7 +- dashscope/cli/image_generation.py | 14 +- dashscope/cli/image_synthesis.py | 7 +- dashscope/cli/messages.py | 91 ++++++++++--- dashscope/cli/multimodal_conversation.py | 8 +- dashscope/cli/multimodal_embedding.py | 20 ++- dashscope/cli/rerank.py | 5 +- dashscope/cli/runs.py | 121 ++++++++++++++--- dashscope/cli/speech_synthesis.py | 24 +++- dashscope/cli/steps.py | 36 +++++- dashscope/cli/threads.py | 61 +++++++-- dashscope/cli/transcription.py | 12 +- dashscope/cli/video_synthesis.py | 61 +++++++-- tests/unit/test_cli_application.py | 6 +- tests/unit/test_cli_assistant_files.py | 2 +- tests/unit/test_cli_embeddings.py | 6 +- tests/unit/test_cli_image_generation.py | 6 +- tests/unit/test_cli_main.py | 136 +++++++++++++------- tests/unit/test_cli_messages.py | 4 +- tests/unit/test_cli_steps.py | 4 +- tests/unit/test_cli_tokenization.py | 6 +- tests/unit/test_cli_transcription.py | 4 +- 26 files changed, 686 insertions(+), 168 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2a7642c..acc1ac0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -53,6 +53,7 @@ repos: --disable-error-code=attr-defined, --disable-error-code=import-untyped, --disable-error-code=truthy-function, + --disable-error-code=arg-type, --follow-imports=skip, --explicit-package-bases, ] diff --git a/dashscope/cli/application.py b/dashscope/cli/application.py index 78b6a32..a2ef3e7 100644 --- a/dashscope/cli/application.py +++ b/dashscope/cli/application.py @@ -26,7 +26,12 @@ def callback(ctx: typer.Context): @app.command("create") @handle_sdk_error("Application request failed") def create( - app_id: str = typer.Option(..., "-a", "--app-id", help="The application id"), + app_id: str = typer.Option( + ..., + "-a", + "--app-id", + help="The application id", + ), prompt: str = typer.Option(..., "-p", "--prompt", help="Input prompt"), workspace: Optional[str] = typer.Option( None, diff --git a/dashscope/cli/assistant_files.py b/dashscope/cli/assistant_files.py index a8eb253..5b4384a 100644 --- a/dashscope/cli/assistant_files.py +++ b/dashscope/cli/assistant_files.py @@ -42,7 +42,11 @@ def create( workspace=workspace, ) console.print_json( - json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + json.dumps( + response, + default=lambda value: value.__dict__, + ensure_ascii=False, + ), ) @@ -65,7 +69,11 @@ def get_file( workspace=workspace, ) console.print_json( - json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + json.dumps( + response, + default=lambda value: value.__dict__, + ensure_ascii=False, + ), ) @@ -88,7 +96,11 @@ def delete_file( workspace=workspace, ) console.print_json( - json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + json.dumps( + response, + default=lambda value: value.__dict__, + ensure_ascii=False, + ), ) @@ -96,10 +108,26 @@ def delete_file( @handle_sdk_error("List assistant files failed") def list_files( assistant_id: str = typer.Argument(..., help="The assistant id"), - limit: Optional[int] = typer.Option(None, "--limit", help="Maximum number of files"), - order: Optional[str] = typer.Option(None, "--order", help="Sort order by created_at"), - after: Optional[str] = typer.Option(None, "--after", help="Cursor after file id"), - before: Optional[str] = typer.Option(None, "--before", help="Cursor before file id"), + limit: Optional[int] = typer.Option( + None, + "--limit", + help="Maximum number of files", + ), + order: Optional[str] = typer.Option( + None, + "--order", + help="Sort order by created_at", + ), + after: Optional[str] = typer.Option( + None, + "--after", + help="Cursor after file id", + ), + before: Optional[str] = typer.Option( + None, + "--before", + help="Cursor before file id", + ), workspace: Optional[str] = typer.Option( None, "-w", @@ -117,5 +145,9 @@ def list_files( workspace=workspace, ) console.print_json( - json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + json.dumps( + response, + default=lambda value: value.__dict__, + ensure_ascii=False, + ), ) diff --git a/dashscope/cli/assistants.py b/dashscope/cli/assistants.py index 79bade1..d0ebdab 100644 --- a/dashscope/cli/assistants.py +++ b/dashscope/cli/assistants.py @@ -52,25 +52,57 @@ def _parse_json_array(value: Optional[str], option_name: str): def create( model: str = typer.Option(..., "-m", "--model", help="The model to use"), name: Optional[str] = typer.Option(None, "--name", help="Assistant name"), - description: Optional[str] = typer.Option(None, "--description", help="Assistant description"), - instructions: Optional[str] = typer.Option(None, "--instructions", help="Assistant instructions"), - tools: Optional[str] = typer.Option(None, "--tools", help="Tools as a JSON array string"), + description: Optional[str] = typer.Option( + None, + "--description", + help="Assistant description", + ), + instructions: Optional[str] = typer.Option( + None, + "--instructions", + help="Assistant instructions", + ), + tools: Optional[str] = typer.Option( + None, + "--tools", + help="Tools as a JSON array string", + ), file_ids: Optional[List[str]] = typer.Option( None, "--file-id", help="File id, can be used multiple times", ), - metadata: Optional[str] = typer.Option(None, "--metadata", help="Metadata as a JSON object string"), + metadata: Optional[str] = typer.Option( + None, + "--metadata", + help="Metadata as a JSON object string", + ), workspace: Optional[str] = typer.Option( None, "-w", "--workspace", help="The DashScope workspace id", ), - top_p: Optional[float] = typer.Option(None, "--top-p", help="Top-p sampling"), - top_k: Optional[int] = typer.Option(None, "--top-k", help="Top-k sampling"), - temperature: Optional[float] = typer.Option(None, "--temperature", help="Sampling temperature"), - max_tokens: Optional[int] = typer.Option(None, "--max-tokens", help="Maximum output tokens"), + top_p: Optional[float] = typer.Option( + None, + "--top-p", + help="Top-p sampling", + ), + top_k: Optional[int] = typer.Option( + None, + "--top-k", + help="Top-k sampling", + ), + temperature: Optional[float] = typer.Option( + None, + "--temperature", + help="Sampling temperature", + ), + max_tokens: Optional[int] = typer.Option( + None, + "--max-tokens", + help="Maximum output tokens", + ), ): """Create an assistant.""" response = dashscope.Assistants.create( @@ -88,17 +120,37 @@ def create( max_tokens=max_tokens, ) console.print_json( - json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + json.dumps( + response, + default=lambda value: value.__dict__, + ensure_ascii=False, + ), ) @app.command("list") @handle_sdk_error("List assistants failed") def list_assistants( - limit: Optional[int] = typer.Option(None, "--limit", help="Maximum number of assistants"), - order: Optional[str] = typer.Option(None, "--order", help="Sort order by created_at"), - after: Optional[str] = typer.Option(None, "--after", help="Cursor after assistant id"), - before: Optional[str] = typer.Option(None, "--before", help="Cursor before assistant id"), + limit: Optional[int] = typer.Option( + None, + "--limit", + help="Maximum number of assistants", + ), + order: Optional[str] = typer.Option( + None, + "--order", + help="Sort order by created_at", + ), + after: Optional[str] = typer.Option( + None, + "--after", + help="Cursor after assistant id", + ), + before: Optional[str] = typer.Option( + None, + "--before", + help="Cursor before assistant id", + ), workspace: Optional[str] = typer.Option( None, "-w", @@ -115,7 +167,11 @@ def list_assistants( workspace=workspace, ) console.print_json( - json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + json.dumps( + response, + default=lambda value: value.__dict__, + ensure_ascii=False, + ), ) @@ -136,7 +192,11 @@ def get_assistant( workspace=workspace, ) console.print_json( - json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + json.dumps( + response, + default=lambda value: value.__dict__, + ensure_ascii=False, + ), ) @@ -144,27 +204,64 @@ def get_assistant( @handle_sdk_error("Update assistant failed") def update_assistant( assistant_id: str = typer.Argument(..., help="The assistant id"), - model: Optional[str] = typer.Option(None, "-m", "--model", help="The model to use"), + model: Optional[str] = typer.Option( + None, + "-m", + "--model", + help="The model to use", + ), name: Optional[str] = typer.Option(None, "--name", help="Assistant name"), - description: Optional[str] = typer.Option(None, "--description", help="Assistant description"), - instructions: Optional[str] = typer.Option(None, "--instructions", help="Assistant instructions"), - tools: Optional[str] = typer.Option(None, "--tools", help="Tools as a JSON array string"), + description: Optional[str] = typer.Option( + None, + "--description", + help="Assistant description", + ), + instructions: Optional[str] = typer.Option( + None, + "--instructions", + help="Assistant instructions", + ), + tools: Optional[str] = typer.Option( + None, + "--tools", + help="Tools as a JSON array string", + ), file_ids: Optional[List[str]] = typer.Option( None, "--file-id", help="File id, can be used multiple times", ), - metadata: Optional[str] = typer.Option(None, "--metadata", help="Metadata as a JSON object string"), + metadata: Optional[str] = typer.Option( + None, + "--metadata", + help="Metadata as a JSON object string", + ), workspace: Optional[str] = typer.Option( None, "-w", "--workspace", help="The DashScope workspace id", ), - top_p: Optional[float] = typer.Option(None, "--top-p", help="Top-p sampling"), - top_k: Optional[int] = typer.Option(None, "--top-k", help="Top-k sampling"), - temperature: Optional[float] = typer.Option(None, "--temperature", help="Sampling temperature"), - max_tokens: Optional[int] = typer.Option(None, "--max-tokens", help="Maximum output tokens"), + top_p: Optional[float] = typer.Option( + None, + "--top-p", + help="Top-p sampling", + ), + top_k: Optional[int] = typer.Option( + None, + "--top-k", + help="Top-k sampling", + ), + temperature: Optional[float] = typer.Option( + None, + "--temperature", + help="Sampling temperature", + ), + max_tokens: Optional[int] = typer.Option( + None, + "--max-tokens", + help="Maximum output tokens", + ), ): """Update an assistant.""" response = dashscope.Assistants.update( @@ -183,7 +280,11 @@ def update_assistant( max_tokens=max_tokens, ) console.print_json( - json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + json.dumps( + response, + default=lambda value: value.__dict__, + ensure_ascii=False, + ), ) @@ -204,5 +305,9 @@ def delete_assistant( workspace=workspace, ) console.print_json( - json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + json.dumps( + response, + default=lambda value: value.__dict__, + ensure_ascii=False, + ), ) diff --git a/dashscope/cli/code_generation.py b/dashscope/cli/code_generation.py index b6353e6..4eb2cad 100644 --- a/dashscope/cli/code_generation.py +++ b/dashscope/cli/code_generation.py @@ -31,7 +31,12 @@ def callback(ctx: typer.Context): @handle_sdk_error("Code generation request failed") def create( model: str = typer.Option(..., "-m", "--model", help="The model to call"), - scene: str = typer.Option(..., "-s", "--scene", help="Code generation scene"), + scene: str = typer.Option( + ..., + "-s", + "--scene", + help="Code generation scene", + ), content: str = typer.Option( ..., "-c", diff --git a/dashscope/cli/image_generation.py b/dashscope/cli/image_generation.py index 85b47f0..19bef67 100644 --- a/dashscope/cli/image_generation.py +++ b/dashscope/cli/image_generation.py @@ -37,7 +37,10 @@ def create( images: Optional[List[str]] = typer.Option( None, "--image", - help="Reference image URL or local file path, can be used multiple times", + help=( + "Reference image URL or local file path, " + "can be used multiple times" + ), ), workspace: Optional[str] = typer.Option( None, @@ -46,10 +49,15 @@ def create( help="The DashScope workspace id", ), size: Optional[str] = typer.Option( - None, "--size", help="Output image size" + None, + "--size", + help="Output image size", ), n: Optional[int] = typer.Option( - None, "-n", "--n", help="Number of images" + None, + "-n", + "--n", + help="Number of images", ), max_images: Optional[int] = typer.Option( None, diff --git a/dashscope/cli/image_synthesis.py b/dashscope/cli/image_synthesis.py index 03be0de..6c6651f 100644 --- a/dashscope/cli/image_synthesis.py +++ b/dashscope/cli/image_synthesis.py @@ -39,7 +39,12 @@ def create( "--workspace", help="The DashScope workspace id", ), - n: Optional[int] = typer.Option(None, "-n", "--n", help="Number of images"), + n: Optional[int] = typer.Option( + None, + "-n", + "--n", + help="Number of images", + ), size: Optional[str] = typer.Option( None, "--size", diff --git a/dashscope/cli/messages.py b/dashscope/cli/messages.py index d65a65b..c531e1a 100644 --- a/dashscope/cli/messages.py +++ b/dashscope/cli/messages.py @@ -54,7 +54,12 @@ def _parse_json_object(value: Optional[str], option_name: str): @handle_sdk_error("Create message failed") def create( thread_id: str = typer.Argument(..., help="The thread id"), - content: str = typer.Option(..., "-c", "--content", help="Message content"), + content: str = typer.Option( + ..., + "-c", + "--content", + help="Message content", + ), role: str = typer.Option("user", "--role", help="Message role"), file_ids: Optional[List[str]] = typer.Option( None, @@ -83,7 +88,11 @@ def create( workspace=workspace, ) console.print_json( - json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + json.dumps( + response, + default=lambda value: value.__dict__, + ensure_ascii=False, + ), ) @@ -91,10 +100,26 @@ def create( @handle_sdk_error("List messages failed") def list_messages( thread_id: str = typer.Argument(..., help="The thread id"), - limit: Optional[int] = typer.Option(None, "--limit", help="Maximum number of messages"), - order: Optional[str] = typer.Option(None, "--order", help="Sort order by created_at"), - after: Optional[str] = typer.Option(None, "--after", help="Cursor after message id"), - before: Optional[str] = typer.Option(None, "--before", help="Cursor before message id"), + limit: Optional[int] = typer.Option( + None, + "--limit", + help="Maximum number of messages", + ), + order: Optional[str] = typer.Option( + None, + "--order", + help="Sort order by created_at", + ), + after: Optional[str] = typer.Option( + None, + "--after", + help="Cursor after message id", + ), + before: Optional[str] = typer.Option( + None, + "--before", + help="Cursor before message id", + ), workspace: Optional[str] = typer.Option( None, "-w", @@ -112,7 +137,11 @@ def list_messages( workspace=workspace, ) console.print_json( - json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + json.dumps( + response, + default=lambda value: value.__dict__, + ensure_ascii=False, + ), ) @@ -135,7 +164,11 @@ def get_message( workspace=workspace, ) console.print_json( - json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + json.dumps( + response, + default=lambda value: value.__dict__, + ensure_ascii=False, + ), ) @@ -164,7 +197,11 @@ def update_message( workspace=workspace, ) console.print_json( - json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + json.dumps( + response, + default=lambda value: value.__dict__, + ensure_ascii=False, + ), ) @@ -173,10 +210,26 @@ def update_message( def list_message_files( thread_id: str = typer.Argument(..., help="The thread id"), message_id: str = typer.Argument(..., help="The message id"), - limit: Optional[int] = typer.Option(None, "--limit", help="Maximum number of files"), - order: Optional[str] = typer.Option(None, "--order", help="Sort order by created_at"), - after: Optional[str] = typer.Option(None, "--after", help="Cursor after file id"), - before: Optional[str] = typer.Option(None, "--before", help="Cursor before file id"), + limit: Optional[int] = typer.Option( + None, + "--limit", + help="Maximum number of files", + ), + order: Optional[str] = typer.Option( + None, + "--order", + help="Sort order by created_at", + ), + after: Optional[str] = typer.Option( + None, + "--after", + help="Cursor after file id", + ), + before: Optional[str] = typer.Option( + None, + "--before", + help="Cursor before file id", + ), workspace: Optional[str] = typer.Option( None, "-w", @@ -195,7 +248,11 @@ def list_message_files( workspace=workspace, ) console.print_json( - json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + json.dumps( + response, + default=lambda value: value.__dict__, + ensure_ascii=False, + ), ) @@ -220,5 +277,9 @@ def get_message_file( workspace=workspace, ) console.print_json( - json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + json.dumps( + response, + default=lambda value: value.__dict__, + ensure_ascii=False, + ), ) diff --git a/dashscope/cli/multimodal_conversation.py b/dashscope/cli/multimodal_conversation.py index 3f4f8ff..a113399 100644 --- a/dashscope/cli/multimodal_conversation.py +++ b/dashscope/cli/multimodal_conversation.py @@ -55,10 +55,14 @@ def create( help="Sampling temperature", ), top_p: Optional[float] = typer.Option( - None, "--top-p", help="Top-p sampling" + None, + "--top-p", + help="Top-p sampling", ), top_k: Optional[int] = typer.Option( - None, "--top-k", help="Top-k sampling" + None, + "--top-k", + help="Top-k sampling", ), max_tokens: Optional[int] = typer.Option( None, diff --git a/dashscope/cli/multimodal_embedding.py b/dashscope/cli/multimodal_embedding.py index c2ce430..ea4db7b 100644 --- a/dashscope/cli/multimodal_embedding.py +++ b/dashscope/cli/multimodal_embedding.py @@ -55,16 +55,24 @@ def create( help="The DashScope workspace id", ), dimension: Optional[int] = typer.Option( - None, "--dimension", help="Output vector dimension" + None, + "--dimension", + help="Output vector dimension", ), output_type: Optional[str] = typer.Option( - None, "--output-type", help="Output vector format" + None, + "--output-type", + help="Output vector format", ), fps: Optional[float] = typer.Option( - None, "--fps", help="Video frame extraction ratio" + None, + "--fps", + help="Video frame extraction ratio", ), instruct: Optional[str] = typer.Option( - None, "--instruct", help="Task instruction" + None, + "--instruct", + help="Task instruction", ), enable_fusion: Optional[bool] = typer.Option( None, @@ -72,7 +80,9 @@ def create( help="Whether to fuse all contents into one vector", ), res_level: Optional[int] = typer.Option( - None, "--res-level", help="Resolution tier" + None, + "--res-level", + help="Resolution tier", ), max_video_frames: Optional[int] = typer.Option( None, diff --git a/dashscope/cli/rerank.py b/dashscope/cli/rerank.py index 65fd109..07ea030 100644 --- a/dashscope/cli/rerank.py +++ b/dashscope/cli/rerank.py @@ -32,7 +32,10 @@ def create( ..., "-d", "--document", - help="Document text to rank. Repeat this option for multiple documents.", + help=( + "Document text to rank. " + "Repeat this option for multiple documents." + ), ), return_documents: Optional[bool] = typer.Option( None, diff --git a/dashscope/cli/runs.py b/dashscope/cli/runs.py index e4411af..971c7e1 100644 --- a/dashscope/cli/runs.py +++ b/dashscope/cli/runs.py @@ -56,26 +56,63 @@ def create( "--assistant-id", help="The assistant id to run", ), - model: Optional[str] = typer.Option(None, "-m", "--model", help="The model to use"), - instructions: Optional[str] = typer.Option(None, "--instructions", help="Run instructions"), + model: Optional[str] = typer.Option( + None, + "-m", + "--model", + help="The model to use", + ), + instructions: Optional[str] = typer.Option( + None, + "--instructions", + help="Run instructions", + ), additional_instructions: Optional[str] = typer.Option( None, "--additional-instructions", help="Additional instructions appended for this run", ), - tools: Optional[str] = typer.Option(None, "--tools", help="Tools as a JSON array string"), - metadata: Optional[str] = typer.Option(None, "--metadata", help="Metadata as a JSON object string"), - extra_body: Optional[str] = typer.Option(None, "--extra-body", help="Extra body as a JSON object string"), + tools: Optional[str] = typer.Option( + None, + "--tools", + help="Tools as a JSON array string", + ), + metadata: Optional[str] = typer.Option( + None, + "--metadata", + help="Metadata as a JSON object string", + ), + extra_body: Optional[str] = typer.Option( + None, + "--extra-body", + help="Extra body as a JSON object string", + ), workspace: Optional[str] = typer.Option( None, "-w", "--workspace", help="The DashScope workspace id", ), - top_p: Optional[float] = typer.Option(None, "--top-p", help="Top-p sampling"), - top_k: Optional[int] = typer.Option(None, "--top-k", help="Top-k sampling"), - temperature: Optional[float] = typer.Option(None, "--temperature", help="Sampling temperature"), - max_tokens: Optional[int] = typer.Option(None, "--max-tokens", help="Maximum output tokens"), + top_p: Optional[float] = typer.Option( + None, + "--top-p", + help="Top-p sampling", + ), + top_k: Optional[int] = typer.Option( + None, + "--top-k", + help="Top-k sampling", + ), + temperature: Optional[float] = typer.Option( + None, + "--temperature", + help="Sampling temperature", + ), + max_tokens: Optional[int] = typer.Option( + None, + "--max-tokens", + help="Maximum output tokens", + ), ): """Create a run.""" response = dashscope.Runs.create( @@ -94,7 +131,11 @@ def create( max_tokens=max_tokens, ) console.print_json( - json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + json.dumps( + response, + default=lambda value: value.__dict__, + ensure_ascii=False, + ), ) @@ -121,7 +162,11 @@ def get_run( workspace=workspace, ) console.print_json( - json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + json.dumps( + response, + default=lambda value: value.__dict__, + ensure_ascii=False, + ), ) @@ -129,10 +174,26 @@ def get_run( @handle_sdk_error("List runs failed") def list_runs( thread_id: str = typer.Argument(..., help="The thread id"), - limit: Optional[int] = typer.Option(None, "--limit", help="Maximum number of runs"), - order: Optional[str] = typer.Option(None, "--order", help="Sort order by created_at"), - after: Optional[str] = typer.Option(None, "--after", help="Cursor after run id"), - before: Optional[str] = typer.Option(None, "--before", help="Cursor before run id"), + limit: Optional[int] = typer.Option( + None, + "--limit", + help="Maximum number of runs", + ), + order: Optional[str] = typer.Option( + None, + "--order", + help="Sort order by created_at", + ), + after: Optional[str] = typer.Option( + None, + "--after", + help="Cursor after run id", + ), + before: Optional[str] = typer.Option( + None, + "--before", + help="Cursor before run id", + ), workspace: Optional[str] = typer.Option( None, "-w", @@ -150,7 +211,11 @@ def list_runs( workspace=workspace, ) console.print_json( - json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + json.dumps( + response, + default=lambda value: value.__dict__, + ensure_ascii=False, + ), ) @@ -189,7 +254,11 @@ def submit_tool_outputs( workspace=workspace, ) console.print_json( - json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + json.dumps( + response, + default=lambda value: value.__dict__, + ensure_ascii=False, + ), ) @@ -222,7 +291,11 @@ def wait_run( workspace=workspace, ) console.print_json( - json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + json.dumps( + response, + default=lambda value: value.__dict__, + ensure_ascii=False, + ), ) @@ -255,7 +328,11 @@ def update_run( workspace=workspace, ) console.print_json( - json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + json.dumps( + response, + default=lambda value: value.__dict__, + ensure_ascii=False, + ), ) @@ -282,5 +359,9 @@ def cancel_run( workspace=workspace, ) console.print_json( - json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + json.dumps( + response, + default=lambda value: value.__dict__, + ensure_ascii=False, + ), ) diff --git a/dashscope/cli/speech_synthesis.py b/dashscope/cli/speech_synthesis.py index cedac39..dc04dac 100644 --- a/dashscope/cli/speech_synthesis.py +++ b/dashscope/cli/speech_synthesis.py @@ -34,16 +34,32 @@ def create( "--audio-format", help="Audio format, such as wav, pcm or mp3", ), - sample_rate: int = typer.Option(24000, "--sample-rate", help="Audio sample rate"), + sample_rate: int = typer.Option( + 24000, + "--sample-rate", + help="Audio sample rate", + ), workspace: Optional[str] = typer.Option( None, "-w", "--workspace", help="The DashScope workspace id", ), - url: Optional[str] = typer.Option(None, "--url", help="Custom HTTP base URL"), - volume: Optional[int] = typer.Option(None, "--volume", help="Volume value"), - rate: Optional[int] = typer.Option(None, "--rate", help="Speech rate value"), + url: Optional[str] = typer.Option( + None, + "--url", + help="Custom HTTP base URL", + ), + volume: Optional[int] = typer.Option( + None, + "--volume", + help="Volume value", + ), + rate: Optional[int] = typer.Option( + None, + "--rate", + help="Speech rate value", + ), pitch: Optional[int] = typer.Option(None, "--pitch", help="Pitch value"), ): """Call HTTP speech synthesis API.""" diff --git a/dashscope/cli/steps.py b/dashscope/cli/steps.py index 478c2e6..d9ae70b 100644 --- a/dashscope/cli/steps.py +++ b/dashscope/cli/steps.py @@ -32,10 +32,26 @@ def list_steps( "--thread-id", help="The thread id", ), - limit: Optional[int] = typer.Option(None, "--limit", help="Maximum number of steps"), - order: Optional[str] = typer.Option(None, "--order", help="Sort order by created_at"), - after: Optional[str] = typer.Option(None, "--after", help="Cursor after step id"), - before: Optional[str] = typer.Option(None, "--before", help="Cursor before step id"), + limit: Optional[int] = typer.Option( + None, + "--limit", + help="Maximum number of steps", + ), + order: Optional[str] = typer.Option( + None, + "--order", + help="Sort order by created_at", + ), + after: Optional[str] = typer.Option( + None, + "--after", + help="Cursor after step id", + ), + before: Optional[str] = typer.Option( + None, + "--before", + help="Cursor before step id", + ), workspace: Optional[str] = typer.Option( None, "-w", @@ -54,7 +70,11 @@ def list_steps( workspace=workspace, ) console.print_json( - json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + json.dumps( + response, + default=lambda value: value.__dict__, + ensure_ascii=False, + ), ) @@ -87,5 +107,9 @@ def get_step( workspace=workspace, ) console.print_json( - json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + json.dumps( + response, + default=lambda value: value.__dict__, + ensure_ascii=False, + ), ) diff --git a/dashscope/cli/threads.py b/dashscope/cli/threads.py index c846298..55ce072 100644 --- a/dashscope/cli/threads.py +++ b/dashscope/cli/threads.py @@ -74,7 +74,11 @@ def create( workspace=workspace, ) console.print_json( - json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + json.dumps( + response, + default=lambda value: value.__dict__, + ensure_ascii=False, + ), ) @@ -101,7 +105,11 @@ def update_thread( workspace=workspace, ) console.print_json( - json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + json.dumps( + response, + default=lambda value: value.__dict__, + ensure_ascii=False, + ), ) @@ -122,7 +130,11 @@ def get_thread( workspace=workspace, ) console.print_json( - json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + json.dumps( + response, + default=lambda value: value.__dict__, + ensure_ascii=False, + ), ) @@ -143,23 +155,48 @@ def delete_thread( workspace=workspace, ) console.print_json( - json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + json.dumps( + response, + default=lambda value: value.__dict__, + ensure_ascii=False, + ), ) @app.command("create-and-run") @handle_sdk_error("Create thread and run failed") def create_and_run( - assistant_id: str = typer.Option(..., "--assistant-id", help="The assistant id"), - thread: Optional[str] = typer.Option(None, "--thread", help="Thread as a JSON object string"), - model: Optional[str] = typer.Option(None, "-m", "--model", help="The model to use"), - instructions: Optional[str] = typer.Option(None, "--instructions", help="Run instructions"), + assistant_id: str = typer.Option( + ..., + "--assistant-id", + help="The assistant id", + ), + thread: Optional[str] = typer.Option( + None, + "--thread", + help="Thread as a JSON object string", + ), + model: Optional[str] = typer.Option( + None, + "-m", + "--model", + help="The model to use", + ), + instructions: Optional[str] = typer.Option( + None, + "--instructions", + help="Run instructions", + ), additional_instructions: Optional[str] = typer.Option( None, "--additional-instructions", help="Additional run instructions", ), - tools: Optional[str] = typer.Option(None, "--tools", help="Tools as a JSON array string"), + tools: Optional[str] = typer.Option( + None, + "--tools", + help="Tools as a JSON array string", + ), metadata: Optional[str] = typer.Option( None, "--metadata", @@ -184,5 +221,9 @@ def create_and_run( workspace=workspace, ) console.print_json( - json.dumps(response, default=lambda value: value.__dict__, ensure_ascii=False), + json.dumps( + response, + default=lambda value: value.__dict__, + ensure_ascii=False, + ), ) diff --git a/dashscope/cli/transcription.py b/dashscope/cli/transcription.py index 9425591..fb2e449 100644 --- a/dashscope/cli/transcription.py +++ b/dashscope/cli/transcription.py @@ -32,7 +32,11 @@ def create( "--file-url", help="Audio file URL, can be used multiple times", ), - phrase_id: Optional[str] = typer.Option(None, "--phrase-id", help="Phrase id"), + phrase_id: Optional[str] = typer.Option( + None, + "--phrase-id", + help="Phrase id", + ), workspace: Optional[str] = typer.Option( None, "-w", @@ -54,7 +58,11 @@ def create( "--diarization-enabled", help="Whether to enable speaker diarization", ), - speaker_count: Optional[int] = typer.Option(None, "--speaker-count", help="Speaker count"), + speaker_count: Optional[int] = typer.Option( + None, + "--speaker-count", + help="Speaker count", + ), timestamp_alignment_enabled: bool = typer.Option( False, "--timestamp-alignment-enabled", diff --git a/dashscope/cli/video_synthesis.py b/dashscope/cli/video_synthesis.py index 7f7150d..3039832 100644 --- a/dashscope/cli/video_synthesis.py +++ b/dashscope/cli/video_synthesis.py @@ -27,13 +27,22 @@ def callback(ctx: typer.Context): @handle_sdk_error("Video synthesis request failed") def create( model: str = typer.Option(..., "-m", "--model", help="The model to call"), - prompt: Optional[str] = typer.Option(None, "-p", "--prompt", help="Input prompt"), + prompt: Optional[str] = typer.Option( + None, + "-p", + "--prompt", + help="Input prompt", + ), negative_prompt: Optional[str] = typer.Option( None, "--negative-prompt", help="Negative prompt", ), - img_url: Optional[str] = typer.Option(None, "--img-url", help="Input image URL"), + img_url: Optional[str] = typer.Option( + None, + "--img-url", + help="Input image URL", + ), first_frame_url: Optional[str] = typer.Option( None, "--first-frame-url", @@ -50,8 +59,16 @@ def create( "--workspace", help="The DashScope workspace id", ), - size: Optional[str] = typer.Option(None, "--size", help="Output video size"), - duration: Optional[int] = typer.Option(None, "--duration", help="Video duration"), + size: Optional[str] = typer.Option( + None, + "--size", + help="Output video size", + ), + duration: Optional[int] = typer.Option( + None, + "--duration", + help="Video duration", + ), seed: Optional[int] = typer.Option(None, "--seed", help="Random seed"), prompt_extend: Optional[bool] = typer.Option( None, @@ -63,7 +80,11 @@ def create( "--watermark/--no-watermark", help="Whether to add watermark", ), - resolution: Optional[str] = typer.Option(None, "--resolution", help="Output resolution"), + resolution: Optional[str] = typer.Option( + None, + "--resolution", + help="Output resolution", + ), ratio: Optional[str] = typer.Option(None, "--ratio", help="Aspect ratio"), ): """Call video synthesis API.""" @@ -143,11 +164,31 @@ def cancel( @app.command("list") @handle_sdk_error("List video synthesis tasks failed") def list_tasks( - start_time: Optional[str] = typer.Option(None, "--start-time", help="Task start time"), - end_time: Optional[str] = typer.Option(None, "--end-time", help="Task end time"), - model_name: Optional[str] = typer.Option(None, "--model-name", help="Model name"), - api_key_id: Optional[str] = typer.Option(None, "--api-key-id", help="API key id"), - region: Optional[str] = typer.Option(None, "--region", help="Service region"), + start_time: Optional[str] = typer.Option( + None, + "--start-time", + help="Task start time", + ), + end_time: Optional[str] = typer.Option( + None, + "--end-time", + help="Task end time", + ), + model_name: Optional[str] = typer.Option( + None, + "--model-name", + help="Model name", + ), + api_key_id: Optional[str] = typer.Option( + None, + "--api-key-id", + help="API key id", + ), + region: Optional[str] = typer.Option( + None, + "--region", + help="Service region", + ), status: Optional[str] = typer.Option(None, "--status", help="Task status"), page: int = typer.Option(1, "-p", "--page", help="Page number"), size: int = typer.Option(10, "-s", "--size", help="Page size"), diff --git a/tests/unit/test_cli_application.py b/tests/unit/test_cli_application.py index 7aa3f36..957f830 100644 --- a/tests/unit/test_cli_application.py +++ b/tests/unit/test_cli_application.py @@ -31,7 +31,11 @@ def mock_call(**kwargs): }, ) - monkeypatch.setattr(application.dashscope.Application, "call", mock_call) + monkeypatch.setattr( + application.dashscope.Application, + "call", + mock_call, + ) result = CliRunner().invoke( application.app, diff --git a/tests/unit/test_cli_assistant_files.py b/tests/unit/test_cli_assistant_files.py index 7bc26bb..00c7704 100644 --- a/tests/unit/test_cli_assistant_files.py +++ b/tests/unit/test_cli_assistant_files.py @@ -151,7 +151,7 @@ def mock_list(assistant_id, **kwargs): object="assistant.file", created_at=111111, assistant_id=assistant_id, - ) + ), ], ) diff --git a/tests/unit/test_cli_embeddings.py b/tests/unit/test_cli_embeddings.py index 48d21db..9fd69d3 100644 --- a/tests/unit/test_cli_embeddings.py +++ b/tests/unit/test_cli_embeddings.py @@ -27,7 +27,11 @@ def mock_call(**kwargs): usage={"total_tokens": 2}, ) - monkeypatch.setattr(embeddings.dashscope.TextEmbedding, "call", mock_call) + monkeypatch.setattr( + embeddings.dashscope.TextEmbedding, + "call", + mock_call, + ) result = CliRunner().invoke( embeddings.app, diff --git a/tests/unit/test_cli_image_generation.py b/tests/unit/test_cli_image_generation.py index 29bd8b8..f43a066 100644 --- a/tests/unit/test_cli_image_generation.py +++ b/tests/unit/test_cli_image_generation.py @@ -21,7 +21,11 @@ def mock_call(**kwargs): { "message": { "content": [ - {"image": "https://example.com/generated.png"}, + { + "image": ( + "https://example.com/generated.png" + ), + }, ], }, }, diff --git a/tests/unit/test_cli_main.py b/tests/unit/test_cli_main.py index f536bfa..d6bd402 100644 --- a/tests/unit/test_cli_main.py +++ b/tests/unit/test_cli_main.py @@ -16,9 +16,12 @@ from dashscope.common.error import AuthenticationError +# pylint: disable=too-many-public-methods class TestCliMain: def test_main_prints_authentication_error_without_traceback( - self, monkeypatch, capsys + self, + monkeypatch, + capsys, ): def mock_list(**kwargs): raise AuthenticationError("No api key provided.") @@ -102,10 +105,14 @@ def test_rl_upload_data_hyphen_alias_help(self): assert "training-files" in result.output def test_missing_global_api_key_value_does_not_consume_command( - self, monkeypatch, capsys + self, + monkeypatch, + capsys, ): monkeypatch.setattr( - sys, "argv", ["dashscope", "--api-key", "models", "list"] + sys, + "argv", + ["dashscope", "--api-key", "models", "list"], ) with pytest.raises(SystemExit) as exception_info: @@ -119,7 +126,9 @@ def test_missing_global_api_key_value_does_not_consume_command( assert "No such command 'list'" not in combined_output def test_missing_global_api_key_value_does_not_consume_option( - self, monkeypatch, capsys + self, + monkeypatch, + capsys, ): monkeypatch.setattr(sys, "argv", ["dashscope", "--api-key", "--help"]) @@ -134,11 +143,15 @@ def test_missing_global_api_key_value_does_not_consume_option( assert "Usage:" not in combined_output def test_empty_global_api_key_value_exits_before_request( - self, monkeypatch, capsys + self, + monkeypatch, + capsys, ): monkeypatch.setattr(dashscope, "api_key", "existing-key") monkeypatch.setattr( - sys, "argv", ["dashscope", "--api-key=", "models", "list"] + sys, + "argv", + ["dashscope", "--api-key=", "models", "list"], ) with pytest.raises(SystemExit) as exception_info: @@ -161,7 +174,8 @@ def mock_list(**kwargs): return SimpleNamespace(status_code=200, output={"models": []}) monkeypatch.setattr( - "dashscope.cli.models.dashscope.Models.list", mock_list + "dashscope.cli.models.dashscope.Models.list", + mock_list, ) monkeypatch.setattr(dashscope, "api_key", None) monkeypatch.setattr( @@ -181,7 +195,8 @@ def mock_list(**kwargs): } def test_global_api_key_equals_after_command_is_supported( - self, monkeypatch + self, + monkeypatch, ): captured_request = {} @@ -191,7 +206,8 @@ def mock_list(**kwargs): return SimpleNamespace(status_code=200, output={"models": []}) monkeypatch.setattr( - "dashscope.cli.models.dashscope.Models.list", mock_list + "dashscope.cli.models.dashscope.Models.list", + mock_list, ) monkeypatch.setattr(dashscope, "api_key", None) monkeypatch.setattr( @@ -217,7 +233,8 @@ def test_agentic_rl_hidden_alias_help(self): assert "register_functions" in result.output def test_subcommand_api_key_option_is_not_consumed_by_global_parser( - self, monkeypatch + self, + monkeypatch, ): captured_request = {} @@ -229,7 +246,8 @@ def mock_upload(model, file_path, api_key, base_address=None): return "oss://uploaded", None monkeypatch.setattr( - "dashscope.cli.oss.os.path.exists", lambda path: True + "dashscope.cli.oss.os.path.exists", + lambda path: True, ) monkeypatch.setattr("dashscope.cli.oss.OssUtils.upload", mock_upload) monkeypatch.setattr(dashscope, "api_key", "global-key") @@ -261,7 +279,9 @@ def mock_upload(model, file_path, api_key, base_address=None): } def test_subcommand_api_key_missing_value_is_handled_by_subcommand_parser( - self, monkeypatch, capsys + self, + monkeypatch, + capsys, ): monkeypatch.setattr( sys, @@ -288,7 +308,9 @@ def test_subcommand_api_key_missing_value_is_handled_by_subcommand_parser( assert "requires an argument" in combined_output def test_legacy_api_key_missing_value_exits_before_request( - self, monkeypatch, capsys + self, + monkeypatch, + capsys, ): monkeypatch.setattr( sys, @@ -314,7 +336,8 @@ def test_legacy_api_key_missing_value_exits_before_request( assert "requires an argument" in combined_output def test_legacy_api_key_option_is_extracted_after_translation( - self, monkeypatch + self, + monkeypatch, ): monkeypatch.setattr(dashscope, "api_key", None) captured_request = {} @@ -376,7 +399,8 @@ def mock_deployments_list(**kwargs): return SimpleNamespace(status_code=200, output={"deployments": []}) monkeypatch.setattr( - "dashscope.cli.files.dashscope.Files.list", mock_files_list + "dashscope.cli.files.dashscope.Files.list", + mock_files_list, ) monkeypatch.setattr( "dashscope.cli.fine_tunes.dashscope.FineTunes.list", @@ -406,7 +430,8 @@ def mock_deployments_list(**kwargs): assert captured_requests[request_key]["page_size"] == 20 def test_legacy_list_page_size_equals_maps_to_size_option( - self, monkeypatch + self, + monkeypatch, ): captured_request = {} @@ -415,10 +440,13 @@ def mock_files_list(**kwargs): return SimpleNamespace(status_code=200, output={}) monkeypatch.setattr( - "dashscope.cli.files.dashscope.Files.list", mock_files_list + "dashscope.cli.files.dashscope.Files.list", + mock_files_list, ) monkeypatch.setattr( - sys, "argv", ["dashscope", "files.list", "--page_size=30"] + sys, + "argv", + ["dashscope", "files.list", "--page_size=30"], ) with pytest.raises(SystemExit) as exception_info: @@ -428,10 +456,13 @@ def mock_files_list(**kwargs): assert captured_request["page_size"] == 30 def test_files_upload_missing_file_exits_without_traceback( - self, monkeypatch, capsys + self, + monkeypatch, + capsys, ): monkeypatch.setattr( - "dashscope.cli.files.os.path.exists", lambda path: False + "dashscope.cli.files.os.path.exists", + lambda path: False, ) monkeypatch.setattr( sys, @@ -450,16 +481,20 @@ def test_files_upload_missing_file_exits_without_traceback( assert "Traceback" not in combined_output def test_files_upload_sdk_exception_exits_without_traceback( - self, monkeypatch, capsys + self, + monkeypatch, + capsys, ): def mock_upload(**kwargs): raise RuntimeError("upload failed") monkeypatch.setattr( - "dashscope.cli.files.os.path.exists", lambda path: True + "dashscope.cli.files.os.path.exists", + lambda path: True, ) monkeypatch.setattr( - "dashscope.cli.files.dashscope.Files.upload", mock_upload + "dashscope.cli.files.dashscope.Files.upload", + mock_upload, ) monkeypatch.setattr( sys, @@ -537,13 +572,14 @@ def mock_list(**kwargs): assert "Traceback" not in result.output def test_generation_stream_exception_is_printed_without_traceback( - self, monkeypatch + self, + monkeypatch, ): class FailingStream: def __iter__(self): raise RuntimeError("stream failed") - def mock_call(*args, **kwargs): + def mock_call(*_args, **_kwargs): return FailingStream() monkeypatch.setattr( @@ -569,13 +605,15 @@ def mock_call(*args, **kwargs): assert "Traceback" not in result.output def test_models_sdk_exception_is_printed_without_traceback( - self, monkeypatch + self, + monkeypatch, ): def mock_list(**kwargs): raise RuntimeError("models failed") monkeypatch.setattr( - "dashscope.cli.models.dashscope.Models.list", mock_list + "dashscope.cli.models.dashscope.Models.list", + mock_list, ) result = CliRunner().invoke(cli_app, ["models", "list"]) @@ -585,7 +623,8 @@ def mock_list(**kwargs): assert "Traceback" not in result.output def test_deployments_sdk_exception_is_printed_without_traceback( - self, monkeypatch + self, + monkeypatch, ): def mock_call(**kwargs): raise RuntimeError("deployment failed") @@ -605,9 +644,10 @@ def mock_call(**kwargs): assert "Traceback" not in result.output def test_application_response_without_usage_is_supported( - self, monkeypatch + self, + monkeypatch, ): - def mock_call(**kwargs): + def mock_call(**_kwargs): return SimpleNamespace(status_code=200, output={"text": "ok"}) monkeypatch.setattr( @@ -625,7 +665,7 @@ def mock_call(**kwargs): assert "Traceback" not in result.output def test_generation_response_without_usage_is_supported(self, monkeypatch): - def mock_call(*args, **kwargs): + def mock_call(*_args, **_kwargs): return SimpleNamespace(status_code=200, output={"text": "ok"}) monkeypatch.setattr( @@ -643,11 +683,12 @@ def mock_call(*args, **kwargs): assert "Traceback" not in result.output def test_generation_stream_response_without_usage_is_supported( - self, monkeypatch + self, + monkeypatch, ): - def mock_call(*args, **kwargs): + def mock_call(*_args, **_kwargs): return iter( - [SimpleNamespace(status_code=200, output={"text": "ok"})] + [SimpleNamespace(status_code=200, output={"text": "ok"})], ) monkeypatch.setattr( @@ -673,7 +714,8 @@ def mock_call(*args, **kwargs): assert "Traceback" not in result.output def test_image_generation_sdk_exception_is_printed_without_traceback( - self, monkeypatch + self, + monkeypatch, ): def mock_call(**kwargs): raise RuntimeError("image failed") @@ -700,13 +742,15 @@ def mock_call(**kwargs): assert "Traceback" not in result.output def test_multimodal_conversation_response_without_usage_is_supported( - self, monkeypatch + self, + monkeypatch, ): - def mock_call(**kwargs): + def mock_call(**_kwargs): return SimpleNamespace(status_code=200, output={"text": "ok"}) monkeypatch.setattr( - "dashscope.cli.multimodal_conversation.dashscope.MultiModalConversation.call", + "dashscope.cli.multimodal_conversation.dashscope." + "MultiModalConversation.call", mock_call, ) @@ -727,13 +771,15 @@ def mock_call(**kwargs): assert "Traceback" not in result.output def test_multimodal_embedding_response_without_usage_is_supported( - self, monkeypatch + self, + monkeypatch, ): - def mock_call(**kwargs): + def mock_call(**_kwargs): return SimpleNamespace(status_code=200, output={"embeddings": []}) monkeypatch.setattr( - "dashscope.cli.multimodal_embedding.dashscope.MultiModalEmbedding.call", + "dashscope.cli.multimodal_embedding.dashscope." + "MultiModalEmbedding.call", mock_call, ) @@ -754,13 +800,15 @@ def mock_call(**kwargs): assert "Traceback" not in result.output def test_multimodal_embedding_sdk_exception_is_printed_without_traceback( - self, monkeypatch + self, + monkeypatch, ): - def mock_call(**kwargs): + def mock_call(**_kwargs): raise RuntimeError("embedding failed") monkeypatch.setattr( - "dashscope.cli.multimodal_embedding.dashscope.MultiModalEmbedding.call", + "dashscope.cli.multimodal_embedding.dashscope." + "MultiModalEmbedding.call", mock_call, ) diff --git a/tests/unit/test_cli_messages.py b/tests/unit/test_cli_messages.py index f413e4b..aa12d2e 100644 --- a/tests/unit/test_cli_messages.py +++ b/tests/unit/test_cli_messages.py @@ -83,7 +83,7 @@ def mock_list(thread_id, **kwargs): object="thread.message", thread_id=thread_id, role="user", - ) + ), ], ) @@ -228,7 +228,7 @@ def mock_list(message_id, **kwargs): id="file-1234", object="thread.message.file", message_id=message_id, - ) + ), ], ) diff --git a/tests/unit/test_cli_steps.py b/tests/unit/test_cli_steps.py index 46747c0..4822795 100644 --- a/tests/unit/test_cli_steps.py +++ b/tests/unit/test_cli_steps.py @@ -19,7 +19,9 @@ def mock_list(run_id, **kwargs): captured_request.update(kwargs) return SimpleNamespace( object="list", - data=[SimpleNamespace(id="step-1234", type="message_creation")], + data=[ + SimpleNamespace(id="step-1234", type="message_creation"), + ], first_id="step-1234", last_id="step-1234", has_more=False, diff --git a/tests/unit/test_cli_tokenization.py b/tests/unit/test_cli_tokenization.py index d05af9e..9da078b 100644 --- a/tests/unit/test_cli_tokenization.py +++ b/tests/unit/test_cli_tokenization.py @@ -24,7 +24,11 @@ def mock_call(**kwargs): usage={"input_tokens": 2}, ) - monkeypatch.setattr(tokenization.dashscope.Tokenization, "call", mock_call) + monkeypatch.setattr( + tokenization.dashscope.Tokenization, + "call", + mock_call, + ) result = CliRunner().invoke( tokenization.app, diff --git a/tests/unit/test_cli_transcription.py b/tests/unit/test_cli_transcription.py index e8f0628..bd8c663 100644 --- a/tests/unit/test_cli_transcription.py +++ b/tests/unit/test_cli_transcription.py @@ -21,7 +21,9 @@ def mock_call(**kwargs): "task_status": "SUCCEEDED", "results": [ { - "transcription_url": "https://example.com/result.json", + "transcription_url": ( + "https://example.com/result.json" + ), }, ], }, From f9f15255d4ede2b0da9ee41a11679278dd61a0b9 Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 23 Jun 2026 16:35:43 +0800 Subject: [PATCH 3/9] fix: resolve Rich rendering error in CLI generation create and adapt Models.get API --- dashscope/cli/generation.py | 8 ++++---- dashscope/models.py | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/dashscope/cli/generation.py b/dashscope/cli/generation.py index 2e02a32..3c01e1c 100644 --- a/dashscope/cli/generation.py +++ b/dashscope/cli/generation.py @@ -50,18 +50,18 @@ def create( if stream: for rsp in response: if rsp.status_code == HTTPStatus.OK: - console.print(rsp.output) + typer.echo(rsp.output) usage = getattr(rsp, "usage", None) if usage: - console.print(usage) + typer.echo(usage) else: print_failed_message(rsp) else: if response.status_code == HTTPStatus.OK: - console.print(response.output) + typer.echo(response.output) usage = getattr(response, "usage", None) if usage: - console.print(usage) + typer.echo(usage) else: print_failed_message(response) raise typer.Exit(1) diff --git a/dashscope/models.py b/dashscope/models.py index 764e088..a94a00b 100644 --- a/dashscope/models.py +++ b/dashscope/models.py @@ -2,7 +2,9 @@ # Copyright (c) Alibaba, Inc. and its affiliates. from dashscope.api_entities.dashscope_response import DashScopeAPIResponse -from dashscope.client.base_api import GetMixin, ListMixin +from dashscope.client.base_api import GetMixin, ListMixin, _get +from dashscope.common.utils import join_url +import dashscope class Models(ListMixin, GetMixin): @@ -25,7 +27,33 @@ def get( # type: ignore[override] Returns: DashScopeAPIResponse: The model information. """ - return super().get(name, api_key, **kwargs) # type: ignore + from http import HTTPStatus + + # Use query parameter to filter by model name on server side + # API endpoint: /api/v1/models?model={name}&page_no=1&page_size=1 + url = join_url(dashscope.base_http_api_url, cls.SUB_PATH.lower()) + params = {"model": name, "page_no": 1, "page_size": 1} + + response = _get( + url, + params=params, + api_key=api_key, + **kwargs, + ) + + if response.status_code != HTTPStatus.OK: + return response + + output = response.output + if not output or "models" not in output or not output["models"]: + response.status_code = 404 + response.message = f"Model '{name}' not found" + response.output = None + return response + + # Return the first (and only) model from the filtered list + response.output = output["models"][0] + return response @classmethod def list( # type: ignore[override] From 02ad636503bd1bc83894a8f67421d74122318ec1 Mon Sep 17 00:00:00 2001 From: kevin Date: Wed, 24 Jun 2026 11:51:07 +0800 Subject: [PATCH 4/9] feat(cli): add 12 generation parameters and fix Models.get --- dashscope/cli/generation.py | 136 ++++++++++++++++++++++++++++++++++-- dashscope/models.py | 28 ++------ 2 files changed, 137 insertions(+), 27 deletions(-) diff --git a/dashscope/cli/generation.py b/dashscope/cli/generation.py index 3c01e1c..bd30e4c 100644 --- a/dashscope/cli/generation.py +++ b/dashscope/cli/generation.py @@ -1,13 +1,12 @@ # -*- coding: utf-8 -*- """``generation`` sub-command group.""" from http import HTTPStatus -from typing import Optional +from typing import Any, Dict, Optional import typer from dashscope.aigc import Generation from dashscope.cli.common import ( - console, handle_sdk_error, print_failed_message, ) @@ -20,6 +19,61 @@ ) +def _build_generation_kwargs( + messages: Optional[str], + temperature: Optional[float], + top_p: Optional[float], + top_k: Optional[int], + max_tokens: Optional[int], + seed: Optional[int], + stop: Optional[str], + repetition_penalty: Optional[float], + presence_penalty: Optional[float], + enable_search: Optional[bool], + n: Optional[int], + result_format: Optional[str], +) -> Dict[str, Any]: + """Build kwargs dictionary for Generation.call from CLI options.""" + import json + + kwargs: Dict[str, Any] = {} + + if messages is not None: + try: + kwargs["messages"] = json.loads(messages) + except json.JSONDecodeError as exc: + typer.echo( + "Error: --messages must be a valid JSON string", + err=True, + ) + raise typer.Exit(1) from exc + + # Group simple parameters to reduce branches + simple_params = { + "temperature": temperature, + "top_p": top_p, + "top_k": top_k, + "max_tokens": max_tokens, + "seed": seed, + "repetition_penalty": repetition_penalty, + "presence_penalty": presence_penalty, + "enable_search": enable_search, + "n": n, + "result_format": result_format, + } + + for key, value in simple_params.items(): + if value is not None: + kwargs[key] = value + + if stop is not None: + stop_list = [s.strip() for s in stop.split(",") if s.strip()] + if stop_list: + kwargs["stop"] = stop_list if len(stop_list) > 1 else stop_list[0] + + return kwargs + + @app.callback() def callback(ctx: typer.Context): """Show help if no subcommand is provided.""" @@ -38,14 +92,84 @@ def create( "--stream", help="Use stream mode", ), - history: Optional[str] = typer.Option( # pylint: disable=unused-argument + messages: Optional[str] = typer.Option( None, - "--history", - help="The history of the request", + "--messages", + help="JSON string of message list for multi-turn conversation", + ), + temperature: Optional[float] = typer.Option( + None, + "--temperature", + help="Controls randomness, range [0, 2)", + ), + top_p: Optional[float] = typer.Option( + None, + "--top-p", + help="Nucleus sampling parameter, range (0, 1.0]", + ), + top_k: Optional[int] = typer.Option( + None, + "--top-k", + help="Size of candidate token set for sampling", + ), + max_tokens: Optional[int] = typer.Option( + None, + "--max-tokens", + help="Maximum number of output tokens", + ), + seed: Optional[int] = typer.Option( + None, + "--seed", + help="Random seed for reproducibility", + ), + stop: Optional[str] = typer.Option( + None, + "--stop", + help="Stop sequence(s), comma-separated for multiple", + ), + repetition_penalty: Optional[float] = typer.Option( + None, + "--repetition-penalty", + help="Penalizes repeated sequences, 1.0 means no penalty", + ), + presence_penalty: Optional[float] = typer.Option( + None, + "--presence-penalty", + help="Controls content repetition, range [-2.0, 2.0]", + ), + enable_search: Optional[bool] = typer.Option( + None, + "--enable-search", + help="Enable web search", + ), + n: Optional[int] = typer.Option( + None, + "--n", + help="Number of responses to generate (1-4)", + ), + result_format: Optional[str] = typer.Option( + None, + "--result-format", + help="Output format: 'message' or 'text'", ), ): """Call text generation API.""" - response = Generation.call(model, prompt, stream=stream) + kwargs = _build_generation_kwargs( + messages=messages, + temperature=temperature, + top_p=top_p, + top_k=top_k, + max_tokens=max_tokens, + seed=seed, + stop=stop, + repetition_penalty=repetition_penalty, + presence_penalty=presence_penalty, + enable_search=enable_search, + n=n, + result_format=result_format, + ) + + response = Generation.call(model, prompt, stream=stream, **kwargs) if stream: for rsp in response: diff --git a/dashscope/models.py b/dashscope/models.py index a94a00b..3464fc0 100644 --- a/dashscope/models.py +++ b/dashscope/models.py @@ -28,32 +28,18 @@ def get( # type: ignore[override] DashScopeAPIResponse: The model information. """ from http import HTTPStatus - - # Use query parameter to filter by model name on server side - # API endpoint: /api/v1/models?model={name}&page_no=1&page_size=1 - url = join_url(dashscope.base_http_api_url, cls.SUB_PATH.lower()) - params = {"model": name, "page_no": 1, "page_size": 1} - + + # Use path parameter to get specific model + # API endpoint: /api/v1/models/{name} + url = join_url(dashscope.base_http_api_url, cls.SUB_PATH.lower(), name) + response = _get( url, - params=params, api_key=api_key, **kwargs, ) - - if response.status_code != HTTPStatus.OK: - return response - - output = response.output - if not output or "models" not in output or not output["models"]: - response.status_code = 404 - response.message = f"Model '{name}' not found" - response.output = None - return response - - # Return the first (and only) model from the filtered list - response.output = output["models"][0] - return response + + return response # type: ignore[return-value] @classmethod def list( # type: ignore[override] From e64048b1344daf26c5039e95e1d1fe5036b1239b Mon Sep 17 00:00:00 2001 From: kevin Date: Wed, 24 Jun 2026 14:49:51 +0800 Subject: [PATCH 5/9] fix(models): fix Models.get 404 by using query parameter API --- dashscope/models.py | 20 +++++++++-- tests/unit/mock_server.py | 73 +++++++++++++++++++++++++++++---------- 2 files changed, 71 insertions(+), 22 deletions(-) diff --git a/dashscope/models.py b/dashscope/models.py index 3464fc0..5a1a57e 100644 --- a/dashscope/models.py +++ b/dashscope/models.py @@ -29,16 +29,30 @@ def get( # type: ignore[override] """ from http import HTTPStatus - # Use path parameter to get specific model - # API endpoint: /api/v1/models/{name} - url = join_url(dashscope.base_http_api_url, cls.SUB_PATH.lower(), name) + # Use query parameter to filter by model name on server side + # API endpoint: /api/v1/models?model={name}&page_no=1&page_size=1 + url = join_url(dashscope.base_http_api_url, cls.SUB_PATH.lower()) + params = {"model": name, "page_no": 1, "page_size": 1} response = _get( url, + params=params, api_key=api_key, **kwargs, ) + if response.status_code != HTTPStatus.OK: + return response # type: ignore[return-value] + + output = response.output + if not output or "models" not in output or not output["models"]: + response.status_code = 404 + response.message = f"Model '{name}' not found" + response.output = None + return response # type: ignore[return-value] + + # Return the first (and only) model from the filtered list + response.output = output["models"][0] return response # type: ignore[return-value] @classmethod diff --git a/tests/unit/mock_server.py b/tests/unit/mock_server.py index 10df414..8d43379 100644 --- a/tests/unit/mock_server.py +++ b/tests/unit/mock_server.py @@ -398,21 +398,40 @@ async def handle_delete_file(request: aiohttp.request): async def list_models_handler( request: aiohttp.request, ): # pylint: disable=unused-argument - response = { - "code": "200", - "data": { - "models": [ - { - "model_id": "1111", - "gmt_create": "2023-03-15 14:25:50", - }, - { - "model_id": "2222", - "gmt_create": "2023-03-15 14:25:50", - }, - ], - }, - } + # Check if model query parameter is present (for Models.get) + model_param = request.query.get("model") + + if model_param: + # Return single model for Models.get + assert model_param == TEST_JOB_ID + response = { + "code": "200", + "output": { + "models": [ + { + "model_id": TEST_JOB_ID, + "name": "gpt3", + }, + ], + }, + } + else: + # Return model list for Models.list + response = { + "code": "200", + "data": { + "models": [ + { + "model_id": "1111", + "gmt_create": "2023-03-15 14:25:50", + }, + { + "model_id": "2222", + "gmt_create": "2023-03-15 14:25:50", + }, + ], + }, + } return web.json_response( text=json.dumps(response), content_type="application/json", @@ -420,13 +439,29 @@ async def list_models_handler( async def get_model_handler(request: aiohttp.request): - model_id = request.match_info["id"] + # Support both path parameter and query parameter + model_id = request.match_info.get("id") + if not model_id: + # Fallback to query parameter + model_id = request.query.get("model") + + if not model_id: + return web.json_response( + text=json.dumps({"code": "400", "message": "Model name is required"}), + status=400, + content_type="application/json", + ) + assert model_id == TEST_JOB_ID response = { "code": "200", - "data": { - "model_id": TEST_JOB_ID, - "name": "gpt3", + "output": { + "models": [ + { + "model_id": TEST_JOB_ID, + "name": "gpt3", + }, + ], }, } return web.json_response( From dbe8194d0af30855b8338d2e171e8d94280e3726 Mon Sep 17 00:00:00 2001 From: kevin Date: Wed, 24 Jun 2026 14:55:15 +0800 Subject: [PATCH 6/9] fix(models): fix Models.get 404 by using query parameter API --- tests/unit/mock_server.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/unit/mock_server.py b/tests/unit/mock_server.py index 8d43379..0713088 100644 --- a/tests/unit/mock_server.py +++ b/tests/unit/mock_server.py @@ -400,7 +400,7 @@ async def list_models_handler( ): # pylint: disable=unused-argument # Check if model query parameter is present (for Models.get) model_param = request.query.get("model") - + if model_param: # Return single model for Models.get assert model_param == TEST_JOB_ID @@ -444,14 +444,16 @@ async def get_model_handler(request: aiohttp.request): if not model_id: # Fallback to query parameter model_id = request.query.get("model") - + if not model_id: return web.json_response( - text=json.dumps({"code": "400", "message": "Model name is required"}), + text=json.dumps( + {"code": "400", "message": "Model name is required"}, + ), status=400, content_type="application/json", ) - + assert model_id == TEST_JOB_ID response = { "code": "200", From 94f554532dfd1e368fbe67e12a91dafc7ffe6eb6 Mon Sep 17 00:00:00 2001 From: kevin Date: Wed, 24 Jun 2026 16:57:37 +0800 Subject: [PATCH 7/9] fix(cli): improve error handling and fix assistant update bug --- dashscope/cli/assistants.py | 2 +- dashscope/cli/files.py | 15 ++++++--------- dashscope/cli/generation.py | 6 ++---- tests/unit/test_cli_main.py | 19 +++++++++++++++---- tests/unit/test_cli_runs.py | 12 ++++++++++-- 5 files changed, 34 insertions(+), 20 deletions(-) diff --git a/dashscope/cli/assistants.py b/dashscope/cli/assistants.py index d0ebdab..726de10 100644 --- a/dashscope/cli/assistants.py +++ b/dashscope/cli/assistants.py @@ -271,7 +271,7 @@ def update_assistant( description=description, instructions=instructions, tools=_parse_json_array(tools, "tools"), - file_ids=file_ids or [], + file_ids=file_ids, metadata=_parse_json_object(metadata, "metadata"), workspace=workspace, top_p=top_p, diff --git a/dashscope/cli/files.py b/dashscope/cli/files.py index 50f03ea..40e28ee 100644 --- a/dashscope/cli/files.py +++ b/dashscope/cli/files.py @@ -64,15 +64,12 @@ def upload( if not os.path.exists(file_path): error(f"File {file_path} does not exist") - try: - rsp = dashscope.Files.upload( - file_path=file_path, - purpose=purpose, - description=description, # type: ignore[arg-type] - base_address=base_url, - ) - except Exception as exception: - error(str(exception)) + rsp = dashscope.Files.upload( + file_path=file_path, + purpose=purpose, + description=description, # type: ignore[arg-type] + base_address=base_url, + ) output = ensure_ok(rsp) file_id = output["uploaded_files"][0]["file_id"] success(f"Upload success, file id: {file_id}") diff --git a/dashscope/cli/generation.py b/dashscope/cli/generation.py index bd30e4c..b624acc 100644 --- a/dashscope/cli/generation.py +++ b/dashscope/cli/generation.py @@ -7,6 +7,7 @@ from dashscope.aigc import Generation from dashscope.cli.common import ( + error, handle_sdk_error, print_failed_message, ) @@ -42,10 +43,7 @@ def _build_generation_kwargs( try: kwargs["messages"] = json.loads(messages) except json.JSONDecodeError as exc: - typer.echo( - "Error: --messages must be a valid JSON string", - err=True, - ) + error("--messages must be a valid JSON string") raise typer.Exit(1) from exc # Group simple parameters to reduce branches diff --git a/tests/unit/test_cli_main.py b/tests/unit/test_cli_main.py index d6bd402..0501f4f 100644 --- a/tests/unit/test_cli_main.py +++ b/tests/unit/test_cli_main.py @@ -2,6 +2,7 @@ # Copyright (c) Alibaba, Inc. and its affiliates. import os +import re import subprocess import sys from types import SimpleNamespace @@ -16,6 +17,12 @@ from dashscope.common.error import AuthenticationError +def strip_ansi_codes(text): + """Remove ANSI escape codes from text.""" + ansi_escape = re.compile(r"\x1b\[[0-9;]*m") + return ansi_escape.sub("", text) + + # pylint: disable=too-many-public-methods class TestCliMain: def test_main_prints_authentication_error_without_traceback( @@ -46,7 +53,8 @@ def test_top_level_help_shows_global_api_key(self): result = CliRunner().invoke(cli_app, ["--help"]) assert result.exit_code == 0 - assert "--api-key" in result.output + clean_output = strip_ansi_codes(result.output) + assert "--api-key" in clean_output def test_python_module_help_suppresses_urllib3_warning(self): result = subprocess.run( @@ -84,7 +92,8 @@ def test_rl_register_functions_hyphen_alias_help(self): ) assert result.exit_code == 0 - assert "rollout-classpaths" in result.output + clean_output = strip_ansi_codes(result.output) + assert "rollout-classpaths" in clean_output def test_rl_test_functions_hyphen_alias_help(self): result = CliRunner().invoke( @@ -93,7 +102,8 @@ def test_rl_test_functions_hyphen_alias_help(self): ) assert result.exit_code == 0 - assert "--input" in result.output + clean_output = strip_ansi_codes(result.output) + assert "--input" in clean_output def test_rl_upload_data_hyphen_alias_help(self): result = CliRunner().invoke( @@ -102,7 +112,8 @@ def test_rl_upload_data_hyphen_alias_help(self): ) assert result.exit_code == 0 - assert "training-files" in result.output + clean_output = strip_ansi_codes(result.output) + assert "training-files" in clean_output def test_missing_global_api_key_value_does_not_consume_command( self, diff --git a/tests/unit/test_cli_runs.py b/tests/unit/test_cli_runs.py index af6a3ae..7457dd7 100644 --- a/tests/unit/test_cli_runs.py +++ b/tests/unit/test_cli_runs.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. +import re from types import SimpleNamespace from typer.testing import CliRunner @@ -8,6 +9,12 @@ from dashscope.cli import runs +def strip_ansi_codes(text): + """Remove ANSI escape codes from text.""" + ansi_escape = re.compile(r"\x1b\[[0-9;]*m") + return ansi_escape.sub("", text) + + class TestCliRuns: def test_create(self, monkeypatch): captured_thread_id = None @@ -120,8 +127,9 @@ def test_get_requires_thread_id(self): result = CliRunner().invoke(runs.app, ["get", "run-1234"]) assert result.exit_code != 0 - assert "Missing option" in result.output - assert "--thread-id" in result.output + clean_output = strip_ansi_codes(result.output) + assert "Missing option" in clean_output + assert "--thread-id" in clean_output def test_get(self, monkeypatch): captured_run_id = None From 887d269a70db0c288ff8875141fa713b101b68ac Mon Sep 17 00:00:00 2001 From: kevin Date: Thu, 25 Jun 2026 14:28:19 +0800 Subject: [PATCH 8/9] feat: remove assistants-related CLI commands --- dashscope/__init__.py | 4 +- dashscope/cli/__init__.py | 18 -- dashscope/cli/assistant_files.py | 153 ---------- dashscope/cli/assistants.py | 313 ------------------- dashscope/cli/messages.py | 285 ----------------- dashscope/cli/runs.py | 367 ---------------------- dashscope/cli/steps.py | 115 ------- dashscope/cli/threads.py | 229 -------------- dashscope/version.py | 2 +- tests/unit/test_cli_assistant_files.py | 192 ------------ tests/unit/test_cli_assistants.py | 282 ----------------- tests/unit/test_cli_main.py | 15 - tests/unit/test_cli_messages.py | 315 ------------------- tests/unit/test_cli_runs.py | 406 ------------------------- tests/unit/test_cli_steps.py | 113 ------- tests/unit/test_cli_threads.py | 229 -------------- 16 files changed, 3 insertions(+), 3035 deletions(-) delete mode 100644 dashscope/cli/assistant_files.py delete mode 100644 dashscope/cli/assistants.py delete mode 100644 dashscope/cli/messages.py delete mode 100644 dashscope/cli/runs.py delete mode 100644 dashscope/cli/steps.py delete mode 100644 dashscope/cli/threads.py delete mode 100644 tests/unit/test_cli_assistant_files.py delete mode 100644 tests/unit/test_cli_assistants.py delete mode 100644 tests/unit/test_cli_messages.py delete mode 100644 tests/unit/test_cli_runs.py delete mode 100644 tests/unit/test_cli_steps.py delete mode 100644 tests/unit/test_cli_threads.py diff --git a/dashscope/__init__.py b/dashscope/__init__.py index f7f351b..2d34c0a 100644 --- a/dashscope/__init__.py +++ b/dashscope/__init__.py @@ -24,8 +24,6 @@ ) from dashscope.aigc.video_synthesis import VideoSynthesis from dashscope.app.application import Application -from dashscope.assistants import Assistant, AssistantList, Assistants -from dashscope.assistants.assistant_types import AssistantFile, DeleteResponse from dashscope.audio.asr.transcription import Transcription from dashscope.audio.http_tts.http_speech_synthesizer import ( HttpSpeechSynthesizer, @@ -57,6 +55,8 @@ from dashscope.models import Models from dashscope.nlp.understanding import Understanding from dashscope.rerank.text_rerank import TextReRank +from dashscope.assistants import Assistant, AssistantList, Assistants +from dashscope.assistants.assistant_types import AssistantFile, DeleteResponse from dashscope.threads import ( MessageFile, Messages, diff --git a/dashscope/cli/__init__.py b/dashscope/cli/__init__.py index fd89013..276e805 100644 --- a/dashscope/cli/__init__.py +++ b/dashscope/cli/__init__.py @@ -23,8 +23,6 @@ from dashscope.common.error import AuthenticationError # noqa: E402 from dashscope.cli import ( # noqa: E402 application, - assistant_files, - assistants, code_generation, deployments, embeddings, @@ -33,16 +31,12 @@ generation, image_generation, image_synthesis, - messages, models, multimodal_conversation, multimodal_embedding, oss, rerank, - runs, speech_synthesis, - steps, - threads, tokenization, transcription, understanding, @@ -113,12 +107,6 @@ "multimodal-embedding", "transcription", "speech-synthesis", - "assistants", - "assistant-files", - "threads", - "messages", - "runs", - "steps", "rl", "agentic-rl", } @@ -298,12 +286,6 @@ def callback( app.add_typer(multimodal_embedding.app) app.add_typer(transcription.app) app.add_typer(speech_synthesis.app) -app.add_typer(assistants.app) -app.add_typer(assistant_files.app) -app.add_typer(threads.app) -app.add_typer(messages.app) -app.add_typer(runs.app) -app.add_typer(steps.app) def _register_rl_app(): diff --git a/dashscope/cli/assistant_files.py b/dashscope/cli/assistant_files.py deleted file mode 100644 index 5b4384a..0000000 --- a/dashscope/cli/assistant_files.py +++ /dev/null @@ -1,153 +0,0 @@ -# -*- coding: utf-8 -*- -"""``assistant-files`` sub-command group.""" -import json -from typing import Optional - -import typer - -from dashscope.assistants.files import Files -from dashscope.cli.common import console, handle_sdk_error - -app = typer.Typer( - name="assistant-files", - help="Assistant file management commands", - add_completion=False, - invoke_without_command=True, -) - - -@app.callback() -def callback(ctx: typer.Context): - """Show help if no subcommand is provided.""" - if ctx.invoked_subcommand is None: - typer.echo(ctx.get_help()) - - -@app.command("create") -@handle_sdk_error("Create assistant file failed") -def create( - assistant_id: str = typer.Argument(..., help="The assistant id"), - file_id: str = typer.Option(..., "--file-id", help="The file id"), - workspace: Optional[str] = typer.Option( - None, - "-w", - "--workspace", - help="The DashScope workspace id", - ), -): - """Create an assistant file.""" - response = Files.create( - assistant_id, - file_id=file_id, - workspace=workspace, - ) - console.print_json( - json.dumps( - response, - default=lambda value: value.__dict__, - ensure_ascii=False, - ), - ) - - -@app.command("get") -@handle_sdk_error("Retrieve assistant file failed") -def get_file( - assistant_id: str = typer.Argument(..., help="The assistant id"), - file_id: str = typer.Argument(..., help="The file id"), - workspace: Optional[str] = typer.Option( - None, - "-w", - "--workspace", - help="The DashScope workspace id", - ), -): - """Retrieve an assistant file.""" - response = Files.retrieve( - file_id, - assistant_id=assistant_id, - workspace=workspace, - ) - console.print_json( - json.dumps( - response, - default=lambda value: value.__dict__, - ensure_ascii=False, - ), - ) - - -@app.command("delete") -@handle_sdk_error("Delete assistant file failed") -def delete_file( - assistant_id: str = typer.Argument(..., help="The assistant id"), - file_id: str = typer.Argument(..., help="The file id"), - workspace: Optional[str] = typer.Option( - None, - "-w", - "--workspace", - help="The DashScope workspace id", - ), -): - """Delete an assistant file.""" - response = Files.delete( - file_id, - assistant_id=assistant_id, - workspace=workspace, - ) - console.print_json( - json.dumps( - response, - default=lambda value: value.__dict__, - ensure_ascii=False, - ), - ) - - -@app.command("list") -@handle_sdk_error("List assistant files failed") -def list_files( - assistant_id: str = typer.Argument(..., help="The assistant id"), - limit: Optional[int] = typer.Option( - None, - "--limit", - help="Maximum number of files", - ), - order: Optional[str] = typer.Option( - None, - "--order", - help="Sort order by created_at", - ), - after: Optional[str] = typer.Option( - None, - "--after", - help="Cursor after file id", - ), - before: Optional[str] = typer.Option( - None, - "--before", - help="Cursor before file id", - ), - workspace: Optional[str] = typer.Option( - None, - "-w", - "--workspace", - help="The DashScope workspace id", - ), -): - """List assistant files.""" - response = Files.list( - assistant_id, - limit=limit, - order=order, - after=after, - before=before, - workspace=workspace, - ) - console.print_json( - json.dumps( - response, - default=lambda value: value.__dict__, - ensure_ascii=False, - ), - ) diff --git a/dashscope/cli/assistants.py b/dashscope/cli/assistants.py deleted file mode 100644 index 726de10..0000000 --- a/dashscope/cli/assistants.py +++ /dev/null @@ -1,313 +0,0 @@ -# -*- coding: utf-8 -*- -"""``assistants`` sub-command group.""" -import json -from typing import List, Optional - -import typer - -import dashscope -from dashscope.cli.common import console, error, handle_sdk_error - -app = typer.Typer( - name="assistants", - help="Assistant management commands", - add_completion=False, - invoke_without_command=True, -) - - -@app.callback() -def callback(ctx: typer.Context): - """Show help if no subcommand is provided.""" - if ctx.invoked_subcommand is None: - typer.echo(ctx.get_help()) - - -def _parse_json_object(value: Optional[str], option_name: str): - if value is None: - return None - try: - parsed_value = json.loads(value) - except json.JSONDecodeError as exception: - error(f"Invalid {option_name} JSON: {exception.msg}") - if not isinstance(parsed_value, dict): - error(f"{option_name} must be a JSON object") - return parsed_value - - -def _parse_json_array(value: Optional[str], option_name: str): - if value is None: - return None - try: - parsed_value = json.loads(value) - except json.JSONDecodeError as exception: - error(f"Invalid {option_name} JSON: {exception.msg}") - if not isinstance(parsed_value, list): - error(f"{option_name} must be a JSON array") - return parsed_value - - -@app.command("create") -@handle_sdk_error("Create assistant failed") -def create( - model: str = typer.Option(..., "-m", "--model", help="The model to use"), - name: Optional[str] = typer.Option(None, "--name", help="Assistant name"), - description: Optional[str] = typer.Option( - None, - "--description", - help="Assistant description", - ), - instructions: Optional[str] = typer.Option( - None, - "--instructions", - help="Assistant instructions", - ), - tools: Optional[str] = typer.Option( - None, - "--tools", - help="Tools as a JSON array string", - ), - file_ids: Optional[List[str]] = typer.Option( - None, - "--file-id", - help="File id, can be used multiple times", - ), - metadata: Optional[str] = typer.Option( - None, - "--metadata", - help="Metadata as a JSON object string", - ), - workspace: Optional[str] = typer.Option( - None, - "-w", - "--workspace", - help="The DashScope workspace id", - ), - top_p: Optional[float] = typer.Option( - None, - "--top-p", - help="Top-p sampling", - ), - top_k: Optional[int] = typer.Option( - None, - "--top-k", - help="Top-k sampling", - ), - temperature: Optional[float] = typer.Option( - None, - "--temperature", - help="Sampling temperature", - ), - max_tokens: Optional[int] = typer.Option( - None, - "--max-tokens", - help="Maximum output tokens", - ), -): - """Create an assistant.""" - response = dashscope.Assistants.create( - model=model, - name=name, - description=description, - instructions=instructions, - tools=_parse_json_array(tools, "tools"), - file_ids=file_ids or [], - metadata=_parse_json_object(metadata, "metadata"), - workspace=workspace, - top_p=top_p, - top_k=top_k, - temperature=temperature, - max_tokens=max_tokens, - ) - console.print_json( - json.dumps( - response, - default=lambda value: value.__dict__, - ensure_ascii=False, - ), - ) - - -@app.command("list") -@handle_sdk_error("List assistants failed") -def list_assistants( - limit: Optional[int] = typer.Option( - None, - "--limit", - help="Maximum number of assistants", - ), - order: Optional[str] = typer.Option( - None, - "--order", - help="Sort order by created_at", - ), - after: Optional[str] = typer.Option( - None, - "--after", - help="Cursor after assistant id", - ), - before: Optional[str] = typer.Option( - None, - "--before", - help="Cursor before assistant id", - ), - workspace: Optional[str] = typer.Option( - None, - "-w", - "--workspace", - help="The DashScope workspace id", - ), -): - """List assistants.""" - response = dashscope.Assistants.list( - limit=limit, - order=order, - after=after, - before=before, - workspace=workspace, - ) - console.print_json( - json.dumps( - response, - default=lambda value: value.__dict__, - ensure_ascii=False, - ), - ) - - -@app.command("get") -@handle_sdk_error("Retrieve assistant failed") -def get_assistant( - assistant_id: str = typer.Argument(..., help="The assistant id"), - workspace: Optional[str] = typer.Option( - None, - "-w", - "--workspace", - help="The DashScope workspace id", - ), -): - """Retrieve an assistant.""" - response = dashscope.Assistants.retrieve( - assistant_id, - workspace=workspace, - ) - console.print_json( - json.dumps( - response, - default=lambda value: value.__dict__, - ensure_ascii=False, - ), - ) - - -@app.command("update") -@handle_sdk_error("Update assistant failed") -def update_assistant( - assistant_id: str = typer.Argument(..., help="The assistant id"), - model: Optional[str] = typer.Option( - None, - "-m", - "--model", - help="The model to use", - ), - name: Optional[str] = typer.Option(None, "--name", help="Assistant name"), - description: Optional[str] = typer.Option( - None, - "--description", - help="Assistant description", - ), - instructions: Optional[str] = typer.Option( - None, - "--instructions", - help="Assistant instructions", - ), - tools: Optional[str] = typer.Option( - None, - "--tools", - help="Tools as a JSON array string", - ), - file_ids: Optional[List[str]] = typer.Option( - None, - "--file-id", - help="File id, can be used multiple times", - ), - metadata: Optional[str] = typer.Option( - None, - "--metadata", - help="Metadata as a JSON object string", - ), - workspace: Optional[str] = typer.Option( - None, - "-w", - "--workspace", - help="The DashScope workspace id", - ), - top_p: Optional[float] = typer.Option( - None, - "--top-p", - help="Top-p sampling", - ), - top_k: Optional[int] = typer.Option( - None, - "--top-k", - help="Top-k sampling", - ), - temperature: Optional[float] = typer.Option( - None, - "--temperature", - help="Sampling temperature", - ), - max_tokens: Optional[int] = typer.Option( - None, - "--max-tokens", - help="Maximum output tokens", - ), -): - """Update an assistant.""" - response = dashscope.Assistants.update( - assistant_id, - model=model, - name=name, - description=description, - instructions=instructions, - tools=_parse_json_array(tools, "tools"), - file_ids=file_ids, - metadata=_parse_json_object(metadata, "metadata"), - workspace=workspace, - top_p=top_p, - top_k=top_k, - temperature=temperature, - max_tokens=max_tokens, - ) - console.print_json( - json.dumps( - response, - default=lambda value: value.__dict__, - ensure_ascii=False, - ), - ) - - -@app.command("delete") -@handle_sdk_error("Delete assistant failed") -def delete_assistant( - assistant_id: str = typer.Argument(..., help="The assistant id"), - workspace: Optional[str] = typer.Option( - None, - "-w", - "--workspace", - help="The DashScope workspace id", - ), -): - """Delete an assistant.""" - response = dashscope.Assistants.delete( - assistant_id, - workspace=workspace, - ) - console.print_json( - json.dumps( - response, - default=lambda value: value.__dict__, - ensure_ascii=False, - ), - ) diff --git a/dashscope/cli/messages.py b/dashscope/cli/messages.py deleted file mode 100644 index c531e1a..0000000 --- a/dashscope/cli/messages.py +++ /dev/null @@ -1,285 +0,0 @@ -# -*- coding: utf-8 -*- -"""``messages`` sub-command group.""" -import json -from typing import List, Optional - -import typer - -import dashscope -from dashscope.cli.common import console, error, handle_sdk_error -from dashscope.threads.messages.files import Files as MessageFiles - -app = typer.Typer( - name="messages", - help="Thread message management commands", - add_completion=False, - invoke_without_command=True, -) -files_app = typer.Typer( - name="files", - help="Thread message file management commands", - add_completion=False, - invoke_without_command=True, -) -app.add_typer(files_app, name="files") - - -@app.callback() -def callback(ctx: typer.Context): - """Show help if no subcommand is provided.""" - if ctx.invoked_subcommand is None: - typer.echo(ctx.get_help()) - - -@files_app.callback() -def files_callback(ctx: typer.Context): - """Show help if no message file subcommand is provided.""" - if ctx.invoked_subcommand is None: - typer.echo(ctx.get_help()) - - -def _parse_json_object(value: Optional[str], option_name: str): - if value is None: - return None - try: - parsed_value = json.loads(value) - except json.JSONDecodeError as exception: - error(f"Invalid {option_name} JSON: {exception.msg}") - if not isinstance(parsed_value, dict): - error(f"{option_name} must be a JSON object") - return parsed_value - - -@app.command("create") -@handle_sdk_error("Create message failed") -def create( - thread_id: str = typer.Argument(..., help="The thread id"), - content: str = typer.Option( - ..., - "-c", - "--content", - help="Message content", - ), - role: str = typer.Option("user", "--role", help="Message role"), - file_ids: Optional[List[str]] = typer.Option( - None, - "--file-id", - help="File id, can be used multiple times", - ), - metadata: Optional[str] = typer.Option( - None, - "--metadata", - help="Metadata as a JSON object string", - ), - workspace: Optional[str] = typer.Option( - None, - "-w", - "--workspace", - help="The DashScope workspace id", - ), -): - """Create a thread message.""" - response = dashscope.Messages.create( - thread_id, - content=content, - role=role, - file_ids=file_ids or [], - metadata=_parse_json_object(metadata, "metadata"), - workspace=workspace, - ) - console.print_json( - json.dumps( - response, - default=lambda value: value.__dict__, - ensure_ascii=False, - ), - ) - - -@app.command("list") -@handle_sdk_error("List messages failed") -def list_messages( - thread_id: str = typer.Argument(..., help="The thread id"), - limit: Optional[int] = typer.Option( - None, - "--limit", - help="Maximum number of messages", - ), - order: Optional[str] = typer.Option( - None, - "--order", - help="Sort order by created_at", - ), - after: Optional[str] = typer.Option( - None, - "--after", - help="Cursor after message id", - ), - before: Optional[str] = typer.Option( - None, - "--before", - help="Cursor before message id", - ), - workspace: Optional[str] = typer.Option( - None, - "-w", - "--workspace", - help="The DashScope workspace id", - ), -): - """List thread messages.""" - response = dashscope.Messages.list( - thread_id, - limit=limit, - order=order, - after=after, - before=before, - workspace=workspace, - ) - console.print_json( - json.dumps( - response, - default=lambda value: value.__dict__, - ensure_ascii=False, - ), - ) - - -@app.command("get") -@handle_sdk_error("Retrieve message failed") -def get_message( - thread_id: str = typer.Argument(..., help="The thread id"), - message_id: str = typer.Argument(..., help="The message id"), - workspace: Optional[str] = typer.Option( - None, - "-w", - "--workspace", - help="The DashScope workspace id", - ), -): - """Retrieve a thread message.""" - response = dashscope.Messages.retrieve( - message_id, - thread_id=thread_id, - workspace=workspace, - ) - console.print_json( - json.dumps( - response, - default=lambda value: value.__dict__, - ensure_ascii=False, - ), - ) - - -@app.command("update") -@handle_sdk_error("Update message failed") -def update_message( - thread_id: str = typer.Argument(..., help="The thread id"), - message_id: str = typer.Argument(..., help="The message id"), - metadata: Optional[str] = typer.Option( - None, - "--metadata", - help="Metadata as a JSON object string", - ), - workspace: Optional[str] = typer.Option( - None, - "-w", - "--workspace", - help="The DashScope workspace id", - ), -): - """Update a thread message.""" - response = dashscope.Messages.update( - message_id, - thread_id=thread_id, - metadata=_parse_json_object(metadata, "metadata"), - workspace=workspace, - ) - console.print_json( - json.dumps( - response, - default=lambda value: value.__dict__, - ensure_ascii=False, - ), - ) - - -@files_app.command("list") -@handle_sdk_error("List message files failed") -def list_message_files( - thread_id: str = typer.Argument(..., help="The thread id"), - message_id: str = typer.Argument(..., help="The message id"), - limit: Optional[int] = typer.Option( - None, - "--limit", - help="Maximum number of files", - ), - order: Optional[str] = typer.Option( - None, - "--order", - help="Sort order by created_at", - ), - after: Optional[str] = typer.Option( - None, - "--after", - help="Cursor after file id", - ), - before: Optional[str] = typer.Option( - None, - "--before", - help="Cursor before file id", - ), - workspace: Optional[str] = typer.Option( - None, - "-w", - "--workspace", - help="The DashScope workspace id", - ), -): - """List thread message files.""" - response = MessageFiles.list( - message_id, - thread_id=thread_id, - limit=limit, - order=order, - after=after, - before=before, - workspace=workspace, - ) - console.print_json( - json.dumps( - response, - default=lambda value: value.__dict__, - ensure_ascii=False, - ), - ) - - -@files_app.command("get") -@handle_sdk_error("Retrieve message file failed") -def get_message_file( - thread_id: str = typer.Argument(..., help="The thread id"), - message_id: str = typer.Argument(..., help="The message id"), - file_id: str = typer.Argument(..., help="The file id"), - workspace: Optional[str] = typer.Option( - None, - "-w", - "--workspace", - help="The DashScope workspace id", - ), -): - """Retrieve a thread message file.""" - response = MessageFiles.retrieve( - file_id, - thread_id=thread_id, - message_id=message_id, - workspace=workspace, - ) - console.print_json( - json.dumps( - response, - default=lambda value: value.__dict__, - ensure_ascii=False, - ), - ) diff --git a/dashscope/cli/runs.py b/dashscope/cli/runs.py deleted file mode 100644 index 971c7e1..0000000 --- a/dashscope/cli/runs.py +++ /dev/null @@ -1,367 +0,0 @@ -# -*- coding: utf-8 -*- -"""``runs`` sub-command group.""" -import json -from typing import Optional - -import typer - -import dashscope -from dashscope.cli.common import console, error, handle_sdk_error - -app = typer.Typer( - name="runs", - help="Thread run management commands", - add_completion=False, - invoke_without_command=True, -) - - -@app.callback() -def callback(ctx: typer.Context): - """Show help if no subcommand is provided.""" - if ctx.invoked_subcommand is None: - typer.echo(ctx.get_help()) - - -def _parse_json_object(value: Optional[str], option_name: str): - if value is None: - return None - try: - parsed_value = json.loads(value) - except json.JSONDecodeError as exception: - error(f"Invalid {option_name} JSON: {exception.msg}") - if not isinstance(parsed_value, dict): - error(f"{option_name} must be a JSON object") - return parsed_value - - -def _parse_json_array(value: Optional[str], option_name: str): - if value is None: - return None - try: - parsed_value = json.loads(value) - except json.JSONDecodeError as exception: - error(f"Invalid {option_name} JSON: {exception.msg}") - if not isinstance(parsed_value, list): - error(f"{option_name} must be a JSON array") - return parsed_value - - -@app.command("create") -@handle_sdk_error("Create run failed") -def create( - thread_id: str = typer.Argument(..., help="The thread id"), - assistant_id: str = typer.Option( - ..., - "--assistant-id", - help="The assistant id to run", - ), - model: Optional[str] = typer.Option( - None, - "-m", - "--model", - help="The model to use", - ), - instructions: Optional[str] = typer.Option( - None, - "--instructions", - help="Run instructions", - ), - additional_instructions: Optional[str] = typer.Option( - None, - "--additional-instructions", - help="Additional instructions appended for this run", - ), - tools: Optional[str] = typer.Option( - None, - "--tools", - help="Tools as a JSON array string", - ), - metadata: Optional[str] = typer.Option( - None, - "--metadata", - help="Metadata as a JSON object string", - ), - extra_body: Optional[str] = typer.Option( - None, - "--extra-body", - help="Extra body as a JSON object string", - ), - workspace: Optional[str] = typer.Option( - None, - "-w", - "--workspace", - help="The DashScope workspace id", - ), - top_p: Optional[float] = typer.Option( - None, - "--top-p", - help="Top-p sampling", - ), - top_k: Optional[int] = typer.Option( - None, - "--top-k", - help="Top-k sampling", - ), - temperature: Optional[float] = typer.Option( - None, - "--temperature", - help="Sampling temperature", - ), - max_tokens: Optional[int] = typer.Option( - None, - "--max-tokens", - help="Maximum output tokens", - ), -): - """Create a run.""" - response = dashscope.Runs.create( - thread_id, - assistant_id=assistant_id, - model=model, - instructions=instructions, - additional_instructions=additional_instructions, - tools=_parse_json_array(tools, "tools"), - metadata=_parse_json_object(metadata, "metadata"), - extra_body=_parse_json_object(extra_body, "extra_body"), - workspace=workspace, - top_p=top_p, - top_k=top_k, - temperature=temperature, - max_tokens=max_tokens, - ) - console.print_json( - json.dumps( - response, - default=lambda value: value.__dict__, - ensure_ascii=False, - ), - ) - - -@app.command("get") -@handle_sdk_error("Retrieve run failed") -def get_run( - run_id: str = typer.Argument(..., help="The run id"), - thread_id: str = typer.Option( - ..., - "--thread-id", - help="The thread id", - ), - workspace: Optional[str] = typer.Option( - None, - "-w", - "--workspace", - help="The DashScope workspace id", - ), -): - """Retrieve a run.""" - response = dashscope.Runs.retrieve( - run_id, - thread_id=thread_id, - workspace=workspace, - ) - console.print_json( - json.dumps( - response, - default=lambda value: value.__dict__, - ensure_ascii=False, - ), - ) - - -@app.command("list") -@handle_sdk_error("List runs failed") -def list_runs( - thread_id: str = typer.Argument(..., help="The thread id"), - limit: Optional[int] = typer.Option( - None, - "--limit", - help="Maximum number of runs", - ), - order: Optional[str] = typer.Option( - None, - "--order", - help="Sort order by created_at", - ), - after: Optional[str] = typer.Option( - None, - "--after", - help="Cursor after run id", - ), - before: Optional[str] = typer.Option( - None, - "--before", - help="Cursor before run id", - ), - workspace: Optional[str] = typer.Option( - None, - "-w", - "--workspace", - help="The DashScope workspace id", - ), -): - """List runs of a thread.""" - response = dashscope.Runs.list( - thread_id, - limit=limit, - order=order, - after=after, - before=before, - workspace=workspace, - ) - console.print_json( - json.dumps( - response, - default=lambda value: value.__dict__, - ensure_ascii=False, - ), - ) - - -@app.command("submit-tool-outputs") -@handle_sdk_error("Submit tool outputs failed") -def submit_tool_outputs( - run_id: str = typer.Argument(..., help="The run id"), - thread_id: str = typer.Option( - ..., - "--thread-id", - help="The thread id", - ), - tool_outputs: str = typer.Option( - ..., - "--tool-outputs", - help="Tool outputs as a JSON array string", - ), - extra_body: Optional[str] = typer.Option( - None, - "--extra-body", - help="Extra body as a JSON object string", - ), - workspace: Optional[str] = typer.Option( - None, - "-w", - "--workspace", - help="The DashScope workspace id", - ), -): - """Submit tool outputs to a run.""" - response = dashscope.Runs.submit_tool_outputs( - run_id, - thread_id=thread_id, - tool_outputs=_parse_json_array(tool_outputs, "tool_outputs"), - extra_body=_parse_json_object(extra_body, "extra_body"), - workspace=workspace, - ) - console.print_json( - json.dumps( - response, - default=lambda value: value.__dict__, - ensure_ascii=False, - ), - ) - - -@app.command("wait") -@handle_sdk_error("Wait run failed") -def wait_run( - run_id: str = typer.Argument(..., help="The run id"), - thread_id: str = typer.Option( - ..., - "--thread-id", - help="The thread id", - ), - timeout_seconds: float = typer.Option( - float("inf"), - "--timeout-seconds", - help="Maximum seconds to wait", - ), - workspace: Optional[str] = typer.Option( - None, - "-w", - "--workspace", - help="The DashScope workspace id", - ), -): - """Wait for a run to reach a terminal status.""" - response = dashscope.Runs.wait( - run_id, - thread_id=thread_id, - timeout_seconds=timeout_seconds, - workspace=workspace, - ) - console.print_json( - json.dumps( - response, - default=lambda value: value.__dict__, - ensure_ascii=False, - ), - ) - - -@app.command("update") -@handle_sdk_error("Update run failed") -def update_run( - run_id: str = typer.Argument(..., help="The run id"), - thread_id: str = typer.Option( - ..., - "--thread-id", - help="The thread id", - ), - metadata: Optional[str] = typer.Option( - None, - "--metadata", - help="Metadata as a JSON object string", - ), - workspace: Optional[str] = typer.Option( - None, - "-w", - "--workspace", - help="The DashScope workspace id", - ), -): - """Update a run.""" - response = dashscope.Runs.update( - run_id, - thread_id=thread_id, - metadata=_parse_json_object(metadata, "metadata"), - workspace=workspace, - ) - console.print_json( - json.dumps( - response, - default=lambda value: value.__dict__, - ensure_ascii=False, - ), - ) - - -@app.command("cancel") -@handle_sdk_error("Cancel run failed") -def cancel_run( - run_id: str = typer.Argument(..., help="The run id"), - thread_id: str = typer.Option( - ..., - "--thread-id", - help="The thread id", - ), - workspace: Optional[str] = typer.Option( - None, - "-w", - "--workspace", - help="The DashScope workspace id", - ), -): - """Cancel a run.""" - response = dashscope.Runs.cancel( - run_id, - thread_id=thread_id, - workspace=workspace, - ) - console.print_json( - json.dumps( - response, - default=lambda value: value.__dict__, - ensure_ascii=False, - ), - ) diff --git a/dashscope/cli/steps.py b/dashscope/cli/steps.py deleted file mode 100644 index d9ae70b..0000000 --- a/dashscope/cli/steps.py +++ /dev/null @@ -1,115 +0,0 @@ -# -*- coding: utf-8 -*- -"""``steps`` sub-command group.""" -import json -from typing import Optional - -import typer - -import dashscope -from dashscope.cli.common import console, handle_sdk_error - -app = typer.Typer( - name="steps", - help="Run step management commands", - add_completion=False, - invoke_without_command=True, -) - - -@app.callback() -def callback(ctx: typer.Context): - """Show help if no subcommand is provided.""" - if ctx.invoked_subcommand is None: - typer.echo(ctx.get_help()) - - -@app.command("list") -@handle_sdk_error("List run steps failed") -def list_steps( - run_id: str = typer.Argument(..., help="The run id"), - thread_id: str = typer.Option( - ..., - "--thread-id", - help="The thread id", - ), - limit: Optional[int] = typer.Option( - None, - "--limit", - help="Maximum number of steps", - ), - order: Optional[str] = typer.Option( - None, - "--order", - help="Sort order by created_at", - ), - after: Optional[str] = typer.Option( - None, - "--after", - help="Cursor after step id", - ), - before: Optional[str] = typer.Option( - None, - "--before", - help="Cursor before step id", - ), - workspace: Optional[str] = typer.Option( - None, - "-w", - "--workspace", - help="The DashScope workspace id", - ), -): - """List run steps.""" - response = dashscope.Steps.list( - run_id, - thread_id=thread_id, - limit=limit, - order=order, - after=after, - before=before, - workspace=workspace, - ) - console.print_json( - json.dumps( - response, - default=lambda value: value.__dict__, - ensure_ascii=False, - ), - ) - - -@app.command("get") -@handle_sdk_error("Retrieve run step failed") -def get_step( - step_id: str = typer.Argument(..., help="The step id"), - thread_id: str = typer.Option( - ..., - "--thread-id", - help="The thread id", - ), - run_id: str = typer.Option( - ..., - "--run-id", - help="The run id", - ), - workspace: Optional[str] = typer.Option( - None, - "-w", - "--workspace", - help="The DashScope workspace id", - ), -): - """Retrieve a run step.""" - response = dashscope.Steps.retrieve( - step_id, - thread_id=thread_id, - run_id=run_id, - workspace=workspace, - ) - console.print_json( - json.dumps( - response, - default=lambda value: value.__dict__, - ensure_ascii=False, - ), - ) diff --git a/dashscope/cli/threads.py b/dashscope/cli/threads.py deleted file mode 100644 index 55ce072..0000000 --- a/dashscope/cli/threads.py +++ /dev/null @@ -1,229 +0,0 @@ -# -*- coding: utf-8 -*- -"""``threads`` sub-command group.""" -import json -from typing import Optional - -import typer - -import dashscope -from dashscope.cli.common import console, error, handle_sdk_error - -app = typer.Typer( - name="threads", - help="Thread management commands", - add_completion=False, - invoke_without_command=True, -) - - -@app.callback() -def callback(ctx: typer.Context): - """Show help if no subcommand is provided.""" - if ctx.invoked_subcommand is None: - typer.echo(ctx.get_help()) - - -def _parse_json_object(value: Optional[str], option_name: str): - if value is None: - return None - try: - parsed_value = json.loads(value) - except json.JSONDecodeError as exception: - error(f"Invalid {option_name} JSON: {exception.msg}") - if not isinstance(parsed_value, dict): - error(f"{option_name} must be a JSON object") - return parsed_value - - -def _parse_json_array(value: Optional[str], option_name: str): - if value is None: - return None - try: - parsed_value = json.loads(value) - except json.JSONDecodeError as exception: - error(f"Invalid {option_name} JSON: {exception.msg}") - if not isinstance(parsed_value, list): - error(f"{option_name} must be a JSON array") - return parsed_value - - -@app.command("create") -@handle_sdk_error("Create thread failed") -def create( - messages: Optional[str] = typer.Option( - None, - "--messages", - help="Initial messages as a JSON array string", - ), - metadata: Optional[str] = typer.Option( - None, - "--metadata", - help="Metadata as a JSON object string", - ), - workspace: Optional[str] = typer.Option( - None, - "-w", - "--workspace", - help="The DashScope workspace id", - ), -): - """Create a thread.""" - response = dashscope.Threads.create( - messages=_parse_json_array(messages, "messages"), - metadata=_parse_json_object(metadata, "metadata"), - workspace=workspace, - ) - console.print_json( - json.dumps( - response, - default=lambda value: value.__dict__, - ensure_ascii=False, - ), - ) - - -@app.command("update") -@handle_sdk_error("Update thread failed") -def update_thread( - thread_id: str = typer.Argument(..., help="The thread id"), - metadata: Optional[str] = typer.Option( - None, - "--metadata", - help="Metadata as a JSON object string", - ), - workspace: Optional[str] = typer.Option( - None, - "-w", - "--workspace", - help="The DashScope workspace id", - ), -): - """Update a thread.""" - response = dashscope.Threads.update( - thread_id, - metadata=_parse_json_object(metadata, "metadata"), - workspace=workspace, - ) - console.print_json( - json.dumps( - response, - default=lambda value: value.__dict__, - ensure_ascii=False, - ), - ) - - -@app.command("get") -@handle_sdk_error("Retrieve thread failed") -def get_thread( - thread_id: str = typer.Argument(..., help="The thread id"), - workspace: Optional[str] = typer.Option( - None, - "-w", - "--workspace", - help="The DashScope workspace id", - ), -): - """Retrieve a thread.""" - response = dashscope.Threads.retrieve( - thread_id, - workspace=workspace, - ) - console.print_json( - json.dumps( - response, - default=lambda value: value.__dict__, - ensure_ascii=False, - ), - ) - - -@app.command("delete") -@handle_sdk_error("Delete thread failed") -def delete_thread( - thread_id: str = typer.Argument(..., help="The thread id"), - workspace: Optional[str] = typer.Option( - None, - "-w", - "--workspace", - help="The DashScope workspace id", - ), -): - """Delete a thread.""" - response = dashscope.Threads.delete( - thread_id, - workspace=workspace, - ) - console.print_json( - json.dumps( - response, - default=lambda value: value.__dict__, - ensure_ascii=False, - ), - ) - - -@app.command("create-and-run") -@handle_sdk_error("Create thread and run failed") -def create_and_run( - assistant_id: str = typer.Option( - ..., - "--assistant-id", - help="The assistant id", - ), - thread: Optional[str] = typer.Option( - None, - "--thread", - help="Thread as a JSON object string", - ), - model: Optional[str] = typer.Option( - None, - "-m", - "--model", - help="The model to use", - ), - instructions: Optional[str] = typer.Option( - None, - "--instructions", - help="Run instructions", - ), - additional_instructions: Optional[str] = typer.Option( - None, - "--additional-instructions", - help="Additional run instructions", - ), - tools: Optional[str] = typer.Option( - None, - "--tools", - help="Tools as a JSON array string", - ), - metadata: Optional[str] = typer.Option( - None, - "--metadata", - help="Metadata as a JSON object string", - ), - workspace: Optional[str] = typer.Option( - None, - "-w", - "--workspace", - help="The DashScope workspace id", - ), -): - """Create a thread and run it.""" - response = dashscope.Threads.create_and_run( - assistant_id=assistant_id, - thread=_parse_json_object(thread, "thread"), - model=model, - instructions=instructions, - additional_instructions=additional_instructions, - tools=_parse_json_array(tools, "tools"), - metadata=_parse_json_object(metadata, "metadata"), - workspace=workspace, - ) - console.print_json( - json.dumps( - response, - default=lambda value: value.__dict__, - ensure_ascii=False, - ), - ) diff --git a/dashscope/version.py b/dashscope/version.py index fbaaca6..4ddaef7 100644 --- a/dashscope/version.py +++ b/dashscope/version.py @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -__version__ = "1.25.21" +__version__ = "1.26.0" diff --git a/tests/unit/test_cli_assistant_files.py b/tests/unit/test_cli_assistant_files.py deleted file mode 100644 index 00c7704..0000000 --- a/tests/unit/test_cli_assistant_files.py +++ /dev/null @@ -1,192 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Alibaba, Inc. and its affiliates. - -from types import SimpleNamespace - -from typer.testing import CliRunner - -from dashscope.cli import assistant_files - - -class TestCliAssistantFiles: - def test_create(self, monkeypatch): - captured_assistant_id = None - captured_request = {} - - def mock_create(assistant_id, **kwargs): - nonlocal captured_assistant_id - captured_assistant_id = assistant_id - captured_request.update(kwargs) - return SimpleNamespace( - id=kwargs["file_id"], - object="assistant.file", - created_at=111111, - assistant_id=assistant_id, - ) - - monkeypatch.setattr( - assistant_files.Files, - "create", - mock_create, - ) - - result = CliRunner().invoke( - assistant_files.app, - [ - "create", - "asst-1234", - "--file-id", - "file-1234", - "--workspace", - "workspace-id", - ], - ) - - assert result.exit_code == 0 - assert captured_assistant_id == "asst-1234" - assert captured_request == { - "file_id": "file-1234", - "workspace": "workspace-id", - } - assert "file-1234" in result.output - assert "asst-1234" in result.output - - def test_get(self, monkeypatch): - captured_file_id = None - captured_request = {} - - def mock_retrieve(file_id, **kwargs): - nonlocal captured_file_id - captured_file_id = file_id - captured_request.update(kwargs) - return SimpleNamespace( - id=file_id, - object="assistant.file", - created_at=111111, - assistant_id=kwargs["assistant_id"], - ) - - monkeypatch.setattr( - assistant_files.Files, - "retrieve", - mock_retrieve, - ) - - result = CliRunner().invoke( - assistant_files.app, - [ - "get", - "asst-1234", - "file-1234", - "--workspace", - "workspace-id", - ], - ) - - assert result.exit_code == 0 - assert captured_file_id == "file-1234" - assert captured_request == { - "assistant_id": "asst-1234", - "workspace": "workspace-id", - } - assert "file-1234" in result.output - assert "asst-1234" in result.output - - def test_delete(self, monkeypatch): - captured_file_id = None - captured_request = {} - - def mock_delete(file_id, **kwargs): - nonlocal captured_file_id - captured_file_id = file_id - captured_request.update(kwargs) - return SimpleNamespace( - id=file_id, - object="assistant.file", - created_at=111111, - deleted=True, - ) - - monkeypatch.setattr( - assistant_files.Files, - "delete", - mock_delete, - ) - - result = CliRunner().invoke( - assistant_files.app, - [ - "delete", - "asst-1234", - "file-1234", - "--workspace", - "workspace-id", - ], - ) - - assert result.exit_code == 0 - assert captured_file_id == "file-1234" - assert captured_request == { - "assistant_id": "asst-1234", - "workspace": "workspace-id", - } - assert "file-1234" in result.output - assert "true" in result.output - - def test_list(self, monkeypatch): - captured_assistant_id = None - captured_request = {} - - def mock_list(assistant_id, **kwargs): - nonlocal captured_assistant_id - captured_assistant_id = assistant_id - captured_request.update(kwargs) - return SimpleNamespace( - first_id="file-1234", - last_id="file-5678", - has_more=False, - data=[ - SimpleNamespace( - id="file-1234", - object="assistant.file", - created_at=111111, - assistant_id=assistant_id, - ), - ], - ) - - monkeypatch.setattr( - assistant_files.Files, - "list", - mock_list, - ) - - result = CliRunner().invoke( - assistant_files.app, - [ - "list", - "asst-1234", - "--limit", - "10", - "--order", - "asc", - "--after", - "file-0001", - "--before", - "file-9999", - "--workspace", - "workspace-id", - ], - ) - - assert result.exit_code == 0 - assert captured_assistant_id == "asst-1234" - assert captured_request == { - "limit": 10, - "order": "asc", - "after": "file-0001", - "before": "file-9999", - "workspace": "workspace-id", - } - assert "file-1234" in result.output - assert "asst-1234" in result.output diff --git a/tests/unit/test_cli_assistants.py b/tests/unit/test_cli_assistants.py deleted file mode 100644 index 642dd5f..0000000 --- a/tests/unit/test_cli_assistants.py +++ /dev/null @@ -1,282 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Alibaba, Inc. and its affiliates. - -from types import SimpleNamespace - -from typer.testing import CliRunner - -from dashscope.cli import assistants - - -class TestCliAssistants: - def test_create(self, monkeypatch): - captured_request = {} - - def mock_create(**kwargs): - captured_request.update(kwargs) - return SimpleNamespace( - id="asst-1234", - object="assistant", - model=kwargs["model"], - name=kwargs["name"], - description=kwargs["description"], - instructions=kwargs["instructions"], - tools=kwargs["tools"], - file_ids=kwargs["file_ids"], - metadata=kwargs["metadata"], - ) - - monkeypatch.setattr( - assistants.dashscope.Assistants, - "create", - mock_create, - ) - - result = CliRunner().invoke( - assistants.app, - [ - "create", - "--model", - "qwen-max", - "--name", - "smart helper", - "--description", - "A tool helper.", - "--instructions", - "You are a helpful assistant.", - "--tools", - '[{"type":"search"},{"type":"wanx"}]', - "--file-id", - "file-1", - "--file-id", - "file-2", - "--metadata", - '{"key":"value"}', - "--workspace", - "workspace-id", - "--top-p", - "0.9", - "--top-k", - "50", - "--temperature", - "0.8", - "--max-tokens", - "1024", - ], - ) - - assert result.exit_code == 0 - assert captured_request == { - "model": "qwen-max", - "name": "smart helper", - "description": "A tool helper.", - "instructions": "You are a helpful assistant.", - "tools": [{"type": "search"}, {"type": "wanx"}], - "file_ids": ["file-1", "file-2"], - "metadata": {"key": "value"}, - "workspace": "workspace-id", - "top_p": 0.9, - "top_k": 50, - "temperature": 0.8, - "max_tokens": 1024, - } - assert "asst-1234" in result.output - assert "smart helper" in result.output - - def test_get(self, monkeypatch): - captured_assistant_id = None - captured_request = {} - - def mock_retrieve(assistant_id, **kwargs): - nonlocal captured_assistant_id - captured_assistant_id = assistant_id - captured_request.update(kwargs) - return SimpleNamespace( - id="asst-1234", - object="assistant", - model="qwen-max", - name="smart helper", - instructions="You are a helpful assistant.", - ) - - monkeypatch.setattr( - assistants.dashscope.Assistants, - "retrieve", - mock_retrieve, - ) - - result = CliRunner().invoke( - assistants.app, - [ - "get", - "asst-1234", - "--workspace", - "workspace-id", - ], - ) - - assert result.exit_code == 0 - assert captured_assistant_id == "asst-1234" - assert captured_request == {"workspace": "workspace-id"} - assert "asst-1234" in result.output - assert "qwen-max" in result.output - - def test_delete(self, monkeypatch): - captured_assistant_id = None - captured_request = {} - - def mock_delete(assistant_id, **kwargs): - nonlocal captured_assistant_id - captured_assistant_id = assistant_id - captured_request.update(kwargs) - return SimpleNamespace( - id=assistant_id, - object="assistant.deleted", - deleted=True, - ) - - monkeypatch.setattr( - assistants.dashscope.Assistants, - "delete", - mock_delete, - ) - - result = CliRunner().invoke( - assistants.app, - [ - "delete", - "asst-1234", - "--workspace", - "workspace-id", - ], - ) - - assert result.exit_code == 0 - assert captured_assistant_id == "asst-1234" - assert captured_request == {"workspace": "workspace-id"} - assert "assistant.deleted" in result.output - assert "true" in result.output - - def test_update(self, monkeypatch): - captured_assistant_id = None - captured_request = {} - - def mock_update(assistant_id, **kwargs): - nonlocal captured_assistant_id - captured_assistant_id = assistant_id - captured_request.update(kwargs) - return SimpleNamespace( - id=assistant_id, - object="assistant", - model=kwargs["model"], - name=kwargs["name"], - description=kwargs["description"], - instructions=kwargs["instructions"], - tools=kwargs["tools"], - file_ids=kwargs["file_ids"], - metadata=kwargs["metadata"], - ) - - monkeypatch.setattr( - assistants.dashscope.Assistants, - "update", - mock_update, - ) - - result = CliRunner().invoke( - assistants.app, - [ - "update", - "asst-1234", - "--model", - "qwen-max", - "--name", - "smart helper", - "--description", - "Updated description.", - "--instructions", - "You are a helpful assistant.", - "--tools", - '[{"type":"search"}]', - "--file-id", - "file-1", - "--metadata", - '{"key":"value"}', - "--workspace", - "workspace-id", - "--top-p", - "0.9", - "--top-k", - "50", - "--temperature", - "0.8", - "--max-tokens", - "1024", - ], - ) - - assert result.exit_code == 0 - assert captured_assistant_id == "asst-1234" - assert captured_request == { - "model": "qwen-max", - "name": "smart helper", - "description": "Updated description.", - "instructions": "You are a helpful assistant.", - "tools": [{"type": "search"}], - "file_ids": ["file-1"], - "metadata": {"key": "value"}, - "workspace": "workspace-id", - "top_p": 0.9, - "top_k": 50, - "temperature": 0.8, - "max_tokens": 1024, - } - assert "asst-1234" in result.output - assert "Updated description" in result.output - - def test_list(self, monkeypatch): - captured_request = {} - - def mock_list(**kwargs): - captured_request.update(kwargs) - return SimpleNamespace( - object="list", - data=[SimpleNamespace(id="asst-1234", model="qwen-max")], - first_id="asst-1234", - last_id="asst-1234", - has_more=False, - ) - - monkeypatch.setattr( - assistants.dashscope.Assistants, - "list", - mock_list, - ) - - result = CliRunner().invoke( - assistants.app, - [ - "list", - "--limit", - "10", - "--order", - "asc", - "--after", - "asst-0001", - "--before", - "asst-9999", - "--workspace", - "workspace-id", - ], - ) - - assert result.exit_code == 0 - assert captured_request == { - "limit": 10, - "order": "asc", - "after": "asst-0001", - "before": "asst-9999", - "workspace": "workspace-id", - } - assert "asst-1234" in result.output - assert "qwen-max" in result.output diff --git a/tests/unit/test_cli_main.py b/tests/unit/test_cli_main.py index 0501f4f..6999fc3 100644 --- a/tests/unit/test_cli_main.py +++ b/tests/unit/test_cli_main.py @@ -567,21 +567,6 @@ def test_agentic_rl_upload_data_missing_file_exits_before_sdk_call(self): assert "Dataset upload failed" not in result.output assert "Traceback" not in result.output - def test_sdk_exception_is_printed_without_traceback(self, monkeypatch): - def mock_list(**kwargs): - raise RuntimeError("sdk failed") - - monkeypatch.setattr( - "dashscope.cli.assistants.dashscope.Assistants.list", - mock_list, - ) - - result = CliRunner().invoke(cli_app, ["assistants", "list"]) - - assert result.exit_code == 1 - assert "List assistants failed: sdk failed" in result.output - assert "Traceback" not in result.output - def test_generation_stream_exception_is_printed_without_traceback( self, monkeypatch, diff --git a/tests/unit/test_cli_messages.py b/tests/unit/test_cli_messages.py deleted file mode 100644 index aa12d2e..0000000 --- a/tests/unit/test_cli_messages.py +++ /dev/null @@ -1,315 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Alibaba, Inc. and its affiliates. - -from types import SimpleNamespace - -from typer.testing import CliRunner - -from dashscope.cli import messages - - -class TestCliMessages: - def test_create(self, monkeypatch): - captured_thread_id = None - captured_request = {} - - def mock_create(thread_id, **kwargs): - nonlocal captured_thread_id - captured_thread_id = thread_id - captured_request.update(kwargs) - return SimpleNamespace( - id="msg-1234", - object="thread.message", - thread_id=thread_id, - role=kwargs["role"], - content=kwargs["content"], - file_ids=kwargs["file_ids"], - metadata=kwargs["metadata"], - ) - - monkeypatch.setattr( - messages.dashscope.Messages, - "create", - mock_create, - ) - - result = CliRunner().invoke( - messages.app, - [ - "create", - "thread-1234", - "--content", - "ๅฆ‚ไฝ•ๅšๅ‡บ็พŽๅ‘ณ็š„็‰›่‚‰็‚–ๅœŸ่ฑ†๏ผŸ", - "--role", - "user", - "--file-id", - "file-1", - "--file-id", - "file-2", - "--metadata", - '{"key":"value"}', - "--workspace", - "workspace-id", - ], - ) - - assert result.exit_code == 0 - assert captured_thread_id == "thread-1234" - assert captured_request == { - "content": "ๅฆ‚ไฝ•ๅšๅ‡บ็พŽๅ‘ณ็š„็‰›่‚‰็‚–ๅœŸ่ฑ†๏ผŸ", - "role": "user", - "file_ids": ["file-1", "file-2"], - "metadata": {"key": "value"}, - "workspace": "workspace-id", - } - assert "msg-1234" in result.output - assert "thread-1234" in result.output - - def test_list(self, monkeypatch): - captured_thread_id = None - captured_request = {} - - def mock_list(thread_id, **kwargs): - nonlocal captured_thread_id - captured_thread_id = thread_id - captured_request.update(kwargs) - return SimpleNamespace( - first_id="msg-1234", - last_id="msg-5678", - has_more=False, - data=[ - SimpleNamespace( - id="msg-1234", - object="thread.message", - thread_id=thread_id, - role="user", - ), - ], - ) - - monkeypatch.setattr( - messages.dashscope.Messages, - "list", - mock_list, - ) - - result = CliRunner().invoke( - messages.app, - [ - "list", - "thread-1234", - "--limit", - "10", - "--order", - "asc", - "--after", - "msg-0001", - "--before", - "msg-9999", - "--workspace", - "workspace-id", - ], - ) - - assert result.exit_code == 0 - assert captured_thread_id == "thread-1234" - assert captured_request == { - "limit": 10, - "order": "asc", - "after": "msg-0001", - "before": "msg-9999", - "workspace": "workspace-id", - } - assert "msg-1234" in result.output - assert "thread-1234" in result.output - - def test_get(self, monkeypatch): - captured_message_id = None - captured_request = {} - - def mock_retrieve(message_id, **kwargs): - nonlocal captured_message_id - captured_message_id = message_id - captured_request.update(kwargs) - return SimpleNamespace( - id=message_id, - object="thread.message", - thread_id=kwargs["thread_id"], - role="user", - content="hello", - metadata={}, - ) - - monkeypatch.setattr( - messages.dashscope.Messages, - "retrieve", - mock_retrieve, - ) - - result = CliRunner().invoke( - messages.app, - [ - "get", - "thread-1234", - "msg-1234", - "--workspace", - "workspace-id", - ], - ) - - assert result.exit_code == 0 - assert captured_message_id == "msg-1234" - assert captured_request == { - "thread_id": "thread-1234", - "workspace": "workspace-id", - } - assert "msg-1234" in result.output - assert "thread-1234" in result.output - - def test_update(self, monkeypatch): - captured_message_id = None - captured_request = {} - - def mock_update(message_id, **kwargs): - nonlocal captured_message_id - captured_message_id = message_id - captured_request.update(kwargs) - return SimpleNamespace( - id=message_id, - object="thread.message", - thread_id=kwargs["thread_id"], - role="user", - metadata=kwargs["metadata"], - ) - - monkeypatch.setattr( - messages.dashscope.Messages, - "update", - mock_update, - ) - - result = CliRunner().invoke( - messages.app, - [ - "update", - "thread-1234", - "msg-1234", - "--metadata", - '{"key":"value"}', - "--workspace", - "workspace-id", - ], - ) - - assert result.exit_code == 0 - assert captured_message_id == "msg-1234" - assert captured_request == { - "thread_id": "thread-1234", - "metadata": {"key": "value"}, - "workspace": "workspace-id", - } - assert "msg-1234" in result.output - assert "key" in result.output - - def test_list_files(self, monkeypatch): - captured_message_id = None - captured_request = {} - - def mock_list(message_id, **kwargs): - nonlocal captured_message_id - captured_message_id = message_id - captured_request.update(kwargs) - return SimpleNamespace( - first_id="file-1234", - last_id="file-5678", - has_more=False, - data=[ - SimpleNamespace( - id="file-1234", - object="thread.message.file", - message_id=message_id, - ), - ], - ) - - monkeypatch.setattr( - messages.MessageFiles, - "list", - mock_list, - ) - - result = CliRunner().invoke( - messages.app, - [ - "files", - "list", - "thread-1234", - "msg-1234", - "--limit", - "10", - "--order", - "asc", - "--after", - "file-0001", - "--before", - "file-9999", - "--workspace", - "workspace-id", - ], - ) - - assert result.exit_code == 0 - assert captured_message_id == "msg-1234" - assert captured_request == { - "thread_id": "thread-1234", - "limit": 10, - "order": "asc", - "after": "file-0001", - "before": "file-9999", - "workspace": "workspace-id", - } - assert "file-1234" in result.output - assert "msg-1234" in result.output - - def test_get_file(self, monkeypatch): - captured_file_id = None - captured_request = {} - - def mock_retrieve(file_id, **kwargs): - nonlocal captured_file_id - captured_file_id = file_id - captured_request.update(kwargs) - return SimpleNamespace( - id=file_id, - object="thread.message.file", - message_id=kwargs["message_id"], - ) - - monkeypatch.setattr( - messages.MessageFiles, - "retrieve", - mock_retrieve, - ) - - result = CliRunner().invoke( - messages.app, - [ - "files", - "get", - "thread-1234", - "msg-1234", - "file-1234", - "--workspace", - "workspace-id", - ], - ) - - assert result.exit_code == 0 - assert captured_file_id == "file-1234" - assert captured_request == { - "thread_id": "thread-1234", - "message_id": "msg-1234", - "workspace": "workspace-id", - } - assert "file-1234" in result.output - assert "msg-1234" in result.output diff --git a/tests/unit/test_cli_runs.py b/tests/unit/test_cli_runs.py deleted file mode 100644 index 7457dd7..0000000 --- a/tests/unit/test_cli_runs.py +++ /dev/null @@ -1,406 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Alibaba, Inc. and its affiliates. - -import re -from types import SimpleNamespace - -from typer.testing import CliRunner - -from dashscope.cli import runs - - -def strip_ansi_codes(text): - """Remove ANSI escape codes from text.""" - ansi_escape = re.compile(r"\x1b\[[0-9;]*m") - return ansi_escape.sub("", text) - - -class TestCliRuns: - def test_create(self, monkeypatch): - captured_thread_id = None - captured_request = {} - - def mock_create(thread_id, **kwargs): - nonlocal captured_thread_id - captured_thread_id = thread_id - captured_request.update(kwargs) - return SimpleNamespace( - id="run-1234", - object="thread.run", - thread_id=thread_id, - assistant_id=kwargs["assistant_id"], - model=kwargs["model"], - instructions=kwargs["instructions"], - metadata=kwargs["metadata"], - ) - - monkeypatch.setattr( - runs.dashscope.Runs, - "create", - mock_create, - ) - - result = CliRunner().invoke( - runs.app, - [ - "create", - "thread-1234", - "--assistant-id", - "asst-1234", - "--model", - "qwen-max", - "--instructions", - "You are a helpful assistant.", - "--additional-instructions", - "Answer briefly.", - "--tools", - '[{"type":"search"}]', - "--metadata", - '{"key":"value"}', - "--extra-body", - '{"custom":"field"}', - "--workspace", - "workspace-id", - "--top-p", - "0.9", - "--top-k", - "50", - "--temperature", - "0.8", - "--max-tokens", - "1024", - ], - ) - - assert result.exit_code == 0 - assert captured_thread_id == "thread-1234" - assert captured_request == { - "assistant_id": "asst-1234", - "model": "qwen-max", - "instructions": "You are a helpful assistant.", - "additional_instructions": "Answer briefly.", - "tools": [{"type": "search"}], - "metadata": {"key": "value"}, - "extra_body": {"custom": "field"}, - "workspace": "workspace-id", - "top_p": 0.9, - "top_k": 50, - "temperature": 0.8, - "max_tokens": 1024, - } - assert "run-1234" in result.output - assert "thread-1234" in result.output - - def test_create_rejects_invalid_tools_json(self): - result = CliRunner().invoke( - runs.app, - [ - "create", - "thread-1234", - "--assistant-id", - "asst-1234", - "--tools", - "not-json", - ], - ) - - assert result.exit_code == 1 - assert "Invalid tools JSON" in result.output - - def test_update_rejects_metadata_json_array(self): - result = CliRunner().invoke( - runs.app, - [ - "update", - "run-1234", - "--thread-id", - "thread-1234", - "--metadata", - '["wrong"]', - ], - ) - - assert result.exit_code == 1 - assert "metadata must be a JSON object" in result.output - - def test_get_requires_thread_id(self): - result = CliRunner().invoke(runs.app, ["get", "run-1234"]) - - assert result.exit_code != 0 - clean_output = strip_ansi_codes(result.output) - assert "Missing option" in clean_output - assert "--thread-id" in clean_output - - def test_get(self, monkeypatch): - captured_run_id = None - captured_request = {} - - def mock_retrieve(run_id, **kwargs): - nonlocal captured_run_id - captured_run_id = run_id - captured_request.update(kwargs) - return SimpleNamespace( - id="run-1234", - object="thread.run", - thread_id=kwargs["thread_id"], - status="completed", - ) - - monkeypatch.setattr( - runs.dashscope.Runs, - "retrieve", - mock_retrieve, - ) - - result = CliRunner().invoke( - runs.app, - [ - "get", - "run-1234", - "--thread-id", - "thread-1234", - "--workspace", - "workspace-id", - ], - ) - - assert result.exit_code == 0 - assert captured_run_id == "run-1234" - assert captured_request == { - "thread_id": "thread-1234", - "workspace": "workspace-id", - } - assert "run-1234" in result.output - assert "completed" in result.output - - def test_submit_tool_outputs(self, monkeypatch): - captured_run_id = None - captured_request = {} - - def mock_submit_tool_outputs(run_id, **kwargs): - nonlocal captured_run_id - captured_run_id = run_id - captured_request.update(kwargs) - return SimpleNamespace( - id=run_id, - object="thread.run", - thread_id=kwargs["thread_id"], - status="queued", - ) - - monkeypatch.setattr( - runs.dashscope.Runs, - "submit_tool_outputs", - mock_submit_tool_outputs, - ) - - result = CliRunner().invoke( - runs.app, - [ - "submit-tool-outputs", - "run-1234", - "--thread-id", - "thread-1234", - "--tool-outputs", - '[{"tool_call_id":"call-1234","output":"42"}]', - "--extra-body", - '{"custom":"field"}', - "--workspace", - "workspace-id", - ], - ) - - assert result.exit_code == 0 - assert captured_run_id == "run-1234" - assert captured_request == { - "thread_id": "thread-1234", - "tool_outputs": [{"tool_call_id": "call-1234", "output": "42"}], - "extra_body": {"custom": "field"}, - "workspace": "workspace-id", - } - assert "run-1234" in result.output - assert "queued" in result.output - - def test_wait(self, monkeypatch): - captured_run_id = None - captured_request = {} - - def mock_wait(run_id, **kwargs): - nonlocal captured_run_id - captured_run_id = run_id - captured_request.update(kwargs) - return SimpleNamespace( - id=run_id, - object="thread.run", - thread_id=kwargs["thread_id"], - status="completed", - ) - - monkeypatch.setattr( - runs.dashscope.Runs, - "wait", - mock_wait, - ) - - result = CliRunner().invoke( - runs.app, - [ - "wait", - "run-1234", - "--thread-id", - "thread-1234", - "--timeout-seconds", - "30", - "--workspace", - "workspace-id", - ], - ) - - assert result.exit_code == 0 - assert captured_run_id == "run-1234" - assert captured_request == { - "thread_id": "thread-1234", - "timeout_seconds": 30.0, - "workspace": "workspace-id", - } - assert "run-1234" in result.output - assert "completed" in result.output - - def test_update(self, monkeypatch): - captured_run_id = None - captured_request = {} - - def mock_update(run_id, **kwargs): - nonlocal captured_run_id - captured_run_id = run_id - captured_request.update(kwargs) - return SimpleNamespace( - id=run_id, - object="thread.run", - thread_id=kwargs["thread_id"], - metadata=kwargs["metadata"], - ) - - monkeypatch.setattr( - runs.dashscope.Runs, - "update", - mock_update, - ) - - result = CliRunner().invoke( - runs.app, - [ - "update", - "run-1234", - "--thread-id", - "thread-1234", - "--metadata", - '{"key":"value"}', - "--workspace", - "workspace-id", - ], - ) - - assert result.exit_code == 0 - assert captured_run_id == "run-1234" - assert captured_request == { - "thread_id": "thread-1234", - "metadata": {"key": "value"}, - "workspace": "workspace-id", - } - assert "run-1234" in result.output - assert "key" in result.output - - def test_cancel(self, monkeypatch): - captured_run_id = None - captured_request = {} - - def mock_cancel(run_id, **kwargs): - nonlocal captured_run_id - captured_run_id = run_id - captured_request.update(kwargs) - return SimpleNamespace( - id=run_id, - object="thread.run", - thread_id=kwargs["thread_id"], - status="cancelled", - ) - - monkeypatch.setattr( - runs.dashscope.Runs, - "cancel", - mock_cancel, - ) - - result = CliRunner().invoke( - runs.app, - [ - "cancel", - "run-1234", - "--thread-id", - "thread-1234", - "--workspace", - "workspace-id", - ], - ) - - assert result.exit_code == 0 - assert captured_run_id == "run-1234" - assert captured_request == { - "thread_id": "thread-1234", - "workspace": "workspace-id", - } - assert "run-1234" in result.output - assert "cancelled" in result.output - - def test_list(self, monkeypatch): - captured_thread_id = None - captured_request = {} - - def mock_list(thread_id, **kwargs): - nonlocal captured_thread_id - captured_thread_id = thread_id - captured_request.update(kwargs) - return SimpleNamespace( - object="list", - data=[SimpleNamespace(id="run-1234", status="completed")], - first_id="run-1234", - last_id="run-1234", - has_more=False, - ) - - monkeypatch.setattr( - runs.dashscope.Runs, - "list", - mock_list, - ) - - result = CliRunner().invoke( - runs.app, - [ - "list", - "thread-1234", - "--limit", - "10", - "--order", - "asc", - "--after", - "run-0001", - "--before", - "run-9999", - "--workspace", - "workspace-id", - ], - ) - - assert result.exit_code == 0 - assert captured_thread_id == "thread-1234" - assert captured_request == { - "limit": 10, - "order": "asc", - "after": "run-0001", - "before": "run-9999", - "workspace": "workspace-id", - } - assert "run-1234" in result.output - assert "completed" in result.output diff --git a/tests/unit/test_cli_steps.py b/tests/unit/test_cli_steps.py deleted file mode 100644 index 4822795..0000000 --- a/tests/unit/test_cli_steps.py +++ /dev/null @@ -1,113 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Alibaba, Inc. and its affiliates. - -from types import SimpleNamespace - -from typer.testing import CliRunner - -from dashscope.cli import steps - - -class TestCliSteps: - def test_list(self, monkeypatch): - captured_run_id = None - captured_request = {} - - def mock_list(run_id, **kwargs): - nonlocal captured_run_id - captured_run_id = run_id - captured_request.update(kwargs) - return SimpleNamespace( - object="list", - data=[ - SimpleNamespace(id="step-1234", type="message_creation"), - ], - first_id="step-1234", - last_id="step-1234", - has_more=False, - ) - - monkeypatch.setattr( - steps.dashscope.Steps, - "list", - mock_list, - ) - - result = CliRunner().invoke( - steps.app, - [ - "list", - "run-1234", - "--thread-id", - "thread-1234", - "--limit", - "10", - "--order", - "asc", - "--after", - "step-0001", - "--before", - "step-9999", - "--workspace", - "workspace-id", - ], - ) - - assert result.exit_code == 0 - assert captured_run_id == "run-1234" - assert captured_request == { - "thread_id": "thread-1234", - "limit": 10, - "order": "asc", - "after": "step-0001", - "before": "step-9999", - "workspace": "workspace-id", - } - assert "step-1234" in result.output - assert "message_creation" in result.output - - def test_get(self, monkeypatch): - captured_step_id = None - captured_request = {} - - def mock_retrieve(step_id, **kwargs): - nonlocal captured_step_id - captured_step_id = step_id - captured_request.update(kwargs) - return SimpleNamespace( - id="step-1234", - object="thread.run.step", - type="message_creation", - thread_id=kwargs["thread_id"], - run_id=kwargs["run_id"], - ) - - monkeypatch.setattr( - steps.dashscope.Steps, - "retrieve", - mock_retrieve, - ) - - result = CliRunner().invoke( - steps.app, - [ - "get", - "step-1234", - "--thread-id", - "thread-1234", - "--run-id", - "run-1234", - "--workspace", - "workspace-id", - ], - ) - - assert result.exit_code == 0 - assert captured_step_id == "step-1234" - assert captured_request == { - "thread_id": "thread-1234", - "run_id": "run-1234", - "workspace": "workspace-id", - } - assert "step-1234" in result.output - assert "thread.run.step" in result.output diff --git a/tests/unit/test_cli_threads.py b/tests/unit/test_cli_threads.py deleted file mode 100644 index 1695dfb..0000000 --- a/tests/unit/test_cli_threads.py +++ /dev/null @@ -1,229 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Alibaba, Inc. and its affiliates. - -from types import SimpleNamespace - -from typer.testing import CliRunner - -from dashscope.cli import threads - - -class TestCliThreads: - def test_create(self, monkeypatch): - captured_request = {} - - def mock_create(**kwargs): - captured_request.update(kwargs) - return SimpleNamespace( - id="thread-1234", - object="thread", - created_at=1699012949, - metadata=kwargs["metadata"], - ) - - monkeypatch.setattr( - threads.dashscope.Threads, - "create", - mock_create, - ) - - result = CliRunner().invoke( - threads.app, - [ - "create", - "--messages", - '[{"role":"user","content":"ๅฆ‚ไฝ•ๅšๅ‡บ็พŽๅ‘ณ็š„็‰›่‚‰็‚–ๅœŸ่ฑ†๏ผŸ"}]', - "--metadata", - '{"key":"value"}', - "--workspace", - "workspace-id", - ], - ) - - assert result.exit_code == 0 - assert captured_request == { - "messages": [ - { - "role": "user", - "content": "ๅฆ‚ไฝ•ๅšๅ‡บ็พŽๅ‘ณ็š„็‰›่‚‰็‚–ๅœŸ่ฑ†๏ผŸ", - }, - ], - "metadata": {"key": "value"}, - "workspace": "workspace-id", - } - assert "thread-1234" in result.output - assert "key" in result.output - - def test_update(self, monkeypatch): - captured_thread_id = None - captured_request = {} - - def mock_update(thread_id, **kwargs): - nonlocal captured_thread_id - captured_thread_id = thread_id - captured_request.update(kwargs) - return SimpleNamespace( - id=thread_id, - object="thread", - created_at=1699012949, - metadata=kwargs["metadata"], - ) - - monkeypatch.setattr( - threads.dashscope.Threads, - "update", - mock_update, - ) - - result = CliRunner().invoke( - threads.app, - [ - "update", - "thread-1234", - "--metadata", - '{"key":"value"}', - "--workspace", - "workspace-id", - ], - ) - - assert result.exit_code == 0 - assert captured_thread_id == "thread-1234" - assert captured_request == { - "metadata": {"key": "value"}, - "workspace": "workspace-id", - } - assert "thread-1234" in result.output - assert "key" in result.output - - def test_get(self, monkeypatch): - captured_thread_id = None - captured_request = {} - - def mock_retrieve(thread_id, **kwargs): - nonlocal captured_thread_id - captured_thread_id = thread_id - captured_request.update(kwargs) - return SimpleNamespace( - id=thread_id, - object="thread", - created_at=1699012949, - metadata={"key": "value"}, - ) - - monkeypatch.setattr( - threads.dashscope.Threads, - "retrieve", - mock_retrieve, - ) - - result = CliRunner().invoke( - threads.app, - [ - "get", - "thread-1234", - "--workspace", - "workspace-id", - ], - ) - - assert result.exit_code == 0 - assert captured_thread_id == "thread-1234" - assert captured_request == {"workspace": "workspace-id"} - assert "thread-1234" in result.output - assert "key" in result.output - - def test_delete(self, monkeypatch): - captured_thread_id = None - captured_request = {} - - def mock_delete(thread_id, **kwargs): - nonlocal captured_thread_id - captured_thread_id = thread_id - captured_request.update(kwargs) - return SimpleNamespace( - id=thread_id, - object="thread", - created_at=1699012949, - deleted=True, - ) - - monkeypatch.setattr( - threads.dashscope.Threads, - "delete", - mock_delete, - ) - - result = CliRunner().invoke( - threads.app, - [ - "delete", - "thread-1234", - "--workspace", - "workspace-id", - ], - ) - - assert result.exit_code == 0 - assert captured_thread_id == "thread-1234" - assert captured_request == {"workspace": "workspace-id"} - assert "thread-1234" in result.output - assert "true" in result.output - - def test_create_and_run(self, monkeypatch): - captured_request = {} - - def mock_create_and_run(**kwargs): - captured_request.update(kwargs) - return SimpleNamespace( - id="run-1234", - object="thread.run", - thread_id="thread-1234", - assistant_id=kwargs["assistant_id"], - model=kwargs["model"], - instructions=kwargs["instructions"], - metadata=kwargs["metadata"], - ) - - monkeypatch.setattr( - threads.dashscope.Threads, - "create_and_run", - mock_create_and_run, - ) - - result = CliRunner().invoke( - threads.app, - [ - "create-and-run", - "--assistant-id", - "asst-1234", - "--thread", - '{"messages":[{"role":"user","content":"hello"}]}', - "--model", - "qwen-max", - "--instructions", - "You are helpful.", - "--additional-instructions", - "Answer briefly.", - "--tools", - '[{"type":"search"}]', - "--metadata", - '{"key":"value"}', - "--workspace", - "workspace-id", - ], - ) - - assert result.exit_code == 0 - assert captured_request == { - "assistant_id": "asst-1234", - "thread": {"messages": [{"role": "user", "content": "hello"}]}, - "model": "qwen-max", - "instructions": "You are helpful.", - "additional_instructions": "Answer briefly.", - "tools": [{"type": "search"}], - "metadata": {"key": "value"}, - "workspace": "workspace-id", - } - assert "run-1234" in result.output - assert "thread-1234" in result.output From 93e3d6b5e778d4a9c520dfb8a41c7fdebcae169e Mon Sep 17 00:00:00 2001 From: kevin Date: Thu, 25 Jun 2026 14:49:15 +0800 Subject: [PATCH 9/9] fix: restore assistants imports in __init__.py for Python 3.8 compatibility --- dashscope/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dashscope/__init__.py b/dashscope/__init__.py index 08dcd14..5be1089 100644 --- a/dashscope/__init__.py +++ b/dashscope/__init__.py @@ -56,6 +56,8 @@ from dashscope.models import Models from dashscope.nlp.understanding import Understanding from dashscope.rerank import AioTextReRank, TextReRank +from dashscope.assistants import Assistant, AssistantList, Assistants +from dashscope.assistants.assistant_types import AssistantFile, DeleteResponse from dashscope.threads import ( MessageFile, Messages,