arcade chat: Auto open browser for auth links (#112)
Fixes two points of feedback: - In terminals that don't support links, we need to print the whole URL as a backup - Just open the browser dammit! -- Wils ### Demo https://github.com/user-attachments/assets/91ced5ef-43d3-45a4-8af7-beb1a7bcde8d
This commit is contained in:
parent
8a1114ab62
commit
1c6e3f4495
3 changed files with 75 additions and 35 deletions
4
.vscode/launch.json
vendored
4
.vscode/launch.json
vendored
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue