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.
Files changed (168) hide show
  1. package/Dockerfile +24 -0
  2. package/LICENSE +21 -0
  3. package/README.md +143 -0
  4. package/alive_ai/__init__.py +3 -0
  5. package/brain/__init__.py +59 -0
  6. package/brain/almost_said.py +154 -0
  7. package/brain/bid_detector.py +636 -0
  8. package/brain/conversation_flow.py +135 -0
  9. package/brain/curiosity.py +328 -0
  10. package/brain/default_mode.py +1438 -0
  11. package/brain/dreams.py +220 -0
  12. package/brain/embeddings/__init__.py +82 -0
  13. package/brain/emotional_memory.py +949 -0
  14. package/brain/global_activity.py +173 -0
  15. package/brain/group_dynamics.py +63 -0
  16. package/brain/linguistic.py +235 -0
  17. package/brain/llm/__init__.py +63 -0
  18. package/brain/llm/base.py +33 -0
  19. package/brain/llm/fallback_router.py +309 -0
  20. package/brain/llm/manifest.md +30 -0
  21. package/brain/llm/ollama.py +218 -0
  22. package/brain/llm/openrouter.py +151 -0
  23. package/brain/llm/provider.py +205 -0
  24. package/brain/llm/unified.py +423 -0
  25. package/brain/llm/zai.py +169 -0
  26. package/brain/manifest.md +23 -0
  27. package/brain/memory/__init__.py +123 -0
  28. package/brain/memory/episodic.py +92 -0
  29. package/brain/memory/fact_extractor.py +209 -0
  30. package/brain/memory/index.py +54 -0
  31. package/brain/memory/manager.py +151 -0
  32. package/brain/memory/summarizer.py +102 -0
  33. package/brain/memory/vector_store.py +297 -0
  34. package/brain/memory/working.py +43 -0
  35. package/brain/narrative.py +343 -0
  36. package/brain/stt/__init__.py +4 -0
  37. package/brain/stt/google_stt.py +83 -0
  38. package/brain/stt/whisper_stt.py +82 -0
  39. package/brain/subconscious/__init__.py +33 -0
  40. package/brain/subconscious/actions.py +136 -0
  41. package/brain/subconscious/evaluation.py +166 -0
  42. package/brain/subconscious/goal_system.py +90 -0
  43. package/brain/subconscious/goals.py +41 -0
  44. package/brain/subconscious/impulse_generator.py +200 -0
  45. package/brain/subconscious/impulses.py +48 -0
  46. package/brain/subconscious/learning.py +24 -0
  47. package/brain/subconscious/learning_system.py +79 -0
  48. package/brain/subconscious/loop.py +398 -0
  49. package/brain/subconscious/manifest.md +32 -0
  50. package/brain/subconscious/relationship.py +47 -0
  51. package/brain/subconscious/relationship_memory.py +83 -0
  52. package/brain/subconscious/response_analyzer.py +74 -0
  53. package/brain/subconscious/templates.py +70 -0
  54. package/brain/subconscious/thought.py +37 -0
  55. package/brain/subconscious/working_memory.py +97 -0
  56. package/cli/index.js +371 -0
  57. package/config/directives.example.json +28 -0
  58. package/config/instructions.example.md +16 -0
  59. package/config/self.example.json +74 -0
  60. package/config/settings.example.json +95 -0
  61. package/core/__init__.py +1 -0
  62. package/core/config.py +54 -0
  63. package/core/directives.py +198 -0
  64. package/core/events.py +50 -0
  65. package/core/follow_up.py +267 -0
  66. package/core/hot_reload.py +174 -0
  67. package/core/initialization.py +253 -0
  68. package/core/manifest.md +28 -0
  69. package/core/media_handler.py +241 -0
  70. package/core/memory_monitor.py +200 -0
  71. package/core/message_handler.py +1440 -0
  72. package/core/proactive_generator.py +277 -0
  73. package/core/self.py +188 -0
  74. package/core/settings.py +169 -0
  75. package/core/skills_registry.py +357 -0
  76. package/core/state.py +27 -0
  77. package/core/subconscious_bridge.py +93 -0
  78. package/core/thinking.py +175 -0
  79. package/core/user_manager.py +306 -0
  80. package/core/user_tracker.py +144 -0
  81. package/demo/index.html +144 -0
  82. package/docker-compose.yml +28 -0
  83. package/docs/assets/logo.svg +15 -0
  84. package/docs/index.html +355 -0
  85. package/heart/__init__.py +93 -0
  86. package/heart/afterglow.py +215 -0
  87. package/heart/attachment.py +186 -0
  88. package/heart/circadian.py +251 -0
  89. package/heart/complex_emotions.py +114 -0
  90. package/heart/conflicts.py +589 -0
  91. package/heart/core.py +387 -0
  92. package/heart/emotional_decay.py +59 -0
  93. package/heart/emotional_memory.py +261 -0
  94. package/heart/emotional_state.py +146 -0
  95. package/heart/emotional_variability.py +156 -0
  96. package/heart/hormonal.py +424 -0
  97. package/heart/inconsistency.py +1222 -0
  98. package/heart/integrity.py +469 -0
  99. package/heart/interoception.py +997 -0
  100. package/heart/love.py +120 -0
  101. package/heart/manifest.md +25 -0
  102. package/heart/mood_shifts.py +169 -0
  103. package/heart/phantom_somatic.py +259 -0
  104. package/heart/predictive.py +374 -0
  105. package/heart/scars.py +474 -0
  106. package/heart/somatic.py +482 -0
  107. package/heart/soul.py +633 -0
  108. package/heart/telemetry.py +942 -0
  109. package/heart/triggers.py +119 -0
  110. package/heart/unconscious.py +443 -0
  111. package/input/__init__.py +1 -0
  112. package/input/manifest.md +24 -0
  113. package/input/telegram/__init__.py +1 -0
  114. package/input/telegram/commands.py +762 -0
  115. package/input/telegram/listener.py +532 -0
  116. package/main.py +90 -0
  117. package/manifest.md +28 -0
  118. package/mypics/.gitkeep +1 -0
  119. package/myvids/.gitkeep +1 -0
  120. package/output/__init__.py +1 -0
  121. package/output/images/__init__.py +1 -0
  122. package/output/images/fal_gen.py +43 -0
  123. package/output/manifest.md +26 -0
  124. package/output/text/__init__.py +1 -0
  125. package/output/text/sender.py +22 -0
  126. package/output/voice/__init__.py +64 -0
  127. package/output/voice/google_tts.py +252 -0
  128. package/output/voice/gtts_tts.py +214 -0
  129. package/output/voice/vibe_tts.py +190 -0
  130. package/package.json +58 -0
  131. package/pyproject.toml +23 -0
  132. package/requirements.txt +21 -0
  133. package/skills/__init__.py +1 -0
  134. package/skills/anticipation_engine/__init__.py +8 -0
  135. package/skills/anticipation_engine/engine.py +618 -0
  136. package/skills/anticipation_engine/manifest.md +192 -0
  137. package/skills/calendar/__init__.py +1 -0
  138. package/skills/content_unlocks/__init__.py +8 -0
  139. package/skills/content_unlocks/manifest.md +231 -0
  140. package/skills/content_unlocks/unlocks.py +945 -0
  141. package/skills/exclusive_moments/__init__.py +8 -0
  142. package/skills/exclusive_moments/manifest.md +145 -0
  143. package/skills/exclusive_moments/moments.py +506 -0
  144. package/skills/intimacy_layers/__init__.py +8 -0
  145. package/skills/intimacy_layers/layers.py +703 -0
  146. package/skills/intimacy_layers/manifest.md +203 -0
  147. package/skills/manifest.md +67 -0
  148. package/skills/memory_callbacks/__init__.py +9 -0
  149. package/skills/memory_callbacks/callbacks.py +748 -0
  150. package/skills/memory_callbacks/manifest.md +170 -0
  151. package/skills/message_scheduler/__init__.py +19 -0
  152. package/skills/message_scheduler/manifest.md +107 -0
  153. package/skills/message_scheduler/scheduler.py +510 -0
  154. package/skills/photo_manager/__init__.py +1 -0
  155. package/skills/photo_manager/scanner.py +296 -0
  156. package/skills/relationship_milestones/__init__.py +8 -0
  157. package/skills/relationship_milestones/manifest.md +206 -0
  158. package/skills/relationship_milestones/tracker.py +494 -0
  159. package/skills/self_authorship/__init__.py +23 -0
  160. package/skills/self_authorship/author.py +331 -0
  161. package/skills/self_authorship/manifest.md +24 -0
  162. package/skills/video_manager/__init__.py +5 -0
  163. package/skills/video_manager/manifest.md +37 -0
  164. package/skills/video_manager/scanner.py +229 -0
  165. package/webui/__init__.py +3 -0
  166. package/webui/app.py +936 -0
  167. package/webui/bridge.py +366 -0
  168. 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)