roampal 0.1.4__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 (44) hide show
  1. roampal/__init__.py +29 -0
  2. roampal/__main__.py +6 -0
  3. roampal/backend/__init__.py +1 -0
  4. roampal/backend/modules/__init__.py +1 -0
  5. roampal/backend/modules/memory/__init__.py +43 -0
  6. roampal/backend/modules/memory/chromadb_adapter.py +623 -0
  7. roampal/backend/modules/memory/config.py +102 -0
  8. roampal/backend/modules/memory/content_graph.py +543 -0
  9. roampal/backend/modules/memory/context_service.py +455 -0
  10. roampal/backend/modules/memory/embedding_service.py +96 -0
  11. roampal/backend/modules/memory/knowledge_graph_service.py +1052 -0
  12. roampal/backend/modules/memory/memory_bank_service.py +433 -0
  13. roampal/backend/modules/memory/memory_types.py +296 -0
  14. roampal/backend/modules/memory/outcome_service.py +400 -0
  15. roampal/backend/modules/memory/promotion_service.py +473 -0
  16. roampal/backend/modules/memory/routing_service.py +444 -0
  17. roampal/backend/modules/memory/scoring_service.py +324 -0
  18. roampal/backend/modules/memory/search_service.py +646 -0
  19. roampal/backend/modules/memory/tests/__init__.py +1 -0
  20. roampal/backend/modules/memory/tests/conftest.py +12 -0
  21. roampal/backend/modules/memory/tests/unit/__init__.py +1 -0
  22. roampal/backend/modules/memory/tests/unit/conftest.py +7 -0
  23. roampal/backend/modules/memory/tests/unit/test_knowledge_graph_service.py +517 -0
  24. roampal/backend/modules/memory/tests/unit/test_memory_bank_service.py +504 -0
  25. roampal/backend/modules/memory/tests/unit/test_outcome_service.py +485 -0
  26. roampal/backend/modules/memory/tests/unit/test_scoring_service.py +255 -0
  27. roampal/backend/modules/memory/tests/unit/test_search_service.py +413 -0
  28. roampal/backend/modules/memory/tests/unit/test_unified_memory_system.py +418 -0
  29. roampal/backend/modules/memory/unified_memory_system.py +1277 -0
  30. roampal/cli.py +638 -0
  31. roampal/hooks/__init__.py +16 -0
  32. roampal/hooks/session_manager.py +587 -0
  33. roampal/hooks/stop_hook.py +176 -0
  34. roampal/hooks/user_prompt_submit_hook.py +103 -0
  35. roampal/mcp/__init__.py +7 -0
  36. roampal/mcp/server.py +611 -0
  37. roampal/server/__init__.py +7 -0
  38. roampal/server/main.py +744 -0
  39. roampal-0.1.4.dist-info/METADATA +179 -0
  40. roampal-0.1.4.dist-info/RECORD +44 -0
  41. roampal-0.1.4.dist-info/WHEEL +5 -0
  42. roampal-0.1.4.dist-info/entry_points.txt +2 -0
  43. roampal-0.1.4.dist-info/licenses/LICENSE +190 -0
  44. roampal-0.1.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,418 @@
