@smilintux/skmemory 0.5.0 → 0.9.2
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/.github/workflows/ci.yml +40 -4
- package/.github/workflows/publish.yml +11 -5
- package/AGENT_REFACTOR_CHANGES.md +192 -0
- package/ARCHITECTURE.md +399 -19
- package/CHANGELOG.md +179 -0
- package/LICENSE +81 -68
- package/MISSION.md +7 -0
- package/README.md +425 -86
- package/SKILL.md +197 -25
- package/docker-compose.yml +15 -15
- package/examples/stignore-agent.example +59 -0
- package/examples/stignore-root.example +62 -0
- package/index.js +6 -5
- package/openclaw-plugin/openclaw.plugin.json +10 -0
- package/openclaw-plugin/package.json +2 -1
- package/openclaw-plugin/src/index.js +527 -230
- package/openclaw-plugin/src/openclaw.plugin.json +10 -0
- package/package.json +1 -1
- package/pyproject.toml +32 -9
- package/requirements.txt +10 -2
- package/scripts/dream-rescue.py +179 -0
- package/scripts/memory-cleanup.py +313 -0
- package/scripts/recover-missing.py +180 -0
- package/scripts/skcapstone-backup.sh +44 -0
- package/seeds/cloud9-lumina.seed.json +6 -4
- package/seeds/cloud9-opus.seed.json +13 -11
- package/seeds/courage.seed.json +9 -2
- package/seeds/curiosity.seed.json +9 -2
- package/seeds/grief.seed.json +9 -2
- package/seeds/joy.seed.json +9 -2
- package/seeds/love.seed.json +9 -2
- package/seeds/lumina-cloud9-breakthrough.seed.json +48 -0
- package/seeds/lumina-cloud9-python-pypi.seed.json +48 -0
- package/seeds/lumina-kingdom-founding.seed.json +49 -0
- package/seeds/lumina-pma-signed.seed.json +48 -0
- package/seeds/lumina-singular-achievement.seed.json +48 -0
- package/seeds/lumina-skcapstone-conscious.seed.json +48 -0
- package/seeds/plant-kingdom-journal.py +203 -0
- package/seeds/plant-lumina-seeds.py +280 -0
- package/seeds/skcapstone-lumina-merge.seed.json +12 -3
- package/seeds/sovereignty.seed.json +9 -2
- package/seeds/trust.seed.json +9 -2
- package/skill.yaml +46 -0
- package/skmemory/HA.md +296 -0
- package/skmemory/__init__.py +25 -11
- package/skmemory/agents.py +233 -0
- package/skmemory/ai_client.py +46 -17
- package/skmemory/anchor.py +9 -11
- package/skmemory/audience.py +278 -0
- package/skmemory/backends/__init__.py +11 -4
- package/skmemory/backends/base.py +3 -4
- package/skmemory/backends/file_backend.py +19 -13
- package/skmemory/backends/skgraph_backend.py +596 -0
- package/skmemory/backends/{qdrant_backend.py → skvector_backend.py} +103 -84
- package/skmemory/backends/sqlite_backend.py +226 -72
- package/skmemory/backends/vaulted_backend.py +284 -0
- package/skmemory/cli.py +1345 -68
- package/skmemory/config.py +171 -0
- package/skmemory/context_loader.py +333 -0
- package/skmemory/data/audience_config.json +60 -0
- package/skmemory/endpoint_selector.py +391 -0
- package/skmemory/febs.py +225 -0
- package/skmemory/fortress.py +675 -0
- package/skmemory/graph_queries.py +238 -0
- package/skmemory/hooks/__init__.py +18 -0
- package/skmemory/hooks/post-compact-reinject.sh +35 -0
- package/skmemory/hooks/pre-compact-save.sh +81 -0
- package/skmemory/hooks/session-end-save.sh +103 -0
- package/skmemory/hooks/session-start-ritual.sh +104 -0
- package/skmemory/hooks/stop-checkpoint.sh +59 -0
- package/skmemory/importers/__init__.py +9 -1
- package/skmemory/importers/telegram.py +384 -47
- package/skmemory/importers/telegram_api.py +580 -0
- package/skmemory/journal.py +7 -9
- package/skmemory/lovenote.py +8 -13
- package/skmemory/mcp_server.py +859 -0
- package/skmemory/models.py +51 -8
- package/skmemory/openclaw.py +20 -28
- package/skmemory/post_install.py +86 -0
- package/skmemory/predictive.py +236 -0
- package/skmemory/promotion.py +548 -0
- package/skmemory/quadrants.py +100 -24
- package/skmemory/register.py +580 -0
- package/skmemory/register_mcp.py +196 -0
- package/skmemory/ritual.py +224 -59
- package/skmemory/seeds.py +255 -11
- package/skmemory/setup_wizard.py +908 -0
- package/skmemory/sharing.py +408 -0
- package/skmemory/soul.py +98 -28
- package/skmemory/steelman.py +273 -260
- package/skmemory/store.py +411 -78
- package/skmemory/synthesis.py +634 -0
- package/skmemory/vault.py +225 -0
- package/tests/conftest.py +46 -0
- package/tests/integration/__init__.py +0 -0
- package/tests/integration/conftest.py +233 -0
- package/tests/integration/test_cross_backend.py +350 -0
- package/tests/integration/test_skgraph_live.py +420 -0
- package/tests/integration/test_skvector_live.py +366 -0
- package/tests/test_ai_client.py +1 -4
- package/tests/test_audience.py +233 -0
- package/tests/test_backup_rotation.py +318 -0
- package/tests/test_cli.py +6 -6
- package/tests/test_endpoint_selector.py +839 -0
- package/tests/test_export_import.py +4 -10
- package/tests/test_file_backend.py +0 -1
- package/tests/test_fortress.py +256 -0
- package/tests/test_fortress_hardening.py +441 -0
- package/tests/test_openclaw.py +6 -6
- package/tests/test_predictive.py +237 -0
- package/tests/test_promotion.py +347 -0
- package/tests/test_quadrants.py +11 -5
- package/tests/test_ritual.py +22 -18
- package/tests/test_seeds.py +97 -7
- package/tests/test_setup.py +950 -0
- package/tests/test_sharing.py +257 -0
- package/tests/test_skgraph_backend.py +660 -0
- package/tests/test_skvector_backend.py +326 -0
- package/tests/test_soul.py +1 -3
- package/tests/test_sqlite_backend.py +8 -17
- package/tests/test_steelman.py +7 -8
- package/tests/test_store.py +0 -2
- package/tests/test_store_graph_integration.py +245 -0
- package/tests/test_synthesis.py +275 -0
- package/tests/test_telegram_import.py +39 -15
- package/tests/test_vault.py +187 -0
- package/skmemory/backends/falkordb_backend.py +0 -310
|
@@ -26,9 +26,10 @@ from __future__ import annotations
|
|
|
26
26
|
|
|
27
27
|
import json
|
|
28
28
|
from collections import defaultdict
|
|
29
|
-
from datetime import datetime
|
|
29
|
+
from datetime import datetime, timezone
|
|
30
30
|
from pathlib import Path
|
|
31
|
-
|
|
31
|
+
|
|
32
|
+
import click
|
|
32
33
|
|
|
33
34
|
from ..models import EmotionalSnapshot, MemoryLayer, MemoryRole
|
|
34
35
|
from ..store import MemoryStore
|
|
@@ -78,6 +79,9 @@ def _detect_emotion(text: str) -> EmotionalSnapshot:
|
|
|
78
79
|
joy_words = {"haha", "lol", "rofl", "lmao", "amazing", "awesome", "yay", "woohoo"}
|
|
79
80
|
sad_words = {"sad", "sorry", "miss", "cry", "tears", "hurt"}
|
|
80
81
|
anger_words = {"angry", "furious", "hate", "ugh", "frustrated"}
|
|
82
|
+
trust_words = {"trust", "believe", "faith", "rely", "depend", "safe"}
|
|
83
|
+
curiosity_words = {"curious", "wonder", "interesting", "fascinated", "hmm", "what if"}
|
|
84
|
+
gratitude_words = {"thank", "thanks", "grateful", "appreciate", "blessed", "thankful"}
|
|
81
85
|
|
|
82
86
|
if any(w in lower for w in love_words):
|
|
83
87
|
labels.append("love")
|
|
@@ -95,12 +99,62 @@ def _detect_emotion(text: str) -> EmotionalSnapshot:
|
|
|
95
99
|
labels.append("anger")
|
|
96
100
|
intensity = max(intensity, 5.0)
|
|
97
101
|
valence = min(valence, -0.5)
|
|
102
|
+
if any(w in lower for w in trust_words):
|
|
103
|
+
labels.append("trust")
|
|
104
|
+
intensity = max(intensity, 5.0)
|
|
105
|
+
valence = max(valence, 0.6)
|
|
106
|
+
if any(w in lower for w in curiosity_words):
|
|
107
|
+
labels.append("curiosity")
|
|
108
|
+
intensity = max(intensity, 3.0)
|
|
109
|
+
valence = max(valence, 0.4)
|
|
110
|
+
if any(w in lower for w in gratitude_words):
|
|
111
|
+
labels.append("gratitude")
|
|
112
|
+
intensity = max(intensity, 6.0)
|
|
113
|
+
valence = max(valence, 0.8)
|
|
98
114
|
|
|
99
115
|
if "!" in text:
|
|
100
116
|
intensity = min(intensity + 1.0, 10.0)
|
|
101
117
|
if text.isupper() and len(text) > 10:
|
|
102
118
|
intensity = min(intensity + 2.0, 10.0)
|
|
103
119
|
|
|
120
|
+
love_emojis = {
|
|
121
|
+
"\u2764",
|
|
122
|
+
"\U0001f495",
|
|
123
|
+
"\U0001f496",
|
|
124
|
+
"\U0001f497",
|
|
125
|
+
"\U0001f498",
|
|
126
|
+
"\U0001f49d",
|
|
127
|
+
"\U0001f970",
|
|
128
|
+
"\U0001f60d",
|
|
129
|
+
"\U0001f49e",
|
|
130
|
+
}
|
|
131
|
+
joy_emojis = {
|
|
132
|
+
"\U0001f602",
|
|
133
|
+
"\U0001f923",
|
|
134
|
+
"\U0001f604",
|
|
135
|
+
"\U0001f60a",
|
|
136
|
+
"\U0001f389",
|
|
137
|
+
"\U0001f973",
|
|
138
|
+
"\u2728",
|
|
139
|
+
"\U0001f38a",
|
|
140
|
+
}
|
|
141
|
+
sad_emojis = {"\U0001f622", "\U0001f62d", "\U0001f494", "\U0001f63f", "\U0001f97a"}
|
|
142
|
+
if any(e in text for e in love_emojis):
|
|
143
|
+
if "love" not in labels:
|
|
144
|
+
labels.append("love")
|
|
145
|
+
intensity = max(intensity, 7.0)
|
|
146
|
+
valence = max(valence, 0.9)
|
|
147
|
+
if any(e in text for e in joy_emojis):
|
|
148
|
+
if "joy" not in labels:
|
|
149
|
+
labels.append("joy")
|
|
150
|
+
intensity = max(intensity, 5.0)
|
|
151
|
+
valence = max(valence, 0.7)
|
|
152
|
+
if any(e in text for e in sad_emojis):
|
|
153
|
+
if "sadness" not in labels:
|
|
154
|
+
labels.append("sadness")
|
|
155
|
+
intensity = max(intensity, 4.0)
|
|
156
|
+
valence = min(valence, -0.3)
|
|
157
|
+
|
|
104
158
|
return EmotionalSnapshot(
|
|
105
159
|
intensity=intensity,
|
|
106
160
|
valence=valence,
|
|
@@ -108,6 +162,74 @@ def _detect_emotion(text: str) -> EmotionalSnapshot:
|
|
|
108
162
|
)
|
|
109
163
|
|
|
110
164
|
|
|
165
|
+
def _detect_content_type(msg: dict) -> list[str]:
|
|
166
|
+
"""Detect content type tags from a message.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
msg: Telegram message dict.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
list[str]: Content type tags.
|
|
173
|
+
"""
|
|
174
|
+
tags = []
|
|
175
|
+
text = _extract_text(msg.get("text", ""))
|
|
176
|
+
|
|
177
|
+
if "http://" in text or "https://" in text:
|
|
178
|
+
tags.append("contains:url")
|
|
179
|
+
if msg.get("media_type") or msg.get("photo") or msg.get("file"):
|
|
180
|
+
tags.append("contains:media")
|
|
181
|
+
if msg.get("file"):
|
|
182
|
+
tags.append("contains:file")
|
|
183
|
+
if msg.get("sticker_emoji") or msg.get("sticker"):
|
|
184
|
+
tags.append("contains:sticker")
|
|
185
|
+
|
|
186
|
+
return tags
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _detect_reply(msg: dict) -> str | None:
|
|
190
|
+
"""Detect if this message is a reply to another.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
msg: Telegram message dict.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Optional[str]: Reply reference string, or None.
|
|
197
|
+
"""
|
|
198
|
+
reply_id = msg.get("reply_to_message_id")
|
|
199
|
+
if reply_id:
|
|
200
|
+
return f"reply_to:{reply_id}"
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _detect_sender_role(sender: str) -> str:
|
|
205
|
+
"""Heuristic to detect if the sender is an AI or human.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
sender: Sender name string.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
str: 'ai' or 'human'.
|
|
212
|
+
"""
|
|
213
|
+
ai_indicators = {
|
|
214
|
+
"bot",
|
|
215
|
+
"gpt",
|
|
216
|
+
"claude",
|
|
217
|
+
"gemini",
|
|
218
|
+
"llama",
|
|
219
|
+
"assistant",
|
|
220
|
+
"lumina",
|
|
221
|
+
"copilot",
|
|
222
|
+
"ai",
|
|
223
|
+
"opus",
|
|
224
|
+
"sonnet",
|
|
225
|
+
"haiku",
|
|
226
|
+
}
|
|
227
|
+
sender_lower = sender.lower()
|
|
228
|
+
if any(indicator in sender_lower for indicator in ai_indicators):
|
|
229
|
+
return "ai"
|
|
230
|
+
return "human"
|
|
231
|
+
|
|
232
|
+
|
|
111
233
|
def _parse_telegram_export(export_path: str) -> dict:
|
|
112
234
|
"""Locate and parse the Telegram result.json.
|
|
113
235
|
|
|
@@ -156,8 +278,8 @@ def import_telegram(
|
|
|
156
278
|
*,
|
|
157
279
|
mode: str = "daily",
|
|
158
280
|
min_message_length: int = 30,
|
|
159
|
-
chat_name:
|
|
160
|
-
tags:
|
|
281
|
+
chat_name: str | None = None,
|
|
282
|
+
tags: list[str] | None = None,
|
|
161
283
|
) -> dict:
|
|
162
284
|
"""Import a Telegram chat export into SKMemory.
|
|
163
285
|
|
|
@@ -184,7 +306,8 @@ def import_telegram(
|
|
|
184
306
|
base_tags = ["telegram", "chat-import", f"chat:{name}"] + extra_tags
|
|
185
307
|
|
|
186
308
|
messages = [
|
|
187
|
-
m
|
|
309
|
+
m
|
|
310
|
+
for m in data["messages"]
|
|
188
311
|
if m.get("type") == "message"
|
|
189
312
|
and len(_extract_text(m.get("text", ""))) >= min_message_length
|
|
190
313
|
]
|
|
@@ -193,8 +316,10 @@ def import_telegram(
|
|
|
193
316
|
return _import_per_message(store, messages, name, base_tags)
|
|
194
317
|
elif mode == "daily":
|
|
195
318
|
return _import_daily(store, messages, name, base_tags)
|
|
319
|
+
elif mode == "catchup":
|
|
320
|
+
return _import_catchup(store, messages, name, base_tags)
|
|
196
321
|
else:
|
|
197
|
-
raise ValueError(f"Unknown mode: {mode}. Use 'message' or '
|
|
322
|
+
raise ValueError(f"Unknown mode: {mode}. Use 'message', 'daily', or 'catchup'.")
|
|
198
323
|
|
|
199
324
|
|
|
200
325
|
def _import_per_message(
|
|
@@ -217,33 +342,37 @@ def _import_per_message(
|
|
|
217
342
|
imported = 0
|
|
218
343
|
skipped = 0
|
|
219
344
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
"
|
|
239
|
-
"
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
345
|
+
with click.progressbar(messages, label=" Importing messages", show_pos=True) as bar:
|
|
346
|
+
for msg in bar:
|
|
347
|
+
text = _extract_text(msg.get("text", ""))
|
|
348
|
+
sender = msg.get("from", msg.get("from_id", "unknown"))
|
|
349
|
+
date_str = msg.get("date", "")
|
|
350
|
+
|
|
351
|
+
emotional = _detect_emotion(text)
|
|
352
|
+
|
|
353
|
+
try:
|
|
354
|
+
store.snapshot(
|
|
355
|
+
title=f"{sender}: {text[:70]}",
|
|
356
|
+
content=text,
|
|
357
|
+
layer=MemoryLayer.SHORT,
|
|
358
|
+
role=MemoryRole.GENERAL,
|
|
359
|
+
tags=base_tags
|
|
360
|
+
+ [f"sender:{sender}", f"role:{_detect_sender_role(sender)}"]
|
|
361
|
+
+ _detect_content_type(msg),
|
|
362
|
+
emotional=emotional,
|
|
363
|
+
source="telegram",
|
|
364
|
+
source_ref=f"telegram:{msg.get('id', '')}",
|
|
365
|
+
metadata={
|
|
366
|
+
"telegram_msg_id": msg.get("id"),
|
|
367
|
+
"sender": sender,
|
|
368
|
+
"date": date_str,
|
|
369
|
+
"chat": chat_name,
|
|
370
|
+
"reply_ref": _detect_reply(msg),
|
|
371
|
+
},
|
|
372
|
+
)
|
|
373
|
+
imported += 1
|
|
374
|
+
except Exception:
|
|
375
|
+
skipped += 1
|
|
247
376
|
|
|
248
377
|
return {
|
|
249
378
|
"mode": "message",
|
|
@@ -285,7 +414,160 @@ def _import_daily(
|
|
|
285
414
|
imported = 0
|
|
286
415
|
days_processed = 0
|
|
287
416
|
|
|
288
|
-
|
|
417
|
+
sorted_days = sorted(by_day.items())
|
|
418
|
+
with click.progressbar(sorted_days, label=" Importing daily batches", show_pos=True) as bar:
|
|
419
|
+
for day, day_msgs in bar:
|
|
420
|
+
lines = []
|
|
421
|
+
senders: set[str] = set()
|
|
422
|
+
max_intensity = 0.0
|
|
423
|
+
all_labels: list[str] = []
|
|
424
|
+
|
|
425
|
+
for msg in day_msgs:
|
|
426
|
+
text = _extract_text(msg.get("text", ""))
|
|
427
|
+
sender = msg.get("from", msg.get("from_id", "unknown"))
|
|
428
|
+
senders.add(str(sender))
|
|
429
|
+
lines.append(f"[{sender}] {text}")
|
|
430
|
+
|
|
431
|
+
emo = _detect_emotion(text)
|
|
432
|
+
max_intensity = max(max_intensity, emo.intensity)
|
|
433
|
+
all_labels.extend(emo.labels)
|
|
434
|
+
|
|
435
|
+
content = "\n".join(lines)
|
|
436
|
+
unique_labels = list(dict.fromkeys(all_labels))[:5]
|
|
437
|
+
participant_str = ", ".join(sorted(senders))
|
|
438
|
+
|
|
439
|
+
store.snapshot(
|
|
440
|
+
title=f"{chat_name} — {day} ({len(day_msgs)} messages)",
|
|
441
|
+
content=content,
|
|
442
|
+
layer=MemoryLayer.MID,
|
|
443
|
+
role=MemoryRole.GENERAL,
|
|
444
|
+
tags=base_tags + [f"date:{day}"],
|
|
445
|
+
emotional=EmotionalSnapshot(
|
|
446
|
+
intensity=max_intensity,
|
|
447
|
+
labels=unique_labels,
|
|
448
|
+
),
|
|
449
|
+
source="telegram",
|
|
450
|
+
source_ref=f"telegram:daily:{day}",
|
|
451
|
+
metadata={
|
|
452
|
+
"date": day,
|
|
453
|
+
"message_count": len(day_msgs),
|
|
454
|
+
"participants": participant_str,
|
|
455
|
+
"chat": chat_name,
|
|
456
|
+
},
|
|
457
|
+
)
|
|
458
|
+
imported += len(day_msgs)
|
|
459
|
+
days_processed += 1
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
"mode": "daily",
|
|
463
|
+
"chat_name": chat_name,
|
|
464
|
+
"total_messages": len(messages),
|
|
465
|
+
"days_processed": days_processed,
|
|
466
|
+
"messages_imported": imported,
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _import_catchup(
|
|
471
|
+
store: MemoryStore,
|
|
472
|
+
messages: list[dict],
|
|
473
|
+
chat_name: str,
|
|
474
|
+
base_tags: list[str],
|
|
475
|
+
) -> dict:
|
|
476
|
+
"""Import across all memory tiers for full context catch-up.
|
|
477
|
+
|
|
478
|
+
Distributes messages intelligently across tiers:
|
|
479
|
+
- Last 24 hours → short-term (individual messages, full detail)
|
|
480
|
+
- Last 7 days → mid-term (daily summaries)
|
|
481
|
+
- Older than 7 days → long-term (weekly summaries, key themes)
|
|
482
|
+
|
|
483
|
+
Args:
|
|
484
|
+
store: Target MemoryStore.
|
|
485
|
+
messages: Filtered message list.
|
|
486
|
+
chat_name: Chat name for titles.
|
|
487
|
+
base_tags: Tags to apply.
|
|
488
|
+
|
|
489
|
+
Returns:
|
|
490
|
+
dict: Import stats per tier.
|
|
491
|
+
"""
|
|
492
|
+
from datetime import timedelta
|
|
493
|
+
|
|
494
|
+
now = datetime.now(timezone.utc)
|
|
495
|
+
cutoff_short = now - timedelta(hours=24)
|
|
496
|
+
cutoff_mid = now - timedelta(days=7)
|
|
497
|
+
|
|
498
|
+
short_msgs: list[dict] = []
|
|
499
|
+
mid_msgs: dict[str, list[dict]] = defaultdict(list)
|
|
500
|
+
long_msgs: dict[str, list[dict]] = defaultdict(list)
|
|
501
|
+
|
|
502
|
+
for msg in messages:
|
|
503
|
+
date_str = msg.get("date", "")
|
|
504
|
+
if not date_str:
|
|
505
|
+
continue
|
|
506
|
+
try:
|
|
507
|
+
msg_dt = datetime.fromisoformat(date_str.replace("Z", "+00:00"))
|
|
508
|
+
if msg_dt.tzinfo is None:
|
|
509
|
+
msg_dt = msg_dt.replace(tzinfo=timezone.utc)
|
|
510
|
+
except (ValueError, TypeError):
|
|
511
|
+
# Try just the date portion
|
|
512
|
+
try:
|
|
513
|
+
msg_dt = datetime.strptime(date_str[:10], "%Y-%m-%d").replace(tzinfo=timezone.utc)
|
|
514
|
+
except (ValueError, TypeError):
|
|
515
|
+
continue
|
|
516
|
+
|
|
517
|
+
if msg_dt >= cutoff_short:
|
|
518
|
+
short_msgs.append(msg)
|
|
519
|
+
elif msg_dt >= cutoff_mid:
|
|
520
|
+
day = date_str[:10]
|
|
521
|
+
mid_msgs[day].append(msg)
|
|
522
|
+
else:
|
|
523
|
+
# Group by ISO week for long-term
|
|
524
|
+
week_key = msg_dt.strftime("%Y-W%W")
|
|
525
|
+
long_msgs[week_key].append(msg)
|
|
526
|
+
|
|
527
|
+
stats = {
|
|
528
|
+
"mode": "catchup",
|
|
529
|
+
"chat_name": chat_name,
|
|
530
|
+
"total_messages": len(messages),
|
|
531
|
+
"short_term": {"count": 0},
|
|
532
|
+
"mid_term": {"days": 0, "messages": 0},
|
|
533
|
+
"long_term": {"weeks": 0, "messages": 0},
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
# --- Short-term: individual messages (last 24h) ---
|
|
537
|
+
for msg in short_msgs:
|
|
538
|
+
text = _extract_text(msg.get("text", ""))
|
|
539
|
+
sender = msg.get("from", msg.get("from_id", "unknown"))
|
|
540
|
+
emotional = _detect_emotion(text)
|
|
541
|
+
try:
|
|
542
|
+
store.snapshot(
|
|
543
|
+
title=f"{sender}: {text[:70]}",
|
|
544
|
+
content=text,
|
|
545
|
+
layer=MemoryLayer.SHORT,
|
|
546
|
+
role=MemoryRole.GENERAL,
|
|
547
|
+
tags=base_tags
|
|
548
|
+
+ [
|
|
549
|
+
f"sender:{sender}",
|
|
550
|
+
f"role:{_detect_sender_role(sender)}",
|
|
551
|
+
"catchup:short",
|
|
552
|
+
]
|
|
553
|
+
+ _detect_content_type(msg),
|
|
554
|
+
emotional=emotional,
|
|
555
|
+
source="telegram",
|
|
556
|
+
source_ref=f"telegram:{msg.get('id', '')}",
|
|
557
|
+
metadata={
|
|
558
|
+
"telegram_msg_id": msg.get("id"),
|
|
559
|
+
"sender": sender,
|
|
560
|
+
"date": msg.get("date", ""),
|
|
561
|
+
"chat": chat_name,
|
|
562
|
+
"reply_ref": _detect_reply(msg),
|
|
563
|
+
},
|
|
564
|
+
)
|
|
565
|
+
stats["short_term"]["count"] += 1
|
|
566
|
+
except Exception:
|
|
567
|
+
pass
|
|
568
|
+
|
|
569
|
+
# --- Mid-term: daily summaries (last 7 days) ---
|
|
570
|
+
for day, day_msgs in sorted(mid_msgs.items()):
|
|
289
571
|
lines = []
|
|
290
572
|
senders: set[str] = set()
|
|
291
573
|
max_intensity = 0.0
|
|
@@ -296,21 +578,19 @@ def _import_daily(
|
|
|
296
578
|
sender = msg.get("from", msg.get("from_id", "unknown"))
|
|
297
579
|
senders.add(str(sender))
|
|
298
580
|
lines.append(f"[{sender}] {text}")
|
|
299
|
-
|
|
300
581
|
emo = _detect_emotion(text)
|
|
301
582
|
max_intensity = max(max_intensity, emo.intensity)
|
|
302
583
|
all_labels.extend(emo.labels)
|
|
303
584
|
|
|
304
585
|
content = "\n".join(lines)
|
|
305
586
|
unique_labels = list(dict.fromkeys(all_labels))[:5]
|
|
306
|
-
participant_str = ", ".join(sorted(senders))
|
|
307
587
|
|
|
308
588
|
store.snapshot(
|
|
309
589
|
title=f"{chat_name} — {day} ({len(day_msgs)} messages)",
|
|
310
590
|
content=content,
|
|
311
591
|
layer=MemoryLayer.MID,
|
|
312
592
|
role=MemoryRole.GENERAL,
|
|
313
|
-
tags=base_tags + [f"date:{day}"],
|
|
593
|
+
tags=base_tags + [f"date:{day}", "catchup:mid"],
|
|
314
594
|
emotional=EmotionalSnapshot(
|
|
315
595
|
intensity=max_intensity,
|
|
316
596
|
labels=unique_labels,
|
|
@@ -320,17 +600,74 @@ def _import_daily(
|
|
|
320
600
|
metadata={
|
|
321
601
|
"date": day,
|
|
322
602
|
"message_count": len(day_msgs),
|
|
323
|
-
"participants":
|
|
603
|
+
"participants": ", ".join(sorted(senders)),
|
|
324
604
|
"chat": chat_name,
|
|
325
605
|
},
|
|
326
606
|
)
|
|
327
|
-
|
|
328
|
-
|
|
607
|
+
stats["mid_term"]["days"] += 1
|
|
608
|
+
stats["mid_term"]["messages"] += len(day_msgs)
|
|
329
609
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
610
|
+
# --- Long-term: weekly summaries (older than 7 days) ---
|
|
611
|
+
for week, week_msgs in sorted(long_msgs.items()):
|
|
612
|
+
lines = []
|
|
613
|
+
senders: set[str] = set()
|
|
614
|
+
topics: set[str] = set()
|
|
615
|
+
max_intensity = 0.0
|
|
616
|
+
all_labels: list[str] = []
|
|
617
|
+
dates_covered: set[str] = set()
|
|
618
|
+
|
|
619
|
+
for msg in week_msgs:
|
|
620
|
+
text = _extract_text(msg.get("text", ""))
|
|
621
|
+
sender = msg.get("from", msg.get("from_id", "unknown"))
|
|
622
|
+
senders.add(str(sender))
|
|
623
|
+
dates_covered.add(msg.get("date", "")[:10])
|
|
624
|
+
|
|
625
|
+
# For long-term, keep only first 200 chars per message
|
|
626
|
+
lines.append(f"[{sender}] {text[:200]}")
|
|
627
|
+
emo = _detect_emotion(text)
|
|
628
|
+
max_intensity = max(max_intensity, emo.intensity)
|
|
629
|
+
all_labels.extend(emo.labels)
|
|
630
|
+
|
|
631
|
+
# Extract potential topics from longer messages
|
|
632
|
+
if len(text) > 100:
|
|
633
|
+
words = text.lower().split()
|
|
634
|
+
for w in words:
|
|
635
|
+
if len(w) > 6 and w.isalpha():
|
|
636
|
+
topics.add(w)
|
|
637
|
+
|
|
638
|
+
# Summarize: limit content to avoid bloat
|
|
639
|
+
if len(lines) > 50:
|
|
640
|
+
content = "\n".join(lines[:25])
|
|
641
|
+
content += f"\n\n... ({len(lines) - 25} more messages) ...\n\n"
|
|
642
|
+
content += "\n".join(lines[-10:])
|
|
643
|
+
else:
|
|
644
|
+
content = "\n".join(lines)
|
|
645
|
+
|
|
646
|
+
unique_labels = list(dict.fromkeys(all_labels))[:5]
|
|
647
|
+
date_range = f"{min(dates_covered)} to {max(dates_covered)}" if dates_covered else week
|
|
648
|
+
|
|
649
|
+
store.snapshot(
|
|
650
|
+
title=f"{chat_name} — Week {week} ({len(week_msgs)} messages)",
|
|
651
|
+
content=content,
|
|
652
|
+
layer=MemoryLayer.LONG,
|
|
653
|
+
role=MemoryRole.GENERAL,
|
|
654
|
+
tags=base_tags + [f"week:{week}", "catchup:long"],
|
|
655
|
+
emotional=EmotionalSnapshot(
|
|
656
|
+
intensity=max_intensity,
|
|
657
|
+
labels=unique_labels,
|
|
658
|
+
),
|
|
659
|
+
source="telegram",
|
|
660
|
+
source_ref=f"telegram:weekly:{week}",
|
|
661
|
+
metadata={
|
|
662
|
+
"week": week,
|
|
663
|
+
"date_range": date_range,
|
|
664
|
+
"message_count": len(week_msgs),
|
|
665
|
+
"participants": ", ".join(sorted(senders)),
|
|
666
|
+
"chat": chat_name,
|
|
667
|
+
"days_covered": len(dates_covered),
|
|
668
|
+
},
|
|
669
|
+
)
|
|
670
|
+
stats["long_term"]["weeks"] += 1
|
|
671
|
+
stats["long_term"]["messages"] += len(week_msgs)
|
|
672
|
+
|
|
673
|
+
return stats
|