Spaces:
Build error
Build error
| """Evaluation CLI commands.""" | |
| from __future__ import annotations | |
| from collections.abc import Callable | |
| import click | |
| from .main import main | |
| def evals() -> None: | |
| """Memory evaluation commands. | |
| \b | |
| Examples: | |
| headroom evals memory Run LoCoMo memory evaluation | |
| headroom evals memory-v2 Run V2 evaluation with LLM-controlled tools | |
| """ | |
| pass | |
| def memory_eval( | |
| n_conversations: int | None, | |
| categories: str | None, | |
| include_adversarial: bool, | |
| top_k: int, | |
| f1_threshold: float, | |
| answer_model: str | None, | |
| llm_judge: bool, | |
| judge_provider: str, | |
| judge_model: str, | |
| output: str | None, | |
| no_extract: bool, | |
| extraction_model: str, | |
| pass_all: bool, | |
| parallel: int, | |
| debug: bool, | |
| ) -> None: | |
| """Run LoCoMo memory evaluation benchmark. | |
| \b | |
| LoCoMo (Long-term Conversational Memory) tests memory across: | |
| - Single-hop questions (simple fact recall) | |
| - Temporal questions (time-based) | |
| - Multi-hop questions (reasoning across memories) | |
| - Open-domain questions (interpretation required) | |
| \b | |
| Examples: | |
| headroom evals memory -n 3 | |
| headroom evals memory --answer-model gpt-4o --llm-judge | |
| """ | |
| _run_memory_eval( | |
| n_conversations=n_conversations, | |
| categories=categories, | |
| include_adversarial=include_adversarial, | |
| top_k=top_k, | |
| f1_threshold=f1_threshold, | |
| answer_model=answer_model, | |
| llm_judge=llm_judge, | |
| judge_provider=judge_provider, | |
| judge_model=judge_model, | |
| output=output, | |
| no_extract=no_extract, | |
| extraction_model=extraction_model, | |
| pass_all=pass_all, | |
| parallel=parallel, | |
| debug=debug, | |
| ) | |
| def memory_eval_v2( | |
| n_conversations: int | None, | |
| categories: str | None, | |
| include_adversarial: bool, | |
| f1_threshold: float, | |
| save_model: str, | |
| answer_model: str, | |
| max_results: int, | |
| no_graph: bool, | |
| llm_judge: bool, | |
| judge_model: str, | |
| output: str | None, | |
| parallel: int, | |
| debug: bool, | |
| ) -> None: | |
| """Run LoCoMo V2 evaluation with LLM-controlled memory tools. | |
| \b | |
| This evaluator tests the new architecture where: | |
| - LLM decides what to save (memory_save tool) | |
| - LLM decides when to search (memory_search tool) | |
| - Graph relationships enable multi-hop reasoning | |
| \b | |
| Examples: | |
| headroom evals memory-v2 -n 3 | |
| headroom evals memory-v2 --answer-model gpt-4o --save-model gpt-4o-mini | |
| """ | |
| _run_memory_eval_v2( | |
| n_conversations=n_conversations, | |
| categories=categories, | |
| include_adversarial=include_adversarial, | |
| f1_threshold=f1_threshold, | |
| save_model=save_model, | |
| answer_model=answer_model, | |
| max_results=max_results, | |
| no_graph=no_graph, | |
| llm_judge=llm_judge, | |
| judge_model=judge_model, | |
| output=output, | |
| parallel=parallel, | |
| debug=debug, | |
| ) | |
| # ----------------------------------------------------------------------------- | |
| # Backwards compatibility: old command names (hidden) | |
| # ----------------------------------------------------------------------------- | |
| def memory_eval_compat( | |
| n_conversations: int | None, | |
| categories: str | None, | |
| include_adversarial: bool, | |
| top_k: int, | |
| f1_threshold: float, | |
| answer_model: str | None, | |
| llm_judge: bool, | |
| judge_provider: str, | |
| judge_model: str, | |
| output: str | None, | |
| no_extract: bool, | |
| extraction_model: str, | |
| pass_all: bool, | |
| parallel: int, | |
| debug: bool, | |
| ) -> None: | |
| """Deprecated: Use 'headroom evals memory' instead.""" | |
| click.echo("Note: 'memory-eval' is deprecated. Use 'headroom evals memory'", err=True) | |
| _run_memory_eval( | |
| n_conversations=n_conversations, | |
| categories=categories, | |
| include_adversarial=include_adversarial, | |
| top_k=top_k, | |
| f1_threshold=f1_threshold, | |
| answer_model=answer_model, | |
| llm_judge=llm_judge, | |
| judge_provider=judge_provider, | |
| judge_model=judge_model, | |
| output=output, | |
| no_extract=no_extract, | |
| extraction_model=extraction_model, | |
| pass_all=pass_all, | |
| parallel=parallel, | |
| debug=debug, | |
| ) | |
| def memory_eval_v2_compat( | |
| n_conversations: int | None, | |
| categories: str | None, | |
| include_adversarial: bool, | |
| f1_threshold: float, | |
| save_model: str, | |
| answer_model: str, | |
| max_results: int, | |
| no_graph: bool, | |
| llm_judge: bool, | |
| judge_model: str, | |
| output: str | None, | |
| parallel: int, | |
| debug: bool, | |
| ) -> None: | |
| """Deprecated: Use 'headroom evals memory-v2' instead.""" | |
| click.echo("Note: 'memory-eval-v2' is deprecated. Use 'headroom evals memory-v2'", err=True) | |
| _run_memory_eval_v2( | |
| n_conversations=n_conversations, | |
| categories=categories, | |
| include_adversarial=include_adversarial, | |
| f1_threshold=f1_threshold, | |
| save_model=save_model, | |
| answer_model=answer_model, | |
| max_results=max_results, | |
| no_graph=no_graph, | |
| llm_judge=llm_judge, | |
| judge_model=judge_model, | |
| output=output, | |
| parallel=parallel, | |
| debug=debug, | |
| ) | |
| # ----------------------------------------------------------------------------- | |
| # Implementation functions (shared by new and compat commands) | |
| # ----------------------------------------------------------------------------- | |
| def _run_memory_eval( | |
| *, | |
| n_conversations: int | None, | |
| categories: str | None, | |
| include_adversarial: bool, | |
| top_k: int, | |
| f1_threshold: float, | |
| answer_model: str | None, | |
| llm_judge: bool, | |
| judge_provider: str, | |
| judge_model: str, | |
| output: str | None, | |
| no_extract: bool, | |
| extraction_model: str, | |
| pass_all: bool, | |
| parallel: int, | |
| debug: bool, | |
| ) -> None: | |
| """Run LoCoMo memory evaluation.""" | |
| # Suppress noisy pydantic warnings from litellm | |
| import warnings | |
| warnings.filterwarnings("ignore", message=".*Pydantic serializer warnings.*") | |
| warnings.filterwarnings("ignore", category=UserWarning, module="pydantic") | |
| try: | |
| from headroom.evals.memory import ( | |
| LoCoMoEvaluator, | |
| MemoryEvalConfig, | |
| create_anthropic_judge, | |
| create_litellm_judge, | |
| create_openai_judge, | |
| simple_judge, | |
| ) | |
| from headroom.memory import MemoryConfig | |
| except ImportError as e: | |
| click.echo("Error: Memory eval dependencies not installed.") | |
| click.echo("Run: pip install headroom[memory,evals]") | |
| click.echo(f"Details: {e}") | |
| raise SystemExit(1) from None | |
| import asyncio | |
| # Build configuration | |
| parsed_categories = None | |
| if categories: | |
| parsed_categories = [int(c) for c in categories.split(",")] | |
| memory_config = MemoryConfig() | |
| eval_config = MemoryEvalConfig( | |
| n_conversations=n_conversations, | |
| categories=parsed_categories, | |
| skip_adversarial=not include_adversarial, | |
| top_k_memories=top_k, | |
| llm_judge_enabled=llm_judge, | |
| llm_judge_model=judge_model, | |
| memory_config=memory_config, | |
| f1_threshold=f1_threshold, | |
| extract_memories=not no_extract, | |
| extraction_model=extraction_model, | |
| pass_all_memories=pass_all, | |
| parallel_workers=parallel, | |
| debug=debug, | |
| ) | |
| # Create answer function based on provider | |
| answer_fn = None | |
| if answer_model: | |
| try: | |
| import litellm | |
| def answer_fn(question: str, memories: list[str]) -> str: | |
| if not memories: | |
| return "I don't have information about that." | |
| # Format memories - use all if pass_all, else top 10 | |
| context = "\n".join(f"- {m}" for m in memories) | |
| prompt = f"""You are answering questions about a conversation between two people based on extracted memories/facts. | |
| ## Memories from the conversation: | |
| {context} | |
| ## Question: {question} | |
| ## Instructions: | |
| 1. Find the specific fact(s) in the memories that answer this question | |
| 2. Answer with JUST the key information requested - be concise | |
| 3. For "when" questions: give the specific date if mentioned (e.g., "7 May 2023", "2022") | |
| 4. For "what" questions: give the specific thing/action | |
| 5. For "who" questions: give the name | |
| 6. If the exact answer is in the memories, use those exact words/dates | |
| 7. If you cannot find the answer, say "Information not found" | |
| ## Answer (be concise - just the facts):""" | |
| response = litellm.completion( | |
| model=answer_model, | |
| messages=[{"role": "user", "content": prompt}], | |
| temperature=0.0, | |
| max_tokens=150, | |
| ) | |
| return response.choices[0].message.content or "" | |
| except ImportError: | |
| click.echo("Error: litellm required for --answer-model. Run: pip install litellm") | |
| raise SystemExit(1) from None | |
| # Create LLM judge if enabled | |
| llm_judge_fn: Callable[[str, str, str], tuple[float, str]] | None = None | |
| if llm_judge: | |
| # Use answer model for judge if not explicitly set | |
| effective_judge_model = judge_model | |
| if answer_model and judge_model == "gpt-4o": | |
| effective_judge_model = answer_model # Match the answer model | |
| if judge_provider == "simple": | |
| llm_judge_fn = simple_judge | |
| elif judge_provider == "openai": | |
| llm_judge_fn = create_openai_judge(model=effective_judge_model) | |
| elif judge_provider == "anthropic": | |
| llm_judge_fn = create_anthropic_judge(model=effective_judge_model) | |
| else: | |
| llm_judge_fn = create_litellm_judge(model=effective_judge_model) | |
| # Determine judge info for display | |
| judge_info = "DISABLED" | |
| if llm_judge: | |
| if judge_provider == "simple": | |
| judge_info = "ENABLED (rule-based F1)" | |
| else: | |
| jm = judge_model | |
| if answer_model and judge_model == "gpt-4o": | |
| jm = answer_model | |
| judge_info = f"ENABLED ({judge_provider}: {jm})" | |
| extract_info = f"ENABLED ({extraction_model})" if not no_extract else "DISABLED (raw dialogue)" | |
| retrieval_info = "ALL memories (Path A)" if pass_all else f"Top-{top_k} retrieval" | |
| click.echo(f""" | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| β HEADROOM MEMORY EVALUATION β | |
| β LoCoMo Benchmark β | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| Configuration: | |
| Conversations: {n_conversations or "all"} | |
| Categories: {parsed_categories or "[1,2,3,4]"} | |
| Retrieval: {retrieval_info} | |
| Memory Extract: {extract_info} | |
| Answer Model: {answer_model or "default (retrieval)"} | |
| LLM Judge: {judge_info} | |
| Parallelism: {parallel} workers | |
| Debug: {"ENABLED" if debug else "DISABLED"} | |
| Running evaluation... | |
| """) | |
| # Run evaluation | |
| evaluator = LoCoMoEvaluator( | |
| answer_fn=answer_fn, | |
| llm_judge_fn=llm_judge_fn, | |
| config=eval_config, | |
| ) | |
| try: | |
| result = asyncio.run(evaluator.run()) | |
| except KeyboardInterrupt: | |
| click.echo("\nEvaluation interrupted.") | |
| raise SystemExit(1) from None | |
| # Print results | |
| click.echo(result.summary()) | |
| # Save results if output path specified | |
| if output: | |
| result.save(output) | |
| click.echo(f"\nResults saved to: {output}") | |
| def _run_memory_eval_v2( | |
| *, | |
| n_conversations: int | None, | |
| categories: str | None, | |
| include_adversarial: bool, | |
| f1_threshold: float, | |
| save_model: str, | |
| answer_model: str, | |
| max_results: int, | |
| no_graph: bool, | |
| llm_judge: bool, | |
| judge_model: str, | |
| output: str | None, | |
| parallel: int, | |
| debug: bool, | |
| ) -> None: | |
| """Run LoCoMo V2 memory evaluation (LLM-controlled tools).""" | |
| # Suppress noisy pydantic warnings from litellm | |
| import warnings | |
| warnings.filterwarnings("ignore", message=".*Pydantic serializer warnings.*") | |
| warnings.filterwarnings("ignore", category=UserWarning, module="pydantic") | |
| try: | |
| from headroom.evals.memory import ( | |
| LoCoMoEvaluatorV2, | |
| MemoryEvalConfigV2, | |
| ) | |
| except ImportError as e: | |
| click.echo("Error: Memory eval V2 dependencies not installed.") | |
| click.echo("Run: pip install headroom[memory,evals]") | |
| click.echo(f"Details: {e}") | |
| raise SystemExit(1) from None | |
| import asyncio | |
| # Build configuration | |
| parsed_categories = None | |
| if categories: | |
| parsed_categories = [int(c) for c in categories.split(",")] | |
| eval_config = MemoryEvalConfigV2( | |
| n_conversations=n_conversations, | |
| categories=parsed_categories, | |
| skip_adversarial=not include_adversarial, | |
| llm_judge_enabled=llm_judge, | |
| llm_judge_model=judge_model, | |
| f1_threshold=f1_threshold, | |
| parallel_workers=parallel, | |
| debug=debug, | |
| save_model=save_model, | |
| answer_model=answer_model, | |
| max_search_results=max_results, | |
| include_graph_expansion=not no_graph, | |
| ) | |
| click.echo(f""" | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| β HEADROOM MEMORY EVALUATION V2 β | |
| β LLM-Controlled Memory Architecture β | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| Configuration: | |
| Conversations: {n_conversations or "all"} | |
| Categories: {parsed_categories or "[1,2,3,4]"} | |
| Save Model: {save_model} | |
| Answer Model: {answer_model} | |
| Max Results: {max_results} | |
| Graph Expansion: {"DISABLED" if no_graph else "ENABLED"} | |
| LLM Judge: {"ENABLED" if llm_judge else "DISABLED"} | |
| Parallelism: {parallel} workers | |
| Debug: {"ENABLED" if debug else "DISABLED"} | |
| Key Differences from V1: | |
| - LLM decides WHAT to save (memory_save tool) | |
| - LLM decides HOW to search (memory_search tool) | |
| - Graph expansion enables multi-hop reasoning | |
| Running evaluation... | |
| """) | |
| # Run evaluation | |
| evaluator = LoCoMoEvaluatorV2( | |
| answer_model=answer_model, | |
| config=eval_config, | |
| ) | |
| try: | |
| result = asyncio.run(evaluator.run()) | |
| except KeyboardInterrupt: | |
| click.echo("\nEvaluation interrupted.") | |
| raise SystemExit(1) from None | |
| # Print results | |
| click.echo(result.summary()) | |
| # Save results if output path specified | |
| if output: | |
| result.save(output) | |
| click.echo(f"\nResults saved to: {output}") | |