novelty: bad LLM replies cached or misreported (judge retry, conflict silent drop, retry warning) — follow-up to #40
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 48/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- python
- Domain
- ai, testing-qa
Research direction
Start with dokimi/novelty/classify.py for judge_value and detect_conflicts, then read dokimi/novelty/atoms.py for extraction retries and warnings. Update the related wording in novelty.v1.json and skill-novelty.md; use the stub transport probe described in the issue to check malformed rows, retries, and cache acceptance. Done means unusable replies are retried and excluded from the cache, dropped conflict rows are reported, and the max-token hint appears only for parse failures.
Written by the indexing model from the issue text.
Description
Follow-up to #40 (merged). Three places in the novelty pipeline where a bad LLM reply is either cached as a real result or reported with the wrong cause. All three were reproduced with a stub transport on main (5a133b2) and on the #40 head (3848e20); the probe script is at the end.
1. judge_value: a reply where every row is malformed is not retried, and it gets cached
Setup: 10 atoms, batch=5, and a judge that answers each row with needed: "false", or leaves out needed, or leaves out n.
| 5a133b2 (before #40) | 3848e20 (#40 head) | |
|---|---|---|
needed: "false" |
10 verdicts, all needed=True (bool("false")) |
0 verdicts, 10 counted as unjudged_skipped |
needed missing |
10 verdicts, needed=False |
0 verdicts, 10 unjudged_skipped |
| calls / retried batches | 2 / 0 | 2 / 0 |
would the response cache store it (accept(reply)) |
yes | yes |
#40 stopped the wrong verdicts, but a judge reply with no usable row is neither split nor re-asked. The cache stores it, so every later run replays it. The warning says the atoms were "skipped by the judge's reply", but the reply could not be read at all. Extraction already handles the same case through _NoUsableRows (split the batch, keep the reply out of the cache). The judge parser has no equivalent check.
Suggested fix: if a judge reply has no row with a valid n and a real boolean needed, raise the same kind of "no usable rows" error, so the batch is split and the reply is not cached. Report malformed rows apart from rows the judge actually skipped, and fix the docstring at classify.py:118-119 and the warning at :154.
2. detect_conflicts: unreadable rows are dropped without being counted (already on main)
Setup: the reply is {"conflicts": [...]} and holds only rows that cannot be used: a == b, rows with no a/b, non-dict rows, or an index out of range.
- On both commits:
conflicts=0,pairs_compared=45/45,unchecked=0, no warning, and the cache would store the reply. The report says every pair was checked and none conflict. - The only difference is the self-pair: 5a133b2 reported a false conflict for it, and 3848e20 drops it silently.
Suggested fix: count the rows that were dropped and warn about them. If every row in a non-empty reply was dropped, treat the reply as unusable (no cache, retry) instead of as "no conflicts".
3. Extraction: the retry warning gives --max-tokens advice for replies that parsed fine (new in #40)
Setup: every row parses as JSON but none is usable ([{"foo": 1}, ...]).
- 5a133b2: 0 atoms, 2 calls, no split. The reply was silently taken as empty, which is what #40 fixes.
- 3848e20: the first warning is right ("replied with rows but none usable"). The second still says "re-asked 8 unparseable batches … a higher --max-tokens avoids the extra calls". These batches did parse, so a higher
--max-tokenswould not change anything.
Suggested fix: count retries by cause (parse failure vs. no usable rows) and give the --max-tokens hint only for parse failures. Update the same wording in the retried_batches docstring (atoms.py:189), the comment at :363-366, novelty.v1.json:295/306/313 and skill-novelty.md:799-800/817.
Repro
Run with PYTHONPATH=<checkout>/dokimi python probe.py, once on each commit. The script calls no LLM: it replaces LLMClient._default_transport and records what accept() returns, which is what the response cache uses to decide whether to store a reply.
probe.py
"""Stub-transport probes for the pr-review-toolkit findings on dokimi PR 40.
Run with PYTHONPATH=<checkout>/dokimi on main (5a133b2) and the PR head (3848e20)."""
import json, re
from dokimi.novelty.atoms import ExtractionReport, extract_atoms
from dokimi.novelty.client import LLMClient
from dokimi.novelty.spans import segment
from dokimi.novelty import classify
from dokimi.novelty.classify import ClassifyReport, judge_value, detect_conflicts
DOC = "\n\n".join(f"Setting number {i} defaults to {i * 10}." for i in range(1, 11))
def ids(p): return re.findall(r"^\[(s\d+)\]", p, flags=re.M)
def good(s): return {"statement": f"{s} x", "question": f"q {s}?", "span_ids": [s]}
def client(reply_fn, role):
"""Stub client; also records what the response cache would be told (cache_if = accept)."""
c = LLMClient(model="x", role=role)
c.seen = []
def fake(prompt, system, *, accept=None):
r = reply_fn(prompt)
c.seen.append(None if accept is None else bool(accept(r)))
return r, None
c._default_transport = fake
return c
atoms = extract_atoms(segment(DOC), client=client(lambda p: json.dumps([good(s) for s in ids(p)]), "extractor"), batch_spans=5)
print("atoms", len(atoms))
print("\n== F1 judge: every row malformed ==")
for name, row in {"needed:'false'": lambda n: {"n": n, "needed": "false"},
"needed missing": lambda n: {"n": n, "verdict": "no"},
"n missing": lambda n: {"needed": True}}.items():
c = client(lambda p: json.dumps([row(i + 1) for i in range(len(re.findall(r"^\d+\. ", p, flags=re.M)))]), "judge")
rep = ClassifyReport()
try:
v = judge_value(atoms, client=c, task="t", batch=5, report=rep)
print(f" {name:15} verdicts={len(v)} needed={[x.needed for x in v.values()][:3]} unjudged={len(rep.unjudged)} "
f"skipped={rep.unjudged_skipped} calls={rep.calls} retried={rep.retried_batches} cached={c.seen}")
for w in rep.warnings(len(atoms)) if hasattr(rep, "warnings") else []: print(" W:", w[:220])
except Exception as e:
print(f" {name:15} RAISE {type(e).__name__}: {e}")
print("\n== F2 detect_conflicts: only rows that cannot be read ==")
for name, rows in {"self-pair a==b": [{"a": 1, "b": 1, "why": "w"}],
"no a/b": [{"x": 1}], "non-dict": ["junk", 3],
"out of range": [{"a": 1, "b": 99}]}.items():
c = client(lambda p, rows=rows: json.dumps({"conflicts": rows}), "conflict")
rep = ClassifyReport()
try:
out = detect_conflicts(atoms, client=c, batch=10, report=rep)
print(f" {name:15} conflicts={len(out)} pairs={rep.pairs_compared}/{rep.pairs_total} unchecked={len(rep.unchecked)} "
f"calls={rep.calls} cached={c.seen}")
for w in rep.warnings(len(atoms)) if hasattr(rep, "warnings") else []: print(" W:", w[:220])
except Exception as e:
print(f" {name:15} RAISE {type(e).__name__}: {e}")
print("\n== F3 extraction warning when rows parse but none is usable ==")
for name, f in {"all rows junk": lambda p: [{"foo": 1} for _ in ids(p)],
"truncated JSON (real parse failure)": None}.items():
if f is None:
fn = lambda p: '[{"statement": "cut'
else:
fn = lambda p, f=f: json.dumps(f(p))
rep = ExtractionReport()
try:
a = extract_atoms(segment(DOC), client=client(fn, "extractor"), batch_spans=5, report=rep)
print(f" {name}: atoms={len(a)} calls={rep.calls} retried={rep.retried_batches}")
except Exception as e:
print(f" {name}: RAISE {type(e).__name__} calls={rep.calls} retried={rep.retried_batches}")
for w in rep.warnings(10): print(" W:", w[:400])
These were found by an automated review (pr-review-toolkit) of #40 and then confirmed by hand with the stubs above.
- Dominant language
- Python
- Stars
- 8
- Forks
- 7
- Avg merge
- 1d 4h
- Merged PRs (30d)
- 199
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from intel/dokimi
-
eval cross-judge drops the provider when splitting the judge specPossibly taken @vlad-perevezentsev claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
Maintainers usually reply within 1 day
-
Difficulty 5/5 Over a week Newbie friendliness 38/100
Maintainers usually reply within 1 day
-
Consolidate duplicated subsystems (stats, LLM routing, judge loop, containers, digests, writers)Open
Difficulty 5/5 Over a week Newbie friendliness 25/100
Maintainers usually reply within 1 day
-
Answering stack: one implementation for evaluate/benchmark/answers and arms/subjectsPossibly taken A pull request linked to this issue is open or already merged. Open
Difficulty 5/5 Over a week Newbie friendliness 25/100
Maintainers usually reply within 1 day
-
Difficulty 5/5 Over a week Newbie friendliness 24/100
Maintainers usually reply within 1 day
Similar issues
-
backlog
Difficulty 1/5 Under an hour Newbie friendliness 91/100
EchoTools/nevr-runtime#1108 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
btclib-org/btclib-wallet#587 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
documentation
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Judgment-Pack/judgment-pack-spec#275 ·
Maintainers usually reply within 1 day