feat: integrate safer vehicle CAN reasoning
This commit is contained in:
@@ -53,6 +53,12 @@ from cognitive_agent.application.model_plan_adapter import ( # noqa: E402
|
||||
SEAT_MOTION_CAPABILITIES,
|
||||
)
|
||||
from cognitive_agent.application.native_can_intent import merge_native_can_intent # noqa: E402
|
||||
from cognitive_agent.application.native_can_relative import ( # noqa: E402
|
||||
NativeCanRelativeResolver,
|
||||
)
|
||||
from cognitive_agent.application.semantic_ir import ( # noqa: E402
|
||||
apply_language_safety_guards,
|
||||
)
|
||||
from cognitive_agent.application.native_can_feedback import ( # noqa: E402
|
||||
VehicleSafetyPreflightError,
|
||||
authoritative_physical_ch1_feedback_state,
|
||||
@@ -75,6 +81,7 @@ from cognitive_agent.application.zcan_seat_executor import ( # noqa: E402
|
||||
from cognitive_agent.infrastructure.llm.openai_responses import ( # noqa: E402
|
||||
OpenAIIntentError,
|
||||
OpenAIIntentInterpreter,
|
||||
REASONING_EFFORTS,
|
||||
)
|
||||
from cognitive_agent.infrastructure.can.hardware import ( # noqa: E402
|
||||
HardwareSafetyError,
|
||||
@@ -202,6 +209,7 @@ class SeatDemoService:
|
||||
self.hardware_settings = SeatHardwareSettings.from_env(repository_root)
|
||||
self.vehicle_can_settings = VehicleCanSettings.from_env(repository_root)
|
||||
self.native_can_runtime = NativeCanRuntime(repository_root)
|
||||
self.native_can_relative_resolver = NativeCanRelativeResolver(repository_root)
|
||||
self.hardware_execution = PHYSICAL_BENCH_COORDINATOR
|
||||
self.hardware_access = PHYSICAL_BENCH_ACCESS
|
||||
# Shared by the short-lived hardware executor instances. A process must
|
||||
@@ -217,6 +225,16 @@ class SeatDemoService:
|
||||
repository_root, {"seat", *self.domain_runtimes}
|
||||
)
|
||||
self.capability_context = self.capability_registry.snapshot()
|
||||
self.semantic_capability_metadata = {
|
||||
capability["name"]: {
|
||||
"risk_level": capability.get("risk_level", "high"),
|
||||
"confirmation_policy": capability.get(
|
||||
"confirmation_policy", "on_ambiguity"
|
||||
),
|
||||
}
|
||||
for domain in self.capability_context["domains"]
|
||||
for capability in domain["capabilities"]
|
||||
}
|
||||
self.model_plan_adapter = ModelPlanAdapter(self.capability_context)
|
||||
self.vehicle_profiles = VehicleProfileRegistry(repository_root / "config" / "vehicles")
|
||||
self.mode_profiles = ModeProfileRegistry(repository_root / "config" / "modes")
|
||||
@@ -316,6 +334,9 @@ class SeatDemoService:
|
||||
},
|
||||
"openai_available": self.openai.available,
|
||||
"openai_model": self.openai.model,
|
||||
"openai_models": self.openai.model_catalog,
|
||||
"openai_reasoning_effort_default": self.openai.reasoning_effort,
|
||||
"openai_reasoning_efforts": list(REASONING_EFFORTS),
|
||||
"openai_responses_endpoint": self.openai.responses_endpoint,
|
||||
"conversation_memory": True,
|
||||
"default_vehicle_id": default_vehicle.id,
|
||||
@@ -633,6 +654,7 @@ class SeatDemoService:
|
||||
*,
|
||||
hardware_client_id: str | None = None,
|
||||
hardware_access_bypass: bool = True,
|
||||
reasoning_effort: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
# Creating a reusable scene is a configuration workflow. Synthesize
|
||||
# and validate it in dry-run even when the console currently targets
|
||||
@@ -649,6 +671,7 @@ class SeatDemoService:
|
||||
approval_token,
|
||||
vehicle_id,
|
||||
simulation_fault,
|
||||
reasoning_effort,
|
||||
)
|
||||
if hardware_access_bypass or self.driver_only_ch1_execution_active:
|
||||
with self.hardware_execution.reserve():
|
||||
@@ -660,6 +683,7 @@ class SeatDemoService:
|
||||
approval_token,
|
||||
vehicle_id,
|
||||
simulation_fault,
|
||||
reasoning_effort,
|
||||
)
|
||||
authorized_client_id = self.hardware_access.ensure_authorized(hardware_client_id)
|
||||
with self.hardware_execution.reserve():
|
||||
@@ -672,6 +696,7 @@ class SeatDemoService:
|
||||
approval_token,
|
||||
vehicle_id,
|
||||
simulation_fault,
|
||||
reasoning_effort,
|
||||
)
|
||||
|
||||
def _process_once(
|
||||
@@ -683,6 +708,7 @@ class SeatDemoService:
|
||||
approval_token: str | None = None,
|
||||
vehicle_id: str | None = None,
|
||||
simulation_fault: str = "normal",
|
||||
reasoning_effort: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
utterance = utterance.strip()
|
||||
if not utterance:
|
||||
@@ -691,6 +717,11 @@ class SeatDemoService:
|
||||
raise ValueError("utterance must be at most 1000 characters")
|
||||
if provider not in {"auto", "openai", "local"}:
|
||||
raise ValueError("provider must be auto, openai, or local")
|
||||
effective_reasoning_effort = (
|
||||
self.openai.reasoning_effort
|
||||
if reasoning_effort is None
|
||||
else self.openai.validate_reasoning_effort(reasoning_effort)
|
||||
)
|
||||
if execution_mode not in {"dry_run", "simulated_hardware", "hardware"}:
|
||||
raise ValueError(
|
||||
"execution_mode must be dry_run, simulated_hardware, or hardware"
|
||||
@@ -755,6 +786,8 @@ class SeatDemoService:
|
||||
"conversation_turn": context["turn_count"],
|
||||
"status": "processing",
|
||||
"provider_requested": provider,
|
||||
"reasoning_effort_requested": effective_reasoning_effort,
|
||||
"reasoning_effort_used": None,
|
||||
"execution_mode": execution_mode,
|
||||
"hardware_execution_allowed": (
|
||||
hardware_request and not control_plane_request
|
||||
@@ -828,6 +861,7 @@ class SeatDemoService:
|
||||
provider,
|
||||
context,
|
||||
vehicle_profile,
|
||||
reasoning_effort=reasoning_effort,
|
||||
)
|
||||
if self._is_dynamic_mode_stop_request(utterance):
|
||||
parsed = self._apply_dynamic_mode_stop_request(parsed, utterance)
|
||||
@@ -835,7 +869,25 @@ class SeatDemoService:
|
||||
parsed = self._apply_mode_profiles(
|
||||
parsed, matched_modes, context, vehicle_profile
|
||||
)
|
||||
parsed = merge_native_can_intent(parsed, utterance)
|
||||
relative_resolution = (
|
||||
self.native_can_relative_resolver.resolve(utterance, context)
|
||||
if self.native_can_relative_resolver.matches(utterance)
|
||||
else None
|
||||
)
|
||||
parsed = merge_native_can_intent(
|
||||
parsed,
|
||||
utterance,
|
||||
relative_actions=(
|
||||
relative_resolution.actions if relative_resolution else None
|
||||
),
|
||||
relative_ambiguities=(
|
||||
relative_resolution.ambiguities if relative_resolution else None
|
||||
),
|
||||
relative_claimed_controls=(
|
||||
relative_resolution.claimed_controls if relative_resolution else None
|
||||
),
|
||||
)
|
||||
parsed = apply_language_safety_guards(parsed, utterance)
|
||||
parsed = self._partition_native_actions(
|
||||
parsed,
|
||||
promote_supported=(hardware_request and self.vehicle_can_settings.enabled),
|
||||
@@ -891,10 +943,43 @@ class SeatDemoService:
|
||||
if provider_notice:
|
||||
parsed["decision_summary"].append(provider_notice)
|
||||
result["provider_used"] = provider_used
|
||||
if provider_used.startswith("openai:"):
|
||||
result["reasoning_effort_used"] = effective_reasoning_effort
|
||||
result["decision_summary"] = parsed["decision_summary"]
|
||||
result["deferred_requests"] = parsed.get("deferred_requests", [])
|
||||
result["model_requests"] = parsed.get("model_requests", [])
|
||||
|
||||
if (
|
||||
parsed.get("semantic_constraints")
|
||||
and not parsed.get("actions")
|
||||
and not parsed.get("domain_actions")
|
||||
and not parsed.get("native_can_actions")
|
||||
and not parsed.get("ambiguities")
|
||||
and not parsed.get("unresolved_spans")
|
||||
):
|
||||
intent_ir = build_intent_ir(
|
||||
parsed,
|
||||
utterance,
|
||||
request_id,
|
||||
provider_used,
|
||||
self.semantic_capability_metadata,
|
||||
)
|
||||
result["intent_ir"] = intent_ir
|
||||
result["validation"] = self.contracts.validate(intent_ir)
|
||||
result["status"] = "no_action"
|
||||
self._trace(
|
||||
traces,
|
||||
request_id,
|
||||
"safety_check",
|
||||
"succeeded",
|
||||
"检测到禁止或保持约束;未生成、编译或发送任何 CAN 动作。",
|
||||
time.perf_counter(),
|
||||
{"constraint_count": len(intent_ir["spec"]["constraints"]), "execution_eligible": False},
|
||||
domain="orchestration",
|
||||
)
|
||||
self._persist(result)
|
||||
return result
|
||||
|
||||
context_started = time.perf_counter()
|
||||
self._trace(
|
||||
traces,
|
||||
@@ -960,7 +1045,13 @@ class SeatDemoService:
|
||||
vehicle_profile=vehicle_profile,
|
||||
)
|
||||
|
||||
intent_ir = build_intent_ir(parsed, utterance, request_id, provider_used)
|
||||
intent_ir = build_intent_ir(
|
||||
parsed,
|
||||
utterance,
|
||||
request_id,
|
||||
provider_used,
|
||||
self.semantic_capability_metadata,
|
||||
)
|
||||
result["intent_ir"] = intent_ir
|
||||
generation_started = time.perf_counter()
|
||||
function_config = build_function_config(parsed, request_id, provider_used)
|
||||
@@ -2791,7 +2882,13 @@ class SeatDemoService:
|
||||
"""Run isolated adapters under the selected physical-bus policy."""
|
||||
vehicle_profile = vehicle_profile or self.vehicle_profiles.default()
|
||||
generation_started = time.perf_counter()
|
||||
intent_ir = build_intent_ir(parsed, utterance, request_id, provider_used)
|
||||
intent_ir = build_intent_ir(
|
||||
parsed,
|
||||
utterance,
|
||||
request_id,
|
||||
provider_used,
|
||||
self.semantic_capability_metadata,
|
||||
)
|
||||
intent_actions = [
|
||||
*parsed.get("domain_actions", []),
|
||||
*parsed.get("native_can_actions", []),
|
||||
@@ -2823,6 +2920,47 @@ class SeatDemoService:
|
||||
)
|
||||
result["intent_ir"] = intent_ir
|
||||
|
||||
if intent_ir["spec"]["requires_confirmation"]:
|
||||
validation = self.contracts.validate(intent_ir)
|
||||
validation.extend(
|
||||
{
|
||||
"severity": "error",
|
||||
"code": "AMBIGUOUS_INTENT",
|
||||
"path": item.get("field", "utterance"),
|
||||
"message": item.get("question", "控制目标需要确认。"),
|
||||
}
|
||||
for item in intent_ir["spec"]["ambiguities"]
|
||||
)
|
||||
validation.extend(
|
||||
{
|
||||
"severity": "error",
|
||||
"code": "UNRESOLVED_UTTERANCE_SPAN",
|
||||
"path": "utterance",
|
||||
"message": (
|
||||
f"未解析原文“{item['text']}”:{item['reason']}。"
|
||||
),
|
||||
}
|
||||
for item in intent_ir["spec"]["unresolved_spans"]
|
||||
)
|
||||
result["validation"] = validation
|
||||
result["status"] = "needs_confirmation"
|
||||
self._trace(
|
||||
traces,
|
||||
request_id,
|
||||
"safety_check",
|
||||
"failed",
|
||||
"语义意图存在歧义或未覆盖片段;未进入 CAN 编译与执行。",
|
||||
time.perf_counter(),
|
||||
{
|
||||
"ambiguity_count": len(intent_ir["spec"]["ambiguities"]),
|
||||
"unresolved_span_count": len(intent_ir["spec"]["unresolved_spans"]),
|
||||
"execution_eligible": False,
|
||||
},
|
||||
domain="orchestration",
|
||||
)
|
||||
self._persist(result)
|
||||
return result
|
||||
|
||||
configs: dict[str, dict[str, Any]] = {}
|
||||
if parsed.get("actions"):
|
||||
seat_config = build_function_config(parsed, request_id, provider_used)
|
||||
@@ -3531,6 +3669,8 @@ class SeatDemoService:
|
||||
provider: str,
|
||||
context: dict[str, Any],
|
||||
vehicle_profile: VehicleProfile | None = None,
|
||||
*,
|
||||
reasoning_effort: str | None = None,
|
||||
) -> tuple[dict[str, Any], str, str | None]:
|
||||
vehicle_profile = vehicle_profile or self.vehicle_profiles.default()
|
||||
if provider == "local":
|
||||
@@ -3539,7 +3679,15 @@ class SeatDemoService:
|
||||
if not self.openai.available:
|
||||
raise ValueError("GPT 模式需要设置 OPENAI_API_KEY")
|
||||
try:
|
||||
plan = self.openai.interpret(utterance, context, self.capability_context)
|
||||
interpret_options = {}
|
||||
if reasoning_effort is not None:
|
||||
interpret_options["reasoning_effort"] = reasoning_effort
|
||||
plan = self.openai.interpret(
|
||||
utterance,
|
||||
context,
|
||||
self.capability_context,
|
||||
**interpret_options,
|
||||
)
|
||||
return (
|
||||
self.model_plan_adapter.adapt(
|
||||
plan,
|
||||
@@ -3558,7 +3706,15 @@ class SeatDemoService:
|
||||
)
|
||||
if self.openai.available:
|
||||
try:
|
||||
plan = self.openai.interpret(utterance, context, self.capability_context)
|
||||
interpret_options = {}
|
||||
if reasoning_effort is not None:
|
||||
interpret_options["reasoning_effort"] = reasoning_effort
|
||||
plan = self.openai.interpret(
|
||||
utterance,
|
||||
context,
|
||||
self.capability_context,
|
||||
**interpret_options,
|
||||
)
|
||||
return (
|
||||
self.model_plan_adapter.adapt(
|
||||
plan,
|
||||
|
||||
Reference in New Issue
Block a user