dp-zp-agent/tools/answer_evaluation/rag_preparation.py
2026-11-03 13:36:42 +01:00

92 lines
5.7 KiB
Python

"""Resolve named source profiles and scope content before the shared token budget."""
from __future__ import annotations
import json
import re
import sqlite3
from contextlib import closing
from .context_policy import normalized, person_pattern, text_contains_value
from .question_intent import content_question
from .rag_evidence import PROFILE, academic_scope, academic_windows, requested_types, WORK_LABELS
from tools.retrieval.question_constraints import requested_years
def clarify(payload: dict, reason: str, message: str) -> dict:
return {**payload,'status':'ambiguous','reason':reason,'complete':False,
'clarification':message}
def prepare_named_content(db_file, question: str, payload: dict) -> dict:
if not content_question(question):
return payload
if re.search(r'\b(?:s tagom|s klucovym slovom|z kategorie|s autorom dokumentu)\b',normalized(question)):
return clarify(payload,'content_filter_requires_graph',
'Najprv over podmienku tagu, kategórie alebo autora cez /graphrag. '
'Potom môžem opísať obsah konkrétnej vyhovujúcej práce; túto podmienku nevynechám.')
from tools.rag.rag_utils import build_context_text
with closing(sqlite3.connect(db_file.resolve().as_uri()+'?mode=ro',uri=True)) as conn:
conn.row_factory=sqlite3.Row
if not {'documents','chunks'} <= {r[0] for r in conn.execute("SELECT name FROM sqlite_master WHERE type='table'")}:
return payload
catalog=[dict(r) for r in conn.execute('SELECT path,title,author,published FROM documents WHERE COALESCE(published,1)=1')
if PROFILE.fullmatch(r['path']) and r['title']]
named=[r for r in catalog if text_contains_value(question,r['title'],person=True)]
# Preserve question order rather than the original retrieval ranking.
named.sort(key=lambda r:re.search(person_pattern(r['title']),normalized(question)).start())
if len(named)>4:
return clarify(payload,'too_many_subjects','Pre podrobné obsahové porovnanie vyber najviac štyri osoby naraz.')
from tools.graphrag.graph_search import requested_name_phrases
first_names={normalized(r['title']).split()[0] for r in catalog}
capitalized={normalized(match[0]) for match in re.finditer(
r'\b[A-ZÁÄČĎÉÍĹĽŇÓÔŔŠŤÚÝŽ][^\W\d_]+(?:\s+[A-ZÁÄČĎÉÍĹĽŇÓÔŔŠŤÚÝŽ][^\W\d_]+){1,3}\b',question)}
unknown=[]
for phrase in requested_name_phrases(question):
words=phrase.split()
named_companion=(named and phrase in capitalized and
re.search(r'\ba\s+'+re.escape(phrase)+r'\b',normalized(question)))
if (2<=len(words)<=4 and (words[0] in first_names or named_companion) and
not any(text_contains_value(phrase,r['title'],person=True) for r in catalog)):
unknown.append(phrase)
if unknown:
return clarify(payload,'unresolved_subject','Meno „'+unknown[0]+'“ sa nepodarilo spoľahlivo priradiť. '
'Uveď jeho presné celé meno; ostatné osoby nebudem používať ako náhradu.')
if not named:
return payload
types=requested_types(question,[r['title'] for r in named])
years=requested_years(question)
selected=[]
for person in named:
rows=list(conn.execute('SELECT text,heading_paths_json FROM chunks WHERE document_path=? '
'AND COALESCE(published,1)=1 ORDER BY chunk_index',(person['path'],)))
frames=[]; records={};seen=set()
for row in rows:
paths=json.loads(row['heading_paths_json'] or '[]')
headings=[' > '.join(p) if isinstance(p,list) else str(p) for p in paths]
first=headings[0] if headings else ''
for scope,text in academic_windows('Sekcia: '+first+'\n'+str(row['text'] or '')):
if not scope or (types.get(person['title']) and scope[0]!=types[person['title']]) or (years and scope[1] not in years):
continue
label=WORK_LABELS[scope[0]]+(f' {scope[1]}' if scope[1] is not None else '')
records[scope]=label
if (scope,text) not in seen:
seen.add((scope,text));frames.append('Sekcia: '+label+'\n'+text)
if len(records)>1:
options='; '.join(dict.fromkeys(records.values()))
return clarify(payload,'ambiguous_work',f'Ktorú prácu osoby {person["title"]} máš na mysli? '
f'V indexe sú tieto možnosti: {options}. Uveď typ práce a rok.')
source=next((s for s in payload.get('sources',[]) if s.get('document_path')==person['path']),None)
if source is None:
from tools.rag.search_core import make_source_url
source={'title':person['title'],'document_path':person['path'],'source_url':make_source_url(person['path']),
'author':person['author'],'published':person['published']}
selected.append({**source,'text':'\n\n'.join(frames)})
# Keep already-retrieved child documents of the requested profiles.
for source in payload.get('sources',[]):
if any(source.get('document_path','').startswith(p['path'].removesuffix('README.md'))
and source.get('document_path')!=p['path'] for p in named):
selected.append(source)
selected=[{**source,'source_id':f'S{i}'} for i,source in enumerate(selected,1)]
return {**payload,'sources':selected,'source_count':len(selected),'context':build_context_text(selected),
'named_subjects':[p['title'] for p in named]}