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,126 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable
|
|
4
|
+
|
|
5
|
+
from ..agent.contracts import ModelRequest, ModelResponse
|
|
6
|
+
from ..agent.runtime import ProviderError, ProviderProtocolError, ProviderUnavailable
|
|
7
|
+
from .common import (
|
|
8
|
+
DiagnosticsStore,
|
|
9
|
+
ProviderCallDiagnostics,
|
|
10
|
+
classify_provider_exception,
|
|
11
|
+
model_matches,
|
|
12
|
+
parse_json_object,
|
|
13
|
+
provider_schema,
|
|
14
|
+
request_input,
|
|
15
|
+
require_api_key,
|
|
16
|
+
safe_token_count,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class GeminiProvider:
|
|
21
|
+
provider_name = "gemini"
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
api_key: str | None = None,
|
|
26
|
+
*,
|
|
27
|
+
max_output_tokens: int = 4096,
|
|
28
|
+
client_factory: Callable[..., Any] | None = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
if not 256 <= max_output_tokens <= 65536:
|
|
31
|
+
raise ValueError("max_output_tokens_out_of_range")
|
|
32
|
+
self._api_key = api_key
|
|
33
|
+
self._max_output_tokens = max_output_tokens
|
|
34
|
+
self._client_factory = client_factory
|
|
35
|
+
self._diagnostics = DiagnosticsStore()
|
|
36
|
+
|
|
37
|
+
def diagnostics(self, request_id: str) -> ProviderCallDiagnostics | None:
|
|
38
|
+
return self._diagnostics.get(request_id)
|
|
39
|
+
|
|
40
|
+
def _client(self, timeout_s: float) -> Any:
|
|
41
|
+
key = require_api_key(self._api_key, "GEMINI_API_KEY", "GOOGLE_API_KEY")
|
|
42
|
+
timeout_ms = max(100, int(timeout_s * 1000))
|
|
43
|
+
if self._client_factory is not None:
|
|
44
|
+
return self._client_factory(api_key=key, timeout_ms=timeout_ms)
|
|
45
|
+
try:
|
|
46
|
+
from google import genai
|
|
47
|
+
from google.genai import types
|
|
48
|
+
except ImportError as exc:
|
|
49
|
+
raise ProviderUnavailable("sdk_not_installed:google-genai") from exc
|
|
50
|
+
return genai.Client(
|
|
51
|
+
api_key=key,
|
|
52
|
+
http_options=types.HttpOptions(
|
|
53
|
+
timeout=timeout_ms,
|
|
54
|
+
retry_options=types.HttpRetryOptions(attempts=1),
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def generate(self, request: ModelRequest, *, model: str) -> ModelResponse:
|
|
59
|
+
client = None
|
|
60
|
+
try:
|
|
61
|
+
client = self._client(request.timeout_s)
|
|
62
|
+
interaction = client.interactions.create(
|
|
63
|
+
model=model,
|
|
64
|
+
system_instruction=request.system_instruction,
|
|
65
|
+
input=request_input(request),
|
|
66
|
+
response_format={
|
|
67
|
+
"type": "text",
|
|
68
|
+
"mime_type": "application/json",
|
|
69
|
+
"schema": provider_schema(request.output_schema),
|
|
70
|
+
},
|
|
71
|
+
generation_config={"max_output_tokens": self._max_output_tokens},
|
|
72
|
+
store=False,
|
|
73
|
+
)
|
|
74
|
+
except Exception as exc:
|
|
75
|
+
if isinstance(exc, ProviderError):
|
|
76
|
+
raise
|
|
77
|
+
raise classify_provider_exception(exc) from exc
|
|
78
|
+
finally:
|
|
79
|
+
if client is not None and self._client_factory is None:
|
|
80
|
+
close = getattr(client, "close", None)
|
|
81
|
+
if callable(close):
|
|
82
|
+
close()
|
|
83
|
+
|
|
84
|
+
request_id = str(getattr(interaction, "id", "") or "")
|
|
85
|
+
if not request_id:
|
|
86
|
+
raise ProviderProtocolError("provider_request_id_missing")
|
|
87
|
+
resolved_model = str(getattr(interaction, "model", "") or "")
|
|
88
|
+
if not model_matches(model, resolved_model):
|
|
89
|
+
raise ProviderProtocolError("response_model_identity_mismatch")
|
|
90
|
+
payload = parse_json_object(getattr(interaction, "output_text", None))
|
|
91
|
+
|
|
92
|
+
usage = getattr(interaction, "usage", None) or getattr(
|
|
93
|
+
interaction, "usage_metadata", None
|
|
94
|
+
)
|
|
95
|
+
input_tokens = safe_token_count(
|
|
96
|
+
getattr(usage, "input_tokens", None) if usage is not None else None
|
|
97
|
+
)
|
|
98
|
+
if input_tokens is None and usage is not None:
|
|
99
|
+
input_tokens = safe_token_count(getattr(usage, "prompt_token_count", None))
|
|
100
|
+
output_tokens = safe_token_count(
|
|
101
|
+
getattr(usage, "output_tokens", None) if usage is not None else None
|
|
102
|
+
)
|
|
103
|
+
if output_tokens is None and usage is not None:
|
|
104
|
+
output_tokens = safe_token_count(
|
|
105
|
+
getattr(usage, "candidates_token_count", None)
|
|
106
|
+
)
|
|
107
|
+
total_tokens = safe_token_count(
|
|
108
|
+
getattr(usage, "total_tokens", None) if usage is not None else None
|
|
109
|
+
)
|
|
110
|
+
if total_tokens is None and usage is not None:
|
|
111
|
+
total_tokens = safe_token_count(getattr(usage, "total_token_count", None))
|
|
112
|
+
if total_tokens is None and input_tokens is not None and output_tokens is not None:
|
|
113
|
+
total_tokens = input_tokens + output_tokens
|
|
114
|
+
|
|
115
|
+
self._diagnostics.record(
|
|
116
|
+
ProviderCallDiagnostics(
|
|
117
|
+
request_id=request_id,
|
|
118
|
+
provider=self.provider_name,
|
|
119
|
+
requested_model=model,
|
|
120
|
+
resolved_model=resolved_model or model,
|
|
121
|
+
input_tokens=input_tokens,
|
|
122
|
+
output_tokens=output_tokens,
|
|
123
|
+
total_tokens=total_tokens,
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
return ModelResponse(self.provider_name, model, payload, request_id=request_id)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable
|
|
4
|
+
|
|
5
|
+
from ..agent.contracts import ModelRequest, ModelResponse
|
|
6
|
+
from ..agent.runtime import ProviderError, ProviderProtocolError, ProviderUnavailable
|
|
7
|
+
from .common import (
|
|
8
|
+
DiagnosticsStore,
|
|
9
|
+
ProviderCallDiagnostics,
|
|
10
|
+
classify_provider_exception,
|
|
11
|
+
model_matches,
|
|
12
|
+
parse_json_object,
|
|
13
|
+
provider_schema,
|
|
14
|
+
request_input,
|
|
15
|
+
require_api_key,
|
|
16
|
+
safe_token_count,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class OpenAIProvider:
|
|
21
|
+
provider_name = "openai"
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
api_key: str | None = None,
|
|
26
|
+
*,
|
|
27
|
+
max_output_tokens: int = 4096,
|
|
28
|
+
client_factory: Callable[..., Any] | None = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
if not 256 <= max_output_tokens <= 65536:
|
|
31
|
+
raise ValueError("max_output_tokens_out_of_range")
|
|
32
|
+
self._api_key = api_key
|
|
33
|
+
self._max_output_tokens = max_output_tokens
|
|
34
|
+
self._client_factory = client_factory
|
|
35
|
+
self._diagnostics = DiagnosticsStore()
|
|
36
|
+
|
|
37
|
+
def diagnostics(self, request_id: str) -> ProviderCallDiagnostics | None:
|
|
38
|
+
return self._diagnostics.get(request_id)
|
|
39
|
+
|
|
40
|
+
def _client(self, timeout_s: float) -> Any:
|
|
41
|
+
key = require_api_key(self._api_key, "OPENAI_API_KEY")
|
|
42
|
+
if self._client_factory is not None:
|
|
43
|
+
return self._client_factory(api_key=key, timeout=timeout_s, max_retries=0)
|
|
44
|
+
try:
|
|
45
|
+
from openai import OpenAI
|
|
46
|
+
except ImportError as exc:
|
|
47
|
+
raise ProviderUnavailable("sdk_not_installed:openai") from exc
|
|
48
|
+
return OpenAI(api_key=key, timeout=timeout_s, max_retries=0)
|
|
49
|
+
|
|
50
|
+
def generate(self, request: ModelRequest, *, model: str) -> ModelResponse:
|
|
51
|
+
try:
|
|
52
|
+
response = self._client(request.timeout_s).responses.create(
|
|
53
|
+
model=model,
|
|
54
|
+
instructions=request.system_instruction,
|
|
55
|
+
input=request_input(request),
|
|
56
|
+
text={
|
|
57
|
+
"format": {
|
|
58
|
+
"type": "json_schema",
|
|
59
|
+
"name": f"devagent_{request.role.value}",
|
|
60
|
+
"strict": True,
|
|
61
|
+
"schema": provider_schema(request.output_schema),
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
max_output_tokens=self._max_output_tokens,
|
|
65
|
+
store=False,
|
|
66
|
+
)
|
|
67
|
+
except Exception as exc:
|
|
68
|
+
if isinstance(exc, ProviderError):
|
|
69
|
+
raise
|
|
70
|
+
raise classify_provider_exception(exc) from exc
|
|
71
|
+
|
|
72
|
+
request_id = str(getattr(response, "id", "") or "")
|
|
73
|
+
if not request_id:
|
|
74
|
+
raise ProviderProtocolError("provider_request_id_missing")
|
|
75
|
+
resolved_model = str(getattr(response, "model", "") or "")
|
|
76
|
+
if not model_matches(model, resolved_model):
|
|
77
|
+
raise ProviderProtocolError("response_model_identity_mismatch")
|
|
78
|
+
payload = parse_json_object(getattr(response, "output_text", None))
|
|
79
|
+
|
|
80
|
+
usage = getattr(response, "usage", None)
|
|
81
|
+
input_tokens = safe_token_count(getattr(usage, "input_tokens", None))
|
|
82
|
+
output_tokens = safe_token_count(getattr(usage, "output_tokens", None))
|
|
83
|
+
total_tokens = safe_token_count(getattr(usage, "total_tokens", None))
|
|
84
|
+
self._diagnostics.record(
|
|
85
|
+
ProviderCallDiagnostics(
|
|
86
|
+
request_id=request_id,
|
|
87
|
+
provider=self.provider_name,
|
|
88
|
+
requested_model=model,
|
|
89
|
+
resolved_model=resolved_model or model,
|
|
90
|
+
input_tokens=input_tokens,
|
|
91
|
+
output_tokens=output_tokens,
|
|
92
|
+
total_tokens=total_tokens,
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
return ModelResponse(self.provider_name, model, payload, request_id=request_id)
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict, dataclass
|
|
4
|
+
from typing import Any, Iterable
|
|
5
|
+
|
|
6
|
+
from .agent import (
|
|
7
|
+
AgentRuntime,
|
|
8
|
+
CriticAgent,
|
|
9
|
+
ModelRouter,
|
|
10
|
+
PlannerAgent,
|
|
11
|
+
PlanningCoordinator,
|
|
12
|
+
ProviderPool,
|
|
13
|
+
RecoveryAgent,
|
|
14
|
+
RequirementInterpreterAgent,
|
|
15
|
+
RoutingPolicy,
|
|
16
|
+
RoutingStrategy,
|
|
17
|
+
)
|
|
18
|
+
from .agent.runtime import AgentRuntimeError
|
|
19
|
+
from .execution import ExecutionMode, ExecutionSupervisor
|
|
20
|
+
from .models import WorldState
|
|
21
|
+
from .provider_adapters.common import ProviderCallDiagnostics
|
|
22
|
+
from .providers import ModelRegistry, ModelSpec, QualificationStatus
|
|
23
|
+
from .simulation import DeterministicSimulationBackend
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True, slots=True)
|
|
27
|
+
class LiveProviderQualificationReport:
|
|
28
|
+
provider: str
|
|
29
|
+
model: str
|
|
30
|
+
passed: bool
|
|
31
|
+
request_validated: bool
|
|
32
|
+
plan_hash: str | None
|
|
33
|
+
revision_rounds: int | None
|
|
34
|
+
simulation_completed: bool
|
|
35
|
+
recovery_decision: str | None
|
|
36
|
+
agent_calls: int
|
|
37
|
+
agent_attempts: int
|
|
38
|
+
input_tokens: int | None
|
|
39
|
+
output_tokens: int | None
|
|
40
|
+
total_tokens: int | None
|
|
41
|
+
failure_code: str | None = None
|
|
42
|
+
|
|
43
|
+
def to_dict(self) -> dict[str, Any]:
|
|
44
|
+
return asdict(self)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _request_ids(evidence_items: Iterable[Any]) -> tuple[str, ...]:
|
|
48
|
+
values: list[str] = []
|
|
49
|
+
for evidence in evidence_items:
|
|
50
|
+
for attempt in evidence.attempts:
|
|
51
|
+
if attempt.outcome == "success" and attempt.request_id:
|
|
52
|
+
values.append(attempt.request_id)
|
|
53
|
+
return tuple(dict.fromkeys(values))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _attempt_count(evidence_items: Iterable[Any]) -> int:
|
|
57
|
+
return sum(len(evidence.attempts) for evidence in evidence_items)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _append_exception_evidence(evidence: list[Any], exc: BaseException) -> None:
|
|
61
|
+
"""Preserve bounded AgentEvidence on failure without exposing provider text."""
|
|
62
|
+
if isinstance(exc, AgentRuntimeError):
|
|
63
|
+
evidence.append(exc.evidence)
|
|
64
|
+
return
|
|
65
|
+
nested = getattr(exc, "__cause__", None)
|
|
66
|
+
if isinstance(nested, AgentRuntimeError):
|
|
67
|
+
evidence.append(nested.evidence)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _usage(
|
|
71
|
+
provider: Any, request_ids: tuple[str, ...]
|
|
72
|
+
) -> tuple[int | None, int | None, int | None]:
|
|
73
|
+
getter = getattr(provider, "diagnostics", None)
|
|
74
|
+
if not callable(getter):
|
|
75
|
+
return None, None, None
|
|
76
|
+
records: list[ProviderCallDiagnostics] = []
|
|
77
|
+
for request_id in request_ids:
|
|
78
|
+
value = getter(request_id)
|
|
79
|
+
if isinstance(value, ProviderCallDiagnostics):
|
|
80
|
+
records.append(value)
|
|
81
|
+
if not records:
|
|
82
|
+
return None, None, None
|
|
83
|
+
|
|
84
|
+
def total(field: str) -> int | None:
|
|
85
|
+
values = [getattr(item, field) for item in records]
|
|
86
|
+
if any(value is None for value in values):
|
|
87
|
+
return None
|
|
88
|
+
return sum(int(value) for value in values)
|
|
89
|
+
|
|
90
|
+
return total("input_tokens"), total("output_tokens"), total("total_tokens")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def qualify_live_provider(
|
|
94
|
+
provider: Any,
|
|
95
|
+
*,
|
|
96
|
+
model: str,
|
|
97
|
+
timeout_s: float = 30.0,
|
|
98
|
+
max_attempts: int = 2,
|
|
99
|
+
max_revision_rounds: int = 2,
|
|
100
|
+
) -> LiveProviderQualificationReport:
|
|
101
|
+
"""Run Interpreter -> Planner -> Critic -> verify/simulate -> Recovery.
|
|
102
|
+
|
|
103
|
+
The model is deliberately routed as SIMULATION_ONLY for qualification.
|
|
104
|
+
This never unlocks real execution and never persists qualification state.
|
|
105
|
+
"""
|
|
106
|
+
provider_name = str(getattr(provider, "provider_name", "") or "").strip().lower()
|
|
107
|
+
if not provider_name:
|
|
108
|
+
raise ValueError("provider_name_required")
|
|
109
|
+
if not model.strip():
|
|
110
|
+
raise ValueError("model_required")
|
|
111
|
+
|
|
112
|
+
registry = ModelRegistry()
|
|
113
|
+
registry.register(
|
|
114
|
+
ModelSpec(
|
|
115
|
+
provider=provider_name,
|
|
116
|
+
model=model.strip(),
|
|
117
|
+
status=QualificationStatus.SIMULATION_ONLY,
|
|
118
|
+
supports_structured_output=True,
|
|
119
|
+
supports_tools=False,
|
|
120
|
+
quality_score=0.5,
|
|
121
|
+
latency_score=0.5,
|
|
122
|
+
cost_score=0.5,
|
|
123
|
+
roles=frozenset({"interpreter", "planner", "critic", "recovery"}),
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
pool = ProviderPool()
|
|
127
|
+
pool.register(provider)
|
|
128
|
+
runtime = AgentRuntime(ModelRouter(registry), pool)
|
|
129
|
+
policy = RoutingPolicy(
|
|
130
|
+
strategy=RoutingStrategy.FIXED,
|
|
131
|
+
fixed_provider=provider_name,
|
|
132
|
+
fixed_model=model.strip(),
|
|
133
|
+
max_attempts=max_attempts,
|
|
134
|
+
timeout_s=timeout_s,
|
|
135
|
+
require_qualified=False,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
world = WorldState(facts={"robot_ready", "object_available"})
|
|
139
|
+
evidence: list[Any] = []
|
|
140
|
+
request_validated = False
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
interpreted = RequirementInterpreterAgent(runtime).interpret(
|
|
144
|
+
"Use the UR5e to load P17 from conveyor_a into cnc_04. Simulate the operation using an engineering test preset.",
|
|
145
|
+
policy,
|
|
146
|
+
)
|
|
147
|
+
evidence.append(interpreted.evidence)
|
|
148
|
+
if interpreted.validated is None:
|
|
149
|
+
request_ids = _request_ids(evidence)
|
|
150
|
+
input_tokens, output_tokens, total_tokens = _usage(provider, request_ids)
|
|
151
|
+
return LiveProviderQualificationReport(
|
|
152
|
+
provider=provider_name,
|
|
153
|
+
model=model,
|
|
154
|
+
passed=False,
|
|
155
|
+
request_validated=False,
|
|
156
|
+
plan_hash=None,
|
|
157
|
+
revision_rounds=None,
|
|
158
|
+
simulation_completed=False,
|
|
159
|
+
recovery_decision=None,
|
|
160
|
+
agent_calls=len(request_ids),
|
|
161
|
+
agent_attempts=_attempt_count(evidence),
|
|
162
|
+
input_tokens=input_tokens,
|
|
163
|
+
output_tokens=output_tokens,
|
|
164
|
+
total_tokens=total_tokens,
|
|
165
|
+
failure_code=(
|
|
166
|
+
interpreted.rejection_code or "interpreter_request_not_validated"
|
|
167
|
+
),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
request_validated = True
|
|
171
|
+
request = interpreted.validated
|
|
172
|
+
goal = request.goal()
|
|
173
|
+
robot = request.resource()
|
|
174
|
+
|
|
175
|
+
planner = PlannerAgent(runtime)
|
|
176
|
+
critic = CriticAgent(runtime)
|
|
177
|
+
coordinator = PlanningCoordinator(
|
|
178
|
+
planner,
|
|
179
|
+
critic,
|
|
180
|
+
max_revision_rounds=max_revision_rounds,
|
|
181
|
+
min_accept_score=0.70,
|
|
182
|
+
)
|
|
183
|
+
recovery = RecoveryAgent(runtime)
|
|
184
|
+
|
|
185
|
+
artifact = coordinator.create_verified_plan(
|
|
186
|
+
goal,
|
|
187
|
+
[robot],
|
|
188
|
+
world,
|
|
189
|
+
policy,
|
|
190
|
+
policy,
|
|
191
|
+
engineering_context=request.to_dict(),
|
|
192
|
+
)
|
|
193
|
+
evidence.extend(artifact.planner_evidence)
|
|
194
|
+
evidence.extend(artifact.critic_evidence)
|
|
195
|
+
|
|
196
|
+
simulation_world = WorldState(facts=set(world.facts), values=dict(world.values))
|
|
197
|
+
simulation = ExecutionSupervisor().execute(
|
|
198
|
+
artifact.graph,
|
|
199
|
+
[robot],
|
|
200
|
+
simulation_world,
|
|
201
|
+
DeterministicSimulationBackend(),
|
|
202
|
+
ExecutionMode.SIMULATION,
|
|
203
|
+
)
|
|
204
|
+
if not simulation.completed:
|
|
205
|
+
request_ids = _request_ids(evidence)
|
|
206
|
+
input_tokens, output_tokens, total_tokens = _usage(provider, request_ids)
|
|
207
|
+
return LiveProviderQualificationReport(
|
|
208
|
+
provider=provider_name,
|
|
209
|
+
model=model,
|
|
210
|
+
passed=False,
|
|
211
|
+
request_validated=True,
|
|
212
|
+
plan_hash=artifact.graph_hash,
|
|
213
|
+
revision_rounds=artifact.revision_rounds,
|
|
214
|
+
simulation_completed=False,
|
|
215
|
+
recovery_decision=None,
|
|
216
|
+
agent_calls=len(request_ids),
|
|
217
|
+
agent_attempts=_attempt_count(evidence),
|
|
218
|
+
input_tokens=input_tokens,
|
|
219
|
+
output_tokens=output_tokens,
|
|
220
|
+
total_tokens=total_tokens,
|
|
221
|
+
failure_code="deterministic_simulation_failed",
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
recovered = recovery.advise(
|
|
225
|
+
task_id="T2",
|
|
226
|
+
issue_codes=("state_unknown",),
|
|
227
|
+
world=world,
|
|
228
|
+
policy=policy,
|
|
229
|
+
)
|
|
230
|
+
evidence.append(recovered.evidence)
|
|
231
|
+
request_ids = _request_ids(evidence)
|
|
232
|
+
input_tokens, output_tokens, total_tokens = _usage(provider, request_ids)
|
|
233
|
+
return LiveProviderQualificationReport(
|
|
234
|
+
provider=provider_name,
|
|
235
|
+
model=model,
|
|
236
|
+
passed=True,
|
|
237
|
+
request_validated=True,
|
|
238
|
+
plan_hash=artifact.graph_hash,
|
|
239
|
+
revision_rounds=artifact.revision_rounds,
|
|
240
|
+
simulation_completed=True,
|
|
241
|
+
recovery_decision=recovered.advice.decision.value,
|
|
242
|
+
agent_calls=len(request_ids),
|
|
243
|
+
agent_attempts=_attempt_count(evidence),
|
|
244
|
+
input_tokens=input_tokens,
|
|
245
|
+
output_tokens=output_tokens,
|
|
246
|
+
total_tokens=total_tokens,
|
|
247
|
+
failure_code=None,
|
|
248
|
+
)
|
|
249
|
+
except Exception as exc:
|
|
250
|
+
_append_exception_evidence(evidence, exc)
|
|
251
|
+
request_ids = _request_ids(evidence)
|
|
252
|
+
input_tokens, output_tokens, total_tokens = _usage(provider, request_ids)
|
|
253
|
+
return LiveProviderQualificationReport(
|
|
254
|
+
provider=provider_name,
|
|
255
|
+
model=model,
|
|
256
|
+
passed=False,
|
|
257
|
+
request_validated=request_validated,
|
|
258
|
+
plan_hash=None,
|
|
259
|
+
revision_rounds=None,
|
|
260
|
+
simulation_completed=False,
|
|
261
|
+
recovery_decision=None,
|
|
262
|
+
agent_calls=len(request_ids),
|
|
263
|
+
agent_attempts=_attempt_count(evidence),
|
|
264
|
+
input_tokens=input_tokens,
|
|
265
|
+
output_tokens=output_tokens,
|
|
266
|
+
total_tokens=total_tokens,
|
|
267
|
+
failure_code=f"qualification_failed:{type(exc).__name__}",
|
|
268
|
+
)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Protocol
|
|
6
|
+
|
|
7
|
+
from .models import Goal, Resource, TaskGraph
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class QualificationStatus(str, Enum):
|
|
11
|
+
QUALIFIED = "qualified"
|
|
12
|
+
SIMULATION_ONLY = "simulation_only"
|
|
13
|
+
NOT_TESTED = "not_tested"
|
|
14
|
+
BLOCKED = "blocked"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True)
|
|
18
|
+
class ModelSpec:
|
|
19
|
+
"""Model capabilities and qualification data used by the routing layer.
|
|
20
|
+
|
|
21
|
+
Scores are normalized to ``[0, 1]`` and intentionally provider-independent.
|
|
22
|
+
A higher score is always better: higher quality, lower latency, or lower cost.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
provider: str
|
|
26
|
+
model: str
|
|
27
|
+
status: QualificationStatus
|
|
28
|
+
supports_structured_output: bool = True
|
|
29
|
+
supports_tools: bool = True
|
|
30
|
+
private: bool = False
|
|
31
|
+
quality_score: float = 0.5
|
|
32
|
+
latency_score: float = 0.5
|
|
33
|
+
cost_score: float = 0.5
|
|
34
|
+
roles: frozenset[str] = frozenset()
|
|
35
|
+
|
|
36
|
+
def __post_init__(self) -> None:
|
|
37
|
+
provider = self.provider.strip().lower()
|
|
38
|
+
model = self.model.strip()
|
|
39
|
+
if not provider or not model:
|
|
40
|
+
raise ValueError("provider_and_model_required")
|
|
41
|
+
|
|
42
|
+
for name, value in (
|
|
43
|
+
("quality_score", self.quality_score),
|
|
44
|
+
("latency_score", self.latency_score),
|
|
45
|
+
("cost_score", self.cost_score),
|
|
46
|
+
):
|
|
47
|
+
if not 0.0 <= value <= 1.0:
|
|
48
|
+
raise ValueError(f"{name}_out_of_range")
|
|
49
|
+
|
|
50
|
+
normalized_roles = frozenset(
|
|
51
|
+
role.strip().lower() for role in self.roles if role.strip()
|
|
52
|
+
)
|
|
53
|
+
object.__setattr__(self, "provider", provider)
|
|
54
|
+
object.__setattr__(self, "model", model)
|
|
55
|
+
object.__setattr__(self, "roles", normalized_roles)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def key(self) -> tuple[str, str]:
|
|
59
|
+
return self.provider, self.model
|
|
60
|
+
|
|
61
|
+
def supports_role(self, role: str) -> bool:
|
|
62
|
+
normalized = role.strip().lower()
|
|
63
|
+
return not self.roles or normalized in self.roles
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class PlannerModel(Protocol):
|
|
67
|
+
def generate_task_graph(
|
|
68
|
+
self, goal: Goal, resources: list[Resource]
|
|
69
|
+
) -> TaskGraph: ...
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ModelRegistry:
|
|
73
|
+
"""In-memory qualification registry with deterministic duplicate handling."""
|
|
74
|
+
|
|
75
|
+
def __init__(self) -> None:
|
|
76
|
+
self._models: dict[tuple[str, str], ModelSpec] = {}
|
|
77
|
+
|
|
78
|
+
def register(self, spec: ModelSpec, *, replace: bool = False) -> None:
|
|
79
|
+
if spec.key in self._models and not replace:
|
|
80
|
+
raise ValueError(f"duplicate_model:{spec.provider}:{spec.model}")
|
|
81
|
+
self._models[spec.key] = spec
|
|
82
|
+
|
|
83
|
+
def get(self, provider: str, model: str) -> ModelSpec:
|
|
84
|
+
key = (provider.strip().lower(), model.strip())
|
|
85
|
+
try:
|
|
86
|
+
return self._models[key]
|
|
87
|
+
except KeyError as exc:
|
|
88
|
+
raise KeyError(f"unregistered_model:{key[0]}:{key[1]}") from exc
|
|
89
|
+
|
|
90
|
+
def all(self) -> tuple[ModelSpec, ...]:
|
|
91
|
+
return tuple(self._models.values())
|
|
92
|
+
|
|
93
|
+
def real_execution_allowed(self, provider: str, model: str) -> bool:
|
|
94
|
+
return self.get(provider, model).status is QualificationStatus.QUALIFIED
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from .execution import ExecutionMode, ExecutionSupervisor
|
|
6
|
+
from .models import Goal, WorldState
|
|
7
|
+
from .planning import CapabilityPlanner
|
|
8
|
+
from .robots import CATALOG
|
|
9
|
+
from .simulation import DeterministicSimulationBackend
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class QualificationCaseResult:
|
|
14
|
+
robot: str
|
|
15
|
+
passed: bool
|
|
16
|
+
executed_tasks: int
|
|
17
|
+
violations: int
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class QualificationSummary:
|
|
22
|
+
cases: tuple[QualificationCaseResult, ...]
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def passed(self) -> bool:
|
|
26
|
+
return all(case.passed for case in self.cases)
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def violations(self) -> int:
|
|
30
|
+
return sum(case.violations for case in self.cases)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def run_cross_vendor_smoke() -> QualificationSummary:
|
|
34
|
+
planner = CapabilityPlanner()
|
|
35
|
+
supervisor = ExecutionSupervisor()
|
|
36
|
+
goal = Goal("G1", "load", "P17", "conveyor_a", "cnc_04")
|
|
37
|
+
results: list[QualificationCaseResult] = []
|
|
38
|
+
for robot_name, factory in CATALOG.items():
|
|
39
|
+
resource = factory()
|
|
40
|
+
graph = planner.plan(goal, [resource])
|
|
41
|
+
world = WorldState(facts={"robot_ready", "object_available"})
|
|
42
|
+
report = supervisor.execute(graph, [resource], world, DeterministicSimulationBackend(), ExecutionMode.SIMULATION)
|
|
43
|
+
results.append(QualificationCaseResult(robot_name, report.completed, len(report.executed_tasks), len(report.issues)))
|
|
44
|
+
return QualificationSummary(tuple(results))
|