Skip to content

feat(bench): parametric retrieval flags for experimental matrix - #37

Merged
jaylfc merged 1 commit into
masterfrom
feat/locomo-param-configs
Apr 22, 2026
Merged

feat(bench): parametric retrieval flags for experimental matrix#37
jaylfc merged 1 commit into
masterfrom
feat/locomo-param-configs

Conversation

@jaylfc

@jaylfc jaylfc commented Apr 20, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds 6 new optional flags to benchmarks/locomo_runner.py for the C1–C6 experiment matrix; all defaults preserve baseline behaviour exactly.
  • Each flag value is recorded in the output JSON meta block so scorecard runs are fully attributable.
  • No changes to scoring, aggregation, or any other module.

Flags added

Flag Default Purpose
--retrieval-top-k INT falls back to --top-k Override retrieval K independently from the scoring metric K
--context-format {plain,session_date,both} plain Change hit prefix format in the answer context
--adjacent-turns INT 0 Include ±N neighbouring turns per hit; deduplicated across hits
--llm-query-expansion off Expand query via expand_query_llm before retrieval
--reranker {ms-marco,bge-v2-m3,off} ms-marco Swap or disable the cross-encoder reranker
--multihop-decompose off LLM-split question into 2–3 sub-queries, union results

Integration decisions

bge-v2-m3 reranker: raises NotImplementedError if selected (with a clear message pointing the user to huggingface-cli download). The CrossEncoderReranker class is tightly coupled to the ms-marco tokenizer path; wiring a second model would require a deeper refactor. ms-marco and off are fully functional.

--reranker default is ms-marco: matches current behaviour — the cross-encoder was already being passed to retrieve() when strategy=full. For vector-only strategy, reranking is now applied inline in _retrieve() when a reranker is loaded.

--adjacent-turns plumbing: _ingest_conversation now returns a turn_index dict alongside the count/elapsed values; this is threaded through _guarded_process_qa as an explicit kwarg.

Test plan

  • python3 benchmarks/locomo_runner.py --help — all 6 flags visible with correct defaults
  • python3 -c "import ast; ast.parse(open('benchmarks/locomo_runner.py').read())" — clean parse
  • Full matrix runs to be executed on Fedora against the complete LoCoMo dataset

Summary by CodeRabbit

  • New Features
    • Multi-hop query decomposition: Complex queries are now broken into 2–3 sub-queries for improved retrieval coverage.
    • LLM-powered query expansion to enhance search relevance.
    • Reranker support with multiple model options (ms-marco, bge-v2-m3) to improve result ranking.
    • Adjacent-turn context injection: Retrieved results now include neighboring conversation turns for better context.
    • Enhanced conversation tracking with global turn indices for improved session management.
    • New CLI arguments to control retrieval, formatting, and reranker behavior.

Adds --retrieval-top-k / --context-format / --adjacent-turns /
--llm-query-expansion / --reranker / --multihop-decompose to
locomo_runner.py so the forthcoming C1-C6 experiment matrix can
toggle each architectural lever independently.

Defaults preserve baseline behaviour — the flag additions alone
don't change any existing run's output.

Each config gets recorded in the output JSON's meta block so the
scorecard doc can attribute per-category gains correctly.
@coderabbitai

coderabbitai Bot commented Apr 20, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Enhanced the benchmarking runner with multihop query decomposition via LLM, optional reranking, adjacent-turn context injection, and global turn-index tracking to improve retrieval-augmented question answering capability and evaluation fidelity.

Changes

Cohort / File(s) Summary
Multihop Query Decomposition & LLM Integration
benchmarks/locomo_runner.py
Added _decompose_query() to split queries into 2–3 sub-queries using MULTIHOP_PROMPT, and integrated optional LLM-based query expansion via taosmd.query_expansion.expand_query_llm within QA processing.
Reranking Pipeline
benchmarks/locomo_runner.py
Introduced _load_reranker() to initialize rerankers (ms-marco, bge-v2-m3, or off), and modified _retrieve() to optionally apply reranking when reranker.available is true for single-query vector-only retrieval.
Turn Index Tracking & Context Enrichment
benchmarks/locomo_runner.py
Extended _ingest_conversation() to return turn index and store turn_idx in vmem metadata; added _build_adjacent_map() to inject neighboring turn context; expanded _build_context() to support formatted datetime prefixes (plain, session_date, both) and append adjacent-turn text.
Retrieval Pipeline Restructuring & CLI Arguments
benchmarks/locomo_runner.py
Reworked _process_qa() to support both multihop decomposition (union/dedupe across sub-queries) and single-query retrieval; updated run() to initialize reranker, resolve retrieval_top_k, and pass turn indices to concurrent tasks; added CLI flags: --retrieval-top-k, --context-format, --adjacent-turns, --llm-query-expansion, --reranker, --multihop-decompose.

