superlocalmemory 2.6.5 → 2.7.1
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.
- package/CHANGELOG.md +57 -0
- package/README.md +96 -13
- package/bin/slm +179 -3
- package/bin/superlocalmemoryv2:learning +4 -0
- package/bin/superlocalmemoryv2:patterns +4 -0
- package/docs/ARCHITECTURE.md +12 -6
- package/docs/MCP-MANUAL-SETUP.md +14 -4
- package/install.sh +125 -3
- package/mcp_server.py +291 -1
- package/package.json +2 -1
- package/requirements-learning.txt +12 -0
- package/scripts/verify-v27.sh +233 -0
- package/skills/slm-show-patterns/SKILL.md +224 -0
- package/src/learning/synthetic_bootstrap.py +1047 -0
- package/src/learning/tests/__init__.py +0 -0
- package/src/learning/tests/test_adaptive_ranker.py +328 -0
- package/src/learning/tests/test_aggregator.py +309 -0
- package/src/learning/tests/test_feedback_collector.py +295 -0
- package/src/learning/tests/test_learning_db.py +606 -0
- package/src/learning/tests/test_project_context.py +296 -0
- package/src/learning/tests/test_source_quality.py +355 -0
- package/src/learning/tests/test_synthetic_bootstrap.py +433 -0
- package/src/learning/tests/test_workflow_miner.py +322 -0
- package/ui/index.html +144 -0
- package/ui/js/init.js +4 -0
- package/ui/js/learning.js +318 -0
- package/ui_server.py +9 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
SuperLocalMemory V2 - Tests for WorkflowPatternMiner (v2.7)
|
|
4
|
+
Copyright (c) 2026 Varun Pratap Bhardwaj
|
|
5
|
+
Licensed under MIT License
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# ---------------------------------------------------------------------------
|
|
12
|
+
# Fixtures
|
|
13
|
+
# ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
@pytest.fixture(autouse=True)
|
|
16
|
+
def reset_singleton():
|
|
17
|
+
from src.learning.learning_db import LearningDB
|
|
18
|
+
LearningDB.reset_instance()
|
|
19
|
+
yield
|
|
20
|
+
LearningDB.reset_instance()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def learning_db(tmp_path):
|
|
25
|
+
from src.learning.learning_db import LearningDB
|
|
26
|
+
db_path = tmp_path / "learning.db"
|
|
27
|
+
return LearningDB(db_path=db_path)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.fixture
|
|
31
|
+
def miner(learning_db):
|
|
32
|
+
from src.learning.workflow_pattern_miner import WorkflowPatternMiner
|
|
33
|
+
return WorkflowPatternMiner(learning_db=learning_db)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.fixture
|
|
37
|
+
def miner_no_db():
|
|
38
|
+
from src.learning.workflow_pattern_miner import WorkflowPatternMiner
|
|
39
|
+
return WorkflowPatternMiner(learning_db=None)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
# Activity Classification
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
class TestClassifyActivity:
|
|
47
|
+
"""Test _classify_activity for all 7 types + unknown."""
|
|
48
|
+
|
|
49
|
+
def test_docs(self, miner):
|
|
50
|
+
assert miner._classify_activity("Updated the documentation for API") == "docs"
|
|
51
|
+
assert miner._classify_activity("Added README and wiki pages") == "docs"
|
|
52
|
+
|
|
53
|
+
def test_architecture(self, miner):
|
|
54
|
+
assert miner._classify_activity("Designed the system architecture") == "architecture"
|
|
55
|
+
assert miner._classify_activity("Created ERD for data model") == "architecture"
|
|
56
|
+
|
|
57
|
+
def test_code(self, miner):
|
|
58
|
+
assert miner._classify_activity("Implemented the new function for parsing") == "code"
|
|
59
|
+
assert miner._classify_activity("Refactored the module structure") == "code"
|
|
60
|
+
|
|
61
|
+
def test_test(self, miner):
|
|
62
|
+
assert miner._classify_activity("Added pytest fixtures and assertions") == "test"
|
|
63
|
+
assert miner._classify_activity("Wrote unit test for the parser") == "test"
|
|
64
|
+
|
|
65
|
+
def test_debug(self, miner):
|
|
66
|
+
assert miner._classify_activity("Fixed the bug in authentication") == "debug"
|
|
67
|
+
assert miner._classify_activity("Analyzed error traceback in production") == "debug"
|
|
68
|
+
|
|
69
|
+
def test_deploy(self, miner):
|
|
70
|
+
assert miner._classify_activity("Deployed via Docker to production") == "deploy"
|
|
71
|
+
assert miner._classify_activity("Updated the CI/CD pipeline") == "deploy"
|
|
72
|
+
|
|
73
|
+
def test_config(self, miner):
|
|
74
|
+
assert miner._classify_activity("Updated config settings and env variables") == "config"
|
|
75
|
+
assert miner._classify_activity("Added package dependency to requirements") == "config"
|
|
76
|
+
|
|
77
|
+
def test_unknown(self, miner):
|
|
78
|
+
assert miner._classify_activity("Had a meeting about the roadmap") == "unknown"
|
|
79
|
+
assert miner._classify_activity("") == "unknown"
|
|
80
|
+
assert miner._classify_activity("random unrelated text") == "unknown"
|
|
81
|
+
|
|
82
|
+
def test_word_boundary_matching(self, miner):
|
|
83
|
+
"""'test' should match 'test' but not 'latest'."""
|
|
84
|
+
assert miner._classify_activity("ran the test suite") == "test"
|
|
85
|
+
# "latest" contains "test" but word boundary prevents match
|
|
86
|
+
# However, 'release' is a deploy keyword
|
|
87
|
+
result = miner._classify_activity("checked the latest version of release")
|
|
88
|
+
assert result == "deploy" # 'release' is deploy keyword
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
# Hour to Bucket Mapping
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
class TestHourToBucket:
|
|
96
|
+
def test_night_hours(self, miner):
|
|
97
|
+
for hour in [0, 1, 2, 3, 4, 5]:
|
|
98
|
+
assert miner._hour_to_bucket(hour) == "night", f"hour={hour}"
|
|
99
|
+
|
|
100
|
+
def test_morning_hours(self, miner):
|
|
101
|
+
for hour in [6, 7, 8, 9, 10, 11]:
|
|
102
|
+
assert miner._hour_to_bucket(hour) == "morning", f"hour={hour}"
|
|
103
|
+
|
|
104
|
+
def test_afternoon_hours(self, miner):
|
|
105
|
+
for hour in [12, 13, 14, 15, 16, 17]:
|
|
106
|
+
assert miner._hour_to_bucket(hour) == "afternoon", f"hour={hour}"
|
|
107
|
+
|
|
108
|
+
def test_evening_hours(self, miner):
|
|
109
|
+
for hour in [18, 19, 20, 21, 22, 23]:
|
|
110
|
+
assert miner._hour_to_bucket(hour) == "evening", f"hour={hour}"
|
|
111
|
+
|
|
112
|
+
def test_boundary_cases(self, miner):
|
|
113
|
+
assert miner._hour_to_bucket(0) == "night"
|
|
114
|
+
assert miner._hour_to_bucket(5) == "night"
|
|
115
|
+
assert miner._hour_to_bucket(6) == "morning"
|
|
116
|
+
assert miner._hour_to_bucket(11) == "morning"
|
|
117
|
+
assert miner._hour_to_bucket(12) == "afternoon"
|
|
118
|
+
assert miner._hour_to_bucket(17) == "afternoon"
|
|
119
|
+
assert miner._hour_to_bucket(18) == "evening"
|
|
120
|
+
assert miner._hour_to_bucket(23) == "evening"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
# Sequence Mining
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
class TestMineSequences:
|
|
128
|
+
def test_basic_sequence(self, miner):
|
|
129
|
+
"""A repeating pattern should be detected."""
|
|
130
|
+
memories = []
|
|
131
|
+
# Repeat: docs -> code -> test, 5 times
|
|
132
|
+
activity_words = {
|
|
133
|
+
"docs": "Updated the documentation",
|
|
134
|
+
"code": "Implemented new feature function",
|
|
135
|
+
"test": "Added pytest coverage",
|
|
136
|
+
}
|
|
137
|
+
for _ in range(5):
|
|
138
|
+
for act in ["docs", "code", "test"]:
|
|
139
|
+
memories.append({
|
|
140
|
+
"content": activity_words[act],
|
|
141
|
+
"created_at": "2026-02-16 10:00:00",
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
results = miner.mine_sequences(memories=memories, min_support=0.1)
|
|
145
|
+
assert len(results) > 0
|
|
146
|
+
# The docs->code->test pattern should appear
|
|
147
|
+
sequences_as_tuples = [tuple(r["sequence"]) for r in results]
|
|
148
|
+
assert ("docs", "code") in sequences_as_tuples or \
|
|
149
|
+
("code", "test") in sequences_as_tuples or \
|
|
150
|
+
("docs", "code", "test") in sequences_as_tuples
|
|
151
|
+
|
|
152
|
+
def test_min_support_filter(self, miner):
|
|
153
|
+
"""High min_support should filter out rare 2-gram patterns.
|
|
154
|
+
|
|
155
|
+
With 4 distinct activities (docs, code, deploy, debug), each 2-gram
|
|
156
|
+
has support 1/3 ~ 0.33. Setting min_support=0.5 should exclude them.
|
|
157
|
+
Note: longer n-grams (4-gram) may have support=1.0 because there is
|
|
158
|
+
only 1 window of that length, so we specifically check 2-grams.
|
|
159
|
+
"""
|
|
160
|
+
memories = [
|
|
161
|
+
{"content": "Updated documentation", "created_at": "2026-02-16 10:00:00"},
|
|
162
|
+
{"content": "Implemented function code", "created_at": "2026-02-16 11:00:00"},
|
|
163
|
+
{"content": "Deployed to production", "created_at": "2026-02-16 12:00:00"},
|
|
164
|
+
{"content": "Fixed the error bug", "created_at": "2026-02-16 13:00:00"},
|
|
165
|
+
]
|
|
166
|
+
results = miner.mine_sequences(memories=memories, min_support=0.5)
|
|
167
|
+
# Filter results to only 2-grams (which cannot reach 0.5 support)
|
|
168
|
+
bigram_results = [r for r in results if r["length"] == 2]
|
|
169
|
+
assert len(bigram_results) == 0
|
|
170
|
+
|
|
171
|
+
def test_empty_memories(self, miner):
|
|
172
|
+
results = miner.mine_sequences(memories=[], min_support=0.1)
|
|
173
|
+
assert results == []
|
|
174
|
+
|
|
175
|
+
def test_single_memory(self, miner):
|
|
176
|
+
"""Need at least 2 classified activities for sequence mining."""
|
|
177
|
+
memories = [{"content": "one code function", "created_at": "2026-02-16 10:00:00"}]
|
|
178
|
+
results = miner.mine_sequences(memories=memories, min_support=0.1)
|
|
179
|
+
assert results == []
|
|
180
|
+
|
|
181
|
+
def test_consecutive_identical_filtered(self, miner):
|
|
182
|
+
"""N-grams with consecutive identical activities are filtered as noise."""
|
|
183
|
+
memories = [
|
|
184
|
+
{"content": "code function", "created_at": "2026-02-16 10:00:00"},
|
|
185
|
+
{"content": "code function", "created_at": "2026-02-16 11:00:00"},
|
|
186
|
+
{"content": "code function", "created_at": "2026-02-16 12:00:00"},
|
|
187
|
+
]
|
|
188
|
+
results = miner.mine_sequences(memories=memories, min_support=0.1)
|
|
189
|
+
# All n-grams would be (code, code) or (code, code, code) -> filtered
|
|
190
|
+
assert len(results) == 0
|
|
191
|
+
|
|
192
|
+
def test_top_20_limit(self, miner):
|
|
193
|
+
"""Results should be capped at 20."""
|
|
194
|
+
memories = []
|
|
195
|
+
types = ["docs", "code", "test", "debug", "deploy", "config"]
|
|
196
|
+
for i in range(100):
|
|
197
|
+
memories.append({
|
|
198
|
+
"content": f"Working on {types[i % len(types)]} implementation",
|
|
199
|
+
"created_at": f"2026-02-16 {i % 24:02d}:00:00",
|
|
200
|
+
})
|
|
201
|
+
results = miner.mine_sequences(memories=memories, min_support=0.01)
|
|
202
|
+
assert len(results) <= 20
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# ---------------------------------------------------------------------------
|
|
206
|
+
# Temporal Pattern Mining
|
|
207
|
+
# ---------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
class TestMineTemporalPatterns:
|
|
210
|
+
def test_morning_coding(self, miner):
|
|
211
|
+
"""Memories at morning hours with code content -> morning=code."""
|
|
212
|
+
memories = []
|
|
213
|
+
for hour in range(6, 12):
|
|
214
|
+
memories.append({
|
|
215
|
+
"content": "Implemented new function in the module",
|
|
216
|
+
"created_at": f"2026-02-16 {hour:02d}:30:00",
|
|
217
|
+
})
|
|
218
|
+
result = miner.mine_temporal_patterns(memories=memories)
|
|
219
|
+
assert "morning" in result
|
|
220
|
+
assert result["morning"]["dominant_activity"] == "code"
|
|
221
|
+
assert result["morning"]["evidence_count"] >= 5
|
|
222
|
+
|
|
223
|
+
def test_minimum_evidence_threshold(self, miner):
|
|
224
|
+
"""Buckets with fewer than 5 evidence memories are omitted."""
|
|
225
|
+
memories = [
|
|
226
|
+
{"content": "Wrote code function", "created_at": "2026-02-16 09:00:00"},
|
|
227
|
+
{"content": "Wrote code class", "created_at": "2026-02-16 10:00:00"},
|
|
228
|
+
]
|
|
229
|
+
result = miner.mine_temporal_patterns(memories=memories)
|
|
230
|
+
assert "morning" not in result # Only 2 morning memories, need 5+
|
|
231
|
+
|
|
232
|
+
def test_time_bucketing(self, miner):
|
|
233
|
+
"""Verify that memories are assigned to the correct time bucket."""
|
|
234
|
+
memories = []
|
|
235
|
+
# 6 evening test activities (18-23)
|
|
236
|
+
for i in range(6):
|
|
237
|
+
memories.append({
|
|
238
|
+
"content": f"Running pytest assertions {i}",
|
|
239
|
+
"created_at": f"2026-02-16 {20 + (i % 4):02d}:00:00",
|
|
240
|
+
})
|
|
241
|
+
result = miner.mine_temporal_patterns(memories=memories)
|
|
242
|
+
if "evening" in result:
|
|
243
|
+
assert result["evening"]["dominant_activity"] == "test"
|
|
244
|
+
|
|
245
|
+
def test_empty_memories(self, miner):
|
|
246
|
+
result = miner.mine_temporal_patterns(memories=[])
|
|
247
|
+
assert result == {}
|
|
248
|
+
|
|
249
|
+
def test_only_unknown_activities(self, miner):
|
|
250
|
+
"""All unknown activities should produce empty result."""
|
|
251
|
+
memories = [
|
|
252
|
+
{"content": "random chat", "created_at": f"2026-02-16 {h:02d}:00:00"}
|
|
253
|
+
for h in range(6, 12)
|
|
254
|
+
]
|
|
255
|
+
result = miner.mine_temporal_patterns(memories=memories)
|
|
256
|
+
assert "morning" not in result
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# ---------------------------------------------------------------------------
|
|
260
|
+
# mine_all + persistence
|
|
261
|
+
# ---------------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
class TestMineAll:
|
|
264
|
+
def test_mine_all_persists(self, miner, learning_db):
|
|
265
|
+
"""mine_all should store patterns in learning_db."""
|
|
266
|
+
memories = []
|
|
267
|
+
for _ in range(3):
|
|
268
|
+
memories.extend([
|
|
269
|
+
{"content": "docs documentation wiki", "created_at": "2026-02-16 09:00:00"},
|
|
270
|
+
{"content": "code implement function", "created_at": "2026-02-16 10:00:00"},
|
|
271
|
+
{"content": "test pytest assertion", "created_at": "2026-02-16 11:00:00"},
|
|
272
|
+
])
|
|
273
|
+
|
|
274
|
+
results = miner.mine_all(memories=memories)
|
|
275
|
+
assert "sequences" in results
|
|
276
|
+
assert "temporal" in results
|
|
277
|
+
|
|
278
|
+
# Check that patterns were stored
|
|
279
|
+
stored_seq = learning_db.get_workflow_patterns(pattern_type="sequence")
|
|
280
|
+
stored_temp = learning_db.get_workflow_patterns(pattern_type="temporal")
|
|
281
|
+
total_stored = len(stored_seq) + len(stored_temp)
|
|
282
|
+
assert total_stored >= 0 # At least attempted storage
|
|
283
|
+
|
|
284
|
+
def test_mine_all_no_db(self, miner_no_db):
|
|
285
|
+
"""mine_all without DB should still return results, just not persist."""
|
|
286
|
+
memories = [
|
|
287
|
+
{"content": "docs documentation", "created_at": "2026-02-16 09:00:00"},
|
|
288
|
+
{"content": "code function", "created_at": "2026-02-16 10:00:00"},
|
|
289
|
+
]
|
|
290
|
+
results = miner_no_db.mine_all(memories=memories)
|
|
291
|
+
assert "sequences" in results
|
|
292
|
+
assert "temporal" in results
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
# ---------------------------------------------------------------------------
|
|
296
|
+
# Parse Hour
|
|
297
|
+
# ---------------------------------------------------------------------------
|
|
298
|
+
|
|
299
|
+
class TestParseHour:
|
|
300
|
+
def test_iso_format(self, miner):
|
|
301
|
+
assert miner._parse_hour("2026-02-16T09:30:00") == 9
|
|
302
|
+
|
|
303
|
+
def test_sqlite_format(self, miner):
|
|
304
|
+
assert miner._parse_hour("2026-02-16 14:45:00") == 14
|
|
305
|
+
|
|
306
|
+
def test_with_microseconds(self, miner):
|
|
307
|
+
assert miner._parse_hour("2026-02-16 23:59:59.123456") == 23
|
|
308
|
+
|
|
309
|
+
def test_date_only_returns_none(self, miner):
|
|
310
|
+
# fromisoformat may or may not parse date-only; if it does, hour=0
|
|
311
|
+
result = miner._parse_hour("2026-02-16")
|
|
312
|
+
# Either None or 0 is acceptable
|
|
313
|
+
assert result is None or result == 0
|
|
314
|
+
|
|
315
|
+
def test_none_input(self, miner):
|
|
316
|
+
assert miner._parse_hour(None) is None
|
|
317
|
+
|
|
318
|
+
def test_empty_string(self, miner):
|
|
319
|
+
assert miner._parse_hour("") is None
|
|
320
|
+
|
|
321
|
+
def test_invalid_string(self, miner):
|
|
322
|
+
assert miner._parse_hour("not-a-timestamp") is None
|
package/ui/index.html
CHANGED
|
@@ -720,6 +720,11 @@
|
|
|
720
720
|
<i class="bi bi-clock-history"></i> Timeline
|
|
721
721
|
</button>
|
|
722
722
|
</li>
|
|
723
|
+
<li class="nav-item">
|
|
724
|
+
<button class="nav-link" id="learning-tab" data-bs-toggle="tab" data-bs-target="#learning-pane">
|
|
725
|
+
<i class="bi bi-mortarboard"></i> Learning <span class="badge bg-warning text-dark" style="font-size:0.6rem;vertical-align:top;">v2.7</span>
|
|
726
|
+
</button>
|
|
727
|
+
</li>
|
|
723
728
|
<li class="nav-item">
|
|
724
729
|
<button class="nav-link" id="events-tab" data-bs-toggle="tab" data-bs-target="#events-pane">
|
|
725
730
|
<i class="bi bi-broadcast"></i> Live Events
|
|
@@ -906,6 +911,144 @@
|
|
|
906
911
|
</div>
|
|
907
912
|
</div>
|
|
908
913
|
|
|
914
|
+
<!-- Learning System (v2.7) -->
|
|
915
|
+
<div class="tab-pane fade" id="learning-pane">
|
|
916
|
+
<!-- Ranking Phase & Engagement -->
|
|
917
|
+
<div class="row g-3 mb-3">
|
|
918
|
+
<div class="col-md-4">
|
|
919
|
+
<div class="card p-3 text-center">
|
|
920
|
+
<div class="fw-bold text-muted small">RANKING PHASE</div>
|
|
921
|
+
<div class="fs-3 fw-bold" id="learning-phase" style="color: var(--bs-primary);">--</div>
|
|
922
|
+
<small class="text-muted" id="learning-phase-detail">Loading...</small>
|
|
923
|
+
</div>
|
|
924
|
+
</div>
|
|
925
|
+
<div class="col-md-4">
|
|
926
|
+
<div class="card p-3 text-center">
|
|
927
|
+
<div class="fw-bold text-muted small">FEEDBACK SIGNALS</div>
|
|
928
|
+
<div class="fs-3 fw-bold" id="learning-feedback-count">0</div>
|
|
929
|
+
<small class="text-muted" id="learning-feedback-detail">0 unique queries</small>
|
|
930
|
+
</div>
|
|
931
|
+
</div>
|
|
932
|
+
<div class="col-md-4">
|
|
933
|
+
<div class="card p-3 text-center">
|
|
934
|
+
<div class="fw-bold text-muted small">ENGAGEMENT HEALTH</div>
|
|
935
|
+
<div class="fs-3 fw-bold" id="learning-health" style="color: var(--bs-success);">--</div>
|
|
936
|
+
<small class="text-muted" id="learning-health-detail">Loading...</small>
|
|
937
|
+
</div>
|
|
938
|
+
</div>
|
|
939
|
+
</div>
|
|
940
|
+
|
|
941
|
+
<!-- Phase Progress Bar -->
|
|
942
|
+
<div class="card p-3 mb-3">
|
|
943
|
+
<h6 class="mb-2"><i class="bi bi-bar-chart-steps"></i> Adaptive Ranking Progress</h6>
|
|
944
|
+
<div class="d-flex align-items-center gap-2 mb-2">
|
|
945
|
+
<div class="flex-grow-1">
|
|
946
|
+
<div class="progress" style="height: 24px; border-radius: 12px;">
|
|
947
|
+
<div class="progress-bar" id="learning-progress" role="progressbar" style="width: 0%; border-radius: 12px; transition: width 0.8s ease;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
|
|
948
|
+
</div>
|
|
949
|
+
</div>
|
|
950
|
+
</div>
|
|
951
|
+
</div>
|
|
952
|
+
<div class="d-flex justify-content-between small text-muted">
|
|
953
|
+
<span>Baseline (0)</span>
|
|
954
|
+
<span>Rule-Based (20+)</span>
|
|
955
|
+
<span>ML Model (200+)</span>
|
|
956
|
+
</div>
|
|
957
|
+
</div>
|
|
958
|
+
|
|
959
|
+
<div class="row g-3">
|
|
960
|
+
<!-- Tech Preferences (Layer 1) -->
|
|
961
|
+
<div class="col-md-6">
|
|
962
|
+
<div class="card p-3 h-100">
|
|
963
|
+
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
964
|
+
<h6 class="mb-0"><i class="bi bi-cpu"></i> Tech Preferences</h6>
|
|
965
|
+
<span class="badge bg-primary">Layer 1</span>
|
|
966
|
+
</div>
|
|
967
|
+
<div id="learning-tech-prefs">
|
|
968
|
+
<div class="text-center text-muted py-3">
|
|
969
|
+
<i class="bi bi-hourglass-split" style="font-size: 1.5rem;"></i>
|
|
970
|
+
<p class="mt-2 mb-0 small">Patterns detected after using recall with feedback</p>
|
|
971
|
+
</div>
|
|
972
|
+
</div>
|
|
973
|
+
</div>
|
|
974
|
+
</div>
|
|
975
|
+
|
|
976
|
+
<!-- Workflow Patterns (Layer 3) -->
|
|
977
|
+
<div class="col-md-6">
|
|
978
|
+
<div class="card p-3 h-100">
|
|
979
|
+
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
980
|
+
<h6 class="mb-0"><i class="bi bi-diagram-3"></i> Workflow Patterns</h6>
|
|
981
|
+
<span class="badge bg-info">Layer 3</span>
|
|
982
|
+
</div>
|
|
983
|
+
<div id="learning-workflows">
|
|
984
|
+
<div class="text-center text-muted py-3">
|
|
985
|
+
<i class="bi bi-hourglass-split" style="font-size: 1.5rem;"></i>
|
|
986
|
+
<p class="mt-2 mb-0 small">Sequences detected after 30+ memories</p>
|
|
987
|
+
</div>
|
|
988
|
+
</div>
|
|
989
|
+
</div>
|
|
990
|
+
</div>
|
|
991
|
+
</div>
|
|
992
|
+
|
|
993
|
+
<div class="row g-3 mt-0">
|
|
994
|
+
<!-- Source Quality -->
|
|
995
|
+
<div class="col-md-6">
|
|
996
|
+
<div class="card p-3 h-100">
|
|
997
|
+
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
998
|
+
<h6 class="mb-0"><i class="bi bi-stars"></i> Source Quality</h6>
|
|
999
|
+
<span class="badge bg-success">Per-Tool Scoring</span>
|
|
1000
|
+
</div>
|
|
1001
|
+
<div id="learning-sources">
|
|
1002
|
+
<div class="text-center text-muted py-3">
|
|
1003
|
+
<i class="bi bi-hourglass-split" style="font-size: 1.5rem;"></i>
|
|
1004
|
+
<p class="mt-2 mb-0 small">Quality scores computed after feedback signals</p>
|
|
1005
|
+
</div>
|
|
1006
|
+
</div>
|
|
1007
|
+
</div>
|
|
1008
|
+
</div>
|
|
1009
|
+
|
|
1010
|
+
<!-- Engagement & Privacy -->
|
|
1011
|
+
<div class="col-md-6">
|
|
1012
|
+
<div class="card p-3 h-100">
|
|
1013
|
+
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
1014
|
+
<h6 class="mb-0"><i class="bi bi-shield-lock"></i> Privacy & Data</h6>
|
|
1015
|
+
<span class="badge bg-secondary">GDPR Compliant</span>
|
|
1016
|
+
</div>
|
|
1017
|
+
<div id="learning-privacy">
|
|
1018
|
+
<div class="small">
|
|
1019
|
+
<div class="d-flex justify-content-between border-bottom py-1">
|
|
1020
|
+
<span class="text-muted">Learning DB</span>
|
|
1021
|
+
<span class="fw-bold" id="learning-db-size">--</span>
|
|
1022
|
+
</div>
|
|
1023
|
+
<div class="d-flex justify-content-between border-bottom py-1">
|
|
1024
|
+
<span class="text-muted">Patterns learned</span>
|
|
1025
|
+
<span class="fw-bold" id="learning-pattern-count">0</span>
|
|
1026
|
+
</div>
|
|
1027
|
+
<div class="d-flex justify-content-between border-bottom py-1">
|
|
1028
|
+
<span class="text-muted">Models trained</span>
|
|
1029
|
+
<span class="fw-bold" id="learning-model-count">0</span>
|
|
1030
|
+
</div>
|
|
1031
|
+
<div class="d-flex justify-content-between border-bottom py-1">
|
|
1032
|
+
<span class="text-muted">Sources tracked</span>
|
|
1033
|
+
<span class="fw-bold" id="learning-source-count">0</span>
|
|
1034
|
+
</div>
|
|
1035
|
+
<div class="d-flex justify-content-between py-1">
|
|
1036
|
+
<span class="text-muted">Telemetry</span>
|
|
1037
|
+
<span class="text-success fw-bold">None</span>
|
|
1038
|
+
</div>
|
|
1039
|
+
</div>
|
|
1040
|
+
<div class="mt-2">
|
|
1041
|
+
<button class="btn btn-sm btn-outline-danger" onclick="resetLearning()">
|
|
1042
|
+
<i class="bi bi-trash"></i> Reset Learning Data
|
|
1043
|
+
</button>
|
|
1044
|
+
<small class="text-muted d-block mt-1">Deletes learning.db. Memories preserved.</small>
|
|
1045
|
+
</div>
|
|
1046
|
+
</div>
|
|
1047
|
+
</div>
|
|
1048
|
+
</div>
|
|
1049
|
+
</div>
|
|
1050
|
+
</div>
|
|
1051
|
+
|
|
909
1052
|
<!-- Live Events (v2.5) -->
|
|
910
1053
|
<div class="tab-pane fade" id="events-pane">
|
|
911
1054
|
<div class="card p-3 mb-3">
|
|
@@ -1175,6 +1318,7 @@
|
|
|
1175
1318
|
<script src="static/js/settings.js"></script>
|
|
1176
1319
|
<script src="static/js/events.js"></script>
|
|
1177
1320
|
<script src="static/js/agents.js"></script>
|
|
1321
|
+
<script src="static/js/learning.js"></script>
|
|
1178
1322
|
<script src="static/js/init.js"></script>
|
|
1179
1323
|
|
|
1180
1324
|
<footer>
|
package/ui/js/init.js
CHANGED
|
@@ -29,3 +29,7 @@ if (eventsTab) eventsTab.addEventListener('shown.bs.tab', loadEventStats);
|
|
|
29
29
|
|
|
30
30
|
var agentsTab = document.getElementById('agents-tab');
|
|
31
31
|
if (agentsTab) agentsTab.addEventListener('shown.bs.tab', loadAgents);
|
|
32
|
+
|
|
33
|
+
// v2.7 learning tab (graceful)
|
|
34
|
+
var learningTab = document.getElementById('learning-tab');
|
|
35
|
+
if (learningTab) learningTab.addEventListener('shown.bs.tab', loadLearning);
|