opencastor 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- castor/__init__.py +43 -0
- castor/__main__.py +6 -0
- castor/action_validator.py +295 -0
- castor/agent_tools.py +632 -0
- castor/agents/__init__.py +61 -0
- castor/agents/base.py +138 -0
- castor/agents/communicator.py +197 -0
- castor/agents/guardian.py +233 -0
- castor/agents/manipulator_agent.py +97 -0
- castor/agents/navigator.py +326 -0
- castor/agents/observer.py +311 -0
- castor/agents/orchestrator.py +309 -0
- castor/agents/registry.py +147 -0
- castor/agents/shared_state.py +349 -0
- castor/api.py +10562 -0
- castor/api_errors.py +76 -0
- castor/apikeys.py +196 -0
- castor/approvals.py +221 -0
- castor/attestation_generator.py +334 -0
- castor/audit.py +449 -0
- castor/auth/__init__.py +226 -0
- castor/auth/jwt_pqc.py +156 -0
- castor/auth/m2m_trusted.py +230 -0
- castor/auth/provider_auth.py +353 -0
- castor/auth/robot_handshake.py +104 -0
- castor/auth.py +218 -0
- castor/auth_jwt.py +328 -0
- castor/authority.py +364 -0
- castor/avoidance.py +272 -0
- castor/backup.py +193 -0
- castor/battery.py +132 -0
- castor/behaviors.py +2449 -0
- castor/benchmark.py +164 -0
- castor/benchmarker.py +254 -0
- castor/brain/__init__.py +19 -0
- castor/brain/autodream.py +272 -0
- castor/brain/autodream_issues.py +113 -0
- castor/brain/autodream_runner.py +338 -0
- castor/brain/compaction.py +73 -0
- castor/brain/memory_schema.py +292 -0
- castor/brain/robot_context.py +152 -0
- castor/calibrate.py +389 -0
- castor/camera.py +462 -0
- castor/channels/__init__.py +164 -0
- castor/channels/base.py +382 -0
- castor/channels/discord_channel.py +165 -0
- castor/channels/homeassistant_channel.py +211 -0
- castor/channels/matrix_channel.py +116 -0
- castor/channels/mqtt_channel.py +480 -0
- castor/channels/rcan_mqtt_transport.py +184 -0
- castor/channels/scope_resolver.py +162 -0
- castor/channels/session.py +240 -0
- castor/channels/signal_channel.py +276 -0
- castor/channels/slack_channel.py +201 -0
- castor/channels/teams_channel.py +126 -0
- castor/channels/telegram_channel.py +149 -0
- castor/channels/whatsapp.py +9 -0
- castor/channels/whatsapp_neonize.py +540 -0
- castor/channels/whatsapp_twilio.py +105 -0
- castor/claude_proxy.py +203 -0
- castor/cli.py +9117 -0
- castor/client.py +451 -0
- castor/cloud/__init__.py +3 -0
- castor/cloud/bridge.py +2410 -0
- castor/cloud/consent_manager.py +258 -0
- castor/cloud/firestore_models.py +151 -0
- castor/command_interpreter.py +144 -0
- castor/commands/__init__.py +1 -0
- castor/commands/benchmark.py +385 -0
- castor/commands/compete.py +173 -0
- castor/commands/deploy.py +270 -0
- castor/commands/hub.py +462 -0
- castor/commands/leaderboard.py +167 -0
- castor/commands/recommend.py +378 -0
- castor/commands/research.py +225 -0
- castor/commands/season.py +103 -0
- castor/commands/swarm.py +589 -0
- castor/commands/update.py +286 -0
- castor/competitions/__init__.py +1 -0
- castor/competitions/bracket_season.py +463 -0
- castor/competitions/models.py +335 -0
- castor/competitions/sprint.py +325 -0
- castor/competitions/threshold_race.py +468 -0
- castor/compliance.py +204 -0
- castor/components.py +305 -0
- castor/confidence_gate.py +114 -0
- castor/config_history.py +186 -0
- castor/configure.py +379 -0
- castor/conformance.py +2176 -0
- castor/connectivity.py +132 -0
- castor/context.py +444 -0
- castor/contribute/__init__.py +1 -0
- castor/contribute/coordinator.py +550 -0
- castor/contribute/credits.py +284 -0
- castor/contribute/fleet.py +123 -0
- castor/contribute/hardware_profile.py +25 -0
- castor/contribute/harness_eval.py +374 -0
- castor/contribute/runner.py +177 -0
- castor/contribute/work_unit.py +43 -0
- castor/crash.py +137 -0
- castor/crypto/__init__.py +17 -0
- castor/crypto/pqc.py +345 -0
- castor/daemon.py +657 -0
- castor/dashboard.py +1745 -0
- castor/dashboard_memory_timeline.py +284 -0
- castor/dashboard_tui.py +690 -0
- castor/delegation.py +77 -0
- castor/demo.py +706 -0
- castor/deploy_ota.py +298 -0
- castor/depth.py +222 -0
- castor/detection.py +302 -0
- castor/diff.py +123 -0
- castor/doctor.py +1026 -0
- castor/drivers/__init__.py +195 -0
- castor/drivers/acb_driver.py +700 -0
- castor/drivers/arduino_driver.py +223 -0
- castor/drivers/base.py +166 -0
- castor/drivers/battery_driver.py +550 -0
- castor/drivers/can_transport.py +121 -0
- castor/drivers/composite.py +272 -0
- castor/drivers/dynamixel.py +172 -0
- castor/drivers/esp32_ble_driver.py +261 -0
- castor/drivers/esp32_websocket.py +232 -0
- castor/drivers/ev3dev_driver.py +287 -0
- castor/drivers/feetech_driver.py +358 -0
- castor/drivers/gpio_driver.py +155 -0
- castor/drivers/imu_driver.py +1607 -0
- castor/drivers/ipc.py +246 -0
- castor/drivers/lidar_driver.py +1564 -0
- castor/drivers/odrive_driver.py +328 -0
- castor/drivers/pca9685.py +335 -0
- castor/drivers/picamera2_driver.py +233 -0
- castor/drivers/reachy_driver.py +249 -0
- castor/drivers/ros2_driver.py +188 -0
- castor/drivers/simulation_driver.py +274 -0
- castor/drivers/spike_driver.py +219 -0
- castor/drivers/stepper_driver.py +170 -0
- castor/drivers/thermal_driver.py +251 -0
- castor/drivers/worker.py +29 -0
- castor/dual_model.py +470 -0
- castor/embedding_interpreter.py +577 -0
- castor/episode_search.py +196 -0
- castor/eu_register.py +89 -0
- castor/eval_harness.py +354 -0
- castor/export.py +295 -0
- castor/finetune.py +287 -0
- castor/firmware.py +441 -0
- castor/fix.py +200 -0
- castor/fleet/__init__.py +0 -0
- castor/fleet/group_policy.py +183 -0
- castor/fleet.py +163 -0
- castor/fleet_telemetry.py +508 -0
- castor/fria.py +368 -0
- castor/fs/__init__.py +307 -0
- castor/fs/context.py +331 -0
- castor/fs/memory.py +326 -0
- castor/fs/namespace.py +235 -0
- castor/fs/permissions.py +374 -0
- castor/fs/proc.py +205 -0
- castor/fs/safety.py +822 -0
- castor/generate_sdk.py +457 -0
- castor/geofence.py +301 -0
- castor/gestures.py +263 -0
- castor/hailo_vision.py +417 -0
- castor/hardware/__init__.py +0 -0
- castor/hardware/so_arm101/__init__.py +11 -0
- castor/hardware/so_arm101/assembly_guide.py +120 -0
- castor/hardware/so_arm101/cli.py +387 -0
- castor/hardware/so_arm101/config_generator.py +134 -0
- castor/hardware/so_arm101/constants.py +176 -0
- castor/hardware/so_arm101/lerobot_bridge.py +231 -0
- castor/hardware/so_arm101/motor_setup.py +261 -0
- castor/hardware/so_arm101/port_finder.py +200 -0
- castor/hardware/so_arm101/rcan_bridge.py +91 -0
- castor/hardware/so_arm101/safety_bridge.py +152 -0
- castor/hardware/so_arm101/vision.py +49 -0
- castor/hardware_detect.py +1356 -0
- castor/harness/__init__.py +110 -0
- castor/harness/circuit_breaker.py +120 -0
- castor/harness/context_compressor.py +125 -0
- castor/harness/core.py +975 -0
- castor/harness/cost_meter.py +145 -0
- castor/harness/default_harness.yaml +166 -0
- castor/harness/dlq.py +146 -0
- castor/harness/memory.py +151 -0
- castor/harness/pattern.py +99 -0
- castor/harness/prompt_guard.py +91 -0
- castor/harness/rollback.py +140 -0
- castor/harness/security.py +123 -0
- castor/harness/span_tracer.py +196 -0
- castor/harness/visual_planner.py +367 -0
- castor/harness/working_memory.py +87 -0
- castor/harness.py +986 -0
- castor/healthcheck.py +588 -0
- castor/hitl_gate.py +139 -0
- castor/hooks/__init__.py +5 -0
- castor/hooks/default_hooks.py +79 -0
- castor/hooks/runner.py +99 -0
- castor/hotword.py +325 -0
- castor/hub.py +605 -0
- castor/i18n.py +314 -0
- castor/idle.py +385 -0
- castor/ina219.py +154 -0
- castor/incidents.py +111 -0
- castor/inference/__init__.py +5 -0
- castor/inference/streaming.py +287 -0
- castor/init_config.py +211 -0
- castor/init_wizard.py +142 -0
- castor/instructions_for_use.py +135 -0
- castor/learn.py +216 -0
- castor/learner/__init__.py +58 -0
- castor/learner/alma.py +227 -0
- castor/learner/apply_stage.py +226 -0
- castor/learner/dev_stage.py +167 -0
- castor/learner/episode.py +52 -0
- castor/learner/episode_store.py +151 -0
- castor/learner/patches.py +150 -0
- castor/learner/pm_stage.py +318 -0
- castor/learner/qa_stage.py +197 -0
- castor/learner/sisyphus.py +171 -0
- castor/lint.py +328 -0
- castor/llmfit.py +493 -0
- castor/llmfit_helper.py +362 -0
- castor/loa.py +132 -0
- castor/logs.py +190 -0
- castor/logs_viewer.py +126 -0
- castor/main.py +1854 -0
- castor/mcp_auth.py +104 -0
- castor/mcp_fleet.py +262 -0
- castor/mcp_server.py +780 -0
- castor/memory/__init__.py +20 -0
- castor/memory/compaction.py +129 -0
- castor/memory/consolidator.py +501 -0
- castor/memory/episode.py +1584 -0
- castor/memory/episodic.py +169 -0
- castor/memory/replay.py +324 -0
- castor/memory_search.py +285 -0
- castor/mesh.py +429 -0
- castor/metrics.py +1087 -0
- castor/migrate.py +527 -0
- castor/mission.py +384 -0
- castor/nav.py +97 -0
- castor/network.py +228 -0
- castor/offline_fallback.py +200 -0
- castor/optimizer.py +635 -0
- castor/orchestration/__init__.py +0 -0
- castor/orchestration/thread_context.py +60 -0
- castor/peripherals.py +1093 -0
- castor/personalities.py +284 -0
- castor/plugins.py +468 -0
- castor/pointcloud.py +235 -0
- castor/privacy.py +163 -0
- castor/privacy_mode.py +129 -0
- castor/profiles/__init__.py +215 -0
- castor/profiles/arduino/__init__.py +0 -0
- castor/profiles/arduino/uno.yaml +13 -0
- castor/profiles/coral/__init__.py +0 -0
- castor/profiles/coral/tpu-inference.yaml +16 -0
- castor/profiles/hlabs/__init__.py +1 -0
- castor/profiles/hlabs/acb-arm-3dof.yaml +35 -0
- castor/profiles/hlabs/acb-biped-6dof.yaml +65 -0
- castor/profiles/hlabs/acb-single.yaml +21 -0
- castor/profiles/lerobot/__init__.py +0 -0
- castor/profiles/lerobot/aloha.yaml +35 -0
- castor/profiles/lerobot/koch-arm.yaml +20 -0
- castor/profiles/lerobot/so-arm101-bimanual.yaml +36 -0
- castor/profiles/lerobot/so-arm101-follower.yaml +22 -0
- castor/profiles/lerobot/so-arm101-leader.yaml +22 -0
- castor/profiles/odrive/__init__.py +0 -0
- castor/profiles/odrive/differential.yaml +14 -0
- castor/profiles/pollen/__init__.py +0 -0
- castor/profiles/pollen/reachy-mini.yaml +13 -0
- castor/profiles/pollen/reachy2.yaml +13 -0
- castor/prompt_cache.py +192 -0
- castor/provider_fallback.py +197 -0
- castor/providers/__init__.py +111 -0
- castor/providers/anthropic_provider.py +360 -0
- castor/providers/apple_preflight.py +192 -0
- castor/providers/apple_provider.py +226 -0
- castor/providers/base.py +578 -0
- castor/providers/clap_embedding_provider.py +208 -0
- castor/providers/clip_embedding_provider.py +217 -0
- castor/providers/consensus_provider.py +398 -0
- castor/providers/deepseek_provider.py +166 -0
- castor/providers/embedding_backend.py +41 -0
- castor/providers/failover.py +290 -0
- castor/providers/gated.py +328 -0
- castor/providers/gemini_embedding_provider.py +241 -0
- castor/providers/google_provider.py +259 -0
- castor/providers/grok_provider.py +163 -0
- castor/providers/groq_provider.py +134 -0
- castor/providers/huggingface_provider.py +334 -0
- castor/providers/imagebind_provider.py +157 -0
- castor/providers/kv_compression.py +93 -0
- castor/providers/llamacpp_provider.py +350 -0
- castor/providers/mistral_provider.py +166 -0
- castor/providers/mlx_provider.py +288 -0
- castor/providers/ollama_provider.py +648 -0
- castor/providers/onnx_provider.py +309 -0
- castor/providers/openai_provider.py +184 -0
- castor/providers/openrouter_provider.py +166 -0
- castor/providers/pool_provider.py +1241 -0
- castor/providers/sentence_transformers_provider.py +201 -0
- castor/providers/taalas_provider.py +215 -0
- castor/providers/task_router.py +78 -0
- castor/providers/vertex_provider.py +211 -0
- castor/providers/vla_provider.py +248 -0
- castor/py.typed +0 -0
- castor/quantum_commitment.py +189 -0
- castor/rate_limiting.py +289 -0
- castor/rcan/__init__.py +71 -0
- castor/rcan/capabilities.py +120 -0
- castor/rcan/commitment_chain.py +299 -0
- castor/rcan/config_safety.py +278 -0
- castor/rcan/http_transport.py +96 -0
- castor/rcan/invoke.py +308 -0
- castor/rcan/jwt_auth.py +216 -0
- castor/rcan/key_rotation.py +226 -0
- castor/rcan/mdns.py +255 -0
- castor/rcan/message.py +443 -0
- castor/rcan/message_signing.py +210 -0
- castor/rcan/node_broadcaster.py +105 -0
- castor/rcan/node_resolver.py +273 -0
- castor/rcan/parallel_invoke.py +126 -0
- castor/rcan/rbac.py +455 -0
- castor/rcan/registry.py +467 -0
- castor/rcan/router.py +271 -0
- castor/rcan/ruri.py +154 -0
- castor/rcan/sdk_bridge.py +342 -0
- castor/rcan/sdk_compat.py +108 -0
- castor/rcan/telemetry_fields.py +67 -0
- castor/rcan/verification.py +79 -0
- castor/rcan3/__init__.py +40 -0
- castor/rcan3/castor_harness.py +35 -0
- castor/rcan3/compliance.py +177 -0
- castor/rcan3/harness_protocol.py +51 -0
- castor/rcan3/identity.py +155 -0
- castor/rcan3/reader.py +87 -0
- castor/rcan3/rrf_client.py +94 -0
- castor/rcan3/signer.py +69 -0
- castor/rcan_generator.py +227 -0
- castor/record.py +185 -0
- castor/recorder.py +414 -0
- castor/registry.py +364 -0
- castor/repl.py +129 -0
- castor/response_cache.py +321 -0
- castor/rrf_cmd.py +185 -0
- castor/runtime_stats.py +203 -0
- castor/safety/__init__.py +32 -0
- castor/safety/anti_subversion.py +312 -0
- castor/safety/authorization.py +408 -0
- castor/safety/bounds.py +643 -0
- castor/safety/monitor.py +490 -0
- castor/safety/p66_manifest.py +602 -0
- castor/safety/protocol.py +846 -0
- castor/safety/state.py +222 -0
- castor/safety_benchmark.py +306 -0
- castor/safety_telemetry.py +160 -0
- castor/sbom.py +356 -0
- castor/schedule.py +214 -0
- castor/sdk/__init__.py +9 -0
- castor/secret_provider.py +248 -0
- castor/security_posture.py +149 -0
- castor/services/__init__.py +1 -0
- castor/services/rrf_poller.py +239 -0
- castor/setup_catalog.py +785 -0
- castor/setup_service.py +1248 -0
- castor/shell.py +227 -0
- castor/sim.py +275 -0
- castor/sim_bridge.py +315 -0
- castor/skills/__init__.py +5 -0
- castor/skills/contribute.py +193 -0
- castor/skills/loader.py +532 -0
- castor/skills/rcan_skills.py +252 -0
- castor/slam.py +300 -0
- castor/snapshot.py +210 -0
- castor/specialists/__init__.py +48 -0
- castor/specialists/base_specialist.py +65 -0
- castor/specialists/dock.py +246 -0
- castor/specialists/manipulator.py +185 -0
- castor/specialists/responder.py +171 -0
- castor/specialists/scout.py +296 -0
- castor/specialists/task_planner.py +247 -0
- castor/stream.py +226 -0
- castor/swarm/__init__.py +54 -0
- castor/swarm/consensus.py +338 -0
- castor/swarm/coordinator.py +276 -0
- castor/swarm/events.py +33 -0
- castor/swarm/oak_worker.py +141 -0
- castor/swarm/patch_sync.py +160 -0
- castor/swarm/peer.py +170 -0
- castor/swarm/shared_memory.py +148 -0
- castor/swarm/worker.py +209 -0
- castor/swarm_memory.py +275 -0
- castor/system_info.py +294 -0
- castor/telemetry/__init__.py +414 -0
- castor/telemetry/prometheus.py +212 -0
- castor/templates/__init__.py +36 -0
- castor/templates/fria.html.j2 +164 -0
- castor/templates/geofence.html +364 -0
- castor/templates/systemd/__init__.py +0 -0
- castor/templates/systemd/castor-gateway.service.tmpl +29 -0
- castor/templates/systemd/opencastor-attestation.service.tmpl +18 -0
- castor/test_hardware.py +255 -0
- castor/thought_log.py +93 -0
- castor/tiered_brain.py +470 -0
- castor/timelapse.py +251 -0
- castor/tools.py +340 -0
- castor/tools_ext/__init__.py +13 -0
- castor/tools_ext/permissions.py +68 -0
- castor/tools_ext/profiles.py +85 -0
- castor/trajectory.py +390 -0
- castor/tts/__init__.py +48 -0
- castor/tts/elevenlabs_backend.py +188 -0
- castor/tts_local.py +229 -0
- castor/update_check.py +158 -0
- castor/updater.py +97 -0
- castor/usage.py +402 -0
- castor/voice.py +1061 -0
- castor/voice_loop.py +305 -0
- castor/watch.py +388 -0
- castor/watchdog.py +111 -0
- castor/watermark.py +63 -0
- castor/web_wizard/__init__.py +5 -0
- castor/web_wizard/server.py +831 -0
- castor/web_wizard.py +537 -0
- castor/webhooks.py +269 -0
- castor/webrtc_audio.py +336 -0
- castor/wizard.py +3174 -0
- castor/workspace.py +234 -0
- castor/world/__init__.py +5 -0
- castor/world/model.py +122 -0
- opencastor-3.0.0.dist-info/METADATA +426 -0
- opencastor-3.0.0.dist-info/RECORD +438 -0
- opencastor-3.0.0.dist-info/WHEEL +5 -0
- opencastor-3.0.0.dist-info/entry_points.txt +3 -0
- opencastor-3.0.0.dist-info/licenses/LICENSE +189 -0
- opencastor-3.0.0.dist-info/top_level.txt +1 -0
castor/__init__.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""OpenCastor: The Universal Runtime for Embodied AI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from importlib.metadata import version as _pkg_version
|
|
7
|
+
|
|
8
|
+
__version__ = _pkg_version("opencastor")
|
|
9
|
+
except Exception:
|
|
10
|
+
__version__ = "3.0.0" # fallback
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def initialize_safety(safety_layer, config: dict):
|
|
14
|
+
"""Initialize and wire the full Protocol 66 safety stack.
|
|
15
|
+
|
|
16
|
+
Creates a :class:`~castor.safety.monitor.SensorMonitor` from the
|
|
17
|
+
``config["monitor"]`` section and connects it to *safety_layer* via
|
|
18
|
+
:func:`~castor.safety.monitor.wire_safety_layer`. Call this once during
|
|
19
|
+
runtime startup, then call ``monitor.start()`` to begin polling.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
safety_layer: A :class:`~castor.fs.safety.SafetyLayer` (or
|
|
23
|
+
``CastorFS.safety``) instance.
|
|
24
|
+
config: Runtime config dict (uses ``config["monitor"]`` sub-key).
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
The started :class:`~castor.safety.monitor.SensorMonitor` instance.
|
|
28
|
+
"""
|
|
29
|
+
from castor.safety.monitor import MonitorThresholds, SensorMonitor, wire_safety_layer
|
|
30
|
+
|
|
31
|
+
monitor_cfg = config.get("monitor", {})
|
|
32
|
+
thresholds_cfg = monitor_cfg.get("thresholds", {})
|
|
33
|
+
thresholds = MonitorThresholds(**thresholds_cfg) if thresholds_cfg else None
|
|
34
|
+
monitor = SensorMonitor(
|
|
35
|
+
thresholds=thresholds,
|
|
36
|
+
interval=float(monitor_cfg.get("interval", 5.0)),
|
|
37
|
+
consecutive_critical=int(monitor_cfg.get("consecutive_critical", 3)),
|
|
38
|
+
)
|
|
39
|
+
wire_safety_layer(monitor, safety_layer)
|
|
40
|
+
return monitor
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
__all__ = ["__version__", "initialize_safety"]
|
castor/__main__.py
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Structured action validation for OpenCastor (#271).
|
|
3
|
+
|
|
4
|
+
Validates parsed robot action dicts against built-in or custom JSON schemas.
|
|
5
|
+
Uses jsonschema (>=4.20.0, already a core dependency) for schema enforcement.
|
|
6
|
+
|
|
7
|
+
Usage::
|
|
8
|
+
|
|
9
|
+
from castor.action_validator import validate_action, ActionValidator
|
|
10
|
+
|
|
11
|
+
# Validate a move action
|
|
12
|
+
result = validate_action({"type": "move", "linear": 0.5, "angular": 0.0})
|
|
13
|
+
if not result.valid:
|
|
14
|
+
for err in result.errors:
|
|
15
|
+
print(f"ERROR: {err}")
|
|
16
|
+
|
|
17
|
+
# Custom schemas merged with built-ins
|
|
18
|
+
validator = ActionValidator(custom_schemas={"spray": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"required": ["type"],
|
|
21
|
+
"properties": {
|
|
22
|
+
"type": {"type": "string"},
|
|
23
|
+
"duration_s": {"type": "number", "minimum": 0},
|
|
24
|
+
},
|
|
25
|
+
}})
|
|
26
|
+
result = validator.validate({"type": "spray", "duration_s": 2.0})
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import logging
|
|
32
|
+
import threading
|
|
33
|
+
from dataclasses import dataclass, field
|
|
34
|
+
from typing import Any, Optional
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger("OpenCastor.ActionValidator")
|
|
37
|
+
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
# Built-in JSON schemas for the core action vocabulary
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
_ACTION_SCHEMAS: dict[str, dict] = {
|
|
43
|
+
"move": {
|
|
44
|
+
"type": "object",
|
|
45
|
+
"required": ["type"],
|
|
46
|
+
"properties": {
|
|
47
|
+
"type": {"type": "string"},
|
|
48
|
+
"linear": {"type": "number"},
|
|
49
|
+
"angular": {"type": "number"},
|
|
50
|
+
"linear_x": {"type": "number"}, # legacy alias
|
|
51
|
+
"angular_z": {"type": "number"}, # legacy alias
|
|
52
|
+
"speed": {"type": "number", "minimum": 0, "maximum": 1},
|
|
53
|
+
"duration_s": {"type": "number", "minimum": 0},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
"stop": {
|
|
57
|
+
"type": "object",
|
|
58
|
+
"required": ["type"],
|
|
59
|
+
"properties": {
|
|
60
|
+
"type": {"type": "string"},
|
|
61
|
+
},
|
|
62
|
+
"additionalProperties": True,
|
|
63
|
+
},
|
|
64
|
+
"wait": {
|
|
65
|
+
"type": "object",
|
|
66
|
+
"required": ["type"],
|
|
67
|
+
"properties": {
|
|
68
|
+
"type": {"type": "string"},
|
|
69
|
+
"duration_s": {"type": "number", "minimum": 0},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
"grip": {
|
|
73
|
+
"type": "object",
|
|
74
|
+
"required": ["type"],
|
|
75
|
+
"properties": {
|
|
76
|
+
"type": {"type": "string"},
|
|
77
|
+
"position": {"type": "number", "minimum": 0, "maximum": 1},
|
|
78
|
+
"force": {"type": "number", "minimum": 0, "maximum": 1},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
"nav_waypoint": {
|
|
82
|
+
"type": "object",
|
|
83
|
+
"required": ["type"],
|
|
84
|
+
"properties": {
|
|
85
|
+
"type": {"type": "string"},
|
|
86
|
+
"distance_m": {"type": "number", "minimum": 0},
|
|
87
|
+
"heading_deg": {"type": "number"},
|
|
88
|
+
"speed": {"type": "number", "minimum": 0, "maximum": 1},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
# Data classes
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass
|
|
100
|
+
class ValidationResult:
|
|
101
|
+
"""Result of validating a single action dict."""
|
|
102
|
+
|
|
103
|
+
valid: bool
|
|
104
|
+
action_type: str
|
|
105
|
+
errors: list[str] = field(default_factory=list)
|
|
106
|
+
warnings: list[str] = field(default_factory=list)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
# ActionValidator
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class ActionValidator:
|
|
115
|
+
"""Validate robot action dicts against built-in or custom JSON schemas.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
custom_schemas: Dict mapping action type name → JSON schema dict.
|
|
119
|
+
These are merged with the built-in schemas; custom
|
|
120
|
+
entries take precedence over built-ins of the same name.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
def __init__(self, custom_schemas: Optional[dict[str, dict]] = None) -> None:
|
|
124
|
+
self._schemas: dict[str, dict] = {**_ACTION_SCHEMAS}
|
|
125
|
+
self._schema_sources: dict[str, str] = {t: "builtin" for t in _ACTION_SCHEMAS}
|
|
126
|
+
if custom_schemas:
|
|
127
|
+
self._schemas.update(custom_schemas)
|
|
128
|
+
for t in custom_schemas:
|
|
129
|
+
self._schema_sources[t] = "rcan_config"
|
|
130
|
+
|
|
131
|
+
# ------------------------------------------------------------------
|
|
132
|
+
# Public API
|
|
133
|
+
# ------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
def validate(self, action: dict[str, Any]) -> ValidationResult:
|
|
136
|
+
"""Validate an action dict.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
action: Parsed action dictionary (e.g. ``{"type": "move", "linear": 0.5}``).
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
A :class:`ValidationResult` describing whether the action is valid,
|
|
143
|
+
any hard errors, and any non-fatal warnings.
|
|
144
|
+
"""
|
|
145
|
+
# Step 1 — must be a non-None dict
|
|
146
|
+
if action is None or not isinstance(action, dict):
|
|
147
|
+
return ValidationResult(
|
|
148
|
+
valid=False,
|
|
149
|
+
action_type="",
|
|
150
|
+
errors=["action must be a dict"],
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Step 2 — must contain a "type" key with a non-None value
|
|
154
|
+
action_type = action.get("type")
|
|
155
|
+
if not action_type:
|
|
156
|
+
return ValidationResult(
|
|
157
|
+
valid=False,
|
|
158
|
+
action_type="",
|
|
159
|
+
errors=["missing 'type' field"],
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
action_type = str(action_type)
|
|
163
|
+
|
|
164
|
+
# Step 3 — look up schema; unknown types pass with a warning
|
|
165
|
+
schema = self._schemas.get(action_type)
|
|
166
|
+
if schema is None:
|
|
167
|
+
logger.debug("Unknown action type '%s' — skipping validation", action_type)
|
|
168
|
+
return ValidationResult(
|
|
169
|
+
valid=True,
|
|
170
|
+
action_type=action_type,
|
|
171
|
+
warnings=[f"unknown action type '{action_type}' — skipping validation"],
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Step 4 — attempt jsonschema validation (lazy import)
|
|
175
|
+
try:
|
|
176
|
+
import jsonschema # noqa: PLC0415
|
|
177
|
+
except ImportError:
|
|
178
|
+
logger.warning(
|
|
179
|
+
"jsonschema not installed — skipping schema validation for '%s'", action_type
|
|
180
|
+
)
|
|
181
|
+
return ValidationResult(
|
|
182
|
+
valid=True,
|
|
183
|
+
action_type=action_type,
|
|
184
|
+
warnings=["jsonschema not installed — skipping schema validation"],
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
jsonschema.validate(action, schema)
|
|
189
|
+
except jsonschema.ValidationError as exc:
|
|
190
|
+
return ValidationResult(
|
|
191
|
+
valid=False,
|
|
192
|
+
action_type=action_type,
|
|
193
|
+
errors=[exc.message],
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Step 5 — warn about unknown properties when additionalProperties is
|
|
197
|
+
# not explicitly set to True
|
|
198
|
+
warnings: list[str] = []
|
|
199
|
+
schema_props = schema.get("properties", {})
|
|
200
|
+
allows_additional = schema.get("additionalProperties", False) is True
|
|
201
|
+
if schema_props and not allows_additional:
|
|
202
|
+
for key in action:
|
|
203
|
+
if key not in schema_props:
|
|
204
|
+
warnings.append(f"unknown field '{key}' for action type '{action_type}'")
|
|
205
|
+
logger.debug("Action '%s' contains unknown field '%s'", action_type, key)
|
|
206
|
+
|
|
207
|
+
return ValidationResult(valid=True, action_type=action_type, warnings=warnings)
|
|
208
|
+
|
|
209
|
+
def known_types(self) -> list[str]:
|
|
210
|
+
"""Return a sorted list of known action type names."""
|
|
211
|
+
return sorted(self._schemas.keys())
|
|
212
|
+
|
|
213
|
+
def schema_source_for(self, action_type: str) -> str:
|
|
214
|
+
"""Return the source of the schema for *action_type*.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
``"builtin"`` — schema is part of OpenCastor's built-in vocabulary.
|
|
218
|
+
``"rcan_config"`` — schema was loaded from the RCAN ``action_schemas`` block.
|
|
219
|
+
``"unknown"`` — no schema registered for this action type.
|
|
220
|
+
"""
|
|
221
|
+
return self._schema_sources.get(action_type, "unknown")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# ---------------------------------------------------------------------------
|
|
225
|
+
# Module-level singleton helpers
|
|
226
|
+
# ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
_default_validator: Optional[ActionValidator] = None
|
|
229
|
+
_validator_lock = threading.Lock()
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def get_validator(custom_schemas: Optional[dict[str, dict]] = None) -> ActionValidator:
|
|
233
|
+
"""Return the singleton :class:`ActionValidator`.
|
|
234
|
+
|
|
235
|
+
If *custom_schemas* is provided the singleton is (re-)created with those
|
|
236
|
+
schemas merged in. Subsequent calls without *custom_schemas* return the
|
|
237
|
+
previously created instance.
|
|
238
|
+
"""
|
|
239
|
+
global _default_validator
|
|
240
|
+
with _validator_lock:
|
|
241
|
+
if _default_validator is None or custom_schemas is not None:
|
|
242
|
+
_default_validator = ActionValidator(custom_schemas=custom_schemas)
|
|
243
|
+
return _default_validator
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def validate_action(action: dict[str, Any]) -> ValidationResult:
|
|
247
|
+
"""Validate *action* using the default singleton :class:`ActionValidator`.
|
|
248
|
+
|
|
249
|
+
This is the most convenient entry-point for one-off validation::
|
|
250
|
+
|
|
251
|
+
from castor.action_validator import validate_action
|
|
252
|
+
|
|
253
|
+
result = validate_action({"type": "move", "linear": 0.5})
|
|
254
|
+
assert result.valid
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
action: Parsed action dictionary.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
:class:`ValidationResult`
|
|
261
|
+
"""
|
|
262
|
+
return get_validator().validate(action)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def init_from_config(config: dict[str, Any]) -> ActionValidator:
|
|
266
|
+
"""Initialise the singleton validator with custom schemas from RCAN *config*.
|
|
267
|
+
|
|
268
|
+
Reads the ``action_schemas`` mapping from the RCAN config dict and merges
|
|
269
|
+
any entries with the built-in action schemas. Custom entries take
|
|
270
|
+
precedence over built-ins with the same action type name.
|
|
271
|
+
|
|
272
|
+
Usage in RCAN config::
|
|
273
|
+
|
|
274
|
+
action_schemas:
|
|
275
|
+
spray:
|
|
276
|
+
type: object
|
|
277
|
+
required: [type]
|
|
278
|
+
properties:
|
|
279
|
+
type: {type: string}
|
|
280
|
+
duration_s: {type: number, minimum: 0}
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
config: RCAN agent config dict (the top-level mapping).
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
The (re-)initialised :class:`ActionValidator` singleton.
|
|
287
|
+
"""
|
|
288
|
+
raw: dict[str, dict] = config.get("action_schemas") or {}
|
|
289
|
+
if raw:
|
|
290
|
+
logger.info(
|
|
291
|
+
"ActionValidator: loading %d custom schema(s) from RCAN config: %s",
|
|
292
|
+
len(raw),
|
|
293
|
+
list(raw.keys()),
|
|
294
|
+
)
|
|
295
|
+
return get_validator(custom_schemas=raw if raw else None)
|