fix: harden task recovery and DR contract handling

This commit is contained in:
2026-09-20 13:36:48 +08:00
parent 492676344a
commit f9d8feb6f0
49 changed files with 2083 additions and 165 deletions
+1 -1
View File
@@ -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')
+4 -30
View File
@@ -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']
+19 -1
View File
@@ -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)
+12 -3
View File
@@ -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')