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.
Files changed (84) hide show
  1. devagent_physical_engine/__init__.py +44 -0
  2. devagent_physical_engine/agent/__init__.py +40 -0
  3. devagent_physical_engine/agent/compiler.py +285 -0
  4. devagent_physical_engine/agent/contracts.py +129 -0
  5. devagent_physical_engine/agent/coordinator.py +108 -0
  6. devagent_physical_engine/agent/critic.py +72 -0
  7. devagent_physical_engine/agent/evidence.py +34 -0
  8. devagent_physical_engine/agent/interpreter.py +179 -0
  9. devagent_physical_engine/agent/planner.py +105 -0
  10. devagent_physical_engine/agent/recovery.py +54 -0
  11. devagent_physical_engine/agent/routing.py +76 -0
  12. devagent_physical_engine/agent/runtime.py +270 -0
  13. devagent_physical_engine/agent/semantic.py +304 -0
  14. devagent_physical_engine/agent/structured.py +423 -0
  15. devagent_physical_engine/ai_cli.py +226 -0
  16. devagent_physical_engine/cli.py +392 -0
  17. devagent_physical_engine/doctor.py +20 -0
  18. devagent_physical_engine/engineering_agent.py +243 -0
  19. devagent_physical_engine/engineering_request.py +630 -0
  20. devagent_physical_engine/execution.py +90 -0
  21. devagent_physical_engine/models.py +143 -0
  22. devagent_physical_engine/operating_envelope.py +120 -0
  23. devagent_physical_engine/optimization/__init__.py +50 -0
  24. devagent_physical_engine/optimization/benchmark.py +122 -0
  25. devagent_physical_engine/optimization/candidates.py +198 -0
  26. devagent_physical_engine/optimization/contracts.py +235 -0
  27. devagent_physical_engine/optimization/evaluator.py +107 -0
  28. devagent_physical_engine/optimization/evidence.py +53 -0
  29. devagent_physical_engine/optimization/experience.py +105 -0
  30. devagent_physical_engine/optimization/measured.py +125 -0
  31. devagent_physical_engine/optimization/optimizer.py +215 -0
  32. devagent_physical_engine/optimization/orchestrator.py +155 -0
  33. devagent_physical_engine/physical_campaign.py +413 -0
  34. devagent_physical_engine/physical_evidence.py +214 -0
  35. devagent_physical_engine/physical_motion.py +196 -0
  36. devagent_physical_engine/planning.py +80 -0
  37. devagent_physical_engine/preexecution_contract.py +65 -0
  38. devagent_physical_engine/provider_adapters/__init__.py +22 -0
  39. devagent_physical_engine/provider_adapters/anthropic.py +112 -0
  40. devagent_physical_engine/provider_adapters/common.py +187 -0
  41. devagent_physical_engine/provider_adapters/factory.py +20 -0
  42. devagent_physical_engine/provider_adapters/gemini.py +126 -0
  43. devagent_physical_engine/provider_adapters/openai.py +95 -0
  44. devagent_physical_engine/provider_qualification.py +268 -0
  45. devagent_physical_engine/providers.py +94 -0
  46. devagent_physical_engine/qualification.py +44 -0
  47. devagent_physical_engine/qualification_cli.py +195 -0
  48. devagent_physical_engine/qualification_harness.py +917 -0
  49. devagent_physical_engine/robot_platform.py +411 -0
  50. devagent_physical_engine/robots.py +76 -0
  51. devagent_physical_engine/ros2/__init__.py +35 -0
  52. devagent_physical_engine/ros2/acceptance.py +324 -0
  53. devagent_physical_engine/ros2/commands.py +175 -0
  54. devagent_physical_engine/ros2/doctor.py +116 -0
  55. devagent_physical_engine/ros2/fk_probe.py +83 -0
  56. devagent_physical_engine/ros2/frame_alignment.py +61 -0
  57. devagent_physical_engine/ros2/gazebo_world.py +125 -0
  58. devagent_physical_engine/ros2/joint_state_recorder.py +64 -0
  59. devagent_physical_engine/ros2/measured_motion.py +233 -0
  60. devagent_physical_engine/ros2/moveit_scene.py +121 -0
  61. devagent_physical_engine/ros2/preexecution.py +113 -0
  62. devagent_physical_engine/ros2/qualification.py +81 -0
  63. devagent_physical_engine/ros2/qualification_v10.py +252 -0
  64. devagent_physical_engine/ros2/scene_probe.py +219 -0
  65. devagent_physical_engine/ros2/state_validity_probe.py +125 -0
  66. devagent_physical_engine/ros2/tf_probe.py +51 -0
  67. devagent_physical_engine/ros2/trajectory.py +188 -0
  68. devagent_physical_engine/ros2/ur5e.py +59 -0
  69. devagent_physical_engine/ros2/ur5e_adapter.py +349 -0
  70. devagent_physical_engine/ros2/ur5e_v10_adapter.py +292 -0
  71. devagent_physical_engine/setup_profile.py +356 -0
  72. devagent_physical_engine/simulation.py +32 -0
  73. devagent_physical_engine/simulation_platform.py +269 -0
  74. devagent_physical_engine/trajectory_qualification.py +201 -0
  75. devagent_physical_engine/twin.py +939 -0
  76. devagent_physical_engine/twin_builder.py +309 -0
  77. devagent_physical_engine/twin_materialization.py +404 -0
  78. devagent_physical_engine/verification.py +46 -0
  79. devagent_physical_engine-0.10.0.dist-info/METADATA +315 -0
  80. devagent_physical_engine-0.10.0.dist-info/RECORD +84 -0
  81. devagent_physical_engine-0.10.0.dist-info/WHEEL +5 -0
  82. devagent_physical_engine-0.10.0.dist-info/entry_points.txt +3 -0
  83. devagent_physical_engine-0.10.0.dist-info/licenses/NOTICE +2 -0
  84. devagent_physical_engine-0.10.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,423 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from math import isfinite
