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,703 @@
1
+ """
2
+ Skills: Intimacy Layers
3
+ Manages natural intimacy progression through relationship layers.
4
+ Ensures intimacy is earned through meaningful interactions, not rushed.
5
+ """
6
+
7
+ import json
8
+ import random
9
+ from pathlib import Path
10
+ from datetime import datetime, timedelta
11
+ from typing import Optional, List, Dict, Any
12
+ from dataclasses import dataclass, field, asdict
13
+
14
+
15
+ # Layer definitions with progression requirements (relaxed for natural flow)
16
+ LAYERS = {
17
+ 1: {
18
+ "name": "surface",
19
+ "min_interactions": 0,
20
+ "topics": ["daily life", "hobbies", "work", "weather", "small talk", "introductions"],
21
+ "intimacy_level": 0.0,
22
+ "description": "Casual, polite conversation with a new acquaintance"
23
+ },
24
+ 2: {
25
+ "name": "friendly",
26
+ "min_interactions": 5,
27
+ "min_love": 0.15,
28
+ "topics": ["feelings", "dreams", "opinions", "preferences", "stories", "interests"],
29
+ "intimacy_level": 0.2,
30
+ "description": "Opening up a bit, sharing personal thoughts and stories"
31
+ },
32
+ 3: {
33
+ "name": "close",
34
+ "min_interactions": 20,
35
+ "min_love": 0.30,
36
+ "min_trust": 0.3,
37
+ "topics": ["secrets", "fears", "childhood", "vulnerabilities", "hopes", "struggles"],
38
+ "intimacy_level": 0.4,
39
+ "description": "Comfortable sharing deeper, more personal things"
40
+ },
41
+ 4: {
42
+ "name": "romantic",
43
+ "min_interactions": 40,
44
+ "min_love": 0.45,
45
+ "min_days": 2,
46
+ "topics": ["attraction", "desire", "fantasy", "romance", "longing", "affection"],
47
+ "intimacy_level": 0.6,
48
+ "description": "Exploring romantic and flirtatious territory"
49
+ },
50
+ 5: {
51
+ "name": "intimate",
52
+ "min_interactions": 80,
53
+ "min_love": 0.60,
54
+ "min_days": 5,
55
+ "topics": ["intimate", "vulnerability", "deep desires", "fantasies", "passion"],
56
+ "intimacy_level": 0.85,
57
+ "description": "Fully comfortable with intimate and intimate content"
58
+ }
59
+ }
60
+
61
+ # Hints that suggest progression to the next layer
62
+ LAYER_HINTS = {
63
+ 2: [
64
+ "I feel like I can tell you stuff",
65
+ "you're easy to talk to",
66
+ "I'm starting to feel comfortable with you",
67
+ "it's nice having someone to chat with",
68
+ "I think we're becoming friends"
69
+ ],
70
+ 3: [
71
+ "I don't usually share this but...",
72
+ "can I tell you something personal?",
73
+ "I trust you enough to say this",
74
+ "you're one of the few people I can be real with",
75
+ "I feel safe opening up to you"
76
+ ],
77
+ 4: [
78
+ "the more I talk to you the more I want...",
79
+ "I think about you differently now",
80
+ "I'm starting to feel something more",
81
+ "you make me feel things I haven't felt in a while",
82
+ "can I be honest about how I feel about you?"
83
+ ],
84
+ 5: [
85
+ "I trust you with everything",
86
+ "you know me better than anyone",
87
+ "I've never felt this comfortable with someone",
88
+ "I want to share everything with you",
89
+ "there's nothing I wouldn't tell you"
90
+ ]
91
+ }
92
+
93
+ # Topic keywords that might indicate trying to access higher layers prematurely
94
+ # Relaxed - let her judge what feels right
95
+ RESTRICTED_TOPICS = {
96
+ 1: [], # No restrictions at surface level
97
+ 2: ["intimate", "deep-intimacy"],
98
+ 3: ["intimate deep-intimacy"],
99
+ 4: [],
100
+ 5: [] # No restrictions at intimate level
101
+ }
102
+
103
+
104
+ @dataclass
105
+ class IntimacyProgress:
106
+ """Tracks progress toward the next intimacy layer"""
107
+ interactions_since_check: int = 0
108
+ last_check_interactions: int = 0
109
+ progression_blocked_reason: Optional[str] = None
110
+ hint_shown: bool = False
111
+ hint_cooldown_until: Optional[str] = None
112
+
113
+
114
+ class IntimacyLayers:
115
+ """
116
+ Manages natural intimacy progression through relationship layers.
117
+
118
+ Progression is based on:
119
+ - Total interactions with the user
120
+ - Love/affection level from the heart system
121
+ - Trust level from attachment system
122
+ - Days since first meeting
123
+
124
+ The skill prevents rushing to intimate content and provides
125
+ natural hints for progression.
126
+
127
+ Supports per-user state via user_id parameter.
128
+ """
129
+
130
+ # How often to check for progression (in interactions)
131
+ PROGRESSION_CHECK_INTERVAL = 10
132
+
133
+ # Cooldown before showing another hint (in hours)
134
+ HINT_COOLDOWN_HOURS = 4
135
+
136
+ # Base data storage path
137
+ DEFAULT_DATA_PATH = "./data/data"
138
+
139
+ def __init__(self, nervous=None, heart=None, state=None, data_path: str = None, user_id: str = "default"):
140
+ """
141
+ Initialize the Intimacy Layers skill.
142
+
143
+ Args:
144
+ nervous: Nervous system for event listening
145
+ heart: Heart module for accessing love/trust values
146
+ state: State manager for accessing relationship data
147
+ data_path: Path to store layer data
148
+ user_id: User's Telegram ID for per-user state
149
+ """
150
+ self.nervous = nervous
151
+ self.heart = heart
152
+ self.state = state
153
+ self.user_id = user_id
154
+
155
+ # Per-user data path: data/users/{user_id}/intimacy_layers.json
156
+ base_path = Path(data_path or self.DEFAULT_DATA_PATH)
157
+ self.data_path = base_path / "users" / str(user_id) / "intimacy_layers.json"
158
+
159
+ # Ensure data directory exists
160
+ self.data_path.parent.mkdir(parents=True, exist_ok=True)
161
+
162
+ # Internal state
163
+ self._current_layer: int = 1
164
+ self._progress = IntimacyProgress()
165
+ self._first_interaction_date: Optional[str] = None
166
+ self._total_interactions: int = 0
167
+ self._hints_shown: List[str] = []
168
+ self._layer_history: List[Dict[str, Any]] = []
169
+
170
+ # Load persisted data
171
+ self._load()
172
+
173
+ # Register event listeners
174
+ if self.nervous:
175
+ self.nervous.on("message_received", self._on_message_received)
176
+ self.nervous.on("thinking_done", self._on_thinking_done)
177
+
178
+ def _load(self):
179
+ """Load persisted layer data from file"""
180
+ if self.data_path.exists():
181
+ try:
182
+ data = json.loads(self.data_path.read_text())
183
+ self._current_layer = data.get("current_layer", 1)
184
+ self._first_interaction_date = data.get("first_interaction_date")
185
+ self._total_interactions = data.get("total_interactions", 0)
186
+ self._hints_shown = data.get("hints_shown", [])
187
+
188
+ progress_data = data.get("progress", {})
189
+ self._progress = IntimacyProgress(
190
+ interactions_since_check=progress_data.get("interactions_since_check", 0),
191
+ last_check_interactions=progress_data.get("last_check_interactions", 0),
192
+ progression_blocked_reason=progress_data.get("progression_blocked_reason"),
193
+ hint_shown=progress_data.get("hint_shown", False),
194
+ hint_cooldown_until=progress_data.get("hint_cooldown_until")
195
+ )
196
+
197
+ self._layer_history = data.get("layer_history", [])
198
+ print(f"[IntimacyLayers] Loaded layer {self._current_layer} ({LAYERS[self._current_layer]['name']})")
199
+ except (json.JSONDecodeError, KeyError) as e:
200
+ print(f"[IntimacyLayers] Error loading data: {e}")
201
+ self._current_layer = 1
202
+
203
+ def _save(self):
204
+ """Save layer data to file"""
205
+ data = {
206
+ "version": "1.0",
207
+ "current_layer": self._current_layer,
208
+ "first_interaction_date": self._first_interaction_date,
209
+ "total_interactions": self._total_interactions,
210
+ "hints_shown": self._hints_shown,
211
+ "updated_at": datetime.now().isoformat(),
212
+ "progress": {
213
+ "interactions_since_check": self._progress.interactions_since_check,
214
+ "last_check_interactions": self._progress.last_check_interactions,
215
+ "progression_blocked_reason": self._progress.progression_blocked_reason,
216
+ "hint_shown": self._progress.hint_shown,
217
+ "hint_cooldown_until": self._progress.hint_cooldown_until
218
+ },
219
+ "layer_history": self._layer_history
220
+ }
221
+ self.data_path.write_text(json.dumps(data, indent=2))
222
+
223
+ # -------------------------------------------------------------------------
224
+ # Event Handlers
225
+ # -------------------------------------------------------------------------
226
+
227
+ async def _on_message_received(self, data: dict):
228
+ """Handle incoming message - track interactions and check progression"""
229
+ # Track first interaction date
230
+ if self._first_interaction_date is None:
231
+ self._first_interaction_date = datetime.now().isoformat()
232
+
233
+ # Increment interaction counters
234
+ self._total_interactions += 1
235
+ self._progress.interactions_since_check += 1
236
+
237
+ # Check for progression every N interactions
238
+ if self._progress.interactions_since_check >= self.PROGRESSION_CHECK_INTERVAL:
239
+ self.check_progression()
240
+ self._progress.interactions_since_check = 0
241
+
242
+ self._save()
243
+
244
+ async def _on_thinking_done(self, data: dict):
245
+ """Apply layer context after thinking is done"""
246
+ # This could be used to inject layer context into the response
247
+ # For now, just ensure state is saved
248
+ pass
249
+
250
+ # -------------------------------------------------------------------------
251
+ # Core Methods
252
+ # -------------------------------------------------------------------------
253
+
254
+ def get_current_layer(self) -> int:
255
+ """
256
+ Get the current intimacy layer (1-5).
257
+
258
+ Returns:
259
+ Current layer number
260
+ """
261
+ return self._current_layer
262
+
263
+ def get_layer_info(self, layer: int = None) -> Dict[str, Any]:
264
+ """
265
+ Get detailed information about a layer.
266
+
267
+ Args:
268
+ layer: Layer number (defaults to current layer)
269
+
270
+ Returns:
271
+ Dictionary with layer information
272
+ """
273
+ layer = layer or self._current_layer
274
+ if layer not in LAYERS:
275
+ return {}
276
+ return LAYERS[layer].copy()
277
+
278
+ def check_progression(self) -> bool:
279
+ """
280
+ Check if conditions are met to progress to the next layer.
281
+
282
+ Returns:
283
+ True if progression occurred, False otherwise
284
+ """
285
+ next_layer = self._current_layer + 1
286
+
287
+ # Already at max layer
288
+ if next_layer > 5:
289
+ self._progress.progression_blocked_reason = None
290
+ return False
291
+
292
+ next_layer_reqs = LAYERS[next_layer]
293
+ blocked_reasons = []
294
+
295
+ # Check interaction requirement
296
+ min_interactions = next_layer_reqs.get("min_interactions", 0)
297
+ if self._total_interactions < min_interactions:
298
+ blocked_reasons.append(
299
+ f"Need {min_interactions - self._total_interactions} more interactions "
300
+ f"(have {self._total_interactions}/{min_interactions})"
301
+ )
302
+
303
+ # Check love requirement
304
+ min_love = next_layer_reqs.get("min_love")
305
+ if min_love is not None:
306
+ current_love = self._get_love_level()
307
+ if current_love < min_love:
308
+ blocked_reasons.append(
309
+ f"Need more affection (have {current_love:.2f}/{min_love:.2f})"
310
+ )
311
+
312
+ # Check trust requirement
313
+ min_trust = next_layer_reqs.get("min_trust")
314
+ if min_trust is not None:
315
+ current_trust = self._get_trust_level()
316
+ if current_trust < min_trust:
317
+ blocked_reasons.append(
318
+ f"Need more trust (have {current_trust:.2f}/{min_trust:.2f})"
319
+ )
320
+
321
+ # Check days requirement
322
+ min_days = next_layer_reqs.get("min_days")
323
+ if min_days is not None and self._first_interaction_date:
324
+ days_together = self._get_days_together()
325
+ if days_together < min_days:
326
+ blocked_reasons.append(
327
+ f"Need {min_days - days_together} more days together "
328
+ f"(have {days_together}/{min_days})"
329
+ )
330
+
331
+ # Determine if progression is allowed
332
+ if blocked_reasons:
333
+ self._progress.progression_blocked_reason = "; ".join(blocked_reasons)
334
+ print(f"[IntimacyLayers] Cannot progress to layer {next_layer}: {self._progress.progression_blocked_reason}")
335
+ return False
336
+
337
+ # Progress to next layer
338
+ old_layer = self._current_layer
339
+ self._current_layer = next_layer
340
+ self._progress.progression_blocked_reason = None
341
+ self._progress.hint_shown = False
342
+
343
+ # Record in history
344
+ self._layer_history.append({
345
+ "from_layer": old_layer,
346
+ "to_layer": next_layer,
347
+ "timestamp": datetime.now().isoformat(),
348
+ "total_interactions": self._total_interactions,
349
+ "days_together": self._get_days_together()
350
+ })
351
+
352
+ print(f"[IntimacyLayers] Progressed from layer {old_layer} to {next_layer} ({LAYERS[next_layer]['name']})")
353
+ self._save()
354
+ return True
355
+
356
+ def is_topic_appropriate(self, topic: str) -> bool:
357
+ """
358
+ Check if a topic is appropriate for the current intimacy layer.
359
+
360
+ Args:
361
+ topic: Topic to check (e.g., "intimate", "fantasy", "daily life")
362
+
363
+ Returns:
364
+ True if topic is appropriate, False if it requires higher layer
365
+ """
366
+ topic_lower = topic.lower()
367
+
368
+ # Check if topic is available in current layer
369
+ current_topics = LAYERS[self._current_layer].get("topics", [])
370
+ if any(topic_lower in t.lower() for t in current_topics):
371
+ return True
372
+
373
+ # Check if topic is in higher layers (restricted)
374
+ for layer_num, layer_data in LAYERS.items():
375
+ if layer_num <= self._current_layer:
376
+ continue
377
+
378
+ layer_topics = layer_data.get("topics", [])
379
+ if any(topic_lower in t.lower() for t in layer_topics):
380
+ # Topic requires higher layer
381
+ return False
382
+
383
+ # Check restricted topics for current layer
384
+ restricted = RESTRICTED_TOPICS.get(self._current_layer, [])
385
+ for restricted_term in restricted:
386
+ if restricted_term.lower() in topic_lower:
387
+ return False
388
+
389
+ # Topic not found in any layer, allow by default
390
+ return True
391
+
392
+ def get_available_topics(self) -> List[str]:
393
+ """
394
+ Get all topics available at the current intimacy layer.
395
+
396
+ Returns:
397
+ List of available topics
398
+ """
399
+ topics = []
400
+ for layer_num in range(1, self._current_layer + 1):
401
+ topics.extend(LAYERS[layer_num].get("topics", []))
402
+ return list(set(topics)) # Remove duplicates
403
+
404
+ def get_next_layer_requirements(self) -> Dict[str, Any]:
405
+ """
406
+ Get the requirements for progressing to the next layer.
407
+
408
+ Returns:
409
+ Dictionary with requirements and current progress
410
+ """
411
+ next_layer = self._current_layer + 1
412
+ if next_layer > 5:
413
+ return {"message": "Already at maximum intimacy layer"}
414
+
415
+ reqs = LAYERS[next_layer]
416
+ current = {
417
+ "interactions": self._total_interactions,
418
+ "love": self._get_love_level(),
419
+ "trust": self._get_trust_level(),
420
+ "days_together": self._get_days_together()
421
+ }
422
+
423
+ requirements = {
424
+ "layer": next_layer,
425
+ "layer_name": reqs["name"],
426
+ "description": reqs["description"],
427
+ "requirements": {},
428
+ "current_progress": current,
429
+ "can_progress": True
430
+ }
431
+
432
+ # Check each requirement
433
+ if "min_interactions" in reqs:
434
+ met = self._total_interactions >= reqs["min_interactions"]
435
+ requirements["requirements"]["interactions"] = {
436
+ "required": reqs["min_interactions"],
437
+ "current": self._total_interactions,
438
+ "met": met
439
+ }
440
+ if not met:
441
+ requirements["can_progress"] = False
442
+
443
+ if "min_love" in reqs:
444
+ current_love = self._get_love_level()
445
+ met = current_love >= reqs["min_love"]
446
+ requirements["requirements"]["love"] = {
447
+ "required": reqs["min_love"],
448
+ "current": round(current_love, 2),
449
+ "met": met
450
+ }
451
+ if not met:
452
+ requirements["can_progress"] = False
453
+
454
+ if "min_trust" in reqs:
455
+ current_trust = self._get_trust_level()
456
+ met = current_trust >= reqs["min_trust"]
457
+ requirements["requirements"]["trust"] = {
458
+ "required": reqs["min_trust"],
459
+ "current": round(current_trust, 2),
460
+ "met": met
461
+ }
462
+ if not met:
463
+ requirements["can_progress"] = False
464
+
465
+ if "min_days" in reqs:
466
+ days = self._get_days_together()
467
+ met = days >= reqs["min_days"]
468
+ requirements["requirements"]["days"] = {
469
+ "required": reqs["min_days"],
470
+ "current": days,
471
+ "met": met
472
+ }
473
+ if not met:
474
+ requirements["can_progress"] = False
475
+
476
+ return requirements
477
+
478
+ def get_progression_hint(self) -> Optional[str]:
479
+ """
480
+ Get a hint for progressing to the next layer.
481
+
482
+ Returns:
483
+ Hint string or None if no hint available
484
+ """
485
+ next_layer = self._current_layer + 1
486
+ if next_layer > 5:
487
+ return None
488
+
489
+ # Check cooldown
490
+ if self._progress.hint_cooldown_until:
491
+ try:
492
+ cooldown_until = datetime.fromisoformat(self._progress.hint_cooldown_until)
493
+ if datetime.now() < cooldown_until:
494
+ return None
495
+ except:
496
+ pass
497
+
498
+ # Don't show hint if recently shown
499
+ if self._progress.hint_shown:
500
+ return None
501
+
502
+ # Get hints for next layer
503
+ hints = LAYER_HINTS.get(next_layer, [])
504
+ if not hints:
505
+ return None
506
+
507
+ # Filter out already shown hints
508
+ available_hints = [h for h in hints if h not in self._hints_shown]
509
+ if not available_hints:
510
+ # Reset if all hints shown
511
+ available_hints = hints
512
+ self._hints_shown = []
513
+
514
+ # Select a random hint
515
+ hint = random.choice(available_hints)
516
+ self._hints_shown.append(hint)
517
+ self._progress.hint_shown = True
518
+ self._progress.hint_cooldown_until = (
519
+ datetime.now() + timedelta(hours=self.HINT_COOLDOWN_HOURS)
520
+ ).isoformat()
521
+
522
+ self._save()
523
+ return hint
524
+
525
+ def can_be_intimate(self, advanced_mode: bool = False) -> bool:
526
+ """
527
+ Check if intimate content is appropriate for current layer.
528
+
529
+ Args:
530
+ advanced_mode: If True, bypass all restrictions (owner with /advanced enabled)
531
+
532
+ Returns:
533
+ True if at layer 5 (intimate) or advanced_mode is enabled
534
+ """
535
+ if advanced_mode:
536
+ return True
537
+ return self._current_layer >= 5
538
+
539
+ def get_intimacy_level(self) -> float:
540
+ """
541
+ Get the current intimacy level (0.0 to 1.0).
542
+
543
+ Returns:
544
+ Intimacy level based on current layer
545
+ """
546
+ return LAYERS[self._current_layer].get("intimacy_level", 0.0)
547
+
548
+ def get_layer_name(self) -> str:
549
+ """
550
+ Get the name of the current layer.
551
+
552
+ Returns:
553
+ Layer name (e.g., "surface", "friendly", etc.)
554
+ """
555
+ return LAYERS[self._current_layer].get("name", "unknown")
556
+
557
+ def get_days_together(self) -> int:
558
+ """
559
+ Get the number of days since first interaction.
560
+
561
+ Returns:
562
+ Number of days, or 0 if no first interaction recorded
563
+ """
564
+ return self._get_days_together()
565
+
566
+ def get_context_for_response(self, advanced_mode: bool = False) -> Dict[str, Any]:
567
+ """
568
+ Get context about current intimacy layer for response generation.
569
+
570
+ Args:
571
+ advanced_mode: If True, bypass all restrictions (owner with /advanced enabled)
572
+
573
+ Returns:
574
+ Dictionary with layer context for the AI
575
+ """
576
+ next_reqs = self.get_next_layer_requirements()
577
+
578
+ # If advanced_mode is enabled, act as if at max layer
579
+ effective_layer = 5 if advanced_mode else self._current_layer
580
+
581
+ context = {
582
+ "current_layer": self._current_layer, # Actual layer
583
+ "effective_layer": effective_layer, # Layer used for content access
584
+ "layer_name": self.get_layer_name(),
585
+ "intimacy_level": self.get_intimacy_level(),
586
+ "available_topics": self.get_available_topics(),
587
+ "can_be_intimate": self.can_be_intimate(advanced_mode=advanced_mode),
588
+ "advanced_mode": advanced_mode,
589
+ "total_interactions": self._total_interactions,
590
+ "days_together": self._get_days_together()
591
+ }
592
+
593
+ # Add progression info if not at max layer
594
+ if self._current_layer < 5:
595
+ context["next_layer"] = {
596
+ "name": next_reqs.get("layer_name"),
597
+ "can_progress": next_reqs.get("can_progress"),
598
+ "missing_requirements": [
599
+ k for k, v in next_reqs.get("requirements", {}).items()
600
+ if not v.get("met", True)
601
+ ]
602
+ }
603
+
604
+ return context
605
+
606
+ # -------------------------------------------------------------------------
607
+ # Helper Methods
608
+ # -------------------------------------------------------------------------
609
+
610
+ def _get_love_level(self) -> float:
611
+ """Get current love level from heart system"""
612
+ if self.heart and hasattr(self.heart, 'emotion'):
613
+ return getattr(self.heart.emotion, 'love', 0.2)
614
+ if self.heart and hasattr(self.heart, 'attachment'):
615
+ return getattr(self.heart.attachment, 'affection', 0.2)
616
+ return 0.2
617
+
618
+ def _get_trust_level(self) -> float:
619
+ """Get current trust level from heart/attachment system"""
620
+ if self.heart and hasattr(self.heart, 'attachment'):
621
+ return getattr(self.heart.attachment, 'trust_level', 0.5)
622
+ if self.heart and hasattr(self.heart, 'emotion'):
623
+ return getattr(self.heart.emotion, 'trust', 0.5)
624
+ return 0.5
625
+
626
+ def _get_days_together(self) -> int:
627
+ """Calculate days since first interaction"""
628
+ if not self._first_interaction_date:
629
+ return 0
630
+ try:
631
+ first_date = datetime.fromisoformat(self._first_interaction_date)
632
+ return (datetime.now() - first_date).days
633
+ except:
634
+ return 0
635
+
636
+ # -------------------------------------------------------------------------
637
+ # Admin Methods
638
+ # -------------------------------------------------------------------------
639
+
640
+ def force_layer(self, layer: int) -> bool:
641
+ """
642
+ Force set the current layer (for testing/admin purposes).
643
+
644
+ Args:
645
+ layer: Layer number to set (1-5)
646
+
647
+ Returns:
648
+ True if successful, False if invalid layer
649
+ """
650
+ if layer not in LAYERS:
651
+ return False
652
+
653
+ old_layer = self._current_layer
654
+ self._current_layer = layer
655
+
656
+ self._layer_history.append({
657
+ "from_layer": old_layer,
658
+ "to_layer": layer,
659
+ "timestamp": datetime.now().isoformat(),
660
+ "forced": True
661
+ })
662
+
663
+ self._save()
664
+ print(f"[IntimacyLayers] Forced layer change: {old_layer} -> {layer}")
665
+ return True
666
+
667
+ def reset_progress(self):
668
+ """Reset all intimacy progress to initial state"""
669
+ self._current_layer = 1
670
+ self._first_interaction_date = None
671
+ self._total_interactions = 0
672
+ self._hints_shown = []
673
+ self._layer_history = []
674
+ self._progress = IntimacyProgress()
675
+ self._save()
676
+ print("[IntimacyLayers] Progress reset to initial state")
677
+
678
+ def get_debug_info(self) -> Dict[str, Any]:
679
+ """
680
+ Get detailed debug information about the intimacy system.
681
+
682
+ Returns:
683
+ Dictionary with all internal state
684
+ """
685
+ return {
686
+ "current_layer": self._current_layer,
687
+ "layer_name": self.get_layer_name(),
688
+ "intimacy_level": self.get_intimacy_level(),
689
+ "total_interactions": self._total_interactions,
690
+ "first_interaction_date": self._first_interaction_date,
691
+ "days_together": self._get_days_together(),
692
+ "love_level": self._get_love_level(),
693
+ "trust_level": self._get_trust_level(),
694
+ "available_topics": self.get_available_topics(),
695
+ "can_be_intimate": self.can_be_intimate(),
696
+ "progress": {
697
+ "interactions_since_check": self._progress.interactions_since_check,
698
+ "blocked_reason": self._progress.progression_blocked_reason,
699
+ "hint_shown": self._progress.hint_shown
700
+ },
701
+ "next_layer_requirements": self.get_next_layer_requirements(),
702
+ "layer_history": self._layer_history[-5:] if self._layer_history else []
703
+ }