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}"
|
"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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue