Guide entity resolution with descriptive names and scoped citations
This commit is contained in:
1 parent
2fb81ef4af
commit
482df4579d
7 files changed
+196
-16
No files matched your search
@@ -62,16 +62,16 @@ class Model:
|
||||
version = request["prompt_version"]
|
||||
if version == self.unavailable_stage:
|
||||
raise ProviderUnavailable("LLM_UNAVAILABLE")
|
||||
if version.startswith('entity_resolution_') and version.endswith(('_v2','_v3')):
|
||||
if version.startswith('entity_resolution_') and version.endswith(('_v2','_v3','_v4')):
|
||||
# This is the channel json_object providers actually receive.
|
||||
assert json.loads(request['messages'][1]['content'])['output_schema'] == request['output_schema']
|
||||
version = version.replace('_v1', '_v3').replace('_v2', '_v3') if version.startswith('entity_resolution_') else version
|
||||
if version == "entity_resolution_plan_v3":
|
||||
version = version.replace('_v1', '_v4').replace('_v2', '_v4').replace('_v3', '_v4') if version.startswith('entity_resolution_') else version
|
||||
if version == "entity_resolution_plan_v4":
|
||||
document = copy.deepcopy(self.plan)
|
||||
if self.invalid_plan:
|
||||
self.invalid_plan -= 1
|
||||
document["searches"][0]["term"] = "invented device"
|
||||
elif version == "entity_resolution_assessment_v3":
|
||||
elif version == "entity_resolution_assessment_v4":
|
||||
document = {"resolutions": [{"search_index": i, "status": self.status,
|
||||
"candidate_id": self.candidate if self.status == "selected" else None,
|
||||
"reason": "The affected subject is the wall switch; the operator is an interaction participant.",
|
||||
@@ -132,7 +132,7 @@ def test_resolves_unrecorded_subject_preserves_case_and_portable_replay(built_ca
|
||||
model = Model(); service = service_for(store)
|
||||
result = run(store, model, service)
|
||||
assert result.status == "COMPLETED" and PackageValidator().validate(result.package).valid
|
||||
assert [r["prompt_version"] for r in model.requests] == ["entity_resolution_plan_v3","entity_resolution_assessment_v3","case_interpretation_resolved_v6"]
|
||||
assert [r["prompt_version"] for r in model.requests] == ["entity_resolution_plan_v4","entity_resolution_assessment_v4","case_interpretation_resolved_v6"]
|
||||
assert len(service.database.calls) == 1
|
||||
assert service.database.calls[0]["filters"] == [{"column":"name","operator":"contains","value":"switch"}]
|
||||
with PackageReader(result.package) as reader:
|
||||
@@ -200,7 +200,7 @@ def test_database_outage_preserves_accepted_plan(built_case,monkeypatch):
|
||||
|
||||
|
||||
def test_provider_retry_resume_uses_frozen_input(built_case):
|
||||
store=built_case[0];model=Model(); model.unavailable_stage='entity_resolution_plan_v3'; service=service_for(store)
|
||||
store=built_case[0];model=Model(); model.unavailable_stage='entity_resolution_plan_v4'; service=service_for(store)
|
||||
# Tiny positive retry delay permits resumption in this offline fixture.
|
||||
g=gateway(model,retry_delay_seconds=0.001)
|
||||
owner=CaseInterpreter(store,g,project_research=service)
|
||||
@@ -344,7 +344,7 @@ def test_missing_capture_feedback_resumes_and_replays_without_repeating_research
|
||||
class MissingCaptureModel(Model):
|
||||
async def generate(self, config, request, *, max_response_bytes):
|
||||
response = await super().generate(config, request, max_response_bytes=max_response_bytes)
|
||||
if request['prompt_version'] == 'entity_resolution_assessment_v3':
|
||||
if request['prompt_version'] in {'entity_resolution_assessment_v3','entity_resolution_assessment_v4'}:
|
||||
data = json.loads(request['messages'][1]['content'])
|
||||
if 'validation_feedback' not in data:
|
||||
for resolution in response.document['resolutions']:
|
||||
@@ -359,6 +359,11 @@ def test_missing_capture_feedback_resumes_and_replays_without_repeating_research
|
||||
store = built_case[0]
|
||||
model = MissingCaptureModel()
|
||||
service = service_for(store)
|
||||
from viano_bug_analysis.analysis.entity_resolution import EntityResolver
|
||||
owner=CaseInterpreter(store,gateway(model),project_research=service)
|
||||
context=context_of(store)
|
||||
EntityResolver(owner).save(context,'input',{'context':context,'policy':policy(SETTINGS),
|
||||
'binding':service.database.binding(),'protocol_version':3,'interpretation_protocol_version':6})
|
||||
rejected = run(store, model, service)
|
||||
assert rejected.status == 'REJECTED'
|
||||
assert rejected.code == 'ENTITY_RESOLUTION_CAPTURE_SOURCE'
|
||||
@@ -383,7 +388,7 @@ def test_third_assessment_can_use_lower_reasoning_without_resetting_history(buil
|
||||
efforts = []
|
||||
async def generate(self, config, request, *, max_response_bytes):
|
||||
self.efforts.append((request['prompt_version'], config.reasoning_effort))
|
||||
if request['prompt_version']=='entity_resolution_assessment_v3' and self.failures:
|
||||
if request['prompt_version']=='entity_resolution_assessment_v4' and self.failures:
|
||||
self.failures -= 1
|
||||
raise LLMError('LLM_INCOMPLETE_OUTPUT')
|
||||
return await super().generate(config, request, max_response_bytes=max_response_bytes)
|
||||
@@ -401,9 +406,9 @@ def test_third_assessment_can_use_lower_reasoning_without_resetting_history(buil
|
||||
result = asyncio.run(owner.interpret('case-A','stable-package'))
|
||||
assert result.status == 'COMPLETED'
|
||||
assert len(service.database.calls) == 1
|
||||
assert model.efforts == [('entity_resolution_plan_v3','medium'),
|
||||
('entity_resolution_assessment_v3','medium'),('entity_resolution_assessment_v3','medium'),
|
||||
('entity_resolution_assessment_v3','low'),('case_interpretation_resolved_v6','medium')]
|
||||
assert model.efforts == [('entity_resolution_plan_v4','medium'),
|
||||
('entity_resolution_assessment_v4','medium'),('entity_resolution_assessment_v4','medium'),
|
||||
('entity_resolution_assessment_v4','low'),('case_interpretation_resolved_v6','medium')]
|
||||
with PackageReader(result.package) as reader:
|
||||
saved = json.loads(reader.read_bytes('analysis/interpretations/0001/entity-resolution/assessment.json'))
|
||||
assert saved['payload']['attempt'] == 3
|
||||
@@ -426,3 +431,38 @@ def test_completed_interpretation_keeps_its_historical_contract(built_case, vers
|
||||
assert model.requests[-1]['prompt_version']==f'case_interpretation_resolved_v{version}'
|
||||
assert run(store,model).reused
|
||||
assert len(model.requests)==3
|
||||
|
||||
|
||||
def test_v4_assessment_schema_requires_capture_and_original_mention_per_index():
|
||||
from jsonschema import Draft202012Validator
|
||||
from viano_bug_analysis.analysis.entity_resolution import request_for
|
||||
context={'sources':{'O0001':{'kind':'observation','data':{'text':'Missing switch'}}}}
|
||||
plan={'searches':[PLAN['searches'][0],{**PLAN['searches'][0],'term':'Missing switch'}]}
|
||||
request=request_for('assessment',context,policy(SETTINGS),plan=plan,captures=[{},{}],candidates=[],protocol_version=4)
|
||||
validator=Draft202012Validator(request['output_schema'])
|
||||
valid={'resolutions':[{'search_index':i,'status':'no_match','candidate_id':None,
|
||||
'reason':'No rows','source_ids':['O0001',f'V{i+1:04d}']} for i in range(2)]}
|
||||
validator.validate(valid)
|
||||
invalid=copy.deepcopy(valid)
|
||||
invalid['resolutions'][1]['source_ids']=['O0001','V0001']
|
||||
assert list(validator.iter_errors(invalid))
|
||||
invalid=copy.deepcopy(valid)
|
||||
invalid['resolutions'][0]['source_ids']=['V0001']
|
||||
assert list(validator.iter_errors(invalid))
|
||||
|
||||
|
||||
def test_description_matches_prioritize_specific_names_without_automatic_selection():
|
||||
from viano_bug_analysis.analysis.entity_resolution import resolution_guidance
|
||||
context={'sources':{
|
||||
'F0001':{'kind':'fact','data':{'name':'project_description','value':{'texts':[
|
||||
{'column':'title','text':'Find the Wild Sample Creature!'}]}}},
|
||||
'F0002':{'kind':'fact','data':{'name':'project_entity_name','value':{
|
||||
'type':'creature','id':'42','name':'Known quest giver'}}}}}
|
||||
candidates=[{'candidate_id':'K0001','type':'creature','id':'17','name':'Wild Sample Creature'},
|
||||
{'candidate_id':'K0002','type':'creature','id':'18','name':'Sample Creature'}]
|
||||
guidance=resolution_guidance(context,candidates)
|
||||
assert guidance['known_name_bindings'][0]['id']=='42'
|
||||
matches=guidance['candidate_description_matches']
|
||||
assert next(m for m in matches if m['candidate_id']=='K0002')['more_specific_matches']==['K0001']
|
||||
assert next(m for m in matches if m['candidate_id']=='K0001')['more_specific_matches']==[]
|
||||
assert 'selected' not in str(guidance)
|
||||
@@ -123,7 +123,7 @@ def test_repeated_resolution_of_one_id_produces_one_entity_in_complete_pipeline(
|
||||
for term in ['switch','The switch']]}
|
||||
class Repeated(Model):
|
||||
async def generate(self,config,request,*,max_response_bytes):
|
||||
if request['prompt_version']=='entity_resolution_assessment_v3':
|
||||
if request['prompt_version']=='entity_resolution_assessment_v4':
|
||||
self.requests.append(copy.deepcopy(request))
|
||||
data=json.loads(request['messages'][1]['content'])
|
||||
resolutions=[]
|
||||
|
||||
@@ -75,12 +75,12 @@ def test_real_contextual_identity_resolution_with_fixture_llm(service, tmp_path)
|
||||
async def generate(self, config, request, *, max_response_bytes):
|
||||
self.calls.append(request['prompt_version'])
|
||||
data=json.loads(request['messages'][1]['content'])
|
||||
if request['prompt_version']=='entity_resolution_plan_v3':
|
||||
if request['prompt_version']=='entity_resolution_plan_v4':
|
||||
self.mention=next(k for k,v in data['context']['sources'].items() if v['kind']=='fact'
|
||||
and v['data'].get('name')=='project_description'
|
||||
and 'Wild Wolpertinger' in json.dumps(v['data']['value']))
|
||||
doc={'searches':[{'catalog':'creatures','term':'Wild Wolpertinger','role':'affected_subject','source_ids':[self.mention]}]}
|
||||
elif request['prompt_version']=='entity_resolution_assessment_v3':
|
||||
elif request['prompt_version']=='entity_resolution_assessment_v4':
|
||||
candidates=data['candidates']
|
||||
assert len(candidates)==1 and candidates[0]['id']=='23487'
|
||||
assert data['captures']['V0001']['result']['items'][0]['row']['name']=='Wild Wolpertinger'
|
||||
|
||||
Reference in new issue
Block a user