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.
- roampal/__init__.py +29 -0
- roampal/__main__.py +6 -0
- roampal/backend/__init__.py +1 -0
- roampal/backend/modules/__init__.py +1 -0
- roampal/backend/modules/memory/__init__.py +43 -0
- roampal/backend/modules/memory/chromadb_adapter.py +623 -0
- roampal/backend/modules/memory/config.py +102 -0
- roampal/backend/modules/memory/content_graph.py +543 -0
- roampal/backend/modules/memory/context_service.py +455 -0
- roampal/backend/modules/memory/embedding_service.py +96 -0
- roampal/backend/modules/memory/knowledge_graph_service.py +1052 -0
- roampal/backend/modules/memory/memory_bank_service.py +433 -0
- roampal/backend/modules/memory/memory_types.py +296 -0
- roampal/backend/modules/memory/outcome_service.py +400 -0
- roampal/backend/modules/memory/promotion_service.py +473 -0
- roampal/backend/modules/memory/routing_service.py +444 -0
- roampal/backend/modules/memory/scoring_service.py +324 -0
- roampal/backend/modules/memory/search_service.py +646 -0
- roampal/backend/modules/memory/tests/__init__.py +1 -0
- roampal/backend/modules/memory/tests/conftest.py +12 -0
- roampal/backend/modules/memory/tests/unit/__init__.py +1 -0
- roampal/backend/modules/memory/tests/unit/conftest.py +7 -0
- roampal/backend/modules/memory/tests/unit/test_knowledge_graph_service.py +517 -0
- roampal/backend/modules/memory/tests/unit/test_memory_bank_service.py +504 -0
- roampal/backend/modules/memory/tests/unit/test_outcome_service.py +485 -0
- roampal/backend/modules/memory/tests/unit/test_scoring_service.py +255 -0
- roampal/backend/modules/memory/tests/unit/test_search_service.py +413 -0
- roampal/backend/modules/memory/tests/unit/test_unified_memory_system.py +418 -0
- roampal/backend/modules/memory/unified_memory_system.py +1277 -0
- roampal/cli.py +638 -0
- roampal/hooks/__init__.py +16 -0
- roampal/hooks/session_manager.py +587 -0
- roampal/hooks/stop_hook.py +176 -0
- roampal/hooks/user_prompt_submit_hook.py +103 -0
- roampal/mcp/__init__.py +7 -0
- roampal/mcp/server.py +611 -0
- roampal/server/__init__.py +7 -0
- roampal/server/main.py +744 -0
- roampal-0.1.4.dist-info/METADATA +179 -0
- roampal-0.1.4.dist-info/RECORD +44 -0
- roampal-0.1.4.dist-info/WHEEL +5 -0
- roampal-0.1.4.dist-info/entry_points.txt +2 -0
- roampal-0.1.4.dist-info/licenses/LICENSE +190 -0
- roampal-0.1.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit Tests for KnowledgeGraphService
|
|
3
|
+
|
|
4
|
+
Tests the extracted KG logic including the race condition fix.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import os
|
|
9
|
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', '..', '..', '..')))
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import json
|
|
13
|
+
import pytest
|
|
14
|
+
import tempfile
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from unittest.mock import MagicMock, AsyncMock, patch
|
|
17
|
+
|
|
18
|
+
from roampal.backend.modules.memory.knowledge_graph_service import KnowledgeGraphService
|
|
19
|
+
from roampal.backend.modules.memory.config import MemoryConfig
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TestConceptExtraction:
|
|
23
|
+
"""Test concept extraction from text."""
|
|
24
|
+
|
|
25
|
+
@pytest.fixture
|
|
26
|
+
def temp_paths(self, tmp_path):
|
|
27
|
+
"""Create temporary paths for KG files."""
|
|
28
|
+
return {
|
|
29
|
+
"kg_path": tmp_path / "knowledge_graph.json",
|
|
30
|
+
"content_graph_path": tmp_path / "content_graph.json",
|
|
31
|
+
"relationships_path": tmp_path / "relationships.json",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@pytest.fixture
|
|
35
|
+
def service(self, temp_paths):
|
|
36
|
+
"""Create KnowledgeGraphService instance."""
|
|
37
|
+
return KnowledgeGraphService(
|
|
38
|
+
kg_path=temp_paths["kg_path"],
|
|
39
|
+
content_graph_path=temp_paths["content_graph_path"],
|
|
40
|
+
relationships_path=temp_paths["relationships_path"],
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def test_extract_unigrams(self, service):
|
|
44
|
+
"""Should extract single words longer than 3 chars."""
|
|
45
|
+
concepts = service.extract_concepts("Python programming language")
|
|
46
|
+
assert "python" in concepts
|
|
47
|
+
assert "programming" in concepts
|
|
48
|
+
assert "language" in concepts
|
|
49
|
+
|
|
50
|
+
def test_extract_bigrams(self, service):
|
|
51
|
+
"""Should extract two-word phrases."""
|
|
52
|
+
concepts = service.extract_concepts("Python programming")
|
|
53
|
+
bigrams = [c for c in concepts if "_" in c and c.count("_") == 1]
|
|
54
|
+
assert len(bigrams) > 0
|
|
55
|
+
assert "python_programming" in bigrams
|
|
56
|
+
|
|
57
|
+
def test_extract_trigrams(self, service):
|
|
58
|
+
"""Should extract three-word phrases."""
|
|
59
|
+
concepts = service.extract_concepts("Python programming language rocks")
|
|
60
|
+
trigrams = [c for c in concepts if c.count("_") == 2]
|
|
61
|
+
assert len(trigrams) > 0
|
|
62
|
+
|
|
63
|
+
def test_filter_stop_words(self, service):
|
|
64
|
+
"""Should filter out stop words."""
|
|
65
|
+
concepts = service.extract_concepts("The quick brown fox is a test")
|
|
66
|
+
assert "the" not in concepts
|
|
67
|
+
assert "is" not in concepts
|
|
68
|
+
assert "quick" in concepts
|
|
69
|
+
assert "brown" in concepts
|
|
70
|
+
|
|
71
|
+
def test_filter_short_words(self, service):
|
|
72
|
+
"""Should filter out words with 3 or fewer chars."""
|
|
73
|
+
concepts = service.extract_concepts("Go is a fun language")
|
|
74
|
+
unigrams = [c for c in concepts if "_" not in c]
|
|
75
|
+
# "fun" is exactly 3 chars, should be filtered for unigrams (>3 required)
|
|
76
|
+
assert "fun" not in unigrams
|
|
77
|
+
assert "language" in unigrams
|
|
78
|
+
|
|
79
|
+
def test_filter_tool_blocklist(self, service):
|
|
80
|
+
"""Should filter out MCP tool names and internal terms."""
|
|
81
|
+
concepts = service.extract_concepts("Use search_memory to find memory_bank items")
|
|
82
|
+
assert "search_memory" not in concepts
|
|
83
|
+
assert "memory_bank" not in concepts
|
|
84
|
+
|
|
85
|
+
def test_empty_text(self, service):
|
|
86
|
+
"""Empty text should return empty list."""
|
|
87
|
+
concepts = service.extract_concepts("")
|
|
88
|
+
assert concepts == []
|
|
89
|
+
|
|
90
|
+
def test_case_insensitive(self, service):
|
|
91
|
+
"""Extraction should be case-insensitive."""
|
|
92
|
+
concepts = service.extract_concepts("Python PYTHON python")
|
|
93
|
+
python_concepts = [c for c in concepts if "python" in c.lower()]
|
|
94
|
+
# Should have python unigrams, all normalized to lowercase
|
|
95
|
+
assert "python" in concepts
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class TestKGLoading:
|
|
99
|
+
"""Test KG loading from disk."""
|
|
100
|
+
|
|
101
|
+
@pytest.fixture
|
|
102
|
+
def temp_paths(self, tmp_path):
|
|
103
|
+
return {
|
|
104
|
+
"kg_path": tmp_path / "knowledge_graph.json",
|
|
105
|
+
"content_graph_path": tmp_path / "content_graph.json",
|
|
106
|
+
"relationships_path": tmp_path / "relationships.json",
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
def test_load_empty_kg(self, temp_paths):
|
|
110
|
+
"""Should create default KG when file doesn't exist."""
|
|
111
|
+
service = KnowledgeGraphService(**temp_paths)
|
|
112
|
+
assert "routing_patterns" in service.knowledge_graph
|
|
113
|
+
assert "problem_solutions" in service.knowledge_graph
|
|
114
|
+
assert "context_action_effectiveness" in service.knowledge_graph
|
|
115
|
+
|
|
116
|
+
def test_load_existing_kg(self, temp_paths):
|
|
117
|
+
"""Should load existing KG from file."""
|
|
118
|
+
# Create a KG file first
|
|
119
|
+
kg_data = {
|
|
120
|
+
"routing_patterns": {"test_concept": {"best_collection": "history"}},
|
|
121
|
+
"success_rates": {},
|
|
122
|
+
"failure_patterns": {},
|
|
123
|
+
"problem_categories": {},
|
|
124
|
+
"problem_solutions": {},
|
|
125
|
+
"solution_patterns": {},
|
|
126
|
+
"context_action_effectiveness": {},
|
|
127
|
+
}
|
|
128
|
+
temp_paths["kg_path"].parent.mkdir(exist_ok=True, parents=True)
|
|
129
|
+
with open(temp_paths["kg_path"], "w") as f:
|
|
130
|
+
json.dump(kg_data, f)
|
|
131
|
+
|
|
132
|
+
service = KnowledgeGraphService(**temp_paths)
|
|
133
|
+
assert "test_concept" in service.knowledge_graph["routing_patterns"]
|
|
134
|
+
|
|
135
|
+
def test_load_partial_kg_fills_missing_keys(self, temp_paths):
|
|
136
|
+
"""Should fill missing keys when loading partial KG."""
|
|
137
|
+
# Create a partial KG file
|
|
138
|
+
kg_data = {"routing_patterns": {}}
|
|
139
|
+
temp_paths["kg_path"].parent.mkdir(exist_ok=True, parents=True)
|
|
140
|
+
with open(temp_paths["kg_path"], "w") as f:
|
|
141
|
+
json.dump(kg_data, f)
|
|
142
|
+
|
|
143
|
+
service = KnowledgeGraphService(**temp_paths)
|
|
144
|
+
# Should have all required keys
|
|
145
|
+
assert "problem_solutions" in service.knowledge_graph
|
|
146
|
+
assert "context_action_effectiveness" in service.knowledge_graph
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class TestConceptRelationships:
|
|
150
|
+
"""Test building concept relationships."""
|
|
151
|
+
|
|
152
|
+
@pytest.fixture
|
|
153
|
+
def temp_paths(self, tmp_path):
|
|
154
|
+
return {
|
|
155
|
+
"kg_path": tmp_path / "knowledge_graph.json",
|
|
156
|
+
"content_graph_path": tmp_path / "content_graph.json",
|
|
157
|
+
"relationships_path": tmp_path / "relationships.json",
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@pytest.fixture
|
|
161
|
+
def service(self, temp_paths):
|
|
162
|
+
return KnowledgeGraphService(**temp_paths)
|
|
163
|
+
|
|
164
|
+
def test_build_concept_relationships(self, service):
|
|
165
|
+
"""Should create relationships between concept pairs."""
|
|
166
|
+
concepts = ["python", "django", "web"]
|
|
167
|
+
service.build_concept_relationships(concepts)
|
|
168
|
+
|
|
169
|
+
relationships = service.knowledge_graph.get("relationships", {})
|
|
170
|
+
# Should have 3 relationships: python-django, python-web, django-web
|
|
171
|
+
assert len(relationships) == 3
|
|
172
|
+
|
|
173
|
+
# Check relationship keys are sorted
|
|
174
|
+
assert "django|python" in relationships # Sorted alphabetically
|
|
175
|
+
assert "python|web" in relationships
|
|
176
|
+
assert "django|web" in relationships
|
|
177
|
+
|
|
178
|
+
def test_relationship_co_occurrence_increments(self, service):
|
|
179
|
+
"""Co-occurrence should increment on repeated builds."""
|
|
180
|
+
concepts = ["python", "django"]
|
|
181
|
+
service.build_concept_relationships(concepts)
|
|
182
|
+
service.build_concept_relationships(concepts)
|
|
183
|
+
|
|
184
|
+
rel_key = "django|python"
|
|
185
|
+
assert service.knowledge_graph["relationships"][rel_key]["co_occurrence"] == 2
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class TestKGRouting:
|
|
189
|
+
"""Test KG routing pattern updates."""
|
|
190
|
+
|
|
191
|
+
@pytest.fixture
|
|
192
|
+
def temp_paths(self, tmp_path):
|
|
193
|
+
return {
|
|
194
|
+
"kg_path": tmp_path / "knowledge_graph.json",
|
|
195
|
+
"content_graph_path": tmp_path / "content_graph.json",
|
|
196
|
+
"relationships_path": tmp_path / "relationships.json",
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
@pytest.fixture
|
|
200
|
+
def service(self, temp_paths):
|
|
201
|
+
return KnowledgeGraphService(**temp_paths)
|
|
202
|
+
|
|
203
|
+
@pytest.mark.asyncio
|
|
204
|
+
async def test_update_kg_routing_creates_pattern(self, service):
|
|
205
|
+
"""Should create routing pattern for new concept."""
|
|
206
|
+
await service.update_kg_routing("Python tutorial", "books", "worked")
|
|
207
|
+
|
|
208
|
+
patterns = service.knowledge_graph["routing_patterns"]
|
|
209
|
+
assert "python" in patterns
|
|
210
|
+
assert patterns["python"]["best_collection"] == "books"
|
|
211
|
+
|
|
212
|
+
@pytest.mark.asyncio
|
|
213
|
+
async def test_update_kg_routing_tracks_outcomes(self, service):
|
|
214
|
+
"""Should track success/failure outcomes."""
|
|
215
|
+
await service.update_kg_routing("Python help", "history", "worked")
|
|
216
|
+
await service.update_kg_routing("Python help", "history", "failed")
|
|
217
|
+
|
|
218
|
+
stats = service.knowledge_graph["routing_patterns"]["python"]["collections_used"]["history"]
|
|
219
|
+
assert stats["successes"] == 1
|
|
220
|
+
assert stats["failures"] == 1
|
|
221
|
+
assert stats["total"] == 2
|
|
222
|
+
|
|
223
|
+
@pytest.mark.asyncio
|
|
224
|
+
async def test_update_kg_routing_updates_best_collection(self, service):
|
|
225
|
+
"""Should update best collection based on success rate."""
|
|
226
|
+
# Books has 2 successes
|
|
227
|
+
await service.update_kg_routing("Python docs", "books", "worked")
|
|
228
|
+
await service.update_kg_routing("Python docs", "books", "worked")
|
|
229
|
+
|
|
230
|
+
# History has 1 success, 1 failure
|
|
231
|
+
await service.update_kg_routing("Python help", "history", "worked")
|
|
232
|
+
await service.update_kg_routing("Python help", "history", "failed")
|
|
233
|
+
|
|
234
|
+
pattern = service.knowledge_graph["routing_patterns"]["python"]
|
|
235
|
+
assert pattern["best_collection"] == "books" # 100% vs 50%
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class TestRaceConditionFix:
|
|
239
|
+
"""Test the race condition fix in debounced saves."""
|
|
240
|
+
|
|
241
|
+
@pytest.fixture
|
|
242
|
+
def temp_paths(self, tmp_path):
|
|
243
|
+
return {
|
|
244
|
+
"kg_path": tmp_path / "knowledge_graph.json",
|
|
245
|
+
"content_graph_path": tmp_path / "content_graph.json",
|
|
246
|
+
"relationships_path": tmp_path / "relationships.json",
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
@pytest.fixture
|
|
250
|
+
def service(self, temp_paths):
|
|
251
|
+
# Use short debounce for testing
|
|
252
|
+
config = MemoryConfig(kg_debounce_seconds=0.1)
|
|
253
|
+
return KnowledgeGraphService(**temp_paths, config=config)
|
|
254
|
+
|
|
255
|
+
@pytest.mark.asyncio
|
|
256
|
+
async def test_debounced_save_serializes_access(self, service):
|
|
257
|
+
"""Multiple concurrent debounced saves should not race."""
|
|
258
|
+
# Launch multiple concurrent saves
|
|
259
|
+
tasks = [
|
|
260
|
+
asyncio.create_task(service._debounced_save_kg())
|
|
261
|
+
for _ in range(10)
|
|
262
|
+
]
|
|
263
|
+
await asyncio.gather(*tasks)
|
|
264
|
+
|
|
265
|
+
# Wait for debounce to complete
|
|
266
|
+
await asyncio.sleep(0.2)
|
|
267
|
+
|
|
268
|
+
# Should have completed without errors
|
|
269
|
+
assert not service._kg_save_pending or service._kg_save_task.done()
|
|
270
|
+
|
|
271
|
+
@pytest.mark.asyncio
|
|
272
|
+
async def test_debounced_save_batches_updates(self, service):
|
|
273
|
+
"""Multiple updates within debounce window should batch."""
|
|
274
|
+
save_count = 0
|
|
275
|
+
original_save = service._save_kg
|
|
276
|
+
|
|
277
|
+
async def counting_save():
|
|
278
|
+
nonlocal save_count
|
|
279
|
+
save_count += 1
|
|
280
|
+
await original_save()
|
|
281
|
+
|
|
282
|
+
service._save_kg = counting_save
|
|
283
|
+
|
|
284
|
+
# Rapid updates
|
|
285
|
+
for i in range(5):
|
|
286
|
+
await service._debounced_save_kg()
|
|
287
|
+
|
|
288
|
+
# Wait for debounce
|
|
289
|
+
await asyncio.sleep(0.2)
|
|
290
|
+
|
|
291
|
+
# Should only save once due to batching
|
|
292
|
+
assert save_count == 1
|
|
293
|
+
|
|
294
|
+
@pytest.mark.asyncio
|
|
295
|
+
async def test_cleanup_cancels_pending_save(self, service):
|
|
296
|
+
"""Cleanup should cancel pending save task."""
|
|
297
|
+
await service._debounced_save_kg()
|
|
298
|
+
assert service._kg_save_task is not None
|
|
299
|
+
|
|
300
|
+
await service.cleanup()
|
|
301
|
+
|
|
302
|
+
# Task should be cancelled or done
|
|
303
|
+
assert service._kg_save_task.done() or service._kg_save_task.cancelled()
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class TestProblemSolutionTracking:
|
|
307
|
+
"""Test problem-solution pattern tracking."""
|
|
308
|
+
|
|
309
|
+
@pytest.fixture
|
|
310
|
+
def temp_paths(self, tmp_path):
|
|
311
|
+
return {
|
|
312
|
+
"kg_path": tmp_path / "knowledge_graph.json",
|
|
313
|
+
"content_graph_path": tmp_path / "content_graph.json",
|
|
314
|
+
"relationships_path": tmp_path / "relationships.json",
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
@pytest.fixture
|
|
318
|
+
def service(self, temp_paths):
|
|
319
|
+
config = MemoryConfig(kg_debounce_seconds=0) # No debounce for tests
|
|
320
|
+
return KnowledgeGraphService(**temp_paths, config=config)
|
|
321
|
+
|
|
322
|
+
@pytest.mark.asyncio
|
|
323
|
+
async def test_track_problem_solution_creates_entry(self, service):
|
|
324
|
+
"""Should create problem-solution mapping."""
|
|
325
|
+
metadata = {
|
|
326
|
+
"original_context": "How do I fix Python import errors?",
|
|
327
|
+
"text": "Use sys.path.insert to add the module path",
|
|
328
|
+
}
|
|
329
|
+
await service.track_problem_solution("doc_123", metadata, None)
|
|
330
|
+
|
|
331
|
+
assert len(service.knowledge_graph["problem_solutions"]) > 0
|
|
332
|
+
|
|
333
|
+
@pytest.mark.asyncio
|
|
334
|
+
async def test_track_problem_solution_increments_on_repeat(self, service):
|
|
335
|
+
"""Should increment success_count for existing solutions."""
|
|
336
|
+
metadata = {
|
|
337
|
+
"original_context": "Python import errors fix",
|
|
338
|
+
"text": "Use sys.path.insert",
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
await service.track_problem_solution("doc_123", metadata, None)
|
|
342
|
+
await service.track_problem_solution("doc_123", metadata, None)
|
|
343
|
+
|
|
344
|
+
# Find the entry
|
|
345
|
+
for sig, solutions in service.knowledge_graph["problem_solutions"].items():
|
|
346
|
+
for sol in solutions:
|
|
347
|
+
if sol["doc_id"] == "doc_123":
|
|
348
|
+
assert sol["success_count"] == 2
|
|
349
|
+
return
|
|
350
|
+
|
|
351
|
+
pytest.fail("Solution not found")
|
|
352
|
+
|
|
353
|
+
@pytest.mark.asyncio
|
|
354
|
+
async def test_find_known_solutions_exact_match(self, service):
|
|
355
|
+
"""Should find exact problem-solution matches."""
|
|
356
|
+
# First track a solution
|
|
357
|
+
metadata = {
|
|
358
|
+
"original_context": "Python import errors debugging",
|
|
359
|
+
"text": "Use sys.path.insert to fix",
|
|
360
|
+
}
|
|
361
|
+
await service.track_problem_solution("history_doc_1", metadata, None)
|
|
362
|
+
|
|
363
|
+
# Mock get_fragment_fn
|
|
364
|
+
def get_fragment(coll_name, doc_id):
|
|
365
|
+
if doc_id == "history_doc_1":
|
|
366
|
+
return {
|
|
367
|
+
"id": doc_id,
|
|
368
|
+
"content": "Use sys.path.insert to fix",
|
|
369
|
+
"distance": 1.0,
|
|
370
|
+
}
|
|
371
|
+
return None
|
|
372
|
+
|
|
373
|
+
# Search for similar problem
|
|
374
|
+
solutions = await service.find_known_solutions(
|
|
375
|
+
"Python import errors debugging",
|
|
376
|
+
get_fragment
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
# Should find the tracked solution
|
|
380
|
+
assert len(solutions) > 0
|
|
381
|
+
assert solutions[0]["id"] == "history_doc_1"
|
|
382
|
+
assert solutions[0]["is_known_solution"] is True
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
class TestKGCleanup:
|
|
386
|
+
"""Test KG cleanup operations."""
|
|
387
|
+
|
|
388
|
+
@pytest.fixture
|
|
389
|
+
def temp_paths(self, tmp_path):
|
|
390
|
+
return {
|
|
391
|
+
"kg_path": tmp_path / "knowledge_graph.json",
|
|
392
|
+
"content_graph_path": tmp_path / "content_graph.json",
|
|
393
|
+
"relationships_path": tmp_path / "relationships.json",
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
@pytest.fixture
|
|
397
|
+
def service(self, temp_paths):
|
|
398
|
+
return KnowledgeGraphService(**temp_paths)
|
|
399
|
+
|
|
400
|
+
@pytest.mark.asyncio
|
|
401
|
+
async def test_cleanup_dead_references(self, service):
|
|
402
|
+
"""Should remove references to non-existent documents."""
|
|
403
|
+
# Add some problem_solutions with doc_ids
|
|
404
|
+
service.knowledge_graph["problem_solutions"] = {
|
|
405
|
+
"test_problem": [
|
|
406
|
+
{"doc_id": "history_valid", "success_count": 1},
|
|
407
|
+
{"doc_id": "history_invalid", "success_count": 1},
|
|
408
|
+
]
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
# Mock doc_exists - only valid doc exists
|
|
412
|
+
def doc_exists(doc_id):
|
|
413
|
+
return doc_id == "history_valid"
|
|
414
|
+
|
|
415
|
+
cleaned = await service.cleanup_dead_references(doc_exists)
|
|
416
|
+
|
|
417
|
+
# Should have cleaned 1 reference
|
|
418
|
+
assert cleaned == 1
|
|
419
|
+
solutions = service.knowledge_graph["problem_solutions"]["test_problem"]
|
|
420
|
+
assert len(solutions) == 1
|
|
421
|
+
assert solutions[0]["doc_id"] == "history_valid"
|
|
422
|
+
|
|
423
|
+
@pytest.mark.asyncio
|
|
424
|
+
async def test_cleanup_action_kg_for_doc_ids(self, service):
|
|
425
|
+
"""Should remove Action KG examples for deleted doc_ids."""
|
|
426
|
+
service.knowledge_graph["context_action_effectiveness"] = {
|
|
427
|
+
"context|action|coll": {
|
|
428
|
+
"successes": 2,
|
|
429
|
+
"failures": 0,
|
|
430
|
+
"examples": [
|
|
431
|
+
{"doc_id": "doc_1", "text": "example 1"},
|
|
432
|
+
{"doc_id": "doc_2", "text": "example 2"},
|
|
433
|
+
{"doc_id": "doc_3", "text": "example 3"},
|
|
434
|
+
]
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
cleaned = await service.cleanup_action_kg_for_doc_ids(["doc_1", "doc_3"])
|
|
439
|
+
|
|
440
|
+
assert cleaned == 2
|
|
441
|
+
examples = service.knowledge_graph["context_action_effectiveness"]["context|action|coll"]["examples"]
|
|
442
|
+
assert len(examples) == 1
|
|
443
|
+
assert examples[0]["doc_id"] == "doc_2"
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
class TestKGEntitiesVisualization:
|
|
447
|
+
"""Test KG entity retrieval for visualization."""
|
|
448
|
+
|
|
449
|
+
@pytest.fixture
|
|
450
|
+
def temp_paths(self, tmp_path):
|
|
451
|
+
return {
|
|
452
|
+
"kg_path": tmp_path / "knowledge_graph.json",
|
|
453
|
+
"content_graph_path": tmp_path / "content_graph.json",
|
|
454
|
+
"relationships_path": tmp_path / "relationships.json",
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
@pytest.fixture
|
|
458
|
+
def service(self, temp_paths):
|
|
459
|
+
return KnowledgeGraphService(**temp_paths)
|
|
460
|
+
|
|
461
|
+
@pytest.mark.asyncio
|
|
462
|
+
async def test_get_kg_entities_routing(self, service):
|
|
463
|
+
"""Should return routing KG entities."""
|
|
464
|
+
service.knowledge_graph["routing_patterns"] = {
|
|
465
|
+
"python": {
|
|
466
|
+
"collections_used": {"history": {"total": 5}},
|
|
467
|
+
"best_collection": "history",
|
|
468
|
+
"success_rate": 0.8,
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
# Save to disk so reload_kg finds it (get_kg_entities calls reload_kg)
|
|
472
|
+
service._save_kg_sync()
|
|
473
|
+
|
|
474
|
+
entities = await service.get_kg_entities()
|
|
475
|
+
|
|
476
|
+
assert len(entities) > 0
|
|
477
|
+
python_entity = next((e for e in entities if e["entity"] == "python"), None)
|
|
478
|
+
assert python_entity is not None
|
|
479
|
+
assert python_entity["source"] == "routing"
|
|
480
|
+
assert python_entity["success_rate"] == 0.8
|
|
481
|
+
|
|
482
|
+
@pytest.mark.asyncio
|
|
483
|
+
async def test_get_kg_entities_with_filter(self, service):
|
|
484
|
+
"""Should filter entities by text."""
|
|
485
|
+
service.knowledge_graph["routing_patterns"] = {
|
|
486
|
+
"python": {"collections_used": {"history": {"total": 5}}, "best_collection": "history"},
|
|
487
|
+
"javascript": {"collections_used": {"books": {"total": 3}}, "best_collection": "books"},
|
|
488
|
+
}
|
|
489
|
+
# Save to disk so reload_kg finds it
|
|
490
|
+
service._save_kg_sync()
|
|
491
|
+
|
|
492
|
+
entities = await service.get_kg_entities(filter_text="python")
|
|
493
|
+
|
|
494
|
+
assert len(entities) == 1
|
|
495
|
+
assert entities[0]["entity"] == "python"
|
|
496
|
+
|
|
497
|
+
@pytest.mark.asyncio
|
|
498
|
+
async def test_get_kg_relationships(self, service):
|
|
499
|
+
"""Should return merged relationships."""
|
|
500
|
+
service.knowledge_graph["relationships"] = {
|
|
501
|
+
"django|python": {
|
|
502
|
+
"co_occurrence": 10,
|
|
503
|
+
"success_together": 5,
|
|
504
|
+
"failure_together": 1,
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
relationships = await service.get_kg_relationships("python")
|
|
509
|
+
|
|
510
|
+
assert len(relationships) > 0
|
|
511
|
+
django_rel = next((r for r in relationships if r["related_entity"] == "django"), None)
|
|
512
|
+
assert django_rel is not None
|
|
513
|
+
assert django_rel["strength"] == 10
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
if __name__ == "__main__":
|
|
517
|
+
pytest.main([__file__, "-v"])
|