foodforthought-cli 0.2.8__py3-none-any.whl → 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.
- ate/__init__.py +6 -0
- ate/__main__.py +16 -0
- ate/auth/__init__.py +1 -0
- ate/auth/device_flow.py +141 -0
- ate/auth/token_store.py +96 -0
- ate/behaviors/__init__.py +12 -0
- ate/behaviors/approach.py +399 -0
- ate/cli.py +855 -4551
- ate/client.py +90 -0
- ate/commands/__init__.py +168 -0
- ate/commands/auth.py +389 -0
- ate/commands/bridge.py +448 -0
- ate/commands/data.py +185 -0
- ate/commands/deps.py +111 -0
- ate/commands/generate.py +384 -0
- ate/commands/memory.py +907 -0
- ate/commands/parts.py +166 -0
- ate/commands/primitive.py +399 -0
- ate/commands/protocol.py +288 -0
- ate/commands/recording.py +524 -0
- ate/commands/repo.py +154 -0
- ate/commands/simulation.py +291 -0
- ate/commands/skill.py +303 -0
- ate/commands/skills.py +487 -0
- ate/commands/team.py +147 -0
- ate/commands/workflow.py +271 -0
- ate/detection/__init__.py +38 -0
- ate/detection/base.py +142 -0
- ate/detection/color_detector.py +399 -0
- ate/detection/trash_detector.py +322 -0
- ate/drivers/__init__.py +18 -6
- ate/drivers/ble_transport.py +405 -0
- ate/drivers/mechdog.py +360 -24
- ate/drivers/wifi_camera.py +477 -0
- ate/interfaces/__init__.py +16 -0
- ate/interfaces/base.py +2 -0
- ate/interfaces/sensors.py +247 -0
- ate/llm_proxy.py +239 -0
- ate/memory/__init__.py +35 -0
- ate/memory/cloud.py +244 -0
- ate/memory/context.py +269 -0
- ate/memory/embeddings.py +184 -0
- ate/memory/export.py +26 -0
- ate/memory/merge.py +146 -0
- ate/memory/migrate/__init__.py +34 -0
- ate/memory/migrate/base.py +89 -0
- ate/memory/migrate/pipeline.py +189 -0
- ate/memory/migrate/sources/__init__.py +13 -0
- ate/memory/migrate/sources/chroma.py +170 -0
- ate/memory/migrate/sources/pinecone.py +120 -0
- ate/memory/migrate/sources/qdrant.py +110 -0
- ate/memory/migrate/sources/weaviate.py +160 -0
- ate/memory/reranker.py +353 -0
- ate/memory/search.py +26 -0
- ate/memory/store.py +548 -0
- ate/recording/__init__.py +42 -3
- ate/recording/session.py +12 -2
- ate/recording/visual.py +416 -0
- ate/robot/__init__.py +142 -0
- ate/robot/agentic_servo.py +856 -0
- ate/robot/behaviors.py +493 -0
- ate/robot/ble_capture.py +1000 -0
- ate/robot/ble_enumerate.py +506 -0
- ate/robot/calibration.py +88 -3
- ate/robot/calibration_state.py +388 -0
- ate/robot/commands.py +143 -11
- ate/robot/direction_calibration.py +554 -0
- ate/robot/discovery.py +104 -2
- ate/robot/llm_system_id.py +654 -0
- ate/robot/locomotion_calibration.py +508 -0
- ate/robot/marker_generator.py +611 -0
- ate/robot/perception.py +502 -0
- ate/robot/primitives.py +614 -0
- ate/robot/profiles.py +6 -0
- ate/robot/registry.py +5 -2
- ate/robot/servo_mapper.py +1153 -0
- ate/robot/skill_upload.py +285 -3
- ate/robot/target_calibration.py +500 -0
- ate/robot/teach.py +515 -0
- ate/robot/types.py +242 -0
- ate/robot/visual_labeler.py +9 -0
- ate/robot/visual_servo_loop.py +494 -0
- ate/robot/visual_servoing.py +570 -0
- ate/robot/visual_system_id.py +906 -0
- ate/transports/__init__.py +121 -0
- ate/transports/base.py +394 -0
- ate/transports/ble.py +405 -0
- ate/transports/hybrid.py +444 -0
- ate/transports/serial.py +345 -0
- ate/urdf/__init__.py +30 -0
- ate/urdf/capture.py +582 -0
- ate/urdf/cloud.py +491 -0
- ate/urdf/collision.py +271 -0
- ate/urdf/commands.py +708 -0
- ate/urdf/depth.py +360 -0
- ate/urdf/inertial.py +312 -0
- ate/urdf/kinematics.py +330 -0
- ate/urdf/lifting.py +415 -0
- ate/urdf/meshing.py +300 -0
- ate/urdf/models/__init__.py +110 -0
- ate/urdf/models/depth_anything.py +253 -0
- ate/urdf/models/sam2.py +324 -0
- ate/urdf/motion_analysis.py +396 -0
- ate/urdf/pipeline.py +468 -0
- ate/urdf/scale.py +256 -0
- ate/urdf/scan_session.py +411 -0
- ate/urdf/segmentation.py +299 -0
- ate/urdf/synthesis.py +319 -0
- ate/urdf/topology.py +336 -0
- ate/urdf/validation.py +371 -0
- {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.0.dist-info}/METADATA +1 -1
- foodforthought_cli-0.3.0.dist-info/RECORD +166 -0
- {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.0.dist-info}/WHEEL +1 -1
- foodforthought_cli-0.2.8.dist-info/RECORD +0 -73
- {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.0.dist-info}/entry_points.txt +0 -0
- {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.0.dist-info}/top_level.txt +0 -0
ate/memory/store.py
ADDED
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
"""Core MemoryStore implementation."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from typing import List, Dict, Any, Optional
|
|
6
|
+
|
|
7
|
+
import memvid_sdk
|
|
8
|
+
|
|
9
|
+
from .search import SearchResult
|
|
10
|
+
from .export import MemoryInfo
|
|
11
|
+
from .embeddings import EmbeddingConfig, EmbeddingManager
|
|
12
|
+
from . import reranker
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MemoryStore:
|
|
16
|
+
"""Main interface for memory operations using memvid-sdk backend."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, mem):
|
|
19
|
+
"""Initialize with a memvid memory instance."""
|
|
20
|
+
self._mem = mem
|
|
21
|
+
self._embedding_config = None
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def _extract_metadata(frame_data):
|
|
25
|
+
"""Extract metadata from a frame_data dict.
|
|
26
|
+
|
|
27
|
+
Decodes JSON-encoded values, filters out ``extractous_metadata``,
|
|
28
|
+
and returns a plain dict. Returns ``{}`` for non-dict input or
|
|
29
|
+
when ``extra_metadata`` is missing.
|
|
30
|
+
"""
|
|
31
|
+
metadata = {}
|
|
32
|
+
if not isinstance(frame_data, dict):
|
|
33
|
+
return metadata
|
|
34
|
+
for key, value in frame_data.get('extra_metadata', {}).items():
|
|
35
|
+
if key == 'extractous_metadata':
|
|
36
|
+
continue
|
|
37
|
+
try:
|
|
38
|
+
if isinstance(value, str) and (value.startswith('{') or value.startswith('"')):
|
|
39
|
+
metadata[key] = json.loads(value)
|
|
40
|
+
else:
|
|
41
|
+
metadata[key] = value
|
|
42
|
+
except json.JSONDecodeError:
|
|
43
|
+
metadata[key] = value
|
|
44
|
+
return metadata
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def create(cls, path: str, enable_vec: bool = True, enable_lex: bool = True,
|
|
48
|
+
embedding_config: Optional[EmbeddingConfig] = None) -> 'MemoryStore':
|
|
49
|
+
"""Create a new memory file.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
path: Path where the .mv2 file will be created
|
|
53
|
+
enable_vec: Whether to enable vector indexing
|
|
54
|
+
enable_lex: Whether to enable lexical indexing
|
|
55
|
+
embedding_config: Optional embedding configuration. If None, auto-detects.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
MemoryStore instance
|
|
59
|
+
"""
|
|
60
|
+
if embedding_config is None:
|
|
61
|
+
embedding_config = EmbeddingManager.detect()
|
|
62
|
+
|
|
63
|
+
mem = memvid_sdk.use("basic", path, mode="create",
|
|
64
|
+
enable_vec=enable_vec, enable_lex=enable_lex)
|
|
65
|
+
|
|
66
|
+
store = cls(mem)
|
|
67
|
+
store._embedding_config = embedding_config
|
|
68
|
+
return store
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def from_context(cls) -> 'MemoryStore':
|
|
72
|
+
"""Open the active memory from context. Auto-initializes on first use."""
|
|
73
|
+
from .context import ContextManager
|
|
74
|
+
ctx = ContextManager.get_context()
|
|
75
|
+
return cls.open(ctx.path)
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def from_context_or_path(cls, path: Optional[str] = None) -> 'MemoryStore':
|
|
79
|
+
"""Open from explicit path or fall back to active context."""
|
|
80
|
+
if path:
|
|
81
|
+
return cls.open(path)
|
|
82
|
+
return cls.from_context()
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def open(cls, path: str) -> 'MemoryStore':
|
|
86
|
+
"""Open an existing memory file.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
path: Path to the existing .mv2 file
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
MemoryStore instance
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
FileNotFoundError: If the file doesn't exist
|
|
96
|
+
"""
|
|
97
|
+
try:
|
|
98
|
+
mem = memvid_sdk.use("basic", path, mode="open")
|
|
99
|
+
except (FileNotFoundError, OSError):
|
|
100
|
+
raise FileNotFoundError(f"Memory file not found: {path}")
|
|
101
|
+
except Exception as e:
|
|
102
|
+
# Check if it's a file-not-found from memvid
|
|
103
|
+
if not os.path.exists(path):
|
|
104
|
+
raise FileNotFoundError(f"Memory file not found: {path}")
|
|
105
|
+
raise
|
|
106
|
+
|
|
107
|
+
store = cls(mem)
|
|
108
|
+
# TODO: Read embedding config from metadata when implemented
|
|
109
|
+
store._embedding_config = EmbeddingManager.detect()
|
|
110
|
+
return store
|
|
111
|
+
|
|
112
|
+
def close(self):
|
|
113
|
+
"""Close the memory store."""
|
|
114
|
+
if self._mem:
|
|
115
|
+
self._mem.close()
|
|
116
|
+
|
|
117
|
+
def __enter__(self):
|
|
118
|
+
"""Context manager entry."""
|
|
119
|
+
return self
|
|
120
|
+
|
|
121
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
122
|
+
"""Context manager exit - ensures cleanup."""
|
|
123
|
+
self.close()
|
|
124
|
+
|
|
125
|
+
def add(self, text: str, tags: Optional[List[str]] = None,
|
|
126
|
+
metadata: Optional[Dict[str, Any]] = None, title: Optional[str] = None,
|
|
127
|
+
enable_embedding: Optional[bool] = None) -> str:
|
|
128
|
+
"""Add content to memory.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
text: The content text to store
|
|
132
|
+
tags: Optional list of tags
|
|
133
|
+
metadata: Optional metadata dictionary
|
|
134
|
+
title: Optional title for the content
|
|
135
|
+
enable_embedding: Whether to enable embedding for this entry
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Frame ID as string
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
ValueError: If text is empty
|
|
142
|
+
TypeError: If text is None
|
|
143
|
+
"""
|
|
144
|
+
if text is None:
|
|
145
|
+
raise TypeError("text cannot be None")
|
|
146
|
+
if text == "":
|
|
147
|
+
raise ValueError("text cannot be empty")
|
|
148
|
+
|
|
149
|
+
# Prepare put arguments
|
|
150
|
+
put_args = {
|
|
151
|
+
'text': text,
|
|
152
|
+
'tags': tags,
|
|
153
|
+
'metadata': metadata,
|
|
154
|
+
'title': title
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# Add embedding args if specified
|
|
158
|
+
if enable_embedding is not None:
|
|
159
|
+
put_args['enable_embedding'] = enable_embedding
|
|
160
|
+
|
|
161
|
+
frame_id = self._mem.put(**put_args)
|
|
162
|
+
return str(frame_id)
|
|
163
|
+
|
|
164
|
+
def add_batch(self, items: List[Dict[str, Any]]) -> List[str]:
|
|
165
|
+
"""Add multiple items to memory.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
items: List of dicts with 'text' key and optional 'tags', 'metadata', 'title'
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
List of frame IDs as strings
|
|
172
|
+
|
|
173
|
+
Raises:
|
|
174
|
+
ValueError: If any item is missing 'text' key
|
|
175
|
+
"""
|
|
176
|
+
if not items:
|
|
177
|
+
return []
|
|
178
|
+
|
|
179
|
+
frame_ids = []
|
|
180
|
+
for item in items:
|
|
181
|
+
if 'text' not in item:
|
|
182
|
+
raise ValueError("Each item must have a 'text' key")
|
|
183
|
+
|
|
184
|
+
text = item['text']
|
|
185
|
+
tags = item.get('tags')
|
|
186
|
+
metadata = item.get('metadata')
|
|
187
|
+
title = item.get('title')
|
|
188
|
+
|
|
189
|
+
frame_id = self._mem.put(text=text, tags=tags, metadata=metadata, title=title)
|
|
190
|
+
frame_ids.append(str(frame_id))
|
|
191
|
+
|
|
192
|
+
return frame_ids
|
|
193
|
+
|
|
194
|
+
def search(self, query: str, top_k: int = 5, engine: Optional[str] = None) -> List[SearchResult]:
|
|
195
|
+
"""Search memory for relevant content.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
query: Search query string
|
|
199
|
+
top_k: Maximum number of results to return
|
|
200
|
+
engine: Search engine ("auto" | "vec" | "lex" | "hybrid" | "rerank")
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
List of SearchResult objects ordered by relevance
|
|
204
|
+
|
|
205
|
+
Raises:
|
|
206
|
+
ValueError: If query is empty
|
|
207
|
+
"""
|
|
208
|
+
if not query.strip():
|
|
209
|
+
raise ValueError("query cannot be empty")
|
|
210
|
+
|
|
211
|
+
# Handle rerank engine
|
|
212
|
+
if engine == "rerank":
|
|
213
|
+
return self._search_with_rerank(query, top_k)
|
|
214
|
+
|
|
215
|
+
# Handle auto engine with potential rerank fallback
|
|
216
|
+
if engine == "auto":
|
|
217
|
+
# Check if vector index exists
|
|
218
|
+
try:
|
|
219
|
+
stats = self._mem.stats()
|
|
220
|
+
has_vec = stats.has_vec_index if hasattr(stats, 'has_vec_index') else stats.get('has_vec_index', False)
|
|
221
|
+
|
|
222
|
+
if has_vec:
|
|
223
|
+
# Use vector search if available
|
|
224
|
+
engine = "vec"
|
|
225
|
+
else:
|
|
226
|
+
# Check if LLM reranker is available
|
|
227
|
+
reranker = self._get_reranker()
|
|
228
|
+
if reranker is not None:
|
|
229
|
+
return self._search_with_rerank(query, top_k)
|
|
230
|
+
else:
|
|
231
|
+
# Fallback to lexical
|
|
232
|
+
engine = "lex"
|
|
233
|
+
except:
|
|
234
|
+
# Fallback to lexical on any error
|
|
235
|
+
engine = "lex"
|
|
236
|
+
|
|
237
|
+
# Prepare find arguments for non-rerank engines
|
|
238
|
+
find_args = {'k': top_k}
|
|
239
|
+
# Note: real memvid find() does not accept 'engine' — it auto-selects
|
|
240
|
+
# based on available indices. We just call find() and read the engine
|
|
241
|
+
# from the response.
|
|
242
|
+
|
|
243
|
+
result = self._mem.find(query, **find_args)
|
|
244
|
+
|
|
245
|
+
# Handle both dict and object interfaces for result
|
|
246
|
+
if hasattr(result, 'hits'):
|
|
247
|
+
# Object interface (for mocking)
|
|
248
|
+
hits = result.hits
|
|
249
|
+
result_engine = getattr(result, 'engine', 'tantivy')
|
|
250
|
+
else:
|
|
251
|
+
# Dict interface (real implementation)
|
|
252
|
+
hits = result.get('hits', [])
|
|
253
|
+
result_engine = result.get('engine', 'tantivy')
|
|
254
|
+
|
|
255
|
+
# Map engine names
|
|
256
|
+
if result_engine == 'tantivy':
|
|
257
|
+
mapped_engine = 'lex'
|
|
258
|
+
elif result_engine == 'vector':
|
|
259
|
+
mapped_engine = 'vec'
|
|
260
|
+
elif result_engine == 'hybrid':
|
|
261
|
+
mapped_engine = 'hybrid'
|
|
262
|
+
else:
|
|
263
|
+
mapped_engine = result_engine
|
|
264
|
+
|
|
265
|
+
search_results = []
|
|
266
|
+
for hit in hits:
|
|
267
|
+
# Get metadata from the frame
|
|
268
|
+
# Handle both dict and object interfaces for hit
|
|
269
|
+
if hasattr(hit, 'frame_id'):
|
|
270
|
+
frame_id = hit.frame_id
|
|
271
|
+
snippet = hit.snippet
|
|
272
|
+
title = hit.title
|
|
273
|
+
score = hit.score
|
|
274
|
+
tags = getattr(hit, 'tags', [])
|
|
275
|
+
|
|
276
|
+
# For mocked tests, uri might not be set properly
|
|
277
|
+
uri = getattr(hit, 'uri', f"mv2://frames/{frame_id}")
|
|
278
|
+
else:
|
|
279
|
+
frame_id = hit['frame_id']
|
|
280
|
+
snippet = hit['snippet']
|
|
281
|
+
title = hit.get('title')
|
|
282
|
+
score = hit['score']
|
|
283
|
+
tags = hit.get('tags', [])
|
|
284
|
+
uri = hit['uri']
|
|
285
|
+
|
|
286
|
+
frame_data = self._mem.frame(uri)
|
|
287
|
+
metadata = {}
|
|
288
|
+
|
|
289
|
+
# Handle both dict and object interfaces for frame_data
|
|
290
|
+
if hasattr(frame_data, 'metadata'):
|
|
291
|
+
# Object interface (for mocking)
|
|
292
|
+
if frame_data.metadata:
|
|
293
|
+
metadata = frame_data.metadata
|
|
294
|
+
else:
|
|
295
|
+
# Dict interface (real implementation)
|
|
296
|
+
metadata = self._extract_metadata(frame_data)
|
|
297
|
+
|
|
298
|
+
search_result = SearchResult(
|
|
299
|
+
frame_id=frame_id,
|
|
300
|
+
text=snippet,
|
|
301
|
+
title=title,
|
|
302
|
+
score=score,
|
|
303
|
+
tags=tags,
|
|
304
|
+
metadata=metadata,
|
|
305
|
+
engine=mapped_engine
|
|
306
|
+
)
|
|
307
|
+
search_results.append(search_result)
|
|
308
|
+
|
|
309
|
+
# Sort by score descending
|
|
310
|
+
search_results.sort(key=lambda x: x.score, reverse=True)
|
|
311
|
+
return search_results
|
|
312
|
+
|
|
313
|
+
def _search_with_rerank(self, query: str, top_k: int) -> List[SearchResult]:
|
|
314
|
+
"""Search using LLM reranking."""
|
|
315
|
+
# Phase 1: BM25 wide retrieval (get more candidates than needed)
|
|
316
|
+
candidate_count = min(top_k * 4, 50)
|
|
317
|
+
bm25_results = self._bm25_search(query, candidate_count)
|
|
318
|
+
|
|
319
|
+
# If BM25 returns nothing (common with natural language queries),
|
|
320
|
+
# try fetching ALL frames as candidates for the LLM to evaluate
|
|
321
|
+
if not bm25_results:
|
|
322
|
+
bm25_results = self._get_all_frames(max_frames=50)
|
|
323
|
+
|
|
324
|
+
# Phase 2: LLM re-ranking
|
|
325
|
+
reranker = self._get_reranker()
|
|
326
|
+
if reranker is None:
|
|
327
|
+
# Fallback to BM25 if no LLM available
|
|
328
|
+
return bm25_results[:top_k]
|
|
329
|
+
|
|
330
|
+
return reranker.rerank(query, bm25_results, top_k=top_k)
|
|
331
|
+
|
|
332
|
+
def _bm25_search(self, query: str, top_k: int) -> List[SearchResult]:
|
|
333
|
+
"""Perform BM25 search and return SearchResult objects."""
|
|
334
|
+
# Note: real memvid find() does not accept 'engine' kwarg.
|
|
335
|
+
# It auto-selects based on available indices (lex by default).
|
|
336
|
+
result = self._mem.find(query, k=top_k)
|
|
337
|
+
|
|
338
|
+
# Handle both dict and object interfaces
|
|
339
|
+
if hasattr(result, 'hits'):
|
|
340
|
+
hits = result.hits
|
|
341
|
+
else:
|
|
342
|
+
hits = result.get('hits', [])
|
|
343
|
+
|
|
344
|
+
search_results = []
|
|
345
|
+
for hit in hits:
|
|
346
|
+
# Handle both dict and object interfaces for hit
|
|
347
|
+
if hasattr(hit, 'frame_id'):
|
|
348
|
+
frame_id = hit.frame_id
|
|
349
|
+
snippet = hit.snippet
|
|
350
|
+
title = hit.title
|
|
351
|
+
score = hit.score
|
|
352
|
+
tags = getattr(hit, 'tags', [])
|
|
353
|
+
uri = getattr(hit, 'uri', f"mv2://frames/{frame_id}")
|
|
354
|
+
else:
|
|
355
|
+
frame_id = hit['frame_id']
|
|
356
|
+
snippet = hit['snippet']
|
|
357
|
+
title = hit.get('title')
|
|
358
|
+
score = hit['score']
|
|
359
|
+
tags = hit.get('tags', [])
|
|
360
|
+
uri = hit['uri']
|
|
361
|
+
|
|
362
|
+
frame_data = self._mem.frame(uri)
|
|
363
|
+
metadata = {}
|
|
364
|
+
|
|
365
|
+
# Handle both dict and object interfaces for frame_data
|
|
366
|
+
if hasattr(frame_data, 'metadata'):
|
|
367
|
+
if frame_data.metadata:
|
|
368
|
+
metadata = frame_data.metadata
|
|
369
|
+
else:
|
|
370
|
+
metadata = self._extract_metadata(frame_data)
|
|
371
|
+
|
|
372
|
+
search_result = SearchResult(
|
|
373
|
+
frame_id=frame_id,
|
|
374
|
+
text=snippet,
|
|
375
|
+
title=title,
|
|
376
|
+
score=score,
|
|
377
|
+
tags=tags,
|
|
378
|
+
metadata=metadata,
|
|
379
|
+
engine="lex" # BM25 is lexical
|
|
380
|
+
)
|
|
381
|
+
search_results.append(search_result)
|
|
382
|
+
|
|
383
|
+
return search_results
|
|
384
|
+
|
|
385
|
+
def _get_all_frames(self, max_frames: int = 50) -> List[SearchResult]:
|
|
386
|
+
"""Get all frames as SearchResult objects for LLM re-ranking.
|
|
387
|
+
|
|
388
|
+
Used when BM25 returns no candidates but we still want the LLM
|
|
389
|
+
to evaluate all stored memories against the query.
|
|
390
|
+
"""
|
|
391
|
+
results = []
|
|
392
|
+
try:
|
|
393
|
+
timeline = self._mem.timeline()
|
|
394
|
+
if timeline is None:
|
|
395
|
+
return results
|
|
396
|
+
for entry in timeline[:max_frames]:
|
|
397
|
+
if isinstance(entry, dict):
|
|
398
|
+
uri = entry.get('uri', f"mv2://frames/{entry.get('frame_id', len(results))}")
|
|
399
|
+
frame_data = self._mem.frame(uri)
|
|
400
|
+
text = entry.get('preview', '').split('\ntitle:')[0].split('\ntags:')[0].strip()
|
|
401
|
+
if isinstance(frame_data, dict):
|
|
402
|
+
title = frame_data.get('title')
|
|
403
|
+
tags = frame_data.get('tags', [])
|
|
404
|
+
metadata = self._extract_metadata(frame_data)
|
|
405
|
+
else:
|
|
406
|
+
title = getattr(frame_data, 'title', None)
|
|
407
|
+
tags = getattr(frame_data, 'tags', [])
|
|
408
|
+
metadata = getattr(frame_data, 'metadata', {}) or {}
|
|
409
|
+
results.append(SearchResult(
|
|
410
|
+
frame_id=entry.get('frame_id', len(results)),
|
|
411
|
+
text=text,
|
|
412
|
+
title=title,
|
|
413
|
+
score=0.0,
|
|
414
|
+
tags=tags,
|
|
415
|
+
metadata=metadata,
|
|
416
|
+
engine="lex"
|
|
417
|
+
))
|
|
418
|
+
except Exception:
|
|
419
|
+
pass
|
|
420
|
+
return results
|
|
421
|
+
|
|
422
|
+
def _get_reranker(self) -> Optional['reranker.LLMReranker']:
|
|
423
|
+
"""Get a reranker instance if LLM provider is available."""
|
|
424
|
+
config = reranker.LLMReranker.detect()
|
|
425
|
+
if config is None:
|
|
426
|
+
return None
|
|
427
|
+
return reranker.LLMReranker(config)
|
|
428
|
+
|
|
429
|
+
def export_jsonl(self, output_path: str) -> int:
|
|
430
|
+
"""Export all memory content to JSONL file.
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
output_path: Path to output JSONL file
|
|
434
|
+
|
|
435
|
+
Returns:
|
|
436
|
+
Number of items exported
|
|
437
|
+
"""
|
|
438
|
+
count = 0
|
|
439
|
+
with open(output_path, 'w', encoding='utf-8') as f:
|
|
440
|
+
# Try timeline-based iteration first (works with real memvid)
|
|
441
|
+
try:
|
|
442
|
+
timeline = self._mem.timeline()
|
|
443
|
+
if timeline is not None:
|
|
444
|
+
for entry in timeline:
|
|
445
|
+
if isinstance(entry, dict):
|
|
446
|
+
uri = entry.get('uri', f"mv2://frames/{entry.get('frame_id', count)}")
|
|
447
|
+
frame_data = self._mem.frame(uri)
|
|
448
|
+
|
|
449
|
+
# Extract text — preview from timeline has the original text
|
|
450
|
+
text = entry.get('preview', '').split('\ntitle:')[0].split('\ntags:')[0].strip()
|
|
451
|
+
if isinstance(frame_data, dict):
|
|
452
|
+
title = frame_data.get('title')
|
|
453
|
+
tags = frame_data.get('tags', [])
|
|
454
|
+
|
|
455
|
+
# Extract metadata from extra_metadata
|
|
456
|
+
metadata = self._extract_metadata(frame_data)
|
|
457
|
+
else:
|
|
458
|
+
title = getattr(frame_data, 'title', None)
|
|
459
|
+
tags = getattr(frame_data, 'tags', [])
|
|
460
|
+
metadata = getattr(frame_data, 'metadata', {}) or {}
|
|
461
|
+
text = getattr(frame_data, 'text', '') or getattr(frame_data, 'snippet', '')
|
|
462
|
+
|
|
463
|
+
record = {
|
|
464
|
+
'frame_id': entry.get('frame_id', count),
|
|
465
|
+
'text': text,
|
|
466
|
+
'title': title,
|
|
467
|
+
'tags': tags,
|
|
468
|
+
'metadata': metadata
|
|
469
|
+
}
|
|
470
|
+
f.write(json.dumps(record) + '\n')
|
|
471
|
+
count += 1
|
|
472
|
+
|
|
473
|
+
if count > 0:
|
|
474
|
+
return count
|
|
475
|
+
except (AttributeError, TypeError):
|
|
476
|
+
pass # Fall through to find-based approach
|
|
477
|
+
|
|
478
|
+
# Fallback: use find with wildcard (for mocked tests)
|
|
479
|
+
result = self._mem.find("*", k=10000)
|
|
480
|
+
|
|
481
|
+
if hasattr(result, 'hits'):
|
|
482
|
+
hits = result.hits
|
|
483
|
+
else:
|
|
484
|
+
hits = result.get('hits', [])
|
|
485
|
+
|
|
486
|
+
for hit in hits:
|
|
487
|
+
if hasattr(hit, 'frame_id'):
|
|
488
|
+
frame_id = hit.frame_id
|
|
489
|
+
snippet = hit.snippet
|
|
490
|
+
title = hit.title
|
|
491
|
+
tags = getattr(hit, 'tags', [])
|
|
492
|
+
uri = getattr(hit, 'uri', f"mv2://frames/{frame_id}")
|
|
493
|
+
else:
|
|
494
|
+
frame_id = hit['frame_id']
|
|
495
|
+
snippet = hit['snippet']
|
|
496
|
+
title = hit.get('title')
|
|
497
|
+
tags = hit.get('tags', [])
|
|
498
|
+
uri = hit['uri']
|
|
499
|
+
|
|
500
|
+
frame_data = self._mem.frame(uri)
|
|
501
|
+
metadata = {}
|
|
502
|
+
if hasattr(frame_data, 'metadata'):
|
|
503
|
+
if frame_data.metadata:
|
|
504
|
+
metadata = frame_data.metadata
|
|
505
|
+
elif isinstance(frame_data, dict):
|
|
506
|
+
metadata = self._extract_metadata(frame_data)
|
|
507
|
+
|
|
508
|
+
record = {
|
|
509
|
+
'frame_id': frame_id,
|
|
510
|
+
'text': snippet,
|
|
511
|
+
'title': title,
|
|
512
|
+
'tags': tags,
|
|
513
|
+
'metadata': metadata
|
|
514
|
+
}
|
|
515
|
+
f.write(json.dumps(record) + '\n')
|
|
516
|
+
count += 1
|
|
517
|
+
|
|
518
|
+
return count
|
|
519
|
+
|
|
520
|
+
def info(self) -> MemoryInfo:
|
|
521
|
+
"""Get information about the memory store.
|
|
522
|
+
|
|
523
|
+
Returns:
|
|
524
|
+
MemoryInfo object with store statistics
|
|
525
|
+
"""
|
|
526
|
+
stats = self._mem.stats()
|
|
527
|
+
|
|
528
|
+
# Handle both dict and object interfaces for stats
|
|
529
|
+
if hasattr(stats, 'frame_count'):
|
|
530
|
+
# Object interface (for mocking)
|
|
531
|
+
return MemoryInfo(
|
|
532
|
+
path=self._mem.path,
|
|
533
|
+
frame_count=stats.frame_count,
|
|
534
|
+
size_bytes=stats.size_bytes,
|
|
535
|
+
has_lex_index=stats.has_lex_index,
|
|
536
|
+
has_vec_index=stats.has_vec_index,
|
|
537
|
+
has_time_index=stats.has_time_index
|
|
538
|
+
)
|
|
539
|
+
else:
|
|
540
|
+
# Dict interface (real implementation)
|
|
541
|
+
return MemoryInfo(
|
|
542
|
+
path=self._mem.path,
|
|
543
|
+
frame_count=stats['frame_count'],
|
|
544
|
+
size_bytes=stats['size_bytes'],
|
|
545
|
+
has_lex_index=stats['has_lex_index'],
|
|
546
|
+
has_vec_index=stats['has_vec_index'],
|
|
547
|
+
has_time_index=stats['has_time_index']
|
|
548
|
+
)
|
ate/recording/__init__.py
CHANGED
|
@@ -7,7 +7,7 @@ Records all interface method calls as transferable data that can be:
|
|
|
7
7
|
- Used to train policies
|
|
8
8
|
- Replayed on different hardware
|
|
9
9
|
|
|
10
|
-
Example:
|
|
10
|
+
Example - Basic recording:
|
|
11
11
|
from ate.drivers import MechDogDriver
|
|
12
12
|
from ate.recording import RecordingSession
|
|
13
13
|
|
|
@@ -24,21 +24,60 @@ Example:
|
|
|
24
24
|
# Save the recording
|
|
25
25
|
session.save("pickup_toy.demonstration")
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
Example - Visual recording with object detection:
|
|
28
|
+
from ate.drivers import MechDogDriver
|
|
29
|
+
from ate.recording import VisualRecordingSession
|
|
30
|
+
from ate.detection import TrashDetector
|
|
31
|
+
|
|
32
|
+
dog = MechDogDriver(port="/dev/cu.usbserial-10", config=config)
|
|
33
|
+
dog.connect()
|
|
34
|
+
|
|
35
|
+
# Create detector for automatic object detection
|
|
36
|
+
detector = TrashDetector()
|
|
37
|
+
|
|
38
|
+
# Record with visual capture at 5 fps
|
|
39
|
+
with VisualRecordingSession(
|
|
40
|
+
dog,
|
|
41
|
+
name="trash_pickup",
|
|
42
|
+
capture_fps=5.0,
|
|
43
|
+
detector=detector,
|
|
44
|
+
) as session:
|
|
45
|
+
dog.stand()
|
|
46
|
+
dog.walk(Vector3.forward(), speed=0.2)
|
|
47
|
+
time.sleep(5)
|
|
48
|
+
|
|
49
|
+
# Save recording and visual frames separately
|
|
50
|
+
session.save("trash_pickup.demonstration")
|
|
51
|
+
session.save_frames("trash_pickup_frames.json")
|
|
52
|
+
|
|
53
|
+
print(session.summary())
|
|
29
54
|
"""
|
|
30
55
|
|
|
31
56
|
from .session import RecordingSession, RecordedCall
|
|
32
57
|
from .wrapper import RecordingWrapper
|
|
33
58
|
from .demonstration import Demonstration, load_demonstration
|
|
34
59
|
from .upload import DemonstrationUploader, upload_demonstration
|
|
60
|
+
from .visual import (
|
|
61
|
+
VisualRecordingSession,
|
|
62
|
+
RecordedFrame,
|
|
63
|
+
DetectionResult,
|
|
64
|
+
load_frames,
|
|
65
|
+
)
|
|
35
66
|
|
|
36
67
|
__all__ = [
|
|
68
|
+
# Core recording
|
|
37
69
|
"RecordingSession",
|
|
38
70
|
"RecordedCall",
|
|
39
71
|
"RecordingWrapper",
|
|
72
|
+
# Demonstrations
|
|
40
73
|
"Demonstration",
|
|
41
74
|
"load_demonstration",
|
|
75
|
+
# Upload
|
|
42
76
|
"DemonstrationUploader",
|
|
43
77
|
"upload_demonstration",
|
|
78
|
+
# Visual recording with detection
|
|
79
|
+
"VisualRecordingSession",
|
|
80
|
+
"RecordedFrame",
|
|
81
|
+
"DetectionResult",
|
|
82
|
+
"load_frames",
|
|
44
83
|
]
|
ate/recording/session.py
CHANGED
|
@@ -54,14 +54,24 @@ class RecordedCall:
|
|
|
54
54
|
# Handle common types
|
|
55
55
|
if value is None or isinstance(value, (bool, int, float, str)):
|
|
56
56
|
return value
|
|
57
|
+
if isinstance(value, bytes):
|
|
58
|
+
# Don't include large binary data in JSON - just note its size
|
|
59
|
+
if len(value) > 1000:
|
|
60
|
+
return {"__type__": "bytes", "__size__": len(value), "__truncated__": True}
|
|
61
|
+
import base64
|
|
62
|
+
return {"__type__": "bytes", "__b64__": base64.b64encode(value).decode('ascii')}
|
|
57
63
|
if isinstance(value, (list, tuple)):
|
|
58
64
|
return [self._serialize_value(v) for v in value]
|
|
59
65
|
if isinstance(value, dict):
|
|
60
66
|
return {k: self._serialize_value(v) for k, v in value.items()}
|
|
61
67
|
|
|
62
|
-
# Handle our interface types
|
|
68
|
+
# Handle our interface types (serialize fields individually to handle bytes)
|
|
63
69
|
if hasattr(value, '__dataclass_fields__'):
|
|
64
|
-
|
|
70
|
+
result = {"__type__": type(value).__name__}
|
|
71
|
+
for field_name in value.__dataclass_fields__:
|
|
72
|
+
field_value = getattr(value, field_name)
|
|
73
|
+
result[field_name] = self._serialize_value(field_value)
|
|
74
|
+
return result
|
|
65
75
|
if hasattr(value, 'to_dict'):
|
|
66
76
|
return {"__type__": type(value).__name__, **value.to_dict()}
|
|
67
77
|
|