slashml-robotics 1.0.0__tar.gz
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.
- slashml_robotics-1.0.0/PKG-INFO +65 -0
- slashml_robotics-1.0.0/README.md +52 -0
- slashml_robotics-1.0.0/pyproject.toml +27 -0
- slashml_robotics-1.0.0/setup.cfg +4 -0
- slashml_robotics-1.0.0/slashml_robotics/__init__.py +44 -0
- slashml_robotics-1.0.0/slashml_robotics/client.py +369 -0
- slashml_robotics-1.0.0/slashml_robotics/mcp/__init__.py +5 -0
- slashml_robotics-1.0.0/slashml_robotics/mcp/server.py +205 -0
- slashml_robotics-1.0.0/slashml_robotics/ros2/__init__.py +5 -0
- slashml_robotics-1.0.0/slashml_robotics/ros2/bridge_node.py +123 -0
- slashml_robotics-1.0.0/slashml_robotics/types.py +247 -0
- slashml_robotics-1.0.0/slashml_robotics/vocabulary.py +173 -0
- slashml_robotics-1.0.0/slashml_robotics.egg-info/PKG-INFO +65 -0
- slashml_robotics-1.0.0/slashml_robotics.egg-info/SOURCES.txt +17 -0
- slashml_robotics-1.0.0/slashml_robotics.egg-info/dependency_links.txt +1 -0
- slashml_robotics-1.0.0/slashml_robotics.egg-info/requires.txt +2 -0
- slashml_robotics-1.0.0/slashml_robotics.egg-info/top_level.txt +1 -0
- slashml_robotics-1.0.0/tests/test_bridge_and_mcp.py +98 -0
- slashml_robotics-1.0.0/tests/test_robotics_client.py +227 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: slashml-robotics
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Robotics & Spatial Memory SDK for SlashML
|
|
5
|
+
Author: SlashML Robotics Inc.
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://slashml.ai
|
|
8
|
+
Project-URL: Repository, https://github.com/slashml-ai/slashml-robotics
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: slashml-memory>=1.0.9
|
|
12
|
+
Requires-Dist: numpy>=1.24.0
|
|
13
|
+
|
|
14
|
+
# SlashML Robotics Python SDK
|
|
15
|
+
|
|
16
|
+
Ultra-low-latency spatial memory and dynamic belief tracking for embodied AI, mobile manipulation, and autonomous robots.
|
|
17
|
+
|
|
18
|
+
## Overview
|
|
19
|
+
|
|
20
|
+
`slashml-robotics` provides a sub-millisecond, zero-recomputation spatial memory substrate for robotics perception pipelines and mission planners:
|
|
21
|
+
- **O(1) Dynamic Spatial Supersession**: Updates object locations instantly and marks obsolete historical coordinates as superseded without full-database reindexing.
|
|
22
|
+
- **Sub-Microsecond `where_is`**: Query active 6-DoF poses and room zones with sub-microsecond retrieval latency.
|
|
23
|
+
- **Chronological Trajectory Reconstruction**: Full movement history tracking across workspaces and rooms.
|
|
24
|
+
- **Physical Affordance Search**: Discover tools and objects based on functional capabilities (e.g. `fastening`, `measuring`, `stripping`).
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install slashml-robotics
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Quickstart
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from slashml_robotics import RoboticMemory, Pose6D
|
|
36
|
+
|
|
37
|
+
# Initialize client connected to SlashML engine daemon or cloud endpoint
|
|
38
|
+
memory = RoboticMemory(base_url="http://localhost:8000", api_key="sk_...")
|
|
39
|
+
|
|
40
|
+
# 1. Ingest camera / perception detection
|
|
41
|
+
memory.add_observation(
|
|
42
|
+
object_name="torque_wrench",
|
|
43
|
+
room_zone="machining_bay",
|
|
44
|
+
pose=Pose6D(x=1.24, y=-0.52, z=0.85, qx=0.0, qy=0.0, qz=0.0, qw=1.0),
|
|
45
|
+
tags=["tool", "hardware"],
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# 2. Query active location
|
|
49
|
+
loc = memory.where_is("torque_wrench")
|
|
50
|
+
if loc:
|
|
51
|
+
print(f"Found {loc.object_name} in {loc.room_zone} at ({loc.pose.x}, {loc.pose.y}, {loc.pose.z})")
|
|
52
|
+
|
|
53
|
+
# 3. Retrieve movement trajectory
|
|
54
|
+
trajectory = memory.get_trajectory("torque_wrench")
|
|
55
|
+
print(f"Total waypoints recorded: {trajectory.total_waypoints}")
|
|
56
|
+
|
|
57
|
+
# 4. Search by physical affordance
|
|
58
|
+
tools = memory.find_by_affordance("fastening", room_zone="machining_bay")
|
|
59
|
+
for t in tools:
|
|
60
|
+
print(f"Available fastening tool: {t.object_name}")
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
Apache-2.0
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# SlashML Robotics Python SDK
|
|
2
|
+
|
|
3
|
+
Ultra-low-latency spatial memory and dynamic belief tracking for embodied AI, mobile manipulation, and autonomous robots.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
`slashml-robotics` provides a sub-millisecond, zero-recomputation spatial memory substrate for robotics perception pipelines and mission planners:
|
|
8
|
+
- **O(1) Dynamic Spatial Supersession**: Updates object locations instantly and marks obsolete historical coordinates as superseded without full-database reindexing.
|
|
9
|
+
- **Sub-Microsecond `where_is`**: Query active 6-DoF poses and room zones with sub-microsecond retrieval latency.
|
|
10
|
+
- **Chronological Trajectory Reconstruction**: Full movement history tracking across workspaces and rooms.
|
|
11
|
+
- **Physical Affordance Search**: Discover tools and objects based on functional capabilities (e.g. `fastening`, `measuring`, `stripping`).
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install slashml-robotics
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quickstart
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from slashml_robotics import RoboticMemory, Pose6D
|
|
23
|
+
|
|
24
|
+
# Initialize client connected to SlashML engine daemon or cloud endpoint
|
|
25
|
+
memory = RoboticMemory(base_url="http://localhost:8000", api_key="sk_...")
|
|
26
|
+
|
|
27
|
+
# 1. Ingest camera / perception detection
|
|
28
|
+
memory.add_observation(
|
|
29
|
+
object_name="torque_wrench",
|
|
30
|
+
room_zone="machining_bay",
|
|
31
|
+
pose=Pose6D(x=1.24, y=-0.52, z=0.85, qx=0.0, qy=0.0, qz=0.0, qw=1.0),
|
|
32
|
+
tags=["tool", "hardware"],
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# 2. Query active location
|
|
36
|
+
loc = memory.where_is("torque_wrench")
|
|
37
|
+
if loc:
|
|
38
|
+
print(f"Found {loc.object_name} in {loc.room_zone} at ({loc.pose.x}, {loc.pose.y}, {loc.pose.z})")
|
|
39
|
+
|
|
40
|
+
# 3. Retrieve movement trajectory
|
|
41
|
+
trajectory = memory.get_trajectory("torque_wrench")
|
|
42
|
+
print(f"Total waypoints recorded: {trajectory.total_waypoints}")
|
|
43
|
+
|
|
44
|
+
# 4. Search by physical affordance
|
|
45
|
+
tools = memory.find_by_affordance("fastening", room_zone="machining_bay")
|
|
46
|
+
for t in tools:
|
|
47
|
+
print(f"Available fastening tool: {t.object_name}")
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## License
|
|
51
|
+
|
|
52
|
+
Apache-2.0
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "slashml-robotics"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Official Robotics & Spatial Memory SDK for SlashML"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
authors = [{name = "SlashML Robotics Inc."}]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"slashml-memory>=1.0.9",
|
|
15
|
+
"numpy>=1.24.0",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Homepage = "https://slashml.ai"
|
|
20
|
+
Repository = "https://github.com/slashml-ai/slashml-robotics"
|
|
21
|
+
|
|
22
|
+
[tool.setuptools]
|
|
23
|
+
packages = [
|
|
24
|
+
"slashml_robotics",
|
|
25
|
+
"slashml_robotics.ros2",
|
|
26
|
+
"slashml_robotics.mcp",
|
|
27
|
+
]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""SlashML Robotics: Ultra-low-latency spatial memory SDK for embodied AI.
|
|
2
|
+
|
|
3
|
+
Public interface for physical robotics stacks, perception pipelines, and task planners.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from slashml_robotics.client import RoboticMemory
|
|
7
|
+
from slashml_robotics.types import (
|
|
8
|
+
BoundingBox3D,
|
|
9
|
+
LocationResult,
|
|
10
|
+
PhysicalObservation,
|
|
11
|
+
Pose6D,
|
|
12
|
+
Trajectory,
|
|
13
|
+
Waypoint,
|
|
14
|
+
)
|
|
15
|
+
from slashml_robotics.vocabulary import (
|
|
16
|
+
AFFORDANCES_AND_STATES,
|
|
17
|
+
AFFORDANCE_TO_OBJECTS,
|
|
18
|
+
CONTAINERS_AND_STORAGE,
|
|
19
|
+
ELECTRONICS_AND_COMPONENTS,
|
|
20
|
+
TOOLS_AND_HARDWARE,
|
|
21
|
+
ZONES_AND_FURNITURE,
|
|
22
|
+
get_objects_for_affordance,
|
|
23
|
+
is_known_tool,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__version__ = "1.0.0"
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"RoboticMemory",
|
|
30
|
+
"Pose6D",
|
|
31
|
+
"BoundingBox3D",
|
|
32
|
+
"Waypoint",
|
|
33
|
+
"LocationResult",
|
|
34
|
+
"Trajectory",
|
|
35
|
+
"PhysicalObservation",
|
|
36
|
+
"get_objects_for_affordance",
|
|
37
|
+
"is_known_tool",
|
|
38
|
+
"TOOLS_AND_HARDWARE",
|
|
39
|
+
"CONTAINERS_AND_STORAGE",
|
|
40
|
+
"ELECTRONICS_AND_COMPONENTS",
|
|
41
|
+
"ZONES_AND_FURNITURE",
|
|
42
|
+
"AFFORDANCES_AND_STATES",
|
|
43
|
+
"AFFORDANCE_TO_OBJECTS",
|
|
44
|
+
]
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"""High-level client for SlashML Robotics spatial memory.
|
|
2
|
+
|
|
3
|
+
Wraps the underlying SlashML transport to deliver sub-millisecond, zero-recomputation
|
|
4
|
+
spatial memory operations:
|
|
5
|
+
- O(1) dynamic spatial supersession
|
|
6
|
+
- Sub-microsecond where_is lookup
|
|
7
|
+
- Chronological trajectory reconstruction
|
|
8
|
+
- Affordance-based object discovery
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import time
|
|
14
|
+
import urllib.parse
|
|
15
|
+
from typing import Any, Dict, List, Optional, Sequence, Union
|
|
16
|
+
|
|
17
|
+
from slashml import MemoryClient, SlashMLMemory
|
|
18
|
+
from slashml_robotics.types import (
|
|
19
|
+
BoundingBox3D,
|
|
20
|
+
LocationResult,
|
|
21
|
+
PhysicalObservation,
|
|
22
|
+
Pose6D,
|
|
23
|
+
Trajectory,
|
|
24
|
+
Waypoint,
|
|
25
|
+
)
|
|
26
|
+
from slashml_robotics.vocabulary import get_objects_for_affordance
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RoboticMemory:
|
|
30
|
+
"""Specialized robotics memory interface built on top of SlashML.
|
|
31
|
+
|
|
32
|
+
Provides physical spatial semantics, 6-DoF poses, automatic spatial supersession,
|
|
33
|
+
and trajectory queries with sub-millisecond retrieval latency.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
client: Optional[Union[MemoryClient, SlashMLMemory]] = None,
|
|
39
|
+
base_url: Optional[str] = None,
|
|
40
|
+
api_key: Optional[str] = None,
|
|
41
|
+
robot_id: str = "robot_default",
|
|
42
|
+
) -> None:
|
|
43
|
+
if client is not None:
|
|
44
|
+
self._client = client
|
|
45
|
+
else:
|
|
46
|
+
self._client = MemoryClient(base_url=base_url, api_key=api_key)
|
|
47
|
+
self.robot_id = robot_id
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def client(self) -> Union[MemoryClient, SlashMLMemory]:
|
|
51
|
+
return self._client
|
|
52
|
+
|
|
53
|
+
def add_observation(
|
|
54
|
+
self,
|
|
55
|
+
object_name: str,
|
|
56
|
+
room_zone: str,
|
|
57
|
+
pose: Union[Pose6D, Sequence[float]],
|
|
58
|
+
tags: Optional[List[str]] = None,
|
|
59
|
+
confidence: float = 1.0,
|
|
60
|
+
timestamp: Optional[float] = None,
|
|
61
|
+
bounding_box: Optional[BoundingBox3D] = None,
|
|
62
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
63
|
+
) -> LocationResult:
|
|
64
|
+
"""Record a physical detection/observation into spatial memory.
|
|
65
|
+
|
|
66
|
+
Automatically updates active belief and marks older locations of this
|
|
67
|
+
exact object as superseded in O(1) time.
|
|
68
|
+
"""
|
|
69
|
+
pose_obj = pose if isinstance(pose, Pose6D) else Pose6D.from_list(pose)
|
|
70
|
+
ts = timestamp if timestamp is not None else time.time()
|
|
71
|
+
tag_list = list(tags) if tags is not None else []
|
|
72
|
+
meta = dict(metadata) if metadata is not None else {}
|
|
73
|
+
|
|
74
|
+
# Canonical memory payload for SlashML engine
|
|
75
|
+
payload: Dict[str, Any] = {
|
|
76
|
+
"object_name": object_name,
|
|
77
|
+
"room_zone": room_zone,
|
|
78
|
+
"pose": pose_obj.to_dict(),
|
|
79
|
+
"confidence": confidence,
|
|
80
|
+
"timestamp": ts,
|
|
81
|
+
"robot_id": self.robot_id,
|
|
82
|
+
"spatial_memory_type": "physical_observation",
|
|
83
|
+
**meta,
|
|
84
|
+
}
|
|
85
|
+
if bounding_box:
|
|
86
|
+
payload["bounding_box"] = bounding_box.to_dict()
|
|
87
|
+
|
|
88
|
+
# Canonical normalized key for object identity (normalize spaces and underscores)
|
|
89
|
+
norm_name = object_name.lower().strip().replace(" ", "_")
|
|
90
|
+
all_tags = [f"obj:{norm_name}", f"obj:{object_name.lower().strip()}"]
|
|
91
|
+
for t in tag_list:
|
|
92
|
+
clean_t = t.lower().strip()
|
|
93
|
+
if not clean_t.startswith("obj:"):
|
|
94
|
+
all_tags.append(clean_t)
|
|
95
|
+
|
|
96
|
+
# Try native live /v1/robotics/observations endpoint first
|
|
97
|
+
if hasattr(self._client, "_request"):
|
|
98
|
+
try:
|
|
99
|
+
res_native = self._client._request(
|
|
100
|
+
"POST",
|
|
101
|
+
"/v1/robotics/observations",
|
|
102
|
+
payload={
|
|
103
|
+
"object_name": object_name,
|
|
104
|
+
"room_zone": room_zone,
|
|
105
|
+
"pose": pose_obj.to_dict(),
|
|
106
|
+
"tags": all_tags,
|
|
107
|
+
"confidence": confidence,
|
|
108
|
+
"timestamp": ts,
|
|
109
|
+
"bounding_box": bounding_box.to_dict() if bounding_box else None,
|
|
110
|
+
"metadata": meta,
|
|
111
|
+
},
|
|
112
|
+
)
|
|
113
|
+
if isinstance(res_native, dict) and res_native.get("status") == "recorded":
|
|
114
|
+
p_data = res_native.get("pose", pose_obj.to_dict())
|
|
115
|
+
p_obj = Pose6D(**p_data) if isinstance(p_data, dict) else Pose6D.from_list(p_data)
|
|
116
|
+
return LocationResult(
|
|
117
|
+
object_name=str(res_native.get("object_name", object_name)),
|
|
118
|
+
room_zone=str(res_native.get("room_zone", room_zone)),
|
|
119
|
+
pose=p_obj,
|
|
120
|
+
timestamp=float(res_native.get("timestamp", ts)),
|
|
121
|
+
confidence=float(res_native.get("confidence", confidence)),
|
|
122
|
+
is_superseded=bool(res_native.get("is_superseded", False)),
|
|
123
|
+
observation_id=str(res_native.get("observation_id", "")),
|
|
124
|
+
metadata=meta,
|
|
125
|
+
)
|
|
126
|
+
except Exception:
|
|
127
|
+
pass
|
|
128
|
+
|
|
129
|
+
# Fallback to general memory add()
|
|
130
|
+
obs_text = f"Observed {object_name} in {room_zone} at position ({pose_obj.x:.3f}, {pose_obj.y:.3f}, {pose_obj.z:.3f})"
|
|
131
|
+
|
|
132
|
+
# Use add() with structured metadata
|
|
133
|
+
res = self._client.add(
|
|
134
|
+
messages=[{"role": "user", "content": obs_text}],
|
|
135
|
+
user_id=self.robot_id,
|
|
136
|
+
metadata=payload,
|
|
137
|
+
filters={"tags": all_tags},
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
obs_id = ""
|
|
141
|
+
if isinstance(res, dict):
|
|
142
|
+
obs_id = str(res.get("id") or res.get("record_id") or "")
|
|
143
|
+
elif isinstance(res, list) and len(res) > 0 and isinstance(res[0], dict):
|
|
144
|
+
obs_id = str(res[0].get("id") or res[0].get("record_id") or "")
|
|
145
|
+
|
|
146
|
+
return LocationResult(
|
|
147
|
+
object_name=object_name,
|
|
148
|
+
room_zone=room_zone,
|
|
149
|
+
pose=pose_obj,
|
|
150
|
+
timestamp=ts,
|
|
151
|
+
confidence=confidence,
|
|
152
|
+
is_superseded=False,
|
|
153
|
+
observation_id=obs_id,
|
|
154
|
+
metadata=meta,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def where_is(self, object_name: str) -> Optional[LocationResult]:
|
|
158
|
+
"""Sub-microsecond query returning the latest active 6-DoF location of an object.
|
|
159
|
+
|
|
160
|
+
Returns None if the object has never been observed or has been removed.
|
|
161
|
+
"""
|
|
162
|
+
# Try native live /v1/robotics/where_is/{object_name} endpoint first
|
|
163
|
+
if hasattr(self._client, "_request"):
|
|
164
|
+
try:
|
|
165
|
+
encoded_name = urllib.parse.quote(object_name, safe="")
|
|
166
|
+
res_native = self._client._request("GET", f"/v1/robotics/where_is/{encoded_name}")
|
|
167
|
+
if isinstance(res_native, dict):
|
|
168
|
+
p_data = res_native.get("pose", [0.0, 0.0, 0.0])
|
|
169
|
+
p_obj = Pose6D(**p_data) if isinstance(p_data, dict) else Pose6D.from_list(p_data)
|
|
170
|
+
return LocationResult(
|
|
171
|
+
object_name=str(res_native.get("object_name", object_name)),
|
|
172
|
+
room_zone=str(res_native.get("room_zone", "unknown")),
|
|
173
|
+
pose=p_obj,
|
|
174
|
+
timestamp=float(res_native.get("timestamp", 0.0)),
|
|
175
|
+
confidence=float(res_native.get("confidence", 1.0)),
|
|
176
|
+
is_superseded=bool(res_native.get("is_superseded", False)),
|
|
177
|
+
observation_id=str(res_native.get("observation_id", "")),
|
|
178
|
+
metadata={},
|
|
179
|
+
)
|
|
180
|
+
except Exception as e:
|
|
181
|
+
status_code = getattr(e, "status_code", None)
|
|
182
|
+
if status_code == 404 or "404" in str(e):
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
obj_key = object_name.lower().strip()
|
|
186
|
+
obj_key_norm = obj_key.replace(" ", "_")
|
|
187
|
+
|
|
188
|
+
# Query engine with object tag filter, requesting active un-superseded records
|
|
189
|
+
try:
|
|
190
|
+
memories = self._client.get_all(
|
|
191
|
+
user_id=self.robot_id,
|
|
192
|
+
filters={"tags": [f"obj:{obj_key}", f"obj:{obj_key_norm}"], "is_superseded": False},
|
|
193
|
+
limit=1,
|
|
194
|
+
)
|
|
195
|
+
except Exception:
|
|
196
|
+
# Fallback query if filtering is unsupported by mock transport
|
|
197
|
+
memories = self._client.search(
|
|
198
|
+
query=f"where is {object_name}",
|
|
199
|
+
user_id=self.robot_id,
|
|
200
|
+
limit=1,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
if not memories:
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
# Extract record
|
|
207
|
+
item = memories[0] if isinstance(memories, list) else memories
|
|
208
|
+
meta = item.get("metadata", {}) if isinstance(item, dict) else {}
|
|
209
|
+
|
|
210
|
+
pose_data = meta.get("pose", [0.0, 0.0, 0.0])
|
|
211
|
+
pose_obj = Pose6D.from_list(pose_data) if isinstance(pose_data, (list, tuple)) else Pose6D(**pose_data)
|
|
212
|
+
|
|
213
|
+
return LocationResult(
|
|
214
|
+
object_name=meta.get("object_name", object_name),
|
|
215
|
+
room_zone=meta.get("room_zone", "unknown"),
|
|
216
|
+
pose=pose_obj,
|
|
217
|
+
timestamp=float(meta.get("timestamp", item.get("created_at_epoch", 0.0))),
|
|
218
|
+
confidence=float(meta.get("confidence", 1.0)),
|
|
219
|
+
is_superseded=bool(meta.get("is_superseded", False)),
|
|
220
|
+
observation_id=str(item.get("id", "")),
|
|
221
|
+
metadata={k: v for k, v in meta.items() if k not in ("pose", "room_zone", "object_name")},
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
def get_trajectory(self, object_name: str) -> Trajectory:
|
|
225
|
+
"""Reconstruct chronological waypoint trajectory for an object across all time."""
|
|
226
|
+
# Try native live /v1/robotics/trajectory/{object_name} endpoint first
|
|
227
|
+
if hasattr(self._client, "_request"):
|
|
228
|
+
try:
|
|
229
|
+
encoded_name = urllib.parse.quote(object_name, safe="")
|
|
230
|
+
res_native = self._client._request("GET", f"/v1/robotics/trajectory/{encoded_name}")
|
|
231
|
+
if isinstance(res_native, dict) and "waypoints" in res_native:
|
|
232
|
+
wps = []
|
|
233
|
+
for wp in res_native.get("waypoints", []):
|
|
234
|
+
p_data = wp.get("pose", [0.0, 0.0, 0.0])
|
|
235
|
+
p_obj = Pose6D(**p_data) if isinstance(p_data, dict) else Pose6D.from_list(p_data)
|
|
236
|
+
wps.append(Waypoint(
|
|
237
|
+
timestamp=float(wp.get("timestamp", 0.0)),
|
|
238
|
+
room_zone=str(wp.get("room_zone", "unknown")),
|
|
239
|
+
pose=p_obj,
|
|
240
|
+
confidence=float(wp.get("confidence", 1.0)),
|
|
241
|
+
metadata={},
|
|
242
|
+
))
|
|
243
|
+
return Trajectory(object_name=object_name, waypoints=wps)
|
|
244
|
+
except Exception:
|
|
245
|
+
pass
|
|
246
|
+
|
|
247
|
+
obj_key = object_name.lower().strip()
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
records = self._client.get_all(
|
|
251
|
+
user_id=self.robot_id,
|
|
252
|
+
filters={"tags": [f"obj:{obj_key}"]},
|
|
253
|
+
limit=1000,
|
|
254
|
+
)
|
|
255
|
+
except Exception:
|
|
256
|
+
records = self._client.search(
|
|
257
|
+
query=f"trajectory history of {object_name}",
|
|
258
|
+
user_id=self.robot_id,
|
|
259
|
+
limit=100,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
if not isinstance(records, list):
|
|
263
|
+
records = [records] if records else []
|
|
264
|
+
|
|
265
|
+
waypoints: List[Waypoint] = []
|
|
266
|
+
for item in records:
|
|
267
|
+
if not isinstance(item, dict):
|
|
268
|
+
continue
|
|
269
|
+
meta = item.get("metadata", {})
|
|
270
|
+
if meta.get("object_name", "").lower().strip() != obj_key:
|
|
271
|
+
continue
|
|
272
|
+
pose_data = meta.get("pose", [0.0, 0.0, 0.0])
|
|
273
|
+
pose_obj = Pose6D.from_list(pose_data) if isinstance(pose_data, (list, tuple)) else Pose6D(**pose_data)
|
|
274
|
+
wp = Waypoint(
|
|
275
|
+
timestamp=float(meta.get("timestamp", 0.0)),
|
|
276
|
+
room_zone=str(meta.get("room_zone", "unknown")),
|
|
277
|
+
pose=pose_obj,
|
|
278
|
+
confidence=float(meta.get("confidence", 1.0)),
|
|
279
|
+
metadata={k: v for k, v in meta.items() if k not in ("pose", "room_zone", "object_name")},
|
|
280
|
+
)
|
|
281
|
+
waypoints.append(wp)
|
|
282
|
+
|
|
283
|
+
# Sort chronologically
|
|
284
|
+
waypoints.sort(key=lambda w: w.timestamp)
|
|
285
|
+
return Trajectory(object_name=object_name, waypoints=waypoints)
|
|
286
|
+
|
|
287
|
+
def search_spatial(
|
|
288
|
+
self,
|
|
289
|
+
query: str,
|
|
290
|
+
room_zone: Optional[str] = None,
|
|
291
|
+
top_k: int = 5,
|
|
292
|
+
) -> List[LocationResult]:
|
|
293
|
+
"""Perform semantic/visual similarity search filtered by spatial context."""
|
|
294
|
+
# Try native live /v1/robotics/search endpoint first
|
|
295
|
+
if hasattr(self._client, "_request"):
|
|
296
|
+
try:
|
|
297
|
+
res_native = self._client._request(
|
|
298
|
+
"POST",
|
|
299
|
+
"/v1/robotics/search",
|
|
300
|
+
payload={"query": query, "room_zone": room_zone, "top_k": top_k},
|
|
301
|
+
)
|
|
302
|
+
if isinstance(res_native, dict) and "results" in res_native:
|
|
303
|
+
out: List[LocationResult] = []
|
|
304
|
+
for r in res_native.get("results", []):
|
|
305
|
+
p_data = r.get("pose", [0.0, 0.0, 0.0])
|
|
306
|
+
p_obj = Pose6D(**p_data) if isinstance(p_data, dict) else Pose6D.from_list(p_data)
|
|
307
|
+
out.append(LocationResult(
|
|
308
|
+
object_name=str(r.get("object_name", "")),
|
|
309
|
+
room_zone=str(r.get("room_zone", "unknown")),
|
|
310
|
+
pose=p_obj,
|
|
311
|
+
timestamp=float(r.get("timestamp", 0.0)),
|
|
312
|
+
confidence=float(r.get("confidence", 1.0)),
|
|
313
|
+
is_superseded=bool(r.get("is_superseded", False)),
|
|
314
|
+
observation_id=str(r.get("observation_id", "")),
|
|
315
|
+
metadata={},
|
|
316
|
+
))
|
|
317
|
+
return out
|
|
318
|
+
except Exception:
|
|
319
|
+
pass
|
|
320
|
+
|
|
321
|
+
filters: Dict[str, Any] = {"is_superseded": False}
|
|
322
|
+
if room_zone:
|
|
323
|
+
filters["room_zone"] = room_zone
|
|
324
|
+
|
|
325
|
+
results = self._client.search(
|
|
326
|
+
query=query,
|
|
327
|
+
user_id=self.robot_id,
|
|
328
|
+
filters=filters,
|
|
329
|
+
limit=top_k,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
loc_results: List[LocationResult] = []
|
|
333
|
+
for item in results if isinstance(results, list) else [results]:
|
|
334
|
+
if not isinstance(item, dict):
|
|
335
|
+
continue
|
|
336
|
+
meta = item.get("metadata", {})
|
|
337
|
+
pose_data = meta.get("pose", [0.0, 0.0, 0.0])
|
|
338
|
+
pose_obj = Pose6D.from_list(pose_data) if isinstance(pose_data, (list, tuple)) else Pose6D(**pose_data)
|
|
339
|
+
loc_results.append(
|
|
340
|
+
LocationResult(
|
|
341
|
+
object_name=str(meta.get("object_name", item.get("memory", ""))),
|
|
342
|
+
room_zone=str(meta.get("room_zone", "unknown")),
|
|
343
|
+
pose=pose_obj,
|
|
344
|
+
timestamp=float(meta.get("timestamp", 0.0)),
|
|
345
|
+
confidence=float(item.get("score", 1.0)),
|
|
346
|
+
is_superseded=bool(meta.get("is_superseded", False)),
|
|
347
|
+
observation_id=str(item.get("id", "")),
|
|
348
|
+
metadata=meta,
|
|
349
|
+
)
|
|
350
|
+
)
|
|
351
|
+
return loc_results
|
|
352
|
+
|
|
353
|
+
def find_by_affordance(
|
|
354
|
+
self,
|
|
355
|
+
action: str,
|
|
356
|
+
room_zone: Optional[str] = None,
|
|
357
|
+
) -> List[LocationResult]:
|
|
358
|
+
"""Find active physical objects in the workspace that satisfy an affordance.
|
|
359
|
+
|
|
360
|
+
Example: find_by_affordance("fastening") -> returns active screwdrivers, wrenches.
|
|
361
|
+
"""
|
|
362
|
+
candidates = get_objects_for_affordance(action)
|
|
363
|
+
matches: List[LocationResult] = []
|
|
364
|
+
for cand in candidates:
|
|
365
|
+
loc = self.where_is(cand)
|
|
366
|
+
if loc is not None and not loc.is_superseded:
|
|
367
|
+
if room_zone is None or loc.room_zone.lower() == room_zone.lower():
|
|
368
|
+
matches.append(loc)
|
|
369
|
+
return matches
|