Spaces:
Runtime error
Runtime error
| """CLI entry point for the LLM Evaluation Framework. | |
| Commands: | |
| llm-eval run — Evaluate a system against a dataset | |
| llm-eval generate — Generate a synthetic QA dataset from documents | |
| llm-eval report — Print historical evaluation reports | |
| llm-eval compare — Compare two evaluation runs | |
| llm-eval calibrate — Run judge calibration from a labeled CSV | |
| Teaching note: Typer is Click with type hints — function arguments become | |
| CLI flags automatically. This is the pattern modern Python CLIs use. | |
| The key idea: each command is just an async function wrapped with asyncio.run(). | |
| """ | |
| import asyncio | |
| import json | |
| import sys | |
| from pathlib import Path | |
| from typing import Optional | |
| import typer | |
| from rich.console import Console | |
| from rich.table import Table | |
| from rich.panel import Panel | |
| from rich import print as rprint | |
| # Add src to path (when running from project root without pip install) | |
| sys.path.insert(0, str(Path(__file__).parent.parent / "src")) | |
| from eval_framework.config import get_settings, configure_logging | |
| from eval_framework.storage.database import EvalDatabase | |
| from eval_framework.evaluators import ( | |
| FaithfulnessEvaluator, | |
| RelevanceEvaluator, | |
| CompletenessEvaluator, | |
| HallucinationRateEvaluator, | |
| LatencyEvaluator, | |
| CostEvaluator, | |
| ) | |
| from eval_framework.utils.llm_client import get_llm_client | |
| from eval_framework.types import QAPair, SystemOutput, EvaluationMetric | |
| app = typer.Typer( | |
| name="llm-eval", | |
| help="LLM Evaluation Framework — measure AI quality like a pro", | |
| add_completion=False, | |
| ) | |
| console = Console() | |
| def _get_model_name(provider: str) -> str: | |
| """Return the configured model name for a provider.""" | |
| settings = get_settings() | |
| return { | |
| "openai": settings.openai_model, | |
| "anthropic": settings.anthropic_model, | |
| "groq": settings.groq_model, | |
| }.get(provider, provider) | |
| def _get_all_evaluators(provider: str = "groq"): | |
| """Build all evaluators using the configured LLM judge.""" | |
| from eval_framework.evaluators import ( | |
| ConcisenessEvaluator, | |
| CoherenceEvaluator, | |
| ToxicityEvaluator, | |
| ContextPrecisionEvaluator, | |
| ) | |
| client = get_llm_client(provider) | |
| model_name = _get_model_name(provider) | |
| return [ | |
| FaithfulnessEvaluator(client, model_name), | |
| RelevanceEvaluator(client, model_name), | |
| CompletenessEvaluator(client, model_name), | |
| HallucinationRateEvaluator(client, model_name), | |
| ConcisenessEvaluator(client, model_name), | |
| CoherenceEvaluator(client, model_name), | |
| ToxicityEvaluator(client, model_name), | |
| ContextPrecisionEvaluator(client, model_name), | |
| LatencyEvaluator(model_name=model_name), | |
| CostEvaluator(model_name=model_name), | |
| ] | |
| def run( | |
| dataset_file: Path = typer.Argument(..., help="Path to JSON dataset file"), | |
| system_name: str = typer.Option("system-under-test", "--name", "-n", help="System name for the report"), | |
| provider: str = typer.Option("groq", "--provider", "-p", help="LLM provider: groq, openai, or anthropic"), | |
| metrics: Optional[str] = typer.Option(None, "--metrics", "-m", help="Comma-separated metrics to run (default: all)"), | |
| concurrency: int = typer.Option(2, "--concurrency", "-c", help="Max concurrent judge calls (use 2 for Groq free tier, 5+ for paid)"), | |
| db_path: str = typer.Option("data/results.db", "--db", help="SQLite database path"), | |
| output: Optional[Path] = typer.Option(None, "--output", "-o", help="Save report JSON to file"), | |
| doc: Optional[Path] = typer.Option(None, "--doc", help="If set, build a real RAG system from this .txt or .pdf and evaluate it"), | |
| ): | |
| """Evaluate a system against a QA dataset. | |
| The dataset file should be a JSON array of objects with keys: | |
| question, answer, context (optional) | |
| By default uses a mock echo system (answers = reference answers). | |
| Pass --doc to evaluate a real RAG pipeline built on that document. | |
| Examples: | |
| llm-eval run data/rag_dataset.json --name baseline | |
| llm-eval run data/rag_dataset.json --doc data/knowledge_base.txt --name rag-v1 | |
| """ | |
| configure_logging() | |
| if not dataset_file.exists(): | |
| console.print(f"[red]Dataset file not found: {dataset_file}[/red]") | |
| raise typer.Exit(1) | |
| if doc and not doc.exists(): | |
| console.print(f"[red]Document not found: {doc}[/red]") | |
| raise typer.Exit(1) | |
| async def _run(): | |
| from eval_framework.judges.pipeline import EvaluationPipeline | |
| # Load dataset | |
| with open(dataset_file) as f: | |
| raw_data = json.load(f) | |
| dataset = [ | |
| QAPair( | |
| question=item["question"], | |
| answer=item["answer"], | |
| context=item.get("context"), | |
| ) | |
| for item in raw_data | |
| ] | |
| console.print(f"[green]Loaded {len(dataset)} examples from {dataset_file}[/green]") | |
| # Build the system under test | |
| if doc: | |
| from eval_framework.rag.pipeline import RAGPipeline | |
| console.print(f"[green]Building RAG index from {doc}...[/green]") | |
| with console.status("[bold green]Indexing document..."): | |
| rag = RAGPipeline( | |
| doc_path=doc, | |
| model_name=_get_model_name(provider), | |
| ).build() | |
| system_fn = rag.query | |
| console.print(f"[green]RAG pipeline ready[/green]") | |
| else: | |
| async def system_fn(qa: QAPair) -> SystemOutput: | |
| """Mock: echoes back the reference answer.""" | |
| await asyncio.sleep(0.01) | |
| return SystemOutput( | |
| answer=qa.answer, | |
| latency_ms=500, | |
| cost_usd=0.002, | |
| model="mock-echo", | |
| ) | |
| console.print( | |
| "[yellow]Using mock echo system (answers = reference answers). " | |
| "Pass --doc <file> to evaluate a real RAG pipeline.[/yellow]" | |
| ) | |
| # Build evaluators | |
| all_evaluators = _get_all_evaluators(provider) | |
| if metrics: | |
| wanted = set(m.strip() for m in metrics.split(",")) | |
| all_evaluators = [e for e in all_evaluators if e.metric.value in wanted] | |
| console.print(f"[green]Running {len(all_evaluators)} evaluators[/green]") | |
| pipeline = EvaluationPipeline(evaluators=all_evaluators, concurrency=concurrency) | |
| with console.status(f"[bold green]Evaluating {len(dataset)} examples..."): | |
| report = await pipeline.run( | |
| system=system_fn, | |
| dataset=dataset, | |
| system_name=system_name, | |
| ) | |
| # Save to database | |
| db = EvalDatabase(db_path) | |
| db.save_report(report) | |
| console.print(f"[green]Saved report {report.id} to {db_path}[/green]") | |
| # Print results table | |
| _print_report_table(report) | |
| # Optionally save JSON | |
| if output: | |
| with open(output, "w") as f: | |
| json.dump( | |
| { | |
| "id": report.id, | |
| "system_name": report.system_name, | |
| "timestamp": report.timestamp.isoformat(), | |
| "summary_scores": { | |
| k.value if hasattr(k, "value") else k: v | |
| for k, v in report.summary_scores.items() | |
| }, | |
| "total_examples": report.total_examples_evaluated, | |
| "total_cost": report.total_cost, | |
| }, | |
| f, | |
| indent=2, | |
| ) | |
| console.print(f"[green]Report JSON saved to {output}[/green]") | |
| asyncio.run(_run()) | |
| def generate( | |
| input_file: Path = typer.Argument(..., help="Text file or directory of .txt files to generate from"), | |
| output_file: Path = typer.Option(Path("dataset.json"), "--output", "-o", help="Output JSON file"), | |
| n_questions: int = typer.Option(3, "--n-questions", "-n", help="Questions per chunk"), | |
| provider: str = typer.Option("groq", "--provider", "-p", help="LLM provider: groq, openai, or anthropic"), | |
| no_validate: bool = typer.Option(False, "--no-validate", help="Skip quality validation pass"), | |
| ): | |
| """Generate a synthetic QA dataset from text documents. | |
| Example: | |
| llm-eval generate corpus.txt --n-questions 5 --output my_dataset.json | |
| """ | |
| configure_logging() | |
| async def _run(): | |
| from eval_framework.dataset.generator import DatasetGenerator | |
| client = get_llm_client(provider) | |
| generator = DatasetGenerator( | |
| llm_client=client, | |
| validate=not no_validate, | |
| ) | |
| # Load documents | |
| documents = [] | |
| if input_file.is_dir(): | |
| for txt_file in input_file.glob("*.txt"): | |
| documents.append(txt_file.read_text(encoding="utf-8")) | |
| console.print(f"[green]Loaded {len(documents)} .txt files from {input_file}[/green]") | |
| else: | |
| documents.append(input_file.read_text(encoding="utf-8")) | |
| console.print(f"[green]Loaded {input_file}[/green]") | |
| with console.status("[bold green]Generating dataset..."): | |
| examples = await generator.generate_from_documents( | |
| documents=documents, | |
| n_questions_per_chunk=n_questions, | |
| ) | |
| # Save as JSON | |
| output_data = [ | |
| { | |
| "question": ex.qa_pair.question, | |
| "answer": ex.qa_pair.answer, | |
| "context": ex.qa_pair.context, | |
| "difficulty": ex.difficulty, | |
| "tags": ex.tags, | |
| "id": ex.id, | |
| } | |
| for ex in examples | |
| ] | |
| with open(output_file, "w") as f: | |
| json.dump(output_data, f, indent=2) | |
| console.print(f"[green]Generated {len(examples)} QA pairs → {output_file}[/green]") | |
| asyncio.run(_run()) | |
| def report( | |
| system_name: Optional[str] = typer.Option(None, "--system", "-s", help="Filter by system name"), | |
| db_path: str = typer.Option("data/results.db", "--db", help="SQLite database path"), | |
| limit: int = typer.Option(20, "--limit", "-l", help="Max runs to show"), | |
| ): | |
| """List historical evaluation runs.""" | |
| db = EvalDatabase(db_path) | |
| if system_name: | |
| runs = db.get_runs_for_system(system_name) | |
| else: | |
| runs = db.list_all_runs(limit=limit) | |
| if not runs: | |
| console.print("[yellow]No evaluation runs found.[/yellow]") | |
| return | |
| table = Table(title="Evaluation History", show_header=True, header_style="bold magenta") | |
| table.add_column("System", style="cyan") | |
| table.add_column("Timestamp", style="dim") | |
| table.add_column("Examples") | |
| table.add_column("Faithfulness") | |
| table.add_column("Relevance") | |
| table.add_column("Completeness") | |
| table.add_column("Run ID", style="dim") | |
| for run in runs: | |
| scores = run["summary_scores"] | |
| table.add_row( | |
| run["system_name"], | |
| run["timestamp"][:19], | |
| str(run["total_examples"]), | |
| f"{scores.get('faithfulness', '-'):.2f}" if scores.get('faithfulness') is not None else "-", | |
| f"{scores.get('relevance', '-'):.2f}" if scores.get('relevance') is not None else "-", | |
| f"{scores.get('completeness', '-'):.2f}" if scores.get('completeness') is not None else "-", | |
| run["id"][:8] + "...", | |
| ) | |
| console.print(table) | |
| def compare( | |
| run_id_1: str = typer.Argument(..., help="First run ID"), | |
| run_id_2: str = typer.Argument(..., help="Second run ID"), | |
| db_path: str = typer.Option("data/results.db", "--db", help="SQLite database path"), | |
| ): | |
| """Compare two evaluation runs side-by-side.""" | |
| db = EvalDatabase(db_path) | |
| try: | |
| comparison = db.compare_runs(run_id_1, run_id_2) | |
| except ValueError as e: | |
| console.print(f"[red]{e}[/red]") | |
| raise typer.Exit(1) | |
| console.print(Panel( | |
| f"[bold]Run 1:[/bold] {comparison['run1']['system']} @ {comparison['run1']['timestamp'][:19]}\n" | |
| f"[bold]Run 2:[/bold] {comparison['run2']['system']} @ {comparison['run2']['timestamp'][:19]}", | |
| title="Comparing Evaluation Runs", | |
| )) | |
| table = Table(show_header=True, header_style="bold magenta") | |
| table.add_column("Metric", style="cyan") | |
| table.add_column("Run 1") | |
| table.add_column("Run 2") | |
| table.add_column("Delta") | |
| table.add_column("Change") | |
| for metric, data in comparison["metrics"].items(): | |
| delta = data["delta"] | |
| delta_color = "green" if delta > 0 else "red" if delta < 0 else "dim" | |
| arrow = "▲" if delta > 0 else "▼" if delta < 0 else "─" | |
| table.add_row( | |
| metric, | |
| f"{data['run1_score']:.3f}", | |
| f"{data['run2_score']:.3f}", | |
| f"[{delta_color}]{delta:+.3f}[/{delta_color}]", | |
| f"[{delta_color}]{arrow}[/{delta_color}]", | |
| ) | |
| console.print(table) | |
| def rag_eval( | |
| doc_path: Path = typer.Argument(..., help="Path to .txt or .pdf knowledge base"), | |
| dataset_file: Path = typer.Option(Path("data/rag_dataset.json"), "--dataset", "-d", help="QA dataset JSON"), | |
| system_name: str = typer.Option("rag-langchain-groq", "--name", "-n", help="System name for the report"), | |
| concurrency: int = typer.Option(2, "--concurrency", "-c", help="Max concurrent Groq calls"), | |
| db_path: str = typer.Option("data/results.db", "--db", help="SQLite database path"), | |
| chunk_size: int = typer.Option(500, "--chunk-size", help="Document chunk size in characters"), | |
| top_k: int = typer.Option(3, "--top-k", help="Number of chunks to retrieve per question"), | |
| ): | |
| """Evaluate a real RAG system built on a document. | |
| Builds a LangChain + FAISS + Groq RAG pipeline on your document, | |
| runs it against the dataset, evaluates every answer, and saves results. | |
| Examples: | |
| llm-eval rag-eval data/knowledge_base.txt | |
| llm-eval rag-eval my_report.pdf --name rag-v2 --top-k 5 | |
| """ | |
| configure_logging() | |
| if not doc_path.exists(): | |
| console.print(f"[red]Document not found: {doc_path}[/red]") | |
| raise typer.Exit(1) | |
| if not dataset_file.exists(): | |
| console.print(f"[red]Dataset not found: {dataset_file}[/red]") | |
| raise typer.Exit(1) | |
| async def _run(): | |
| import json | |
| from eval_framework.rag.pipeline import RAGPipeline | |
| from eval_framework.judges.pipeline import EvaluationPipeline | |
| from eval_framework.evaluators.conciseness import ConcisenessEvaluator | |
| from eval_framework.evaluators.coherence import CoherenceEvaluator | |
| settings = get_settings() | |
| model_name = _get_model_name("groq") | |
| # Build RAG system | |
| console.print(f"[green]Building RAG index from {doc_path}...[/green]") | |
| rag = RAGPipeline( | |
| doc_path=doc_path, | |
| chunk_size=chunk_size, | |
| top_k=top_k, | |
| model_name=model_name, | |
| ).build() | |
| # Load dataset | |
| with open(dataset_file) as f: | |
| raw_data = json.load(f) | |
| dataset = [ | |
| QAPair( | |
| question=item["question"], | |
| answer=item["answer"], | |
| context=item.get("context"), | |
| ) | |
| for item in raw_data | |
| ] | |
| console.print(f"[green]Loaded {len(dataset)} questions[/green]") | |
| # Build evaluators | |
| client = get_llm_client("groq") | |
| evaluators = [ | |
| FaithfulnessEvaluator(client, model_name), | |
| RelevanceEvaluator(client, model_name), | |
| CompletenessEvaluator(client, model_name), | |
| HallucinationRateEvaluator(client, model_name), | |
| ConcisenessEvaluator(client, model_name), | |
| CoherenceEvaluator(client, model_name), | |
| LatencyEvaluator(model_name=model_name), | |
| CostEvaluator(model_name=model_name), | |
| ] | |
| pipeline = EvaluationPipeline(evaluators=evaluators, concurrency=concurrency) | |
| with console.status(f"[bold green]Evaluating {len(dataset)} questions..."): | |
| report = await pipeline.run( | |
| system=rag.query, | |
| dataset=dataset, | |
| system_name=system_name, | |
| ) | |
| # Save to database | |
| db = EvalDatabase(db_path) | |
| db.save_report(report) | |
| console.print(f"[green]Saved report {report.id[:8]}... to {db_path}[/green]") | |
| _print_report_table(report) | |
| asyncio.run(_run()) | |
| def _print_report_table(report) -> None: | |
| """Print a formatted summary table for an evaluation report.""" | |
| table = Table( | |
| title=f"Evaluation Results: {report.system_name}", | |
| show_header=True, | |
| header_style="bold magenta", | |
| ) | |
| table.add_column("Metric", style="cyan", min_width=20) | |
| table.add_column("Score", min_width=10) | |
| table.add_column("Examples Evaluated", min_width=20) | |
| table.add_column("Rating", min_width=12) | |
| for metric, score in report.summary_scores.items(): | |
| metric_name = metric.value if hasattr(metric, 'value') else metric | |
| n_examples = len(report.results.get(metric, [])) | |
| if score >= 0.8: | |
| rating = "[green]EXCELLENT[/green]" | |
| elif score >= 0.6: | |
| rating = "[yellow]GOOD[/yellow]" | |
| elif score >= 0.4: | |
| rating = "[orange1]FAIR[/orange1]" | |
| else: | |
| rating = "[red]POOR[/red]" | |
| table.add_row(metric_name, f"{score:.3f}", str(n_examples), rating) | |
| console.print(table) | |
| console.print( | |
| f"\n[dim]Run ID: {report.id} | " | |
| f"Total cost: ${report.total_cost:.4f} | " | |
| f"Examples: {report.total_examples_evaluated}[/dim]" | |
| ) | |
| if __name__ == "__main__": | |
| app() | |