Otázky a formulácie

This commit is contained in:
Ján Pták 2026-11-04 01:54:59 +01:00
parent 151eca3571
commit dd487d0c05
17 changed files with 244 additions and 19 deletions

View File

@ -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.3.0) a over správanie v novom chate.
novými ZIPmi z `packages/claude/dist/` (verzia 1.4.0) a over správanie v novom chate.
### Nahratie Skills do webového Claude
@ -1503,6 +1503,16 @@ známe rozkazovacie formulácie zadania menia na pomenovania cieľov, napríklad
pravidlá po kontrole doslovného dôkazu, nie o voľné dopĺňanie faktov modelom.
Neznámy opis, podmienky, negácie a údaje o dosiahnutom stave zostávajú zachované.
Známe gramatické úpravy sa uplatňujú iba na text mimo URL, kódu a HTML značiek.
Citované výňatky v dôkazoch sa nemenia. Konektor posiela aktuálnu otázku bez
parafrázy; môže odstrániť iba úvodný príkaz `/rag` alebo `/graphrag`. Odpoveď
z `/answer` nesie aj `query` a `engine`; klient odmietne nezhodu s požiadavkou.
Skills a inicializačné pokyny MCP vyžadujú porovnať otázku aj s aktuálnou správou
používateľa. Výber otázky samotným Claude zostáva potrebné overiť v jeho rozhraní.
Pri názve a roku práce model dostáva typované fakty a stručné zdroje. RAG tvrdenia
nepoužívajú grafové `record_id` ani `work_label`; každé tvrdenie musí mať doloženú
wiki URL. Označenie zdroja ako `S1` nenahrádza citáciu.
Pri obsahovom porovnaní viacerých študentov sa pre každého načíta požadovaný typ
práce z rovnakého SQLite snapshotu; všetky podklady naďalej podliehajú spoločnému
tokenovému rozpočtu. Zadanie a zaznamenaný stav sa kontrolujú a zobrazujú oddelene.

View File

@ -194,6 +194,7 @@ def verified_answer(request: AnswerRequest) -> dict:
if (result.get('accepted_for_delivery') is not True or
result.get('status') not in {'ok', 'no_answer', 'ambiguous'}):
result = public_failure(result)
result={**result,'query':request.query.strip(),'engine':request.engine}
if not request.include_diagnostics:
result.pop('attempts', None)
result.pop('retrieval_context', None)

View File

