CLI Application Example¶
Source:
examples/08_cli_application.py
Demonstrates building a production-quality CLI application with Amplifier. Covers application architecture patterns, session lifecycle, error handling, logging, and configuration management.
Time to value: 15 minutes
What You'll Learn¶
- Application architecture patterns with Amplifier
- Proper session lifecycle management
- Error handling and recovery
- Logging and observability
- Configuration management
- Building reusable application classes
Real-World Use Cases¶
- Internal developer tools
- Data analysis assistants
- Code review helpers
- Automation scripts
Architecture Overview¶
The example structures an Amplifier app in four sections:
- Configuration management — Load from environment/config files, validate
- Application class — Encapsulate bundle/session lifecycle
- CLI interface — Interactive and single-prompt modes
- Entry point — Argument handling and startup
Section 1: Configuration Management¶
from dataclasses import dataclass
from pathlib import Path
@dataclass
class AppConfig:
"""Application configuration loaded from environment."""
# LLM Provider
provider_bundle: str = "anthropic-sonnet.yaml"
api_key: str | None = None
# Application Settings
log_level: str = "INFO"
storage_path: Path = Path.home() / ".amplifier" / "app_sessions"
@classmethod
def from_env(cls) -> "AppConfig":
return cls(
provider_bundle=os.getenv("PROVIDER", "anthropic-sonnet.yaml"),
api_key=os.getenv("ANTHROPIC_API_KEY"),
log_level=os.getenv("LOG_LEVEL", "INFO"),
)
def validate(self) -> None:
if not self.api_key:
raise ValueError("ANTHROPIC_API_KEY not set")
if not self.storage_path.exists():
self.storage_path.mkdir(parents=True, exist_ok=True)
In production, extend from_env() to load from .amplifier/settings.yaml, AWS Secrets Manager, or CLI arguments.
Section 2: Application Class¶
class AmplifierApp:
"""Encapsulates bundle management, session lifecycle, and error handling."""
def __init__(self, config: AppConfig):
self.config = config
self.session = None
self.logger = self._setup_logging()
def _setup_logging(self) -> logging.Logger:
logging.basicConfig(
level=getattr(logging, self.config.log_level),
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
handlers=[logging.StreamHandler(sys.stdout)],
)
return logging.getLogger("amplifier_app")
async def initialize(self) -> None:
"""Initialize the application and create session."""
# Load foundation bundle
foundation_path = Path(__file__).parent.parent
foundation = await load_bundle(str(foundation_path))
# Load provider
provider_path = foundation_path / "providers" / self.config.provider_bundle
provider = await load_bundle(str(provider_path))
# Build tools config
tools_config = Bundle(
name="app-tools",
version="1.0.0",
tools=[
{
"module": "tool-filesystem",
"source": "git+https://github.com/microsoft/amplifier-module-tool-filesystem@main",
},
{
"module": "tool-bash",
"source": "git+https://github.com/microsoft/amplifier-module-tool-bash@main",
},
],
)
# Compose and prepare
composed = foundation.compose(provider).compose(tools_config)
prepared = await composed.prepare()
# Create session (note: not using context manager here for lifecycle control)
self.session = await prepared.create_session()
async def execute(self, prompt: str) -> str:
"""Execute a prompt. Raises RuntimeError if not initialized."""
if not self.session:
raise RuntimeError("Session not initialized. Call initialize() first.")
return await self.session.execute(prompt)
async def shutdown(self) -> None:
"""Gracefully release session resources."""
if self.session:
await self.session.cleanup()
async def __aenter__(self):
await self.initialize()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.shutdown()
Key Patterns¶
Lifecycle control without context manager: create_session() is called without async with when the session needs to outlive a single block. shutdown() must explicitly call session.cleanup().
Context manager support: __aenter__/__aexit__ enables async with AmplifierApp(config) as app: for clean lifecycle in the entry point.
Section 3: CLI Interface¶
async def run_interactive_cli(app: AmplifierApp):
"""Multi-turn interactive REPL."""
print("Amplifier CLI App - Interactive Mode")
print("Type your prompts, or 'quit' to exit.\n")
while True:
try:
prompt = input("\nYou: ")
if prompt.lower() in ("quit", "exit", "q"):
break
if not prompt.strip():
continue
response = await app.execute(prompt)
print(f"\nAgent: {response}")
except KeyboardInterrupt:
break
except Exception as e:
print(f"\nError: {e}")
print("The session is still active. You can continue.")
async def run_single_prompt(app: AmplifierApp, prompt: str):
"""Non-interactive single-prompt mode."""
response = await app.execute(prompt)
print(response)
Error resilience: Errors in execute() are caught in the REPL loop so the session survives individual failures. The session is still usable after a tool error or provider timeout.
Section 4: Entry Point¶
async def main():
# Load and validate configuration
try:
config = AppConfig.from_env()
config.validate()
except Exception as e:
print(f"Configuration error: {e}")
return 1
# Initialize and run
async with AmplifierApp(config) as app:
if len(sys.argv) > 1:
prompt = " ".join(sys.argv[1:])
await run_single_prompt(app, prompt)
else:
await run_interactive_cli(app)
return 0
if __name__ == "__main__":
exit_code = asyncio.run(main())
Dual mode via sys.argv: No argument parsing library needed — sys.argv[1:] provides single-prompt mode; no args defaults to interactive.
Running the Example¶
# Set API key
export ANTHROPIC_API_KEY=sk-ant-...
# Interactive mode
python examples/08_cli_application.py
# Single-prompt mode
python examples/08_cli_application.py "Summarize this directory"
# Custom log level
LOG_LEVEL=DEBUG python examples/08_cli_application.py
Adapting for Production¶
Replace the hard-coded foundation_path with your bundle's actual location:
# Point to your bundle
foundation = await load_bundle("git+https://github.com/your-org/your-bundle@main")
Add persistent context for sessions that survive process restarts:
# Use context-persistent instead of context-simple
from amplifier_foundation import Bundle
persistence_overlay = Bundle(
name="persistence",
version="1.0.0",
session={
"context": {
"module": "context-persistent",
"source": "git+https://github.com/microsoft/amplifier-module-context-persistent@main",
"config": {"persist_dir": str(config.storage_path)},
}
},
)
composed = foundation.compose(provider).compose(tools_config).compose(persistence_overlay)
See Also¶
- Common Patterns — Bundle composition patterns
- Sessions — Session lifecycle reference
- AmplifierSession API — Constructor and methods