Bound investigation prompts and paginate research evidence
This commit is contained in:
1 parent
377644ffa0
commit
682f275590
24 files changed
+1005
-37
No files matched your search
@@ -0,0 +1,196 @@
|
||||
import copy
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from test_investigation_lean import lean_context
|
||||
from test_case_builder import case_setup
|
||||
from test_investigation_startup import Adapter
|
||||
from test_investigation_tools import tool_case
|
||||
from test_investigator import CASE, run, state
|
||||
from test_working_brief import service_for
|
||||
from test_case_interpreter import gateway
|
||||
from viano_bug_analysis.analysis import Investigator, InvestigationConfig, InvestigationError
|
||||
from viano_bug_analysis.analysis.investigation_prompt import build_prompt
|
||||
from viano_bug_analysis.analysis.investigation_lean_v20 import validate_reply
|
||||
from viano_bug_analysis.analysis.investigation_window import evidence_page, pages, PAGE_CHARACTERS
|
||||
from viano_bug_analysis.llm import LLMError, ProviderResponse
|
||||
from viano_bug_analysis.llm.investigation_size import characters, check, check_wire, LIMIT
|
||||
from viano_bug_analysis.llm.gemini import GeminiProvider
|
||||
from viano_bug_analysis.tools import InvestigationToolRouter
|
||||
from viano_bug_analysis.package import PackageReader, PackageValidator
|
||||
|
||||
|
||||
def context():
|
||||
c = lean_context()
|
||||
c['lean_protocol'] = 2
|
||||
c['policy']['prompt_window_version'] = 1
|
||||
return c
|
||||
|
||||
|
||||
def test_window_is_small_and_canonical_context_is_untouched():
|
||||
c = context()
|
||||
before = copy.deepcopy(c)
|
||||
request = build_prompt(c)
|
||||
assert characters(request) <= 10_000
|
||||
body = json.loads(request['messages'][1]['content'])
|
||||
assert 'response_contract' in body and 'tool_schema' not in body['input']
|
||||
assert c == before
|
||||
|
||||
|
||||
def test_evidence_pages_are_lossless_and_explicitly_partial():
|
||||
entry = {'record': {'hypothesis_id': 'H1'}, 'content': {'items': [
|
||||
{'text': '\u00e4\U0001f600' * 8000, 'coverage': 'bounded'}]}}
|
||||
raw = pages(entry)
|
||||
first = evidence_page(entry)
|
||||
assert first['complete_payload'] is False
|
||||
fragments = [evidence_page(entry,i) for i in range(first['pages'])]
|
||||
assert ''.join(p['fragment'] for p in fragments) == raw
|
||||
assert all(len(p['fragment']) <= PAGE_CHARACTERS for p in fragments)
|
||||
assert len({p['sha256'] for p in fragments}) == 1
|
||||
with pytest.raises(InvestigationError, match='INVESTIGATION_EVIDENCE_PAGE'):
|
||||
evidence_page(entry, first['pages'])
|
||||
|
||||
|
||||
def test_long_history_does_not_accumulate_raw_results():
|
||||
c = context()
|
||||
seed = copy.deepcopy(next(iter(c['evidence'].values())))
|
||||
sizes = []
|
||||
for count in (3, 20, 100, 138):
|
||||
c['evidence'] = {f'E{i:06d}': copy.deepcopy(seed) for i in range(1,count+1)}
|
||||
for eid, entry in c['evidence'].items():
|
||||
entry['record']['id'] = eid
|
||||
entry['content']['items'] = [{'text': eid + 'x' * 30_000}]
|
||||
c['previous_result'] = c['evidence'][f'E{count:06d}']
|
||||
c['lean_session']['turns'] = []
|
||||
request = build_prompt(c)
|
||||
sizes.append(characters(request))
|
||||
body = json.loads(request['messages'][1]['content'])
|
||||
assert body['input']['archive']['omitted_from_view'] > 0
|
||||
assert f'E{count:06d}' in body['input']['evidence']
|
||||
assert max(sizes) < 20_000
|
||||
assert max(sizes) - min(sizes) < 1500
|
||||
|
||||
|
||||
def test_strict_controller_still_rejects_unknown_actions():
|
||||
c = context()
|
||||
with pytest.raises(InvestigationError, match='INVESTIGATION_OUTPUT'):
|
||||
validate_reply(c, {'assessment': None, 'observation': '',
|
||||
'next': {'kind': 'execute_shell', 'command': 'anything'}})
|
||||
|
||||
|
||||
def test_size_guard_counts_schema_and_cached_wire_prefix():
|
||||
request = {'messages': [{'role': 'system', 'content': 'a' * 10_000}],
|
||||
'output_schema': {'type': 'object', 'description': 'b' * 10_000}}
|
||||
with pytest.raises(LLMError, match='INVESTIGATION_PROMPT_LIMIT'):
|
||||
check(request)
|
||||
with pytest.raises(LLMError, match='INVESTIGATION_PROMPT_LIMIT'):
|
||||
check_wire({'parts': [{'text': 'a' * 10_000}]},
|
||||
[{'parts': [{'text': 'b' * 10_001}]}])
|
||||
|
||||
|
||||
@pytest.mark.parametrize('version', ['investigation_project_v20', 'investigation_startup_v1',
|
||||
'investigation_startup_review_v1', 'investigation_format_repair_v1'])
|
||||
def test_oversized_requests_never_reach_adapter(version):
|
||||
import asyncio
|
||||
adapter = Adapter()
|
||||
request = {'prompt_version':version,'privacy':'configured',
|
||||
'messages':[{'role':'user','content':'x' * 20_001}],
|
||||
'output_schema':{'type':'object'}}
|
||||
with pytest.raises(LLMError, match='INVESTIGATION_PROMPT_LIMIT'):
|
||||
asyncio.run(gateway(adapter).complete_round(request))
|
||||
assert adapter.calls == []
|
||||
request['prompt_version'] = 'case_plan_v2'
|
||||
assert gateway(adapter).validate_request(request)['prompt_version'] == 'case_plan_v2'
|
||||
|
||||
|
||||
def test_exact_character_limit_and_diagnostics():
|
||||
from viano_bug_analysis.llm.investigation_size import compact
|
||||
schema = {'type':'object'}
|
||||
request = {'messages':[{'role':'user','content':'x' * (LIMIT-len(compact(schema)))}],
|
||||
'output_schema':schema}
|
||||
assert check(request) == LIMIT
|
||||
request['messages'][0]['content'] += 'x'
|
||||
with pytest.raises(LLMError) as error:
|
||||
check(request)
|
||||
assert error.value.diagnostics['prompt_characters'] == LIMIT+1
|
||||
|
||||
|
||||
def loop_for(tool_case, adapter):
|
||||
return Investigator(tool_case[0], gateway(adapter), InvestigationConfig(
|
||||
lean_loop=True, lean_protocol=2, startup_batch_review=True,
|
||||
lean_checkpoint_steps=0, prompt_window_version=1),
|
||||
router=InvestigationToolRouter(project_research=service_for(tool_case)))
|
||||
|
||||
|
||||
def test_bounded_startup_review_and_import(tool_case, tmp_path):
|
||||
adapter = Adapter()
|
||||
loop = loop_for(tool_case, adapter)
|
||||
result = run(loop, steps=4)
|
||||
assert state(result)['startup']['done']
|
||||
assert len(adapter.calls) == 4
|
||||
assert all('tool_schema' not in body['input'] for body in adapter.calls)
|
||||
assert PackageValidator().validate(result.package).valid
|
||||
# Replaying the stored bounded requests after an import retains bindings.
|
||||
from viano_bug_analysis.lifecycle import CheckpointStore, Workspace
|
||||
recipient = CheckpointStore(Workspace(tmp_path/'recipient'))
|
||||
recipient.intake(result.package)
|
||||
restored = Investigator(recipient, gateway(adapter), router=loop.router)
|
||||
with PackageReader(recipient.latest(*CASE)) as reader:
|
||||
assert restored._load(reader)['startup']['done']
|
||||
|
||||
|
||||
def test_bounded_completion_after_checkpoints(tool_case):
|
||||
class Finish(Adapter):
|
||||
async def generate(self, config, request, **kwargs):
|
||||
body = json.loads(request['messages'][1]['content'])
|
||||
assert characters(request) <= LIMIT
|
||||
if body['task'] == 'investigate_one_step':
|
||||
out = {'assessment': None, 'observation': 'Ready to consolidate.',
|
||||
'next': {'kind': 'checkpoint', 'intent': 'completion'}}
|
||||
elif body['task'] == 'consolidate_investigation_session':
|
||||
out = {'assessment': None, 'confidence': 'plausible', 'findings': [],
|
||||
'uncertainties': ['No proven cause.'], 'next_question': None,
|
||||
'retire': [], 'next': {'kind': 'finish', 'reason': 'Bounded investigation complete.'}}
|
||||
else:
|
||||
return await super().generate(config, request, **kwargs)
|
||||
return ProviderResponse(out, 'fixture', 100, 50)
|
||||
result = run(loop_for(tool_case, Finish()), steps=20)
|
||||
assert result.status == 'COMPLETED'
|
||||
assert PackageValidator().validate(result.package).valid
|
||||
|
||||
|
||||
def test_recall_page_is_persisted_and_replayed(tool_case):
|
||||
class Recall(Adapter):
|
||||
async def generate(self, config, request, **kwargs):
|
||||
body = json.loads(request['messages'][1]['content'])
|
||||
if body['task'] == 'investigate_one_step':
|
||||
self.calls.append(body)
|
||||
out = {'assessment': None, 'observation': 'Read archived evidence.',
|
||||
'next': {'kind':'recall','evidence_refs':['E000001'],'page':0}}
|
||||
return ProviderResponse(out,'fixture',100,50)
|
||||
return await super().generate(config,request,**kwargs)
|
||||
loop = loop_for(tool_case, Recall())
|
||||
result = run(loop, steps=5)
|
||||
assert state(result)['recalled_pages']['H1'] == {'E000001':0}
|
||||
result = run(loop, steps=1)
|
||||
assert result.status != 'BLOCKED'
|
||||
|
||||
|
||||
def test_bounded_cache_partition_is_exact_and_wire_is_bounded():
|
||||
from viano_bug_analysis.llm.stable_context import split_document, restore_document
|
||||
request = build_prompt(context())
|
||||
body = json.loads(request['messages'][1]['content'])
|
||||
parts = split_document(body,request['prompt_version'])
|
||||
assert parts and restore_document(*parts) == body
|
||||
system,prefix,live = GeminiProvider._messages(request,stable_case_context=True)
|
||||
assert check_wire(system,prefix+live) <= LIMIT
|
||||
|
||||
|
||||
def test_early_review_keeps_the_low_reasoning_route():
|
||||
from viano_bug_analysis.llm.gateway import is_investigation_review
|
||||
c = context()
|
||||
c['lean_session']['checkpoint'] = True
|
||||
c['early_review'] = {'request':{'status':'preparing'}}
|
||||
request = build_prompt(c)
|
||||
assert is_investigation_review(request)
|
||||
@@ -189,30 +189,17 @@ def test_gateway_schema_location_persists_in_retry_without_leaking_values(tool_c
|
||||
assert info['validation_path']=='/update/working_brief' and info['validation_fields']==['retired']
|
||||
|
||||
|
||||
def test_v14_positive_early_review_reaches_independent_diagnosis_with_rest_budget(tool_case):
|
||||
service=service_for(tool_case)
|
||||
def change(reply,c):
|
||||
early=c['early_review']['request']
|
||||
if early and early['status']=='preparing':
|
||||
reply['next']={'kind':'defer','reason':'Settle pending evidence.'}
|
||||
elif c['selected_hypothesis']=='H1' and c['previous_result']:
|
||||
eid=c['previous_result']['evidence_id']
|
||||
other=next(e for e,v in c['evidence'].items() if v['record']['hypothesis_id']=='H2')
|
||||
reply['update'].update(confidence='strongly_supported',
|
||||
causal_review={'effect':'supports','support':[eid],'against':[],'reason':'Fixture causal defect.'})
|
||||
reply['next']={'kind':'request_diagnosis','reason':'Scoped defect with countercheck.','countercheck_refs':[other]}
|
||||
else:reply['next']={'kind':'tool','operation':'project.query','arguments':rows()}
|
||||
loop=Investigator(tool_case[0],gateway(LoopAdapter(change)),router=InvestigationToolRouter(project_research=service))
|
||||
def test_oversized_legacy_v14_request_is_blocked_without_spending_budget(tool_case):
|
||||
# The old verbose v14 contract exceeds the new universal character limit.
|
||||
# Compatibility does not authorize sending an oversized request.
|
||||
loop=Investigator(tool_case[0],gateway(LoopAdapter()),
|
||||
router=InvestigationToolRouter(project_research=service_for(tool_case)))
|
||||
original=loop._context
|
||||
loop._context=lambda *args:dict(original(*args),prompt_version='investigation_project_v14')
|
||||
done=run(loop,steps=10)
|
||||
assert done.status=='COMPLETED' and state(done)['early_stop']['status']=='requested'
|
||||
assert done.status=='BLOCKED' and done.code=='INVESTIGATION_PROMPT_LIMIT'
|
||||
budget=BudgetManager(tool_case[0]).snapshot(*CASE).state
|
||||
assert all(a['remaining']>0 for a in budget['accounts'].values())
|
||||
adapter=FinalAdapter(True,lambda p,c:p.update(confidence='strongly_supported'))
|
||||
result=build(DiagnosisBuilder(tool_case[0],gateway(adapter)))
|
||||
assert result.status=='COMPLETED' and adapter.calls==1
|
||||
assert BudgetManager(tool_case[0]).snapshot(*CASE).state==budget
|
||||
assert all(a['remaining']==a['initial'] and not a['actions'] for a in budget['accounts'].values())
|
||||
|
||||
|
||||
def test_v14_can_finish_inconclusively_without_invented_cause(tool_case):
|
||||
|
||||
Reference in new issue
Block a user