Sequence Diagram

sequenceDiagram
    participant User as Input Query
    participant LLM as LLM Decomposer
    participant Ret as Retriever
    participant Rer as Reranker
    participant Ctx as Context Builder
    participant QA as QA Processor
    
    User->>LLM: Optional: decompose into<br/>sub-queries
    LLM-->>Ret: Sub-queries (or original query)
    Ret->>Ret: Retrieve hits for each query<br/>(union/dedupe)
    Ret-->>Rer: Retrieved candidates
    Rer->>Rer: Optional: rerank by relevance
    Rer-->>Ctx: Ranked hits with turn_idx
    Ctx->>Ctx: Build adjacent_turns_map<br/>from neighboring turns
    Ctx-->>QA: Context with formatted dates<br/>+ adjacent turn text
    QA->>QA: Optional: LLM query expansion
    QA-->>User: Final QA response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰✨ Hops through queries, splitting them with care,
Reranking finds the finest thread to share,
Turn by turn, the neighbors draw near,
Each context richer, answers crystalline clear!
A multihop dance through retrieval's domain,
Where every context matters to explain. 🔍

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding parametric retrieval flags to support an experimental configuration matrix in benchmarks.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/locomo-param-configs

Comment @coderabbitai help to get the list of available commands and usage tips.

if context_format == "session_date":
prefix = f"[Session date: {dt}] " if dt else ""
elif context_format == "both":
prefix = f"[Session date: {dt}] [{dt}] " if dt else ""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Duplicate date prefix in "both" context format. This line outputs the same date string twice, which adds unnecessary noise to the LLM context.

raw = await vmem.search(query, limit=top_k)
hits = [{"text": r["text"], "metadata": r.get("metadata", {}),
"score": r.get("similarity", 0.0)} for r in raw]
if reranker is not None and getattr(reranker, "available", False):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Safe attribute check on reranker. If reranker object doesn't have an available attribute, this will silently skip reranking. Consider checking for existence explicitly before using getattr.

seen_texts.add(t)
all_hits.append(h)
# Cap at retrieval_top_k, preserving order
hits = all_hits[:retrieval_top_k]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Multihop hits are unsorted when truncated. Results are preserved in first seen order across sub-queries, not by relevance score. Higher scoring hits from later sub-queries can be dropped when truncating.

continue
idx = int(idx)
neighbours = []
for offset in range(-adjacent_turns, adjacent_turns + 1):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Range includes 2adjacent_turns neighbours. The help text says ±N, but this loop will return exactly 2N neighbours. This is technically correct but documentation should clarify that it's full window size.

lines = [l.strip() for l in raw.splitlines() if l.strip()]
if len(lines) >= 2:
return lines
except Exception:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Exception swallowed silently in query decomposition. At minimum log a warning when decomposition fails for debugging purposes.

@kilo-code-bot

kilo-code-bot Bot commented Apr 20, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 5 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 3
Issue Details (click to expand)

WARNING

File Line Issue
benchmarks/locomo_runner.py 276 Duplicate date prefix in "both" context format
benchmarks/locomo_runner.py 423 Multihop hits are unsorted when truncated

SUGGESTION

File Line Issue
benchmarks/locomo_runner.py 362 Safe attribute check on reranker
benchmarks/locomo_runner.py 244 Range includes 2*adjacent_turns neighbours
benchmarks/locomo_runner.py 346 Exception swallowed silently in query decomposition
Files Reviewed (1 file)
  • benchmarks/locomo_runner.py - 5 issues

Fix these issues in Kilo Cloud


Reviewed by seed-2-0-pro-260328 · 173,765 tokens

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
benchmarks/locomo_runner.py (1)

337-348: Surface fallback paths for the LLM-assisted features.

Both blocks swallow every exception and silently revert to the baseline path. That keeps the benchmark running, but it also means a run can be recorded with llm_query_expansion=true or multihop_decompose=true even when the feature never actually executed. A warning or counter here would make the experiment matrix auditable.

