From 8e775aa20baae6e3521df09f50a984d4acf60ff3 Mon Sep 17 00:00:00 2001 From: jp170na Date: Mon, 2 Nov 2026 21:06:25 +0100 Subject: [PATCH] =?UTF-8?q?Presnej=C5=A1ie=20RAG=20odpovede?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 19 +- packages/claude/client.py | 9 +- packages/claude/dist/packages.json | 4 +- packages/claude/dist/zpwiki-graphrag.zip | Bin 6101 -> 6276 bytes packages/claude/dist/zpwiki-rag.zip | Bin 6046 -> 6353 bytes packages/claude/mcp_server.py | 4 + packages/claude/rag/SKILL.md | 6 +- scripts/build_claude_packages.py | 2 +- test/test_claude_packages.py | 19 ++ test/test_rag_answer_coverage.py | 198 +++++++++++++++++++++ test/test_rag_work_content.py | 3 +- test/test_verified_rag.py | 3 +- tools/answer_evaluation/answer_coverage.py | 77 ++++++++ tools/answer_evaluation/rag_evidence.py | 2 + tools/answer_evaluation/verified_rag.py | 74 +++++++- tools/rag/rag_utils.py | 8 +- 16 files changed, 409 insertions(+), 19 deletions(-) create mode 100644 test/test_rag_answer_coverage.py create mode 100644 tools/answer_evaluation/answer_coverage.py diff --git a/README.md b/README.md index 1c4a883..8281a51 100644 --- a/README.md +++ b/README.md @@ -1160,7 +1160,7 @@ upozornenie na neúplnosť, nezamieňať autora dokumentu s autorom práce a pri sa nevysvetľujú odhadom; verzia dát neoznačuje verziu programu. Tieto pokyny ovplyvňujú správanie Claude, jeho finálny text však náš backend nemôže programovo vynútiť. Po aktualizácii už nahratých Skills nahraď ich -novými ZIPmi z `packages/claude/dist/` (verzia 1.1.0) a over správanie v novom chate. +novými ZIPmi z `packages/claude/dist/` (verzia 1.2.0) a over správanie v novom chate. ### Nahratie Skills do webového Claude @@ -1475,6 +1475,23 @@ staré ploché cesty `data/nazov.json` sa presmerujú do príslušnej oblasti. ### Širšie overenie kontrolovaného RAG +`complete`, `truncated` a `truncation_reasons` zostávajú technickým opisom +vyhľadávania. Overená odpoveď navyše obsahuje `answer_coverage` so scope, status, +warnings a limitations. Status `supported` znamená doložené tvrdenia/výňatky, +nie úplné prehľadanie korpusu. Bežný výber kandidátov a zdrojov preto nepridáva +automatické upozornenie k názvu, roku alebo krátkemu opisu. Obmedzený zoznam +záznamov a orezané citované zadanie dostanú konkrétne upozornenie. Pri nenájdenej +informácii sa zachová opatrné tvrdenie o dostupných podkladoch. Žiadny limit sa +kvôli tomu nezvyšuje ani sa neprepisuje na `complete=true`. + +Otázka na stručný obsah pri zobrazení vyberá najviac dva krátke overené výňatky +na osobu (do 600 znakov a dvoch bodov na osobu), bez voľnej modelovej parafrázy. +Skrátenie robí renderer z už overených citátov; dlhší platný citát nie je dôvod +na odmietnutie faktov ani ďalšie modelové volanie. Pri neoddeliteľnom technickom +reťazci má zachovanie dôkazu prednosť pred limitom stručnosti. Otázka na zadanie +môže zobraziť všetky doložené body zadania. Citácie a kontroly ich doslovnosti +sa uplatňujú v oboch režimoch. + Otázky na obsah, zadanie, ciele alebo metódy práce používajú textové výňatky z príslušnej akademickej sekcie, nie iba metadáta o názve a roku. Výňatok musí patriť požadovanej osobe, typu práce a prípadne roku. Názov, rok ani samotný diff --git a/packages/claude/client.py b/packages/claude/client.py index f334712..bf659af 100644 --- a/packages/claude/client.py +++ b/packages/claude/client.py @@ -163,7 +163,14 @@ def query_backend(query: str, engine: str, *, mode='answer', limit=5, config=Non or not all(isinstance(x, str) for x in value['source_urls']) or not isinstance(value.get('claims'), list)): raise ClientError('invalid_backend_response') - fields |= {'answer', 'source_urls', 'claims', 'count', 'accepted_for_delivery'} + if 'answer_coverage' in value: + coverage = value['answer_coverage'] + if (not isinstance(coverage, dict) or coverage.get('scope') not in ('fact','overview','assignment','list','count') + or coverage.get('status') not in ('supported','partial','unknown','not_found','unavailable') + or any(not isinstance(coverage.get(key), list) or + any(not isinstance(item, str) for item in coverage[key]) for key in ['warnings','limitations'])): + raise ClientError('invalid_backend_response') + fields |= {'answer', 'source_urls', 'claims', 'count', 'accepted_for_delivery', 'answer_coverage'} else: if not isinstance(value.get('sources'), list): raise ClientError('invalid_backend_response') diff --git a/packages/claude/dist/packages.json b/packages/claude/dist/packages.json index a60a0b7..bd17241 100644 --- a/packages/claude/dist/packages.json +++ b/packages/claude/dist/packages.json @@ -3,12 +3,12 @@ { "archive": "zpwiki-rag.zip", "name": "rag", - "sha256": "3382847549d88c6b3c25dc29cc95c9d8ceb8df1b3558680e772374a06b1a04c8" + "sha256": "8f0c51e27dc68a1e3ee845f2cfe5987e51714d4408f98c7d275861dc1e22953f" }, { "archive": "zpwiki-graphrag.zip", "name": "graphrag", - "sha256": "f1090fa4e22fb1cd4ebe3cc1a5c9b0c7d8fb7a57a8057713223007060df7f59a" + "sha256": "aa65a97211f48fa0352a98e50c6ca6a243e1d23105c0ff144c7f360ad192c376" } ], "schema_version": 1 diff --git a/packages/claude/dist/zpwiki-graphrag.zip b/packages/claude/dist/zpwiki-graphrag.zip index 4d1fb91c4181996f7c09bff0227ed45af2d6ba6e..4dbee2f49133cf8de9b2c897cc2c9b86f014adff 100644 GIT binary patch delta 3812 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 aE0anWSOQBHlK}}6leZTb23!{a0000*i8b*6 delta 3637 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 Sk^>QOQaA}NayQ9;{Qm+eaxf?W delta 5574 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 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 = 'llm_context_budget' in reasons + 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 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ť.', + } + return [messages[reason] for reason in coverage['warnings']] diff --git a/tools/answer_evaluation/rag_evidence.py b/tools/answer_evaluation/rag_evidence.py index fbeee5d..4db2e63 100644 --- a/tools/answer_evaluation/rag_evidence.py +++ b/tools/answer_evaluation/rag_evidence.py @@ -313,6 +313,8 @@ def rag_evidence_context(question: str, payload: dict) -> dict: 'start_year': start_year, 'response_mode': 'claims' if academic or start_year else 'quotes'} if content: + from .answer_coverage import response_scope + requirements['content_style'] = response_scope(question, content=True) requirements['response_mode'] = 'mixed' if any(requirements[key] for key in ['title', 'year', 'start_year']) else 'quotes' stated_years = re.findall(r'\b(?:v roku|z roku|pre rok|na rok|roku|rok)\s+(\d+)\b', query) if academic else [] requirements['years'] = ([int(value) for value in stated_years if len(value) == 4] diff --git a/tools/answer_evaluation/verified_rag.py b/tools/answer_evaluation/verified_rag.py index f3af2d7..6dde0b7 100644 --- a/tools/answer_evaluation/verified_rag.py +++ b/tools/answer_evaluation/verified_rag.py @@ -10,6 +10,7 @@ from pydantic import BaseModel, ConfigDict, Field from .answer_format import Claim from .context_policy import answer_context_issues, citation_check, allowed_urls, normalized, text_urls from .rag_evidence import WORK_LABELS, descriptive_text, rag_evidence_context +from .answer_coverage import answer_coverage, coverage_notices class RagClaim(Claim): @@ -50,6 +51,10 @@ Vyber opisujúce evidence_quotes z content_passages zodpovedajúcej osoby, typu Samotný názov, rok, meno vedúceho ani nadpis zadania nenahrádzajú opis. Pri response_mode=mixed uveď požadované claims aj opisujúce evidence_quotes. Použi iba dodané content_passages; opis inej práce alebo iného roku nie je odpoveď. +Pri requirements.content_style=overview vyber najviac DVA krátke doslovné výňatky +na požadovanú osobu, najviac 600 znakov a dve odrážky/body na osobu. Odpovedaj stručne na obsah; +neprepisuj celé zadanie a históriu stretnutí. Pri content_style=assignment zachovaj +požadované body zadania; tento stručný limit sa na zadanie nevzťahuje. Každý výňatok musí byť JEDEN súvislý úsek z konkrétneho zdroja vrátane Markdownu. Nespájaj oddelené pasáže cez vynechané riadky; použi viac samostatných kratších quotes. Vyber iba minimum potrebné na zodpovedanie otázky. Výňatok nesmieš parafrázovať ani meniť čísla. @@ -218,6 +223,49 @@ def rag_answer_issues(answer: dict, context: dict) -> list[str]: return list(dict.fromkeys(issues)) +def overview_excerpts(answer: dict, context: dict) -> list[str]: + """Shorten validated quotes by selection, never by factual paraphrase. + + Presentation length is not a reason to reject otherwise supported evidence + or to spend another model request rewriting the same facts. + """ + selected = []; counts = {}; sizes = {}; seen = set() + for quote in answer.get('evidence_quotes', []): + sid = quote['source_id'] + owner = next((p.get('person') for p in context.get('content_passages', []) + if p['source_id'] == sid and p.get('person')), sid) + identities = [row.get(key) for row in context.get('facts', []) if row.get('source_id') == sid + for key in ('person', 'title', 'document_author')] + blocks = re.split(r'\n\s*\n|(?=^[ \t]*(?:\d+[.)]|[-+*])\s+)', quote['quote'], flags=re.M) + for block in blocks: + if counts.get(owner, 0) >= 2: + break + block = block.strip() + if not descriptive_text(block, identities): + continue + # Prefer complete sentences when a prose paragraph is very long. + units = re.split(r'(?<=[.!?])\s+(?=[A-ZÁČĎÉÍĽŇÓŠŤÚÝŽ])', block) if len(block) > 600 else [block] + for unit in units: + remaining = 600 - sizes.get(owner, 0) + if counts.get(owner, 0) >= 2 or remaining < 30: + break + unit = unit.strip() + if not descriptive_text(unit, identities) or (owner, unit) in seen: + continue + if len(unit) > remaining: + # An explicit ellipsis makes a shortened literal prefix + # visible; never cut a word/URL and pretend it is complete. + prefix = unit[:remaining - 2].rsplit(' ', 1)[0].rstrip() + if not prefix or ' ' not in prefix or not descriptive_text(prefix, identities): + continue + unit = prefix + ' …' + selected.append(unit); seen.add((owner, unit)) + counts[owner] = counts.get(owner, 0) + 1 + sizes[owner] = sizes.get(owner, 0) + len(unit) + # An indivisible technical string is better retained than silently omitted. + return selected or [q['quote'] for q in answer.get('evidence_quotes', [])] + + def render_rag_answer(answer: dict, context: dict) -> str: lines = [] if answer['status'] == 'ok': @@ -227,9 +275,18 @@ def render_rag_answer(answer: dict, context: dict) -> str: values = [WORK_LABELS[claim['work_type']]] if claim.get('work_type') else [] values += [f'{label}: {claim[key]}' for key, label in labels.items() if claim.get(key) is not None] lines.append(claim['person'] + ' — ' + '; '.join(values) + '.') - sources = {row['source_id']: row for row in context['source_blocks']} - lines += ['Zo zdroja ' + str(sources[quote['source_id']].get('title') or quote['source_id']) + - ': „' + quote['quote'] + '“' for quote in answer.get('evidence_quotes', [])] + quotes = answer.get('evidence_quotes', []) + style = context.get('requirements', {}).get('content_style') + if quotes and style in {'overview', 'assignment'}: + if style == 'overview': + lines.append('Stručne z dostupných podkladov:') + lines += overview_excerpts(answer, context) + else: + lines += [quote['quote'] for quote in quotes] + else: + sources = {row['source_id']: row for row in context['source_blocks']} + lines += ['Zo zdroja ' + str(sources[quote['source_id']].get('title') or quote['source_id']) + + ': „' + quote['quote'] + '“' for quote in quotes] if answer.get('count') is not None: lines.append(f'Počet: {answer["count"]}.') elif answer['status'] == 'no_answer': @@ -238,10 +295,7 @@ def render_rag_answer(answer: dict, context: dict) -> str: lines.append('Údaje sú nejednoznačné; požiadavku treba spresniť.') else: lines.append('Overenú odpoveď sa nepodarilo pripraviť.') - if context.get('truncated') or context.get('complete') is False: - lines.append('Výpis je neúplný.') - elif context.get('complete') is None: - lines.append('Úplnosť vyhľadania nemožno zaručiť.') + lines.extend(coverage_notices(answer_coverage(answer, context))) if answer.get('source_urls'): lines.append('Zdroje: ' + ', '.join(answer['source_urls'])) return '\n'.join(lines) @@ -254,6 +308,7 @@ def generate_verified_rag(question: str, payload: dict, complete) -> dict: empty = {'status': status, 'claims': [], 'evidence_quotes': [], 'source_urls': []} return {'status': status, 'reason': payload.get('reason'), 'accepted_for_delivery': status != 'error', 'answer': render_rag_answer(empty, context), 'source_urls': [], 'claims': [], + 'answer_coverage': answer_coverage(empty, context), 'attempts': [], 'index_version': context['index_version'], 'complete': context['complete'], 'truncated': context['truncated'], 'truncation_reasons': context['truncation_reasons']} sent = {key: context[key] for key in ['status', 'reason', 'complete', 'truncated', @@ -287,6 +342,7 @@ def generate_verified_rag(question: str, payload: dict, complete) -> dict: text = render_rag_answer(answer, context) return {'status': answer['status'], 'reason': None, 'accepted_for_delivery': True, 'answer': text, 'source_urls': answer['source_urls'], 'claims': answer['claims'], + 'answer_coverage': answer_coverage(answer, context), 'attempts': attempts, 'index_version': context['index_version'], 'complete': context['complete'], 'truncated': context['truncated'], 'truncation_reasons': context['truncation_reasons']} @@ -299,7 +355,9 @@ def generate_verified_rag(question: str, payload: dict, complete) -> dict: 'Nepotrebné citáty a metadáta vynechaj.' if context['requirements']['response_mode'] == 'quotes' else '') + (' Opis práce musí obsahovať doslovné evidence_quotes z content_passages. ' 'Samotný názov a rok nestačia; pri mixed zachovaj aj požadované claims.' - if context['requirements'].get('content') else '')}] + if context['requirements'].get('content') else '') + + (' Pri overview vyber najviac dva krátke výňatky, najviac 600 znakov a dva body na požadovanú osobu.' + if context['requirements'].get('content_style') == 'overview' else '')}] return {'status': 'error', 'reason': 'answer_rejected', 'accepted_for_delivery': False, 'answer': 'Odpoveď neprešla kontrolou podkladov; neoverené údaje sa nezobrazia.', 'attempts': attempts, 'index_version': context['index_version']} diff --git a/tools/rag/rag_utils.py b/tools/rag/rag_utils.py index bc96356..929a110 100644 --- a/tools/rag/rag_utils.py +++ b/tools/rag/rag_utils.py @@ -29,9 +29,11 @@ NO_ANSWER_TEXT = ( RAG_INSTRUCTIONS = [ - 'Rešpektuj complete, truncated, truncation_reasons a index_version. Pri truncated=true alebo ' - 'complete=false v odpovedi jasne uveď, že výpis je neúplný. Pri complete=null nemožno zaručiť ' - 'úplnosť; neoznačuj výber za všetky práce alebo úplný zoznam. Cituj iba URL zo sources.', + 'Rešpektuj complete, truncated, truncation_reasons a index_version ako rozsah vyhľadania. ' + 'Výber najlepších kandidátov a zdrojov sám osebe nezneplatňuje doložený fakt ani opis. ' + 'Pri požiadavke na zoznam upozorni, ak nemožno zaručiť všetky záznamy. Pri orezanom ' + 'texte a chýbajúcej informácii pomenuj konkrétne obmedzenie. Nikdy neoznačuj neúplný ' + 'alebo neistý výber za úplný zoznam. Cituj iba URL zo sources.', ( "Odpovedaj výhradne podľa informácií vrátených " "v poli context a sources zo ZP Wiki."