88 lines
4.9 KiB
Python
88 lines
4.9 KiB
Python
"""Separate supported answer scope from the search's technical selection limits.
|
|
|
|
Supported means the delivered facts/excerpts have evidence; it never promises
|
|
exhaustive retrieval, a complete source document or absence of other records.
|
|
"""
|
|
import re
|
|
|
|
from .context_policy import normalized
|
|
|
|
|
|
def response_scope(question: str, *, content: bool = False) -> str:
|
|
query = normalized(re.sub(r'[„“"«][^„“”"»]+[“”"»]', ' ', question))
|
|
# Record lists differ from the numbered assignment of one selected work.
|
|
records = r'(?:prac\w*|projekt\w*|student\w*|osob\w*|autor\w*|tag\w*|kategori\w*|zdroj\w*)'
|
|
modifiers = r'(?:(?:vsetk\w*|diplomov\w*|bakalarsk\w*|zaverecn\w*|timov\w*|vyskumn\w*|a|aj)\s+)*'
|
|
if re.search(r'\b(?:kolko|pocet|spocitaj)\b', query):
|
|
return 'count'
|
|
if re.search(rf'\b(?:vsetk\w*|upln\w*|kompletn\w*)\s+{modifiers}{records}\b'
|
|
rf'|\b(?:ake|ktore|vypis|zoznam|prehlad)\s+{modifiers}{records}\b', query):
|
|
return 'list'
|
|
if content:
|
|
if re.search(r'\b(?:zadani\w*|ciel\w*)\b', query) and not re.search(r'\b(?:strucn\w*|zhrn\w*)\b', query):
|
|
return 'assignment'
|
|
if re.search(r'\b(?:comu|o com|opis\w*|popis\w*|obsah\w*|zhrn\w*|strucn\w*)\b', query):
|
|
return 'overview'
|
|
return 'fact'
|
|
|
|
|
|
def answer_coverage(answer: dict, context: dict) -> dict:
|
|
"""Describe only the scope of an already validated RAG selection."""
|
|
requirements = context.get('requirements', {})
|
|
scope = response_scope(context.get('question', ''), content=requirements.get('content', False))
|
|
reasons = list(context.get('truncation_reasons') or [])
|
|
selected = {row.get('source_id') for row in answer.get('evidence_quotes', [])}
|
|
cited = set(answer.get('source_urls', []))
|
|
clipped = any('[OBSAH ZDROJA OREZANÝ]' in row.get('body', '')
|
|
for row in context.get('source_blocks', [])
|
|
if row.get('source_id') in selected or (not selected and
|
|
(row.get('source_url') in cited or answer.get('status') == 'no_answer')))
|
|
# A context budget may drop only unrelated sources. A clipping marker in
|
|
# the actual cited block is stronger evidence than this global reason.
|
|
context_limited = any(reason in reasons for reason in ['llm_context_budget','answer_candidate_budget','evidence_unit_limit'])
|
|
limited = context.get('complete') is not True or context.get('truncated') is True
|
|
limitations = [*reasons]
|
|
if clipped:
|
|
limitations.append('cited_source_text_clipped')
|
|
warnings = []
|
|
status = answer.get('status')
|
|
if status == 'ok':
|
|
coverage = 'supported'
|
|
if any(requirements.get(key) and key not in fields for fields in requirements.get('available_fields',{}).values()
|
|
for key in ['title','year','start_year']):
|
|
coverage='partial'; warnings.append('partial_requested_fields')
|
|
if requirements.get('facets'):
|
|
from .content_selection import missing_facets
|
|
if missing_facets(answer,context):
|
|
coverage='partial'; warnings.append('partial_requested_aspects')
|
|
if scope == 'list' and (limited or clipped):
|
|
coverage = 'partial'; warnings.append('list_may_be_incomplete')
|
|
elif scope == 'count' and (context.get('aggregation') or {}).get('complete') is not True:
|
|
coverage = 'unknown'; warnings.append('count_not_established')
|
|
elif scope == 'assignment' and clipped:
|
|
coverage = 'partial'; warnings.append('assignment_text_clipped')
|
|
elif scope == 'assignment' and limited and re.search(r'\b(?:vsetk\w*|upln\w*|kompletn\w*|cele)\b', normalized(context.get('question', ''))):
|
|
coverage = 'partial'; warnings.append('assignment_completeness_unknown')
|
|
elif status == 'no_answer':
|
|
coverage = 'not_found'
|
|
if clipped or context_limited:
|
|
warnings.append('missing_information_with_clipped_context')
|
|
else:
|
|
coverage = 'unavailable'
|
|
return {'scope': scope, 'status': coverage, 'warnings': warnings,
|
|
'limitations': list(dict.fromkeys(limitations))}
|
|
|
|
|
|
def coverage_notices(coverage: dict) -> list[str]:
|
|
messages = {
|
|
'list_may_be_incomplete': 'Uvedený zoznam nemusí obsahovať všetky záznamy.',
|
|
'assignment_text_clipped': 'Časť podkladov k zadaniu bola skrátená; uvedené body nemusia tvoriť celé zadanie.',
|
|
'assignment_completeness_unknown': 'Z dostupných podkladov nemožno zaručiť všetky body zadania.',
|
|
'missing_information_with_clipped_context': 'Časť podkladov bola pri príprave kontextu skrátená.',
|
|
'count_not_established': 'Celkový počet sa z dostupných podkladov nedá spoľahlivo určiť.',
|
|
}
|
|
notices=[messages[reason] for reason in coverage['warnings'] if reason in messages]
|
|
if 'list_may_be_incomplete' in coverage['warnings']:
|
|
notices.append('Výpis môžeš zúžiť uvedením osoby, typu práce alebo konkrétneho roku.')
|
|
return notices
|