Also applies to: 399-407

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@benchmarks/locomo_runner.py`:
- Around line 202-207: The adjacent-turn expansion is crossing session
boundaries because global_idx is compared numerically in _build_adjacent_map();
restrict adjacency to the same session by checking dia_id (or session id) before
treating numeric neighbors as adjacent. Update _build_adjacent_map() (and any
code that uses turn_index) to only consider neighbor indices when
turn_index[str(neighbor_idx)]["dia_id"] ==
turn_index[str(global_idx)]["dia_id"], or else compute adjacency using
session-local indices rather than the global_idx, so last-turn/first-turn of
adjacent sessions are never linked.
- Around line 413-421: The current dedupe uses the hit's "text" which can
collapse distinct dialogue turns; change the dedupe key to turn identity instead
(preferably a tuple of h.get("dia_id") and h.get("turn_idx") ) when building
seen_texts/seen_ids before appending to all_hits; fall back to h.get("text")
only if both dia_id and turn_idx are missing. Update the loop that iterates
sq_hits (and the seen_texts variable name if desired) to compute this identity
per hit and use it for membership checks and adding to the seen set so distinct
turns with identical text are not discarded.
- Around line 411-425: The benchmark is incorrectly using retrieval_top_k as the
effective evaluation K instead of trimming candidates to the intended top_k:
when multihop_decompose is true you build all_hits from subquery results using
retrieval_top_k and then slice to retrieval_top_k, but nowhere do you trim to
the final top_k before building the answer context/evidence_hits; update the
logic in the block that computes hits (affecting multihop_decompose path and the
else path that calls _retrieve) so that after collecting candidate hits you
always trim hits = hits[:top_k] (or compute evidence_hits from hits[:top_k])
before any downstream context construction or evidence selection, ensuring both
branches use top_k for evaluation while still using retrieval_top_k only as the
candidate pool size for _retrieve/_decompose_query.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cad2bb2f-d2b5-4a9a-8fb8-af4a9a493200

📥 Commits

Reviewing files that changed from the base of the PR and between 7d2c780 and d89e349.

📒 Files selected for processing (1)
  • benchmarks/locomo_runner.py

Comment on lines +202 to +207
turn_index[str(global_idx)] = {
"datetime": dt,
"text": f"[{speaker}] {text}",
"speaker": speaker,
"dia_id": dia_id,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep adjacent-turn expansion inside the same session.

global_idx spans every session, and _build_adjacent_map() only checks numeric adjacency. A hit on the last turn of one session can therefore pull the first turn of the next session as a “neighbor”, which injects unrelated context into the prompt.

Suggested fix
             turn_index[str(global_idx)] = {
+                "session": session_key,
                 "datetime": dt,
                 "text": f"[{speaker}] {text}",
                 "speaker": speaker,
                 "dia_id": dia_id,
             }
     for hit in hits:
         meta = hit.get("metadata", {}) or {}
         idx = meta.get("turn_idx")
         if idx is None:
             continue
         idx = int(idx)
+        current_session = turn_index.get(str(idx), {}).get("session")
         neighbours = []
         for offset in range(-adjacent_turns, adjacent_turns + 1):
             if offset == 0:
                 continue
             ni = idx + offset
@@
             if ni in seen_indices or ni in primary_indices:
                 continue
             if str(ni) not in turn_index:
                 continue
+            if turn_index[str(ni)].get("session") != current_session:
+                continue
             neighbours.append(turn_index[str(ni)]["text"])
             seen_indices.add(ni)

Also applies to: 224-255

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@benchmarks/locomo_runner.py` around lines 202 - 207, The adjacent-turn
expansion is crossing session boundaries because global_idx is compared
numerically in _build_adjacent_map(); restrict adjacency to the same session by
checking dia_id (or session id) before treating numeric neighbors as adjacent.
Update _build_adjacent_map() (and any code that uses turn_index) to only
consider neighbor indices when turn_index[str(neighbor_idx)]["dia_id"] ==
turn_index[str(global_idx)]["dia_id"], or else compute adjacency using
session-local indices rather than the global_idx, so last-turn/first-turn of
adjacent sessions are never linked.

Comment on lines +411 to +425
if multihop_decompose:
sub_queries = await _decompose_query(client, ollama_url, retrieval_query)
seen_texts: set[str] = set()
all_hits: list[dict] = []
for sq in sub_queries:
sq_hits = await _retrieve(strategy, sq, vmem, retrieval_top_k, reranker)
for h in sq_hits:
t = h.get("text", "")
if t not in seen_texts:
seen_texts.add(t)
all_hits.append(h)
# Cap at retrieval_top_k, preserving order
hits = all_hits[:retrieval_top_k]
else:
hits = await _retrieve(strategy, retrieval_query, vmem, retrieval_top_k, reranker)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

--retrieval-top-k is changing the effective evaluation K too.

Line 425 retrieves retrieval_top_k hits, but nothing trims back to top_k before building the answer context or computing evidence_hits. That means the new flag widens the benchmarked K instead of just the retrieval candidate pool, which changes results and breaks the CLI contract.