@ -6,6 +6,7 @@ from dataclasses import dataclass
import ipaddress
import json
import os
import re
from pathlib import Path
import stat
import urllib.error
@ -85,6 +86,8 @@ def error_result(reason):
'unauthorized':'Prístup nebol autorizovaný. Skontroluj prihlásenie konektora a jeho konfiguráciu.',
'forbidden':'Pre tento nástroj nemáš potrebné oprávnenie.',
'invalid_query':'Otázku sa nepodarilo spracovať. Zadaj konkrétnu otázku a primeraný počet výsledkov.',
'question_mismatch':'Odpoveď patrí k inej otázke a nezobrazí sa. Zopakuj aktuálnu otázku.',
'engine_mismatch':'Príkaz a zvolený nástroj sa nezhodujú. Použi nástroj podľa /rag alebo /graphrag.',
'model_unavailable':'Jazykový model momentálne nie je dostupný. Skús otázku neskôr.',
'answer_rejected':'Odpoveď sa nepodarilo spoľahlivo overiť podľa podkladov. '
'Spresni osobu, typ práce alebo rok; neoverený text sa nezobrazí.',
@ -146,8 +149,14 @@ def query_backend(query: str, engine: str, *, mode='answer', limit=5, config=Non
"""Return only approved answer fields, or explicitly marked retrieval context."""
try:
if (engine not in ENGINES or mode not in {'answer', 'retrieve'} or type(limit) is not int
or not 1 <= limit <= 20 or not isinstance(query, str) or not 1 <= len(query.strip()) <= 500):
or not 1 <= limit <= 20 or not isinstance(query, str)):
raise ClientError('invalid_query')
query=query.strip()
command=re.match(r'^/(rag|graphrag)(?=\s|$)',query,re.I)
if command:
if command[1].lower()!=engine:raise ClientError('engine_mismatch')
query=query[command.end():].strip()
if not 1 <= len(query) <= 500:raise ClientError('invalid_query')
config = config or Config.load()
endpoint = '/answer' if mode == 'answer' else '/' + engine
body = {'query': query.strip(), 'limit': limit}
@ -170,6 +179,10 @@ def query_backend(query: str, engine: str, *, mode='answer', limit=5, config=Non
validate_metadata(value)
fields = {'status', 'reason', 'index_version', 'complete', 'truncated', 'truncation_reasons'}
if mode == 'answer':
if not isinstance(value.get('query'),str):
raise ClientError('invalid_backend_response')
if value['query']!=body['query'] or value.get('engine')!=engine:
raise ClientError('question_mismatch')
if value.get('accepted_for_delivery') is not True:
raise ClientError('answer_rejected')
if (not isinstance(value.get('answer'), str) or not value['answer'].strip()
@ -184,7 +197,7 @@ def query_backend(query: str, engine: str, *, mode='answer', limit=5, config=Non
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'}
fields |= {'query','answer', 'source_urls', 'claims', 'count', 'accepted_for_delivery', 'answer_coverage'}
else:
if not isinstance(value.get('sources'), list):
raise ClientError('invalid_backend_response')

View File

@ -3,12 +3,12 @@
{
"archive": "zpwiki-rag.zip",
"name": "rag",
"sha256": "09d3e6b196b61f6c025eb86ccd1773b913b37660163a995ad1a2acfa6d947a0c"
"sha256": "0c4b1a65d7ec97e89505000c7f60239ae220098ba11dc8a6ae9fca8be396f151"
},
{
"archive": "zpwiki-graphrag.zip",
"name": "graphrag",
"sha256": "cfcb60213e103d40c2adbc6c7d4133751120148415836cfffa7935c020167e95"
"sha256": "cbfd609ec66c24a03e4c75b33e5a92438a1131c25019df8f2ea522bc917ba5e3"
}
],
"schema_version": 1

Binary file not shown.

Binary file not shown.

View File

@ -10,6 +10,15 @@ For `/graphrag <question>`, send the text after the leading `/graphrag` as the q
to `zpwiki_graphrag`. Preserve every condition. If no question follows, ask for one.
Use this graph tool rather than RAG when the user explicitly selects `/graphrag`.
For every call, copy the latest user question verbatim after removing only its leading
command. Do not reuse an earlier question or an example, paraphrase, silently correct a
name, add a year or drop a condition. Before calling, check names, work types, years,
negations and comparisons against the current message. Before presenting the result,
check that its `query` matches that question. A mismatch is not a usable answer.
If clarification is needed, ask specifically for the missing or ambiguous condition.
For multiple explicit commands in one message, handle each question separately in order;
keep all conditions belonging to each question together.
1. With a ZP Wiki MCP connector, call `zpwiki_graphrag` with the complete question.
2. Otherwise, with securely configured backend access in a Python execution environment,
run `python scripts/query.py "the complete question"` relative to this directory.

View File

@ -19,6 +19,15 @@ from starlette.responses import JSONResponse
# Host-model rules belong to MCP initialization and Skills, not tool-result data.
QUESTION_POLICY = (
'For each tool call, copy the latest user question verbatim after removing only its leading '
'/rag or /graphrag command. Keep names, work types, years, negations, comparisons and every condition. '
'Never reuse a previous question or an example, paraphrase it or split its conditions across searches. '
'If the user supplies multiple explicit commands, handle each exact question separately in order. '
'Before calling, compare the query with the current user message. Respect the explicitly selected engine. '
'The returned query is audit data: compare it with the current question before presenting its answer. '
'If they differ, do not present the mismatched answer. Ask for a concrete clarification when needed. '
)
ANSWER_PRESENTATION_POLICY = (
'For this question, present the current accepted answer unchanged, including its sources and caveats. '
'Do not add factual claims, guesses or commentary from earlier chat answers. '
@ -71,7 +80,7 @@ async def zpwiki_health() -> CallToolResult:
def create_server(**kwargs):
server = FastMCP('ZP Wiki', instructions=(
'Use zpwiki_rag for text and individual facts; zpwiki_graphrag for relationships and counts. '
'Tools return backend-verified answers. ' + ANSWER_PRESENTATION_POLICY), **kwargs)
'Tools return backend-verified answers. ' + QUESTION_POLICY + ANSWER_PRESENTATION_POLICY), **kwargs)
for handler in (zpwiki_rag, zpwiki_graphrag, zpwiki_health):
server.add_tool(handler, annotations=ToolAnnotations(
readOnlyHint=True, destructiveHint=False, openWorldHint=True))

View File

@ -10,6 +10,15 @@ For `/rag <question>`, send the text after the leading `/rag` as the question to
`zpwiki_rag`. Preserve every condition. If no question follows, ask for one.
Use this text-search tool rather than GraphRAG when the user explicitly selects `/rag`.
For every call, copy the latest user question verbatim after removing only its leading
command. Do not reuse an earlier question or an example, paraphrase, silently correct a
name, add a year or drop a condition. Before calling, check names, work types, years,
negations and comparisons against the current message. Before presenting the result,
check that its `query` matches that question. A mismatch is not a usable answer.
If clarification is needed, ask specifically for the missing or ambiguous condition.
For multiple explicit commands in one message, handle each question separately in order;
keep all conditions belonging to each question together.
1. If a ZP Wiki MCP connector is available, call `zpwiki_rag` with the complete question.
2. Otherwise, in a Python execution environment with securely configured backend access, run
`python scripts/query.py "the complete question"` relative to this skill's directory.

View File

@ -11,7 +11,7 @@ import zipfile
ROOT = Path(__file__).resolve().parents[1]
BASE = ROOT / 'packages/claude'
ENGINES = ('rag', 'graphrag')
PACKAGE_VERSION = '1.3.0'
PACKAGE_VERSION = '1.4.0'
def json_bytes(value):

View File

@ -81,6 +81,37 @@ def test_progress_negations_and_unknown_prose_are_not_reinterpreted(text):
assert text in rendered and 'tieto ciele' not in rendered
@pytest.mark.parametrize('command',['vyhodnoťte','vyhodnotte','vyhodnnotte'])
def test_known_coordinated_command_variants_are_natural_without_claiming_completion(command):
raw='Preštudujte si spôsoby vyhodnotenia RAG - RAGAS, DeepEval a '+command+' systém.'
text,converted=goal_text(raw)
assert converted and text=='Preštudovať si spôsoby vyhodnotenia RAG - RAGAS, DeepEval a vyhodnotiť systém.'
assert 'vyhodnotený' not in text and 'dokončený' not in text
def test_goal_formatting_protects_urls_code_attributes_and_conditions():
raw=('Vypracujte prehľad iba pre slovenské texty a vyhodnnotte systém podľa '
'https://example.test/implementujte/vyhodnnotte?x=navrhnite '
's kódom `implementujte("vyhodnnotte")` a <a href="/vyhodnnotte">odkazom</a>.')
text,converted=goal_text(raw)
assert converted and 'Vypracovať prehľad iba pre slovenské texty a vyhodnotiť systém' in text
assert 'https://example.test/implementujte/vyhodnnotte?x=navrhnite' in text
assert '`implementujte("vyhodnnotte")`' in text and '<a href="/vyhodnnotte">' in text
@pytest.mark.parametrize('complete',[True,False])
def test_shared_document_authors_have_a_grammatical_bounded_summary(complete):
people=['Eva Nová','Boris Nový'];urls=['https://example.test/eva','https://example.test/boris']
claims=[{'person':person,'document_author':'Daniel Hladek'} for person in people]
c={'status':'ok','facts':[{**claim,'source_url':url} for claim,url in zip(claims,urls)],
'sources':[{'source_url':url} for url in urls], 'complete':complete,'truncated':not complete,
'shared_properties':[{'field':'document_author','value':'Daniel Hladek','people':people}]}
rendered=render_graph_answer({'status':'ok','claims':claims,'source_urls':urls},c)
prefix='Spoloční autori dokumentov:' if complete else 'Doložení spoloční autori dokumentov:'
assert rendered['answer'].startswith(prefix)
assert answer_context_issues(rendered,c)==[]
def test_assignment_heading_does_not_keep_imperative_style():
lines = content_lines(['*Zadanie diplomovej práce*:', '1. Vypracujte prehľad metód.',
'2. Implementujte systém.'], 'Ján Holp', 'diploma_thesis')

View File

@ -74,10 +74,13 @@ def test_goal_headings_are_not_extra_evidence_and_sentences_keep_capitals():
assert 'Ďalšie podklady' not in text and 'Zlepšiť proces. Zlepšiť extrakciu.' in text
@pytest.mark.parametrize('literal', ['Použite https://example.test/pouzite/vypracujte.',
'Implementujte parser pre `navrhnite a implementujte`.'])
def test_grammar_does_not_change_urls_or_code_literals(literal):
assert goal_text(literal)==(literal,False)
@pytest.mark.parametrize('literal,expected', [
('Použite https://example.test/pouzite/vypracujte.',
'Použiť https://example.test/pouzite/vypracujte.'),
('Implementujte parser pre `navrhnite a implementujte`.',
'Implementovať parser pre `navrhnite a implementujte`.')])
def test_grammar_does_not_change_urls_or_code_literals(literal,expected):
assert goal_text(literal)==(expected,True)
@pytest.fixture

View File

@ -0,0 +1,81 @@
"""Question/engine binding across actual HTTP/MCP boundaries, without an LLM."""
import asyncio
import json
import os
import sys
import pytest
from test_claude_packages import backend, ROOT
from packages.claude import client
QUESTIONS=[
'Čomu sa venuje diplomová práca Jána Holpa? Stručne ju predstav a uveď zdroj.',
'Porovnaj zadanie a výsledky Ondreja Megelu a Jána Holpa.\nPlán nie je výsledok; uveď zdroje.',
'Ktorí študenti majú tag "RAG" a zároveň diplomový projekt z roku 2026?']
@pytest.mark.parametrize('engine',['rag','graphrag'])
def test_latest_question_and_all_conditions_cross_http_unchanged(backend,engine):
for question in QUESTIONS:
r=client.query_backend(question,engine,limit=7)
assert r['accepted_for_delivery'] and r['query']==question and r['engine']==engine
path,body,_=backend['requests'][-1]
assert path=='/answer' and body['query']==question and body['engine']==engine and body['limit']==7
assert len(backend['requests'])==len(QUESTIONS)
@pytest.mark.parametrize('engine',['rag','graphrag'])
def test_only_a_matching_leading_command_is_removed(backend,engine):
question='Aké práce má Ján Holp v roku 2020? Zachovaj výraz "RAG".'
r=client.query_backend(' /'+engine.upper()+' '+question+' ',engine)
assert r['accepted_for_delivery'] and r['query']==question
assert backend['requests'][-1][1]['query']==question
other='graphrag' if engine=='rag' else 'rag'
r=client.query_backend('/'+other+' '+question,engine)
assert r['reason']=='engine_mismatch' and not r['accepted_for_delivery']
assert len(backend['requests'])==1
@pytest.mark.parametrize('question',['/rag','/rag ','/graphrag'])
def test_command_without_a_question_never_reaches_backend(backend,question):
engine='graphrag' if question.startswith('/graphrag') else 'rag'
assert client.query_backend(question,engine)['reason']=='invalid_query'
assert not backend['requests']
@pytest.mark.parametrize('change',[
{'query':'A previous unrelated question','engine':'rag'},
{'query':QUESTIONS[0],'engine':'graphrag'},
])
def test_stale_question_or_wrong_engine_cannot_deliver_another_answer(backend,change):
backend['raw']=json.dumps({**backend['checked'],'answer':'OTHER_ANSWER_SHOULD_NOT_ESCAPE',**change}).encode()
r=client.query_backend(QUESTIONS[0],'rag')
assert r['reason']=='question_mismatch' and not r['accepted_for_delivery']
assert 'OTHER_ANSWER' not in json.dumps(r)
def test_missing_question_echo_is_not_accepted(backend):
backend['raw']=json.dumps({**backend['checked'],'engine':'rag'}).encode()
r=client.query_backend(QUESTIONS[0],'rag')
assert r['reason']=='invalid_backend_response' and not r['accepted_for_delivery']
def test_consecutive_mcp_calls_do_not_reuse_previous_queries(backend):
from mcp import ClientSession,StdioServerParameters
from mcp.client.stdio import stdio_client
from packages.claude.mcp_server import QUESTION_POLICY
async def run():
params=StdioServerParameters(command=sys.executable,args=[str(ROOT/'packages/claude/mcp_server.py')],
env={**os.environ,'PYTHONDONTWRITEBYTECODE':'1'})
async with stdio_client(params) as (read,write):
async with ClientSession(read,write) as session:
initialized=await session.initialize()
assert QUESTION_POLICY in initialized.instructions
for engine,question in [('rag',QUESTIONS[0]),('graphrag',QUESTIONS[2]),('rag',QUESTIONS[1])]:
result=await session.call_tool('zpwiki_'+engine,{'query':question})
assert not result.isError and result.structuredContent['query']==question
assert backend['requests'][-1][1]['query']==question
assert backend['requests'][-1][1]['engine']==engine
asyncio.run(run())

View File

@ -109,6 +109,29 @@ def test_bad_answer_is_repaired_once_and_only_verified_fields_are_rendered(paylo
assert 'unsupported_person_work_type_year_binding' in result['attempts'][0]['issues']
def test_scalar_claims_require_real_source_urls_without_graph_record_ids(payload,answer):
empty=copy.deepcopy(answer);empty['source_urls']=[]
outputs=iter([empty,answer]);calls=[]
def complete(messages):
calls.append(copy.deepcopy(messages))
assert 'source_urls pri kladnej odpovedi nesmie byť prázdne' in messages[0]['content']
sent=json.loads(messages[1]['content'])['context']
assert all('text' not in row for row in sent['sources'])
return completion(next(outputs))
r=generate_verified_rag(QUESTION,payload,complete)
assert r['accepted_for_delivery'] and len(calls)==2
assert 'missing_required_citations' in r['attempts'][0]['issues']
assert 'skutočnú wiki URL' in calls[1][-1]['content']
assert r['source_urls']==answer['source_urls']
def test_rag_claim_schema_does_not_accept_a_source_id_as_a_graph_record(payload,answer):
wrong=copy.deepcopy(answer);wrong['claims'][0]['record_id']='S1'
r=generate_verified_rag(QUESTION,payload,lambda _:completion(wrong))
assert not r['accepted_for_delivery']
assert all(attempt['issues']==['invalid_answer_format'] for attempt in r['attempts'])
def test_two_bad_attempts_never_return_unverified_model_text(payload, answer):
answer['claims'][0]['year'] = 2021
calls = []

View File

@ -107,15 +107,29 @@ GOAL_VERBS = {
'natrénujte': 'Natrénovať', 'optimalizujte': 'Optimalizovať',
'identifikujte': 'Identifikovať', 'preštudujte': 'Preštudovať',
'pripravte': 'Pripraviť',
'vyhodnotte': 'Vyhodnotiť', 'vyhodnnotte': 'Vyhodnotiť',
}
def goal_text(unit: str) -> tuple[str, bool]:
"""Format only known goal verbs, keeping URLs and code byte-for-byte intact."""
prefix='_ZP_LITERAL_'
while prefix in unit:prefix+='X'
literals={}
def protect(value):
key=prefix+str(len(literals))+'_';literals[key]=value
return key
text=re.sub(r'(`+)([\s\S]*?)\1',lambda m:protect(m[0]),unit)
text=re.sub(r'<[^>]+>',lambda m:protect(m[0]),text)
for url in sorted(text_urls(text),key=len,reverse=True):
text=text.replace(url,protect(url))
result,is_goal=_goal_text(text)
for key,value in literals.items():result=result.replace(key,value)
return result,is_goal
def _goal_text(unit: str) -> tuple[str, bool]:
"""Only bounded grammatical substitutions; unknown prose stays verbatim."""
if text_urls(unit) or '`' in unit:
# A word inside a URL/code literal must never be conjugated. Preserve
# technical excerpts exactly rather than changing their references.
return unit.strip(), False
text = re.sub(r'^\s*(?:\d+[.)]|[-+*])\s+', '', unit.strip())
for original, replacement in GOAL_NOUNS:
if re.match(re.escape(original) + r'\b', text, re.I):

View File

@ -19,7 +19,9 @@ def render_graph_answer(answer: dict, context: dict) -> dict:
for person in row['people'])})
if values:
shared_fields.add(field)
prefix='Spoločné ' if context.get('complete') is True and not context.get('truncated') else 'Doložené spoločné '
full=context.get('complete') is True and not context.get('truncated')
prefix=(('Spoloční ' if full else 'Doložení spoloční ') if field=='document_author' else
('Spoločné ' if full else 'Doložené spoločné '))
lines.append(prefix+label+': '+', '.join('**'+v+'**' for v in values)+'.')
for row in context.get('shared_properties',[]):
if row['field']==field and row['value'] in values:

