File size: 6,959 Bytes
f850954
 
 
 
 
 
 
 
 
 
 
 
1a36c6b
f850954
1a36c6b
 
f850954
1a36c6b
 
 
 
 
f850954
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
da839c2
f850954
 
 
 
 
 
6772038
52ba0e1
536e671
f850954
1a36c6b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
"""Shared fixtures: an offline vector store, a stand-in G1 model, and an app wired to a scripted LLM."""

from __future__ import annotations

import hashlib
import re
from dataclasses import replace
from pathlib import Path

import numpy as np
import pytest
from chromadb.api.types import Documents, EmbeddingFunction, Embeddings
from fastapi.testclient import TestClient

from api import keys
from api.auth import Allowlist
from api.config import get_settings
from api.llm import ScriptedLLM
from api.logstore import LogStore
from api.main import Services, create_app
from api.runpod_client import RunPodError
from api.storage import LocalStorage
from rag.chunking import Chunk
from rag.store import RagStore

FAKE_G1 = """<mujoco model="fake g1">
  <compiler angle="radian"/>
  <worldbody>
    <body name="pelvis" pos="0 0 0.8">
      <freejoint name="floating_base_joint"/>
      <geom name="torso" type="capsule" size="0.1 0.3" mass="10"/>
      <body name="left_knee_link" pos="0 0.1 -0.4">
        <joint name="left_knee_joint" axis="0 1 0" range="0 2.8"/>
        <geom type="capsule" size="0.05 0.2"/>
      </body>
    </body>
  </worldbody>
  <actuator><motor name="left_knee_joint" joint="left_knee_joint"/></actuator>
  <keyframe><key name="stand" qpos="0 0 0.8 1 0 0 0 0"/></keyframe>
</mujoco>
"""

GOOD_SCENE = """<mujoco model="g1 on slope">
  <include file="g1.xml"/>
  <worldbody>
    <light name="sun" pos="0 0 4"/>
    <geom name="floor" type="plane" size="0 0 0.05" friction="1 0.005 0.0001"/>
    <geom name="slope" type="box" size="1 1 0.05" pos="2 0 0.2" euler="0 -0.26 0"/>
    <body name="crate" pos="1 1 0.3"><freejoint/><geom name="crate_geom" type="box" size="0.2 0.2 0.2" mass="3"/></body>
  </worldbody>
</mujoco>"""


class HashEmbedding(EmbeddingFunction[Documents]):
    """Bag-of-words hashing embedding: deterministic, offline, good enough to rank by word overlap."""

    def __init__(self) -> None:
        pass

    def __call__(self, input: Documents) -> Embeddings:
        vectors = []
        for text in input:
            v = np.zeros(256, dtype=np.float32)
            for word in re.findall(r"[a-z0-9_]+", text.lower()):
                v[int(hashlib.md5(word.encode()).hexdigest(), 16) % 256] += 1.0
            n = np.linalg.norm(v)
            vectors.append(v / n if n else v)
        return vectors

    @staticmethod
    def name() -> str:
        return "test-hash"

    def get_config(self) -> dict:
        return {}

    @staticmethod
    def build_from_config(config: dict) -> HashEmbedding:
        return HashEmbedding()


@pytest.fixture
def store(tmp_path) -> RagStore:
    s = RagStore(tmp_path / "chroma", embedding_function=HashEmbedding())
    s.add(
        "mujoco",
        [
            Chunk(
                "[XML reference > geom] geom type plane box sphere friction size pos euler", "doc#geom", {"kind": "doc"}
            ),
            Chunk("[XML reference > light] light pos dir diffuse", "doc#light", {"kind": "doc"}),
            Chunk(
                '[MJCF example scene.xml] <include file="g1.xml"/> floor plane',
                "menagerie:scene.xml",
                {"kind": "example", "robot": "unitree_g1"},
            ),
        ],
    )
    s.add(
        "ros2",
        [
            Chunk(
                "[Writing a simple py publisher] create_publisher String timer_callback rclpy",
                "docs:pubsub",
                {"kind": "doc", "distro": "humble"},
            ),
            Chunk(
                "[Launch file formats] LaunchDescription Node remappings parameters",
                "docs:launch",
                {"kind": "doc", "distro": "humble"},
            ),
            Chunk(
                "[ROS 2 humble Message sensor_msgs/msg/LaserScan]\nfloat32[] ranges",
                "ci:LaserScan.msg",
                {"kind": "interface", "distro": "humble", "interface": "sensor_msgs/msg/LaserScan"},
            ),
        ],
    )
    return s


@pytest.fixture
def menagerie(tmp_path) -> Path:
    root = tmp_path / "menagerie"
    (root / "unitree_g1").mkdir(parents=True)
    (root / "unitree_g1" / "g1.xml").write_text(FAKE_G1)
    return root


@pytest.fixture
def settings(tmp_path, menagerie):
    base = get_settings()
    return replace(
        base,
        llm_api_key="test",
        data_dir=tmp_path / "data",
        testers_file=tmp_path / "testers.yaml",
        log_db=tmp_path / "data" / "log.sqlite3",
        menagerie_dir=menagerie,
        mjcf_sim_seconds=0.2,
        codegen_backend="direct",
        auto_review=False,
        prompt_enhance=False,
        free_debug=False,
    )


# --------------------------------------------------------------------------- job backend


class FakeBackend:
    name = "runpod"

    def __init__(self):
        self.jobs: dict[str, dict] = {}
        self.submitted: list[dict] = []
        self.fail_submit = False
        self.functions: list[str] = []

    def supports(self, function):
        return True

    def submit(self, job_input, execution_timeout_s, function="run_simulation"):
        self.functions.append(function)
        if self.fail_submit:
            raise RunPodError("RunPod /run returned HTTP 500: boom")
        job_id = f"job-{len(self.submitted) + 1}"
        self.submitted.append(job_input)
        self.jobs[job_id] = {"id": job_id, "status": "IN_QUEUE"}
        return dict(self.jobs[job_id])

    def status(self, job_id):
        return dict(self.jobs[job_id])


@pytest.fixture
def run_env(settings, store, tmp_path):
    alice = keys.add(settings.testers_file, "alice")
    bob = keys.add(settings.testers_file, "bob")
    llm = ScriptedLLM([])
    backend = FakeBackend()
    storage = LocalStorage(tmp_path / "r2", base_url="https://r2.example/bucket")
    services = Services(
        settings=settings,
        store=store,
        logs=LogStore(settings.log_db),
        allowlist=Allowlist(settings.testers_file),
        llm_factory=lambda: llm,
        backend=backend,
        storage=storage,
    )
    client = TestClient(create_app(services))
    llm.responses[:] = [f"```xml\n{GOOD_SCENE}\n```"]
    gen = client.post(
        "/simulate/generate", json={"description": "G1 near a slope and a crate"}, headers={"X-API-Key": alice}
    ).json()
    return client, {"alice": alice, "bob": bob}, backend, services, gen["id"]


def complete(backend, job_id, run_id, **summary):
    backend.jobs[job_id] = {
        "id": job_id,
        "status": "COMPLETED",
        "executionTime": 12500,
        "delayTime": 3000,
        "output": {
            "run_id": run_id,
            "summary": {"simulated_seconds": 10.0, "fell": False, **summary},
            "files": {
                "mcap": f"runs/{run_id}/run.mcap",
                "video": f"runs/{run_id}/video.mp4",
                "video_webm": f"runs/{run_id}/video.webm",
                "summary": f"runs/{run_id}/summary.json",
            },
        },
    }