fix: harden task recovery and DR contract handling
This commit is contained in:
@@ -18,7 +18,7 @@ class BrainDemoBackend(DemoBackend):
|
||||
self.brain=BrainService(FixtureBackend(fixture),self.state_dir/'planning_records')
|
||||
def start_planning(self,t):
|
||||
self.plans.append(t)
|
||||
result=self.brain.plan(dict(task_id=t['task_id'],task_revision=t['task_revision'],planning_generation=t['planning_generation'],instruction=t['request']['instruction'],known_info=t['request']['known_info'],context=self.site,constraints={'schema_version':2,'route':self.site['execution_route']},timeout=10))
|
||||
result=self.brain.plan(dict(task_id=t['task_id'],task_revision=t['task_revision'],planning_generation=t['planning_generation'],instruction=t['request']['instruction'],known_info=t['request']['known_info'],context=self.site,constraints={'schema_version':2,'route':self.site['execution_route'],'clarification_confirmed':t.get('clarification_confirmed') is True},timeout=10))
|
||||
event=dict(type='plan',task_id=t['task_id'],task_revision=t['task_revision'],planning_generation=t['planning_generation'],status=result['status'],error_code=result.get('error_code',''),planning_record_ref=result['record_ref'])
|
||||
if result['status']=='NEEDS_CLARIFICATION':event['questions']=result['plan']['missing_information']
|
||||
else:event['plan']=result.get('plan')
|
||||
|
||||
@@ -1,30 +1,4 @@
|
||||
"""Conservative independent intent gate for instructions without confirmed slots.
|
||||
Only registered aliases and explicit quantities are accepted. Other language asks
|
||||
for structured clarification; this parser is not claimed to cover arbitrary NLP.
|
||||
"""
|
||||
import re
|
||||
COUNTS={'一':1,'二':2,'两':2,'三':3,'四':4,'五':5,'六':6,'七':7,'八':8,'九':9,'十':10,'one':1,'two':2,'three':3}
|
||||
def matches(instruction,registry,aliases):
|
||||
candidates=[]
|
||||
for key in registry:
|
||||
for term in [key]+list(aliases.get(key,[])):
|
||||
if not term:continue
|
||||
for m in re.finditer(re.escape(term),instruction):candidates.append((m.start(),m.end(),key))
|
||||
chosen=[]
|
||||
for item in sorted(candidates,key=lambda x:(-(x[1]-x[0]),x[0])):
|
||||
if not any(item[0]<q[1] and q[0]<item[1] for q in chosen):chosen.append(item)
|
||||
return sorted(chosen)
|
||||
def extract(instruction,site):
|
||||
names=site.get('object_locations',{}) or site.get('object_aliases',{})
|
||||
objects=matches(instruction,names,site.get('object_aliases',{}))
|
||||
sources=matches(instruction,site.get('sources',{}),site.get('source_aliases',{}))
|
||||
destinations=matches(instruction,site.get('destinations',{}),site.get('destination_aliases',{}))
|
||||
if not objects or len({x[2] for x in sources})!=1 or len({x[2] for x in destinations})!=1:return None
|
||||
items=[]
|
||||
for start,end,name in objects:
|
||||
m=re.search(r'(\d+|一|二|两|三|四|五|六|七|八|九|十|one|two|three)\s*(?:瓶|个|件|盒|袋)?\s*$',instruction[:start])
|
||||
if not m:return None
|
||||
count=COUNTS.get(m[1],int(m[1]) if m[1].isdigit() else 0)
|
||||
if not 1<=count<=20:return None
|
||||
items.append(dict(target_name=name,quantity=count,source_location=sources[0][2]))
|
||||
return dict(items=items,destination=destinations[0][2])
|
||||
"""Compatibility imports for the shared, conservative intent checker."""
|
||||
from robot_bt_coordinator.intent import COUNTS, matches, extract, conflicts_with_instruction
|
||||
|
||||
__all__ = ['COUNTS', 'matches', 'extract', 'conflicts_with_instruction']
|
||||
|
||||
@@ -4,10 +4,28 @@ loader is a deployment-pinned callable returning the already loaded model.
|
||||
No guessed vendor import, checkpoint download, remote code execution flag or CUDA map.
|
||||
"""
|
||||
import importlib
|
||||
import re
|
||||
|
||||
def create(config):
|
||||
if not isinstance(config,dict) or not isinstance(config.get('loader'),str) or not re.fullmatch(r'[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*:[A-Za-z_]\w*',config['loader']):
|
||||
raise ValueError('loader must name a deployment-provided module:callable')
|
||||
mapping=config.get('task_mapping')
|
||||
if not isinstance(mapping,dict) or not mapping or any(not isinstance(value,str) or not value.strip() for value in mapping.values()):
|
||||
raise ValueError('task_mapping must contain deployment-validated model tasks')
|
||||
if 'dense_feedback' in mapping:
|
||||
raise ValueError('dense feedback requires a deployment-provided multi-frame adapter; this adapter accepts a single image')
|
||||
if set(mapping)-{'plan','shelf','localize3d'}:
|
||||
raise ValueError('unsupported capability in task_mapping')
|
||||
if not isinstance(config.get('model'),dict):
|
||||
raise ValueError('model must contain deployment-provided loading configuration')
|
||||
module,name=config['loader'].split(':',1)
|
||||
model=getattr(importlib.import_module(module),name)(config['model'])
|
||||
try:loader=getattr(importlib.import_module(module),name)
|
||||
except (ImportError,AttributeError) as ex:
|
||||
raise ValueError('deployment model loader unavailable: '+config['loader']) from ex
|
||||
if not callable(loader):raise ValueError('deployment model loader is not callable')
|
||||
model=loader(config['model'])
|
||||
if not callable(getattr(model,'inference',None)):
|
||||
raise ValueError('deployment model must expose inference(prompt, image, task=...)')
|
||||
def infer(request):
|
||||
capability=request['capability']
|
||||
task=config.get('task_mapping',{}).get(capability)
|
||||
|
||||
@@ -37,7 +37,9 @@ class BrainService:
|
||||
if type(timeout) not in (int,float) or not math.isfinite(timeout) or not 0<timeout<=3600:raise InferenceError('INVALID_TIMEOUT')
|
||||
request=dict(capability=capability,prompt=prompt,input=goal)
|
||||
if observation:request['observation']=observation
|
||||
raw=self.backend.infer(request,goal['timeout'],cancel)
|
||||
try:raw=self.backend.infer(request,goal['timeout'],cancel)
|
||||
except InferenceError:raise
|
||||
except Exception as ex:raise InferenceError('INFERENCE_FAILED',str(ex)) from ex
|
||||
if cancel is not None and cancel.is_set():raise InferenceError('CANCELED')
|
||||
if not isinstance(raw,str) or len(raw.encode())>262144:raise InferenceError('OUTPUT_TOO_LARGE')
|
||||
return raw
|
||||
@@ -49,6 +51,10 @@ class BrainService:
|
||||
if type(goal.get(key)) is not int or goal[key]<1:raise InferenceError('INVALID_INPUT')
|
||||
if not goal.get('task_id') or goal['constraints'].get('route') not in ROUTES:raise InferenceError('INVALID_INPUT')
|
||||
known=validate_known(goal.get('known_info',{}))
|
||||
from .intent import conflicts_with_instruction
|
||||
confirmed=(goal['task_revision']>1 and goal['constraints'].get('clarification_confirmed') is True)
|
||||
if conflicts_with_instruction(goal['instruction'],known,goal.get('context',{}),clarification_confirmed=confirmed):
|
||||
raise InferenceError('SEMANTIC_MISMATCH','structured information contradicts explicit instruction')
|
||||
complete=('items' in known and 'destination' in known) or all(k in known for k in ('target_name','quantity','source_location','destination'))
|
||||
if not complete:
|
||||
from .intent import extract
|
||||
@@ -71,6 +77,8 @@ class BrainService:
|
||||
try:plan=validate_plan(data)
|
||||
except ApiError as ex:raise InferenceError('PLAN_INVALID',str(ex))
|
||||
if plan['schema_version']!=2 or plan['route']!=goal['constraints']['route']:raise InferenceError('PLAN_INVALID')
|
||||
if conflicts_with_instruction(goal['instruction'],plan['slots'],goal.get('context',{}),clarification_confirmed=confirmed):
|
||||
raise InferenceError('SEMANTIC_MISMATCH','model plan contradicts explicit instruction')
|
||||
# Explicit user-confirmed structured slots are an independent check.
|
||||
expected=plan['slots']
|
||||
if 'items' not in known and known:
|
||||
@@ -90,7 +98,7 @@ class BrainService:
|
||||
obs=self._snapshot(obs)
|
||||
raw=self._call('shelf',goal,'Identify target in this one registered shelf. JSON only {status:SUCCEEDED|NOT_FOUND|AMBIGUOUS,shelf_id,side_id,column_id,tier_id,confidence}. Never guess missing row/column. Target and input: '+canonical(goal),cancel,obs)
|
||||
data=strict_json(raw)
|
||||
if data.get('status') in ('NOT_FOUND','AMBIGUOUS'):result=dict(status=data['status'],error_code=data['status'])
|
||||
if data.get('status') in ('NOT_FOUND','AMBIGUOUS'):result=dict(status=data['status'],error_code=data['status'],observation_id=observation.observation_id,observed_at=observation.stamp_ns)
|
||||
else:
|
||||
if set(data)!={'status','shelf_id','side_id','column_id','tier_id','confidence'} or data['status']!='SUCCEEDED' or data['shelf_id']!=observation.shelf_id or any(not isinstance(data[k],str) or not data[k].strip() or len(data[k])>100 for k in ('side_id','column_id')) or not isinstance(data['tier_id'],str) or len(data['tier_id'])>100 or (data['tier_id'] and not data['tier_id'].strip()):raise InferenceError('SHELF_INVALID')
|
||||
c=data['confidence']
|
||||
@@ -106,7 +114,8 @@ class BrainService:
|
||||
obs=self._snapshot(obs)
|
||||
raw=self._call('localize3d',goal,'Diagnostic object-center estimate only, never grasp point or navigation pose. Return JSON {point:[x,y,z]} or {status:NOT_FOUND|AMBIGUOUS}. Input: '+canonical(goal),cancel,obs)
|
||||
data=strict_json(raw)
|
||||
if data.get('status') in ('NOT_FOUND','AMBIGUOUS'):result=dict(status=data['status'],geometry_valid=False)
|
||||
if data.get('status') in ('NOT_FOUND','AMBIGUOUS'):
|
||||
result=dict(status=data['status'],geometry_valid=False,error_code=data['status'],observation_id=observation.observation_id,observed_at=observation.stamp_ns,calibration_id=observation.calibration_id,geometry_epoch=observation.geometry_epoch)
|
||||
else:
|
||||
point=data['point']
|
||||
if set(data)!={'point'} or not isinstance(point,list) or len(point)!=3 or any(type(v) not in (int,float) or not math.isfinite(v) for v in point):raise InferenceError('POINT_INVALID')
|
||||
|
||||
Reference in New Issue
Block a user