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}" "cwd": "${workspaceFolder}"
}, },
{ {
"name": "Debug `arcade chat -h localhost`", "name": "Debug `arcade chat -s -h localhost`",
"type": "python", "type": "python",
"request": "launch", "request": "launch",
"program": "${workspaceFolder}/arcade/run_cli.py", "program": "${workspaceFolder}/arcade/run_cli.py",
"args": ["chat", "-h", "localhost"], "args": ["chat", "-s", "-h", "localhost"],
"console": "integratedTerminal", "console": "integratedTerminal",
"jinja": true, "jinja": true,
"justMyCode": true, "justMyCode": true,

View file

@ -30,11 +30,11 @@ from arcade.cli.utils import (
get_eval_files, get_eval_files,
get_tools_from_engine, get_tools_from_engine,
handle_chat_interaction, handle_chat_interaction,
handle_tool_authorization,
is_authorization_pending, is_authorization_pending,
load_eval_suites, load_eval_suites,
log_engine_health, log_engine_health,
validate_and_get_config, validate_and_get_config,
wait_for_authorization_completion,
) )
from arcade.client import Arcade from arcade.client import Arcade
@ -264,31 +264,28 @@ def chat(
chat_result = handle_chat_interaction( chat_result = handle_chat_interaction(
openai_client, model, history, user_email, stream 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 history = chat_result.history
tool_messages = chat_result.tool_messages 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: if debug:
display_tool_messages(tool_messages) display_tool_messages(tool_messages)

View file

@ -1,7 +1,8 @@
import importlib.util import importlib.util
import webbrowser
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Callable, Union from typing import Any, Callable, Union
import typer import typer
from openai import OpenAI 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 import Choice as ChatCompletionChoice
from openai.types.chat.chat_completion_chunk import Choice as ChatCompletionChunkChoice from openai.types.chat.chat_completion_chunk import Choice as ChatCompletionChunkChoice
from rich.console import Console from rich.console import Console
from rich.live import Live
from rich.markdown import Markdown from rich.markdown import Markdown
from rich.text import Text
from typer.core import TyperGroup from typer.core import TyperGroup
from typer.models import Context from typer.models import Context
@ -120,23 +123,31 @@ def handle_streaming_content(stream: Stream[ChatCompletionChunk], model: str) ->
tool_messages = [] tool_messages = []
tool_authorization = None tool_authorization = None
role = "" role = ""
printed_role: bool = False
with Live(console=console, refresh_per_second=10) as live: with Live(console=console, refresh_per_second=10) as live:
for chunk in stream: for chunk in stream:
choice = chunk.choices[0] choice = chunk.choices[0]
chunk_message = choice.delta.content role = choice.delta.role or role
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)
# Display and get tool messages if they exist # Display and get tool messages if they exist
tool_messages += get_tool_messages(choice) # type: ignore[arg-type] tool_messages += get_tool_messages(choice) # type: ignore[arg-type]
tool_authorization = get_tool_authorization(choice) 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 # Markdownify URLs in the final message if applicable
if role == "assistant": if role == "assistant":
full_message = markdownify_urls(full_message) full_message = markdownify_urls(full_message)
@ -289,7 +300,10 @@ def handle_chat_interaction(
tool_authorization = get_tool_authorization(response.choices[0]) tool_authorization = get_tool_authorization(response.choices[0])
role = response.choices[0].message.role 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) message_content = markdownify_urls(message_content)
console.print( console.print(
f"\n[blue][bold]Assistant[/bold] ({model}):[/blue] ", Markdown(message_content) 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) 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: 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 Wait for the authorization for a tool call to complete i.e., wait for the user to click on