feat: integrate NFC infrared peripherals and orchestration
This commit is contained in:
+219
-59
@@ -38,6 +38,8 @@ from cognitive_agent.application.self_extension import ( # noqa: E402
|
||||
ExtensionIntegrationError,
|
||||
GatewayError,
|
||||
SelfExtensionService,
|
||||
active_peripheral_plan_catalog,
|
||||
resolve_active_peripheral_command,
|
||||
)
|
||||
from cognitive_agent.application.self_repair import SelfRepairService # noqa: E402
|
||||
from cognitive_agent.infrastructure.can.hardware import ( # noqa: E402
|
||||
@@ -48,6 +50,7 @@ from cognitive_agent.infrastructure.can.access import ( # noqa: E402
|
||||
HardwareAccessRequiredError,
|
||||
)
|
||||
from cognitive_agent.infrastructure.llm.openai_responses import OpenAIIntentError # noqa: E402
|
||||
from cognitive_agent.infrastructure.nfc import NfcReaderService # noqa: E402
|
||||
|
||||
|
||||
STATIC_ROOT = REPOSITORY_ROOT / "apps" / "web-console"
|
||||
@@ -140,6 +143,15 @@ class VehicleDisplaySync:
|
||||
payload["extension_health"] = self._safe_copy(health)
|
||||
return payload
|
||||
|
||||
def extension_catalog(self) -> list[dict[str, object]]:
|
||||
with self._lock:
|
||||
extension = self._snapshot.get("extension", {})
|
||||
catalog = extension.get("catalog", []) if isinstance(extension, dict) else []
|
||||
payload = self._safe_copy(catalog)
|
||||
if not isinstance(payload, list):
|
||||
return []
|
||||
return [item for item in payload if isinstance(item, dict)]
|
||||
|
||||
|
||||
def _lan_ipv4_address() -> str | None:
|
||||
"""Return the IPv4 address selected by the host's default route."""
|
||||
@@ -158,6 +170,7 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
service = SeatDemoService()
|
||||
gesture = GestureCapabilityBridge(REPOSITORY_ROOT)
|
||||
extension = SelfExtensionService()
|
||||
nfc = NfcReaderService()
|
||||
calibration = CalibrationService(
|
||||
REPOSITORY_ROOT / "artifacts" / "tmp" / "self-calibration",
|
||||
repository_root=REPOSITORY_ROOT,
|
||||
@@ -231,7 +244,7 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
self._json(self.service.runtime_info())
|
||||
return
|
||||
if path == "/api/vehicle-display-sync":
|
||||
self._json(self.vehicle_display_sync.snapshot(self.extension.health()))
|
||||
self._json(self.vehicle_display_sync.snapshot(self._extension_health()))
|
||||
return
|
||||
if path in {"/api/hardware/seat-state", "/api/hardware/vehicle-state"}:
|
||||
query = parse_qs(parsed_url.query)
|
||||
@@ -296,7 +309,10 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
self._serve_gesture_model()
|
||||
return
|
||||
if path == "/api/extension/health":
|
||||
self._json(self.extension.health())
|
||||
self._json(self._extension_health())
|
||||
return
|
||||
if path == "/api/extension/nfc/status":
|
||||
self._json({"success": True, **self.nfc.snapshot()})
|
||||
return
|
||||
if path == "/api/extension/gscu/info":
|
||||
self._handle_extension_gateway("info", {})
|
||||
@@ -434,17 +450,18 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
if payload.get("reasoning_effort") is not None
|
||||
else None
|
||||
)
|
||||
provider = str(payload.get("provider", "auto"))
|
||||
execution_mode = str(payload.get("execution_mode", "dry_run"))
|
||||
conversation_id = (
|
||||
str(payload["conversation_id"])
|
||||
if payload.get("conversation_id")
|
||||
else None
|
||||
)
|
||||
common_arguments = {
|
||||
"utterance": utterance,
|
||||
"provider": str(payload.get("provider", "auto")),
|
||||
"conversation_id": (
|
||||
str(payload["conversation_id"])
|
||||
if payload.get("conversation_id")
|
||||
else None
|
||||
),
|
||||
"execution_mode": str(
|
||||
payload.get("execution_mode", "dry_run")
|
||||
),
|
||||
"provider": provider,
|
||||
"conversation_id": conversation_id,
|
||||
"execution_mode": execution_mode,
|
||||
# Hardware authorization is a server deployment policy. The
|
||||
# browser never receives or transmits this secret.
|
||||
"approval_token": os.getenv(
|
||||
@@ -503,25 +520,35 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
payload.get("control_transport", "wired_can")
|
||||
),
|
||||
)
|
||||
elif path == "/api/requests/preview":
|
||||
result = self.service.preview_native_can(**common_arguments)
|
||||
elif supplement_match:
|
||||
result = self.service.supplement_native_can(
|
||||
supplement_match.group("request_id"),
|
||||
provider=(
|
||||
str(payload["provider"])
|
||||
if payload.get("provider")
|
||||
else None
|
||||
),
|
||||
approval_token=os.getenv(
|
||||
"COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN"
|
||||
),
|
||||
hardware_client_id=hardware_client_id,
|
||||
hardware_access_bypass=self._is_unproxied_loopback_request(),
|
||||
reasoning_effort=reasoning_effort,
|
||||
)
|
||||
else:
|
||||
result = self.service.process(**common_arguments)
|
||||
result = None
|
||||
if path in {"/api/requests", "/api/requests/preview"}:
|
||||
result = self._execute_active_peripheral_request(
|
||||
utterance,
|
||||
provider=provider,
|
||||
conversation_id=conversation_id,
|
||||
execution_mode=execution_mode,
|
||||
)
|
||||
if result is None:
|
||||
if path == "/api/requests/preview":
|
||||
result = self.service.preview_native_can(**common_arguments)
|
||||
elif supplement_match:
|
||||
result = self.service.supplement_native_can(
|
||||
supplement_match.group("request_id"),
|
||||
provider=(
|
||||
str(payload["provider"])
|
||||
if payload.get("provider")
|
||||
else None
|
||||
),
|
||||
approval_token=os.getenv(
|
||||
"COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN"
|
||||
),
|
||||
hardware_client_id=hardware_client_id,
|
||||
hardware_access_bypass=self._is_unproxied_loopback_request(),
|
||||
reasoning_effort=reasoning_effort,
|
||||
)
|
||||
else:
|
||||
result = self.service.process(**common_arguments)
|
||||
utterance = str(result.get("utterance") or utterance)
|
||||
self.vehicle_display_sync.update_task(
|
||||
status=str(result.get("status", "succeeded")),
|
||||
@@ -557,6 +584,18 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
{"error": "hardware_safety_gate", "message": str(exc)},
|
||||
HTTPStatus.FORBIDDEN,
|
||||
)
|
||||
except GatewayError as exc:
|
||||
fail_display_sync("gscu_unavailable", exc)
|
||||
self._json(
|
||||
{"error": "gscu_unavailable", "message": str(exc)},
|
||||
HTTPStatus.SERVICE_UNAVAILABLE,
|
||||
)
|
||||
except ExtensionIntegrationError as exc:
|
||||
fail_display_sync("extension_request_ambiguous", exc)
|
||||
self._json(
|
||||
{"error": "extension_request_ambiguous", "message": str(exc)},
|
||||
HTTPStatus.UNPROCESSABLE_ENTITY,
|
||||
)
|
||||
except (ValueError, json.JSONDecodeError) as exc:
|
||||
fail_display_sync("invalid_request", exc)
|
||||
self._json({"error": "invalid_request", "message": str(exc)}, HTTPStatus.BAD_REQUEST)
|
||||
@@ -581,6 +620,94 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
def _execute_active_peripheral_request(
|
||||
self,
|
||||
utterance: str,
|
||||
*,
|
||||
provider: str,
|
||||
conversation_id: str | None,
|
||||
execution_mode: str,
|
||||
) -> dict[str, object] | None:
|
||||
"""Route a command to the NFC-bound peripheral when one is present."""
|
||||
|
||||
extension_catalog = self.vehicle_display_sync.extension_catalog()
|
||||
nfc_snapshot = self.nfc.snapshot()
|
||||
plan_catalog = active_peripheral_plan_catalog(
|
||||
utterance,
|
||||
extension_catalog,
|
||||
nfc_snapshot,
|
||||
)
|
||||
peripheral = None
|
||||
if not plan_catalog:
|
||||
peripheral = resolve_active_peripheral_command(
|
||||
utterance,
|
||||
extension_catalog,
|
||||
nfc_snapshot,
|
||||
)
|
||||
if not plan_catalog and peripheral is None:
|
||||
return None
|
||||
if conversation_id and not re.fullmatch(
|
||||
r"conv-[a-z0-9-]+", conversation_id
|
||||
):
|
||||
raise ValueError(
|
||||
"conversation_id must start with conv- and contain only "
|
||||
"lowercase letters, numbers, or hyphens"
|
||||
)
|
||||
conversation_turn = 1
|
||||
if conversation_id:
|
||||
context = self.service.conversations.snapshot(conversation_id)
|
||||
conversation_turn = int(context.get("turn_count", 0)) + 1
|
||||
if plan_catalog:
|
||||
plan_result = self.extension.plan(
|
||||
{
|
||||
"utterance": utterance,
|
||||
"peripherals": plan_catalog,
|
||||
"vehicle_devices": [],
|
||||
}
|
||||
)
|
||||
if plan_result.get("needs_confirmation"):
|
||||
raise ExtensionIntegrationError(
|
||||
str(plan_result.get("question") or "请明确要控制的外设。")
|
||||
)
|
||||
result = self.extension.execute_peripheral_plan_request(
|
||||
utterance,
|
||||
plan_result,
|
||||
plan_catalog,
|
||||
conversation_id=conversation_id,
|
||||
conversation_turn=conversation_turn,
|
||||
execution_mode=execution_mode,
|
||||
provider_requested=provider,
|
||||
)
|
||||
self.vehicle_display_sync.update_extension(
|
||||
action={
|
||||
"type": "sequence_emit",
|
||||
"source": "main_orchestration",
|
||||
"step_count": len(plan_result.get("steps", [])),
|
||||
},
|
||||
result=result,
|
||||
)
|
||||
return result
|
||||
|
||||
assert peripheral is not None
|
||||
result = self.extension.execute_peripheral_request(
|
||||
utterance,
|
||||
peripheral,
|
||||
conversation_id=conversation_id,
|
||||
conversation_turn=conversation_turn,
|
||||
execution_mode=execution_mode,
|
||||
provider_requested=provider,
|
||||
)
|
||||
self.vehicle_display_sync.update_extension(
|
||||
action={
|
||||
"type": "emit",
|
||||
"source": "main_orchestration",
|
||||
"slot": peripheral["slot"],
|
||||
"capability_id": peripheral.get("id"),
|
||||
},
|
||||
result=result,
|
||||
)
|
||||
return result
|
||||
|
||||
def _stream_control_request(
|
||||
self,
|
||||
path: str,
|
||||
@@ -620,40 +747,55 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
|
||||
utterance = str(common_arguments.get("utterance") or "")
|
||||
try:
|
||||
if path == "/api/requests/preview":
|
||||
result = self.service.preview_native_can(
|
||||
**common_arguments,
|
||||
on_commands_validated=on_commands_validated,
|
||||
)
|
||||
elif supplement_match:
|
||||
result = self.service.supplement_native_can(
|
||||
supplement_match.group("request_id"),
|
||||
provider=(
|
||||
str(payload["provider"])
|
||||
if payload.get("provider")
|
||||
result = None
|
||||
if path in {"/api/requests", "/api/requests/preview"}:
|
||||
result = self._execute_active_peripheral_request(
|
||||
utterance,
|
||||
provider=str(common_arguments.get("provider") or "auto"),
|
||||
conversation_id=(
|
||||
str(common_arguments["conversation_id"])
|
||||
if common_arguments.get("conversation_id")
|
||||
else None
|
||||
),
|
||||
approval_token=os.getenv(
|
||||
"COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN"
|
||||
execution_mode=str(
|
||||
common_arguments.get("execution_mode") or "dry_run"
|
||||
),
|
||||
hardware_client_id=(
|
||||
str(payload["hardware_client_id"])
|
||||
if payload.get("hardware_client_id")
|
||||
else None
|
||||
),
|
||||
hardware_access_bypass=self._is_unproxied_loopback_request(),
|
||||
reasoning_effort=(
|
||||
str(payload["reasoning_effort"])
|
||||
if payload.get("reasoning_effort") is not None
|
||||
else None
|
||||
),
|
||||
on_commands_validated=on_commands_validated,
|
||||
)
|
||||
else:
|
||||
result = self.service.process(
|
||||
**common_arguments,
|
||||
_on_commands_validated=on_commands_validated,
|
||||
)
|
||||
if result is None:
|
||||
if path == "/api/requests/preview":
|
||||
result = self.service.preview_native_can(
|
||||
**common_arguments,
|
||||
on_commands_validated=on_commands_validated,
|
||||
)
|
||||
elif supplement_match:
|
||||
result = self.service.supplement_native_can(
|
||||
supplement_match.group("request_id"),
|
||||
provider=(
|
||||
str(payload["provider"])
|
||||
if payload.get("provider")
|
||||
else None
|
||||
),
|
||||
approval_token=os.getenv(
|
||||
"COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN"
|
||||
),
|
||||
hardware_client_id=(
|
||||
str(payload["hardware_client_id"])
|
||||
if payload.get("hardware_client_id")
|
||||
else None
|
||||
),
|
||||
hardware_access_bypass=self._is_unproxied_loopback_request(),
|
||||
reasoning_effort=(
|
||||
str(payload["reasoning_effort"])
|
||||
if payload.get("reasoning_effort") is not None
|
||||
else None
|
||||
),
|
||||
on_commands_validated=on_commands_validated,
|
||||
)
|
||||
else:
|
||||
result = self.service.process(
|
||||
**common_arguments,
|
||||
_on_commands_validated=on_commands_validated,
|
||||
)
|
||||
utterance = str(result.get("utterance") or utterance)
|
||||
self.vehicle_display_sync.update_task(
|
||||
status=str(result.get("status", "succeeded")),
|
||||
@@ -700,6 +842,18 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
"error": "hardware_safety_gate",
|
||||
"message": str(exc),
|
||||
})
|
||||
except GatewayError as exc:
|
||||
emit({
|
||||
"type": "error",
|
||||
"error": "gscu_unavailable",
|
||||
"message": str(exc),
|
||||
})
|
||||
except ExtensionIntegrationError as exc:
|
||||
emit({
|
||||
"type": "error",
|
||||
"error": "extension_request_ambiguous",
|
||||
"message": str(exc),
|
||||
})
|
||||
except (ValueError, json.JSONDecodeError) as exc:
|
||||
emit({
|
||||
"type": "error",
|
||||
@@ -1136,6 +1290,9 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
HTTPStatus.SERVICE_UNAVAILABLE,
|
||||
)
|
||||
|
||||
def _extension_health(self) -> dict[str, object]:
|
||||
return {**self.extension.health(), "nfc": self.nfc.snapshot()}
|
||||
|
||||
def _handle_hardware_access_post(self, path: str) -> None:
|
||||
local_path = path.startswith("/api/local/")
|
||||
if local_path and not self._require_local_decision_point():
|
||||
@@ -1464,6 +1621,7 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
"style-src 'self'; "
|
||||
"img-src 'self' data: blob:; "
|
||||
"media-src 'self' blob:; "
|
||||
"frame-src 'self' http://*:3001; "
|
||||
"connect-src 'self' blob: http://172.20.10.6:8889; "
|
||||
"object-src 'none'; base-uri 'none'; frame-ancestors 'none'",
|
||||
)
|
||||
@@ -1482,6 +1640,7 @@ def main() -> None:
|
||||
server = ThreadingHTTPServer((args.host, args.port), SeatDemoHandler)
|
||||
SeatDemoHandler.service.start()
|
||||
SeatDemoHandler.extension.start()
|
||||
SeatDemoHandler.nfc.start()
|
||||
SeatDemoHandler.calibration.start()
|
||||
SeatDemoHandler.repair.start()
|
||||
if args.host in {"0.0.0.0", "::"}:
|
||||
@@ -1520,6 +1679,7 @@ def main() -> None:
|
||||
server.server_close()
|
||||
SeatDemoHandler.service.close()
|
||||
SeatDemoHandler.extension.close()
|
||||
SeatDemoHandler.nfc.close()
|
||||
SeatDemoHandler.calibration.close()
|
||||
SeatDemoHandler.repair.close()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user