foodforthought-cli 0.2.8__py3-none-any.whl → 0.3.1__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.
Files changed (116) hide show
  1. ate/__init__.py +6 -0
  2. ate/__main__.py +16 -0
  3. ate/auth/__init__.py +1 -0
  4. ate/auth/device_flow.py +141 -0
  5. ate/auth/token_store.py +96 -0
  6. ate/behaviors/__init__.py +12 -0
  7. ate/behaviors/approach.py +399 -0
  8. ate/cli.py +855 -4551
  9. ate/client.py +90 -0
  10. ate/commands/__init__.py +168 -0
  11. ate/commands/auth.py +389 -0
  12. ate/commands/bridge.py +448 -0
  13. ate/commands/data.py +185 -0
  14. ate/commands/deps.py +111 -0
  15. ate/commands/generate.py +384 -0
  16. ate/commands/memory.py +907 -0
  17. ate/commands/parts.py +166 -0
  18. ate/commands/primitive.py +399 -0
  19. ate/commands/protocol.py +288 -0
  20. ate/commands/recording.py +524 -0
  21. ate/commands/repo.py +154 -0
  22. ate/commands/simulation.py +291 -0
  23. ate/commands/skill.py +303 -0
  24. ate/commands/skills.py +487 -0
  25. ate/commands/team.py +147 -0
  26. ate/commands/workflow.py +271 -0
  27. ate/detection/__init__.py +38 -0
  28. ate/detection/base.py +142 -0
  29. ate/detection/color_detector.py +402 -0
  30. ate/detection/trash_detector.py +322 -0
  31. ate/drivers/__init__.py +18 -6
  32. ate/drivers/ble_transport.py +405 -0
  33. ate/drivers/mechdog.py +360 -24
  34. ate/drivers/wifi_camera.py +477 -0
  35. ate/interfaces/__init__.py +16 -0
  36. ate/interfaces/base.py +2 -0
  37. ate/interfaces/sensors.py +247 -0
  38. ate/llm_proxy.py +239 -0
  39. ate/memory/__init__.py +35 -0
  40. ate/memory/cloud.py +244 -0
  41. ate/memory/context.py +269 -0
  42. ate/memory/embeddings.py +184 -0
  43. ate/memory/export.py +26 -0
  44. ate/memory/merge.py +146 -0
  45. ate/memory/migrate/__init__.py +34 -0
  46. ate/memory/migrate/base.py +89 -0
  47. ate/memory/migrate/pipeline.py +189 -0
  48. ate/memory/migrate/sources/__init__.py +13 -0
  49. ate/memory/migrate/sources/chroma.py +170 -0
  50. ate/memory/migrate/sources/pinecone.py +120 -0
  51. ate/memory/migrate/sources/qdrant.py +110 -0
  52. ate/memory/migrate/sources/weaviate.py +160 -0
  53. ate/memory/reranker.py +353 -0
  54. ate/memory/search.py +26 -0
  55. ate/memory/store.py +548 -0
  56. ate/recording/__init__.py +42 -3
  57. ate/recording/session.py +12 -2
  58. ate/recording/visual.py +416 -0
  59. ate/robot/__init__.py +142 -0
  60. ate/robot/agentic_servo.py +856 -0
  61. ate/robot/behaviors.py +493 -0
  62. ate/robot/ble_capture.py +1000 -0
  63. ate/robot/ble_enumerate.py +506 -0
  64. ate/robot/calibration.py +88 -3
  65. ate/robot/calibration_state.py +388 -0
  66. ate/robot/commands.py +143 -11
  67. ate/robot/direction_calibration.py +554 -0
  68. ate/robot/discovery.py +104 -2
  69. ate/robot/llm_system_id.py +654 -0
  70. ate/robot/locomotion_calibration.py +508 -0
  71. ate/robot/marker_generator.py +611 -0
  72. ate/robot/perception.py +502 -0
  73. ate/robot/primitives.py +614 -0
  74. ate/robot/profiles.py +6 -0
  75. ate/robot/registry.py +5 -2
  76. ate/robot/servo_mapper.py +1153 -0
  77. ate/robot/skill_upload.py +285 -3
  78. ate/robot/target_calibration.py +500 -0
  79. ate/robot/teach.py +515 -0
  80. ate/robot/types.py +242 -0
  81. ate/robot/visual_labeler.py +9 -0
  82. ate/robot/visual_servo_loop.py +494 -0
  83. ate/robot/visual_servoing.py +570 -0
  84. ate/robot/visual_system_id.py +906 -0
  85. ate/transports/__init__.py +121 -0
  86. ate/transports/base.py +394 -0
  87. ate/transports/ble.py +405 -0
  88. ate/transports/hybrid.py +444 -0
  89. ate/transports/serial.py +345 -0
  90. ate/urdf/__init__.py +30 -0
  91. ate/urdf/capture.py +582 -0
  92. ate/urdf/cloud.py +491 -0
  93. ate/urdf/collision.py +271 -0
  94. ate/urdf/commands.py +708 -0
  95. ate/urdf/depth.py +360 -0
  96. ate/urdf/inertial.py +312 -0
  97. ate/urdf/kinematics.py +330 -0
  98. ate/urdf/lifting.py +415 -0
  99. ate/urdf/meshing.py +300 -0
  100. ate/urdf/models/__init__.py +110 -0
  101. ate/urdf/models/depth_anything.py +253 -0
  102. ate/urdf/models/sam2.py +324 -0
  103. ate/urdf/motion_analysis.py +396 -0
  104. ate/urdf/pipeline.py +468 -0
  105. ate/urdf/scale.py +256 -0
  106. ate/urdf/scan_session.py +411 -0
  107. ate/urdf/segmentation.py +299 -0
  108. ate/urdf/synthesis.py +319 -0
  109. ate/urdf/topology.py +336 -0
  110. ate/urdf/validation.py +371 -0
  111. {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.1.dist-info}/METADATA +1 -1
  112. foodforthought_cli-0.3.1.dist-info/RECORD +166 -0
  113. {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.1.dist-info}/WHEEL +1 -1
  114. foodforthought_cli-0.2.8.dist-info/RECORD +0 -73
  115. {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.1.dist-info}/entry_points.txt +0 -0
  116. {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.1.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
- # Later: upload to FoodforThought
28
- session.upload()
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
- return {"__type__": type(value).__name__, **asdict(value)}
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