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,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from hashlib import sha256
|
|
4
|
+
|
|
5
|
+
from .structured import canonical_json
|
|
6
|
+
from ..models import TaskGraph
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def task_graph_to_dict(graph: TaskGraph) -> dict:
|
|
10
|
+
return {
|
|
11
|
+
"goal_id": graph.goal_id,
|
|
12
|
+
"tasks": [
|
|
13
|
+
{
|
|
14
|
+
"task_id": task.task_id,
|
|
15
|
+
"action": task.contract.action.value,
|
|
16
|
+
"resource_id": task.contract.resource_id,
|
|
17
|
+
"object_id": task.contract.object_id,
|
|
18
|
+
"source": task.contract.source,
|
|
19
|
+
"destination": task.contract.destination,
|
|
20
|
+
"preconditions": list(task.contract.preconditions),
|
|
21
|
+
"expected_effects": list(task.contract.expected_effects),
|
|
22
|
+
"constraints": list(task.contract.constraints),
|
|
23
|
+
"timeout_ms": task.contract.timeout_ms,
|
|
24
|
+
"depends_on": list(task.depends_on),
|
|
25
|
+
}
|
|
26
|
+
for task in graph.tasks
|
|
27
|
+
],
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def task_graph_hash(graph: TaskGraph) -> str:
|
|
32
|
+
return sha256(
|
|
33
|
+
canonical_json(task_graph_to_dict(graph)).encode("utf-8")
|
|
34
|
+
).hexdigest()
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Mapping, Sequence
|
|
5
|
+
|
|
6
|
+
from .contracts import AgentEvidence, AgentRole, RoutingPolicy
|
|
7
|
+
from .runtime import AgentRuntime, InvocationResult
|
|
8
|
+
from .structured import StructuredOutputError
|
|
9
|
+
from ..engineering_request import (
|
|
10
|
+
ENGINEERING_REQUEST_SCHEMA,
|
|
11
|
+
EngineeringRequestCompiler,
|
|
12
|
+
EngineeringRequestDraft,
|
|
13
|
+
EngineeringRequestError,
|
|
14
|
+
RequestAssessment,
|
|
15
|
+
RequestCompletenessValidator,
|
|
16
|
+
RequestState,
|
|
17
|
+
ValidatedEngineeringRequest,
|
|
18
|
+
parse_engineering_request_draft,
|
|
19
|
+
)
|
|
20
|
+
from ..planning import SUPPORTED_GOAL_ACTIONS
|
|
21
|
+
from ..robots import CATALOG, PROFILE_REGISTRY
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
INTERPRETER_SYSTEM = """You are the DevAgent Engineering Requirement Interpreter.
|
|
25
|
+
Convert the user's natural-language robotic engineering request into the exact structured schema.
|
|
26
|
+
|
|
27
|
+
Rules:
|
|
28
|
+
1. Extract only information the user explicitly supplied or that is unambiguous from the supplied conversation.
|
|
29
|
+
2. Never invent or silently assume engineering-critical facts such as robot, part, source, destination, payload, dimensions, geometry, tool, clearances, or limits.
|
|
30
|
+
3. If a field is missing, output null. If a field is ambiguous or you are not confident it is exactly what the user meant, include that field name in uncertain_fields. Never choose among multiple plausible values.
|
|
31
|
+
4. Safe system constraints such as collision checking and joint-limit enforcement are owned by deterministic code and must not be invented as user statements.
|
|
32
|
+
5. Normalize units into the schema units only when the conversion is exact. Preserve uncertainty instead of guessing.
|
|
33
|
+
6. 'Fastest' or 'shortest cycle' maps to cycle_time. 'Most robust/reliable' maps to robustness. 'Largest safety margin/clearance' maps to clearance. Otherwise use balanced unless the user explicitly asks for energy.
|
|
34
|
+
7. Use custom test_preset only when the user supplied an exact run count. Otherwise map ordinary wording to quick, engineering, robustness, or stress conservatively.
|
|
35
|
+
8. purpose is commissioning only when the user explicitly asks about onsite readiness, commissioning, deployment confidence, or transfer to the real robot; use qualify for broad qualification/testing, simulate for simulation, otherwise plan.
|
|
36
|
+
9. When a user names a registered robot or alias, emit the canonical robot key supplied in engine_context.robot_profiles. Do not invent a different robot key.
|
|
37
|
+
10. notes may summarize non-critical user statements. Do not put hidden reasoning in notes.
|
|
38
|
+
11. Output only schema fields. The deterministic compiler decides whether the request is supported, complete, or rejected.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True, slots=True)
|
|
43
|
+
class InterpretedEngineeringRequest:
|
|
44
|
+
draft: EngineeringRequestDraft
|
|
45
|
+
assessment: RequestAssessment
|
|
46
|
+
evidence: AgentEvidence
|
|
47
|
+
validated: ValidatedEngineeringRequest | None = None
|
|
48
|
+
rejection_code: str | None = None
|
|
49
|
+
|
|
50
|
+
def to_dict(self) -> dict[str, Any]:
|
|
51
|
+
return {
|
|
52
|
+
"draft": self.draft.to_dict(),
|
|
53
|
+
"assessment": self.assessment.to_dict(),
|
|
54
|
+
"validated": self.validated.to_dict() if self.validated is not None else None,
|
|
55
|
+
"rejection_code": self.rejection_code,
|
|
56
|
+
"evidence": {
|
|
57
|
+
"trace_id": self.evidence.trace_id,
|
|
58
|
+
"role": self.evidence.role.value,
|
|
59
|
+
"input_hash": self.evidence.input_hash,
|
|
60
|
+
"output_hash": self.evidence.output_hash,
|
|
61
|
+
"selected_provider": self.evidence.selected_provider,
|
|
62
|
+
"selected_model": self.evidence.selected_model,
|
|
63
|
+
"attempts": [
|
|
64
|
+
{
|
|
65
|
+
"provider": attempt.provider,
|
|
66
|
+
"model": attempt.model,
|
|
67
|
+
"outcome": attempt.outcome,
|
|
68
|
+
"error_code": attempt.error_code,
|
|
69
|
+
"latency_ms": attempt.latency_ms,
|
|
70
|
+
"request_id": attempt.request_id,
|
|
71
|
+
}
|
|
72
|
+
for attempt in self.evidence.attempts
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class RequirementInterpreterAgent:
|
|
79
|
+
"""Natural-language front door with deterministic completeness/compile gates."""
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
runtime: AgentRuntime,
|
|
84
|
+
*,
|
|
85
|
+
completeness: RequestCompletenessValidator | None = None,
|
|
86
|
+
compiler: EngineeringRequestCompiler | None = None,
|
|
87
|
+
) -> None:
|
|
88
|
+
self.runtime = runtime
|
|
89
|
+
self.completeness = completeness or RequestCompletenessValidator()
|
|
90
|
+
self.compiler = compiler or EngineeringRequestCompiler(self.completeness)
|
|
91
|
+
|
|
92
|
+
def interpret(
|
|
93
|
+
self,
|
|
94
|
+
text: str,
|
|
95
|
+
policy: RoutingPolicy,
|
|
96
|
+
*,
|
|
97
|
+
follow_ups: Sequence[str] = (),
|
|
98
|
+
) -> InterpretedEngineeringRequest:
|
|
99
|
+
initial = text.strip()
|
|
100
|
+
if not initial:
|
|
101
|
+
raise ValueError("request_text_required")
|
|
102
|
+
if len(initial) > 16_384:
|
|
103
|
+
raise ValueError("request_text_too_long")
|
|
104
|
+
normalized_follow_ups = tuple(item.strip() for item in follow_ups if item.strip())
|
|
105
|
+
if len(normalized_follow_ups) > 16:
|
|
106
|
+
raise ValueError("too_many_follow_ups")
|
|
107
|
+
if any(len(item) > 8_192 for item in normalized_follow_ups):
|
|
108
|
+
raise ValueError("follow_up_too_long")
|
|
109
|
+
|
|
110
|
+
robot_profiles = [
|
|
111
|
+
{
|
|
112
|
+
"key": profile.key,
|
|
113
|
+
"vendor": profile.vendor,
|
|
114
|
+
"model": profile.model,
|
|
115
|
+
"aliases": list(profile.aliases),
|
|
116
|
+
}
|
|
117
|
+
for profile in PROFILE_REGISTRY.all()
|
|
118
|
+
]
|
|
119
|
+
input_payload = {
|
|
120
|
+
"conversation": {
|
|
121
|
+
"initial_request": initial,
|
|
122
|
+
"follow_ups": list(normalized_follow_ups),
|
|
123
|
+
},
|
|
124
|
+
"engine_context": {
|
|
125
|
+
"known_robot_keys": sorted(CATALOG),
|
|
126
|
+
"robot_profiles": robot_profiles,
|
|
127
|
+
"currently_supported_goal_actions": sorted(SUPPORTED_GOAL_ACTIONS),
|
|
128
|
+
"instruction": (
|
|
129
|
+
"Known engine capabilities are context for interpretation only. "
|
|
130
|
+
"Map registered aliases to their canonical key. Do not rewrite an "
|
|
131
|
+
"unsupported user request into a supported one."
|
|
132
|
+
),
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
def validate(payload: Mapping[str, Any]) -> EngineeringRequestDraft:
|
|
137
|
+
try:
|
|
138
|
+
return parse_engineering_request_draft(payload)
|
|
139
|
+
except EngineeringRequestError as exc:
|
|
140
|
+
raise StructuredOutputError(str(exc)) from exc
|
|
141
|
+
|
|
142
|
+
invocation: InvocationResult = self.runtime.invoke(
|
|
143
|
+
role=AgentRole.INTERPRETER,
|
|
144
|
+
system_instruction=INTERPRETER_SYSTEM,
|
|
145
|
+
input_payload=input_payload,
|
|
146
|
+
output_schema=ENGINEERING_REQUEST_SCHEMA,
|
|
147
|
+
policy=policy,
|
|
148
|
+
validator=validate,
|
|
149
|
+
)
|
|
150
|
+
draft = invocation.value
|
|
151
|
+
assessment = self.completeness.assess(draft)
|
|
152
|
+
if not assessment.ready:
|
|
153
|
+
return InterpretedEngineeringRequest(
|
|
154
|
+
draft=draft,
|
|
155
|
+
assessment=assessment,
|
|
156
|
+
evidence=invocation.evidence,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
validated = self.compiler.compile(draft)
|
|
161
|
+
except EngineeringRequestError as exc:
|
|
162
|
+
rejection_code = str(exc)
|
|
163
|
+
rejected = RequestAssessment(
|
|
164
|
+
state=RequestState.REJECTED,
|
|
165
|
+
rejection_codes=(rejection_code,),
|
|
166
|
+
open_items=assessment.open_items,
|
|
167
|
+
)
|
|
168
|
+
return InterpretedEngineeringRequest(
|
|
169
|
+
draft=draft,
|
|
170
|
+
assessment=rejected,
|
|
171
|
+
evidence=invocation.evidence,
|
|
172
|
+
rejection_code=rejection_code,
|
|
173
|
+
)
|
|
174
|
+
return InterpretedEngineeringRequest(
|
|
175
|
+
draft=draft,
|
|
176
|
+
assessment=assessment,
|
|
177
|
+
evidence=invocation.evidence,
|
|
178
|
+
validated=validated,
|
|
179
|
+
)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Mapping
|
|
5
|
+
|
|
6
|
+
from .compiler import AGENT_PLAN_SCHEMA, AgentPlanCompiler
|
|
7
|
+
from .contracts import AgentEvidence, AgentRole, RoutingPolicy
|
|
8
|
+
from .runtime import AgentRuntime, InvocationResult
|
|
9
|
+
from .structured import StructuredOutputError, json_safe
|
|
10
|
+
from ..models import Goal, Resource, TaskGraph, WorldState
|
|
11
|
+
from ..planning import PlanningError, SUPPORTED_GOAL_ACTIONS
|
|
12
|
+
from ..verification import DeterministicVerifier
|
|
13
|
+
|
|
14
|
+
PLANNER_SYSTEM = """You are the DevAgent task planner. Propose only high-level tasks using the requested schema. Never emit raw servo commands, preconditions, effects, safety claims, controller permissions, or motion constraints. The deterministic engine owns those contracts. Unknown state must never be guessed. When validated engineering context is provided, use it to seek a suitable high-level decomposition, but never convert optimization preferences into permission to violate hard constraints."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True)
|
|
18
|
+
class PlannerResult:
|
|
19
|
+
graph: TaskGraph
|
|
20
|
+
evidence: AgentEvidence
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _resource_payload(resource: Resource) -> dict[str, Any]:
|
|
24
|
+
return {
|
|
25
|
+
"resource_id": resource.resource_id,
|
|
26
|
+
"resource_type": resource.resource_type,
|
|
27
|
+
"capabilities": sorted(
|
|
28
|
+
capability.value for capability in resource.capabilities
|
|
29
|
+
),
|
|
30
|
+
"metadata": dict(resource.metadata),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PlannerAgent:
|
|
35
|
+
"""LLM planner whose proposal is compiled into deterministic contracts."""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
runtime: AgentRuntime,
|
|
40
|
+
verifier: DeterministicVerifier | None = None,
|
|
41
|
+
compiler: AgentPlanCompiler | None = None,
|
|
42
|
+
) -> None:
|
|
43
|
+
self.runtime = runtime
|
|
44
|
+
self.verifier = verifier or DeterministicVerifier()
|
|
45
|
+
self.compiler = compiler or AgentPlanCompiler()
|
|
46
|
+
|
|
47
|
+
def plan(
|
|
48
|
+
self,
|
|
49
|
+
goal: Goal,
|
|
50
|
+
resources: list[Resource],
|
|
51
|
+
world: WorldState,
|
|
52
|
+
policy: RoutingPolicy,
|
|
53
|
+
*,
|
|
54
|
+
feedback: tuple[str, ...] = (),
|
|
55
|
+
engineering_context: Mapping[str, Any] | None = None,
|
|
56
|
+
) -> PlannerResult:
|
|
57
|
+
goal_action = goal.action.strip().lower()
|
|
58
|
+
if goal_action not in SUPPORTED_GOAL_ACTIONS:
|
|
59
|
+
raise PlanningError(f"unsupported_goal_action:{goal_action}")
|
|
60
|
+
|
|
61
|
+
input_payload = {
|
|
62
|
+
"goal": {
|
|
63
|
+
"goal_id": goal.goal_id,
|
|
64
|
+
"action": goal.action,
|
|
65
|
+
"object_id": goal.object_id,
|
|
66
|
+
"source": goal.source,
|
|
67
|
+
"destination": goal.destination,
|
|
68
|
+
},
|
|
69
|
+
"resources": [_resource_payload(resource) for resource in resources],
|
|
70
|
+
"world": {
|
|
71
|
+
"facts": sorted(world.facts),
|
|
72
|
+
"values": dict(world.values),
|
|
73
|
+
},
|
|
74
|
+
"feedback": list(feedback),
|
|
75
|
+
}
|
|
76
|
+
if engineering_context is not None:
|
|
77
|
+
input_payload["validated_engineering_context"] = json_safe(engineering_context)
|
|
78
|
+
|
|
79
|
+
def validate(payload: Mapping[str, Any]) -> TaskGraph:
|
|
80
|
+
graph = self.compiler.compile(
|
|
81
|
+
payload,
|
|
82
|
+
goal=goal,
|
|
83
|
+
resources=resources,
|
|
84
|
+
world=world,
|
|
85
|
+
)
|
|
86
|
+
structural = self.verifier.verify_graph(graph, resources)
|
|
87
|
+
if not structural.passed:
|
|
88
|
+
detail = ";".join(
|
|
89
|
+
f"{issue.code}:{issue.task_id or ''}:{issue.detail}"
|
|
90
|
+
for issue in structural.issues
|
|
91
|
+
)
|
|
92
|
+
raise StructuredOutputError(
|
|
93
|
+
f"deterministic_verification_failed:{detail}"
|
|
94
|
+
)
|
|
95
|
+
return graph
|
|
96
|
+
|
|
97
|
+
invocation: InvocationResult = self.runtime.invoke(
|
|
98
|
+
role=AgentRole.PLANNER,
|
|
99
|
+
system_instruction=PLANNER_SYSTEM,
|
|
100
|
+
input_payload=input_payload,
|
|
101
|
+
output_schema=AGENT_PLAN_SCHEMA,
|
|
102
|
+
policy=policy,
|
|
103
|
+
validator=validate,
|
|
104
|
+
)
|
|
105
|
+
return PlannerResult(invocation.value, invocation.evidence)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from .contracts import AgentEvidence, AgentRole, RecoveryAdvice, RoutingPolicy
|
|
6
|
+
from .runtime import AgentRuntime, InvocationResult
|
|
7
|
+
from .structured import RECOVERY_SCHEMA, parse_recovery
|
|
8
|
+
from ..models import WorldState
|
|
9
|
+
|
|
10
|
+
RECOVERY_SYSTEM = """You are the DevAgent recovery advisor. Recommend only bounded recovery actions: retry, replan, wait, observe, or abort. You cannot issue hardware commands, override interlocks, or bypass deterministic verification. Prefer abort or observe when state is uncertain."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True, slots=True)
|
|
14
|
+
class RecoveryResult:
|
|
15
|
+
advice: RecoveryAdvice
|
|
16
|
+
evidence: AgentEvidence
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RecoveryAgent:
|
|
20
|
+
"""Generate bounded recovery advice; never direct hardware commands."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, runtime: AgentRuntime) -> None:
|
|
23
|
+
self.runtime = runtime
|
|
24
|
+
|
|
25
|
+
def advise(
|
|
26
|
+
self,
|
|
27
|
+
*,
|
|
28
|
+
task_id: str,
|
|
29
|
+
issue_codes: tuple[str, ...],
|
|
30
|
+
world: WorldState,
|
|
31
|
+
policy: RoutingPolicy,
|
|
32
|
+
) -> RecoveryResult:
|
|
33
|
+
if not task_id.strip():
|
|
34
|
+
raise ValueError("task_id_required")
|
|
35
|
+
if not issue_codes:
|
|
36
|
+
raise ValueError("issue_codes_required")
|
|
37
|
+
|
|
38
|
+
input_payload = {
|
|
39
|
+
"task_id": task_id,
|
|
40
|
+
"issue_codes": list(issue_codes),
|
|
41
|
+
"world": {
|
|
42
|
+
"facts": sorted(world.facts),
|
|
43
|
+
"values": dict(world.values),
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
invocation: InvocationResult = self.runtime.invoke(
|
|
47
|
+
role=AgentRole.RECOVERY,
|
|
48
|
+
system_instruction=RECOVERY_SYSTEM,
|
|
49
|
+
input_payload=input_payload,
|
|
50
|
+
output_schema=RECOVERY_SCHEMA,
|
|
51
|
+
policy=policy,
|
|
52
|
+
validator=parse_recovery,
|
|
53
|
+
)
|
|
54
|
+
return RecoveryResult(invocation.value, invocation.evidence)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .contracts import AgentRole, RoutingPolicy, RoutingStrategy
|
|
4
|
+
from ..providers import ModelRegistry, ModelSpec, QualificationStatus
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class RoutingError(RuntimeError):
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ModelRouter:
|
|
12
|
+
"""Choose qualified models without coupling agents to a provider SDK."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, registry: ModelRegistry) -> None:
|
|
15
|
+
self.registry = registry
|
|
16
|
+
|
|
17
|
+
def candidates(
|
|
18
|
+
self, role: AgentRole, policy: RoutingPolicy
|
|
19
|
+
) -> tuple[ModelSpec, ...]:
|
|
20
|
+
statuses = (
|
|
21
|
+
{QualificationStatus.QUALIFIED}
|
|
22
|
+
if policy.require_qualified
|
|
23
|
+
else {
|
|
24
|
+
QualificationStatus.QUALIFIED,
|
|
25
|
+
QualificationStatus.SIMULATION_ONLY,
|
|
26
|
+
}
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
specs = [
|
|
30
|
+
spec
|
|
31
|
+
for spec in self.registry.all()
|
|
32
|
+
if spec.status in statuses
|
|
33
|
+
and spec.supports_structured_output
|
|
34
|
+
and spec.supports_role(role.value)
|
|
35
|
+
and spec.quality_score >= policy.min_quality
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
if policy.strategy is RoutingStrategy.FIXED:
|
|
39
|
+
specs = [
|
|
40
|
+
spec
|
|
41
|
+
for spec in specs
|
|
42
|
+
if spec.provider == policy.fixed_provider
|
|
43
|
+
and spec.model == policy.fixed_model
|
|
44
|
+
]
|
|
45
|
+
elif policy.strategy is RoutingStrategy.PRIVATE_ONLY:
|
|
46
|
+
specs = [spec for spec in specs if spec.private]
|
|
47
|
+
|
|
48
|
+
if not specs:
|
|
49
|
+
raise RoutingError(
|
|
50
|
+
f"no_eligible_model:{role.value}:{policy.strategy.value}"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if policy.strategy is RoutingStrategy.LOWEST_COST:
|
|
54
|
+
score = lambda spec: (
|
|
55
|
+
spec.cost_score,
|
|
56
|
+
spec.quality_score,
|
|
57
|
+
spec.latency_score,
|
|
58
|
+
)
|
|
59
|
+
elif policy.strategy is RoutingStrategy.LOWEST_LATENCY:
|
|
60
|
+
score = lambda spec: (
|
|
61
|
+
spec.latency_score,
|
|
62
|
+
spec.quality_score,
|
|
63
|
+
spec.cost_score,
|
|
64
|
+
)
|
|
65
|
+
else:
|
|
66
|
+
# AUTO_BEST intentionally prioritizes reasoning quality. Users who prefer
|
|
67
|
+
# cost or latency can select those explicit strategies instead.
|
|
68
|
+
score = lambda spec: (
|
|
69
|
+
0.80 * spec.quality_score
|
|
70
|
+
+ 0.10 * spec.latency_score
|
|
71
|
+
+ 0.10 * spec.cost_score,
|
|
72
|
+
spec.quality_score,
|
|
73
|
+
spec.latency_score,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return tuple(sorted(specs, key=score, reverse=True))
|