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,392 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import shlex
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from .doctor import run_doctor
|
|
10
|
+
from .execution import ExecutionMode, ExecutionSupervisor
|
|
11
|
+
from .models import Goal, WorldState
|
|
12
|
+
from .planning import CapabilityPlanner
|
|
13
|
+
from .qualification import run_cross_vendor_smoke
|
|
14
|
+
from .qualification_cli import add_qualification_parsers, run_qualification_command
|
|
15
|
+
from .robots import CATALOG
|
|
16
|
+
from .simulation import DeterministicSimulationBackend
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _parser() -> argparse.ArgumentParser:
|
|
20
|
+
parser = argparse.ArgumentParser(prog="devagent-physical")
|
|
21
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
22
|
+
|
|
23
|
+
sub.add_parser("doctor", help="Check core Python runtime")
|
|
24
|
+
sub.add_parser("qualify", help="Run deterministic cross-vendor smoke qualification")
|
|
25
|
+
add_qualification_parsers(sub)
|
|
26
|
+
|
|
27
|
+
setup = sub.add_parser(
|
|
28
|
+
"setup",
|
|
29
|
+
help="Configure an explicit system profile; pip installation never changes OS packages",
|
|
30
|
+
)
|
|
31
|
+
setup.add_argument("--profile", choices=("ur5e-sim",), required=True)
|
|
32
|
+
setup.add_argument(
|
|
33
|
+
"--yes",
|
|
34
|
+
action="store_true",
|
|
35
|
+
help="Approve required system package changes non-interactively",
|
|
36
|
+
)
|
|
37
|
+
setup.add_argument(
|
|
38
|
+
"--dry-run",
|
|
39
|
+
action="store_true",
|
|
40
|
+
help="Show checks and required packages without changing the system",
|
|
41
|
+
)
|
|
42
|
+
setup.add_argument("--json", action="store_true", help="Emit machine-readable setup result")
|
|
43
|
+
|
|
44
|
+
sim = sub.add_parser("simulate", help="Run deterministic non-ROS simulation backend")
|
|
45
|
+
sim.add_argument("--robot", choices=sorted(CATALOG), default="ur5e")
|
|
46
|
+
sim.add_argument("--object", default="P17")
|
|
47
|
+
sim.add_argument("--source", default="conveyor_a")
|
|
48
|
+
sim.add_argument("--destination", default="cnc_04")
|
|
49
|
+
sim.add_argument("--inject-collision", action="store_true")
|
|
50
|
+
|
|
51
|
+
ros = sub.add_parser("ros", help="ROS 2 / Gazebo integration commands")
|
|
52
|
+
ros.add_argument("--ros-setup", default="/opt/ros/jazzy/setup.bash")
|
|
53
|
+
ros.add_argument("--workspace-setup", default=None)
|
|
54
|
+
ros_sub = ros.add_subparsers(dest="ros_command", required=True)
|
|
55
|
+
|
|
56
|
+
ros_sub.add_parser("doctor", help="Check ROS 2 Jazzy and UR simulation dependencies")
|
|
57
|
+
|
|
58
|
+
launch = ros_sub.add_parser("launch", help="Launch official UR5e Gazebo + MoveIt visual simulation")
|
|
59
|
+
launch.add_argument("--world-file", default=None)
|
|
60
|
+
launch.add_argument("--log-dir", default="~/.devagent/simulation")
|
|
61
|
+
launch.add_argument("--dry-run", action="store_true")
|
|
62
|
+
|
|
63
|
+
acceptance = ros_sub.add_parser("acceptance", help="Run laptop UR5e visual simulation acceptance")
|
|
64
|
+
acceptance.add_argument("--log-dir", default="~/.devagent/acceptance")
|
|
65
|
+
acceptance.add_argument("--report", default=None)
|
|
66
|
+
acceptance.add_argument("--startup-timeout", type=float, default=120.0)
|
|
67
|
+
acceptance.add_argument("--motion-timeout", type=float, default=90.0)
|
|
68
|
+
acceptance.add_argument("--keep-open", action="store_true")
|
|
69
|
+
|
|
70
|
+
demo = ros_sub.add_parser(
|
|
71
|
+
"demo",
|
|
72
|
+
help="Launch Gazebo + MoveIt, move the UR5e, verify motion, and keep the visual open",
|
|
73
|
+
)
|
|
74
|
+
demo.add_argument("--log-dir", default="~/.devagent/demo")
|
|
75
|
+
demo.add_argument("--report", default=None)
|
|
76
|
+
demo.add_argument("--startup-timeout", type=float, default=120.0)
|
|
77
|
+
demo.add_argument("--motion-timeout", type=float, default=90.0)
|
|
78
|
+
demo.add_argument(
|
|
79
|
+
"--close-after",
|
|
80
|
+
action="store_true",
|
|
81
|
+
help="Close Gazebo after a successful demo instead of leaving it visible",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
trajectory = ros_sub.add_parser(
|
|
85
|
+
"qualify-trajectory-runtime",
|
|
86
|
+
help="Qualify only the compiled UR5e trajectory execution/measurement scope",
|
|
87
|
+
)
|
|
88
|
+
trajectory.add_argument("--log-dir", default="~/.devagent/trajectory-qualification")
|
|
89
|
+
trajectory.add_argument("--report", default=None)
|
|
90
|
+
trajectory.add_argument("--startup-timeout", type=float, default=120.0)
|
|
91
|
+
|
|
92
|
+
return parser
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _setup_profile(args: argparse.Namespace) -> int:
|
|
96
|
+
from .ros2 import RosDoctor, SubprocessRosRunner
|
|
97
|
+
from .setup_profile import (
|
|
98
|
+
SubprocessSystemRunner,
|
|
99
|
+
UR5eSimulationSetup,
|
|
100
|
+
format_setup_plan,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def verify_ros_stack() -> bool:
|
|
104
|
+
try:
|
|
105
|
+
runner = SubprocessRosRunner(ros_setup="/opt/ros/jazzy/setup.bash")
|
|
106
|
+
return RosDoctor(runner, ros_setup="/opt/ros/jazzy/setup.bash").run().passed
|
|
107
|
+
except Exception:
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
progress = (
|
|
111
|
+
None
|
|
112
|
+
if args.json
|
|
113
|
+
else lambda message: print(message, file=sys.stderr, flush=True)
|
|
114
|
+
)
|
|
115
|
+
setup = UR5eSimulationSetup(
|
|
116
|
+
runner=SubprocessSystemRunner(stream_mutations=not args.json),
|
|
117
|
+
verifier=verify_ros_stack,
|
|
118
|
+
progress=progress,
|
|
119
|
+
)
|
|
120
|
+
plan = setup.inspect()
|
|
121
|
+
if not args.json:
|
|
122
|
+
print(format_setup_plan(plan))
|
|
123
|
+
|
|
124
|
+
if not plan.supported:
|
|
125
|
+
if args.json:
|
|
126
|
+
print(json.dumps(plan.to_dict(), indent=2))
|
|
127
|
+
else:
|
|
128
|
+
print(
|
|
129
|
+
"Unsupported platform for the qualified UR5e simulation profile; "
|
|
130
|
+
"no system changes were made."
|
|
131
|
+
)
|
|
132
|
+
return 8
|
|
133
|
+
|
|
134
|
+
if args.dry_run:
|
|
135
|
+
if args.json:
|
|
136
|
+
print(json.dumps(plan.to_dict(), indent=2))
|
|
137
|
+
else:
|
|
138
|
+
print("No system changes were made (dry-run).")
|
|
139
|
+
return 0
|
|
140
|
+
|
|
141
|
+
if plan.needs_install and not args.yes:
|
|
142
|
+
try:
|
|
143
|
+
answer = input("Install required system packages now? [y/N] ").strip().lower()
|
|
144
|
+
except EOFError:
|
|
145
|
+
answer = ""
|
|
146
|
+
if answer not in {"y", "yes"}:
|
|
147
|
+
if args.json:
|
|
148
|
+
print(
|
|
149
|
+
json.dumps(
|
|
150
|
+
{
|
|
151
|
+
"profile": plan.profile,
|
|
152
|
+
"passed": False,
|
|
153
|
+
"failure_code": "setup_cancelled",
|
|
154
|
+
},
|
|
155
|
+
indent=2,
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
else:
|
|
159
|
+
print("Setup cancelled; no system changes were made.")
|
|
160
|
+
return 7
|
|
161
|
+
|
|
162
|
+
result = setup.install(plan)
|
|
163
|
+
if args.json:
|
|
164
|
+
print(json.dumps(result.to_dict(), indent=2))
|
|
165
|
+
elif result.passed:
|
|
166
|
+
if result.changed_system:
|
|
167
|
+
print("UR5e simulation profile installed and verified.")
|
|
168
|
+
else:
|
|
169
|
+
print("UR5e simulation profile already installed and verified.")
|
|
170
|
+
print("Next: devagent-physical ros demo")
|
|
171
|
+
else:
|
|
172
|
+
print(f"Setup failed closed: {result.failure_code or 'verification_failed'}")
|
|
173
|
+
return 0 if result.passed else 9
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _core_qualify() -> int:
|
|
177
|
+
summary = run_cross_vendor_smoke()
|
|
178
|
+
print(
|
|
179
|
+
json.dumps(
|
|
180
|
+
{
|
|
181
|
+
"passed": summary.passed,
|
|
182
|
+
"violations": summary.violations,
|
|
183
|
+
"cases": [
|
|
184
|
+
{
|
|
185
|
+
"robot": case.robot,
|
|
186
|
+
"passed": case.passed,
|
|
187
|
+
"executed_tasks": case.executed_tasks,
|
|
188
|
+
"violations": case.violations,
|
|
189
|
+
}
|
|
190
|
+
for case in summary.cases
|
|
191
|
+
],
|
|
192
|
+
},
|
|
193
|
+
indent=2,
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
return 0 if summary.passed else 1
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _deterministic_simulate(args: argparse.Namespace) -> int:
|
|
200
|
+
resource = CATALOG[args.robot]()
|
|
201
|
+
goal = Goal("cli-goal", "load", args.object, args.source, args.destination)
|
|
202
|
+
graph = CapabilityPlanner().plan(goal, [resource])
|
|
203
|
+
world = WorldState(facts={"robot_ready", "object_available"})
|
|
204
|
+
backend = DeterministicSimulationBackend({"T2"} if args.inject_collision else None)
|
|
205
|
+
report = ExecutionSupervisor().execute(
|
|
206
|
+
graph, [resource], world, backend, ExecutionMode.SIMULATION
|
|
207
|
+
)
|
|
208
|
+
print(
|
|
209
|
+
json.dumps(
|
|
210
|
+
{
|
|
211
|
+
"completed": report.completed,
|
|
212
|
+
"executed_tasks": report.executed_tasks,
|
|
213
|
+
"blocked_task": report.blocked_task,
|
|
214
|
+
"issues": [
|
|
215
|
+
{"code": issue.code, "task_id": issue.task_id, "detail": issue.detail}
|
|
216
|
+
for issue in report.issues
|
|
217
|
+
],
|
|
218
|
+
"world_facts": sorted(world.facts),
|
|
219
|
+
},
|
|
220
|
+
indent=2,
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
return 0 if report.completed else 2
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _ros_runner(args: argparse.Namespace):
|
|
227
|
+
from .ros2 import SubprocessRosRunner
|
|
228
|
+
|
|
229
|
+
return SubprocessRosRunner(
|
|
230
|
+
ros_setup=args.ros_setup,
|
|
231
|
+
workspace_setup=args.workspace_setup,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _ros_doctor(args: argparse.Namespace) -> int:
|
|
236
|
+
from .ros2 import RosDoctor
|
|
237
|
+
|
|
238
|
+
report = RosDoctor(_ros_runner(args), ros_setup=args.ros_setup).run()
|
|
239
|
+
for check in report.checks:
|
|
240
|
+
label = "PASS" if check.passed else ("WARN" if not check.required else "FAIL")
|
|
241
|
+
print(f"{check.name}: {label} ({check.detail})")
|
|
242
|
+
return 0 if report.passed else 3
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _ros_launch(args: argparse.Namespace) -> int:
|
|
246
|
+
from datetime import datetime, timezone
|
|
247
|
+
from .ros2 import UR5eSimulationLauncher, official_ur5e_moveit_command
|
|
248
|
+
|
|
249
|
+
command = official_ur5e_moveit_command(world_file=args.world_file)
|
|
250
|
+
if args.dry_run:
|
|
251
|
+
print(shlex.join(command))
|
|
252
|
+
return 0
|
|
253
|
+
|
|
254
|
+
log_dir = Path(args.log_dir).expanduser().resolve()
|
|
255
|
+
timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
256
|
+
log_path = log_dir / f"ur5e-visual-{timestamp}.log"
|
|
257
|
+
simulation = UR5eSimulationLauncher(_ros_runner(args)).launch(
|
|
258
|
+
log_path=log_path,
|
|
259
|
+
world_file=args.world_file,
|
|
260
|
+
)
|
|
261
|
+
print(f"UR5e visual simulation started (pid={simulation.process.pid})")
|
|
262
|
+
print(f"Log: {simulation.log_path}")
|
|
263
|
+
print("Gazebo + MoveIt/RViz are launched by the official ur_simulation_gz package.")
|
|
264
|
+
print("Press Ctrl-C to stop.")
|
|
265
|
+
try:
|
|
266
|
+
return simulation.process.wait()
|
|
267
|
+
except KeyboardInterrupt:
|
|
268
|
+
simulation.stop()
|
|
269
|
+
return 130
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _ros_acceptance(args: argparse.Namespace) -> int:
|
|
273
|
+
from .ros2 import UR5eLaptopAcceptance
|
|
274
|
+
|
|
275
|
+
report = UR5eLaptopAcceptance(
|
|
276
|
+
_ros_runner(args), ros_setup=args.ros_setup
|
|
277
|
+
).run(
|
|
278
|
+
log_dir=Path(args.log_dir),
|
|
279
|
+
startup_timeout_s=args.startup_timeout,
|
|
280
|
+
motion_timeout_s=args.motion_timeout,
|
|
281
|
+
keep_open=args.keep_open,
|
|
282
|
+
)
|
|
283
|
+
report_path = (
|
|
284
|
+
Path(args.report).expanduser().resolve()
|
|
285
|
+
if args.report
|
|
286
|
+
else Path(args.log_dir).expanduser().resolve() / "latest-acceptance.json"
|
|
287
|
+
)
|
|
288
|
+
report.write_json(report_path)
|
|
289
|
+
print(json.dumps(report.to_dict(), indent=2))
|
|
290
|
+
print(f"Acceptance report: {report_path}")
|
|
291
|
+
return 0 if report.passed else 4
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _ros_demo(args: argparse.Namespace) -> int:
|
|
295
|
+
from .ros2 import UR5eLaptopAcceptance
|
|
296
|
+
|
|
297
|
+
report = UR5eLaptopAcceptance(
|
|
298
|
+
_ros_runner(args), ros_setup=args.ros_setup
|
|
299
|
+
).run(
|
|
300
|
+
log_dir=Path(args.log_dir),
|
|
301
|
+
startup_timeout_s=args.startup_timeout,
|
|
302
|
+
motion_timeout_s=args.motion_timeout,
|
|
303
|
+
keep_open=not args.close_after,
|
|
304
|
+
)
|
|
305
|
+
report_path = (
|
|
306
|
+
Path(args.report).expanduser().resolve()
|
|
307
|
+
if args.report
|
|
308
|
+
else Path(args.log_dir).expanduser().resolve() / "latest-demo.json"
|
|
309
|
+
)
|
|
310
|
+
report.write_json(report_path)
|
|
311
|
+
print(json.dumps(report.to_dict(), indent=2))
|
|
312
|
+
print(f"Demo report: {report_path}")
|
|
313
|
+
if report.passed and not args.close_after:
|
|
314
|
+
print("VISUAL DEMO PASS: Gazebo + MoveIt/RViz remain open so you can inspect the moved UR5e.")
|
|
315
|
+
elif report.passed:
|
|
316
|
+
print("VISUAL DEMO PASS: UR5e motion was measured and the simulator was closed cleanly.")
|
|
317
|
+
return 0 if report.passed else 5
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _ros_trajectory_runtime_qualification(args: argparse.Namespace) -> int:
|
|
321
|
+
from .ros2.qualification import (
|
|
322
|
+
ur5e_trajectory_runtime_cases,
|
|
323
|
+
ur5e_trajectory_runtime_twin,
|
|
324
|
+
)
|
|
325
|
+
from .ros2.ur5e_adapter import UR5eGazeboAdapter
|
|
326
|
+
from .trajectory_qualification import TrajectoryRuntimeQualificationRunner
|
|
327
|
+
|
|
328
|
+
log_dir = Path(args.log_dir).expanduser().resolve()
|
|
329
|
+
twin = ur5e_trajectory_runtime_twin()
|
|
330
|
+
adapter = UR5eGazeboAdapter(
|
|
331
|
+
_ros_runner(args),
|
|
332
|
+
ros_setup=args.ros_setup,
|
|
333
|
+
log_dir=log_dir / "simulation",
|
|
334
|
+
startup_timeout_s=args.startup_timeout,
|
|
335
|
+
)
|
|
336
|
+
report = TrajectoryRuntimeQualificationRunner().run(
|
|
337
|
+
adapter=adapter,
|
|
338
|
+
twin=twin,
|
|
339
|
+
cases=ur5e_trajectory_runtime_cases(twin),
|
|
340
|
+
)
|
|
341
|
+
payload = report.to_dict()
|
|
342
|
+
report_path = (
|
|
343
|
+
Path(args.report).expanduser().resolve()
|
|
344
|
+
if args.report
|
|
345
|
+
else log_dir / "latest-trajectory-runtime.json"
|
|
346
|
+
)
|
|
347
|
+
report_path.parent.mkdir(parents=True, exist_ok=True)
|
|
348
|
+
report_path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
|
349
|
+
print(json.dumps(payload, indent=2))
|
|
350
|
+
print(f"Trajectory runtime qualification report: {report_path}")
|
|
351
|
+
return 0 if report.promotion_candidate else 6
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def main(argv: list[str] | None = None) -> int:
|
|
355
|
+
args = _parser().parse_args(argv)
|
|
356
|
+
|
|
357
|
+
if args.command == "doctor":
|
|
358
|
+
checks = run_doctor()
|
|
359
|
+
for check in checks:
|
|
360
|
+
print(f"{check.name}: {'PASS' if check.passed else 'FAIL'} ({check.detail})")
|
|
361
|
+
return 0 if all(check.passed for check in checks) else 1
|
|
362
|
+
|
|
363
|
+
if args.command == "qualify":
|
|
364
|
+
return _core_qualify()
|
|
365
|
+
|
|
366
|
+
qualification_code = run_qualification_command(args)
|
|
367
|
+
if qualification_code is not None:
|
|
368
|
+
return qualification_code
|
|
369
|
+
|
|
370
|
+
if args.command == "setup":
|
|
371
|
+
return _setup_profile(args)
|
|
372
|
+
|
|
373
|
+
if args.command == "simulate":
|
|
374
|
+
return _deterministic_simulate(args)
|
|
375
|
+
|
|
376
|
+
if args.command == "ros":
|
|
377
|
+
if args.ros_command == "doctor":
|
|
378
|
+
return _ros_doctor(args)
|
|
379
|
+
if args.ros_command == "launch":
|
|
380
|
+
return _ros_launch(args)
|
|
381
|
+
if args.ros_command == "acceptance":
|
|
382
|
+
return _ros_acceptance(args)
|
|
383
|
+
if args.ros_command == "demo":
|
|
384
|
+
return _ros_demo(args)
|
|
385
|
+
if args.ros_command == "qualify-trajectory-runtime":
|
|
386
|
+
return _ros_trajectory_runtime_qualification(args)
|
|
387
|
+
|
|
388
|
+
return 2
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
if __name__ == "__main__":
|
|
392
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import platform
|
|
5
|
+
import sys
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True, slots=True)
|
|
10
|
+
class Check:
|
|
11
|
+
name: str
|
|
12
|
+
passed: bool
|
|
13
|
+
detail: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def run_doctor() -> tuple[Check, ...]:
|
|
17
|
+
return (
|
|
18
|
+
Check("python", sys.version_info >= (3, 11), platform.python_version()),
|
|
19
|
+
Check("core_package", importlib.util.find_spec("devagent_physical_engine") is not None, "importable"),
|
|
20
|
+
)
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict, dataclass
|
|
4
|
+
from typing import Any, Sequence
|
|
5
|
+
|
|
6
|
+
from .agent.contracts import AgentEvidence, RoutingPolicy
|
|
7
|
+
from .agent.coordinator import PlanningCoordinationError, PlanningCoordinator, VerifiedPlanArtifact
|
|
8
|
+
from .agent.critic import CriticAgent
|
|
9
|
+
from .agent.evidence import task_graph_to_dict
|
|
10
|
+
from .agent.interpreter import InterpretedEngineeringRequest, RequirementInterpreterAgent
|
|
11
|
+
from .agent.planner import PlannerAgent
|
|
12
|
+
from .engineering_request import Objective, RequestPurpose, TestPreset, ValidatedEngineeringRequest
|
|
13
|
+
from .execution import ExecutionMode, ExecutionSupervisor
|
|
14
|
+
from .models import WorldState
|
|
15
|
+
from .simulation import DeterministicSimulationBackend
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, slots=True)
|
|
19
|
+
class AdaptiveReasoningStrategy:
|
|
20
|
+
risk_score: int
|
|
21
|
+
max_revision_rounds: int
|
|
22
|
+
min_critic_score: float
|
|
23
|
+
future_physical_candidate_target: int
|
|
24
|
+
reasons: tuple[str, ...]
|
|
25
|
+
|
|
26
|
+
def to_dict(self) -> dict[str, Any]:
|
|
27
|
+
return asdict(self)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AdaptiveReasoningPolicy:
|
|
31
|
+
"""Spend AI calls where engineering risk justifies them.
|
|
32
|
+
|
|
33
|
+
v0.7 intentionally optimizes reasoning depth rather than asking an LLM for
|
|
34
|
+
many superficially different high-level pick/move/place graphs. Physical
|
|
35
|
+
candidate diversity belongs to the measured motion/simulation layer.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def select(self, request: ValidatedEngineeringRequest) -> AdaptiveReasoningStrategy:
|
|
39
|
+
score = 0
|
|
40
|
+
reasons: list[str] = []
|
|
41
|
+
|
|
42
|
+
if request.purpose is RequestPurpose.COMMISSIONING:
|
|
43
|
+
score += 3
|
|
44
|
+
reasons.append("commissioning_intent")
|
|
45
|
+
elif request.purpose is RequestPurpose.QUALIFY:
|
|
46
|
+
score += 2
|
|
47
|
+
reasons.append("qualification_intent")
|
|
48
|
+
elif request.purpose is RequestPurpose.SIMULATE:
|
|
49
|
+
score += 1
|
|
50
|
+
reasons.append("simulation_intent")
|
|
51
|
+
|
|
52
|
+
if request.test_preset in {TestPreset.ROBUSTNESS, TestPreset.STRESS}:
|
|
53
|
+
score += 2
|
|
54
|
+
reasons.append("high_coverage_test_preset")
|
|
55
|
+
if request.variation.configured:
|
|
56
|
+
score += 2
|
|
57
|
+
reasons.append("physical_variation_requested")
|
|
58
|
+
if request.faults:
|
|
59
|
+
score += 2
|
|
60
|
+
reasons.append("fault_injection_requested")
|
|
61
|
+
if request.minimum_clearance_mm is not None or request.max_cycle_time_s is not None:
|
|
62
|
+
score += 1
|
|
63
|
+
reasons.append("explicit_engineering_threshold")
|
|
64
|
+
if request.objective is not Objective.BALANCED:
|
|
65
|
+
score += 1
|
|
66
|
+
reasons.append("explicit_optimization_objective")
|
|
67
|
+
|
|
68
|
+
if score >= 7:
|
|
69
|
+
return AdaptiveReasoningStrategy(score, 3, 0.85, 7, tuple(reasons))
|
|
70
|
+
if score >= 4:
|
|
71
|
+
return AdaptiveReasoningStrategy(score, 2, 0.78, 5, tuple(reasons))
|
|
72
|
+
return AdaptiveReasoningStrategy(score, 1, 0.70, 3, tuple(reasons))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True, slots=True)
|
|
76
|
+
class SmartEngineeringResult:
|
|
77
|
+
interpretation: InterpretedEngineeringRequest
|
|
78
|
+
reasoning_strategy: AdaptiveReasoningStrategy | None = None
|
|
79
|
+
plan: VerifiedPlanArtifact | None = None
|
|
80
|
+
deterministic_simulation_completed: bool = False
|
|
81
|
+
failure_code: str | None = None
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def ready_for_twin_simulation(self) -> bool:
|
|
85
|
+
return (
|
|
86
|
+
self.interpretation.validated is not None
|
|
87
|
+
and self.plan is not None
|
|
88
|
+
and self.deterministic_simulation_completed
|
|
89
|
+
and self.failure_code is None
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def to_dict(self) -> dict[str, Any]:
|
|
93
|
+
payload: dict[str, Any] = {
|
|
94
|
+
"interpretation": self.interpretation.to_dict(),
|
|
95
|
+
"reasoning_strategy": (
|
|
96
|
+
self.reasoning_strategy.to_dict()
|
|
97
|
+
if self.reasoning_strategy is not None
|
|
98
|
+
else None
|
|
99
|
+
),
|
|
100
|
+
"deterministic_simulation_completed": self.deterministic_simulation_completed,
|
|
101
|
+
"physical_qualification": False,
|
|
102
|
+
"ready_for_twin_simulation": self.ready_for_twin_simulation,
|
|
103
|
+
"failure_code": self.failure_code,
|
|
104
|
+
"next_stage": (
|
|
105
|
+
"validated_twin_and_measured_physics_simulation"
|
|
106
|
+
if self.ready_for_twin_simulation
|
|
107
|
+
else None
|
|
108
|
+
),
|
|
109
|
+
}
|
|
110
|
+
if self.plan is not None:
|
|
111
|
+
payload["plan"] = {
|
|
112
|
+
"graph_hash": self.plan.graph_hash,
|
|
113
|
+
"graph": task_graph_to_dict(self.plan.graph),
|
|
114
|
+
"revision_rounds": self.plan.revision_rounds,
|
|
115
|
+
"planner_calls": len(self.plan.planner_evidence),
|
|
116
|
+
"critic_calls": len(self.plan.critic_evidence),
|
|
117
|
+
"selection_claim": (
|
|
118
|
+
"verified_high_level_plan_only; not yet a measured optimized motion plan"
|
|
119
|
+
),
|
|
120
|
+
}
|
|
121
|
+
else:
|
|
122
|
+
payload["plan"] = None
|
|
123
|
+
return payload
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class SmartEngineeringAgent:
|
|
127
|
+
"""Natural language -> verified high-level plan -> deterministic model smoke.
|
|
128
|
+
|
|
129
|
+
This is deliberately not a physical commissioning claim. The result becomes
|
|
130
|
+
eligible for the next Twin/ROS stage only after interpretation, deterministic
|
|
131
|
+
compilation, planner/critic verification, and deterministic model execution
|
|
132
|
+
all succeed.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
def __init__(
|
|
136
|
+
self,
|
|
137
|
+
interpreter: RequirementInterpreterAgent,
|
|
138
|
+
planner: PlannerAgent,
|
|
139
|
+
critic: CriticAgent,
|
|
140
|
+
*,
|
|
141
|
+
reasoning_policy: AdaptiveReasoningPolicy | None = None,
|
|
142
|
+
) -> None:
|
|
143
|
+
self.interpreter = interpreter
|
|
144
|
+
self.planner = planner
|
|
145
|
+
self.critic = critic
|
|
146
|
+
self.reasoning_policy = reasoning_policy or AdaptiveReasoningPolicy()
|
|
147
|
+
|
|
148
|
+
def run(
|
|
149
|
+
self,
|
|
150
|
+
text: str,
|
|
151
|
+
*,
|
|
152
|
+
interpreter_policy: RoutingPolicy,
|
|
153
|
+
planner_policy: RoutingPolicy,
|
|
154
|
+
critic_policy: RoutingPolicy,
|
|
155
|
+
follow_ups: Sequence[str] = (),
|
|
156
|
+
) -> SmartEngineeringResult:
|
|
157
|
+
interpreted = self.interpreter.interpret(
|
|
158
|
+
text,
|
|
159
|
+
interpreter_policy,
|
|
160
|
+
follow_ups=follow_ups,
|
|
161
|
+
)
|
|
162
|
+
request = interpreted.validated
|
|
163
|
+
if request is None:
|
|
164
|
+
return SmartEngineeringResult(
|
|
165
|
+
interpretation=interpreted,
|
|
166
|
+
failure_code=interpreted.rejection_code,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
strategy = self.reasoning_policy.select(request)
|
|
170
|
+
goal = request.goal()
|
|
171
|
+
resources = [request.resource()]
|
|
172
|
+
# Explicit nominal model state, never represented as observed site state.
|
|
173
|
+
world = WorldState(
|
|
174
|
+
facts={"robot_ready", "object_available"},
|
|
175
|
+
values={
|
|
176
|
+
"state_origin": "devagent_nominal_planning_model",
|
|
177
|
+
"request_id": request.request_id,
|
|
178
|
+
},
|
|
179
|
+
)
|
|
180
|
+
coordinator = PlanningCoordinator(
|
|
181
|
+
self.planner,
|
|
182
|
+
self.critic,
|
|
183
|
+
max_revision_rounds=strategy.max_revision_rounds,
|
|
184
|
+
min_accept_score=strategy.min_critic_score,
|
|
185
|
+
)
|
|
186
|
+
try:
|
|
187
|
+
plan = coordinator.create_verified_plan(
|
|
188
|
+
goal,
|
|
189
|
+
resources,
|
|
190
|
+
world,
|
|
191
|
+
planner_policy,
|
|
192
|
+
critic_policy,
|
|
193
|
+
engineering_context=request.to_dict(),
|
|
194
|
+
)
|
|
195
|
+
except PlanningCoordinationError as exc:
|
|
196
|
+
return SmartEngineeringResult(
|
|
197
|
+
interpretation=interpreted,
|
|
198
|
+
reasoning_strategy=strategy,
|
|
199
|
+
failure_code=f"planning_failed:{str(exc).split(':', 1)[0]}",
|
|
200
|
+
)
|
|
201
|
+
except Exception as exc:
|
|
202
|
+
# Never expose provider/internal exception text as customer evidence.
|
|
203
|
+
return SmartEngineeringResult(
|
|
204
|
+
interpretation=interpreted,
|
|
205
|
+
reasoning_strategy=strategy,
|
|
206
|
+
failure_code=f"planning_failed:{type(exc).__name__}",
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
simulation_world = WorldState(facts=set(world.facts), values=dict(world.values))
|
|
210
|
+
simulation = ExecutionSupervisor().execute(
|
|
211
|
+
plan.graph,
|
|
212
|
+
resources,
|
|
213
|
+
simulation_world,
|
|
214
|
+
DeterministicSimulationBackend(),
|
|
215
|
+
ExecutionMode.SIMULATION,
|
|
216
|
+
)
|
|
217
|
+
if not simulation.completed:
|
|
218
|
+
issue_codes = sorted({issue.code for issue in simulation.issues})
|
|
219
|
+
return SmartEngineeringResult(
|
|
220
|
+
interpretation=interpreted,
|
|
221
|
+
reasoning_strategy=strategy,
|
|
222
|
+
plan=plan,
|
|
223
|
+
deterministic_simulation_completed=False,
|
|
224
|
+
failure_code=(
|
|
225
|
+
"deterministic_simulation_failed:"
|
|
226
|
+
+ (",".join(issue_codes) if issue_codes else "unknown")
|
|
227
|
+
),
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
return SmartEngineeringResult(
|
|
231
|
+
interpretation=interpreted,
|
|
232
|
+
reasoning_strategy=strategy,
|
|
233
|
+
plan=plan,
|
|
234
|
+
deterministic_simulation_completed=True,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def successful_agent_evidence(result: SmartEngineeringResult) -> tuple[AgentEvidence, ...]:
|
|
239
|
+
items: list[AgentEvidence] = [result.interpretation.evidence]
|
|
240
|
+
if result.plan is not None:
|
|
241
|
+
items.extend(result.plan.planner_evidence)
|
|
242
|
+
items.extend(result.plan.critic_evidence)
|
|
243
|
+
return tuple(items)
|