lang2sim 2.0.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.
- lang2sim/__init__.py +47 -0
- lang2sim/agent/__init__.py +5 -0
- lang2sim/agent/nemotron_client.py +109 -0
- lang2sim/agent/react_loop.py +848 -0
- lang2sim/agent/tools.py +998 -0
- lang2sim/cli/app.py +183 -0
- lang2sim/cli/screens/__init__.py +8 -0
- lang2sim/cli/screens/command_palette_modal.py +146 -0
- lang2sim/cli/screens/landing_screen.py +96 -0
- lang2sim/cli/screens/sessions_modal.py +236 -0
- lang2sim/cli/screens/workspace_screen.py +709 -0
- lang2sim/cli/widgets/__init__.py +16 -0
- lang2sim/cli/widgets/action_pill.py +67 -0
- lang2sim/cli/widgets/command_card.py +265 -0
- lang2sim/cli/widgets/plan_card.py +106 -0
- lang2sim/cli/widgets/prompt_card.py +96 -0
- lang2sim/cli/widgets/telemetry_sidebar.py +117 -0
- lang2sim/cli/widgets/thought_box.py +64 -0
- lang2sim/compiler/__init__.py +15 -0
- lang2sim/compiler/sdf_compiler.py +65 -0
- lang2sim/compiler/sir_generator.py +46 -0
- lang2sim/compiler/sir_validator.py +87 -0
- lang2sim/core/__init__.py +50 -0
- lang2sim/core/config.py +116 -0
- lang2sim/core/models.py +279 -0
- lang2sim/core/session_manager.py +189 -0
- lang2sim/perturbation/__init__.py +50 -0
- lang2sim/perturbation/_compat.py +19 -0
- lang2sim/perturbation/gazebo_bridge.py +520 -0
- lang2sim/perturbation/mcp_server.py +129 -0
- lang2sim/perturbation/routers/__init__.py +19 -0
- lang2sim/perturbation/routers/world.py +110 -0
- lang2sim/perturbation/sdf_snippets.py +112 -0
- lang2sim/runtime/__init__.py +7 -0
- lang2sim/runtime/control_api/__init__.py +7 -0
- lang2sim/runtime/control_api/client.py +187 -0
- lang2sim/runtime/control_api/server.py +504 -0
- lang2sim/runtime/display.py +141 -0
- lang2sim/runtime/docker_runner.py +286 -0
- lang2sim/runtime/doctor.py +220 -0
- lang2sim/runtime/hardware_probe.py +144 -0
- lang2sim/runtime/health_monitor.py +66 -0
- lang2sim/runtime/launch_builder.py +137 -0
- lang2sim/runtime/nav2_dispatcher.py +215 -0
- lang2sim/runtime/platform.py +137 -0
- lang2sim/runtime/ros_bridge.py +183 -0
- lang2sim/runtime/run_folder.py +86 -0
- lang2sim/runtime/self_healing.py +387 -0
- lang2sim/runtime/session.py +136 -0
- lang2sim/runtime/world_check.py +197 -0
- lang2sim/telemetry/__init__.py +85 -0
- lang2sim/telemetry/nqi_scorer.py +645 -0
- lang2sim/telemetry/ros_subscribers.py +489 -0
- lang2sim/telemetry/trajectory.py +258 -0
- lang2sim/telemetry/vision.py +477 -0
- lang2sim/world_gen/__init__.py +16 -0
- lang2sim/world_gen/catalog_loader.py +83 -0
- lang2sim/world_gen/map_slicer.py +131 -0
- lang2sim/world_gen/shapely_solver.py +256 -0
- lang2sim-2.0.0.dist-info/METADATA +53 -0
- lang2sim-2.0.0.dist-info/RECORD +65 -0
- lang2sim-2.0.0.dist-info/WHEEL +5 -0
- lang2sim-2.0.0.dist-info/entry_points.txt +2 -0
- lang2sim-2.0.0.dist-info/licenses/LICENSE +21 -0
- lang2sim-2.0.0.dist-info/top_level.txt +1 -0
lang2sim/core/models.py
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Canonical Simulation Specification Schemas and Control API Data Contracts.
|
|
3
|
+
File: lang2sim/core/models.py
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import math
|
|
7
|
+
from typing import List, Optional, Literal, Dict, Any
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# ---------------------------------------------------------------------------
|
|
12
|
+
# 1. Spatial and Kinematic Primitives
|
|
13
|
+
# ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
class Pose2D(BaseModel):
|
|
16
|
+
"""2D ground-plane coordinate pose with yaw orientation."""
|
|
17
|
+
x: float = Field(..., description="X coordinate in meters relative to world origin")
|
|
18
|
+
y: float = Field(..., description="Y coordinate in meters relative to world origin")
|
|
19
|
+
yaw: float = Field(0.0, ge=-math.pi, le=math.pi, description="Yaw orientation in radians, [-pi, pi]")
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def from_anchor(cls, anchor: Any) -> "Pose2D":
|
|
23
|
+
"""Convert either a list [x, y, yaw] or a dict {'x':..., 'y':..., 'yaw':...}."""
|
|
24
|
+
if isinstance(anchor, (list, tuple)):
|
|
25
|
+
yaw = float(anchor[2]) if len(anchor) > 2 else 0.0
|
|
26
|
+
return cls(x=float(anchor[0]), y=float(anchor[1]), yaw=yaw)
|
|
27
|
+
elif isinstance(anchor, dict):
|
|
28
|
+
return cls(**anchor)
|
|
29
|
+
elif isinstance(anchor, cls):
|
|
30
|
+
return anchor
|
|
31
|
+
raise ValueError(f"Cannot parse anchor of type {type(anchor)}: {anchor}")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Vector3D(BaseModel):
|
|
35
|
+
"""3D bounding dimensions in meters."""
|
|
36
|
+
x: float = Field(..., description="Bounding size X (length) in meters")
|
|
37
|
+
y: float = Field(..., description="Bounding size Y (width) in meters")
|
|
38
|
+
z: float = Field(..., description="Bounding size Z (height) in meters")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
# 2. Environmental & Weather Specifications
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
class SkyWeatherSpec(BaseModel):
|
|
46
|
+
"""Procedural atmospheric sky, sunlight angle, and volumetric fog controls."""
|
|
47
|
+
sky_enabled: bool = True
|
|
48
|
+
clouds_speed: float = Field(10.0, ge=0.0, le=50.0, description="Procedural cloud animation speed")
|
|
49
|
+
fog_enabled: bool = False
|
|
50
|
+
fog_density: float = Field(0.0, ge=0.0, le=0.5, description="Volumetric fog density (0.05 = light, 0.2 = heavy mist)")
|
|
51
|
+
sun_elevation: float = Field(0.8, ge=0.0, le=1.57, description="Solar elevation angle in radians (0.0 = sunset/dusk)")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class EnvironmentSpec(BaseModel):
|
|
55
|
+
"""Complete physical room and terrain parameters."""
|
|
56
|
+
environment_type: Literal[
|
|
57
|
+
"indoor_warehouse", "indoor_office", "outdoor_field", "outdoor_terrain"
|
|
58
|
+
] = "indoor_warehouse"
|
|
59
|
+
room_dimensions: Vector3D = Field(..., description="Outer room/field boundaries in meters")
|
|
60
|
+
wall_height: float = Field(2.5, ge=0.0, le=10.0, description="Wall boundary height (0.0 for open outdoor fields)")
|
|
61
|
+
ambient_light_luminance: float = Field(
|
|
62
|
+
1.0, ge=0.0, le=1.0, description="Ambient light factor (1.0 = nominal, 0.15 = degraded night)"
|
|
63
|
+
)
|
|
64
|
+
ground_terrain: Literal[
|
|
65
|
+
"concrete", "grass_soil", "dirt_mud", "asphalt_road", "uneven_heightmap", "tiles"
|
|
66
|
+
] = "concrete"
|
|
67
|
+
surface_friction: float = Field(
|
|
68
|
+
0.8, ge=0.05, le=1.5, description="Surface friction coefficient (0.8 = standard, 0.15 = slippery ice/mud)"
|
|
69
|
+
)
|
|
70
|
+
sky_weather: SkyWeatherSpec = Field(default_factory=SkyWeatherSpec)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
# 3. Object & Obstacle Specifications
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
class ObjectSpec(BaseModel):
|
|
78
|
+
"""Physical entity to be placed in the simulation world."""
|
|
79
|
+
id: str = Field(..., description="Unique alphanumeric identifier (e.g., 'rack_1')")
|
|
80
|
+
model_name: str = Field(..., description="Asset identifier matching catalog (e.g., 'aws_warehouse_rack')")
|
|
81
|
+
dimensions: Vector3D = Field(..., description="Bounding dimensions of the object in meters")
|
|
82
|
+
pose: Pose2D = Field(..., description="2D ground plane pose")
|
|
83
|
+
is_static: bool = Field(True, description="Static obstacle vs dynamically mutable entity")
|
|
84
|
+
semantic_parent_id: Optional[str] = Field(None, description="Anchor ID for grouped objects")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
# 4. Sensor Perturbations & Noise Models
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
class CameraPerturbation(BaseModel):
|
|
92
|
+
"""Optical degradation filters for on-board robot camera."""
|
|
93
|
+
enabled: bool = False
|
|
94
|
+
motion_blur_kernel: int = Field(0, ge=0, le=31, description="Blur kernel size (odd integer, 0 = off)")
|
|
95
|
+
brightness_attenuation: float = Field(1.0, ge=0.0, le=1.0, description="Illumination multiplier")
|
|
96
|
+
gaussian_noise_stddev: float = Field(0.0, ge=0.0, le=0.5, description="Additive noise standard deviation")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class LidarPerturbation(BaseModel):
|
|
100
|
+
"""LiDAR beam degradation filters."""
|
|
101
|
+
enabled: bool = False
|
|
102
|
+
dropout_probability: float = Field(0.0, ge=0.0, le=0.9, description="Random beam dropout rate")
|
|
103
|
+
range_noise_stddev: float = Field(0.0, ge=0.0, le=0.2, description="Gaussian noise standard deviation in meters")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class SensorNoiseSpec(BaseModel):
|
|
107
|
+
"""Aggregated sensor degradation models."""
|
|
108
|
+
camera: CameraPerturbation = Field(default_factory=CameraPerturbation)
|
|
109
|
+
lidar: LidarPerturbation = Field(default_factory=LidarPerturbation)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# ---------------------------------------------------------------------------
|
|
113
|
+
# 5. Dynamic Perturbations & Adversarial Triggers
|
|
114
|
+
# ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
class DynamicObstacleTrigger(BaseModel):
|
|
117
|
+
"""Runtime perturbation trigger dispatched via FastMCP."""
|
|
118
|
+
trigger_type: Literal["time", "waypoint_distance", "immediate"]
|
|
119
|
+
trigger_value: float = Field(..., description="Seconds from start or distance in meters to target")
|
|
120
|
+
model_name: str = Field("wooden_crate", description="Asset identifier to spawn dynamically")
|
|
121
|
+
spawn_pose: Pose2D = Field(..., description="Spawn coordinate in world reference frame")
|
|
122
|
+
velocity: Optional[Pose2D] = Field(None, description="Linear and angular velocity vector")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
# 6. Mission & Simulation Specifications
|
|
127
|
+
# ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
class MissionSpec(BaseModel):
|
|
130
|
+
"""Navigation task definition dispatched via Nav2 Simple Commander."""
|
|
131
|
+
initial_pose: Pose2D = Field(..., description="Robot starting coordinate and heading")
|
|
132
|
+
waypoints: List[Pose2D] = Field(default_factory=list, description="Intermediate navigation waypoints")
|
|
133
|
+
goal_pose: Pose2D = Field(..., description="Final navigation goal pose")
|
|
134
|
+
timeout_seconds: float = Field(120.0, ge=10.0, le=600.0, description="Maximum operational allowance")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class SimSpec(BaseModel):
|
|
138
|
+
"""Canonical top-level specification emitted by the supervisor agent."""
|
|
139
|
+
spec_version: str = "2.0.0"
|
|
140
|
+
environment: EnvironmentSpec
|
|
141
|
+
objects: List[ObjectSpec] = Field(default_factory=list)
|
|
142
|
+
robot_model: Literal["turtlebot3_waffle", "turtlebot4", "ridgeback"] = "turtlebot3_waffle"
|
|
143
|
+
sensor_noise: SensorNoiseSpec = Field(default_factory=SensorNoiseSpec)
|
|
144
|
+
adversarial_triggers: List[DynamicObstacleTrigger] = Field(default_factory=list)
|
|
145
|
+
mission: MissionSpec
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
# 7. Simulation Intermediate Representation (SIR)
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
class AisleCorridor(BaseModel):
|
|
153
|
+
"""Validated navigation corridor between obstacles."""
|
|
154
|
+
corridor_id: str
|
|
155
|
+
width: float = Field(..., ge=2.2, description="Aisle width in meters (must be >= 2.2m)")
|
|
156
|
+
start_point: Pose2D
|
|
157
|
+
end_point: Pose2D
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class SimulationIR(BaseModel):
|
|
161
|
+
"""Validated intermediate representation graph linking entities and corridors."""
|
|
162
|
+
spec: SimSpec
|
|
163
|
+
world_sdf_path: Optional[str] = None
|
|
164
|
+
map_yaml_path: Optional[str] = None
|
|
165
|
+
map_pgm_path: Optional[str] = None
|
|
166
|
+
resolved_anchors: Dict[str, List[float]] = Field(default_factory=dict)
|
|
167
|
+
aisle_corridors: List[AisleCorridor] = Field(default_factory=list)
|
|
168
|
+
is_valid: bool = False
|
|
169
|
+
validation_messages: List[str] = Field(default_factory=list)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# ---------------------------------------------------------------------------
|
|
173
|
+
# 8. Status Models (PR #7 Aligned)
|
|
174
|
+
# ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
class NavStatus(BaseModel):
|
|
177
|
+
"""Navigation task state streamed via WebSocket or GET /nav/status."""
|
|
178
|
+
is_active: bool = False
|
|
179
|
+
current_task: Optional[str] = None
|
|
180
|
+
distance_remaining: float = 0.0
|
|
181
|
+
target_pose: Optional[Pose2D] = None
|
|
182
|
+
status: Literal["idle", "navigating", "succeeded", "failed", "canceled"] = "idle"
|
|
183
|
+
waypoint_index: int = Field(0, description="0-based index of the waypoint being driven to")
|
|
184
|
+
waypoints_total: int = Field(0, description="Number of poses in the current task (1 for a single goal)")
|
|
185
|
+
navigation_time: float = Field(0.0, description="Sim seconds spent on the current leg")
|
|
186
|
+
eta_seconds: Optional[float] = Field(None, description="Nav2's estimated time remaining on this leg")
|
|
187
|
+
recoveries: int = Field(0, description="Nav2 recovery behaviours run on this leg")
|
|
188
|
+
message: Optional[str] = Field(None, description="Why a task failed or was canceled")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class SimStatus(BaseModel):
|
|
192
|
+
"""Simulation state monitor model (PR #7 and HealthMonitor aligned)."""
|
|
193
|
+
is_running: bool = False
|
|
194
|
+
sim_time: float = 0.0
|
|
195
|
+
real_time_factor: float = 1.0
|
|
196
|
+
robot_pose: Pose2D = Field(default_factory=lambda: Pose2D(x=0.0, y=0.0, yaw=0.0))
|
|
197
|
+
robot_velocity: Optional[Pose2D] = None
|
|
198
|
+
render_mode: Literal["gpu", "dri", "software", "headless"] = "software"
|
|
199
|
+
health_status: Literal["healthy", "degraded", "crashed"] = "healthy"
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# ---------------------------------------------------------------------------
|
|
203
|
+
# 9. Control API Contracts (Dev 3 Container Server & Dev 4)
|
|
204
|
+
# ---------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
class SimStartRequest(BaseModel):
|
|
207
|
+
"""Request payload sent to POST /sim/start."""
|
|
208
|
+
run_dir: str = Field(..., description="Absolute path to active run directory inside container")
|
|
209
|
+
world_sdf_path: str = Field(..., description="Path to world.sdf")
|
|
210
|
+
map_yaml_path: str = Field(..., description="Path to map.yaml")
|
|
211
|
+
robot_model: str = "turtlebot3_waffle"
|
|
212
|
+
initial_pose: Pose2D
|
|
213
|
+
use_gui: bool = True
|
|
214
|
+
use_rviz: bool = Field(True, description="Open RViz2 with the Nav2 panels (ignored without a display)")
|
|
215
|
+
world_name: Optional[str] = Field(None, description="World name; read from the SDF when omitted")
|
|
216
|
+
nav2_params_path: Optional[str] = Field(
|
|
217
|
+
None, description="Nav2 params file inside the container; default: TurtleBot3 Waffle params"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class RunManifest(BaseModel):
|
|
222
|
+
"""Contract for one run folder (decision 1): ~/.lang2sim/projects/<project>/runs/<timestamp>/manifest.json.
|
|
223
|
+
|
|
224
|
+
Paths are relative to the run folder. Part 1 writes the files, Part 3 only reads them.
|
|
225
|
+
"""
|
|
226
|
+
manifest_version: Literal["1.0"] = "1.0"
|
|
227
|
+
world_sdf: str = Field("world.sdf", description="Gazebo Harmonic world (SDF 1.9+), no robot inside")
|
|
228
|
+
world_name: Optional[str] = Field(None, description="<world name=...>; read from the SDF when omitted")
|
|
229
|
+
map_yaml: str = Field("map.yaml", description="Nav2 map whose frame equals the world frame")
|
|
230
|
+
spec_json: Optional[str] = Field("spec.json", description="The SimSpec this run was generated from")
|
|
231
|
+
robot_model: Literal["turtlebot3_waffle"] = Field(
|
|
232
|
+
"turtlebot3_waffle", description="Only TurtleBot3 Waffle is supported for the MVP (decision 4)"
|
|
233
|
+
)
|
|
234
|
+
initial_pose: Pose2D = Field(..., description="Robot spawn pose in the world/map frame")
|
|
235
|
+
nav2_params: Optional[str] = Field(None, description="Custom Nav2 params; default: TurtleBot3 Waffle params")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class SimStopRequest(BaseModel):
|
|
239
|
+
"""Request payload sent to POST /sim/stop."""
|
|
240
|
+
force: bool = False
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class SimStatusResponse(BaseModel):
|
|
244
|
+
"""Response payload returned by GET /sim/status."""
|
|
245
|
+
status: Literal["idle", "launching", "running", "degraded", "stopped", "error"]
|
|
246
|
+
rtf: float = Field(0.0, description="Real-Time Factor (Δsim/Δwall)")
|
|
247
|
+
sim_time: float = Field(0.0, description="Current simulation time in seconds")
|
|
248
|
+
active_nodes: List[str] = Field(default_factory=list)
|
|
249
|
+
active_topics: List[str] = Field(default_factory=list)
|
|
250
|
+
ready: bool = Field(False, description="All readiness checks pass: robot can be given goals")
|
|
251
|
+
readiness: Dict[str, bool] = Field(
|
|
252
|
+
default_factory=dict, description="gazebo_clock, odometry, laser_scan, nav2_active, localized"
|
|
253
|
+
)
|
|
254
|
+
world_report: List[str] = Field(default_factory=list, description="World pre-flight findings for this run")
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class NavInitialPoseRequest(BaseModel):
|
|
258
|
+
"""Request payload sent to POST /nav/initial_pose."""
|
|
259
|
+
pose: Pose2D
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class NavGoalRequest(BaseModel):
|
|
263
|
+
"""Request payload sent to POST /nav/goal."""
|
|
264
|
+
goal_pose: Pose2D
|
|
265
|
+
timeout_seconds: float = 120.0
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class NavWaypointsRequest(BaseModel):
|
|
269
|
+
"""Request payload sent to POST /nav/waypoints."""
|
|
270
|
+
waypoints: List[Pose2D]
|
|
271
|
+
timeout_seconds: float = 180.0
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class HealEvent(BaseModel):
|
|
275
|
+
"""Log record of self-healing error recovery."""
|
|
276
|
+
timestamp: float
|
|
277
|
+
error_signature: str
|
|
278
|
+
action_taken: str
|
|
279
|
+
success: bool
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Chat Session Memory & Persistence Manager
|
|
3
|
+
File: lang2sim/core/session_manager.py
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import time
|
|
8
|
+
import uuid
|
|
9
|
+
from datetime import datetime, date, timedelta
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import List, Dict, Any, Optional
|
|
12
|
+
from pydantic import BaseModel, Field
|
|
13
|
+
from .config import DEFAULT_LANG2SIM_ROOT
|
|
14
|
+
|
|
15
|
+
SESSIONS_DIR = DEFAULT_LANG2SIM_ROOT / "sessions"
|
|
16
|
+
SESSIONS_DIR.mkdir(parents=True, exist_ok=True)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ToolCallRecord(BaseModel):
|
|
20
|
+
"""Execution record of an agentic tool call."""
|
|
21
|
+
tool_name: str
|
|
22
|
+
arguments: Dict[str, Any] = Field(default_factory=dict)
|
|
23
|
+
duration_str: str = "0.0s"
|
|
24
|
+
status: str = "completed" # "running", "completed", "failed"
|
|
25
|
+
output: Optional[Any] = None
|
|
26
|
+
error: Optional[str] = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ArtifactRecord(BaseModel):
|
|
30
|
+
"""Artifact produced during the session (worlds, maps, benchmark reports)."""
|
|
31
|
+
type: str # "world_sdf", "map_pgm", "map_yaml", "benchmark_report"
|
|
32
|
+
path: str
|
|
33
|
+
created_at: float = Field(default_factory=time.time)
|
|
34
|
+
size_bytes: int = 0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class SimulationStateSnapshot(BaseModel):
|
|
38
|
+
"""Real-time simulation state snapshot."""
|
|
39
|
+
world_sdf: Optional[str] = None
|
|
40
|
+
map_yaml: Optional[str] = None
|
|
41
|
+
active_robot: str = "turtlebot3_waffle"
|
|
42
|
+
ate_drift_m: float = 0.0
|
|
43
|
+
status: str = "idle" # "idle", "sim_running", "navigating", "paused"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ChatMessage(BaseModel):
|
|
47
|
+
id: str = Field(default_factory=lambda: str(uuid.uuid4())[:8])
|
|
48
|
+
role: str # "user", "assistant", "system", "tool"
|
|
49
|
+
content: str = ""
|
|
50
|
+
timestamp: float = Field(default_factory=time.time)
|
|
51
|
+
tokens: int = 0
|
|
52
|
+
# Thinking & Cognitive Reasoning
|
|
53
|
+
thought: Optional[str] = None
|
|
54
|
+
thought_duration_str: Optional[str] = None
|
|
55
|
+
# Tool Invocation Actions
|
|
56
|
+
actions: List[ToolCallRecord] = Field(default_factory=list)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ChatSession(BaseModel):
|
|
60
|
+
id: str = Field(
|
|
61
|
+
default_factory=lambda: datetime.now().strftime("session_%Y%m%d_%H%M%S_") + str(uuid.uuid4())[:4]
|
|
62
|
+
)
|
|
63
|
+
title: str = "New Session"
|
|
64
|
+
created_at: float = Field(default_factory=time.time)
|
|
65
|
+
updated_at: float = Field(default_factory=time.time)
|
|
66
|
+
pinned: bool = False
|
|
67
|
+
messages: List[ChatMessage] = Field(default_factory=list)
|
|
68
|
+
total_tokens: int = 0
|
|
69
|
+
workspace_name: str = "active_project"
|
|
70
|
+
active_run_dir: Optional[str] = None
|
|
71
|
+
artifacts: List[ArtifactRecord] = Field(default_factory=list)
|
|
72
|
+
simulation_state: SimulationStateSnapshot = Field(default_factory=SimulationStateSnapshot)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class SessionManager:
|
|
76
|
+
"""Manages creation, persistence, retrieval, and grouping of chat sessions."""
|
|
77
|
+
|
|
78
|
+
def __init__(self, sessions_dir: Optional[Path] = None):
|
|
79
|
+
self.sessions_dir = sessions_dir or SESSIONS_DIR
|
|
80
|
+
self.sessions_dir.mkdir(parents=True, exist_ok=True)
|
|
81
|
+
|
|
82
|
+
def _file_path(self, session_id: str) -> Path:
|
|
83
|
+
return self.sessions_dir / f"{session_id}.json"
|
|
84
|
+
|
|
85
|
+
def create_session(
|
|
86
|
+
self,
|
|
87
|
+
title: str = "New Session",
|
|
88
|
+
workspace_name: str = "active_project",
|
|
89
|
+
system_prompt: Optional[str] = None,
|
|
90
|
+
) -> ChatSession:
|
|
91
|
+
session = ChatSession(title=title, workspace_name=workspace_name)
|
|
92
|
+
if system_prompt:
|
|
93
|
+
session.messages.append(ChatMessage(role="system", content=system_prompt))
|
|
94
|
+
self.save_session(session)
|
|
95
|
+
return session
|
|
96
|
+
|
|
97
|
+
def get_session(self, session_id: str) -> Optional[ChatSession]:
|
|
98
|
+
p = self._file_path(session_id)
|
|
99
|
+
if not p.is_file():
|
|
100
|
+
return None
|
|
101
|
+
try:
|
|
102
|
+
data = json.loads(p.read_text(encoding="utf-8"))
|
|
103
|
+
return ChatSession.model_validate(data)
|
|
104
|
+
except Exception:
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
def save_session(self, session: ChatSession) -> None:
|
|
108
|
+
session.updated_at = time.time()
|
|
109
|
+
p = self._file_path(session.id)
|
|
110
|
+
p.write_text(session.model_dump_json(indent=2), encoding="utf-8")
|
|
111
|
+
|
|
112
|
+
def delete_session(self, session_id: str) -> bool:
|
|
113
|
+
p = self._file_path(session_id)
|
|
114
|
+
if p.is_file():
|
|
115
|
+
try:
|
|
116
|
+
p.unlink()
|
|
117
|
+
return True
|
|
118
|
+
except OSError:
|
|
119
|
+
return False
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
def pin_session(self, session_id: str, pinned: Optional[bool] = None) -> bool:
|
|
123
|
+
session = self.get_session(session_id)
|
|
124
|
+
if not session:
|
|
125
|
+
return False
|
|
126
|
+
if pinned is None:
|
|
127
|
+
session.pinned = not session.pinned
|
|
128
|
+
else:
|
|
129
|
+
session.pinned = pinned
|
|
130
|
+
self.save_session(session)
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
def rename_session(self, session_id: str, new_title: str) -> bool:
|
|
134
|
+
session = self.get_session(session_id)
|
|
135
|
+
if not session:
|
|
136
|
+
return False
|
|
137
|
+
session.title = new_title.strip() or "Untitled Session"
|
|
138
|
+
self.save_session(session)
|
|
139
|
+
return True
|
|
140
|
+
|
|
141
|
+
def list_sessions(self, workspace_name: Optional[str] = None) -> List[ChatSession]:
|
|
142
|
+
"""Lists all stored sessions sorted by pinned first, then by updated_at descending."""
|
|
143
|
+
sessions: List[ChatSession] = []
|
|
144
|
+
for p in self.sessions_dir.glob("*.json"):
|
|
145
|
+
try:
|
|
146
|
+
data = json.loads(p.read_text(encoding="utf-8"))
|
|
147
|
+
s = ChatSession.model_validate(data)
|
|
148
|
+
if workspace_name is None or s.workspace_name == workspace_name:
|
|
149
|
+
sessions.append(s)
|
|
150
|
+
except Exception:
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
# Sort: pinned first (True > False), then updated_at descending
|
|
154
|
+
sessions.sort(key=lambda s: (not s.pinned, -s.updated_at))
|
|
155
|
+
return sessions
|
|
156
|
+
|
|
157
|
+
def group_sessions_by_date(
|
|
158
|
+
self, sessions: List[ChatSession]
|
|
159
|
+
) -> Dict[str, List[ChatSession]]:
|
|
160
|
+
"""
|
|
161
|
+
Groups sessions chronologically matching OpenCode:
|
|
162
|
+
- "Today"
|
|
163
|
+
- "Yesterday"
|
|
164
|
+
- "%a %b %d %Y" (e.g. "Sun Oct 25 2026", "Wed Oct 07 2026")
|
|
165
|
+
"""
|
|
166
|
+
today = date.today()
|
|
167
|
+
yesterday = today - timedelta(days=1)
|
|
168
|
+
grouped: Dict[str, List[ChatSession]] = {}
|
|
169
|
+
|
|
170
|
+
for s in sessions:
|
|
171
|
+
s_date = datetime.fromtimestamp(s.updated_at).date()
|
|
172
|
+
if s.pinned:
|
|
173
|
+
header = "Pinned"
|
|
174
|
+
elif s_date == today:
|
|
175
|
+
header = "Today"
|
|
176
|
+
elif s_date == yesterday:
|
|
177
|
+
header = "Yesterday"
|
|
178
|
+
else:
|
|
179
|
+
header = datetime.fromtimestamp(s.updated_at).strftime("%a %b %d %Y")
|
|
180
|
+
|
|
181
|
+
if header not in grouped:
|
|
182
|
+
grouped[header] = []
|
|
183
|
+
grouped[header].append(s)
|
|
184
|
+
|
|
185
|
+
return grouped
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# Global instance
|
|
189
|
+
session_manager = SessionManager()
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Perturbation and FastMCP package for Lang2Sim."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
# Backward-compatibility alias
|
|
6
|
+
sys.modules.setdefault("sim2lang.perturbation", sys.modules[__name__])
|
|
7
|
+
|
|
8
|
+
from lang2sim.perturbation.gazebo_bridge import (
|
|
9
|
+
BridgeResult,
|
|
10
|
+
GazeboBridge,
|
|
11
|
+
DryRunBridge,
|
|
12
|
+
HarmonicBridge,
|
|
13
|
+
ClassicBridge,
|
|
14
|
+
get_bridge,
|
|
15
|
+
reset_bridge_instance,
|
|
16
|
+
)
|
|
17
|
+
from lang2sim.perturbation.mcp_server import (
|
|
18
|
+
mcp,
|
|
19
|
+
call_tool_inprocess,
|
|
20
|
+
spawn_dynamic_entity,
|
|
21
|
+
set_ambient_lighting,
|
|
22
|
+
reset_simulation,
|
|
23
|
+
apply_chassis_perturbation,
|
|
24
|
+
bridge_health,
|
|
25
|
+
list_spawned_entities,
|
|
26
|
+
)
|
|
27
|
+
from lang2sim.perturbation.routers.world import (
|
|
28
|
+
router as world_router,
|
|
29
|
+
mount_world_router,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"BridgeResult",
|
|
34
|
+
"GazeboBridge",
|
|
35
|
+
"DryRunBridge",
|
|
36
|
+
"HarmonicBridge",
|
|
37
|
+
"ClassicBridge",
|
|
38
|
+
"get_bridge",
|
|
39
|
+
"reset_bridge_instance",
|
|
40
|
+
"mcp",
|
|
41
|
+
"call_tool_inprocess",
|
|
42
|
+
"spawn_dynamic_entity",
|
|
43
|
+
"set_ambient_lighting",
|
|
44
|
+
"reset_simulation",
|
|
45
|
+
"apply_chassis_perturbation",
|
|
46
|
+
"bridge_health",
|
|
47
|
+
"list_spawned_entities",
|
|
48
|
+
"world_router",
|
|
49
|
+
"mount_world_router",
|
|
50
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Compatibility shim for Pose2D schema until Day-0 core models are merged."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from lang2sim.core.models import Pose2D
|
|
7
|
+
except ImportError:
|
|
8
|
+
try:
|
|
9
|
+
from lang2sim.core.models import Pose2D
|
|
10
|
+
except ImportError:
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
class Pose2D(BaseModel):
|
|
14
|
+
"""2D ground pose (x, y, yaw)."""
|
|
15
|
+
x: float = Field(..., description="X coordinate in meters")
|
|
16
|
+
y: float = Field(..., description="Y coordinate in meters")
|
|
17
|
+
yaw: float = Field(0.0, ge=-3.14159, le=3.14159, description="Heading in radians")
|
|
18
|
+
|
|
19
|
+
__all__ = ["Pose2D"]
|