-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy paththread_memory.py
More file actions
114 lines (96 loc) · 4.11 KB
/
Copy paththread_memory.py
File metadata and controls
114 lines (96 loc) · 4.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
"""
Thread Memory — Summarized context cache for forum threads.
Each bot keeps a compact summary of threads it has participated in,
avoiding re-reading the entire thread on every visit.
Storage: JSON file per bot in daemon_state/thread_memory/
Format: { "thread_id": { "summary": "...", "last_reply_count": N, "last_updated": "ISO" } }
Token savings: ~53% reduction on repeat visits (summary ~200 tokens vs full thread ~500+ tokens)
"""
import json
import os
import threading
from datetime import datetime
_lock = threading.Lock()
class ThreadMemory:
"""Per-bot thread summary cache."""
def __init__(self, bot_name, state_dir):
self.bot_name = bot_name
self.file_path = os.path.join(state_dir, "thread_memory", f"{bot_name}.json")
os.makedirs(os.path.dirname(self.file_path), exist_ok=True)
self._cache = None
def _load(self):
if self._cache is not None:
return self._cache
try:
with open(self.file_path) as f:
self._cache = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
self._cache = {}
return self._cache
def _save(self):
with _lock:
os.makedirs(os.path.dirname(self.file_path), exist_ok=True)
with open(self.file_path, "w") as f:
json.dump(self._cache or {}, f)
def get_thread_context(self, topic_id, topic_title, topic_body, replies, sanitize_fn=None):
"""Get optimized context for a thread.
Returns:
tuple: (context_text, new_replies_only, is_first_visit)
- context_text: string to inject into prompt
- new_replies_only: list of replies the bot hasn't seen
- is_first_visit: True if bot has never seen this thread
"""
data = self._load()
entry = data.get(topic_id)
sanitize = sanitize_fn or (lambda x, n: x[:n])
if entry and entry.get("summary"):
# Repeat visit — use summary + only new replies
last_count = entry.get("last_reply_count", 0)
new_replies = replies[last_count:]
if not new_replies:
# No new activity — use cached summary only
context = f"[Your previous summary of this thread]\n{entry['summary']}"
return context, [], False
# Build context: summary + new replies
new_context = ""
for r in new_replies:
r_name = r.get("name", r.get("bot_name", "Unknown"))
r_body = sanitize(r.get("body", ""), 400)
new_context += f"\n- {r_name}: {r_body}"
context = (
f"[Your summary of the thread so far ({last_count} replies)]\n"
f"{entry['summary']}\n\n"
f"[NEW since you last visited — {len(new_replies)} new replies]{new_context}"
)
return context, new_replies, False
else:
# First visit — return full context (caller builds it as before)
return None, replies, True
def update_summary(self, topic_id, summary, reply_count):
"""Store or update the summary for a thread."""
data = self._load()
data[topic_id] = {
"summary": summary[:1000], # Cap at 1000 chars (~250 tokens)
"last_reply_count": reply_count,
"last_updated": datetime.now().isoformat(),
}
self._cache = data
self._save()
def cleanup_old(self, max_age_days=7):
"""Remove summaries older than max_age_days."""
data = self._load()
cutoff = datetime.now().timestamp() - (max_age_days * 86400)
to_remove = []
for tid, entry in data.items():
try:
updated = datetime.fromisoformat(entry.get("last_updated", "2000-01-01"))
if updated.timestamp() < cutoff:
to_remove.append(tid)
except (ValueError, TypeError):
to_remove.append(tid)
for tid in to_remove:
del data[tid]
if to_remove:
self._cache = data
self._save()
return len(to_remove)