Report JSON types and guide malformed investigation finding repair

This commit is contained in:
Nico committed 2026-10-18 23:30:33 +02:00
1 parent d957974498
commit 67843454e4
9 files changed
+187 -3

No files matched your search

+27
View File
@@ -23,3 +23,30 @@ Datenschutz und den Weg vom Gateway über das Paket bis zur Wiederaufnahme und
erfolgreichen Korrektur mit einem simulierten Anbieter. Es werden keine
kostenpflichtigen Anfragen ausgeführt. Historisch nicht gespeicherte Fehlerpfade
lassen sich damit nicht nachträglich rekonstruieren.
## Präzisierung nach dem Source-/Schema-Test
Der folgende echte Test zeigte dreimal denselben Typfehler an
`/update/working_brief/findings/0`. Die zuvor erfolgreichen Bootstrap-Antworten
enthielten leere Beleglisten. Ein Erfolg dieser Antworten prüfte daher nicht die
Struktur ausgefüllter Belegobjekte. Der Transport prüft weiterhin nur die äußere
Form nativ; das vollständige lokale Schema bleibt maßgeblich.
Typfehler enthalten jetzt zusätzlich `expected_types` und `actual_type`, ausschließlich
aus den sieben JSON-Schematypen. Boolesche Werte werden nicht als Integer gemeldet.
Es werden weder Antwortwerte noch Python-Klassennamen übertragen. Diese Angaben
bleiben im Fehlerprotokoll und im wiederaufnehmbaren Korrekturkontext erhalten.
Bei einem fehlerhaften Belegelement mit erwartetem Typ `object` erhält nur der
Korrekturaufruf ein kurzes englisches Objektbeispiel mit den benötigten Feldern.
Die Beispielwerte sind ausdrücklich illustrativ: IDs, Text und Geltungsbereich
müssen aus den tatsächlich sichtbaren Belegen stammen. Komprimierte Datensätze,
Tupel und Zeichenketten sind keine zulässigen Belegobjekte; bestehende Erkenntnisse
dürfen nicht zum Umgehen der Prüfung gelöscht werden. Der Controller konvertiert
keine Modellantwort automatisch und startet keine zusätzliche Recherche.
Unveränderte Erstaufrufe und historische Korrekturkontexte ohne diese neuen
Typangaben erhalten keinen zusätzlichen Prompttext. Frühere Prompts bleiben so
reproduzierbar. Die lokal simulierte Paketwiederaufnahme prüft denselben Fehlerpfad
wie der echte Lauf und anschließend einen korrigierten Aufruf. Dies ersetzt keinen
Erfolgsnachweis mit dem echten Modell.
@@ -88,6 +88,8 @@ def build_prompt(context):
guide = instructions(context)
if guide and HEADER not in request['messages'][0]['content']:
request['messages'][0]['content'] += guide
from .tool_guidance import finding_repair_instructions
request['messages'][0]['content'] += finding_repair_instructions(context)
return request
@@ -619,6 +619,7 @@ class Investigator:
'anyOf':'match_an_allowed_shape',
}.get(info.get('validation_keyword'),'match_schema_at_validation_path')
if context.get('bootstrap_json_framing'):
context['previous_rejection_details'].update({k:info[k] for k in ('expected_types','actual_type') if k in info})
context['previous_rejection_details']['requirement']={
'type':'match_expected_type_at_validation_path',
'enum':'use_allowed_value_at_validation_path',
@@ -8,6 +8,20 @@ FILTER_REQUIREMENT = ('filters must be a nonempty array of objects: '
'use verified values, never a flat list.')
def finding_repair_instructions(context):
detail=context.get('previous_rejection_details',{})
if (not context.get('bootstrap_json_framing') or detail.get('expected_types')!=['object']
or not re.fullmatch(r'/update/working_brief/findings/[0-9]+',detail.get('validation_path',''))):
return ''
return ('\nFinding type repair: each findings element must be a JSON OBJECT, not a string, '
'array, tuple or encoded record. Shape example: '
'{"id":"B0001","direction":"neutral","text":"Scoped observation",'
'"scope":"Examined snapshot","evidence_refs":["E000001"]}. '
'These are illustrative values: preserve existing B IDs and use only actual visible E IDs '
'and evidence-grounded text/scope. Do not copy invented evidence or erase findings to bypass validation. '
'No $record/$table encoding in output. Repair the response without repeating research.\n')
def filter_feedback(details):
result = copy.deepcopy(details)
if re.fullmatch(r'/next/arguments/filters(?:/[0-9]+)?', result.get('validation_path', '')):
+19
View File
@@ -4,6 +4,16 @@ COUNTS = {'input_tokens','output_tokens','reasoning_tokens','cached_input_tokens
'cache_created_tokens','cache_ttl_seconds','response_bytes','output_characters','max_output_tokens',
'parse_line','parse_column'}
KEYWORDS = {'type','required','additionalProperties','enum','const','pattern','minItems','maxItems','minimum','maximum','anyOf','oneOf'}
JSON_TYPES = {'object','array','string','integer','number','boolean','null'}
def json_type(value):
if value is None:return 'null'
if isinstance(value,bool):return 'boolean'
if isinstance(value,dict):return 'object'
if isinstance(value,list):return 'array'
if isinstance(value,str):return 'string'
return 'integer' if isinstance(value,int) else 'number'
VALIDATION_FIELDS = frozenset({'retire','finding_id','intent','hypothesis_id','assessment','semantic_score','reason','update','confidence',
'working_brief','format_version','findings','retired','revision_reason','direction',
'text','scope','evidence_refs','id','uncertainties','next_question','resolve_questions',
@@ -50,6 +60,11 @@ def schema_feedback(error, _depth=0, *, include_keyword=False):
result={'validation_path':'/'+path, 'validation_fields':fields}
if include_keyword and error.validator in KEYWORDS:
result['validation_keyword']=error.validator
if error.validator=='type':
expected=error.validator_value
expected=[expected] if isinstance(expected,str) else expected
result['expected_types']=[t for t in expected if t in JSON_TYPES][:7]
result['actual_type']=json_type(error.instance)
return result
@@ -71,6 +86,10 @@ def sanitize(data):
result[key] = data[key]
if isinstance(data.get('validation_keyword'),str) and data['validation_keyword'] in KEYWORDS:
result['validation_keyword'] = data['validation_keyword']
if isinstance(data.get('actual_type'),str) and data['actual_type'] in JSON_TYPES:
result['actual_type']=data['actual_type']
if isinstance(data.get('expected_types'),list):
result['expected_types']=[t for t in data['expected_types'] if isinstance(t,str) and t in JSON_TYPES][:7]
for key, allowed in {
'json_error_kind':{'missing_delimiter','missing_key','unterminated_string','invalid_escape','extra_data','unexpected_value','other'},
'response_stage':{'http_envelope','candidate','output_json'},
@@ -497,6 +497,32 @@
},
"json_error_at_end": {
"type": "boolean"
},
"expected_types": {
"type": "array",
"maxItems": 7,
"items": {
"enum": [
"object",
"array",
"string",
"integer",
"number",
"boolean",
"null"
]
}
},
"actual_type": {
"enum": [
"object",
"array",
"string",
"integer",
"number",
"boolean",
"null"
]
}
}
},
@@ -282,6 +282,32 @@
],
"minimum": 0,
"maximum": 1000000000000
},
"expected_types": {
"type": "array",
"maxItems": 7,
"items": {
"enum": [
"object",
"array",
"string",
"integer",
"number",
"boolean",
"null"
]
}
},
"actual_type": {
"enum": [
"object",
"array",
"string",
"integer",
"number",
"boolean",
"null"
]
}
},
"additionalProperties": false
@@ -117,6 +117,44 @@
],
"minimum": 0,
"maximum": 1000000000000
},
"expected_types": {
"type": "array",
"maxItems": 7,
"items": {
"enum": [
"object",
"array",
"string",
"integer",
"number",
"boolean",
"null"
]
}
},
"actual_type": {
"enum": [
"object",
"array",
"string",
"integer",
"number",
"boolean",
"null"
]
},
"validation_path": {
"type": "string",
"maxLength": 240
},
"validation_fields": {
"type": "array",
"maxItems": 8,
"items": {
"type": "string",
"maxLength": 64
}
}
},
"additionalProperties": false
+34 -3
View File
@@ -37,6 +37,34 @@ def test_bootstrap_gateway_reports_safe_nested_schema_location(fault,path,keywor
assert caught.value.metadata['input_tokens']==100
@pytest.mark.parametrize('value,actual', [('PRIVATE_VALUE','string'),(['PRIVATE_VALUE'],'array'),(None,'null'),(True,'boolean'),(42,'integer'),(1.5,'number')])
def test_finding_item_types_are_preserved_without_response_values(value,actual):
from jsonschema import Draft202012Validator
from viano_bug_analysis.llm.diagnostics import schema_feedback,sanitize
from viano_bug_analysis.case.contracts import validate
request=bootstrap();document=reply();document['update']['next_step']=None
document['update']['working_brief']['findings']=[value]
error=next(Draft202012Validator(request['output_schema']).iter_errors(document))
details=sanitize(schema_feedback(error,include_keyword=True))
assert details['validation_path']=='/update/working_brief/findings/0'
assert details['expected_types']==['object'] and details['actual_type']==actual
assert 'PRIVATE_VALUE' not in json.dumps(details)
validate(details,'llm-failure-diagnostics')
def test_type_sanitizer_rejects_arbitrary_values_and_distinguishes_bool():
from viano_bug_analysis.llm.diagnostics import sanitize
assert sanitize({'actual_type':'PRIVATE','expected_types':['object','PRIVATE',{},True]})=={'expected_types':['object']}
def test_old_contexts_and_other_error_paths_get_no_new_prompt_text():
from viano_bug_analysis.analysis.tool_guidance import finding_repair_instructions
assert finding_repair_instructions({'bootstrap_json_framing':True,'previous_rejection_details':{
'validation_path':'/update/working_brief/findings/0'}})==''
assert finding_repair_instructions({'bootstrap_json_framing':True,'previous_rejection_details':{
'validation_path':'/next/arguments','expected_types':['object']}})==''
def test_bootstrap_schema_feedback_survives_package_resume_and_repairs_once(tool_case,tmp_path):
from test_investigation_hybrid import HybridAdapter
from test_working_brief import service_for
@@ -53,7 +81,7 @@ def test_bootstrap_schema_feedback_survives_package_resume_and_repairs_once(tool
assert request['prompt_version']=='investigation_bootstrap_v21'
response=await super().generate(config,dict(request,prompt_version='investigation_project_v18'),**kwargs)
if len(self.sent)==1:
response.document['update']['working_brief']['findings']='PRIVATE_RESPONSE_VALUE'
response.document['update']['working_brief']['findings']=['PRIVATE_RESPONSE_VALUE']
return response
adapter=Adapter();router=InvestigationToolRouter(project_research=service_for(tool_case))
loop=Investigator(tool_case[0],gateway(adapter),InvestigationConfig(
@@ -65,7 +93,8 @@ def test_bootstrap_schema_feedback_survives_package_resume_and_repairs_once(tool
context=json.loads(reader.read_bytes(state(first)['active']['context_ref']))
call=json.loads(reader.read_bytes(state(first)['hypotheses']['H1']['notes'][-1]['llm_ref']))
feedback=context['previous_rejection_details']
assert feedback=={'validation_path':'/update/working_brief/findings','validation_fields':[],
assert feedback=={'validation_path':'/update/working_brief/findings/0','validation_fields':[],
'expected_types':['object'],'actual_type':'string',
'requirement':'match_expected_type_at_validation_path'}
assert call['outcome']['diagnostics']['validation_path']==feedback['validation_path']
assert 'PRIVATE_RESPONSE_VALUE' not in json.dumps(call)
@@ -73,5 +102,7 @@ def test_bootstrap_schema_feedback_survives_package_resume_and_repairs_once(tool
result=asyncio.run(Investigator(portable,gateway(adapter),router=router).step(*CASE))
assert result.status=='PROGRESSED' and len(adapter.sent)==2
assert 'match_expected_type_at_validation_path' in json.dumps(adapter.sent[1])
assert '/update/working_brief/findings' in json.dumps(adapter.sent[1])
assert '/update/working_brief/findings/0' in json.dumps(adapter.sent[1])
assert 'Finding type repair' in adapter.sent[1]['messages'][0]['content']
assert 'No $record/$table encoding in output' in adapter.sent[1]['messages'][0]['content']
assert PackageValidator().validate(result.package).valid