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:
Nate Barbettini 2024-10-17 15:18:26 -07:00 committed by GitHub
parent 8a1114ab62
commit 1c6e3f4495
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 75 additions and 35 deletions

4
.vscode/launch.json vendored
View file

@ -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,

View file

@ -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)

View file

@ -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