1
+ """
2
+ Unit Tests for UnifiedMemorySystem (Core)
3
+
4
+ Tests the Core UMS which is more monolithic than Desktop's facade pattern.
5
+ Core UMS handles collections and services internally.
6
+ """
7
+
8
+ import sys
9
+ import os
10
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', '..', '..', '..')))
11
+
12
+ import pytest
13
+ from unittest.mock import MagicMock, AsyncMock, patch
14
+ from datetime import datetime
15
+
16
+ from roampal.backend.modules.memory.unified_memory_system import UnifiedMemorySystem
17
+ from roampal.backend.modules.memory.config import MemoryConfig
18
+
19
+
20
+ class TestUnifiedMemorySystemInit:
21
+ """Test initialization."""
22
+
23
+ def test_init_creates_data_dir(self, tmp_path):
24
+ """Should create data directory."""
25
+ ums = UnifiedMemorySystem(data_path=str(tmp_path / "data"))
26
+ assert (tmp_path / "data").exists()
27
+
28
+ def test_init_with_custom_config(self, tmp_path):
29
+ """Should use custom config."""
30
+ config = MemoryConfig(promotion_score_threshold=0.8)
31
+ ums = UnifiedMemorySystem(
32
+ data_path=str(tmp_path / "data"),
33
+ config=config
34
+ )
35
+ assert ums.config.promotion_score_threshold == 0.8
36
+
37
+ def test_init_not_initialized(self, tmp_path):
38
+ """Should not be initialized until initialize() called."""
39
+ ums = UnifiedMemorySystem(data_path=str(tmp_path / "data"))
40
+ assert not ums.initialized
41
+
42
+ def test_init_loads_kg(self, tmp_path):
43
+ """Should load knowledge graph on init."""
44
+ ums = UnifiedMemorySystem(data_path=str(tmp_path / "data"))
45
+ assert "routing_patterns" in ums.knowledge_graph
46
+ assert "context_action_effectiveness" in ums.knowledge_graph
47
+
48
+
49
+ class TestInitialize:
50
+ """Test initialization process."""
51
+
52
+ @pytest.fixture
53
+ def ums(self, tmp_path):
54
+ """Create UMS instance."""
55
+ return UnifiedMemorySystem(data_path=str(tmp_path / "data"))
56
+
57
+ @pytest.mark.asyncio
58
+ async def test_initialize_creates_collections(self, ums):
59
+ """Should create all collections."""
60
+ await ums.initialize()
61
+
62
+ assert "books" in ums.collections
63
+ assert "working" in ums.collections
64
+ assert "history" in ums.collections
65
+ assert "patterns" in ums.collections
66
+ assert "memory_bank" in ums.collections
67
+
68
+ @pytest.mark.asyncio
69
+ async def test_initialize_creates_services(self, ums):
70
+ """Should initialize all services."""
71
+ await ums.initialize()
72
+
73
+ assert ums._embedding_service is not None
74
+ assert ums._scoring_service is not None
75
+ assert ums._promotion_service is not None
76
+ assert ums._outcome_service is not None
77
+ assert ums._memory_bank_service is not None
78
+ assert ums._context_service is not None
79
+
80
+ @pytest.mark.asyncio
81
+ async def test_initialize_only_once(self, ums):
82
+ """Should only initialize once."""
83
+ await ums.initialize()
84
+ first_embedding = ums._embedding_service
85
+
86
+ await ums.initialize()
87
+ assert ums._embedding_service is first_embedding
88
+
89
+
90
+ class TestStoreWorking:
91
+ """Test store_working functionality."""
92
+
93
+ @pytest.fixture
94
+ def mock_ums(self, tmp_path):
95
+ """Create UMS with mocked embedding."""
96
+ ums = UnifiedMemorySystem(data_path=str(tmp_path / "data"))
97
+ ums.initialized = True
98
+
99
+ # Mock collections
100
+ working = MagicMock()
101
+ working.upsert_vectors = AsyncMock()
102
+ ums.collections = {"working": working}
103
+
104
+ # Mock embedding
105
+ ums._embedding_service = MagicMock()
106
+ ums._embedding_service.embed_text = AsyncMock(return_value=[0.1] * 384)
107
+
108
+ return ums
109
+
110
+ @pytest.mark.asyncio
111
+ async def test_store_working_generates_doc_id(self, mock_ums):
112
+ """Should generate document ID."""
113
+ doc_id = await mock_ums.store_working("test text")
114
+
115
+ assert doc_id.startswith("working_")
116
+ mock_ums.collections["working"].upsert_vectors.assert_called_once()
117
+
118
+ @pytest.mark.asyncio
119
+ async def test_store_working_embeds_text(self, mock_ums):
120
+ """Should embed the text."""
121
+ await mock_ums.store_working("test text")
122
+
123
+ mock_ums._embedding_service.embed_text.assert_called_with("test text")
124
+
125
+
126
+ class TestSearch:
127
+ """Test search functionality."""
128
+
129
+ @pytest.fixture
130
+ def mock_ums(self, tmp_path):
131
+ """Create UMS with search mocks."""
132
+ ums = UnifiedMemorySystem(data_path=str(tmp_path / "data"))
133
+ ums.initialized = True
134
+
135
+ # Mock embedding
136
+ ums._embedding_service = MagicMock()
137
+ ums._embedding_service.embed_text = AsyncMock(return_value=[0.1] * 384)
138
+
139
+ # Mock scoring
140
+ ums._scoring_service = MagicMock()
141
+ ums._scoring_service.calculate_final_score = MagicMock(return_value={
142
+ "final_rank_score": 0.8,
143
+ "wilson_score": 0.7,
144
+ "embedding_similarity": 0.9,
145
+ "learned_score": 0.7,
146
+ "embedding_weight": 0.5,
147
+ "learned_weight": 0.5
148
+ })
149
+
150
+ # Mock collections with query results
151
+ mock_collection = MagicMock()
152
+ mock_collection.hybrid_query = AsyncMock(return_value=[
153
+ {"id": "doc_1", "text": "result 1", "distance": 0.5, "metadata": {"score": 0.7, "uses": 3}},
154
+ {"id": "doc_2", "text": "result 2", "distance": 0.8, "metadata": {"score": 0.5, "uses": 1}},
155
+ ])
156
+
157
+ ums.collections = {
158
+ "working": mock_collection,
159
+ "history": mock_collection,
160
+ "patterns": mock_collection,
161
+ "books": mock_collection,
162
+ "memory_bank": mock_collection,
163
+ }
164
+
165
+ return ums
166
+
167
+ @pytest.mark.asyncio
168
+ async def test_search_returns_results(self, mock_ums):
169
+ """Should return search results."""
170
+ results = await mock_ums.search("test query")
171
+
172
+ # Returns list (may be empty if mocked collections return nothing)
173
+ assert isinstance(results, list)
174
+
175
+ @pytest.mark.asyncio
176
+ async def test_search_generates_embedding(self, mock_ums):
177
+ """Should generate embedding for query."""
178
+ await mock_ums.search("test query")
179
+
180
+ mock_ums._embedding_service.embed_text.assert_called()
181
+
182
+
183
+ class TestRecordOutcome:
184
+ """Test outcome recording."""
185
+
186
+ @pytest.fixture
187
+ def mock_ums(self, tmp_path):
188
+ """Create UMS with outcome mock."""
189
+ ums = UnifiedMemorySystem(data_path=str(tmp_path / "data"))
190
+ ums.initialized = True
191
+
192
+ # Mock outcome service
193
+ ums._outcome_service = MagicMock()
194
+ ums._outcome_service.record_outcome = AsyncMock(return_value={"score": 0.7})
195
+
196
+ return ums
197
+
198
+ @pytest.mark.asyncio
199
+ async def test_record_outcome_delegates(self, mock_ums):
200
+ """Should delegate to outcome service."""
201
+ # Core's record_outcome takes doc_ids (list), not doc_id
202
+ await mock_ums.record_outcome(
203
+ doc_ids=["working_test123"],
204
+ outcome="worked"
205
+ )
206
+
207
+ mock_ums._outcome_service.record_outcome.assert_called()
208
+
209
+ @pytest.mark.asyncio
210
+ async def test_record_outcome_with_reason(self, mock_ums):
211
+ """Should pass failure reason."""
212
+ # Core's record_outcome takes doc_ids (list), not doc_id
213
+ await mock_ums.record_outcome(
214
+ doc_ids=["working_test123"],
215
+ outcome="failed",
216
+ failure_reason="Test failure"
217
+ )
218
+
219
+ # Check failure_reason was passed
220
+ mock_ums._outcome_service.record_outcome.assert_called()
221
+
222
+
223
+ class TestMemoryBankAPI:
224
+ """Test memory bank API."""
225
+
226
+ @pytest.fixture
227
+ def mock_ums(self, tmp_path):
228
+ """Create UMS with memory bank mock."""
229
+ ums = UnifiedMemorySystem(data_path=str(tmp_path / "data"))
230
+ ums.initialized = True
231
+
232
+ # Mock memory bank service
233
+ ums._memory_bank_service = MagicMock()
234
+ ums._memory_bank_service.store = AsyncMock(return_value="memory_bank_123")
235
+ ums._memory_bank_service.update = AsyncMock(return_value="memory_bank_123")
236
+ ums._memory_bank_service.archive = AsyncMock(return_value=True)
237
+ ums._memory_bank_service.search = AsyncMock(return_value=[])
238
+
239
+ return ums
240
+
241
+ @pytest.mark.asyncio
242
+ async def test_store_memory_bank(self, mock_ums):
243
+ """Should delegate to memory bank service."""
244
+ doc_id = await mock_ums.store_memory_bank(
245
+ text="User prefers dark mode",
246
+ tags=["preference"]
247
+ )
248
+
249
+ assert doc_id == "memory_bank_123"
250
+ mock_ums._memory_bank_service.store.assert_called_once()
251
+
252
+ @pytest.mark.asyncio
253
+ async def test_update_memory_bank(self, mock_ums):
254
+ """Should delegate update."""
255
+ await mock_ums.update_memory_bank(
256
+ old_content="old text",
257
+ new_content="new text"
258
+ )
259
+
260
+ mock_ums._memory_bank_service.update.assert_called_once()
261
+
262
+ @pytest.mark.asyncio
263
+ async def test_archive_memory_bank(self, mock_ums):
264
+ """Should delegate archive."""
265
+ result = await mock_ums.archive_memory_bank("some content to archive")
266
+ assert result is True
267
+
268
+
269
+ class TestContextAPI:
270
+ """Test context analysis API."""
271
+
272
+ @pytest.fixture
273
+ def mock_ums(self, tmp_path):
274
+ """Create UMS with mocked collections for context analysis."""
275
+ ums = UnifiedMemorySystem(data_path=str(tmp_path / "data"))
276
+ ums.initialized = True
277
+
278
+ # Mock embedding
279
+ ums._embedding_service = MagicMock()
280
+ ums._embedding_service.embed_text = AsyncMock(return_value=[0.1] * 384)
281
+
282
+ # Mock collections
283
+ mock_collection = MagicMock()
284
+ mock_collection.hybrid_query = AsyncMock(return_value=[])
285
+
286
+ ums.collections = {
287
+ "working": mock_collection,
288
+ "history": mock_collection,
289
+ "patterns": mock_collection,
290
+ "books": mock_collection,
291
+ "memory_bank": mock_collection,
292
+ }
293
+
294
+ return ums
295
+
296
+ @pytest.mark.asyncio
297
+ async def test_analyze_context(self, mock_ums):
298
+ """Should return context analysis result."""
299
+ # Core's analyze_conversation_context does work internally
300
+ context = await mock_ums.analyze_conversation_context(
301
+ current_message="test",
302
+ recent_conversation=[],
303
+ conversation_id="conv123"
304
+ )
305
+
306
+ # Core returns these keys
307
+ assert "relevant_patterns" in context
308
+ assert "past_outcomes" in context
309
+ assert "topic_continuity" in context
310
+ assert "proactive_insights" in context
311
+
312
+
313
+ class TestKGAccess:
314
+ """Test Knowledge Graph access."""
315
+
316
+ @pytest.fixture
317
+ def ums(self, tmp_path):
318
+ """Create UMS instance."""
319
+ return UnifiedMemorySystem(data_path=str(tmp_path / "data"))
320
+
321
+ def test_knowledge_graph_property(self, ums):
322
+ """Should expose knowledge graph."""
323
+ kg = ums.knowledge_graph
324
+
325
+ assert "routing_patterns" in kg
326
+ assert "context_action_effectiveness" in kg
327
+ assert "problem_solutions" in kg
328
+
329
+ def test_knowledge_graph_is_dict(self, ums):
330
+ """Knowledge graph should be a dict."""
331
+ assert isinstance(ums.knowledge_graph, dict)
332
+
333
+
334
+ class TestStats:
335
+ """Test statistics retrieval."""
336
+
337
+ @pytest.fixture
338
+ def mock_ums(self, tmp_path):
339
+ """Create UMS with collection mocks."""
340
+ ums = UnifiedMemorySystem(data_path=str(tmp_path / "data"))
341
+ ums.initialized = True
342
+
343
+ # Mock collections
344
+ mock_collection = MagicMock()
345
+ mock_collection.collection = MagicMock()
346
+ mock_collection.collection.count = MagicMock(return_value=10)
347
+
348
+ ums.collections = {
349
+ "working": mock_collection,
350
+ "history": mock_collection,
351
+ "patterns": mock_collection,
352
+ "books": mock_collection,
353
+ "memory_bank": mock_collection,
354
+ }
355
+
356
+ return ums
357
+
358
+ def test_get_stats(self, mock_ums):
359
+ """Should return statistics."""
360
+ stats = mock_ums.get_stats()
361
+
362
+ assert "initialized" in stats
363
+ assert "data_path" in stats
364
+ assert "collections" in stats
365
+
366
+
367
+ class TestConceptExtraction:
368
+ """Test concept extraction."""
369
+
370
+ @pytest.fixture
371
+ def ums(self, tmp_path):
372
+ """Create UMS instance."""
373
+ return UnifiedMemorySystem(data_path=str(tmp_path / "data"))
374
+
375
+ def test_extract_concepts(self, ums):
376
+ """Should extract concepts from text."""
377
+ concepts = ums._extract_concepts("Python programming language")
378
+
379
+ assert "python" in concepts
380
+ assert "programming" in concepts
381
+
382
+ def test_extract_concepts_filters_short(self, ums):
383
+ """Should filter short words."""
384
+ concepts = ums._extract_concepts("Go is a fun language")
385
+
386
+ # Short words filtered
387
+ assert "go" not in concepts
388
+ assert "is" not in concepts
389
+ assert "language" in concepts
390
+
391
+
392
+ class TestTierRecommendations:
393
+ """Test tier recommendation logic."""
394
+
395
+ @pytest.fixture
396
+ def ums(self, tmp_path):
397
+ """Create UMS with routing patterns."""
398
+ ums = UnifiedMemorySystem(data_path=str(tmp_path / "data"))
399
+ ums.knowledge_graph["routing_patterns"] = {
400
+ "python": {
401
+ "best_collection": "patterns",
402
+ "collections_used": {"patterns": {"total": 10, "successes": 8}}
403
+ }
404
+ }
405
+ return ums
406
+
407
+ def test_get_tier_recommendations(self, ums):
408
+ """Should return recommendations based on KG."""
409
+ recs = ums.get_tier_recommendations(["python"])
410
+
411
+ # Core returns these keys
412
+ assert "top_collections" in recs
413
+ assert "match_count" in recs
414
+ assert "confidence_level" in recs
415
+
416
+
417
+ if __name__ == "__main__":
418
+ pytest.main([__file__, "-v"])