superlocalmemory 3.2.3 → 3.3.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 +43 -1
- package/README.md +106 -71
- package/package.json +1 -2
- package/pyproject.toml +16 -1
- package/src/superlocalmemory/cli/commands.py +419 -15
- package/src/superlocalmemory/cli/main.py +44 -0
- package/src/superlocalmemory/core/config.py +276 -4
- package/src/superlocalmemory/core/consolidation_engine.py +37 -0
- package/src/superlocalmemory/core/engine.py +21 -0
- package/src/superlocalmemory/core/engine_wiring.py +58 -8
- package/src/superlocalmemory/dynamics/activation_guided_quantization.py +374 -0
- package/src/superlocalmemory/dynamics/eap_scheduler.py +276 -0
- package/src/superlocalmemory/dynamics/ebbinghaus_langevin_coupling.py +171 -0
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +804 -0
- package/src/superlocalmemory/hooks/auto_invoker.py +46 -8
- package/src/superlocalmemory/hooks/auto_parameterize.py +147 -0
- package/src/superlocalmemory/infra/heartbeat_monitor.py +140 -0
- package/src/superlocalmemory/infra/pid_manager.py +193 -0
- package/src/superlocalmemory/infra/process_reaper.py +572 -0
- package/src/superlocalmemory/learning/consolidation_quantization_worker.py +115 -0
- package/src/superlocalmemory/learning/forgetting_scheduler.py +263 -0
- package/src/superlocalmemory/learning/quantization_scheduler.py +320 -0
- package/src/superlocalmemory/math/ebbinghaus.py +309 -0
- package/src/superlocalmemory/math/fisher_quantized.py +251 -0
- package/src/superlocalmemory/math/hopfield.py +279 -0
- package/src/superlocalmemory/math/polar_quant.py +379 -0
- package/src/superlocalmemory/math/qjl.py +115 -0
- package/src/superlocalmemory/mcp/server.py +2 -0
- package/src/superlocalmemory/mcp/tools_v3.py +10 -0
- package/src/superlocalmemory/mcp/tools_v33.py +351 -0
- package/src/superlocalmemory/parameterization/__init__.py +47 -0
- package/src/superlocalmemory/parameterization/pattern_extractor.py +534 -0
- package/src/superlocalmemory/parameterization/pii_filter.py +106 -0
- package/src/superlocalmemory/parameterization/prompt_injector.py +216 -0
- package/src/superlocalmemory/parameterization/prompt_lifecycle.py +275 -0
- package/src/superlocalmemory/parameterization/soft_prompt_generator.py +425 -0
- package/src/superlocalmemory/retrieval/engine.py +21 -3
- package/src/superlocalmemory/retrieval/forgetting_filter.py +145 -0
- package/src/superlocalmemory/retrieval/hopfield_channel.py +335 -0
- package/src/superlocalmemory/retrieval/quantization_aware_search.py +133 -0
- package/src/superlocalmemory/retrieval/strategy.py +16 -6
- package/src/superlocalmemory/server/routes/agents.py +68 -8
- package/src/superlocalmemory/server/routes/learning.py +18 -1
- package/src/superlocalmemory/server/routes/lifecycle.py +36 -17
- package/src/superlocalmemory/server/routes/v3_api.py +503 -1
- package/src/superlocalmemory/storage/database.py +206 -0
- package/src/superlocalmemory/storage/embedding_migrator.py +178 -0
- package/src/superlocalmemory/storage/migration_v33.py +140 -0
- package/src/superlocalmemory/storage/quantized_store.py +261 -0
- package/src/superlocalmemory/storage/schema_v32.py +137 -0
- package/conftest.py +0 -5
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under the MIT License - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V3.3
|
|
4
|
+
|
|
5
|
+
"""PromptInjector — Inject soft prompts into context with token budget.
|
|
6
|
+
|
|
7
|
+
Priority: soft prompts > regular memories (soft prompts come first).
|
|
8
|
+
Token budget split: prompt_budget (500 default) + memory_budget (1500 default).
|
|
9
|
+
Dedup with existing context.
|
|
10
|
+
|
|
11
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from superlocalmemory.storage.database import DatabaseManager
|
|
22
|
+
from superlocalmemory.core.config import ParameterizationConfig
|
|
23
|
+
|
|
24
|
+
from superlocalmemory.parameterization.soft_prompt_generator import (
|
|
25
|
+
SoftPromptGenerator,
|
|
26
|
+
SoftPromptTemplate,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PromptInjector:
|
|
33
|
+
"""Inject soft prompts into conversation context.
|
|
34
|
+
|
|
35
|
+
Handles retrieval from DB, token budget enforcement, and
|
|
36
|
+
combination with regular memory context.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
db: DatabaseManager,
|
|
42
|
+
generator: SoftPromptGenerator,
|
|
43
|
+
config: ParameterizationConfig,
|
|
44
|
+
) -> None:
|
|
45
|
+
self._db = db
|
|
46
|
+
self._generator = generator
|
|
47
|
+
self._config = config
|
|
48
|
+
|
|
49
|
+
# ------------------------------------------------------------------
|
|
50
|
+
# Public API
|
|
51
|
+
# ------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
def get_injection_context(self, profile_id: str) -> str:
|
|
54
|
+
"""Retrieve stored soft prompts, assemble for injection.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
profile_id: Profile to load prompts for.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Assembled soft prompt text, or "" if disabled/empty.
|
|
61
|
+
"""
|
|
62
|
+
if not self._config.enabled:
|
|
63
|
+
return ""
|
|
64
|
+
|
|
65
|
+
rows = self._db.execute(
|
|
66
|
+
"SELECT prompt_id, category, content, confidence, "
|
|
67
|
+
"token_count, retention_score, profile_id, "
|
|
68
|
+
"source_pattern_ids, effectiveness, active, version "
|
|
69
|
+
"FROM soft_prompt_templates "
|
|
70
|
+
"WHERE profile_id = ? AND active = 1 AND retention_score >= 0.1 "
|
|
71
|
+
"ORDER BY confidence DESC",
|
|
72
|
+
(profile_id,),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if not rows:
|
|
76
|
+
return ""
|
|
77
|
+
|
|
78
|
+
templates: list[SoftPromptTemplate] = []
|
|
79
|
+
for row in rows:
|
|
80
|
+
raw_ids = row.get("source_pattern_ids", "[]")
|
|
81
|
+
try:
|
|
82
|
+
source_ids = json.loads(raw_ids) if isinstance(raw_ids, str) else raw_ids
|
|
83
|
+
except (json.JSONDecodeError, TypeError):
|
|
84
|
+
source_ids = []
|
|
85
|
+
|
|
86
|
+
templates.append(SoftPromptTemplate(
|
|
87
|
+
prompt_id=row["prompt_id"],
|
|
88
|
+
profile_id=row.get("profile_id", profile_id),
|
|
89
|
+
category=row["category"],
|
|
90
|
+
content=row["content"],
|
|
91
|
+
source_pattern_ids=source_ids,
|
|
92
|
+
confidence=row["confidence"],
|
|
93
|
+
effectiveness=row.get("effectiveness", 0.5),
|
|
94
|
+
token_count=row.get("token_count", 0),
|
|
95
|
+
retention_score=row.get("retention_score", 1.0),
|
|
96
|
+
active=bool(row.get("active", 1)),
|
|
97
|
+
version=row.get("version", 1),
|
|
98
|
+
))
|
|
99
|
+
|
|
100
|
+
# Budget enforcement
|
|
101
|
+
total = sum(t.token_count for t in templates)
|
|
102
|
+
if total > self._config.max_prompt_tokens:
|
|
103
|
+
selected: list[SoftPromptTemplate] = []
|
|
104
|
+
accumulated = 0
|
|
105
|
+
for t in templates: # Already sorted by confidence DESC
|
|
106
|
+
if accumulated + t.token_count > self._config.max_prompt_tokens:
|
|
107
|
+
break
|
|
108
|
+
selected.append(t)
|
|
109
|
+
accumulated += t.token_count
|
|
110
|
+
templates = selected
|
|
111
|
+
|
|
112
|
+
return self._generator.assemble(templates)
|
|
113
|
+
|
|
114
|
+
def inject_into_context(
|
|
115
|
+
self,
|
|
116
|
+
soft_prompt_text: str,
|
|
117
|
+
memory_context: str,
|
|
118
|
+
total_budget: int | None = None,
|
|
119
|
+
) -> str:
|
|
120
|
+
"""Combine soft prompts with regular memory context.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
soft_prompt_text: Assembled soft prompt text.
|
|
124
|
+
memory_context: Regular memory recall context.
|
|
125
|
+
total_budget: Optional total token budget override.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Combined context string with soft prompts first.
|
|
129
|
+
"""
|
|
130
|
+
budget = total_budget or (
|
|
131
|
+
self._config.max_prompt_tokens + self._config.max_memory_tokens
|
|
132
|
+
)
|
|
133
|
+
prompt_budget = self._config.max_prompt_tokens
|
|
134
|
+
memory_budget = self._config.max_memory_tokens
|
|
135
|
+
|
|
136
|
+
# Trim soft prompt to its budget
|
|
137
|
+
if soft_prompt_text:
|
|
138
|
+
soft_tokens = SoftPromptGenerator._estimate_tokens(soft_prompt_text)
|
|
139
|
+
if soft_tokens > prompt_budget:
|
|
140
|
+
soft_prompt_text = SoftPromptGenerator._trim_to_tokens(
|
|
141
|
+
soft_prompt_text, prompt_budget,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Trim memory context to its budget
|
|
145
|
+
if memory_context:
|
|
146
|
+
mem_tokens = SoftPromptGenerator._estimate_tokens(memory_context)
|
|
147
|
+
if mem_tokens > memory_budget:
|
|
148
|
+
memory_context = SoftPromptGenerator._trim_to_tokens(
|
|
149
|
+
memory_context, memory_budget,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
if not soft_prompt_text:
|
|
153
|
+
return memory_context
|
|
154
|
+
if not memory_context:
|
|
155
|
+
return soft_prompt_text
|
|
156
|
+
|
|
157
|
+
return f"{soft_prompt_text}\n\n{memory_context}"
|
|
158
|
+
|
|
159
|
+
def store_prompts(
|
|
160
|
+
self, templates: list[SoftPromptTemplate],
|
|
161
|
+
) -> int:
|
|
162
|
+
"""Persist generated soft prompts, deactivating old versions.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
templates: Generated templates to store.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Number of templates stored.
|
|
169
|
+
"""
|
|
170
|
+
count = 0
|
|
171
|
+
for template in templates:
|
|
172
|
+
# Deactivate existing for same (profile_id, category)
|
|
173
|
+
self._db.execute(
|
|
174
|
+
"UPDATE soft_prompt_templates SET active = 0, "
|
|
175
|
+
"updated_at = datetime('now') "
|
|
176
|
+
"WHERE profile_id = ? AND category = ? AND active = 1",
|
|
177
|
+
(template.profile_id, template.category),
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Get max version for this (profile_id, category)
|
|
181
|
+
version_rows = self._db.execute(
|
|
182
|
+
"SELECT COALESCE(MAX(version), 0) AS max_version "
|
|
183
|
+
"FROM soft_prompt_templates "
|
|
184
|
+
"WHERE profile_id = ? AND category = ?",
|
|
185
|
+
(template.profile_id, template.category),
|
|
186
|
+
)
|
|
187
|
+
max_version = 0
|
|
188
|
+
if version_rows:
|
|
189
|
+
max_version = version_rows[0].get("max_version", 0) or 0
|
|
190
|
+
new_version = max_version + 1
|
|
191
|
+
|
|
192
|
+
# Insert new prompt
|
|
193
|
+
self._db.execute(
|
|
194
|
+
"INSERT INTO soft_prompt_templates "
|
|
195
|
+
"(prompt_id, profile_id, category, content, "
|
|
196
|
+
"source_pattern_ids, confidence, effectiveness, "
|
|
197
|
+
"token_count, retention_score, active, version, "
|
|
198
|
+
"created_at, updated_at) "
|
|
199
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, "
|
|
200
|
+
"datetime('now'), datetime('now'))",
|
|
201
|
+
(
|
|
202
|
+
template.prompt_id,
|
|
203
|
+
template.profile_id,
|
|
204
|
+
template.category,
|
|
205
|
+
template.content,
|
|
206
|
+
json.dumps(template.source_pattern_ids),
|
|
207
|
+
template.confidence,
|
|
208
|
+
template.effectiveness,
|
|
209
|
+
template.token_count,
|
|
210
|
+
template.retention_score,
|
|
211
|
+
new_version,
|
|
212
|
+
),
|
|
213
|
+
)
|
|
214
|
+
count += 1
|
|
215
|
+
|
|
216
|
+
return count
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under the MIT License - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V3.3
|
|
4
|
+
|
|
5
|
+
"""PromptLifecycleManager — Ebbinghaus decay and effectiveness for soft prompts.
|
|
6
|
+
|
|
7
|
+
Applies Ebbinghaus forgetting curve to soft prompts. Higher effectiveness
|
|
8
|
+
and more versions (evidence proxy) slow decay. 48h floor prevents cold-start death.
|
|
9
|
+
|
|
10
|
+
[AUDIT FIX F-2] 48h floor on prompt strength.
|
|
11
|
+
|
|
12
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
import math
|
|
19
|
+
from datetime import datetime, timezone
|
|
20
|
+
from typing import TYPE_CHECKING
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from superlocalmemory.storage.database import DatabaseManager
|
|
24
|
+
from superlocalmemory.math.ebbinghaus import EbbinghausCurve
|
|
25
|
+
from superlocalmemory.core.config import ParameterizationConfig
|
|
26
|
+
|
|
27
|
+
from superlocalmemory.parameterization.pattern_extractor import (
|
|
28
|
+
PatternAssertion,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
# Minimum prompt strength floor — prevents cold-start death
|
|
34
|
+
_PROMPT_STRENGTH_FLOOR = 48.0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PromptLifecycleManager:
|
|
38
|
+
"""Manage soft prompt lifecycle: effectiveness tracking + Ebbinghaus decay.
|
|
39
|
+
|
|
40
|
+
Effectiveness is updated via session feedback signals.
|
|
41
|
+
Retention decays via Ebbinghaus curve with strength derived from
|
|
42
|
+
effectiveness * version (evidence proxy).
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
db: DatabaseManager,
|
|
48
|
+
ebbinghaus: EbbinghausCurve,
|
|
49
|
+
config: ParameterizationConfig,
|
|
50
|
+
) -> None:
|
|
51
|
+
self._db = db
|
|
52
|
+
self._ebbinghaus = ebbinghaus
|
|
53
|
+
self._config = config
|
|
54
|
+
|
|
55
|
+
# ------------------------------------------------------------------
|
|
56
|
+
# Effectiveness tracking
|
|
57
|
+
# ------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
def update_effectiveness(
|
|
60
|
+
self,
|
|
61
|
+
profile_id: str,
|
|
62
|
+
category: str,
|
|
63
|
+
signals: dict[str, float],
|
|
64
|
+
) -> float:
|
|
65
|
+
"""Update prompt effectiveness from feedback signals.
|
|
66
|
+
|
|
67
|
+
Signals: followed, session_success (positive),
|
|
68
|
+
corrected, session_failure (negative).
|
|
69
|
+
Uses exponential moving average: 0.7 * raw + 0.3 * current.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
profile_id: Target profile.
|
|
73
|
+
category: Prompt category to update.
|
|
74
|
+
signals: Feedback signal dict.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
New effectiveness score in [0.0, 1.0].
|
|
78
|
+
"""
|
|
79
|
+
rows = self._db.execute(
|
|
80
|
+
"SELECT prompt_id, effectiveness, retention_score "
|
|
81
|
+
"FROM soft_prompt_templates "
|
|
82
|
+
"WHERE profile_id = ? AND category = ? AND active = 1",
|
|
83
|
+
(profile_id, category),
|
|
84
|
+
)
|
|
85
|
+
if not rows:
|
|
86
|
+
return 0.5
|
|
87
|
+
|
|
88
|
+
row = rows[0]
|
|
89
|
+
current_effectiveness = row.get("effectiveness", 0.5)
|
|
90
|
+
prompt_id = row["prompt_id"]
|
|
91
|
+
|
|
92
|
+
# Compute signal summary
|
|
93
|
+
positive = (
|
|
94
|
+
signals.get("followed", 0.0)
|
|
95
|
+
+ signals.get("session_success", 0.0)
|
|
96
|
+
)
|
|
97
|
+
negative = (
|
|
98
|
+
signals.get("corrected", 0.0)
|
|
99
|
+
+ signals.get("session_failure", 0.0)
|
|
100
|
+
)
|
|
101
|
+
total = positive + negative + 1.0 # Laplace smoothing
|
|
102
|
+
|
|
103
|
+
raw_score = positive / total
|
|
104
|
+
new_effectiveness = 0.7 * raw_score + 0.3 * current_effectiveness
|
|
105
|
+
new_effectiveness = max(0.0, min(1.0, new_effectiveness))
|
|
106
|
+
|
|
107
|
+
self._db.execute(
|
|
108
|
+
"UPDATE soft_prompt_templates "
|
|
109
|
+
"SET effectiveness = ?, updated_at = datetime('now') "
|
|
110
|
+
"WHERE prompt_id = ?",
|
|
111
|
+
(new_effectiveness, prompt_id),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return new_effectiveness
|
|
115
|
+
|
|
116
|
+
# ------------------------------------------------------------------
|
|
117
|
+
# Ebbinghaus decay
|
|
118
|
+
# ------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
def compute_prompt_retention(self, prompt_id: str) -> float:
|
|
121
|
+
"""Compute Ebbinghaus retention for a soft prompt.
|
|
122
|
+
|
|
123
|
+
Strength = max(48h floor, 2 * effectiveness * version).
|
|
124
|
+
R(t) = e^(-t/S) via EbbinghausCurve.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
prompt_id: Prompt to compute retention for.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Retention score in [0.0, 1.0], or 0.0 if not found.
|
|
131
|
+
"""
|
|
132
|
+
rows = self._db.execute(
|
|
133
|
+
"SELECT prompt_id, effectiveness, retention_score, "
|
|
134
|
+
"version, created_at, updated_at "
|
|
135
|
+
"FROM soft_prompt_templates WHERE prompt_id = ?",
|
|
136
|
+
(prompt_id,),
|
|
137
|
+
)
|
|
138
|
+
if not rows:
|
|
139
|
+
return 0.0
|
|
140
|
+
|
|
141
|
+
row = rows[0]
|
|
142
|
+
effectiveness = row.get("effectiveness", 0.5)
|
|
143
|
+
version = max(1, row.get("version", 1))
|
|
144
|
+
updated_at = row.get("updated_at", "")
|
|
145
|
+
|
|
146
|
+
# Compute hours since last update
|
|
147
|
+
now = datetime.now(timezone.utc)
|
|
148
|
+
try:
|
|
149
|
+
last_update = datetime.fromisoformat(updated_at)
|
|
150
|
+
if last_update.tzinfo is None:
|
|
151
|
+
last_update = last_update.replace(tzinfo=timezone.utc)
|
|
152
|
+
hours = (now - last_update).total_seconds() / 3600.0
|
|
153
|
+
except (ValueError, TypeError):
|
|
154
|
+
hours = 0.0
|
|
155
|
+
|
|
156
|
+
# Prompt strength: effectiveness * version proxy
|
|
157
|
+
s_raw = 2.0 * effectiveness * version
|
|
158
|
+
s_prompt = max(_PROMPT_STRENGTH_FLOOR, s_raw)
|
|
159
|
+
|
|
160
|
+
retention = self._ebbinghaus.retention(hours, s_prompt)
|
|
161
|
+
return retention
|
|
162
|
+
|
|
163
|
+
# ------------------------------------------------------------------
|
|
164
|
+
# Lifecycle review
|
|
165
|
+
# ------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
def run_lifecycle_review(self, profile_id: str) -> dict:
|
|
168
|
+
"""Periodic review: decay prompts, deactivate dead ones.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
profile_id: Profile to review.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Stats dict: {reviewed, decayed, removed, refreshed}.
|
|
175
|
+
"""
|
|
176
|
+
rows = self._db.execute(
|
|
177
|
+
"SELECT prompt_id, category, effectiveness, retention_score, "
|
|
178
|
+
"version, created_at, updated_at "
|
|
179
|
+
"FROM soft_prompt_templates "
|
|
180
|
+
"WHERE profile_id = ? AND active = 1",
|
|
181
|
+
(profile_id,),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
stats = {"reviewed": 0, "decayed": 0, "removed": 0, "refreshed": 0}
|
|
185
|
+
|
|
186
|
+
for row in rows:
|
|
187
|
+
prompt_id = row["prompt_id"]
|
|
188
|
+
old_retention = row.get("retention_score", 1.0)
|
|
189
|
+
stats["reviewed"] += 1
|
|
190
|
+
|
|
191
|
+
new_retention = self.compute_prompt_retention(prompt_id)
|
|
192
|
+
|
|
193
|
+
if new_retention < 0.1:
|
|
194
|
+
# Deactivate
|
|
195
|
+
self._db.execute(
|
|
196
|
+
"UPDATE soft_prompt_templates "
|
|
197
|
+
"SET active = 0, retention_score = ?, "
|
|
198
|
+
"updated_at = datetime('now') WHERE prompt_id = ?",
|
|
199
|
+
(new_retention, prompt_id),
|
|
200
|
+
)
|
|
201
|
+
stats["removed"] += 1
|
|
202
|
+
logger.info(
|
|
203
|
+
"Prompt %s (category=%s) deactivated: retention %.3f < 0.1",
|
|
204
|
+
prompt_id, row.get("category", "?"), new_retention,
|
|
205
|
+
)
|
|
206
|
+
else:
|
|
207
|
+
# Update retention score
|
|
208
|
+
self._db.execute(
|
|
209
|
+
"UPDATE soft_prompt_templates "
|
|
210
|
+
"SET retention_score = ?, updated_at = datetime('now') "
|
|
211
|
+
"WHERE prompt_id = ?",
|
|
212
|
+
(new_retention, prompt_id),
|
|
213
|
+
)
|
|
214
|
+
delta = abs(new_retention - old_retention)
|
|
215
|
+
if delta > 0.1:
|
|
216
|
+
stats["decayed"] += 1
|
|
217
|
+
|
|
218
|
+
return stats
|
|
219
|
+
|
|
220
|
+
# ------------------------------------------------------------------
|
|
221
|
+
# Evolution
|
|
222
|
+
# ------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
def evolve_prompt(
|
|
225
|
+
self,
|
|
226
|
+
profile_id: str,
|
|
227
|
+
category: str,
|
|
228
|
+
new_pattern: PatternAssertion,
|
|
229
|
+
) -> str:
|
|
230
|
+
"""Handle preference evolution: new pattern vs existing prompt.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
profile_id: Target profile.
|
|
234
|
+
category: Category being evolved.
|
|
235
|
+
new_pattern: New pattern assertion to compare.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
"new" | "replaced" | "kept_existing" | "user_review_needed"
|
|
239
|
+
"""
|
|
240
|
+
rows = self._db.execute(
|
|
241
|
+
"SELECT prompt_id, category, confidence, effectiveness "
|
|
242
|
+
"FROM soft_prompt_templates "
|
|
243
|
+
"WHERE profile_id = ? AND category = ? AND active = 1",
|
|
244
|
+
(profile_id, category),
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
if not rows:
|
|
248
|
+
return "new"
|
|
249
|
+
|
|
250
|
+
current = rows[0]
|
|
251
|
+
current_confidence = current.get("confidence", 0.0)
|
|
252
|
+
|
|
253
|
+
if new_pattern.confidence > current_confidence + 0.1:
|
|
254
|
+
# New pattern clearly better — replace
|
|
255
|
+
self._db.execute(
|
|
256
|
+
"UPDATE soft_prompt_templates "
|
|
257
|
+
"SET active = 0, updated_at = datetime('now') "
|
|
258
|
+
"WHERE prompt_id = ?",
|
|
259
|
+
(current["prompt_id"],),
|
|
260
|
+
)
|
|
261
|
+
return "replaced"
|
|
262
|
+
|
|
263
|
+
if new_pattern.confidence < current_confidence - 0.1:
|
|
264
|
+
logger.info(
|
|
265
|
+
"Keeping existing prompt for %s (conf=%.2f > new=%.2f)",
|
|
266
|
+
category, current_confidence, new_pattern.confidence,
|
|
267
|
+
)
|
|
268
|
+
return "kept_existing"
|
|
269
|
+
|
|
270
|
+
# Close confidence — needs user review
|
|
271
|
+
logger.warning(
|
|
272
|
+
"Equal confidence conflict in %s for %s. User review needed.",
|
|
273
|
+
category, new_pattern.key,
|
|
274
|
+
)
|
|
275
|
+
return "user_review_needed"
|