"""Repeatable app-functioning RAG retrieval workload. Drives the real retrieval critical path -- `app.websockets.handlers._get_chunks`, the exact helper `CHAT_TURN`/`LEARN_NODE` call in production -- against the fixed corpus/query-set in `backend/benchmarks/rag/`. Never times or profiles the product independently: every duration this module writes out is read back from the canonical `observe_operation` boundary's own local-store rows, never from a clock the runner reads itself. What this module *does* do is supply experiment identity through context: it wraps each measured `_get_chunks()` call in its own `observe_operation("rag.benchmark_query", ...)`, calls `op.set_experiment(identity)` on that wrapper before the call, and everything `_get_chunks()` opens underneath (`rag.retrieval`, `embedding.query`, `chroma.*`) shares that wrapper's trace -- the OTel ambient-context mechanism, not an app-level side channel. The wrapper's own row (in `operational.jsonl`) then carries both the identity and copies of the canonical retrieval duration/attributes it read back from the nested `rag.retrieval` row, so every local row, span, and metric for one call join on one `trace_id`. Ingestion (`ingest_text`) is one-time setup, not part of what's measured, and reasonably uses the real `ingest_text()` directly per the task-4 brief -- that's the same function the app's own upload path calls internally. Corpus isolation: ingested chunks are tagged with a dedicated `project_id` (`BENCHMARK_PROJECT_ID`) into the *same* "library" ChromaDB collection every real session/project uses (matching how the product actually isolates content -- by `project_id` metadata filter, never a separate collection; `_get_chunks()` always queries the "library" collection by name). Re-running the harness deletes and re-ingests only that project_id's chunks first, so repeated runs stay idempotent without ever touching another project's data. Cache-state caveat: "cold" here means "first touch of this query in this process, before its own warm-up repetition" -- a same-process proxy, not a fresh-process/cold-OS-cache measurement. True process-level cold isolation is out of scope for a single harness process; this is documented rather than silently assumed away. """ from __future__ import annotations import argparse import asyncio import json import os import random import time import uuid from dataclasses import asdict from pathlib import Path from typing import Any os.environ.setdefault("EVALUATION_RUN", "true") os.environ.setdefault("CEREBRAS_API_KEY", "test-key") # never called: retrieval-only workload import chromadb from opentelemetry import trace as otel_trace from app.benchmarks.rag_observability import manifest as manifest_mod from app.benchmarks.rag_observability import summarize as summarize_mod from app.benchmarks.rag_observability.corpus import ( Corpus, QueryLabel, QuerySet, load_corpus, load_query_set, ) from app.benchmarks.rag_observability.quality import score_outcome from app.observability import bootstrap from app.observability.config import ObservabilityConfig, get_observability_config from app.observability.contracts import ExperimentIdentity, RetrievalOutcome from app.observability.local_store import LocalObservationStore from app.observability.logging import trace_context_ids from app.observability.operation import observe_operation from app.rag.ingestion import LIBRARY_COLLECTION, ingest_text from app.rag.pipeline_version import get_pipeline_version BENCHMARK_PROJECT_ID = "rag-observability-benchmark" DEFAULT_TOP_K = 5 DEFAULT_WARM_REPETITIONS = 10 DEFAULT_COLD_COUNT = 3 SMOKE_QUERY_COUNT = 2 SMOKE_WARM_REPETITIONS = 2 # --- Corpus setup (idempotent) ------------------------------------------------- def reset_and_ingest_corpus(db, corpus: Corpus) -> None: """Delete any previously-ingested benchmark chunks for our project_id, then re-ingest the fixed corpus fresh, so repeated runs never accumulate duplicate chunks or drift from the current corpus text.""" db.delete_where(LIBRARY_COLLECTION, {"project_id": BENCHMARK_PROJECT_ID}) for doc in corpus.documents: ingest_text( doc.text, source_label=doc.document_id, collection=LIBRARY_COLLECTION, chunk_type="content", db=db, project_id=BENCHMARK_PROJECT_ID, document_id=doc.document_id, ) # --- Execution order ------------------------------------------------- def seeded_order(queries: list[QueryLabel], seed: int) -> list[QueryLabel]: """A fixed, seed-reproducible query order -- reused identically across pipeline versions so v1/v2 measure the same sequence (protocol: "fixed seeded execution order and repeat it for v1 and v2").""" order = list(range(len(queries))) random.Random(seed).shuffle(order) return [queries[i] for i in order] # --- Reading back the canonical retrieval row ------------------------------- _RETRIEVAL_STAGES = ( "embedding.query", "chroma.collection_lookup", "chroma.collection_count", "chroma.vector_search", "rag.result_prepare", ) def _rows_for_trace(store: LocalObservationStore, trace_id: str | None) -> dict[str, dict[str, Any]]: """The canonical local-store rows belonging to one trace, keyed by operation name. Reads back what `_get_chunks()`'s own instrumentation already wrote via the shared `observe_operation` boundary -- not an independent measurement. """ if not trace_id: return {} return {row["operation"]: row for row in store.all() if row.get("trace_id") == trace_id} def _outcome_from_rows(rows: dict[str, dict[str, Any]], candidates: list[dict[str, Any]]) -> RetrievalOutcome: retrieval_row = rows.get("rag.retrieval") if retrieval_row and retrieval_row.get("retrieval"): return RetrievalOutcome(**retrieval_row["retrieval"]) # Degraded fallback (telemetry mode disabled / row not captured): cannot # distinguish success_empty from error_fallback here, which is exactly why # the primary path above -- reading the canonical row -- is preferred. return RetrievalOutcome.success(candidates) # --- One measured invocation ------------------------------------------------- async def invoke_once( label: QueryLabel, *, handlers_module: Any, global_store: LocalObservationStore, operational_store: LocalObservationStore, identity: ExperimentIdentity, top_k: int, ) -> tuple[str | None, RetrievalOutcome]: """Run one real `_get_chunks()` call wrapped in its own experiment-tagged operation, and return `(trace_id, outcome)` for the caller to score and (if this is a measured, non-warm-up call) persist. `identity.pipeline_version` is threaded straight into `_get_chunks()`'s own `pipeline_version` parameter, so this is the one and only place a non-default pipeline version enters the real retrieval critical path -- ordinary product call sites never pass it. `_get_chunks()` treats `"rag-naive-v1"` and `None` identically (see `ChromaDBClient.query_observed`), so passing it explicitly here changes nothing for v1 runs. """ with observe_operation( "rag.benchmark_query", subsystem="benchmark", consumer="benchmark_runner", evaluation_run=True, store=operational_store, ) as op: op.set_experiment(identity) trace_id, _span_id = trace_context_ids(otel_trace.get_current_span()) candidates = await handlers_module._get_chunks( BENCHMARK_PROJECT_ID, label.question, n=top_k, consumer="benchmark_runner", pipeline_version=identity.pipeline_version, ) rows = _rows_for_trace(global_store, trace_id) outcome = _outcome_from_rows(rows, candidates) op.set_retrieval(outcome) op.set("retrieval_status", outcome.status) op.set("empty_result", outcome.empty_result) op.set("retrieval_error", outcome.retrieval_error) if outcome.retrieval_error: op.mark_error(outcome.error_type) elif outcome.status == "success_empty": op.mark_terminal("success_empty") retrieval_row = rows.get("rag.retrieval") if retrieval_row is not None: if retrieval_row.get("duration_ms") is not None: op.add_stage("retrieval_ms", retrieval_row["duration_ms"]) for key, value in (retrieval_row.get("attributes") or {}).items(): if key not in ("error.type", "error.message"): op.set(key, value) for stage in _RETRIEVAL_STAGES: stage_row = rows.get(stage) if stage_row is not None and stage_row.get("duration_ms") is not None: op.add_stage(stage, stage_row["duration_ms"]) if stage_row is not None: for key, value in (stage_row.get("attributes") or {}).items(): op.set(f"{stage}.{key}", value) return trace_id, outcome def _append_jsonl(path: Path, row: dict[str, Any]) -> None: with path.open("a", encoding="utf-8") as handle: handle.write(json.dumps(row, ensure_ascii=False) + "\n") def _quality_row(trace_id: str | None, identity: ExperimentIdentity, label: QueryLabel, outcome: RetrievalOutcome) -> dict[str, Any]: quality = score_outcome(label, outcome) return { "trace_id": trace_id, "experiment_id": identity.experiment_id, "run_id": identity.run_id, "pipeline_version": identity.pipeline_version, "query_id": identity.query_id, "query_category": identity.query_category, "repetition": identity.repetition, "cache_state": identity.cache_state, **asdict(quality), } # --- Full workload ------------------------------------------------- async def run_workload( *, corpus: Corpus, query_set: QuerySet, pipeline_version: str, run_id: str, experiment_id: str, seed: int, top_k: int, warm_repetitions: int, cold_count: int, smoke: bool, output_dir: Path, ) -> dict[str, Any]: import app.websockets.handlers as handlers_module db = handlers_module.get_db() reset_and_ingest_corpus(db, corpus) global_store = bootstrap.get_state().local_store if global_store is None: raise RuntimeError( "observability must be initialized (mode='local' or 'full') before running " "the benchmark -- disabled mode cannot capture the canonical rag.retrieval row." ) operational_store = LocalObservationStore(output_dir, filename="operational.jsonl") warmup_store = LocalObservationStore(output_dir, filename="_warmup_discard.jsonl") quality_path = output_dir / "quality.jsonl" queries = seeded_order(list(query_set.queries), seed) if smoke: queries = queries[:SMOKE_QUERY_COUNT] warm_repetitions = SMOKE_WARM_REPETITIONS cold_count = 0 cold_ids = {q.query_id for q in queries[:cold_count]} def identity_for(label: QueryLabel, *, repetition: int, cache_state: str) -> ExperimentIdentity: return ExperimentIdentity( experiment_id=experiment_id, run_id=run_id, pipeline_version=pipeline_version, query_set_version=query_set.version, corpus_version=corpus.version, query_id=label.query_id, query_category=label.category, repetition=repetition, cache_state=cache_state, git_commit=manifest_mod.git_commit(), embedding_model=manifest_mod.EMBEDDING_MODEL, cerebras_model=None, chroma_version=chromadb.__version__, cognee_version=None, ) completed = 0 # Cold pass: exactly one measured call per cold-eligible query, before that # query's own warm-up has run in this process. for label in queries: if label.query_id not in cold_ids: continue identity = identity_for(label, repetition=0, cache_state="cold") trace_id, outcome = await invoke_once( label, handlers_module=handlers_module, global_store=global_store, operational_store=operational_store, identity=identity, top_k=top_k, ) _append_jsonl(quality_path, _quality_row(trace_id, identity, label, outcome)) completed += 1 # Warm pass: one unmeasured warm-up (discarded) + N measured warm repetitions. for label in queries: warmup_identity = identity_for(label, repetition=0, cache_state="warmup") await invoke_once( label, handlers_module=handlers_module, global_store=global_store, operational_store=warmup_store, identity=warmup_identity, top_k=top_k, ) for repetition in range(1, warm_repetitions + 1): identity = identity_for(label, repetition=repetition, cache_state="warm") trace_id, outcome = await invoke_once( label, handlers_module=handlers_module, global_store=global_store, operational_store=operational_store, identity=identity, top_k=top_k, ) _append_jsonl(quality_path, _quality_row(trace_id, identity, label, outcome)) completed += 1 return {"completed_measured_calls": completed, "queries_run": len(queries)} # --- CLI ------------------------------------------------- def _generate_run_id(pipeline_version: str) -> str: stamp = time.strftime("%Y%m%dT%H%M%SZ", time.gmtime()) return f"{pipeline_version}-{stamp}-{uuid.uuid4().hex[:8]}" def parse_args(argv: list[str] | None = None) -> argparse.Namespace: parser = argparse.ArgumentParser(description="RAG observability benchmark runner") parser.add_argument("--pipeline", default="rag-naive-v1", help="pipeline_version tag, e.g. rag-naive-v1") parser.add_argument("--smoke", action="store_true", help="run a fast 2-query smoke workload, marked smoke=true") parser.add_argument("--seed", type=int, default=42) parser.add_argument("--top-k", type=int, default=DEFAULT_TOP_K) parser.add_argument("--warm-repetitions", type=int, default=DEFAULT_WARM_REPETITIONS) parser.add_argument("--cold-count", type=int, default=DEFAULT_COLD_COUNT) parser.add_argument("--otel-mode", default="local", choices=["disabled", "local", "full"]) parser.add_argument("--run-id", default=None) parser.add_argument("--output-dir", default=None, type=Path) parser.add_argument( "--corpus", default=None, type=Path, help="override path to corpus.v1.json" ) parser.add_argument( "--query-set", default=None, type=Path, help="override path to query_set.v1.json" ) return parser.parse_args(argv) def main(argv: list[str] | None = None) -> dict[str, Any]: args = parse_args(argv) # Reject an unknown --pipeline before writing any run artifacts (manifest, # local store, OTel export) -- an evaluation run for a mistyped/unregistered # pipeline version must fail loudly at startup, never silently measure # rag-naive-v1 under the wrong label. get_pipeline_version(args.pipeline) corpus = load_corpus(args.corpus) if args.corpus else load_corpus() query_set = load_query_set(args.query_set) if args.query_set else load_query_set() run_id = args.run_id or _generate_run_id(args.pipeline) experiment_id = f"{args.pipeline}-{run_id}" base_cfg = get_observability_config() output_dir = args.output_dir or (base_cfg.artifact_root / "benchmarks" / run_id) output_dir = Path(output_dir) output_dir.mkdir(parents=True, exist_ok=True) cfg = ObservabilityConfig( enabled=True, mode=args.otel_mode, otlp_endpoint=base_cfg.otlp_endpoint, signoz_ui_url=base_cfg.signoz_ui_url, artifact_root=output_dir, ) bootstrap.initialize_observability(config=cfg) created_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) run_manifest = manifest_mod.build_manifest( run_id=run_id, experiment_id=experiment_id, pipeline_version=args.pipeline, smoke=args.smoke, seed=args.seed, warm_repetitions=(SMOKE_WARM_REPETITIONS if args.smoke else args.warm_repetitions), cold_query_count=(0 if args.smoke else args.cold_count), created_at=created_at, corpus=corpus, query_set=query_set, ) manifest_mod.write_manifest(run_manifest, output_dir / "manifest.json") try: result = asyncio.run( run_workload( corpus=corpus, query_set=query_set, pipeline_version=args.pipeline, run_id=run_id, experiment_id=experiment_id, seed=args.seed, top_k=args.top_k, warm_repetitions=args.warm_repetitions, cold_count=args.cold_count, smoke=args.smoke, output_dir=output_dir, ) ) finally: bootstrap.shutdown_observability() summary = summarize_mod.summarize_run_dir(output_dir) summarize_mod.write_summary(summary, output_dir / "summary.json") print(f"run_id={run_id}") print(f"output_dir={output_dir}") print(f"smoke={args.smoke}") print(f"completed_measured_calls={result['completed_measured_calls']}") print(f"queries_run={result['queries_run']}") return {"run_id": run_id, "output_dir": str(output_dir), "summary": summary, **result} if __name__ == "__main__": main()