Build an LLM-Powered CLI
To run this notebook, click on the molab shield above or run the following command at the terminal:
uvx marimo edit --sandbox --mcp --no-token --watch https://github.com/ericmjl/llamabot/blob/main/docs/how-to/cli-powered-by-llm.py
import marimo as mo
How to Build an LLM-Powered CLI
Learn how to build a command-line interface that uses LLMs to generate structured outputs. This guide shows you how to create a CLI tool that automatically generates commit messages from git diffs using StructuredBot.
Prerequisites
Before you begin, ensure you have:
- Ollama installed and running locally: Visit ollama.ai to install
- Required Ollama model: Run
ollama pull gemma3n:latest(or another model that supports structured outputs) - Python 3.10+ with llamabot installed
- A git repository to test the CLI with
All llamabot models in this guide use the ollama_chat/ prefix for local execution.
Goal
By the end of this guide, you'll have built a CLI command that:
- Takes a git diff as input
- Uses StructuredBot to generate a conventional commit message
- Returns a validated, structured commit message
- Can be integrated into git hooks for automatic commit message generation
from enum import Enum
from pydantic import BaseModel, Field, model_validator
import llamabot as lmb
from llamabot.bot.structuredbot import StructuredBot
from llamabot.prompt_manager import prompt
Step 1: Define Your Data Schema
First, define the Pydantic model that represents your structured output. For commit messages, we'll use the conventional commit format.
class CommitType(str, Enum):
"""Type of commit following conventional commits."""
fix = "fix"
feat = "feat"
build = "build"
chore = "chore"
ci = "ci"
docs = "docs"
style = "style"
refactor = "refactor"
perf = "perf"
test = "test"
class DescriptionEntry(BaseModel):
"""A single bullet point in the commit body."""
txt: str = Field(
...,
description="A single bullet point describing one major change in the commit.",
)
@model_validator(mode="after")
def validate_description(self):
"""Validate description length."""
if len(self.txt) > 160:
raise ValueError(
"Description should be less than or equal to 160 characters."
)
return self
class CommitMessage(BaseModel):
"""Structured commit message following conventional commits format."""
commit_type: CommitType = Field(
...,
description="Type of change (fix, feat, docs, etc.)",
)
scope: str = Field(
...,
description="Scope of change (e.g., 'api', 'ui', 'auth')",
)
description: str = Field(
...,
description="Concise summary of what the commit accomplishes (present tense)",
)
body: list[DescriptionEntry] = Field(
default_factory=list,
description="Optional detailed explanation as bullet points",
)
breaking_change: bool = Field(
default=False,
description="Whether this commit introduces a breaking change",
)
Step 2: Create the StructuredBot
StructuredBot ensures the LLM output matches your Pydantic schema exactly. It automatically retries if validation fails.
@prompt("system")
def commitbot_sysprompt() -> str:
"""You are an expert software developer who writes excellent and accurate commit messages.
You will be given a git diff as input, and you will generate a structured commit message
following the conventional commits format. Ensure your output matches the provided schema exactly.
"""
commit_bot = StructuredBot(
system_prompt=commitbot_sysprompt(),
pydantic_model=CommitMessage,
model_name="ollama_chat/gemma3n:latest",
stream_target="none",
)
Step 3: Test the Bot
Let's test the bot with a sample git diff to see how it generates structured commit messages.
# Example git diff (in practice, you'd get this from `git diff --cached`)
sample_diff = """
diff --git a/src/api.py b/src/api.py
index 1234567..abcdefg 100644
--- a/src/api.py
+++ b/src/api.py
@@ -10,6 +10,8 @@ def get_user(user_id: int):
raise ValueError("User ID must be positive")
return db.query(User).filter(User.id == user_id).first()
+def create_user(name: str, email: str):
+ return db.add(User(name=name, email=email))
+
def delete_user(user_id: int):
db.query(User).filter(User.id == user_id).delete()
"""
# Generate commit message
commit_message = commit_bot(sample_diff)
commit_message
Step 4: View Observability with Spans
StructuredBot automatically creates spans for observability. Let's see what information is tracked.
# Display spans to see observability data
commit_bot.spans
The spans show:
- query: The input (git diff)
- model: Which model was used
- validation_attempts: How many times validation was attempted
- validation_success: Whether validation succeeded
- schema_fields: Fields in the Pydantic model
- duration_ms: How long the operation took
This observability helps you debug issues and understand bot behavior.
Step 5: Create the CLI Command
Now let's wrap this in a Typer CLI command that can be used from the terminal.
import subprocess
from pathlib import Path
import typer
app = typer.Typer()
@app.command()
def compose():
"""Generate a commit message from the current git diff."""
# Get the git diff
result = subprocess.run(
["git", "diff", "--cached"],
capture_output=True,
text=True,
)
if not result.stdout.strip():
typer.echo(
"No staged changes found. Stage some changes with `git add` first."
)
raise typer.Exit(1)
# Generate commit message
try:
commit_msg = commit_bot(result.stdout)
# Format the commit message
formatted = f"{commit_msg.commit_type.value}({commit_msg.scope}){': ' if commit_msg.breaking_change else ': '}{commit_msg.description}\n\n"
if commit_msg.body:
formatted += "\n".join(f"- {entry.txt}" for entry in commit_msg.body)
if commit_msg.breaking_change:
formatted += (
"\n\nBREAKING CHANGE: This commit introduces breaking changes."
)
typer.echo(formatted)
# Optionally write to .git/COMMIT_EDITMSG
commit_editmsg = Path(".git/COMMIT_EDITMSG")
if commit_editmsg.parent.exists():
commit_editmsg.write_text(formatted)
typer.echo(f"\nCommit message written to {commit_editmsg}")
except Exception as e:
typer.echo(f"Error generating commit message: {e}", err=True)
raise typer.Exit(1)
Step 6: Test the CLI
You can now use this CLI command. In a real implementation, you'd register it with your main CLI app. For testing, you can call the function directly.
Step 7: Integrate with Git Hooks (Optional)
To automatically generate commit messages, you can create a git hook:
# Create the hook
cat > .git/hooks/prepare-commit-msg << 'EOF'
#!/bin/sh
llamabot git compose
EOF
chmod +x .git/hooks/prepare-commit-msg
Now when you run git commit without a message, it will automatically generate one.
Summary
You've built an LLM-powered CLI that:
- Uses StructuredBot to ensure validated, structured outputs
- Integrates with git to generate commit messages automatically
- Provides observability through spans
- Handles validation retries automatically
Key Takeaways:
- Define your Pydantic schema first
- Use StructuredBot for guaranteed schema compliance
- Leverage spans for debugging and observability
- Wrap bots in CLI commands for easy terminal access