How to Build an Interactive Chat for Your Python CLI Using Introspection, Click, and Rich Formatting
If you’ve ever wanted to make your CLI more interactive and dynamic, building a real-time command interaction system could be the answer. By leveraging Python’s introspection capabilities, Click for managing commands, and Rich for formatting output, you can create a powerful, flexible CLI that responds intelligently to user input. Instead of manually hardcoding each command, your CLI can automatically discover and execute commands, making the user experience smoother and more engaging.
Why Use Click and Markdown?
By combining these two libraries with Python introspection, you can build an interactive chat feature that dynamically discovers and executes commands while displaying output in a rich, readable format.
Building the Interactive Chat System
Let’s dive into the steps for building an interactive chat feature that can dynamically execute CLI commands.
1. Setting Up the Basic Chat Command
The chat command initializes the session, allowing users to interact with the CLI. Here, we capture user input, which will be dynamically mapped to the appropriate Click commands.
import os
import click
import shlex
from rich.console import Console
from rich.markdown import Markdown
console = Console()
@click.command()
@click.option("--project-path", type=click.Path(), help="Path to the project directory")
def chat(project_path=None):
"""
Start a chat session with the assistant for the given project.
"""
if not project_path:
project_path = os.getcwd()
console.print(
f"Starting chat for [bold]{project_path}[/bold]. Type [bold green]exit()[/bold green] to quit."
)
# Start the interactive session
while True:
user_input = console.input("[bold blue]You:[/bold blue] ")
# Handle exit
if user_input.lower() == "exit()":
console.print("[bold red]Exiting chat...[/bold red]")
break
# Call the function to handle command execution
execute_cli_command(user_input)
2. Introspection to Discover and Execute Commands
Using Python introspection, we dynamically discover available commands and execute them. One crucial part here is that Click commands are decorated functions. To execute the actual logic, we need to call the undecorated function (i.e., the callback).
Here’s how you can dynamically execute commands using introspection and handle Click’s decorators:
import inspect
import your_project_cmd # Replace with your actual module containing commands
command_modules = {"project": your_project_cmd} # List your command modules here
def execute_cli_command(user_input):
"""
Function to execute CLI commands dynamically based on the available modules,
calling the undecorated function directly.
"""
try:
# Use shlex.split to handle quotes and separate arguments correctly
parts = shlex.split(user_input)
module_name = parts[0]
command_name = parts[1].replace("-", "_") # Replace hyphens with underscores
command_args = parts[2:] # Keep the rest of the arguments as a list
# Check if the module exists in command_modules
if module_name in command_modules:
module = command_modules[module_name]
# Introspection: Get the function by name
if hasattr(module, command_name):
cmd_func = getattr(module, command_name)
# Check if it's a Click command and strip the decorator
if hasattr(cmd_func, "callback"):
# Call the underlying undecorated function
cmd_func = cmd_func.callback
# Check if it's a callable (function)
if callable(cmd_func):
console.print(
f"Executing command from module: [bold]{module_name}[/bold]"
)
# Directly call the function with the argument list
cmd_func(*command_args)
else:
console.print(
f"[bold red]'{command_name}' is not a valid command[/bold red]"
)
else:
console.print(
f"[bold red]Command '{command_name}' not found in {module_name}[/bold red]"
)
else:
console.print(f"[bold red]Module {module_name} not found[/bold red]")
except Exception as e:
console.print(f"[bold red]Error executing command: {str(e)}[/bold red]")
How Does This Work?
Recommended by LinkedIn
3. Example CLI Commands
Let’s consider some sample commands within a project module that users can call interactively via the chat:
@click.group()
def project():
"""Project management CLI."""
pass
@project.command()
def init():
"""Initialize a new project."""
console.print("[bold green]Project initialized![/bold green]")
@project.command()
@click.argument("name")
def create(name):
"""Create a new component in the project."""
console.print(f"[bold cyan]Component {name} created.[/bold cyan]")
@project.command()
def status():
"""Check the project status."""
console.print("[bold yellow]All systems operational.[/bold yellow]")
Executing the Chat Interface
To run the interactive chat system:
python your_cli.py chat --project-path /path/to/project
Once the session starts, users can input commands like:
You: project init
You: project create "Homepage"
The output will be displayed in a well-formatted manner using Rich Markdown:
[bold green]Project initialized![/bold green]
[bold cyan]Component Homepage created.[/bold cyan]
Conclusion
By combining Click for command management, Rich for Markdown formatting, and Python introspection, we can build a powerful and interactive chat system for CLIs. This approach allows you to dynamically discover and execute commands while presenting output in an elegant, readable format.
Key Highlights:
If you're building a CLI and want to improve its interactivity, this approach will give you both flexibility and a great user experience.