"""CLI for model forge operations.""" from __future__ import annotations from pathlib import Path import typer from companion.config import load_config from companion.forge.extract import TrainingDataExtractor from companion.forge.reload import get_model_status, reload_model from companion.forge.train import train as train_model app = typer.Typer(help="Companion model forge - training pipeline") @app.command() def extract( output: Path = typer.Option( Path("~/.companion/training_data/extracted.jsonl"), help="Output JSONL file path", ), ) -> None: """Extract training examples from vault.""" config = load_config() typer.echo("Scanning vault for reflection examples...") extractor = TrainingDataExtractor(config) examples = extractor.extract() if not examples: typer.echo("No reflection examples found in vault.") typer.echo( "Try adding tags like #reflection, #insight, or #learning to your notes." ) raise typer.Exit(1) # Save to JSONL output = output.expanduser() output.parent.mkdir(parents=True, exist_ok=True) count = extractor.save_to_jsonl(output) stats = extractor.get_stats() typer.echo(f"\nExtracted {count} training examples:") typer.echo(f" - Average length: {stats.get('avg_length', 0)} chars") if stats.get("top_tags"): typer.echo( f" - Top tags: {', '.join(f'{tag}({cnt})' for tag, cnt in stats['top_tags'][:5])}" ) typer.echo(f"\nSaved to: {output}") @app.command() def status() -> None: """Check model status.""" config = load_config() model_status = get_model_status(config) typer.echo(f"Model Status:") typer.echo(f" Path: {model_status['path']}") typer.echo(f" Exists: {'Yes' if model_status['exists'] else 'No'}") if model_status["exists"]: typer.echo(f" Type: {model_status['type']}") typer.echo(f" Size: {model_status['size_mb']} MB") @app.command() def reload( model_path: Path = typer.Argument( ..., help="Path to new model directory or GGUF file", ), no_backup: bool = typer.Option( False, "--no-backup", help="Skip backing up current model", ), ) -> None: """Reload model with a new fine-tuned version.""" config = load_config() model_path = model_path.expanduser() try: active_path = reload_model(config, model_path, backup=not no_backup) typer.echo(f"Model reloaded successfully: {active_path}") except FileNotFoundError as e: typer.echo(f"Error: {e}") raise typer.Exit(1) @app.command() def train( data: Path = typer.Option( Path("~/.companion/training_data/extracted.jsonl"), help="Path to training data JSONL", ), output: Path = typer.Option( Path("~/.companion/training"), help="Output directory for checkpoints", ), epochs: int = typer.Option(3, help="Number of training epochs"), lr: float = typer.Option(2e-4, help="Learning rate"), ) -> None: """Train model using QLoRA fine-tuning.""" data = data.expanduser() output = output.expanduser() if not data.exists(): typer.echo(f"Training data not found: {data}") typer.echo("Run 'forge extract' first to generate training data.") raise typer.Exit(1) try: final_path = train_model( data_path=data, output_dir=output, num_epochs=epochs, learning_rate=lr, ) typer.echo(f"\nTraining complete! Model saved to: {final_path}") typer.echo("\nTo use this model:") typer.echo(f" forge reload {final_path}") except Exception as e: typer.echo(f"Training failed: {e}") raise typer.Exit(1) if __name__ == "__main__": app()