diff --git a/.vscode/launch.json b/.vscode/launch.json index 6a6c309e..4b81308b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -29,11 +29,11 @@ "cwd": "${workspaceFolder}" }, { - "name": "Debug `arcade chat -h localhost`", + "name": "Debug `arcade chat -s -h localhost`", "type": "python", "request": "launch", "program": "${workspaceFolder}/arcade/run_cli.py", - "args": ["chat", "-h", "localhost"], + "args": ["chat", "-s", "-h", "localhost"], "console": "integratedTerminal", "jinja": true, "justMyCode": true, diff --git a/arcade/arcade/cli/main.py b/arcade/arcade/cli/main.py index 2e1194dd..81018eb2 100644 --- a/arcade/arcade/cli/main.py +++ b/arcade/arcade/cli/main.py @@ -30,11 +30,11 @@ from arcade.cli.utils import ( get_eval_files, get_tools_from_engine, handle_chat_interaction, + handle_tool_authorization, is_authorization_pending, load_eval_suites, log_engine_health, validate_and_get_config, - wait_for_authorization_completion, ) from arcade.client import Arcade @@ -264,31 +264,28 @@ def chat( chat_result = handle_chat_interaction( openai_client, model, history, user_email, stream ) - except OpenAIError as e: - console.print(f"❌ Arcade Chat failed with error: {e!s}", style="bold red") - continue - - history = chat_result.history - tool_messages = chat_result.tool_messages - tool_authorization = chat_result.tool_authorization - - # wait for tool authorizations to complete, if any - if is_authorization_pending(tool_authorization): - with console.status("Waiting for you to authorize the action...", spinner="dots"): - wait_for_authorization_completion(client, tool_authorization) - # re-run the chat request now that authorization is complete - try: - history.pop() - chat_result = handle_chat_interaction( - openai_client, model, history, user_email, stream - ) - except OpenAIError as e: - console.print(f"❌ Arcade Chat failed with error: {e!s}", style="bold red") - continue history = chat_result.history tool_messages = chat_result.tool_messages + tool_authorization = chat_result.tool_authorization + # wait for tool authorizations to complete, if any + if tool_authorization and is_authorization_pending(tool_authorization): + chat_result = handle_tool_authorization( + client, + tool_authorization, + history, + openai_client, + model, + user_email, + stream, + ) + history = chat_result.history + tool_messages = chat_result.tool_messages + + except OpenAIError as e: + console.print(f"❌ Arcade Chat failed with error: {e!s}", style="bold red") + continue if debug: display_tool_messages(tool_messages) diff --git a/arcade/arcade/cli/utils.py b/arcade/arcade/cli/utils.py index f8a4c004..89d0df0c 100644 --- a/arcade/arcade/cli/utils.py +++ b/arcade/arcade/cli/utils.py @@ -1,7 +1,8 @@ import importlib.util +import webbrowser from dataclasses import dataclass from pathlib import Path -from typing import Callable, Union +from typing import Any, Callable, Union import typer from openai import OpenAI @@ -9,7 +10,9 @@ from openai.resources.chat.completions import ChatCompletionChunk, Stream from openai.types.chat.chat_completion import Choice as ChatCompletionChoice from openai.types.chat.chat_completion_chunk import Choice as ChatCompletionChunkChoice from rich.console import Console +from rich.live import Live from rich.markdown import Markdown +from rich.text import Text from typer.core import TyperGroup from typer.models import Context @@ -120,23 +123,31 @@ def handle_streaming_content(stream: Stream[ChatCompletionChunk], model: str) -> tool_messages = [] tool_authorization = None role = "" + printed_role: bool = False + with Live(console=console, refresh_per_second=10) as live: for chunk in stream: choice = chunk.choices[0] - chunk_message = choice.delta.content - if role == "": - role = choice.delta.role or "" - if role == "assistant": - console.print(f"\n[blue][bold]Assistant[/bold] ({model}):[/blue] ") - if chunk_message: - full_message += chunk_message - markdown_chunk = Markdown(full_message) - live.update(markdown_chunk) + role = choice.delta.role or role # Display and get tool messages if they exist tool_messages += get_tool_messages(choice) # type: ignore[arg-type] tool_authorization = get_tool_authorization(choice) + chunk_message = choice.delta.content + + if role == "assistant" and tool_authorization: + continue # Skip the message if it's an auth request (handled later in handle_tool_authorization) + + if role == "assistant" and not printed_role: + console.print(f"\n[blue][bold]Assistant[/bold] ({model}):[/blue] ") + printed_role = True + + if chunk_message: + full_message += chunk_message + markdown_chunk = Markdown(full_message) + live.update(markdown_chunk) + # Markdownify URLs in the final message if applicable if role == "assistant": full_message = markdownify_urls(full_message) @@ -289,7 +300,10 @@ def handle_chat_interaction( tool_authorization = get_tool_authorization(response.choices[0]) role = response.choices[0].message.role - if role == "assistant": + + if role == "assistant" and tool_authorization: + pass # Skip the message if it's an auth request (handled later in handle_tool_authorization) + elif role == "assistant": message_content = markdownify_urls(message_content) console.print( f"\n[blue][bold]Assistant[/bold] ({model}):[/blue] ", Markdown(message_content) @@ -303,6 +317,35 @@ def handle_chat_interaction( return ChatInteractionResult(history, tool_messages, tool_authorization) +def handle_tool_authorization( + arcade_client: Arcade, + tool_authorization: dict, + history: list[dict[str, Any]], + openai_client: OpenAI, + model: str, + user_email: str | None, + stream: bool, +) -> ChatInteractionResult: + with Live(console=console, refresh_per_second=4) as live: + if "authorization_url" in tool_authorization: + authorization_url = str(tool_authorization["authorization_url"]) + webbrowser.open(authorization_url) + message = ( + "You'll need to authorize this action in your browser.\n\n" + f"If a browser doesn't open automatically, click [this link]({authorization_url}) " + f"or copy this URL and paste it into your browser:\n\n{authorization_url}" + ) + live.update(Markdown(message, style="dim")) + + wait_for_authorization_completion(arcade_client, tool_authorization) + + message = "Thanks for authorizing the action! Sending your request..." + live.update(Text(message, style="dim")) + + history.pop() + return handle_chat_interaction(openai_client, model, history, user_email, stream) + + def wait_for_authorization_completion(client: Arcade, tool_authorization: dict | None) -> None: """ Wait for the authorization for a tool call to complete i.e., wait for the user to click on