Suggested fix
     if multihop_decompose:
         sub_queries = await _decompose_query(client, ollama_url, retrieval_query)
         seen_texts: set[str] = set()
         all_hits: list[dict] = []
         for sq in sub_queries:
             sq_hits = await _retrieve(strategy, sq, vmem, retrieval_top_k, reranker)
             for h in sq_hits:
                 t = h.get("text", "")
                 if t not in seen_texts:
                     seen_texts.add(t)
                     all_hits.append(h)
         # Cap at retrieval_top_k, preserving order
-        hits = all_hits[:retrieval_top_k]
+        candidate_hits = all_hits[:retrieval_top_k]
     else:
-        hits = await _retrieve(strategy, retrieval_query, vmem, retrieval_top_k, reranker)
+        candidate_hits = await _retrieve(strategy, retrieval_query, vmem, retrieval_top_k, reranker)
+
+    hits = candidate_hits[:top_k]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@benchmarks/locomo_runner.py` around lines 411 - 425, The benchmark is
incorrectly using retrieval_top_k as the effective evaluation K instead of
trimming candidates to the intended top_k: when multihop_decompose is true you
build all_hits from subquery results using retrieval_top_k and then slice to
retrieval_top_k, but nowhere do you trim to the final top_k before building the
answer context/evidence_hits; update the logic in the block that computes hits
(affecting multihop_decompose path and the else path that calls _retrieve) so
that after collecting candidate hits you always trim hits = hits[:top_k] (or
compute evidence_hits from hits[:top_k]) before any downstream context
construction or evidence selection, ensuring both branches use top_k for
evaluation while still using retrieval_top_k only as the candidate pool size for
_retrieve/_decompose_query.

Comment on lines +413 to +421
seen_texts: set[str] = set()
all_hits: list[dict] = []
for sq in sub_queries:
sq_hits = await _retrieve(strategy, sq, vmem, retrieval_top_k, reranker)
for h in sq_hits:
t = h.get("text", "")
if t not in seen_texts:
seen_texts.add(t)
all_hits.append(h)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Deduplicate multihop hits by turn identity, not by text.

Using text as the dedupe key will collapse distinct turns that happen to contain the same utterance, which is common in dialogue. That can drop the real evidence hit and attach adjacent context from the wrong turn. Prefer dia_id or turn_idx, with text only as a last-resort fallback.

Suggested fix
-        seen_texts: set[str] = set()
+        seen_hit_ids: set[str] = set()
         all_hits: list[dict] = []
         for sq in sub_queries:
             sq_hits = await _retrieve(strategy, sq, vmem, retrieval_top_k, reranker)
             for h in sq_hits:
-                t = h.get("text", "")
-                if t not in seen_texts:
-                    seen_texts.add(t)
+                meta = h.get("metadata", {}) or {}
+                hit_id = str(
+                    meta.get("dia_id")
+                    or meta.get("turn_idx")
+                    or h.get("text", "")
+                )
+                if hit_id not in seen_hit_ids:
+                    seen_hit_ids.add(hit_id)
                     all_hits.append(h)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
seen_texts: set[str] = set()
all_hits: list[dict] = []
for sq in sub_queries:
sq_hits = await _retrieve(strategy, sq, vmem, retrieval_top_k, reranker)
for h in sq_hits:
t = h.get("text", "")
if t not in seen_texts:
seen_texts.add(t)
all_hits.append(h)
seen_hit_ids: set[str] = set()
all_hits: list[dict] = []
for sq in sub_queries:
sq_hits = await _retrieve(strategy, sq, vmem, retrieval_top_k, reranker)
for h in sq_hits:
meta = h.get("metadata", {}) or {}
hit_id = str(
meta.get("dia_id")
or meta.get("turn_idx")
or h.get("text", "")
)
if hit_id not in seen_hit_ids:
seen_hit_ids.add(hit_id)
all_hits.append(h)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@benchmarks/locomo_runner.py` around lines 413 - 421, The current dedupe uses
the hit's "text" which can collapse distinct dialogue turns; change the dedupe
key to turn identity instead (preferably a tuple of h.get("dia_id") and
h.get("turn_idx") ) when building seen_texts/seen_ids before appending to
all_hits; fall back to h.get("text") only if both dia_id and turn_idx are
missing. Update the loop that iterates sq_hits (and the seen_texts variable name
if desired) to compute this identity per hit and use it for membership checks
and adding to the seen set so distinct turns with identical text are not
discarded.

@jaylfc
jaylfc merged commit b09147c into master Apr 22, 2026
2 checks passed
@jaylfc
jaylfc deleted the feat/locomo-param-configs branch April 22, 2026 20:37
jaylfc added a commit that referenced this pull request Apr 22, 2026
…rks (#40)

Adds the LoCoMo runner invocation to the Running Benchmarks section,
including the tunable retrieval-lever flags landed in #37. Links
docs/benchmarks.md as the source of per-commit number provenance.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant