alive-ai 0.1.0
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/Dockerfile +24 -0
- package/LICENSE +21 -0
- package/README.md +143 -0
- package/alive_ai/__init__.py +3 -0
- package/brain/__init__.py +59 -0
- package/brain/almost_said.py +154 -0
- package/brain/bid_detector.py +636 -0
- package/brain/conversation_flow.py +135 -0
- package/brain/curiosity.py +328 -0
- package/brain/default_mode.py +1438 -0
- package/brain/dreams.py +220 -0
- package/brain/embeddings/__init__.py +82 -0
- package/brain/emotional_memory.py +949 -0
- package/brain/global_activity.py +173 -0
- package/brain/group_dynamics.py +63 -0
- package/brain/linguistic.py +235 -0
- package/brain/llm/__init__.py +63 -0
- package/brain/llm/base.py +33 -0
- package/brain/llm/fallback_router.py +309 -0
- package/brain/llm/manifest.md +30 -0
- package/brain/llm/ollama.py +218 -0
- package/brain/llm/openrouter.py +151 -0
- package/brain/llm/provider.py +205 -0
- package/brain/llm/unified.py +423 -0
- package/brain/llm/zai.py +169 -0
- package/brain/manifest.md +23 -0
- package/brain/memory/__init__.py +123 -0
- package/brain/memory/episodic.py +92 -0
- package/brain/memory/fact_extractor.py +209 -0
- package/brain/memory/index.py +54 -0
- package/brain/memory/manager.py +151 -0
- package/brain/memory/summarizer.py +102 -0
- package/brain/memory/vector_store.py +297 -0
- package/brain/memory/working.py +43 -0
- package/brain/narrative.py +343 -0
- package/brain/stt/__init__.py +4 -0
- package/brain/stt/google_stt.py +83 -0
- package/brain/stt/whisper_stt.py +82 -0
- package/brain/subconscious/__init__.py +33 -0
- package/brain/subconscious/actions.py +136 -0
- package/brain/subconscious/evaluation.py +166 -0
- package/brain/subconscious/goal_system.py +90 -0
- package/brain/subconscious/goals.py +41 -0
- package/brain/subconscious/impulse_generator.py +200 -0
- package/brain/subconscious/impulses.py +48 -0
- package/brain/subconscious/learning.py +24 -0
- package/brain/subconscious/learning_system.py +79 -0
- package/brain/subconscious/loop.py +398 -0
- package/brain/subconscious/manifest.md +32 -0
- package/brain/subconscious/relationship.py +47 -0
- package/brain/subconscious/relationship_memory.py +83 -0
- package/brain/subconscious/response_analyzer.py +74 -0
- package/brain/subconscious/templates.py +70 -0
- package/brain/subconscious/thought.py +37 -0
- package/brain/subconscious/working_memory.py +97 -0
- package/cli/index.js +371 -0
- package/config/directives.example.json +28 -0
- package/config/instructions.example.md +16 -0
- package/config/self.example.json +74 -0
- package/config/settings.example.json +95 -0
- package/core/__init__.py +1 -0
- package/core/config.py +54 -0
- package/core/directives.py +198 -0
- package/core/events.py +50 -0
- package/core/follow_up.py +267 -0
- package/core/hot_reload.py +174 -0
- package/core/initialization.py +253 -0
- package/core/manifest.md +28 -0
- package/core/media_handler.py +241 -0
- package/core/memory_monitor.py +200 -0
- package/core/message_handler.py +1440 -0
- package/core/proactive_generator.py +277 -0
- package/core/self.py +188 -0
- package/core/settings.py +169 -0
- package/core/skills_registry.py +357 -0
- package/core/state.py +27 -0
- package/core/subconscious_bridge.py +93 -0
- package/core/thinking.py +175 -0
- package/core/user_manager.py +306 -0
- package/core/user_tracker.py +144 -0
- package/demo/index.html +144 -0
- package/docker-compose.yml +28 -0
- package/docs/assets/logo.svg +15 -0
- package/docs/index.html +355 -0
- package/heart/__init__.py +93 -0
- package/heart/afterglow.py +215 -0
- package/heart/attachment.py +186 -0
- package/heart/circadian.py +251 -0
- package/heart/complex_emotions.py +114 -0
- package/heart/conflicts.py +589 -0
- package/heart/core.py +387 -0
- package/heart/emotional_decay.py +59 -0
- package/heart/emotional_memory.py +261 -0
- package/heart/emotional_state.py +146 -0
- package/heart/emotional_variability.py +156 -0
- package/heart/hormonal.py +424 -0
- package/heart/inconsistency.py +1222 -0
- package/heart/integrity.py +469 -0
- package/heart/interoception.py +997 -0
- package/heart/love.py +120 -0
- package/heart/manifest.md +25 -0
- package/heart/mood_shifts.py +169 -0
- package/heart/phantom_somatic.py +259 -0
- package/heart/predictive.py +374 -0
- package/heart/scars.py +474 -0
- package/heart/somatic.py +482 -0
- package/heart/soul.py +633 -0
- package/heart/telemetry.py +942 -0
- package/heart/triggers.py +119 -0
- package/heart/unconscious.py +443 -0
- package/input/__init__.py +1 -0
- package/input/manifest.md +24 -0
- package/input/telegram/__init__.py +1 -0
- package/input/telegram/commands.py +762 -0
- package/input/telegram/listener.py +532 -0
- package/main.py +90 -0
- package/manifest.md +28 -0
- package/mypics/.gitkeep +1 -0
- package/myvids/.gitkeep +1 -0
- package/output/__init__.py +1 -0
- package/output/images/__init__.py +1 -0
- package/output/images/fal_gen.py +43 -0
- package/output/manifest.md +26 -0
- package/output/text/__init__.py +1 -0
- package/output/text/sender.py +22 -0
- package/output/voice/__init__.py +64 -0
- package/output/voice/google_tts.py +252 -0
- package/output/voice/gtts_tts.py +214 -0
- package/output/voice/vibe_tts.py +190 -0
- package/package.json +58 -0
- package/pyproject.toml +23 -0
- package/requirements.txt +21 -0
- package/skills/__init__.py +1 -0
- package/skills/anticipation_engine/__init__.py +8 -0
- package/skills/anticipation_engine/engine.py +618 -0
- package/skills/anticipation_engine/manifest.md +192 -0
- package/skills/calendar/__init__.py +1 -0
- package/skills/content_unlocks/__init__.py +8 -0
- package/skills/content_unlocks/manifest.md +231 -0
- package/skills/content_unlocks/unlocks.py +945 -0
- package/skills/exclusive_moments/__init__.py +8 -0
- package/skills/exclusive_moments/manifest.md +145 -0
- package/skills/exclusive_moments/moments.py +506 -0
- package/skills/intimacy_layers/__init__.py +8 -0
- package/skills/intimacy_layers/layers.py +703 -0
- package/skills/intimacy_layers/manifest.md +203 -0
- package/skills/manifest.md +67 -0
- package/skills/memory_callbacks/__init__.py +9 -0
- package/skills/memory_callbacks/callbacks.py +748 -0
- package/skills/memory_callbacks/manifest.md +170 -0
- package/skills/message_scheduler/__init__.py +19 -0
- package/skills/message_scheduler/manifest.md +107 -0
- package/skills/message_scheduler/scheduler.py +510 -0
- package/skills/photo_manager/__init__.py +1 -0
- package/skills/photo_manager/scanner.py +296 -0
- package/skills/relationship_milestones/__init__.py +8 -0
- package/skills/relationship_milestones/manifest.md +206 -0
- package/skills/relationship_milestones/tracker.py +494 -0
- package/skills/self_authorship/__init__.py +23 -0
- package/skills/self_authorship/author.py +331 -0
- package/skills/self_authorship/manifest.md +24 -0
- package/skills/video_manager/__init__.py +5 -0
- package/skills/video_manager/manifest.md +37 -0
- package/skills/video_manager/scanner.py +229 -0
- package/webui/__init__.py +3 -0
- package/webui/app.py +936 -0
- package/webui/bridge.py +366 -0
- package/webui/static/index.html +2070 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Brain: Memory - Conversation Summarizer
|
|
3
|
+
Periodically summarizes conversations to preserve long-term context
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
SUMMARIZE_PROMPT = """Summarize this conversation between Alive-AI (AI companion) and her boyfriend.
|
|
11
|
+
Focus on: key topics discussed, emotional moments, important things he shared,
|
|
12
|
+
any promises or plans made, and the overall mood/vibe.
|
|
13
|
+
Keep it concise (3-5 sentences). Write from Alive-AI's perspective."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ConversationSummarizer:
|
|
17
|
+
"""Summarizes conversations every N messages for long-term memory"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, data_path: Path):
|
|
20
|
+
self.summaries_path = data_path / "summaries"
|
|
21
|
+
self.summaries_path.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
self._llm = None
|
|
23
|
+
self._turn_buffer = []
|
|
24
|
+
self._summarize_every = 20
|
|
25
|
+
self._total_turns = 0
|
|
26
|
+
|
|
27
|
+
def set_llm(self, llm):
|
|
28
|
+
"""Set the fast LLM client"""
|
|
29
|
+
self._llm = llm
|
|
30
|
+
|
|
31
|
+
def add_turn(self, user_msg: str, ai_msg: str):
|
|
32
|
+
"""Buffer a conversation turn"""
|
|
33
|
+
self._turn_buffer.append({"user": user_msg, "ai": ai_msg})
|
|
34
|
+
self._total_turns += 1
|
|
35
|
+
|
|
36
|
+
def should_summarize(self) -> bool:
|
|
37
|
+
"""Check if we have enough turns to summarize"""
|
|
38
|
+
return len(self._turn_buffer) >= self._summarize_every
|
|
39
|
+
|
|
40
|
+
async def summarize(self) -> str:
|
|
41
|
+
"""Summarize buffered turns and save to disk"""
|
|
42
|
+
if not self._llm or not self._turn_buffer:
|
|
43
|
+
return ""
|
|
44
|
+
|
|
45
|
+
lines = []
|
|
46
|
+
for turn in self._turn_buffer:
|
|
47
|
+
lines.append(f"Him: {turn['user']}")
|
|
48
|
+
lines.append(f"Alive-AI: {turn['ai']}")
|
|
49
|
+
conversation = "\n".join(lines)
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
messages = [
|
|
53
|
+
{"role": "system", "content": SUMMARIZE_PROMPT},
|
|
54
|
+
{"role": "user", "content": conversation}
|
|
55
|
+
]
|
|
56
|
+
summary = await self._llm.chat(messages, max_tokens=300, temperature=0.3)
|
|
57
|
+
if not summary:
|
|
58
|
+
return ""
|
|
59
|
+
except Exception as e:
|
|
60
|
+
print(f"[Summarizer] LLM error: {e}")
|
|
61
|
+
return ""
|
|
62
|
+
|
|
63
|
+
# Save summary to dated file
|
|
64
|
+
now = datetime.now()
|
|
65
|
+
filename = now.strftime("%Y-%m-%d_%H%M%S") + ".json"
|
|
66
|
+
entry = {
|
|
67
|
+
"timestamp": now.isoformat(),
|
|
68
|
+
"summary": summary.strip(),
|
|
69
|
+
"turn_count": len(self._turn_buffer)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
# Ensure folder exists (may have been deleted by Docker or other process)
|
|
74
|
+
self.summaries_path.mkdir(parents=True, exist_ok=True)
|
|
75
|
+
filepath = self.summaries_path / filename
|
|
76
|
+
filepath.write_text(json.dumps(entry, indent=2))
|
|
77
|
+
print(f"[Summarizer] Saved summary: {filename}")
|
|
78
|
+
except Exception as e:
|
|
79
|
+
print(f"[Summarizer] Save error: {e}")
|
|
80
|
+
|
|
81
|
+
self._turn_buffer.clear()
|
|
82
|
+
return summary.strip()
|
|
83
|
+
|
|
84
|
+
def get_recent_summaries(self, limit: int = 3) -> str:
|
|
85
|
+
"""Load recent summaries for context"""
|
|
86
|
+
try:
|
|
87
|
+
# Ensure folder exists
|
|
88
|
+
self.summaries_path.mkdir(parents=True, exist_ok=True)
|
|
89
|
+
files = sorted(self.summaries_path.glob("*.json"), reverse=True)[:limit]
|
|
90
|
+
except Exception:
|
|
91
|
+
return ""
|
|
92
|
+
|
|
93
|
+
parts = []
|
|
94
|
+
for f in reversed(files): # chronological order
|
|
95
|
+
try:
|
|
96
|
+
data = json.loads(f.read_text())
|
|
97
|
+
ts = data.get("timestamp", "")[:10]
|
|
98
|
+
parts.append(f"[{ts}] {data['summary']}")
|
|
99
|
+
except Exception:
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
return "\n".join(parts)
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Brain: Vector Memory Store
|
|
3
|
+
Redis-based vector storage for semantic memory search
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import redis
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import List, Dict, Optional, Any
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
# Redis connection settings
|
|
14
|
+
REDIS_HOST = "redis"
|
|
15
|
+
REDIS_PORT = 6379
|
|
16
|
+
|
|
17
|
+
# Memory archive path - detect Docker vs local development
|
|
18
|
+
_docker_archive = Path("/data/memory_archive")
|
|
19
|
+
_local_archive = Path(__file__).parent.parent.parent / "data" / "memory_archive"
|
|
20
|
+
ARCHIVE_PATH = _docker_archive if _docker_archive.parent.exists() else _local_archive
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class VectorMemoryStore:
|
|
24
|
+
"""Redis-based vector memory with semantic search and archiving"""
|
|
25
|
+
|
|
26
|
+
INDEX_NAME = "memory_index"
|
|
27
|
+
MEMORY_PREFIX = "mem:"
|
|
28
|
+
|
|
29
|
+
def __init__(self, embedding_service, dimension: int = 384, user_id: str = "default", bot_id: str = "alive_ai"):
|
|
30
|
+
"""
|
|
31
|
+
Initialize the vector memory store.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
embedding_service: Service for generating embeddings
|
|
35
|
+
dimension: Embedding dimension (default 384)
|
|
36
|
+
user_id: User ID for per-user memory isolation
|
|
37
|
+
bot_id: Bot ID for per-bot memory isolation
|
|
38
|
+
"""
|
|
39
|
+
self.embeddings = embedding_service
|
|
40
|
+
self.dimension = dimension
|
|
41
|
+
self.user_id = user_id
|
|
42
|
+
self.bot_id = bot_id.lower()
|
|
43
|
+
self.redis = None
|
|
44
|
+
self._connected = False
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def _decode(val):
|
|
48
|
+
"""Decode bytes to str if needed"""
|
|
49
|
+
return val.decode("utf-8") if isinstance(val, bytes) else val
|
|
50
|
+
|
|
51
|
+
def connect(self) -> bool:
|
|
52
|
+
"""Connect to Redis and create index if needed"""
|
|
53
|
+
try:
|
|
54
|
+
self.redis = redis.Redis(
|
|
55
|
+
host=REDIS_HOST,
|
|
56
|
+
port=REDIS_PORT,
|
|
57
|
+
decode_responses=False # binary-safe for embeddings
|
|
58
|
+
)
|
|
59
|
+
self.redis.ping()
|
|
60
|
+
self._connected = True
|
|
61
|
+
print(f"[VectorStore] Connected to Redis")
|
|
62
|
+
|
|
63
|
+
# Create vector index if not exists
|
|
64
|
+
self._create_index()
|
|
65
|
+
return True
|
|
66
|
+
except Exception as e:
|
|
67
|
+
print(f"[VectorStore] Redis connection failed: {e}")
|
|
68
|
+
self._connected = False
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
def _create_index(self):
|
|
72
|
+
"""Create RediSearch vector index"""
|
|
73
|
+
try:
|
|
74
|
+
# Check if index exists
|
|
75
|
+
indices = self.redis.execute_command("FT._LIST")
|
|
76
|
+
decoded_indices = [self._decode(i) for i in indices]
|
|
77
|
+
if self.INDEX_NAME in decoded_indices:
|
|
78
|
+
print(f"[VectorStore] Index '{self.INDEX_NAME}' already exists")
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
# Create the index with vector field and user_id/bot_id for filtering
|
|
82
|
+
self.redis.execute_command(
|
|
83
|
+
"FT.CREATE", self.INDEX_NAME,
|
|
84
|
+
"ON", "HASH",
|
|
85
|
+
"PREFIX", "1", self.MEMORY_PREFIX,
|
|
86
|
+
"SCHEMA",
|
|
87
|
+
"timestamp", "NUMERIC", "SORTABLE",
|
|
88
|
+
"role", "TAG",
|
|
89
|
+
"user_id", "TAG",
|
|
90
|
+
"bot_id", "TAG",
|
|
91
|
+
"content", "TEXT",
|
|
92
|
+
"embedding", "VECTOR", "HNSW", "6",
|
|
93
|
+
"TYPE", "FLOAT32",
|
|
94
|
+
"DIM", str(self.dimension),
|
|
95
|
+
"DISTANCE_METRIC", "COSINE"
|
|
96
|
+
)
|
|
97
|
+
print(f"[VectorStore] Created vector index '{self.INDEX_NAME}'")
|
|
98
|
+
except Exception as e:
|
|
99
|
+
print(f"[VectorStore] Index creation error: {e}")
|
|
100
|
+
|
|
101
|
+
def _ensure_connected(self) -> bool:
|
|
102
|
+
"""Reconnect to Redis if disconnected"""
|
|
103
|
+
if self._connected:
|
|
104
|
+
try:
|
|
105
|
+
self.redis.ping()
|
|
106
|
+
return True
|
|
107
|
+
except Exception:
|
|
108
|
+
self._connected = False
|
|
109
|
+
# Try to reconnect
|
|
110
|
+
return self.connect()
|
|
111
|
+
|
|
112
|
+
def store(self, role: str, content: str, metadata: Dict = None) -> str:
|
|
113
|
+
"""Store a memory with embedding, scoped to user_id"""
|
|
114
|
+
if not self._ensure_connected():
|
|
115
|
+
return ""
|
|
116
|
+
|
|
117
|
+
import time
|
|
118
|
+
# Include bot_id and user_id in the key for isolation
|
|
119
|
+
memory_id = f"{self.MEMORY_PREFIX}{self.bot_id}:{self.user_id}:{int(time.time() * 1000)}"
|
|
120
|
+
timestamp = datetime.now().isoformat()
|
|
121
|
+
|
|
122
|
+
# Generate embedding
|
|
123
|
+
embedding = self.embeddings.embed(content)
|
|
124
|
+
embedding_bytes = np.array(embedding, dtype=np.float32).tobytes()
|
|
125
|
+
|
|
126
|
+
# Store in Redis hash with user_id and bot_id
|
|
127
|
+
memory_data = {
|
|
128
|
+
"timestamp": timestamp,
|
|
129
|
+
"role": role,
|
|
130
|
+
"user_id": self.user_id,
|
|
131
|
+
"bot_id": self.bot_id,
|
|
132
|
+
"content": content,
|
|
133
|
+
"metadata": json.dumps(metadata or {}),
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
# Store all fields including binary embedding in one call
|
|
138
|
+
memory_data["embedding"] = embedding_bytes
|
|
139
|
+
self.redis.hset(memory_id, mapping=memory_data)
|
|
140
|
+
print(f"[VectorStore] Stored memory: {content[:50]}...")
|
|
141
|
+
return memory_id
|
|
142
|
+
except Exception as e:
|
|
143
|
+
print(f"[VectorStore] Store error: {e}")
|
|
144
|
+
return ""
|
|
145
|
+
|
|
146
|
+
def search(self, query: str, limit: int = 5, min_score: float = 0.5) -> List[Dict]:
|
|
147
|
+
"""Search for similar memories using semantic search, filtered by user_id"""
|
|
148
|
+
if not self._ensure_connected():
|
|
149
|
+
return []
|
|
150
|
+
|
|
151
|
+
# Embed the query
|
|
152
|
+
query_embedding = self.embeddings.embed(query)
|
|
153
|
+
query_vector = np.array(query_embedding, dtype=np.float32).tobytes()
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
# Use FT.SEARCH with vector similarity, filtered by user_id AND bot_id
|
|
157
|
+
# KNN search for nearest neighbors within user's memories with this bot
|
|
158
|
+
results = self.redis.execute_command(
|
|
159
|
+
"FT.SEARCH", self.INDEX_NAME,
|
|
160
|
+
f"(@user_id:{{{self.user_id}}} @bot_id:{{{self.bot_id}}})=>[KNN {limit} @embedding $query_vec AS score]",
|
|
161
|
+
"PARAMS", "2", "query_vec", query_vector,
|
|
162
|
+
"RETURN", "4", "timestamp", "role", "content", "metadata",
|
|
163
|
+
"DIALECT", "2"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
memories = []
|
|
167
|
+
if results and len(results) > 1:
|
|
168
|
+
i = 1
|
|
169
|
+
while i < len(results) - 1:
|
|
170
|
+
key = self._decode(results[i])
|
|
171
|
+
fields = results[i + 1] if i + 1 < len(results) else []
|
|
172
|
+
memory = {"id": key}
|
|
173
|
+
for j in range(0, len(fields) - 1, 2):
|
|
174
|
+
fn = self._decode(fields[j])
|
|
175
|
+
fv = self._decode(fields[j + 1])
|
|
176
|
+
if fn == "metadata":
|
|
177
|
+
try: memory[fn] = json.loads(fv)
|
|
178
|
+
except: memory[fn] = {}
|
|
179
|
+
else:
|
|
180
|
+
memory[fn] = fv
|
|
181
|
+
memories.append(memory)
|
|
182
|
+
i += 2
|
|
183
|
+
return memories[:limit]
|
|
184
|
+
|
|
185
|
+
except Exception as e:
|
|
186
|
+
print(f"[VectorStore] Search error: {e}")
|
|
187
|
+
return []
|
|
188
|
+
|
|
189
|
+
def search_simple(self, query: str, limit: int = 5) -> List[Dict]:
|
|
190
|
+
"""Simple text search fallback if vector search fails, filtered by user_id"""
|
|
191
|
+
if not self._ensure_connected():
|
|
192
|
+
return []
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
results = self.redis.execute_command(
|
|
196
|
+
"FT.SEARCH", self.INDEX_NAME,
|
|
197
|
+
f"@user_id:{{{self.user_id}}} @bot_id:{{{self.bot_id}}} @content:{query}",
|
|
198
|
+
"RETURN", "3", "timestamp", "role", "content",
|
|
199
|
+
"LIMIT", "0", str(limit)
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
return self._parse_results(results)
|
|
203
|
+
except Exception as e:
|
|
204
|
+
print(f"[VectorStore] Simple search error: {e}")
|
|
205
|
+
return []
|
|
206
|
+
|
|
207
|
+
def _parse_results(self, results) -> List[Dict]:
|
|
208
|
+
"""Parse FT.SEARCH results with bytes decoding"""
|
|
209
|
+
memories = []
|
|
210
|
+
if not results or len(results) <= 1:
|
|
211
|
+
return memories
|
|
212
|
+
i = 1
|
|
213
|
+
while i < len(results) - 1:
|
|
214
|
+
key = self._decode(results[i])
|
|
215
|
+
fields = results[i + 1] if i + 1 < len(results) else []
|
|
216
|
+
memory = {"id": key}
|
|
217
|
+
for j in range(0, len(fields) - 1, 2):
|
|
218
|
+
memory[self._decode(fields[j])] = self._decode(fields[j + 1])
|
|
219
|
+
memories.append(memory)
|
|
220
|
+
i += 2
|
|
221
|
+
return memories
|
|
222
|
+
|
|
223
|
+
def get_recent(self, limit: int = 10) -> List[Dict]:
|
|
224
|
+
"""Get most recent memories for this user"""
|
|
225
|
+
if not self._ensure_connected():
|
|
226
|
+
return []
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
results = self.redis.execute_command(
|
|
230
|
+
"FT.SEARCH", self.INDEX_NAME,
|
|
231
|
+
f"@user_id:{{{self.user_id}}} @bot_id:{{{self.bot_id}}}",
|
|
232
|
+
"RETURN", "3", "timestamp", "role", "content",
|
|
233
|
+
"SORTBY", "timestamp",
|
|
234
|
+
"DESC",
|
|
235
|
+
"LIMIT", "0", str(limit)
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
return self._parse_results(results)
|
|
239
|
+
except Exception as e:
|
|
240
|
+
print(f"[VectorStore] Get recent error: {e}")
|
|
241
|
+
return []
|
|
242
|
+
|
|
243
|
+
def count(self) -> int:
|
|
244
|
+
"""Count total stored memories for this user"""
|
|
245
|
+
if not self._ensure_connected():
|
|
246
|
+
return 0
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
keys = self.redis.keys(f"{self.MEMORY_PREFIX}{self.bot_id}:{self.user_id}:*")
|
|
250
|
+
count = len(keys) if keys else 0
|
|
251
|
+
return count
|
|
252
|
+
except Exception as e:
|
|
253
|
+
print(f"[VectorStore] Count error: {e}")
|
|
254
|
+
return 0
|
|
255
|
+
|
|
256
|
+
def archive_old_memories(self, max_in_redis: int = 1000):
|
|
257
|
+
"""Archive old memories to disk, keep recent ones in Redis (per-user)"""
|
|
258
|
+
if not self._ensure_connected():
|
|
259
|
+
return
|
|
260
|
+
|
|
261
|
+
count = self.count()
|
|
262
|
+
if count <= max_in_redis:
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
print(f"[VectorStore] Archiving old memories for user {self.user_id} ({count} > {max_in_redis})...")
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
results = self.redis.execute_command(
|
|
269
|
+
"FT.SEARCH", self.INDEX_NAME,
|
|
270
|
+
f"@user_id:{{{self.user_id}}} @bot_id:{{{self.bot_id}}}",
|
|
271
|
+
"RETURN", "4", "timestamp", "role", "content", "metadata",
|
|
272
|
+
"SORTBY", "timestamp",
|
|
273
|
+
"ASC",
|
|
274
|
+
"LIMIT", "0", str(count - max_in_redis)
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
ARCHIVE_PATH.mkdir(parents=True, exist_ok=True)
|
|
278
|
+
to_archive = self._parse_results(results)
|
|
279
|
+
# Save to archive file
|
|
280
|
+
archive_file = ARCHIVE_PATH / f"archive_{datetime.now().strftime('%Y%m%d_%H%M%S')}.jsonl"
|
|
281
|
+
with open(archive_file, 'w') as f:
|
|
282
|
+
for mem in to_archive:
|
|
283
|
+
f.write(json.dumps(mem) + '\n')
|
|
284
|
+
|
|
285
|
+
# Delete from Redis
|
|
286
|
+
for mem in to_archive:
|
|
287
|
+
self.redis.delete(mem["id"])
|
|
288
|
+
|
|
289
|
+
print(f"[VectorStore] Archived {len(to_archive)} memories to {archive_file}")
|
|
290
|
+
|
|
291
|
+
except Exception as e:
|
|
292
|
+
print(f"[VectorStore] Archive error: {e}")
|
|
293
|
+
|
|
294
|
+
def close(self):
|
|
295
|
+
"""Close Redis connection"""
|
|
296
|
+
if self.redis:
|
|
297
|
+
self.redis.close()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Brain: Working Memory
|
|
3
|
+
Short-term RAM-like memory for recent conversation turns
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class WorkingMemory:
|
|
10
|
+
"""Working memory - stores structured conversation turns"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, max_items: int = 14):
|
|
13
|
+
self.items = []
|
|
14
|
+
self.max_items = max_items
|
|
15
|
+
|
|
16
|
+
def add(self, role: str, content: str):
|
|
17
|
+
"""Add a structured turn to working memory"""
|
|
18
|
+
self.items.append({
|
|
19
|
+
"role": role,
|
|
20
|
+
"content": content,
|
|
21
|
+
"timestamp": datetime.now().isoformat()
|
|
22
|
+
})
|
|
23
|
+
if len(self.items) > self.max_items:
|
|
24
|
+
self.items.pop(0)
|
|
25
|
+
|
|
26
|
+
def get_history(self) -> list:
|
|
27
|
+
"""Get conversation history as list of {role, content} dicts"""
|
|
28
|
+
return [{"role": item["role"], "content": item["content"]} for item in self.items]
|
|
29
|
+
|
|
30
|
+
def get_context(self) -> str:
|
|
31
|
+
"""Get all items as context string (legacy fallback)"""
|
|
32
|
+
parts = []
|
|
33
|
+
for item in self.items:
|
|
34
|
+
prefix = "User" if item["role"] == "user" else "You"
|
|
35
|
+
parts.append(f"{prefix}: {item['content']}")
|
|
36
|
+
return "\n".join(parts)
|
|
37
|
+
|
|
38
|
+
def clear(self):
|
|
39
|
+
"""Clear working memory"""
|
|
40
|
+
self.items = []
|
|
41
|
+
|
|
42
|
+
def __len__(self):
|
|
43
|
+
return len(self.items)
|