diff --git a/arcade/arcade/cli/main.py b/arcade/arcade/cli/main.py index 844e4f7f..51ad9ceb 100644 --- a/arcade/arcade/cli/main.py +++ b/arcade/arcade/cli/main.py @@ -31,8 +31,10 @@ from arcade.cli.utils import ( delete_deprecated_config_file, get_eval_files, get_tools_from_engine, + get_user_input, handle_chat_interaction, handle_tool_authorization, + handle_user_command, is_authorization_pending, load_eval_suites, log_engine_health, @@ -270,16 +272,19 @@ def chat( log_engine_health(client) while True: - console.print(f"\n[magenta][bold]User[/bold] ({user_email}):[/magenta] ") + console.print( + f"\n[magenta][bold]User[/bold] {user_email}: [/magenta]" + + "([bold][default]/?[/default][/bold] for help)" + ) - # Use input() instead of console.input() to leverage readline history - user_input = input() - while not user_input.strip(): - user_input = input() + user_input = get_user_input() # Add the input to history readline.add_history(user_input) + if handle_user_command(user_input, history, host, port, force_tls, force_no_tls, show): + continue + history.append({"role": "user", "content": user_input}) try: diff --git a/arcade/arcade/cli/utils.py b/arcade/arcade/cli/utils.py index 0023311f..21aa61ad 100644 --- a/arcade/arcade/cli/utils.py +++ b/arcade/arcade/cli/utils.py @@ -3,7 +3,9 @@ import ipaddress import os import webbrowser from dataclasses import dataclass +from enum import Enum from pathlib import Path +from textwrap import dedent from typing import Any, Callable, Union, cast from urllib.parse import urlencode, urlparse @@ -36,6 +38,13 @@ class OrderCommands(TyperGroup): return list(self.commands) # get commands using self.commands +class ChatCommand(str, Enum): + HELP = "/?" + CLEAR = "/clear" + SHOW = "/show" + EXIT = "/exit" + + def create_cli_catalog( toolkit: str | None = None, show_toolkits: bool = False, @@ -587,3 +596,89 @@ def delete_deprecated_config_file() -> None: if os.path.exists(deprecated_config_file_path): os.remove(deprecated_config_file_path) + + +def get_user_input() -> str: + """ + Get input from the user, handling multi-line input. + """ + MULTI_LINE_PROMPT = '"""' + user_input = input() + # Handle multi-line input + if user_input.startswith(MULTI_LINE_PROMPT): + user_input = user_input[len(MULTI_LINE_PROMPT) :] + + while not user_input.endswith(MULTI_LINE_PROMPT): + line = input() + if not line: + print() + user_input += "\n" + line + + user_input = user_input.rstrip(MULTI_LINE_PROMPT) + else: + # Handle single-line input + while not user_input.strip(): + user_input = input() + + return user_input.strip() + + +def display_chat_help() -> None: + """Display the help message for arcade chat.""" + help_message = dedent(f"""\ + [default] + Available Commands: + {ChatCommand.SHOW:<10} Show all available tools + {ChatCommand.CLEAR:<10} Clear the chat history + {ChatCommand.HELP:<10} Help for a command + {ChatCommand.EXIT:<10} Exit the chat + + Use \"\"\" to begin & end a multi-line message[/default] + """) + console.print(help_message) + + +def handle_user_command( + user_input: str, + history: list, + host: str, + port: int, + force_tls: bool, + force_no_tls: bool, + show: Callable, +) -> bool: + """ + Handle user commands during `arcade chat` and return True if a command was processed, otherwise False. + """ + if user_input == ChatCommand.HELP: + display_chat_help() + return True + elif user_input == ChatCommand.EXIT: + raise KeyboardInterrupt + elif user_input == ChatCommand.CLEAR: + console.print("Chat history cleared.", style="bold green") + history.clear() + return True + elif user_input == ChatCommand.SHOW: + show( + toolkit=None, + tool=None, + host=host, + port=port, + force_tls=force_tls, + force_no_tls=force_no_tls, + debug=False, + ) + return True + return False + + +def parse_user_command(user_input: str) -> ChatCommand | None: + """ + Parse the user command and return the corresponding ChatCommand enum. + Returns None if the input is not a valid chat command. + """ + try: + return ChatCommand(user_input) + except ValueError: + return None