devagent-physical-engine 0.10.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.
- devagent_physical_engine/__init__.py +44 -0
- devagent_physical_engine/agent/__init__.py +40 -0
- devagent_physical_engine/agent/compiler.py +285 -0
- devagent_physical_engine/agent/contracts.py +129 -0
- devagent_physical_engine/agent/coordinator.py +108 -0
- devagent_physical_engine/agent/critic.py +72 -0
- devagent_physical_engine/agent/evidence.py +34 -0
- devagent_physical_engine/agent/interpreter.py +179 -0
- devagent_physical_engine/agent/planner.py +105 -0
- devagent_physical_engine/agent/recovery.py +54 -0
- devagent_physical_engine/agent/routing.py +76 -0
- devagent_physical_engine/agent/runtime.py +270 -0
- devagent_physical_engine/agent/semantic.py +304 -0
- devagent_physical_engine/agent/structured.py +423 -0
- devagent_physical_engine/ai_cli.py +226 -0
- devagent_physical_engine/cli.py +392 -0
- devagent_physical_engine/doctor.py +20 -0
- devagent_physical_engine/engineering_agent.py +243 -0
- devagent_physical_engine/engineering_request.py +630 -0
- devagent_physical_engine/execution.py +90 -0
- devagent_physical_engine/models.py +143 -0
- devagent_physical_engine/operating_envelope.py +120 -0
- devagent_physical_engine/optimization/__init__.py +50 -0
- devagent_physical_engine/optimization/benchmark.py +122 -0
- devagent_physical_engine/optimization/candidates.py +198 -0
- devagent_physical_engine/optimization/contracts.py +235 -0
- devagent_physical_engine/optimization/evaluator.py +107 -0
- devagent_physical_engine/optimization/evidence.py +53 -0
- devagent_physical_engine/optimization/experience.py +105 -0
- devagent_physical_engine/optimization/measured.py +125 -0
- devagent_physical_engine/optimization/optimizer.py +215 -0
- devagent_physical_engine/optimization/orchestrator.py +155 -0
- devagent_physical_engine/physical_campaign.py +413 -0
- devagent_physical_engine/physical_evidence.py +214 -0
- devagent_physical_engine/physical_motion.py +196 -0
- devagent_physical_engine/planning.py +80 -0
- devagent_physical_engine/preexecution_contract.py +65 -0
- devagent_physical_engine/provider_adapters/__init__.py +22 -0
- devagent_physical_engine/provider_adapters/anthropic.py +112 -0
- devagent_physical_engine/provider_adapters/common.py +187 -0
- devagent_physical_engine/provider_adapters/factory.py +20 -0
- devagent_physical_engine/provider_adapters/gemini.py +126 -0
- devagent_physical_engine/provider_adapters/openai.py +95 -0
- devagent_physical_engine/provider_qualification.py +268 -0
- devagent_physical_engine/providers.py +94 -0
- devagent_physical_engine/qualification.py +44 -0
- devagent_physical_engine/qualification_cli.py +195 -0
- devagent_physical_engine/qualification_harness.py +917 -0
- devagent_physical_engine/robot_platform.py +411 -0
- devagent_physical_engine/robots.py +76 -0
- devagent_physical_engine/ros2/__init__.py +35 -0
- devagent_physical_engine/ros2/acceptance.py +324 -0
- devagent_physical_engine/ros2/commands.py +175 -0
- devagent_physical_engine/ros2/doctor.py +116 -0
- devagent_physical_engine/ros2/fk_probe.py +83 -0
- devagent_physical_engine/ros2/frame_alignment.py +61 -0
- devagent_physical_engine/ros2/gazebo_world.py +125 -0
- devagent_physical_engine/ros2/joint_state_recorder.py +64 -0
- devagent_physical_engine/ros2/measured_motion.py +233 -0
- devagent_physical_engine/ros2/moveit_scene.py +121 -0
- devagent_physical_engine/ros2/preexecution.py +113 -0
- devagent_physical_engine/ros2/qualification.py +81 -0
- devagent_physical_engine/ros2/qualification_v10.py +252 -0
- devagent_physical_engine/ros2/scene_probe.py +219 -0
- devagent_physical_engine/ros2/state_validity_probe.py +125 -0
- devagent_physical_engine/ros2/tf_probe.py +51 -0
- devagent_physical_engine/ros2/trajectory.py +188 -0
- devagent_physical_engine/ros2/ur5e.py +59 -0
- devagent_physical_engine/ros2/ur5e_adapter.py +349 -0
- devagent_physical_engine/ros2/ur5e_v10_adapter.py +292 -0
- devagent_physical_engine/setup_profile.py +356 -0
- devagent_physical_engine/simulation.py +32 -0
- devagent_physical_engine/simulation_platform.py +269 -0
- devagent_physical_engine/trajectory_qualification.py +201 -0
- devagent_physical_engine/twin.py +939 -0
- devagent_physical_engine/twin_builder.py +309 -0
- devagent_physical_engine/twin_materialization.py +404 -0
- devagent_physical_engine/verification.py +46 -0
- devagent_physical_engine-0.10.0.dist-info/METADATA +315 -0
- devagent_physical_engine-0.10.0.dist-info/RECORD +84 -0
- devagent_physical_engine-0.10.0.dist-info/WHEEL +5 -0
- devagent_physical_engine-0.10.0.dist-info/entry_points.txt +3 -0
- devagent_physical_engine-0.10.0.dist-info/licenses/NOTICE +2 -0
- devagent_physical_engine-0.10.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict, dataclass, field
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from hashlib import sha256
|
|
6
|
+
import json
|
|
7
|
+
from math import isfinite
|
|
8
|
+
import re
|
|
9
|
+
from typing import Any, Mapping, Sequence
|
|
10
|
+
|
|
11
|
+
from .models import Goal, Resource
|
|
12
|
+
from .planning import SUPPORTED_GOAL_ACTIONS
|
|
13
|
+
from .robots import CATALOG
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
MAX_REQUEST_TEXT = 4096
|
|
17
|
+
MAX_LIST_ITEMS = 32
|
|
18
|
+
MAX_RUNS = 10_000
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EngineeringRequestError(ValueError):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RequestState(str, Enum):
|
|
26
|
+
DRAFT = "draft"
|
|
27
|
+
NEEDS_INFORMATION = "needs_information"
|
|
28
|
+
READY_FOR_VALIDATION = "ready_for_validation"
|
|
29
|
+
VALIDATED = "validated"
|
|
30
|
+
REJECTED = "rejected"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class RequestPurpose(str, Enum):
|
|
34
|
+
PLAN = "plan"
|
|
35
|
+
SIMULATE = "simulate"
|
|
36
|
+
QUALIFY = "qualify"
|
|
37
|
+
COMMISSIONING = "commissioning"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Objective(str, Enum):
|
|
41
|
+
BALANCED = "balanced"
|
|
42
|
+
CYCLE_TIME = "cycle_time"
|
|
43
|
+
ROBUSTNESS = "robustness"
|
|
44
|
+
CLEARANCE = "clearance"
|
|
45
|
+
ENERGY = "energy"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TestPreset(str, Enum):
|
|
49
|
+
QUICK = "quick"
|
|
50
|
+
ENGINEERING = "engineering"
|
|
51
|
+
ROBUSTNESS = "robustness"
|
|
52
|
+
STRESS = "stress"
|
|
53
|
+
CUSTOM = "custom"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class FaultIntent(str, Enum):
|
|
57
|
+
MISSING_OBJECT = "missing_object"
|
|
58
|
+
DESTINATION_OCCUPIED = "destination_occupied"
|
|
59
|
+
ROBOT_UNAVAILABLE = "robot_unavailable"
|
|
60
|
+
CONTROLLER_TIMEOUT = "controller_timeout"
|
|
61
|
+
COLLISION_OBSTACLE = "collision_obstacle"
|
|
62
|
+
SENSOR_LATENCY = "sensor_latency"
|
|
63
|
+
POSE_UNCERTAINTY = "pose_uncertainty"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
_OPERATION_ALIASES = {
|
|
67
|
+
"load": "load",
|
|
68
|
+
"machine_load": "load",
|
|
69
|
+
"machine_loading": "load",
|
|
70
|
+
"machine loading": "load",
|
|
71
|
+
"pick_and_place": "pick_place",
|
|
72
|
+
"pick-place": "pick_place",
|
|
73
|
+
"pick place": "pick_place",
|
|
74
|
+
"pick_place": "pick_place",
|
|
75
|
+
"palletize": "palletize",
|
|
76
|
+
"palletizing": "palletize",
|
|
77
|
+
"inspect": "inspect",
|
|
78
|
+
"inspection": "inspect",
|
|
79
|
+
"transfer": "transfer",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
_ROBOT_ALIASES = {
|
|
83
|
+
"ur5e": "ur5e",
|
|
84
|
+
"ur 5e": "ur5e",
|
|
85
|
+
"universal robots ur5e": "ur5e",
|
|
86
|
+
"universal robot ur5e": "ur5e",
|
|
87
|
+
"fanuc crx": "fanuc_crx",
|
|
88
|
+
"fanuc_crx": "fanuc_crx",
|
|
89
|
+
"crx": "fanuc_crx",
|
|
90
|
+
"kuka kr": "kuka_kr",
|
|
91
|
+
"kuka_kr": "kuka_kr",
|
|
92
|
+
"abb irb": "abb_irb",
|
|
93
|
+
"abb_irb": "abb_irb",
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
_CORE_REQUIRED_FIELDS = ("robot", "operation", "object_id", "source", "destination")
|
|
97
|
+
_CRITICAL_FIELDS = frozenset(_CORE_REQUIRED_FIELDS + ("payload_kg",))
|
|
98
|
+
_SAFE_SYSTEM_CONSTRAINTS = ("collision_free", "joint_limits")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _clean_text(value: object, *, field_name: str, required: bool = False) -> str | None:
|
|
102
|
+
if value is None:
|
|
103
|
+
if required:
|
|
104
|
+
raise EngineeringRequestError(f"{field_name}_required")
|
|
105
|
+
return None
|
|
106
|
+
if not isinstance(value, str):
|
|
107
|
+
raise EngineeringRequestError(f"{field_name}_must_be_string")
|
|
108
|
+
text = value.strip()
|
|
109
|
+
if not text:
|
|
110
|
+
if required:
|
|
111
|
+
raise EngineeringRequestError(f"{field_name}_required")
|
|
112
|
+
return None
|
|
113
|
+
if len(text) > MAX_REQUEST_TEXT:
|
|
114
|
+
raise EngineeringRequestError(f"{field_name}_too_long")
|
|
115
|
+
return text
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _optional_number(value: object, *, field_name: str, minimum: float | None = None, maximum: float | None = None) -> float | None:
|
|
119
|
+
if value is None:
|
|
120
|
+
return None
|
|
121
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
122
|
+
raise EngineeringRequestError(f"{field_name}_must_be_number")
|
|
123
|
+
result = float(value)
|
|
124
|
+
if not isfinite(result):
|
|
125
|
+
raise EngineeringRequestError(f"{field_name}_non_finite")
|
|
126
|
+
if minimum is not None and result < minimum:
|
|
127
|
+
raise EngineeringRequestError(f"{field_name}_below_minimum")
|
|
128
|
+
if maximum is not None and result > maximum:
|
|
129
|
+
raise EngineeringRequestError(f"{field_name}_above_maximum")
|
|
130
|
+
return result
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _optional_int(value: object, *, field_name: str, minimum: int, maximum: int) -> int | None:
|
|
134
|
+
if value is None:
|
|
135
|
+
return None
|
|
136
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
137
|
+
raise EngineeringRequestError(f"{field_name}_must_be_integer")
|
|
138
|
+
if not minimum <= value <= maximum:
|
|
139
|
+
raise EngineeringRequestError(f"{field_name}_out_of_range")
|
|
140
|
+
return value
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _string_tuple(value: object, *, field_name: str, allowed: set[str] | None = None) -> tuple[str, ...]:
|
|
144
|
+
if value is None:
|
|
145
|
+
return ()
|
|
146
|
+
if not isinstance(value, Sequence) or isinstance(value, (str, bytes, bytearray)):
|
|
147
|
+
raise EngineeringRequestError(f"{field_name}_must_be_array")
|
|
148
|
+
if len(value) > MAX_LIST_ITEMS:
|
|
149
|
+
raise EngineeringRequestError(f"{field_name}_too_many_items")
|
|
150
|
+
output: list[str] = []
|
|
151
|
+
for item in value:
|
|
152
|
+
text = _clean_text(item, field_name=field_name, required=True)
|
|
153
|
+
assert text is not None
|
|
154
|
+
normalized = text.strip().lower()
|
|
155
|
+
if allowed is not None and normalized not in allowed:
|
|
156
|
+
raise EngineeringRequestError(f"unknown_{field_name}:{normalized}")
|
|
157
|
+
if normalized not in output:
|
|
158
|
+
output.append(normalized)
|
|
159
|
+
return tuple(output)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def normalize_robot(value: str | None) -> str | None:
|
|
163
|
+
if value is None:
|
|
164
|
+
return None
|
|
165
|
+
key = re.sub(r"\s+", " ", value.strip().lower())
|
|
166
|
+
key = _ROBOT_ALIASES.get(key, key.replace("-", "_").replace(" ", "_"))
|
|
167
|
+
return key
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def normalize_operation(value: str | None) -> str | None:
|
|
171
|
+
if value is None:
|
|
172
|
+
return None
|
|
173
|
+
key = re.sub(r"\s+", " ", value.strip().lower())
|
|
174
|
+
return _OPERATION_ALIASES.get(key, key.replace("-", "_").replace(" ", "_"))
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass(frozen=True, slots=True)
|
|
178
|
+
class PhysicalVariation:
|
|
179
|
+
object_x_mm: float | None = None
|
|
180
|
+
object_y_mm: float | None = None
|
|
181
|
+
object_z_mm: float | None = None
|
|
182
|
+
yaw_deg: float | None = None
|
|
183
|
+
payload_min_kg: float | None = None
|
|
184
|
+
payload_max_kg: float | None = None
|
|
185
|
+
speed_min_pct: float | None = None
|
|
186
|
+
speed_max_pct: float | None = None
|
|
187
|
+
|
|
188
|
+
def __post_init__(self) -> None:
|
|
189
|
+
for name in ("object_x_mm", "object_y_mm", "object_z_mm", "yaw_deg"):
|
|
190
|
+
value = getattr(self, name)
|
|
191
|
+
if value is not None and (not isfinite(value) or value < 0):
|
|
192
|
+
raise EngineeringRequestError(f"{name}_invalid")
|
|
193
|
+
for name in ("payload_min_kg", "payload_max_kg"):
|
|
194
|
+
value = getattr(self, name)
|
|
195
|
+
if value is not None and (not isfinite(value) or value < 0):
|
|
196
|
+
raise EngineeringRequestError(f"{name}_invalid")
|
|
197
|
+
if self.payload_min_kg is not None and self.payload_max_kg is not None and self.payload_min_kg > self.payload_max_kg:
|
|
198
|
+
raise EngineeringRequestError("payload_variation_range_invalid")
|
|
199
|
+
for name in ("speed_min_pct", "speed_max_pct"):
|
|
200
|
+
value = getattr(self, name)
|
|
201
|
+
if value is not None and (not isfinite(value) or not 0 < value <= 100):
|
|
202
|
+
raise EngineeringRequestError(f"{name}_invalid")
|
|
203
|
+
if self.speed_min_pct is not None and self.speed_max_pct is not None and self.speed_min_pct > self.speed_max_pct:
|
|
204
|
+
raise EngineeringRequestError("speed_variation_range_invalid")
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def configured(self) -> bool:
|
|
208
|
+
return any(value is not None for value in asdict(self).values())
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@dataclass(frozen=True, slots=True)
|
|
212
|
+
class EngineeringRequestDraft:
|
|
213
|
+
robot: str | None = None
|
|
214
|
+
operation: str | None = None
|
|
215
|
+
object_id: str | None = None
|
|
216
|
+
source: str | None = None
|
|
217
|
+
destination: str | None = None
|
|
218
|
+
tool: str | None = None
|
|
219
|
+
payload_kg: float | None = None
|
|
220
|
+
purpose: RequestPurpose = RequestPurpose.SIMULATE
|
|
221
|
+
objective: Objective = Objective.BALANCED
|
|
222
|
+
test_preset: TestPreset = TestPreset.ENGINEERING
|
|
223
|
+
runs: int | None = None
|
|
224
|
+
variation: PhysicalVariation = field(default_factory=PhysicalVariation)
|
|
225
|
+
faults: tuple[str, ...] = ()
|
|
226
|
+
minimum_clearance_mm: float | None = None
|
|
227
|
+
max_cycle_time_s: float | None = None
|
|
228
|
+
uncertain_fields: tuple[str, ...] = ()
|
|
229
|
+
notes: tuple[str, ...] = ()
|
|
230
|
+
|
|
231
|
+
def __post_init__(self) -> None:
|
|
232
|
+
if self.robot is not None:
|
|
233
|
+
object.__setattr__(self, "robot", normalize_robot(self.robot))
|
|
234
|
+
if self.operation is not None:
|
|
235
|
+
object.__setattr__(self, "operation", normalize_operation(self.operation))
|
|
236
|
+
if self.payload_kg is not None and (not isfinite(self.payload_kg) or self.payload_kg < 0):
|
|
237
|
+
raise EngineeringRequestError("payload_kg_invalid")
|
|
238
|
+
if self.minimum_clearance_mm is not None and (not isfinite(self.minimum_clearance_mm) or self.minimum_clearance_mm <= 0):
|
|
239
|
+
raise EngineeringRequestError("minimum_clearance_mm_invalid")
|
|
240
|
+
if self.max_cycle_time_s is not None and (not isfinite(self.max_cycle_time_s) or self.max_cycle_time_s <= 0):
|
|
241
|
+
raise EngineeringRequestError("max_cycle_time_s_invalid")
|
|
242
|
+
if self.runs is not None and not 1 <= self.runs <= MAX_RUNS:
|
|
243
|
+
raise EngineeringRequestError("runs_out_of_range")
|
|
244
|
+
unknown_uncertain = set(self.uncertain_fields) - {
|
|
245
|
+
"robot", "operation", "object_id", "source", "destination", "tool", "payload_kg",
|
|
246
|
+
"objective", "test_preset", "runs", "variation", "faults",
|
|
247
|
+
}
|
|
248
|
+
if unknown_uncertain:
|
|
249
|
+
raise EngineeringRequestError("unknown_uncertain_fields:" + ",".join(sorted(unknown_uncertain)))
|
|
250
|
+
|
|
251
|
+
def to_dict(self) -> dict[str, Any]:
|
|
252
|
+
return {
|
|
253
|
+
"robot": self.robot,
|
|
254
|
+
"operation": self.operation,
|
|
255
|
+
"object_id": self.object_id,
|
|
256
|
+
"source": self.source,
|
|
257
|
+
"destination": self.destination,
|
|
258
|
+
"tool": self.tool,
|
|
259
|
+
"payload_kg": self.payload_kg,
|
|
260
|
+
"purpose": self.purpose.value,
|
|
261
|
+
"objective": self.objective.value,
|
|
262
|
+
"test_preset": self.test_preset.value,
|
|
263
|
+
"runs": self.runs,
|
|
264
|
+
"variation": asdict(self.variation),
|
|
265
|
+
"faults": list(self.faults),
|
|
266
|
+
"minimum_clearance_mm": self.minimum_clearance_mm,
|
|
267
|
+
"max_cycle_time_s": self.max_cycle_time_s,
|
|
268
|
+
"uncertain_fields": list(self.uncertain_fields),
|
|
269
|
+
"notes": list(self.notes),
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@dataclass(frozen=True, slots=True)
|
|
274
|
+
class InformationRequest:
|
|
275
|
+
field: str
|
|
276
|
+
question: str
|
|
277
|
+
reason: str
|
|
278
|
+
|
|
279
|
+
def to_dict(self) -> dict[str, str]:
|
|
280
|
+
return asdict(self)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
@dataclass(frozen=True, slots=True)
|
|
284
|
+
class RequestAssessment:
|
|
285
|
+
state: RequestState
|
|
286
|
+
missing: tuple[InformationRequest, ...] = ()
|
|
287
|
+
rejection_codes: tuple[str, ...] = ()
|
|
288
|
+
open_items: tuple[str, ...] = ()
|
|
289
|
+
|
|
290
|
+
@property
|
|
291
|
+
def ready(self) -> bool:
|
|
292
|
+
return self.state is RequestState.READY_FOR_VALIDATION
|
|
293
|
+
|
|
294
|
+
def to_dict(self) -> dict[str, Any]:
|
|
295
|
+
return {
|
|
296
|
+
"state": self.state.value,
|
|
297
|
+
"missing": [item.to_dict() for item in self.missing],
|
|
298
|
+
"rejection_codes": list(self.rejection_codes),
|
|
299
|
+
"open_items": list(self.open_items),
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@dataclass(frozen=True, slots=True)
|
|
304
|
+
class ValidatedEngineeringRequest:
|
|
305
|
+
request_id: str
|
|
306
|
+
robot_key: str
|
|
307
|
+
operation: str
|
|
308
|
+
object_id: str
|
|
309
|
+
source: str
|
|
310
|
+
destination: str
|
|
311
|
+
tool: str | None
|
|
312
|
+
payload_kg: float | None
|
|
313
|
+
purpose: RequestPurpose
|
|
314
|
+
objective: Objective
|
|
315
|
+
test_preset: TestPreset
|
|
316
|
+
runs: int
|
|
317
|
+
variation: PhysicalVariation
|
|
318
|
+
faults: tuple[str, ...]
|
|
319
|
+
hard_constraints: tuple[str, ...]
|
|
320
|
+
minimum_clearance_mm: float | None
|
|
321
|
+
max_cycle_time_s: float | None
|
|
322
|
+
open_items: tuple[str, ...]
|
|
323
|
+
|
|
324
|
+
def goal(self) -> Goal:
|
|
325
|
+
return Goal(
|
|
326
|
+
goal_id=f"{self.request_id}-goal",
|
|
327
|
+
action=self.operation,
|
|
328
|
+
object_id=self.object_id,
|
|
329
|
+
source=self.source,
|
|
330
|
+
destination=self.destination,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
def resource(self) -> Resource:
|
|
334
|
+
return CATALOG[self.robot_key]()
|
|
335
|
+
|
|
336
|
+
def to_dict(self) -> dict[str, Any]:
|
|
337
|
+
return {
|
|
338
|
+
"request_id": self.request_id,
|
|
339
|
+
"state": RequestState.VALIDATED.value,
|
|
340
|
+
"robot": self.robot_key,
|
|
341
|
+
"operation": self.operation,
|
|
342
|
+
"object_id": self.object_id,
|
|
343
|
+
"source": self.source,
|
|
344
|
+
"destination": self.destination,
|
|
345
|
+
"tool": self.tool,
|
|
346
|
+
"payload_kg": self.payload_kg,
|
|
347
|
+
"purpose": self.purpose.value,
|
|
348
|
+
"objective": self.objective.value,
|
|
349
|
+
"test_preset": self.test_preset.value,
|
|
350
|
+
"runs": self.runs,
|
|
351
|
+
"variation": asdict(self.variation),
|
|
352
|
+
"faults": list(self.faults),
|
|
353
|
+
"hard_constraints": list(self.hard_constraints),
|
|
354
|
+
"minimum_clearance_mm": self.minimum_clearance_mm,
|
|
355
|
+
"max_cycle_time_s": self.max_cycle_time_s,
|
|
356
|
+
"open_items": list(self.open_items),
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
_QUESTIONS = {
|
|
361
|
+
"robot": ("Which robot or robot model should perform this task?", "A robot must be selected before capability validation."),
|
|
362
|
+
"operation": ("What operation should the robot perform?", "The requested task must be known before planning."),
|
|
363
|
+
"object_id": ("What object or part should the robot move or process?", "The workpiece is required to create a deterministic goal."),
|
|
364
|
+
"source": ("Where should the robot pick up or start with the object?", "The source location is required to create the task goal."),
|
|
365
|
+
"destination": ("Where should the robot place or deliver the object?", "The destination is required to create the task goal."),
|
|
366
|
+
"payload_kg": ("What is the expected payload or part weight in kilograms?", "Commissioning qualification needs the payload instead of silently assuming one."),
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class RequestCompletenessValidator:
|
|
371
|
+
"""Deterministically decide whether enough information exists to proceed.
|
|
372
|
+
|
|
373
|
+
Critical fields marked uncertain by the model are treated as missing even if
|
|
374
|
+
the model emitted a guessed value. Commissioning requests additionally
|
|
375
|
+
require payload because payload materially affects physical qualification.
|
|
376
|
+
"""
|
|
377
|
+
|
|
378
|
+
def assess(self, draft: EngineeringRequestDraft) -> RequestAssessment:
|
|
379
|
+
uncertain = set(draft.uncertain_fields)
|
|
380
|
+
required = list(_CORE_REQUIRED_FIELDS)
|
|
381
|
+
if draft.purpose is RequestPurpose.COMMISSIONING:
|
|
382
|
+
required.append("payload_kg")
|
|
383
|
+
|
|
384
|
+
missing_fields: list[str] = []
|
|
385
|
+
for field_name in required:
|
|
386
|
+
value = getattr(draft, field_name)
|
|
387
|
+
if value is None or field_name in uncertain:
|
|
388
|
+
missing_fields.append(field_name)
|
|
389
|
+
|
|
390
|
+
if missing_fields:
|
|
391
|
+
return RequestAssessment(
|
|
392
|
+
state=RequestState.NEEDS_INFORMATION,
|
|
393
|
+
missing=tuple(
|
|
394
|
+
InformationRequest(field_name, *_QUESTIONS[field_name])
|
|
395
|
+
for field_name in missing_fields
|
|
396
|
+
),
|
|
397
|
+
open_items=self._open_items(draft),
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
return RequestAssessment(
|
|
401
|
+
state=RequestState.READY_FOR_VALIDATION,
|
|
402
|
+
open_items=self._open_items(draft),
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
@staticmethod
|
|
406
|
+
def _open_items(draft: EngineeringRequestDraft) -> tuple[str, ...]:
|
|
407
|
+
output: list[str] = []
|
|
408
|
+
if draft.tool is None:
|
|
409
|
+
output.append("tool_not_specified")
|
|
410
|
+
if draft.payload_kg is None:
|
|
411
|
+
output.append("payload_not_specified")
|
|
412
|
+
if draft.purpose in {RequestPurpose.SIMULATE, RequestPurpose.QUALIFY, RequestPurpose.COMMISSIONING} and not draft.variation.configured:
|
|
413
|
+
output.append("physical_variation_not_specified")
|
|
414
|
+
return tuple(output)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _default_runs(preset: TestPreset) -> int:
|
|
418
|
+
return {
|
|
419
|
+
TestPreset.QUICK: 25,
|
|
420
|
+
TestPreset.ENGINEERING: 250,
|
|
421
|
+
TestPreset.ROBUSTNESS: 1_000,
|
|
422
|
+
TestPreset.STRESS: 10_000,
|
|
423
|
+
TestPreset.CUSTOM: 250,
|
|
424
|
+
}[preset]
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
class EngineeringRequestCompiler:
|
|
428
|
+
"""Compile a complete AI draft into bounded deterministic engine input."""
|
|
429
|
+
|
|
430
|
+
def __init__(self, completeness: RequestCompletenessValidator | None = None) -> None:
|
|
431
|
+
self.completeness = completeness or RequestCompletenessValidator()
|
|
432
|
+
|
|
433
|
+
def compile(self, draft: EngineeringRequestDraft) -> ValidatedEngineeringRequest:
|
|
434
|
+
assessment = self.completeness.assess(draft)
|
|
435
|
+
if not assessment.ready:
|
|
436
|
+
if assessment.state is RequestState.NEEDS_INFORMATION:
|
|
437
|
+
raise EngineeringRequestError(
|
|
438
|
+
"request_needs_information:" + ",".join(item.field for item in assessment.missing)
|
|
439
|
+
)
|
|
440
|
+
raise EngineeringRequestError("request_not_ready")
|
|
441
|
+
|
|
442
|
+
assert draft.robot is not None
|
|
443
|
+
assert draft.operation is not None
|
|
444
|
+
assert draft.object_id is not None
|
|
445
|
+
assert draft.source is not None
|
|
446
|
+
assert draft.destination is not None
|
|
447
|
+
|
|
448
|
+
rejection_codes: list[str] = []
|
|
449
|
+
if draft.robot not in CATALOG:
|
|
450
|
+
rejection_codes.append(f"unsupported_robot:{draft.robot}")
|
|
451
|
+
if draft.operation not in SUPPORTED_GOAL_ACTIONS:
|
|
452
|
+
rejection_codes.append(f"unsupported_operation:{draft.operation}")
|
|
453
|
+
if draft.runs is not None and draft.test_preset is not TestPreset.CUSTOM:
|
|
454
|
+
rejection_codes.append("runs_require_custom_test_preset")
|
|
455
|
+
if draft.test_preset is TestPreset.CUSTOM and draft.runs is None:
|
|
456
|
+
rejection_codes.append("custom_test_preset_requires_runs")
|
|
457
|
+
if rejection_codes:
|
|
458
|
+
raise EngineeringRequestError("request_rejected:" + ";".join(rejection_codes))
|
|
459
|
+
|
|
460
|
+
runs = draft.runs if draft.runs is not None else _default_runs(draft.test_preset)
|
|
461
|
+
hard_constraints = list(_SAFE_SYSTEM_CONSTRAINTS)
|
|
462
|
+
if draft.minimum_clearance_mm is not None:
|
|
463
|
+
hard_constraints.append("minimum_clearance")
|
|
464
|
+
if draft.max_cycle_time_s is not None:
|
|
465
|
+
hard_constraints.append("max_cycle_time")
|
|
466
|
+
|
|
467
|
+
canonical = {
|
|
468
|
+
"robot": draft.robot,
|
|
469
|
+
"operation": draft.operation,
|
|
470
|
+
"object_id": draft.object_id,
|
|
471
|
+
"source": draft.source,
|
|
472
|
+
"destination": draft.destination,
|
|
473
|
+
"tool": draft.tool,
|
|
474
|
+
"payload_kg": draft.payload_kg,
|
|
475
|
+
"purpose": draft.purpose.value,
|
|
476
|
+
"objective": draft.objective.value,
|
|
477
|
+
"test_preset": draft.test_preset.value,
|
|
478
|
+
"runs": runs,
|
|
479
|
+
"variation": asdict(draft.variation),
|
|
480
|
+
"faults": list(draft.faults),
|
|
481
|
+
"minimum_clearance_mm": draft.minimum_clearance_mm,
|
|
482
|
+
"max_cycle_time_s": draft.max_cycle_time_s,
|
|
483
|
+
}
|
|
484
|
+
digest = sha256(
|
|
485
|
+
json.dumps(canonical, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
486
|
+
).hexdigest()[:12]
|
|
487
|
+
|
|
488
|
+
return ValidatedEngineeringRequest(
|
|
489
|
+
request_id=f"REQ-{digest}",
|
|
490
|
+
robot_key=draft.robot,
|
|
491
|
+
operation=draft.operation,
|
|
492
|
+
object_id=draft.object_id,
|
|
493
|
+
source=draft.source,
|
|
494
|
+
destination=draft.destination,
|
|
495
|
+
tool=draft.tool,
|
|
496
|
+
payload_kg=draft.payload_kg,
|
|
497
|
+
purpose=draft.purpose,
|
|
498
|
+
objective=draft.objective,
|
|
499
|
+
test_preset=draft.test_preset,
|
|
500
|
+
runs=runs,
|
|
501
|
+
variation=draft.variation,
|
|
502
|
+
faults=draft.faults,
|
|
503
|
+
hard_constraints=tuple(hard_constraints),
|
|
504
|
+
minimum_clearance_mm=draft.minimum_clearance_mm,
|
|
505
|
+
max_cycle_time_s=draft.max_cycle_time_s,
|
|
506
|
+
open_items=assessment.open_items,
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _reject_unknown(obj: Mapping[str, Any], allowed: set[str], name: str) -> None:
|
|
511
|
+
extra = set(obj) - allowed
|
|
512
|
+
if extra:
|
|
513
|
+
raise EngineeringRequestError(f"{name}_unknown_fields:" + ",".join(sorted(extra)))
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def parse_engineering_request_draft(payload: Mapping[str, Any]) -> EngineeringRequestDraft:
|
|
517
|
+
if not isinstance(payload, Mapping):
|
|
518
|
+
raise EngineeringRequestError("engineering_request_must_be_object")
|
|
519
|
+
allowed = {
|
|
520
|
+
"robot", "operation", "object_id", "source", "destination", "tool",
|
|
521
|
+
"payload_kg", "purpose", "objective", "test_preset", "runs",
|
|
522
|
+
"variation", "faults", "minimum_clearance_mm", "max_cycle_time_s",
|
|
523
|
+
"uncertain_fields", "notes",
|
|
524
|
+
}
|
|
525
|
+
_reject_unknown(payload, allowed, "engineering_request")
|
|
526
|
+
|
|
527
|
+
variation_raw = payload.get("variation") or {}
|
|
528
|
+
if not isinstance(variation_raw, Mapping):
|
|
529
|
+
raise EngineeringRequestError("variation_must_be_object")
|
|
530
|
+
_reject_unknown(
|
|
531
|
+
variation_raw,
|
|
532
|
+
{
|
|
533
|
+
"object_x_mm", "object_y_mm", "object_z_mm", "yaw_deg",
|
|
534
|
+
"payload_min_kg", "payload_max_kg", "speed_min_pct", "speed_max_pct",
|
|
535
|
+
},
|
|
536
|
+
"variation",
|
|
537
|
+
)
|
|
538
|
+
variation = PhysicalVariation(
|
|
539
|
+
object_x_mm=_optional_number(variation_raw.get("object_x_mm"), field_name="object_x_mm", minimum=0),
|
|
540
|
+
object_y_mm=_optional_number(variation_raw.get("object_y_mm"), field_name="object_y_mm", minimum=0),
|
|
541
|
+
object_z_mm=_optional_number(variation_raw.get("object_z_mm"), field_name="object_z_mm", minimum=0),
|
|
542
|
+
yaw_deg=_optional_number(variation_raw.get("yaw_deg"), field_name="yaw_deg", minimum=0),
|
|
543
|
+
payload_min_kg=_optional_number(variation_raw.get("payload_min_kg"), field_name="payload_min_kg", minimum=0),
|
|
544
|
+
payload_max_kg=_optional_number(variation_raw.get("payload_max_kg"), field_name="payload_max_kg", minimum=0),
|
|
545
|
+
speed_min_pct=_optional_number(variation_raw.get("speed_min_pct"), field_name="speed_min_pct", minimum=0.001, maximum=100),
|
|
546
|
+
speed_max_pct=_optional_number(variation_raw.get("speed_max_pct"), field_name="speed_max_pct", minimum=0.001, maximum=100),
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
try:
|
|
550
|
+
purpose = RequestPurpose(str(payload.get("purpose", RequestPurpose.SIMULATE.value)).strip().lower())
|
|
551
|
+
objective = Objective(str(payload.get("objective", Objective.BALANCED.value)).strip().lower())
|
|
552
|
+
test_preset = TestPreset(str(payload.get("test_preset", TestPreset.ENGINEERING.value)).strip().lower())
|
|
553
|
+
except ValueError as exc:
|
|
554
|
+
raise EngineeringRequestError("request_enum_invalid") from exc
|
|
555
|
+
|
|
556
|
+
faults = _string_tuple(
|
|
557
|
+
payload.get("faults"),
|
|
558
|
+
field_name="faults",
|
|
559
|
+
allowed={item.value for item in FaultIntent},
|
|
560
|
+
)
|
|
561
|
+
uncertain_fields = _string_tuple(payload.get("uncertain_fields"), field_name="uncertain_fields")
|
|
562
|
+
notes = _string_tuple(payload.get("notes"), field_name="notes")
|
|
563
|
+
|
|
564
|
+
return EngineeringRequestDraft(
|
|
565
|
+
robot=_clean_text(payload.get("robot"), field_name="robot"),
|
|
566
|
+
operation=_clean_text(payload.get("operation"), field_name="operation"),
|
|
567
|
+
object_id=_clean_text(payload.get("object_id"), field_name="object_id"),
|
|
568
|
+
source=_clean_text(payload.get("source"), field_name="source"),
|
|
569
|
+
destination=_clean_text(payload.get("destination"), field_name="destination"),
|
|
570
|
+
tool=_clean_text(payload.get("tool"), field_name="tool"),
|
|
571
|
+
payload_kg=_optional_number(payload.get("payload_kg"), field_name="payload_kg", minimum=0),
|
|
572
|
+
purpose=purpose,
|
|
573
|
+
objective=objective,
|
|
574
|
+
test_preset=test_preset,
|
|
575
|
+
runs=_optional_int(payload.get("runs"), field_name="runs", minimum=1, maximum=MAX_RUNS),
|
|
576
|
+
variation=variation,
|
|
577
|
+
faults=faults,
|
|
578
|
+
minimum_clearance_mm=_optional_number(payload.get("minimum_clearance_mm"), field_name="minimum_clearance_mm", minimum=0.001),
|
|
579
|
+
max_cycle_time_s=_optional_number(payload.get("max_cycle_time_s"), field_name="max_cycle_time_s", minimum=0.001),
|
|
580
|
+
uncertain_fields=uncertain_fields,
|
|
581
|
+
notes=notes,
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
ENGINEERING_REQUEST_SCHEMA: dict[str, Any] = {
|
|
586
|
+
"type": "object",
|
|
587
|
+
"additionalProperties": False,
|
|
588
|
+
"required": [
|
|
589
|
+
"robot", "operation", "object_id", "source", "destination", "tool",
|
|
590
|
+
"payload_kg", "purpose", "objective", "test_preset", "runs",
|
|
591
|
+
"variation", "faults", "minimum_clearance_mm", "max_cycle_time_s",
|
|
592
|
+
"uncertain_fields", "notes",
|
|
593
|
+
],
|
|
594
|
+
"properties": {
|
|
595
|
+
"robot": {"type": ["string", "null"], "maxLength": 512},
|
|
596
|
+
"operation": {"type": ["string", "null"], "maxLength": 512},
|
|
597
|
+
"object_id": {"type": ["string", "null"], "maxLength": 512},
|
|
598
|
+
"source": {"type": ["string", "null"], "maxLength": 512},
|
|
599
|
+
"destination": {"type": ["string", "null"], "maxLength": 512},
|
|
600
|
+
"tool": {"type": ["string", "null"], "maxLength": 512},
|
|
601
|
+
"payload_kg": {"type": ["number", "null"], "minimum": 0},
|
|
602
|
+
"purpose": {"enum": [item.value for item in RequestPurpose]},
|
|
603
|
+
"objective": {"enum": [item.value for item in Objective]},
|
|
604
|
+
"test_preset": {"enum": [item.value for item in TestPreset]},
|
|
605
|
+
"runs": {"type": ["integer", "null"], "minimum": 1, "maximum": MAX_RUNS},
|
|
606
|
+
"variation": {
|
|
607
|
+
"type": "object",
|
|
608
|
+
"additionalProperties": False,
|
|
609
|
+
"required": [
|
|
610
|
+
"object_x_mm", "object_y_mm", "object_z_mm", "yaw_deg",
|
|
611
|
+
"payload_min_kg", "payload_max_kg", "speed_min_pct", "speed_max_pct",
|
|
612
|
+
],
|
|
613
|
+
"properties": {
|
|
614
|
+
"object_x_mm": {"type": ["number", "null"], "minimum": 0},
|
|
615
|
+
"object_y_mm": {"type": ["number", "null"], "minimum": 0},
|
|
616
|
+
"object_z_mm": {"type": ["number", "null"], "minimum": 0},
|
|
617
|
+
"yaw_deg": {"type": ["number", "null"], "minimum": 0},
|
|
618
|
+
"payload_min_kg": {"type": ["number", "null"], "minimum": 0},
|
|
619
|
+
"payload_max_kg": {"type": ["number", "null"], "minimum": 0},
|
|
620
|
+
"speed_min_pct": {"type": ["number", "null"], "exclusiveMinimum": 0, "maximum": 100},
|
|
621
|
+
"speed_max_pct": {"type": ["number", "null"], "exclusiveMinimum": 0, "maximum": 100},
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
"faults": {"type": "array", "maxItems": MAX_LIST_ITEMS, "uniqueItems": True, "items": {"enum": [item.value for item in FaultIntent]}},
|
|
625
|
+
"minimum_clearance_mm": {"type": ["number", "null"], "exclusiveMinimum": 0},
|
|
626
|
+
"max_cycle_time_s": {"type": ["number", "null"], "exclusiveMinimum": 0},
|
|
627
|
+
"uncertain_fields": {"type": "array", "maxItems": MAX_LIST_ITEMS, "uniqueItems": True, "items": {"type": "string", "maxLength": 128}},
|
|
628
|
+
"notes": {"type": "array", "maxItems": MAX_LIST_ITEMS, "uniqueItems": True, "items": {"type": "string", "maxLength": 512}},
|
|
629
|
+
},
|
|
630
|
+
}
|