View File

@ -8,7 +8,6 @@ from typing import Literal
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 descriptive_text, rag_evidence_context, quote_sections, quote_matches_passage
from .answer_coverage import answer_coverage, coverage_notices
@ -19,7 +18,16 @@ from .evidence_selection import (resolve_selection, EvidenceSelectionError,
missing_required_evidence, overview_units)
class RagClaim(Claim):
class RagClaim(BaseModel):
# RAG facts carry source_id, not the graph's record_id/work_label fields.
model_config = ConfigDict(extra='forbid', strict=True)
person: str
title: str | None = None
year: int | None = None
start_year: int | None = None
keyword: str | None = None
category: str | None = None
document_author: str | None = None
work_type: Literal['bachelor_thesis', 'diploma_thesis', 'doctoral_thesis',
'bachelor_project', 'diploma_project', 'team_project', 'research_project'] | None = None
@ -503,6 +511,9 @@ def generate_verified_rag(question: str, payload: dict, complete) -> dict:
sent['content_passages'] = context['content_passages']
elif not context['requirements']['academic']:
sent['context'] = payload.get('context', '')
elif context['requirements']['response_mode']=='claims':
sent['sources']=[{key:source[key] for key in ['source_id','source_url','title','document_path'] if key in source}
for source in context['sources']]
if context['requirements']['response_mode'] == 'quotes':
sent.pop('facts')
if 'content_candidates' in context:
@ -528,6 +539,12 @@ def generate_verified_rag(question: str, payload: dict, complete) -> dict:
'Aplikácia konkrétne pomenuje chýbajúce informácie. no_answer tu nesmie odmietnuť '
'aj dostupnú časť. Plánovaný experiment alebo vyhodnotenie označené phase=planned '
'odpovedá na otázku, čo mali pripraviť; nie je to dosiahnutý výsledok.')
elif context['requirements']['response_mode']=='claims':
selection_policy=('\nPre tento dotaz je response_mode=claims: vyber požadované údaje z facts. '
'Každý claim MUSÍ mať citáciu v source_urls: použi source_url príslušného faktu alebo zdroja. '
'source_urls pri kladnej odpovedi nesmie byť prázdne. evidence_quotes=[] a evidence_ids=[]. '
'source_id ako S1 je len označenie podkladu; nevkladaj ho do claims. '
'Polia record_id a work_label patria iba grafovému nástroju, tu sa nepoužívajú.')
messages = [{'role': 'system', 'content': PROMPT + selection_policy + '\nSchéma: ' + json.dumps(RagAnswer.model_json_schema(), ensure_ascii=False)},
{'role': 'user', 'content': json.dumps({'question': question, 'context': sent}, ensure_ascii=False)}]
attempts = []
@ -577,7 +594,10 @@ def generate_verified_rag(question: str, payload: dict, complete) -> dict:
if context['requirements'].get('content') else '')
+ (' Použi evidence_ids z content_candidates a evidence_quotes=[]. Vyber všetky required=true; '
'doslovné texty aj ich citácie dosadí aplikácia. Pri response_mode=quotes musí byť claims=[].'
if 'content_candidates' in context else '')}]
if 'content_candidates' in context else '')
+ (' Ku každému claimu uveď vo source_urls jeho skutočnú wiki URL zo sources/facts. '
'Prázdne source_urls ani source_id=S1 nie sú citácia. Do claims nepridávaj record_id ani work_label.'
if context['requirements']['response_mode']=='claims' 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']}