5
+ from typing import Any, Mapping
6
+
7
+ from .contracts import CriticDecision, Critique, RecoveryAdvice, RecoveryDecision
8
+ from ..models import ActionContract, Capability, TaskGraph, TaskNode
9
+
10
+ MAX_TASKS = 64
11
+ MAX_ITEMS = 32
12
+ MAX_TEXT = 512
13
+ MAX_JSON_DEPTH = 8
14
+ MAX_JSON_ITEMS = 256
15
+ MAX_JSON_TEXT = 8192
16
+
17
+
18
+ class StructuredOutputError(ValueError):
19
+ pass
20
+
21
+
22
+ class JsonSafetyError(ValueError):
23
+ pass
24
+
25
+
26
+ def json_safe(value: Any, *, path: str = "root", depth: int = 0) -> Any:
27
+ """Return a JSON-safe copy or reject unsupported/unbounded input."""
28
+
29
+ if depth > MAX_JSON_DEPTH:
30
+ raise JsonSafetyError(f"json_depth_exceeded:{path}")
31
+ if value is None or isinstance(value, (bool, int)):
32
+ return value
33
+ if isinstance(value, float):
34
+ if not isfinite(value):
35
+ raise JsonSafetyError(f"non_finite_number:{path}")
36
+ return value
37
+ if isinstance(value, str):
38
+ if len(value) > MAX_JSON_TEXT:
39
+ raise JsonSafetyError(f"json_text_too_long:{path}")
40
+ return value
41
+ if isinstance(value, (list, tuple)):
42
+ if len(value) > MAX_JSON_ITEMS:
43
+ raise JsonSafetyError(f"json_array_too_large:{path}")
44
+ return [
45
+ json_safe(item, path=f"{path}[{index}]", depth=depth + 1)
46
+ for index, item in enumerate(value)
47
+ ]
48
+ if isinstance(value, Mapping):
49
+ if len(value) > MAX_JSON_ITEMS:
50
+ raise JsonSafetyError(f"json_object_too_large:{path}")
51
+ output: dict[str, Any] = {}
52
+ for key, item in value.items():
53
+ if not isinstance(key, str) or not key:
54
+ raise JsonSafetyError(f"json_key_invalid:{path}")
55
+ if len(key) > 256:
56
+ raise JsonSafetyError(f"json_key_too_long:{path}")
57
+ output[key] = json_safe(
58
+ item, path=f"{path}.{key}", depth=depth + 1
59
+ )
60
+ return output
61
+ raise JsonSafetyError(f"json_type_unsupported:{path}:{type(value).__name__}")
62
+
63
+
64
+ SENSITIVE_KEY_PARTS = (
65
+ "api_key",
66
+ "apikey",
67
+ "authorization",
68
+ "credential",
69
+ "password",
70
+ "private_key",
71
+ "secret",
72
+ "token",
73
+ )
74
+
75
+
76
+ def redact_sensitive(
77
+ value: Any,
78
+ *,
79
+ path: str = "root",
80
+ depth: int = 0,
81
+ seen: set[int] | None = None,
82
+ ) -> Any:
83
+ """Redact common credential-shaped fields before model invocation.
84
+
85
+ This is defense in depth, not a substitute for keeping credentials out of
86
+ the world model and resource metadata entirely. JSON validation happens
87
+ after redaction so secret-shaped unsupported objects are never serialized.
88
+ """
89
+
90
+ if depth > MAX_JSON_DEPTH:
91
+ raise JsonSafetyError(f"redaction_depth_exceeded:{path}")
92
+
93
+ if seen is None:
94
+ seen = set()
95
+
96
+ if isinstance(value, Mapping):
97
+ identity = id(value)
98
+ if identity in seen:
99
+ raise JsonSafetyError(f"cyclic_context:{path}")
100
+ seen.add(identity)
101
+ try:
102
+ output: dict[Any, Any] = {}
103
+ for key, child in value.items():
104
+ if isinstance(key, str):
105
+ normalized_key = key.lower().replace("-", "_")
106
+ if any(
107
+ part in normalized_key for part in SENSITIVE_KEY_PARTS
108
+ ):
109
+ output[key] = "<redacted>"
110
+ continue
111
+ output[key] = redact_sensitive(
112
+ child,
113
+ path=f"{path}.{key}",
114
+ depth=depth + 1,
115
+ seen=seen,
116
+ )
117
+ return output
118
+ finally:
119
+ seen.remove(identity)
120
+
121
+ if isinstance(value, (list, tuple)):
122
+ identity = id(value)
123
+ if identity in seen:
124
+ raise JsonSafetyError(f"cyclic_context:{path}")
125
+ seen.add(identity)
126
+ try:
127
+ return [
128
+ redact_sensitive(
129
+ child,
130
+ path=f"{path}[{index}]",
131
+ depth=depth + 1,
132
+ seen=seen,
133
+ )
134
+ for index, child in enumerate(value)
135
+ ]
136
+ finally:
137
+ seen.remove(identity)
138
+
139
+ return value
140
+
141
+
142
+ def _object(value: Any, name: str) -> Mapping[str, Any]:
143
+ if not isinstance(value, Mapping):
144
+ raise StructuredOutputError(f"{name}_must_be_object")
145
+ return value
146
+
147
+
148
+ def _text(value: Any, name: str, *, required: bool = True) -> str | None:
149
+ if value is None and not required:
150
+ return None
151
+ if not isinstance(value, str):
152
+ raise StructuredOutputError(f"{name}_must_be_string")
153
+ normalized = value.strip()
154
+ if required and not normalized:
155
+ raise StructuredOutputError(f"{name}_required")
156
+ if len(normalized) > MAX_TEXT:
157
+ raise StructuredOutputError(f"{name}_too_long")
158
+ return normalized
159
+
160
+
161
+ def _string_tuple(value: Any, name: str) -> tuple[str, ...]:
162
+ if value is None:
163
+ return ()
164
+ if not isinstance(value, list):
165
+ raise StructuredOutputError(f"{name}_must_be_array")
166
+ if len(value) > MAX_ITEMS:
167
+ raise StructuredOutputError(f"{name}_too_many_items")
168
+
169
+ items: list[str] = []
170
+ for item in value:
171
+ normalized = _text(item, name)
172
+ if normalized is None: # defensive; required=True above
173
+ raise StructuredOutputError(f"{name}_required")
174
+ items.append(normalized)
175
+ if len(items) != len(set(items)):
176
+ raise StructuredOutputError(f"{name}_contains_duplicates")
177
+ return tuple(items)
178
+
179
+
180
+ def _reject_unknown(
181
+ obj: Mapping[str, Any], allowed: set[str], name: str
182
+ ) -> None:
183
+ extra = set(obj) - allowed
184
+ if extra:
185
+ raise StructuredOutputError(
186
+ f"{name}_unknown_fields:{','.join(sorted(extra))}"
187
+ )
188
+
189
+
190
+ TASK_GRAPH_SCHEMA = {
191
+ "type": "object",
192
+ "additionalProperties": False,
193
+ "required": ["goal_id", "tasks"],
194
+ "properties": {
195
+ "goal_id": {"type": "string", "minLength": 1, "maxLength": MAX_TEXT},
196
+ "tasks": {
197
+ "type": "array",
198
+ "minItems": 1,
199
+ "maxItems": MAX_TASKS,
200
+ "items": {
201
+ "type": "object",
202
+ "additionalProperties": False,
203
+ "required": ["task_id", "action", "resource_id"],
204
+ "properties": {
205
+ "task_id": {"type": "string", "minLength": 1, "maxLength": MAX_TEXT},
206
+ "action": {"enum": [capability.value for capability in Capability]},
207
+ "resource_id": {"type": "string", "minLength": 1, "maxLength": MAX_TEXT},
208
+ "object_id": {"type": ["string", "null"], "maxLength": MAX_TEXT},
209
+ "source": {"type": ["string", "null"], "maxLength": MAX_TEXT},
210
+ "destination": {"type": ["string", "null"], "maxLength": MAX_TEXT},
211
+ "preconditions": {"type": "array", "maxItems": MAX_ITEMS, "items": {"type": "string", "minLength": 1, "maxLength": MAX_TEXT}, "uniqueItems": True},
212
+ "expected_effects": {"type": "array", "maxItems": MAX_ITEMS, "items": {"type": "string", "minLength": 1, "maxLength": MAX_TEXT}, "uniqueItems": True},
213
+ "constraints": {"type": "array", "maxItems": MAX_ITEMS, "items": {"type": "string", "minLength": 1, "maxLength": MAX_TEXT}, "uniqueItems": True},
214
+ "timeout_ms": {"type": "integer", "minimum": 1, "maximum": 300_000},
215
+ "depends_on": {"type": "array", "maxItems": MAX_ITEMS, "items": {"type": "string", "minLength": 1, "maxLength": MAX_TEXT}, "uniqueItems": True},
216
+ },
217
+ },
218
+ },
219
+ },
220
+ }
221
+
222
+
223
+ CRITIQUE_SCHEMA = {
224
+ "type": "object",
225
+ "additionalProperties": False,
226
+ "required": ["decision", "score", "issues"],
227
+ "properties": {
228
+ "decision": {"enum": [decision.value for decision in CriticDecision]},
229
+ "score": {"type": "number", "minimum": 0, "maximum": 1},
230
+ "issues": {
231
+ "type": "array",
232
+ "maxItems": 16,
233
+ "items": {"type": "string"},
234
+ },
235
+ },
236
+ }
237
+
238
+ RECOVERY_SCHEMA = {
239
+ "type": "object",
240
+ "additionalProperties": False,
241
+ "required": ["decision", "reason"],
242
+ "properties": {
243
+ "decision": {"enum": [decision.value for decision in RecoveryDecision]},
244
+ "reason": {"type": "string"},
245
+ "retry_after_ms": {
246
+ "type": ["integer", "null"],
247
+ "minimum": 0,
248
+ "maximum": 60_000,
249
+ },
250
+ },
251
+ }
252
+
253
+
254
+ def parse_task_graph(
255
+ payload: Mapping[str, Any], *, expected_goal_id: str | None = None
256
+ ) -> TaskGraph:
257
+ obj = _object(payload, "task_graph")
258
+ _reject_unknown(obj, {"goal_id", "tasks"}, "task_graph")
259
+
260
+ goal_id = _text(obj.get("goal_id"), "goal_id")
261
+ if goal_id is None: # defensive; required=True above
262
+ raise StructuredOutputError("goal_id_required")
263
+ if expected_goal_id is not None and goal_id != expected_goal_id:
264
+ raise StructuredOutputError("goal_id_mismatch")
265
+
266
+ raw_tasks = obj.get("tasks")
267
+ if not isinstance(raw_tasks, list):
268
+ raise StructuredOutputError("tasks_must_be_array")
269
+ if not raw_tasks:
270
+ raise StructuredOutputError("tasks_required")
271
+ if len(raw_tasks) > MAX_TASKS:
272
+ raise StructuredOutputError("too_many_tasks")
273
+
274
+ allowed = {
275
+ "task_id",
276
+ "action",
277
+ "resource_id",
278
+ "object_id",
279
+ "source",
280
+ "destination",
281
+ "preconditions",
282
+ "expected_effects",
283
+ "constraints",
284
+ "timeout_ms",
285
+ "depends_on",
286
+ }
287
+ tasks: list[TaskNode] = []
288
+
289
+ for index, raw in enumerate(raw_tasks):
290
+ item = _object(raw, f"task_{index}")
291
+ _reject_unknown(item, allowed, f"task_{index}")
292
+
293
+ action_text = _text(item.get("action"), f"task_{index}.action")
294
+ if action_text is None:
295
+ raise StructuredOutputError(f"task_{index}.action_required")
296
+ try:
297
+ action = Capability(action_text)
298
+ except ValueError as exc:
299
+ raise StructuredOutputError(
300
+ f"task_{index}.unsupported_action:{action_text}"
301
+ ) from exc
302
+
303
+ timeout_ms = item.get("timeout_ms", 10_000)
304
+ if (
305
+ isinstance(timeout_ms, bool)
306
+ or not isinstance(timeout_ms, int)
307
+ or not 1 <= timeout_ms <= 300_000
308
+ ):
309
+ raise StructuredOutputError(f"task_{index}.invalid_timeout")
310
+
311
+ resource_id = _text(
312
+ item.get("resource_id"), f"task_{index}.resource_id"
313
+ )
314
+ task_id = _text(item.get("task_id"), f"task_{index}.task_id")
315
+ if resource_id is None or task_id is None:
316
+ raise StructuredOutputError(f"task_{index}.required_field_missing")
317
+
318
+ contract = ActionContract(
319
+ action=action,
320
+ resource_id=resource_id,
321
+ object_id=_text(
322
+ item.get("object_id"), f"task_{index}.object_id", required=False
323
+ ),
324
+ source=_text(
325
+ item.get("source"), f"task_{index}.source", required=False
326
+ ),
327
+ destination=_text(
328
+ item.get("destination"),
329
+ f"task_{index}.destination",
330
+ required=False,
331
+ ),
332
+ preconditions=_string_tuple(
333
+ item.get("preconditions"), f"task_{index}.preconditions"
334
+ ),
335
+ expected_effects=_string_tuple(
336
+ item.get("expected_effects"),
337
+ f"task_{index}.expected_effects",
338
+ ),
339
+ constraints=_string_tuple(
340
+ item.get("constraints"), f"task_{index}.constraints"
341
+ ),
342
+ timeout_ms=timeout_ms,
343
+ )
344
+ tasks.append(
345
+ TaskNode(
346
+ task_id=task_id,
347
+ contract=contract,
348
+ depends_on=_string_tuple(
349
+ item.get("depends_on"), f"task_{index}.depends_on"
350
+ ),
351
+ )
352
+ )
353
+
354
+ graph = TaskGraph(goal_id=goal_id, tasks=tuple(tasks))
355
+ errors = graph.validate_structure()
356
+ if errors:
357
+ raise StructuredOutputError("invalid_task_graph:" + ";".join(errors))
358
+ try:
359
+ graph.topological_order()
360
+ except ValueError as exc:
361
+ raise StructuredOutputError(str(exc)) from exc
362
+ return graph
363
+
364
+
365
+ def parse_critique(payload: Mapping[str, Any]) -> Critique:
366
+ obj = _object(payload, "critique")
367
+ _reject_unknown(obj, {"decision", "score", "issues"}, "critique")
368
+
369
+ decision_text = _text(obj.get("decision"), "decision")
370
+ try:
371
+ decision = CriticDecision(decision_text)
372
+ except ValueError as exc:
373
+ raise StructuredOutputError("invalid_critic_decision") from exc
374
+
375
+ score = obj.get("score")
376
+ if (
377
+ isinstance(score, bool)
378
+ or not isinstance(score, (int, float))
379
+ or not isfinite(float(score))
380
+ or not 0 <= float(score) <= 1
381
+ ):
382
+ raise StructuredOutputError("invalid_critic_score")
383
+
384
+ issues = _string_tuple(obj.get("issues"), "issues")
385
+ if len(issues) > 16:
386
+ raise StructuredOutputError("too_many_critic_issues")
387
+ return Critique(decision, float(score), issues)
388
+
389
+
390
+ def parse_recovery(payload: Mapping[str, Any]) -> RecoveryAdvice:
391
+ obj = _object(payload, "recovery")
392
+ _reject_unknown(obj, {"decision", "reason", "retry_after_ms"}, "recovery")
393
+
394
+ decision_text = _text(obj.get("decision"), "decision")
395
+ try:
396
+ decision = RecoveryDecision(decision_text)
397
+ except ValueError as exc:
398
+ raise StructuredOutputError("invalid_recovery_decision") from exc
399
+
400
+ reason = _text(obj.get("reason"), "reason")
401
+ if reason is None:
402
+ raise StructuredOutputError("reason_required")
403
+
404
+ retry_after_ms = obj.get("retry_after_ms")
405
+ if retry_after_ms is not None and (
406
+ isinstance(retry_after_ms, bool)
407
+ or not isinstance(retry_after_ms, int)
408
+ or not 0 <= retry_after_ms <= 60_000
409
+ ):
410
+ raise StructuredOutputError("invalid_retry_after_ms")
411
+ if decision is RecoveryDecision.WAIT and retry_after_ms is None:
412
+ raise StructuredOutputError("wait_requires_retry_after_ms")
413
+
414
+ return RecoveryAdvice(decision, reason, retry_after_ms)
415
+
416
+
417
+ def canonical_json(value: Any) -> str:
418
+ return json.dumps(
419
+ json_safe(value),
420
+ sort_keys=True,
421
+ separators=(",", ":"),
422
+ ensure_ascii=False,
423
+ )
@@ -0,0 +1,226 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import importlib.util
5
+ import json
6
+ import os
7
+
8
+ from .agent import (
9
+ AgentRuntime,
10
+ CriticAgent,
11
+ ModelRouter,
12
+ PlannerAgent,
13
+ ProviderPool,
14
+ RequirementInterpreterAgent,
15
+ RoutingPolicy,
16
+ RoutingStrategy,
17
+ )
18
+ from .engineering_agent import SmartEngineeringAgent
19
+ from .provider_adapters import PROVIDER_NAMES, create_provider
20
+ from .provider_qualification import qualify_live_provider
21
+ from .providers import ModelRegistry, ModelSpec, QualificationStatus
22
+
23
+
24
+ def _add_live_request_args(parser: argparse.ArgumentParser) -> None:
25
+ parser.add_argument("text", help="Natural-language engineering request")
26
+ parser.add_argument("--provider", choices=PROVIDER_NAMES, required=True)
27
+ parser.add_argument("--model", required=True)
28
+ parser.add_argument(
29
+ "--follow-up",
30
+ action="append",
31
+ default=[],
32
+ help="Additional user information; repeat as needed",
33
+ )
34
+ parser.add_argument("--timeout", type=float, default=30.0)
35
+ parser.add_argument("--max-attempts", type=int, default=2)
36
+
37
+
38
+ def _parser() -> argparse.ArgumentParser:
39
+ parser = argparse.ArgumentParser(prog="devagent-physical-ai")
40
+ sub = parser.add_subparsers(dest="command", required=True)
41
+
42
+ doctor = sub.add_parser(
43
+ "doctor",
44
+ help="Check provider SDK and credential availability without a network call",
45
+ )
46
+ doctor.add_argument("--provider", choices=PROVIDER_NAMES, required=True)
47
+
48
+ qualify = sub.add_parser(
49
+ "qualify",
50
+ help="Run live Interpreter+Planner+Critic+Recovery qualification (uses provider API credits)",
51
+ )
52
+ qualify.add_argument("--provider", choices=PROVIDER_NAMES, required=True)
53
+ qualify.add_argument("--model", required=True)
54
+ qualify.add_argument("--timeout", type=float, default=30.0)
55
+ qualify.add_argument("--max-attempts", type=int, default=2)
56
+ qualify.add_argument("--max-revisions", type=int, default=2)
57
+
58
+ request = sub.add_parser(
59
+ "request",
60
+ help="Interpret natural language and deterministically decide whether more information is required",
61
+ )
62
+ _add_live_request_args(request)
63
+
64
+ engineer = sub.add_parser(
65
+ "engineer",
66
+ help="Natural language -> validated request -> AI plan/critic -> deterministic model smoke",
67
+ )
68
+ _add_live_request_args(engineer)
69
+ return parser
70
+
71
+
72
+ def _module_available(module: str) -> bool:
73
+ try:
74
+ return importlib.util.find_spec(module) is not None
75
+ except (ImportError, ModuleNotFoundError, ValueError):
76
+ return False
77
+
78
+
79
+ def provider_doctor(provider: str) -> dict:
80
+ if provider == "openai":
81
+ module, envs = "openai", ("OPENAI_API_KEY",)
82
+ elif provider == "anthropic":
83
+ module, envs = "anthropic", ("ANTHROPIC_API_KEY",)
84
+ else:
85
+ module, envs = "google.genai", ("GEMINI_API_KEY", "GOOGLE_API_KEY")
86
+ sdk = _module_available(module)
87
+ credential = any(bool(os.getenv(name, "").strip()) for name in envs)
88
+ return {
89
+ "provider": provider,
90
+ "sdk_installed": sdk,
91
+ "credential_configured": credential,
92
+ "ready_for_live_qualification": sdk and credential,
93
+ }
94
+
95
+
96
+ def _live_runtime(provider_name: str, model: str, *, timeout: float, max_attempts: int):
97
+ provider = create_provider(provider_name)
98
+ registry = ModelRegistry()
99
+ registry.register(
100
+ ModelSpec(
101
+ provider=provider_name,
102
+ model=model,
103
+ status=QualificationStatus.SIMULATION_ONLY,
104
+ supports_structured_output=True,
105
+ supports_tools=False,
106
+ quality_score=0.5,
107
+ latency_score=0.5,
108
+ cost_score=0.5,
109
+ roles=frozenset({"interpreter", "planner", "critic", "recovery"}),
110
+ )
111
+ )
112
+ pool = ProviderPool()
113
+ pool.register(provider)
114
+ runtime = AgentRuntime(ModelRouter(registry), pool)
115
+ policy = RoutingPolicy(
116
+ strategy=RoutingStrategy.FIXED,
117
+ fixed_provider=provider_name,
118
+ fixed_model=model,
119
+ max_attempts=max_attempts,
120
+ timeout_s=timeout,
121
+ require_qualified=False,
122
+ )
123
+ return provider, runtime, policy
124
+
125
+
126
+ def _request(args: argparse.Namespace) -> int:
127
+ _, runtime, policy = _live_runtime(
128
+ args.provider,
129
+ args.model,
130
+ timeout=args.timeout,
131
+ max_attempts=args.max_attempts,
132
+ )
133
+ interpreted = RequirementInterpreterAgent(runtime).interpret(
134
+ args.text,
135
+ policy,
136
+ follow_ups=args.follow_up,
137
+ )
138
+ payload = interpreted.to_dict()
139
+ payload["provider_mode"] = "simulation_only_ephemeral_route"
140
+ payload["real_execution_allowed"] = False
141
+ print(json.dumps(payload, indent=2))
142
+ if interpreted.validated is not None:
143
+ return 0
144
+ if interpreted.rejection_code:
145
+ return 4
146
+ return 3
147
+
148
+
149
+ def _engineer(args: argparse.Namespace) -> int:
150
+ _, runtime, policy = _live_runtime(
151
+ args.provider,
152
+ args.model,
153
+ timeout=args.timeout,
154
+ max_attempts=args.max_attempts,
155
+ )
156
+ interpreter = RequirementInterpreterAgent(runtime)
157
+ result = SmartEngineeringAgent(
158
+ interpreter,
159
+ PlannerAgent(runtime),
160
+ CriticAgent(runtime),
161
+ ).run(
162
+ args.text,
163
+ interpreter_policy=policy,
164
+ planner_policy=policy,
165
+ critic_policy=policy,
166
+ follow_ups=args.follow_up,
167
+ )
168
+ payload = result.to_dict()
169
+ payload["provider_mode"] = "simulation_only_ephemeral_route"
170
+ payload["real_execution_allowed"] = False
171
+ print(json.dumps(payload, indent=2))
172
+ if result.ready_for_twin_simulation:
173
+ return 0
174
+ if result.interpretation.validated is None and not result.interpretation.rejection_code:
175
+ return 3
176
+ return 4
177
+
178
+
179
+ def _natural_language_failure(exc: BaseException) -> int:
180
+ print(
181
+ json.dumps(
182
+ {
183
+ "passed": False,
184
+ "failure_code": f"natural_language_request_failed:{type(exc).__name__}",
185
+ "physical_qualification": False,
186
+ "real_execution_allowed": False,
187
+ },
188
+ indent=2,
189
+ )
190
+ )
191
+ return 5
192
+
193
+
194
+ def main(argv: list[str] | None = None) -> int:
195
+ args = _parser().parse_args(argv)
196
+ if args.command == "doctor":
197
+ result = provider_doctor(args.provider)
198
+ print(json.dumps(result, indent=2))
199
+ return 0 if result["ready_for_live_qualification"] else 1
200
+
201
+ if args.command == "request":
202
+ try:
203
+ return _request(args)
204
+ except Exception as exc:
205
+ return _natural_language_failure(exc)
206
+
207
+ if args.command == "engineer":
208
+ try:
209
+ return _engineer(args)
210
+ except Exception as exc:
211
+ return _natural_language_failure(exc)
212
+
213
+ provider = create_provider(args.provider)
214
+ report = qualify_live_provider(
215
+ provider,
216
+ model=args.model,
217
+ timeout_s=args.timeout,
218
+ max_attempts=args.max_attempts,
219
+ max_revision_rounds=args.max_revisions,
220
+ )
221
+ print(json.dumps(report.to_dict(), indent=2))
222
+ return 0 if report.passed else 2
223
+
224
+
225
+ if __name__ == "__main__":
226
+ raise SystemExit(main())