Recover chunk ids mistyped from search results
Resolve a cited id that misses exactly to the nearest id the run retrieved, above a 0.75 similarity cutoff.
This commit is contained in:
parent
685a7c393d
commit
5752f61f2c
3 changed files with 65 additions and 4 deletions
|
|
@ -8,6 +8,7 @@
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- A capability that reaches its request limit keeps its cite tool for two further model requests while its other tools are removed, so an exhausted run can still register citations.
|
- A capability that reaches its request limit keeps its cite tool for two further model requests while its other tools are removed, so an exhausted run can still register citations.
|
||||||
|
- A cited chunk id that does not match a retrieved id exactly resolves to the closest one above a 0.75 similarity cutoff, recovering ids mistyped from search results.
|
||||||
- Dotfiles are parsed as their actual format instead of a single unstructured text block. Docling ignores the extension of a name starting with a dot, so converters strip leading dots from the name they hand it.
|
- Dotfiles are parsed as their actual format instead of a single unstructured text block. Docling ignores the extension of a name starting with a dot, so converters strip leading dots from the name they hand it.
|
||||||
|
|
||||||
### Documentation
|
### Documentation
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
from dataclasses import dataclass, field, replace
|
from dataclasses import dataclass, field, replace
|
||||||
|
from difflib import get_close_matches
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
|
|
@ -30,6 +31,27 @@ from haiku.rag.tools.search import build_binary_parts_from_results
|
||||||
CITATION_GRACE_REQUESTS = 2
|
CITATION_GRACE_REQUESTS = 2
|
||||||
"""Requests the cite tool outlives this capability's other tools by."""
|
"""Requests the cite tool outlives this capability's other tools by."""
|
||||||
|
|
||||||
|
CHUNK_ID_MATCH_CUTOFF = 0.75
|
||||||
|
"""Similarity a cited chunk id needs to be treated as a corrupted known id.
|
||||||
|
|
||||||
|
Calibration knob. Two unrelated UUID4s reach about 0.5, while dropping or
|
||||||
|
duplicating a character or a whole group stays above 0.75, so the gap is wide.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _nearest_known_id(chunk_id: str, known_ids: list[str]) -> str:
|
||||||
|
"""Recover a chunk id the model damaged while transcribing it.
|
||||||
|
|
||||||
|
Models copying opaque UUIDs drop and duplicate characters and whole
|
||||||
|
hyphen-separated groups. Candidates are limited to ids the run actually
|
||||||
|
retrieved, so a wrong match needs both a near miss and a same-run neighbour.
|
||||||
|
Ids that match nothing are returned unchanged for the caller to report.
|
||||||
|
"""
|
||||||
|
if not known_ids or chunk_id in known_ids:
|
||||||
|
return chunk_id
|
||||||
|
match = get_close_matches(chunk_id, known_ids, n=1, cutoff=CHUNK_ID_MATCH_CUTOFF)
|
||||||
|
return match[0] if match else chunk_id
|
||||||
|
|
||||||
|
|
||||||
def resolve_db_path(db_path: Path | None, config: AppConfig) -> Path:
|
def resolve_db_path(db_path: Path | None, config: AppConfig) -> Path:
|
||||||
if db_path is not None:
|
if db_path is not None:
|
||||||
|
|
@ -293,11 +315,11 @@ class RAGCapabilityBase[StateT: BaseModel](AbstractCapability[Any]):
|
||||||
state = cast(Any, self.state)
|
state = cast(Any, self.state)
|
||||||
for results in state.searches.values():
|
for results in state.searches.values():
|
||||||
all_results.extend(results)
|
all_results.extend(results)
|
||||||
citations = resolve_citations(chunk_ids, all_results)
|
known_ids = [result.chunk_id for result in all_results if result.chunk_id]
|
||||||
|
requested = [_nearest_known_id(cid.strip("[]"), known_ids) for cid in chunk_ids]
|
||||||
|
citations = resolve_citations(requested, all_results)
|
||||||
resolved = {citation.chunk_id for citation in citations}
|
resolved = {citation.chunk_id for citation in citations}
|
||||||
missing = [
|
missing = [cid for cid in requested if cid not in resolved]
|
||||||
cid.strip("[]") for cid in chunk_ids if cid.strip("[]") not in resolved
|
|
||||||
]
|
|
||||||
|
|
||||||
if missing:
|
if missing:
|
||||||
async with self.rag_lock:
|
async with self.rag_lock:
|
||||||
|
|
|
||||||
|
|
@ -353,6 +353,44 @@ async def test_cite_reports_unresolved_ids_on_partial_success(temp_db_path):
|
||||||
assert capability.state.citations == ["chunk-1"]
|
assert capability.state.citations == ["chunk-1"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_cite_repairs_chunk_ids_damaged_in_transcription(temp_db_path):
|
||||||
|
"""Models mistype opaque UUIDs; near misses resolve to the retrieved id."""
|
||||||
|
true_id = "b8e25ea1-0bb3-48b1-8fea-2ac1f148bf7c"
|
||||||
|
unrelated = "9c2cd07e-5a3f-45a6-968d-cbd6f06ab57b"
|
||||||
|
capability = create_rag(db_path=temp_db_path, config=AppConfig())
|
||||||
|
capability.state = RAGState(
|
||||||
|
searches={
|
||||||
|
"q": [
|
||||||
|
SearchResult(
|
||||||
|
content="evidence",
|
||||||
|
score=1.0,
|
||||||
|
chunk_id=true_id,
|
||||||
|
document_id="doc-1",
|
||||||
|
document_uri="test://document",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
client = AsyncMock()
|
||||||
|
client.get_chunk_by_id.return_value = None
|
||||||
|
capability.rag = client
|
||||||
|
|
||||||
|
dropped_char = "b8e25ea1-0bb3-48b1-8fea-2ac1f148bf7"
|
||||||
|
dropped_group = "0bb3-48b1-8fea-2ac1f148bf7c"
|
||||||
|
|
||||||
|
assert await capability._cite([dropped_char]) == "Registered 1 citation(s)."
|
||||||
|
assert await capability._cite([dropped_group]) == "Registered 1 citation(s)."
|
||||||
|
assert capability.state.citations == [true_id]
|
||||||
|
|
||||||
|
# An unrelated UUID is never attributed to a retrieved neighbour.
|
||||||
|
with pytest.raises(ModelRetry, match=unrelated):
|
||||||
|
await capability._cite([unrelated])
|
||||||
|
|
||||||
|
assert capability.state.citations == [true_id]
|
||||||
|
client.get_chunk_by_id.assert_awaited_once_with(unrelated)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_analysis_records_new_sandbox_search_results(temp_db_path):
|
async def test_analysis_records_new_sandbox_search_results(temp_db_path):
|
||||||
capability = create_analysis(db_path=temp_db_path, config=AppConfig())
|
capability = create_analysis(db_path=temp_db_path, config=AppConfig())
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue