|
| 1 | +# -*- coding: utf-8 -*- |
| 2 | +# Copyright (c) Alibaba, Inc. and its affiliates. |
| 3 | +"""``dashscope apps`` -- Bailian application calls. |
| 4 | +
|
| 5 | +An application carries its own prompt, flow, plugins and knowledge base, so |
| 6 | +the request has no ``model``: ``--app-id`` identifies what to call. What the |
| 7 | +CLI adds on top is the streaming default -- the service sends cumulative text |
| 8 | +unless asked otherwise, which would print the same sentence over and over, so |
| 9 | +``--stream`` asks for incremental frames unless told not to. |
| 10 | +""" |
| 11 | + |
| 12 | +from __future__ import annotations |
| 13 | + |
| 14 | +from typing import Any, Dict, List, Optional, Tuple |
| 15 | + |
| 16 | +import typer |
| 17 | + |
| 18 | +from dashscope.cli.errors import usage_error |
| 19 | +from dashscope.cli.input import ( |
| 20 | + parse_json_object_arg, |
| 21 | + parse_messages_arg, |
| 22 | + read_stdin, |
| 23 | + read_text_arg, |
| 24 | + stdin_is_piped, |
| 25 | +) |
| 26 | +from dashscope.cli.output import Renderer, render_text_stream |
| 27 | + |
| 28 | +_HELP = r"""Call a Bailian application. |
| 29 | +
|
| 30 | +An application is addressed by `--app-id`; its own prompt, flow, plugins and |
| 31 | +knowledge base decide the rest. Send one turn with `--prompt`, a transcript |
| 32 | +you maintain with `--messages`, or let the service keep the state with |
| 33 | +`--session-id` / `--memory-id`. |
| 34 | +
|
| 35 | +`--stream` writes incremental text to stdout in table mode, and one JSON |
| 36 | +object per line in json/yaml mode. |
| 37 | +""" |
| 38 | + |
| 39 | +_COMPLETIONS_HELP = r"""Application completions.""" |
| 40 | + |
| 41 | + |
| 42 | +def _read_inputs( |
| 43 | + prompt: Optional[str], |
| 44 | + messages: Optional[str], |
| 45 | +) -> Tuple[Optional[str], Optional[List[Dict[str, Any]]]]: |
| 46 | + """Unlike chat, ``--prompt`` and ``--messages`` combine here. |
| 47 | +
|
| 48 | + The SDK appends ``prompt`` as the latest user turn when both are given, |
| 49 | + so neither one shadows the other. |
| 50 | + """ |
| 51 | + text = read_text_arg(prompt, option_name="--prompt") |
| 52 | + conversation = ( |
| 53 | + parse_messages_arg(messages) if messages is not None else None |
| 54 | + ) |
| 55 | + if text is None and conversation is None and stdin_is_piped(): |
| 56 | + text = read_stdin().strip() or None |
| 57 | + if not text and not conversation: |
| 58 | + raise usage_error( |
| 59 | + "no input given: pass --prompt, --messages, or pipe text on stdin", |
| 60 | + ) |
| 61 | + return text, conversation |
| 62 | + |
| 63 | + |
| 64 | +def _render_completion(renderer: Renderer, result: Any) -> None: |
| 65 | + """Answer on stdout; thoughts, citations and usage on stderr. |
| 66 | +
|
| 67 | + Citations go to stderr because they are diagnostics for a human, not |
| 68 | + part of the answer -- ``dashscope apps completions create ... > a.txt`` |
| 69 | + should capture the text alone. |
| 70 | + """ |
| 71 | + if not renderer.table_mode: |
| 72 | + renderer.print_object(result) |
| 73 | + return |
| 74 | + for thought in result.thoughts or (): |
| 75 | + if thought.thought: |
| 76 | + renderer.note(thought.thought) |
| 77 | + renderer.echo(result.text or "") |
| 78 | + for reference in result.doc_references or (): |
| 79 | + name = reference.doc_name or reference.title or reference.doc_id or "-" |
| 80 | + renderer.note(f"source: {name} {reference.doc_url or ''}".rstrip()) |
| 81 | + if result.session_id: |
| 82 | + renderer.note(f"session_id: {result.session_id}") |
| 83 | + if result.usage is not None: |
| 84 | + renderer.note(f"usage: {result.usage.model_dump(exclude_none=True)}") |
| 85 | + |
| 86 | + |
| 87 | +def build_app() -> typer.Typer: |
| 88 | + app = typer.Typer(help=_HELP, no_args_is_help=True) |
| 89 | + |
| 90 | + completions_app = typer.Typer(help=_COMPLETIONS_HELP, no_args_is_help=True) |
| 91 | + |
| 92 | + @completions_app.command("create") |
| 93 | + def completions_create( # pylint: disable=too-many-arguments,too-many-locals |
| 94 | + ctx: typer.Context, |
| 95 | + app_id: str = typer.Option( |
| 96 | + ..., |
| 97 | + "--app-id", |
| 98 | + help="Application id, from the Bailian console.", |
| 99 | + ), |
| 100 | + prompt: Optional[str] = typer.Option( |
| 101 | + None, |
| 102 | + "--prompt", |
| 103 | + help="Prompt text, @file, or - to read stdin.", |
| 104 | + ), |
| 105 | + messages: Optional[str] = typer.Option( |
| 106 | + None, |
| 107 | + "--messages", |
| 108 | + help="Conversation as a JSON array, JSONL, or an object with a " |
| 109 | + "messages key. Accepts @file and - for stdin.", |
| 110 | + ), |
| 111 | + stream: bool = typer.Option( |
| 112 | + False, |
| 113 | + "--stream", |
| 114 | + help="Stream the response instead of waiting for it to finish.", |
| 115 | + ), |
| 116 | + session_id: Optional[str] = typer.Option( |
| 117 | + None, |
| 118 | + "--session-id", |
| 119 | + help="Let the service keep the conversation state.", |
| 120 | + ), |
| 121 | + memory_id: Optional[str] = typer.Option(None, "--memory-id"), |
| 122 | + biz_params: Optional[str] = typer.Option( |
| 123 | + None, |
| 124 | + "--biz-params", |
| 125 | + help="Application flow and plugin inputs as a JSON object. " |
| 126 | + "Accepts @file.", |
| 127 | + ), |
| 128 | + image: Optional[List[str]] = typer.Option( |
| 129 | + None, |
| 130 | + "--image", |
| 131 | + help="Image URL for the application to read. Repeatable.", |
| 132 | + ), |
| 133 | + file: Optional[List[str]] = typer.Option( |
| 134 | + None, |
| 135 | + "--file", |
| 136 | + help="File URL for the application to read. Repeatable.", |
| 137 | + ), |
| 138 | + rag_options: Optional[str] = typer.Option( |
| 139 | + None, |
| 140 | + "--rag-options", |
| 141 | + help="Retrieval options as a JSON object. Accepts @file.", |
| 142 | + ), |
| 143 | + has_thoughts: Optional[bool] = typer.Option( |
| 144 | + None, |
| 145 | + "--has-thoughts/--no-has-thoughts", |
| 146 | + help="Ask for the planning and tool-call steps.", |
| 147 | + ), |
| 148 | + doc_tag_codes: Optional[List[str]] = typer.Option( |
| 149 | + None, |
| 150 | + "--doc-tag-code", |
| 151 | + help="Restrict retrieval to a tag. Repeatable.", |
| 152 | + ), |
| 153 | + doc_reference_type: Optional[str] = typer.Option( |
| 154 | + None, |
| 155 | + "--doc-reference-type", |
| 156 | + help="Citation style for retrieved documents.", |
| 157 | + ), |
| 158 | + incremental_output: Optional[bool] = typer.Option( |
| 159 | + None, |
| 160 | + "--incremental-output/--no-incremental-output", |
| 161 | + help="Stream deltas rather than cumulative text. Defaults to on " |
| 162 | + "with --stream.", |
| 163 | + ), |
| 164 | + temperature: Optional[float] = typer.Option(None, "--temperature"), |
| 165 | + top_p: Optional[float] = typer.Option(None, "--top-p"), |
| 166 | + top_k: Optional[int] = typer.Option(None, "--top-k"), |
| 167 | + seed: Optional[int] = typer.Option(None, "--seed"), |
| 168 | + parameters: Optional[str] = typer.Option( |
| 169 | + None, |
| 170 | + "--parameters", |
| 171 | + help="Extra parameters as a JSON object, passed through " |
| 172 | + "verbatim. Accepts @file.", |
| 173 | + ), |
| 174 | + ) -> None: |
| 175 | + """Send one application completion request.""" |
| 176 | + obj = ctx.obj |
| 177 | + renderer = obj.renderer |
| 178 | + text, conversation = _read_inputs(prompt, messages) |
| 179 | + completions = obj.client.apps.completions |
| 180 | + kwargs: Dict[str, Any] = { |
| 181 | + "app_id": app_id, |
| 182 | + "prompt": text, |
| 183 | + "messages": conversation, |
| 184 | + "session_id": session_id, |
| 185 | + "memory_id": memory_id, |
| 186 | + "biz_params": parse_json_object_arg( |
| 187 | + biz_params, |
| 188 | + option_name="--biz-params", |
| 189 | + ), |
| 190 | + "image_list": list(image) if image else None, |
| 191 | + "file_list": list(file) if file else None, |
| 192 | + "rag_options": parse_json_object_arg( |
| 193 | + rag_options, |
| 194 | + option_name="--rag-options", |
| 195 | + ), |
| 196 | + "has_thoughts": has_thoughts, |
| 197 | + "doc_tag_codes": list(doc_tag_codes) if doc_tag_codes else None, |
| 198 | + "doc_reference_type": doc_reference_type, |
| 199 | + "incremental_output": ( |
| 200 | + incremental_output |
| 201 | + if incremental_output is not None |
| 202 | + else (True if stream else None) |
| 203 | + ), |
| 204 | + "temperature": temperature, |
| 205 | + "top_p": top_p, |
| 206 | + "top_k": top_k, |
| 207 | + "seed": seed, |
| 208 | + "parameters": parse_json_object_arg( |
| 209 | + parameters, |
| 210 | + option_name="--parameters", |
| 211 | + ), |
| 212 | + } |
| 213 | + |
| 214 | + if not stream: |
| 215 | + _render_completion(renderer, completions.create(**kwargs)) |
| 216 | + return |
| 217 | + |
| 218 | + result = completions.create(stream=True, **kwargs) |
| 219 | + try: |
| 220 | + render_text_stream(renderer, result) |
| 221 | + finally: |
| 222 | + # Runs on Ctrl-C too, so the httpx stream is not left open. |
| 223 | + result.close() |
| 224 | + |
| 225 | + app.add_typer(completions_app, name="completions") |
| 226 | + return app |
0 commit comments