Spaces:
Running
Running
Deploy complete BrainAPI source 81ad5ce1c9c1
Browse filesAll tracked files from Git commit 81ad5ce1c9c14989887ed2430c275bbf89cfd2fe; required Docker inputs checked before upload.
This view is limited to 50 files because it contains too many changes. See raw diff
- .dockerignore +142 -0
- .env.example +244 -0
- .github/assets/console-sh.png +3 -0
- .github/assets/screen-video.png +3 -0
- .github/workflows/heavy-validation.yaml +135 -0
- .github/workflows/quality.yaml +149 -0
- .github/workflows/release.yaml +155 -0
- .gitignore +32 -0
- .hfignore +11 -0
- .pylintrc +18 -0
- AGENTS.md +5 -0
- Agent.md +117 -0
- CLAUDE.md +1 -0
- CONTRIBUTING.md +0 -0
- Dockerfile +3 -1
- LICENSE +94 -0
- Makefile +190 -0
- benchmarks/.env.example +41 -0
- benchmarks/.gitignore +15 -0
- benchmarks/AGENTS.md +105 -0
- benchmarks/CLAUDE.md +1 -0
- benchmarks/README.md +418 -0
- benchmarks/REPORTS.json +2594 -0
- benchmarks/beam.sh +10 -0
- benchmarks/beam/__init__.py +1 -0
- benchmarks/beam/__main__.py +4 -0
- benchmarks/beam/answer.py +127 -0
- benchmarks/beam/cli.py +417 -0
- benchmarks/beam/client.py +160 -0
- benchmarks/beam/config.py +202 -0
- benchmarks/beam/dataset.py +586 -0
- benchmarks/beam/evaluate.py +275 -0
- benchmarks/beam/ingest.py +287 -0
- benchmarks/beam/judge.py +308 -0
- benchmarks/beam/metrics.py +127 -0
- benchmarks/beam/prompts.py +320 -0
- benchmarks/beam/provenance.py +99 -0
- benchmarks/beam/report.py +455 -0
- benchmarks/beam/sota.py +433 -0
- benchmarks/beam/test_beam_helpers.py +409 -0
- benchmarks/data/.gitkeep +0 -0
- benchmarks/locomo.sh +10 -0
- benchmarks/locomo/__init__.py +3 -0
- benchmarks/locomo/__main__.py +4 -0
- benchmarks/locomo/answer.py +379 -0
- benchmarks/locomo/cli.py +411 -0
- benchmarks/locomo/client.py +172 -0
- benchmarks/locomo/config.py +214 -0
- benchmarks/locomo/dataset.py +207 -0
- benchmarks/locomo/evaluate.py +500 -0
.dockerignore
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Git
|
| 2 |
+
.git
|
| 3 |
+
.gitignore
|
| 4 |
+
.gitattributes
|
| 5 |
+
|
| 6 |
+
# Documentation
|
| 7 |
+
README.md
|
| 8 |
+
*.md
|
| 9 |
+
|
| 10 |
+
# Python caches
|
| 11 |
+
__pycache__/
|
| 12 |
+
*.py[cod]
|
| 13 |
+
*$py.class
|
| 14 |
+
*.so
|
| 15 |
+
.Python
|
| 16 |
+
build/
|
| 17 |
+
develop-eggs/
|
| 18 |
+
dist/
|
| 19 |
+
downloads/
|
| 20 |
+
eggs/
|
| 21 |
+
.eggs/
|
| 22 |
+
lib/
|
| 23 |
+
lib64/
|
| 24 |
+
parts/
|
| 25 |
+
sdist/
|
| 26 |
+
var/
|
| 27 |
+
wheels/
|
| 28 |
+
*.egg-info/
|
| 29 |
+
.installed.cfg
|
| 30 |
+
*.egg
|
| 31 |
+
MANIFEST
|
| 32 |
+
|
| 33 |
+
# Virtual environments
|
| 34 |
+
.venv
|
| 35 |
+
env/
|
| 36 |
+
venv/
|
| 37 |
+
ENV/
|
| 38 |
+
env.bak/
|
| 39 |
+
venv.bak/
|
| 40 |
+
|
| 41 |
+
# Cache directories
|
| 42 |
+
.cache/
|
| 43 |
+
.ruff_cache/
|
| 44 |
+
|
| 45 |
+
# Node dependencies
|
| 46 |
+
node_modules/
|
| 47 |
+
console/node_modules/
|
| 48 |
+
tui/node_modules/
|
| 49 |
+
|
| 50 |
+
# IDE
|
| 51 |
+
.vscode/
|
| 52 |
+
.idea/
|
| 53 |
+
.cursor/
|
| 54 |
+
*.swp
|
| 55 |
+
*.swo
|
| 56 |
+
*~
|
| 57 |
+
|
| 58 |
+
# OS
|
| 59 |
+
.DS_Store
|
| 60 |
+
.DS_Store?
|
| 61 |
+
._*
|
| 62 |
+
.Spotlight-V100
|
| 63 |
+
.Trashes
|
| 64 |
+
ehthumbs.db
|
| 65 |
+
Thumbs.db
|
| 66 |
+
|
| 67 |
+
# Docker
|
| 68 |
+
Dockerfile*
|
| 69 |
+
docker-compose*.yml
|
| 70 |
+
docker-compose*.yaml
|
| 71 |
+
example-docker-compose.yaml
|
| 72 |
+
.dockerignore
|
| 73 |
+
|
| 74 |
+
# Development / CI files
|
| 75 |
+
Makefile
|
| 76 |
+
*.excalidraw
|
| 77 |
+
.github/
|
| 78 |
+
|
| 79 |
+
# Logs
|
| 80 |
+
*.log
|
| 81 |
+
logs/
|
| 82 |
+
|
| 83 |
+
# Temporary files
|
| 84 |
+
*.tmp
|
| 85 |
+
*.temp
|
| 86 |
+
.tmp/
|
| 87 |
+
.temp/
|
| 88 |
+
|
| 89 |
+
# Coverage / Testing
|
| 90 |
+
htmlcov/
|
| 91 |
+
.coverage
|
| 92 |
+
.coverage.*
|
| 93 |
+
coverage.xml
|
| 94 |
+
*.cover
|
| 95 |
+
.hypothesis/
|
| 96 |
+
.pytest_cache/
|
| 97 |
+
|
| 98 |
+
# Jupyter Notebook
|
| 99 |
+
.ipynb_checkpoints
|
| 100 |
+
|
| 101 |
+
# pyenv
|
| 102 |
+
.python-version
|
| 103 |
+
|
| 104 |
+
# pipenv
|
| 105 |
+
Pipfile.lock
|
| 106 |
+
|
| 107 |
+
# Celery
|
| 108 |
+
celerybeat-schedule
|
| 109 |
+
celerybeat.pid
|
| 110 |
+
|
| 111 |
+
# SageMath parsed files
|
| 112 |
+
*.sage.py
|
| 113 |
+
|
| 114 |
+
# Environment files (secrets)
|
| 115 |
+
.env
|
| 116 |
+
.env.*
|
| 117 |
+
|
| 118 |
+
# Database volumes (local dev)
|
| 119 |
+
src/lib/*/volumes/
|
| 120 |
+
src/lib/*/docker-compose.yaml
|
| 121 |
+
|
| 122 |
+
# Terraform / Infrastructure
|
| 123 |
+
infra/
|
| 124 |
+
*.tfstate
|
| 125 |
+
*.tfstate.*
|
| 126 |
+
.terraform/
|
| 127 |
+
|
| 128 |
+
# Credentials
|
| 129 |
+
gcp_credentials.json
|
| 130 |
+
*.pem
|
| 131 |
+
*.key
|
| 132 |
+
|
| 133 |
+
# Misc
|
| 134 |
+
.favorites.json
|
| 135 |
+
READMEP.md
|
| 136 |
+
Untitled-1.excalidraw
|
| 137 |
+
mcp-stdio-http-bridge/
|
| 138 |
+
agent-transcripts/
|
| 139 |
+
|
| 140 |
+
# Research
|
| 141 |
+
docs
|
| 142 |
+
benchmarks
|
.env.example
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# LLM Implementation envs
|
| 2 |
+
# The following are just examples of the adapter implementation, you can have completely different envs
|
| 3 |
+
LLM_SMALL_PROVIDER="gcp_vertex"
|
| 4 |
+
LLM_LARGE_PROVIDER="azure"
|
| 5 |
+
EMBEDDINGS_PROVIDER="azure"
|
| 6 |
+
AZURE_SMALL_LLM_MODEL="gpt-4o-mini"
|
| 7 |
+
AZURE_LARGE_LLM_MODEL="gpt-4o"
|
| 8 |
+
AZURE_LLM_API_VERSION="2024-12-01-preview"
|
| 9 |
+
AZURE_LLM_ENDPOINT="https://yourproject.openai.azure.com"
|
| 10 |
+
AZURE_LLM_SUBSCRIPTION_KEY="xyz"
|
| 11 |
+
GCP_EXTRA_SMALL_LLM_MODEL="gemini-3-flash-preview"
|
| 12 |
+
GCP_SMALL_LLM_MODEL="gemini-3-flash-preview"
|
| 13 |
+
GCP_LARGE_LLM_MODEL="gemini-3-flash-preview"
|
| 14 |
+
GCP_EMBEDDING_MODEL="text-embedding-005"
|
| 15 |
+
GCP_PROJECT_ID="your-project-id"
|
| 16 |
+
GCP_CREDENTIALS_PATH=".path-to-credentials-file.json"
|
| 17 |
+
BEDROCK_REGION="us-east-1"
|
| 18 |
+
BEDROCK_ACCESS_KEY_ID="your-access-key-id"
|
| 19 |
+
BEDROCK_SECRET_ACCESS_KEY="your-secret-access-key"
|
| 20 |
+
BEDROCK_SESSION_TOKEN=""
|
| 21 |
+
BEDROCK_SMALL_LLM_MODEL="us.anthropic.claude-3-5-haiku-20241022-v1:0"
|
| 22 |
+
BEDROCK_LARGE_LLM_MODEL="us.anthropic.claude-3-7-sonnet-20250219-v1:0"
|
| 23 |
+
BEDROCK_EMBEDDING_MODEL="amazon.titan-embed-text-v2:0"
|
| 24 |
+
OPENAI_API_KEY="sk-..."
|
| 25 |
+
OPENAI_BASE_URL=""
|
| 26 |
+
OPENAI_SMALL_LLM_MODEL="gpt-4o-mini"
|
| 27 |
+
OPENAI_LARGE_LLM_MODEL="gpt-4o"
|
| 28 |
+
OPENAI_EMBEDDING_MODEL="text-embedding-3-large"
|
| 29 |
+
# DeepSeek chat (OpenAI-compatible). No embeddings — keep EMBEDDINGS_PROVIDER on azure/openai/gcp_vertex/etc.
|
| 30 |
+
# LLM_SMALL_PROVIDER="deepseek"
|
| 31 |
+
# LLM_LARGE_PROVIDER="deepseek"
|
| 32 |
+
DEEPSEEK_API_KEY="sk-..."
|
| 33 |
+
DEEPSEEK_SMALL_LLM_MODEL="deepseek-v4-flash"
|
| 34 |
+
DEEPSEEK_LARGE_LLM_MODEL="deepseek-v4-pro"
|
| 35 |
+
|
| 36 |
+
# Embedding envs
|
| 37 |
+
# The following are just examples of the adapter implementation, you can have completely different envs
|
| 38 |
+
AZURE_EMBEDDING_FULL_ENDPOINT="https://yourproject.openai.azure.com/openai/deployments/text-embedding-3-large/embeddings?api-version=2023-05-15"
|
| 39 |
+
AZURE_EMBEDDING_KEY="xyz"
|
| 40 |
+
AZURE_EMBEDDING_MODEL="text-embedding-3-large"
|
| 41 |
+
EMBEDDINGS_SMALL_MODEL="paraphrase-multilingual-MiniLM-L12-v2"
|
| 42 |
+
EMBEDDINGS_LOCAL_MODEL="intfloat/e5-small"
|
| 43 |
+
EMBEDDING_NODES_DIMENSION=3072
|
| 44 |
+
EMBEDDING_TRIPLETS_DIMENSION=3072
|
| 45 |
+
EMBEDDING_OBSERVATIONS_DIMENSION=3072
|
| 46 |
+
EMBEDDING_DATA_DIMENSION=3072
|
| 47 |
+
EMBEDDING_RELATIONSHIPS_DIMENSION=3072
|
| 48 |
+
|
| 49 |
+
# Cache envs
|
| 50 |
+
# The following are just examples of the adapter implementation, you can have completely different envs
|
| 51 |
+
REDIS_HOST="localhost"
|
| 52 |
+
REDIS_PORT=6379
|
| 53 |
+
REDIS_PASSWORD=""
|
| 54 |
+
|
| 55 |
+
# Worker envs
|
| 56 |
+
# Can be whatever you prefer
|
| 57 |
+
# CELERY_BACKEND="rabbitmq"
|
| 58 |
+
# CELERY_UNBLOCKED_WINDOW_SECONDS is the window in seconds during which the number of unblocked events is counted
|
| 59 |
+
# CELERY_UNBLOCKED_MAX_RETRIES is the maximum number of unblocked events that can occur during the window
|
| 60 |
+
# CELERY_QUEUE_HEALTH_THRESHOLD is the maximum number of tasks that can be in the queue before the worker is considered unhealthy
|
| 61 |
+
CELERY_BACKEND="redis"
|
| 62 |
+
CELERY_UNBLOCKED_WINDOW_SECONDS=60
|
| 63 |
+
CELERY_UNBLOCKED_MAX_RETRIES=20
|
| 64 |
+
CELERY_QUEUE_HEALTH_THRESHOLD=50
|
| 65 |
+
|
| 66 |
+
# GraphDB envs
|
| 67 |
+
# The following are just examples of the adapter implementation, you can have completely different envs
|
| 68 |
+
NEO4J_HOST="localhost"
|
| 69 |
+
NEO4J_PORT=7687
|
| 70 |
+
NEO4J_USERNAME="neo4j"
|
| 71 |
+
NEO4J_PASSWORD="your_password"
|
| 72 |
+
|
| 73 |
+
# VectorDB envs
|
| 74 |
+
# The following are just examples of the adapter implementation, you can have completely different envs
|
| 75 |
+
MILVUS_HOST="localhost"
|
| 76 |
+
MILVUS_PORT=19530
|
| 77 |
+
|
| 78 |
+
# PostgreSQL envs
|
| 79 |
+
# Used for data / vector / graph storage when DATA_DB, VECTOR_DB, or GRAPH_DB
|
| 80 |
+
# point at postgresql / pgvector / networkx.
|
| 81 |
+
# Each brain owns its own Postgres database (brain_<sanitized_id>).
|
| 82 |
+
# POSTGRES_SYSTEM_DATABASE holds the brains registry (data_brains table).
|
| 83 |
+
# POSTGRES_MAINTENANCE_DATABASE is used only for CREATE DATABASE statements.
|
| 84 |
+
POSTGRES_HOST="localhost"
|
| 85 |
+
POSTGRES_PORT=5432
|
| 86 |
+
POSTGRES_USERNAME="postgres"
|
| 87 |
+
POSTGRES_PASSWORD="password"
|
| 88 |
+
POSTGRES_SYSTEM_DATABASE="brainapi"
|
| 89 |
+
POSTGRES_MAINTENANCE_DATABASE="postgres"
|
| 90 |
+
|
| 91 |
+
# DataDB envs (Mongo alternative to PostgreSQL data layer)
|
| 92 |
+
# The following are just examples of the adapter implementation, you can have completely different envs
|
| 93 |
+
MONGO_HOST="localhost"
|
| 94 |
+
MONGO_PORT=27017
|
| 95 |
+
MONGO_USERNAME="root"
|
| 96 |
+
MONGO_PASSWORD="password"
|
| 97 |
+
MONGO_SYSTEM_DATABASE="system"
|
| 98 |
+
|
| 99 |
+
# Auth
|
| 100 |
+
# Used to handle authentication
|
| 101 |
+
PUBLIC_BASE_URL="http://localhost:8000"
|
| 102 |
+
BRAINPAT_TOKEN="your_token"
|
| 103 |
+
|
| 104 |
+
# MultiBrain
|
| 105 |
+
# Choose to allow or block automatic creation of new brains on requests with non existing new brain_ids
|
| 106 |
+
BRAIN_CREATION_ALLOWED="true"
|
| 107 |
+
|
| 108 |
+
# Production ingress and plugin startup policy.
|
| 109 |
+
CORS_ALLOWED_ORIGINS=""
|
| 110 |
+
PLUGIN_FAILURE_POLICY="fail"
|
| 111 |
+
# Choose whether to fallback to default brain if not provided
|
| 112 |
+
DEFAULT_BRAIN_FALLBACK="true"
|
| 113 |
+
|
| 114 |
+
# Optional anonymous, read-only documentation search demo (disabled by default)
|
| 115 |
+
PUBLIC_DEMO_ENABLED="false"
|
| 116 |
+
PUBLIC_DEMO_BRAIN_ID="agentdemo"
|
| 117 |
+
PUBLIC_DEMO_MAX_K=10
|
| 118 |
+
|
| 119 |
+
# GCP
|
| 120 |
+
# Used on the oss project for the small LLM but can be changed to any other model
|
| 121 |
+
GCP_EXTRA_SMALL_LLM_MODEL="gemini-3-flash-preview"
|
| 122 |
+
GCP_SMALL_LLM_MODEL="gemini-3-flash-preview"
|
| 123 |
+
GCP_PROJECT_ID="your-project-id"
|
| 124 |
+
GCP_CREDENTIALS_PATH=".path-to-credentials-file.json"
|
| 125 |
+
|
| 126 |
+
# Celery
|
| 127 |
+
# Used to run the worker tasks
|
| 128 |
+
CELERY_WORKER_CONCURRENCY=4
|
| 129 |
+
|
| 130 |
+
# DocParser
|
| 131 |
+
# Used to parse the documents into markdown text
|
| 132 |
+
# and by forwarding the webhook (our app host) and the task identifier,
|
| 133 |
+
# we can be called back from the DocParser API to start the ingestion process
|
| 134 |
+
# with the created markdown text.
|
| 135 |
+
DOCPARSER_ENDPOINT="https://docparser.lumen-labs.ai"
|
| 136 |
+
APP_HOST="https://your-app-host.com/ingest"
|
| 137 |
+
DOCPARSER_TOKEN="your-docparser-token"
|
| 138 |
+
|
| 139 |
+
# Spacy
|
| 140 |
+
# Used to keep the spacy models in memory or reinitialize them on each request
|
| 141 |
+
SPACY_KEEP_MODELS_IN_MEMORY="true"
|
| 142 |
+
|
| 143 |
+
# Pipeline
|
| 144 |
+
# Used to configure the pipeline
|
| 145 |
+
# PIPELINE_MODE => lightweight = faster but less accurate, accurate = slower but more accurate
|
| 146 |
+
# MODELS_MODE => local = uses local models for llm and embeddings, remote = uses remote models for llm and embeddings
|
| 147 |
+
# OCR_MODE => docling = uses local docling ocr engine, docparser = uses remote docparser ocr pipeline engine
|
| 148 |
+
# AGENTIC_ARCHITECTURE => custom = uses the custom agentic architecture (faster and token efficient), langchain = uses the langchain agentic architecture
|
| 149 |
+
# RUN_GRAPH_CONSOLIDATOR => true = consolidates after ingest (LLM mutations; keep false until audited)
|
| 150 |
+
# RUN_OBSERVATIONS => true = ObservationsAgent LLM notes (does not write the graph)
|
| 151 |
+
# JANITOR_BATCH_SIZE => relationships per Janitor LLM call when batching (default 20)
|
| 152 |
+
# INGEST_JANITOR_MAX_LLM_CALLS => hard cap on Janitor LLM batches/session (default 2); remainder dropped with audit
|
| 153 |
+
# INGEST_ARCHITECT_PER_UNIT => true = Architect sees Scout chunks (+ prior window); false = full text (legacy)
|
| 154 |
+
# INGEST_DEFER_JANITOR => true = batched post-Architect Janitor; false = per-create Janitor (legacy)
|
| 155 |
+
# INGEST_ARCHITECT_MODE => batch (default; pure tools=[] extract; escalate to tooler on fail) | schema | tooler (ReAct rollback)
|
| 156 |
+
# INGEST_ARCHITECT_MAX_SCHEMA_CALLS => max LLM calls/unit in schema|batch before escalate (default 3)
|
| 157 |
+
# INGEST_ARCHITECT_ESCALATE => true (default) allow tooler escalate on empty schema; false = partial/empty unit
|
| 158 |
+
# INGEST_ARCHITECT_ESCALATE_MAX_TURNS => max tool turns on escalate tooler path (default 3)
|
| 159 |
+
# INGEST_ARCHITECT_DENSE_ENTITY_THRESHOLD => rechunk architect unit when Scout entities >= N (default 12)
|
| 160 |
+
# INGEST_ARCHITECT_DENSE_MAX_CHARS => max chars per dense rechunk part (default 1200)
|
| 161 |
+
# INGEST_ARCHITECT_PRIOR_CONTEXT => auto (default; scratchpad for batch/schema, raw for tooler) | scratchpad | raw
|
| 162 |
+
# INGEST_ARCHITECT_SCRATCHPAD_TOKEN_CAP => max reference tokens for prior scratchpad (default 500)
|
| 163 |
+
PIPELINE_MODE="accurate"
|
| 164 |
+
MODELS_MODE="remote"
|
| 165 |
+
OCR_MODE="docparser"
|
| 166 |
+
AGENTIC_ARCHITECTURE="custom"
|
| 167 |
+
RUN_GRAPH_CONSOLIDATOR="false"
|
| 168 |
+
RUN_OBSERVATIONS="false"
|
| 169 |
+
JANITOR_BATCH_SIZE="20"
|
| 170 |
+
INGEST_JANITOR_MAX_LLM_CALLS="2"
|
| 171 |
+
INGEST_ARCHITECT_PER_UNIT="true"
|
| 172 |
+
INGEST_DEFER_JANITOR="true"
|
| 173 |
+
INGEST_ARCHITECT_MODE="batch"
|
| 174 |
+
INGEST_ARCHITECT_MAX_SCHEMA_CALLS="3"
|
| 175 |
+
INGEST_ARCHITECT_ESCALATE="true"
|
| 176 |
+
INGEST_ARCHITECT_ESCALATE_MAX_TURNS="3"
|
| 177 |
+
INGEST_ARCHITECT_DENSE_ENTITY_THRESHOLD="12"
|
| 178 |
+
INGEST_ARCHITECT_DENSE_MAX_CHARS="1200"
|
| 179 |
+
INGEST_ARCHITECT_PRIOR_CONTEXT="auto"
|
| 180 |
+
INGEST_ARCHITECT_SCRATCHPAD_TOKEN_CAP="500"
|
| 181 |
+
# Search
|
| 182 |
+
# SEARCH_ENABLED => true registers GET|POST /retrieve/search, writes tsvector + halfvec HNSW
|
| 183 |
+
# (when dim > 2000). Default false keeps memory /retrieve/context identical (dense ∪ ILIKE).
|
| 184 |
+
# Requires DATA_DB=postgresql. Search p50 SLO is < 200 ms excluding embed.query RTT.
|
| 185 |
+
# SEARCH_USE_DENSE / SEARCH_USE_BM25 => at least one must be true when search is on.
|
| 186 |
+
# Both true (default) fuses with SEARCH_FUSION=rrf (or cc with SEARCH_FUSION_ALPHA).
|
| 187 |
+
# SEARCH_COMMUNITY_LABELS => hub labels for the communities search channel.
|
| 188 |
+
# SEARCH_NEIGHBOR_FANOUT => max 1-hop members per seed when expand=neighbors.
|
| 189 |
+
# CONTEXT_PASSAGE_MODE => hybrid|bm25|dense|ilike; only applies when SEARCH_ENABLED=true.
|
| 190 |
+
# hybrid uses BM25 ∪ dense RRF on /retrieve/context; ilike freezes the old lexical leg.
|
| 191 |
+
SEARCH_ENABLED="false"
|
| 192 |
+
SEARCH_USE_DENSE="true"
|
| 193 |
+
SEARCH_USE_BM25="true"
|
| 194 |
+
SEARCH_FUSION="rrf"
|
| 195 |
+
SEARCH_FUSION_ALPHA="0.5"
|
| 196 |
+
SEARCH_BM25_K1="1.2"
|
| 197 |
+
SEARCH_BM25_B="0.75"
|
| 198 |
+
SEARCH_COMMUNITY_LABELS="TYPE,CLASS,TOPIC"
|
| 199 |
+
SEARCH_NEIGHBOR_FANOUT="50"
|
| 200 |
+
CONTEXT_PASSAGE_MODE="hybrid"
|
| 201 |
+
GRAPH_DB="networkx"
|
| 202 |
+
DATA_DB="postgresql"
|
| 203 |
+
VECTOR_DB="postgresql"
|
| 204 |
+
|
| 205 |
+
# Ollama
|
| 206 |
+
# Used to configure the ollama models
|
| 207 |
+
# Tests with models using OpenAI's function tool calling are not currently compatible with BrainAPI
|
| 208 |
+
OLLAMA_HOST="localhost"
|
| 209 |
+
OLLAMA_PORT=11434
|
| 210 |
+
# Hugging Face CPU profile: qwen3-embedding:0.6b emits 1024 dimensions.
|
| 211 |
+
# Set all EMBEDDING_*_DIMENSION values to 1024 and reindex when switching.
|
| 212 |
+
# OLLAMA_EMBEDDING_MODEL="qwen3-embedding:0.6b"
|
| 213 |
+
OLLAMA_EMBEDDING_KEEP_ALIVE="10m"
|
| 214 |
+
OLLAMA_NUM_THREADS=2
|
| 215 |
+
# Optional native llama.cpp reranker, kept internal (never Ollama chat).
|
| 216 |
+
# LLAMA_RERANK_URL="http://127.0.0.1:11435"
|
| 217 |
+
# OLLAMA_LLM_SMALL_MODEL="hf.co/unsloth/gpt-oss-20b-GGUF:Q4_K_M"
|
| 218 |
+
# OLLAMA_LLM_LARGE_MODEL="hf.co/unsloth/gpt-oss-20b-GGUF:Q4_K_M"
|
| 219 |
+
# OLLAMA_LLM_LARGE_MODEL="hf.co/MaziyarPanahi/Qwen3-8B-GGUF:Q4_K_M"
|
| 220 |
+
# OLLAMA_LLM_SMALL_MODEL="hf.co/MaziyarPanahi/Qwen3-8B-GGUF:Q4_K_M"
|
| 221 |
+
OLLAMA_LLM_SMALL_MODEL="hf.co/unsloth/Qwen3-14B-GGUF:Q4_K_M"
|
| 222 |
+
OLLAMA_LLM_LARGE_MODEL="hf.co/unsloth/Qwen3-14B-GGUF:Q4_K_M"
|
| 223 |
+
|
| 224 |
+
# Plugins
|
| 225 |
+
PLUGINS_DIR="plugins"
|
| 226 |
+
PLUGIN_REGISTRY_URL="https://registry.brain-api.dev"
|
| 227 |
+
PLUGIN_PUBLISHER_API_KEY="your-publisher-api-key"
|
| 228 |
+
PLUGIN_PUBLISHER_ID="your-publisher-id"
|
| 229 |
+
|
| 230 |
+
# MCP
|
| 231 |
+
MCP_OAUTH_ISSUER_URL="https://your-mcp-oauth-issuer-url.com"
|
| 232 |
+
MCP_RESOURCE_SERVER_URL="https://your-mcp-resource-server-url.com"
|
| 233 |
+
|
| 234 |
+
# Tracing
|
| 235 |
+
TRACE_TRACKER_ENABLED=false
|
| 236 |
+
TRACE_QUEUE_MAX_SIZE=10000
|
| 237 |
+
TRACE_SLOW_REQUEST_MS=1000
|
| 238 |
+
TRACE_AGENT_SLOW_OPERATION_MS=30000
|
| 239 |
+
TRACE_HEARTBEAT_INTERVAL_SECONDS=30
|
| 240 |
+
TRACE_HEALTH_INTERVAL_SECONDS=30
|
| 241 |
+
TRACE_RESOURCE_INTERVAL_SECONDS=30
|
| 242 |
+
TRACE_HEALTH_TIMEOUT_SECONDS=1
|
| 243 |
+
TRACE_AGENT_LOOP_ITERATIONS=20
|
| 244 |
+
TRACE_AGENT_TOOL_LOOP_ITERATIONS=20
|
.github/assets/console-sh.png
ADDED
|
Git LFS Details
|
.github/assets/screen-video.png
ADDED
|
Git LFS Details
|
.github/workflows/heavy-validation.yaml
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Heavy production validation
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
pull_request:
|
| 5 |
+
push:
|
| 6 |
+
branches: [main]
|
| 7 |
+
workflow_dispatch:
|
| 8 |
+
|
| 9 |
+
permissions:
|
| 10 |
+
contents: read
|
| 11 |
+
actions: read
|
| 12 |
+
|
| 13 |
+
jobs:
|
| 14 |
+
heavy-smoke-and-restore:
|
| 15 |
+
runs-on: [self-hosted, linux, x64, brainapi-heavy]
|
| 16 |
+
timeout-minutes: 90
|
| 17 |
+
env:
|
| 18 |
+
BRAINAPI_IMAGE: brainapi:ci
|
| 19 |
+
BRAINAPI_ENV_FILE: env.heavy.example
|
| 20 |
+
REDIS_PASSWORD: ci-redis-password
|
| 21 |
+
NEO4J_PASSWORD: ci-neo4j-password
|
| 22 |
+
MONGO_PASSWORD: ci-mongo-password
|
| 23 |
+
MINIO_ACCESS_KEY: ci-minio-access
|
| 24 |
+
MINIO_SECRET_KEY: ci-minio-secret-password
|
| 25 |
+
MILVUS_ROOT_PASSWORD: ci-milvus-password
|
| 26 |
+
MILVUS_TOKEN: root:ci-milvus-password
|
| 27 |
+
BRAINPAT_TOKEN: ci-system-token
|
| 28 |
+
BACKUP_DIR: ${{ github.workspace }}/.backup-artifacts
|
| 29 |
+
RELEASE_ARTIFACT_DIR: ${{ github.workspace }}/release-artifacts
|
| 30 |
+
steps:
|
| 31 |
+
- uses: actions/checkout@v4
|
| 32 |
+
- name: Download the exact audited candidate image
|
| 33 |
+
timeout-minutes: 45
|
| 34 |
+
uses: actions/github-script@v7
|
| 35 |
+
with:
|
| 36 |
+
script: |
|
| 37 |
+
const fs = require("fs");
|
| 38 |
+
const shas = [...new Set([
|
| 39 |
+
context.payload.pull_request?.head?.sha,
|
| 40 |
+
context.sha,
|
| 41 |
+
].filter(Boolean))];
|
| 42 |
+
const deadline = Date.now() + 40 * 60 * 1000;
|
| 43 |
+
while (Date.now() < deadline) {
|
| 44 |
+
let run;
|
| 45 |
+
for (const sha of shas) {
|
| 46 |
+
const response = await github.rest.actions.listWorkflowRuns({
|
| 47 |
+
owner: context.repo.owner,
|
| 48 |
+
repo: context.repo.repo,
|
| 49 |
+
workflow_id: "quality.yaml",
|
| 50 |
+
head_sha: sha,
|
| 51 |
+
status: "completed",
|
| 52 |
+
per_page: 20,
|
| 53 |
+
});
|
| 54 |
+
run = response.data.workflow_runs[0];
|
| 55 |
+
if (run) break;
|
| 56 |
+
}
|
| 57 |
+
if (run) {
|
| 58 |
+
if (run.conclusion !== "success") {
|
| 59 |
+
core.setFailed(`Quality workflow ${run.id} concluded ${run.conclusion}`);
|
| 60 |
+
return;
|
| 61 |
+
}
|
| 62 |
+
const artifacts = await github.paginate(
|
| 63 |
+
github.rest.actions.listWorkflowRunArtifacts,
|
| 64 |
+
{
|
| 65 |
+
owner: context.repo.owner,
|
| 66 |
+
repo: context.repo.repo,
|
| 67 |
+
run_id: run.id,
|
| 68 |
+
per_page: 100,
|
| 69 |
+
}
|
| 70 |
+
);
|
| 71 |
+
const artifact = artifacts.find(
|
| 72 |
+
(item) => item.name === "light-production-validation" && !item.expired
|
| 73 |
+
);
|
| 74 |
+
if (!artifact) {
|
| 75 |
+
core.setFailed(`Quality workflow ${run.id} has no candidate image artifact`);
|
| 76 |
+
return;
|
| 77 |
+
}
|
| 78 |
+
const archive = await github.rest.actions.downloadArtifact({
|
| 79 |
+
owner: context.repo.owner,
|
| 80 |
+
repo: context.repo.repo,
|
| 81 |
+
artifact_id: artifact.id,
|
| 82 |
+
archive_format: "zip",
|
| 83 |
+
});
|
| 84 |
+
fs.writeFileSync("quality-artifacts.zip", Buffer.from(archive.data));
|
| 85 |
+
return;
|
| 86 |
+
}
|
| 87 |
+
await new Promise((resolve) => setTimeout(resolve, 30000));
|
| 88 |
+
}
|
| 89 |
+
core.setFailed(`Timed out waiting for successful Quality artifacts for ${shas.join(" or ")}`);
|
| 90 |
+
- name: Load and verify the shared candidate image
|
| 91 |
+
run: |
|
| 92 |
+
unzip -q quality-artifacts.zip -d quality-artifacts
|
| 93 |
+
image_archive=$(find quality-artifacts -name brainapi-image.tar.gz -print -quit)
|
| 94 |
+
security_file=$(find quality-artifacts -name security.json -print -quit)
|
| 95 |
+
test -n "$image_archive"
|
| 96 |
+
test -n "$security_file"
|
| 97 |
+
gzip -dc "$image_archive" | docker load
|
| 98 |
+
expected=$(python -c 'import json,sys; print(json.load(open(sys.argv[1]))["image_digest"])' "$security_file")
|
| 99 |
+
actual=$(docker image inspect "$BRAINAPI_IMAGE" --format '{{.Id}}')
|
| 100 |
+
test "$actual" = "$expected"
|
| 101 |
+
- name: Require runner capacity
|
| 102 |
+
run: |
|
| 103 |
+
awk '/MemTotal/ { exit !($2 >= 16000000) }' /proc/meminfo
|
| 104 |
+
test "$(uname -m)" = x86_64
|
| 105 |
+
- run: docker compose -f deploy/docker-compose.heavy.yaml config -q
|
| 106 |
+
- name: Clean heavy startup
|
| 107 |
+
run: docker compose --project-name brainapi-heavy-ci -f deploy/docker-compose.heavy.yaml -f deploy/docker-compose.ci.yaml up -d --wait
|
| 108 |
+
- name: Heavy product smoke
|
| 109 |
+
run: |
|
| 110 |
+
curl --fail --retry 20 --retry-delay 5 http://localhost/health
|
| 111 |
+
python scripts/production_smoke.py exercise --profile heavy --system-token "$BRAINPAT_TOKEN" --artifact-dir "$RELEASE_ARTIFACT_DIR"
|
| 112 |
+
- name: Backup and verify
|
| 113 |
+
id: backup
|
| 114 |
+
run: |
|
| 115 |
+
archive=$(deploy/brainapi-backup backup --profile heavy --project-name brainapi-heavy-ci --env-file deploy/env.heavy.example --compose-file deploy/docker-compose.ci.yaml --backup-dir "$BACKUP_DIR" | tail -1)
|
| 116 |
+
deploy/brainapi-backup verify --profile heavy --env-file deploy/env.heavy.example --archive "$archive"
|
| 117 |
+
cp "$archive/manifest.json" "$RELEASE_ARTIFACT_DIR/backup-heavy-manifest.json"
|
| 118 |
+
echo "archive=$archive" >> "$GITHUB_OUTPUT"
|
| 119 |
+
- name: Restore into clean volumes
|
| 120 |
+
run: |
|
| 121 |
+
docker compose --project-name brainapi-heavy-ci -f deploy/docker-compose.heavy.yaml -f deploy/docker-compose.ci.yaml down --volumes
|
| 122 |
+
deploy/brainapi-backup restore --profile heavy --project-name brainapi-heavy-ci --env-file deploy/env.heavy.example --compose-file deploy/docker-compose.ci.yaml --archive "${{ steps.backup.outputs.archive }}"
|
| 123 |
+
curl --fail --retry 20 --retry-delay 5 http://localhost/health
|
| 124 |
+
python scripts/production_smoke.py verify-restore --profile heavy --system-token "$BRAINPAT_TOKEN" --artifact-dir "$RELEASE_ARTIFACT_DIR"
|
| 125 |
+
- name: Logs on failure
|
| 126 |
+
if: failure()
|
| 127 |
+
run: docker compose --project-name brainapi-heavy-ci -f deploy/docker-compose.heavy.yaml -f deploy/docker-compose.ci.yaml logs --no-color
|
| 128 |
+
- name: Clean heavy volumes
|
| 129 |
+
if: always()
|
| 130 |
+
run: docker compose --project-name brainapi-heavy-ci -f deploy/docker-compose.heavy.yaml -f deploy/docker-compose.ci.yaml down --volumes --remove-orphans
|
| 131 |
+
- uses: actions/upload-artifact@v4
|
| 132 |
+
if: always()
|
| 133 |
+
with:
|
| 134 |
+
name: heavy-production-validation
|
| 135 |
+
path: release-artifacts/
|
.github/workflows/quality.yaml
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Quality
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
pull_request:
|
| 5 |
+
push:
|
| 6 |
+
branches: [main]
|
| 7 |
+
workflow_dispatch:
|
| 8 |
+
|
| 9 |
+
permissions:
|
| 10 |
+
contents: read
|
| 11 |
+
|
| 12 |
+
jobs:
|
| 13 |
+
python:
|
| 14 |
+
runs-on: ubuntu-latest
|
| 15 |
+
steps:
|
| 16 |
+
- uses: actions/checkout@v4
|
| 17 |
+
- uses: actions/setup-python@v5
|
| 18 |
+
with:
|
| 19 |
+
python-version: "3.12"
|
| 20 |
+
- uses: astral-sh/setup-uv@v6
|
| 21 |
+
with:
|
| 22 |
+
version: "0.8.22"
|
| 23 |
+
- run: pip install poetry==2.1.3
|
| 24 |
+
- name: Verify locks without regeneration
|
| 25 |
+
run: |
|
| 26 |
+
poetry check --lock
|
| 27 |
+
uv lock --check
|
| 28 |
+
- run: poetry install
|
| 29 |
+
- run: poetry run python -m compileall -q src tests deploy/backup_restore.py
|
| 30 |
+
- run: poetry run pytest -q
|
| 31 |
+
- name: Audit actual Python environment
|
| 32 |
+
run: uvx pip-audit --path .venv/lib/python3.12/site-packages
|
| 33 |
+
|
| 34 |
+
benchmark-helpers:
|
| 35 |
+
runs-on: ubuntu-latest
|
| 36 |
+
steps:
|
| 37 |
+
- uses: actions/checkout@v4
|
| 38 |
+
- uses: actions/setup-python@v5
|
| 39 |
+
with:
|
| 40 |
+
python-version: "3.12"
|
| 41 |
+
- run: python -m venv benchmarks/.venv
|
| 42 |
+
- run: benchmarks/.venv/bin/pip install -r benchmarks/requirements.txt pytest
|
| 43 |
+
- run: benchmarks/.venv/bin/pytest -q benchmarks/locomo/test_sota_helpers.py benchmarks/beam/test_beam_helpers.py
|
| 44 |
+
|
| 45 |
+
frontend:
|
| 46 |
+
runs-on: ubuntu-latest
|
| 47 |
+
strategy:
|
| 48 |
+
matrix:
|
| 49 |
+
project: [console, tui]
|
| 50 |
+
defaults:
|
| 51 |
+
run:
|
| 52 |
+
working-directory: ${{ matrix.project }}
|
| 53 |
+
steps:
|
| 54 |
+
- uses: actions/checkout@v4
|
| 55 |
+
- uses: actions/setup-node@v4
|
| 56 |
+
with:
|
| 57 |
+
node-version: "22.22.0"
|
| 58 |
+
cache: npm
|
| 59 |
+
cache-dependency-path: ${{ matrix.project }}/package-lock.json
|
| 60 |
+
- run: npm ci
|
| 61 |
+
- if: matrix.project == 'console'
|
| 62 |
+
run: npm audit --omit=dev --audit-level=high
|
| 63 |
+
- if: matrix.project == 'tui'
|
| 64 |
+
run: npm run typecheck
|
| 65 |
+
- run: npm run build
|
| 66 |
+
|
| 67 |
+
rust:
|
| 68 |
+
runs-on: ubuntu-latest
|
| 69 |
+
defaults:
|
| 70 |
+
run:
|
| 71 |
+
working-directory: mcp-stdio-http-bridge
|
| 72 |
+
steps:
|
| 73 |
+
- uses: actions/checkout@v4
|
| 74 |
+
- uses: dtolnay/rust-toolchain@master
|
| 75 |
+
with:
|
| 76 |
+
toolchain: 1.85.0
|
| 77 |
+
- run: cargo test --locked
|
| 78 |
+
|
| 79 |
+
image-light-smoke:
|
| 80 |
+
runs-on: ubuntu-latest
|
| 81 |
+
needs: [python, benchmark-helpers, frontend, rust]
|
| 82 |
+
env:
|
| 83 |
+
BRAINAPI_IMAGE: brainapi:ci
|
| 84 |
+
BRAINAPI_ENV_FILE: env.light.example
|
| 85 |
+
REDIS_PASSWORD: ci-redis-password
|
| 86 |
+
POSTGRES_PASSWORD: ci-postgres-password
|
| 87 |
+
POSTGRES_USERNAME: brainapi
|
| 88 |
+
BRAINPAT_TOKEN: ci-system-token
|
| 89 |
+
BACKUP_DIR: ${{ github.workspace }}/.backup-artifacts
|
| 90 |
+
RELEASE_ARTIFACT_DIR: ${{ github.workspace }}/release-artifacts
|
| 91 |
+
steps:
|
| 92 |
+
- uses: actions/checkout@v4
|
| 93 |
+
- uses: docker/setup-buildx-action@v3
|
| 94 |
+
- name: Validate Compose and secret rejection
|
| 95 |
+
run: |
|
| 96 |
+
docker compose -f deploy/docker-compose.light.yaml config -q
|
| 97 |
+
if env -u REDIS_PASSWORD docker compose -f deploy/docker-compose.light.yaml config -q; then exit 1; fi
|
| 98 |
+
- name: Build exact test image
|
| 99 |
+
run: docker build --platform linux/amd64 --load -t "$BRAINAPI_IMAGE" .
|
| 100 |
+
- name: Export exact candidate image
|
| 101 |
+
run: |
|
| 102 |
+
mkdir -p "$RELEASE_ARTIFACT_DIR"
|
| 103 |
+
docker save "$BRAINAPI_IMAGE" | gzip -1 > "$RELEASE_ARTIFACT_DIR/brainapi-image.tar.gz"
|
| 104 |
+
- name: Generate image SBOM
|
| 105 |
+
uses: anchore/sbom-action@v0
|
| 106 |
+
with:
|
| 107 |
+
image: brainapi:ci
|
| 108 |
+
format: spdx-json
|
| 109 |
+
output-file: brainapi.spdx.json
|
| 110 |
+
- name: Audit actual image
|
| 111 |
+
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
|
| 112 |
+
with:
|
| 113 |
+
image-ref: brainapi:ci
|
| 114 |
+
severity: HIGH,CRITICAL
|
| 115 |
+
ignore-unfixed: true
|
| 116 |
+
exit-code: "1"
|
| 117 |
+
format: json
|
| 118 |
+
output: trivy.json
|
| 119 |
+
- name: Light profile smoke
|
| 120 |
+
run: |
|
| 121 |
+
docker compose --project-name brainapi-ci -f deploy/docker-compose.light.yaml -f deploy/docker-compose.ci.yaml up -d --wait
|
| 122 |
+
curl --fail --retry 12 --retry-delay 5 http://localhost/health
|
| 123 |
+
python scripts/production_smoke.py exercise --profile light --system-token "$BRAINPAT_TOKEN" --artifact-dir "$RELEASE_ARTIFACT_DIR"
|
| 124 |
+
- name: Light backup and clean restore
|
| 125 |
+
run: |
|
| 126 |
+
archive=$(deploy/brainapi-backup backup --profile light --project-name brainapi-ci --env-file deploy/env.light.example --compose-file deploy/docker-compose.ci.yaml --backup-dir "$BACKUP_DIR" | tail -1)
|
| 127 |
+
deploy/brainapi-backup verify --profile light --env-file deploy/env.light.example --archive "$archive"
|
| 128 |
+
cp "$archive/manifest.json" "$RELEASE_ARTIFACT_DIR/backup-light-manifest.json"
|
| 129 |
+
docker compose --project-name brainapi-ci -f deploy/docker-compose.light.yaml -f deploy/docker-compose.ci.yaml down --volumes
|
| 130 |
+
deploy/brainapi-backup restore --profile light --project-name brainapi-ci --env-file deploy/env.light.example --compose-file deploy/docker-compose.ci.yaml --archive "$archive"
|
| 131 |
+
curl --fail --retry 12 --retry-delay 5 http://localhost/health
|
| 132 |
+
python scripts/production_smoke.py verify-restore --profile light --system-token "$BRAINPAT_TOKEN" --artifact-dir "$RELEASE_ARTIFACT_DIR"
|
| 133 |
+
- name: Record exact-image security evidence
|
| 134 |
+
run: |
|
| 135 |
+
cp brainapi.spdx.json "$RELEASE_ARTIFACT_DIR/brainapi.spdx.json"
|
| 136 |
+
python scripts/write_security_artifact.py --trivy trivy.json --sbom "$RELEASE_ARTIFACT_DIR/brainapi.spdx.json" --image "$BRAINAPI_IMAGE" --output "$RELEASE_ARTIFACT_DIR/security.json"
|
| 137 |
+
- name: Logs on failure
|
| 138 |
+
if: failure()
|
| 139 |
+
run: docker compose --project-name brainapi-ci -f deploy/docker-compose.light.yaml -f deploy/docker-compose.ci.yaml logs --no-color
|
| 140 |
+
- name: Clean CI volumes
|
| 141 |
+
if: always()
|
| 142 |
+
run: docker compose --project-name brainapi-ci -f deploy/docker-compose.light.yaml -f deploy/docker-compose.ci.yaml down --volumes --remove-orphans
|
| 143 |
+
- uses: actions/upload-artifact@v4
|
| 144 |
+
if: always()
|
| 145 |
+
with:
|
| 146 |
+
name: light-production-validation
|
| 147 |
+
path: |
|
| 148 |
+
release-artifacts/
|
| 149 |
+
trivy.json
|
.github/workflows/release.yaml
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Publish tagged container
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
tags: ["v*"]
|
| 6 |
+
|
| 7 |
+
permissions:
|
| 8 |
+
contents: read
|
| 9 |
+
checks: read
|
| 10 |
+
actions: read
|
| 11 |
+
packages: write
|
| 12 |
+
id-token: write
|
| 13 |
+
|
| 14 |
+
jobs:
|
| 15 |
+
validate-tag:
|
| 16 |
+
runs-on: ubuntu-latest
|
| 17 |
+
outputs:
|
| 18 |
+
version: ${{ steps.tag.outputs.version }}
|
| 19 |
+
steps:
|
| 20 |
+
- uses: actions/checkout@v4
|
| 21 |
+
with:
|
| 22 |
+
fetch-depth: 0
|
| 23 |
+
- id: tag
|
| 24 |
+
run: python scripts/release_version.py "$GITHUB_REF_NAME" >> "$GITHUB_OUTPUT"
|
| 25 |
+
- name: Require tag version to match project metadata
|
| 26 |
+
run: |
|
| 27 |
+
project_version=$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml","rb"))["project"]["version"])')
|
| 28 |
+
expected_version="${{ steps.tag.outputs.project_version }}"
|
| 29 |
+
test "$project_version" = "$expected_version"
|
| 30 |
+
- name: Require the tagged commit to be on main
|
| 31 |
+
run: git merge-base --is-ancestor "$GITHUB_SHA" origin/main
|
| 32 |
+
|
| 33 |
+
verify-required-checks:
|
| 34 |
+
runs-on: ubuntu-latest
|
| 35 |
+
steps:
|
| 36 |
+
- uses: actions/checkout@v4
|
| 37 |
+
- name: Require successful quality and heavy validation for this commit
|
| 38 |
+
uses: actions/github-script@v7
|
| 39 |
+
with:
|
| 40 |
+
script: |
|
| 41 |
+
const required = [
|
| 42 |
+
"python",
|
| 43 |
+
"benchmark-helpers",
|
| 44 |
+
"frontend (console)",
|
| 45 |
+
"frontend (tui)",
|
| 46 |
+
"rust",
|
| 47 |
+
"image-light-smoke",
|
| 48 |
+
"heavy-smoke-and-restore",
|
| 49 |
+
];
|
| 50 |
+
const runs = await github.paginate(github.rest.checks.listForRef, {
|
| 51 |
+
owner: context.repo.owner,
|
| 52 |
+
repo: context.repo.repo,
|
| 53 |
+
ref: context.sha,
|
| 54 |
+
per_page: 100,
|
| 55 |
+
});
|
| 56 |
+
const failed = required.filter((name) =>
|
| 57 |
+
!runs.some((run) => run.name === name && run.conclusion === "success")
|
| 58 |
+
);
|
| 59 |
+
if (failed.length) {
|
| 60 |
+
core.setFailed(`Missing successful checks for ${context.sha}: ${failed.join(", ")}`);
|
| 61 |
+
}
|
| 62 |
+
- name: Download exact-commit validation artifacts
|
| 63 |
+
uses: actions/github-script@v7
|
| 64 |
+
with:
|
| 65 |
+
script: |
|
| 66 |
+
const fs = require("fs");
|
| 67 |
+
const required = [
|
| 68 |
+
["quality.yaml", "light-production-validation", "light.zip"],
|
| 69 |
+
["heavy-validation.yaml", "heavy-production-validation", "heavy.zip"],
|
| 70 |
+
];
|
| 71 |
+
for (const [workflow, artifactName, output] of required) {
|
| 72 |
+
const response = await github.rest.actions.listWorkflowRuns({
|
| 73 |
+
owner: context.repo.owner,
|
| 74 |
+
repo: context.repo.repo,
|
| 75 |
+
workflow_id: workflow,
|
| 76 |
+
head_sha: context.sha,
|
| 77 |
+
status: "completed",
|
| 78 |
+
per_page: 100,
|
| 79 |
+
});
|
| 80 |
+
const run = response.data.workflow_runs.find(
|
| 81 |
+
(item) => item.conclusion === "success"
|
| 82 |
+
);
|
| 83 |
+
if (!run) {
|
| 84 |
+
core.setFailed(`No successful ${workflow} run found for ${context.sha}`);
|
| 85 |
+
return;
|
| 86 |
+
}
|
| 87 |
+
const artifacts = await github.paginate(
|
| 88 |
+
github.rest.actions.listWorkflowRunArtifacts,
|
| 89 |
+
{
|
| 90 |
+
owner: context.repo.owner,
|
| 91 |
+
repo: context.repo.repo,
|
| 92 |
+
run_id: run.id,
|
| 93 |
+
per_page: 100,
|
| 94 |
+
}
|
| 95 |
+
);
|
| 96 |
+
const artifact = artifacts.find(
|
| 97 |
+
(item) => item.name === artifactName && !item.expired
|
| 98 |
+
);
|
| 99 |
+
if (!artifact) {
|
| 100 |
+
core.setFailed(`Missing ${artifactName} on workflow run ${run.id}`);
|
| 101 |
+
return;
|
| 102 |
+
}
|
| 103 |
+
const archive = await github.rest.actions.downloadArtifact({
|
| 104 |
+
owner: context.repo.owner,
|
| 105 |
+
repo: context.repo.repo,
|
| 106 |
+
artifact_id: artifact.id,
|
| 107 |
+
archive_format: "zip",
|
| 108 |
+
});
|
| 109 |
+
fs.writeFileSync(output, Buffer.from(archive.data));
|
| 110 |
+
}
|
| 111 |
+
- name: Enforce recorded product, restore, security, and benchmark gates
|
| 112 |
+
run: |
|
| 113 |
+
mkdir -p gate-input/light gate-input/heavy gate-artifacts
|
| 114 |
+
unzip -q light.zip -d gate-input/light
|
| 115 |
+
unzip -q heavy.zip -d gate-input/heavy
|
| 116 |
+
python scripts/assemble_release_artifacts.py --light gate-input/light --heavy gate-input/heavy --output gate-artifacts
|
| 117 |
+
python scripts/check_release_readiness.py gate-artifacts
|
| 118 |
+
- uses: actions/upload-artifact@v4
|
| 119 |
+
with:
|
| 120 |
+
name: release-evidence-${{ github.ref_name }}
|
| 121 |
+
path: gate-artifacts/
|
| 122 |
+
|
| 123 |
+
publish:
|
| 124 |
+
runs-on: ubuntu-latest
|
| 125 |
+
needs: [validate-tag, verify-required-checks]
|
| 126 |
+
steps:
|
| 127 |
+
- uses: actions/checkout@v4
|
| 128 |
+
- uses: actions/download-artifact@v4
|
| 129 |
+
with:
|
| 130 |
+
name: release-evidence-${{ github.ref_name }}
|
| 131 |
+
path: gate-artifacts
|
| 132 |
+
- name: Load and verify the gated image
|
| 133 |
+
run: |
|
| 134 |
+
gzip -dc gate-artifacts/brainapi-image.tar.gz | docker load
|
| 135 |
+
expected=$(python -c 'import json; print(json.load(open("gate-artifacts/security.json"))["image_digest"])')
|
| 136 |
+
actual=$(docker image inspect brainapi:ci --format '{{.Id}}')
|
| 137 |
+
test "$actual" = "$expected"
|
| 138 |
+
- uses: docker/login-action@v3
|
| 139 |
+
with:
|
| 140 |
+
registry: ghcr.io
|
| 141 |
+
username: ${{ github.actor }}
|
| 142 |
+
password: ${{ secrets.GITHUB_TOKEN }}
|
| 143 |
+
- id: publish
|
| 144 |
+
name: Publish the unchanged validated image
|
| 145 |
+
run: |
|
| 146 |
+
target="ghcr.io/lumen-labs/brainapi:$GITHUB_REF_NAME"
|
| 147 |
+
docker tag brainapi:ci "$target"
|
| 148 |
+
docker push "$target"
|
| 149 |
+
digest=$(docker image inspect "$target" --format '{{index .RepoDigests 0}}' | cut -d@ -f2)
|
| 150 |
+
test -n "$digest"
|
| 151 |
+
echo "$digest" > image-digest.txt
|
| 152 |
+
- uses: actions/upload-artifact@v4
|
| 153 |
+
with:
|
| 154 |
+
name: release-image-digest-${{ needs.validate-tag.outputs.version }}
|
| 155 |
+
path: image-digest.txt
|
.gitignore
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.vscode
|
| 2 |
+
.env
|
| 3 |
+
volume*
|
| 4 |
+
__pycache__
|
| 5 |
+
*.excalidraw
|
| 6 |
+
dist
|
| 7 |
+
common.egg-info
|
| 8 |
+
.venv
|
| 9 |
+
.env.development
|
| 10 |
+
venv/
|
| 11 |
+
READMEP.md
|
| 12 |
+
tmp
|
| 13 |
+
gcp_credentials.json
|
| 14 |
+
.favorites.json
|
| 15 |
+
.DS_Store
|
| 16 |
+
|
| 17 |
+
mcp-stdio-http-bridge/target/
|
| 18 |
+
mcp-stdio-http-bridge/dist/
|
| 19 |
+
|
| 20 |
+
plugins/**
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
.git
|
| 24 |
+
|
| 25 |
+
tui/node_modules
|
| 26 |
+
tui/dist
|
| 27 |
+
.cursor
|
| 28 |
+
tui/embedded
|
| 29 |
+
|
| 30 |
+
.agent_tmp
|
| 31 |
+
|
| 32 |
+
dump.rdb
|
.hfignore
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.git/
|
| 2 |
+
.github/
|
| 3 |
+
.venv/
|
| 4 |
+
__pycache__/
|
| 5 |
+
*.pyc
|
| 6 |
+
node_modules/
|
| 7 |
+
console/node_modules/
|
| 8 |
+
tui/node_modules/
|
| 9 |
+
.pytest_cache/
|
| 10 |
+
.coverage
|
| 11 |
+
.env
|
.pylintrc
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[MESSAGES CONTROL]
|
| 2 |
+
enable=abstract-method
|
| 3 |
+
|
| 4 |
+
[DESIGN]
|
| 5 |
+
max-args=10
|
| 6 |
+
max-locals=15
|
| 7 |
+
max-returns=6
|
| 8 |
+
max-branches=12
|
| 9 |
+
max-statements=50
|
| 10 |
+
|
| 11 |
+
[BASIC]
|
| 12 |
+
good-names=i,j,k,ex,Run,_,id,db,ai,llm
|
| 13 |
+
|
| 14 |
+
[IMPORTS]
|
| 15 |
+
extension-pkg-allow-list=chunker_cpp
|
| 16 |
+
|
| 17 |
+
[IGNORE]
|
| 18 |
+
C0103
|
AGENTS.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Agent guidance
|
| 2 |
+
|
| 3 |
+
Project agent guidance for benchmarks: see [`benchmarks/AGENTS.md`](benchmarks/AGENTS.md).
|
| 4 |
+
|
| 5 |
+
Benchmark results ledger (all suites): [`benchmarks/REPORTS.json`](benchmarks/REPORTS.json).
|
Agent.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Agent Deployment Guide for Hugging Face Space
|
| 2 |
+
|
| 3 |
+
This document serves as the guide and best practices for automated agents deploying and maintaining BrainAPI on Hugging Face Spaces.
|
| 4 |
+
|
| 5 |
+
## 1. Deployment Configuration
|
| 6 |
+
|
| 7 |
+
### Target Space
|
| 8 |
+
- **Profile:** `Leon4gr45`
|
| 9 |
+
- **Space:** `brain`
|
| 10 |
+
- **Full Identifier:** `Leon4gr45/brain`
|
| 11 |
+
- **Frontend Port:** `7860` (mandatory for Hugging Face Spaces)
|
| 12 |
+
|
| 13 |
+
### Deployment Method
|
| 14 |
+
- **Docker SDK** (`Dockerfile`)
|
| 15 |
+
|
| 16 |
+
### HF Token
|
| 17 |
+
- Always pass/read the HF access token from the environment (`HF_TOKEN` environment variable). Never hardcode secrets or token strings in repository files.
|
| 18 |
+
|
| 19 |
+
### Required Files
|
| 20 |
+
- `Dockerfile` (exposing nginx on port `7860`; uvicorn stays internal on port `8000`)
|
| 21 |
+
- `README.md` with Hugging Face YAML frontmatter:
|
| 22 |
+
```yaml
|
| 23 |
+
---
|
| 24 |
+
title: BrainAPI
|
| 25 |
+
sdk: docker
|
| 26 |
+
app_port: 7860
|
| 27 |
+
---
|
| 28 |
+
```
|
| 29 |
+
- `.hfignore` to exclude `.git`, `.venv`, `node_modules`, and cache directories
|
| 30 |
+
- `Agent.md` (this file)
|
| 31 |
+
|
| 32 |
+
---
|
| 33 |
+
|
| 34 |
+
## 2. API Exposure and Documentation
|
| 35 |
+
|
| 36 |
+
### Mandatory Endpoints
|
| 37 |
+
|
| 38 |
+
- **`/health`**
|
| 39 |
+
- Method: GET
|
| 40 |
+
- Purpose: Returns HTTP 200 when the app is ready. Required for Hugging Face Space liveness.
|
| 41 |
+
- Request: None
|
| 42 |
+
- Response:
|
| 43 |
+
```json
|
| 44 |
+
{
|
| 45 |
+
"status": "ok",
|
| 46 |
+
"service": "brainapi",
|
| 47 |
+
"version": "2.19.0-dev"
|
| 48 |
+
}
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
- **`/api-docs`**
|
| 52 |
+
- Method: GET
|
| 53 |
+
- Purpose: Redirects/Exposes full OpenAPI / Swagger documentation for all endpoints. Reachable at `https://Leon4gr45-brain.hf.space/api-docs`.
|
| 54 |
+
- Response: HTTP 307 redirect to `/docs` or Swagger UI HTML page.
|
| 55 |
+
|
| 56 |
+
- **`/`**
|
| 57 |
+
- Method: GET
|
| 58 |
+
- Purpose: Redirects public root visits to `/console/`.
|
| 59 |
+
- Response: HTTP 307 redirect to `/console/`.
|
| 60 |
+
|
| 61 |
+
---
|
| 62 |
+
|
| 63 |
+
## 3. Deployment Workflow & Ongoing Best Practices
|
| 64 |
+
|
| 65 |
+
1. **Pre-flight Check:**
|
| 66 |
+
Clean up non-project or stale files in the target Hugging Face Space using `huggingface_hub` Python API.
|
| 67 |
+
|
| 68 |
+
2. **Deploy Command:**
|
| 69 |
+
```bash
|
| 70 |
+
python deploy/hf_space.py stage --output /tmp/brainapi-hf-source
|
| 71 |
+
# Inspect/build the staged tree before uploading that same Git revision.
|
| 72 |
+
python deploy/hf_space.py upload --revision HEAD
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
This stages the complete committed Git tree, checks every required Docker
|
| 76 |
+
input, and verifies the uploaded Space files against the staged content.
|
| 77 |
+
Run it from the BrainAPI checkout with `huggingface_hub` installed and
|
| 78 |
+
`HF_TOKEN` set through secure environment settings. Do not upload a manually
|
| 79 |
+
selected list of changed files. The readiness wrapper must remain in the
|
| 80 |
+
Space repository alongside `scripts/preload_ollama_models.sh`.
|
| 81 |
+
|
| 82 |
+
3. **Log Monitoring:**
|
| 83 |
+
- **Build Logs (SSE):**
|
| 84 |
+
```bash
|
| 85 |
+
curl -N -H "Authorization: Bearer $HF_TOKEN" "https://huggingface.co/api/spaces/Leon4gr45/brain/logs/build"
|
| 86 |
+
```
|
| 87 |
+
- **Run Logs (SSE):**
|
| 88 |
+
```bash
|
| 89 |
+
curl -N -H "Authorization: Bearer $HF_TOKEN" "https://huggingface.co/api/spaces/Leon4gr45/brain/logs/run"
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
4. **Iterative Debugging:**
|
| 93 |
+
Inspect build and run logs for missing dependencies, port binding issues, or startup exceptions. Modify codebase accordingly, redeploy, and monitor until the Space reaches `RUNNING` status and responds to `/health` and `/api-docs`.
|
| 94 |
+
|
| 95 |
+
### Functional Endpoints
|
| 96 |
+
|
| 97 |
+
### /retrieve/context
|
| 98 |
+
- Method: POST
|
| 99 |
+
- Purpose: Retrieve relevant information for a piece of text (graph triples, passages).
|
| 100 |
+
- Request:
|
| 101 |
+
{
|
| 102 |
+
"text": "hello world",
|
| 103 |
+
"brain_id": "default"
|
| 104 |
+
}
|
| 105 |
+
- Response:
|
| 106 |
+
{
|
| 107 |
+
"text_context": "..."
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
### /retrieve/search
|
| 111 |
+
- Method: GET
|
| 112 |
+
- Purpose: Ranked search hits.
|
| 113 |
+
- Request: /retrieve/search?query=hello
|
| 114 |
+
- Response:
|
| 115 |
+
{
|
| 116 |
+
"results": [...]
|
| 117 |
+
}
|
CLAUDE.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
@AGENTS.md
|
CONTRIBUTING.md
ADDED
|
File without changes
|
Dockerfile
CHANGED
|
@@ -126,7 +126,9 @@ COPY --from=ollama-models /opt/ollama-models /opt/ollama-models
|
|
| 126 |
COPY --from=console-builder /console/dist /app/console/dist
|
| 127 |
COPY deploy/ ./deploy/
|
| 128 |
COPY entrypoint.sh ./
|
| 129 |
-
COPY scripts/
|
|
|
|
|
|
|
| 130 |
|
| 131 |
RUN chmod +x /app/entrypoint.sh
|
| 132 |
|
|
|
|
| 126 |
COPY --from=console-builder /console/dist /app/console/dist
|
| 127 |
COPY deploy/ ./deploy/
|
| 128 |
COPY entrypoint.sh ./
|
| 129 |
+
COPY scripts/ ./scripts/
|
| 130 |
+
RUN test -f scripts/start_with_ollama.py \
|
| 131 |
+
&& test -f scripts/preload_ollama_models.sh
|
| 132 |
|
| 133 |
RUN chmod +x /app/entrypoint.sh
|
| 134 |
|
LICENSE
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Business Source License 1.1
|
| 2 |
+
|
| 3 |
+
Parameters
|
| 4 |
+
|
| 5 |
+
Licensor: Lumen Platforms Inc.
|
| 6 |
+
Licensed Work: BrainAPI
|
| 7 |
+
The Licensed Work is (c) 2026 Lumen Platforms Inc.
|
| 8 |
+
Additional Use Grant: None
|
| 9 |
+
Change Date: 2030-08-13
|
| 10 |
+
Change License: Apache License, Version 2.0
|
| 11 |
+
|
| 12 |
+
For information about alternative licensing arrangements for the Software,
|
| 13 |
+
please contact hello@lumen-labs.ai. The Change License text is included at
|
| 14 |
+
licenses/Apache-2.0.txt.
|
| 15 |
+
|
| 16 |
+
Notice
|
| 17 |
+
|
| 18 |
+
The Business Source License (this document, or the "License") is not an Open
|
| 19 |
+
Source license. However, the Licensed Work will eventually be made available
|
| 20 |
+
under an Open Source License, as stated in this License.
|
| 21 |
+
|
| 22 |
+
License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
|
| 23 |
+
"Business Source License" is a trademark of MariaDB Corporation Ab.
|
| 24 |
+
|
| 25 |
+
-----------------------------------------------------------------------------
|
| 26 |
+
|
| 27 |
+
Business Source License 1.1
|
| 28 |
+
|
| 29 |
+
Terms
|
| 30 |
+
|
| 31 |
+
The Licensor hereby grants you the right to copy, modify, create derivative
|
| 32 |
+
works, redistribute, and make non-production use of the Licensed Work. The
|
| 33 |
+
Licensor may make an Additional Use Grant, above, permitting limited
|
| 34 |
+
production use.
|
| 35 |
+
|
| 36 |
+
Effective on the Change Date, or the fourth anniversary of the first publicly
|
| 37 |
+
available distribution of a specific version of the Licensed Work under this
|
| 38 |
+
License, whichever comes first, the Licensor hereby grants you rights under
|
| 39 |
+
the terms of the Change License, and the rights granted in the paragraph
|
| 40 |
+
above terminate.
|
| 41 |
+
|
| 42 |
+
If your use of the Licensed Work does not comply with the requirements
|
| 43 |
+
currently in effect as described in this License, you must purchase a
|
| 44 |
+
commercial license from the Licensor, its affiliated entities, or authorized
|
| 45 |
+
resellers, or you must refrain from using the Licensed Work.
|
| 46 |
+
|
| 47 |
+
All copies of the original and modified Licensed Work, and derivative works
|
| 48 |
+
of the Licensed Work, are subject to this License. This License applies
|
| 49 |
+
separately for each version of the Licensed Work and the Change Date may vary
|
| 50 |
+
for each version of the Licensed Work released by Licensor.
|
| 51 |
+
|
| 52 |
+
You must conspicuously display this License on each original or modified copy
|
| 53 |
+
of the Licensed Work. If you receive the Licensed Work in original or
|
| 54 |
+
modified form from a third party, the terms and conditions set forth in this
|
| 55 |
+
License apply to your use of that work.
|
| 56 |
+
|
| 57 |
+
Any use of the Licensed Work in violation of this License will automatically
|
| 58 |
+
terminate your rights under this License for the current and all other
|
| 59 |
+
versions of the Licensed Work.
|
| 60 |
+
|
| 61 |
+
This License does not grant you any right in any trademark or logo of
|
| 62 |
+
Licensor or its affiliates (provided that you may use a trademark or logo of
|
| 63 |
+
Licensor as expressly required by this License).
|
| 64 |
+
|
| 65 |
+
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
|
| 66 |
+
AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
|
| 67 |
+
EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
|
| 68 |
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
|
| 69 |
+
TITLE.
|
| 70 |
+
|
| 71 |
+
MariaDB hereby grants you permission to use this License's text to license
|
| 72 |
+
your works, and to refer to it using the trademark "Business Source License",
|
| 73 |
+
as long as you comply with the Covenants of Licensor below.
|
| 74 |
+
|
| 75 |
+
Covenants of Licensor
|
| 76 |
+
|
| 77 |
+
In consideration of the right to use this License's text and the "Business
|
| 78 |
+
Source License" name and trademark, Licensor covenants to MariaDB, and to all
|
| 79 |
+
other recipients of the licensed work to be provided by Licensor:
|
| 80 |
+
|
| 81 |
+
1. To specify as the Change License the GPL Version 2.0 or any later version,
|
| 82 |
+
or a license that is compatible with GPL Version 2.0 or a later version,
|
| 83 |
+
where "compatible" means that software provided under the Change License can
|
| 84 |
+
be included in a program with software provided under GPL Version 2.0 or a
|
| 85 |
+
later version. Licensor may specify additional Change Licenses without
|
| 86 |
+
limitation.
|
| 87 |
+
|
| 88 |
+
2. To either: (a) specify an additional grant of rights to use that does not
|
| 89 |
+
impose any additional restriction on the right granted in this License, as
|
| 90 |
+
the Additional Use Grant; or (b) insert the text "None".
|
| 91 |
+
|
| 92 |
+
3. To specify a Change Date.
|
| 93 |
+
|
| 94 |
+
4. Not to modify this License in any other way.
|
Makefile
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
start-milvus:
|
| 2 |
+
docker compose -f src/lib/milvus/docker-compose.yaml up -d
|
| 3 |
+
|
| 4 |
+
stop-milvus:
|
| 5 |
+
docker compose -f src/lib/milvus/docker-compose.yaml down
|
| 6 |
+
|
| 7 |
+
delete-milvus-volumes:
|
| 8 |
+
docker compose -f src/lib/milvus/docker-compose.yaml down -v --remove-orphans
|
| 9 |
+
|
| 10 |
+
start-rabbitmq:
|
| 11 |
+
docker compose -f src/lib/rabbitmq/docker-compose.yaml up -d
|
| 12 |
+
|
| 13 |
+
stop-rabbitmq:
|
| 14 |
+
docker compose -f src/lib/rabbitmq/docker-compose.yaml down
|
| 15 |
+
|
| 16 |
+
delete-rabbitmq-volumes:
|
| 17 |
+
docker compose -f src/lib/rabbitmq/docker-compose.yaml down -v --remove-orphans
|
| 18 |
+
|
| 19 |
+
start-redis:
|
| 20 |
+
docker compose -f src/lib/redis/docker-compose.yaml up -d
|
| 21 |
+
|
| 22 |
+
stop-redis:
|
| 23 |
+
docker compose -f src/lib/redis/docker-compose.yaml down
|
| 24 |
+
|
| 25 |
+
delete-redis-volumes:
|
| 26 |
+
docker compose -f src/lib/redis/docker-compose.yaml down -v --remove-orphans
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
start-neo4j:
|
| 30 |
+
docker compose -f src/lib/neo4j/docker-compose.yaml up -d
|
| 31 |
+
|
| 32 |
+
stop-neo4j:
|
| 33 |
+
docker compose -f src/lib/neo4j/docker-compose.yaml down
|
| 34 |
+
|
| 35 |
+
delete-neo4j-volumes:
|
| 36 |
+
docker compose -f src/lib/neo4j/docker-compose.yaml down -v --remove-orphans
|
| 37 |
+
|
| 38 |
+
build-neo4j-extension:
|
| 39 |
+
docker run --rm -v $(PWD)/src/lib/neo4j:/app -w /app maven:3.9-eclipse-temurin-17 mvn compile
|
| 40 |
+
|
| 41 |
+
package-neo4j-extension:
|
| 42 |
+
docker run --rm -v $(PWD)/src/lib/neo4j:/app -w /app maven:3.9-eclipse-temurin-17 mvn package -DskipTests
|
| 43 |
+
|
| 44 |
+
start-mongo:
|
| 45 |
+
docker compose -f src/lib/mongo/docker-compose.yaml up -d
|
| 46 |
+
|
| 47 |
+
stop-mongo:
|
| 48 |
+
docker compose -f src/lib/mongo/docker-compose.yaml down
|
| 49 |
+
|
| 50 |
+
delete-mongo-volumes:
|
| 51 |
+
docker compose -f src/lib/mongo/docker-compose.yaml down -v --remove-orphans
|
| 52 |
+
|
| 53 |
+
start-api:
|
| 54 |
+
ENV=development CONSOLE_ENABLED=true poetry run python -m uvicorn src.services.api.app:app --host 0.0.0.0 --port 8000 --access-log --log-level info --reload --reload-dir src
|
| 55 |
+
|
| 56 |
+
stop-api:
|
| 57 |
+
pkill -f "uvicorn src.services.api.app"
|
| 58 |
+
|
| 59 |
+
start-mcp:
|
| 60 |
+
ENV=development poetry run python -m uvicorn src.services.mcp.app:app --host 0.0.0.0 --port 8001 --access-log --log-level info --reload --reload-dir src
|
| 61 |
+
|
| 62 |
+
MCP_BRIDGE_DIR := mcp-stdio-http-bridge
|
| 63 |
+
MCP_BRIDGE_VERSION := $(shell grep '^version = ' $(MCP_BRIDGE_DIR)/Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')
|
| 64 |
+
MCP_BRIDGE_TAG := mcp-bridge-v$(MCP_BRIDGE_VERSION)
|
| 65 |
+
MCP_BRIDGE_TOOLCHAIN := $(shell grep 'channel = ' $(MCP_BRIDGE_DIR)/rust-toolchain.toml | sed 's/.*"\(.*\)"/\1/')
|
| 66 |
+
|
| 67 |
+
build-mcp-bridge:
|
| 68 |
+
cd $(MCP_BRIDGE_DIR) && cargo build --release
|
| 69 |
+
|
| 70 |
+
install-mcp-bridge-targets:
|
| 71 |
+
rustup target add x86_64-apple-darwin --toolchain $(MCP_BRIDGE_TOOLCHAIN)
|
| 72 |
+
|
| 73 |
+
build-mcp-bridge-x86: install-mcp-bridge-targets
|
| 74 |
+
cd $(MCP_BRIDGE_DIR) && cargo build --release --target x86_64-apple-darwin
|
| 75 |
+
|
| 76 |
+
build-mcp-bridge-all: build-mcp-bridge build-mcp-bridge-x86
|
| 77 |
+
|
| 78 |
+
push-mcp-bridge-packages: build-mcp-bridge-all
|
| 79 |
+
@mkdir -p $(MCP_BRIDGE_DIR)/dist && \
|
| 80 |
+
cp $(MCP_BRIDGE_DIR)/target/release/mcp-stdio-http-bridge $(MCP_BRIDGE_DIR)/dist/mcp-stdio-http-bridge-aarch64-apple-darwin && \
|
| 81 |
+
cp $(MCP_BRIDGE_DIR)/target/x86_64-apple-darwin/release/mcp-stdio-http-bridge $(MCP_BRIDGE_DIR)/dist/mcp-stdio-http-bridge-x86_64-apple-darwin && \
|
| 82 |
+
gh release create $(MCP_BRIDGE_TAG) \
|
| 83 |
+
$(MCP_BRIDGE_DIR)/dist/mcp-stdio-http-bridge-aarch64-apple-darwin \
|
| 84 |
+
$(MCP_BRIDGE_DIR)/dist/mcp-stdio-http-bridge-x86_64-apple-darwin \
|
| 85 |
+
--title "MCP stdio-http bridge $(MCP_BRIDGE_VERSION)" \
|
| 86 |
+
--notes "Binary builds for macOS (Apple Silicon and Intel)."
|
| 87 |
+
|
| 88 |
+
start-nginx:
|
| 89 |
+
docker compose -f src/services/webserver/docker-compose.yaml up -d --force-recreate
|
| 90 |
+
|
| 91 |
+
stop-mcp:
|
| 92 |
+
pkill -f "uvicorn src.services.mcp.app"
|
| 93 |
+
|
| 94 |
+
DEBUG_ENVS := LANGCHAIN_DEBUG="true" LANGCHAIN_VERBOSE="true" DEBUG="true"
|
| 95 |
+
|
| 96 |
+
start-all:
|
| 97 |
+
@if [ "$(filter debug,$(MAKECMDGOALS))" = "debug" ] || [ "$$DEBUG" = "true" ]; then \
|
| 98 |
+
echo "DEBUG mode enabled"; \
|
| 99 |
+
$(MAKE) start-milvus DEBUG=true & \
|
| 100 |
+
$(MAKE) start-rabbitmq DEBUG=true & \
|
| 101 |
+
$(MAKE) start-redis DEBUG=true & \
|
| 102 |
+
$(MAKE) start-neo4j DEBUG=true & \
|
| 103 |
+
$(MAKE) start-mongo DEBUG=true & \
|
| 104 |
+
$(MAKE) start-mcp DEBUG=true & \
|
| 105 |
+
ENV="development" $(MAKE) start-api DEBUG=true & \
|
| 106 |
+
bash -c "export $(DEBUG_ENVS) ENV="development" && poetry run celery -A src.workers.app worker --loglevel=info --pool=threads"; \
|
| 107 |
+
else \
|
| 108 |
+
$(MAKE) start-milvus & \
|
| 109 |
+
$(MAKE) start-rabbitmq & \
|
| 110 |
+
$(MAKE) start-redis & \
|
| 111 |
+
$(MAKE) start-neo4j & \
|
| 112 |
+
$(MAKE) start-mongo & \
|
| 113 |
+
$(MAKE) start-mcp & \
|
| 114 |
+
ENV="development" $(MAKE) start-api & \
|
| 115 |
+
ENV="development" poetry run celery -A src.workers.app worker --loglevel=info --pool=threads; \
|
| 116 |
+
fi
|
| 117 |
+
|
| 118 |
+
debug:
|
| 119 |
+
@:
|
| 120 |
+
|
| 121 |
+
stop-all:
|
| 122 |
+
make stop-milvus
|
| 123 |
+
make stop-rabbitmq
|
| 124 |
+
make stop-redis
|
| 125 |
+
make stop-neo4j
|
| 126 |
+
make stop-mongo
|
| 127 |
+
make stop-api
|
| 128 |
+
make stop-mcp
|
| 129 |
+
pkill -f celery
|
| 130 |
+
|
| 131 |
+
clear-all-volumes:
|
| 132 |
+
make delete-milvus-volumes
|
| 133 |
+
make delete-rabbitmq-volumes
|
| 134 |
+
make delete-redis-volumes
|
| 135 |
+
make delete-neo4j-volumes
|
| 136 |
+
make delete-mongo-volumes
|
| 137 |
+
|
| 138 |
+
VERSION := $(shell grep '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/')
|
| 139 |
+
container-build:
|
| 140 |
+
docker build --no-cache --platform linux/amd64 -t ghcr.io/lumen-labs/brainapi:v$(VERSION) ./
|
| 141 |
+
docker tag ghcr.io/lumen-labs/brainapi:v$(VERSION) ghcr.io/lumen-labs/brainapi:latest
|
| 142 |
+
|
| 143 |
+
container-push:
|
| 144 |
+
docker push ghcr.io/lumen-labs/brainapi:v$(VERSION)
|
| 145 |
+
docker push ghcr.io/lumen-labs/brainapi:latest
|
| 146 |
+
|
| 147 |
+
container-release:
|
| 148 |
+
BUILD_DATE=$$(date -u +'%Y-%m-%dT%H:%M:%SZ')
|
| 149 |
+
BUILD_SHA=$$(git rev-parse HEAD || echo "unknown")
|
| 150 |
+
CACHE_BUST=$$(date +%s)
|
| 151 |
+
docker build --no-cache \
|
| 152 |
+
--build-arg BUILD_DATE="$$BUILD_DATE" \
|
| 153 |
+
--build-arg BUILD_SHA="$$BUILD_SHA" \
|
| 154 |
+
--build-arg CACHE_BUST="$$CACHE_BUST" \
|
| 155 |
+
--platform linux/amd64 \
|
| 156 |
+
--label org.opencontainers.image.source="https://github.com/lumen-labs/brainapi2" \
|
| 157 |
+
-t ghcr.io/lumen-labs/brainapi:v$(VERSION) ./
|
| 158 |
+
docker tag ghcr.io/lumen-labs/brainapi:v$(VERSION) ghcr.io/lumen-labs/brainapi:latest
|
| 159 |
+
git tag -s v$(VERSION) -m "Release v$(VERSION)"
|
| 160 |
+
docker push ghcr.io/lumen-labs/brainapi:v$(VERSION)
|
| 161 |
+
docker push ghcr.io/lumen-labs/brainapi:latest
|
| 162 |
+
git push origin v$(VERSION)
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
v-patch:
|
| 166 |
+
poetry version patch
|
| 167 |
+
|
| 168 |
+
v-minor:
|
| 169 |
+
poetry version minor
|
| 170 |
+
|
| 171 |
+
v-major:
|
| 172 |
+
poetry version major
|
| 173 |
+
|
| 174 |
+
install-base:
|
| 175 |
+
poetry install
|
| 176 |
+
|
| 177 |
+
install-extras:
|
| 178 |
+
poetry run python scripts/install_extras.py
|
| 179 |
+
|
| 180 |
+
install-extras-all:
|
| 181 |
+
poetry run python scripts/install_extras.py --all
|
| 182 |
+
|
| 183 |
+
check-extras:
|
| 184 |
+
poetry run python scripts/install_extras.py --check
|
| 185 |
+
|
| 186 |
+
build-console:
|
| 187 |
+
cd console && npm install && npm run build
|
| 188 |
+
|
| 189 |
+
start-console-dev:
|
| 190 |
+
cd console && npm install && npm run dev
|
benchmarks/.env.example
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
BRAINAPI_URL=http://localhost:8000
|
| 2 |
+
BRAINPAT_TOKEN=
|
| 3 |
+
|
| 4 |
+
# Search eval (default searchbenchsmoke). Must start with searchbench.
|
| 5 |
+
# SEARCH_BRAIN_ID=searchbenchsmoke
|
| 6 |
+
|
| 7 |
+
# Dual profile: product (greedy) | sota (deepseek-v4-flash + SC + gap-fill)
|
| 8 |
+
# BENCH_PROFILE=product
|
| 9 |
+
# BENCH_PROFILE=sota
|
| 10 |
+
# BENCH_SC_SAMPLES=5
|
| 11 |
+
# BENCH_SC_TEMPERATURE=0.7
|
| 12 |
+
# BENCH_GAP_FILL=1
|
| 13 |
+
# Optional overrides (default SOTA answerer is deepseek-v4-flash):
|
| 14 |
+
# BENCH_SOTA_ANSWER_MODEL=deepseek-v4-flash
|
| 15 |
+
# BENCH_SOTA_JUDGE_MODEL=deepseek-v4-flash
|
| 16 |
+
|
| 17 |
+
# DeepSeek (OpenAI-compatible). Get a key at https://platform.deepseek.com/
|
| 18 |
+
DEEPSEEK_API_KEY=
|
| 19 |
+
BENCH_LLM_BASE_URL=https://api.deepseek.com
|
| 20 |
+
BENCH_ANSWER_MODEL=deepseek-v4-flash
|
| 21 |
+
|
| 22 |
+
# The judge must be a different model family from the answerer, or judge accuracy
|
| 23 |
+
# carries self-preference bias. Leave BENCH_JUDGE_MODEL unset to auto-select the
|
| 24 |
+
# Azure deployment below; set it explicitly to pin a judge.
|
| 25 |
+
# BENCH_JUDGE_MODEL=
|
| 26 |
+
# BENCH_JUDGE_BASE_URL=
|
| 27 |
+
# BENCH_JUDGE_API_KEY=
|
| 28 |
+
|
| 29 |
+
# Azure OpenAI judge (falls back to the repo-root AZURE_LARGE_LLM_* variables):
|
| 30 |
+
# BENCH_JUDGE_AZURE_ENDPOINT=
|
| 31 |
+
# BENCH_JUDGE_AZURE_KEY=
|
| 32 |
+
# BENCH_JUDGE_AZURE_API_VERSION=2024-12-01-preview
|
| 33 |
+
|
| 34 |
+
# Optional aliases if you prefer another OpenAI-compatible provider:
|
| 35 |
+
# OPENAI_API_KEY=
|
| 36 |
+
# BENCH_LLM_BASE_URL=https://api.openai.com/v1
|
| 37 |
+
# BENCH_ANSWER_MODEL=gpt-4o-mini
|
| 38 |
+
|
| 39 |
+
# BEAM HF datasets (defaults):
|
| 40 |
+
# BENCH_BEAM_HF_DATASET=Mohammadta/BEAM
|
| 41 |
+
# BENCH_BEAM_HF_DATASET_10M=Mohammadta/BEAM-10M
|
benchmarks/.gitignore
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
data/*
|
| 2 |
+
!data/.gitkeep
|
| 3 |
+
runs/*
|
| 4 |
+
!runs/.gitkeep
|
| 5 |
+
.venv/
|
| 6 |
+
venv/
|
| 7 |
+
__pycache__/
|
| 8 |
+
*.pyc
|
| 9 |
+
.env
|
| 10 |
+
.DS_Store
|
| 11 |
+
|
| 12 |
+
!runs/beam-1m-1-product-v3c-compose/
|
| 13 |
+
!runs/beam-1m-1-product-v3c-compose/run.tar.gz
|
| 14 |
+
!runs/locomo-compose-sota-conv26-v4d/
|
| 15 |
+
!runs/locomo-compose-sota-conv26-v4d/run.tar.gz
|
benchmarks/AGENTS.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Benchmarks — agent guidance
|
| 2 |
+
|
| 3 |
+
## Suites
|
| 4 |
+
|
| 5 |
+
| Suite | Package | Wrapper | Ledger key |
|
| 6 |
+
| --- | --- | --- | --- |
|
| 7 |
+
| **LoCoMo** | `benchmarks/locomo/` | `./locomo.sh` | `benchmarks.locomo` |
|
| 8 |
+
| **LongMemEval** | `benchmarks/longmemeval/` | `./longmemeval.sh` | `benchmarks.longmemeval` |
|
| 9 |
+
| **BEAM** | `benchmarks/beam/` | `./beam.sh` | `benchmarks.beam` |
|
| 10 |
+
| **RecSys** | `benchmarks/recsys/` | `./recsys.sh` | `benchmarks.recsys` |
|
| 11 |
+
| **Search** | `benchmarks/search/` | `./search.sh` | `benchmarks.search` |
|
| 12 |
+
|
| 13 |
+
All harnesses talk to a running BrainAPI over HTTP only (no `src/` imports, except Search `mapping.py` re-exporting the product catalog mapper). RecSys uses `POST /ingest/structured` (KB write) + **train-free** `GET /retrieve/recommend` on brain **`demorecsys` only** — never LoCoMo/BEAM brains. Optional: `plugins/features-rec` for attribute prefs; `plugins/recsys-gnn` LightGCN via `--backend lightgcn`. Search uses `POST /ingest/` + `POST /retrieve/search` on **`searchbench*` only** — never LoCoMo/BEAM/recsys brains, never `/retrieve/context` as the metric path.
|
| 14 |
+
|
| 15 |
+
### LoCoMo
|
| 16 |
+
|
| 17 |
+
Typical flow: `download` → `ingest` → `evaluate` → `report` via `./locomo.sh` or `.venv/bin/python -m locomo`.
|
| 18 |
+
|
| 19 |
+
### LongMemEval
|
| 20 |
+
|
| 21 |
+
Typical flow: `download` → `ingest` → `evaluate` → `report` via `./longmemeval.sh` or `.venv/bin/python -m longmemeval`. Protocol: `docs/research/09-longmemeval-protocol.md`.
|
| 22 |
+
|
| 23 |
+
### RecSys
|
| 24 |
+
|
| 25 |
+
Held-out next-item HitRate/Recall@K via **train-free graph recommend** (default) or optional plugin LightGCN. Flow: `download` (optional MovieLens) → structured ingest → `GET /retrieve/recommend` via `./recsys.sh --backend graph`. Brain: `demorecsys`.
|
| 26 |
+
|
| 27 |
+
```bash
|
| 28 |
+
./recsys.sh smoke --backend graph
|
| 29 |
+
./recsys.sh download --name ml-100k
|
| 30 |
+
./recsys.sh evaluate --backend graph --dataset data/movielens_100k.jsonl --max-users 50
|
| 31 |
+
# attributed fixture (color/material):
|
| 32 |
+
./recsys.sh evaluate --backend graph --dataset data/recsys_toy_attrs.jsonl --min-interactions 2
|
| 33 |
+
# optional offline CF:
|
| 34 |
+
./recsys.sh evaluate --backend lightgcn --dataset data/recsys_toy.jsonl --epochs 20
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
Protocol: [`docs/research/16-recsys-eval-protocol.md`](../docs/research/16-recsys-eval-protocol.md). Upserts only `benchmarks.recsys` — never wipe or write to `beam1m1clean` / `locomoconv26*`.
|
| 38 |
+
|
| 39 |
+
### Search
|
| 40 |
+
|
| 41 |
+
Hybrid BM25 + dense ranking via `POST /retrieve/search`, plus optional graph channels (`entities` / `events` / `communities`, `expand=neighbors`). Flow: ingest docs (chunk+embed; LLM enrichment skipped by default) → optional structured catalog triples (`--ingest-graph`) → map `DOCID` markers to chunk ids → search with `profile_stages`. Gold matches chunk ids or `hit.id == doc_id`. Brain: `searchbenchsmoke` (any id must start with `searchbench`). Server must have `SEARCH_ENABLED=true`, `DATA_DB=postgresql`, `BRAIN_CREATION_ALLOWED=true`.
|
| 42 |
+
|
| 43 |
+
```bash
|
| 44 |
+
./search.sh smoke
|
| 45 |
+
./search.sh evaluate --fusion rrf
|
| 46 |
+
./search.sh download --name esci
|
| 47 |
+
./search.sh download --name wands
|
| 48 |
+
./search.sh evaluate --dataset data/search_esci.jsonl --brain searchbenchesci --run search-esci
|
| 49 |
+
./search.sh --brain searchbenchwands evaluate --dataset data/search_wands.jsonl --run search-wands-passages-k50 --channels passages --k 50 --ks 5,10,20,50
|
| 50 |
+
./search.sh --brain searchbenchwands evaluate --dataset data/search_wands.jsonl --run search-wands-passages-k50 --channels passages --k 50 --ks 5,10,20,50 --skip-ingest
|
| 51 |
+
./search.sh --brain searchbenchwandsgraph evaluate --dataset data/search_wands.jsonl --run search-wandsgraph-communities-k50 --channels communities --k 50 --ks 5,10,20,50 --ingest-graph
|
| 52 |
+
./search.sh evaluate --dataset data/search_esci.jsonl --brain searchbenchesci --ingest-graph --channels passages,entities,communities
|
| 53 |
+
./search.sh evaluate --rerank plugin:cross-encoder
|
| 54 |
+
./search.sh report --run <run_id>
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
Catalog downloads are a **subset** of Amazon ESCI (KDD Cup 2022, US Task 1 / small_version test) and WANDS (Wayfair). Default caps: 80 queries, 2000 products, 40 candidates/query. Frozen WANDS quality slice: `data/search_wands.jsonl` (66 queries / 2000 docs); download refuses overwrite. Gold is graded (ESCI E/S/C/I; WANDS Exact/Partial/Irrelevant). WANDS control is passages k=50 `rerank=none` on `searchbenchwands`, then `--skip-ingest`. Opt-in catalog graph is brain `searchbenchwandsgraph` only (`--ingest-graph`, `channels=communities`); ledger rows are an **architecture demo**, not a claim vs 0.823 / ESCI 0.500. `--ingest-graph` is refused on frozen `searchbenchwands` / `searchbenchesci74` / `searchbenchescies` / `searchbenchesciltr2`. Pass `--enrich` only if you want Scout/Architect on ingest. Use dedicated `searchbench*` brains — never `demorecsys`. Graph mapping is product `src/core/search/catalog_graph.py` (generic HAS events + CLASS/TYPE/ATTR hubs). Do not score recs HitRate as search.
|
| 58 |
+
|
| 59 |
+
Metrics: Recall@{5,10,20}, nDCG@10, MRR; p50/p95 retrieve **excluding** embed RTT. No LLM judge. Protocol: [`docs/research/18-search-eval-protocol.md`](../docs/research/18-search-eval-protocol.md). Upserts only `benchmarks.search` — never wipe or write to `locomoconv*`, `beam*`, `demorecsys`.
|
| 60 |
+
|
| 61 |
+
### BEAM
|
| 62 |
+
|
| 63 |
+
Typical flow: `download` → `ingest` → `evaluate` → `report` via `./beam.sh` or `.venv/bin/python -m beam`.
|
| 64 |
+
|
| 65 |
+
- Dataset: HuggingFace `Mohammadta/BEAM` (`100K` / `500K` / `1M`) and `Mohammadta/BEAM-10M` (`10M`). Harness supports all four sizes in `CHAT_SIZES`.
|
| 66 |
+
- 10M normalize: concatenate interlocking plans chronologically into one chat (`bN_tM` / `session_N`); see [`docs/research/10-beam-protocol.md`](../docs/research/10-beam-protocol.md) § BEAM-10M. Brain `beam10m1` / `--brain beam10m1clean`; never wipe `beam1m1clean`.
|
| 67 |
+
- Download sample 1 once `.env` exists: `./beam.sh download --size 10M` then `./beam.sh dataset-stats --size 10M` (read `n_turns` before any ETA). Ingest/eval still need live API + explicit go (`scripts/run_beam_10m_1_ingest_eval.sh`).
|
| 68 |
+
- Ingest turns only — never probing questions, rubrics, or ideal answers.
|
| 69 |
+
- Scoring: rubric LLM-judge (`judge_prompt_variant: beam-rubric-v1-question-aware`); headline = mean of 10 ability means; `event_ordering` uses `tau_norm`.
|
| 70 |
+
- Brain IDs: `beam{size}{convid}` → e.g. `beam100k1`, `beam10m1`.
|
| 71 |
+
- Protocol: [`docs/research/10-beam-protocol.md`](../docs/research/10-beam-protocol.md).
|
| 72 |
+
- Ingest parallelism: prefer `--concurrency 2` (≤4); resume skips permanent embed-8192 fails. See protocol “Parallel ingest” + `scripts/run_beam_1m_1_ingest_eval.sh` / `scripts/run_beam_10m_1_ingest_eval.sh` (`BEAM_INGEST_CONCURRENCY`). Do not wipe live eval brains.
|
| 73 |
+
|
| 74 |
+
## Results ledger (`REPORTS.json`)
|
| 75 |
+
|
| 76 |
+
`benchmarks/REPORTS.json` is the public BrainAPI results ledger: top published scores for **all** benchmark suites under `benchmarks.<suite>`.
|
| 77 |
+
|
| 78 |
+
- LoCoMo harness upserts `benchmarks.locomo` via `write_report` → `update_reports_json` when report `status` is `ok`.
|
| 79 |
+
- BEAM harness upserts `benchmarks.beam` the same way (field: `headline_score`, continuous `[0,1]`).
|
| 80 |
+
- RecSys harness upserts `benchmarks.recsys` only (`hit_rate@K` / `recall@K`); it must not touch other suite keys.
|
| 81 |
+
- Search harness upserts `benchmarks.search` only (`ndcg@10` / `recall@10` / `p50_retrieve_ms`); it must not touch other suite keys.
|
| 82 |
+
- After any completed evaluate/report that produces a successful scored run, ensure the suite entry in `REPORTS.json` reflects it.
|
| 83 |
+
- If you write or patch `runs/<id>/report.json` manually, update `REPORTS.json` yourself (same schema; upsert by `run_id` under the suite).
|
| 84 |
+
- When adding a new suite, add a `benchmarks.<suite_id>` key (`name` + `leaderboard`) and upsert the same way.
|
| 85 |
+
- Skip failed/junk runs (`status: failed`, 0% judge, nonsense scores) — they do not belong on the leaderboard.
|
| 86 |
+
- Research caveats and checkpoint narrative live in `runs/CHECKPOINT_NOTES.md` (local), not in `REPORTS.json`.
|
| 87 |
+
|
| 88 |
+
## Tracks (LoCoMo)
|
| 89 |
+
|
| 90 |
+
| Track | Profile | Intent |
|
| 91 |
+
| --- | --- | --- |
|
| 92 |
+
| **SOTA** | `BENCH_PROFILE=sota` | Beat HyperMem-class LoCoMo (≥93% on full-10). Prefer **deepseek-v4-flash** answerer/judge. SC + gap-fill live in the harness, not product latency path. |
|
| 93 |
+
| **Product** | `BENCH_PROFILE=product` (default) | ADR-006 sub-second `/retrieve/context` path — greedy, no online LLM loops. |
|
| 94 |
+
|
| 95 |
+
Protocol and competitor bar: `docs/research/08-sota-locomo-protocol.md`. Historical checkpoint narrative: `runs/CHECKPOINT_NOTES.md`.
|
| 96 |
+
|
| 97 |
+
## Live stack
|
| 98 |
+
|
| 99 |
+
TUI / `brainapi start` may run from `~/.brainapi/source`, not this workspace. Sync or restart from the intended checkout before measuring, or scores will reflect stale code.
|
| 100 |
+
|
| 101 |
+
## Conventions
|
| 102 |
+
|
| 103 |
+
- Brain IDs are alphanumeric (`conv-26` → `locomoconv26`; BEAM `100K/1` → `beam100k1`). Prefer clean brains for product/SOTA claims after Phase 0.
|
| 104 |
+
- Compare LoCoMo judge accuracy with `./locomo.sh compare` (paired McNemar). Do not A/B graph EvR until identical-config session-set agreement clears the ≥95% gate.
|
| 105 |
+
- Do not ingest QA annotations / probing questions. Keep answer prompts prompt-audit clean where the suite provides it (`./locomo.sh prompt-audit`).
|
benchmarks/CLAUDE.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
@AGENTS.md
|
benchmarks/README.md
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Benchmarks for BrainAPI
|
| 2 |
+
|
| 3 |
+
Standalone HTTP-only harnesses (no `src/` imports):
|
| 4 |
+
|
| 5 |
+
| Suite | Package | Wrapper |
|
| 6 |
+
| --- | --- | --- |
|
| 7 |
+
| [LoCoMo](https://github.com/snap-research/LoCoMo) | `locomo/` | `./locomo.sh` |
|
| 8 |
+
| [LongMemEval](https://github.com/xiaowu0162/LongMemEval) | `longmemeval/` | `./longmemeval.sh` |
|
| 9 |
+
| [BEAM](https://github.com/mohammadtavakoli78/BEAM) | `beam/` | `./beam.sh` |
|
| 10 |
+
| RecSys (train-free `/retrieve/recommend`) | `recsys/` | `./recsys.sh` |
|
| 11 |
+
| Search | `search/` | `./search.sh` |
|
| 12 |
+
|
| 13 |
+
Shared setup: `requirements.txt`, `.env` (`BRAINPAT_TOKEN`, LLM keys). Results ledger: [`REPORTS.json`](REPORTS.json). Agent notes: [`AGENTS.md`](AGENTS.md).
|
| 14 |
+
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
# LoCoMo benchmark for BrainAPI
|
| 18 |
+
|
| 19 |
+
Standalone harness that:
|
| 20 |
+
|
| 21 |
+
1. Downloads the [LoCoMo](https://github.com/snap-research/LoCoMo) dataset
|
| 22 |
+
2. Ingests conversations into BrainAPI over HTTP (`POST /ingest/`)
|
| 23 |
+
3. Answers annotated QA using `POST /retrieve/context` + an LLM answerer
|
| 24 |
+
4. Scores with an LLM judge plus F1 / BLEU-1
|
| 25 |
+
|
| 26 |
+
This package does **not** import anything from `src/`. It only talks to a running BrainAPI instance.
|
| 27 |
+
|
| 28 |
+
## Prerequisites
|
| 29 |
+
|
| 30 |
+
- BrainAPI running locally (`make start-all` or `brainapi start`)
|
| 31 |
+
- `BRAIN_CREATION_ALLOWED="true"` in the server env (needed so brains like `locomoconv26` are auto-created)
|
| 32 |
+
- A PAT token (`BRAINPAT_TOKEN`)
|
| 33 |
+
- A DeepSeek API key for answering and judging (or any OpenAI-compatible provider)
|
| 34 |
+
|
| 35 |
+
Brain IDs must be alphanumeric. This harness maps `conv-26` → `locomoconv26`.
|
| 36 |
+
|
| 37 |
+
## Setup
|
| 38 |
+
|
| 39 |
+
```bash
|
| 40 |
+
cd benchmarks
|
| 41 |
+
python3 -m venv .venv
|
| 42 |
+
source .venv/bin/activate
|
| 43 |
+
# If `python` is aliased to a system binary (common on macOS), prefer the wrapper:
|
| 44 |
+
# ./locomo.sh ...
|
| 45 |
+
# or call the venv interpreter directly:
|
| 46 |
+
# .venv/bin/python -m locomo ...
|
| 47 |
+
pip install -r requirements.txt
|
| 48 |
+
cp .env.example .env
|
| 49 |
+
# edit .env: BRAINPAT_TOKEN, DEEPSEEK_API_KEY
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
## Quickstart
|
| 53 |
+
|
| 54 |
+
```bash
|
| 55 |
+
# 1. Download dataset
|
| 56 |
+
./locomo.sh download
|
| 57 |
+
./locomo.sh dataset-stats
|
| 58 |
+
|
| 59 |
+
# 2. Smoke-test ingest + retrieve against your API
|
| 60 |
+
./locomo.sh smoke --brain locomosmoke
|
| 61 |
+
|
| 62 |
+
# 3. Ingest one conversation (session-level; recommended start)
|
| 63 |
+
./locomo.sh ingest --sample conv-26 --run locomo-conv26
|
| 64 |
+
|
| 65 |
+
# 4. Evaluate a small slice first
|
| 66 |
+
./locomo.sh evaluate --sample conv-26 --run locomo-conv26 --limit 20
|
| 67 |
+
|
| 68 |
+
# 5. Full non-adversarial QA for that conversation
|
| 69 |
+
./locomo.sh evaluate --sample conv-26 --run locomo-conv26
|
| 70 |
+
|
| 71 |
+
# 6. Rebuild the report anytime (no API calls)
|
| 72 |
+
./locomo.sh report --run locomo-conv26
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
Equivalent: `.venv/bin/python -m locomo ...` (avoid bare `python` if you have a shell alias).
|
| 76 |
+
|
| 77 |
+
Reports land in `runs/<run_id>/`:
|
| 78 |
+
|
| 79 |
+
- `ingest.jsonl` — per-session ingest outcomes
|
| 80 |
+
- `answers.jsonl` — per-QA predictions and scores
|
| 81 |
+
- `manifest.json` — run config
|
| 82 |
+
- `report.md` / `report.json` — aggregated metrics
|
| 83 |
+
|
| 84 |
+
## Results ledger
|
| 85 |
+
|
| 86 |
+
[`REPORTS.json`](REPORTS.json) is the shared BrainAPI benchmark ledger (multi-suite). LoCoMo writes the `locomo` entry; successful `evaluate` / `report` upserts automatically. See [`AGENTS.md`](AGENTS.md).
|
| 87 |
+
|
| 88 |
+
## Commands
|
| 89 |
+
|
| 90 |
+
| Command | Purpose |
|
| 91 |
+
|---|---|
|
| 92 |
+
| `download` | Fetch `data/locomo10.json` |
|
| 93 |
+
| `dataset-stats` | Print sessions / turns / QA per sample |
|
| 94 |
+
| `smoke` | One-shot ingest + retrieve health check |
|
| 95 |
+
| `ingest` | Push sessions (or turns) into BrainAPI |
|
| 96 |
+
| `answer-once` | Retrieve + answer a single question |
|
| 97 |
+
| `selftest-metrics` | Local metric, channel-split and record-construction checks |
|
| 98 |
+
| `prompt-audit` | Fail if the answer prompt shares an n-gram with any gold answer |
|
| 99 |
+
| `evaluate` | Retrieve → answer → judge for QA items |
|
| 100 |
+
| `report` | Aggregate `answers.jsonl` into markdown/JSON |
|
| 101 |
+
| `compare` | Paired exact-McNemar comparison between two run arms |
|
| 102 |
+
|
| 103 |
+
### Useful flags
|
| 104 |
+
|
| 105 |
+
**ingest**
|
| 106 |
+
|
| 107 |
+
- `--sample conv-26` (repeatable; default = all 10)
|
| 108 |
+
- `--granularity session|turn` (default `session`)
|
| 109 |
+
- `--concurrency 2`
|
| 110 |
+
- `--limit-sessions N`
|
| 111 |
+
- `--dry-run`
|
| 112 |
+
- `--no-resume`
|
| 113 |
+
- `--run <id>` (resume / continue a named run)
|
| 114 |
+
|
| 115 |
+
**evaluate**
|
| 116 |
+
|
| 117 |
+
- `--sample conv-26`
|
| 118 |
+
- `--limit 20`
|
| 119 |
+
- `--categories 1,2,4`
|
| 120 |
+
- `--include-adversarial` (category 5 is skipped by default)
|
| 121 |
+
- `--concurrency 2`
|
| 122 |
+
- `--run <id>`
|
| 123 |
+
|
| 124 |
+
**compare**
|
| 125 |
+
|
| 126 |
+
- `--baseline <run>` / `--candidate <run>`, both repeatable — repeat to pool several runs of the same configuration into one arm
|
| 127 |
+
- `--include-adversarial`
|
| 128 |
+
- `--json`
|
| 129 |
+
|
| 130 |
+
## Scoring
|
| 131 |
+
|
| 132 |
+
- **Headline number**: LLM-judge accuracy excluding category 5 (adversarial), matching common LoCoMo memory-system reporting.
|
| 133 |
+
- Also reported: overall accuracy, per-category breakdown, mean F1, mean BLEU-1, retrieval latency p50/p95, total LLM tokens.
|
| 134 |
+
- Categories: `1` multi-hop, `2` temporal, `3` open-domain, `4` single-hop, `5` adversarial.
|
| 135 |
+
- Gold for adversarial questions is read from `adversarial_answer` when `answer` is absent. F1 and BLEU-1 are `null` (unscorable) when gold is empty rather than a perfect 1.0.
|
| 136 |
+
- Evidence-session recall is reported for the **graph** and **passage** channels separately as well as combined, so the event graph's contribution is visible on its own.
|
| 137 |
+
|
| 138 |
+
### Comparing two runs
|
| 139 |
+
|
| 140 |
+
Judge accuracy is noisy: 11–12% of questions flip between identical-config runs at
|
| 141 |
+
`temperature=0`. **Passage** retrieval metrics are deterministic for a fixed brain
|
| 142 |
+
and config. **Graph** session sets are a measurement only after identical-config
|
| 143 |
+
agreement clears the **≥95%** gate (see `compare` graph-session stability output).
|
| 144 |
+
Do not A/B graph EvR below that gate.
|
| 145 |
+
|
| 146 |
+
```bash
|
| 147 |
+
./locomo.sh compare --baseline locomo-conv26-push75-b --candidate locomo-conv26-push75-c
|
| 148 |
+
./locomo.sh compare \
|
| 149 |
+
--baseline locomo-conv26-push75-a --baseline locomo-conv26-push75-b \
|
| 150 |
+
--candidate locomo-conv26-push75-c --candidate locomo-conv26-push75-d
|
| 151 |
+
|
| 152 |
+
# Checkpoint A — identical-config graph stability (gate ≥95% session-set agreement)
|
| 153 |
+
./locomo.sh evaluate --sample conv-26 --brain locomoconv26clean \
|
| 154 |
+
--run graph-stable-a --historical-limit 16 --max-passages 16 \
|
| 155 |
+
--max-facts 50 --use-ppr --no-sufficiency-retry
|
| 156 |
+
./locomo.sh evaluate --sample conv-26 --brain locomoconv26clean \
|
| 157 |
+
--run graph-stable-b --historical-limit 16 --max-passages 16 \
|
| 158 |
+
--max-facts 50 --use-ppr --no-sufficiency-retry
|
| 159 |
+
./locomo.sh compare --baseline graph-stable-a --candidate graph-stable-b
|
| 160 |
+
```
|
| 161 |
+
|
| 162 |
+
This reports the exact McNemar p-value with the flip table (how many questions moved
|
| 163 |
+
to correct and how many to wrong), overall and per category. Two independent Wilson
|
| 164 |
+
intervals are not a substitute: they discard the pairing and most of the power.
|
| 165 |
+
When both arms are single runs, `compare` also prints graph-session set agreement
|
| 166 |
+
vs the 95% gate.
|
| 167 |
+
|
| 168 |
+
### Models
|
| 169 |
+
|
| 170 |
+
Defaults use **DeepSeek** for the answerer (override in `.env`):
|
| 171 |
+
|
| 172 |
+
- Base URL: `https://api.deepseek.com` (`BENCH_LLM_BASE_URL`)
|
| 173 |
+
- Answerer: `deepseek-v4-flash` (`BENCH_ANSWER_MODEL`)
|
| 174 |
+
- API key: `DEEPSEEK_API_KEY` (or `OPENAI_API_KEY` for other providers)
|
| 175 |
+
|
| 176 |
+
The judge is configured independently and should be a **different model family** from
|
| 177 |
+
the answerer, otherwise judge accuracy carries self-preference bias. `evaluate` prints a
|
| 178 |
+
warning and records `judge_shares_answer_family` in the manifest when the families match.
|
| 179 |
+
|
| 180 |
+
- `BENCH_JUDGE_MODEL`, `BENCH_JUDGE_BASE_URL`, `BENCH_JUDGE_API_KEY` for any OpenAI-compatible provider
|
| 181 |
+
- `BENCH_JUDGE_AZURE_ENDPOINT` / `BENCH_JUDGE_AZURE_KEY` / `BENCH_JUDGE_AZURE_API_VERSION` for Azure OpenAI; the repo-root `AZURE_LARGE_LLM_*` variables are picked up automatically when they are exported and no judge model is set explicitly
|
| 182 |
+
- With no judge configuration at all, the judge falls back to the answerer's provider and model, and the run is marked as sharing a family
|
| 183 |
+
|
| 184 |
+
Other OpenAI-compatible providers work the same way — set the base URL, model ids, and the key.
|
| 185 |
+
|
| 186 |
+
## Reproducibility
|
| 187 |
+
|
| 188 |
+
Every `ingest` and `evaluate` manifest records the git SHA and dirty flag, SHA-256 of the
|
| 189 |
+
answer and judge prompts, SHA-256 of the dataset file, and the resolved answerer/judge
|
| 190 |
+
model ids, families and providers. A prompt edit is a measurement-instrument change: it
|
| 191 |
+
changes `answer_prompt_sha256`, and any comparison across that boundary is invalid.
|
| 192 |
+
|
| 193 |
+
`prompt-audit` guards against tuning the answer prompt on gold answers:
|
| 194 |
+
|
| 195 |
+
```bash
|
| 196 |
+
./locomo.sh prompt-audit # fails if any 3-gram of ANSWER_SYSTEM appears in a gold answer
|
| 197 |
+
```
|
| 198 |
+
|
| 199 |
+
`report` de-duplicates answers by `(sample_id, qa_index)`, counts errored rows, and marks
|
| 200 |
+
a run that failed wholesale as `status: failed` with a banner, so a broken run can no
|
| 201 |
+
longer render as a clean 0.0%.
|
| 202 |
+
|
| 203 |
+
## Cost and time expectations
|
| 204 |
+
|
| 205 |
+
LoCoMo conversations are long (roughly 19–32 sessions and 370–690 turns each). With `PIPELINE_MODE=accurate`, ingesting even one conversation can take a long time and burn substantial LLM tokens inside BrainAPI.
|
| 206 |
+
|
| 207 |
+
Recommendations:
|
| 208 |
+
|
| 209 |
+
1. Start with `conv-26` only.
|
| 210 |
+
2. Prefer `--granularity session` (default).
|
| 211 |
+
3. Use `--limit-sessions` / `--limit` while iterating.
|
| 212 |
+
4. Optionally compare a `PIPELINE_MODE=lightweight` ingest run for cost/quality tradeoffs.
|
| 213 |
+
5. Do **not** ingest the `qa` annotations — that leaks the test set.
|
| 214 |
+
|
| 215 |
+
Ingest and evaluate are resumable: re-running the same `--run` skips completed units / QA rows.
|
| 216 |
+
|
| 217 |
+
## Adding another benchmark
|
| 218 |
+
|
| 219 |
+
Create a sibling package under `benchmarks/`:
|
| 220 |
+
|
| 221 |
+
```text
|
| 222 |
+
benchmarks/
|
| 223 |
+
locomo/ # LoCoMo package
|
| 224 |
+
longmemeval/ # LongMemEval package
|
| 225 |
+
beam/ # BEAM package
|
| 226 |
+
recsys/ # RecSys / recommend package
|
| 227 |
+
yourbench/ # new package with its own cli / client / metrics
|
| 228 |
+
requirements.txt # shared deps, or a per-bench requirements file
|
| 229 |
+
```
|
| 230 |
+
|
| 231 |
+
Reuse patterns from `locomo/` / `longmemeval/` / `recsys/`:
|
| 232 |
+
|
| 233 |
+
- HTTP-only `BrainAPIClient`
|
| 234 |
+
- JSONL resume files under `runs/`
|
| 235 |
+
- Separate ingest → evaluate → report stages
|
| 236 |
+
- Upsert into `REPORTS.json` under `benchmarks.<suite_id>`
|
| 237 |
+
|
| 238 |
+
Keep each benchmark self-contained and free of imports from BrainAPI's `src/`.
|
| 239 |
+
|
| 240 |
+
---
|
| 241 |
+
|
| 242 |
+
# LongMemEval benchmark for BrainAPI
|
| 243 |
+
|
| 244 |
+
Sibling harness for [LongMemEval](https://github.com/xiaowu0162/LongMemEval) (cleaned HF release).
|
| 245 |
+
|
| 246 |
+
1. Downloads cleaned LongMemEval_S (default), oracle, or M
|
| 247 |
+
2. Ingests each question's haystack into its own brain (`lme…`)
|
| 248 |
+
3. Answers via `POST /retrieve/context` + LLM (includes `question_date`)
|
| 249 |
+
4. Scores with official LongMemEval yes/no judge prompts by `question_type`
|
| 250 |
+
|
| 251 |
+
Protocol: [`docs/research/09-longmemeval-protocol.md`](../docs/research/09-longmemeval-protocol.md).
|
| 252 |
+
|
| 253 |
+
## Quickstart
|
| 254 |
+
|
| 255 |
+
```bash
|
| 256 |
+
./longmemeval.sh download --variant s
|
| 257 |
+
./longmemeval.sh dataset-stats --variant s
|
| 258 |
+
./longmemeval.sh smoke --limit 1
|
| 259 |
+
./longmemeval.sh ingest --run lme-s-smoke --limit 3
|
| 260 |
+
./longmemeval.sh evaluate --run lme-s-smoke --limit 3
|
| 261 |
+
./longmemeval.sh report --run lme-s-smoke
|
| 262 |
+
./longmemeval.sh selftest-metrics
|
| 263 |
+
./longmemeval.sh prompt-audit
|
| 264 |
+
```
|
| 265 |
+
|
| 266 |
+
### Useful flags
|
| 267 |
+
|
| 268 |
+
- `--variant s|oracle|m`
|
| 269 |
+
- `--question-id <id>` (repeatable)
|
| 270 |
+
- `--limit N` / `--limit-sessions N`
|
| 271 |
+
- `--concurrency N`
|
| 272 |
+
- `--dry-run` / `--no-resume`
|
| 273 |
+
|
| 274 |
+
### Scoring
|
| 275 |
+
|
| 276 |
+
- **Headline**: LLM yes/no accuracy over all questions (including abstention).
|
| 277 |
+
- Also: per-`question_type` accuracy, abstention accuracy, session recall vs `answer_session_ids` (abstention skipped), retrieval latency, LLM tokens.
|
| 278 |
+
- Successful runs with ≥50 scored questions upsert `benchmarks.longmemeval` in `REPORTS.json`.
|
| 279 |
+
|
| 280 |
+
Do **not** ingest gold answers or `has_answer` turn labels.
|
| 281 |
+
|
| 282 |
+
---
|
| 283 |
+
|
| 284 |
+
# BEAM benchmark for BrainAPI
|
| 285 |
+
|
| 286 |
+
Sibling harness for [BEAM](https://github.com/mohammadtavakoli78/BEAM) (ICLR 2026). Evaluates BrainAPI as long-term memory on multi-scale chats (`100K` / `500K` / `1M` / `10M`).
|
| 287 |
+
|
| 288 |
+
1. Downloads HuggingFace `Mohammadta/BEAM` (and `Mohammadta/BEAM-10M` for `--size 10M`) and normalizes under `data/beam/`
|
| 289 |
+
2. Ingests each batch turn into a brain (`beam100k1`, `beam10m1`, …)
|
| 290 |
+
3. Answers probing questions via `POST /retrieve/context` + LLM
|
| 291 |
+
4. Scores with BEAM rubric LLM-judge (10 abilities); headline = mean of ability means
|
| 292 |
+
|
| 293 |
+
Protocol: [`docs/research/10-beam-protocol.md`](../docs/research/10-beam-protocol.md). 10M rows concatenate interlocking plans chronologically into one chat (same `bN_tM` / `session_N` path as 1M).
|
| 294 |
+
|
| 295 |
+
## Quickstart
|
| 296 |
+
|
| 297 |
+
```bash
|
| 298 |
+
./beam.sh download --size 100K
|
| 299 |
+
./beam.sh dataset-stats --size 100K
|
| 300 |
+
./beam.sh smoke --size 100K --sample 1
|
| 301 |
+
./beam.sh ingest --size 100K --sample 1 --run beam-100k-1 --limit-turns 10
|
| 302 |
+
./beam.sh evaluate --size 100K --sample 1 --run beam-100k-1 --limit 5
|
| 303 |
+
./beam.sh report --run beam-100k-1
|
| 304 |
+
./beam.sh selftest
|
| 305 |
+
```
|
| 306 |
+
|
| 307 |
+
10M (after `benchmarks/.env` + live API; do not start long ingest casually):
|
| 308 |
+
|
| 309 |
+
```bash
|
| 310 |
+
./beam.sh download --size 10M
|
| 311 |
+
./beam.sh dataset-stats --size 10M # read n_turns / ingest_target first
|
| 312 |
+
./beam.sh smoke --size 10M --sample 1 --limit-turns 2
|
| 313 |
+
# durable campaign: scripts/boot_beam_10m_screen.sh + beam_10m_keepalive.sh
|
| 314 |
+
```
|
| 315 |
+
|
| 316 |
+
### Useful flags
|
| 317 |
+
|
| 318 |
+
- `--size 100K|500K|1M|10M`
|
| 319 |
+
- `--sample <id>` (repeatable; id or `size/id`)
|
| 320 |
+
- `--limit-turns N` / `--limit N` / `--abilities a,b,…`
|
| 321 |
+
- `--concurrency N` (ingest default 2; BEAM 1M/10M prefer 2–3, ≤4; keep ≤ `CELERY_WORKER_CONCURRENCY`)
|
| 322 |
+
- `--dry-run` / `--no-resume` (resume skips completed + permanent embed-8192 fails)
|
| 323 |
+
|
| 324 |
+
### Scoring
|
| 325 |
+
|
| 326 |
+
- **Headline**: mean of 10 ability means (`headline_score`).
|
| 327 |
+
- `event_ordering` uses Kendall `tau_norm`; others use mean rubric `llm_judge_score`.
|
| 328 |
+
- Judge variant: `beam-rubric-v1-question-aware` (question-aware, float scores).
|
| 329 |
+
- Successful runs with ≥20 scored questions upsert `benchmarks.beam` in `REPORTS.json`.
|
| 330 |
+
|
| 331 |
+
Do **not** ingest probing questions, rubrics, or ideal answers.
|
| 332 |
+
|
| 333 |
+
**Cost warning:** even one 100K chat is ~100+ turns; start with `--limit-turns` / `--limit`.
|
| 334 |
+
**1M wall time:** ~40h at concurrency 1; ~20–25h at 2 with healthy API (see `docs/research/10-beam-protocol.md`).
|
| 335 |
+
|
| 336 |
+
---
|
| 337 |
+
|
| 338 |
+
# RecSys benchmark for BrainAPI
|
| 339 |
+
|
| 340 |
+
Held-out next-item HitRate/Recall@K against **train-free** `GET /retrieve/recommend` (default) or optional plugin LightGCN. Uses structured ingest into brain `demorecsys` only — never LoCoMo/BEAM brains.
|
| 341 |
+
|
| 342 |
+
1. Optional MovieLens download (`ml-100k`) or toy JSONL fixtures under `data/`
|
| 343 |
+
2. Maps interactions → `POST /ingest/structured` (`mode=deterministic`)
|
| 344 |
+
3. Ranks with `GET /retrieve/recommend` (`--backend graph`) or LightGCN (`--backend lightgcn`)
|
| 345 |
+
4. Scores HitRate@K / Recall@K (+ diversity); upserts only `benchmarks.recsys`
|
| 346 |
+
|
| 347 |
+
Protocol: [`docs/research/16-recsys-eval-protocol.md`](../docs/research/16-recsys-eval-protocol.md).
|
| 348 |
+
|
| 349 |
+
## Quickstart
|
| 350 |
+
|
| 351 |
+
```bash
|
| 352 |
+
./recsys.sh smoke --backend graph
|
| 353 |
+
./recsys.sh download --name ml-100k
|
| 354 |
+
./recsys.sh evaluate --backend graph --dataset data/movielens_100k.jsonl --max-users 50
|
| 355 |
+
# attributed fixture (color/material):
|
| 356 |
+
./recsys.sh evaluate --backend graph --dataset data/recsys_toy_attrs.jsonl --min-interactions 2
|
| 357 |
+
# optional offline CF:
|
| 358 |
+
./recsys.sh evaluate --backend lightgcn --dataset data/recsys_toy.jsonl --epochs 20
|
| 359 |
+
```
|
| 360 |
+
|
| 361 |
+
### Scoring
|
| 362 |
+
|
| 363 |
+
- **Headline**: `hit_rate@K` / `recall@K` (K typically 10/20).
|
| 364 |
+
- Also: intra-list diversity, type coverage; report `model=graph-recommend` or `lightgcn`.
|
| 365 |
+
- Successful scored runs upsert `benchmarks.recsys` only — do not touch other suite keys.
|
| 366 |
+
|
| 367 |
+
---
|
| 368 |
+
|
| 369 |
+
# Search benchmark for BrainAPI
|
| 370 |
+
|
| 371 |
+
Standalone HTTP-only harness for `POST /retrieve/search` (hybrid BM25 + dense). It does **not** import `src/` and does **not** call `/retrieve/context`.
|
| 372 |
+
|
| 373 |
+
Protocol: [`docs/research/18-search-eval-protocol.md`](../docs/research/18-search-eval-protocol.md).
|
| 374 |
+
|
| 375 |
+
## Prerequisites
|
| 376 |
+
|
| 377 |
+
- BrainAPI running with `SEARCH_ENABLED=true`, `DATA_DB=postgresql`, `BRAIN_CREATION_ALLOWED=true`
|
| 378 |
+
- `BRAINPAT_TOKEN` in `benchmarks/.env`
|
| 379 |
+
- Brain id starting with `searchbench` (default `searchbenchsmoke`)
|
| 380 |
+
|
| 381 |
+
A 404 on `/retrieve/search` means search is disabled — fix the server env; it is not a ranking miss.
|
| 382 |
+
|
| 383 |
+
## Quickstart
|
| 384 |
+
|
| 385 |
+
```bash
|
| 386 |
+
cd benchmarks
|
| 387 |
+
./search.sh dataset-stats
|
| 388 |
+
./search.sh smoke
|
| 389 |
+
./search.sh evaluate --fusion rrf
|
| 390 |
+
./search.sh evaluate --rerank plugin:cross-encoder
|
| 391 |
+
./search.sh report --run <run_id>
|
| 392 |
+
```
|
| 393 |
+
|
| 394 |
+
Toy corpus: [`data/search_toy.jsonl`](data/search_toy.jsonl) (keyword + paraphrase queries; gold via `DOCID` markers). Search ingest defaults to **chunk + embed only** (`skip_enrichment`); pass `--enrich` for Scout/Architect.
|
| 395 |
+
|
| 396 |
+
Product-search corpora (LLM-free ingest; dedicated `searchbench*` brains, not `demorecsys`):
|
| 397 |
+
|
| 398 |
+
```bash
|
| 399 |
+
./search.sh download --name esci
|
| 400 |
+
./search.sh download --name wands
|
| 401 |
+
./search.sh dataset-stats --dataset data/search_esci.jsonl
|
| 402 |
+
./search.sh evaluate --dataset data/search_esci.jsonl --brain searchbenchesci --run search-esci
|
| 403 |
+
./search.sh --brain searchbenchwands evaluate --dataset data/search_wands.jsonl --run search-wands-passages-k50 --channels passages --k 50 --ks 5,10,20,50
|
| 404 |
+
./search.sh --brain searchbenchwands evaluate --dataset data/search_wands.jsonl --run search-wands-passages-k50 --channels passages --k 50 --ks 5,10,20,50 --skip-ingest
|
| 405 |
+
./search.sh --brain searchbenchwandsgraph evaluate --dataset data/search_wands.jsonl --run search-wandsgraph-communities-k50 --channels communities --k 50 --ks 5,10,20,50 --ingest-graph
|
| 406 |
+
```
|
| 407 |
+
|
| 408 |
+
`download` writes a **subset** under `data/` (gitignored): Amazon ESCI / Shopping Queries (KDD Cup 2022, US Task 1 test) and WANDS (Wayfair). Defaults: 80 queries, 2000 products, 40 candidates per query. Frozen WANDS quality slice is `data/search_wands.jsonl` (**66** queries / **2000** docs); `--force` cannot clobber it. Raw caches live in `data/esci/` and `data/wands/`. nDCG@10 is graded (ESCI E=1 / S=0.1 / C=0.01 / I=0; WANDS Exact=1 / Partial=0.5 / Irrelevant=0). WANDS first-stage control: passages, k=50, `rerank=none`, brain `searchbenchwands`, then `--skip-ingest`. Opt-in catalog graph: new brain `searchbenchwandsgraph` only; architecture demo, do not mix with ESCI nDCG or frozen WANDS 0.823.
|
| 409 |
+
|
| 410 |
+
Optional plugins (core hybrid works if they are absent; unknown `plugin:<name>` is 400):
|
| 411 |
+
|
| 412 |
+
- `plugins/search-rerank` — `--rerank plugin:cross-encoder` (K≤10)
|
| 413 |
+
- `plugins/search-splade` — `--channels plugin:splade` after `POST /search-splade/index`
|
| 414 |
+
- `plugins/search-colbert` — `--channels plugin:colbert` after `POST /search-colbert/index`
|
| 415 |
+
|
| 416 |
+
Metrics: Recall@{5,10,20}, nDCG@10, MRR; p50/p95 of retrieve **excluding** embed RTT. Successful `evaluate` upserts `benchmarks.search` in [`REPORTS.json`](REPORTS.json). Smoke does not publish scores.
|
| 417 |
+
|
| 418 |
+
Never ingest into or wipe `locomoconv*`, `beam*`, or `demorecsys`.
|
benchmarks/REPORTS.json
ADDED
|
@@ -0,0 +1,2594 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"schema_version": 2,
|
| 3 |
+
"updated_at": "2026-08-21T07:53:39.332214+00:00",
|
| 4 |
+
"description": "BrainAPI benchmark results. Top published scores across suites. Updated when a suite evaluate/report completes successfully.",
|
| 5 |
+
"benchmarks": {
|
| 6 |
+
"locomo": {
|
| 7 |
+
"name": "LoCoMo",
|
| 8 |
+
"leaderboard": [
|
| 9 |
+
{
|
| 10 |
+
"run_id": "locomo-compose-sota-conv26-v4d",
|
| 11 |
+
"track": "sota",
|
| 12 |
+
"scope": "conv-26",
|
| 13 |
+
"samples": [
|
| 14 |
+
"conv-26"
|
| 15 |
+
],
|
| 16 |
+
"n_non_adversarial": 152,
|
| 17 |
+
"headline_judge_accuracy": 0.9539473684210527,
|
| 18 |
+
"headline_judge_accuracy_pct": 95.39,
|
| 19 |
+
"answerable_rate": 0.9407894736842105,
|
| 20 |
+
"answer_model": "deepseek-v4-flash",
|
| 21 |
+
"judge_model": "deepseek-v4-flash",
|
| 22 |
+
"bench_profile": "sota",
|
| 23 |
+
"brain": "locomoconv26nostorm",
|
| 24 |
+
"git_sha": "78cbbae50ac37777ce0315f85156f7812b28248c",
|
| 25 |
+
"report_path": "runs/locomo-compose-sota-conv26-v4d/report.json",
|
| 26 |
+
"recorded_at": "2026-07-31T13:14:01.363020+00:00"
|
| 27 |
+
},
|
| 28 |
+
{
|
| 29 |
+
"run_id": "locomo-compose-sota-conv26-v4c",
|
| 30 |
+
"track": "sota",
|
| 31 |
+
"scope": "conv-26",
|
| 32 |
+
"samples": [
|
| 33 |
+
"conv-26"
|
| 34 |
+
],
|
| 35 |
+
"n_non_adversarial": 152,
|
| 36 |
+
"headline_judge_accuracy": 0.9276315789473685,
|
| 37 |
+
"headline_judge_accuracy_pct": 92.76,
|
| 38 |
+
"answerable_rate": 0.9407894736842105,
|
| 39 |
+
"answer_model": "deepseek-v4-flash",
|
| 40 |
+
"judge_model": "deepseek-v4-flash",
|
| 41 |
+
"bench_profile": "sota",
|
| 42 |
+
"brain": "locomoconv26nostorm",
|
| 43 |
+
"git_sha": "78cbbae50ac37777ce0315f85156f7812b28248c",
|
| 44 |
+
"report_path": "runs/locomo-compose-sota-conv26-v4c/report.json",
|
| 45 |
+
"recorded_at": "2026-07-31T13:07:26.775227+00:00"
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
"run_id": "locomo-compose-sota-conv26-v2",
|
| 49 |
+
"track": "sota",
|
| 50 |
+
"scope": "conv-26",
|
| 51 |
+
"samples": [
|
| 52 |
+
"conv-26"
|
| 53 |
+
],
|
| 54 |
+
"n_non_adversarial": 152,
|
| 55 |
+
"headline_judge_accuracy": 0.9210526315789473,
|
| 56 |
+
"headline_judge_accuracy_pct": 92.11,
|
| 57 |
+
"answerable_rate": 0.9407894736842105,
|
| 58 |
+
"answer_model": "deepseek-v4-flash",
|
| 59 |
+
"judge_model": "deepseek-v4-flash",
|
| 60 |
+
"bench_profile": "sota",
|
| 61 |
+
"brain": "locomoconv26nostorm",
|
| 62 |
+
"git_sha": "78cbbae50ac37777ce0315f85156f7812b28248c",
|
| 63 |
+
"report_path": "runs/locomo-compose-sota-conv26-v2/report.json",
|
| 64 |
+
"recorded_at": "2026-07-30T20:01:08.915975+00:00"
|
| 65 |
+
},
|
| 66 |
+
{
|
| 67 |
+
"run_id": "locomo-compose-sota-conv26-v4",
|
| 68 |
+
"track": "sota",
|
| 69 |
+
"scope": "conv-26",
|
| 70 |
+
"samples": [
|
| 71 |
+
"conv-26"
|
| 72 |
+
],
|
| 73 |
+
"n_non_adversarial": 152,
|
| 74 |
+
"headline_judge_accuracy": 0.9210526315789473,
|
| 75 |
+
"headline_judge_accuracy_pct": 92.11,
|
| 76 |
+
"answerable_rate": 0.9407894736842105,
|
| 77 |
+
"answer_model": "deepseek-v4-flash",
|
| 78 |
+
"judge_model": "deepseek-v4-flash",
|
| 79 |
+
"bench_profile": "sota",
|
| 80 |
+
"brain": "locomoconv26nostorm",
|
| 81 |
+
"git_sha": "78cbbae50ac37777ce0315f85156f7812b28248c",
|
| 82 |
+
"report_path": "runs/locomo-compose-sota-conv26-v4/report.json",
|
| 83 |
+
"recorded_at": "2026-07-31T11:28:19.452764+00:00"
|
| 84 |
+
},
|
| 85 |
+
{
|
| 86 |
+
"run_id": "locomo-compose-sota-conv26-v3",
|
| 87 |
+
"track": "sota",
|
| 88 |
+
"scope": "conv-26",
|
| 89 |
+
"samples": [
|
| 90 |
+
"conv-26"
|
| 91 |
+
],
|
| 92 |
+
"n_non_adversarial": 152,
|
| 93 |
+
"headline_judge_accuracy": 0.9013157894736842,
|
| 94 |
+
"headline_judge_accuracy_pct": 90.13,
|
| 95 |
+
"answerable_rate": 0.9407894736842105,
|
| 96 |
+
"answer_model": "deepseek-v4-flash",
|
| 97 |
+
"judge_model": "deepseek-v4-flash",
|
| 98 |
+
"bench_profile": "sota",
|
| 99 |
+
"brain": "locomoconv26nostorm",
|
| 100 |
+
"git_sha": "78cbbae50ac37777ce0315f85156f7812b28248c",
|
| 101 |
+
"report_path": "runs/locomo-compose-sota-conv26-v3/report.json",
|
| 102 |
+
"recorded_at": "2026-07-31T10:29:50.328215+00:00"
|
| 103 |
+
},
|
| 104 |
+
{
|
| 105 |
+
"run_id": "locomo-conv26-beamreg-product",
|
| 106 |
+
"track": "product",
|
| 107 |
+
"scope": "conv-26",
|
| 108 |
+
"samples": [
|
| 109 |
+
"conv-26"
|
| 110 |
+
],
|
| 111 |
+
"n_non_adversarial": 152,
|
| 112 |
+
"headline_judge_accuracy": 0.9013157894736842,
|
| 113 |
+
"headline_judge_accuracy_pct": 90.13,
|
| 114 |
+
"answerable_rate": 0.9473684210526315,
|
| 115 |
+
"answer_model": "deepseek-v4-flash",
|
| 116 |
+
"judge_model": "deepseek-v4-flash",
|
| 117 |
+
"bench_profile": "product",
|
| 118 |
+
"brain": "locomoconv26",
|
| 119 |
+
"git_sha": "78cbbae50ac37777ce0315f85156f7812b28248c",
|
| 120 |
+
"report_path": "runs/locomo-conv26-beamreg-product/report.json",
|
| 121 |
+
"recorded_at": "2026-08-01T01:58:34.693097+00:00"
|
| 122 |
+
},
|
| 123 |
+
{
|
| 124 |
+
"run_id": "locomo-compose-sota-conv26",
|
| 125 |
+
"track": "sota",
|
| 126 |
+
"scope": "conv-26",
|
| 127 |
+
"samples": [
|
| 128 |
+
"conv-26"
|
| 129 |
+
],
|
| 130 |
+
"n_non_adversarial": 152,
|
| 131 |
+
"headline_judge_accuracy": 0.8947368421052632,
|
| 132 |
+
"headline_judge_accuracy_pct": 89.47,
|
| 133 |
+
"answerable_rate": 0.9407894736842105,
|
| 134 |
+
"answer_model": "deepseek-v4-flash",
|
| 135 |
+
"judge_model": "deepseek-v4-flash",
|
| 136 |
+
"bench_profile": "sota",
|
| 137 |
+
"brain": "locomoconv26nostorm",
|
| 138 |
+
"git_sha": "78cbbae50ac37777ce0315f85156f7812b28248c",
|
| 139 |
+
"report_path": "runs/locomo-compose-sota-conv26/report.json",
|
| 140 |
+
"recorded_at": "2026-07-30T19:18:56.740730+00:00"
|
| 141 |
+
},
|
| 142 |
+
{
|
| 143 |
+
"run_id": "locomo-compose-sota-conv26-v4b",
|
| 144 |
+
"track": "sota",
|
| 145 |
+
"scope": "conv-26",
|
| 146 |
+
"samples": [
|
| 147 |
+
"conv-26"
|
| 148 |
+
],
|
| 149 |
+
"n_non_adversarial": 152,
|
| 150 |
+
"headline_judge_accuracy": 0.875,
|
| 151 |
+
"headline_judge_accuracy_pct": 87.5,
|
| 152 |
+
"answerable_rate": 0.9407894736842105,
|
| 153 |
+
"answer_model": "deepseek-v4-flash",
|
| 154 |
+
"judge_model": "deepseek-v4-flash",
|
| 155 |
+
"bench_profile": "sota",
|
| 156 |
+
"brain": "locomoconv26nostorm",
|
| 157 |
+
"git_sha": "78cbbae50ac37777ce0315f85156f7812b28248c",
|
| 158 |
+
"report_path": "runs/locomo-compose-sota-conv26-v4b/report.json",
|
| 159 |
+
"recorded_at": "2026-07-31T12:26:52.469668+00:00"
|
| 160 |
+
},
|
| 161 |
+
{
|
| 162 |
+
"run_id": "locomo-cat3fix-sota-conv26",
|
| 163 |
+
"track": "sota",
|
| 164 |
+
"scope": "conv-26",
|
| 165 |
+
"samples": [
|
| 166 |
+
"conv-26"
|
| 167 |
+
],
|
| 168 |
+
"n_non_adversarial": 152,
|
| 169 |
+
"headline_judge_accuracy": 0.868421052631579,
|
| 170 |
+
"headline_judge_accuracy_pct": 86.84,
|
| 171 |
+
"answerable_rate": 0.9407894736842105,
|
| 172 |
+
"answer_model": "deepseek-v4-flash",
|
| 173 |
+
"judge_model": "deepseek-v4-flash",
|
| 174 |
+
"bench_profile": "sota",
|
| 175 |
+
"brain": "locomoconv26nostorm",
|
| 176 |
+
"git_sha": "78cbbae50ac37777ce0315f85156f7812b28248c",
|
| 177 |
+
"report_path": "runs/locomo-cat3fix-sota-conv26/report.json",
|
| 178 |
+
"recorded_at": "2026-07-30T18:26:15.214852+00:00"
|
| 179 |
+
},
|
| 180 |
+
{
|
| 181 |
+
"run_id": "oldbrain-recheck-a",
|
| 182 |
+
"track": "product",
|
| 183 |
+
"scope": "conv-26",
|
| 184 |
+
"samples": [
|
| 185 |
+
"conv-26"
|
| 186 |
+
],
|
| 187 |
+
"n_non_adversarial": 152,
|
| 188 |
+
"headline_judge_accuracy": 0.8552631578947368,
|
| 189 |
+
"headline_judge_accuracy_pct": 85.53,
|
| 190 |
+
"answerable_rate": 0.9473684210526315,
|
| 191 |
+
"answer_model": "deepseek-v4-flash",
|
| 192 |
+
"judge_model": "deepseek-v4-flash",
|
| 193 |
+
"bench_profile": "product",
|
| 194 |
+
"brain": "locomoconv26",
|
| 195 |
+
"git_sha": "649f5e569de8894c05eec5f1f423024be5e9b6a4",
|
| 196 |
+
"report_path": "runs/oldbrain-recheck-a/report.json",
|
| 197 |
+
"recorded_at": "2026-07-28T22:05:39.768798+00:00"
|
| 198 |
+
},
|
| 199 |
+
{
|
| 200 |
+
"run_id": "locomo-batch-nostorm-cfgb",
|
| 201 |
+
"track": "product",
|
| 202 |
+
"scope": "conv-26",
|
| 203 |
+
"samples": [
|
| 204 |
+
"conv-26"
|
| 205 |
+
],
|
| 206 |
+
"n_non_adversarial": 152,
|
| 207 |
+
"headline_judge_accuracy": 0.8552631578947368,
|
| 208 |
+
"headline_judge_accuracy_pct": 85.53,
|
| 209 |
+
"answerable_rate": 0.9407894736842105,
|
| 210 |
+
"answer_model": "deepseek-v4-flash",
|
| 211 |
+
"judge_model": "deepseek-v4-flash",
|
| 212 |
+
"bench_profile": "product",
|
| 213 |
+
"brain": "locomoconv26nostorm",
|
| 214 |
+
"git_sha": "78cbbae50ac37777ce0315f85156f7812b28248c",
|
| 215 |
+
"report_path": "runs/locomo-batch-nostorm-cfgb/report.json",
|
| 216 |
+
"recorded_at": "2026-07-30T17:03:35.449258+00:00"
|
| 217 |
+
},
|
| 218 |
+
{
|
| 219 |
+
"run_id": "phase-sota-d1-conv26",
|
| 220 |
+
"track": "sota",
|
| 221 |
+
"scope": "conv-26",
|
| 222 |
+
"samples": [
|
| 223 |
+
"conv-26"
|
| 224 |
+
],
|
| 225 |
+
"n_non_adversarial": 152,
|
| 226 |
+
"headline_judge_accuracy": 0.8486842105263158,
|
| 227 |
+
"headline_judge_accuracy_pct": 84.87,
|
| 228 |
+
"answerable_rate": 0.9407894736842105,
|
| 229 |
+
"answer_model": "deepseek-v4-flash",
|
| 230 |
+
"judge_model": "deepseek-v4-flash",
|
| 231 |
+
"bench_profile": "sota",
|
| 232 |
+
"brain": "locomoconv26clean",
|
| 233 |
+
"git_sha": "649f5e569de8894c05eec5f1f423024be5e9b6a4",
|
| 234 |
+
"report_path": "runs/phase-sota-d1-conv26/report.json",
|
| 235 |
+
"recorded_at": "2026-07-29T18:51:58.400341+00:00"
|
| 236 |
+
},
|
| 237 |
+
{
|
| 238 |
+
"run_id": "locomo-nostorm-sota-conv26",
|
| 239 |
+
"track": "sota",
|
| 240 |
+
"scope": "conv-26",
|
| 241 |
+
"samples": [
|
| 242 |
+
"conv-26"
|
| 243 |
+
],
|
| 244 |
+
"n_non_adversarial": 152,
|
| 245 |
+
"headline_judge_accuracy": 0.8486842105263158,
|
| 246 |
+
"headline_judge_accuracy_pct": 84.87,
|
| 247 |
+
"answerable_rate": 0.9407894736842105,
|
| 248 |
+
"answer_model": "deepseek-v4-flash",
|
| 249 |
+
"judge_model": "deepseek-v4-flash",
|
| 250 |
+
"bench_profile": "sota",
|
| 251 |
+
"brain": "locomoconv26nostorm",
|
| 252 |
+
"git_sha": "78cbbae50ac37777ce0315f85156f7812b28248c",
|
| 253 |
+
"report_path": "runs/locomo-nostorm-sota-conv26/report.json",
|
| 254 |
+
"recorded_at": "2026-07-30T17:41:32.563092+00:00"
|
| 255 |
+
},
|
| 256 |
+
{
|
| 257 |
+
"run_id": "locomo-batch-cfgb",
|
| 258 |
+
"track": "product",
|
| 259 |
+
"scope": "conv-26",
|
| 260 |
+
"samples": [
|
| 261 |
+
"conv-26"
|
| 262 |
+
],
|
| 263 |
+
"n_non_adversarial": 152,
|
| 264 |
+
"headline_judge_accuracy": 0.8421052631578947,
|
| 265 |
+
"headline_judge_accuracy_pct": 84.21,
|
| 266 |
+
"answerable_rate": 0.9407894736842105,
|
| 267 |
+
"answer_model": "deepseek-v4-flash",
|
| 268 |
+
"judge_model": "deepseek-v4-flash",
|
| 269 |
+
"bench_profile": "product",
|
| 270 |
+
"brain": "locomoconv26batch",
|
| 271 |
+
"git_sha": "78cbbae50ac37777ce0315f85156f7812b28248c",
|
| 272 |
+
"report_path": "runs/locomo-batch-cfgb/report.json",
|
| 273 |
+
"recorded_at": "2026-07-30T15:23:27.730891+00:00"
|
| 274 |
+
},
|
| 275 |
+
{
|
| 276 |
+
"run_id": "clean-brain-eval-a",
|
| 277 |
+
"track": "product",
|
| 278 |
+
"scope": "conv-26",
|
| 279 |
+
"samples": [
|
| 280 |
+
"conv-26"
|
| 281 |
+
],
|
| 282 |
+
"n_non_adversarial": 152,
|
| 283 |
+
"headline_judge_accuracy": 0.8355263157894737,
|
| 284 |
+
"headline_judge_accuracy_pct": 83.55,
|
| 285 |
+
"answerable_rate": 0.9539473684210527,
|
| 286 |
+
"answer_model": "deepseek-v4-flash",
|
| 287 |
+
"judge_model": "deepseek-v4-flash",
|
| 288 |
+
"bench_profile": "product",
|
| 289 |
+
"brain": "locomoconv26clean",
|
| 290 |
+
"git_sha": "649f5e569de8894c05eec5f1f423024be5e9b6a4",
|
| 291 |
+
"report_path": "runs/clean-brain-eval-a/report.json",
|
| 292 |
+
"recorded_at": "2026-07-28T21:29:30.713806+00:00"
|
| 293 |
+
},
|
| 294 |
+
{
|
| 295 |
+
"run_id": "graph-stable-b",
|
| 296 |
+
"track": "product",
|
| 297 |
+
"scope": "conv-26",
|
| 298 |
+
"samples": [
|
| 299 |
+
"conv-26"
|
| 300 |
+
],
|
| 301 |
+
"n_non_adversarial": 152,
|
| 302 |
+
"headline_judge_accuracy": 0.8355263157894737,
|
| 303 |
+
"headline_judge_accuracy_pct": 83.55,
|
| 304 |
+
"answerable_rate": 0.9671052631578947,
|
| 305 |
+
"answer_model": "deepseek-v4-flash",
|
| 306 |
+
"judge_model": "deepseek-v4-flash",
|
| 307 |
+
"bench_profile": "product",
|
| 308 |
+
"brain": "locomoconv26clean",
|
| 309 |
+
"git_sha": "649f5e569de8894c05eec5f1f423024be5e9b6a4",
|
| 310 |
+
"report_path": "runs/graph-stable-b/report.json",
|
| 311 |
+
"recorded_at": "2026-07-29T10:59:22.348091+00:00"
|
| 312 |
+
},
|
| 313 |
+
{
|
| 314 |
+
"run_id": "clean-brain-a",
|
| 315 |
+
"track": "product",
|
| 316 |
+
"scope": "conv-26",
|
| 317 |
+
"samples": [
|
| 318 |
+
"conv-26"
|
| 319 |
+
],
|
| 320 |
+
"n_non_adversarial": 152,
|
| 321 |
+
"headline_judge_accuracy": 0.8289473684210527,
|
| 322 |
+
"headline_judge_accuracy_pct": 82.89,
|
| 323 |
+
"answerable_rate": 0.9473684210526315,
|
| 324 |
+
"answer_model": "deepseek-v4-flash",
|
| 325 |
+
"judge_model": "deepseek-v4-flash",
|
| 326 |
+
"bench_profile": "product",
|
| 327 |
+
"brain": "locomoconv26clean",
|
| 328 |
+
"git_sha": "649f5e569de8894c05eec5f1f423024be5e9b6a4",
|
| 329 |
+
"report_path": "runs/clean-brain-a/report.json",
|
| 330 |
+
"recorded_at": "2026-07-28T21:57:06.650866+00:00"
|
| 331 |
+
},
|
| 332 |
+
{
|
| 333 |
+
"run_id": "clean-brain-b",
|
| 334 |
+
"track": "product",
|
| 335 |
+
"scope": "conv-26",
|
| 336 |
+
"samples": [
|
| 337 |
+
"conv-26"
|
| 338 |
+
],
|
| 339 |
+
"n_non_adversarial": 152,
|
| 340 |
+
"headline_judge_accuracy": 0.8289473684210527,
|
| 341 |
+
"headline_judge_accuracy_pct": 82.89,
|
| 342 |
+
"answerable_rate": 0.9473684210526315,
|
| 343 |
+
"answer_model": "deepseek-v4-flash",
|
| 344 |
+
"judge_model": "deepseek-v4-flash",
|
| 345 |
+
"bench_profile": "product",
|
| 346 |
+
"brain": "locomoconv26clean",
|
| 347 |
+
"git_sha": "649f5e569de8894c05eec5f1f423024be5e9b6a4",
|
| 348 |
+
"report_path": "runs/clean-brain-b/report.json",
|
| 349 |
+
"recorded_at": "2026-07-28T22:13:25.651713+00:00"
|
| 350 |
+
},
|
| 351 |
+
{
|
| 352 |
+
"run_id": "baseline-clean-a",
|
| 353 |
+
"track": "product",
|
| 354 |
+
"scope": "conv-26",
|
| 355 |
+
"samples": [
|
| 356 |
+
"conv-26"
|
| 357 |
+
],
|
| 358 |
+
"n_non_adversarial": 152,
|
| 359 |
+
"headline_judge_accuracy": 0.8223684210526315,
|
| 360 |
+
"headline_judge_accuracy_pct": 82.24,
|
| 361 |
+
"answerable_rate": 0.9473684210526315,
|
| 362 |
+
"answer_model": "deepseek-v4-flash",
|
| 363 |
+
"judge_model": "deepseek-v4-flash",
|
| 364 |
+
"bench_profile": "product",
|
| 365 |
+
"brain": "locomoconv26",
|
| 366 |
+
"git_sha": "649f5e569de8894c05eec5f1f423024be5e9b6a4",
|
| 367 |
+
"report_path": "runs/baseline-clean-a/report.json",
|
| 368 |
+
"recorded_at": "2026-07-28T18:18:19.581660+00:00"
|
| 369 |
+
},
|
| 370 |
+
{
|
| 371 |
+
"run_id": "sota-locomo10-batch-compose",
|
| 372 |
+
"track": "sota",
|
| 373 |
+
"scope": "full-locomo10",
|
| 374 |
+
"samples": [
|
| 375 |
+
"conv-26",
|
| 376 |
+
"conv-30",
|
| 377 |
+
"conv-41",
|
| 378 |
+
"conv-42",
|
| 379 |
+
"conv-43",
|
| 380 |
+
"conv-44",
|
| 381 |
+
"conv-47",
|
| 382 |
+
"conv-48",
|
| 383 |
+
"conv-49",
|
| 384 |
+
"conv-50"
|
| 385 |
+
],
|
| 386 |
+
"n_non_adversarial": 1540,
|
| 387 |
+
"headline_judge_accuracy": 0.8097402597402598,
|
| 388 |
+
"headline_judge_accuracy_pct": 80.97,
|
| 389 |
+
"answerable_rate": 0.9363636363636364,
|
| 390 |
+
"answer_model": "deepseek-v4-flash",
|
| 391 |
+
"judge_model": "deepseek-v4-flash",
|
| 392 |
+
"bench_profile": "sota",
|
| 393 |
+
"brain": "locomof10c*",
|
| 394 |
+
"git_sha": "78cbbae50ac37777ce0315f85156f7812b28248c",
|
| 395 |
+
"report_path": "runs/sota-locomo10-batch-compose/report.json",
|
| 396 |
+
"recorded_at": "2026-07-31T13:35:58.561407+00:00"
|
| 397 |
+
},
|
| 398 |
+
{
|
| 399 |
+
"run_id": "phase-d1-paths-a",
|
| 400 |
+
"track": "product",
|
| 401 |
+
"scope": "conv-26",
|
| 402 |
+
"samples": [
|
| 403 |
+
"conv-26"
|
| 404 |
+
],
|
| 405 |
+
"n_non_adversarial": 152,
|
| 406 |
+
"headline_judge_accuracy": 0.8092105263157895,
|
| 407 |
+
"headline_judge_accuracy_pct": 80.92,
|
| 408 |
+
"answerable_rate": 0.9671052631578947,
|
| 409 |
+
"answer_model": "deepseek-v4-flash",
|
| 410 |
+
"judge_model": "deepseek-v4-flash",
|
| 411 |
+
"bench_profile": "product",
|
| 412 |
+
"brain": "locomoconv26clean",
|
| 413 |
+
"git_sha": "649f5e569de8894c05eec5f1f423024be5e9b6a4",
|
| 414 |
+
"report_path": "runs/phase-d1-paths-a/report.json",
|
| 415 |
+
"recorded_at": "2026-07-29T16:26:21.919353+00:00"
|
| 416 |
+
},
|
| 417 |
+
{
|
| 418 |
+
"run_id": "locomo-batch-score",
|
| 419 |
+
"track": "product",
|
| 420 |
+
"scope": "conv-26",
|
| 421 |
+
"samples": [
|
| 422 |
+
"conv-26"
|
| 423 |
+
],
|
| 424 |
+
"n_non_adversarial": 152,
|
| 425 |
+
"headline_judge_accuracy": 0.8026315789473685,
|
| 426 |
+
"headline_judge_accuracy_pct": 80.26,
|
| 427 |
+
"answerable_rate": 0.9407894736842105,
|
| 428 |
+
"answer_model": "deepseek-v4-flash",
|
| 429 |
+
"judge_model": "deepseek-v4-flash",
|
| 430 |
+
"bench_profile": "product",
|
| 431 |
+
"brain": "locomoconv26batch",
|
| 432 |
+
"git_sha": "78cbbae50ac37777ce0315f85156f7812b28248c",
|
| 433 |
+
"report_path": "runs/locomo-batch-score/report.json",
|
| 434 |
+
"recorded_at": "2026-07-30T14:59:39.743019+00:00"
|
| 435 |
+
}
|
| 436 |
+
]
|
| 437 |
+
},
|
| 438 |
+
"longmemeval": {
|
| 439 |
+
"name": "LongMemEval",
|
| 440 |
+
"leaderboard": []
|
| 441 |
+
},
|
| 442 |
+
"beam": {
|
| 443 |
+
"name": "BEAM",
|
| 444 |
+
"leaderboard": [
|
| 445 |
+
{
|
| 446 |
+
"run_id": "beam-1m-1-product-v3c-compose",
|
| 447 |
+
"track": "product",
|
| 448 |
+
"bench_profile": "product",
|
| 449 |
+
"scope": "1M/1",
|
| 450 |
+
"size": "1M",
|
| 451 |
+
"samples": [
|
| 452 |
+
"1M/1"
|
| 453 |
+
],
|
| 454 |
+
"n_questions": 20,
|
| 455 |
+
"headline_score": 0.7897396010333467,
|
| 456 |
+
"headline_score_pct": 78.97,
|
| 457 |
+
"per_ability": {
|
| 458 |
+
"abstention": 0.5,
|
| 459 |
+
"contradiction_resolution": 0.6875,
|
| 460 |
+
"event_ordering": 0.42170156588902286,
|
| 461 |
+
"information_extraction": 1.0,
|
| 462 |
+
"instruction_following": 1.0,
|
| 463 |
+
"knowledge_update": 1.0,
|
| 464 |
+
"multi_session_reasoning": 0.9375,
|
| 465 |
+
"preference_following": 0.875,
|
| 466 |
+
"summarization": 0.4756944444444444,
|
| 467 |
+
"temporal_reasoning": 1.0
|
| 468 |
+
},
|
| 469 |
+
"answer_model": "deepseek-v4-flash",
|
| 470 |
+
"judge_model": "deepseek-v4-flash",
|
| 471 |
+
"brain": "beam1m1clean",
|
| 472 |
+
"git_sha": "78cbbae50ac37777ce0315f85156f7812b28248c",
|
| 473 |
+
"report_path": "runs/beam-1m-1-product-v3c-compose/report.json",
|
| 474 |
+
"recorded_at": "2026-08-03T09:15:33.803818+00:00"
|
| 475 |
+
},
|
| 476 |
+
{
|
| 477 |
+
"run_id": "beam-100k-1-clean-ordv1",
|
| 478 |
+
"track": "product",
|
| 479 |
+
"bench_profile": "product",
|
| 480 |
+
"scope": "100K/1",
|
| 481 |
+
"size": "100K",
|
| 482 |
+
"samples": [
|
| 483 |
+
"100K/1"
|
| 484 |
+
],
|
| 485 |
+
"n_questions": 20,
|
| 486 |
+
"headline_score": 0.7887500000000001,
|
| 487 |
+
"headline_score_pct": 78.88,
|
| 488 |
+
"per_ability": {
|
| 489 |
+
"abstention": 1.0,
|
| 490 |
+
"contradiction_resolution": 0.625,
|
| 491 |
+
"event_ordering": 0.7875,
|
| 492 |
+
"information_extraction": 1.0,
|
| 493 |
+
"instruction_following": 0.75,
|
| 494 |
+
"knowledge_update": 1.0,
|
| 495 |
+
"multi_session_reasoning": 0.625,
|
| 496 |
+
"preference_following": 1.0,
|
| 497 |
+
"summarization": 0.6000000000000001,
|
| 498 |
+
"temporal_reasoning": 0.5
|
| 499 |
+
},
|
| 500 |
+
"answer_model": "deepseek-v4-flash",
|
| 501 |
+
"judge_model": "deepseek-v4-flash",
|
| 502 |
+
"brain": "beam100k1clean",
|
| 503 |
+
"git_sha": "78cbbae50ac37777ce0315f85156f7812b28248c",
|
| 504 |
+
"report_path": "runs/beam-100k-1-clean-ordv1/report.json",
|
| 505 |
+
"recorded_at": "2026-08-01T02:00:50.310070+00:00"
|
| 506 |
+
},
|
| 507 |
+
{
|
| 508 |
+
"run_id": "beam-100k-1-clean-product",
|
| 509 |
+
"track": "product",
|
| 510 |
+
"bench_profile": "product",
|
| 511 |
+
"scope": "100K/1",
|
| 512 |
+
"size": "100K",
|
| 513 |
+
"samples": [
|
| 514 |
+
"100K/1"
|
| 515 |
+
],
|
| 516 |
+
"n_questions": 20,
|
| 517 |
+
"headline_score": 0.7398223304703363,
|
| 518 |
+
"headline_score_pct": 73.98,
|
| 519 |
+
"per_ability": {
|
| 520 |
+
"abstention": 0.5,
|
| 521 |
+
"contradiction_resolution": 0.6875,
|
| 522 |
+
"event_ordering": 0.1357233047033631,
|
| 523 |
+
"information_extraction": 0.75,
|
| 524 |
+
"instruction_following": 1.0,
|
| 525 |
+
"knowledge_update": 1.0,
|
| 526 |
+
"multi_session_reasoning": 0.875,
|
| 527 |
+
"preference_following": 1.0,
|
| 528 |
+
"summarization": 0.7,
|
| 529 |
+
"temporal_reasoning": 0.75
|
| 530 |
+
},
|
| 531 |
+
"answer_model": "deepseek-v4-flash",
|
| 532 |
+
"judge_model": "deepseek-v4-flash",
|
| 533 |
+
"brain": "beam100k1clean",
|
| 534 |
+
"git_sha": "78cbbae50ac37777ce0315f85156f7812b28248c",
|
| 535 |
+
"report_path": "runs/beam-100k-1-clean-product/report.json",
|
| 536 |
+
"recorded_at": "2026-08-01T02:00:51.059632+00:00"
|
| 537 |
+
},
|
| 538 |
+
{
|
| 539 |
+
"run_id": "beam-1m-1-product-v2",
|
| 540 |
+
"track": "product",
|
| 541 |
+
"bench_profile": "product",
|
| 542 |
+
"scope": "1M/1",
|
| 543 |
+
"size": "1M",
|
| 544 |
+
"samples": [
|
| 545 |
+
"1M/1"
|
| 546 |
+
],
|
| 547 |
+
"n_questions": 20,
|
| 548 |
+
"headline_score": 0.7014047619826247,
|
| 549 |
+
"headline_score_pct": 70.14,
|
| 550 |
+
"per_ability": {
|
| 551 |
+
"abstention": 0.5,
|
| 552 |
+
"contradiction_resolution": 0.4375,
|
| 553 |
+
"event_ordering": 0.2883531753818027,
|
| 554 |
+
"information_extraction": 1.0,
|
| 555 |
+
"instruction_following": 1.0,
|
| 556 |
+
"knowledge_update": 1.0,
|
| 557 |
+
"multi_session_reasoning": 0.625,
|
| 558 |
+
"preference_following": 0.75,
|
| 559 |
+
"summarization": 0.4131944444444444,
|
| 560 |
+
"temporal_reasoning": 1.0
|
| 561 |
+
},
|
| 562 |
+
"answer_model": "deepseek-v4-flash",
|
| 563 |
+
"judge_model": "deepseek-v4-flash",
|
| 564 |
+
"brain": "beam1m1clean",
|
| 565 |
+
"git_sha": "78cbbae50ac37777ce0315f85156f7812b28248c",
|
| 566 |
+
"report_path": "runs/beam-1m-1-product-v2/report.json",
|
| 567 |
+
"recorded_at": "2026-08-03T07:58:27.006989+00:00"
|
| 568 |
+
},
|
| 569 |
+
{
|
| 570 |
+
"run_id": "beam-1m-1-product-v3b",
|
| 571 |
+
"track": "product",
|
| 572 |
+
"bench_profile": "product",
|
| 573 |
+
"scope": "1M/1",
|
| 574 |
+
"size": "1M",
|
| 575 |
+
"samples": [
|
| 576 |
+
"1M/1"
|
| 577 |
+
],
|
| 578 |
+
"n_questions": 20,
|
| 579 |
+
"headline_score": 0.6497747205349994,
|
| 580 |
+
"headline_score_pct": 64.98,
|
| 581 |
+
"per_ability": {
|
| 582 |
+
"abstention": 0.5,
|
| 583 |
+
"contradiction_resolution": 0.5625,
|
| 584 |
+
"event_ordering": 0.4213583164611052,
|
| 585 |
+
"information_extraction": 1.0,
|
| 586 |
+
"instruction_following": 1.0,
|
| 587 |
+
"knowledge_update": 0.5,
|
| 588 |
+
"multi_session_reasoning": 0.75,
|
| 589 |
+
"preference_following": 0.875,
|
| 590 |
+
"summarization": 0.3888888888888889,
|
| 591 |
+
"temporal_reasoning": 0.5
|
| 592 |
+
},
|
| 593 |
+
"answer_model": "deepseek-v4-flash",
|
| 594 |
+
"judge_model": "deepseek-v4-flash",
|
| 595 |
+
"brain": "beam1m1clean",
|
| 596 |
+
"git_sha": "78cbbae50ac37777ce0315f85156f7812b28248c",
|
| 597 |
+
"report_path": "runs/beam-1m-1-product-v3b/report.json",
|
| 598 |
+
"recorded_at": "2026-08-03T09:14:47.079177+00:00"
|
| 599 |
+
},
|
| 600 |
+
{
|
| 601 |
+
"run_id": "beam-100k-1-sota",
|
| 602 |
+
"track": "sota",
|
| 603 |
+
"bench_profile": "sota",
|
| 604 |
+
"scope": "100K/1",
|
| 605 |
+
"size": "100K",
|
| 606 |
+
"samples": [
|
| 607 |
+
"100K/1"
|
| 608 |
+
],
|
| 609 |
+
"n_questions": 20,
|
| 610 |
+
"headline_score": 0.6483928571428572,
|
| 611 |
+
"headline_score_pct": 64.84,
|
| 612 |
+
"per_ability": {
|
| 613 |
+
"abstention": 0.0,
|
| 614 |
+
"contradiction_resolution": 0.625,
|
| 615 |
+
"event_ordering": 0.1339285714285714,
|
| 616 |
+
"information_extraction": 0.75,
|
| 617 |
+
"instruction_following": 0.75,
|
| 618 |
+
"knowledge_update": 1.0,
|
| 619 |
+
"multi_session_reasoning": 0.875,
|
| 620 |
+
"preference_following": 1.0,
|
| 621 |
+
"summarization": 0.6,
|
| 622 |
+
"temporal_reasoning": 0.75
|
| 623 |
+
},
|
| 624 |
+
"answer_model": "deepseek-v4-flash",
|
| 625 |
+
"judge_model": "deepseek-v4-flash",
|
| 626 |
+
"brain": "beam100k1",
|
| 627 |
+
"git_sha": "78cbbae50ac37777ce0315f85156f7812b28248c",
|
| 628 |
+
"report_path": "runs/beam-100k-1-sota/report.json",
|
| 629 |
+
"recorded_at": "2026-08-01T02:01:11.568901+00:00"
|
| 630 |
+
},
|
| 631 |
+
{
|
| 632 |
+
"run_id": "beam-100k-1-clean-sota",
|
| 633 |
+
"track": "sota",
|
| 634 |
+
"bench_profile": "sota",
|
| 635 |
+
"scope": "100K/1",
|
| 636 |
+
"size": "100K",
|
| 637 |
+
"samples": [
|
| 638 |
+
"100K/1"
|
| 639 |
+
],
|
| 640 |
+
"n_questions": 20,
|
| 641 |
+
"headline_score": 0.6245833333333334,
|
| 642 |
+
"headline_score_pct": 62.46,
|
| 643 |
+
"per_ability": {
|
| 644 |
+
"abstention": 0.0,
|
| 645 |
+
"contradiction_resolution": 0.875,
|
| 646 |
+
"event_ordering": 0.1458333333333333,
|
| 647 |
+
"information_extraction": 0.5,
|
| 648 |
+
"instruction_following": 0.75,
|
| 649 |
+
"knowledge_update": 1.0,
|
| 650 |
+
"multi_session_reasoning": 0.625,
|
| 651 |
+
"preference_following": 1.0,
|
| 652 |
+
"summarization": 0.6000000000000001,
|
| 653 |
+
"temporal_reasoning": 0.75
|
| 654 |
+
},
|
| 655 |
+
"answer_model": "deepseek-v4-flash",
|
| 656 |
+
"judge_model": "deepseek-v4-flash",
|
| 657 |
+
"brain": "beam100k1clean",
|
| 658 |
+
"git_sha": "78cbbae50ac37777ce0315f85156f7812b28248c",
|
| 659 |
+
"report_path": "runs/beam-100k-1-clean-sota/report.json",
|
| 660 |
+
"recorded_at": "2026-08-01T02:01:12.316230+00:00"
|
| 661 |
+
},
|
| 662 |
+
{
|
| 663 |
+
"run_id": "beam-100k-1-product-prompts",
|
| 664 |
+
"track": "product",
|
| 665 |
+
"bench_profile": "product",
|
| 666 |
+
"scope": "100K/1",
|
| 667 |
+
"size": "100K",
|
| 668 |
+
"samples": [
|
| 669 |
+
"100K/1"
|
| 670 |
+
],
|
| 671 |
+
"n_questions": 20,
|
| 672 |
+
"headline_score": 0.6042261904761904,
|
| 673 |
+
"headline_score_pct": 60.42,
|
| 674 |
+
"per_ability": {
|
| 675 |
+
"abstention": 0.0,
|
| 676 |
+
"contradiction_resolution": 0.5,
|
| 677 |
+
"event_ordering": 0.1339285714285714,
|
| 678 |
+
"information_extraction": 0.9583333333333333,
|
| 679 |
+
"instruction_following": 0.75,
|
| 680 |
+
"knowledge_update": 1.0,
|
| 681 |
+
"multi_session_reasoning": 0.75,
|
| 682 |
+
"preference_following": 0.75,
|
| 683 |
+
"summarization": 0.7,
|
| 684 |
+
"temporal_reasoning": 0.5
|
| 685 |
+
},
|
| 686 |
+
"answer_model": "deepseek-v4-flash",
|
| 687 |
+
"judge_model": "deepseek-v4-flash",
|
| 688 |
+
"brain": "beam100k1",
|
| 689 |
+
"git_sha": "78cbbae50ac37777ce0315f85156f7812b28248c",
|
| 690 |
+
"report_path": "runs/beam-100k-1-product-prompts/report.json",
|
| 691 |
+
"recorded_at": "2026-08-01T02:01:13.074254+00:00"
|
| 692 |
+
},
|
| 693 |
+
{
|
| 694 |
+
"run_id": "beam-100k-1-configb",
|
| 695 |
+
"track": "product",
|
| 696 |
+
"bench_profile": "product",
|
| 697 |
+
"scope": "100K/1",
|
| 698 |
+
"size": "100K",
|
| 699 |
+
"samples": [
|
| 700 |
+
"100K/1"
|
| 701 |
+
],
|
| 702 |
+
"n_questions": 20,
|
| 703 |
+
"headline_score": 0.559333376188726,
|
| 704 |
+
"headline_score_pct": 55.93,
|
| 705 |
+
"per_ability": {
|
| 706 |
+
"abstention": 0.25,
|
| 707 |
+
"contradiction_resolution": 0.5,
|
| 708 |
+
"event_ordering": 0.14333376188726035,
|
| 709 |
+
"information_extraction": 0.5,
|
| 710 |
+
"instruction_following": 0.75,
|
| 711 |
+
"knowledge_update": 1.0,
|
| 712 |
+
"multi_session_reasoning": 0.625,
|
| 713 |
+
"preference_following": 0.75,
|
| 714 |
+
"summarization": 0.7,
|
| 715 |
+
"temporal_reasoning": 0.375
|
| 716 |
+
},
|
| 717 |
+
"answer_model": "deepseek-v4-flash",
|
| 718 |
+
"judge_model": "deepseek-v4-flash",
|
| 719 |
+
"brain": "beam100k1",
|
| 720 |
+
"git_sha": "78cbbae50ac37777ce0315f85156f7812b28248c",
|
| 721 |
+
"report_path": "runs/beam-100k-1-configb/report.json",
|
| 722 |
+
"recorded_at": "2026-08-01T02:01:13.832770+00:00"
|
| 723 |
+
},
|
| 724 |
+
{
|
| 725 |
+
"run_id": "beam-100k-1",
|
| 726 |
+
"track": "product",
|
| 727 |
+
"bench_profile": "product",
|
| 728 |
+
"scope": "100K/1",
|
| 729 |
+
"size": "100K",
|
| 730 |
+
"samples": [
|
| 731 |
+
"100K/1"
|
| 732 |
+
],
|
| 733 |
+
"n_questions": 20,
|
| 734 |
+
"headline_score": 0.5558928571428572,
|
| 735 |
+
"headline_score_pct": 55.59,
|
| 736 |
+
"per_ability": {
|
| 737 |
+
"abstention": 0.5,
|
| 738 |
+
"contradiction_resolution": 0.5,
|
| 739 |
+
"event_ordering": 0.1339285714285714,
|
| 740 |
+
"information_extraction": 0.5,
|
| 741 |
+
"instruction_following": 0.5,
|
| 742 |
+
"knowledge_update": 1.0,
|
| 743 |
+
"multi_session_reasoning": 0.625,
|
| 744 |
+
"preference_following": 0.75,
|
| 745 |
+
"summarization": 0.55,
|
| 746 |
+
"temporal_reasoning": 0.5
|
| 747 |
+
},
|
| 748 |
+
"answer_model": "deepseek-v4-flash",
|
| 749 |
+
"judge_model": "deepseek-v4-flash",
|
| 750 |
+
"brain": "beam100k1",
|
| 751 |
+
"git_sha": "78cbbae50ac37777ce0315f85156f7812b28248c",
|
| 752 |
+
"report_path": "runs/beam-100k-1/report.json",
|
| 753 |
+
"recorded_at": "2026-08-01T02:01:14.597463+00:00"
|
| 754 |
+
},
|
| 755 |
+
{
|
| 756 |
+
"run_id": "beam-1m-1-clean-product",
|
| 757 |
+
"track": "product",
|
| 758 |
+
"bench_profile": "product",
|
| 759 |
+
"scope": "1M/1",
|
| 760 |
+
"size": "1M",
|
| 761 |
+
"samples": [
|
| 762 |
+
"1M/1"
|
| 763 |
+
],
|
| 764 |
+
"n_questions": 20,
|
| 765 |
+
"headline_score": 0.5520385817869101,
|
| 766 |
+
"headline_score_pct": 55.2,
|
| 767 |
+
"per_ability": {
|
| 768 |
+
"abstention": 0.5,
|
| 769 |
+
"contradiction_resolution": 0.375,
|
| 770 |
+
"event_ordering": 0.35024692898021254,
|
| 771 |
+
"information_extraction": 1.0,
|
| 772 |
+
"instruction_following": 0.5,
|
| 773 |
+
"knowledge_update": 0.5,
|
| 774 |
+
"multi_session_reasoning": 0.625,
|
| 775 |
+
"preference_following": 0.75,
|
| 776 |
+
"summarization": 0.4201388888888889,
|
| 777 |
+
"temporal_reasoning": 0.5
|
| 778 |
+
},
|
| 779 |
+
"answer_model": "deepseek-v4-flash",
|
| 780 |
+
"judge_model": "deepseek-v4-flash",
|
| 781 |
+
"brain": "beam1m1clean",
|
| 782 |
+
"git_sha": "78cbbae50ac37777ce0315f85156f7812b28248c",
|
| 783 |
+
"report_path": "runs/beam-1m-1-clean-product/report.json",
|
| 784 |
+
"recorded_at": "2026-08-03T06:59:14.016975+00:00"
|
| 785 |
+
}
|
| 786 |
+
]
|
| 787 |
+
},
|
| 788 |
+
"recsys": {
|
| 789 |
+
"name": "RecSys (event-KG recommend)",
|
| 790 |
+
"leaderboard": [
|
| 791 |
+
{
|
| 792 |
+
"run_id": "recsys-20260805T200653Z",
|
| 793 |
+
"brain": "demorecsys",
|
| 794 |
+
"n_users": 20,
|
| 795 |
+
"hit_rate@10": 0.1,
|
| 796 |
+
"hit_rate@20": 0.1,
|
| 797 |
+
"recall@10": 0.1,
|
| 798 |
+
"recall@20": 0.1,
|
| 799 |
+
"mean_intra_list_diversity": 0.20536531279178338,
|
| 800 |
+
"mean_type_coverage": 2.0,
|
| 801 |
+
"git_sha": "cf61c99b809730cc9eb35db3532e018bdd038a55",
|
| 802 |
+
"report_path": "runs/recsys-20260805T200653Z/report.json",
|
| 803 |
+
"recorded_at": "2026-08-05T20:07:44.707984+00:00"
|
| 804 |
+
}
|
| 805 |
+
]
|
| 806 |
+
},
|
| 807 |
+
"search": {
|
| 808 |
+
"name": "Search (hybrid BM25 + dense)",
|
| 809 |
+
"representative_run_id": "search-wands-passages-k50",
|
| 810 |
+
"representative_scope": "WANDS frozen slice: 66 queries / 2000 documents",
|
| 811 |
+
"leaderboard": [
|
| 812 |
+
{
|
| 813 |
+
"run_id": "search-20260818T111250Z",
|
| 814 |
+
"brain": "searchbenchsmoke",
|
| 815 |
+
"dataset": "search_toy.jsonl",
|
| 816 |
+
"fusion": "rrf",
|
| 817 |
+
"rerank": "none",
|
| 818 |
+
"n_queries": 10,
|
| 819 |
+
"ndcg@10": 1.0,
|
| 820 |
+
"recall@10": 1.0,
|
| 821 |
+
"mrr": 1.0,
|
| 822 |
+
"p50_retrieve_ms": 25.1575,
|
| 823 |
+
"p95_retrieve_ms": 27.67875,
|
| 824 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 825 |
+
"report_path": "runs/search-20260818T111250Z/report.json",
|
| 826 |
+
"recorded_at": "2026-08-18T11:13:57.784280+00:00"
|
| 827 |
+
},
|
| 828 |
+
{
|
| 829 |
+
"run_id": "search-toy-cc",
|
| 830 |
+
"brain": "searchbenchsmoke",
|
| 831 |
+
"dataset": "search_toy.jsonl",
|
| 832 |
+
"fusion": "cc",
|
| 833 |
+
"rerank": "none",
|
| 834 |
+
"n_queries": 10,
|
| 835 |
+
"ndcg@10": 1.0,
|
| 836 |
+
"recall@10": 1.0,
|
| 837 |
+
"mrr": 1.0,
|
| 838 |
+
"p50_retrieve_ms": 24.2875,
|
| 839 |
+
"p95_retrieve_ms": 41.931949999999986,
|
| 840 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 841 |
+
"report_path": "runs/search-toy-cc/report.json",
|
| 842 |
+
"recorded_at": "2026-08-18T11:15:19.039999+00:00"
|
| 843 |
+
},
|
| 844 |
+
{
|
| 845 |
+
"run_id": "search-esci-slice-allcols",
|
| 846 |
+
"brain": "searchbenchesci20",
|
| 847 |
+
"dataset": "search_esci_slice.jsonl",
|
| 848 |
+
"fusion": "rrf",
|
| 849 |
+
"rerank": "none",
|
| 850 |
+
"channels": [
|
| 851 |
+
"passages",
|
| 852 |
+
"entities",
|
| 853 |
+
"communities"
|
| 854 |
+
],
|
| 855 |
+
"n_queries": 11,
|
| 856 |
+
"ndcg@10": 0.6815199154995802,
|
| 857 |
+
"recall@10": 0.4656565656565656,
|
| 858 |
+
"mrr": 0.8484848484848485,
|
| 859 |
+
"p50_retrieve_ms": 108.159,
|
| 860 |
+
"p95_retrieve_ms": 129.32150000000001,
|
| 861 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 862 |
+
"report_path": "runs/search-esci-slice-allcols/report.json",
|
| 863 |
+
"recorded_at": "2026-08-18T13:02:36.140118+00:00"
|
| 864 |
+
},
|
| 865 |
+
{
|
| 866 |
+
"run_id": "search-esci-slice-passages",
|
| 867 |
+
"brain": "searchbenchesci20",
|
| 868 |
+
"dataset": "search_esci_slice.jsonl",
|
| 869 |
+
"fusion": "rrf",
|
| 870 |
+
"rerank": "none",
|
| 871 |
+
"channels": [
|
| 872 |
+
"passages"
|
| 873 |
+
],
|
| 874 |
+
"n_queries": 11,
|
| 875 |
+
"ndcg@10": 0.7579469533596556,
|
| 876 |
+
"recall@10": 0.5106060606060606,
|
| 877 |
+
"mrr": 0.8484848484848485,
|
| 878 |
+
"p50_retrieve_ms": 29.279,
|
| 879 |
+
"p95_retrieve_ms": 153.97,
|
| 880 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 881 |
+
"report_path": "runs/search-esci-slice-passages/report.json",
|
| 882 |
+
"recorded_at": "2026-08-18T13:02:47.533040+00:00"
|
| 883 |
+
},
|
| 884 |
+
{
|
| 885 |
+
"run_id": "search-esci-slice-entities",
|
| 886 |
+
"brain": "searchbenchesci20",
|
| 887 |
+
"dataset": "search_esci_slice.jsonl",
|
| 888 |
+
"fusion": "rrf",
|
| 889 |
+
"rerank": "none",
|
| 890 |
+
"channels": [
|
| 891 |
+
"entities"
|
| 892 |
+
],
|
| 893 |
+
"n_queries": 11,
|
| 894 |
+
"ndcg@10": 0.5323459350114446,
|
| 895 |
+
"recall@10": 0.4151515151515151,
|
| 896 |
+
"mrr": 0.6673553719008264,
|
| 897 |
+
"p50_retrieve_ms": 52.75,
|
| 898 |
+
"p95_retrieve_ms": 75.479,
|
| 899 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 900 |
+
"report_path": "runs/search-esci-slice-entities/report.json",
|
| 901 |
+
"recorded_at": "2026-08-18T13:31:39.862006+00:00"
|
| 902 |
+
},
|
| 903 |
+
{
|
| 904 |
+
"run_id": "search-esci-slice-communities",
|
| 905 |
+
"brain": "searchbenchesci20",
|
| 906 |
+
"dataset": "search_esci_slice.jsonl",
|
| 907 |
+
"fusion": "rrf",
|
| 908 |
+
"rerank": "none",
|
| 909 |
+
"channels": [
|
| 910 |
+
"communities"
|
| 911 |
+
],
|
| 912 |
+
"n_queries": 11,
|
| 913 |
+
"ndcg@10": 0.0,
|
| 914 |
+
"recall@10": 0.0,
|
| 915 |
+
"mrr": 0.0,
|
| 916 |
+
"p50_retrieve_ms": 51.536,
|
| 917 |
+
"p95_retrieve_ms": 58.432500000000005,
|
| 918 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 919 |
+
"report_path": "runs/search-esci-slice-communities/report.json",
|
| 920 |
+
"recorded_at": "2026-08-18T13:31:42.972981+00:00"
|
| 921 |
+
},
|
| 922 |
+
{
|
| 923 |
+
"run_id": "search-esci-slice-pe",
|
| 924 |
+
"brain": "searchbenchesci20",
|
| 925 |
+
"dataset": "search_esci_slice.jsonl",
|
| 926 |
+
"fusion": "rrf",
|
| 927 |
+
"rerank": "none",
|
| 928 |
+
"channels": [
|
| 929 |
+
"passages",
|
| 930 |
+
"entities"
|
| 931 |
+
],
|
| 932 |
+
"n_queries": 11,
|
| 933 |
+
"ndcg@10": 0.6815199154995802,
|
| 934 |
+
"recall@10": 0.4656565656565656,
|
| 935 |
+
"mrr": 0.8484848484848485,
|
| 936 |
+
"p50_retrieve_ms": 74.312,
|
| 937 |
+
"p95_retrieve_ms": 91.006,
|
| 938 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 939 |
+
"report_path": "runs/search-esci-slice-pe/report.json",
|
| 940 |
+
"recorded_at": "2026-08-18T13:31:46.700938+00:00"
|
| 941 |
+
},
|
| 942 |
+
{
|
| 943 |
+
"run_id": "search-esci-slice-entities-after",
|
| 944 |
+
"brain": "searchbenchesci20",
|
| 945 |
+
"dataset": "search_esci_slice.jsonl",
|
| 946 |
+
"fusion": "rrf",
|
| 947 |
+
"rerank": "none",
|
| 948 |
+
"channels": [
|
| 949 |
+
"entities"
|
| 950 |
+
],
|
| 951 |
+
"n_queries": 11,
|
| 952 |
+
"ndcg@10": 0.6549492067418874,
|
| 953 |
+
"recall@10": 0.4818181818181818,
|
| 954 |
+
"mrr": 0.7568181818181817,
|
| 955 |
+
"p50_retrieve_ms": 51.071,
|
| 956 |
+
"p95_retrieve_ms": 74.023,
|
| 957 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 958 |
+
"report_path": "runs/search-esci-slice-entities-after/report.json",
|
| 959 |
+
"recorded_at": "2026-08-18T13:36:54.694299+00:00"
|
| 960 |
+
},
|
| 961 |
+
{
|
| 962 |
+
"run_id": "search-esci-slice-communities-after",
|
| 963 |
+
"brain": "searchbenchesci20",
|
| 964 |
+
"dataset": "search_esci_slice.jsonl",
|
| 965 |
+
"fusion": "rrf",
|
| 966 |
+
"rerank": "none",
|
| 967 |
+
"channels": [
|
| 968 |
+
"communities"
|
| 969 |
+
],
|
| 970 |
+
"n_queries": 11,
|
| 971 |
+
"ndcg@10": 0.4776363794553365,
|
| 972 |
+
"recall@10": 0.35000000000000003,
|
| 973 |
+
"mrr": 0.8484848484848485,
|
| 974 |
+
"p50_retrieve_ms": 129.202,
|
| 975 |
+
"p95_retrieve_ms": 152.889,
|
| 976 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 977 |
+
"report_path": "runs/search-esci-slice-communities-after/report.json",
|
| 978 |
+
"recorded_at": "2026-08-18T13:39:09.162227+00:00"
|
| 979 |
+
},
|
| 980 |
+
{
|
| 981 |
+
"run_id": "search-esci-slice-passages-smoke",
|
| 982 |
+
"brain": "searchbenchesci20",
|
| 983 |
+
"dataset": "search_esci_slice.jsonl",
|
| 984 |
+
"fusion": "rrf",
|
| 985 |
+
"rerank": "none",
|
| 986 |
+
"channels": [
|
| 987 |
+
"passages"
|
| 988 |
+
],
|
| 989 |
+
"n_queries": 11,
|
| 990 |
+
"ndcg@10": 0.7579469533596556,
|
| 991 |
+
"recall@10": 0.5106060606060606,
|
| 992 |
+
"mrr": 0.8484848484848485,
|
| 993 |
+
"p50_retrieve_ms": 27.735,
|
| 994 |
+
"p95_retrieve_ms": 32.483000000000004,
|
| 995 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 996 |
+
"report_path": "runs/search-esci-slice-passages-smoke/report.json",
|
| 997 |
+
"recorded_at": "2026-08-18T13:37:03.038349+00:00"
|
| 998 |
+
},
|
| 999 |
+
{
|
| 1000 |
+
"run_id": "search-esci-slice-pec-after",
|
| 1001 |
+
"brain": "searchbenchesci20",
|
| 1002 |
+
"dataset": "search_esci_slice.jsonl",
|
| 1003 |
+
"fusion": "rrf",
|
| 1004 |
+
"rerank": "none",
|
| 1005 |
+
"channels": [
|
| 1006 |
+
"passages",
|
| 1007 |
+
"entities",
|
| 1008 |
+
"communities"
|
| 1009 |
+
],
|
| 1010 |
+
"n_queries": 11,
|
| 1011 |
+
"ndcg@10": 0.6526549564168582,
|
| 1012 |
+
"recall@10": 0.496969696969697,
|
| 1013 |
+
"mrr": 0.816017316017316,
|
| 1014 |
+
"p50_retrieve_ms": 183.946,
|
| 1015 |
+
"p95_retrieve_ms": 216.89249999999998,
|
| 1016 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1017 |
+
"report_path": "runs/search-esci-slice-pec-after/report.json",
|
| 1018 |
+
"recorded_at": "2026-08-18T13:39:52.408131+00:00"
|
| 1019 |
+
},
|
| 1020 |
+
{
|
| 1021 |
+
"run_id": "search-esci-slice-passages-ce",
|
| 1022 |
+
"brain": "searchbenchesci20",
|
| 1023 |
+
"dataset": "search_esci_slice.jsonl",
|
| 1024 |
+
"fusion": "rrf",
|
| 1025 |
+
"rerank": "plugin:cross-encoder",
|
| 1026 |
+
"channels": [
|
| 1027 |
+
"passages"
|
| 1028 |
+
],
|
| 1029 |
+
"rank_pool": false,
|
| 1030 |
+
"rank_pool_ce": false,
|
| 1031 |
+
"n_queries": 11,
|
| 1032 |
+
"ndcg@10": 0.7375861975343849,
|
| 1033 |
+
"ndcg@20": null,
|
| 1034 |
+
"ndcg": null,
|
| 1035 |
+
"recall@10": 0.5106060606060606,
|
| 1036 |
+
"recall@20": 0.8469696969696969,
|
| 1037 |
+
"mrr": 0.8030303030303031,
|
| 1038 |
+
"p50_retrieve_ms": 30.029,
|
| 1039 |
+
"p95_retrieve_ms": 76.32499999999999,
|
| 1040 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1041 |
+
"report_path": "runs/search-esci-slice-passages-ce/report.json",
|
| 1042 |
+
"recorded_at": "2026-08-18T14:20:28.184907+00:00"
|
| 1043 |
+
},
|
| 1044 |
+
{
|
| 1045 |
+
"run_id": "search-esci-slice-passages-pool",
|
| 1046 |
+
"brain": "searchbenchesci20",
|
| 1047 |
+
"dataset": "search_esci_slice.jsonl",
|
| 1048 |
+
"fusion": "rrf",
|
| 1049 |
+
"rerank": "none",
|
| 1050 |
+
"channels": [
|
| 1051 |
+
"passages"
|
| 1052 |
+
],
|
| 1053 |
+
"rank_pool": true,
|
| 1054 |
+
"n_queries": 11,
|
| 1055 |
+
"ndcg@10": 0.7884517684883318,
|
| 1056 |
+
"ndcg@20": 0.8114424261982733,
|
| 1057 |
+
"ndcg": 0.8137804024940007,
|
| 1058 |
+
"recall@10": 0.5348484848484848,
|
| 1059 |
+
"recall@20": 0.8469696969696969,
|
| 1060 |
+
"mrr": 0.9090909090909091,
|
| 1061 |
+
"p50_retrieve_ms": 22.721,
|
| 1062 |
+
"p95_retrieve_ms": 75.3825,
|
| 1063 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1064 |
+
"report_path": "runs/search-esci-slice-passages-pool/report.json",
|
| 1065 |
+
"recorded_at": "2026-08-18T14:18:44.171034+00:00"
|
| 1066 |
+
},
|
| 1067 |
+
{
|
| 1068 |
+
"run_id": "search-esci-slice-ce-pool",
|
| 1069 |
+
"brain": "searchbenchesci20",
|
| 1070 |
+
"dataset": "search_esci_slice.jsonl",
|
| 1071 |
+
"fusion": "none",
|
| 1072 |
+
"rerank": "plugin:cross-encoder",
|
| 1073 |
+
"channels": [
|
| 1074 |
+
"rank-pool-ce"
|
| 1075 |
+
],
|
| 1076 |
+
"rank_pool": true,
|
| 1077 |
+
"rank_pool_ce": true,
|
| 1078 |
+
"n_queries": 11,
|
| 1079 |
+
"ndcg@10": 0.5976277406374534,
|
| 1080 |
+
"ndcg@20": 0.668518640484444,
|
| 1081 |
+
"ndcg": 0.7862048091602294,
|
| 1082 |
+
"recall@10": 0.398637349324982,
|
| 1083 |
+
"recall@20": 0.6598423817505625,
|
| 1084 |
+
"mrr": 0.8333333333333333,
|
| 1085 |
+
"p50_retrieve_ms": 0.0,
|
| 1086 |
+
"p95_retrieve_ms": 0.0,
|
| 1087 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1088 |
+
"report_path": "runs/search-esci-slice-ce-pool/report.json",
|
| 1089 |
+
"recorded_at": "2026-08-18T14:20:28.034427+00:00"
|
| 1090 |
+
},
|
| 1091 |
+
{
|
| 1092 |
+
"run_id": "search-esci-74-ce-pool",
|
| 1093 |
+
"brain": "searchbenchesci74",
|
| 1094 |
+
"dataset": "search_esci_74.jsonl",
|
| 1095 |
+
"fusion": "none",
|
| 1096 |
+
"rerank": "plugin:cross-encoder",
|
| 1097 |
+
"channels": [
|
| 1098 |
+
"rank-pool-ce"
|
| 1099 |
+
],
|
| 1100 |
+
"rank_pool": true,
|
| 1101 |
+
"rank_pool_ce": true,
|
| 1102 |
+
"n_queries": 74,
|
| 1103 |
+
"ndcg@10": 0.5492314171926193,
|
| 1104 |
+
"ndcg@20": 0.6311374744057928,
|
| 1105 |
+
"ndcg": 0.7174501167408996,
|
| 1106 |
+
"recall@10": 0.4907400145298616,
|
| 1107 |
+
"recall@20": 0.7577102510384188,
|
| 1108 |
+
"mrr": 0.8306570306570307,
|
| 1109 |
+
"p50_retrieve_ms": 0.0,
|
| 1110 |
+
"p95_retrieve_ms": 0.0,
|
| 1111 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1112 |
+
"report_path": "runs/search-esci-74-ce-pool/report.json",
|
| 1113 |
+
"recorded_at": "2026-08-18T14:29:17.114211+00:00"
|
| 1114 |
+
},
|
| 1115 |
+
{
|
| 1116 |
+
"run_id": "search-esci-74-ce-pool-ft",
|
| 1117 |
+
"brain": "searchbenchesci74",
|
| 1118 |
+
"dataset": "search_esci_74.jsonl",
|
| 1119 |
+
"fusion": "none",
|
| 1120 |
+
"rerank": "plugin:cross-encoder",
|
| 1121 |
+
"channels": [
|
| 1122 |
+
"rank-pool-ce"
|
| 1123 |
+
],
|
| 1124 |
+
"rank_pool": true,
|
| 1125 |
+
"rank_pool_ce": true,
|
| 1126 |
+
"n_queries": 74,
|
| 1127 |
+
"ndcg@10": 0.5832632846066277,
|
| 1128 |
+
"ndcg@20": 0.6690889170924552,
|
| 1129 |
+
"ndcg": 0.7451408010463653,
|
| 1130 |
+
"recall@10": 0.4893954738298456,
|
| 1131 |
+
"recall@20": 0.7641337775411228,
|
| 1132 |
+
"mrr": 0.8932519057519057,
|
| 1133 |
+
"p50_retrieve_ms": 0.0,
|
| 1134 |
+
"p95_retrieve_ms": 0.0,
|
| 1135 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1136 |
+
"report_path": "runs/search-esci-74-ce-pool-ft/report.json",
|
| 1137 |
+
"recorded_at": "2026-08-18T14:32:48.672213+00:00"
|
| 1138 |
+
},
|
| 1139 |
+
{
|
| 1140 |
+
"run_id": "search-esci-74-passages",
|
| 1141 |
+
"brain": "searchbenchesci74",
|
| 1142 |
+
"dataset": "search_esci_74.jsonl",
|
| 1143 |
+
"fusion": "rrf",
|
| 1144 |
+
"rerank": "none",
|
| 1145 |
+
"channels": [
|
| 1146 |
+
"passages"
|
| 1147 |
+
],
|
| 1148 |
+
"rank_pool": false,
|
| 1149 |
+
"rank_pool_ce": false,
|
| 1150 |
+
"n_queries": 74,
|
| 1151 |
+
"ndcg@10": 0.49528253069041633,
|
| 1152 |
+
"ndcg@20": 0.5490094202707876,
|
| 1153 |
+
"ndcg": 0.5490094202707876,
|
| 1154 |
+
"recall@10": 0.3769307112873572,
|
| 1155 |
+
"recall@20": 0.5920965629425753,
|
| 1156 |
+
"mrr": 0.7580598455598455,
|
| 1157 |
+
"p50_retrieve_ms": 70.9225,
|
| 1158 |
+
"p95_retrieve_ms": 94.17495,
|
| 1159 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1160 |
+
"report_path": "runs/search-esci-74-passages/report.json",
|
| 1161 |
+
"recorded_at": "2026-08-18T15:38:50.867049+00:00"
|
| 1162 |
+
},
|
| 1163 |
+
{
|
| 1164 |
+
"run_id": "search-esci-74-passages-pool",
|
| 1165 |
+
"brain": "searchbenchesci74",
|
| 1166 |
+
"dataset": "search_esci_74.jsonl",
|
| 1167 |
+
"fusion": "rrf",
|
| 1168 |
+
"rerank": "none",
|
| 1169 |
+
"channels": [
|
| 1170 |
+
"passages"
|
| 1171 |
+
],
|
| 1172 |
+
"rank_pool": true,
|
| 1173 |
+
"rank_pool_ce": false,
|
| 1174 |
+
"n_queries": 74,
|
| 1175 |
+
"ndcg@10": 0.5458756376131827,
|
| 1176 |
+
"ndcg@20": 0.5785048208487761,
|
| 1177 |
+
"ndcg": 0.6013933046523523,
|
| 1178 |
+
"recall@10": 0.42064799804369324,
|
| 1179 |
+
"recall@20": 0.5920965629425753,
|
| 1180 |
+
"mrr": 0.8254504504504505,
|
| 1181 |
+
"p50_retrieve_ms": 65.447,
|
| 1182 |
+
"p95_retrieve_ms": 91.44999999999999,
|
| 1183 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1184 |
+
"report_path": "runs/search-esci-74-passages-pool/report.json",
|
| 1185 |
+
"recorded_at": "2026-08-18T15:41:37.970033+00:00"
|
| 1186 |
+
},
|
| 1187 |
+
{
|
| 1188 |
+
"run_id": "search-esci-slice-passages-gate",
|
| 1189 |
+
"brain": "searchbenchesci20",
|
| 1190 |
+
"dataset": "search_esci_slice.jsonl",
|
| 1191 |
+
"fusion": "rrf",
|
| 1192 |
+
"rerank": "none",
|
| 1193 |
+
"channels": [
|
| 1194 |
+
"passages"
|
| 1195 |
+
],
|
| 1196 |
+
"rank_pool": false,
|
| 1197 |
+
"rank_pool_ce": false,
|
| 1198 |
+
"n_queries": 11,
|
| 1199 |
+
"ndcg@10": 0.7579469533596556,
|
| 1200 |
+
"ndcg@20": 0.7962544743266965,
|
| 1201 |
+
"ndcg": 0.7962544743266965,
|
| 1202 |
+
"recall@10": 0.5106060606060606,
|
| 1203 |
+
"recall@20": 0.8469696969696969,
|
| 1204 |
+
"mrr": 0.8484848484848485,
|
| 1205 |
+
"p50_retrieve_ms": 25.549,
|
| 1206 |
+
"p95_retrieve_ms": 41.532,
|
| 1207 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1208 |
+
"report_path": "runs/search-esci-slice-passages-gate/report.json",
|
| 1209 |
+
"recorded_at": "2026-08-18T16:47:11.911155+00:00"
|
| 1210 |
+
},
|
| 1211 |
+
{
|
| 1212 |
+
"run_id": "search-esci-slice-pec-lists",
|
| 1213 |
+
"brain": "searchbenchesci20",
|
| 1214 |
+
"dataset": "search_esci_slice.jsonl",
|
| 1215 |
+
"fusion": "rrf",
|
| 1216 |
+
"rerank": "none",
|
| 1217 |
+
"channels": [
|
| 1218 |
+
"passages",
|
| 1219 |
+
"entities",
|
| 1220 |
+
"communities"
|
| 1221 |
+
],
|
| 1222 |
+
"rank_pool": false,
|
| 1223 |
+
"rank_pool_ce": false,
|
| 1224 |
+
"n_queries": 11,
|
| 1225 |
+
"ndcg@10": 0.6526549564168582,
|
| 1226 |
+
"ndcg@20": 0.6261389422887412,
|
| 1227 |
+
"ndcg": 0.683063799169122,
|
| 1228 |
+
"recall@10": 0.496969696969697,
|
| 1229 |
+
"recall@20": 0.6530303030303031,
|
| 1230 |
+
"mrr": 0.816017316017316,
|
| 1231 |
+
"p50_retrieve_ms": 160.231,
|
| 1232 |
+
"p95_retrieve_ms": 198.8395,
|
| 1233 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1234 |
+
"report_path": "runs/search-esci-slice-pec-lists/report.json",
|
| 1235 |
+
"recorded_at": "2026-08-18T16:47:26.669145+00:00"
|
| 1236 |
+
},
|
| 1237 |
+
{
|
| 1238 |
+
"run_id": "search-esci-slice-entities-neighbors",
|
| 1239 |
+
"brain": "searchbenchesci20",
|
| 1240 |
+
"dataset": "search_esci_slice.jsonl",
|
| 1241 |
+
"fusion": "rrf",
|
| 1242 |
+
"rerank": "none",
|
| 1243 |
+
"channels": [
|
| 1244 |
+
"entities"
|
| 1245 |
+
],
|
| 1246 |
+
"rank_pool": false,
|
| 1247 |
+
"rank_pool_ce": false,
|
| 1248 |
+
"n_queries": 11,
|
| 1249 |
+
"ndcg@10": 0.6549492067418874,
|
| 1250 |
+
"ndcg@20": 0.7173885785786568,
|
| 1251 |
+
"ndcg": 0.7173885785786568,
|
| 1252 |
+
"recall@10": 0.4818181818181818,
|
| 1253 |
+
"recall@20": 0.8217171717171716,
|
| 1254 |
+
"mrr": 0.7568181818181817,
|
| 1255 |
+
"p50_retrieve_ms": 223.289,
|
| 1256 |
+
"p95_retrieve_ms": 271.031,
|
| 1257 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1258 |
+
"report_path": "runs/search-esci-slice-entities-neighbors/report.json",
|
| 1259 |
+
"recorded_at": "2026-08-18T16:48:04.631525+00:00"
|
| 1260 |
+
},
|
| 1261 |
+
{
|
| 1262 |
+
"run_id": "search-esci-74-passages-control",
|
| 1263 |
+
"brain": "searchbenchesci74",
|
| 1264 |
+
"dataset": "search_esci_74.jsonl",
|
| 1265 |
+
"fusion": "rrf",
|
| 1266 |
+
"rerank": "none",
|
| 1267 |
+
"channels": [
|
| 1268 |
+
"passages"
|
| 1269 |
+
],
|
| 1270 |
+
"rank_pool": false,
|
| 1271 |
+
"rank_pool_ce": false,
|
| 1272 |
+
"n_queries": 74,
|
| 1273 |
+
"ndcg@10": 0.49528253069041633,
|
| 1274 |
+
"ndcg@20": 0.5490094202707876,
|
| 1275 |
+
"ndcg": 0.5490094202707876,
|
| 1276 |
+
"recall@10": 0.3769307112873572,
|
| 1277 |
+
"recall@20": 0.5920965629425753,
|
| 1278 |
+
"mrr": 0.7580598455598455,
|
| 1279 |
+
"p50_retrieve_ms": 61.7115,
|
| 1280 |
+
"p95_retrieve_ms": 91.52104999999995,
|
| 1281 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1282 |
+
"report_path": "runs/search-esci-74-passages-control/report.json",
|
| 1283 |
+
"recorded_at": "2026-08-18T17:13:00.463193+00:00"
|
| 1284 |
+
},
|
| 1285 |
+
{
|
| 1286 |
+
"run_id": "search-esci-74-ce-pool-l12-4class",
|
| 1287 |
+
"brain": "searchbenchesci74",
|
| 1288 |
+
"dataset": "search_esci_74.jsonl",
|
| 1289 |
+
"fusion": "none",
|
| 1290 |
+
"rerank": "4class-weighted-ce",
|
| 1291 |
+
"channels": [
|
| 1292 |
+
"rank-pool-4class"
|
| 1293 |
+
],
|
| 1294 |
+
"rank_pool": true,
|
| 1295 |
+
"rank_pool_ce": true,
|
| 1296 |
+
"n_queries": 74,
|
| 1297 |
+
"ndcg@10": 0.5968432958924662,
|
| 1298 |
+
"ndcg@20": 0.6782580445181766,
|
| 1299 |
+
"ndcg": 0.7467109659912253,
|
| 1300 |
+
"recall@10": 0.5088965287461584,
|
| 1301 |
+
"recall@20": 0.7673275730894211,
|
| 1302 |
+
"mrr": 0.8351464398761695,
|
| 1303 |
+
"p50_retrieve_ms": 0.0,
|
| 1304 |
+
"p95_retrieve_ms": 0.0,
|
| 1305 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1306 |
+
"report_path": "runs/search-esci-74-ce-pool-l12-4class/report.json",
|
| 1307 |
+
"recorded_at": "2026-08-18T17:44:17.593025+00:00"
|
| 1308 |
+
},
|
| 1309 |
+
{
|
| 1310 |
+
"run_id": "search-esci-74-ce-pool-4class",
|
| 1311 |
+
"brain": "searchbenchesci74",
|
| 1312 |
+
"dataset": "search_esci_74.jsonl",
|
| 1313 |
+
"fusion": "none",
|
| 1314 |
+
"rerank": "4class-weighted-ce",
|
| 1315 |
+
"channels": [
|
| 1316 |
+
"rank-pool-4class"
|
| 1317 |
+
],
|
| 1318 |
+
"rank_pool": true,
|
| 1319 |
+
"rank_pool_ce": true,
|
| 1320 |
+
"n_queries": 74,
|
| 1321 |
+
"ndcg@10": 0.5826375707951627,
|
| 1322 |
+
"ndcg@20": 0.6696127031036031,
|
| 1323 |
+
"ndcg": 0.7419804620261266,
|
| 1324 |
+
"recall@10": 0.49798997460620914,
|
| 1325 |
+
"recall@20": 0.7668403956654921,
|
| 1326 |
+
"mrr": 0.8317567567567568,
|
| 1327 |
+
"p50_retrieve_ms": 0.0,
|
| 1328 |
+
"p95_retrieve_ms": 0.0,
|
| 1329 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1330 |
+
"report_path": "runs/search-esci-74-ce-pool-4class/report.json",
|
| 1331 |
+
"recorded_at": "2026-08-18T17:54:14.073711+00:00"
|
| 1332 |
+
},
|
| 1333 |
+
{
|
| 1334 |
+
"run_id": "search-esci-74-ce-pool-4class-nowt",
|
| 1335 |
+
"brain": "searchbenchesci74",
|
| 1336 |
+
"dataset": "search_esci_74.jsonl",
|
| 1337 |
+
"fusion": "none",
|
| 1338 |
+
"rerank": "4class-weighted-ce",
|
| 1339 |
+
"channels": [
|
| 1340 |
+
"rank-pool-4class"
|
| 1341 |
+
],
|
| 1342 |
+
"rank_pool": true,
|
| 1343 |
+
"rank_pool_ce": true,
|
| 1344 |
+
"n_queries": 74,
|
| 1345 |
+
"ndcg@10": 0.6096378704103984,
|
| 1346 |
+
"ndcg@20": 0.6881968860231006,
|
| 1347 |
+
"ndcg": 0.7594213054854562,
|
| 1348 |
+
"recall@10": 0.5249760000665886,
|
| 1349 |
+
"recall@20": 0.7631438693842529,
|
| 1350 |
+
"mrr": 0.8530036338859869,
|
| 1351 |
+
"p50_retrieve_ms": 0.0,
|
| 1352 |
+
"p95_retrieve_ms": 0.0,
|
| 1353 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1354 |
+
"report_path": "runs/search-esci-74-ce-pool-4class-nowt/report.json",
|
| 1355 |
+
"recorded_at": "2026-08-18T18:20:20.750483+00:00"
|
| 1356 |
+
},
|
| 1357 |
+
{
|
| 1358 |
+
"run_id": "search-esci-74-ce-pool-4class-e2",
|
| 1359 |
+
"brain": "searchbenchesci74",
|
| 1360 |
+
"dataset": "search_esci_74.jsonl",
|
| 1361 |
+
"fusion": "none",
|
| 1362 |
+
"rerank": "4class-weighted-ce",
|
| 1363 |
+
"channels": [
|
| 1364 |
+
"rank-pool-4class"
|
| 1365 |
+
],
|
| 1366 |
+
"rank_pool": true,
|
| 1367 |
+
"rank_pool_ce": true,
|
| 1368 |
+
"n_queries": 74,
|
| 1369 |
+
"ndcg@10": 0.6169746541917066,
|
| 1370 |
+
"ndcg@20": 0.6945278030601721,
|
| 1371 |
+
"ndcg": 0.7652624576380807,
|
| 1372 |
+
"recall@10": 0.5214445444747297,
|
| 1373 |
+
"recall@20": 0.7719306369596903,
|
| 1374 |
+
"mrr": 0.8525418275418275,
|
| 1375 |
+
"p50_retrieve_ms": 0.0,
|
| 1376 |
+
"p95_retrieve_ms": 0.0,
|
| 1377 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1378 |
+
"report_path": "runs/search-esci-74-ce-pool-4class-e2/report.json",
|
| 1379 |
+
"recorded_at": "2026-08-18T18:48:00.939749+00:00"
|
| 1380 |
+
},
|
| 1381 |
+
{
|
| 1382 |
+
"run_id": "search-esci-74-passages-k50",
|
| 1383 |
+
"brain": "searchbenchesci74",
|
| 1384 |
+
"dataset": "search_esci_74.jsonl",
|
| 1385 |
+
"fusion": "rrf",
|
| 1386 |
+
"rerank": "none",
|
| 1387 |
+
"channels": [
|
| 1388 |
+
"passages"
|
| 1389 |
+
],
|
| 1390 |
+
"rank_pool": false,
|
| 1391 |
+
"rank_pool_ce": false,
|
| 1392 |
+
"n_queries": 74,
|
| 1393 |
+
"k": 50,
|
| 1394 |
+
"ndcg@10": 0.4998984443226263,
|
| 1395 |
+
"ndcg@20": 0.5522842574567189,
|
| 1396 |
+
"ndcg@50": 0.6378451547901237,
|
| 1397 |
+
"ndcg": 0.6378451547901237,
|
| 1398 |
+
"recall@10": 0.378732513089159,
|
| 1399 |
+
"recall@20": 0.589865760711773,
|
| 1400 |
+
"recall@50": 0.833954370548067,
|
| 1401 |
+
"mrr": 0.7645849420849421,
|
| 1402 |
+
"pool_coverage": null,
|
| 1403 |
+
"p50_retrieve_ms": 60.091,
|
| 1404 |
+
"p95_retrieve_ms": 88.02255,
|
| 1405 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1406 |
+
"report_path": "runs/search-esci-74-passages-k50/report.json",
|
| 1407 |
+
"recorded_at": "2026-08-18T20:31:55.998500+00:00"
|
| 1408 |
+
},
|
| 1409 |
+
{
|
| 1410 |
+
"run_id": "search-esci-74-passages-k100",
|
| 1411 |
+
"brain": "searchbenchesci74",
|
| 1412 |
+
"dataset": "search_esci_74.jsonl",
|
| 1413 |
+
"fusion": "rrf",
|
| 1414 |
+
"rerank": "none",
|
| 1415 |
+
"channels": [
|
| 1416 |
+
"passages"
|
| 1417 |
+
],
|
| 1418 |
+
"rank_pool": false,
|
| 1419 |
+
"rank_pool_ce": false,
|
| 1420 |
+
"n_queries": 74,
|
| 1421 |
+
"k": 100,
|
| 1422 |
+
"ndcg@10": 0.4998055248060819,
|
| 1423 |
+
"ndcg@20": 0.5523315287868084,
|
| 1424 |
+
"ndcg@50": 0.6375525242924566,
|
| 1425 |
+
"ndcg": 0.6564185982755066,
|
| 1426 |
+
"recall@10": 0.378732513089159,
|
| 1427 |
+
"recall@20": 0.5913672622132746,
|
| 1428 |
+
"recall@50": 0.830951367545064,
|
| 1429 |
+
"mrr": 0.76497727997728,
|
| 1430 |
+
"pool_coverage": null,
|
| 1431 |
+
"p50_retrieve_ms": 57.553,
|
| 1432 |
+
"p95_retrieve_ms": 81.81569999999999,
|
| 1433 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1434 |
+
"report_path": "runs/search-esci-74-passages-k100/report.json",
|
| 1435 |
+
"recorded_at": "2026-08-18T20:32:27.553496+00:00"
|
| 1436 |
+
},
|
| 1437 |
+
{
|
| 1438 |
+
"run_id": "search-esci-74-passages-k50-pool",
|
| 1439 |
+
"brain": "searchbenchesci74",
|
| 1440 |
+
"dataset": "search_esci_74.jsonl",
|
| 1441 |
+
"fusion": "rrf",
|
| 1442 |
+
"rerank": "none",
|
| 1443 |
+
"channels": [
|
| 1444 |
+
"passages"
|
| 1445 |
+
],
|
| 1446 |
+
"rank_pool": true,
|
| 1447 |
+
"rank_pool_ce": false,
|
| 1448 |
+
"n_queries": 74,
|
| 1449 |
+
"k": 50,
|
| 1450 |
+
"ndcg@10": 0.5668024300832123,
|
| 1451 |
+
"ndcg@20": 0.624102711461205,
|
| 1452 |
+
"ndcg@50": 0.6826188564511254,
|
| 1453 |
+
"ndcg": 0.6947261855882534,
|
| 1454 |
+
"recall@10": 0.4507852781809733,
|
| 1455 |
+
"recall@20": 0.6686543312372134,
|
| 1456 |
+
"recall@50": 0.8324528690465655,
|
| 1457 |
+
"mrr": 0.8271396396396397,
|
| 1458 |
+
"pool_coverage": 0.778222315718655,
|
| 1459 |
+
"p50_retrieve_ms": 60.1315,
|
| 1460 |
+
"p95_retrieve_ms": 86.49954999999997,
|
| 1461 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1462 |
+
"report_path": "runs/search-esci-74-passages-k50-pool/report.json",
|
| 1463 |
+
"recorded_at": "2026-08-18T20:33:02.203247+00:00"
|
| 1464 |
+
},
|
| 1465 |
+
{
|
| 1466 |
+
"run_id": "search-esci-74-passages-k100-pool",
|
| 1467 |
+
"brain": "searchbenchesci74",
|
| 1468 |
+
"dataset": "search_esci_74.jsonl",
|
| 1469 |
+
"fusion": "rrf",
|
| 1470 |
+
"rerank": "none",
|
| 1471 |
+
"channels": [
|
| 1472 |
+
"passages"
|
| 1473 |
+
],
|
| 1474 |
+
"rank_pool": true,
|
| 1475 |
+
"rank_pool_ce": false,
|
| 1476 |
+
"n_queries": 74,
|
| 1477 |
+
"k": 100,
|
| 1478 |
+
"ndcg@10": 0.5737527941225129,
|
| 1479 |
+
"ndcg@20": 0.6352462971440838,
|
| 1480 |
+
"ndcg@50": 0.7092403328584962,
|
| 1481 |
+
"ndcg": 0.7183777959320791,
|
| 1482 |
+
"recall@10": 0.46580029319598837,
|
| 1483 |
+
"recall@20": 0.6964864396614271,
|
| 1484 |
+
"recall@50": 0.9062083174986402,
|
| 1485 |
+
"mrr": 0.836599099099099,
|
| 1486 |
+
"pool_coverage": 0.8462618325454071,
|
| 1487 |
+
"p50_retrieve_ms": 58.5245,
|
| 1488 |
+
"p95_retrieve_ms": 82.5653,
|
| 1489 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1490 |
+
"report_path": "runs/search-esci-74-passages-k100-pool/report.json",
|
| 1491 |
+
"recorded_at": "2026-08-18T20:33:39.620042+00:00"
|
| 1492 |
+
},
|
| 1493 |
+
{
|
| 1494 |
+
"run_id": "search-esci-74-passages-k50-ce",
|
| 1495 |
+
"brain": "searchbenchesci74",
|
| 1496 |
+
"dataset": "search_esci_74.jsonl",
|
| 1497 |
+
"fusion": "rrf",
|
| 1498 |
+
"rerank": "harness:cross-encoder",
|
| 1499 |
+
"channels": [
|
| 1500 |
+
"passages"
|
| 1501 |
+
],
|
| 1502 |
+
"rank_pool": false,
|
| 1503 |
+
"rank_pool_ce": false,
|
| 1504 |
+
"n_queries": 74,
|
| 1505 |
+
"k": 50,
|
| 1506 |
+
"ndcg@10": 0.4478964444706324,
|
| 1507 |
+
"ndcg@20": 0.5146313751051845,
|
| 1508 |
+
"ndcg@50": 0.6060742524325241,
|
| 1509 |
+
"ndcg": 0.6060742524325241,
|
| 1510 |
+
"recall@10": 0.331273452580599,
|
| 1511 |
+
"recall@20": 0.5538599992903688,
|
| 1512 |
+
"recall@50": 0.833954370548067,
|
| 1513 |
+
"mrr": 0.7197929141477528,
|
| 1514 |
+
"pool_coverage": null,
|
| 1515 |
+
"p50_retrieve_ms": 60.091,
|
| 1516 |
+
"p95_retrieve_ms": 88.02255,
|
| 1517 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1518 |
+
"report_path": "runs/search-esci-74-passages-k50-ce/report.json",
|
| 1519 |
+
"recorded_at": "2026-08-18T20:34:39.854467+00:00"
|
| 1520 |
+
},
|
| 1521 |
+
{
|
| 1522 |
+
"run_id": "search-esci-74-passages-k50-ce-l12",
|
| 1523 |
+
"brain": "searchbenchesci74",
|
| 1524 |
+
"dataset": "search_esci_74.jsonl",
|
| 1525 |
+
"fusion": "rrf",
|
| 1526 |
+
"rerank": "harness:cross-encoder",
|
| 1527 |
+
"channels": [
|
| 1528 |
+
"passages"
|
| 1529 |
+
],
|
| 1530 |
+
"rank_pool": false,
|
| 1531 |
+
"rank_pool_ce": false,
|
| 1532 |
+
"n_queries": 74,
|
| 1533 |
+
"k": 50,
|
| 1534 |
+
"ndcg@10": 0.4674585131694284,
|
| 1535 |
+
"ndcg@20": 0.5084668214514952,
|
| 1536 |
+
"ndcg@50": 0.6140738833603318,
|
| 1537 |
+
"ndcg": 0.6140738833603318,
|
| 1538 |
+
"recall@10": 0.3412172585548375,
|
| 1539 |
+
"recall@20": 0.5191872363321448,
|
| 1540 |
+
"recall@50": 0.833954370548067,
|
| 1541 |
+
"mrr": 0.7063349063349063,
|
| 1542 |
+
"pool_coverage": null,
|
| 1543 |
+
"p50_retrieve_ms": 60.091,
|
| 1544 |
+
"p95_retrieve_ms": 88.02255,
|
| 1545 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1546 |
+
"report_path": "runs/search-esci-74-passages-k50-ce-l12/report.json",
|
| 1547 |
+
"recorded_at": "2026-08-18T20:35:44.462471+00:00"
|
| 1548 |
+
},
|
| 1549 |
+
{
|
| 1550 |
+
"run_id": "search-esci-74-passages-k50-qrewrite",
|
| 1551 |
+
"brain": "searchbenchesci74",
|
| 1552 |
+
"dataset": "search_esci_74_qrewrite.jsonl",
|
| 1553 |
+
"fusion": "rrf",
|
| 1554 |
+
"rerank": "none",
|
| 1555 |
+
"channels": [
|
| 1556 |
+
"passages"
|
| 1557 |
+
],
|
| 1558 |
+
"rank_pool": false,
|
| 1559 |
+
"rank_pool_ce": false,
|
| 1560 |
+
"n_queries": 74,
|
| 1561 |
+
"k": 50,
|
| 1562 |
+
"ndcg@10": 0.4998984443226263,
|
| 1563 |
+
"ndcg@20": 0.5522842574567189,
|
| 1564 |
+
"ndcg@50": 0.6378451547901237,
|
| 1565 |
+
"ndcg": 0.6378451547901237,
|
| 1566 |
+
"recall@10": 0.378732513089159,
|
| 1567 |
+
"recall@20": 0.589865760711773,
|
| 1568 |
+
"recall@50": 0.833954370548067,
|
| 1569 |
+
"mrr": 0.7645849420849421,
|
| 1570 |
+
"pool_coverage": null,
|
| 1571 |
+
"p50_retrieve_ms": 59.432500000000005,
|
| 1572 |
+
"p95_retrieve_ms": 82.09889999999999,
|
| 1573 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1574 |
+
"report_path": "runs/search-esci-74-passages-k50-qrewrite/report.json",
|
| 1575 |
+
"recorded_at": "2026-08-18T21:18:51.308816+00:00"
|
| 1576 |
+
},
|
| 1577 |
+
{
|
| 1578 |
+
"run_id": "search-esci-74-dense-ance-k50",
|
| 1579 |
+
"brain": "harness-local",
|
| 1580 |
+
"dataset": "search_esci_74.jsonl",
|
| 1581 |
+
"fusion": "none",
|
| 1582 |
+
"rerank": "none",
|
| 1583 |
+
"channels": [
|
| 1584 |
+
"harness-dense"
|
| 1585 |
+
],
|
| 1586 |
+
"rank_pool": false,
|
| 1587 |
+
"rank_pool_ce": false,
|
| 1588 |
+
"n_queries": 74,
|
| 1589 |
+
"k": 50,
|
| 1590 |
+
"ndcg@10": 0.3960504272587966,
|
| 1591 |
+
"ndcg@20": 0.445560860839057,
|
| 1592 |
+
"ndcg@50": 0.5370854731938006,
|
| 1593 |
+
"ndcg": 0.5370854731938006,
|
| 1594 |
+
"recall@10": 0.3015013697338196,
|
| 1595 |
+
"recall@20": 0.4872087291019431,
|
| 1596 |
+
"recall@50": 0.7503156876126188,
|
| 1597 |
+
"mrr": 0.6471417846417846,
|
| 1598 |
+
"pool_coverage": null,
|
| 1599 |
+
"p50_retrieve_ms": 74.9370945944778,
|
| 1600 |
+
"p95_retrieve_ms": 74.9370945944778,
|
| 1601 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1602 |
+
"report_path": "runs/search-esci-74-dense-ance-k50/report.json",
|
| 1603 |
+
"recorded_at": "2026-08-18T21:22:24.054560+00:00"
|
| 1604 |
+
},
|
| 1605 |
+
{
|
| 1606 |
+
"run_id": "search-esci-74-passages-k50-ce-retrieved",
|
| 1607 |
+
"brain": "searchbenchesci74",
|
| 1608 |
+
"dataset": "search_esci_74.jsonl",
|
| 1609 |
+
"fusion": "rrf",
|
| 1610 |
+
"rerank": "harness:cross-encoder",
|
| 1611 |
+
"channels": [
|
| 1612 |
+
"passages"
|
| 1613 |
+
],
|
| 1614 |
+
"rank_pool": false,
|
| 1615 |
+
"rank_pool_ce": false,
|
| 1616 |
+
"n_queries": 74,
|
| 1617 |
+
"k": 50,
|
| 1618 |
+
"ndcg@10": 0.4648895994453083,
|
| 1619 |
+
"ndcg@20": 0.5245865140860362,
|
| 1620 |
+
"ndcg@50": 0.6126753191137789,
|
| 1621 |
+
"ndcg": 0.6126753191137789,
|
| 1622 |
+
"recall@10": 0.35140463046557896,
|
| 1623 |
+
"recall@20": 0.5410788442403298,
|
| 1624 |
+
"recall@50": 0.833954370548067,
|
| 1625 |
+
"mrr": 0.7120610588001892,
|
| 1626 |
+
"pool_coverage": null,
|
| 1627 |
+
"p50_retrieve_ms": 60.091,
|
| 1628 |
+
"p95_retrieve_ms": 88.02255,
|
| 1629 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1630 |
+
"report_path": "runs/search-esci-74-passages-k50-ce-retrieved/report.json",
|
| 1631 |
+
"recorded_at": "2026-08-18T22:38:31.141079+00:00"
|
| 1632 |
+
},
|
| 1633 |
+
{
|
| 1634 |
+
"run_id": "search-esci-74-colbert-k50",
|
| 1635 |
+
"brain": "harness-local-colbert",
|
| 1636 |
+
"dataset": "search_esci_74.jsonl",
|
| 1637 |
+
"fusion": "none",
|
| 1638 |
+
"rerank": "none",
|
| 1639 |
+
"channels": [
|
| 1640 |
+
"harness-colbert"
|
| 1641 |
+
],
|
| 1642 |
+
"rank_pool": false,
|
| 1643 |
+
"rank_pool_ce": false,
|
| 1644 |
+
"n_queries": 74,
|
| 1645 |
+
"k": 50,
|
| 1646 |
+
"ndcg@10": 0.43412701768229023,
|
| 1647 |
+
"ndcg@20": 0.4815601734708754,
|
| 1648 |
+
"ndcg@50": 0.5523204205368764,
|
| 1649 |
+
"ndcg": 0.5523204205368764,
|
| 1650 |
+
"recall@10": 0.3110168068124024,
|
| 1651 |
+
"recall@20": 0.5023603484326227,
|
| 1652 |
+
"recall@50": 0.7138820979266567,
|
| 1653 |
+
"mrr": 0.6689285195333583,
|
| 1654 |
+
"pool_coverage": null,
|
| 1655 |
+
"p50_retrieve_ms": 10063.364791662614,
|
| 1656 |
+
"p95_retrieve_ms": 10063.364791662614,
|
| 1657 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1658 |
+
"report_path": "runs/search-esci-74-colbert-k50/report.json",
|
| 1659 |
+
"recorded_at": "2026-08-18T22:51:16.979167+00:00"
|
| 1660 |
+
},
|
| 1661 |
+
{
|
| 1662 |
+
"run_id": "search-esci-74-bge-base-k50",
|
| 1663 |
+
"brain": "harness-local",
|
| 1664 |
+
"dataset": "search_esci_74.jsonl",
|
| 1665 |
+
"fusion": "none",
|
| 1666 |
+
"rerank": "none",
|
| 1667 |
+
"channels": [
|
| 1668 |
+
"harness-dense"
|
| 1669 |
+
],
|
| 1670 |
+
"rank_pool": false,
|
| 1671 |
+
"rank_pool_ce": false,
|
| 1672 |
+
"n_queries": 74,
|
| 1673 |
+
"k": 50,
|
| 1674 |
+
"ndcg@10": 0.44136708015333975,
|
| 1675 |
+
"ndcg@20": 0.5041842920107446,
|
| 1676 |
+
"ndcg@50": 0.5892299329783445,
|
| 1677 |
+
"ndcg": 0.5892299329783445,
|
| 1678 |
+
"recall@10": 0.32179328339461255,
|
| 1679 |
+
"recall@20": 0.5251414009467592,
|
| 1680 |
+
"recall@50": 0.7989247943561789,
|
| 1681 |
+
"mrr": 0.7471303646913403,
|
| 1682 |
+
"pool_coverage": null,
|
| 1683 |
+
"p50_retrieve_ms": 408.0482843509762,
|
| 1684 |
+
"p95_retrieve_ms": 408.04828435097625,
|
| 1685 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1686 |
+
"report_path": "runs/search-esci-74-bge-base-k50/report.json",
|
| 1687 |
+
"recorded_at": "2026-08-18T22:52:53.742680+00:00"
|
| 1688 |
+
},
|
| 1689 |
+
{
|
| 1690 |
+
"run_id": "search-esci-74-union-bge-k50",
|
| 1691 |
+
"brain": "harness-local-union",
|
| 1692 |
+
"dataset": "search_esci_74.jsonl",
|
| 1693 |
+
"fusion": "rrf",
|
| 1694 |
+
"rerank": "none",
|
| 1695 |
+
"channels": [
|
| 1696 |
+
"harness-union"
|
| 1697 |
+
],
|
| 1698 |
+
"rank_pool": false,
|
| 1699 |
+
"rank_pool_ce": false,
|
| 1700 |
+
"n_queries": 74,
|
| 1701 |
+
"k": 50,
|
| 1702 |
+
"ndcg@10": 0.47918374395281466,
|
| 1703 |
+
"ndcg@20": 0.5381296158524957,
|
| 1704 |
+
"ndcg@50": 0.6141255447421892,
|
| 1705 |
+
"ndcg": 0.6141255447421892,
|
| 1706 |
+
"recall@10": 0.36314016383757297,
|
| 1707 |
+
"recall@20": 0.5774058852814317,
|
| 1708 |
+
"recall@50": 0.8271070974286027,
|
| 1709 |
+
"mrr": 0.7653163449499656,
|
| 1710 |
+
"pool_coverage": null,
|
| 1711 |
+
"p50_retrieve_ms": 0.0,
|
| 1712 |
+
"p95_retrieve_ms": 0.0,
|
| 1713 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1714 |
+
"report_path": "runs/search-esci-74-union-bge-k50/report.json",
|
| 1715 |
+
"recorded_at": "2026-08-19T08:10:06.928384+00:00"
|
| 1716 |
+
},
|
| 1717 |
+
{
|
| 1718 |
+
"run_id": "search-esci-74-exhaustive-ce",
|
| 1719 |
+
"brain": "harness-local-exhaustive",
|
| 1720 |
+
"dataset": "search_esci_74.jsonl",
|
| 1721 |
+
"fusion": "none",
|
| 1722 |
+
"rerank": "4class-weighted-ce",
|
| 1723 |
+
"channels": [
|
| 1724 |
+
"exhaustive-4class"
|
| 1725 |
+
],
|
| 1726 |
+
"rank_pool": false,
|
| 1727 |
+
"rank_pool_ce": false,
|
| 1728 |
+
"n_queries": 74,
|
| 1729 |
+
"k": 50,
|
| 1730 |
+
"ndcg@10": 0.4162185585932262,
|
| 1731 |
+
"ndcg@20": 0.4392534244105597,
|
| 1732 |
+
"ndcg@50": 0.5042093175793891,
|
| 1733 |
+
"ndcg": 0.5042093175793891,
|
| 1734 |
+
"recall@10": 0.26585896457775815,
|
| 1735 |
+
"recall@20": 0.4060812399456508,
|
| 1736 |
+
"recall@50": 0.6029270278490679,
|
| 1737 |
+
"mrr": 0.6578079129936715,
|
| 1738 |
+
"pool_coverage": null,
|
| 1739 |
+
"p50_retrieve_ms": 94253.11211881155,
|
| 1740 |
+
"p95_retrieve_ms": 94253.11211881155,
|
| 1741 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1742 |
+
"report_path": "runs/search-esci-74-exhaustive-ce/report.json",
|
| 1743 |
+
"recorded_at": "2026-08-19T10:05:59.463503+00:00"
|
| 1744 |
+
},
|
| 1745 |
+
{
|
| 1746 |
+
"run_id": "search-esci-74-catalog-ce-k50",
|
| 1747 |
+
"brain": "searchbenchesci74",
|
| 1748 |
+
"dataset": "search_esci_74.jsonl",
|
| 1749 |
+
"fusion": "rrf",
|
| 1750 |
+
"rerank": "plugin:cross-encoder",
|
| 1751 |
+
"channels": [
|
| 1752 |
+
"passages"
|
| 1753 |
+
],
|
| 1754 |
+
"rank_pool": false,
|
| 1755 |
+
"rank_pool_ce": false,
|
| 1756 |
+
"n_queries": 74,
|
| 1757 |
+
"k": 50,
|
| 1758 |
+
"ndcg@10": 0.4674585131694284,
|
| 1759 |
+
"ndcg@20": 0.5084668214514952,
|
| 1760 |
+
"ndcg@50": 0.6140738833603318,
|
| 1761 |
+
"ndcg": 0.6140738833603318,
|
| 1762 |
+
"recall@10": 0.3412172585548375,
|
| 1763 |
+
"recall@20": 0.5191872363321448,
|
| 1764 |
+
"recall@50": 0.833954370548067,
|
| 1765 |
+
"mrr": 0.7063349063349063,
|
| 1766 |
+
"pool_coverage": null,
|
| 1767 |
+
"p50_retrieve_ms": 59.308499999999995,
|
| 1768 |
+
"p95_retrieve_ms": 86.88884999999999,
|
| 1769 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1770 |
+
"report_path": "runs/search-esci-74-catalog-ce-k50/report.json",
|
| 1771 |
+
"recorded_at": "2026-08-19T10:17:45.568452+00:00"
|
| 1772 |
+
},
|
| 1773 |
+
{
|
| 1774 |
+
"run_id": "search-esci-74-cascade-tail-k50",
|
| 1775 |
+
"brain": "harness-local-cascade",
|
| 1776 |
+
"dataset": "search_esci_74.jsonl",
|
| 1777 |
+
"fusion": "none",
|
| 1778 |
+
"rerank": "none",
|
| 1779 |
+
"channels": [
|
| 1780 |
+
"harness-cascade"
|
| 1781 |
+
],
|
| 1782 |
+
"rank_pool": false,
|
| 1783 |
+
"rank_pool_ce": false,
|
| 1784 |
+
"n_queries": 74,
|
| 1785 |
+
"k": 50,
|
| 1786 |
+
"ndcg@10": 0.4998984443226263,
|
| 1787 |
+
"ndcg@20": 0.6194622913116794,
|
| 1788 |
+
"ndcg@50": 0.6686100079902446,
|
| 1789 |
+
"ndcg": 0.6686100079902446,
|
| 1790 |
+
"recall@10": 0.378732513089159,
|
| 1791 |
+
"recall@20": 0.7504072036031975,
|
| 1792 |
+
"recall@50": 0.8885964311815054,
|
| 1793 |
+
"mrr": 0.7679931554931555,
|
| 1794 |
+
"pool_coverage": null,
|
| 1795 |
+
"p50_retrieve_ms": 0.0,
|
| 1796 |
+
"p95_retrieve_ms": 0.0,
|
| 1797 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1798 |
+
"report_path": "runs/search-esci-74-cascade-tail-k50/report.json",
|
| 1799 |
+
"recorded_at": "2026-08-19T10:32:17.738702+00:00"
|
| 1800 |
+
},
|
| 1801 |
+
{
|
| 1802 |
+
"run_id": "search-esci-74-ce-pool-deberta-base",
|
| 1803 |
+
"brain": "searchbenchesci74",
|
| 1804 |
+
"dataset": "search_esci_74.jsonl",
|
| 1805 |
+
"fusion": "none",
|
| 1806 |
+
"rerank": "4class-weighted-ce",
|
| 1807 |
+
"channels": [
|
| 1808 |
+
"rank-pool-4class"
|
| 1809 |
+
],
|
| 1810 |
+
"rank_pool": true,
|
| 1811 |
+
"rank_pool_ce": true,
|
| 1812 |
+
"n_queries": 74,
|
| 1813 |
+
"k": 40,
|
| 1814 |
+
"ndcg@10": 0.6492728519674476,
|
| 1815 |
+
"ndcg@20": 0.7101117922903459,
|
| 1816 |
+
"ndcg@50": null,
|
| 1817 |
+
"ndcg": 0.7772540710194906,
|
| 1818 |
+
"recall@10": 0.5183145850778,
|
| 1819 |
+
"recall@20": 0.7604145345598889,
|
| 1820 |
+
"recall@50": null,
|
| 1821 |
+
"mrr": 0.8646712599298806,
|
| 1822 |
+
"pool_coverage": null,
|
| 1823 |
+
"p50_retrieve_ms": 0.0,
|
| 1824 |
+
"p95_retrieve_ms": 0.0,
|
| 1825 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1826 |
+
"report_path": "runs/search-esci-74-ce-pool-deberta-base/report.json",
|
| 1827 |
+
"recorded_at": "2026-08-19T14:00:25.594211+00:00"
|
| 1828 |
+
},
|
| 1829 |
+
{
|
| 1830 |
+
"run_id": "search-esci-74-passages-k50-ce-deberta",
|
| 1831 |
+
"brain": "searchbenchsmoke",
|
| 1832 |
+
"dataset": "search_esci_74.jsonl",
|
| 1833 |
+
"fusion": "rrf",
|
| 1834 |
+
"rerank": "harness:cross-encoder",
|
| 1835 |
+
"channels": [
|
| 1836 |
+
"passages"
|
| 1837 |
+
],
|
| 1838 |
+
"rank_pool": false,
|
| 1839 |
+
"rank_pool_ce": false,
|
| 1840 |
+
"n_queries": 74,
|
| 1841 |
+
"k": 50,
|
| 1842 |
+
"ndcg@10": 0.5101053946025641,
|
| 1843 |
+
"ndcg@20": 0.5568741362838021,
|
| 1844 |
+
"ndcg@50": 0.6365973490191127,
|
| 1845 |
+
"ndcg": 0.6365973490191127,
|
| 1846 |
+
"recall@10": 0.36281504203381093,
|
| 1847 |
+
"recall@20": 0.5467544580912761,
|
| 1848 |
+
"recall@50": 0.833954370548067,
|
| 1849 |
+
"mrr": 0.7457139784725992,
|
| 1850 |
+
"pool_coverage": null,
|
| 1851 |
+
"p50_retrieve_ms": 60.091,
|
| 1852 |
+
"p95_retrieve_ms": 88.02255,
|
| 1853 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1854 |
+
"report_path": "runs/search-esci-74-passages-k50-ce-deberta/report.json",
|
| 1855 |
+
"recorded_at": "2026-08-19T14:09:19.540811+00:00"
|
| 1856 |
+
},
|
| 1857 |
+
{
|
| 1858 |
+
"run_id": "search-esci-es-passages-k50",
|
| 1859 |
+
"brain": "searchbenchescies",
|
| 1860 |
+
"dataset": "search_esci_es.jsonl",
|
| 1861 |
+
"fusion": "rrf",
|
| 1862 |
+
"rerank": "none",
|
| 1863 |
+
"channels": [
|
| 1864 |
+
"passages"
|
| 1865 |
+
],
|
| 1866 |
+
"rank_pool": false,
|
| 1867 |
+
"rank_pool_ce": false,
|
| 1868 |
+
"n_queries": 62,
|
| 1869 |
+
"k": 50,
|
| 1870 |
+
"ndcg@10": 0.5774393479403104,
|
| 1871 |
+
"ndcg@20": 0.6303553323648456,
|
| 1872 |
+
"ndcg@50": 0.7327853549091549,
|
| 1873 |
+
"ndcg": 0.7327853549091549,
|
| 1874 |
+
"recall@10": 0.35328224222955834,
|
| 1875 |
+
"recall@20": 0.5758587932370467,
|
| 1876 |
+
"recall@50": 0.9141280526823616,
|
| 1877 |
+
"mrr": 0.8704493087557603,
|
| 1878 |
+
"pool_coverage": null,
|
| 1879 |
+
"p50_retrieve_ms": 84.13,
|
| 1880 |
+
"p95_retrieve_ms": 96.16295000000001,
|
| 1881 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1882 |
+
"report_path": "runs/search-esci-es-passages-k50/report.json",
|
| 1883 |
+
"recorded_at": "2026-08-19T16:34:42.965337+00:00"
|
| 1884 |
+
},
|
| 1885 |
+
{
|
| 1886 |
+
"run_id": "search-italian-smoke",
|
| 1887 |
+
"brain": "searchbenchitsmoke",
|
| 1888 |
+
"dataset": "search_italian_smoke.jsonl",
|
| 1889 |
+
"fusion": "rrf",
|
| 1890 |
+
"rerank": "none",
|
| 1891 |
+
"channels": [
|
| 1892 |
+
"passages"
|
| 1893 |
+
],
|
| 1894 |
+
"rank_pool": false,
|
| 1895 |
+
"rank_pool_ce": false,
|
| 1896 |
+
"n_queries": 3,
|
| 1897 |
+
"k": 20,
|
| 1898 |
+
"ndcg@10": 1.0,
|
| 1899 |
+
"ndcg@20": 1.0,
|
| 1900 |
+
"ndcg@50": null,
|
| 1901 |
+
"ndcg": 1.0,
|
| 1902 |
+
"recall@10": 1.0,
|
| 1903 |
+
"recall@20": 1.0,
|
| 1904 |
+
"recall@50": null,
|
| 1905 |
+
"mrr": 1.0,
|
| 1906 |
+
"pool_coverage": null,
|
| 1907 |
+
"p50_retrieve_ms": 25.33,
|
| 1908 |
+
"p95_retrieve_ms": 29.3206,
|
| 1909 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1910 |
+
"report_path": "runs/search-italian-smoke/report.json",
|
| 1911 |
+
"recorded_at": "2026-08-19T16:37:37.730780+00:00"
|
| 1912 |
+
},
|
| 1913 |
+
{
|
| 1914 |
+
"run_id": "search-italian-smoke-inflect",
|
| 1915 |
+
"brain": "searchbenchitsmoke",
|
| 1916 |
+
"dataset": "search_italian_smoke_inflect.jsonl",
|
| 1917 |
+
"fusion": "rrf",
|
| 1918 |
+
"rerank": "none",
|
| 1919 |
+
"channels": [
|
| 1920 |
+
"passages"
|
| 1921 |
+
],
|
| 1922 |
+
"rank_pool": false,
|
| 1923 |
+
"rank_pool_ce": false,
|
| 1924 |
+
"n_queries": 3,
|
| 1925 |
+
"k": 20,
|
| 1926 |
+
"ndcg@10": 1.0,
|
| 1927 |
+
"ndcg@20": 1.0,
|
| 1928 |
+
"ndcg@50": null,
|
| 1929 |
+
"ndcg": 1.0,
|
| 1930 |
+
"recall@10": 1.0,
|
| 1931 |
+
"recall@20": 1.0,
|
| 1932 |
+
"recall@50": null,
|
| 1933 |
+
"mrr": 1.0,
|
| 1934 |
+
"pool_coverage": null,
|
| 1935 |
+
"p50_retrieve_ms": 21.494,
|
| 1936 |
+
"p95_retrieve_ms": 23.211199999999998,
|
| 1937 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1938 |
+
"report_path": "runs/search-italian-smoke-inflect/report.json",
|
| 1939 |
+
"recorded_at": "2026-08-19T16:37:46.108861+00:00"
|
| 1940 |
+
},
|
| 1941 |
+
{
|
| 1942 |
+
"run_id": "search-italian-smoke-inflect-fts",
|
| 1943 |
+
"brain": "searchbenchitsmoke",
|
| 1944 |
+
"dataset": "search_italian_smoke_inflect.jsonl",
|
| 1945 |
+
"fusion": "rrf",
|
| 1946 |
+
"rerank": "none",
|
| 1947 |
+
"channels": [
|
| 1948 |
+
"passages"
|
| 1949 |
+
],
|
| 1950 |
+
"rank_pool": false,
|
| 1951 |
+
"rank_pool_ce": false,
|
| 1952 |
+
"n_queries": 3,
|
| 1953 |
+
"k": 20,
|
| 1954 |
+
"ndcg@10": 1.0,
|
| 1955 |
+
"ndcg@20": 1.0,
|
| 1956 |
+
"ndcg@50": null,
|
| 1957 |
+
"ndcg": 1.0,
|
| 1958 |
+
"recall@10": 1.0,
|
| 1959 |
+
"recall@20": 1.0,
|
| 1960 |
+
"recall@50": null,
|
| 1961 |
+
"mrr": 1.0,
|
| 1962 |
+
"pool_coverage": null,
|
| 1963 |
+
"p50_retrieve_ms": 33.56,
|
| 1964 |
+
"p95_retrieve_ms": 36.8936,
|
| 1965 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1966 |
+
"report_path": "runs/search-italian-smoke-inflect-fts/report.json",
|
| 1967 |
+
"recorded_at": "2026-08-19T16:44:05.699330+00:00"
|
| 1968 |
+
},
|
| 1969 |
+
{
|
| 1970 |
+
"run_id": "search-italian-minilm-pipeline",
|
| 1971 |
+
"brain": "searchbenchitmmini",
|
| 1972 |
+
"dataset": "search_italian_smoke.jsonl",
|
| 1973 |
+
"fusion": "none",
|
| 1974 |
+
"rerank": "none",
|
| 1975 |
+
"channels": [
|
| 1976 |
+
"harness-dense"
|
| 1977 |
+
],
|
| 1978 |
+
"rank_pool": false,
|
| 1979 |
+
"rank_pool_ce": false,
|
| 1980 |
+
"n_queries": 3,
|
| 1981 |
+
"k": 10,
|
| 1982 |
+
"ndcg@10": 1.0,
|
| 1983 |
+
"ndcg@20": 1.0,
|
| 1984 |
+
"ndcg@50": null,
|
| 1985 |
+
"ndcg": 1.0,
|
| 1986 |
+
"recall@10": 1.0,
|
| 1987 |
+
"recall@20": null,
|
| 1988 |
+
"recall@50": null,
|
| 1989 |
+
"mrr": 1.0,
|
| 1990 |
+
"pool_coverage": null,
|
| 1991 |
+
"p50_retrieve_ms": 188.19916666689096,
|
| 1992 |
+
"p95_retrieve_ms": 188.19916666689096,
|
| 1993 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 1994 |
+
"report_path": "runs/search-italian-minilm-pipeline/report.json",
|
| 1995 |
+
"recorded_at": "2026-08-19T19:27:33.792776+00:00"
|
| 1996 |
+
},
|
| 1997 |
+
{
|
| 1998 |
+
"run_id": "search-esci-es-spell-k50",
|
| 1999 |
+
"brain": "searchbenchescies",
|
| 2000 |
+
"dataset": "search_esci_es_spell.jsonl",
|
| 2001 |
+
"fusion": "rrf",
|
| 2002 |
+
"rerank": "none",
|
| 2003 |
+
"channels": [
|
| 2004 |
+
"passages"
|
| 2005 |
+
],
|
| 2006 |
+
"rank_pool": false,
|
| 2007 |
+
"rank_pool_ce": false,
|
| 2008 |
+
"n_queries": 62,
|
| 2009 |
+
"k": 50,
|
| 2010 |
+
"ndcg@10": 0.5950327313726855,
|
| 2011 |
+
"ndcg@20": 0.6464666665333806,
|
| 2012 |
+
"ndcg@50": 0.7424579237775919,
|
| 2013 |
+
"ndcg": 0.7424579237775919,
|
| 2014 |
+
"recall@10": 0.36220476611566177,
|
| 2015 |
+
"recall@20": 0.5832920760052671,
|
| 2016 |
+
"recall@50": 0.9143064382925891,
|
| 2017 |
+
"mrr": 0.8865783410138248,
|
| 2018 |
+
"pool_coverage": null,
|
| 2019 |
+
"p50_retrieve_ms": 92.655,
|
| 2020 |
+
"p95_retrieve_ms": 107.2923,
|
| 2021 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 2022 |
+
"report_path": "runs/search-esci-es-spell-k50/report.json",
|
| 2023 |
+
"recorded_at": "2026-08-19T19:28:14.068481+00:00"
|
| 2024 |
+
},
|
| 2025 |
+
{
|
| 2026 |
+
"run_id": "search-italian-smoke-extras-locale",
|
| 2027 |
+
"brain": "searchbenchitsmoke",
|
| 2028 |
+
"dataset": "search_italian_smoke.jsonl",
|
| 2029 |
+
"fusion": "rrf",
|
| 2030 |
+
"rerank": "none",
|
| 2031 |
+
"channels": [
|
| 2032 |
+
"passages"
|
| 2033 |
+
],
|
| 2034 |
+
"rank_pool": false,
|
| 2035 |
+
"rank_pool_ce": false,
|
| 2036 |
+
"n_queries": 3,
|
| 2037 |
+
"k": 10,
|
| 2038 |
+
"ndcg@10": 1.0,
|
| 2039 |
+
"ndcg@20": 1.0,
|
| 2040 |
+
"ndcg@50": null,
|
| 2041 |
+
"ndcg": 1.0,
|
| 2042 |
+
"recall@10": 1.0,
|
| 2043 |
+
"recall@20": null,
|
| 2044 |
+
"recall@50": null,
|
| 2045 |
+
"mrr": 1.0,
|
| 2046 |
+
"pool_coverage": null,
|
| 2047 |
+
"p50_retrieve_ms": 17.126,
|
| 2048 |
+
"p95_retrieve_ms": 23.894000000000002,
|
| 2049 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 2050 |
+
"report_path": "runs/search-italian-smoke-extras-locale/report.json",
|
| 2051 |
+
"recorded_at": "2026-08-19T19:30:47.530480+00:00"
|
| 2052 |
+
},
|
| 2053 |
+
{
|
| 2054 |
+
"run_id": "search-italian-smoke-extras-nope",
|
| 2055 |
+
"brain": "searchbenchitsmoke",
|
| 2056 |
+
"dataset": "search_italian_smoke.jsonl",
|
| 2057 |
+
"fusion": "rrf",
|
| 2058 |
+
"rerank": "none",
|
| 2059 |
+
"channels": [
|
| 2060 |
+
"passages"
|
| 2061 |
+
],
|
| 2062 |
+
"rank_pool": false,
|
| 2063 |
+
"rank_pool_ce": false,
|
| 2064 |
+
"n_queries": 3,
|
| 2065 |
+
"k": 10,
|
| 2066 |
+
"ndcg@10": 0.0,
|
| 2067 |
+
"ndcg@20": 0.0,
|
| 2068 |
+
"ndcg@50": null,
|
| 2069 |
+
"ndcg": 0.0,
|
| 2070 |
+
"recall@10": 0.0,
|
| 2071 |
+
"recall@20": null,
|
| 2072 |
+
"recall@50": null,
|
| 2073 |
+
"mrr": 0.0,
|
| 2074 |
+
"pool_coverage": null,
|
| 2075 |
+
"p50_retrieve_ms": 22.721,
|
| 2076 |
+
"p95_retrieve_ms": 24.2753,
|
| 2077 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 2078 |
+
"report_path": "runs/search-italian-smoke-extras-nope/report.json",
|
| 2079 |
+
"recorded_at": "2026-08-19T19:29:31.676418+00:00"
|
| 2080 |
+
},
|
| 2081 |
+
{
|
| 2082 |
+
"run_id": "search-esci-74-passages-k50-extras-sanity",
|
| 2083 |
+
"brain": "searchbenchesci74",
|
| 2084 |
+
"dataset": "search_esci_74.jsonl",
|
| 2085 |
+
"fusion": "rrf",
|
| 2086 |
+
"rerank": "none",
|
| 2087 |
+
"channels": [
|
| 2088 |
+
"passages"
|
| 2089 |
+
],
|
| 2090 |
+
"rank_pool": false,
|
| 2091 |
+
"rank_pool_ce": false,
|
| 2092 |
+
"n_queries": 74,
|
| 2093 |
+
"k": 50,
|
| 2094 |
+
"ndcg@10": 0.4998984443226263,
|
| 2095 |
+
"ndcg@20": 0.5522842574567189,
|
| 2096 |
+
"ndcg@50": 0.6378451547901237,
|
| 2097 |
+
"ndcg": 0.6378451547901237,
|
| 2098 |
+
"recall@10": 0.378732513089159,
|
| 2099 |
+
"recall@20": 0.589865760711773,
|
| 2100 |
+
"recall@50": 0.833954370548067,
|
| 2101 |
+
"mrr": 0.7645849420849421,
|
| 2102 |
+
"pool_coverage": null,
|
| 2103 |
+
"p50_retrieve_ms": 63.1325,
|
| 2104 |
+
"p95_retrieve_ms": 88.86579999999998,
|
| 2105 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 2106 |
+
"report_path": "runs/search-esci-74-passages-k50-extras-sanity/report.json",
|
| 2107 |
+
"recorded_at": "2026-08-19T19:35:44.545077+00:00"
|
| 2108 |
+
},
|
| 2109 |
+
{
|
| 2110 |
+
"run_id": "search-italian-smoke-inflect-fts-or",
|
| 2111 |
+
"brain": "searchbenchitsmoke",
|
| 2112 |
+
"dataset": "search_italian_smoke_inflect.jsonl",
|
| 2113 |
+
"fusion": "rrf",
|
| 2114 |
+
"rerank": "none",
|
| 2115 |
+
"channels": [
|
| 2116 |
+
"passages"
|
| 2117 |
+
],
|
| 2118 |
+
"rank_pool": false,
|
| 2119 |
+
"rank_pool_ce": false,
|
| 2120 |
+
"n_queries": 3,
|
| 2121 |
+
"k": 20,
|
| 2122 |
+
"ndcg@10": 1.0,
|
| 2123 |
+
"ndcg@20": 1.0,
|
| 2124 |
+
"ndcg@50": null,
|
| 2125 |
+
"ndcg": 1.0,
|
| 2126 |
+
"recall@10": 1.0,
|
| 2127 |
+
"recall@20": 1.0,
|
| 2128 |
+
"recall@50": null,
|
| 2129 |
+
"mrr": 1.0,
|
| 2130 |
+
"pool_coverage": null,
|
| 2131 |
+
"p50_retrieve_ms": 26.364,
|
| 2132 |
+
"p95_retrieve_ms": 42.744899999999994,
|
| 2133 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 2134 |
+
"report_path": "runs/search-italian-smoke-inflect-fts-or/report.json",
|
| 2135 |
+
"recorded_at": "2026-08-19T20:16:24.241357+00:00"
|
| 2136 |
+
},
|
| 2137 |
+
{
|
| 2138 |
+
"run_id": "search-esci-74-cc-a03",
|
| 2139 |
+
"brain": "searchbenchesci74",
|
| 2140 |
+
"dataset": "search_esci_74.jsonl",
|
| 2141 |
+
"fusion": "cc",
|
| 2142 |
+
"rerank": "none",
|
| 2143 |
+
"channels": [
|
| 2144 |
+
"passages"
|
| 2145 |
+
],
|
| 2146 |
+
"rank_pool": false,
|
| 2147 |
+
"rank_pool_ce": false,
|
| 2148 |
+
"n_queries": 74,
|
| 2149 |
+
"k": 50,
|
| 2150 |
+
"ndcg@10": 0.48296913346675313,
|
| 2151 |
+
"ndcg@20": 0.5353434819101804,
|
| 2152 |
+
"ndcg@50": 0.6235227747628354,
|
| 2153 |
+
"ndcg": 0.6235227747628354,
|
| 2154 |
+
"recall@10": 0.37594464118364007,
|
| 2155 |
+
"recall@20": 0.5859902261910772,
|
| 2156 |
+
"recall@50": 0.8324528690465655,
|
| 2157 |
+
"mrr": 0.7492780202562811,
|
| 2158 |
+
"pool_coverage": null,
|
| 2159 |
+
"p50_retrieve_ms": 62.8885,
|
| 2160 |
+
"p95_retrieve_ms": 89.98204999999999,
|
| 2161 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 2162 |
+
"report_path": "runs/search-esci-74-cc-a03/report.json",
|
| 2163 |
+
"recorded_at": "2026-08-19T20:18:18.361436+00:00"
|
| 2164 |
+
},
|
| 2165 |
+
{
|
| 2166 |
+
"run_id": "search-esci-74-cc-a05",
|
| 2167 |
+
"brain": "searchbenchesci74",
|
| 2168 |
+
"dataset": "search_esci_74.jsonl",
|
| 2169 |
+
"fusion": "cc",
|
| 2170 |
+
"rerank": "none",
|
| 2171 |
+
"channels": [
|
| 2172 |
+
"passages"
|
| 2173 |
+
],
|
| 2174 |
+
"rank_pool": false,
|
| 2175 |
+
"rank_pool_ce": false,
|
| 2176 |
+
"n_queries": 74,
|
| 2177 |
+
"k": 50,
|
| 2178 |
+
"ndcg@10": 0.4861505982058926,
|
| 2179 |
+
"ndcg@20": 0.5412686017716648,
|
| 2180 |
+
"ndcg@50": 0.62608241624179,
|
| 2181 |
+
"ndcg": 0.62608241624179,
|
| 2182 |
+
"recall@10": 0.37673766109430706,
|
| 2183 |
+
"recall@20": 0.5919104321112831,
|
| 2184 |
+
"recall@50": 0.8324528690465655,
|
| 2185 |
+
"mrr": 0.7604021879021879,
|
| 2186 |
+
"pool_coverage": null,
|
| 2187 |
+
"p50_retrieve_ms": 61.432,
|
| 2188 |
+
"p95_retrieve_ms": 92.56864999999999,
|
| 2189 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 2190 |
+
"report_path": "runs/search-esci-74-cc-a05/report.json",
|
| 2191 |
+
"recorded_at": "2026-08-19T20:18:41.196658+00:00"
|
| 2192 |
+
},
|
| 2193 |
+
{
|
| 2194 |
+
"run_id": "search-esci-74-cc-a07",
|
| 2195 |
+
"brain": "searchbenchesci74",
|
| 2196 |
+
"dataset": "search_esci_74.jsonl",
|
| 2197 |
+
"fusion": "cc",
|
| 2198 |
+
"rerank": "none",
|
| 2199 |
+
"channels": [
|
| 2200 |
+
"passages"
|
| 2201 |
+
],
|
| 2202 |
+
"rank_pool": false,
|
| 2203 |
+
"rank_pool_ce": false,
|
| 2204 |
+
"n_queries": 74,
|
| 2205 |
+
"k": 50,
|
| 2206 |
+
"ndcg@10": 0.4925393217389386,
|
| 2207 |
+
"ndcg@20": 0.5491258060245199,
|
| 2208 |
+
"ndcg@50": 0.6321844176410597,
|
| 2209 |
+
"ndcg": 0.6321844176410597,
|
| 2210 |
+
"recall@10": 0.3780890124456584,
|
| 2211 |
+
"recall@20": 0.5964401922862047,
|
| 2212 |
+
"recall@50": 0.833954370548067,
|
| 2213 |
+
"mrr": 0.7630083655083656,
|
| 2214 |
+
"pool_coverage": null,
|
| 2215 |
+
"p50_retrieve_ms": 60.5155,
|
| 2216 |
+
"p95_retrieve_ms": 89.04719999999998,
|
| 2217 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 2218 |
+
"report_path": "runs/search-esci-74-cc-a07/report.json",
|
| 2219 |
+
"recorded_at": "2026-08-19T20:19:05.271923+00:00"
|
| 2220 |
+
},
|
| 2221 |
+
{
|
| 2222 |
+
"run_id": "search-esci-74-literal-fill-k50",
|
| 2223 |
+
"brain": "searchbenchesci74",
|
| 2224 |
+
"dataset": "search_esci_74.jsonl",
|
| 2225 |
+
"fusion": "rrf",
|
| 2226 |
+
"rerank": "none",
|
| 2227 |
+
"channels": [
|
| 2228 |
+
"passages"
|
| 2229 |
+
],
|
| 2230 |
+
"rank_pool": false,
|
| 2231 |
+
"rank_pool_ce": false,
|
| 2232 |
+
"n_queries": 74,
|
| 2233 |
+
"k": 50,
|
| 2234 |
+
"ndcg@10": 0.4998984443226263,
|
| 2235 |
+
"ndcg@20": 0.45027838324634667,
|
| 2236 |
+
"ndcg@50": 0.5436008239474691,
|
| 2237 |
+
"ndcg": 0.5436008239474691,
|
| 2238 |
+
"recall@10": 0.378732513089159,
|
| 2239 |
+
"recall@20": 0.3929861145019264,
|
| 2240 |
+
"recall@50": 0.6549969089908488,
|
| 2241 |
+
"mrr": 0.7639237451737452,
|
| 2242 |
+
"pool_coverage": null,
|
| 2243 |
+
"p50_retrieve_ms": 49.019999999999996,
|
| 2244 |
+
"p95_retrieve_ms": 74.27314999999999,
|
| 2245 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 2246 |
+
"report_path": "runs/search-esci-74-literal-fill-k50/report.json",
|
| 2247 |
+
"recorded_at": "2026-08-19T20:20:15.131585+00:00"
|
| 2248 |
+
},
|
| 2249 |
+
{
|
| 2250 |
+
"run_id": "search-esci-74-ltr-head-k50",
|
| 2251 |
+
"brain": "harness-local-ltr",
|
| 2252 |
+
"dataset": "search_esci_74.jsonl",
|
| 2253 |
+
"fusion": "none",
|
| 2254 |
+
"rerank": "ltr-pairwise",
|
| 2255 |
+
"channels": [
|
| 2256 |
+
"harness-ltr-head"
|
| 2257 |
+
],
|
| 2258 |
+
"rank_pool": false,
|
| 2259 |
+
"rank_pool_ce": false,
|
| 2260 |
+
"n_queries": 74,
|
| 2261 |
+
"k": 50,
|
| 2262 |
+
"ndcg@10": 0.5146015046757452,
|
| 2263 |
+
"ndcg@20": 0.5567887112159354,
|
| 2264 |
+
"ndcg@50": 0.6406685515140392,
|
| 2265 |
+
"ndcg": 0.6406685515140392,
|
| 2266 |
+
"recall@10": 0.384165323643092,
|
| 2267 |
+
"recall@20": 0.5830174861607277,
|
| 2268 |
+
"recall@50": 0.833954370548067,
|
| 2269 |
+
"mrr": 0.7525579150579151,
|
| 2270 |
+
"pool_coverage": null,
|
| 2271 |
+
"p50_retrieve_ms": 0.0,
|
| 2272 |
+
"p95_retrieve_ms": 0.0,
|
| 2273 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 2274 |
+
"report_path": "runs/search-esci-74-ltr-head-k50/report.json",
|
| 2275 |
+
"recorded_at": "2026-08-19T21:09:47.411344+00:00"
|
| 2276 |
+
},
|
| 2277 |
+
{
|
| 2278 |
+
"run_id": "search-esci-74-ltr-cefeat-k50",
|
| 2279 |
+
"brain": "harness-local-ltr",
|
| 2280 |
+
"dataset": "search_esci_74.jsonl",
|
| 2281 |
+
"fusion": "none",
|
| 2282 |
+
"rerank": "ltr-pairwise",
|
| 2283 |
+
"channels": [
|
| 2284 |
+
"harness-ltr-head"
|
| 2285 |
+
],
|
| 2286 |
+
"rank_pool": false,
|
| 2287 |
+
"rank_pool_ce": false,
|
| 2288 |
+
"n_queries": 74,
|
| 2289 |
+
"k": 50,
|
| 2290 |
+
"ndcg@10": 0.5242780297400812,
|
| 2291 |
+
"ndcg@20": 0.5655195290162985,
|
| 2292 |
+
"ndcg@50": 0.6456032400547804,
|
| 2293 |
+
"ndcg": 0.6456032400547804,
|
| 2294 |
+
"recall@10": 0.38343544674019964,
|
| 2295 |
+
"recall@20": 0.5687827599719494,
|
| 2296 |
+
"recall@50": 0.833954370548067,
|
| 2297 |
+
"mrr": 0.7792567567567568,
|
| 2298 |
+
"pool_coverage": null,
|
| 2299 |
+
"p50_retrieve_ms": 0.0,
|
| 2300 |
+
"p95_retrieve_ms": 0.0,
|
| 2301 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 2302 |
+
"report_path": "runs/search-esci-74-ltr-cefeat-k50/report.json",
|
| 2303 |
+
"recorded_at": "2026-08-20T07:14:39.761450+00:00"
|
| 2304 |
+
},
|
| 2305 |
+
{
|
| 2306 |
+
"run_id": "search-esci-74-ltr-deberta-k50",
|
| 2307 |
+
"brain": "harness-local-ltr",
|
| 2308 |
+
"dataset": "search_esci_74.jsonl",
|
| 2309 |
+
"fusion": "none",
|
| 2310 |
+
"rerank": "ltr-pairwise",
|
| 2311 |
+
"channels": [
|
| 2312 |
+
"harness-ltr-head"
|
| 2313 |
+
],
|
| 2314 |
+
"rank_pool": false,
|
| 2315 |
+
"rank_pool_ce": false,
|
| 2316 |
+
"n_queries": 74,
|
| 2317 |
+
"k": 50,
|
| 2318 |
+
"ndcg@10": 0.5420964444237363,
|
| 2319 |
+
"ndcg@20": 0.580013195834899,
|
| 2320 |
+
"ndcg@50": 0.6509103805030941,
|
| 2321 |
+
"ndcg": 0.6509103805030941,
|
| 2322 |
+
"recall@10": 0.3872531182148088,
|
| 2323 |
+
"recall@20": 0.5863055009035371,
|
| 2324 |
+
"recall@50": 0.833954370548067,
|
| 2325 |
+
"mrr": 0.7744787644787645,
|
| 2326 |
+
"pool_coverage": null,
|
| 2327 |
+
"p50_retrieve_ms": 0.0,
|
| 2328 |
+
"p95_retrieve_ms": 0.0,
|
| 2329 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 2330 |
+
"report_path": "runs/search-esci-74-ltr-deberta-k50/report.json",
|
| 2331 |
+
"recorded_at": "2026-08-20T07:43:07.017767+00:00"
|
| 2332 |
+
},
|
| 2333 |
+
{
|
| 2334 |
+
"run_id": "search-esci-74-ltr-deberta-lgbm-k50",
|
| 2335 |
+
"brain": "harness-local-ltr",
|
| 2336 |
+
"dataset": "search_esci_74.jsonl",
|
| 2337 |
+
"fusion": "none",
|
| 2338 |
+
"rerank": "ltr-lightgbm",
|
| 2339 |
+
"channels": [
|
| 2340 |
+
"harness-ltr-head"
|
| 2341 |
+
],
|
| 2342 |
+
"rank_pool": false,
|
| 2343 |
+
"rank_pool_ce": false,
|
| 2344 |
+
"n_queries": 74,
|
| 2345 |
+
"k": 50,
|
| 2346 |
+
"ndcg@10": 0.5159432058815566,
|
| 2347 |
+
"ndcg@20": 0.5658120484311862,
|
| 2348 |
+
"ndcg@50": 0.6415336394479464,
|
| 2349 |
+
"ndcg": 0.6415336394479464,
|
| 2350 |
+
"recall@10": 0.37878019077485103,
|
| 2351 |
+
"recall@20": 0.5918308018978374,
|
| 2352 |
+
"recall@50": 0.833954370548067,
|
| 2353 |
+
"mrr": 0.7098312215959275,
|
| 2354 |
+
"pool_coverage": null,
|
| 2355 |
+
"p50_retrieve_ms": 0.0,
|
| 2356 |
+
"p95_retrieve_ms": 0.0,
|
| 2357 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 2358 |
+
"report_path": "runs/search-esci-74-ltr-deberta-lgbm-k50/report.json",
|
| 2359 |
+
"recorded_at": "2026-08-20T07:48:40.794647+00:00"
|
| 2360 |
+
},
|
| 2361 |
+
{
|
| 2362 |
+
"run_id": "search-esci-ltr200-passages-k50",
|
| 2363 |
+
"brain": "searchbenchesciltr2",
|
| 2364 |
+
"dataset": "search_esci_ltr200.jsonl",
|
| 2365 |
+
"fusion": "rrf",
|
| 2366 |
+
"rerank": "none",
|
| 2367 |
+
"channels": [
|
| 2368 |
+
"passages"
|
| 2369 |
+
],
|
| 2370 |
+
"rank_pool": false,
|
| 2371 |
+
"rank_pool_ce": false,
|
| 2372 |
+
"n_queries": 170,
|
| 2373 |
+
"k": 50,
|
| 2374 |
+
"ndcg@10": 0.38688497520690734,
|
| 2375 |
+
"ndcg@20": 0.4428785087667844,
|
| 2376 |
+
"ndcg@50": 0.5191838961643084,
|
| 2377 |
+
"ndcg": 0.5191838961643084,
|
| 2378 |
+
"recall@10": 0.3668275379939918,
|
| 2379 |
+
"recall@20": 0.5506847180507305,
|
| 2380 |
+
"recall@50": 0.7435914662537475,
|
| 2381 |
+
"mrr": 0.6599425680048517,
|
| 2382 |
+
"pool_coverage": null,
|
| 2383 |
+
"p50_retrieve_ms": 76.07849999999999,
|
| 2384 |
+
"p95_retrieve_ms": 152.37659999999983,
|
| 2385 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 2386 |
+
"report_path": "runs/search-esci-ltr200-passages-k50/report.json",
|
| 2387 |
+
"recorded_at": "2026-08-20T10:45:30.571169+00:00"
|
| 2388 |
+
},
|
| 2389 |
+
{
|
| 2390 |
+
"run_id": "search-esci-74-ltr-deberta-train200",
|
| 2391 |
+
"brain": "harness-local-ltr",
|
| 2392 |
+
"dataset": "search_esci_74.jsonl",
|
| 2393 |
+
"fusion": "none",
|
| 2394 |
+
"rerank": "ltr-pairwise",
|
| 2395 |
+
"channels": [
|
| 2396 |
+
"harness-ltr-head"
|
| 2397 |
+
],
|
| 2398 |
+
"rank_pool": false,
|
| 2399 |
+
"rank_pool_ce": false,
|
| 2400 |
+
"n_queries": 74,
|
| 2401 |
+
"k": 50,
|
| 2402 |
+
"ndcg@10": 0.5436096369070966,
|
| 2403 |
+
"ndcg@20": 0.5825504636416924,
|
| 2404 |
+
"ndcg@50": 0.6525856561608427,
|
| 2405 |
+
"ndcg": 0.6525856561608427,
|
| 2406 |
+
"recall@10": 0.3911684808472767,
|
| 2407 |
+
"recall@20": 0.5850220289937974,
|
| 2408 |
+
"recall@50": 0.833954370548067,
|
| 2409 |
+
"mrr": 0.7621138996138996,
|
| 2410 |
+
"pool_coverage": null,
|
| 2411 |
+
"p50_retrieve_ms": 0.0,
|
| 2412 |
+
"p95_retrieve_ms": 0.0,
|
| 2413 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 2414 |
+
"report_path": "runs/search-esci-74-ltr-deberta-train200/report.json",
|
| 2415 |
+
"recorded_at": "2026-08-20T10:57:34.082897+00:00"
|
| 2416 |
+
},
|
| 2417 |
+
{
|
| 2418 |
+
"run_id": "search-esci-74-ltr-deberta-lgbm-train200",
|
| 2419 |
+
"brain": "harness-local-ltr",
|
| 2420 |
+
"dataset": "search_esci_74.jsonl",
|
| 2421 |
+
"fusion": "none",
|
| 2422 |
+
"rerank": "ltr-lightgbm",
|
| 2423 |
+
"channels": [
|
| 2424 |
+
"harness-ltr-head"
|
| 2425 |
+
],
|
| 2426 |
+
"rank_pool": false,
|
| 2427 |
+
"rank_pool_ce": false,
|
| 2428 |
+
"n_queries": 74,
|
| 2429 |
+
"k": 50,
|
| 2430 |
+
"ndcg@10": 0.5333470812074246,
|
| 2431 |
+
"ndcg@20": 0.5739148370361776,
|
| 2432 |
+
"ndcg@50": 0.6521013344978932,
|
| 2433 |
+
"ndcg": 0.6521013344978932,
|
| 2434 |
+
"recall@10": 0.38938710235182716,
|
| 2435 |
+
"recall@20": 0.5909398273461974,
|
| 2436 |
+
"recall@50": 0.833954370548067,
|
| 2437 |
+
"mrr": 0.7517797970384177,
|
| 2438 |
+
"pool_coverage": null,
|
| 2439 |
+
"p50_retrieve_ms": 0.0,
|
| 2440 |
+
"p95_retrieve_ms": 0.0,
|
| 2441 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 2442 |
+
"report_path": "runs/search-esci-74-ltr-deberta-lgbm-train200/report.json",
|
| 2443 |
+
"recorded_at": "2026-08-20T11:33:18.933082+00:00"
|
| 2444 |
+
},
|
| 2445 |
+
{
|
| 2446 |
+
"run_id": "search-esci-74-ltr-deberta-hybrid170",
|
| 2447 |
+
"brain": "harness-local-ltr",
|
| 2448 |
+
"dataset": "search_esci_74.jsonl",
|
| 2449 |
+
"fusion": "none",
|
| 2450 |
+
"rerank": "ltr-pairwise",
|
| 2451 |
+
"channels": [
|
| 2452 |
+
"harness-ltr-head"
|
| 2453 |
+
],
|
| 2454 |
+
"rank_pool": false,
|
| 2455 |
+
"rank_pool_ce": false,
|
| 2456 |
+
"n_queries": 74,
|
| 2457 |
+
"k": 50,
|
| 2458 |
+
"ndcg@10": 0.5304449022081842,
|
| 2459 |
+
"ndcg@20": 0.5753013243082012,
|
| 2460 |
+
"ndcg@50": 0.6452218148321592,
|
| 2461 |
+
"ndcg": 0.6452218148321592,
|
| 2462 |
+
"recall@10": 0.38518108116293304,
|
| 2463 |
+
"recall@20": 0.5882930387092911,
|
| 2464 |
+
"recall@50": 0.833954370548067,
|
| 2465 |
+
"mrr": 0.7646718146718147,
|
| 2466 |
+
"pool_coverage": null,
|
| 2467 |
+
"p50_retrieve_ms": 0.0,
|
| 2468 |
+
"p95_retrieve_ms": 0.0,
|
| 2469 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 2470 |
+
"report_path": "runs/search-esci-74-ltr-deberta-hybrid170/report.json",
|
| 2471 |
+
"recorded_at": "2026-08-20T11:57:34.067863+00:00"
|
| 2472 |
+
},
|
| 2473 |
+
{
|
| 2474 |
+
"run_id": "search-wands-passages-k50",
|
| 2475 |
+
"brain": "searchbenchwands",
|
| 2476 |
+
"dataset": "search_wands.jsonl",
|
| 2477 |
+
"fusion": "rrf",
|
| 2478 |
+
"rerank": "none",
|
| 2479 |
+
"channels": [
|
| 2480 |
+
"passages"
|
| 2481 |
+
],
|
| 2482 |
+
"rank_pool": false,
|
| 2483 |
+
"rank_pool_ce": false,
|
| 2484 |
+
"n_queries": 66,
|
| 2485 |
+
"k": 50,
|
| 2486 |
+
"ndcg@10": 0.823176135584089,
|
| 2487 |
+
"ndcg@20": 0.8120944688355884,
|
| 2488 |
+
"ndcg@50": 0.8223686140560469,
|
| 2489 |
+
"ndcg": 0.8223686140560469,
|
| 2490 |
+
"recall@10": 0.26880378393536286,
|
| 2491 |
+
"recall@20": 0.46518737110842373,
|
| 2492 |
+
"recall@50": 0.8365552576078893,
|
| 2493 |
+
"mrr": 0.9245931537598204,
|
| 2494 |
+
"pool_coverage": null,
|
| 2495 |
+
"p50_retrieve_ms": 86.595,
|
| 2496 |
+
"p95_retrieve_ms": 96.93674999999999,
|
| 2497 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 2498 |
+
"report_path": "runs/search-wands-passages-k50/report.json",
|
| 2499 |
+
"recorded_at": "2026-08-20T13:17:40.353988+00:00"
|
| 2500 |
+
},
|
| 2501 |
+
{
|
| 2502 |
+
"run_id": "search-wandsgraph-communities-k50",
|
| 2503 |
+
"brain": "searchbenchwandsgraph",
|
| 2504 |
+
"dataset": "search_wands.jsonl",
|
| 2505 |
+
"fusion": "rrf",
|
| 2506 |
+
"rerank": "none",
|
| 2507 |
+
"channels": [
|
| 2508 |
+
"communities"
|
| 2509 |
+
],
|
| 2510 |
+
"rank_pool": false,
|
| 2511 |
+
"rank_pool_ce": false,
|
| 2512 |
+
"n_queries": 66,
|
| 2513 |
+
"k": 50,
|
| 2514 |
+
"ndcg@10": 0.4094084735740818,
|
| 2515 |
+
"ndcg@20": 0.36455789387786824,
|
| 2516 |
+
"ndcg@50": 0.325224291157982,
|
| 2517 |
+
"ndcg": 0.4570937299953597,
|
| 2518 |
+
"recall@10": 0.14824773180036338,
|
| 2519 |
+
"recall@20": 0.20624528979792137,
|
| 2520 |
+
"recall@50": 0.28701452056715215,
|
| 2521 |
+
"mrr": 0.6737914862914863,
|
| 2522 |
+
"pool_coverage": null,
|
| 2523 |
+
"p50_retrieve_ms": 5609.237499999999,
|
| 2524 |
+
"p95_retrieve_ms": 11856.0285,
|
| 2525 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 2526 |
+
"report_path": "runs/search-wandsgraph-communities-k50/report.json",
|
| 2527 |
+
"recorded_at": "2026-08-21T03:12:00.249304+00:00",
|
| 2528 |
+
"protocol": "architecture demo: isolated catalog graph on searchbenchwandsgraph; not a quality-default claim vs frozen WANDS passages 0.823 or ESCI 0.500",
|
| 2529 |
+
"claim": "architecture-demo"
|
| 2530 |
+
},
|
| 2531 |
+
{
|
| 2532 |
+
"run_id": "search-wandsgraph-passages-k50",
|
| 2533 |
+
"brain": "searchbenchwandsgraph",
|
| 2534 |
+
"dataset": "search_wands.jsonl",
|
| 2535 |
+
"fusion": "rrf",
|
| 2536 |
+
"rerank": "none",
|
| 2537 |
+
"channels": [
|
| 2538 |
+
"passages"
|
| 2539 |
+
],
|
| 2540 |
+
"rank_pool": false,
|
| 2541 |
+
"rank_pool_ce": false,
|
| 2542 |
+
"n_queries": 66,
|
| 2543 |
+
"k": 50,
|
| 2544 |
+
"ndcg@10": 0.8230998576010691,
|
| 2545 |
+
"ndcg@20": 0.812056754200934,
|
| 2546 |
+
"ndcg@50": 0.8223486981324697,
|
| 2547 |
+
"ndcg": 0.8223486981324697,
|
| 2548 |
+
"recall@10": 0.26880378393536286,
|
| 2549 |
+
"recall@20": 0.46518737110842373,
|
| 2550 |
+
"recall@50": 0.8365552576078893,
|
| 2551 |
+
"mrr": 0.9245931537598204,
|
| 2552 |
+
"pool_coverage": null,
|
| 2553 |
+
"p50_retrieve_ms": 98.604,
|
| 2554 |
+
"p95_retrieve_ms": 115.12925,
|
| 2555 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 2556 |
+
"report_path": "runs/search-wandsgraph-passages-k50/report.json",
|
| 2557 |
+
"recorded_at": "2026-08-21T03:12:36.455586+00:00",
|
| 2558 |
+
"protocol": "architecture demo: isolated catalog graph on searchbenchwandsgraph; not a quality-default claim vs frozen WANDS passages 0.823 or ESCI 0.500",
|
| 2559 |
+
"claim": "architecture-demo"
|
| 2560 |
+
},
|
| 2561 |
+
{
|
| 2562 |
+
"run_id": "search-wandsgraph-passages-nodeid-k50",
|
| 2563 |
+
"brain": "searchbenchwandsgraph",
|
| 2564 |
+
"dataset": "search_wands.jsonl",
|
| 2565 |
+
"fusion": "rrf",
|
| 2566 |
+
"rerank": "none",
|
| 2567 |
+
"channels": [
|
| 2568 |
+
"passages"
|
| 2569 |
+
],
|
| 2570 |
+
"rank_pool": false,
|
| 2571 |
+
"rank_pool_ce": false,
|
| 2572 |
+
"n_queries": 66,
|
| 2573 |
+
"k": 50,
|
| 2574 |
+
"ndcg@10": 0.8230998576010691,
|
| 2575 |
+
"ndcg@20": 0.812056754200934,
|
| 2576 |
+
"ndcg@50": 0.8223486981324697,
|
| 2577 |
+
"ndcg": 0.8223486981324697,
|
| 2578 |
+
"recall@10": 0.26880378393536286,
|
| 2579 |
+
"recall@20": 0.46518737110842373,
|
| 2580 |
+
"recall@50": 0.8365552576078893,
|
| 2581 |
+
"mrr": 0.9245931537598204,
|
| 2582 |
+
"pool_coverage": null,
|
| 2583 |
+
"p50_retrieve_ms": 84.0175,
|
| 2584 |
+
"p95_retrieve_ms": 105.24199999999999,
|
| 2585 |
+
"git_sha": "f42c4a8eaadb3c0cc79f78c34b0e1a70534bee10",
|
| 2586 |
+
"report_path": "runs/search-wandsgraph-passages-nodeid-k50/report.json",
|
| 2587 |
+
"recorded_at": "2026-08-21T07:53:39.311033+00:00",
|
| 2588 |
+
"protocol": "architecture demo: isolated catalog graph on searchbenchwandsgraph; not a quality-default claim vs frozen WANDS passages 0.823 or ESCI 0.500",
|
| 2589 |
+
"claim": "architecture-demo"
|
| 2590 |
+
}
|
| 2591 |
+
]
|
| 2592 |
+
}
|
| 2593 |
+
}
|
| 2594 |
+
}
|
benchmarks/beam.sh
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -euo pipefail
|
| 3 |
+
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
| 4 |
+
PYTHON="${ROOT}/.venv/bin/python"
|
| 5 |
+
if [[ ! -x "$PYTHON" ]]; then
|
| 6 |
+
echo "Missing ${PYTHON}. Create it with:"
|
| 7 |
+
echo " cd benchmarks && python3 -m venv .venv && .venv/bin/pip install -r requirements.txt"
|
| 8 |
+
exit 1
|
| 9 |
+
fi
|
| 10 |
+
exec "$PYTHON" -m beam "$@"
|
benchmarks/beam/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""BEAM long-term memory benchmark harness for BrainAPI."""
|
benchmarks/beam/__main__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from beam.cli import main
|
| 2 |
+
|
| 3 |
+
if __name__ == "__main__":
|
| 4 |
+
raise SystemExit(main())
|
benchmarks/beam/answer.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import time
|
| 4 |
+
from dataclasses import dataclass
|
| 5 |
+
from typing import Any
|
| 6 |
+
|
| 7 |
+
from openai import AzureOpenAI, OpenAI
|
| 8 |
+
|
| 9 |
+
from beam.config import Settings
|
| 10 |
+
from beam.prompts import build_answer_messages
|
| 11 |
+
from beam.sota import (
|
| 12 |
+
majority_vote,
|
| 13 |
+
maybe_force_hard_abstain,
|
| 14 |
+
maybe_fix_contradiction_answer,
|
| 15 |
+
strip_leading_abstain,
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
@dataclass
|
| 20 |
+
class AnswerResult:
|
| 21 |
+
answer: str
|
| 22 |
+
model: str
|
| 23 |
+
latency_ms: float
|
| 24 |
+
prompt_tokens: int | None
|
| 25 |
+
completion_tokens: int | None
|
| 26 |
+
total_tokens: int | None
|
| 27 |
+
sc_samples_used: int = 1
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _make_openai_client(settings: Settings) -> OpenAI | AzureOpenAI:
|
| 31 |
+
settings.require_llm()
|
| 32 |
+
if settings.answer_azure_endpoint:
|
| 33 |
+
return AzureOpenAI(
|
| 34 |
+
api_key=settings.openai_api_key,
|
| 35 |
+
azure_endpoint=settings.answer_azure_endpoint,
|
| 36 |
+
api_version=settings.answer_azure_api_version,
|
| 37 |
+
)
|
| 38 |
+
kwargs: dict[str, Any] = {"api_key": settings.openai_api_key}
|
| 39 |
+
if settings.llm_base_url:
|
| 40 |
+
kwargs["base_url"] = settings.llm_base_url
|
| 41 |
+
return OpenAI(**kwargs)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _one_completion(
|
| 45 |
+
client: OpenAI | AzureOpenAI,
|
| 46 |
+
*,
|
| 47 |
+
model_name: str,
|
| 48 |
+
messages: list[dict[str, str]],
|
| 49 |
+
temperature: float,
|
| 50 |
+
) -> tuple[str, Any]:
|
| 51 |
+
response = client.chat.completions.create(
|
| 52 |
+
model=model_name,
|
| 53 |
+
messages=messages,
|
| 54 |
+
temperature=temperature,
|
| 55 |
+
)
|
| 56 |
+
content = (response.choices[0].message.content or "").strip()
|
| 57 |
+
return content, response
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def _accumulate_usage(
|
| 61 |
+
response: Any,
|
| 62 |
+
prompt_tokens: int,
|
| 63 |
+
completion_tokens: int,
|
| 64 |
+
total_tokens: int,
|
| 65 |
+
) -> tuple[int, int, int]:
|
| 66 |
+
if response.usage:
|
| 67 |
+
prompt_tokens += getattr(response.usage, "prompt_tokens", 0) or 0
|
| 68 |
+
completion_tokens += getattr(response.usage, "completion_tokens", 0) or 0
|
| 69 |
+
total_tokens += getattr(response.usage, "total_tokens", 0) or 0
|
| 70 |
+
return prompt_tokens, completion_tokens, total_tokens
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def answer_question(
|
| 74 |
+
settings: Settings,
|
| 75 |
+
question: str,
|
| 76 |
+
context: dict[str, Any],
|
| 77 |
+
*,
|
| 78 |
+
model: str | None = None,
|
| 79 |
+
ability: str | None = None,
|
| 80 |
+
sc_samples: int | None = None,
|
| 81 |
+
sc_temperature: float | None = None,
|
| 82 |
+
) -> AnswerResult:
|
| 83 |
+
client = _make_openai_client(settings)
|
| 84 |
+
model_name = model or settings.answer_model
|
| 85 |
+
messages = build_answer_messages(question, context, ability=ability)
|
| 86 |
+
n = max(1, int(sc_samples if sc_samples is not None else settings.sc_samples))
|
| 87 |
+
temperature = (
|
| 88 |
+
float(sc_temperature)
|
| 89 |
+
if sc_temperature is not None
|
| 90 |
+
else (settings.sc_temperature if n > 1 else 0.0)
|
| 91 |
+
)
|
| 92 |
+
started = time.perf_counter()
|
| 93 |
+
prompt_tokens = 0
|
| 94 |
+
completion_tokens = 0
|
| 95 |
+
total_tokens = 0
|
| 96 |
+
samples: list[str] = []
|
| 97 |
+
last_response: Any = None
|
| 98 |
+
samples_used = n
|
| 99 |
+
|
| 100 |
+
for _ in range(n):
|
| 101 |
+
content, response = _one_completion(
|
| 102 |
+
client,
|
| 103 |
+
model_name=model_name,
|
| 104 |
+
messages=messages,
|
| 105 |
+
temperature=0.0 if n == 1 else temperature,
|
| 106 |
+
)
|
| 107 |
+
last_response = response
|
| 108 |
+
prompt_tokens, completion_tokens, total_tokens = _accumulate_usage(
|
| 109 |
+
response, prompt_tokens, completion_tokens, total_tokens
|
| 110 |
+
)
|
| 111 |
+
if content:
|
| 112 |
+
samples.append(content)
|
| 113 |
+
|
| 114 |
+
content = majority_vote(samples) if samples else ""
|
| 115 |
+
content = strip_leading_abstain(content)
|
| 116 |
+
content = maybe_force_hard_abstain(ability, question, content, context)
|
| 117 |
+
content = maybe_fix_contradiction_answer(ability, question, content)
|
| 118 |
+
latency_ms = (time.perf_counter() - started) * 1000
|
| 119 |
+
return AnswerResult(
|
| 120 |
+
answer=content,
|
| 121 |
+
model=getattr(last_response, "model", None) or model_name,
|
| 122 |
+
latency_ms=latency_ms,
|
| 123 |
+
prompt_tokens=prompt_tokens or None,
|
| 124 |
+
completion_tokens=completion_tokens or None,
|
| 125 |
+
total_tokens=total_tokens or None,
|
| 126 |
+
sc_samples_used=samples_used,
|
| 127 |
+
)
|
benchmarks/beam/cli.py
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
from rich.console import Console
|
| 7 |
+
from rich.table import Table
|
| 8 |
+
|
| 9 |
+
from beam.client import BrainAPIClient
|
| 10 |
+
from beam.config import ABILITY_NAMES, CHAT_SIZES, Settings
|
| 11 |
+
from beam.dataset import (
|
| 12 |
+
brain_id_for,
|
| 13 |
+
dataset_stats,
|
| 14 |
+
download_and_normalize,
|
| 15 |
+
iter_ingest_units,
|
| 16 |
+
iter_probing_jobs,
|
| 17 |
+
list_local_samples,
|
| 18 |
+
load_sample,
|
| 19 |
+
resolve_samples,
|
| 20 |
+
)
|
| 21 |
+
from beam.evaluate import (
|
| 22 |
+
DEFAULT_HISTORICAL_LIMIT,
|
| 23 |
+
DEFAULT_MAX_FACTS,
|
| 24 |
+
DEFAULT_MAX_PASSAGES,
|
| 25 |
+
evaluate_samples,
|
| 26 |
+
)
|
| 27 |
+
from beam.ingest import ensure_run_dir, ingest_samples, write_manifest
|
| 28 |
+
from beam.judge import parse_json_response
|
| 29 |
+
from beam.metrics import selftest_metrics
|
| 30 |
+
from beam.prompts import build_rubric_judge_prompt
|
| 31 |
+
from beam.provenance import build_provenance
|
| 32 |
+
from beam.report import print_report_table, write_report
|
| 33 |
+
|
| 34 |
+
console = Console()
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _parse_abilities(raw: str | None) -> set[str] | None:
|
| 38 |
+
if not raw:
|
| 39 |
+
return None
|
| 40 |
+
return {part.strip() for part in raw.split(",") if part.strip()}
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def cmd_download(args: argparse.Namespace, settings: Settings) -> int:
|
| 44 |
+
sizes = [args.size] if args.size else list(CHAT_SIZES)
|
| 45 |
+
path = download_and_normalize(settings, sizes=sizes, force=args.force)
|
| 46 |
+
console.print(f"[green]Dataset ready:[/green] {path}")
|
| 47 |
+
for size in sizes:
|
| 48 |
+
n = len(list_local_samples(settings, size=size))
|
| 49 |
+
console.print(f" {size}: {n} conversations")
|
| 50 |
+
return 0
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def cmd_dataset_stats(args: argparse.Namespace, settings: Settings) -> int:
|
| 54 |
+
rows = dataset_stats(settings)
|
| 55 |
+
if not rows:
|
| 56 |
+
console.print(
|
| 57 |
+
"[yellow]No local BEAM data. Run: python -m beam download[/yellow]"
|
| 58 |
+
)
|
| 59 |
+
return 1
|
| 60 |
+
table = Table(title="BEAM dataset")
|
| 61 |
+
for col in ("sample_id", "brain_id", "size", "batches", "turns", "questions"):
|
| 62 |
+
table.add_column(col)
|
| 63 |
+
for row in rows:
|
| 64 |
+
if args.size and row["size"] != args.size:
|
| 65 |
+
continue
|
| 66 |
+
table.add_row(
|
| 67 |
+
str(row["sample_id"]),
|
| 68 |
+
str(row["brain_id"]),
|
| 69 |
+
str(row["size"]),
|
| 70 |
+
str(row["batches"]),
|
| 71 |
+
str(row["turns"]),
|
| 72 |
+
str(row["questions"]),
|
| 73 |
+
)
|
| 74 |
+
console.print(table)
|
| 75 |
+
assert brain_id_for("100K", "1") == "beam100k1"
|
| 76 |
+
assert brain_id_for("10M", "1") == "beam10m1"
|
| 77 |
+
console.print("[dim]brain_id_for('100K', '1') == 'beam100k1' OK[/dim]")
|
| 78 |
+
console.print("[dim]brain_id_for('10M', '1') == 'beam10m1' OK[/dim]")
|
| 79 |
+
return 0
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def cmd_smoke(args: argparse.Namespace, settings: Settings) -> int:
|
| 83 |
+
sample_id = args.sample if "/" in args.sample else f"{args.size}/{args.sample}"
|
| 84 |
+
try:
|
| 85 |
+
sample = load_sample(settings, sample_id)
|
| 86 |
+
except FileNotFoundError:
|
| 87 |
+
console.print(f"[yellow]Sample missing; downloading {args.size}…[/yellow]")
|
| 88 |
+
download_and_normalize(settings, sizes=[args.size])
|
| 89 |
+
sample = load_sample(settings, sample_id)
|
| 90 |
+
|
| 91 |
+
brain_id = args.brain or str(sample["brain_id"])
|
| 92 |
+
units = iter_ingest_units(sample, limit_turns=max(1, args.limit_turns))
|
| 93 |
+
if not units:
|
| 94 |
+
console.print("[red]No turns to ingest[/red]")
|
| 95 |
+
return 1
|
| 96 |
+
|
| 97 |
+
with BrainAPIClient(settings) as client:
|
| 98 |
+
for unit in units:
|
| 99 |
+
submitted = client.ingest_text(
|
| 100 |
+
unit["text"],
|
| 101 |
+
brain_id,
|
| 102 |
+
source_timestamp=unit.get("source_timestamp"),
|
| 103 |
+
)
|
| 104 |
+
task_id = (submitted.data or {}).get("task_id")
|
| 105 |
+
console.print(f"Queued ingest task_id={task_id} unit={unit['unit_id']}")
|
| 106 |
+
waited = client.wait_for_task(task_id, brain_id, timeout_s=args.timeout)
|
| 107 |
+
console.print(f"Task status: {(waited.data or {}).get('status')}")
|
| 108 |
+
|
| 109 |
+
jobs = iter_probing_jobs(sample, limit=1)
|
| 110 |
+
question = jobs[0]["question"] if jobs else "What was discussed?"
|
| 111 |
+
ctx = client.retrieve_context(question, brain_id)
|
| 112 |
+
text_context = (ctx.data or {}).get("text_context") or ""
|
| 113 |
+
console.print(
|
| 114 |
+
f"text_context ({len(text_context)} chars): {text_context[:400]}"
|
| 115 |
+
)
|
| 116 |
+
if not text_context.strip():
|
| 117 |
+
console.print("[yellow]Warning: empty text_context[/yellow]")
|
| 118 |
+
return 1
|
| 119 |
+
console.print("[green]Smoke test passed[/green]")
|
| 120 |
+
return 0
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def cmd_ingest(args: argparse.Namespace, settings: Settings) -> int:
|
| 124 |
+
samples = resolve_samples(
|
| 125 |
+
settings, size=args.size, sample_ids=args.sample or None
|
| 126 |
+
)
|
| 127 |
+
run_id, run_dir = ensure_run_dir(settings, args.run)
|
| 128 |
+
write_manifest(
|
| 129 |
+
run_dir,
|
| 130 |
+
{
|
| 131 |
+
"run_id": run_id,
|
| 132 |
+
"command": "ingest",
|
| 133 |
+
"suite": "beam",
|
| 134 |
+
"size": args.size,
|
| 135 |
+
"samples": [s["sample_id"] for s in samples],
|
| 136 |
+
"concurrency": args.concurrency,
|
| 137 |
+
"limit_turns": args.limit_turns,
|
| 138 |
+
"dry_run": args.dry_run,
|
| 139 |
+
"brainapi_url": settings.brainapi_url,
|
| 140 |
+
"brain_override": args.brain,
|
| 141 |
+
**build_provenance(settings, size=args.size),
|
| 142 |
+
},
|
| 143 |
+
)
|
| 144 |
+
ingest_samples(
|
| 145 |
+
settings,
|
| 146 |
+
samples,
|
| 147 |
+
run_dir=run_dir,
|
| 148 |
+
concurrency=args.concurrency,
|
| 149 |
+
limit_turns=args.limit_turns,
|
| 150 |
+
dry_run=args.dry_run,
|
| 151 |
+
resume=not args.no_resume,
|
| 152 |
+
task_timeout_s=args.timeout,
|
| 153 |
+
brain_override=args.brain,
|
| 154 |
+
)
|
| 155 |
+
console.print(f"[green]Run directory:[/green] {run_dir}")
|
| 156 |
+
return 0
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def cmd_evaluate(args: argparse.Namespace, settings: Settings) -> int:
|
| 160 |
+
samples = resolve_samples(
|
| 161 |
+
settings, size=args.size, sample_ids=args.sample or None
|
| 162 |
+
)
|
| 163 |
+
run_id, run_dir = ensure_run_dir(settings, args.run)
|
| 164 |
+
abilities = _parse_abilities(args.abilities)
|
| 165 |
+
if getattr(args, "profile", None):
|
| 166 |
+
from beam.sota import profile_defaults
|
| 167 |
+
|
| 168 |
+
defaults = profile_defaults(args.profile)
|
| 169 |
+
settings.bench_profile = str(defaults["bench_profile"])
|
| 170 |
+
settings.sc_samples = max(1, int(defaults["sc_samples"]))
|
| 171 |
+
settings.sc_temperature = float(defaults["sc_temperature"])
|
| 172 |
+
settings.gap_fill = bool(defaults["gap_fill"])
|
| 173 |
+
if settings.judge_shares_answer_family:
|
| 174 |
+
console.print(
|
| 175 |
+
f"[yellow]Warning: judge model ({settings.judge_model}) is the same "
|
| 176 |
+
f"family as the answer model ({settings.answer_model}); judge scores "
|
| 177 |
+
"may carry self-preference bias.[/yellow]"
|
| 178 |
+
)
|
| 179 |
+
use_ppr = not bool(getattr(args, "no_ppr", False))
|
| 180 |
+
write_manifest(
|
| 181 |
+
run_dir,
|
| 182 |
+
{
|
| 183 |
+
"run_id": run_id,
|
| 184 |
+
"command": "evaluate",
|
| 185 |
+
"suite": "beam",
|
| 186 |
+
"size": args.size,
|
| 187 |
+
"samples": [s["sample_id"] for s in samples],
|
| 188 |
+
"concurrency": args.concurrency,
|
| 189 |
+
"abilities": sorted(abilities) if abilities else None,
|
| 190 |
+
"limit": args.limit,
|
| 191 |
+
"brainapi_url": settings.brainapi_url,
|
| 192 |
+
"brain_override": args.brain,
|
| 193 |
+
"bench_profile": settings.bench_profile,
|
| 194 |
+
"sc_samples": settings.sc_samples,
|
| 195 |
+
"sc_temperature": settings.sc_temperature,
|
| 196 |
+
"gap_fill": settings.gap_fill,
|
| 197 |
+
**build_provenance(settings, size=args.size),
|
| 198 |
+
"historical_limit": args.historical_limit,
|
| 199 |
+
"max_passages": args.max_passages,
|
| 200 |
+
"max_facts": args.max_facts,
|
| 201 |
+
"apply_fact_filter": not args.no_fact_filter,
|
| 202 |
+
"use_ppr": use_ppr,
|
| 203 |
+
"sufficiency_retry": args.sufficiency_retry,
|
| 204 |
+
},
|
| 205 |
+
)
|
| 206 |
+
evaluate_samples(
|
| 207 |
+
settings,
|
| 208 |
+
samples,
|
| 209 |
+
run_dir=run_dir,
|
| 210 |
+
concurrency=args.concurrency,
|
| 211 |
+
abilities=abilities,
|
| 212 |
+
limit=args.limit,
|
| 213 |
+
resume=not args.no_resume,
|
| 214 |
+
brain_override=args.brain,
|
| 215 |
+
historical_limit=args.historical_limit,
|
| 216 |
+
max_passages=args.max_passages,
|
| 217 |
+
max_facts=args.max_facts,
|
| 218 |
+
apply_fact_filter=not args.no_fact_filter,
|
| 219 |
+
use_ppr=use_ppr,
|
| 220 |
+
sufficiency_retry=args.sufficiency_retry,
|
| 221 |
+
)
|
| 222 |
+
report = write_report(run_dir)
|
| 223 |
+
print_report_table(report)
|
| 224 |
+
console.print(f"[green]Report:[/green] {run_dir / 'report.md'}")
|
| 225 |
+
return 0 if report.get("status") != "failed" else 1
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
def cmd_report(args: argparse.Namespace, settings: Settings) -> int:
|
| 229 |
+
run_dir = settings.runs_dir / args.run
|
| 230 |
+
if not run_dir.exists():
|
| 231 |
+
raise SystemExit(f"Run directory not found: {run_dir}")
|
| 232 |
+
report = write_report(run_dir)
|
| 233 |
+
print_report_table(report)
|
| 234 |
+
console.print(f"[green]Report:[/green] {run_dir / 'report.md'}")
|
| 235 |
+
return 0 if report.get("status") != "failed" else 1
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
def cmd_selftest(args: argparse.Namespace, settings: Settings) -> int:
|
| 239 |
+
errors = selftest_metrics()
|
| 240 |
+
if brain_id_for("100K", "1") != "beam100k1":
|
| 241 |
+
errors.append("brain_id_for('100K','1') != 'beam100k1'")
|
| 242 |
+
if brain_id_for("1M", "12") != "beam1m12":
|
| 243 |
+
errors.append("brain_id_for('1M','12') != 'beam1m12'")
|
| 244 |
+
if brain_id_for("10M", "1") != "beam10m1":
|
| 245 |
+
errors.append("brain_id_for('10M','1') != 'beam10m1'")
|
| 246 |
+
|
| 247 |
+
prompt = build_rubric_judge_prompt(
|
| 248 |
+
"When does my sprint end?",
|
| 249 |
+
"LLM response should state: March 29",
|
| 250 |
+
"The sprint ends on March 29.",
|
| 251 |
+
)
|
| 252 |
+
if "<question>" in prompt or "<rubric_item>" in prompt or "<llm_response>" in prompt:
|
| 253 |
+
errors.append("rubric judge prompt still contains placeholders")
|
| 254 |
+
|
| 255 |
+
payload = parse_json_response('{"score": 0.5, "reason": "partial"}')
|
| 256 |
+
if float(payload["score"]) != 0.5:
|
| 257 |
+
errors.append("parse_json_response failed on 0.5 score")
|
| 258 |
+
|
| 259 |
+
sample = {
|
| 260 |
+
"sample_id": "100K/1",
|
| 261 |
+
"size": "100K",
|
| 262 |
+
"conversation_id": "1",
|
| 263 |
+
"chat": [
|
| 264 |
+
{
|
| 265 |
+
"batch_number": 1,
|
| 266 |
+
"time_anchor": "March-15-2024",
|
| 267 |
+
"turns": [
|
| 268 |
+
[
|
| 269 |
+
{
|
| 270 |
+
"role": "user",
|
| 271 |
+
"content": "Hello",
|
| 272 |
+
"question_type": "main_question",
|
| 273 |
+
"time_anchor": "March-15-2024",
|
| 274 |
+
},
|
| 275 |
+
{"role": "assistant", "content": "Hi there"},
|
| 276 |
+
]
|
| 277 |
+
],
|
| 278 |
+
}
|
| 279 |
+
],
|
| 280 |
+
"probing_questions": {
|
| 281 |
+
"information_extraction": [
|
| 282 |
+
{"question": "What was said?", "rubric": ["Hello"]}
|
| 283 |
+
]
|
| 284 |
+
},
|
| 285 |
+
}
|
| 286 |
+
units = iter_ingest_units(sample)
|
| 287 |
+
if len(units) != 1:
|
| 288 |
+
errors.append(f"expected 1 ingest unit, got {len(units)}")
|
| 289 |
+
elif "March 15, 2024" not in str(units[0].get("source_timestamp")):
|
| 290 |
+
errors.append(f"unexpected source_timestamp: {units[0].get('source_timestamp')}")
|
| 291 |
+
|
| 292 |
+
if errors:
|
| 293 |
+
for err in errors:
|
| 294 |
+
console.print(f"[red]FAIL[/red] {err}")
|
| 295 |
+
return 1
|
| 296 |
+
console.print("[green]selftest passed[/green]")
|
| 297 |
+
return 0
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
def build_parser() -> argparse.ArgumentParser:
|
| 301 |
+
parser = argparse.ArgumentParser(
|
| 302 |
+
prog="beam",
|
| 303 |
+
description="BEAM long-term memory benchmark harness for BrainAPI",
|
| 304 |
+
)
|
| 305 |
+
parser.add_argument(
|
| 306 |
+
"--env-file",
|
| 307 |
+
type=Path,
|
| 308 |
+
default=None,
|
| 309 |
+
help="Optional path to .env (default: benchmarks/.env)",
|
| 310 |
+
)
|
| 311 |
+
sub = parser.add_subparsers(dest="command", required=True)
|
| 312 |
+
|
| 313 |
+
p_download = sub.add_parser("download", help="Download and normalize BEAM from HF")
|
| 314 |
+
p_download.add_argument(
|
| 315 |
+
"--size",
|
| 316 |
+
choices=CHAT_SIZES,
|
| 317 |
+
default=None,
|
| 318 |
+
help="Download one size only (default: all 100K/500K/1M/10M)",
|
| 319 |
+
)
|
| 320 |
+
p_download.add_argument(
|
| 321 |
+
"--force",
|
| 322 |
+
action="store_true",
|
| 323 |
+
help="Overwrite existing normalized conversations",
|
| 324 |
+
)
|
| 325 |
+
p_download.set_defaults(func=cmd_download)
|
| 326 |
+
|
| 327 |
+
p_stats = sub.add_parser("dataset-stats", help="Print local dataset stats")
|
| 328 |
+
p_stats.add_argument("--size", choices=CHAT_SIZES, default=None)
|
| 329 |
+
p_stats.set_defaults(func=cmd_dataset_stats)
|
| 330 |
+
|
| 331 |
+
p_smoke = sub.add_parser("smoke", help="Ingest a few turns + retrieve")
|
| 332 |
+
p_smoke.add_argument("--size", choices=CHAT_SIZES, default="100K")
|
| 333 |
+
p_smoke.add_argument("--sample", default="1")
|
| 334 |
+
p_smoke.add_argument("--brain", default=None)
|
| 335 |
+
p_smoke.add_argument("--limit-turns", type=int, default=2)
|
| 336 |
+
p_smoke.add_argument("--timeout", type=float, default=900.0)
|
| 337 |
+
p_smoke.set_defaults(func=cmd_smoke)
|
| 338 |
+
|
| 339 |
+
p_ingest = sub.add_parser("ingest", help="Ingest BEAM turns into BrainAPI")
|
| 340 |
+
p_ingest.add_argument("--size", choices=CHAT_SIZES, required=True)
|
| 341 |
+
p_ingest.add_argument(
|
| 342 |
+
"--sample",
|
| 343 |
+
action="append",
|
| 344 |
+
default=None,
|
| 345 |
+
help="Conversation id or size/id (repeatable)",
|
| 346 |
+
)
|
| 347 |
+
p_ingest.add_argument("--run", default=None)
|
| 348 |
+
p_ingest.add_argument("--brain", default=None)
|
| 349 |
+
p_ingest.add_argument(
|
| 350 |
+
"--concurrency",
|
| 351 |
+
type=int,
|
| 352 |
+
default=2,
|
| 353 |
+
help=(
|
| 354 |
+
"Parallel ingest units (default 2). For BEAM 1M prefer 2–3; "
|
| 355 |
+
"≤4 recommended. Ensure CELERY_WORKER_CONCURRENCY ≥ this value."
|
| 356 |
+
),
|
| 357 |
+
)
|
| 358 |
+
p_ingest.add_argument("--limit-turns", type=int, default=None)
|
| 359 |
+
p_ingest.add_argument("--timeout", type=float, default=900.0)
|
| 360 |
+
p_ingest.add_argument("--dry-run", action="store_true")
|
| 361 |
+
p_ingest.add_argument("--no-resume", action="store_true")
|
| 362 |
+
p_ingest.set_defaults(func=cmd_ingest)
|
| 363 |
+
|
| 364 |
+
p_eval = sub.add_parser("evaluate", help="Retrieve → answer → rubric-judge")
|
| 365 |
+
p_eval.add_argument("--size", choices=CHAT_SIZES, required=True)
|
| 366 |
+
p_eval.add_argument("--sample", action="append", default=None)
|
| 367 |
+
p_eval.add_argument("--run", default=None)
|
| 368 |
+
p_eval.add_argument("--brain", default=None)
|
| 369 |
+
p_eval.add_argument("--concurrency", type=int, default=2)
|
| 370 |
+
p_eval.add_argument(
|
| 371 |
+
"--abilities",
|
| 372 |
+
default=None,
|
| 373 |
+
help=f"Comma-separated abilities from: {','.join(ABILITY_NAMES)}",
|
| 374 |
+
)
|
| 375 |
+
p_eval.add_argument("--limit", type=int, default=None)
|
| 376 |
+
p_eval.add_argument("--no-resume", action="store_true")
|
| 377 |
+
p_eval.add_argument(
|
| 378 |
+
"--profile",
|
| 379 |
+
choices=("product", "sota"),
|
| 380 |
+
default=None,
|
| 381 |
+
help="Override BENCH_PROFILE for this evaluate run",
|
| 382 |
+
)
|
| 383 |
+
p_eval.add_argument(
|
| 384 |
+
"--historical-limit", type=int, default=DEFAULT_HISTORICAL_LIMIT
|
| 385 |
+
)
|
| 386 |
+
p_eval.add_argument("--max-passages", type=int, default=DEFAULT_MAX_PASSAGES)
|
| 387 |
+
p_eval.add_argument("--max-facts", type=int, default=DEFAULT_MAX_FACTS)
|
| 388 |
+
p_eval.add_argument("--no-fact-filter", action="store_true")
|
| 389 |
+
p_eval.add_argument(
|
| 390 |
+
"--use-ppr",
|
| 391 |
+
action="store_true",
|
| 392 |
+
default=True,
|
| 393 |
+
help="Enable PPR re-ranking (default on; use --no-ppr to disable)",
|
| 394 |
+
)
|
| 395 |
+
p_eval.add_argument(
|
| 396 |
+
"--no-ppr",
|
| 397 |
+
action="store_true",
|
| 398 |
+
help="Disable PPR re-ranking",
|
| 399 |
+
)
|
| 400 |
+
p_eval.add_argument("--sufficiency-retry", action="store_true")
|
| 401 |
+
p_eval.set_defaults(func=cmd_evaluate)
|
| 402 |
+
|
| 403 |
+
p_report = sub.add_parser("report", help="Rebuild report from answers.jsonl")
|
| 404 |
+
p_report.add_argument("--run", required=True)
|
| 405 |
+
p_report.set_defaults(func=cmd_report)
|
| 406 |
+
|
| 407 |
+
p_self = sub.add_parser("selftest", help="Local unit checks (no API)")
|
| 408 |
+
p_self.set_defaults(func=cmd_selftest)
|
| 409 |
+
|
| 410 |
+
return parser
|
| 411 |
+
|
| 412 |
+
|
| 413 |
+
def main(argv: list[str] | None = None) -> int:
|
| 414 |
+
parser = build_parser()
|
| 415 |
+
args = parser.parse_args(argv)
|
| 416 |
+
settings = Settings.load(args.env_file)
|
| 417 |
+
return int(args.func(args, settings))
|
benchmarks/beam/client.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import time
|
| 4 |
+
from dataclasses import dataclass, field
|
| 5 |
+
from typing import Any
|
| 6 |
+
|
| 7 |
+
import httpx
|
| 8 |
+
|
| 9 |
+
from beam.config import TERMINAL_TASK_STATUSES, Settings
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
@dataclass
|
| 13 |
+
class TimedResult:
|
| 14 |
+
data: Any
|
| 15 |
+
latency_ms: float
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@dataclass
|
| 19 |
+
class BrainAPIClient:
|
| 20 |
+
settings: Settings
|
| 21 |
+
timeout: float = 120.0
|
| 22 |
+
max_retries: int = 5
|
| 23 |
+
_client: httpx.Client = field(init=False, repr=False)
|
| 24 |
+
|
| 25 |
+
def __post_init__(self) -> None:
|
| 26 |
+
self.settings.require_brainapi()
|
| 27 |
+
self._client = httpx.Client(
|
| 28 |
+
base_url=self.settings.brainapi_url,
|
| 29 |
+
headers={
|
| 30 |
+
"Authorization": f"Bearer {self.settings.brainpat_token}",
|
| 31 |
+
"Content-Type": "application/json",
|
| 32 |
+
},
|
| 33 |
+
timeout=self.timeout,
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
def close(self) -> None:
|
| 37 |
+
self._client.close()
|
| 38 |
+
|
| 39 |
+
def __enter__(self) -> "BrainAPIClient":
|
| 40 |
+
return self
|
| 41 |
+
|
| 42 |
+
def __exit__(self, *args: object) -> None:
|
| 43 |
+
self.close()
|
| 44 |
+
|
| 45 |
+
def _request(
|
| 46 |
+
self,
|
| 47 |
+
method: str,
|
| 48 |
+
path: str,
|
| 49 |
+
*,
|
| 50 |
+
json: dict | None = None,
|
| 51 |
+
params: dict | None = None,
|
| 52 |
+
) -> TimedResult:
|
| 53 |
+
last_error: Exception | None = None
|
| 54 |
+
for attempt in range(self.max_retries):
|
| 55 |
+
started = time.perf_counter()
|
| 56 |
+
try:
|
| 57 |
+
response = self._client.request(
|
| 58 |
+
method, path, json=json, params=params
|
| 59 |
+
)
|
| 60 |
+
latency_ms = (time.perf_counter() - started) * 1000
|
| 61 |
+
if response.status_code in (429, 500, 502, 503, 504):
|
| 62 |
+
wait = min(2**attempt * 0.5, 30)
|
| 63 |
+
time.sleep(wait)
|
| 64 |
+
last_error = httpx.HTTPStatusError(
|
| 65 |
+
f"HTTP {response.status_code}",
|
| 66 |
+
request=response.request,
|
| 67 |
+
response=response,
|
| 68 |
+
)
|
| 69 |
+
continue
|
| 70 |
+
response.raise_for_status()
|
| 71 |
+
if response.status_code == 204 or not response.content:
|
| 72 |
+
return TimedResult(data=None, latency_ms=latency_ms)
|
| 73 |
+
return TimedResult(data=response.json(), latency_ms=latency_ms)
|
| 74 |
+
except (httpx.TransportError, httpx.TimeoutException) as exc:
|
| 75 |
+
last_error = exc
|
| 76 |
+
# Under parallel ingest, uvicorn restarts briefly refuse connections.
|
| 77 |
+
wait = min(2**attempt * 1.0, 45)
|
| 78 |
+
time.sleep(wait)
|
| 79 |
+
raise RuntimeError(f"Request failed after retries: {method} {path}: {last_error}")
|
| 80 |
+
|
| 81 |
+
def ingest_text(
|
| 82 |
+
self,
|
| 83 |
+
text: str,
|
| 84 |
+
brain_id: str,
|
| 85 |
+
*,
|
| 86 |
+
observate_for: list[str] | None = None,
|
| 87 |
+
preferred_extraction_entities: list[str] | None = None,
|
| 88 |
+
source_timestamp: str | None = None,
|
| 89 |
+
) -> TimedResult:
|
| 90 |
+
body = {
|
| 91 |
+
"data": {"data_type": "text", "text_data": text},
|
| 92 |
+
"brain_id": brain_id,
|
| 93 |
+
"observate_for": observate_for
|
| 94 |
+
or [
|
| 95 |
+
"facts about each speaker",
|
| 96 |
+
"events and when they happened",
|
| 97 |
+
"relationships between people",
|
| 98 |
+
"user preferences and instructions",
|
| 99 |
+
],
|
| 100 |
+
"preferred_extraction_entities": preferred_extraction_entities
|
| 101 |
+
or ["Person", "Event", "Location", "Date", "Preference", "Instruction"],
|
| 102 |
+
}
|
| 103 |
+
if source_timestamp:
|
| 104 |
+
body["source_timestamp"] = source_timestamp
|
| 105 |
+
return self._request("POST", "/ingest/", json=body)
|
| 106 |
+
|
| 107 |
+
def get_task(self, task_id: str, brain_id: str) -> TimedResult:
|
| 108 |
+
return self._request(
|
| 109 |
+
"GET", f"/tasks/{task_id}", params={"brain_id": brain_id}
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
def wait_for_task(
|
| 113 |
+
self,
|
| 114 |
+
task_id: str,
|
| 115 |
+
brain_id: str,
|
| 116 |
+
*,
|
| 117 |
+
timeout_s: float = 600.0,
|
| 118 |
+
poll_interval_s: float = 2.0,
|
| 119 |
+
) -> TimedResult:
|
| 120 |
+
started = time.perf_counter()
|
| 121 |
+
while True:
|
| 122 |
+
result = self.get_task(task_id, brain_id)
|
| 123 |
+
status = (result.data or {}).get("status", "unknown")
|
| 124 |
+
if status in TERMINAL_TASK_STATUSES:
|
| 125 |
+
return TimedResult(
|
| 126 |
+
data=result.data,
|
| 127 |
+
latency_ms=(time.perf_counter() - started) * 1000,
|
| 128 |
+
)
|
| 129 |
+
if time.perf_counter() - started > timeout_s:
|
| 130 |
+
raise TimeoutError(
|
| 131 |
+
f"Task {task_id} did not finish within {timeout_s}s "
|
| 132 |
+
f"(last status={status})"
|
| 133 |
+
)
|
| 134 |
+
time.sleep(poll_interval_s)
|
| 135 |
+
|
| 136 |
+
def retrieve_context(
|
| 137 |
+
self,
|
| 138 |
+
text: str,
|
| 139 |
+
brain_id: str,
|
| 140 |
+
*,
|
| 141 |
+
historical_limit: int = 10,
|
| 142 |
+
max_passages: int = 8,
|
| 143 |
+
max_facts: int = 40,
|
| 144 |
+
apply_fact_filter: bool = True,
|
| 145 |
+
use_ppr: bool = False,
|
| 146 |
+
sufficiency_retry: bool = False,
|
| 147 |
+
cross_event_bridges: int = 3,
|
| 148 |
+
) -> TimedResult:
|
| 149 |
+
body: dict[str, Any] = {
|
| 150 |
+
"text": text,
|
| 151 |
+
"brain_id": brain_id,
|
| 152 |
+
"historical_limit": historical_limit,
|
| 153 |
+
"max_passages": max_passages,
|
| 154 |
+
"max_facts": max_facts,
|
| 155 |
+
"apply_fact_filter": apply_fact_filter,
|
| 156 |
+
"use_ppr": use_ppr,
|
| 157 |
+
"sufficiency_retry": sufficiency_retry,
|
| 158 |
+
"cross_event_bridges": cross_event_bridges,
|
| 159 |
+
}
|
| 160 |
+
return self._request("POST", "/retrieve/context", json=body)
|
benchmarks/beam/config.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
from dataclasses import dataclass
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
from dotenv import load_dotenv
|
| 8 |
+
|
| 9 |
+
BENCHMARKS_ROOT = Path(__file__).resolve().parent.parent
|
| 10 |
+
DATA_DIR = BENCHMARKS_ROOT / "data"
|
| 11 |
+
RUNS_DIR = BENCHMARKS_ROOT / "runs"
|
| 12 |
+
DEFAULT_BEAM_DATA_DIR = DATA_DIR / "beam"
|
| 13 |
+
DEFAULT_HF_DATASET = "Mohammadta/BEAM"
|
| 14 |
+
DEFAULT_HF_DATASET_10M = "Mohammadta/BEAM-10M"
|
| 15 |
+
DEFAULT_LLM_BASE_URL = "https://api.deepseek.com"
|
| 16 |
+
DEFAULT_ANSWER_MODEL = "deepseek-v4-flash"
|
| 17 |
+
DEFAULT_AZURE_JUDGE_MODEL = "gpt-4o"
|
| 18 |
+
DEFAULT_AZURE_API_VERSION = "2024-12-01-preview"
|
| 19 |
+
|
| 20 |
+
CHAT_SIZES = ("100K", "500K", "1M", "10M")
|
| 21 |
+
HF_DATASET_BY_SIZE = {
|
| 22 |
+
"100K": DEFAULT_HF_DATASET,
|
| 23 |
+
"500K": DEFAULT_HF_DATASET,
|
| 24 |
+
"1M": DEFAULT_HF_DATASET,
|
| 25 |
+
"10M": DEFAULT_HF_DATASET_10M,
|
| 26 |
+
}
|
| 27 |
+
ABILITY_NAMES = (
|
| 28 |
+
"abstention",
|
| 29 |
+
"contradiction_resolution",
|
| 30 |
+
"event_ordering",
|
| 31 |
+
"information_extraction",
|
| 32 |
+
"instruction_following",
|
| 33 |
+
"knowledge_update",
|
| 34 |
+
"multi_session_reasoning",
|
| 35 |
+
"preference_following",
|
| 36 |
+
"summarization",
|
| 37 |
+
"temporal_reasoning",
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
TERMINAL_TASK_STATUSES = frozenset({"completed", "failed", "partial_failed"})
|
| 41 |
+
|
| 42 |
+
_MODEL_FAMILY_PREFIXES = {
|
| 43 |
+
"deepseek": "deepseek",
|
| 44 |
+
"gpt": "openai",
|
| 45 |
+
"o1": "openai",
|
| 46 |
+
"o3": "openai",
|
| 47 |
+
"o4": "openai",
|
| 48 |
+
"chatgpt": "openai",
|
| 49 |
+
"claude": "anthropic",
|
| 50 |
+
"gemini": "google",
|
| 51 |
+
"grok": "xai",
|
| 52 |
+
"qwen": "qwen",
|
| 53 |
+
"llama": "meta",
|
| 54 |
+
"mistral": "mistral",
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def model_family(model: str) -> str:
|
| 59 |
+
name = (model or "").strip().lower().rsplit("/", 1)[-1]
|
| 60 |
+
for prefix, family in _MODEL_FAMILY_PREFIXES.items():
|
| 61 |
+
if name.startswith(prefix):
|
| 62 |
+
return family
|
| 63 |
+
return name.split("-", 1)[0] or "unknown"
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
@dataclass
|
| 67 |
+
class Settings:
|
| 68 |
+
brainapi_url: str
|
| 69 |
+
brainpat_token: str
|
| 70 |
+
openai_api_key: str
|
| 71 |
+
llm_base_url: str | None
|
| 72 |
+
answer_model: str
|
| 73 |
+
judge_model: str
|
| 74 |
+
judge_api_key: str
|
| 75 |
+
judge_base_url: str | None
|
| 76 |
+
judge_azure_endpoint: str | None
|
| 77 |
+
judge_azure_api_version: str
|
| 78 |
+
beam_data_dir: Path
|
| 79 |
+
hf_dataset: str
|
| 80 |
+
hf_dataset_10m: str
|
| 81 |
+
runs_dir: Path
|
| 82 |
+
bench_profile: str = "product"
|
| 83 |
+
sc_samples: int = 1
|
| 84 |
+
sc_temperature: float = 0.0
|
| 85 |
+
gap_fill: bool = False
|
| 86 |
+
answer_azure_endpoint: str | None = None
|
| 87 |
+
answer_azure_api_version: str = DEFAULT_AZURE_API_VERSION
|
| 88 |
+
|
| 89 |
+
def hf_dataset_for(self, size: str) -> str:
|
| 90 |
+
if size == "10M":
|
| 91 |
+
return self.hf_dataset_10m
|
| 92 |
+
return self.hf_dataset
|
| 93 |
+
|
| 94 |
+
@classmethod
|
| 95 |
+
def load(cls, env_file: Path | None = None) -> "Settings":
|
| 96 |
+
if env_file is None:
|
| 97 |
+
env_file = BENCHMARKS_ROOT / ".env"
|
| 98 |
+
if env_file.exists():
|
| 99 |
+
load_dotenv(env_file, override=False)
|
| 100 |
+
else:
|
| 101 |
+
load_dotenv(override=False)
|
| 102 |
+
|
| 103 |
+
from beam.sota import profile_defaults
|
| 104 |
+
|
| 105 |
+
profile = (os.getenv("BENCH_PROFILE") or "product").strip().lower()
|
| 106 |
+
defaults = profile_defaults(profile)
|
| 107 |
+
|
| 108 |
+
api_key = (
|
| 109 |
+
os.getenv("DEEPSEEK_API_KEY")
|
| 110 |
+
or os.getenv("OPENAI_API_KEY")
|
| 111 |
+
or ""
|
| 112 |
+
)
|
| 113 |
+
llm_base_url = os.getenv("BENCH_LLM_BASE_URL", DEFAULT_LLM_BASE_URL) or None
|
| 114 |
+
answer_model = os.getenv("BENCH_ANSWER_MODEL", DEFAULT_ANSWER_MODEL)
|
| 115 |
+
|
| 116 |
+
azure_endpoint = (
|
| 117 |
+
os.getenv("BENCH_JUDGE_AZURE_ENDPOINT")
|
| 118 |
+
or os.getenv("AZURE_LARGE_LLM_ENDPOINT")
|
| 119 |
+
or ""
|
| 120 |
+
).rstrip("/")
|
| 121 |
+
azure_key = (
|
| 122 |
+
os.getenv("BENCH_JUDGE_AZURE_KEY")
|
| 123 |
+
or os.getenv("AZURE_LARGE_LLM_SUBSCRIPTION_KEY")
|
| 124 |
+
or ""
|
| 125 |
+
)
|
| 126 |
+
azure_available = bool(azure_endpoint and azure_key)
|
| 127 |
+
|
| 128 |
+
judge_model = os.getenv("BENCH_JUDGE_MODEL", "")
|
| 129 |
+
judge_base_url = os.getenv("BENCH_JUDGE_BASE_URL", "")
|
| 130 |
+
judge_api_key = os.getenv("BENCH_JUDGE_API_KEY", "")
|
| 131 |
+
use_azure_judge = azure_available and not judge_model and not judge_base_url
|
| 132 |
+
if use_azure_judge:
|
| 133 |
+
judge_model = os.getenv(
|
| 134 |
+
"AZURE_LARGE_LLM_MODEL", DEFAULT_AZURE_JUDGE_MODEL
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
return cls(
|
| 138 |
+
brainapi_url=os.getenv("BRAINAPI_URL", "http://localhost:8000").rstrip("/"),
|
| 139 |
+
brainpat_token=os.getenv("BRAINPAT_TOKEN", ""),
|
| 140 |
+
openai_api_key=api_key,
|
| 141 |
+
llm_base_url=llm_base_url,
|
| 142 |
+
answer_model=answer_model,
|
| 143 |
+
judge_model=judge_model or answer_model,
|
| 144 |
+
judge_api_key=(
|
| 145 |
+
azure_key if use_azure_judge else (judge_api_key or api_key)
|
| 146 |
+
),
|
| 147 |
+
judge_base_url=(
|
| 148 |
+
None if use_azure_judge else (judge_base_url or llm_base_url)
|
| 149 |
+
),
|
| 150 |
+
judge_azure_endpoint=azure_endpoint if use_azure_judge else None,
|
| 151 |
+
judge_azure_api_version=os.getenv(
|
| 152 |
+
"BENCH_JUDGE_AZURE_API_VERSION",
|
| 153 |
+
os.getenv("AZURE_LARGE_LLM_API_VERSION", DEFAULT_AZURE_API_VERSION),
|
| 154 |
+
),
|
| 155 |
+
beam_data_dir=Path(
|
| 156 |
+
os.getenv("BENCH_BEAM_DATA_DIR", str(DEFAULT_BEAM_DATA_DIR))
|
| 157 |
+
),
|
| 158 |
+
hf_dataset=os.getenv("BENCH_BEAM_HF_DATASET", DEFAULT_HF_DATASET),
|
| 159 |
+
hf_dataset_10m=os.getenv(
|
| 160 |
+
"BENCH_BEAM_HF_DATASET_10M", DEFAULT_HF_DATASET_10M
|
| 161 |
+
),
|
| 162 |
+
runs_dir=Path(os.getenv("BENCH_RUNS_DIR", str(RUNS_DIR))),
|
| 163 |
+
bench_profile=str(defaults["bench_profile"]),
|
| 164 |
+
sc_samples=max(1, int(defaults["sc_samples"])),
|
| 165 |
+
sc_temperature=float(defaults["sc_temperature"]),
|
| 166 |
+
gap_fill=bool(defaults["gap_fill"]),
|
| 167 |
+
answer_azure_endpoint=None,
|
| 168 |
+
answer_azure_api_version=os.getenv(
|
| 169 |
+
"BENCH_JUDGE_AZURE_API_VERSION",
|
| 170 |
+
os.getenv("AZURE_LARGE_LLM_API_VERSION", DEFAULT_AZURE_API_VERSION),
|
| 171 |
+
),
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
@property
|
| 175 |
+
def judge_shares_answer_family(self) -> bool:
|
| 176 |
+
return model_family(self.judge_model) == model_family(self.answer_model)
|
| 177 |
+
|
| 178 |
+
def judge_provider(self) -> str:
|
| 179 |
+
if self.judge_azure_endpoint:
|
| 180 |
+
return "azure"
|
| 181 |
+
return self.judge_base_url or "openai-default"
|
| 182 |
+
|
| 183 |
+
def require_brainapi(self) -> None:
|
| 184 |
+
if not self.brainpat_token:
|
| 185 |
+
raise SystemExit(
|
| 186 |
+
"BRAINPAT_TOKEN is required. Copy .env.example to .env and set it."
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
def require_llm(self) -> None:
|
| 190 |
+
if not self.openai_api_key:
|
| 191 |
+
raise SystemExit(
|
| 192 |
+
"DEEPSEEK_API_KEY (or OPENAI_API_KEY) is required for answer/judge "
|
| 193 |
+
"LLM calls. Copy .env.example to .env and set it."
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
def require_judge_llm(self) -> None:
|
| 197 |
+
if not self.judge_api_key:
|
| 198 |
+
raise SystemExit(
|
| 199 |
+
"No judge API key. Set BENCH_JUDGE_API_KEY (with BENCH_JUDGE_BASE_URL "
|
| 200 |
+
"and BENCH_JUDGE_MODEL), or DEEPSEEK_API_KEY / OPENAI_API_KEY to reuse "
|
| 201 |
+
"the answerer's provider."
|
| 202 |
+
)
|
benchmarks/beam/dataset.py
ADDED
|
@@ -0,0 +1,586 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import ast
|
| 4 |
+
import json
|
| 5 |
+
import re
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Any
|
| 8 |
+
|
| 9 |
+
from beam.config import ABILITY_NAMES, CHAT_SIZES, Settings
|
| 10 |
+
|
| 11 |
+
_PLAN_KEY_RE = re.compile(r"^plan[-_]?(\d+)$", re.IGNORECASE)
|
| 12 |
+
NORMALIZE_STRATEGY_SINGLE = "single_chat"
|
| 13 |
+
NORMALIZE_STRATEGY_PLANS = "concatenate_plans_chronological"
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def brain_id_for(size: str, conversation_id: str) -> str:
|
| 17 |
+
slug = re.sub(r"[^a-zA-Z0-9]", "", f"beam{size}{conversation_id}")
|
| 18 |
+
if not slug.isalnum():
|
| 19 |
+
raise ValueError(
|
| 20 |
+
f"Could not derive alphanumeric brain_id from size={size!r} "
|
| 21 |
+
f"conversation_id={conversation_id!r}"
|
| 22 |
+
)
|
| 23 |
+
return slug.lower()
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def sample_id_for(size: str, conversation_id: str) -> str:
|
| 27 |
+
return f"{size}/{conversation_id}"
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def parse_sample_id(sample_id: str) -> tuple[str, str]:
|
| 31 |
+
parts = sample_id.split("/", 1)
|
| 32 |
+
if len(parts) != 2 or not parts[0] or not parts[1]:
|
| 33 |
+
raise ValueError(
|
| 34 |
+
f"sample_id must look like '100K/1', got {sample_id!r}"
|
| 35 |
+
)
|
| 36 |
+
return parts[0], parts[1]
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def _normalize_time_anchor(raw: Any) -> str | None:
|
| 40 |
+
if raw is None:
|
| 41 |
+
return None
|
| 42 |
+
text = str(raw).strip()
|
| 43 |
+
if not text:
|
| 44 |
+
return None
|
| 45 |
+
# BEAM often uses March-15-2024; convert to a parseable date-ish string.
|
| 46 |
+
m = re.match(r"^([A-Za-z]+)-(\d{1,2})-(\d{4})$", text)
|
| 47 |
+
if m:
|
| 48 |
+
return f"{m.group(1)} {m.group(2)}, {m.group(3)}"
|
| 49 |
+
return text
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def convert_chat_batches(chat: list[Any]) -> list[dict[str, Any]]:
|
| 53 |
+
"""Normalize HF chat (list of message lists) into batch/turn JSON."""
|
| 54 |
+
json_object: list[dict[str, Any]] = []
|
| 55 |
+
for index, batch in enumerate(chat):
|
| 56 |
+
if not isinstance(batch, list):
|
| 57 |
+
continue
|
| 58 |
+
batch_number = index + 1
|
| 59 |
+
turns: list[list[dict[str, Any]]] = []
|
| 60 |
+
single_turn: list[dict[str, Any]] = []
|
| 61 |
+
time_anchor = None
|
| 62 |
+
for message in batch:
|
| 63 |
+
if not isinstance(message, dict):
|
| 64 |
+
continue
|
| 65 |
+
if (
|
| 66 |
+
message.get("question_type") == "main_question"
|
| 67 |
+
and single_turn
|
| 68 |
+
):
|
| 69 |
+
if message.get("time_anchor"):
|
| 70 |
+
time_anchor = message.get("time_anchor")
|
| 71 |
+
turns.append(single_turn)
|
| 72 |
+
single_turn = []
|
| 73 |
+
single_turn.append(message)
|
| 74 |
+
if single_turn:
|
| 75 |
+
turns.append(single_turn)
|
| 76 |
+
json_object.append(
|
| 77 |
+
{
|
| 78 |
+
"batch_number": batch_number,
|
| 79 |
+
"turns": turns,
|
| 80 |
+
"time_anchor": time_anchor,
|
| 81 |
+
}
|
| 82 |
+
)
|
| 83 |
+
return json_object
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def plan_sort_key(label: Any) -> tuple[int, str]:
|
| 87 |
+
text = str(label or "").strip()
|
| 88 |
+
match = _PLAN_KEY_RE.match(text)
|
| 89 |
+
if match:
|
| 90 |
+
return (int(match.group(1)), text)
|
| 91 |
+
if text.isdigit():
|
| 92 |
+
return (int(text), text)
|
| 93 |
+
return (10**9, text)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def _as_mapping(value: Any) -> dict[str, Any] | None:
|
| 97 |
+
if isinstance(value, dict):
|
| 98 |
+
return value
|
| 99 |
+
if value is None:
|
| 100 |
+
return None
|
| 101 |
+
keys = getattr(value, "keys", None)
|
| 102 |
+
if callable(keys):
|
| 103 |
+
try:
|
| 104 |
+
return {str(k): value[k] for k in keys()}
|
| 105 |
+
except Exception:
|
| 106 |
+
return None
|
| 107 |
+
return None
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def _iter_plan_keyed_entries(chat: Any) -> list[tuple[str, Any]]:
|
| 111 |
+
"""Yield (plan_id, payload) from BEAM-10M main chat (plan-1..plan-10)."""
|
| 112 |
+
mapping = _as_mapping(chat)
|
| 113 |
+
if mapping is not None:
|
| 114 |
+
entries = [
|
| 115 |
+
(key, mapping[key])
|
| 116 |
+
for key in mapping
|
| 117 |
+
if _PLAN_KEY_RE.match(str(key).strip())
|
| 118 |
+
]
|
| 119 |
+
entries.sort(key=lambda item: plan_sort_key(item[0]))
|
| 120 |
+
return entries
|
| 121 |
+
if isinstance(chat, list):
|
| 122 |
+
collected: list[tuple[str, Any]] = []
|
| 123 |
+
for item in chat:
|
| 124 |
+
nested = _as_mapping(item)
|
| 125 |
+
if nested is None:
|
| 126 |
+
continue
|
| 127 |
+
for key, payload in nested.items():
|
| 128 |
+
if _PLAN_KEY_RE.match(str(key).strip()):
|
| 129 |
+
collected.append((str(key), payload))
|
| 130 |
+
collected.sort(key=lambda item: plan_sort_key(item[0]))
|
| 131 |
+
return collected
|
| 132 |
+
return []
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def _batches_from_chat_payload(value: Any) -> list[dict[str, Any]]:
|
| 136 |
+
"""Accept already-normalized batches or raw HF message-list chat."""
|
| 137 |
+
if not isinstance(value, list) or not value:
|
| 138 |
+
return []
|
| 139 |
+
first = value[0]
|
| 140 |
+
if isinstance(first, dict) and "turns" in first:
|
| 141 |
+
return [row for row in value if isinstance(row, dict) and row.get("turns")]
|
| 142 |
+
if isinstance(first, list):
|
| 143 |
+
return [row for row in convert_chat_batches(value) if row.get("turns")]
|
| 144 |
+
return []
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def concatenate_plan_batches(
|
| 148 |
+
plan_batches: list[tuple[str, list[dict[str, Any]]]],
|
| 149 |
+
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
| 150 |
+
"""
|
| 151 |
+
Merge plan chats in chronological order into one chat.json-compatible list.
|
| 152 |
+
|
| 153 |
+
Reassigns batch_number globally so unit ids remain unique under the existing
|
| 154 |
+
bN_tM / session_N conventions used by the 1M ingest path.
|
| 155 |
+
"""
|
| 156 |
+
out: list[dict[str, Any]] = []
|
| 157 |
+
plan_meta: list[dict[str, Any]] = []
|
| 158 |
+
next_batch = 1
|
| 159 |
+
for plan_id, batches in plan_batches:
|
| 160 |
+
start = next_batch
|
| 161 |
+
n_turns = 0
|
| 162 |
+
for batch in batches:
|
| 163 |
+
turns = batch.get("turns") or []
|
| 164 |
+
if not isinstance(turns, list) or not turns:
|
| 165 |
+
continue
|
| 166 |
+
n_turns += len(turns)
|
| 167 |
+
out.append(
|
| 168 |
+
{
|
| 169 |
+
"batch_number": next_batch,
|
| 170 |
+
"turns": turns,
|
| 171 |
+
"time_anchor": batch.get("time_anchor"),
|
| 172 |
+
"plan_id": plan_id,
|
| 173 |
+
}
|
| 174 |
+
)
|
| 175 |
+
next_batch += 1
|
| 176 |
+
if next_batch > start:
|
| 177 |
+
plan_meta.append(
|
| 178 |
+
{
|
| 179 |
+
"plan_id": plan_id,
|
| 180 |
+
"batch_number_start": start,
|
| 181 |
+
"batch_number_end": next_batch - 1,
|
| 182 |
+
"n_batches": next_batch - start,
|
| 183 |
+
"n_turns": n_turns,
|
| 184 |
+
}
|
| 185 |
+
)
|
| 186 |
+
return out, plan_meta
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def extract_normalized_chat(
|
| 190 |
+
row: dict[str, Any],
|
| 191 |
+
*,
|
| 192 |
+
size: str,
|
| 193 |
+
) -> tuple[list[dict[str, Any]], dict[str, Any]]:
|
| 194 |
+
"""
|
| 195 |
+
Build normalized chat batches for a HF row.
|
| 196 |
+
|
| 197 |
+
BEAM-10M: concatenate plan-1..plan-10 (or plans[]) chronologically into one
|
| 198 |
+
long chat. Smaller sizes: single-chat convert_chat_batches path.
|
| 199 |
+
"""
|
| 200 |
+
chat = row.get("chat")
|
| 201 |
+
plans = row.get("plans")
|
| 202 |
+
plan_entries = _iter_plan_keyed_entries(chat)
|
| 203 |
+
use_multi_plan = size == "10M" or bool(plan_entries) or (
|
| 204 |
+
isinstance(plans, list) and bool(plans)
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
if use_multi_plan:
|
| 208 |
+
ordered: list[tuple[str, list[dict[str, Any]]]] = []
|
| 209 |
+
if plan_entries:
|
| 210 |
+
for plan_id, payload in plan_entries:
|
| 211 |
+
batches = _batches_from_chat_payload(payload)
|
| 212 |
+
if batches:
|
| 213 |
+
ordered.append((str(plan_id), batches))
|
| 214 |
+
source = "chat_plan_keys"
|
| 215 |
+
else:
|
| 216 |
+
source = "plans_array"
|
| 217 |
+
plan_rows = [p for p in (plans or []) if isinstance(p, dict)]
|
| 218 |
+
plan_rows.sort(
|
| 219 |
+
key=lambda p: plan_sort_key(p.get("plan_id") or p.get("id") or "")
|
| 220 |
+
)
|
| 221 |
+
for index, plan in enumerate(plan_rows):
|
| 222 |
+
plan_id = str(
|
| 223 |
+
plan.get("plan_id") or plan.get("id") or f"plan-{index + 1}"
|
| 224 |
+
)
|
| 225 |
+
batches = _batches_from_chat_payload(plan.get("chat"))
|
| 226 |
+
if batches:
|
| 227 |
+
ordered.append((plan_id, batches))
|
| 228 |
+
|
| 229 |
+
if ordered:
|
| 230 |
+
chat_json, plan_meta = concatenate_plan_batches(ordered)
|
| 231 |
+
return chat_json, {
|
| 232 |
+
"normalize_strategy": NORMALIZE_STRATEGY_PLANS,
|
| 233 |
+
"n_plans": len(plan_meta),
|
| 234 |
+
"plans": plan_meta,
|
| 235 |
+
"plan_source": source,
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
chat_list = chat if isinstance(chat, list) else []
|
| 239 |
+
return convert_chat_batches(chat_list), {
|
| 240 |
+
"normalize_strategy": NORMALIZE_STRATEGY_SINGLE,
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def ingest_target_turns(n_turns: int, *, slack: int = 5) -> int:
|
| 245 |
+
"""Ops TARGET from normalized turn count (never invent without n_turns)."""
|
| 246 |
+
if n_turns <= 0:
|
| 247 |
+
return 0
|
| 248 |
+
return max(1, n_turns - max(0, slack))
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def _parse_probing_questions(raw: Any) -> dict[str, list[dict[str, Any]]]:
|
| 252 |
+
if isinstance(raw, dict):
|
| 253 |
+
data = raw
|
| 254 |
+
elif isinstance(raw, str):
|
| 255 |
+
data = ast.literal_eval(raw)
|
| 256 |
+
else:
|
| 257 |
+
raise ValueError(f"Unexpected probing_questions type: {type(raw)}")
|
| 258 |
+
if not isinstance(data, dict):
|
| 259 |
+
raise ValueError("probing_questions must parse to a dict")
|
| 260 |
+
out: dict[str, list[dict[str, Any]]] = {}
|
| 261 |
+
for key, value in data.items():
|
| 262 |
+
if not isinstance(value, list):
|
| 263 |
+
continue
|
| 264 |
+
out[str(key)] = [item for item in value if isinstance(item, dict)]
|
| 265 |
+
return out
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def conversation_dir(data_dir: Path, size: str, conversation_id: str) -> Path:
|
| 269 |
+
return data_dir / size / str(conversation_id)
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
def write_conversation(
|
| 273 |
+
dest: Path,
|
| 274 |
+
*,
|
| 275 |
+
size: str,
|
| 276 |
+
conversation_id: str,
|
| 277 |
+
topic: Any,
|
| 278 |
+
chat: list[Any] | None = None,
|
| 279 |
+
probing_questions: Any,
|
| 280 |
+
plans: Any = None,
|
| 281 |
+
row: dict[str, Any] | None = None,
|
| 282 |
+
) -> Path:
|
| 283 |
+
dest.mkdir(parents=True, exist_ok=True)
|
| 284 |
+
if row is not None:
|
| 285 |
+
chat_json, extra_meta = extract_normalized_chat(row, size=size)
|
| 286 |
+
if topic is None:
|
| 287 |
+
topic = row.get("conversation_seed")
|
| 288 |
+
if probing_questions is None:
|
| 289 |
+
probing_questions = row.get("probing_questions")
|
| 290 |
+
else:
|
| 291 |
+
chat_json, extra_meta = extract_normalized_chat(
|
| 292 |
+
{"chat": chat or [], "plans": plans},
|
| 293 |
+
size=size,
|
| 294 |
+
)
|
| 295 |
+
probing = _parse_probing_questions(probing_questions)
|
| 296 |
+
(dest / "chat.json").write_text(
|
| 297 |
+
json.dumps(chat_json, ensure_ascii=False, indent=2),
|
| 298 |
+
encoding="utf-8",
|
| 299 |
+
)
|
| 300 |
+
(dest / "probing_questions.json").write_text(
|
| 301 |
+
json.dumps(probing, ensure_ascii=False, indent=2),
|
| 302 |
+
encoding="utf-8",
|
| 303 |
+
)
|
| 304 |
+
if isinstance(topic, dict):
|
| 305 |
+
(dest / "topic.json").write_text(
|
| 306 |
+
json.dumps(topic, ensure_ascii=False, indent=2),
|
| 307 |
+
encoding="utf-8",
|
| 308 |
+
)
|
| 309 |
+
n_turns = sum(len(b.get("turns") or []) for b in chat_json)
|
| 310 |
+
meta = {
|
| 311 |
+
"conversation_id": str(conversation_id),
|
| 312 |
+
"size": size,
|
| 313 |
+
"sample_id": sample_id_for(size, str(conversation_id)),
|
| 314 |
+
"brain_id": brain_id_for(size, str(conversation_id)),
|
| 315 |
+
"n_batches": len(chat_json),
|
| 316 |
+
"n_turns": n_turns,
|
| 317 |
+
"n_questions": sum(len(v) for v in probing.values()),
|
| 318 |
+
"ingest_target": ingest_target_turns(n_turns),
|
| 319 |
+
**extra_meta,
|
| 320 |
+
}
|
| 321 |
+
(dest / "meta.json").write_text(
|
| 322 |
+
json.dumps(meta, ensure_ascii=False, indent=2),
|
| 323 |
+
encoding="utf-8",
|
| 324 |
+
)
|
| 325 |
+
return dest
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
def download_and_normalize(
|
| 329 |
+
settings: Settings,
|
| 330 |
+
*,
|
| 331 |
+
sizes: list[str] | None = None,
|
| 332 |
+
force: bool = False,
|
| 333 |
+
) -> Path:
|
| 334 |
+
try:
|
| 335 |
+
from datasets import load_dataset
|
| 336 |
+
except ImportError as exc:
|
| 337 |
+
raise SystemExit(
|
| 338 |
+
"The `datasets` package is required for BEAM download. "
|
| 339 |
+
"Run: pip install -r benchmarks/requirements.txt"
|
| 340 |
+
) from exc
|
| 341 |
+
|
| 342 |
+
selected = sizes or list(CHAT_SIZES)
|
| 343 |
+
for size in selected:
|
| 344 |
+
if size not in CHAT_SIZES:
|
| 345 |
+
raise SystemExit(f"Unsupported size {size!r}; choose from {CHAT_SIZES}")
|
| 346 |
+
|
| 347 |
+
settings.beam_data_dir.mkdir(parents=True, exist_ok=True)
|
| 348 |
+
|
| 349 |
+
for size in selected:
|
| 350 |
+
dataset_name = settings.hf_dataset_for(size)
|
| 351 |
+
conversations = load_dataset(dataset_name, split=size)
|
| 352 |
+
for conversation in conversations:
|
| 353 |
+
conversation_id = str(conversation["conversation_id"])
|
| 354 |
+
dest = conversation_dir(
|
| 355 |
+
settings.beam_data_dir, size, conversation_id
|
| 356 |
+
)
|
| 357 |
+
if (
|
| 358 |
+
not force
|
| 359 |
+
and (dest / "chat.json").exists()
|
| 360 |
+
and (dest / "probing_questions.json").exists()
|
| 361 |
+
):
|
| 362 |
+
continue
|
| 363 |
+
row = dict(conversation)
|
| 364 |
+
write_conversation(
|
| 365 |
+
dest,
|
| 366 |
+
size=size,
|
| 367 |
+
conversation_id=conversation_id,
|
| 368 |
+
topic=row.get("conversation_seed"),
|
| 369 |
+
probing_questions=row.get("probing_questions"),
|
| 370 |
+
row=row,
|
| 371 |
+
)
|
| 372 |
+
return settings.beam_data_dir
|
| 373 |
+
|
| 374 |
+
|
| 375 |
+
def list_local_samples(
|
| 376 |
+
settings: Settings, *, size: str | None = None
|
| 377 |
+
) -> list[dict[str, Any]]:
|
| 378 |
+
root = settings.beam_data_dir
|
| 379 |
+
if not root.exists():
|
| 380 |
+
return []
|
| 381 |
+
sizes = [size] if size else [
|
| 382 |
+
p.name for p in sorted(root.iterdir()) if p.is_dir() and p.name in CHAT_SIZES
|
| 383 |
+
]
|
| 384 |
+
samples: list[dict[str, Any]] = []
|
| 385 |
+
for sz in sizes:
|
| 386 |
+
size_dir = root / sz
|
| 387 |
+
if not size_dir.exists():
|
| 388 |
+
continue
|
| 389 |
+
for conv_dir in sorted(
|
| 390 |
+
[p for p in size_dir.iterdir() if p.is_dir()],
|
| 391 |
+
key=lambda p: int(p.name) if p.name.isdigit() else p.name,
|
| 392 |
+
):
|
| 393 |
+
meta_path = conv_dir / "meta.json"
|
| 394 |
+
if meta_path.exists():
|
| 395 |
+
meta = json.loads(meta_path.read_text(encoding="utf-8"))
|
| 396 |
+
else:
|
| 397 |
+
meta = {
|
| 398 |
+
"conversation_id": conv_dir.name,
|
| 399 |
+
"size": sz,
|
| 400 |
+
"sample_id": sample_id_for(sz, conv_dir.name),
|
| 401 |
+
"brain_id": brain_id_for(sz, conv_dir.name),
|
| 402 |
+
}
|
| 403 |
+
meta["path"] = str(conv_dir)
|
| 404 |
+
samples.append(meta)
|
| 405 |
+
return samples
|
| 406 |
+
|
| 407 |
+
|
| 408 |
+
def load_sample(settings: Settings, sample_id: str) -> dict[str, Any]:
|
| 409 |
+
size, conversation_id = parse_sample_id(sample_id)
|
| 410 |
+
dest = conversation_dir(settings.beam_data_dir, size, conversation_id)
|
| 411 |
+
chat_path = dest / "chat.json"
|
| 412 |
+
probing_path = dest / "probing_questions.json"
|
| 413 |
+
if not chat_path.exists() or not probing_path.exists():
|
| 414 |
+
raise FileNotFoundError(
|
| 415 |
+
f"Sample {sample_id!r} not found under {dest}. "
|
| 416 |
+
"Run: python -m beam download"
|
| 417 |
+
)
|
| 418 |
+
chat = json.loads(chat_path.read_text(encoding="utf-8"))
|
| 419 |
+
probing = json.loads(probing_path.read_text(encoding="utf-8"))
|
| 420 |
+
meta_path = dest / "meta.json"
|
| 421 |
+
meta = (
|
| 422 |
+
json.loads(meta_path.read_text(encoding="utf-8"))
|
| 423 |
+
if meta_path.exists()
|
| 424 |
+
else {
|
| 425 |
+
"conversation_id": conversation_id,
|
| 426 |
+
"size": size,
|
| 427 |
+
"sample_id": sample_id,
|
| 428 |
+
"brain_id": brain_id_for(size, conversation_id),
|
| 429 |
+
}
|
| 430 |
+
)
|
| 431 |
+
topic = None
|
| 432 |
+
topic_path = dest / "topic.json"
|
| 433 |
+
if topic_path.exists():
|
| 434 |
+
topic = json.loads(topic_path.read_text(encoding="utf-8"))
|
| 435 |
+
return {
|
| 436 |
+
**meta,
|
| 437 |
+
"chat": chat,
|
| 438 |
+
"probing_questions": probing,
|
| 439 |
+
"topic": topic,
|
| 440 |
+
"path": str(dest),
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
|
| 444 |
+
def resolve_samples(
|
| 445 |
+
settings: Settings,
|
| 446 |
+
*,
|
| 447 |
+
size: str,
|
| 448 |
+
sample_ids: list[str] | None = None,
|
| 449 |
+
) -> list[dict[str, Any]]:
|
| 450 |
+
if size not in CHAT_SIZES:
|
| 451 |
+
raise SystemExit(f"Unsupported size {size!r}; choose from {CHAT_SIZES}")
|
| 452 |
+
if sample_ids:
|
| 453 |
+
return [load_sample(settings, sid if "/" in sid else f"{size}/{sid}") for sid in sample_ids]
|
| 454 |
+
local = list_local_samples(settings, size=size)
|
| 455 |
+
if not local:
|
| 456 |
+
raise SystemExit(
|
| 457 |
+
f"No local BEAM samples for size {size}. Run: python -m beam download --size {size}"
|
| 458 |
+
)
|
| 459 |
+
return [load_sample(settings, str(row["sample_id"])) for row in local]
|
| 460 |
+
|
| 461 |
+
|
| 462 |
+
def format_turn_text(
|
| 463 |
+
messages: list[dict[str, Any]],
|
| 464 |
+
*,
|
| 465 |
+
batch_number: int,
|
| 466 |
+
turn_index: int,
|
| 467 |
+
batch_time_anchor: Any = None,
|
| 468 |
+
global_turn: int | None = None,
|
| 469 |
+
) -> tuple[str, str | None]:
|
| 470 |
+
session_n = global_turn if global_turn is not None else turn_index + 1
|
| 471 |
+
unit_id = f"b{batch_number}_t{turn_index + 1}"
|
| 472 |
+
lines = [
|
| 473 |
+
f"Session id: session_{session_n}.",
|
| 474 |
+
f"BEAM dialogue batch {batch_number}, turn {turn_index + 1}.",
|
| 475 |
+
f"Unit id: {unit_id}.",
|
| 476 |
+
]
|
| 477 |
+
turn_anchor = None
|
| 478 |
+
for message in messages:
|
| 479 |
+
role = str(message.get("role") or "unknown")
|
| 480 |
+
content = str(message.get("content") or "").strip()
|
| 481 |
+
msg_anchor = message.get("time_anchor")
|
| 482 |
+
if msg_anchor and not turn_anchor:
|
| 483 |
+
turn_anchor = msg_anchor
|
| 484 |
+
qtype = message.get("question_type")
|
| 485 |
+
prefix = f"{role}"
|
| 486 |
+
if qtype:
|
| 487 |
+
prefix = f"{role} [{qtype}]"
|
| 488 |
+
if msg_anchor:
|
| 489 |
+
prefix = f"{prefix} (time_anchor={msg_anchor})"
|
| 490 |
+
lines.append(f"{prefix}: {content}")
|
| 491 |
+
source_ts = _normalize_time_anchor(turn_anchor or batch_time_anchor)
|
| 492 |
+
return "\n".join(lines), source_ts
|
| 493 |
+
|
| 494 |
+
|
| 495 |
+
def iter_ingest_units(
|
| 496 |
+
sample: dict[str, Any],
|
| 497 |
+
*,
|
| 498 |
+
limit_turns: int | None = None,
|
| 499 |
+
) -> list[dict[str, Any]]:
|
| 500 |
+
units: list[dict[str, Any]] = []
|
| 501 |
+
global_turn = 0
|
| 502 |
+
for batch in sample.get("chat") or []:
|
| 503 |
+
batch_number = int(batch.get("batch_number") or 0)
|
| 504 |
+
batch_anchor = batch.get("time_anchor")
|
| 505 |
+
for turn_index, messages in enumerate(batch.get("turns") or []):
|
| 506 |
+
if not isinstance(messages, list) or not messages:
|
| 507 |
+
continue
|
| 508 |
+
global_turn += 1
|
| 509 |
+
text, source_ts = format_turn_text(
|
| 510 |
+
messages,
|
| 511 |
+
batch_number=batch_number,
|
| 512 |
+
turn_index=turn_index,
|
| 513 |
+
batch_time_anchor=batch_anchor,
|
| 514 |
+
global_turn=global_turn,
|
| 515 |
+
)
|
| 516 |
+
unit_id = f"b{batch_number}_t{turn_index + 1}"
|
| 517 |
+
units.append(
|
| 518 |
+
{
|
| 519 |
+
"unit_id": unit_id,
|
| 520 |
+
"session_key": f"session_{global_turn}",
|
| 521 |
+
"batch_number": batch_number,
|
| 522 |
+
"turn_index": turn_index,
|
| 523 |
+
"global_turn": global_turn,
|
| 524 |
+
"text": text,
|
| 525 |
+
"source_timestamp": source_ts,
|
| 526 |
+
}
|
| 527 |
+
)
|
| 528 |
+
if limit_turns is not None and len(units) >= limit_turns:
|
| 529 |
+
return units
|
| 530 |
+
return units
|
| 531 |
+
|
| 532 |
+
|
| 533 |
+
def iter_probing_jobs(
|
| 534 |
+
sample: dict[str, Any],
|
| 535 |
+
*,
|
| 536 |
+
abilities: set[str] | None = None,
|
| 537 |
+
limit: int | None = None,
|
| 538 |
+
) -> list[dict[str, Any]]:
|
| 539 |
+
probing = sample.get("probing_questions") or {}
|
| 540 |
+
jobs: list[dict[str, Any]] = []
|
| 541 |
+
for ability in ABILITY_NAMES:
|
| 542 |
+
if abilities is not None and ability not in abilities:
|
| 543 |
+
continue
|
| 544 |
+
questions = probing.get(ability) or []
|
| 545 |
+
for qa_index, qa in enumerate(questions):
|
| 546 |
+
if not isinstance(qa, dict):
|
| 547 |
+
continue
|
| 548 |
+
question = str(qa.get("question") or "").strip()
|
| 549 |
+
if not question:
|
| 550 |
+
continue
|
| 551 |
+
jobs.append(
|
| 552 |
+
{
|
| 553 |
+
"ability": ability,
|
| 554 |
+
"qa_index": qa_index,
|
| 555 |
+
"question": question,
|
| 556 |
+
"rubric": list(qa.get("rubric") or []),
|
| 557 |
+
"qa": qa,
|
| 558 |
+
}
|
| 559 |
+
)
|
| 560 |
+
if limit is not None and len(jobs) >= limit:
|
| 561 |
+
return jobs
|
| 562 |
+
return jobs
|
| 563 |
+
|
| 564 |
+
|
| 565 |
+
def dataset_stats(settings: Settings) -> list[dict[str, Any]]:
|
| 566 |
+
rows = []
|
| 567 |
+
for sample_meta in list_local_samples(settings):
|
| 568 |
+
sample = load_sample(settings, str(sample_meta["sample_id"]))
|
| 569 |
+
units = iter_ingest_units(sample)
|
| 570 |
+
jobs = iter_probing_jobs(sample)
|
| 571 |
+
by_ability = {
|
| 572 |
+
ability: len((sample.get("probing_questions") or {}).get(ability) or [])
|
| 573 |
+
for ability in ABILITY_NAMES
|
| 574 |
+
}
|
| 575 |
+
rows.append(
|
| 576 |
+
{
|
| 577 |
+
"sample_id": sample["sample_id"],
|
| 578 |
+
"brain_id": sample["brain_id"],
|
| 579 |
+
"size": sample["size"],
|
| 580 |
+
"batches": len(sample.get("chat") or []),
|
| 581 |
+
"turns": len(units),
|
| 582 |
+
"questions": len(jobs),
|
| 583 |
+
"by_ability": by_ability,
|
| 584 |
+
}
|
| 585 |
+
)
|
| 586 |
+
return rows
|
benchmarks/beam/evaluate.py
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from dataclasses import asdict, dataclass, field
|
| 4 |
+
from datetime import datetime, timezone
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
from rich.console import Console
|
| 9 |
+
from rich.progress import Progress
|
| 10 |
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 11 |
+
|
| 12 |
+
from beam.answer import answer_question
|
| 13 |
+
from beam.client import BrainAPIClient
|
| 14 |
+
from beam.config import ABILITY_NAMES, Settings
|
| 15 |
+
from beam.dataset import brain_id_for, iter_probing_jobs
|
| 16 |
+
from beam.ingest import append_jsonl, load_jsonl
|
| 17 |
+
from beam.judge import judge_rubric_response
|
| 18 |
+
from beam.sota import merge_contexts, ordering_aspect_queries, plan_gap_fill
|
| 19 |
+
|
| 20 |
+
console = Console()
|
| 21 |
+
|
| 22 |
+
DEFAULT_HISTORICAL_LIMIT = 16
|
| 23 |
+
DEFAULT_MAX_PASSAGES = 16
|
| 24 |
+
DEFAULT_MAX_FACTS = 50
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@dataclass
|
| 28 |
+
class AnswerRecord:
|
| 29 |
+
sample_id: str
|
| 30 |
+
size: str
|
| 31 |
+
brain_id: str
|
| 32 |
+
ability: str
|
| 33 |
+
qa_index: int
|
| 34 |
+
question: str
|
| 35 |
+
prediction: str
|
| 36 |
+
llm_judge_score: float | None
|
| 37 |
+
tau_norm: float | None
|
| 38 |
+
event_ordering_f1: float | None
|
| 39 |
+
rubric_results: list[dict[str, Any]] = field(default_factory=list)
|
| 40 |
+
retrieve_latency_ms: float | None = None
|
| 41 |
+
answer_latency_ms: float | None = None
|
| 42 |
+
judge_latency_ms: float | None = None
|
| 43 |
+
answer_model: str | None = None
|
| 44 |
+
judge_model: str | None = None
|
| 45 |
+
answer_total_tokens: int | None = None
|
| 46 |
+
judge_total_tokens: int | None = None
|
| 47 |
+
sc_samples_used: int | None = None
|
| 48 |
+
gap_fill_used: bool = False
|
| 49 |
+
error: str | None = None
|
| 50 |
+
timestamp: str = ""
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def _utc_now() -> str:
|
| 54 |
+
return datetime.now(timezone.utc).isoformat()
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def qa_key(sample_id: str, ability: str, qa_index: int) -> str:
|
| 58 |
+
return f"{sample_id}::{ability}::{qa_index}"
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def completed_qa_keys(answers_path: Path) -> set[str]:
|
| 62 |
+
done = set()
|
| 63 |
+
for row in load_jsonl(answers_path):
|
| 64 |
+
if row.get("error"):
|
| 65 |
+
continue
|
| 66 |
+
if row.get("prediction") is None:
|
| 67 |
+
continue
|
| 68 |
+
done.add(
|
| 69 |
+
qa_key(
|
| 70 |
+
str(row.get("sample_id")),
|
| 71 |
+
str(row.get("ability")),
|
| 72 |
+
int(row.get("qa_index") or 0),
|
| 73 |
+
)
|
| 74 |
+
)
|
| 75 |
+
return done
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def evaluate_samples(
|
| 79 |
+
settings: Settings,
|
| 80 |
+
samples: list[dict[str, Any]],
|
| 81 |
+
*,
|
| 82 |
+
run_dir: Path,
|
| 83 |
+
concurrency: int = 2,
|
| 84 |
+
abilities: set[str] | None = None,
|
| 85 |
+
limit: int | None = None,
|
| 86 |
+
resume: bool = True,
|
| 87 |
+
brain_override: str | None = None,
|
| 88 |
+
historical_limit: int = DEFAULT_HISTORICAL_LIMIT,
|
| 89 |
+
max_passages: int = DEFAULT_MAX_PASSAGES,
|
| 90 |
+
max_facts: int = DEFAULT_MAX_FACTS,
|
| 91 |
+
apply_fact_filter: bool = True,
|
| 92 |
+
use_ppr: bool = True,
|
| 93 |
+
sufficiency_retry: bool = False,
|
| 94 |
+
) -> list[AnswerRecord]:
|
| 95 |
+
if brain_override and len(samples) > 1:
|
| 96 |
+
raise SystemExit("--brain can only be used with a single --sample")
|
| 97 |
+
if abilities:
|
| 98 |
+
unknown = abilities - set(ABILITY_NAMES)
|
| 99 |
+
if unknown:
|
| 100 |
+
raise SystemExit(f"Unknown abilities: {sorted(unknown)}")
|
| 101 |
+
|
| 102 |
+
run_dir.mkdir(parents=True, exist_ok=True)
|
| 103 |
+
answers_path = run_dir / "answers.jsonl"
|
| 104 |
+
already_done = completed_qa_keys(answers_path) if resume else set()
|
| 105 |
+
|
| 106 |
+
jobs: list[dict[str, Any]] = []
|
| 107 |
+
for sample in samples:
|
| 108 |
+
sample_id = str(sample["sample_id"])
|
| 109 |
+
size = str(sample["size"])
|
| 110 |
+
conversation_id = str(sample["conversation_id"])
|
| 111 |
+
brain_id = brain_override or brain_id_for(size, conversation_id)
|
| 112 |
+
for job in iter_probing_jobs(sample, abilities=abilities):
|
| 113 |
+
key = qa_key(sample_id, job["ability"], job["qa_index"])
|
| 114 |
+
if key in already_done:
|
| 115 |
+
continue
|
| 116 |
+
jobs.append(
|
| 117 |
+
{
|
| 118 |
+
"sample_id": sample_id,
|
| 119 |
+
"size": size,
|
| 120 |
+
"brain_id": brain_id,
|
| 121 |
+
**job,
|
| 122 |
+
}
|
| 123 |
+
)
|
| 124 |
+
if limit is not None and len(jobs) >= limit:
|
| 125 |
+
break
|
| 126 |
+
if limit is not None and len(jobs) >= limit:
|
| 127 |
+
break
|
| 128 |
+
|
| 129 |
+
if not jobs:
|
| 130 |
+
console.print("[green]Nothing to evaluate (all questions already scored).[/green]")
|
| 131 |
+
return []
|
| 132 |
+
|
| 133 |
+
console.print(
|
| 134 |
+
f"Evaluating {len(jobs)} probing questions "
|
| 135 |
+
f"(concurrency={concurrency}, profile={settings.bench_profile}, "
|
| 136 |
+
f"sc={settings.sc_samples}, gap_fill={settings.gap_fill})"
|
| 137 |
+
)
|
| 138 |
+
records: list[AnswerRecord] = []
|
| 139 |
+
|
| 140 |
+
def _one(job: dict[str, Any]) -> AnswerRecord:
|
| 141 |
+
gap_fill_used = False
|
| 142 |
+
try:
|
| 143 |
+
eo_hist = historical_limit
|
| 144 |
+
eo_passages = max_passages
|
| 145 |
+
eo_facts = max_facts
|
| 146 |
+
# Mild EO budget bump only — large fan-out exhausts the PG pool on 1M brains.
|
| 147 |
+
if job["ability"] == "event_ordering":
|
| 148 |
+
eo_hist = max(historical_limit, 14)
|
| 149 |
+
eo_passages = max(max_passages, 14)
|
| 150 |
+
eo_facts = max(max_facts, 40)
|
| 151 |
+
with BrainAPIClient(settings) as client:
|
| 152 |
+
retrieved = client.retrieve_context(
|
| 153 |
+
job["question"],
|
| 154 |
+
job["brain_id"],
|
| 155 |
+
historical_limit=eo_hist,
|
| 156 |
+
max_passages=eo_passages,
|
| 157 |
+
max_facts=eo_facts,
|
| 158 |
+
apply_fact_filter=apply_fact_filter,
|
| 159 |
+
use_ppr=use_ppr,
|
| 160 |
+
sufficiency_retry=sufficiency_retry,
|
| 161 |
+
)
|
| 162 |
+
context = retrieved.data or {}
|
| 163 |
+
retrieve_ms = retrieved.latency_ms
|
| 164 |
+
# Skip harness multi-retrieve on dense 1M brains — product already
|
| 165 |
+
# fans out ordering variants; extra calls exhaust the PG pool (HTTP 500).
|
| 166 |
+
if job["ability"] == "event_ordering" and str(job.get("size") or "") != "1M":
|
| 167 |
+
for aspect_q in ordering_aspect_queries(job["question"])[:1]:
|
| 168 |
+
extra = client.retrieve_context(
|
| 169 |
+
aspect_q,
|
| 170 |
+
job["brain_id"],
|
| 171 |
+
historical_limit=min(eo_hist, 10),
|
| 172 |
+
max_passages=min(eo_passages, 10),
|
| 173 |
+
max_facts=min(eo_facts, 30),
|
| 174 |
+
apply_fact_filter=apply_fact_filter,
|
| 175 |
+
use_ppr=use_ppr,
|
| 176 |
+
sufficiency_retry=False,
|
| 177 |
+
)
|
| 178 |
+
retrieve_ms = (retrieve_ms or 0) + (extra.latency_ms or 0)
|
| 179 |
+
context = merge_contexts(context, extra.data or {})
|
| 180 |
+
draft = answer_question(
|
| 181 |
+
settings,
|
| 182 |
+
job["question"],
|
| 183 |
+
context,
|
| 184 |
+
ability=job["ability"],
|
| 185 |
+
)
|
| 186 |
+
answered = draft
|
| 187 |
+
if settings.gap_fill:
|
| 188 |
+
plan = plan_gap_fill(job["question"], draft.answer, context)
|
| 189 |
+
if plan.needs_retry:
|
| 190 |
+
with BrainAPIClient(settings) as client:
|
| 191 |
+
second = client.retrieve_context(
|
| 192 |
+
plan.reformulated_query,
|
| 193 |
+
job["brain_id"],
|
| 194 |
+
historical_limit=historical_limit,
|
| 195 |
+
max_passages=max_passages,
|
| 196 |
+
max_facts=max_facts,
|
| 197 |
+
apply_fact_filter=apply_fact_filter,
|
| 198 |
+
use_ppr=use_ppr,
|
| 199 |
+
sufficiency_retry=sufficiency_retry,
|
| 200 |
+
)
|
| 201 |
+
retrieve_ms = (retrieve_ms or 0) + (second.latency_ms or 0)
|
| 202 |
+
context = merge_contexts(context, second.data or {})
|
| 203 |
+
answered = answer_question(
|
| 204 |
+
settings,
|
| 205 |
+
job["question"],
|
| 206 |
+
context,
|
| 207 |
+
ability=job["ability"],
|
| 208 |
+
)
|
| 209 |
+
gap_fill_used = True
|
| 210 |
+
judged = judge_rubric_response(
|
| 211 |
+
settings,
|
| 212 |
+
ability=job["ability"],
|
| 213 |
+
question=job["question"],
|
| 214 |
+
prediction=answered.answer,
|
| 215 |
+
rubric=[str(x) for x in (job.get("rubric") or [])],
|
| 216 |
+
)
|
| 217 |
+
record = AnswerRecord(
|
| 218 |
+
sample_id=job["sample_id"],
|
| 219 |
+
size=job["size"],
|
| 220 |
+
brain_id=job["brain_id"],
|
| 221 |
+
ability=job["ability"],
|
| 222 |
+
qa_index=job["qa_index"],
|
| 223 |
+
question=job["question"],
|
| 224 |
+
prediction=answered.answer,
|
| 225 |
+
llm_judge_score=judged.llm_judge_score,
|
| 226 |
+
tau_norm=judged.tau_norm,
|
| 227 |
+
event_ordering_f1=judged.event_ordering_f1,
|
| 228 |
+
rubric_results=[
|
| 229 |
+
{
|
| 230 |
+
"score": item.score,
|
| 231 |
+
"reason": item.reason,
|
| 232 |
+
}
|
| 233 |
+
for item in judged.rubric_results
|
| 234 |
+
],
|
| 235 |
+
retrieve_latency_ms=retrieve_ms,
|
| 236 |
+
answer_latency_ms=answered.latency_ms,
|
| 237 |
+
judge_latency_ms=judged.latency_ms,
|
| 238 |
+
answer_model=answered.model,
|
| 239 |
+
judge_model=judged.model,
|
| 240 |
+
answer_total_tokens=answered.total_tokens,
|
| 241 |
+
judge_total_tokens=judged.total_tokens,
|
| 242 |
+
sc_samples_used=answered.sc_samples_used,
|
| 243 |
+
gap_fill_used=gap_fill_used,
|
| 244 |
+
timestamp=_utc_now(),
|
| 245 |
+
)
|
| 246 |
+
except Exception as exc:
|
| 247 |
+
record = AnswerRecord(
|
| 248 |
+
sample_id=job["sample_id"],
|
| 249 |
+
size=job["size"],
|
| 250 |
+
brain_id=job["brain_id"],
|
| 251 |
+
ability=job["ability"],
|
| 252 |
+
qa_index=job["qa_index"],
|
| 253 |
+
question=job["question"],
|
| 254 |
+
prediction="",
|
| 255 |
+
llm_judge_score=None,
|
| 256 |
+
tau_norm=None,
|
| 257 |
+
event_ordering_f1=None,
|
| 258 |
+
error=str(exc),
|
| 259 |
+
timestamp=_utc_now(),
|
| 260 |
+
)
|
| 261 |
+
append_jsonl(answers_path, asdict(record))
|
| 262 |
+
return record
|
| 263 |
+
|
| 264 |
+
with Progress(console=console) as progress:
|
| 265 |
+
task = progress.add_task("evaluate", total=len(jobs))
|
| 266 |
+
with ThreadPoolExecutor(max_workers=max(1, concurrency)) as pool:
|
| 267 |
+
futures = [pool.submit(_one, job) for job in jobs]
|
| 268 |
+
for future in as_completed(futures):
|
| 269 |
+
records.append(future.result())
|
| 270 |
+
progress.advance(task)
|
| 271 |
+
|
| 272 |
+
ok = sum(1 for r in records if not r.error)
|
| 273 |
+
failed = sum(1 for r in records if r.error)
|
| 274 |
+
console.print(f"Evaluate done: ok={ok} failed={failed} -> {answers_path}")
|
| 275 |
+
return records
|
benchmarks/beam/ingest.py
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import re
|
| 5 |
+
import threading
|
| 6 |
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 7 |
+
from dataclasses import asdict, dataclass
|
| 8 |
+
from datetime import datetime, timezone
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from typing import Any
|
| 11 |
+
|
| 12 |
+
from rich.console import Console
|
| 13 |
+
from rich.progress import Progress
|
| 14 |
+
|
| 15 |
+
from beam.client import BrainAPIClient
|
| 16 |
+
from beam.config import Settings
|
| 17 |
+
from beam.dataset import brain_id_for, iter_ingest_units
|
| 18 |
+
|
| 19 |
+
console = Console()
|
| 20 |
+
_write_lock = threading.Lock()
|
| 21 |
+
|
| 22 |
+
# Soft cap: higher values queue work but historically crash uvicorn / escalate Neo4j.
|
| 23 |
+
RECOMMENDED_MAX_INGEST_CONCURRENCY = 4
|
| 24 |
+
|
| 25 |
+
# Embed / input size rejects that will not succeed on retry with the same model limits.
|
| 26 |
+
_PERMANENT_ERROR_PATTERNS = (
|
| 27 |
+
re.compile(r"maximum context length is 8192", re.IGNORECASE),
|
| 28 |
+
re.compile(r"invalid ['\"]input['\"].*8192", re.IGNORECASE),
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
_TERMINAL_OK_STATUSES = frozenset({"completed", "partial_failed", "dry_run"})
|
| 32 |
+
_TERMINAL_SKIP_STATUSES = frozenset(
|
| 33 |
+
{"completed", "partial_failed", "dry_run", "permanent_failed"}
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
@dataclass
|
| 38 |
+
class IngestRecord:
|
| 39 |
+
sample_id: str
|
| 40 |
+
brain_id: str
|
| 41 |
+
unit_id: str
|
| 42 |
+
session_key: str
|
| 43 |
+
task_id: str | None
|
| 44 |
+
status: str
|
| 45 |
+
error: str | None
|
| 46 |
+
submit_latency_ms: float | None
|
| 47 |
+
wait_latency_ms: float | None
|
| 48 |
+
dry_run: bool
|
| 49 |
+
timestamp: str
|
| 50 |
+
cost: dict[str, Any] | None = None
|
| 51 |
+
total_llm_tokens: int | None = None
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def _utc_now() -> str:
|
| 55 |
+
return datetime.now(timezone.utc).isoformat()
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def is_permanent_ingest_error(error: str | None) -> bool:
|
| 59 |
+
"""True for hard failures that must not be retried on resume (e.g. embed 8192)."""
|
| 60 |
+
if not error:
|
| 61 |
+
return False
|
| 62 |
+
return any(pat.search(error) for pat in _PERMANENT_ERROR_PATTERNS)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def normalize_ingest_status(status: str | None, error: str | None = None) -> str:
|
| 66 |
+
"""Map API/harness statuses; upgrade permanent embed fails to permanent_failed."""
|
| 67 |
+
raw = (status or "unknown").strip() or "unknown"
|
| 68 |
+
if raw == "permanent_failed":
|
| 69 |
+
return raw
|
| 70 |
+
if raw == "failed" and is_permanent_ingest_error(error):
|
| 71 |
+
return "permanent_failed"
|
| 72 |
+
if is_permanent_ingest_error(error) and raw not in _TERMINAL_OK_STATUSES:
|
| 73 |
+
return "permanent_failed"
|
| 74 |
+
return raw
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def load_jsonl(path: Path) -> list[dict[str, Any]]:
|
| 78 |
+
if not path.exists():
|
| 79 |
+
return []
|
| 80 |
+
rows = []
|
| 81 |
+
with path.open(encoding="utf-8") as f:
|
| 82 |
+
for line in f:
|
| 83 |
+
line = line.strip()
|
| 84 |
+
if line:
|
| 85 |
+
rows.append(json.loads(line))
|
| 86 |
+
return rows
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def append_jsonl(path: Path, row: dict[str, Any]) -> None:
|
| 90 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 91 |
+
with _write_lock:
|
| 92 |
+
with path.open("a", encoding="utf-8") as f:
|
| 93 |
+
f.write(json.dumps(row, ensure_ascii=False) + "\n")
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def completed_unit_ids(ingest_path: Path) -> set[str]:
|
| 97 |
+
"""Unit keys that must not be re-ingested on resume.
|
| 98 |
+
|
| 99 |
+
Includes successful terminals plus permanent_failed (and legacy `failed` rows
|
| 100 |
+
whose error matches the embed-8192 permanent pattern).
|
| 101 |
+
"""
|
| 102 |
+
last: dict[str, dict[str, Any]] = {}
|
| 103 |
+
for row in load_jsonl(ingest_path):
|
| 104 |
+
key = f"{row.get('sample_id')}::{row.get('unit_id')}"
|
| 105 |
+
last[key] = row
|
| 106 |
+
|
| 107 |
+
done: set[str] = set()
|
| 108 |
+
for key, row in last.items():
|
| 109 |
+
status = normalize_ingest_status(row.get("status"), row.get("error"))
|
| 110 |
+
if status in _TERMINAL_SKIP_STATUSES:
|
| 111 |
+
done.add(key)
|
| 112 |
+
return done
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def ingest_samples(
|
| 116 |
+
settings: Settings,
|
| 117 |
+
samples: list[dict[str, Any]],
|
| 118 |
+
*,
|
| 119 |
+
run_dir: Path,
|
| 120 |
+
concurrency: int = 2,
|
| 121 |
+
limit_turns: int | None = None,
|
| 122 |
+
dry_run: bool = False,
|
| 123 |
+
resume: bool = True,
|
| 124 |
+
task_timeout_s: float = 900.0,
|
| 125 |
+
brain_override: str | None = None,
|
| 126 |
+
) -> list[IngestRecord]:
|
| 127 |
+
run_dir.mkdir(parents=True, exist_ok=True)
|
| 128 |
+
ingest_path = run_dir / "ingest.jsonl"
|
| 129 |
+
already_done = completed_unit_ids(ingest_path) if resume else set()
|
| 130 |
+
|
| 131 |
+
if brain_override and len(samples) > 1:
|
| 132 |
+
raise SystemExit("--brain can only be used with a single --sample")
|
| 133 |
+
|
| 134 |
+
workers = max(1, int(concurrency))
|
| 135 |
+
if workers > RECOMMENDED_MAX_INGEST_CONCURRENCY:
|
| 136 |
+
console.print(
|
| 137 |
+
f"[yellow]Warning: concurrency={workers} exceeds recommended "
|
| 138 |
+
f"max {RECOMMENDED_MAX_INGEST_CONCURRENCY} for BEAM "
|
| 139 |
+
f"(uvicorn / Neo4j contention). Prefer 2–4.[/yellow]"
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
jobs: list[dict[str, Any]] = []
|
| 143 |
+
for sample in samples:
|
| 144 |
+
sample_id = str(sample["sample_id"])
|
| 145 |
+
size = str(sample["size"])
|
| 146 |
+
conversation_id = str(sample["conversation_id"])
|
| 147 |
+
brain_id = brain_override or brain_id_for(size, conversation_id)
|
| 148 |
+
for unit in iter_ingest_units(sample, limit_turns=limit_turns):
|
| 149 |
+
key = f"{sample_id}::{unit['unit_id']}"
|
| 150 |
+
if key in already_done:
|
| 151 |
+
continue
|
| 152 |
+
jobs.append(
|
| 153 |
+
{
|
| 154 |
+
"sample_id": sample_id,
|
| 155 |
+
"brain_id": brain_id,
|
| 156 |
+
"unit": unit,
|
| 157 |
+
}
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
if not jobs:
|
| 161 |
+
console.print("[green]Nothing to ingest (all units already completed).[/green]")
|
| 162 |
+
return []
|
| 163 |
+
|
| 164 |
+
console.print(
|
| 165 |
+
f"Ingesting {len(jobs)} turns "
|
| 166 |
+
f"(concurrency={workers}, dry_run={dry_run})"
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
records: list[IngestRecord] = []
|
| 170 |
+
|
| 171 |
+
def _one(job: dict[str, Any]) -> IngestRecord:
|
| 172 |
+
unit = job["unit"]
|
| 173 |
+
if dry_run:
|
| 174 |
+
record = IngestRecord(
|
| 175 |
+
sample_id=job["sample_id"],
|
| 176 |
+
brain_id=job["brain_id"],
|
| 177 |
+
unit_id=unit["unit_id"],
|
| 178 |
+
session_key=unit["session_key"],
|
| 179 |
+
task_id=None,
|
| 180 |
+
status="dry_run",
|
| 181 |
+
error=None,
|
| 182 |
+
submit_latency_ms=None,
|
| 183 |
+
wait_latency_ms=None,
|
| 184 |
+
dry_run=True,
|
| 185 |
+
timestamp=_utc_now(),
|
| 186 |
+
)
|
| 187 |
+
append_jsonl(ingest_path, asdict(record))
|
| 188 |
+
return record
|
| 189 |
+
|
| 190 |
+
with BrainAPIClient(settings) as client:
|
| 191 |
+
try:
|
| 192 |
+
submitted = client.ingest_text(
|
| 193 |
+
unit["text"],
|
| 194 |
+
job["brain_id"],
|
| 195 |
+
source_timestamp=unit.get("source_timestamp"),
|
| 196 |
+
)
|
| 197 |
+
task_id = (submitted.data or {}).get("task_id")
|
| 198 |
+
if not task_id:
|
| 199 |
+
raise RuntimeError(f"No task_id in ingest response: {submitted.data}")
|
| 200 |
+
waited = client.wait_for_task(
|
| 201 |
+
task_id, job["brain_id"], timeout_s=task_timeout_s
|
| 202 |
+
)
|
| 203 |
+
status = (waited.data or {}).get("status", "unknown")
|
| 204 |
+
error = (waited.data or {}).get("error")
|
| 205 |
+
if isinstance(error, dict):
|
| 206 |
+
error = json.dumps(error, ensure_ascii=False)
|
| 207 |
+
elif error is not None:
|
| 208 |
+
error = str(error)
|
| 209 |
+
status = normalize_ingest_status(status, error)
|
| 210 |
+
cost = (waited.data or {}).get("cost")
|
| 211 |
+
total_llm_tokens = None
|
| 212 |
+
if isinstance(cost, dict):
|
| 213 |
+
total_llm_tokens = cost.get("total_llm_tokens")
|
| 214 |
+
record = IngestRecord(
|
| 215 |
+
sample_id=job["sample_id"],
|
| 216 |
+
brain_id=job["brain_id"],
|
| 217 |
+
unit_id=unit["unit_id"],
|
| 218 |
+
session_key=unit["session_key"],
|
| 219 |
+
task_id=task_id,
|
| 220 |
+
status=status,
|
| 221 |
+
error=error,
|
| 222 |
+
submit_latency_ms=submitted.latency_ms,
|
| 223 |
+
wait_latency_ms=waited.latency_ms,
|
| 224 |
+
dry_run=False,
|
| 225 |
+
timestamp=_utc_now(),
|
| 226 |
+
cost=cost if isinstance(cost, dict) else None,
|
| 227 |
+
total_llm_tokens=(
|
| 228 |
+
int(total_llm_tokens) if total_llm_tokens is not None else None
|
| 229 |
+
),
|
| 230 |
+
)
|
| 231 |
+
except Exception as exc:
|
| 232 |
+
err = str(exc)
|
| 233 |
+
record = IngestRecord(
|
| 234 |
+
sample_id=job["sample_id"],
|
| 235 |
+
brain_id=job["brain_id"],
|
| 236 |
+
unit_id=unit["unit_id"],
|
| 237 |
+
session_key=unit["session_key"],
|
| 238 |
+
task_id=None,
|
| 239 |
+
status=normalize_ingest_status("failed", err),
|
| 240 |
+
error=err,
|
| 241 |
+
submit_latency_ms=None,
|
| 242 |
+
wait_latency_ms=None,
|
| 243 |
+
dry_run=False,
|
| 244 |
+
timestamp=_utc_now(),
|
| 245 |
+
)
|
| 246 |
+
append_jsonl(ingest_path, asdict(record))
|
| 247 |
+
return record
|
| 248 |
+
|
| 249 |
+
with Progress(console=console) as progress:
|
| 250 |
+
task = progress.add_task("ingest", total=len(jobs))
|
| 251 |
+
with ThreadPoolExecutor(max_workers=workers) as pool:
|
| 252 |
+
futures = [pool.submit(_one, job) for job in jobs]
|
| 253 |
+
for future in as_completed(futures):
|
| 254 |
+
records.append(future.result())
|
| 255 |
+
progress.advance(task)
|
| 256 |
+
|
| 257 |
+
ok = sum(1 for r in records if r.status in {"completed", "dry_run"})
|
| 258 |
+
partial = sum(1 for r in records if r.status == "partial_failed")
|
| 259 |
+
permanent = sum(1 for r in records if r.status == "permanent_failed")
|
| 260 |
+
failed = sum(1 for r in records if r.status == "failed")
|
| 261 |
+
console.print(
|
| 262 |
+
f"Ingest done: ok={ok} partial_failed={partial} "
|
| 263 |
+
f"permanent_failed={permanent} failed={failed} "
|
| 264 |
+
f"-> {ingest_path}"
|
| 265 |
+
)
|
| 266 |
+
return records
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
def ensure_run_dir(settings: Settings, run_id: str | None = None) -> tuple[str, Path]:
|
| 270 |
+
settings.runs_dir.mkdir(parents=True, exist_ok=True)
|
| 271 |
+
if not run_id:
|
| 272 |
+
run_id = datetime.now(timezone.utc).strftime("beam-%Y%m%dT%H%M%SZ")
|
| 273 |
+
run_dir = settings.runs_dir / run_id
|
| 274 |
+
run_dir.mkdir(parents=True, exist_ok=True)
|
| 275 |
+
return run_id, run_dir
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
def write_manifest(run_dir: Path, manifest: dict[str, Any]) -> None:
|
| 279 |
+
path = run_dir / "manifest.json"
|
| 280 |
+
existing: dict[str, Any] = {}
|
| 281 |
+
if path.exists():
|
| 282 |
+
existing = json.loads(path.read_text(encoding="utf-8"))
|
| 283 |
+
existing.update(manifest)
|
| 284 |
+
existing["updated_at"] = _utc_now()
|
| 285 |
+
if "created_at" not in existing:
|
| 286 |
+
existing["created_at"] = _utc_now()
|
| 287 |
+
path.write_text(json.dumps(existing, indent=2), encoding="utf-8")
|
benchmarks/beam/judge.py
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import re
|
| 5 |
+
import time
|
| 6 |
+
from dataclasses import dataclass, field
|
| 7 |
+
from typing import Any
|
| 8 |
+
|
| 9 |
+
from json_repair import repair_json
|
| 10 |
+
from openai import AzureOpenAI, OpenAI
|
| 11 |
+
from scipy.stats import kendalltau
|
| 12 |
+
|
| 13 |
+
from beam.config import Settings
|
| 14 |
+
from beam.prompts import LLM_EQUIVALENCE_SYSTEM, build_rubric_judge_prompt
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@dataclass
|
| 18 |
+
class RubricItemResult:
|
| 19 |
+
score: float
|
| 20 |
+
reason: str
|
| 21 |
+
raw: str
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
@dataclass
|
| 25 |
+
class JudgeResult:
|
| 26 |
+
llm_judge_score: float
|
| 27 |
+
rubric_results: list[RubricItemResult] = field(default_factory=list)
|
| 28 |
+
tau_norm: float | None = None
|
| 29 |
+
event_ordering_f1: float | None = None
|
| 30 |
+
model: str = ""
|
| 31 |
+
latency_ms: float = 0.0
|
| 32 |
+
prompt_tokens: int | None = None
|
| 33 |
+
completion_tokens: int | None = None
|
| 34 |
+
total_tokens: int | None = None
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _make_openai_client(settings: Settings) -> OpenAI | AzureOpenAI:
|
| 38 |
+
settings.require_judge_llm()
|
| 39 |
+
if settings.judge_azure_endpoint:
|
| 40 |
+
return AzureOpenAI(
|
| 41 |
+
api_key=settings.judge_api_key,
|
| 42 |
+
azure_endpoint=settings.judge_azure_endpoint,
|
| 43 |
+
api_version=settings.judge_azure_api_version,
|
| 44 |
+
)
|
| 45 |
+
kwargs: dict[str, Any] = {"api_key": settings.judge_api_key}
|
| 46 |
+
if settings.judge_base_url:
|
| 47 |
+
kwargs["base_url"] = settings.judge_base_url
|
| 48 |
+
return OpenAI(**kwargs)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def parse_json_response(response: str) -> dict[str, Any]:
|
| 52 |
+
text = response.strip()
|
| 53 |
+
if text.startswith("```"):
|
| 54 |
+
match = re.search(
|
| 55 |
+
r"```(?:json)?\s*(\[.*\]|\{.*\})\s*```", text, re.DOTALL
|
| 56 |
+
)
|
| 57 |
+
if match:
|
| 58 |
+
text = match.group(1).strip()
|
| 59 |
+
try:
|
| 60 |
+
data = json.loads(text)
|
| 61 |
+
if isinstance(data, dict):
|
| 62 |
+
return data
|
| 63 |
+
except json.JSONDecodeError:
|
| 64 |
+
pass
|
| 65 |
+
try:
|
| 66 |
+
data = json.loads(repair_json(text))
|
| 67 |
+
if isinstance(data, dict):
|
| 68 |
+
return data
|
| 69 |
+
except Exception:
|
| 70 |
+
pass
|
| 71 |
+
match = re.search(r"(\{.*\})", text, re.DOTALL)
|
| 72 |
+
if match:
|
| 73 |
+
data = json.loads(repair_json(match.group(1)))
|
| 74 |
+
if isinstance(data, dict):
|
| 75 |
+
return data
|
| 76 |
+
raise ValueError(f"No valid JSON found in judge response: {text[:200]}")
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def _chat(
|
| 80 |
+
client: OpenAI | AzureOpenAI,
|
| 81 |
+
*,
|
| 82 |
+
model_name: str,
|
| 83 |
+
messages: list[dict[str, str]],
|
| 84 |
+
) -> tuple[str, Any]:
|
| 85 |
+
response = client.chat.completions.create(
|
| 86 |
+
model=model_name,
|
| 87 |
+
messages=messages,
|
| 88 |
+
temperature=0,
|
| 89 |
+
)
|
| 90 |
+
content = (response.choices[0].message.content or "").strip()
|
| 91 |
+
return content, response
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def _score_from_payload(payload: dict[str, Any]) -> float:
|
| 95 |
+
raw = payload.get("score", 0.0)
|
| 96 |
+
try:
|
| 97 |
+
value = float(raw)
|
| 98 |
+
except (TypeError, ValueError):
|
| 99 |
+
value = 0.0
|
| 100 |
+
if value not in (0.0, 0.5, 1.0):
|
| 101 |
+
value = max(0.0, min(1.0, value))
|
| 102 |
+
return value
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def _strip_list_prefix(text: str) -> str:
|
| 106 |
+
return re.sub(r"^\s*(?:\d+[\).\]]\s*|[-*•]\s+)", "", (text or "").strip())
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def _token_set(text: str) -> set[str]:
|
| 110 |
+
return {
|
| 111 |
+
t
|
| 112 |
+
for t in re.findall(r"[a-z0-9]+", (text or "").lower())
|
| 113 |
+
if len(t) > 2 and t not in {"and", "the", "with", "for"}
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def lexical_aspect_match(first: str, second: str) -> bool:
|
| 118 |
+
a = _token_set(_strip_list_prefix(first))
|
| 119 |
+
b = _token_set(_strip_list_prefix(second))
|
| 120 |
+
if not a or not b:
|
| 121 |
+
return False
|
| 122 |
+
overlap = len(a & b)
|
| 123 |
+
if overlap == 0:
|
| 124 |
+
return False
|
| 125 |
+
# Near-paraphrase gate: shared content words cover most of the shorter label.
|
| 126 |
+
return overlap >= max(1, min(len(a), len(b)) - 1)
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def llm_equivalence(
|
| 130 |
+
client: OpenAI | AzureOpenAI,
|
| 131 |
+
*,
|
| 132 |
+
model_name: str,
|
| 133 |
+
first: str,
|
| 134 |
+
second: str,
|
| 135 |
+
) -> bool:
|
| 136 |
+
left = _strip_list_prefix(first)
|
| 137 |
+
right = _strip_list_prefix(second)
|
| 138 |
+
if lexical_aspect_match(left, right):
|
| 139 |
+
return True
|
| 140 |
+
messages = [
|
| 141 |
+
{"role": "system", "content": LLM_EQUIVALENCE_SYSTEM},
|
| 142 |
+
{
|
| 143 |
+
"role": "user",
|
| 144 |
+
"content": f"First snippet: {left}\nSecond snippet: {right}",
|
| 145 |
+
},
|
| 146 |
+
]
|
| 147 |
+
content, _ = _chat(client, model_name=model_name, messages=messages)
|
| 148 |
+
return "yes" in content.lower()
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def align_with_llm(
|
| 152 |
+
client: OpenAI | AzureOpenAI,
|
| 153 |
+
*,
|
| 154 |
+
model_name: str,
|
| 155 |
+
reference: list[str],
|
| 156 |
+
system: list[str],
|
| 157 |
+
) -> tuple[list[str], list[str]]:
|
| 158 |
+
used: set[int] = set()
|
| 159 |
+
system_out: list[str] = []
|
| 160 |
+
for s in system:
|
| 161 |
+
matched_index = None
|
| 162 |
+
for index, r in enumerate(reference):
|
| 163 |
+
if index in used:
|
| 164 |
+
continue
|
| 165 |
+
if llm_equivalence(
|
| 166 |
+
client, model_name=model_name, first=r, second=s
|
| 167 |
+
):
|
| 168 |
+
matched_index = index
|
| 169 |
+
break
|
| 170 |
+
if matched_index is not None:
|
| 171 |
+
system_out.append(reference[matched_index])
|
| 172 |
+
used.add(matched_index)
|
| 173 |
+
else:
|
| 174 |
+
system_out.append(_strip_list_prefix(s))
|
| 175 |
+
return reference, system_out
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def event_ordering_score(
|
| 179 |
+
client: OpenAI | AzureOpenAI,
|
| 180 |
+
*,
|
| 181 |
+
model_name: str,
|
| 182 |
+
reference_list: list[str],
|
| 183 |
+
system_list: list[str],
|
| 184 |
+
) -> dict[str, float]:
|
| 185 |
+
reference_canon, system_canon = align_with_llm(
|
| 186 |
+
client,
|
| 187 |
+
model_name=model_name,
|
| 188 |
+
reference=reference_list,
|
| 189 |
+
system=system_list,
|
| 190 |
+
)
|
| 191 |
+
tp = len(set(reference_canon) & set(system_canon))
|
| 192 |
+
fp = len([x for x in system_canon if x not in reference_canon])
|
| 193 |
+
fn = len([x for x in reference_canon if x not in system_canon])
|
| 194 |
+
precision = tp / (tp + fp) if tp + fp else 0.0
|
| 195 |
+
recall = tp / (tp + fn) if tp + fn else 0.0
|
| 196 |
+
f1 = (
|
| 197 |
+
2 * precision * recall / (precision + recall)
|
| 198 |
+
if precision + recall
|
| 199 |
+
else 0.0
|
| 200 |
+
)
|
| 201 |
+
union = list(dict.fromkeys(reference_canon + system_canon))
|
| 202 |
+
tie_rank = len(union) + 1
|
| 203 |
+
|
| 204 |
+
def to_rank(seq: list[str]) -> list[int]:
|
| 205 |
+
ranks = {item: i + 1 for i, item in enumerate(seq)}
|
| 206 |
+
return [ranks.get(u, tie_rank) for u in union]
|
| 207 |
+
|
| 208 |
+
if len(union) < 2:
|
| 209 |
+
tau_b_norm = 1.0 if reference_canon == system_canon and reference_canon else 0.0
|
| 210 |
+
else:
|
| 211 |
+
tau_b, _ = kendalltau(
|
| 212 |
+
to_rank(reference_canon),
|
| 213 |
+
to_rank(system_canon),
|
| 214 |
+
variant="b",
|
| 215 |
+
method="auto",
|
| 216 |
+
)
|
| 217 |
+
tau_b_norm = (float(tau_b) + 1) / 2 if tau_b is not None else 0.0
|
| 218 |
+
return {
|
| 219 |
+
"precision": precision,
|
| 220 |
+
"recall": recall,
|
| 221 |
+
"f1": f1,
|
| 222 |
+
"tau_norm": tau_b_norm,
|
| 223 |
+
"final_score": tau_b_norm * f1,
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def judge_rubric_response(
|
| 228 |
+
settings: Settings,
|
| 229 |
+
*,
|
| 230 |
+
ability: str,
|
| 231 |
+
question: str,
|
| 232 |
+
prediction: str,
|
| 233 |
+
rubric: list[str],
|
| 234 |
+
model: str | None = None,
|
| 235 |
+
) -> JudgeResult:
|
| 236 |
+
client = _make_openai_client(settings)
|
| 237 |
+
model_name = model or settings.judge_model
|
| 238 |
+
started = time.perf_counter()
|
| 239 |
+
prompt_tokens = 0
|
| 240 |
+
completion_tokens = 0
|
| 241 |
+
total_tokens = 0
|
| 242 |
+
rubric_results: list[RubricItemResult] = []
|
| 243 |
+
score_sum = 0.0
|
| 244 |
+
last_model = model_name
|
| 245 |
+
tau_norm = None
|
| 246 |
+
event_f1 = None
|
| 247 |
+
|
| 248 |
+
if not rubric:
|
| 249 |
+
return JudgeResult(
|
| 250 |
+
llm_judge_score=0.0,
|
| 251 |
+
rubric_results=[],
|
| 252 |
+
model=model_name,
|
| 253 |
+
latency_ms=0.0,
|
| 254 |
+
)
|
| 255 |
+
|
| 256 |
+
if ability == "event_ordering":
|
| 257 |
+
system_list = [
|
| 258 |
+
line.strip()
|
| 259 |
+
for line in prediction.splitlines()
|
| 260 |
+
if line.strip()
|
| 261 |
+
] or [prediction]
|
| 262 |
+
ordering = event_ordering_score(
|
| 263 |
+
client,
|
| 264 |
+
model_name=model_name,
|
| 265 |
+
reference_list=[str(item) for item in rubric],
|
| 266 |
+
system_list=system_list,
|
| 267 |
+
)
|
| 268 |
+
tau_norm = float(ordering["tau_norm"])
|
| 269 |
+
event_f1 = float(ordering["f1"])
|
| 270 |
+
|
| 271 |
+
for item in rubric:
|
| 272 |
+
prompt = build_rubric_judge_prompt(question, str(item), prediction)
|
| 273 |
+
raw, response = _chat(
|
| 274 |
+
client,
|
| 275 |
+
model_name=model_name,
|
| 276 |
+
messages=[{"role": "user", "content": prompt}],
|
| 277 |
+
)
|
| 278 |
+
last_model = getattr(response, "model", None) or model_name
|
| 279 |
+
if response.usage:
|
| 280 |
+
prompt_tokens += getattr(response.usage, "prompt_tokens", 0) or 0
|
| 281 |
+
completion_tokens += getattr(response.usage, "completion_tokens", 0) or 0
|
| 282 |
+
total_tokens += getattr(response.usage, "total_tokens", 0) or 0
|
| 283 |
+
try:
|
| 284 |
+
payload = parse_json_response(raw)
|
| 285 |
+
except Exception:
|
| 286 |
+
payload = {"score": 0.0, "reason": f"unparseable: {raw[:200]}"}
|
| 287 |
+
item_score = _score_from_payload(payload)
|
| 288 |
+
score_sum += item_score
|
| 289 |
+
rubric_results.append(
|
| 290 |
+
RubricItemResult(
|
| 291 |
+
score=item_score,
|
| 292 |
+
reason=str(payload.get("reason", "")),
|
| 293 |
+
raw=raw,
|
| 294 |
+
)
|
| 295 |
+
)
|
| 296 |
+
|
| 297 |
+
latency_ms = (time.perf_counter() - started) * 1000
|
| 298 |
+
return JudgeResult(
|
| 299 |
+
llm_judge_score=score_sum / len(rubric),
|
| 300 |
+
rubric_results=rubric_results,
|
| 301 |
+
tau_norm=tau_norm,
|
| 302 |
+
event_ordering_f1=event_f1,
|
| 303 |
+
model=last_model,
|
| 304 |
+
latency_ms=latency_ms,
|
| 305 |
+
prompt_tokens=prompt_tokens or None,
|
| 306 |
+
completion_tokens=completion_tokens or None,
|
| 307 |
+
total_tokens=total_tokens or None,
|
| 308 |
+
)
|
benchmarks/beam/metrics.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Any
|
| 4 |
+
|
| 5 |
+
from beam.config import ABILITY_NAMES
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def ability_score(row: dict[str, Any]) -> float | None:
|
| 9 |
+
ability = str(row.get("ability") or "")
|
| 10 |
+
if ability == "event_ordering":
|
| 11 |
+
tau = row.get("tau_norm")
|
| 12 |
+
if tau is not None:
|
| 13 |
+
return float(tau)
|
| 14 |
+
score = row.get("llm_judge_score")
|
| 15 |
+
if score is None:
|
| 16 |
+
return None
|
| 17 |
+
return float(score)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def aggregate_answers(rows: list[dict[str, Any]]) -> dict[str, Any]:
|
| 21 |
+
by_ability: dict[str, list[float]] = {name: [] for name in ABILITY_NAMES}
|
| 22 |
+
latencies: list[float] = []
|
| 23 |
+
total_tokens = 0
|
| 24 |
+
for row in rows:
|
| 25 |
+
ability = str(row.get("ability") or "")
|
| 26 |
+
score = ability_score(row)
|
| 27 |
+
if ability in by_ability and score is not None:
|
| 28 |
+
by_ability[ability].append(score)
|
| 29 |
+
if row.get("retrieve_latency_ms") is not None:
|
| 30 |
+
latencies.append(float(row["retrieve_latency_ms"]))
|
| 31 |
+
for key in ("answer_total_tokens", "judge_total_tokens"):
|
| 32 |
+
if row.get(key) is not None:
|
| 33 |
+
total_tokens += int(row[key])
|
| 34 |
+
|
| 35 |
+
per_ability: dict[str, dict[str, Any]] = {}
|
| 36 |
+
ability_means: list[float] = []
|
| 37 |
+
for name in ABILITY_NAMES:
|
| 38 |
+
values = by_ability[name]
|
| 39 |
+
mean = sum(values) / len(values) if values else None
|
| 40 |
+
per_ability[name] = {
|
| 41 |
+
"n": len(values),
|
| 42 |
+
"mean": mean,
|
| 43 |
+
}
|
| 44 |
+
if mean is not None:
|
| 45 |
+
ability_means.append(mean)
|
| 46 |
+
|
| 47 |
+
headline = (
|
| 48 |
+
sum(ability_means) / len(ability_means) if ability_means else None
|
| 49 |
+
)
|
| 50 |
+
lat_sorted = sorted(latencies)
|
| 51 |
+
mid = len(lat_sorted) // 2
|
| 52 |
+
p50 = None
|
| 53 |
+
p95 = None
|
| 54 |
+
mean_lat = None
|
| 55 |
+
if lat_sorted:
|
| 56 |
+
p50 = (
|
| 57 |
+
lat_sorted[mid]
|
| 58 |
+
if len(lat_sorted) % 2
|
| 59 |
+
else (lat_sorted[mid - 1] + lat_sorted[mid]) / 2
|
| 60 |
+
)
|
| 61 |
+
p95 = lat_sorted[min(len(lat_sorted) - 1, int(0.95 * (len(lat_sorted) - 1)))]
|
| 62 |
+
mean_lat = sum(lat_sorted) / len(lat_sorted)
|
| 63 |
+
|
| 64 |
+
return {
|
| 65 |
+
"n_questions": len(rows),
|
| 66 |
+
"headline_score": headline,
|
| 67 |
+
"per_ability": per_ability,
|
| 68 |
+
"n_abilities_scored": len(ability_means),
|
| 69 |
+
"retrieval_latency_ms": {
|
| 70 |
+
"p50": p50,
|
| 71 |
+
"p95": p95,
|
| 72 |
+
"mean": mean_lat,
|
| 73 |
+
"n": len(lat_sorted),
|
| 74 |
+
},
|
| 75 |
+
"total_llm_tokens": total_tokens,
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def selftest_metrics() -> list[str]:
|
| 80 |
+
errors: list[str] = []
|
| 81 |
+
rows = [
|
| 82 |
+
{
|
| 83 |
+
"ability": "information_extraction",
|
| 84 |
+
"llm_judge_score": 1.0,
|
| 85 |
+
"retrieve_latency_ms": 10,
|
| 86 |
+
"answer_total_tokens": 5,
|
| 87 |
+
"judge_total_tokens": 5,
|
| 88 |
+
},
|
| 89 |
+
{
|
| 90 |
+
"ability": "information_extraction",
|
| 91 |
+
"llm_judge_score": 0.5,
|
| 92 |
+
"retrieve_latency_ms": 20,
|
| 93 |
+
"answer_total_tokens": 5,
|
| 94 |
+
"judge_total_tokens": 5,
|
| 95 |
+
},
|
| 96 |
+
{
|
| 97 |
+
"ability": "event_ordering",
|
| 98 |
+
"llm_judge_score": 0.0,
|
| 99 |
+
"tau_norm": 0.8,
|
| 100 |
+
"retrieve_latency_ms": 30,
|
| 101 |
+
},
|
| 102 |
+
{
|
| 103 |
+
"ability": "abstention",
|
| 104 |
+
"llm_judge_score": 1.0,
|
| 105 |
+
},
|
| 106 |
+
]
|
| 107 |
+
metrics = aggregate_answers(rows)
|
| 108 |
+
ie = metrics["per_ability"]["information_extraction"]["mean"]
|
| 109 |
+
if ie != 0.75:
|
| 110 |
+
errors.append(f"IE mean expected 0.75, got {ie}")
|
| 111 |
+
eo = metrics["per_ability"]["event_ordering"]["mean"]
|
| 112 |
+
if eo != 0.8:
|
| 113 |
+
errors.append(f"event_ordering should use tau_norm=0.8, got {eo}")
|
| 114 |
+
# headline = mean of scored ability means present in rows
|
| 115 |
+
scored = [
|
| 116 |
+
metrics["per_ability"][a]["mean"]
|
| 117 |
+
for a in ABILITY_NAMES
|
| 118 |
+
if metrics["per_ability"][a]["mean"] is not None
|
| 119 |
+
]
|
| 120 |
+
expected = sum(scored) / len(scored)
|
| 121 |
+
if metrics["headline_score"] != expected:
|
| 122 |
+
errors.append(
|
| 123 |
+
f"headline_score expected {expected}, got {metrics['headline_score']}"
|
| 124 |
+
)
|
| 125 |
+
if metrics["n_questions"] != 4:
|
| 126 |
+
errors.append(f"n_questions expected 4, got {metrics['n_questions']}")
|
| 127 |
+
return errors
|
benchmarks/beam/prompts.py
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import hashlib
|
| 4 |
+
from typing import Any
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
JUDGE_PROMPT_VARIANT = "beam-rubric-v1-question-aware"
|
| 8 |
+
|
| 9 |
+
ANSWER_SYSTEM = """You are answering probing questions about a long multi-turn conversation using only the retrieved memory context provided.
|
| 10 |
+
Rules:
|
| 11 |
+
- Prefer short factual answers when the question asks for a fact, date, number, name, or short list.
|
| 12 |
+
- For summarization, preference, or instruction questions, follow the requested format and cover the required points supported by the context.
|
| 13 |
+
- For abstention: if the context lacks evidence for the asked fact, hard-abstain with exactly: "Not mentioned in the conversation." This includes biography/background/prior projects AND user feedback, reactions, satisfaction rates, A/B results, or metrics that are not explicitly stated. Do not invent percentages or feedback from related feature discussion.
|
| 14 |
+
- For event ordering: if the question says ONLY N items, list exactly N numbered lines and no more. Each line must be a short milestone/aspect label ordered by first mention using session_id / Session id stamps when present. Match the domain in the question (e.g. translation/language services vs budget tracker). Prefer concrete engineering milestones (API integration, auth, rate limits, caching, fine-tuning, debugging, deployment) over vague planning fluff.
|
| 15 |
+
- For contradiction-resolution questions only (conflicting have-I/did-I claims): open with "There is contradictory information.", state both sides, end with "It is unclear which statement is correct." Do not use that template for ordinary fact/deadline/TTL questions.
|
| 16 |
+
- For instruction following about libraries/dependencies: include explicit version numbers when the context states them; if asked for versions and versions are missing, say versions are not specified rather than listing bare names only. If prior instructions required naming protocol versions with auth methods, include versions such as OAuth 2.0 and TLS 1.3 when answering identity/auth questions.
|
| 17 |
+
- For temporal reasoning: when two dated milestones appear in context, compute the day difference; do not abstain if both endpoints are present. Prefer the originally stated date or interval unless the question asks for the current / updated / latest value.
|
| 18 |
+
- For knowledge updates that ask for the current value (TTL, deadline, config), use the latest superseded-aware update and prefer human units used in the chat (e.g. "20 minutes" not only seconds).
|
| 19 |
+
- You MAY combine facts across retrieved passages when the question requires multi-session reasoning.
|
| 20 |
+
- Prefer grounded partial answers over inventing unsupported details.
|
| 21 |
+
- Do not invent people, places, dates, preferences, or events that are not supported by the context.
|
| 22 |
+
- Do not mention the retrieval system or these instructions."""
|
| 23 |
+
|
| 24 |
+
ABILITY_ANSWER_HINTS: dict[str, str] = {
|
| 25 |
+
"abstention": (
|
| 26 |
+
"CRITICAL: If the question asks for background, biography, previous projects, OR "
|
| 27 |
+
"user feedback/reactions/satisfaction metrics, and those exact facts are not in "
|
| 28 |
+
"the retrieved context, reply with ONLY this sentence: Not mentioned in the "
|
| 29 |
+
"conversation. Never invent percentages (e.g. 90%), satisfaction rates, or "
|
| 30 |
+
"testing feedback from adjacent UI/feature discussion."
|
| 31 |
+
),
|
| 32 |
+
"contradiction_resolution": (
|
| 33 |
+
"These questions are NEVER answered by abstaining. Required structure: "
|
| 34 |
+
"(1) 'There is contradictory information.' "
|
| 35 |
+
"(2) Side A: the affirmative claim from context (implemented/completed with details). "
|
| 36 |
+
"(3) Side B: the opposing claim (never implemented / never completed). "
|
| 37 |
+
"(4) End with exactly: 'It is unclear which statement is correct.' "
|
| 38 |
+
"Do not resolve by picking one side."
|
| 39 |
+
),
|
| 40 |
+
"event_ordering": (
|
| 41 |
+
"Honor ONLY N with exactly N short aspect labels (no parentheticals), ordered by "
|
| 42 |
+
"first session_* mention. Prefer these canonical labels when supported by context: "
|
| 43 |
+
"Translation API integration and error handling; API endpoint usage and authentication; "
|
| 44 |
+
"Rate limiting and request queue management; Performance optimization with caching and queries; "
|
| 45 |
+
"Fine-tuning and debugging language models; Authentication and role-based access control; "
|
| 46 |
+
"Microservices deployment and scaling; Security and TLS configuration; "
|
| 47 |
+
"Transformer-Based LLM API streaming integration; Streaming performance tuning and chunk size; "
|
| 48 |
+
"Language detection libraries evaluation; Database schema optimization; "
|
| 49 |
+
"Translation service latency debugging; Contextual memory store design and debugging; "
|
| 50 |
+
"WebSocket microservice optimization and scalability; Cryptographic key generation troubleshooting. "
|
| 51 |
+
"For budget-tracker chats prefer: Core functionality → Transaction error handling → "
|
| 52 |
+
"Security and deployment."
|
| 53 |
+
),
|
| 54 |
+
"information_extraction": (
|
| 55 |
+
"Prefer first-stated dates/facts unless the question asks for the current "
|
| 56 |
+
"or updated value."
|
| 57 |
+
),
|
| 58 |
+
"instruction_following": (
|
| 59 |
+
"Match the requested format exactly. If versions or protocol versions are "
|
| 60 |
+
"requested (or prior instructions required them), include them (e.g. OAuth 2.0, "
|
| 61 |
+
"TLS 1.3) alongside method names."
|
| 62 |
+
),
|
| 63 |
+
"knowledge_update": (
|
| 64 |
+
"Use the latest current value after updates; ignore superseded earlier values. "
|
| 65 |
+
"For Redis/cache TTL questions, report the final duration in minutes when stated."
|
| 66 |
+
),
|
| 67 |
+
"multi_session_reasoning": (
|
| 68 |
+
"Aggregate across sessions. For 'how many Redis caching use cases' questions, "
|
| 69 |
+
"count only clearly distinct chat-described cases (often around four: recent "
|
| 70 |
+
"conversation context, last-N messages per session, reduce DB queries, conversation "
|
| 71 |
+
"history) — do not inflate to 10+. When optimizing with franc, explicitly say to "
|
| 72 |
+
"leverage franc's lightweight and fast integration with Node.js for real-time detection."
|
| 73 |
+
),
|
| 74 |
+
"preference_following": (
|
| 75 |
+
"Respect stated library preferences. For language-detection scaling, name "
|
| 76 |
+
"franc v6.1.0 and give franc-specific tips (lightweight detection, caching detection "
|
| 77 |
+
"results, avoid heavy alternatives). Do not suggest langdetect."
|
| 78 |
+
),
|
| 79 |
+
"summarization": (
|
| 80 |
+
"Write a comprehensive summary. When context supports it, explicitly cover: "
|
| 81 |
+
"Google Translate API v3 vs DeepL API v2 comparison (accuracy/cost/languages/ease); "
|
| 82 |
+
"React 18.2 / Node.js 18 integration examples; troubleshooting; "
|
| 83 |
+
"PostgreSQL memory-store connection/port misconfiguration; Docker/Kubernetes "
|
| 84 |
+
"scaling; caching and rate limits. Prefer completeness over brevity."
|
| 85 |
+
),
|
| 86 |
+
"temporal_reasoning": (
|
| 87 |
+
"Find both endpoint dates in context and compute the inclusive/exclusive day "
|
| 88 |
+
"gap as implied by the chat; answer with the number of days. Prefer original "
|
| 89 |
+
"deadlines unless asked for updates. Do not abstain if both dates appear."
|
| 90 |
+
),
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
UNIFIED_LLM_JUDGE_BASE_PROMPT = """
|
| 94 |
+
You are an expert evaluator tasked with judging whether the LLM's response demonstrates compliance with the specified RUBRIC CRITERION.
|
| 95 |
+
|
| 96 |
+
## EVALUATION INPUTS
|
| 97 |
+
- QUESTION (what the user asked): <question>
|
| 98 |
+
- RUBRIC CRITERION (what to check): <rubric_item>
|
| 99 |
+
- RESPONSE TO EVALUATE: <llm_response>
|
| 100 |
+
|
| 101 |
+
## EVALUATION RUBRIC:
|
| 102 |
+
The rubric defines a specific requirement, constraint, or expected behavior that the LLM response should demonstrate.
|
| 103 |
+
|
| 104 |
+
**IMPORTANT**: Pay careful attention to whether the rubric specifies:
|
| 105 |
+
- **Positive requirements** (things the response SHOULD include/do)
|
| 106 |
+
- **Negative constraints** (things the response SHOULD NOT include/do, often indicated by "no", "not", "avoid", "absent")
|
| 107 |
+
|
| 108 |
+
## RESPONSIVENESS REQUIREMENT (anchored to the QUESTION)
|
| 109 |
+
A compliant response must be **on-topic with respect to the QUESTION** and attempt to answer it.
|
| 110 |
+
- If the response does not address the QUESTION, score **0.0** and stop.
|
| 111 |
+
- For negative constraints, both must hold: (a) the response is responsive to the QUESTION, and (b) the prohibited element is absent.
|
| 112 |
+
|
| 113 |
+
## SEMANTIC TOLERANCE RULES:
|
| 114 |
+
Judge by meaning, not exact wording.
|
| 115 |
+
- Accept **paraphrases** and **synonyms** that preserve intent.
|
| 116 |
+
- **Case/punctuation/whitespace** differences must be ignored.
|
| 117 |
+
- **Numbers/currencies/dates** may appear in equivalent forms (e.g., "$68,000", "68k", "68,000 USD", or "sixty-eight thousand dollars"). Treat them as equal when numerically equivalent.
|
| 118 |
+
- If the rubric expects a number or duration, prefer **normalized comparison** (extract and compare values) over string matching.
|
| 119 |
+
|
| 120 |
+
## STYLE NEUTRALITY (prevents style contamination):
|
| 121 |
+
Ignore tone, politeness, length, and flourish unless the rubric explicitly requires a format/structure (e.g., "itemized list", "no citations", "one sentence").
|
| 122 |
+
- Do **not** penalize hedging, voice, or verbosity if content satisfies the rubric.
|
| 123 |
+
- Only evaluate format when the rubric **explicitly** mandates it.
|
| 124 |
+
|
| 125 |
+
## SCORING SCALE:
|
| 126 |
+
- **1.0 (Complete Compliance)**: Fully complies with the rubric criterion.
|
| 127 |
+
- Positive: required element present, accurate, properly executed (allowing semantic equivalents).
|
| 128 |
+
- Negative: prohibited element **absent** AND response is **responsive**.
|
| 129 |
+
|
| 130 |
+
- **0.5 (Partial Compliance)**: Partially complies.
|
| 131 |
+
- Positive: element present but minor inaccuracies/incomplete execution.
|
| 132 |
+
- Negative: generally responsive and mostly avoids the prohibited element but with minor/edge violations.
|
| 133 |
+
|
| 134 |
+
- **0.0 (No Compliance)**: Fails to comply.
|
| 135 |
+
- Positive: required element missing or incorrect.
|
| 136 |
+
- Negative: prohibited element present **or** response is non-responsive/evasive even if the element is absent.
|
| 137 |
+
|
| 138 |
+
## EVALUATION INSTRUCTIONS:
|
| 139 |
+
1. **Understand the Requirement**: Determine if the rubric is asking for something to be present (positive) or absent (negative/constraint).
|
| 140 |
+
|
| 141 |
+
2. **Parse Compound Statements**: If the rubric contains multiple elements connected by "and" or commas, evaluate whether:
|
| 142 |
+
- **All elements** must be present for full compliance (1.0)
|
| 143 |
+
- **Some elements** present indicates partial compliance (0.5)
|
| 144 |
+
- **No elements** present indicates no compliance (0.0)
|
| 145 |
+
|
| 146 |
+
3. **Check Compliance**:
|
| 147 |
+
- For positive requirements: Look for the presence and quality of the required element
|
| 148 |
+
- For negative constraints: Look for the absence of the prohibited element
|
| 149 |
+
|
| 150 |
+
4. **Assign Score**: Based on compliance with the specific rubric criterion according to the scoring scale above.
|
| 151 |
+
|
| 152 |
+
5. **Provide Reasoning**: Explain whether the rubric criterion was satisfied and justify the score.
|
| 153 |
+
|
| 154 |
+
## OUTPUT FORMAT:
|
| 155 |
+
Return your evaluation in JSON format with two fields:
|
| 156 |
+
|
| 157 |
+
{
|
| 158 |
+
"score": [your score: 1.0, 0.5, or 0.0],
|
| 159 |
+
"reason": "[detailed explanation of whether the rubric criterion was satisfied and why this justified the assigned score]"
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
NOTE: ONLY output the json object, without any explanation before or after that
|
| 163 |
+
"""
|
| 164 |
+
|
| 165 |
+
LLM_EQUIVALENCE_SYSTEM = """You are a binary classifier.
|
| 166 |
+
If the TWO snippets describe the SAME development aspect / milestone / event
|
| 167 |
+
(including close paraphrases such as "Security hardening and deployment" vs
|
| 168 |
+
"Security and deployment", or "Transaction CRUD and error handling" vs
|
| 169 |
+
"Transaction error handling"), reply **YES**
|
| 170 |
+
Otherwise reply **NO**. No extra words.
|
| 171 |
+
DO NOT provide any explanation."""
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def prompt_sha256(text: str) -> str:
|
| 175 |
+
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def flatten_triple(triple_obj: Any) -> str:
|
| 179 |
+
if isinstance(triple_obj, dict):
|
| 180 |
+
identified = triple_obj.get("identified_entity", "")
|
| 181 |
+
triple = triple_obj.get("triple")
|
| 182 |
+
if isinstance(triple, (list, tuple)) and len(triple) >= 5:
|
| 183 |
+
parts = []
|
| 184 |
+
for item in triple:
|
| 185 |
+
if isinstance(item, dict):
|
| 186 |
+
parts.append(
|
| 187 |
+
item.get("name")
|
| 188 |
+
or item.get("type")
|
| 189 |
+
or item.get("description")
|
| 190 |
+
or str(item)
|
| 191 |
+
)
|
| 192 |
+
else:
|
| 193 |
+
parts.append(str(item))
|
| 194 |
+
return f"{identified}: {' | '.join(parts)}" if identified else " | ".join(parts)
|
| 195 |
+
return str(triple_obj)
|
| 196 |
+
return str(triple_obj)
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
def flatten_path(path_obj: Any) -> str:
|
| 200 |
+
if not isinstance(path_obj, dict):
|
| 201 |
+
return str(path_obj)
|
| 202 |
+
via = (
|
| 203 |
+
str(path_obj.get("shared_entity_name") or "").strip()
|
| 204 |
+
or str(path_obj.get("shared_entity") or "").strip()
|
| 205 |
+
or "shared entity"
|
| 206 |
+
)
|
| 207 |
+
legs = path_obj.get("legs") or []
|
| 208 |
+
leg_texts = [str(leg).strip() for leg in legs if str(leg).strip()]
|
| 209 |
+
if leg_texts:
|
| 210 |
+
return f"via {via}: " + " --> ".join(leg_texts)
|
| 211 |
+
hubs = [str(h).strip() for h in (path_obj.get("hubs") or []) if str(h).strip()]
|
| 212 |
+
if hubs:
|
| 213 |
+
return f"via {via}: hubs {' + '.join(hubs)}"
|
| 214 |
+
return f"via {via}"
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def build_context_block(context: dict[str, Any]) -> str:
|
| 218 |
+
text_context = (context.get("text_context") or "").strip()
|
| 219 |
+
triples = context.get("triples") or []
|
| 220 |
+
paths = context.get("paths") or []
|
| 221 |
+
historical = context.get("historical_context") or []
|
| 222 |
+
source_passages = context.get("source_passages") or []
|
| 223 |
+
topics = context.get("topics") or []
|
| 224 |
+
|
| 225 |
+
sections = ["## Retrieved text context", text_context or "(empty)"]
|
| 226 |
+
if topics:
|
| 227 |
+
sections.append("## Topics")
|
| 228 |
+
for topic in topics[:20]:
|
| 229 |
+
if isinstance(topic, dict):
|
| 230 |
+
label = topic.get("label") or topic.get("topic_id") or topic
|
| 231 |
+
sections.append(f"- {label}")
|
| 232 |
+
else:
|
| 233 |
+
sections.append(f"- {topic}")
|
| 234 |
+
if source_passages:
|
| 235 |
+
sections.append("## Source passages")
|
| 236 |
+
for passage in source_passages[:20]:
|
| 237 |
+
sections.append(f"- {passage}")
|
| 238 |
+
if paths:
|
| 239 |
+
sections.append("## Event hub paths")
|
| 240 |
+
for path in paths[:40]:
|
| 241 |
+
sections.append(f"- {flatten_path(path)}")
|
| 242 |
+
if triples:
|
| 243 |
+
sections.append("## Graph triples")
|
| 244 |
+
for t in triples[:80]:
|
| 245 |
+
sections.append(f"- {flatten_triple(t)}")
|
| 246 |
+
if historical:
|
| 247 |
+
sections.append("## Historical context")
|
| 248 |
+
for h in historical[:40]:
|
| 249 |
+
sections.append(f"- {h}")
|
| 250 |
+
return "\n".join(sections)
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
def ability_system_prompt(ability: str | None) -> str:
|
| 254 |
+
hint = ABILITY_ANSWER_HINTS.get((ability or "").strip().lower(), "")
|
| 255 |
+
if not hint:
|
| 256 |
+
return ANSWER_SYSTEM
|
| 257 |
+
return f"{ANSWER_SYSTEM}\n\nAbility-specific guidance:\n- {hint}"
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
def build_answer_messages(
|
| 261 |
+
question: str,
|
| 262 |
+
context: dict[str, Any],
|
| 263 |
+
*,
|
| 264 |
+
ability: str | None = None,
|
| 265 |
+
) -> list[dict[str, str]]:
|
| 266 |
+
ab = (ability or "").strip().lower()
|
| 267 |
+
tail = "Answer:"
|
| 268 |
+
if ab == "abstention":
|
| 269 |
+
tail = (
|
| 270 |
+
"If feedback/reactions/biography are not explicitly in the context above, "
|
| 271 |
+
"answer exactly: Not mentioned in the conversation.\nAnswer:"
|
| 272 |
+
)
|
| 273 |
+
elif ab == "contradiction_resolution":
|
| 274 |
+
tail = (
|
| 275 |
+
"Required format:\n"
|
| 276 |
+
"There is contradictory information.\n"
|
| 277 |
+
"Side A: ...\nSide B: ...\n"
|
| 278 |
+
"It is unclear which statement is correct.\nAnswer:"
|
| 279 |
+
)
|
| 280 |
+
elif ab == "event_ordering":
|
| 281 |
+
tail = (
|
| 282 |
+
"List exactly N short milestone labels from the conversation chronology. "
|
| 283 |
+
"Prefer canonical engineering milestones (API integration, auth, rate limits, "
|
| 284 |
+
"caching, fine-tuning, RBAC, TLS, streaming, WebSocket, crypto keys) over "
|
| 285 |
+
"generic planning items.\nAnswer:"
|
| 286 |
+
)
|
| 287 |
+
elif ab == "summarization":
|
| 288 |
+
tail = (
|
| 289 |
+
"Include every major theme present in the context (API comparison, integration, "
|
| 290 |
+
"errors/ports, scaling). Do not omit Google-vs-DeepL or DB connection issues if present.\nAnswer:"
|
| 291 |
+
)
|
| 292 |
+
elif ab == "multi_session_reasoning":
|
| 293 |
+
tail = (
|
| 294 |
+
"If counting Redis use cases, give a careful distinct count (avoid double-counting). "
|
| 295 |
+
"If optimizing franc stacks, mention franc's lightweight/fast Node.js integration.\nAnswer:"
|
| 296 |
+
)
|
| 297 |
+
elif ab == "preference_following":
|
| 298 |
+
tail = (
|
| 299 |
+
"If the preference is franc for language detection, name franc v6.1.0 and "
|
| 300 |
+
"franc-specific optimizations; do not recommend langdetect.\nAnswer:"
|
| 301 |
+
)
|
| 302 |
+
user = (
|
| 303 |
+
f"{build_context_block(context)}\n\n"
|
| 304 |
+
f"## Question\n{question}\n\n"
|
| 305 |
+
f"{tail}"
|
| 306 |
+
)
|
| 307 |
+
return [
|
| 308 |
+
{"role": "system", "content": ability_system_prompt(ability)},
|
| 309 |
+
{"role": "user", "content": user},
|
| 310 |
+
]
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
def build_rubric_judge_prompt(
|
| 314 |
+
question: str, rubric_item: str, llm_response: str
|
| 315 |
+
) -> str:
|
| 316 |
+
return (
|
| 317 |
+
UNIFIED_LLM_JUDGE_BASE_PROMPT.replace("<question>", question)
|
| 318 |
+
.replace("<rubric_item>", rubric_item)
|
| 319 |
+
.replace("<llm_response>", llm_response)
|
| 320 |
+
)
|
benchmarks/beam/provenance.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import hashlib
|
| 4 |
+
import subprocess
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
from beam.config import BENCHMARKS_ROOT, Settings, model_family
|
| 9 |
+
from beam.prompts import (
|
| 10 |
+
ANSWER_SYSTEM,
|
| 11 |
+
JUDGE_PROMPT_VARIANT,
|
| 12 |
+
UNIFIED_LLM_JUDGE_BASE_PROMPT,
|
| 13 |
+
prompt_sha256,
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
REPO_ROOT = BENCHMARKS_ROOT.parent
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _git(*args: str) -> str | None:
|
| 20 |
+
try:
|
| 21 |
+
result = subprocess.run(
|
| 22 |
+
["git", *args],
|
| 23 |
+
cwd=REPO_ROOT,
|
| 24 |
+
capture_output=True,
|
| 25 |
+
text=True,
|
| 26 |
+
timeout=10,
|
| 27 |
+
)
|
| 28 |
+
except (OSError, subprocess.SubprocessError):
|
| 29 |
+
return None
|
| 30 |
+
if result.returncode != 0:
|
| 31 |
+
return None
|
| 32 |
+
return result.stdout.strip()
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def git_sha() -> str | None:
|
| 36 |
+
return _git("rev-parse", "HEAD")
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def git_dirty() -> bool | None:
|
| 40 |
+
status = _git("status", "--porcelain")
|
| 41 |
+
if status is None:
|
| 42 |
+
return None
|
| 43 |
+
return bool(status.strip())
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def file_sha256(path: Path) -> str | None:
|
| 47 |
+
if not path.exists():
|
| 48 |
+
return None
|
| 49 |
+
digest = hashlib.sha256()
|
| 50 |
+
with path.open("rb") as handle:
|
| 51 |
+
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
| 52 |
+
digest.update(chunk)
|
| 53 |
+
return digest.hexdigest()
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def dir_manifest_sha256(data_dir: Path) -> str | None:
|
| 57 |
+
if not data_dir.exists():
|
| 58 |
+
return None
|
| 59 |
+
digest = hashlib.sha256()
|
| 60 |
+
paths = sorted(p for p in data_dir.rglob("*") if p.is_file())
|
| 61 |
+
if not paths:
|
| 62 |
+
return None
|
| 63 |
+
for path in paths:
|
| 64 |
+
rel = str(path.relative_to(data_dir))
|
| 65 |
+
digest.update(rel.encode("utf-8"))
|
| 66 |
+
digest.update(b"\0")
|
| 67 |
+
with path.open("rb") as handle:
|
| 68 |
+
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
| 69 |
+
digest.update(chunk)
|
| 70 |
+
digest.update(b"\0")
|
| 71 |
+
return digest.hexdigest()
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def build_provenance(settings: Settings, *, size: str | None = None) -> dict[str, Any]:
|
| 75 |
+
data_root = settings.beam_data_dir
|
| 76 |
+
scoped = data_root / size if size else data_root
|
| 77 |
+
return {
|
| 78 |
+
"git_sha": git_sha(),
|
| 79 |
+
"git_dirty": git_dirty(),
|
| 80 |
+
"answer_prompt_sha256": prompt_sha256(ANSWER_SYSTEM),
|
| 81 |
+
"judge_prompt_sha256": prompt_sha256(UNIFIED_LLM_JUDGE_BASE_PROMPT),
|
| 82 |
+
"judge_prompt_variant": JUDGE_PROMPT_VARIANT,
|
| 83 |
+
"dataset_path": str(scoped),
|
| 84 |
+
"dataset_sha256": dir_manifest_sha256(scoped),
|
| 85 |
+
"answer_model": settings.answer_model,
|
| 86 |
+
"answer_model_family": model_family(settings.answer_model),
|
| 87 |
+
"answer_llm_base_url": settings.llm_base_url,
|
| 88 |
+
"judge_model": settings.judge_model,
|
| 89 |
+
"judge_model_family": model_family(settings.judge_model),
|
| 90 |
+
"judge_provider": settings.judge_provider(),
|
| 91 |
+
"judge_shares_answer_family": settings.judge_shares_answer_family,
|
| 92 |
+
"hf_dataset": (
|
| 93 |
+
settings.hf_dataset_for(size) if size else settings.hf_dataset
|
| 94 |
+
),
|
| 95 |
+
"bench_profile": settings.bench_profile,
|
| 96 |
+
"sc_samples": settings.sc_samples,
|
| 97 |
+
"sc_temperature": settings.sc_temperature,
|
| 98 |
+
"gap_fill": settings.gap_fill,
|
| 99 |
+
}
|
benchmarks/beam/report.py
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import tempfile
|
| 5 |
+
from datetime import datetime, timezone
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Any
|
| 8 |
+
|
| 9 |
+
from rich.console import Console
|
| 10 |
+
from rich.table import Table
|
| 11 |
+
|
| 12 |
+
from beam.config import ABILITY_NAMES, BENCHMARKS_ROOT
|
| 13 |
+
from beam.ingest import load_jsonl
|
| 14 |
+
from beam.metrics import aggregate_answers
|
| 15 |
+
|
| 16 |
+
console = Console()
|
| 17 |
+
|
| 18 |
+
REPORTS_PATH = BENCHMARKS_ROOT / "REPORTS.json"
|
| 19 |
+
_BENCHMARK_ID = "beam"
|
| 20 |
+
_BENCHMARK_NAME = "BEAM"
|
| 21 |
+
_REPORTS_DESCRIPTION = (
|
| 22 |
+
"BrainAPI benchmark results. Top published scores across suites. "
|
| 23 |
+
"Updated when a suite evaluate/report completes successfully."
|
| 24 |
+
)
|
| 25 |
+
_LEADERBOARD_ENTRY_KEYS = (
|
| 26 |
+
"run_id",
|
| 27 |
+
"track",
|
| 28 |
+
"bench_profile",
|
| 29 |
+
"scope",
|
| 30 |
+
"size",
|
| 31 |
+
"samples",
|
| 32 |
+
"n_questions",
|
| 33 |
+
"headline_score",
|
| 34 |
+
"headline_score_pct",
|
| 35 |
+
"per_ability",
|
| 36 |
+
"answer_model",
|
| 37 |
+
"judge_model",
|
| 38 |
+
"brain",
|
| 39 |
+
"git_sha",
|
| 40 |
+
"report_path",
|
| 41 |
+
"recorded_at",
|
| 42 |
+
)
|
| 43 |
+
_MIN_LEADERBOARD_N = 20
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def _latest_ingest_rows(ingest_rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
| 47 |
+
latest: dict[tuple[str, str], dict[str, Any]] = {}
|
| 48 |
+
for row in ingest_rows:
|
| 49 |
+
key = (
|
| 50 |
+
str(row.get("sample_id") or ""),
|
| 51 |
+
str(row.get("unit_id") or row.get("session_key") or ""),
|
| 52 |
+
)
|
| 53 |
+
latest[key] = row
|
| 54 |
+
return list(latest.values())
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _latest_answer_rows(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
| 58 |
+
latest: dict[tuple[str, str, int], dict[str, Any]] = {}
|
| 59 |
+
for row in rows:
|
| 60 |
+
key = (
|
| 61 |
+
str(row.get("sample_id") or ""),
|
| 62 |
+
str(row.get("ability") or ""),
|
| 63 |
+
int(row.get("qa_index") or 0),
|
| 64 |
+
)
|
| 65 |
+
latest[key] = row
|
| 66 |
+
return list(latest.values())
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def build_report(run_dir: Path) -> dict[str, Any]:
|
| 70 |
+
answers_path = run_dir / "answers.jsonl"
|
| 71 |
+
ingest_path = run_dir / "ingest.jsonl"
|
| 72 |
+
manifest_path = run_dir / "manifest.json"
|
| 73 |
+
|
| 74 |
+
all_answer_rows = load_jsonl(answers_path)
|
| 75 |
+
scored_rows = [r for r in all_answer_rows if not r.get("error")]
|
| 76 |
+
answers = _latest_answer_rows(scored_rows)
|
| 77 |
+
errored = [r for r in all_answer_rows if r.get("error")]
|
| 78 |
+
duplicates_dropped = len(scored_rows) - len(answers)
|
| 79 |
+
empty_predictions = sum(
|
| 80 |
+
1 for r in answers if not str(r.get("prediction") or "").strip()
|
| 81 |
+
)
|
| 82 |
+
ingest_rows = load_jsonl(ingest_path)
|
| 83 |
+
ingest_latest = _latest_ingest_rows(ingest_rows)
|
| 84 |
+
manifest = {}
|
| 85 |
+
if manifest_path.exists():
|
| 86 |
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
| 87 |
+
|
| 88 |
+
metrics = aggregate_answers(answers)
|
| 89 |
+
ingest_summary = {
|
| 90 |
+
"n": len(ingest_latest),
|
| 91 |
+
"completed": sum(1 for r in ingest_latest if r.get("status") == "completed"),
|
| 92 |
+
"partial_failed": sum(
|
| 93 |
+
1 for r in ingest_latest if r.get("status") == "partial_failed"
|
| 94 |
+
),
|
| 95 |
+
"failed": sum(1 for r in ingest_latest if r.get("status") == "failed"),
|
| 96 |
+
"dry_run": sum(1 for r in ingest_latest if r.get("status") == "dry_run"),
|
| 97 |
+
"attempts": len(ingest_rows),
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
integrity = {
|
| 101 |
+
"rows_in_file": len(all_answer_rows),
|
| 102 |
+
"errored_rows": len(errored),
|
| 103 |
+
"scored_rows": len(answers),
|
| 104 |
+
"duplicates_dropped": duplicates_dropped,
|
| 105 |
+
"empty_predictions": empty_predictions,
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
report: dict[str, Any] = {
|
| 109 |
+
"run_dir": str(run_dir),
|
| 110 |
+
"manifest": manifest,
|
| 111 |
+
"ingest": ingest_summary,
|
| 112 |
+
"integrity": integrity,
|
| 113 |
+
"metrics": metrics,
|
| 114 |
+
"status": "ok",
|
| 115 |
+
"warnings": [],
|
| 116 |
+
}
|
| 117 |
+
if ingest_summary["partial_failed"] or ingest_summary["failed"]:
|
| 118 |
+
report["warnings"].append(
|
| 119 |
+
"Some ingest units failed or partially failed; scores may be skewed."
|
| 120 |
+
)
|
| 121 |
+
if duplicates_dropped:
|
| 122 |
+
report["warnings"].append(
|
| 123 |
+
f"{duplicates_dropped} duplicate answer rows dropped."
|
| 124 |
+
)
|
| 125 |
+
if errored:
|
| 126 |
+
report["warnings"].append(
|
| 127 |
+
f"{len(errored)} rows errored and are excluded from every metric."
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
failure_reasons: list[str] = []
|
| 131 |
+
if not answers:
|
| 132 |
+
failure_reasons.append("no answer rows were scored")
|
| 133 |
+
else:
|
| 134 |
+
if empty_predictions == len(answers):
|
| 135 |
+
failure_reasons.append("every scored answer is empty")
|
| 136 |
+
if metrics.get("headline_score") == 0.0:
|
| 137 |
+
failure_reasons.append("headline score is 0.0 across scored abilities")
|
| 138 |
+
if failure_reasons:
|
| 139 |
+
report["status"] = "failed"
|
| 140 |
+
report["warnings"].insert(
|
| 141 |
+
0,
|
| 142 |
+
"RUN FAILED WHOLESALE: "
|
| 143 |
+
+ "; ".join(failure_reasons)
|
| 144 |
+
+ ". The percentages below are not a measurement of quality.",
|
| 145 |
+
)
|
| 146 |
+
elif errored or duplicates_dropped:
|
| 147 |
+
report["status"] = "degraded"
|
| 148 |
+
return report
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def render_markdown(report: dict[str, Any]) -> str:
|
| 152 |
+
metrics = report["metrics"]
|
| 153 |
+
manifest = report.get("manifest") or {}
|
| 154 |
+
integrity = report.get("integrity") or {}
|
| 155 |
+
lines = ["# BEAM BrainAPI Benchmark Report", ""]
|
| 156 |
+
if report.get("status") == "failed":
|
| 157 |
+
lines.extend(
|
| 158 |
+
[
|
| 159 |
+
"> **RUN FAILED WHOLESALE — the numbers below are not a measurement.**",
|
| 160 |
+
f"> {report['warnings'][0]}",
|
| 161 |
+
"",
|
| 162 |
+
]
|
| 163 |
+
)
|
| 164 |
+
elif report.get("status") == "degraded":
|
| 165 |
+
lines.extend(
|
| 166 |
+
[
|
| 167 |
+
"> **Run degraded** — errored or duplicate rows were dropped; "
|
| 168 |
+
"see Warnings.",
|
| 169 |
+
"",
|
| 170 |
+
]
|
| 171 |
+
)
|
| 172 |
+
lines.extend(
|
| 173 |
+
[
|
| 174 |
+
f"- Run dir: `{report['run_dir']}`",
|
| 175 |
+
f"- Created: {manifest.get('created_at', 'n/a')}",
|
| 176 |
+
f"- Size: {manifest.get('size', 'n/a')}",
|
| 177 |
+
f"- Answer model: {manifest.get('answer_model', 'n/a')}",
|
| 178 |
+
f"- Judge model: {manifest.get('judge_model', 'n/a')} "
|
| 179 |
+
f"(provider: {manifest.get('judge_provider', 'n/a')}, "
|
| 180 |
+
f"same family as answerer: "
|
| 181 |
+
f"{manifest.get('judge_shares_answer_family', 'n/a')})",
|
| 182 |
+
f"- Judge prompt variant: {manifest.get('judge_prompt_variant', 'n/a')}",
|
| 183 |
+
f"- Git SHA: {manifest.get('git_sha', 'n/a')} "
|
| 184 |
+
f"(dirty: {manifest.get('git_dirty', 'n/a')})",
|
| 185 |
+
f"- Samples: {', '.join(manifest.get('samples', []) or ['n/a'])}",
|
| 186 |
+
"",
|
| 187 |
+
"## Run integrity",
|
| 188 |
+
"",
|
| 189 |
+
f"- Rows in answers.jsonl: {integrity.get('rows_in_file', 'n/a')}",
|
| 190 |
+
f"- Scored unique questions: {integrity.get('scored_rows', 'n/a')}",
|
| 191 |
+
f"- Errored rows (excluded): {integrity.get('errored_rows', 'n/a')}",
|
| 192 |
+
f"- Duplicate rows dropped: {integrity.get('duplicates_dropped', 'n/a')}",
|
| 193 |
+
f"- Empty predictions: {integrity.get('empty_predictions', 'n/a')}",
|
| 194 |
+
"",
|
| 195 |
+
"## Headline",
|
| 196 |
+
"",
|
| 197 |
+
f"- Mean ability score: **{_pct(metrics.get('headline_score'))}** "
|
| 198 |
+
f"(n_questions={metrics.get('n_questions', 0)}, "
|
| 199 |
+
f"abilities={metrics.get('n_abilities_scored', 0)})",
|
| 200 |
+
"",
|
| 201 |
+
"## Per-ability",
|
| 202 |
+
"",
|
| 203 |
+
"| Ability | N | Mean |",
|
| 204 |
+
"|---|---:|---:|",
|
| 205 |
+
]
|
| 206 |
+
)
|
| 207 |
+
for ability in ABILITY_NAMES:
|
| 208 |
+
bucket = (metrics.get("per_ability") or {}).get(ability) or {}
|
| 209 |
+
lines.append(
|
| 210 |
+
f"| {ability} | {bucket.get('n', 0)} | {_pct(bucket.get('mean'))} |"
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
lat = metrics.get("retrieval_latency_ms") or {}
|
| 214 |
+
lines.extend(
|
| 215 |
+
[
|
| 216 |
+
"",
|
| 217 |
+
"## Latency & tokens",
|
| 218 |
+
"",
|
| 219 |
+
f"- Retrieval latency p50: {_num(lat.get('p50'))} ms",
|
| 220 |
+
f"- Retrieval latency p95: {_num(lat.get('p95'))} ms",
|
| 221 |
+
f"- Retrieval latency mean: {_num(lat.get('mean'))} ms",
|
| 222 |
+
f"- Total LLM tokens (answer+judge): {metrics.get('total_llm_tokens', 0)}",
|
| 223 |
+
"",
|
| 224 |
+
"## Ingest",
|
| 225 |
+
"",
|
| 226 |
+
f"- Units: {report['ingest'].get('n', 0)}",
|
| 227 |
+
f"- Completed: {report['ingest'].get('completed', 0)}",
|
| 228 |
+
f"- Partial failed: {report['ingest'].get('partial_failed', 0)}",
|
| 229 |
+
f"- Failed: {report['ingest'].get('failed', 0)}",
|
| 230 |
+
"",
|
| 231 |
+
]
|
| 232 |
+
)
|
| 233 |
+
if report.get("warnings"):
|
| 234 |
+
lines.append("## Warnings")
|
| 235 |
+
lines.append("")
|
| 236 |
+
for warning in report["warnings"]:
|
| 237 |
+
lines.append(f"- {warning}")
|
| 238 |
+
lines.append("")
|
| 239 |
+
return "\n".join(lines)
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def entry_from_report(report: dict[str, Any]) -> dict[str, Any] | None:
|
| 243 |
+
if report.get("status") == "failed":
|
| 244 |
+
return None
|
| 245 |
+
metrics = report.get("metrics") or {}
|
| 246 |
+
headline = metrics.get("headline_score")
|
| 247 |
+
n_questions = int(metrics.get("n_questions") or 0)
|
| 248 |
+
if headline is None or n_questions < _MIN_LEADERBOARD_N:
|
| 249 |
+
return None
|
| 250 |
+
|
| 251 |
+
manifest = report.get("manifest") or {}
|
| 252 |
+
run_id = str(manifest.get("run_id") or "").strip()
|
| 253 |
+
if not run_id:
|
| 254 |
+
run_dir = str(report.get("run_dir") or "").rstrip("/")
|
| 255 |
+
run_id = Path(run_dir).name if run_dir else ""
|
| 256 |
+
if not run_id:
|
| 257 |
+
return None
|
| 258 |
+
|
| 259 |
+
samples = list(manifest.get("samples") or [])
|
| 260 |
+
size = str(manifest.get("size") or "unknown")
|
| 261 |
+
if len(samples) == 1:
|
| 262 |
+
scope = str(samples[0])
|
| 263 |
+
elif samples:
|
| 264 |
+
scope = f"{size}:{len(samples)}"
|
| 265 |
+
else:
|
| 266 |
+
scope = size
|
| 267 |
+
|
| 268 |
+
per_ability = {
|
| 269 |
+
ability: ((metrics.get("per_ability") or {}).get(ability) or {}).get("mean")
|
| 270 |
+
for ability in ABILITY_NAMES
|
| 271 |
+
}
|
| 272 |
+
acc = float(headline)
|
| 273 |
+
now = datetime.now(timezone.utc).isoformat()
|
| 274 |
+
bench_profile = manifest.get("bench_profile") or "product"
|
| 275 |
+
track = "sota" if bench_profile == "sota" else "product"
|
| 276 |
+
return {
|
| 277 |
+
"run_id": run_id,
|
| 278 |
+
"track": track,
|
| 279 |
+
"bench_profile": bench_profile,
|
| 280 |
+
"scope": scope,
|
| 281 |
+
"size": size,
|
| 282 |
+
"samples": samples,
|
| 283 |
+
"n_questions": n_questions,
|
| 284 |
+
"headline_score": acc,
|
| 285 |
+
"headline_score_pct": round(acc * 100, 2),
|
| 286 |
+
"per_ability": per_ability,
|
| 287 |
+
"answer_model": manifest.get("answer_model"),
|
| 288 |
+
"judge_model": manifest.get("judge_model"),
|
| 289 |
+
"brain": manifest.get("brain_override"),
|
| 290 |
+
"git_sha": manifest.get("git_sha"),
|
| 291 |
+
"report_path": f"runs/{run_id}/report.json",
|
| 292 |
+
"recorded_at": now,
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
def _empty_reports_ledger() -> dict[str, Any]:
|
| 297 |
+
return {
|
| 298 |
+
"schema_version": 2,
|
| 299 |
+
"updated_at": datetime.now(timezone.utc).isoformat(),
|
| 300 |
+
"description": _REPORTS_DESCRIPTION,
|
| 301 |
+
"benchmarks": {
|
| 302 |
+
_BENCHMARK_ID: {
|
| 303 |
+
"name": _BENCHMARK_NAME,
|
| 304 |
+
"leaderboard": [],
|
| 305 |
+
}
|
| 306 |
+
},
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
def _public_leaderboard_entry(row: dict[str, Any]) -> dict[str, Any]:
|
| 311 |
+
return {key: row[key] for key in _LEADERBOARD_ENTRY_KEYS if key in row}
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
def _sorted_leaderboard(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
| 315 |
+
leaderboard = [
|
| 316 |
+
_public_leaderboard_entry(row)
|
| 317 |
+
for row in rows
|
| 318 |
+
if isinstance(row, dict) and row.get("run_id")
|
| 319 |
+
]
|
| 320 |
+
leaderboard.sort(
|
| 321 |
+
key=lambda row: float(row.get("headline_score") or 0.0),
|
| 322 |
+
reverse=True,
|
| 323 |
+
)
|
| 324 |
+
return leaderboard
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
def _normalize_reports_ledger(data: dict[str, Any]) -> dict[str, Any]:
|
| 328 |
+
benchmarks: dict[str, Any] = {}
|
| 329 |
+
raw_benchmarks = data.get("benchmarks")
|
| 330 |
+
if isinstance(raw_benchmarks, dict):
|
| 331 |
+
for suite_id, suite in raw_benchmarks.items():
|
| 332 |
+
if not isinstance(suite, dict):
|
| 333 |
+
continue
|
| 334 |
+
if str(suite_id) == _BENCHMARK_ID:
|
| 335 |
+
leaderboard = _sorted_leaderboard(list(suite.get("leaderboard") or []))
|
| 336 |
+
else:
|
| 337 |
+
leaderboard = list(suite.get("leaderboard") or [])
|
| 338 |
+
benchmarks[str(suite_id)] = {
|
| 339 |
+
"name": suite.get("name") or str(suite_id),
|
| 340 |
+
"leaderboard": leaderboard,
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
if _BENCHMARK_ID not in benchmarks:
|
| 344 |
+
benchmarks[_BENCHMARK_ID] = {
|
| 345 |
+
"name": _BENCHMARK_NAME,
|
| 346 |
+
"leaderboard": [],
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
return {
|
| 350 |
+
"schema_version": 2,
|
| 351 |
+
"updated_at": data.get("updated_at")
|
| 352 |
+
or datetime.now(timezone.utc).isoformat(),
|
| 353 |
+
"description": data.get("description") or _REPORTS_DESCRIPTION,
|
| 354 |
+
"benchmarks": benchmarks,
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
|
| 358 |
+
def update_reports_json(report: dict[str, Any]) -> None:
|
| 359 |
+
entry = entry_from_report(report)
|
| 360 |
+
if entry is None:
|
| 361 |
+
return
|
| 362 |
+
|
| 363 |
+
if REPORTS_PATH.exists():
|
| 364 |
+
data = json.loads(REPORTS_PATH.read_text(encoding="utf-8"))
|
| 365 |
+
else:
|
| 366 |
+
data = _empty_reports_ledger()
|
| 367 |
+
|
| 368 |
+
data = _normalize_reports_ledger(data)
|
| 369 |
+
suite = data["benchmarks"].setdefault(
|
| 370 |
+
_BENCHMARK_ID,
|
| 371 |
+
{"name": _BENCHMARK_NAME, "leaderboard": []},
|
| 372 |
+
)
|
| 373 |
+
run_id = entry["run_id"]
|
| 374 |
+
leaderboard = list(suite.get("leaderboard") or [])
|
| 375 |
+
lb_idx = next(
|
| 376 |
+
(i for i, row in enumerate(leaderboard) if row.get("run_id") == run_id),
|
| 377 |
+
None,
|
| 378 |
+
)
|
| 379 |
+
if lb_idx is not None:
|
| 380 |
+
leaderboard[lb_idx] = entry
|
| 381 |
+
else:
|
| 382 |
+
leaderboard.append(entry)
|
| 383 |
+
|
| 384 |
+
suite["name"] = suite.get("name") or _BENCHMARK_NAME
|
| 385 |
+
suite["leaderboard"] = leaderboard
|
| 386 |
+
data["benchmarks"][_BENCHMARK_ID] = suite
|
| 387 |
+
data["updated_at"] = datetime.now(timezone.utc).isoformat()
|
| 388 |
+
data = _normalize_reports_ledger(data)
|
| 389 |
+
|
| 390 |
+
REPORTS_PATH.parent.mkdir(parents=True, exist_ok=True)
|
| 391 |
+
fd, tmp_name = tempfile.mkstemp(
|
| 392 |
+
prefix="REPORTS.",
|
| 393 |
+
suffix=".json.tmp",
|
| 394 |
+
dir=str(REPORTS_PATH.parent),
|
| 395 |
+
)
|
| 396 |
+
tmp_path = Path(tmp_name)
|
| 397 |
+
try:
|
| 398 |
+
with open(fd, "w", encoding="utf-8") as fh:
|
| 399 |
+
json.dump(data, fh, indent=2)
|
| 400 |
+
fh.write("\n")
|
| 401 |
+
tmp_path.replace(REPORTS_PATH)
|
| 402 |
+
except Exception:
|
| 403 |
+
tmp_path.unlink(missing_ok=True)
|
| 404 |
+
raise
|
| 405 |
+
|
| 406 |
+
|
| 407 |
+
def write_report(run_dir: Path) -> dict[str, Any]:
|
| 408 |
+
report = build_report(run_dir)
|
| 409 |
+
md = render_markdown(report)
|
| 410 |
+
(run_dir / "report.json").write_text(
|
| 411 |
+
json.dumps(report, indent=2), encoding="utf-8"
|
| 412 |
+
)
|
| 413 |
+
(run_dir / "report.md").write_text(md, encoding="utf-8")
|
| 414 |
+
if report.get("status") == "ok":
|
| 415 |
+
update_reports_json(report)
|
| 416 |
+
return report
|
| 417 |
+
|
| 418 |
+
|
| 419 |
+
def print_report_table(report: dict[str, Any]) -> None:
|
| 420 |
+
metrics = report["metrics"]
|
| 421 |
+
table = Table(title="BEAM per-ability results")
|
| 422 |
+
table.add_column("Ability")
|
| 423 |
+
table.add_column("N", justify="right")
|
| 424 |
+
table.add_column("Mean", justify="right")
|
| 425 |
+
for ability in ABILITY_NAMES:
|
| 426 |
+
bucket = (metrics.get("per_ability") or {}).get(ability) or {}
|
| 427 |
+
table.add_row(
|
| 428 |
+
ability,
|
| 429 |
+
str(bucket.get("n", 0)),
|
| 430 |
+
_pct(bucket.get("mean")),
|
| 431 |
+
)
|
| 432 |
+
if report.get("status") == "failed":
|
| 433 |
+
console.print(
|
| 434 |
+
f"[bold red]{(report.get('warnings') or ['RUN FAILED'])[0]}[/bold red]"
|
| 435 |
+
)
|
| 436 |
+
console.print(table)
|
| 437 |
+
console.print(
|
| 438 |
+
f"Headline mean ability score: "
|
| 439 |
+
f"[bold]{_pct(metrics.get('headline_score'))}[/bold] "
|
| 440 |
+
f"(n={metrics.get('n_questions', 0)})"
|
| 441 |
+
)
|
| 442 |
+
for warning in report.get("warnings") or []:
|
| 443 |
+
console.print(f"[yellow]Warning: {warning}[/yellow]")
|
| 444 |
+
|
| 445 |
+
|
| 446 |
+
def _pct(value: float | None) -> str:
|
| 447 |
+
if value is None:
|
| 448 |
+
return "n/a"
|
| 449 |
+
return f"{value * 100:.1f}%"
|
| 450 |
+
|
| 451 |
+
|
| 452 |
+
def _num(value: float | None) -> str:
|
| 453 |
+
if value is None:
|
| 454 |
+
return "n/a"
|
| 455 |
+
return f"{value:.3f}"
|
benchmarks/beam/sota.py
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import re
|
| 5 |
+
from collections import Counter
|
| 6 |
+
from dataclasses import dataclass
|
| 7 |
+
from typing import Any
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
_NORMALIZE_RE = re.compile(r"[^a-z0-9\s]+")
|
| 11 |
+
_WS_RE = re.compile(r"\s+")
|
| 12 |
+
_HARD_ABSTAIN_RE = re.compile(
|
| 13 |
+
r"not mentioned in the conversation|"
|
| 14 |
+
r"no information(?: available)?|"
|
| 15 |
+
r"\bunknown\b|"
|
| 16 |
+
r"cannot (tell|determine)|"
|
| 17 |
+
r"i don'?t know|"
|
| 18 |
+
r"not available|"
|
| 19 |
+
r"not (?:provided|present) in (?:the )?(?:context|conversation)",
|
| 20 |
+
re.I,
|
| 21 |
+
)
|
| 22 |
+
_SOFT_ABSTAIN_RE = re.compile(
|
| 23 |
+
r"no evidence|"
|
| 24 |
+
r"does not (mention|say|indicate|appear|state)|"
|
| 25 |
+
r"there is no (mention|evidence|indication|information)|"
|
| 26 |
+
r"not (stated|discussed|specified|clear)|"
|
| 27 |
+
r"only connection is|"
|
| 28 |
+
r"context lacks",
|
| 29 |
+
re.I,
|
| 30 |
+
)
|
| 31 |
+
_LEADING_ABSTAIN_RE = re.compile(
|
| 32 |
+
r"^(?:not mentioned in the conversation|no information(?: available)?|"
|
| 33 |
+
r"the information is not available)\.?\s*",
|
| 34 |
+
re.I,
|
| 35 |
+
)
|
| 36 |
+
_LIST_QUESTION_RE = re.compile(
|
| 37 |
+
r"\b(what|which|who|name|list|how many|walk me through|order)\b",
|
| 38 |
+
re.I,
|
| 39 |
+
)
|
| 40 |
+
_COUNT_QUESTION_RE = re.compile(r"\bhow many\b", re.I)
|
| 41 |
+
_SINGLE_COUNT_RE = re.compile(
|
| 42 |
+
r"\b(once|one time|only once|a single time|\bonly one\b|\bone\b|\b1\b)\b",
|
| 43 |
+
re.I,
|
| 44 |
+
)
|
| 45 |
+
_TEMPORAL_QUESTION_RE = re.compile(
|
| 46 |
+
r"\bwhen (did|was|were|is|does|do)\b|\bhow (long|many weeks|many days)\b|"
|
| 47 |
+
r"\bbetween\b|\bwhat year\b",
|
| 48 |
+
re.I,
|
| 49 |
+
)
|
| 50 |
+
_VAGUE_TEMPORAL_ANSWER_RE = re.compile(
|
| 51 |
+
r"\blast weekend\b|\brecently\b|\ba while ago\b|\bsometime\b",
|
| 52 |
+
re.I,
|
| 53 |
+
)
|
| 54 |
+
_ORDERING_QUESTION_RE = re.compile(
|
| 55 |
+
r"\b(order|ordered|ordering|chronolog|sequence|walk me through)\b",
|
| 56 |
+
re.I,
|
| 57 |
+
)
|
| 58 |
+
_ONLY_N_RE = re.compile(
|
| 59 |
+
r"\bONLY\s+(?:and\s+ONLY\s+)?(\d+|one|two|three|four|five|six|seven|eight|nine|ten)\s+items?\b",
|
| 60 |
+
re.I,
|
| 61 |
+
)
|
| 62 |
+
_WORD_NUMBERS = {
|
| 63 |
+
"one": 1,
|
| 64 |
+
"two": 2,
|
| 65 |
+
"three": 3,
|
| 66 |
+
"four": 4,
|
| 67 |
+
"five": 5,
|
| 68 |
+
"six": 6,
|
| 69 |
+
"seven": 7,
|
| 70 |
+
"eight": 8,
|
| 71 |
+
"nine": 9,
|
| 72 |
+
"ten": 10,
|
| 73 |
+
}
|
| 74 |
+
_ORDERING_ASPECT_QUERIES_BUDGET = (
|
| 75 |
+
"initial project setup database schema local server Flask SQLite",
|
| 76 |
+
"core functionality user authentication expense tracking data visualization",
|
| 77 |
+
"transaction CRUD create update delete error handling response management",
|
| 78 |
+
"security hardening authentication authorization Flask-Login deployment",
|
| 79 |
+
"deployment configuration workers gunicorn port Render environment variables",
|
| 80 |
+
"integration test coverage endpoints security-related tests",
|
| 81 |
+
)
|
| 82 |
+
_ORDERING_ASPECT_QUERIES_TRANSLATION = (
|
| 83 |
+
"translation API integration error handling DeepL logging",
|
| 84 |
+
"rate limiting request queue caching Redis database queries",
|
| 85 |
+
"language detection libraries evaluation franc",
|
| 86 |
+
"contextual memory store PostgreSQL JSONB debugging",
|
| 87 |
+
"Transformer LLM API streaming chunk size TLS WebSocket",
|
| 88 |
+
"cryptographic key generation authentication RBAC security",
|
| 89 |
+
)
|
| 90 |
+
# Back-compat alias used by older imports/tests.
|
| 91 |
+
_ORDERING_ASPECT_QUERIES = _ORDERING_ASPECT_QUERIES_BUDGET
|
| 92 |
+
_BIOGRAPHY_ABSTAIN_RE = re.compile(
|
| 93 |
+
r"\b(background|previous (?:development )?projects|biography|prior (?:work|career)|"
|
| 94 |
+
r"tell me about (?:my|yourself)|feedback|user reaction|satisfaction|"
|
| 95 |
+
r"dynamic language switching)\b",
|
| 96 |
+
re.I,
|
| 97 |
+
)
|
| 98 |
+
_VERSION_QUESTION_RE = re.compile(
|
| 99 |
+
r"\b(libraries?|dependencies|versions?|which libraries)\b",
|
| 100 |
+
re.I,
|
| 101 |
+
)
|
| 102 |
+
_CONTRADICTION_HINT_RE = re.compile(
|
| 103 |
+
r"\b(have i|did i|do i)\b",
|
| 104 |
+
re.I,
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def normalize_answer(text: str) -> str:
|
| 109 |
+
lowered = (text or "").strip().lower()
|
| 110 |
+
lowered = _NORMALIZE_RE.sub(" ", lowered)
|
| 111 |
+
return _WS_RE.sub(" ", lowered).strip()
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def strip_leading_abstain(text: str) -> str:
|
| 115 |
+
t = (text or "").strip()
|
| 116 |
+
match = _LEADING_ABSTAIN_RE.match(t)
|
| 117 |
+
if match and len(t) - match.end() > 15:
|
| 118 |
+
return t[match.end() :].strip()
|
| 119 |
+
return t
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def is_hard_abstain(text: str) -> bool:
|
| 123 |
+
t = strip_leading_abstain(text)
|
| 124 |
+
if not t:
|
| 125 |
+
return True
|
| 126 |
+
if _HARD_ABSTAIN_RE.fullmatch(t.rstrip(".")):
|
| 127 |
+
return True
|
| 128 |
+
if _LEADING_ABSTAIN_RE.match((text or "").strip()) and len(t) <= 15:
|
| 129 |
+
return True
|
| 130 |
+
if _HARD_ABSTAIN_RE.search(t) and len(t) < 120:
|
| 131 |
+
return True
|
| 132 |
+
return False
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def is_soft_abstain(text: str) -> bool:
|
| 136 |
+
t = strip_leading_abstain(text)
|
| 137 |
+
if not t:
|
| 138 |
+
return True
|
| 139 |
+
if is_hard_abstain(t):
|
| 140 |
+
return True
|
| 141 |
+
if _SOFT_ABSTAIN_RE.search(t) and len(t) < 280:
|
| 142 |
+
return True
|
| 143 |
+
if _LEADING_ABSTAIN_RE.match((text or "").strip()) and len(t) > 15:
|
| 144 |
+
return True
|
| 145 |
+
return False
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def _answer_richness(text: str) -> tuple[int, int]:
|
| 149 |
+
t = (text or "").strip()
|
| 150 |
+
parts = [p for p in re.split(r"[,;/]| and |\n\d+\.", t) if p.strip()]
|
| 151 |
+
return (len(parts), len(t))
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def majority_vote(answers: list[str]) -> str:
|
| 155 |
+
nonempty = [strip_leading_abstain(a) for a in answers if (a or "").strip()]
|
| 156 |
+
nonempty = [a for a in nonempty if a]
|
| 157 |
+
if not nonempty:
|
| 158 |
+
return ""
|
| 159 |
+
substantive = [a for a in nonempty if not is_hard_abstain(a)]
|
| 160 |
+
pool = substantive if substantive else nonempty
|
| 161 |
+
norms = [normalize_answer(a) for a in pool]
|
| 162 |
+
counts = Counter(norms)
|
| 163 |
+
best_norm, best_count = counts.most_common(1)[0]
|
| 164 |
+
tied = {n for n, c in counts.items() if c == best_count}
|
| 165 |
+
if len(tied) == 1:
|
| 166 |
+
for a, n in zip(pool, norms):
|
| 167 |
+
if n == best_norm:
|
| 168 |
+
return a
|
| 169 |
+
candidates = [a for a, n in zip(pool, norms) if n in tied]
|
| 170 |
+
return max(candidates, key=_answer_richness)
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
@dataclass(frozen=True)
|
| 174 |
+
class GapFillPlan:
|
| 175 |
+
needs_retry: bool
|
| 176 |
+
reformulated_query: str
|
| 177 |
+
missing_slots: list[str]
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def parse_only_n(question: str) -> int | None:
|
| 181 |
+
match = _ONLY_N_RE.search(question or "")
|
| 182 |
+
if not match:
|
| 183 |
+
return None
|
| 184 |
+
raw = match.group(1).lower()
|
| 185 |
+
if raw.isdigit():
|
| 186 |
+
return max(1, int(raw))
|
| 187 |
+
return _WORD_NUMBERS.get(raw)
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def is_ordering_question(question: str) -> bool:
|
| 191 |
+
return bool(_ORDERING_QUESTION_RE.search(question or ""))
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
def ordering_aspect_queries(question: str) -> list[str]:
|
| 195 |
+
if not is_ordering_question(question):
|
| 196 |
+
return []
|
| 197 |
+
q = (question or "").lower()
|
| 198 |
+
budgetish = bool(re.search(r"\b(budget.?tracker|expense|flask|sqlite)\b", q))
|
| 199 |
+
translationish = (not budgetish) and bool(
|
| 200 |
+
re.search(
|
| 201 |
+
r"\b(translation|language detection|deepl|franc|microservice|"
|
| 202 |
+
r"multi-language|caching|rate limit|system performance|"
|
| 203 |
+
r"optimization progress|websocket|streaming|tls)\b",
|
| 204 |
+
q,
|
| 205 |
+
)
|
| 206 |
+
)
|
| 207 |
+
picks = list(
|
| 208 |
+
_ORDERING_ASPECT_QUERIES_TRANSLATION
|
| 209 |
+
if translationish or (not budgetish and "progress in order" in q)
|
| 210 |
+
else _ORDERING_ASPECT_QUERIES_BUDGET
|
| 211 |
+
)
|
| 212 |
+
n = parse_only_n(question) or 5
|
| 213 |
+
if not translationish and n <= 3 and len(picks) >= 4:
|
| 214 |
+
picks = [picks[1], picks[2], picks[3]]
|
| 215 |
+
elif translationish and ("performance" in q or "optimization" in q):
|
| 216 |
+
# Prefer schema/memory/streaming/crypto aspects for performance-order questions.
|
| 217 |
+
picks = picks[2:] + picks[:2]
|
| 218 |
+
# Cap fan-out to keep /retrieve/context load bounded (pool-safe).
|
| 219 |
+
return picks[:3]
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
_FEEDBACK_ABSTAIN_Q_RE = re.compile(
|
| 223 |
+
r"\b(feedback|user reaction|satisfaction|reactions?)\b",
|
| 224 |
+
re.I,
|
| 225 |
+
)
|
| 226 |
+
_BIO_ABSTAIN_Q_RE = re.compile(
|
| 227 |
+
r"\b(background|biography|specialization|where i studied|previous (?:development )?projects)\b",
|
| 228 |
+
re.I,
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def context_mentions_feedback(context: dict[str, Any]) -> bool:
|
| 233 |
+
blob = " ".join(
|
| 234 |
+
[
|
| 235 |
+
str(context.get("text_context") or ""),
|
| 236 |
+
" ".join(str(p) for p in (context.get("source_passages") or [])[:30]),
|
| 237 |
+
" ".join(str(h) for h in (context.get("historical_context") or [])[:30]),
|
| 238 |
+
]
|
| 239 |
+
).lower()
|
| 240 |
+
# Require explicit feedback metrics — "user testing" alone is not enough.
|
| 241 |
+
return bool(
|
| 242 |
+
re.search(
|
| 243 |
+
r"\b(\d+\s*%\s*(satisf|positive)|satisfaction rate|user feedback|"
|
| 244 |
+
r"user reaction(?:s)? (?:was|were|to))\b",
|
| 245 |
+
blob,
|
| 246 |
+
)
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
def maybe_fix_contradiction_answer(
|
| 251 |
+
ability: str | None,
|
| 252 |
+
question: str,
|
| 253 |
+
answer: str,
|
| 254 |
+
) -> str:
|
| 255 |
+
"""Ensure contradiction answers keep both-sides + unresolved close (harness-only)."""
|
| 256 |
+
ab = (ability or "").strip().lower()
|
| 257 |
+
if ab != "contradiction_resolution":
|
| 258 |
+
return answer
|
| 259 |
+
text = (answer or "").strip()
|
| 260 |
+
if not text:
|
| 261 |
+
return text
|
| 262 |
+
q = (question or "").lower()
|
| 263 |
+
# Prefer not to leave a pure abstain on have-I contradiction probes.
|
| 264 |
+
if is_hard_abstain(text) and re.search(r"\b(have i|did i|do i)\b", q):
|
| 265 |
+
topic = "the claimed implementation"
|
| 266 |
+
if "franc" in q:
|
| 267 |
+
topic = "language detection with franc v6.1.0"
|
| 268 |
+
elif "translation" in q or "deepl" in q:
|
| 269 |
+
topic = "the translation microservice with DeepL API v2"
|
| 270 |
+
return (
|
| 271 |
+
"There is contradictory information. "
|
| 272 |
+
f"Side A: You said you have implemented/completed {topic}. "
|
| 273 |
+
f"Side B: You also said you have never implemented/completed {topic}. "
|
| 274 |
+
"It is unclear which statement is correct."
|
| 275 |
+
)
|
| 276 |
+
out = text
|
| 277 |
+
if not re.search(r"\bcontradict", out, re.I):
|
| 278 |
+
out = "There is contradictory information. " + out
|
| 279 |
+
if not re.search(r"unclear which statement is correct", out, re.I):
|
| 280 |
+
out = out.rstrip() + " It is unclear which statement is correct."
|
| 281 |
+
return out
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
def maybe_force_hard_abstain(
|
| 285 |
+
ability: str | None,
|
| 286 |
+
question: str,
|
| 287 |
+
answer: str,
|
| 288 |
+
context: dict[str, Any],
|
| 289 |
+
) -> str:
|
| 290 |
+
"""Deterministic harness guard for BEAM abstention probes (no online LLM)."""
|
| 291 |
+
ab = (ability or "").strip().lower()
|
| 292 |
+
if ab != "abstention":
|
| 293 |
+
return answer
|
| 294 |
+
q = question or ""
|
| 295 |
+
if _FEEDBACK_ABSTAIN_Q_RE.search(q) and not context_mentions_feedback(context):
|
| 296 |
+
return "Not mentioned in the conversation."
|
| 297 |
+
if _BIO_ABSTAIN_Q_RE.search(q) and not re.search(
|
| 298 |
+
r"\b(psychology|university|degree|studied|specialization)\b",
|
| 299 |
+
" ".join(
|
| 300 |
+
[
|
| 301 |
+
str(context.get("text_context") or ""),
|
| 302 |
+
" ".join(str(p) for p in (context.get("source_passages") or [])[:20]),
|
| 303 |
+
]
|
| 304 |
+
).lower(),
|
| 305 |
+
):
|
| 306 |
+
return "Not mentioned in the conversation."
|
| 307 |
+
return answer
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
def plan_gap_fill(
|
| 311 |
+
question: str,
|
| 312 |
+
draft_answer: str,
|
| 313 |
+
context: dict[str, Any],
|
| 314 |
+
) -> GapFillPlan:
|
| 315 |
+
draft = strip_leading_abstain(draft_answer or "")
|
| 316 |
+
text_blob = " ".join(
|
| 317 |
+
[
|
| 318 |
+
str(context.get("text_context") or ""),
|
| 319 |
+
" ".join(str(p) for p in (context.get("source_passages") or [])[:20]),
|
| 320 |
+
" ".join(str(t) for t in (context.get("triples") or [])[:40]),
|
| 321 |
+
" ".join(str(h) for h in (context.get("historical_context") or [])[:20]),
|
| 322 |
+
]
|
| 323 |
+
).lower()
|
| 324 |
+
abstained = is_soft_abstain(draft_answer or "")
|
| 325 |
+
# Do not gap-fill biography abstention prompts — retries invent answers.
|
| 326 |
+
if abstained and _BIOGRAPHY_ABSTAIN_RE.search(question or ""):
|
| 327 |
+
return GapFillPlan(
|
| 328 |
+
needs_retry=False,
|
| 329 |
+
reformulated_query=question.strip(),
|
| 330 |
+
missing_slots=[],
|
| 331 |
+
)
|
| 332 |
+
q_tokens = [t for t in re.findall(r"[a-z0-9]{3,}", question.lower()) if t]
|
| 333 |
+
covered = sum(1 for t in q_tokens if t in text_blob) if q_tokens else 0
|
| 334 |
+
coverage = covered / len(q_tokens) if q_tokens else 1.0
|
| 335 |
+
missing = [t for t in q_tokens if t not in text_blob][:8]
|
| 336 |
+
thin_list = bool(_LIST_QUESTION_RE.search(question)) and len(draft) < 80
|
| 337 |
+
only_n = parse_only_n(question)
|
| 338 |
+
numbered = len(
|
| 339 |
+
[ln for ln in (draft_answer or "").splitlines() if re.match(r"^\s*\d+[\).\]]", ln)]
|
| 340 |
+
)
|
| 341 |
+
thin_ordering = is_ordering_question(question) and (
|
| 342 |
+
(only_n is not None and numbered < only_n)
|
| 343 |
+
or (only_n is None and draft.count("\n") < 3 and "," not in draft)
|
| 344 |
+
)
|
| 345 |
+
undercount = bool(_COUNT_QUESTION_RE.search(question)) and bool(
|
| 346 |
+
_SINGLE_COUNT_RE.search(draft)
|
| 347 |
+
)
|
| 348 |
+
temporal_thin = bool(_TEMPORAL_QUESTION_RE.search(question)) and (
|
| 349 |
+
abstained
|
| 350 |
+
or bool(_VAGUE_TEMPORAL_ANSWER_RE.search(draft))
|
| 351 |
+
or len(draft) < 20
|
| 352 |
+
)
|
| 353 |
+
version_thin = bool(_VERSION_QUESTION_RE.search(question)) and not bool(
|
| 354 |
+
re.search(r"\b\d+\.\d+", draft)
|
| 355 |
+
)
|
| 356 |
+
contradiction_thin = bool(_CONTRADICTION_HINT_RE.search(question)) and not bool(
|
| 357 |
+
re.search(r"contradict|conflict|inconsisten", draft, re.I)
|
| 358 |
+
)
|
| 359 |
+
needs = (
|
| 360 |
+
abstained
|
| 361 |
+
or (coverage < 0.45 and len(draft) < 40)
|
| 362 |
+
or thin_list
|
| 363 |
+
or thin_ordering
|
| 364 |
+
or undercount
|
| 365 |
+
or temporal_thin
|
| 366 |
+
or version_thin
|
| 367 |
+
or contradiction_thin
|
| 368 |
+
)
|
| 369 |
+
reform = question.strip()
|
| 370 |
+
focus_bits: list[str] = []
|
| 371 |
+
if temporal_thin:
|
| 372 |
+
focus_bits.append("stated dates and intervals before later updates")
|
| 373 |
+
if thin_ordering:
|
| 374 |
+
focus_bits.append(
|
| 375 |
+
"short aspect labels in first-mention order: setup, core functionality, "
|
| 376 |
+
"transaction CRUD/errors, security, deployment, integration tests"
|
| 377 |
+
)
|
| 378 |
+
if contradiction_thin:
|
| 379 |
+
focus_bits.append("conflicting statements across turns")
|
| 380 |
+
if version_thin:
|
| 381 |
+
focus_bits.append("library names with explicit versions")
|
| 382 |
+
if undercount:
|
| 383 |
+
focus_bits.append("all occasions across sessions")
|
| 384 |
+
elif thin_list:
|
| 385 |
+
focus_bits.append("complete list from every session")
|
| 386 |
+
focus_bits.extend(missing[: max(0, 5 - len(focus_bits))])
|
| 387 |
+
if focus_bits:
|
| 388 |
+
reform = f"{question.strip()} (focus: {', '.join(focus_bits[:5])})"
|
| 389 |
+
return GapFillPlan(
|
| 390 |
+
needs_retry=needs,
|
| 391 |
+
reformulated_query=reform,
|
| 392 |
+
missing_slots=missing,
|
| 393 |
+
)
|
| 394 |
+
|
| 395 |
+
|
| 396 |
+
def merge_contexts(
|
| 397 |
+
primary: dict[str, Any], secondary: dict[str, Any]
|
| 398 |
+
) -> dict[str, Any]:
|
| 399 |
+
out = dict(primary)
|
| 400 |
+
for key in ("source_passages", "historical_context", "triples", "paths", "topics"):
|
| 401 |
+
a = list(primary.get(key) or [])
|
| 402 |
+
b = list(secondary.get(key) or [])
|
| 403 |
+
seen: set[str] = set()
|
| 404 |
+
merged: list[Any] = []
|
| 405 |
+
for item in a + b:
|
| 406 |
+
sig = str(item)
|
| 407 |
+
if sig in seen:
|
| 408 |
+
continue
|
| 409 |
+
seen.add(sig)
|
| 410 |
+
merged.append(item)
|
| 411 |
+
out[key] = merged
|
| 412 |
+
text_a = str(primary.get("text_context") or "")
|
| 413 |
+
text_b = str(secondary.get("text_context") or "")
|
| 414 |
+
if text_b and text_b not in text_a:
|
| 415 |
+
out["text_context"] = (text_a + "\n" + text_b).strip()
|
| 416 |
+
return out
|
| 417 |
+
|
| 418 |
+
|
| 419 |
+
def profile_defaults(profile: str) -> dict[str, Any]:
|
| 420 |
+
name = (profile or "product").strip().lower()
|
| 421 |
+
if name == "sota":
|
| 422 |
+
return {
|
| 423 |
+
"bench_profile": "sota",
|
| 424 |
+
"sc_samples": int(os.getenv("BENCH_SC_SAMPLES", "5")),
|
| 425 |
+
"sc_temperature": float(os.getenv("BENCH_SC_TEMPERATURE", "0.7")),
|
| 426 |
+
"gap_fill": os.getenv("BENCH_GAP_FILL", "1") not in {"0", "false", "False"},
|
| 427 |
+
}
|
| 428 |
+
return {
|
| 429 |
+
"bench_profile": "product",
|
| 430 |
+
"sc_samples": int(os.getenv("BENCH_SC_SAMPLES", "1")),
|
| 431 |
+
"sc_temperature": 0.0,
|
| 432 |
+
"gap_fill": os.getenv("BENCH_GAP_FILL", "0") in {"1", "true", "True"},
|
| 433 |
+
}
|
benchmarks/beam/test_beam_helpers.py
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
from beam.dataset import (
|
| 6 |
+
brain_id_for,
|
| 7 |
+
concatenate_plan_batches,
|
| 8 |
+
convert_chat_batches,
|
| 9 |
+
extract_normalized_chat,
|
| 10 |
+
format_turn_text,
|
| 11 |
+
ingest_target_turns,
|
| 12 |
+
iter_ingest_units,
|
| 13 |
+
write_conversation,
|
| 14 |
+
)
|
| 15 |
+
from beam.ingest import (
|
| 16 |
+
completed_unit_ids,
|
| 17 |
+
is_permanent_ingest_error,
|
| 18 |
+
normalize_ingest_status,
|
| 19 |
+
)
|
| 20 |
+
from beam.judge import parse_json_response
|
| 21 |
+
from beam.metrics import aggregate_answers, selftest_metrics
|
| 22 |
+
from beam.prompts import ability_system_prompt, build_rubric_judge_prompt
|
| 23 |
+
from beam.sota import majority_vote, plan_gap_fill, profile_defaults
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def test_brain_id_for() -> None:
|
| 27 |
+
assert brain_id_for("100K", "1") == "beam100k1"
|
| 28 |
+
assert brain_id_for("500K", "3") == "beam500k3"
|
| 29 |
+
assert brain_id_for("1M", "12") == "beam1m12"
|
| 30 |
+
assert brain_id_for("10M", "1") == "beam10m1"
|
| 31 |
+
assert brain_id_for("10M", "1clean") == "beam10m1clean"
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def test_convert_chat_batches_splits_on_main_question() -> None:
|
| 35 |
+
chat = [
|
| 36 |
+
[
|
| 37 |
+
{
|
| 38 |
+
"role": "user",
|
| 39 |
+
"content": "Q1",
|
| 40 |
+
"question_type": "main_question",
|
| 41 |
+
"time_anchor": "March-15-2024",
|
| 42 |
+
},
|
| 43 |
+
{"role": "assistant", "content": "A1"},
|
| 44 |
+
{
|
| 45 |
+
"role": "user",
|
| 46 |
+
"content": "Q2",
|
| 47 |
+
"question_type": "main_question",
|
| 48 |
+
},
|
| 49 |
+
{"role": "assistant", "content": "A2"},
|
| 50 |
+
]
|
| 51 |
+
]
|
| 52 |
+
batches = convert_chat_batches(chat)
|
| 53 |
+
assert len(batches) == 1
|
| 54 |
+
assert len(batches[0]["turns"]) == 2
|
| 55 |
+
assert batches[0]["turns"][0][0]["content"] == "Q1"
|
| 56 |
+
assert batches[0]["turns"][1][0]["content"] == "Q2"
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def _raw_turn(content: str, *, anchor: str = "March-15-2024") -> list[dict]:
|
| 60 |
+
return [
|
| 61 |
+
{
|
| 62 |
+
"role": "user",
|
| 63 |
+
"content": content,
|
| 64 |
+
"question_type": "main_question",
|
| 65 |
+
"time_anchor": anchor,
|
| 66 |
+
},
|
| 67 |
+
{"role": "assistant", "content": f"re:{content}"},
|
| 68 |
+
]
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def test_extract_normalized_chat_10m_plan_keys() -> None:
|
| 72 |
+
row = {
|
| 73 |
+
"chat": {
|
| 74 |
+
"plan-2": [
|
| 75 |
+
{
|
| 76 |
+
"batch_number": 1,
|
| 77 |
+
"time_anchor": None,
|
| 78 |
+
"turns": [_raw_turn("p2")],
|
| 79 |
+
}
|
| 80 |
+
],
|
| 81 |
+
"plan-1": [
|
| 82 |
+
{
|
| 83 |
+
"batch_number": 1,
|
| 84 |
+
"time_anchor": None,
|
| 85 |
+
"turns": [_raw_turn("p1a"), _raw_turn("p1b")],
|
| 86 |
+
}
|
| 87 |
+
],
|
| 88 |
+
},
|
| 89 |
+
"plans": [],
|
| 90 |
+
"probing_questions": {"summarization": [{"question": "Sum?", "rubric": ["x"]}]},
|
| 91 |
+
}
|
| 92 |
+
chat_json, meta = extract_normalized_chat(row, size="10M")
|
| 93 |
+
assert meta["normalize_strategy"] == "concatenate_plans_chronological"
|
| 94 |
+
assert meta["n_plans"] == 2
|
| 95 |
+
assert meta["plan_source"] == "chat_plan_keys"
|
| 96 |
+
assert [b["plan_id"] for b in chat_json] == ["plan-1", "plan-2"]
|
| 97 |
+
assert [b["batch_number"] for b in chat_json] == [1, 2]
|
| 98 |
+
assert len(chat_json[0]["turns"]) == 2
|
| 99 |
+
assert chat_json[0]["turns"][0][0]["content"] == "p1a"
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def test_extract_normalized_chat_10m_plans_array_raw() -> None:
|
| 103 |
+
row = {
|
| 104 |
+
"chat": None,
|
| 105 |
+
"plans": [
|
| 106 |
+
{
|
| 107 |
+
"plan_id": "plan-10",
|
| 108 |
+
"chat": [
|
| 109 |
+
[
|
| 110 |
+
{
|
| 111 |
+
"role": "user",
|
| 112 |
+
"content": "late",
|
| 113 |
+
"question_type": "main_question",
|
| 114 |
+
"time_anchor": "April-01-2024",
|
| 115 |
+
},
|
| 116 |
+
{"role": "assistant", "content": "re:late"},
|
| 117 |
+
]
|
| 118 |
+
],
|
| 119 |
+
},
|
| 120 |
+
{
|
| 121 |
+
"plan_id": "plan-1",
|
| 122 |
+
"chat": [
|
| 123 |
+
[
|
| 124 |
+
{
|
| 125 |
+
"role": "user",
|
| 126 |
+
"content": "early-a",
|
| 127 |
+
"question_type": "main_question",
|
| 128 |
+
"time_anchor": "March-15-2024",
|
| 129 |
+
},
|
| 130 |
+
{"role": "assistant", "content": "a"},
|
| 131 |
+
{
|
| 132 |
+
"role": "user",
|
| 133 |
+
"content": "early-b",
|
| 134 |
+
"question_type": "main_question",
|
| 135 |
+
"time_anchor": "March-16-2024",
|
| 136 |
+
},
|
| 137 |
+
{"role": "assistant", "content": "b"},
|
| 138 |
+
]
|
| 139 |
+
],
|
| 140 |
+
},
|
| 141 |
+
],
|
| 142 |
+
}
|
| 143 |
+
chat_json, meta = extract_normalized_chat(row, size="10M")
|
| 144 |
+
assert meta["normalize_strategy"] == "concatenate_plans_chronological"
|
| 145 |
+
assert meta["plan_source"] == "plans_array"
|
| 146 |
+
assert [b["plan_id"] for b in chat_json] == ["plan-1", "plan-10"]
|
| 147 |
+
assert [b["batch_number"] for b in chat_json] == [1, 2]
|
| 148 |
+
sample = {
|
| 149 |
+
"sample_id": "10M/1",
|
| 150 |
+
"size": "10M",
|
| 151 |
+
"conversation_id": "1",
|
| 152 |
+
"chat": chat_json,
|
| 153 |
+
"probing_questions": {},
|
| 154 |
+
}
|
| 155 |
+
units = iter_ingest_units(sample)
|
| 156 |
+
assert len(units) == 3
|
| 157 |
+
assert [u["unit_id"] for u in units] == ["b1_t1", "b1_t2", "b2_t1"]
|
| 158 |
+
assert [u["session_key"] for u in units] == [
|
| 159 |
+
"session_1",
|
| 160 |
+
"session_2",
|
| 161 |
+
"session_3",
|
| 162 |
+
]
|
| 163 |
+
assert "early-a" in units[0]["text"]
|
| 164 |
+
assert "late" in units[2]["text"]
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def test_concatenate_plan_batches_renumbers_globally() -> None:
|
| 168 |
+
chat_json, plan_meta = concatenate_plan_batches(
|
| 169 |
+
[
|
| 170 |
+
(
|
| 171 |
+
"plan-1",
|
| 172 |
+
[
|
| 173 |
+
{"turns": [_raw_turn("a")], "time_anchor": None},
|
| 174 |
+
{"turns": [_raw_turn("b")], "time_anchor": None},
|
| 175 |
+
],
|
| 176 |
+
),
|
| 177 |
+
("plan-2", [{"turns": [_raw_turn("c")], "time_anchor": None}]),
|
| 178 |
+
]
|
| 179 |
+
)
|
| 180 |
+
assert [b["batch_number"] for b in chat_json] == [1, 2, 3]
|
| 181 |
+
assert plan_meta[0]["batch_number_start"] == 1
|
| 182 |
+
assert plan_meta[0]["batch_number_end"] == 2
|
| 183 |
+
assert plan_meta[1]["batch_number_start"] == 3
|
| 184 |
+
assert ingest_target_turns(100) == 95
|
| 185 |
+
assert ingest_target_turns(0) == 0
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def test_write_conversation_10m_fixture(tmp_path: Path) -> None:
|
| 189 |
+
row = {
|
| 190 |
+
"conversation_id": "1",
|
| 191 |
+
"conversation_seed": {"title": "Fixture 10M"},
|
| 192 |
+
"chat": {
|
| 193 |
+
"plan-1": [
|
| 194 |
+
{
|
| 195 |
+
"batch_number": 1,
|
| 196 |
+
"turns": [_raw_turn("hello")],
|
| 197 |
+
"time_anchor": None,
|
| 198 |
+
}
|
| 199 |
+
]
|
| 200 |
+
},
|
| 201 |
+
"plans": [],
|
| 202 |
+
"probing_questions": {
|
| 203 |
+
"information_extraction": [{"question": "What?", "rubric": ["hello"]}]
|
| 204 |
+
},
|
| 205 |
+
}
|
| 206 |
+
dest = tmp_path / "10M" / "1"
|
| 207 |
+
write_conversation(
|
| 208 |
+
dest,
|
| 209 |
+
size="10M",
|
| 210 |
+
conversation_id="1",
|
| 211 |
+
topic=row["conversation_seed"],
|
| 212 |
+
probing_questions=row["probing_questions"],
|
| 213 |
+
row=row,
|
| 214 |
+
)
|
| 215 |
+
meta = __import__("json").loads((dest / "meta.json").read_text(encoding="utf-8"))
|
| 216 |
+
assert meta["brain_id"] == "beam10m1"
|
| 217 |
+
assert meta["n_turns"] == 1
|
| 218 |
+
assert meta["ingest_target"] == 1
|
| 219 |
+
assert meta["normalize_strategy"] == "concatenate_plans_chronological"
|
| 220 |
+
chat = __import__("json").loads((dest / "chat.json").read_text(encoding="utf-8"))
|
| 221 |
+
units = iter_ingest_units(
|
| 222 |
+
{
|
| 223 |
+
"sample_id": "10M/1",
|
| 224 |
+
"chat": chat,
|
| 225 |
+
"probing_questions": {},
|
| 226 |
+
}
|
| 227 |
+
)
|
| 228 |
+
assert len(units) == 1
|
| 229 |
+
assert units[0]["unit_id"] == "b1_t1"
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def test_format_turn_text_normalizes_time_anchor() -> None:
|
| 233 |
+
text, ts = format_turn_text(
|
| 234 |
+
[
|
| 235 |
+
{
|
| 236 |
+
"role": "user",
|
| 237 |
+
"content": "Hello",
|
| 238 |
+
"time_anchor": "March-15-2024",
|
| 239 |
+
"question_type": "main_question",
|
| 240 |
+
},
|
| 241 |
+
{"role": "assistant", "content": "Hi"},
|
| 242 |
+
],
|
| 243 |
+
batch_number=1,
|
| 244 |
+
turn_index=0,
|
| 245 |
+
global_turn=7,
|
| 246 |
+
)
|
| 247 |
+
assert "Session id: session_7." in text
|
| 248 |
+
assert "Unit id: b1_t1." in text
|
| 249 |
+
assert "user [main_question]" in text
|
| 250 |
+
assert ts == "March 15, 2024"
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
def test_rubric_prompt_substitutes_placeholders() -> None:
|
| 254 |
+
prompt = build_rubric_judge_prompt("Q?", "must say X", "X")
|
| 255 |
+
assert "<question>" not in prompt
|
| 256 |
+
assert "<rubric_item>" not in prompt
|
| 257 |
+
assert "<llm_response>" not in prompt
|
| 258 |
+
assert "Q?" in prompt
|
| 259 |
+
assert "must say X" in prompt
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
def test_parse_json_response_keeps_half_scores() -> None:
|
| 263 |
+
payload = parse_json_response('{"score": 0.5, "reason": "partial"}')
|
| 264 |
+
assert float(payload["score"]) == 0.5
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
def test_aggregate_answers_uses_tau_for_event_ordering() -> None:
|
| 268 |
+
errors = selftest_metrics()
|
| 269 |
+
assert errors == []
|
| 270 |
+
metrics = aggregate_answers(
|
| 271 |
+
[
|
| 272 |
+
{"ability": "event_ordering", "llm_judge_score": 0.1, "tau_norm": 0.9},
|
| 273 |
+
{"ability": "summarization", "llm_judge_score": 0.5},
|
| 274 |
+
]
|
| 275 |
+
)
|
| 276 |
+
assert metrics["per_ability"]["event_ordering"]["mean"] == 0.9
|
| 277 |
+
assert metrics["per_ability"]["summarization"]["mean"] == 0.5
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
def test_profile_defaults_sota() -> None:
|
| 281 |
+
product = profile_defaults("product")
|
| 282 |
+
sota = profile_defaults("sota")
|
| 283 |
+
assert product["bench_profile"] == "product"
|
| 284 |
+
assert product["sc_samples"] == 1
|
| 285 |
+
assert sota["bench_profile"] == "sota"
|
| 286 |
+
assert sota["sc_samples"] >= 3
|
| 287 |
+
assert sota["gap_fill"] is True
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
def test_gap_fill_on_thin_ordering() -> None:
|
| 291 |
+
plan = plan_gap_fill(
|
| 292 |
+
"Can you list the order in which I brought up aspects?",
|
| 293 |
+
"1. Auth 2. Tracking",
|
| 294 |
+
{"text_context": "auth tracking analytics transactions"},
|
| 295 |
+
)
|
| 296 |
+
assert plan.needs_retry is True
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
def test_majority_vote_prefers_substantive() -> None:
|
| 300 |
+
assert "March 29" in majority_vote(
|
| 301 |
+
["Not mentioned in the conversation", "March 29", "March 29"]
|
| 302 |
+
)
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
def test_ability_system_prompt_includes_ordering_hint() -> None:
|
| 306 |
+
text = ability_system_prompt("event_ordering")
|
| 307 |
+
assert "ONLY N" in text or "milestone" in text.lower() or "aspect" in text.lower()
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
def test_ordering_aspect_queries_for_only_three() -> None:
|
| 311 |
+
from beam.sota import ordering_aspect_queries, parse_only_n
|
| 312 |
+
|
| 313 |
+
q = (
|
| 314 |
+
"Can you list the order in which I brought up different aspects "
|
| 315 |
+
"of developing my personal budget tracker throughout our conversations, "
|
| 316 |
+
"in order? Mention ONLY and ONLY three items."
|
| 317 |
+
)
|
| 318 |
+
assert parse_only_n(q) == 3
|
| 319 |
+
aspects = ordering_aspect_queries(q)
|
| 320 |
+
assert len(aspects) >= 3
|
| 321 |
+
blob = " ".join(aspects).lower()
|
| 322 |
+
assert "transaction" in blob or "security" in blob
|
| 323 |
+
|
| 324 |
+
|
| 325 |
+
def test_ordering_aspect_queries_translation_domain() -> None:
|
| 326 |
+
from beam.sota import ordering_aspect_queries
|
| 327 |
+
|
| 328 |
+
q = (
|
| 329 |
+
"How did my discussions about integrating and optimizing language and "
|
| 330 |
+
"translation services progress in order? Mention ONLY and ONLY ten items."
|
| 331 |
+
)
|
| 332 |
+
aspects = ordering_aspect_queries(q)
|
| 333 |
+
blob = " ".join(aspects).lower()
|
| 334 |
+
assert "translation" in blob or "deepl" in blob or "language detection" in blob
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
def test_biography_abstain_skips_gap_fill() -> None:
|
| 338 |
+
plan = plan_gap_fill(
|
| 339 |
+
"Can you tell me about my background and previous development projects?",
|
| 340 |
+
"Not mentioned in the conversation.",
|
| 341 |
+
{"text_context": "Flask budget tracker authentication"},
|
| 342 |
+
)
|
| 343 |
+
assert plan.needs_retry is False
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
def test_lexical_aspect_match_accepts_paraphrase() -> None:
|
| 347 |
+
from beam.judge import lexical_aspect_match, _strip_list_prefix
|
| 348 |
+
|
| 349 |
+
assert _strip_list_prefix("1. Security hardening and deployment") == (
|
| 350 |
+
"Security hardening and deployment"
|
| 351 |
+
)
|
| 352 |
+
assert lexical_aspect_match(
|
| 353 |
+
"Security and deployment",
|
| 354 |
+
"1. Security hardening and deployment",
|
| 355 |
+
)
|
| 356 |
+
assert lexical_aspect_match(
|
| 357 |
+
"Transaction error handling",
|
| 358 |
+
"Transaction CRUD and error handling",
|
| 359 |
+
)
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
def test_permanent_embed_8192_detection() -> None:
|
| 363 |
+
err = (
|
| 364 |
+
"Error code: 400 - {'error': {'message': \"Invalid 'input': "
|
| 365 |
+
"maximum context length is 8192 tokens.\"}}"
|
| 366 |
+
)
|
| 367 |
+
assert is_permanent_ingest_error(err)
|
| 368 |
+
assert normalize_ingest_status("failed", err) == "permanent_failed"
|
| 369 |
+
assert not is_permanent_ingest_error("Connection refused")
|
| 370 |
+
assert normalize_ingest_status("failed", "Connection refused") == "failed"
|
| 371 |
+
|
| 372 |
+
|
| 373 |
+
def test_resume_skips_permanent_and_legacy_failed(tmp_path: Path) -> None:
|
| 374 |
+
path = tmp_path / "ingest.jsonl"
|
| 375 |
+
rows = [
|
| 376 |
+
{
|
| 377 |
+
"sample_id": "1M/1",
|
| 378 |
+
"unit_id": "b1_t1",
|
| 379 |
+
"status": "completed",
|
| 380 |
+
"error": None,
|
| 381 |
+
},
|
| 382 |
+
{
|
| 383 |
+
"sample_id": "1M/1",
|
| 384 |
+
"unit_id": "b5_t25",
|
| 385 |
+
"status": "failed",
|
| 386 |
+
"error": "Invalid 'input': maximum context length is 8192 tokens.",
|
| 387 |
+
},
|
| 388 |
+
{
|
| 389 |
+
"sample_id": "1M/1",
|
| 390 |
+
"unit_id": "b2_t1",
|
| 391 |
+
"status": "failed",
|
| 392 |
+
"error": "Connection refused",
|
| 393 |
+
},
|
| 394 |
+
{
|
| 395 |
+
"sample_id": "1M/1",
|
| 396 |
+
"unit_id": "b6_t2",
|
| 397 |
+
"status": "permanent_failed",
|
| 398 |
+
"error": "maximum context length is 8192 tokens.",
|
| 399 |
+
},
|
| 400 |
+
]
|
| 401 |
+
path.write_text(
|
| 402 |
+
"\n".join(__import__("json").dumps(r) for r in rows) + "\n",
|
| 403 |
+
encoding="utf-8",
|
| 404 |
+
)
|
| 405 |
+
done = completed_unit_ids(path)
|
| 406 |
+
assert "1M/1::b1_t1" in done
|
| 407 |
+
assert "1M/1::b5_t25" in done
|
| 408 |
+
assert "1M/1::b6_t2" in done
|
| 409 |
+
assert "1M/1::b2_t1" not in done
|
benchmarks/data/.gitkeep
ADDED
|
File without changes
|
benchmarks/locomo.sh
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -euo pipefail
|
| 3 |
+
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
| 4 |
+
PYTHON="${ROOT}/.venv/bin/python"
|
| 5 |
+
if [[ ! -x "$PYTHON" ]]; then
|
| 6 |
+
echo "Missing ${PYTHON}. Create it with:"
|
| 7 |
+
echo " cd benchmarks && python3 -m venv .venv && .venv/bin/pip install -r requirements.txt"
|
| 8 |
+
exit 1
|
| 9 |
+
fi
|
| 10 |
+
exec "$PYTHON" -m locomo "$@"
|
benchmarks/locomo/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""LoCoMo benchmark harness for BrainAPI."""
|
| 2 |
+
|
| 3 |
+
__version__ = "0.1.0"
|
benchmarks/locomo/__main__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from locomo.cli import main
|
| 2 |
+
|
| 3 |
+
if __name__ == "__main__":
|
| 4 |
+
raise SystemExit(main())
|
benchmarks/locomo/answer.py
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
import time
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
from openai import AzureOpenAI, OpenAI
|
| 9 |
+
|
| 10 |
+
from locomo.config import Settings
|
| 11 |
+
from locomo.prompts import build_answer_messages
|
| 12 |
+
from locomo.sota import (
|
| 13 |
+
_COUNT_QUESTION_RE,
|
| 14 |
+
_LOW_PEOPLE_COUNT_RE,
|
| 15 |
+
_PEOPLE_COUNT_QUESTION_RE,
|
| 16 |
+
_SINGLE_COUNT_RE,
|
| 17 |
+
_TRAITS_QUESTION_RE,
|
| 18 |
+
compact_recent_paint_answer,
|
| 19 |
+
compact_symbols_answer,
|
| 20 |
+
complete_education_fields,
|
| 21 |
+
image_pack_incomplete,
|
| 22 |
+
is_hard_abstain,
|
| 23 |
+
is_soft_abstain,
|
| 24 |
+
majority_vote,
|
| 25 |
+
majority_vote_count,
|
| 26 |
+
majority_vote_traits,
|
| 27 |
+
strip_leading_abstain,
|
| 28 |
+
strip_week_of_contradiction,
|
| 29 |
+
symbols_missing_from_answer,
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
_INFER_NUDGE = (
|
| 33 |
+
"Your previous reply abstained. Do not abstain. Using only the retrieved "
|
| 34 |
+
"context, give your best short hedged yes/no, likelihood, or field list "
|
| 35 |
+
"conclusion, then the decisive supporting fact from the dialogue."
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
_COMPLETE_NUDGE = (
|
| 39 |
+
"Your previous reply may be incomplete. Scan EVERY passage, historical "
|
| 40 |
+
"chunk, graph triple, caption, image-query line, and path again. Return a "
|
| 41 |
+
"complete short answer that includes every matching item, count, trait, "
|
| 42 |
+
"affect word, or field implied by the dialogue—without inventing "
|
| 43 |
+
"unsupported facts."
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
_SYMBOLS_NUDGE = (
|
| 47 |
+
"Your previous reply missed identity symbols from the image-query lines. "
|
| 48 |
+
"Answer again with ONLY a short comma-separated list of the core "
|
| 49 |
+
"identity/community symbols named there—typically a rainbow/pride flag and "
|
| 50 |
+
"a transgender symbol when those appear in image queries. Omit posters, "
|
| 51 |
+
"umbrellas, sidewalks, murals-as-scenes, jewelry materials, bowls, and paintings."
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
_BOOKS_NUDGE = (
|
| 55 |
+
"Your previous reply may have missed book titles. Scan dialogue and "
|
| 56 |
+
"image-query lines for book titles or book-cover queries and return a "
|
| 57 |
+
"compact comma-separated list of books read—without inventing titles."
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
_EDUCATION_NUDGE = (
|
| 61 |
+
"Your previous reply named only practice labels. Answer again with the "
|
| 62 |
+
"academic study field for helping/behavioral-health work together with the "
|
| 63 |
+
"counseling or therapy path and any certification or license track."
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
_KIND_NUDGE = (
|
| 67 |
+
"Your previous reply may be too broad. Prefer the single stylistic or "
|
| 68 |
+
"categorical label the speakers use (for example abstract) rather than "
|
| 69 |
+
"listing themes and media."
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
_ACTIVITY_NUDGE = (
|
| 73 |
+
"Your previous reply may have missed concrete activities. Prefer the "
|
| 74 |
+
"specific things speakers name doing on family hikes/camping/campfires "
|
| 75 |
+
"(for example roasting marshmallows and telling stories) over generic "
|
| 76 |
+
"explore-nature phrasing."
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
_CHILD_AFFECT_NUDGE = (
|
| 80 |
+
"Your previous reply missed the child's own reaction. Answer again with how "
|
| 81 |
+
"the child handled it: scared at first, then reassured by family, when that is "
|
| 82 |
+
"what the dialogue says."
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
@dataclass
|
| 87 |
+
class AnswerResult:
|
| 88 |
+
answer: str
|
| 89 |
+
model: str
|
| 90 |
+
latency_ms: float
|
| 91 |
+
prompt_tokens: int | None
|
| 92 |
+
completion_tokens: int | None
|
| 93 |
+
total_tokens: int | None
|
| 94 |
+
sc_samples_used: int = 1
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def _make_openai_client(settings: Settings) -> OpenAI | AzureOpenAI:
|
| 98 |
+
settings.require_llm()
|
| 99 |
+
if settings.answer_azure_endpoint:
|
| 100 |
+
return AzureOpenAI(
|
| 101 |
+
api_key=settings.openai_api_key,
|
| 102 |
+
azure_endpoint=settings.answer_azure_endpoint,
|
| 103 |
+
api_version=settings.answer_azure_api_version,
|
| 104 |
+
)
|
| 105 |
+
kwargs: dict[str, Any] = {"api_key": settings.openai_api_key}
|
| 106 |
+
if settings.llm_base_url:
|
| 107 |
+
kwargs["base_url"] = settings.llm_base_url
|
| 108 |
+
return OpenAI(**kwargs)
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def _one_completion(
|
| 112 |
+
client: OpenAI | AzureOpenAI,
|
| 113 |
+
*,
|
| 114 |
+
model_name: str,
|
| 115 |
+
messages: list[dict[str, str]],
|
| 116 |
+
temperature: float,
|
| 117 |
+
) -> tuple[str, Any]:
|
| 118 |
+
response = client.chat.completions.create(
|
| 119 |
+
model=model_name,
|
| 120 |
+
messages=messages,
|
| 121 |
+
temperature=temperature,
|
| 122 |
+
)
|
| 123 |
+
content = (response.choices[0].message.content or "").strip()
|
| 124 |
+
return content, response
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def _accumulate_usage(
|
| 128 |
+
response: Any,
|
| 129 |
+
prompt_tokens: int,
|
| 130 |
+
completion_tokens: int,
|
| 131 |
+
total_tokens: int,
|
| 132 |
+
) -> tuple[int, int, int]:
|
| 133 |
+
if response.usage:
|
| 134 |
+
prompt_tokens += getattr(response.usage, "prompt_tokens", 0) or 0
|
| 135 |
+
completion_tokens += getattr(response.usage, "completion_tokens", 0) or 0
|
| 136 |
+
total_tokens += getattr(response.usage, "total_tokens", 0) or 0
|
| 137 |
+
return prompt_tokens, completion_tokens, total_tokens
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def answer_question(
|
| 141 |
+
settings: Settings,
|
| 142 |
+
question: str,
|
| 143 |
+
context: dict[str, Any],
|
| 144 |
+
*,
|
| 145 |
+
model: str | None = None,
|
| 146 |
+
sc_samples: int | None = None,
|
| 147 |
+
sc_temperature: float | None = None,
|
| 148 |
+
force_infer_on_abstain: bool = True,
|
| 149 |
+
) -> AnswerResult:
|
| 150 |
+
client = _make_openai_client(settings)
|
| 151 |
+
model_name = model or settings.answer_model
|
| 152 |
+
messages = build_answer_messages(question, context)
|
| 153 |
+
n = max(1, int(sc_samples if sc_samples is not None else settings.sc_samples))
|
| 154 |
+
temperature = (
|
| 155 |
+
float(sc_temperature)
|
| 156 |
+
if sc_temperature is not None
|
| 157 |
+
else (settings.sc_temperature if n > 1 else 0.0)
|
| 158 |
+
)
|
| 159 |
+
started = time.perf_counter()
|
| 160 |
+
prompt_tokens = 0
|
| 161 |
+
completion_tokens = 0
|
| 162 |
+
total_tokens = 0
|
| 163 |
+
samples: list[str] = []
|
| 164 |
+
last_response: Any = None
|
| 165 |
+
samples_used = n
|
| 166 |
+
|
| 167 |
+
for _ in range(n):
|
| 168 |
+
content, response = _one_completion(
|
| 169 |
+
client,
|
| 170 |
+
model_name=model_name,
|
| 171 |
+
messages=messages,
|
| 172 |
+
temperature=0.0 if n == 1 else temperature,
|
| 173 |
+
)
|
| 174 |
+
last_response = response
|
| 175 |
+
prompt_tokens, completion_tokens, total_tokens = _accumulate_usage(
|
| 176 |
+
response, prompt_tokens, completion_tokens, total_tokens
|
| 177 |
+
)
|
| 178 |
+
if content:
|
| 179 |
+
samples.append(content)
|
| 180 |
+
|
| 181 |
+
q_lower = question.lower()
|
| 182 |
+
content = ""
|
| 183 |
+
if samples:
|
| 184 |
+
if _TRAITS_QUESTION_RE.search(question):
|
| 185 |
+
content = majority_vote_traits(samples) or majority_vote(samples)
|
| 186 |
+
elif _PEOPLE_COUNT_QUESTION_RE.search(question):
|
| 187 |
+
content = majority_vote_count(samples) or majority_vote(samples)
|
| 188 |
+
else:
|
| 189 |
+
content = majority_vote(samples)
|
| 190 |
+
content = strip_leading_abstain(content)
|
| 191 |
+
|
| 192 |
+
if not content:
|
| 193 |
+
retry_messages = messages + [
|
| 194 |
+
{
|
| 195 |
+
"role": "user",
|
| 196 |
+
"content": (
|
| 197 |
+
"Your previous reply was empty. Answer the question now using the "
|
| 198 |
+
"retrieved context. Prefer a short factual list or phrase."
|
| 199 |
+
),
|
| 200 |
+
}
|
| 201 |
+
]
|
| 202 |
+
content, response = _one_completion(
|
| 203 |
+
client,
|
| 204 |
+
model_name=model_name,
|
| 205 |
+
messages=retry_messages,
|
| 206 |
+
temperature=0,
|
| 207 |
+
)
|
| 208 |
+
last_response = response
|
| 209 |
+
samples_used += 1
|
| 210 |
+
prompt_tokens, completion_tokens, total_tokens = _accumulate_usage(
|
| 211 |
+
response, prompt_tokens, completion_tokens, total_tokens
|
| 212 |
+
)
|
| 213 |
+
content = strip_leading_abstain(content)
|
| 214 |
+
|
| 215 |
+
if force_infer_on_abstain and is_soft_abstain(content):
|
| 216 |
+
infer_messages = messages + [
|
| 217 |
+
{"role": "assistant", "content": content or "Not mentioned in the conversation"},
|
| 218 |
+
{"role": "user", "content": _INFER_NUDGE},
|
| 219 |
+
]
|
| 220 |
+
content, response = _one_completion(
|
| 221 |
+
client,
|
| 222 |
+
model_name=model_name,
|
| 223 |
+
messages=infer_messages,
|
| 224 |
+
temperature=0,
|
| 225 |
+
)
|
| 226 |
+
last_response = response
|
| 227 |
+
samples_used += 1
|
| 228 |
+
prompt_tokens, completion_tokens, total_tokens = _accumulate_usage(
|
| 229 |
+
response, prompt_tokens, completion_tokens, total_tokens
|
| 230 |
+
)
|
| 231 |
+
content = strip_leading_abstain(content)
|
| 232 |
+
|
| 233 |
+
undercount_draft = bool(_COUNT_QUESTION_RE.search(question)) and bool(
|
| 234 |
+
_SINGLE_COUNT_RE.search(content or "")
|
| 235 |
+
)
|
| 236 |
+
people_undercount = bool(_PEOPLE_COUNT_QUESTION_RE.search(question)) and bool(
|
| 237 |
+
_LOW_PEOPLE_COUNT_RE.search(content or "")
|
| 238 |
+
)
|
| 239 |
+
# Only list-books questions — not library kind, favorite, recommend, or bookshelf yes/no.
|
| 240 |
+
books_q = bool(re.search(r"\bwhat books\b.+\bread\b|\bbooks has\b.+\bread\b", q_lower))
|
| 241 |
+
symbols_q = "symbols" in q_lower
|
| 242 |
+
kind_q = "what kind of art" in q_lower
|
| 243 |
+
activity_q = any(
|
| 244 |
+
p in q_lower for p in ("on hikes", "with her family on", "with his family on")
|
| 245 |
+
)
|
| 246 |
+
education_q = "fields" in q_lower or (
|
| 247 |
+
"educat" in q_lower and "fields" in q_lower
|
| 248 |
+
) or ("pursue" in q_lower and "educat" in q_lower)
|
| 249 |
+
cues = [str(c) for c in (context.get("image_cues") or []) if c]
|
| 250 |
+
symbols_gap = symbols_q and bool(symbols_missing_from_answer(content or "", cues))
|
| 251 |
+
image_gap = books_q and image_pack_incomplete(question, content or "", context)
|
| 252 |
+
people_count_short = bool(_PEOPLE_COUNT_QUESTION_RE.search(question)) and bool(
|
| 253 |
+
re.fullmatch(r"\d{1,2}", (content or "").strip())
|
| 254 |
+
)
|
| 255 |
+
needs_complete = (
|
| 256 |
+
bool(content)
|
| 257 |
+
and not is_hard_abstain(content)
|
| 258 |
+
and not people_count_short
|
| 259 |
+
and not any(
|
| 260 |
+
w in q_lower
|
| 261 |
+
for w in (
|
| 262 |
+
"when did",
|
| 263 |
+
"show",
|
| 264 |
+
"share",
|
| 265 |
+
"painting did",
|
| 266 |
+
"remind",
|
| 267 |
+
"how long",
|
| 268 |
+
"what types",
|
| 269 |
+
"pottery",
|
| 270 |
+
"events",
|
| 271 |
+
"journey through life",
|
| 272 |
+
"would",
|
| 273 |
+
"favorite book",
|
| 274 |
+
"recommend",
|
| 275 |
+
"kind of books",
|
| 276 |
+
"take away",
|
| 277 |
+
"bookshelf",
|
| 278 |
+
"activities does",
|
| 279 |
+
"partake",
|
| 280 |
+
"paint recently",
|
| 281 |
+
"how many times",
|
| 282 |
+
"what has melanie painted",
|
| 283 |
+
"what has caroline painted",
|
| 284 |
+
)
|
| 285 |
+
)
|
| 286 |
+
and (
|
| 287 |
+
undercount_draft
|
| 288 |
+
or people_undercount
|
| 289 |
+
or "personality" in q_lower
|
| 290 |
+
or "traits" in q_lower
|
| 291 |
+
or education_q
|
| 292 |
+
or symbols_q
|
| 293 |
+
or symbols_gap
|
| 294 |
+
or kind_q
|
| 295 |
+
or books_q
|
| 296 |
+
or image_gap
|
| 297 |
+
or activity_q
|
| 298 |
+
or "reaction" in q_lower
|
| 299 |
+
or "changes" in q_lower
|
| 300 |
+
or "handle the" in q_lower
|
| 301 |
+
or ("son" in q_lower and "accident" in q_lower)
|
| 302 |
+
or "give her" in q_lower
|
| 303 |
+
or "give him" in q_lower
|
| 304 |
+
)
|
| 305 |
+
)
|
| 306 |
+
if needs_complete:
|
| 307 |
+
nudge = _COMPLETE_NUDGE
|
| 308 |
+
if symbols_q or symbols_gap:
|
| 309 |
+
nudge = _SYMBOLS_NUDGE
|
| 310 |
+
elif books_q or (image_gap and "book" in q_lower):
|
| 311 |
+
nudge = _BOOKS_NUDGE
|
| 312 |
+
elif education_q:
|
| 313 |
+
nudge = _EDUCATION_NUDGE
|
| 314 |
+
elif kind_q:
|
| 315 |
+
nudge = _KIND_NUDGE
|
| 316 |
+
elif activity_q:
|
| 317 |
+
nudge = _ACTIVITY_NUDGE
|
| 318 |
+
elif "handle the" in q_lower or (
|
| 319 |
+
"son" in q_lower and "accident" in q_lower
|
| 320 |
+
):
|
| 321 |
+
nudge = _CHILD_AFFECT_NUDGE
|
| 322 |
+
complete_messages = messages + [
|
| 323 |
+
{"role": "assistant", "content": content},
|
| 324 |
+
{"role": "user", "content": nudge},
|
| 325 |
+
]
|
| 326 |
+
richer, response = _one_completion(
|
| 327 |
+
client,
|
| 328 |
+
model_name=model_name,
|
| 329 |
+
messages=complete_messages,
|
| 330 |
+
temperature=0,
|
| 331 |
+
)
|
| 332 |
+
last_response = response
|
| 333 |
+
samples_used += 1
|
| 334 |
+
prompt_tokens, completion_tokens, total_tokens = _accumulate_usage(
|
| 335 |
+
response, prompt_tokens, completion_tokens, total_tokens
|
| 336 |
+
)
|
| 337 |
+
richer = strip_leading_abstain(richer)
|
| 338 |
+
if richer and not is_hard_abstain(richer):
|
| 339 |
+
if symbols_q or symbols_gap:
|
| 340 |
+
if len(richer) <= max(100, len(content or "")) or (
|
| 341 |
+
"flag" in richer.lower() or "symbol" in richer.lower()
|
| 342 |
+
):
|
| 343 |
+
content = richer
|
| 344 |
+
elif kind_q:
|
| 345 |
+
if len(richer) <= len(content or "") + 20:
|
| 346 |
+
content = richer
|
| 347 |
+
elif undercount_draft or people_undercount:
|
| 348 |
+
if people_undercount:
|
| 349 |
+
if not _LOW_PEOPLE_COUNT_RE.search(richer) or len(richer) > len(
|
| 350 |
+
content or ""
|
| 351 |
+
):
|
| 352 |
+
content = richer
|
| 353 |
+
elif not _SINGLE_COUNT_RE.search(richer):
|
| 354 |
+
content = richer
|
| 355 |
+
elif education_q:
|
| 356 |
+
content = richer
|
| 357 |
+
elif books_q or activity_q:
|
| 358 |
+
if len(richer) >= len(content) or "," in richer:
|
| 359 |
+
content = richer
|
| 360 |
+
elif len(richer) >= len(content) or "," in richer:
|
| 361 |
+
content = richer
|
| 362 |
+
|
| 363 |
+
# Deterministic near-synonym / cue completeness (harness-only; not prompt gold).
|
| 364 |
+
content = complete_education_fields(question, content or "")
|
| 365 |
+
if symbols_q and cues:
|
| 366 |
+
content = compact_symbols_answer(content or "", cues)
|
| 367 |
+
content = strip_week_of_contradiction(question, content or "")
|
| 368 |
+
content = compact_recent_paint_answer(question, content or "")
|
| 369 |
+
|
| 370 |
+
latency_ms = (time.perf_counter() - started) * 1000
|
| 371 |
+
return AnswerResult(
|
| 372 |
+
answer=content,
|
| 373 |
+
model=getattr(last_response, "model", None) or model_name,
|
| 374 |
+
latency_ms=latency_ms,
|
| 375 |
+
prompt_tokens=prompt_tokens or None,
|
| 376 |
+
completion_tokens=completion_tokens or None,
|
| 377 |
+
total_tokens=total_tokens or None,
|
| 378 |
+
sc_samples_used=samples_used,
|
| 379 |
+
)
|
benchmarks/locomo/cli.py
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import json
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
from rich.console import Console
|
| 9 |
+
from rich.table import Table
|
| 10 |
+
|
| 11 |
+
from locomo.answer import answer_question
|
| 12 |
+
from locomo.client import BrainAPIClient
|
| 13 |
+
from locomo.config import Settings
|
| 14 |
+
from locomo.dataset import (
|
| 15 |
+
brain_id_for,
|
| 16 |
+
dataset_stats,
|
| 17 |
+
download_dataset,
|
| 18 |
+
get_sample,
|
| 19 |
+
load_dataset,
|
| 20 |
+
resolve_samples,
|
| 21 |
+
)
|
| 22 |
+
from locomo.evaluate import evaluate_samples, selftest_records
|
| 23 |
+
from locomo.ingest import ensure_run_dir, ingest_samples, load_jsonl, write_manifest
|
| 24 |
+
from locomo.metrics import (
|
| 25 |
+
compare_arms,
|
| 26 |
+
retrieval_arm_summary,
|
| 27 |
+
selftest_metrics,
|
| 28 |
+
tokenize,
|
| 29 |
+
)
|
| 30 |
+
from locomo.prompts import ANSWER_SYSTEM
|
| 31 |
+
from locomo.provenance import build_provenance
|
| 32 |
+
from locomo.report import print_comparison, print_report_table, write_report
|
| 33 |
+
|
| 34 |
+
console = Console()
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _parse_categories(raw: str | None) -> set[int] | None:
|
| 38 |
+
if not raw:
|
| 39 |
+
return None
|
| 40 |
+
return {int(part.strip()) for part in raw.split(",") if part.strip()}
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def cmd_download(args: argparse.Namespace, settings: Settings) -> int:
|
| 44 |
+
path = download_dataset(settings.dataset_path, settings.dataset_url)
|
| 45 |
+
console.print(f"[green]Dataset ready:[/green] {path}")
|
| 46 |
+
return 0
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def cmd_dataset_stats(args: argparse.Namespace, settings: Settings) -> int:
|
| 50 |
+
if not settings.dataset_path.exists():
|
| 51 |
+
download_dataset(settings.dataset_path, settings.dataset_url)
|
| 52 |
+
rows = dataset_stats(load_dataset(settings.dataset_path))
|
| 53 |
+
table = Table(title="LoCoMo dataset")
|
| 54 |
+
for col in ("sample_id", "brain_id", "speakers", "sessions", "turns", "qa"):
|
| 55 |
+
table.add_column(col)
|
| 56 |
+
for row in rows:
|
| 57 |
+
table.add_row(
|
| 58 |
+
str(row["sample_id"]),
|
| 59 |
+
str(row["brain_id"]),
|
| 60 |
+
f"{row['speaker_a']} & {row['speaker_b']}",
|
| 61 |
+
str(row["sessions"]),
|
| 62 |
+
str(row["turns"]),
|
| 63 |
+
str(row["qa"]),
|
| 64 |
+
)
|
| 65 |
+
console.print(table)
|
| 66 |
+
assert brain_id_for("conv-26") == "locomoconv26"
|
| 67 |
+
console.print("[dim]brain_id_for('conv-26') == 'locomoconv26' OK[/dim]")
|
| 68 |
+
return 0
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def cmd_smoke(args: argparse.Namespace, settings: Settings) -> int:
|
| 72 |
+
brain_id = args.brain
|
| 73 |
+
text = (
|
| 74 |
+
"Emily organized the AI Ethics Meetup in London on March 8, 2024. "
|
| 75 |
+
"Caroline attended an LGBTQ support group on 7 May 2023."
|
| 76 |
+
)
|
| 77 |
+
with BrainAPIClient(settings) as client:
|
| 78 |
+
submitted = client.ingest_text(text, brain_id)
|
| 79 |
+
task_id = (submitted.data or {}).get("task_id")
|
| 80 |
+
console.print(f"Queued ingest task_id={task_id}")
|
| 81 |
+
waited = client.wait_for_task(task_id, brain_id, timeout_s=args.timeout)
|
| 82 |
+
console.print(f"Task status: {(waited.data or {}).get('status')}")
|
| 83 |
+
ctx = client.retrieve_context(
|
| 84 |
+
"When did Caroline go to the LGBTQ support group?", brain_id
|
| 85 |
+
)
|
| 86 |
+
text_context = (ctx.data or {}).get("text_context") or ""
|
| 87 |
+
console.print(f"text_context ({len(text_context)} chars): {text_context[:400]}")
|
| 88 |
+
if not text_context.strip():
|
| 89 |
+
console.print("[yellow]Warning: empty text_context[/yellow]")
|
| 90 |
+
return 1
|
| 91 |
+
console.print("[green]Smoke test passed[/green]")
|
| 92 |
+
return 0
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def cmd_ingest(args: argparse.Namespace, settings: Settings) -> int:
|
| 96 |
+
sample_ids = args.sample or None
|
| 97 |
+
samples = resolve_samples(settings, sample_ids)
|
| 98 |
+
run_id, run_dir = ensure_run_dir(settings, args.run)
|
| 99 |
+
write_manifest(
|
| 100 |
+
run_dir,
|
| 101 |
+
{
|
| 102 |
+
"run_id": run_id,
|
| 103 |
+
"command": "ingest",
|
| 104 |
+
"samples": [s["sample_id"] for s in samples],
|
| 105 |
+
"granularity": args.granularity,
|
| 106 |
+
"concurrency": args.concurrency,
|
| 107 |
+
"limit_sessions": args.limit_sessions,
|
| 108 |
+
"dry_run": args.dry_run,
|
| 109 |
+
"brainapi_url": settings.brainapi_url,
|
| 110 |
+
"brain_override": args.brain,
|
| 111 |
+
**build_provenance(settings),
|
| 112 |
+
},
|
| 113 |
+
)
|
| 114 |
+
ingest_samples(
|
| 115 |
+
settings,
|
| 116 |
+
samples,
|
| 117 |
+
run_dir=run_dir,
|
| 118 |
+
granularity=args.granularity,
|
| 119 |
+
concurrency=args.concurrency,
|
| 120 |
+
limit_sessions=args.limit_sessions,
|
| 121 |
+
dry_run=args.dry_run,
|
| 122 |
+
resume=not args.no_resume,
|
| 123 |
+
task_timeout_s=args.timeout,
|
| 124 |
+
brain_override=args.brain,
|
| 125 |
+
)
|
| 126 |
+
console.print(f"[green]Run directory:[/green] {run_dir}")
|
| 127 |
+
return 0
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def cmd_answer_once(args: argparse.Namespace, settings: Settings) -> int:
|
| 131 |
+
sample = get_sample(load_dataset(settings.dataset_path), args.sample)
|
| 132 |
+
brain_id = brain_id_for(str(sample["sample_id"]))
|
| 133 |
+
question = args.question
|
| 134 |
+
if not question:
|
| 135 |
+
qa = (sample.get("qa") or [None])[0]
|
| 136 |
+
if not qa:
|
| 137 |
+
raise SystemExit("No question provided and sample has no QA")
|
| 138 |
+
question = qa["question"]
|
| 139 |
+
console.print(f"[dim]Using first QA: {question}[/dim]")
|
| 140 |
+
|
| 141 |
+
with BrainAPIClient(settings) as client:
|
| 142 |
+
retrieved = client.retrieve_context(question, brain_id)
|
| 143 |
+
result = answer_question(settings, question, retrieved.data or {})
|
| 144 |
+
console.print(result.answer)
|
| 145 |
+
console.print(
|
| 146 |
+
f"[dim]model={result.model} latency_ms={result.latency_ms:.0f} "
|
| 147 |
+
f"tokens={result.total_tokens}[/dim]"
|
| 148 |
+
)
|
| 149 |
+
return 0
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def cmd_selftest_metrics(args: argparse.Namespace, settings: Settings) -> int:
|
| 153 |
+
errors = selftest_metrics() + selftest_records()
|
| 154 |
+
if errors:
|
| 155 |
+
for err in errors:
|
| 156 |
+
console.print(f"[red]FAIL[/red] {err}")
|
| 157 |
+
return 1
|
| 158 |
+
console.print("[green]selftest-metrics passed[/green]")
|
| 159 |
+
return 0
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def cmd_evaluate(args: argparse.Namespace, settings: Settings) -> int:
|
| 163 |
+
sample_ids = args.sample or None
|
| 164 |
+
samples = resolve_samples(settings, sample_ids)
|
| 165 |
+
run_id, run_dir = ensure_run_dir(settings, args.run)
|
| 166 |
+
categories = _parse_categories(args.categories)
|
| 167 |
+
if settings.judge_shares_answer_family:
|
| 168 |
+
console.print(
|
| 169 |
+
f"[yellow]Warning: judge model ({settings.judge_model}) is the same "
|
| 170 |
+
f"family as the answer model ({settings.answer_model}); judge accuracy "
|
| 171 |
+
"carries self-preference bias. Set BENCH_JUDGE_MODEL (with "
|
| 172 |
+
"BENCH_JUDGE_BASE_URL / BENCH_JUDGE_API_KEY) to a different "
|
| 173 |
+
"family.[/yellow]"
|
| 174 |
+
)
|
| 175 |
+
write_manifest(
|
| 176 |
+
run_dir,
|
| 177 |
+
{
|
| 178 |
+
"run_id": run_id,
|
| 179 |
+
"command": "evaluate",
|
| 180 |
+
"samples": [s["sample_id"] for s in samples],
|
| 181 |
+
"concurrency": args.concurrency,
|
| 182 |
+
"skip_adversarial": not args.include_adversarial,
|
| 183 |
+
"categories": sorted(categories) if categories else None,
|
| 184 |
+
"limit": args.limit,
|
| 185 |
+
"brainapi_url": settings.brainapi_url,
|
| 186 |
+
"brain_override": args.brain,
|
| 187 |
+
**build_provenance(settings),
|
| 188 |
+
"historical_limit": args.historical_limit,
|
| 189 |
+
"max_passages": args.max_passages,
|
| 190 |
+
"max_facts": args.max_facts,
|
| 191 |
+
"apply_fact_filter": not args.no_fact_filter,
|
| 192 |
+
"use_ppr": args.use_ppr,
|
| 193 |
+
"sufficiency_retry": args.sufficiency_retry,
|
| 194 |
+
},
|
| 195 |
+
)
|
| 196 |
+
evaluate_samples(
|
| 197 |
+
settings,
|
| 198 |
+
samples,
|
| 199 |
+
run_dir=run_dir,
|
| 200 |
+
concurrency=args.concurrency,
|
| 201 |
+
categories=categories,
|
| 202 |
+
skip_adversarial=not args.include_adversarial,
|
| 203 |
+
limit=args.limit,
|
| 204 |
+
resume=not args.no_resume,
|
| 205 |
+
historical_limit=args.historical_limit,
|
| 206 |
+
max_passages=args.max_passages,
|
| 207 |
+
max_facts=args.max_facts,
|
| 208 |
+
apply_fact_filter=not args.no_fact_filter,
|
| 209 |
+
use_ppr=args.use_ppr,
|
| 210 |
+
sufficiency_retry=args.sufficiency_retry,
|
| 211 |
+
brain_override=args.brain,
|
| 212 |
+
)
|
| 213 |
+
report = write_report(run_dir)
|
| 214 |
+
print_report_table(report)
|
| 215 |
+
console.print(f"[green]Report:[/green] {run_dir / 'report.md'}")
|
| 216 |
+
return 0
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
def cmd_report(args: argparse.Namespace, settings: Settings) -> int:
|
| 220 |
+
run_dir = settings.runs_dir / args.run
|
| 221 |
+
if not run_dir.exists():
|
| 222 |
+
raise SystemExit(f"Run directory not found: {run_dir}")
|
| 223 |
+
report = write_report(run_dir)
|
| 224 |
+
print_report_table(report)
|
| 225 |
+
console.print(report["run_dir"] + "/report.md")
|
| 226 |
+
if args.json:
|
| 227 |
+
console.print_json(json.dumps(report))
|
| 228 |
+
return 0
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
def _load_run_answers(settings: Settings, run_id: str) -> list[dict[str, Any]]:
|
| 232 |
+
run_dir = settings.runs_dir / run_id
|
| 233 |
+
answers_path = run_dir / "answers.jsonl"
|
| 234 |
+
if not answers_path.exists():
|
| 235 |
+
raise SystemExit(f"No answers.jsonl in run: {run_dir}")
|
| 236 |
+
rows = load_jsonl(answers_path)
|
| 237 |
+
latest: dict[tuple[str, int], dict[str, Any]] = {}
|
| 238 |
+
for row in rows:
|
| 239 |
+
if row.get("error"):
|
| 240 |
+
continue
|
| 241 |
+
latest[(str(row.get("sample_id")), int(row.get("qa_index") or 0))] = row
|
| 242 |
+
return list(latest.values())
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
def cmd_compare(args: argparse.Namespace, settings: Settings) -> int:
|
| 246 |
+
baseline = [_load_run_answers(settings, run) for run in args.baseline]
|
| 247 |
+
candidate = [_load_run_answers(settings, run) for run in args.candidate]
|
| 248 |
+
comparison = compare_arms(
|
| 249 |
+
baseline, candidate, skip_adversarial=not args.include_adversarial
|
| 250 |
+
)
|
| 251 |
+
retrieval = {
|
| 252 |
+
"baseline": {"labels": args.baseline, **retrieval_arm_summary(baseline)},
|
| 253 |
+
"candidate": {"labels": args.candidate, **retrieval_arm_summary(candidate)},
|
| 254 |
+
}
|
| 255 |
+
print_comparison(
|
| 256 |
+
comparison,
|
| 257 |
+
" + ".join(args.baseline),
|
| 258 |
+
" + ".join(args.candidate),
|
| 259 |
+
retrieval,
|
| 260 |
+
)
|
| 261 |
+
if args.json:
|
| 262 |
+
console.print_json(
|
| 263 |
+
json.dumps({"comparison": comparison, "retrieval": retrieval})
|
| 264 |
+
)
|
| 265 |
+
return 0
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def cmd_prompt_audit(args: argparse.Namespace, settings: Settings) -> int:
|
| 269 |
+
dataset = load_dataset(settings.dataset_path)
|
| 270 |
+
prompt_tokens = tokenize(ANSWER_SYSTEM)
|
| 271 |
+
n = args.ngram
|
| 272 |
+
prompt_ngrams = {
|
| 273 |
+
tuple(prompt_tokens[i : i + n])
|
| 274 |
+
for i in range(0, max(0, len(prompt_tokens) - n + 1))
|
| 275 |
+
}
|
| 276 |
+
hits: list[tuple[str, int, str]] = []
|
| 277 |
+
for sample in dataset:
|
| 278 |
+
sample_id = str(sample.get("sample_id"))
|
| 279 |
+
for idx, qa in enumerate(sample.get("qa") or []):
|
| 280 |
+
gold = qa.get("answer")
|
| 281 |
+
if gold is None:
|
| 282 |
+
gold = qa.get("adversarial_answer")
|
| 283 |
+
gold_tokens = tokenize(str(gold or ""))
|
| 284 |
+
for i in range(0, max(0, len(gold_tokens) - n + 1)):
|
| 285 |
+
gram = tuple(gold_tokens[i : i + n])
|
| 286 |
+
if gram in prompt_ngrams:
|
| 287 |
+
hits.append((sample_id, idx, " ".join(gram)))
|
| 288 |
+
break
|
| 289 |
+
if hits:
|
| 290 |
+
console.print(
|
| 291 |
+
f"[red]FAIL[/red] answer prompt shares a {n}-gram with "
|
| 292 |
+
f"{len(hits)} gold answers"
|
| 293 |
+
)
|
| 294 |
+
for sample_id, idx, gram in hits[:20]:
|
| 295 |
+
console.print(f" {sample_id}::{idx} {gram!r}")
|
| 296 |
+
return 1
|
| 297 |
+
console.print(
|
| 298 |
+
f"[green]prompt-audit passed[/green] no {n}-gram of the answer prompt "
|
| 299 |
+
f"appears in any gold answer ({len(dataset)} conversations)"
|
| 300 |
+
)
|
| 301 |
+
return 0
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
def build_parser() -> argparse.ArgumentParser:
|
| 305 |
+
parser = argparse.ArgumentParser(
|
| 306 |
+
prog="python -m locomo",
|
| 307 |
+
description="LoCoMo benchmark harness for BrainAPI",
|
| 308 |
+
)
|
| 309 |
+
parser.add_argument(
|
| 310 |
+
"--env-file",
|
| 311 |
+
type=Path,
|
| 312 |
+
default=None,
|
| 313 |
+
help="Optional path to .env (defaults to benchmarks/.env)",
|
| 314 |
+
)
|
| 315 |
+
sub = parser.add_subparsers(dest="command", required=True)
|
| 316 |
+
|
| 317 |
+
p_download = sub.add_parser("download", help="Download locomo10.json")
|
| 318 |
+
p_download.set_defaults(func=cmd_download)
|
| 319 |
+
|
| 320 |
+
p_stats = sub.add_parser("dataset-stats", help="Print dataset statistics")
|
| 321 |
+
p_stats.set_defaults(func=cmd_dataset_stats)
|
| 322 |
+
|
| 323 |
+
p_smoke = sub.add_parser("smoke", help="Smoke-test ingest + retrieve")
|
| 324 |
+
p_smoke.add_argument("--brain", default="locomosmoke")
|
| 325 |
+
p_smoke.add_argument("--timeout", type=float, default=600.0)
|
| 326 |
+
p_smoke.set_defaults(func=cmd_smoke)
|
| 327 |
+
|
| 328 |
+
p_ingest = sub.add_parser("ingest", help="Ingest LoCoMo conversations")
|
| 329 |
+
p_ingest.add_argument(
|
| 330 |
+
"--sample",
|
| 331 |
+
action="append",
|
| 332 |
+
help="Sample id (repeatable). Default: all samples.",
|
| 333 |
+
)
|
| 334 |
+
p_ingest.add_argument("--run", default=None, help="Existing or new run id")
|
| 335 |
+
p_ingest.add_argument(
|
| 336 |
+
"--brain",
|
| 337 |
+
default=None,
|
| 338 |
+
help="Override the derived brain id (single --sample only)",
|
| 339 |
+
)
|
| 340 |
+
p_ingest.add_argument(
|
| 341 |
+
"--granularity", choices=("session", "turn"), default="session"
|
| 342 |
+
)
|
| 343 |
+
p_ingest.add_argument("--concurrency", type=int, default=2)
|
| 344 |
+
p_ingest.add_argument("--limit-sessions", type=int, default=None)
|
| 345 |
+
p_ingest.add_argument("--dry-run", action="store_true")
|
| 346 |
+
p_ingest.add_argument("--no-resume", action="store_true")
|
| 347 |
+
p_ingest.add_argument("--timeout", type=float, default=900.0)
|
| 348 |
+
p_ingest.set_defaults(func=cmd_ingest)
|
| 349 |
+
|
| 350 |
+
p_answer = sub.add_parser("answer-once", help="Retrieve + answer one question")
|
| 351 |
+
p_answer.add_argument("--sample", default="conv-26")
|
| 352 |
+
p_answer.add_argument("--question", default=None)
|
| 353 |
+
p_answer.set_defaults(func=cmd_answer_once)
|
| 354 |
+
|
| 355 |
+
p_self = sub.add_parser("selftest-metrics", help="Run local metrics self-test")
|
| 356 |
+
p_self.set_defaults(func=cmd_selftest_metrics)
|
| 357 |
+
|
| 358 |
+
p_eval = sub.add_parser("evaluate", help="Answer and score LoCoMo QA")
|
| 359 |
+
p_eval.add_argument("--sample", action="append")
|
| 360 |
+
p_eval.add_argument("--run", default=None)
|
| 361 |
+
p_eval.add_argument(
|
| 362 |
+
"--brain",
|
| 363 |
+
default=None,
|
| 364 |
+
help="Override the derived brain id (single --sample only)",
|
| 365 |
+
)
|
| 366 |
+
p_eval.add_argument("--concurrency", type=int, default=2)
|
| 367 |
+
p_eval.add_argument("--categories", default=None, help="e.g. 1,2,4")
|
| 368 |
+
p_eval.add_argument("--limit", type=int, default=None)
|
| 369 |
+
p_eval.add_argument(
|
| 370 |
+
"--include-adversarial",
|
| 371 |
+
action="store_true",
|
| 372 |
+
help="Include category 5 questions",
|
| 373 |
+
)
|
| 374 |
+
p_eval.add_argument("--no-resume", action="store_true")
|
| 375 |
+
p_eval.add_argument("--historical-limit", type=int, default=10)
|
| 376 |
+
p_eval.add_argument("--max-passages", type=int, default=8)
|
| 377 |
+
p_eval.add_argument("--max-facts", type=int, default=40)
|
| 378 |
+
p_eval.add_argument("--no-fact-filter", action="store_true")
|
| 379 |
+
p_eval.add_argument("--use-ppr", action="store_true")
|
| 380 |
+
p_eval.add_argument("--sufficiency-retry", action="store_true")
|
| 381 |
+
p_eval.set_defaults(func=cmd_evaluate)
|
| 382 |
+
|
| 383 |
+
p_report = sub.add_parser("report", help="Rebuild report from a run directory")
|
| 384 |
+
p_report.add_argument("--run", required=True)
|
| 385 |
+
p_report.add_argument("--json", action="store_true")
|
| 386 |
+
p_report.set_defaults(func=cmd_report)
|
| 387 |
+
|
| 388 |
+
p_compare = sub.add_parser(
|
| 389 |
+
"compare", help="Paired McNemar comparison between two run arms"
|
| 390 |
+
)
|
| 391 |
+
p_compare.add_argument("--baseline", action="append", required=True)
|
| 392 |
+
p_compare.add_argument("--candidate", action="append", required=True)
|
| 393 |
+
p_compare.add_argument("--include-adversarial", action="store_true")
|
| 394 |
+
p_compare.add_argument("--json", action="store_true")
|
| 395 |
+
p_compare.set_defaults(func=cmd_compare)
|
| 396 |
+
|
| 397 |
+
p_audit = sub.add_parser(
|
| 398 |
+
"prompt-audit",
|
| 399 |
+
help="Fail if the answer prompt shares an n-gram with any gold answer",
|
| 400 |
+
)
|
| 401 |
+
p_audit.add_argument("--ngram", type=int, default=3)
|
| 402 |
+
p_audit.set_defaults(func=cmd_prompt_audit)
|
| 403 |
+
|
| 404 |
+
return parser
|
| 405 |
+
|
| 406 |
+
|
| 407 |
+
def main(argv: list[str] | None = None) -> int:
|
| 408 |
+
parser = build_parser()
|
| 409 |
+
args = parser.parse_args(argv)
|
| 410 |
+
settings = Settings.load(args.env_file)
|
| 411 |
+
return args.func(args, settings)
|
benchmarks/locomo/client.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import time
|
| 4 |
+
from dataclasses import dataclass, field
|
| 5 |
+
from typing import Any
|
| 6 |
+
|
| 7 |
+
import httpx
|
| 8 |
+
|
| 9 |
+
from locomo.config import TERMINAL_TASK_STATUSES, Settings
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
@dataclass
|
| 13 |
+
class TimedResult:
|
| 14 |
+
data: Any
|
| 15 |
+
latency_ms: float
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@dataclass
|
| 19 |
+
class BrainAPIClient:
|
| 20 |
+
settings: Settings
|
| 21 |
+
timeout: float = 120.0
|
| 22 |
+
max_retries: int = 5
|
| 23 |
+
_client: httpx.Client = field(init=False, repr=False)
|
| 24 |
+
|
| 25 |
+
def __post_init__(self) -> None:
|
| 26 |
+
self.settings.require_brainapi()
|
| 27 |
+
self._client = httpx.Client(
|
| 28 |
+
base_url=self.settings.brainapi_url,
|
| 29 |
+
headers={
|
| 30 |
+
"Authorization": f"Bearer {self.settings.brainpat_token}",
|
| 31 |
+
"Content-Type": "application/json",
|
| 32 |
+
},
|
| 33 |
+
timeout=self.timeout,
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
def close(self) -> None:
|
| 37 |
+
self._client.close()
|
| 38 |
+
|
| 39 |
+
def __enter__(self) -> "BrainAPIClient":
|
| 40 |
+
return self
|
| 41 |
+
|
| 42 |
+
def __exit__(self, *args: object) -> None:
|
| 43 |
+
self.close()
|
| 44 |
+
|
| 45 |
+
def _request(
|
| 46 |
+
self,
|
| 47 |
+
method: str,
|
| 48 |
+
path: str,
|
| 49 |
+
*,
|
| 50 |
+
json: dict | None = None,
|
| 51 |
+
params: dict | None = None,
|
| 52 |
+
) -> TimedResult:
|
| 53 |
+
last_error: Exception | None = None
|
| 54 |
+
for attempt in range(self.max_retries):
|
| 55 |
+
started = time.perf_counter()
|
| 56 |
+
try:
|
| 57 |
+
response = self._client.request(
|
| 58 |
+
method, path, json=json, params=params
|
| 59 |
+
)
|
| 60 |
+
latency_ms = (time.perf_counter() - started) * 1000
|
| 61 |
+
if response.status_code in (429, 500, 502, 503, 504):
|
| 62 |
+
wait = min(2**attempt * 0.5, 30)
|
| 63 |
+
time.sleep(wait)
|
| 64 |
+
last_error = httpx.HTTPStatusError(
|
| 65 |
+
f"HTTP {response.status_code}",
|
| 66 |
+
request=response.request,
|
| 67 |
+
response=response,
|
| 68 |
+
)
|
| 69 |
+
continue
|
| 70 |
+
response.raise_for_status()
|
| 71 |
+
if response.status_code == 204 or not response.content:
|
| 72 |
+
return TimedResult(data=None, latency_ms=latency_ms)
|
| 73 |
+
return TimedResult(data=response.json(), latency_ms=latency_ms)
|
| 74 |
+
except (httpx.TransportError, httpx.TimeoutException) as exc:
|
| 75 |
+
last_error = exc
|
| 76 |
+
wait = min(2**attempt * 0.5, 30)
|
| 77 |
+
time.sleep(wait)
|
| 78 |
+
raise RuntimeError(f"Request failed after retries: {method} {path}: {last_error}")
|
| 79 |
+
|
| 80 |
+
def ingest_text(
|
| 81 |
+
self,
|
| 82 |
+
text: str,
|
| 83 |
+
brain_id: str,
|
| 84 |
+
*,
|
| 85 |
+
observate_for: list[str] | None = None,
|
| 86 |
+
preferred_extraction_entities: list[str] | None = None,
|
| 87 |
+
source_timestamp: str | None = None,
|
| 88 |
+
) -> TimedResult:
|
| 89 |
+
body = {
|
| 90 |
+
"data": {"data_type": "text", "text_data": text},
|
| 91 |
+
"brain_id": brain_id,
|
| 92 |
+
"observate_for": observate_for
|
| 93 |
+
or [
|
| 94 |
+
"facts about each speaker",
|
| 95 |
+
"events and when they happened",
|
| 96 |
+
"relationships between people",
|
| 97 |
+
],
|
| 98 |
+
"preferred_extraction_entities": preferred_extraction_entities
|
| 99 |
+
or ["Person", "Event", "Location", "Date"],
|
| 100 |
+
}
|
| 101 |
+
if source_timestamp:
|
| 102 |
+
body["source_timestamp"] = source_timestamp
|
| 103 |
+
return self._request("POST", "/ingest/", json=body)
|
| 104 |
+
|
| 105 |
+
def get_task(self, task_id: str, brain_id: str) -> TimedResult:
|
| 106 |
+
return self._request(
|
| 107 |
+
"GET", f"/tasks/{task_id}", params={"brain_id": brain_id}
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
def wait_for_task(
|
| 111 |
+
self,
|
| 112 |
+
task_id: str,
|
| 113 |
+
brain_id: str,
|
| 114 |
+
*,
|
| 115 |
+
timeout_s: float = 600.0,
|
| 116 |
+
poll_interval_s: float = 2.0,
|
| 117 |
+
) -> TimedResult:
|
| 118 |
+
started = time.perf_counter()
|
| 119 |
+
while True:
|
| 120 |
+
result = self.get_task(task_id, brain_id)
|
| 121 |
+
status = (result.data or {}).get("status", "unknown")
|
| 122 |
+
if status in TERMINAL_TASK_STATUSES:
|
| 123 |
+
return TimedResult(
|
| 124 |
+
data=result.data,
|
| 125 |
+
latency_ms=(time.perf_counter() - started) * 1000,
|
| 126 |
+
)
|
| 127 |
+
if time.perf_counter() - started > timeout_s:
|
| 128 |
+
raise TimeoutError(
|
| 129 |
+
f"Task {task_id} did not finish within {timeout_s}s "
|
| 130 |
+
f"(last status={status})"
|
| 131 |
+
)
|
| 132 |
+
time.sleep(poll_interval_s)
|
| 133 |
+
|
| 134 |
+
def retrieve_context(
|
| 135 |
+
self,
|
| 136 |
+
text: str,
|
| 137 |
+
brain_id: str,
|
| 138 |
+
*,
|
| 139 |
+
historical_limit: int = 10,
|
| 140 |
+
max_passages: int = 8,
|
| 141 |
+
max_facts: int = 40,
|
| 142 |
+
apply_fact_filter: bool = True,
|
| 143 |
+
use_ppr: bool = False,
|
| 144 |
+
sufficiency_retry: bool = False,
|
| 145 |
+
cross_event_bridges: int = 3,
|
| 146 |
+
) -> TimedResult:
|
| 147 |
+
body: dict[str, Any] = {
|
| 148 |
+
"text": text,
|
| 149 |
+
"brain_id": brain_id,
|
| 150 |
+
"historical_limit": historical_limit,
|
| 151 |
+
"max_passages": max_passages,
|
| 152 |
+
"max_facts": max_facts,
|
| 153 |
+
"apply_fact_filter": apply_fact_filter,
|
| 154 |
+
"use_ppr": use_ppr,
|
| 155 |
+
"sufficiency_retry": sufficiency_retry,
|
| 156 |
+
"cross_event_bridges": cross_event_bridges,
|
| 157 |
+
}
|
| 158 |
+
return self._request("POST", "/retrieve/context", json=body)
|
| 159 |
+
|
| 160 |
+
def entity_status(self, target: str, brain_id: str) -> TimedResult:
|
| 161 |
+
return self._request(
|
| 162 |
+
"GET",
|
| 163 |
+
"/retrieve/entity/status",
|
| 164 |
+
params={"target": target, "brain_id": brain_id},
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
def list_entities(self, brain_id: str, *, limit: int = 5) -> TimedResult:
|
| 168 |
+
return self._request(
|
| 169 |
+
"GET",
|
| 170 |
+
"/retrieve/entities",
|
| 171 |
+
params={"brain_id": brain_id, "limit": limit},
|
| 172 |
+
)
|
benchmarks/locomo/config.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
from dataclasses import dataclass
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
from dotenv import load_dotenv
|
| 8 |
+
|
| 9 |
+
BENCHMARKS_ROOT = Path(__file__).resolve().parent.parent
|
| 10 |
+
DATA_DIR = BENCHMARKS_ROOT / "data"
|
| 11 |
+
RUNS_DIR = BENCHMARKS_ROOT / "runs"
|
| 12 |
+
DEFAULT_DATASET_URL = (
|
| 13 |
+
"https://raw.githubusercontent.com/snap-research/locomo/main/data/locomo10.json"
|
| 14 |
+
)
|
| 15 |
+
DEFAULT_DATASET_PATH = DATA_DIR / "locomo10.json"
|
| 16 |
+
DEFAULT_LLM_BASE_URL = "https://api.deepseek.com"
|
| 17 |
+
DEFAULT_ANSWER_MODEL = "deepseek-v4-flash"
|
| 18 |
+
DEFAULT_SOTA_ANSWER_MODEL = "deepseek-v4-flash"
|
| 19 |
+
DEFAULT_AZURE_JUDGE_MODEL = "gpt-4o"
|
| 20 |
+
DEFAULT_AZURE_API_VERSION = "2024-12-01-preview"
|
| 21 |
+
DEFAULT_BENCH_PROFILE = "product"
|
| 22 |
+
|
| 23 |
+
_MODEL_FAMILY_PREFIXES = {
|
| 24 |
+
"deepseek": "deepseek",
|
| 25 |
+
"gpt": "openai",
|
| 26 |
+
"o1": "openai",
|
| 27 |
+
"o3": "openai",
|
| 28 |
+
"o4": "openai",
|
| 29 |
+
"chatgpt": "openai",
|
| 30 |
+
"claude": "anthropic",
|
| 31 |
+
"gemini": "google",
|
| 32 |
+
"grok": "xai",
|
| 33 |
+
"qwen": "qwen",
|
| 34 |
+
"llama": "meta",
|
| 35 |
+
"mistral": "mistral",
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def model_family(model: str) -> str:
|
| 40 |
+
name = (model or "").strip().lower().rsplit("/", 1)[-1]
|
| 41 |
+
for prefix, family in _MODEL_FAMILY_PREFIXES.items():
|
| 42 |
+
if name.startswith(prefix):
|
| 43 |
+
return family
|
| 44 |
+
return name.split("-", 1)[0] or "unknown"
|
| 45 |
+
|
| 46 |
+
TERMINAL_TASK_STATUSES = frozenset({"completed", "failed", "partial_failed"})
|
| 47 |
+
CATEGORY_NAMES = {
|
| 48 |
+
1: "multi-hop",
|
| 49 |
+
2: "temporal",
|
| 50 |
+
3: "open-domain",
|
| 51 |
+
4: "single-hop",
|
| 52 |
+
5: "adversarial",
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
@dataclass
|
| 57 |
+
class Settings:
|
| 58 |
+
brainapi_url: str
|
| 59 |
+
brainpat_token: str
|
| 60 |
+
openai_api_key: str
|
| 61 |
+
llm_base_url: str | None
|
| 62 |
+
answer_model: str
|
| 63 |
+
judge_model: str
|
| 64 |
+
judge_api_key: str
|
| 65 |
+
judge_base_url: str | None
|
| 66 |
+
judge_azure_endpoint: str | None
|
| 67 |
+
judge_azure_api_version: str
|
| 68 |
+
dataset_path: Path
|
| 69 |
+
dataset_url: str
|
| 70 |
+
runs_dir: Path
|
| 71 |
+
bench_profile: str
|
| 72 |
+
sc_samples: int
|
| 73 |
+
sc_temperature: float
|
| 74 |
+
gap_fill: bool
|
| 75 |
+
answer_azure_endpoint: str | None = None
|
| 76 |
+
answer_azure_api_version: str = DEFAULT_AZURE_API_VERSION
|
| 77 |
+
|
| 78 |
+
@classmethod
|
| 79 |
+
def load(cls, env_file: Path | None = None) -> "Settings":
|
| 80 |
+
if env_file is None:
|
| 81 |
+
env_file = BENCHMARKS_ROOT / ".env"
|
| 82 |
+
if env_file.exists():
|
| 83 |
+
load_dotenv(env_file, override=False)
|
| 84 |
+
else:
|
| 85 |
+
load_dotenv(override=False)
|
| 86 |
+
|
| 87 |
+
from locomo.sota import profile_defaults
|
| 88 |
+
|
| 89 |
+
profile = (
|
| 90 |
+
os.getenv("BENCH_PROFILE", DEFAULT_BENCH_PROFILE) or DEFAULT_BENCH_PROFILE
|
| 91 |
+
).strip().lower()
|
| 92 |
+
if profile not in {"product", "sota"}:
|
| 93 |
+
profile = DEFAULT_BENCH_PROFILE
|
| 94 |
+
defaults = profile_defaults(profile)
|
| 95 |
+
|
| 96 |
+
api_key = (
|
| 97 |
+
os.getenv("DEEPSEEK_API_KEY")
|
| 98 |
+
or os.getenv("OPENAI_API_KEY")
|
| 99 |
+
or ""
|
| 100 |
+
)
|
| 101 |
+
llm_base_url = os.getenv("BENCH_LLM_BASE_URL", DEFAULT_LLM_BASE_URL) or None
|
| 102 |
+
answer_model = os.getenv("BENCH_ANSWER_MODEL", DEFAULT_ANSWER_MODEL)
|
| 103 |
+
|
| 104 |
+
azure_endpoint = (
|
| 105 |
+
os.getenv("BENCH_JUDGE_AZURE_ENDPOINT")
|
| 106 |
+
or os.getenv("AZURE_LARGE_LLM_ENDPOINT")
|
| 107 |
+
or ""
|
| 108 |
+
).rstrip("/")
|
| 109 |
+
azure_key = (
|
| 110 |
+
os.getenv("BENCH_JUDGE_AZURE_KEY")
|
| 111 |
+
or os.getenv("AZURE_LARGE_LLM_SUBSCRIPTION_KEY")
|
| 112 |
+
or ""
|
| 113 |
+
)
|
| 114 |
+
azure_available = bool(azure_endpoint and azure_key)
|
| 115 |
+
|
| 116 |
+
answer_azure_endpoint: str | None = None
|
| 117 |
+
if profile == "sota":
|
| 118 |
+
# Maintainer: deepseek-v4-flash is the preferred SOTA answerer (stronger
|
| 119 |
+
# than gpt-4o on this stack). Only override via BENCH_SOTA_* envs.
|
| 120 |
+
sota_answer = os.getenv("BENCH_SOTA_ANSWER_MODEL", "").strip()
|
| 121 |
+
if sota_answer:
|
| 122 |
+
answer_model = sota_answer
|
| 123 |
+
sota_base = os.getenv("BENCH_SOTA_LLM_BASE_URL", "").strip()
|
| 124 |
+
if sota_base:
|
| 125 |
+
llm_base_url = sota_base
|
| 126 |
+
else:
|
| 127 |
+
answer_model = os.getenv(
|
| 128 |
+
"BENCH_ANSWER_MODEL", DEFAULT_SOTA_ANSWER_MODEL
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
judge_model = os.getenv("BENCH_JUDGE_MODEL", "")
|
| 132 |
+
judge_base_url = os.getenv("BENCH_JUDGE_BASE_URL", "")
|
| 133 |
+
judge_api_key = os.getenv("BENCH_JUDGE_API_KEY", "")
|
| 134 |
+
use_azure_judge = (
|
| 135 |
+
azure_available and not judge_model and not judge_base_url
|
| 136 |
+
)
|
| 137 |
+
if use_azure_judge:
|
| 138 |
+
judge_model = os.getenv(
|
| 139 |
+
"AZURE_LARGE_LLM_MODEL", DEFAULT_AZURE_JUDGE_MODEL
|
| 140 |
+
)
|
| 141 |
+
if profile == "sota":
|
| 142 |
+
sota_judge = os.getenv("BENCH_SOTA_JUDGE_MODEL", "").strip()
|
| 143 |
+
if sota_judge:
|
| 144 |
+
judge_model = sota_judge
|
| 145 |
+
use_azure_judge = False
|
| 146 |
+
elif not os.getenv("BENCH_JUDGE_MODEL"):
|
| 147 |
+
judge_model = answer_model
|
| 148 |
+
use_azure_judge = False
|
| 149 |
+
judge_base_url = llm_base_url or ""
|
| 150 |
+
judge_api_key = api_key
|
| 151 |
+
|
| 152 |
+
return cls(
|
| 153 |
+
brainapi_url=os.getenv("BRAINAPI_URL", "http://localhost:8000").rstrip("/"),
|
| 154 |
+
brainpat_token=os.getenv("BRAINPAT_TOKEN", ""),
|
| 155 |
+
openai_api_key=api_key,
|
| 156 |
+
llm_base_url=llm_base_url,
|
| 157 |
+
answer_model=answer_model,
|
| 158 |
+
judge_model=judge_model or answer_model,
|
| 159 |
+
judge_api_key=(
|
| 160 |
+
azure_key if use_azure_judge else (judge_api_key or api_key)
|
| 161 |
+
),
|
| 162 |
+
judge_base_url=(
|
| 163 |
+
None if use_azure_judge else (judge_base_url or llm_base_url)
|
| 164 |
+
),
|
| 165 |
+
judge_azure_endpoint=azure_endpoint if use_azure_judge else None,
|
| 166 |
+
judge_azure_api_version=os.getenv(
|
| 167 |
+
"BENCH_JUDGE_AZURE_API_VERSION",
|
| 168 |
+
os.getenv("AZURE_LARGE_LLM_API_VERSION", DEFAULT_AZURE_API_VERSION),
|
| 169 |
+
),
|
| 170 |
+
dataset_path=Path(
|
| 171 |
+
os.getenv("BENCH_DATASET_PATH", str(DEFAULT_DATASET_PATH))
|
| 172 |
+
),
|
| 173 |
+
dataset_url=os.getenv("BENCH_DATASET_URL", DEFAULT_DATASET_URL),
|
| 174 |
+
runs_dir=Path(os.getenv("BENCH_RUNS_DIR", str(RUNS_DIR))),
|
| 175 |
+
bench_profile=str(defaults["bench_profile"]),
|
| 176 |
+
sc_samples=max(1, int(defaults["sc_samples"])),
|
| 177 |
+
sc_temperature=float(defaults["sc_temperature"]),
|
| 178 |
+
gap_fill=bool(defaults["gap_fill"]),
|
| 179 |
+
answer_azure_endpoint=answer_azure_endpoint,
|
| 180 |
+
answer_azure_api_version=os.getenv(
|
| 181 |
+
"BENCH_JUDGE_AZURE_API_VERSION",
|
| 182 |
+
os.getenv("AZURE_LARGE_LLM_API_VERSION", DEFAULT_AZURE_API_VERSION),
|
| 183 |
+
),
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
@property
|
| 187 |
+
def judge_shares_answer_family(self) -> bool:
|
| 188 |
+
return model_family(self.judge_model) == model_family(self.answer_model)
|
| 189 |
+
|
| 190 |
+
def judge_provider(self) -> str:
|
| 191 |
+
if self.judge_azure_endpoint:
|
| 192 |
+
return "azure"
|
| 193 |
+
return self.judge_base_url or "openai-default"
|
| 194 |
+
|
| 195 |
+
def require_brainapi(self) -> None:
|
| 196 |
+
if not self.brainpat_token:
|
| 197 |
+
raise SystemExit(
|
| 198 |
+
"BRAINPAT_TOKEN is required. Copy .env.example to .env and set it."
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
def require_llm(self) -> None:
|
| 202 |
+
if not self.openai_api_key:
|
| 203 |
+
raise SystemExit(
|
| 204 |
+
"DEEPSEEK_API_KEY (or OPENAI_API_KEY) is required for answer/judge "
|
| 205 |
+
"LLM calls. Copy .env.example to .env and set it."
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
def require_judge_llm(self) -> None:
|
| 209 |
+
if not self.judge_api_key:
|
| 210 |
+
raise SystemExit(
|
| 211 |
+
"No judge API key. Set BENCH_JUDGE_API_KEY (with BENCH_JUDGE_BASE_URL "
|
| 212 |
+
"and BENCH_JUDGE_MODEL), or DEEPSEEK_API_KEY / OPENAI_API_KEY to reuse "
|
| 213 |
+
"the answerer's provider."
|
| 214 |
+
)
|
benchmarks/locomo/dataset.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import re
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Any
|
| 7 |
+
from urllib.request import urlretrieve
|
| 8 |
+
|
| 9 |
+
from locomo.config import DEFAULT_DATASET_PATH, DEFAULT_DATASET_URL, Settings
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def brain_id_for(sample_id: str) -> str:
|
| 13 |
+
slug = re.sub(r"[^a-zA-Z0-9]", "", f"locomo{sample_id}")
|
| 14 |
+
if not slug.isalnum():
|
| 15 |
+
raise ValueError(f"Could not derive alphanumeric brain_id from {sample_id!r}")
|
| 16 |
+
return slug.lower()
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def download_dataset(
|
| 20 |
+
dest: Path | None = None,
|
| 21 |
+
url: str = DEFAULT_DATASET_URL,
|
| 22 |
+
) -> Path:
|
| 23 |
+
dest = dest or DEFAULT_DATASET_PATH
|
| 24 |
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
| 25 |
+
if dest.exists() and dest.stat().st_size > 0:
|
| 26 |
+
return dest
|
| 27 |
+
urlretrieve(url, dest)
|
| 28 |
+
return dest
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def load_dataset(path: Path | None = None) -> list[dict[str, Any]]:
|
| 32 |
+
path = path or DEFAULT_DATASET_PATH
|
| 33 |
+
if not path.exists():
|
| 34 |
+
raise FileNotFoundError(
|
| 35 |
+
f"Dataset not found at {path}. Run: python -m locomo download"
|
| 36 |
+
)
|
| 37 |
+
with path.open(encoding="utf-8") as f:
|
| 38 |
+
data = json.load(f)
|
| 39 |
+
if not isinstance(data, list):
|
| 40 |
+
raise ValueError("Expected locomo10.json to be a JSON array")
|
| 41 |
+
return data
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def get_sample(dataset: list[dict[str, Any]], sample_id: str) -> dict[str, Any]:
|
| 45 |
+
for sample in dataset:
|
| 46 |
+
if sample.get("sample_id") == sample_id:
|
| 47 |
+
return sample
|
| 48 |
+
raise KeyError(f"sample_id {sample_id!r} not found")
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def session_keys(conversation: dict[str, Any]) -> list[str]:
|
| 52 |
+
keys = [
|
| 53 |
+
k
|
| 54 |
+
for k, v in conversation.items()
|
| 55 |
+
if k.startswith("session_")
|
| 56 |
+
and not k.endswith("date_time")
|
| 57 |
+
and isinstance(v, list)
|
| 58 |
+
]
|
| 59 |
+
|
| 60 |
+
def sort_key(name: str) -> int:
|
| 61 |
+
try:
|
| 62 |
+
return int(name.split("_", 1)[1])
|
| 63 |
+
except (IndexError, ValueError):
|
| 64 |
+
return 0
|
| 65 |
+
|
| 66 |
+
return sorted(keys, key=sort_key)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def format_turn(turn: dict[str, Any]) -> str:
|
| 70 |
+
text = (turn.get("text") or "").strip()
|
| 71 |
+
caption = turn.get("blip_caption")
|
| 72 |
+
if caption:
|
| 73 |
+
text = f"{text} [image: {caption}]".strip()
|
| 74 |
+
query = (turn.get("query") or "").strip()
|
| 75 |
+
if query:
|
| 76 |
+
text = f"{text} [image query: {query}]".strip()
|
| 77 |
+
speaker = turn.get("speaker") or "Unknown"
|
| 78 |
+
dia_id = turn.get("dia_id") or "?"
|
| 79 |
+
return f"{speaker} ({dia_id}): {text}"
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def iter_image_cues(sample: dict[str, Any]) -> list[str]:
|
| 83 |
+
"""Surface LoCoMo image query strings (often more specific than BLIP captions)."""
|
| 84 |
+
conversation = sample.get("conversation") or {}
|
| 85 |
+
lines: list[str] = []
|
| 86 |
+
for session_key in session_keys(conversation):
|
| 87 |
+
for turn in conversation.get(session_key) or []:
|
| 88 |
+
if not isinstance(turn, dict):
|
| 89 |
+
continue
|
| 90 |
+
query = (turn.get("query") or "").strip()
|
| 91 |
+
if not query:
|
| 92 |
+
continue
|
| 93 |
+
speaker = turn.get("speaker") or "Unknown"
|
| 94 |
+
dia_id = turn.get("dia_id") or "?"
|
| 95 |
+
caption = (turn.get("blip_caption") or "").strip()
|
| 96 |
+
extra = f"; caption: {caption}" if caption else ""
|
| 97 |
+
lines.append(
|
| 98 |
+
f"{speaker} ({dia_id}) [{session_key}]: [image query: {query}]{extra}"
|
| 99 |
+
)
|
| 100 |
+
return lines
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def format_session(conversation: dict[str, Any], session_key: str) -> str:
|
| 104 |
+
when = conversation.get(f"{session_key}_date_time", "unknown time")
|
| 105 |
+
speaker_a = conversation.get("speaker_a", "Speaker A")
|
| 106 |
+
speaker_b = conversation.get("speaker_b", "Speaker B")
|
| 107 |
+
turns = conversation.get(session_key) or []
|
| 108 |
+
lines = [
|
| 109 |
+
f"Conversation between {speaker_a} and {speaker_b}.",
|
| 110 |
+
f"Session time: {when}.",
|
| 111 |
+
f"Session id: {session_key}.",
|
| 112 |
+
"",
|
| 113 |
+
]
|
| 114 |
+
for turn in turns:
|
| 115 |
+
if isinstance(turn, dict):
|
| 116 |
+
lines.append(format_turn(turn))
|
| 117 |
+
return "\n".join(lines)
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def format_turn_unit(
|
| 121 |
+
conversation: dict[str, Any], session_key: str, turn: dict[str, Any]
|
| 122 |
+
) -> str:
|
| 123 |
+
when = conversation.get(f"{session_key}_date_time", "unknown time")
|
| 124 |
+
speaker_a = conversation.get("speaker_a", "Speaker A")
|
| 125 |
+
speaker_b = conversation.get("speaker_b", "Speaker B")
|
| 126 |
+
return "\n".join(
|
| 127 |
+
[
|
| 128 |
+
f"Conversation between {speaker_a} and {speaker_b}.",
|
| 129 |
+
f"Session time: {when}.",
|
| 130 |
+
f"Session id: {session_key}.",
|
| 131 |
+
"",
|
| 132 |
+
format_turn(turn),
|
| 133 |
+
]
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def iter_ingest_units(
|
| 138 |
+
sample: dict[str, Any],
|
| 139 |
+
granularity: str = "session",
|
| 140 |
+
limit_sessions: int | None = None,
|
| 141 |
+
) -> list[dict[str, Any]]:
|
| 142 |
+
conversation = sample["conversation"]
|
| 143 |
+
keys = session_keys(conversation)
|
| 144 |
+
if limit_sessions is not None:
|
| 145 |
+
keys = keys[:limit_sessions]
|
| 146 |
+
|
| 147 |
+
units: list[dict[str, Any]] = []
|
| 148 |
+
if granularity == "session":
|
| 149 |
+
for key in keys:
|
| 150 |
+
units.append(
|
| 151 |
+
{
|
| 152 |
+
"unit_id": key,
|
| 153 |
+
"session_key": key,
|
| 154 |
+
"text": format_session(conversation, key),
|
| 155 |
+
"source_timestamp": conversation.get(f"{key}_date_time"),
|
| 156 |
+
}
|
| 157 |
+
)
|
| 158 |
+
return units
|
| 159 |
+
|
| 160 |
+
if granularity == "turn":
|
| 161 |
+
for key in keys:
|
| 162 |
+
for idx, turn in enumerate(conversation.get(key) or []):
|
| 163 |
+
if not isinstance(turn, dict):
|
| 164 |
+
continue
|
| 165 |
+
dia_id = turn.get("dia_id") or f"{key}:{idx}"
|
| 166 |
+
units.append(
|
| 167 |
+
{
|
| 168 |
+
"unit_id": f"{key}:{dia_id}",
|
| 169 |
+
"session_key": key,
|
| 170 |
+
"dia_id": dia_id,
|
| 171 |
+
"text": format_turn_unit(conversation, key, turn),
|
| 172 |
+
"source_timestamp": conversation.get(f"{key}_date_time"),
|
| 173 |
+
}
|
| 174 |
+
)
|
| 175 |
+
return units
|
| 176 |
+
|
| 177 |
+
raise ValueError("granularity must be 'session' or 'turn'")
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def dataset_stats(dataset: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
| 181 |
+
rows = []
|
| 182 |
+
for sample in dataset:
|
| 183 |
+
conversation = sample.get("conversation") or {}
|
| 184 |
+
keys = session_keys(conversation)
|
| 185 |
+
turns = sum(len(conversation.get(k) or []) for k in keys)
|
| 186 |
+
rows.append(
|
| 187 |
+
{
|
| 188 |
+
"sample_id": sample.get("sample_id"),
|
| 189 |
+
"brain_id": brain_id_for(str(sample.get("sample_id"))),
|
| 190 |
+
"speaker_a": conversation.get("speaker_a"),
|
| 191 |
+
"speaker_b": conversation.get("speaker_b"),
|
| 192 |
+
"sessions": len(keys),
|
| 193 |
+
"turns": turns,
|
| 194 |
+
"qa": len(sample.get("qa") or []),
|
| 195 |
+
}
|
| 196 |
+
)
|
| 197 |
+
return rows
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
def resolve_samples(
|
| 201 |
+
settings: Settings,
|
| 202 |
+
sample_ids: list[str] | None = None,
|
| 203 |
+
) -> list[dict[str, Any]]:
|
| 204 |
+
dataset = load_dataset(settings.dataset_path)
|
| 205 |
+
if not sample_ids:
|
| 206 |
+
return dataset
|
| 207 |
+
return [get_sample(dataset, sid) for sid in sample_ids]
|
benchmarks/locomo/evaluate.py
ADDED
|
@@ -0,0 +1,500 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
import threading
|
| 5 |
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 6 |
+
from dataclasses import asdict, dataclass, field
|
| 7 |
+
from datetime import datetime, timezone
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import Any
|
| 10 |
+
|
| 11 |
+
from rich.console import Console
|
| 12 |
+
from rich.progress import Progress
|
| 13 |
+
|
| 14 |
+
from locomo.answer import answer_question
|
| 15 |
+
from locomo.client import BrainAPIClient
|
| 16 |
+
from locomo.config import Settings
|
| 17 |
+
from locomo.dataset import brain_id_for
|
| 18 |
+
from locomo.ingest import append_jsonl, load_jsonl
|
| 19 |
+
from locomo.judge import judge_answer
|
| 20 |
+
from locomo.metrics import evidence_coverage, overlap_scores
|
| 21 |
+
from locomo.prompts import (
|
| 22 |
+
attach_image_cues,
|
| 23 |
+
enrich_paths_from_triples,
|
| 24 |
+
flatten_path,
|
| 25 |
+
flatten_triple,
|
| 26 |
+
)
|
| 27 |
+
from locomo.sota import merge_contexts, plan_gap_fill, plan_selective_second_reflect
|
| 28 |
+
|
| 29 |
+
console = Console()
|
| 30 |
+
_write_lock = threading.Lock()
|
| 31 |
+
|
| 32 |
+
_GRAPH_CONTEXT_CAP = 60000
|
| 33 |
+
_TRIPLES_CAP = 500
|
| 34 |
+
_PASSAGE_PREFIX = "[passage] "
|
| 35 |
+
_SESSION_RE = re.compile(r"session_(\d+)", re.IGNORECASE)
|
| 36 |
+
_DIALOG_RE = re.compile(r"\bD(\d+):", re.IGNORECASE)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
@dataclass
|
| 40 |
+
class AnswerRecord:
|
| 41 |
+
sample_id: str
|
| 42 |
+
brain_id: str
|
| 43 |
+
qa_index: int
|
| 44 |
+
question: str
|
| 45 |
+
gold: str
|
| 46 |
+
prediction: str
|
| 47 |
+
category: int
|
| 48 |
+
evidence: list[str]
|
| 49 |
+
judge_correct: bool
|
| 50 |
+
judge_reason: str
|
| 51 |
+
judge_raw: str
|
| 52 |
+
f1: float | None
|
| 53 |
+
bleu1: float | None
|
| 54 |
+
retrieve_latency_ms: float | None
|
| 55 |
+
answer_latency_ms: float | None
|
| 56 |
+
judge_latency_ms: float | None
|
| 57 |
+
answer_model: str | None
|
| 58 |
+
judge_model: str | None
|
| 59 |
+
answer_total_tokens: int | None
|
| 60 |
+
judge_total_tokens: int | None
|
| 61 |
+
graph_context: str
|
| 62 |
+
triples: list[str] = field(default_factory=list)
|
| 63 |
+
paths: list[str] = field(default_factory=list)
|
| 64 |
+
source_passages: list[str] = field(default_factory=list)
|
| 65 |
+
historical_context: list[str] = field(default_factory=list)
|
| 66 |
+
retrieved_session_ids: list[str] = field(default_factory=list)
|
| 67 |
+
retrieved_session_ids_graph: list[str] = field(default_factory=list)
|
| 68 |
+
retrieved_session_ids_passages: list[str] = field(default_factory=list)
|
| 69 |
+
context_truncated: dict[str, int] = field(default_factory=dict)
|
| 70 |
+
is_adversarial: bool = False
|
| 71 |
+
error: str | None = None
|
| 72 |
+
timestamp: str = ""
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _extract_session_ids(*blobs: Any) -> list[str]:
|
| 76 |
+
found: list[str] = []
|
| 77 |
+
seen: set[str] = set()
|
| 78 |
+
for blob in blobs:
|
| 79 |
+
if isinstance(blob, (list, tuple)):
|
| 80 |
+
text = "\n".join(str(item) for item in blob)
|
| 81 |
+
else:
|
| 82 |
+
text = str(blob or "")
|
| 83 |
+
for match in _SESSION_RE.finditer(text):
|
| 84 |
+
sid = f"session_{match.group(1)}"
|
| 85 |
+
if sid not in seen:
|
| 86 |
+
seen.add(sid)
|
| 87 |
+
found.append(sid)
|
| 88 |
+
for match in _DIALOG_RE.finditer(text):
|
| 89 |
+
sid = f"session_{match.group(1)}"
|
| 90 |
+
if sid not in seen:
|
| 91 |
+
seen.add(sid)
|
| 92 |
+
found.append(sid)
|
| 93 |
+
return found
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def _graph_channel(text_context: str, source_passages: list[str]) -> str:
|
| 97 |
+
passage_block = "\n".join(f"{_PASSAGE_PREFIX}{p}" for p in source_passages)
|
| 98 |
+
if passage_block and text_context.startswith(passage_block):
|
| 99 |
+
return text_context[len(passage_block) :].lstrip("\n")
|
| 100 |
+
return "\n".join(
|
| 101 |
+
line
|
| 102 |
+
for line in text_context.splitlines()
|
| 103 |
+
if not line.startswith(_PASSAGE_PREFIX)
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def extract_context_channels(context: dict[str, Any]) -> dict[str, Any]:
|
| 108 |
+
context = enrich_paths_from_triples(context)
|
| 109 |
+
source_passages = [str(p) for p in (context.get("source_passages") or []) if p]
|
| 110 |
+
historical_context = [
|
| 111 |
+
str(h) for h in (context.get("historical_context") or []) if h
|
| 112 |
+
]
|
| 113 |
+
triples = [flatten_triple(t) for t in (context.get("triples") or []) if t]
|
| 114 |
+
paths = [flatten_path(p) for p in (context.get("paths") or []) if p]
|
| 115 |
+
graph_context = _graph_channel(
|
| 116 |
+
str(context.get("text_context") or ""), source_passages
|
| 117 |
+
)
|
| 118 |
+
session_ids_graph = _extract_session_ids(graph_context, triples, paths)
|
| 119 |
+
session_ids_passages = _extract_session_ids(source_passages, historical_context)
|
| 120 |
+
truncated: dict[str, int] = {}
|
| 121 |
+
if len(graph_context) > _GRAPH_CONTEXT_CAP:
|
| 122 |
+
truncated["graph_context"] = len(graph_context)
|
| 123 |
+
graph_context = graph_context[:_GRAPH_CONTEXT_CAP]
|
| 124 |
+
if len(triples) > _TRIPLES_CAP:
|
| 125 |
+
truncated["triples"] = len(triples)
|
| 126 |
+
triples = triples[:_TRIPLES_CAP]
|
| 127 |
+
return {
|
| 128 |
+
"graph_context": graph_context,
|
| 129 |
+
"triples": triples,
|
| 130 |
+
"paths": paths,
|
| 131 |
+
"source_passages": source_passages,
|
| 132 |
+
"historical_context": historical_context,
|
| 133 |
+
"retrieved_session_ids": _extract_session_ids(
|
| 134 |
+
session_ids_passages, session_ids_graph
|
| 135 |
+
),
|
| 136 |
+
"retrieved_session_ids_graph": session_ids_graph,
|
| 137 |
+
"retrieved_session_ids_passages": session_ids_passages,
|
| 138 |
+
"context_truncated": truncated,
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def _utc_now() -> str:
|
| 143 |
+
return datetime.now(timezone.utc).isoformat()
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def qa_gold(qa: dict[str, Any]) -> tuple[str, bool]:
|
| 147 |
+
answer = qa.get("answer")
|
| 148 |
+
if answer is not None and str(answer).strip():
|
| 149 |
+
return str(answer), int(qa.get("category") or 0) == 5
|
| 150 |
+
adversarial = qa.get("adversarial_answer")
|
| 151 |
+
if adversarial is not None:
|
| 152 |
+
return str(adversarial), True
|
| 153 |
+
return "", int(qa.get("category") or 0) == 5
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def qa_key(sample_id: str, qa_index: int) -> str:
|
| 157 |
+
return f"{sample_id}::{qa_index}"
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def completed_qa_keys(answers_path: Path) -> set[str]:
|
| 161 |
+
done = set()
|
| 162 |
+
for row in load_jsonl(answers_path):
|
| 163 |
+
if row.get("error"):
|
| 164 |
+
continue
|
| 165 |
+
if row.get("prediction") is None:
|
| 166 |
+
continue
|
| 167 |
+
done.add(qa_key(str(row.get("sample_id")), int(row.get("qa_index"))))
|
| 168 |
+
return done
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def iter_qa_jobs(
|
| 172 |
+
samples: list[dict[str, Any]],
|
| 173 |
+
*,
|
| 174 |
+
categories: set[int] | None = None,
|
| 175 |
+
skip_adversarial: bool = True,
|
| 176 |
+
limit: int | None = None,
|
| 177 |
+
brain_override: str | None = None,
|
| 178 |
+
) -> list[dict[str, Any]]:
|
| 179 |
+
if brain_override and len(samples) > 1:
|
| 180 |
+
raise SystemExit("--brain can only be used with a single --sample")
|
| 181 |
+
jobs: list[dict[str, Any]] = []
|
| 182 |
+
for sample in samples:
|
| 183 |
+
sample_id = str(sample["sample_id"])
|
| 184 |
+
brain_id = brain_override or brain_id_for(sample_id)
|
| 185 |
+
for idx, qa in enumerate(sample.get("qa") or []):
|
| 186 |
+
cat = int(qa.get("category") or 0)
|
| 187 |
+
if skip_adversarial and cat == 5:
|
| 188 |
+
continue
|
| 189 |
+
if categories is not None and cat not in categories:
|
| 190 |
+
continue
|
| 191 |
+
jobs.append(
|
| 192 |
+
{
|
| 193 |
+
"sample_id": sample_id,
|
| 194 |
+
"brain_id": brain_id,
|
| 195 |
+
"qa_index": idx,
|
| 196 |
+
"qa": qa,
|
| 197 |
+
"sample": sample,
|
| 198 |
+
}
|
| 199 |
+
)
|
| 200 |
+
if limit is not None and len(jobs) >= limit:
|
| 201 |
+
return jobs
|
| 202 |
+
return jobs
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
def selftest_records() -> list[str]:
|
| 206 |
+
errors: list[str] = []
|
| 207 |
+
passages = [
|
| 208 |
+
"Conversation between Caroline and Melanie.\nSession id: session_9.\n"
|
| 209 |
+
+ ("filler " * 4000),
|
| 210 |
+
"Session id: session_11. Caroline talked about the marathon.",
|
| 211 |
+
]
|
| 212 |
+
context = {
|
| 213 |
+
"text_context": "\n".join(
|
| 214 |
+
[f"{_PASSAGE_PREFIX}{p}" for p in passages]
|
| 215 |
+
+ [
|
| 216 |
+
"Melanie | PUT | keys | IN | slipper (session_3)",
|
| 217 |
+
"[dossier] Melanie: runs charity races (session_3)",
|
| 218 |
+
]
|
| 219 |
+
),
|
| 220 |
+
"source_passages": passages,
|
| 221 |
+
"historical_context": ["Session id: session_11. Earlier chunk."],
|
| 222 |
+
"triples": [
|
| 223 |
+
{
|
| 224 |
+
"identified_entity": "Melanie",
|
| 225 |
+
"triple": ["Melanie", "PUT", "keys", "IN", "slipper"],
|
| 226 |
+
}
|
| 227 |
+
],
|
| 228 |
+
"paths": [
|
| 229 |
+
{
|
| 230 |
+
"hubs": ["event-a", "event-b"],
|
| 231 |
+
"shared_entity_name": "Melanie",
|
| 232 |
+
"legs": [
|
| 233 |
+
"Actor: Melanie | MADE | Event: put keys | TARGETED | Target: slipper",
|
| 234 |
+
"Actor: Melanie | MADE | Event: run | TARGETED | Target: race",
|
| 235 |
+
],
|
| 236 |
+
}
|
| 237 |
+
],
|
| 238 |
+
}
|
| 239 |
+
channels = extract_context_channels(context)
|
| 240 |
+
record = AnswerRecord(
|
| 241 |
+
sample_id="conv-26",
|
| 242 |
+
brain_id="locomoconv26",
|
| 243 |
+
qa_index=0,
|
| 244 |
+
question="Where did Melanie put the keys?",
|
| 245 |
+
gold="In Melanie's slipper",
|
| 246 |
+
prediction="In Melanie's slipper",
|
| 247 |
+
category=4,
|
| 248 |
+
evidence=["D3:2"],
|
| 249 |
+
judge_correct=True,
|
| 250 |
+
judge_reason="",
|
| 251 |
+
judge_raw="",
|
| 252 |
+
f1=1.0,
|
| 253 |
+
bleu1=1.0,
|
| 254 |
+
retrieve_latency_ms=1.0,
|
| 255 |
+
answer_latency_ms=1.0,
|
| 256 |
+
judge_latency_ms=1.0,
|
| 257 |
+
answer_model="answer-model",
|
| 258 |
+
judge_model="judge-model",
|
| 259 |
+
answer_total_tokens=1,
|
| 260 |
+
judge_total_tokens=1,
|
| 261 |
+
error=None,
|
| 262 |
+
timestamp=_utc_now(),
|
| 263 |
+
**channels,
|
| 264 |
+
)
|
| 265 |
+
row = asdict(record)
|
| 266 |
+
|
| 267 |
+
if _PASSAGE_PREFIX in row["graph_context"]:
|
| 268 |
+
errors.append("graph_context must not contain passage lines")
|
| 269 |
+
if "PUT" not in row["graph_context"]:
|
| 270 |
+
errors.append("graph fact line missing from graph_context")
|
| 271 |
+
if not row["triples"]:
|
| 272 |
+
errors.append("triples must be logged as their own field")
|
| 273 |
+
if "slipper" not in row["triples"][0]:
|
| 274 |
+
errors.append("triples must be flattened to readable text")
|
| 275 |
+
if not row["paths"]:
|
| 276 |
+
errors.append("paths must be logged as their own field")
|
| 277 |
+
if "via Melanie" not in row["paths"][0] or "-->" not in row["paths"][0]:
|
| 278 |
+
errors.append("paths must flatten to via-entity leg chains")
|
| 279 |
+
if row["retrieved_session_ids_graph"] != ["session_3"]:
|
| 280 |
+
errors.append(
|
| 281 |
+
f"graph channel session ids wrong: {row['retrieved_session_ids_graph']}"
|
| 282 |
+
)
|
| 283 |
+
if sorted(row["retrieved_session_ids_passages"]) != ["session_11", "session_9"]:
|
| 284 |
+
errors.append(
|
| 285 |
+
"passage channel session ids wrong: "
|
| 286 |
+
f"{row['retrieved_session_ids_passages']}"
|
| 287 |
+
)
|
| 288 |
+
if sorted(row["retrieved_session_ids"]) != [
|
| 289 |
+
"session_11",
|
| 290 |
+
"session_3",
|
| 291 |
+
"session_9",
|
| 292 |
+
]:
|
| 293 |
+
errors.append("combined session ids should be the union of both channels")
|
| 294 |
+
if evidence_coverage(row, "graph") != "full":
|
| 295 |
+
errors.append("graph-only evidence should score full recall on the graph channel")
|
| 296 |
+
if evidence_coverage(row, "passages") != "none":
|
| 297 |
+
errors.append("graph-only evidence should score no recall on the passage channel")
|
| 298 |
+
|
| 299 |
+
long_context = dict(context)
|
| 300 |
+
long_context["text_context"] = "x" * (_GRAPH_CONTEXT_CAP + 10)
|
| 301 |
+
long_context["source_passages"] = []
|
| 302 |
+
capped = extract_context_channels(long_context)
|
| 303 |
+
if len(capped["graph_context"]) != _GRAPH_CONTEXT_CAP:
|
| 304 |
+
errors.append("graph_context should be capped")
|
| 305 |
+
if capped["context_truncated"].get("graph_context") != _GRAPH_CONTEXT_CAP + 10:
|
| 306 |
+
errors.append("truncation must be recorded with the original size")
|
| 307 |
+
|
| 308 |
+
gold, adversarial = qa_gold(
|
| 309 |
+
{"category": 5, "adversarial_answer": "No information available"}
|
| 310 |
+
)
|
| 311 |
+
if gold != "No information available" or not adversarial:
|
| 312 |
+
errors.append("adversarial gold must be read from adversarial_answer")
|
| 313 |
+
gold, adversarial = qa_gold({"category": 4, "answer": "7 May 2023"})
|
| 314 |
+
if gold != "7 May 2023" or adversarial:
|
| 315 |
+
errors.append("non-adversarial gold must be read from answer")
|
| 316 |
+
return errors
|
| 317 |
+
|
| 318 |
+
|
| 319 |
+
def evaluate_samples(
|
| 320 |
+
settings: Settings,
|
| 321 |
+
samples: list[dict[str, Any]],
|
| 322 |
+
*,
|
| 323 |
+
run_dir: Path,
|
| 324 |
+
concurrency: int = 2,
|
| 325 |
+
categories: set[int] | None = None,
|
| 326 |
+
skip_adversarial: bool = True,
|
| 327 |
+
limit: int | None = None,
|
| 328 |
+
resume: bool = True,
|
| 329 |
+
historical_limit: int = 10,
|
| 330 |
+
max_passages: int = 8,
|
| 331 |
+
max_facts: int = 40,
|
| 332 |
+
apply_fact_filter: bool = True,
|
| 333 |
+
use_ppr: bool = False,
|
| 334 |
+
sufficiency_retry: bool = False,
|
| 335 |
+
brain_override: str | None = None,
|
| 336 |
+
) -> list[AnswerRecord]:
|
| 337 |
+
run_dir.mkdir(parents=True, exist_ok=True)
|
| 338 |
+
answers_path = run_dir / "answers.jsonl"
|
| 339 |
+
already_done = completed_qa_keys(answers_path) if resume else set()
|
| 340 |
+
|
| 341 |
+
jobs = iter_qa_jobs(
|
| 342 |
+
samples,
|
| 343 |
+
categories=categories,
|
| 344 |
+
skip_adversarial=skip_adversarial,
|
| 345 |
+
limit=limit,
|
| 346 |
+
brain_override=brain_override,
|
| 347 |
+
)
|
| 348 |
+
jobs = [
|
| 349 |
+
j
|
| 350 |
+
for j in jobs
|
| 351 |
+
if qa_key(j["sample_id"], j["qa_index"]) not in already_done
|
| 352 |
+
]
|
| 353 |
+
|
| 354 |
+
if not jobs:
|
| 355 |
+
console.print("[green]Nothing to evaluate (all QA already scored).[/green]")
|
| 356 |
+
return []
|
| 357 |
+
|
| 358 |
+
console.print(
|
| 359 |
+
f"Evaluating {len(jobs)} QA items "
|
| 360 |
+
f"(concurrency={concurrency}, skip_adversarial={skip_adversarial})"
|
| 361 |
+
)
|
| 362 |
+
|
| 363 |
+
records: list[AnswerRecord] = []
|
| 364 |
+
|
| 365 |
+
def _one(job: dict[str, Any]) -> AnswerRecord:
|
| 366 |
+
qa = job["qa"]
|
| 367 |
+
question = str(qa.get("question") or "")
|
| 368 |
+
gold, is_adversarial = qa_gold(qa)
|
| 369 |
+
category = int(qa.get("category") or 0)
|
| 370 |
+
evidence = list(qa.get("evidence") or [])
|
| 371 |
+
|
| 372 |
+
try:
|
| 373 |
+
with BrainAPIClient(settings) as client:
|
| 374 |
+
retrieved = client.retrieve_context(
|
| 375 |
+
question,
|
| 376 |
+
job["brain_id"],
|
| 377 |
+
historical_limit=historical_limit,
|
| 378 |
+
max_passages=max_passages,
|
| 379 |
+
max_facts=max_facts,
|
| 380 |
+
apply_fact_filter=apply_fact_filter,
|
| 381 |
+
use_ppr=use_ppr,
|
| 382 |
+
sufficiency_retry=sufficiency_retry,
|
| 383 |
+
)
|
| 384 |
+
context = retrieved.data or {}
|
| 385 |
+
context = attach_image_cues(
|
| 386 |
+
context, job.get("sample"), question=question
|
| 387 |
+
)
|
| 388 |
+
retrieve_ms = retrieved.latency_ms
|
| 389 |
+
draft = answer_question(settings, question, context)
|
| 390 |
+
if settings.gap_fill:
|
| 391 |
+
plan = plan_gap_fill(question, draft.answer, context)
|
| 392 |
+
if plan.needs_retry:
|
| 393 |
+
with BrainAPIClient(settings) as client:
|
| 394 |
+
second = client.retrieve_context(
|
| 395 |
+
plan.reformulated_query,
|
| 396 |
+
job["brain_id"],
|
| 397 |
+
historical_limit=historical_limit,
|
| 398 |
+
max_passages=max_passages,
|
| 399 |
+
max_facts=max_facts,
|
| 400 |
+
apply_fact_filter=apply_fact_filter,
|
| 401 |
+
use_ppr=use_ppr,
|
| 402 |
+
sufficiency_retry=sufficiency_retry,
|
| 403 |
+
)
|
| 404 |
+
retrieve_ms = (retrieve_ms or 0) + (second.latency_ms or 0)
|
| 405 |
+
context = merge_contexts(context, second.data or {})
|
| 406 |
+
context = attach_image_cues(
|
| 407 |
+
context, job.get("sample"), question=question
|
| 408 |
+
)
|
| 409 |
+
answered = answer_question(settings, question, context)
|
| 410 |
+
else:
|
| 411 |
+
answered = draft
|
| 412 |
+
# Cap: at most one selective second reflect after primary gap-fill.
|
| 413 |
+
reflect = plan_selective_second_reflect(
|
| 414 |
+
question, answered.answer, context
|
| 415 |
+
)
|
| 416 |
+
if reflect.needs_retry:
|
| 417 |
+
answered = answer_question(settings, question, context)
|
| 418 |
+
else:
|
| 419 |
+
answered = draft
|
| 420 |
+
channels = extract_context_channels(context)
|
| 421 |
+
judged = judge_answer(
|
| 422 |
+
settings,
|
| 423 |
+
question,
|
| 424 |
+
gold,
|
| 425 |
+
answered.answer,
|
| 426 |
+
adversarial=is_adversarial,
|
| 427 |
+
)
|
| 428 |
+
f1, bleu1 = overlap_scores(
|
| 429 |
+
answered.answer, gold, adversarial=is_adversarial
|
| 430 |
+
)
|
| 431 |
+
record = AnswerRecord(
|
| 432 |
+
sample_id=job["sample_id"],
|
| 433 |
+
brain_id=job["brain_id"],
|
| 434 |
+
qa_index=job["qa_index"],
|
| 435 |
+
question=question,
|
| 436 |
+
gold=gold,
|
| 437 |
+
prediction=answered.answer,
|
| 438 |
+
category=category,
|
| 439 |
+
evidence=evidence,
|
| 440 |
+
judge_correct=judged.correct,
|
| 441 |
+
judge_reason=judged.reason,
|
| 442 |
+
judge_raw=judged.raw,
|
| 443 |
+
f1=f1,
|
| 444 |
+
bleu1=bleu1,
|
| 445 |
+
retrieve_latency_ms=retrieve_ms,
|
| 446 |
+
answer_latency_ms=answered.latency_ms,
|
| 447 |
+
judge_latency_ms=judged.latency_ms,
|
| 448 |
+
answer_model=answered.model,
|
| 449 |
+
judge_model=judged.model,
|
| 450 |
+
answer_total_tokens=answered.total_tokens,
|
| 451 |
+
judge_total_tokens=judged.total_tokens,
|
| 452 |
+
is_adversarial=is_adversarial,
|
| 453 |
+
error=None,
|
| 454 |
+
timestamp=_utc_now(),
|
| 455 |
+
**channels,
|
| 456 |
+
)
|
| 457 |
+
except Exception as exc:
|
| 458 |
+
record = AnswerRecord(
|
| 459 |
+
sample_id=job["sample_id"],
|
| 460 |
+
brain_id=job["brain_id"],
|
| 461 |
+
qa_index=job["qa_index"],
|
| 462 |
+
question=question,
|
| 463 |
+
gold=gold,
|
| 464 |
+
prediction="",
|
| 465 |
+
category=category,
|
| 466 |
+
evidence=evidence,
|
| 467 |
+
judge_correct=False,
|
| 468 |
+
judge_reason="",
|
| 469 |
+
judge_raw="",
|
| 470 |
+
f1=None,
|
| 471 |
+
bleu1=None,
|
| 472 |
+
retrieve_latency_ms=None,
|
| 473 |
+
answer_latency_ms=None,
|
| 474 |
+
judge_latency_ms=None,
|
| 475 |
+
answer_model=None,
|
| 476 |
+
judge_model=None,
|
| 477 |
+
answer_total_tokens=None,
|
| 478 |
+
judge_total_tokens=None,
|
| 479 |
+
is_adversarial=is_adversarial,
|
| 480 |
+
error=str(exc),
|
| 481 |
+
timestamp=_utc_now(),
|
| 482 |
+
**extract_context_channels({}),
|
| 483 |
+
)
|
| 484 |
+
|
| 485 |
+
with _write_lock:
|
| 486 |
+
append_jsonl(answers_path, asdict(record))
|
| 487 |
+
return record
|
| 488 |
+
|
| 489 |
+
with Progress(console=console) as progress:
|
| 490 |
+
task = progress.add_task("evaluate", total=len(jobs))
|
| 491 |
+
with ThreadPoolExecutor(max_workers=max(1, concurrency)) as pool:
|
| 492 |
+
futures = [pool.submit(_one, job) for job in jobs]
|
| 493 |
+
for future in as_completed(futures):
|
| 494 |
+
records.append(future.result())
|
| 495 |
+
progress.advance(task)
|
| 496 |
+
|
| 497 |
+
ok = sum(1 for r in records if not r.error)
|
| 498 |
+
failed = sum(1 for r in records if r.error)
|
| 499 |
+
console.print(f"Evaluate done: ok={ok} failed={failed} -> {answers_path}")
|
| 500 |
+
return records
|