From d957974498f0640de3a653bc7dd059bd60b7d314 Mon Sep 17 00:00:00 2001 From: Nitsu45 Date: Sun, 18 Oct 2026 22:37:56 +0200 Subject: [PATCH] Preserve bootstrap schema error paths for targeted correction --- docs/bootstrap-schema-diagnostics.md | 25 ++++++ docs/source-context-relevance-v2.md | 4 +- .../analysis/investigator.py | 6 ++ src/viano_bug_analysis/llm/diagnostics.py | 2 +- src/viano_bug_analysis/llm/gateway.py | 2 +- tests/test_bootstrap_schema_diagnostics.py | 77 +++++++++++++++++++ tests/test_tool_repair_feedback.py | 3 +- 7 files changed, 114 insertions(+), 5 deletions(-) create mode 100644 docs/bootstrap-schema-diagnostics.md create mode 100644 tests/test_bootstrap_schema_diagnostics.py diff --git a/docs/bootstrap-schema-diagnostics.md b/docs/bootstrap-schema-diagnostics.md new file mode 100644 index 0000000..ad96eca --- /dev/null +++ b/docs/bootstrap-schema-diagnostics.md @@ -0,0 +1,25 @@ +# Schemafehler bei Bootstrap v21 + +Der Gateway validiert weiterhin gegen das vollständige lokale Ausgabeschema. +Bootstrap v21 war jedoch nicht für die detaillierte, bereinigte +Schemafehlerdiagnostik freigeschaltet. Deshalb enthielten fehlgeschlagene Aufrufe +teilweise nur `validation_keyword: type`, ohne die betroffene Stelle. + +Jetzt werden auch für diesen Prompt `validation_path`, bekannte +`validation_fields` und das konkrete Validierungskeyword erfasst. Bei verschachtelten +Aktionsalternativen wird die zum vorhandenen Diskriminator passende Schemaalternative +verwendet. Unbekannte Feldnamen werden maskiert; Antwortwerte und rohe +Fehlermeldungen werden nicht in die Diagnose übernommen. Die bekannten Werkzeugfelder +`profile` und `entity_refs` sind für sichere Feldpfade freigegeben. + +Der Controller speichert diese Diagnose append-only mit dem fehlgeschlagenen Aufruf. +Für Bootstrap-Aufrufe bekommen Typ- und Enumfehler einen gezielten Korrekturhinweis +am gemeldeten Pfad. Das Schema wird nicht gelockert und die Antwort nicht automatisch +umgeschrieben. Ein technischer Fehlversuch löst keinen Rechercheauftrag aus. +Die vorhandene Begrenzung ungültiger Antworten bleibt wirksam. + +Lokale Tests prüfen Typfehler, fehlende Pflichtfelder, zusätzliche Felder, +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. diff --git a/docs/source-context-relevance-v2.md b/docs/source-context-relevance-v2.md index d11e5ef..7005b12 100644 --- a/docs/source-context-relevance-v2.md +++ b/docs/source-context-relevance-v2.md @@ -114,5 +114,5 @@ primären Kandidaten und keine historische SQL-Sammlung; das Suchbudget wird dab ausdrücklich als begrenzt gemeldet. Dies ist ein lokaler Werkzeugtest, keine neue LLM-Bewertung des Bugs und kein Nachweis realer Token-/Kostenersparnis. -Die zuvor bekannte fehlende Gateway-Schemafehlerdiagnostik für Bootstrap v21 ist -ein separater Punkt und wird durch diese Source-Überarbeitung nicht behoben. +Die fehlende Gateway-Schemafehlerdiagnostik für Bootstrap v21 wurde anschließend +separat korrigiert; siehe `bootstrap-schema-diagnostics.md`. diff --git a/src/viano_bug_analysis/analysis/investigator.py b/src/viano_bug_analysis/analysis/investigator.py index faf68b4..93a6f6c 100644 --- a/src/viano_bug_analysis/analysis/investigator.py +++ b/src/viano_bug_analysis/analysis/investigator.py @@ -618,6 +618,12 @@ class Investigator: 'oneOf':'choose_one_valid_next_action', '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']['requirement']={ + 'type':'match_expected_type_at_validation_path', + 'enum':'use_allowed_value_at_validation_path', + 'const':'use_allowed_value_at_validation_path', + }.get(info.get('validation_keyword'),context['previous_rejection_details']['requirement']) elif code == 'LLM_RESPONSE' and context.get('prompt_version') in {'investigation_project_v16','investigation_project_v17','investigation_project_v18'}: with PackageReader(self._latest(state['case_id'],state['package_id']),self.store.limits) as reader: call=self._read(reader,llm_ref,'llm-call') diff --git a/src/viano_bug_analysis/llm/diagnostics.py b/src/viano_bug_analysis/llm/diagnostics.py index 53e5596..26e8a61 100644 --- a/src/viano_bug_analysis/llm/diagnostics.py +++ b/src/viano_bug_analysis/llm/diagnostics.py @@ -11,7 +11,7 @@ VALIDATION_FIELDS = frozenset({'retire','finding_id','intent','hypothesis_id','a 'prerequisites','requested_observations','environment','countercheck_refs','path', 'start_line','end_line','limit','offset','table','columns','filters','column','operator', 'value','text','case_sensitive','path_prefix','source_ref','source_refs','note','causal_review', - 'next_step','goal','supports_if','weakens_if','investigate_if'}) + 'next_step','goal','supports_if','weakens_if','investigate_if','profile','entity_refs'}) def schema_feedback(error, _depth=0, *, include_keyword=False): diff --git a/src/viano_bug_analysis/llm/gateway.py b/src/viano_bug_analysis/llm/gateway.py index ebef075..095ab3c 100644 --- a/src/viano_bug_analysis/llm/gateway.py +++ b/src/viano_bug_analysis/llm/gateway.py @@ -155,7 +155,7 @@ class LLMGateway: LOGGER.warning("LLM endpoint unavailable provider=%s code=%s", provider.id, code) except ValidationError as invalid: info={'validation_keyword':invalid.validator} - if request['prompt_version'] in {'investigation_project_v14','investigation_project_v15','investigation_project_v16','investigation_project_v17','investigation_project_v18','investigation_project_v19','investigation_project_v20'}: + if request['prompt_version'] in {'investigation_bootstrap_v21','investigation_project_v14','investigation_project_v15','investigation_project_v16','investigation_project_v17','investigation_project_v18','investigation_project_v19','investigation_project_v20'}: from .diagnostics import schema_feedback info.update(schema_feedback(invalid,include_keyword=True)) error = LLMError("LLM_OUTPUT_SCHEMA",diagnostics=info) diff --git a/tests/test_bootstrap_schema_diagnostics.py b/tests/test_bootstrap_schema_diagnostics.py new file mode 100644 index 0000000..ba3711e --- /dev/null +++ b/tests/test_bootstrap_schema_diagnostics.py @@ -0,0 +1,77 @@ +import asyncio +import copy +import json + +import pytest + +from test_case_builder import case_setup +from test_case_interpreter import gateway +from test_investigation_tools import tool_case +from test_parameter_and_bootstrap_guard import bootstrap +from test_lean_investigation_decision import reply +from viano_bug_analysis.llm import LLMError, ProviderResponse + + +@pytest.mark.parametrize('fault,path,keyword,fields', [ + ('type','/update/working_brief/findings','type',[]), + ('required','/update/working_brief','required',['retired']), + ('extra','/update/working_brief','additionalProperties',['confidence']), +]) +def test_bootstrap_gateway_reports_safe_nested_schema_location(fault,path,keyword,fields): + document=reply();document['update']['next_step']=None + brief=document['update']['working_brief'] + if fault=='type':brief['findings']='PRIVATE_RESPONSE_VALUE' + elif fault=='required':brief.pop('retired') + else:brief['confidence']='PRIVATE_RESPONSE_VALUE' + class Adapter: + async def generate(self,*args,**kwargs): + return ProviderResponse(document,'fixture',100,50) + with pytest.raises(LLMError) as caught: + asyncio.run(gateway(Adapter()).complete_round(bootstrap())) + assert caught.value.code=='LLM_OUTPUT_SCHEMA' + details=caught.value.diagnostics + assert details['validation_path']==path + assert details['validation_keyword']==keyword + assert details['validation_fields']==fields + assert 'PRIVATE_RESPONSE_VALUE' not in json.dumps(details) + assert caught.value.metadata['input_tokens']==100 + + +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 + from test_investigator import CASE,state + from viano_bug_analysis.analysis import Investigator,InvestigationConfig + from viano_bug_analysis.budget import BudgetManager + from viano_bug_analysis.lifecycle import CheckpointStore,Workspace + from viano_bug_analysis.package import PackageReader,PackageValidator + from viano_bug_analysis.tools import InvestigationToolRouter + class Adapter(HybridAdapter): + def __init__(self):super().__init__();self.sent=[] + async def generate(self,config,request,**kwargs): + self.sent.append(copy.deepcopy(request)) + 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' + return response + adapter=Adapter();router=InvestigationToolRouter(project_research=service_for(tool_case)) + loop=Investigator(tool_case[0],gateway(adapter),InvestigationConfig( + lean_loop=True,lean_protocol=2,lean_checkpoint_steps=0,bootstrap_json_framing=True),router=router) + first=asyncio.run(loop.step(*CASE)) + assert first.status=='REJECTED' and first.code=='LLM_OUTPUT_SCHEMA' + assert not BudgetManager(tool_case[0]).snapshot(*CASE).state['accounts']['H1']['actions'] + with PackageReader(first.package) as reader: + 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':[], + '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) + portable=CheckpointStore(Workspace(tmp_path/'portable'));portable.intake(first.package) + 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 PackageValidator().validate(result.package).valid diff --git a/tests/test_tool_repair_feedback.py b/tests/test_tool_repair_feedback.py index 7f65fb5..2725271 100644 --- a/tests/test_tool_repair_feedback.py +++ b/tests/test_tool_repair_feedback.py @@ -34,8 +34,9 @@ def test_feedback_descends_through_nested_filters_and_root_definitions(): 'kind':'database.rows','table':'creature','columns':['guid'], 'filters':[{'column':'id','operator':'eq'}],'offset':0,'limit':20}} with pytest.raises(InvestigationError) as caught:validate_tool_shape(c,r) + from viano_bug_analysis.analysis.tool_guidance import FILTER_REQUIREMENT assert caught.value.feedback=={'validation_path':'/next/arguments/filters/0', - 'validation_fields':['value'],'requirement':'provide_missing_required_fields'} + 'validation_fields':['value'],'requirement':FILTER_REQUIREMENT} def test_feedback_reports_type_error_without_recommending_missing_fields():