creature-lab 0.3.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.
- creature_lab/__init__.py +18 -0
- creature_lab/agents/__init__.py +5 -0
- creature_lab/agents/baseline.py +53 -0
- creature_lab/agents/llm.py +33 -0
- creature_lab/agents/loop.py +112 -0
- creature_lab/agents/prompts.py +63 -0
- creature_lab/agents/tools.py +101 -0
- creature_lab/autopsy.py +376 -0
- creature_lab/backends/__init__.py +1 -0
- creature_lab/backends/base.py +25 -0
- creature_lab/backends/mujoco_backend.py +253 -0
- creature_lab/backends/pybullet_backend.py +573 -0
- creature_lab/cli.py +3317 -0
- creature_lab/controllers/__init__.py +1 -0
- creature_lab/controllers/cpg.py +83 -0
- creature_lab/controllers/factory.py +208 -0
- creature_lab/controllers/pid.py +33 -0
- creature_lab/controllers/policy.py +91 -0
- creature_lab/controllers/pose_seq.py +38 -0
- creature_lab/controllers/posture.py +125 -0
- creature_lab/controllers/sinusoid.py +20 -0
- creature_lab/controllers/target_seek.py +128 -0
- creature_lab/diagnosis.py +435 -0
- creature_lab/diagnostics.py +261 -0
- creature_lab/editor/__init__.py +5 -0
- creature_lab/editor/controls.py +2495 -0
- creature_lab/editor/history.py +85 -0
- creature_lab/editor/jobs.py +176 -0
- creature_lab/editor/labels.py +91 -0
- creature_lab/editor/live.py +672 -0
- creature_lab/editor/playback.py +98 -0
- creature_lab/editor/presets.py +282 -0
- creature_lab/editor/robot_panel.py +288 -0
- creature_lab/editor/robot_stage.py +181 -0
- creature_lab/editor/session.py +1413 -0
- creature_lab/env.py +274 -0
- creature_lab/evolve.py +394 -0
- creature_lab/export/__init__.py +9 -0
- creature_lab/export/mjcf.py +199 -0
- creature_lab/export/urdf.py +152 -0
- creature_lab/export/urdf_import.py +142 -0
- creature_lab/exporting.py +360 -0
- creature_lab/failure_zoo.py +98 -0
- creature_lab/hashing.py +19 -0
- creature_lab/io_utils.py +37 -0
- creature_lab/library.py +311 -0
- creature_lab/qualification.py +299 -0
- creature_lab/reports.py +428 -0
- creature_lab/reports_html.py +549 -0
- creature_lab/rl/__init__.py +9 -0
- creature_lab/rl/gym_env.py +74 -0
- creature_lab/rl/train.py +148 -0
- creature_lab/robots/__init__.py +6 -0
- creature_lab/robots/actuator.py +115 -0
- creature_lab/robots/analysis.py +282 -0
- creature_lab/robots/api.py +30 -0
- creature_lab/robots/card.py +182 -0
- creature_lab/robots/catalog.py +153 -0
- creature_lab/robots/checkup.py +216 -0
- creature_lab/robots/cli.py +275 -0
- creature_lab/robots/compare.py +229 -0
- creature_lab/robots/data/UNITREE_RL_GYM_LICENSE.txt +23 -0
- creature_lab/robots/data/microduck.json +253 -0
- creature_lab/robots/data/unitree_g1.json +174 -0
- creature_lab/robots/data/unitree_g1_policy.npz +0 -0
- creature_lab/robots/fetch.py +115 -0
- creature_lab/robots/film.py +78 -0
- creature_lab/robots/g1.py +103 -0
- creature_lab/robots/microduck.py +153 -0
- creature_lab/robots/overlay.py +384 -0
- creature_lab/robots/policies.py +81 -0
- creature_lab/robots/render.py +236 -0
- creature_lab/robots/report.py +307 -0
- creature_lab/robots/rollout.py +185 -0
- creature_lab/robots/scene.py +53 -0
- creature_lab/robots/story.py +485 -0
- creature_lab/robots/types.py +120 -0
- creature_lab/robots/video.py +277 -0
- creature_lab/robots/visual.py +41 -0
- creature_lab/robustness.py +165 -0
- creature_lab/runs.py +242 -0
- creature_lab/scaffold/__init__.py +29 -0
- creature_lab/scaffold/humanoid.py +227 -0
- creature_lab/scaffold/legged.py +144 -0
- creature_lab/scaffold/mirror.py +90 -0
- creature_lab/scaffold/worm.py +80 -0
- creature_lab/schema/__init__.py +79 -0
- creature_lab/schema/agent.py +28 -0
- creature_lab/schema/base.py +33 -0
- creature_lab/schema/control.py +49 -0
- creature_lab/schema/controller.py +208 -0
- creature_lab/schema/creature.py +235 -0
- creature_lab/schema/summary.py +30 -0
- creature_lab/schema/task.py +137 -0
- creature_lab/schema/trace.py +110 -0
- creature_lab/scoring.py +56 -0
- creature_lab/terrain.py +172 -0
- creature_lab/validation.py +103 -0
- creature_lab/viewers/__init__.py +1 -0
- creature_lab/viewers/overlays.py +71 -0
- creature_lab/viewers/plotting.py +43 -0
- creature_lab/viewers/video_exporter.py +26 -0
- creature_lab/viewers/viser_viewer.py +489 -0
- creature_lab/web/__init__.py +6 -0
- creature_lab/web/builder.py +91 -0
- creature_lab/web/creatures.py +445 -0
- creature_lab/web/jobs.py +172 -0
- creature_lab/web/library.py +254 -0
- creature_lab/web/scene.py +112 -0
- creature_lab/web/server.py +394 -0
- creature_lab/web/static/assets/index-CVJ_iiu_.css +1 -0
- creature_lab/web/static/assets/index-D-2y8anQ.js +4233 -0
- creature_lab/web/static/clips/unitree_g1.webp +0 -0
- creature_lab/web/static/index.html +16 -0
- creature_lab/web/thumbs.py +120 -0
- creature_lab/web/whatif.py +149 -0
- creature_lab/zoo/damaged_quadruped/baselines/recover.json +14 -0
- creature_lab/zoo/damaged_quadruped/baselines/recover.mujoco.json +14 -0
- creature_lab/zoo/damaged_quadruped/creature.json +205 -0
- creature_lab/zoo/damaged_quadruped/tasks/recover.json +8 -0
- creature_lab/zoo/hexapod/baselines/crawl_forward.json +14 -0
- creature_lab/zoo/hexapod/baselines/crawl_forward.mujoco.json +14 -0
- creature_lab/zoo/hexapod/controller.json +42 -0
- creature_lab/zoo/hexapod/creature.json +295 -0
- creature_lab/zoo/hexapod/tasks/crawl_forward.json +7 -0
- creature_lab/zoo/humanoid_12dof/baselines/reach.json +14 -0
- creature_lab/zoo/humanoid_12dof/baselines/reach.mujoco.json +14 -0
- creature_lab/zoo/humanoid_12dof/baselines/walk.json +14 -0
- creature_lab/zoo/humanoid_12dof/baselines/walk.mujoco.json +14 -0
- creature_lab/zoo/humanoid_12dof/controller.json +18 -0
- creature_lab/zoo/humanoid_12dof/creature.json +628 -0
- creature_lab/zoo/humanoid_12dof/tasks/reach.json +8 -0
- creature_lab/zoo/humanoid_12dof/tasks/walk.json +7 -0
- creature_lab/zoo/humanoid_minimal/baselines/balance.json +14 -0
- creature_lab/zoo/humanoid_minimal/baselines/balance.mujoco.json +14 -0
- creature_lab/zoo/humanoid_minimal/baselines/push_recovery.json +14 -0
- creature_lab/zoo/humanoid_minimal/baselines/push_recovery.mujoco.json +14 -0
- creature_lab/zoo/humanoid_minimal/baselines/walk.json +14 -0
- creature_lab/zoo/humanoid_minimal/baselines/walk.mujoco.json +14 -0
- creature_lab/zoo/humanoid_minimal/creature.json +434 -0
- creature_lab/zoo/humanoid_minimal/tasks/balance.json +7 -0
- creature_lab/zoo/humanoid_minimal/tasks/push_recovery.json +8 -0
- creature_lab/zoo/humanoid_minimal/tasks/walk.json +7 -0
- creature_lab/zoo/quadruped/baselines/crawl_forward.json +14 -0
- creature_lab/zoo/quadruped/baselines/crawl_forward.mujoco.json +14 -0
- creature_lab/zoo/quadruped/baselines/gap_cross.json +14 -0
- creature_lab/zoo/quadruped/baselines/gap_cross.mujoco.json +14 -0
- creature_lab/zoo/quadruped/baselines/low_friction_crawl.mujoco.json +14 -0
- creature_lab/zoo/quadruped/baselines/push_recovery.mujoco.json +14 -0
- creature_lab/zoo/quadruped/baselines/reach_target.json +14 -0
- creature_lab/zoo/quadruped/baselines/reach_target.mujoco.json +14 -0
- creature_lab/zoo/quadruped/baselines/slope_climb.json +14 -0
- creature_lab/zoo/quadruped/baselines/slope_climb.mujoco.json +14 -0
- creature_lab/zoo/quadruped/baselines/stability_hold.mujoco.json +14 -0
- creature_lab/zoo/quadruped/baselines/step_over.json +14 -0
- creature_lab/zoo/quadruped/baselines/step_over.mujoco.json +14 -0
- creature_lab/zoo/quadruped/controller.json +30 -0
- creature_lab/zoo/quadruped/creature.json +205 -0
- creature_lab/zoo/quadruped/tasks/crawl_forward.json +7 -0
- creature_lab/zoo/quadruped/tasks/gap_cross.json +7 -0
- creature_lab/zoo/quadruped/tasks/low_friction_crawl.json +7 -0
- creature_lab/zoo/quadruped/tasks/push_recovery.json +8 -0
- creature_lab/zoo/quadruped/tasks/reach_target.json +8 -0
- creature_lab/zoo/quadruped/tasks/slope_climb.json +7 -0
- creature_lab/zoo/quadruped/tasks/stability_hold.json +7 -0
- creature_lab/zoo/quadruped/tasks/step_over.json +7 -0
- creature_lab/zoo/tripod/baselines/crawl_forward.json +14 -0
- creature_lab/zoo/tripod/baselines/crawl_forward.mujoco.json +14 -0
- creature_lab/zoo/tripod/controller.json +24 -0
- creature_lab/zoo/tripod/creature.json +145 -0
- creature_lab/zoo/tripod/tasks/crawl_forward.json +7 -0
- creature_lab/zoo/worm/baselines/crawl_forward.json +14 -0
- creature_lab/zoo/worm/baselines/crawl_forward.mujoco.json +14 -0
- creature_lab/zoo/worm/controller.json +36 -0
- creature_lab/zoo/worm/creature.json +247 -0
- creature_lab/zoo/worm/tasks/crawl_forward.json +7 -0
- creature_lab/zoo.py +171 -0
- creature_lab-0.3.0.dist-info/METADATA +370 -0
- creature_lab-0.3.0.dist-info/RECORD +182 -0
- creature_lab-0.3.0.dist-info/WHEEL +4 -0
- creature_lab-0.3.0.dist-info/entry_points.txt +2 -0
- creature_lab-0.3.0.dist-info/licenses/LICENSE +201 -0
creature_lab/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Creature Lab: a minimal, visual, backend-agnostic creature simulation lab."""
|
|
2
|
+
|
|
3
|
+
from creature_lab.schema import (
|
|
4
|
+
CreatureSpec,
|
|
5
|
+
EpisodeTrace,
|
|
6
|
+
FrameState,
|
|
7
|
+
TaskSpec,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
VERSION = "0.3.0"
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"VERSION",
|
|
14
|
+
"CreatureSpec",
|
|
15
|
+
"EpisodeTrace",
|
|
16
|
+
"FrameState",
|
|
17
|
+
"TaskSpec",
|
|
18
|
+
]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""A no-LLM policy that proposes random, valid tool calls.
|
|
2
|
+
|
|
3
|
+
Lets the agent loop (and ``creature-lab ask --offline``) run without any model
|
|
4
|
+
provider, so the lab is useful and testable out of the box. Deterministic for a
|
|
5
|
+
given seed.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import random
|
|
11
|
+
|
|
12
|
+
from creature_lab.agents.loop import Observation, Proposal
|
|
13
|
+
from creature_lab.schema.creature import ShapeType
|
|
14
|
+
|
|
15
|
+
_LENGTH_SHAPES = (ShapeType.CAPSULE, ShapeType.CYLINDER)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RandomToolPolicy:
|
|
19
|
+
"""Propose a random parameter tweak via the validated tool layer."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, seed: int = 0) -> None:
|
|
22
|
+
self._rng = random.Random(seed)
|
|
23
|
+
|
|
24
|
+
def __call__(self, observation: Observation) -> Proposal:
|
|
25
|
+
creature = observation.creature
|
|
26
|
+
rng = self._rng
|
|
27
|
+
options: list[str] = []
|
|
28
|
+
if creature.motors:
|
|
29
|
+
options += ["amplitude", "frequency", "phase"]
|
|
30
|
+
if any(part.shape in _LENGTH_SHAPES for part in creature.parts):
|
|
31
|
+
options.append("length")
|
|
32
|
+
if not options:
|
|
33
|
+
# Nothing tweakable; a no-op proposal the loop will reject as invalid.
|
|
34
|
+
return Proposal("set_motor", {"joint": "_none_"}, "no tunable parameters")
|
|
35
|
+
|
|
36
|
+
choice = rng.choice(options)
|
|
37
|
+
if choice == "length":
|
|
38
|
+
limbs = [part for part in creature.parts if part.shape in _LENGTH_SHAPES]
|
|
39
|
+
limb = rng.choice(limbs)
|
|
40
|
+
new_length = round(max(0.05, limb.length * rng.uniform(0.85, 1.15)), 4)
|
|
41
|
+
return Proposal("resize_limb", {"part": limb.id, "length": new_length}, "resize limb")
|
|
42
|
+
|
|
43
|
+
motor = rng.choice(creature.motors)
|
|
44
|
+
if choice == "amplitude":
|
|
45
|
+
value = round(max(0.0, motor.amplitude * rng.uniform(0.7, 1.3)), 4)
|
|
46
|
+
args = {"joint": motor.joint, "amplitude": value}
|
|
47
|
+
return Proposal("set_motor", args, "tune amplitude")
|
|
48
|
+
if choice == "frequency":
|
|
49
|
+
value = round(max(0.1, motor.frequency * rng.uniform(0.7, 1.3)), 4)
|
|
50
|
+
args = {"joint": motor.joint, "frequency": value}
|
|
51
|
+
return Proposal("set_motor", args, "tune frequency")
|
|
52
|
+
value = round(motor.phase + rng.uniform(-0.5, 0.5), 4)
|
|
53
|
+
return Proposal("set_motor", {"joint": motor.joint, "phase": value}, "tune phase")
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""LiteLLM-backed policy (optional ``llm`` extra).
|
|
2
|
+
|
|
3
|
+
Imports litellm lazily so the rest of the agent stack works without it. The
|
|
4
|
+
prompt building and response parsing live in ``prompts.py`` and are tested
|
|
5
|
+
without any network call.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from creature_lab.agents.loop import Observation, Proposal
|
|
11
|
+
from creature_lab.agents.prompts import SYSTEM_PROMPT, build_prompt, parse_proposal
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LLMPolicy:
|
|
15
|
+
"""Ask a model (via LiteLLM) for the next tool call."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, model: str = "gpt-4o-mini", temperature: float = 0.3) -> None:
|
|
18
|
+
self.model = model
|
|
19
|
+
self.temperature = temperature
|
|
20
|
+
|
|
21
|
+
def __call__(self, observation: Observation) -> Proposal:
|
|
22
|
+
import litellm
|
|
23
|
+
|
|
24
|
+
response = litellm.completion(
|
|
25
|
+
model=self.model,
|
|
26
|
+
temperature=self.temperature,
|
|
27
|
+
messages=[
|
|
28
|
+
{"role": "system", "content": SYSTEM_PROMPT},
|
|
29
|
+
{"role": "user", "content": build_prompt(observation)},
|
|
30
|
+
],
|
|
31
|
+
)
|
|
32
|
+
text = response.choices[0].message.content or ""
|
|
33
|
+
return parse_proposal(text)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Policy-driven design loop.
|
|
2
|
+
|
|
3
|
+
The loop is provider-agnostic: a ``Policy`` proposes a tool call given an
|
|
4
|
+
``Observation``; the loop validates and applies it through the tool layer,
|
|
5
|
+
re-evaluates the creature with a caller-supplied ``evaluate`` function, keeps the
|
|
6
|
+
best, and records an ``AgentTrace``. This keeps the loop testable without any LLM
|
|
7
|
+
or simulator.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from creature_lab.agents.tools import ToolError, apply_tool
|
|
17
|
+
from creature_lab.schema import AgentStep, AgentTrace, CreatureSpec
|
|
18
|
+
|
|
19
|
+
EvaluateFn = Callable[[CreatureSpec], float]
|
|
20
|
+
#: Optional: summarize why a creature is scoring the way it does, as prompt-ready text.
|
|
21
|
+
DiagnoseFn = Callable[[CreatureSpec], str]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class Observation:
|
|
26
|
+
"""What a policy sees before proposing the next edit."""
|
|
27
|
+
|
|
28
|
+
creature: CreatureSpec
|
|
29
|
+
best_score: float
|
|
30
|
+
attempt: int
|
|
31
|
+
goal: str = ""
|
|
32
|
+
#: Plain-language failure-pattern summary for the current best (empty if none/unavailable).
|
|
33
|
+
diagnosis: str = ""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class Proposal:
|
|
38
|
+
"""A tool call proposed by a policy."""
|
|
39
|
+
|
|
40
|
+
tool: str
|
|
41
|
+
args: dict[str, Any] = field(default_factory=dict)
|
|
42
|
+
note: str = ""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
Policy = Callable[[Observation], Proposal]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class DesignResult:
|
|
50
|
+
best: CreatureSpec
|
|
51
|
+
best_score: float
|
|
52
|
+
trace: AgentTrace
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def design_loop(
|
|
56
|
+
creature: CreatureSpec,
|
|
57
|
+
evaluate: EvaluateFn,
|
|
58
|
+
policy: Policy,
|
|
59
|
+
*,
|
|
60
|
+
attempts: int,
|
|
61
|
+
goal: str = "",
|
|
62
|
+
task_name: str = "design",
|
|
63
|
+
diagnose: DiagnoseFn | None = None,
|
|
64
|
+
) -> DesignResult:
|
|
65
|
+
"""Run the policy for `attempts` steps, keeping any higher-scoring valid edit.
|
|
66
|
+
|
|
67
|
+
When ``diagnose`` is given, it is called on the current best before every proposal
|
|
68
|
+
and its text is attached to the ``Observation`` — so a policy (an LLM prompt, or a
|
|
69
|
+
future rule-based one) can react to *why* the creature is scoring the way it is,
|
|
70
|
+
not just the scalar score.
|
|
71
|
+
"""
|
|
72
|
+
if attempts < 0:
|
|
73
|
+
raise ValueError("attempts must not be negative")
|
|
74
|
+
|
|
75
|
+
best = creature
|
|
76
|
+
best_score = evaluate(creature)
|
|
77
|
+
steps = [AgentStep(attempt=0, action="seed", valid=True, score=best_score, accepted=True)]
|
|
78
|
+
|
|
79
|
+
for attempt in range(1, attempts + 1):
|
|
80
|
+
diagnosis = diagnose(best) if diagnose is not None else ""
|
|
81
|
+
proposal = policy(Observation(best, best_score, attempt, goal, diagnosis))
|
|
82
|
+
action = f"{proposal.tool}({proposal.args})"
|
|
83
|
+
try:
|
|
84
|
+
candidate = apply_tool(best, proposal.tool, proposal.args)
|
|
85
|
+
except ToolError as exc:
|
|
86
|
+
steps.append(
|
|
87
|
+
AgentStep(attempt=attempt, action=action, valid=False, note=str(exc)[:160])
|
|
88
|
+
)
|
|
89
|
+
continue
|
|
90
|
+
score = evaluate(candidate)
|
|
91
|
+
accepted = score > best_score
|
|
92
|
+
if accepted:
|
|
93
|
+
best, best_score = candidate, score
|
|
94
|
+
steps.append(
|
|
95
|
+
AgentStep(
|
|
96
|
+
attempt=attempt,
|
|
97
|
+
action=action,
|
|
98
|
+
valid=True,
|
|
99
|
+
score=score,
|
|
100
|
+
accepted=accepted,
|
|
101
|
+
note=proposal.note,
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
trace = AgentTrace(
|
|
106
|
+
creature_name=creature.name,
|
|
107
|
+
task_name=task_name,
|
|
108
|
+
goal=goal,
|
|
109
|
+
best_score=best_score,
|
|
110
|
+
steps=steps,
|
|
111
|
+
)
|
|
112
|
+
return DesignResult(best=best, best_score=best_score, trace=trace)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Prompt construction and response parsing for the LLM policy.
|
|
2
|
+
|
|
3
|
+
Kept free of any provider import so the parser is unit-testable without network.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
|
|
10
|
+
from creature_lab.agents.loop import Observation, Proposal
|
|
11
|
+
from creature_lab.agents.tools import TOOL_SIGNATURES
|
|
12
|
+
|
|
13
|
+
SYSTEM_PROMPT = (
|
|
14
|
+
"You are a creature-design assistant. You improve a small simulated robot-creature "
|
|
15
|
+
"by proposing ONE tool call at a time. Respond with a single JSON object only, of the "
|
|
16
|
+
'form {"tool": <name>, "args": {<kwargs>}, "note": <short reason>}. Do not add prose.'
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _creature_summary(observation: Observation) -> str:
|
|
21
|
+
creature = observation.creature
|
|
22
|
+
motors = ", ".join(
|
|
23
|
+
f"{m.joint}(amp={m.amplitude}, freq={m.frequency}, phase={m.phase})"
|
|
24
|
+
for m in creature.motors
|
|
25
|
+
)
|
|
26
|
+
limbs = ", ".join(f"{p.id}(len={p.length})" for p in creature.parts if p.length is not None)
|
|
27
|
+
summary = (
|
|
28
|
+
f"goal: {observation.goal}\n"
|
|
29
|
+
f"current best score: {observation.best_score:.4f}\n"
|
|
30
|
+
f"motors: {motors or 'none'}\n"
|
|
31
|
+
f"resizable limbs: {limbs or 'none'}"
|
|
32
|
+
)
|
|
33
|
+
if observation.diagnosis:
|
|
34
|
+
summary += f"\ndiagnosed issues: {observation.diagnosis}"
|
|
35
|
+
return summary
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def build_prompt(observation: Observation) -> str:
|
|
39
|
+
"""Build the user message describing the creature and available tools."""
|
|
40
|
+
tools = "\n".join(f"- {name}({sig})" for name, sig in TOOL_SIGNATURES.items())
|
|
41
|
+
return (
|
|
42
|
+
f"{_creature_summary(observation)}\n\navailable tools:\n{tools}\n\nPropose one tool call."
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def parse_proposal(text: str) -> Proposal:
|
|
47
|
+
"""Parse a model response into a Proposal, tolerating surrounding prose/code fences."""
|
|
48
|
+
start = text.find("{")
|
|
49
|
+
end = text.rfind("}")
|
|
50
|
+
if start == -1 or end == -1 or end < start:
|
|
51
|
+
raise ValueError("no JSON object found in model response")
|
|
52
|
+
try:
|
|
53
|
+
data = json.loads(text[start : end + 1])
|
|
54
|
+
except json.JSONDecodeError as exc:
|
|
55
|
+
raise ValueError(f"invalid JSON in model response: {exc}") from exc
|
|
56
|
+
tool = data.get("tool")
|
|
57
|
+
if not isinstance(tool, str) or not tool:
|
|
58
|
+
raise ValueError("model response missing a 'tool' name")
|
|
59
|
+
args = data.get("args", {})
|
|
60
|
+
if not isinstance(args, dict):
|
|
61
|
+
raise ValueError("'args' must be an object")
|
|
62
|
+
note = str(data.get("note", ""))
|
|
63
|
+
return Proposal(tool=tool, args=args, note=note)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Validated creature-editing tools.
|
|
2
|
+
|
|
3
|
+
Each tool takes a ``CreatureSpec`` plus keyword arguments and returns a new,
|
|
4
|
+
re-validated ``CreatureSpec``. Anything invalid (unknown id, wrong shape, value
|
|
5
|
+
out of range) raises ``ToolError`` rather than producing an unvalidated creature,
|
|
6
|
+
so the agent loop can record the failure and move on.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from pydantic import ValidationError
|
|
14
|
+
|
|
15
|
+
from creature_lab.schema import CreatureSpec
|
|
16
|
+
from creature_lab.schema.creature import ShapeType
|
|
17
|
+
|
|
18
|
+
_LENGTH_SHAPES = (ShapeType.CAPSULE, ShapeType.CYLINDER)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ToolError(ValueError):
|
|
22
|
+
"""Raised when a tool is called with arguments that cannot be applied."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _revalidate(data: dict) -> CreatureSpec:
|
|
26
|
+
try:
|
|
27
|
+
return CreatureSpec.model_validate(data)
|
|
28
|
+
except ValidationError as exc:
|
|
29
|
+
raise ToolError(f"resulting creature is invalid: {exc.errors()[0]['msg']}") from exc
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def set_motor(
|
|
33
|
+
creature: CreatureSpec,
|
|
34
|
+
*,
|
|
35
|
+
joint: str,
|
|
36
|
+
amplitude: float | None = None,
|
|
37
|
+
frequency: float | None = None,
|
|
38
|
+
phase: float | None = None,
|
|
39
|
+
) -> CreatureSpec:
|
|
40
|
+
"""Update an existing motor's sinusoid parameters."""
|
|
41
|
+
data = creature.model_dump()
|
|
42
|
+
motors = [motor for motor in data["motors"] if motor["joint"] == joint]
|
|
43
|
+
if not motors:
|
|
44
|
+
raise ToolError(f"no motor drives joint {joint!r}")
|
|
45
|
+
motor = motors[0]
|
|
46
|
+
if amplitude is not None:
|
|
47
|
+
motor["amplitude"] = amplitude
|
|
48
|
+
if frequency is not None:
|
|
49
|
+
motor["frequency"] = frequency
|
|
50
|
+
if phase is not None:
|
|
51
|
+
motor["phase"] = phase
|
|
52
|
+
return _revalidate(data)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def set_joint_limit(
|
|
56
|
+
creature: CreatureSpec, *, joint: str, lower: float, upper: float
|
|
57
|
+
) -> CreatureSpec:
|
|
58
|
+
"""Set the lower/upper limit of a joint."""
|
|
59
|
+
data = creature.model_dump()
|
|
60
|
+
joints = [item for item in data["joints"] if item["id"] == joint]
|
|
61
|
+
if not joints:
|
|
62
|
+
raise ToolError(f"unknown joint {joint!r}")
|
|
63
|
+
joints[0]["limit"] = [lower, upper]
|
|
64
|
+
return _revalidate(data)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def resize_limb(creature: CreatureSpec, *, part: str, length: float) -> CreatureSpec:
|
|
68
|
+
"""Change the length of a capsule/cylinder part."""
|
|
69
|
+
data = creature.model_dump()
|
|
70
|
+
parts = [item for item in data["parts"] if item["id"] == part]
|
|
71
|
+
if not parts:
|
|
72
|
+
raise ToolError(f"unknown part {part!r}")
|
|
73
|
+
if parts[0]["shape"] not in _LENGTH_SHAPES:
|
|
74
|
+
raise ToolError(f"part {part!r} has no length to resize")
|
|
75
|
+
parts[0]["length"] = length
|
|
76
|
+
return _revalidate(data)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
#: Public tool registry and a short, prompt-friendly description of each tool's args.
|
|
80
|
+
TOOLS = {
|
|
81
|
+
"set_motor": set_motor,
|
|
82
|
+
"set_joint_limit": set_joint_limit,
|
|
83
|
+
"resize_limb": resize_limb,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
TOOL_SIGNATURES = {
|
|
87
|
+
"set_motor": "joint:str, amplitude?:float, frequency?:float, phase?:float",
|
|
88
|
+
"set_joint_limit": "joint:str, lower:float, upper:float",
|
|
89
|
+
"resize_limb": "part:str, length:float",
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def apply_tool(creature: CreatureSpec, name: str, args: dict[str, Any]) -> CreatureSpec:
|
|
94
|
+
"""Dispatch to a tool by name, validating the call."""
|
|
95
|
+
tool = TOOLS.get(name)
|
|
96
|
+
if tool is None:
|
|
97
|
+
raise ToolError(f"unknown tool {name!r}")
|
|
98
|
+
try:
|
|
99
|
+
return tool(creature, **args)
|
|
100
|
+
except TypeError as exc: # wrong/missing keyword arguments
|
|
101
|
+
raise ToolError(f"bad arguments for {name!r}: {exc}") from exc
|