Carry investigation decision criteria and offer grounded query narrowing
This commit is contained in:
1 parent
29816dc45b
commit
d010f70f5e
22 files changed
+747
-38
No files matched your search
@@ -0,0 +1,157 @@
|
||||
import asyncio
|
||||
import copy
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from jsonschema import Draft202012Validator
|
||||
from test_compact_investigation import context,entry
|
||||
from test_lean_investigation_decision import reply
|
||||
from test_case_builder import case_setup
|
||||
from test_investigation_tools import tool_case
|
||||
from viano_bug_analysis.analysis.investigation_plan import framed_view,observed_keys
|
||||
from viano_bug_analysis.analysis.investigation_compact import model_view,validate_tool_shape
|
||||
from viano_bug_analysis.analysis.investigation_prompt import build_prompt,output_schema
|
||||
from viano_bug_analysis.analysis.investigation_decision import normalize
|
||||
from viano_bug_analysis.analysis.investigation_config import InvestigationError
|
||||
from viano_bug_analysis.case.contracts import validate
|
||||
|
||||
|
||||
def plan():
|
||||
return {'goal':'Determine whether these observed objects are event-bound.',
|
||||
'supports_if':'No objects exist in the affected scope.',
|
||||
'weakens_if':'Verified objects already exist there.',
|
||||
'investigate_if':'The result is incomplete or another creation path is untested.'}
|
||||
|
||||
|
||||
def current():
|
||||
c=context();c['prompt_version']='investigation_project_v18';return c
|
||||
|
||||
|
||||
def test_model_goal_and_criteria_persist_unchanged_without_mutating_reply():
|
||||
c=current();r=reply();r['update']['next_step']=plan();before=copy.deepcopy(r)
|
||||
d=normalize(c,r)
|
||||
assert r==before and d['update']['working_brief']['next_step']==plan()
|
||||
validate(d['update']['working_brief'],'investigation-working-brief')
|
||||
c['working_brief']['brief']=d['update']['working_brief']
|
||||
view=framed_view(c,model_view(c))
|
||||
assert view['investigation_step']['decision']==plan()
|
||||
assert view['investigation_step']['decision_source']=='previous_model_plan'
|
||||
assert 'next_step' not in view['working_brief']
|
||||
assert c['working_brief']['brief']['next_step']==plan()
|
||||
|
||||
|
||||
def test_action_needs_a_plan_but_completion_can_use_null():
|
||||
c=current();r=reply();r['update']['next_step']=None
|
||||
r['next']={'kind':'recall','evidence_refs':['E000002']}
|
||||
with pytest.raises(InvestigationError,match='INVESTIGATION_PLAN'):normalize(c,r)
|
||||
r['next']={'kind':'finish','reason':'Enough scoped evidence.'}
|
||||
assert normalize(c,r)['update']['working_brief']['next_step'] is None
|
||||
r['update'].pop('next_step')
|
||||
assert not Draft202012Validator(output_schema(c)).is_valid(r)
|
||||
|
||||
|
||||
def test_missing_plan_criteria_produce_precise_repair_fields():
|
||||
c=current();r=reply();r['update']['next_step']={'goal':'PRIVATE_PLAN'}
|
||||
with pytest.raises(InvestigationError) as caught:validate_tool_shape(c,r)
|
||||
assert caught.value.feedback['validation_path']=='/update/next_step'
|
||||
assert caught.value.feedback['validation_fields']==['supports_if','weakens_if','investigate_if']
|
||||
assert 'PRIVATE_PLAN' not in json.dumps(caught.value.feedback)
|
||||
|
||||
|
||||
def key_context(count=18,origin=None):
|
||||
c=current();c['evidence']={};origin=origin or {'snapshot_id':'verified'}
|
||||
schema=entry(1,'database.schema');capture=schema['content']['items'][0]
|
||||
capture['origin']=copy.deepcopy(origin)
|
||||
capture['result']['items'][0]['columns']=[{'COLUMN_NAME':'object_key','COLUMN_TYPE':'int','COLUMN_KEY':'PRI'}]
|
||||
rows=entry(20);rows['content']['items']=[{'query':{'kind':'database.rows','table':'creature','offset':0},
|
||||
'origin':copy.deepcopy(origin),'result':{'coverage':'complete_filtered_rows','truncated':False,'next_offset':None,
|
||||
'items':[{'row':{'object_key':i,'irrelevant_payload':'large raw text'}} for i in range(count)]}}]
|
||||
c['evidence']={'E000001':schema,'E000020':rows};c['previous_result']=rows
|
||||
return c
|
||||
|
||||
|
||||
def test_known_inputs_are_observed_key_tuples_with_evidence_and_scope():
|
||||
c=key_context();before=copy.deepcopy(c);items=observed_keys(c,model_view(c))
|
||||
assert items==[{'evidence_id':'E000020','capture_index':0,'schema_id':'E000001',
|
||||
'table':'creature','key_columns':['object_key'],'rows':[[i] for i in range(18)],
|
||||
'all_returned_keys_included':True,'query_complete':True}]
|
||||
assert c==before
|
||||
|
||||
|
||||
def test_key_list_limit_and_query_paging_are_distinct_and_explicit():
|
||||
c=key_context(40);items=observed_keys(c,model_view(c))
|
||||
assert len(items[0]['rows'])==32 and items[0]['all_returned_keys_included'] is False
|
||||
assert items[0]['query_complete'] is True
|
||||
c['evidence']['E000020']['content']['items'][0]['result']['truncated']=True
|
||||
assert observed_keys(c,model_view(c))[0]['query_complete'] is False
|
||||
|
||||
|
||||
def test_composite_keys_preserve_tuple_relationships_and_scalar_types():
|
||||
c=key_context(2);schema=c['evidence']['E000001']['content']['items'][0]
|
||||
schema['result']['items'][0]['columns'].append({'COLUMN_NAME':'group_key','COLUMN_TYPE':'varchar','COLUMN_KEY':'PRI'})
|
||||
rows=c['evidence']['E000020']['content']['items'][0]['result']['items']
|
||||
rows[0]['row']['group_key']='alpha';rows[1]['row']['group_key']='beta'
|
||||
item=observed_keys(c,model_view(c))[0]
|
||||
assert item['key_columns']==['object_key','group_key'] and item['rows']==[[0,'alpha'],[1,'beta']]
|
||||
|
||||
|
||||
@pytest.mark.parametrize('change',['origin','no_primary','unknown_shape','bool_value','missing_column'])
|
||||
def test_key_hints_do_not_guess_keys_values_or_snapshot_relationships(change):
|
||||
c=key_context();s=c['evidence']['E000001']['content']['items'][0];r=c['evidence']['E000020']['content']['items'][0]
|
||||
if change=='origin':s['origin']={'snapshot_id':'different'}
|
||||
elif change=='no_primary':s['result']['items'][0]['columns'][0].pop('COLUMN_KEY')
|
||||
elif change=='unknown_shape':s['result']['items']=['unknown provider format']
|
||||
elif change=='bool_value':r['result']['items'][0]['row']['object_key']=True
|
||||
else:r['result']['items'][0]['row'].pop('object_key')
|
||||
assert observed_keys(c,model_view(c))==[]
|
||||
|
||||
|
||||
def test_unread_unrelated_archived_rows_are_not_silently_added_as_hints():
|
||||
c=key_context();c['evidence']['E000020']['record']['hypothesis_id']='H2';c['previous_result']=None
|
||||
assert observed_keys(c,model_view(c))==[]
|
||||
|
||||
|
||||
def test_v18_prompt_is_shorter_and_all_existing_actions_still_available():
|
||||
c=current();old=build_prompt(dict(c,prompt_version='investigation_project_v17'));new=build_prompt(c)
|
||||
assert len(new['messages'][0]['content'])<len(old['messages'][0]['content'])
|
||||
view=json.loads(new['messages'][1]['content'])
|
||||
assert view['task']=='advance_investigation_goal'
|
||||
assert set(new['output_schema']['properties']['next']['properties']['kind']['enum'])=={
|
||||
'tool','test','finish','reject','request_diagnosis','defer','recall'}
|
||||
|
||||
|
||||
def test_narrow_review_keeps_existing_contract_and_low_effort_routing():
|
||||
from test_investigation_review import review_context
|
||||
from viano_bug_analysis.llm.gateway import is_investigation_review
|
||||
c=review_context();old=build_prompt(c);c['prompt_version']='investigation_project_v18'
|
||||
new=build_prompt(c)
|
||||
assert new['messages']==old['messages'] and new['output_schema']==old['output_schema']
|
||||
assert is_investigation_review(new)
|
||||
|
||||
|
||||
def test_plan_survives_real_checkpoint_portable_resume_and_result_assessment(tool_case,tmp_path):
|
||||
from test_investigator import LoopAdapter,CASE,run,state
|
||||
from test_working_brief import service_for
|
||||
from test_project_research import rows
|
||||
from test_case_interpreter import gateway
|
||||
from viano_bug_analysis.analysis import Investigator
|
||||
from viano_bug_analysis.tools import InvestigationToolRouter
|
||||
from viano_bug_analysis.package import PackageReader,PackageValidator
|
||||
from viano_bug_analysis.lifecycle import CheckpointStore,Workspace
|
||||
store=tool_case[0];service=service_for(tool_case);seen=[]
|
||||
def change(reply,c):
|
||||
if c['previous_result']:
|
||||
assert c['investigation_step']['decision']==plan();seen.append(1)
|
||||
reply['next']={'kind':'finish','reason':'Scoped result assessed.'};reply['update']['next_step']=None
|
||||
else:
|
||||
reply['next']={'kind':'tool','operation':'project.query','arguments':rows()}
|
||||
reply['update']['next_step']=plan()
|
||||
adapter=LoopAdapter(change);loop=Investigator(store,gateway(adapter),router=InvestigationToolRouter(project_research=service))
|
||||
first=run(loop,steps=1)
|
||||
with PackageReader(first.package) as reader:
|
||||
record=json.loads(reader.read_bytes(state(first)['hypotheses']['H1']['working_brief_ref']))
|
||||
assert record['brief']['next_step']==plan()
|
||||
portable=CheckpointStore(Workspace(tmp_path/'portable'));portable.intake(first.package)
|
||||
final=run(Investigator(portable,gateway(adapter),router=InvestigationToolRouter(project_research=service)))
|
||||
assert final.status=='COMPLETED' and len(seen)==3 and len(adapter.contexts)==6
|
||||
assert PackageValidator().validate(final.package).valid
|
||||
@@ -136,7 +136,7 @@ def test_focused_loop_keeps_assessment_and_budget_settlement(tool_case):
|
||||
assessed=[]
|
||||
def change(reply,c):
|
||||
step=c['investigation_step']
|
||||
assert step['question']
|
||||
assert step.get('question') or step.get('decision',{}).get('goal')
|
||||
if c['previous_result']:
|
||||
eid=c['previous_result']['evidence_id']
|
||||
assert step['preflight']['previous_id']==eid
|
||||
|
||||
@@ -45,10 +45,10 @@ class LoopAdapter:
|
||||
result["next"]={"kind":"test","executor":"human","question":"What occurs after interaction?","steps":["Interact once"],
|
||||
"prerequisites":["Use the specified build"],"requested_observations":["Record the observed state"],"environment":"build-A"}
|
||||
if self.change:self.change(result,context)
|
||||
if request['prompt_version'] in {'investigation_project_v12','investigation_project_v13','investigation_project_v14','investigation_project_v15','investigation_project_v16','investigation_project_v17'}:
|
||||
if request['prompt_version'] in {'investigation_project_v12','investigation_project_v13','investigation_project_v14','investigation_project_v15','investigation_project_v16','investigation_project_v17','investigation_project_v18'}:
|
||||
result['update'].setdefault('causal_review',{'effect':'neutral','support':[],'against':[],
|
||||
'reason':'This fixture gathers context without establishing a causal defect.'})
|
||||
if request['prompt_version'] in {'investigation_project_v13','investigation_project_v14','investigation_project_v15','investigation_project_v16','investigation_project_v17'}:
|
||||
if request['prompt_version'] in {'investigation_project_v13','investigation_project_v14','investigation_project_v15','investigation_project_v16','investigation_project_v17','investigation_project_v18'}:
|
||||
existing=context.get('working_brief')
|
||||
review=result['update']['causal_review']
|
||||
findings=[]
|
||||
@@ -62,7 +62,7 @@ class LoopAdapter:
|
||||
'uncertainties':['Causal defect not established.'],'next_question':'Check the selected mechanism.',
|
||||
'changes':[{'id':fid,'reason':'Fixture revises the finding.',
|
||||
'evidence_refs':result['update']['evidence_refs']} for fid,f in old.items() if new.get(fid)!=f]})
|
||||
if request['prompt_version'] in {'investigation_project_v14','investigation_project_v15','investigation_project_v16','investigation_project_v17'}:
|
||||
if request['prompt_version'] in {'investigation_project_v14','investigation_project_v15','investigation_project_v16','investigation_project_v17','investigation_project_v18'}:
|
||||
b=result['update']['working_brief']
|
||||
if b['format_version']==1:
|
||||
reasons={v['id']:v['reason'] for v in b.pop('changes')}
|
||||
@@ -72,6 +72,10 @@ class LoopAdapter:
|
||||
b['retired']=[{'id':fid,'reason':reason} for fid,reason in reasons.items() if fid not in {f['id'] for f in b['findings']}]
|
||||
for k in ('evidence_refs','causal_review','note'):result['update'].pop(k,None)
|
||||
if result['assessment']:result['assessment'].pop('evidence_refs',None)
|
||||
if request['prompt_version']=='investigation_project_v18' and not context.get('review_task'):
|
||||
result['update'].setdefault('next_step',{'goal':'Check the selected mechanism.',
|
||||
'supports_if':'Scoped causal defect observed.','weakens_if':'Required path is working.',
|
||||
'investigate_if':'Scope or deployment is unknown.'} if result['next']['kind'] in {'tool','recall','test'} else None)
|
||||
return ProviderResponse(result,"fixture-investigation",100,50)
|
||||
|
||||
|
||||
|
||||
@@ -179,6 +179,7 @@ def test_gateway_schema_location_persists_in_retry_without_leaking_values(tool_c
|
||||
class Bad:
|
||||
async def generate(self,config,request,*,max_response_bytes):
|
||||
r=reply();r['update']['working_brief'].pop('retired')
|
||||
if request['prompt_version']=='investigation_project_v18':r['update']['next_step']=None
|
||||
return ProviderResponse(r,'fixture',10,10)
|
||||
loop=Investigator(tool_case[0],gateway(Bad()),router=InvestigationToolRouter(project_research=service))
|
||||
result=asyncio.run(loop.step(*CASE));assert result.status=='REJECTED'
|
||||
|
||||
Reference in new issue
Block a user