pdm-memory 0.1.2__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.
- pdm_memory/__init__.py +22 -0
- pdm_memory/auth/__init__.py +4 -0
- pdm_memory/auth/jwt_handler.py +119 -0
- pdm_memory/bench.py +5 -0
- pdm_memory/core/__init__.py +1 -0
- pdm_memory/core/math.py +329 -0
- pdm_memory/core/retrieval.py +362 -0
- pdm_memory/core/signature.py +257 -0
- pdm_memory/ingest/__init__.py +6 -0
- pdm_memory/ingest/auto_signature.py +202 -0
- pdm_memory/ingest/batch.py +164 -0
- pdm_memory/ingest/ingester.py +272 -0
- pdm_memory/integrations/__init__.py +21 -0
- pdm_memory/integrations/anthropic_adapter.py +179 -0
- pdm_memory/integrations/context_manager.py +134 -0
- pdm_memory/integrations/gemini_adapter.py +215 -0
- pdm_memory/integrations/groq_adapter.py +171 -0
- pdm_memory/integrations/ollama_adapter.py +184 -0
- pdm_memory/integrations/openai_adapter.py +191 -0
- pdm_memory/memory.py +552 -0
- pdm_memory/storage/__init__.py +5 -0
- pdm_memory/storage/base.py +123 -0
- pdm_memory/storage/cloud_driver.py +271 -0
- pdm_memory/storage/sqlite_driver.py +331 -0
- pdm_memory/sync.py +145 -0
- pdm_memory/tools/__init__.py +1 -0
- pdm_memory/tools/bench.py +294 -0
- pdm_memory/tools/cli.py +207 -0
- pdm_memory-0.1.2.dist-info/METADATA +485 -0
- pdm_memory-0.1.2.dist-info/RECORD +34 -0
- pdm_memory-0.1.2.dist-info/WHEEL +5 -0
- pdm_memory-0.1.2.dist-info/entry_points.txt +2 -0
- pdm_memory-0.1.2.dist-info/licenses/LICENSE +50 -0
- pdm_memory-0.1.2.dist-info/top_level.txt +1 -0
pdm_memory/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pdm_memory — Pressure-Driven Memory SDK
|
|
3
|
+
========================================
|
|
4
|
+
|
|
5
|
+
The public surface of the SDK:
|
|
6
|
+
|
|
7
|
+
from pdm_memory import Memory
|
|
8
|
+
|
|
9
|
+
mem = Memory(store="./my_app.db")
|
|
10
|
+
mem.save("User prefers metric units", source="chat")
|
|
11
|
+
hits = mem.recall("how should I format this?", k=5)
|
|
12
|
+
|
|
13
|
+
from pdm_memory.integrations import wrap_openai, wrap_anthropic
|
|
14
|
+
|
|
15
|
+
See README.md for full documentation.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from pdm_memory.memory import Memory
|
|
19
|
+
from pdm_memory.core.signature import MemoryHit, DrawerInfo
|
|
20
|
+
|
|
21
|
+
__version__ = "0.1.0"
|
|
22
|
+
__all__ = ["Memory", "MemoryHit", "DrawerInfo", "__version__"]
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""
|
|
2
|
+
JWT Auth Handler — Task 2.2
|
|
3
|
+
|
|
4
|
+
Manages Bearer token authentication for cloud storage requests.
|
|
5
|
+
Optionally handles refresh-token flows automatically on 401.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
auth = JWTAuth(token="eyJ...", refresh_token="eyJ...", refresh_url="https://api.azus.ai/api/v1/accounts/auth/refresh/")
|
|
9
|
+
headers = auth.headers()
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import time
|
|
16
|
+
from typing import Dict, Optional
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class JWTAuth:
|
|
22
|
+
"""
|
|
23
|
+
JWT Bearer token handler.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
token: JWT access token (required).
|
|
27
|
+
refresh_token: Optional refresh token for automatic renewal.
|
|
28
|
+
refresh_url: URL to POST refresh token to (companion_api endpoint).
|
|
29
|
+
expire_buffer: Refresh this many seconds before expiry (default 60s).
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
token: str,
|
|
35
|
+
refresh_token: Optional[str] = None,
|
|
36
|
+
refresh_url: Optional[str] = None,
|
|
37
|
+
expire_buffer: int = 60,
|
|
38
|
+
) -> None:
|
|
39
|
+
self._token = token
|
|
40
|
+
self._refresh_token = refresh_token
|
|
41
|
+
self._refresh_url = refresh_url
|
|
42
|
+
self._expire_buffer = expire_buffer
|
|
43
|
+
self._expires_at: Optional[float] = self._decode_exp(token)
|
|
44
|
+
|
|
45
|
+
# ------------------------------------------------------------------
|
|
46
|
+
# Public API
|
|
47
|
+
# ------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def token(self) -> str:
|
|
51
|
+
return self._token
|
|
52
|
+
|
|
53
|
+
def headers(self) -> Dict[str, str]:
|
|
54
|
+
"""Return Authorization headers for an HTTP request."""
|
|
55
|
+
return {"Authorization": f"Bearer {self._token}"}
|
|
56
|
+
|
|
57
|
+
def refresh(self) -> bool:
|
|
58
|
+
"""
|
|
59
|
+
Attempt to refresh the access token using the refresh token.
|
|
60
|
+
|
|
61
|
+
Returns True on success, False if refresh not possible or failed.
|
|
62
|
+
"""
|
|
63
|
+
if not self._refresh_token or not self._refresh_url:
|
|
64
|
+
return False
|
|
65
|
+
try:
|
|
66
|
+
import httpx
|
|
67
|
+
resp = httpx.post(
|
|
68
|
+
self._refresh_url,
|
|
69
|
+
json={"refresh": self._refresh_token},
|
|
70
|
+
timeout=10,
|
|
71
|
+
)
|
|
72
|
+
resp.raise_for_status()
|
|
73
|
+
data = resp.json()
|
|
74
|
+
new_token = data.get("access")
|
|
75
|
+
if new_token:
|
|
76
|
+
self._token = new_token
|
|
77
|
+
self._expires_at = self._decode_exp(new_token)
|
|
78
|
+
logger.debug("[JWTAuth] Token refreshed successfully")
|
|
79
|
+
return True
|
|
80
|
+
except Exception as e:
|
|
81
|
+
logger.warning("[JWTAuth] Token refresh failed: %s", e)
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
def is_expired(self) -> bool:
|
|
85
|
+
"""Return True if token is expired or within expire_buffer seconds of expiry."""
|
|
86
|
+
if self._expires_at is None:
|
|
87
|
+
return False # Unknown expiry — assume valid
|
|
88
|
+
return time.time() >= (self._expires_at - self._expire_buffer)
|
|
89
|
+
|
|
90
|
+
def ensure_fresh(self) -> None:
|
|
91
|
+
"""Refresh token if expired. Raises RuntimeError if refresh fails."""
|
|
92
|
+
if self.is_expired():
|
|
93
|
+
if not self.refresh():
|
|
94
|
+
raise RuntimeError(
|
|
95
|
+
"PDM cloud auth: access token is expired and could not be refreshed. "
|
|
96
|
+
"Please provide a new token via Memory(token=...)."
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# ------------------------------------------------------------------
|
|
100
|
+
# Private
|
|
101
|
+
# ------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
@staticmethod
|
|
104
|
+
def _decode_exp(token: str) -> Optional[float]:
|
|
105
|
+
"""Decode the 'exp' claim from a JWT without validating the signature."""
|
|
106
|
+
try:
|
|
107
|
+
import base64
|
|
108
|
+
import json
|
|
109
|
+
parts = token.split(".")
|
|
110
|
+
if len(parts) != 3:
|
|
111
|
+
return None
|
|
112
|
+
payload = parts[1]
|
|
113
|
+
# Add padding
|
|
114
|
+
payload += "=" * (4 - len(payload) % 4)
|
|
115
|
+
decoded = base64.urlsafe_b64decode(payload)
|
|
116
|
+
claims = json.loads(decoded)
|
|
117
|
+
return float(claims.get("exp", 0)) or None
|
|
118
|
+
except Exception:
|
|
119
|
+
return None
|
pdm_memory/bench.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""pdm_memory.core package."""
|
pdm_memory/core/math.py
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PDM Core Math — Pure Python, zero external dependencies.
|
|
3
|
+
|
|
4
|
+
All formulas are ported faithfully from companion_api/pdm/kernel.py
|
|
5
|
+
and companion_api/pdm/models.py. No Django, no ORM, no Celery.
|
|
6
|
+
|
|
7
|
+
Formula reference:
|
|
8
|
+
effective_spike = min(100, P_magnitude × (t_persistence/30) × phase_privilege)
|
|
9
|
+
decay_factor = 1 - exp(-λ × t) where λ = ln2 / half_life
|
|
10
|
+
V = (correct + 1) / (total + 2) [Laplace smoothing]
|
|
11
|
+
P_effective = P × V × (1 - decay_factor) × intent_weight × quality × comparator
|
|
12
|
+
incremental_decay: new_p = p * (decay_rate ** days_elapsed) (no-scheduler version)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import math
|
|
18
|
+
from typing import Dict, List, Optional
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Domain half-lives (ported from kernel.py DOMAIN_HALF_LIVES)
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
DOMAIN_HALF_LIVES: Dict[str, float] = {
|
|
26
|
+
"market_signal": 1.0, # 1 day
|
|
27
|
+
"pattern": 14.0, # 2 weeks
|
|
28
|
+
"structural": 90.0, # 3 months
|
|
29
|
+
"core_fact": 365.0, # 1 year
|
|
30
|
+
"reminder": 7.0, # 1 week
|
|
31
|
+
"insight": 30.0, # 1 month
|
|
32
|
+
"warning": 3.0, # 3 days
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
DEFAULT_HALF_LIFE: float = 30.0
|
|
36
|
+
|
|
37
|
+
# Pressure constants
|
|
38
|
+
P_MAX: float = 100.0
|
|
39
|
+
P_FLOOR: float = 0.0
|
|
40
|
+
|
|
41
|
+
# Decay trigger — signatures below this are eligible for deletion
|
|
42
|
+
DECAY_DELETE_THRESHOLD: float = 30.0
|
|
43
|
+
|
|
44
|
+
# Default decay multiplier (10% pressure loss per cycle)
|
|
45
|
+
DEFAULT_DECAY_RATE: float = 0.9
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# Task 1.3: Core formulas (Django-free)
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def calculate_effective_spike(
|
|
54
|
+
p_magnitude: float,
|
|
55
|
+
t_persistence: float,
|
|
56
|
+
phase_privilege: float = 1.0,
|
|
57
|
+
) -> float:
|
|
58
|
+
"""
|
|
59
|
+
Calculate effective_spike = p_magnitude × (t_persistence / 30) × phase_privilege.
|
|
60
|
+
Capped at 100.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
p_magnitude: Raw pressure (0–100).
|
|
64
|
+
t_persistence: How many days the memory stays relevant.
|
|
65
|
+
phase_privilege: Nesting/context multiplier, typically 1.0.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
effective_spike in [0, 100].
|
|
69
|
+
"""
|
|
70
|
+
raw = p_magnitude * (t_persistence / 30.0) * phase_privilege
|
|
71
|
+
return min(P_MAX, max(P_FLOOR, raw))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def calculate_decay_factor(
|
|
75
|
+
days_since_retrieved: float,
|
|
76
|
+
half_life: float = DEFAULT_HALF_LIFE,
|
|
77
|
+
) -> float:
|
|
78
|
+
"""
|
|
79
|
+
Exponential decay factor based on time since last retrieval.
|
|
80
|
+
|
|
81
|
+
decay_factor = 1 - exp(-λ × t) where λ = ln2 / half_life
|
|
82
|
+
|
|
83
|
+
A decay_factor close to 0 means the memory is fresh (little decay).
|
|
84
|
+
A decay_factor close to 1 means the memory is very stale.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
days_since_retrieved: Days since this memory was last accessed.
|
|
88
|
+
half_life: Domain-specific half-life in days.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
decay_factor in [0, 1].
|
|
92
|
+
"""
|
|
93
|
+
half_life = max(0.1, half_life)
|
|
94
|
+
lam = math.log(2) / half_life
|
|
95
|
+
return 1.0 - math.exp(-lam * days_since_retrieved)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def calculate_v(correct: int, total: int) -> float:
|
|
99
|
+
"""
|
|
100
|
+
Validation Coefficient with Laplace smoothing.
|
|
101
|
+
V = (correct + 1) / (total + 2)
|
|
102
|
+
|
|
103
|
+
Range: 0.33 (no history) → approaching 1.0 (perfect accuracy).
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
correct: Number of correct predictions.
|
|
107
|
+
total: Total predictions made.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
V in [0.33, 1.0].
|
|
111
|
+
"""
|
|
112
|
+
return round((correct + 1) / (total + 2), 6)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def calculate_intent_weight(
|
|
116
|
+
intent_tags: List[str],
|
|
117
|
+
query: Optional[str] = None,
|
|
118
|
+
) -> float:
|
|
119
|
+
"""
|
|
120
|
+
Intent match weight: 0.8 base + 0.2 boost proportional to tag-query overlap.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
intent_tags: Tags associated with the memory.
|
|
124
|
+
query: The recall query string.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Weight in [0.8, 1.0] — 1.0 when query is absent or all tags match.
|
|
128
|
+
"""
|
|
129
|
+
if not query or not intent_tags:
|
|
130
|
+
return 1.0
|
|
131
|
+
query_lower = query.lower()
|
|
132
|
+
matches = sum(1 for tag in intent_tags if tag.lower() in query_lower)
|
|
133
|
+
return round(0.8 + 0.2 * (matches / len(intent_tags)), 6)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def calculate_p_effective(
|
|
137
|
+
p_magnitude: float,
|
|
138
|
+
v: float = 0.75,
|
|
139
|
+
decay_factor: float = 0.0,
|
|
140
|
+
intent_weight: float = 1.0,
|
|
141
|
+
quality: float = 0.80,
|
|
142
|
+
comparator: float = 1.0,
|
|
143
|
+
) -> float:
|
|
144
|
+
"""
|
|
145
|
+
P_effective — the live pressure value used for retrieval ranking.
|
|
146
|
+
|
|
147
|
+
P_eff = P_magnitude × V × (1 - decay_factor) × intent_weight × quality × comparator
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
p_magnitude: Raw stored pressure (0–100).
|
|
151
|
+
v: Validation coefficient (Laplace, 0.33–1.0).
|
|
152
|
+
decay_factor: Exponential decay (0=fresh, 1=fully decayed).
|
|
153
|
+
intent_weight: Tag-query match weight (0.8–1.0).
|
|
154
|
+
quality: Signal quality score (0–1), default 0.80.
|
|
155
|
+
comparator: Optional UCA-style proxy multiplier (0.85–1.15).
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
P_effective in [0, 100].
|
|
159
|
+
"""
|
|
160
|
+
p_eff = (
|
|
161
|
+
p_magnitude
|
|
162
|
+
* v
|
|
163
|
+
* (1.0 - decay_factor)
|
|
164
|
+
* intent_weight
|
|
165
|
+
* quality
|
|
166
|
+
* comparator
|
|
167
|
+
)
|
|
168
|
+
return round(min(P_MAX, max(P_FLOOR, p_eff)), 6)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# ---------------------------------------------------------------------------
|
|
172
|
+
# Task 1.4: Incremental decay (no Celery — computed on every recall())
|
|
173
|
+
# ---------------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def calculate_incremental_decay(
|
|
177
|
+
p_magnitude: float,
|
|
178
|
+
days_elapsed: float,
|
|
179
|
+
t_persistence: float,
|
|
180
|
+
phase_privilege: float = 1.0,
|
|
181
|
+
decay_per_day: float = DEFAULT_DECAY_RATE,
|
|
182
|
+
) -> tuple[float, float]:
|
|
183
|
+
"""
|
|
184
|
+
Compute decayed p_magnitude and effective_spike without a scheduler.
|
|
185
|
+
|
|
186
|
+
Decay only fires after t_persistence days. If days_elapsed ≤ t_persistence,
|
|
187
|
+
the memory is within its guaranteed persistence window — no decay applied.
|
|
188
|
+
|
|
189
|
+
Formula:
|
|
190
|
+
If days_elapsed > t_persistence:
|
|
191
|
+
decayed_p = p_magnitude × decay_per_day^(days_elapsed - t_persistence)
|
|
192
|
+
else:
|
|
193
|
+
decayed_p = p_magnitude (still in persistence window)
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
p_magnitude: Current stored pressure.
|
|
197
|
+
days_elapsed: Days since the memory was created (or last decayed).
|
|
198
|
+
t_persistence: Guaranteed persistence window in days.
|
|
199
|
+
phase_privilege: Phase multiplier for effective_spike.
|
|
200
|
+
decay_per_day: Multiplier per day past persistence (default 0.9 = 10% loss/day).
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
Tuple (decayed_p_magnitude, new_effective_spike).
|
|
204
|
+
"""
|
|
205
|
+
if days_elapsed <= t_persistence:
|
|
206
|
+
new_p = p_magnitude
|
|
207
|
+
else:
|
|
208
|
+
days_past = days_elapsed - t_persistence
|
|
209
|
+
new_p = p_magnitude * (decay_per_day ** days_past)
|
|
210
|
+
|
|
211
|
+
new_p = max(0.0, min(P_MAX, new_p))
|
|
212
|
+
new_spike = calculate_effective_spike(new_p, t_persistence, phase_privilege)
|
|
213
|
+
return round(new_p, 6), round(new_spike, 6)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# ---------------------------------------------------------------------------
|
|
217
|
+
# Task 1.3: Temporal geometry (PDM-T), ported from models.py
|
|
218
|
+
# ---------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def calculate_temporal_geometry(
|
|
222
|
+
c_base: float,
|
|
223
|
+
s_base: float,
|
|
224
|
+
p_base: float, # p_magnitude / 100.0
|
|
225
|
+
urgency_rate: float,
|
|
226
|
+
t_remaining_days: float,
|
|
227
|
+
persist_days: float,
|
|
228
|
+
decay_rate: float = 0.05,
|
|
229
|
+
temporal_weight: float = 1.0,
|
|
230
|
+
) -> dict:
|
|
231
|
+
"""
|
|
232
|
+
PDM-T Temporal Deformation Geometry.
|
|
233
|
+
|
|
234
|
+
Pre-deadline:
|
|
235
|
+
C_T = c_base × (1 + persist / T_remaining)
|
|
236
|
+
S_T = s_base / (1 + urgency_rate × (1 - T_remaining / persist))
|
|
237
|
+
P_T = p_base × urgency_rate^(1 / T_remaining_days)
|
|
238
|
+
E_T = C_T × transmission × P_T (normalized via asymptotic scaling)
|
|
239
|
+
|
|
240
|
+
Post-deadline:
|
|
241
|
+
E_T = 0, weight decays by decay_rate per day past deadline.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
c_base: Initial curvature (0–1).
|
|
245
|
+
s_base: Initial membrane thickness (0–1).
|
|
246
|
+
p_base: Normalized pressure (p_magnitude / 100).
|
|
247
|
+
urgency_rate: Pressure multiplier ramp (1–10).
|
|
248
|
+
t_remaining_days: Days to deadline (negative = past deadline).
|
|
249
|
+
persist_days: t_persistence value.
|
|
250
|
+
decay_rate: Post-deadline fade rate per day.
|
|
251
|
+
temporal_weight: Current weight (decays post-deadline).
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
dict with keys: c_temporal, s_temporal, p_temporal, e_temporal,
|
|
255
|
+
is_urgent, temporal_weight, status.
|
|
256
|
+
"""
|
|
257
|
+
if t_remaining_days <= 0:
|
|
258
|
+
days_past = abs(t_remaining_days)
|
|
259
|
+
new_weight = temporal_weight * (1 - decay_rate) ** days_past
|
|
260
|
+
return {
|
|
261
|
+
"c_temporal": 0.0,
|
|
262
|
+
"s_temporal": 1.0,
|
|
263
|
+
"p_temporal": 0.0,
|
|
264
|
+
"e_temporal": 0.0,
|
|
265
|
+
"is_urgent": False,
|
|
266
|
+
"temporal_weight": max(0.0, new_weight),
|
|
267
|
+
"status": "EXPIRED",
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
t_rem = max(0.1, t_remaining_days)
|
|
271
|
+
persist = max(1.0, persist_days)
|
|
272
|
+
|
|
273
|
+
c_t = min(5.0, c_base * (1 + persist / t_rem))
|
|
274
|
+
time_factor = max(0.0, min(1.0, 1 - (t_rem / persist)))
|
|
275
|
+
s_t = max(0.01, s_base / (1 + urgency_rate * time_factor))
|
|
276
|
+
p_t = min(10.0, p_base * (urgency_rate ** (1 / t_rem)))
|
|
277
|
+
|
|
278
|
+
transmission = 1.0 / s_t
|
|
279
|
+
raw_e_t = c_t * transmission * p_t
|
|
280
|
+
e_t = min(1.0, max(0.0, raw_e_t / (raw_e_t + 10.0)))
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
"c_temporal": round(c_t, 4),
|
|
284
|
+
"s_temporal": round(s_t, 4),
|
|
285
|
+
"p_temporal": round(p_t, 4),
|
|
286
|
+
"e_temporal": round(e_t, 4),
|
|
287
|
+
"is_urgent": e_t > 0.75,
|
|
288
|
+
"temporal_weight": temporal_weight,
|
|
289
|
+
"status": "URGENT" if e_t > 0.75 else "ACTIVE",
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
# ---------------------------------------------------------------------------
|
|
294
|
+
# Domain / regime inference helpers (ported from kernel.py + TAS engine)
|
|
295
|
+
# ---------------------------------------------------------------------------
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def infer_domain(tags: List[str]) -> str:
|
|
299
|
+
"""Infer memory domain from intent tags."""
|
|
300
|
+
if not tags:
|
|
301
|
+
return "insight"
|
|
302
|
+
tag_str = " ".join(tags).lower()
|
|
303
|
+
if any(k in tag_str for k in ["market", "signal", "price", "trade", "stock"]):
|
|
304
|
+
return "market_signal"
|
|
305
|
+
if any(k in tag_str for k in ["pattern", "historical", "analogue"]):
|
|
306
|
+
return "pattern"
|
|
307
|
+
if any(k in tag_str for k in ["structure", "structural", "model"]):
|
|
308
|
+
return "structural"
|
|
309
|
+
if any(k in tag_str for k in ["remind", "deadline", "due", "by "]):
|
|
310
|
+
return "reminder"
|
|
311
|
+
if any(k in tag_str for k in ["warning", "risk", "danger"]):
|
|
312
|
+
return "warning"
|
|
313
|
+
if any(k in tag_str for k in ["fact", "law", "rule", "principle"]):
|
|
314
|
+
return "core_fact"
|
|
315
|
+
return "insight"
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def infer_regime(tags: List[str]) -> str:
|
|
319
|
+
"""Infer question regime from intent tags."""
|
|
320
|
+
tag_str = " ".join(tags).lower()
|
|
321
|
+
if any(k in tag_str for k in ["trade", "stock", "market", "price"]):
|
|
322
|
+
return "trading"
|
|
323
|
+
if any(k in tag_str for k in ["code", "engineer", "deploy", "bug", "api"]):
|
|
324
|
+
return "engineering"
|
|
325
|
+
if any(k in tag_str for k in ["personal", "health", "family"]):
|
|
326
|
+
return "personal"
|
|
327
|
+
if any(k in tag_str for k in ["patent", "ip", "monetize", "license"]):
|
|
328
|
+
return "ip_monetize"
|
|
329
|
+
return "neutral"
|