Preserve bootstrap schema error paths for targeted correction
This commit is contained in:
1 parent
af2ee6cb91
commit
d957974498
7 files changed
+114
-5
No files matched your search
@@ -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.
|
||||||
@@ -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
|
ausdrücklich als begrenzt gemeldet. Dies ist ein lokaler Werkzeugtest, keine neue
|
||||||
LLM-Bewertung des Bugs und kein Nachweis realer Token-/Kostenersparnis.
|
LLM-Bewertung des Bugs und kein Nachweis realer Token-/Kostenersparnis.
|
||||||
|
|
||||||
Die zuvor bekannte fehlende Gateway-Schemafehlerdiagnostik für Bootstrap v21 ist
|
Die fehlende Gateway-Schemafehlerdiagnostik für Bootstrap v21 wurde anschließend
|
||||||
ein separater Punkt und wird durch diese Source-Überarbeitung nicht behoben.
|
separat korrigiert; siehe `bootstrap-schema-diagnostics.md`.
|
||||||
@@ -618,6 +618,12 @@ class Investigator:
|
|||||||
'oneOf':'choose_one_valid_next_action',
|
'oneOf':'choose_one_valid_next_action',
|
||||||
'anyOf':'match_an_allowed_shape',
|
'anyOf':'match_an_allowed_shape',
|
||||||
}.get(info.get('validation_keyword'),'match_schema_at_validation_path')
|
}.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'}:
|
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:
|
with PackageReader(self._latest(state['case_id'],state['package_id']),self.store.limits) as reader:
|
||||||
call=self._read(reader,llm_ref,'llm-call')
|
call=self._read(reader,llm_ref,'llm-call')
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ VALIDATION_FIELDS = frozenset({'retire','finding_id','intent','hypothesis_id','a
|
|||||||
'prerequisites','requested_observations','environment','countercheck_refs','path',
|
'prerequisites','requested_observations','environment','countercheck_refs','path',
|
||||||
'start_line','end_line','limit','offset','table','columns','filters','column','operator',
|
'start_line','end_line','limit','offset','table','columns','filters','column','operator',
|
||||||
'value','text','case_sensitive','path_prefix','source_ref','source_refs','note','causal_review',
|
'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):
|
def schema_feedback(error, _depth=0, *, include_keyword=False):
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ class LLMGateway:
|
|||||||
LOGGER.warning("LLM endpoint unavailable provider=%s code=%s", provider.id, code)
|
LOGGER.warning("LLM endpoint unavailable provider=%s code=%s", provider.id, code)
|
||||||
except ValidationError as invalid:
|
except ValidationError as invalid:
|
||||||
info={'validation_keyword':invalid.validator}
|
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
|
from .diagnostics import schema_feedback
|
||||||
info.update(schema_feedback(invalid,include_keyword=True))
|
info.update(schema_feedback(invalid,include_keyword=True))
|
||||||
error = LLMError("LLM_OUTPUT_SCHEMA",diagnostics=info)
|
error = LLMError("LLM_OUTPUT_SCHEMA",diagnostics=info)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -34,8 +34,9 @@ def test_feedback_descends_through_nested_filters_and_root_definitions():
|
|||||||
'kind':'database.rows','table':'creature','columns':['guid'],
|
'kind':'database.rows','table':'creature','columns':['guid'],
|
||||||
'filters':[{'column':'id','operator':'eq'}],'offset':0,'limit':20}}
|
'filters':[{'column':'id','operator':'eq'}],'offset':0,'limit':20}}
|
||||||
with pytest.raises(InvestigationError) as caught:validate_tool_shape(c,r)
|
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',
|
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():
|
def test_feedback_reports_type_error_without_recommending_missing_fields():
|
||||||
|
|||||||
Reference in new issue
Block a user