""" CLI — user entry point. Typer interface with -i (input) and -o (output) options. """ from __future__ import annotations import asyncio import logging import os from dataclasses import asdict import dspy import typer from pydantic import ValidationError from rich.console import Console from rich.panel import Panel from rich.table import Table from prometheus.application.bootstrap import SyntheticBootstrap from prometheus.application.dto import OptimizationConfig, OptimizationResult from prometheus.application.evaluator import PromptEvaluator from prometheus.application.use_cases import OptimizePromptUseCase from prometheus.infrastructure.file_io import YamlPersistence from prometheus.infrastructure.judge_adapter import DSPyJudgeAdapter from prometheus.infrastructure.llm_adapter import DSPyLLMAdapter from prometheus.infrastructure.proposer_adapter import DSPyProposerAdapter from prometheus.infrastructure.synth_adapter import DSPySyntheticAdapter app = typer.Typer( name="prometheus", help="PROMETHEUS — Prompt evolution without reference data.", no_args_is_help=True, ) console = Console() @app.command() def optimize( input: str = typer.Option( ..., "-i", "--input", help="Path to input YAML config file.", exists=True, readable=True, ), output: str = typer.Option( "output.yaml", "-o", "--output", help="Path to output YAML result file.", ), verbose: bool = typer.Option( False, "-v", "--verbose", help="Print detailed progress.", ), max_retries: int = typer.Option( 3, "--max-retries", help="Max retry attempts for transient LLM errors (429, timeout, 5xx).", ), error_strategy: str = typer.Option( "retry", "--error-strategy", help="How to handle errors: skip | retry | abort.", ), max_concurrency: int = typer.Option( 5, "--max-concurrency", help="Max parallel LLM calls for minibatch execution and judging.", ), ) -> None: """Optimize a prompt without any reference data. Usage: prometheus optimize -i config.yaml -o result.yaml """ asyncio.run( _async_optimize(input, output, verbose, max_retries, error_strategy, max_concurrency) ) async def _async_optimize( input: str, output: str, verbose: bool, max_retries: int, error_strategy: str, max_concurrency: int, ) -> None: # Configure verbose logging if verbose: logging.basicConfig(level=logging.INFO, format="[PROMETHEUS] %(message)s") console.print( Panel.fit( "PROMETHEUS — Prompt Evolution Engine", subtitle="No reference data required", ) ) # 1. Load & validate config persistence = YamlPersistence() raw_config = persistence.read_config(input) # CLI flags override config file values raw_config.setdefault("max_retries", max_retries) raw_config.setdefault("error_strategy", error_strategy) raw_config.setdefault("max_concurrency", max_concurrency) raw_config["output_path"] = output raw_config["verbose"] = verbose try: config = OptimizationConfig.model_validate(raw_config) except ValidationError as exc: console.print("[bold red]Configuration error:[/bold red]\n") for err in exc.errors(): loc = " → ".join(str(l) for l in err["loc"]) console.print(f" [red]• {loc}: {err['msg']}[/red]") raise typer.Exit(code=1) from exc console.print(f"[dim]Task: {config.task_description[:80]}...[/dim]") console.print(f"[dim]Seed prompt: {config.seed_prompt[:80]}...[/dim]") # 2. Create per-model DSPy LM instances def _model_lm_kwargs( model_api_base: str | None, model_api_key_env: str | None, ) -> dict: """Build kwargs for dspy.LM, using per-model overrides with global fallback.""" kwargs: dict = {} api_base = model_api_base or config.api_base api_key_env = model_api_key_env or config.api_key_env if api_base: kwargs["api_base"] = api_base if api_key_env: kwargs["api_key"] = os.environ.get(api_key_env, "") return kwargs task_lm = dspy.LM( config.task_model, **_model_lm_kwargs(config.task_api_base, config.task_api_key_env), ) judge_lm = dspy.LM( config.judge_model, **_model_lm_kwargs(config.judge_api_base, config.judge_api_key_env), ) proposer_lm = dspy.LM( config.proposer_model, **_model_lm_kwargs(config.proposer_api_base, config.proposer_api_key_env), ) synth_lm = dspy.LM( config.synth_model, **_model_lm_kwargs(config.synth_api_base, config.synth_api_key_env), ) # 3. Build adapters (Dependency Injection — each gets its own LM + retry config) synth_adapter = DSPySyntheticAdapter(lm=synth_lm) llm_adapter = DSPyLLMAdapter( lm=task_lm, max_retries=config.max_retries, retry_delay_base=config.retry_delay_base, ) judge_adapter = DSPyJudgeAdapter( lm=judge_lm, max_retries=config.max_retries, retry_delay_base=config.retry_delay_base, max_concurrency=config.max_concurrency, ) proposer_adapter = DSPyProposerAdapter( lm=proposer_lm, max_retries=config.max_retries, retry_delay_base=config.retry_delay_base, ) bootstrap = SyntheticBootstrap(generator=synth_adapter, seed=config.seed) evaluator = PromptEvaluator( executor=llm_adapter, judge=judge_adapter, max_concurrency=config.max_concurrency, ) use_case = OptimizePromptUseCase( evaluator=evaluator, proposer=proposer_adapter, bootstrap=bootstrap, ) # 4. Execute with console.status("[bold green]Evolving prompt..."): result = await use_case.execute(config) # 5. Display results _display_result(result) # 6. Save _save_result(persistence, output, result) console.print(f"\n[green]Results saved to {output}[/green]") def _display_result(result: OptimizationResult) -> None: """Display a Rich summary in the terminal.""" console.print() console.print( Panel( f"[bold green]Optimized Prompt[/bold green]\n\n{result.optimized_prompt}", title="Result", ) ) table = Table(title="Metrics") table.add_column("Metric", style="cyan") table.add_column("Value", style="bold") table.add_row("Initial Score", f"{result.initial_score:.2f}") table.add_row("Final Score", f"{result.final_score:.2f}") table.add_row("Improvement", f"{result.improvement:+.2f}") table.add_row("Iterations", str(result.iterations_used)) table.add_row("LLM Calls", str(result.total_llm_calls)) console.print(table) def _save_result( persistence: YamlPersistence, path: str, result: OptimizationResult, ) -> None: """Save the result as YAML.""" persistence.write_result(path, asdict(result)) @app.command(hidden=True) def _help() -> None: """Internal placeholder to force multi-command Typer behavior.""" pass if __name__ == "__main__": app()