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,423 @@
1
+ """
2
+ Brain: LLM - Unified LLM Interface with Fallback Chain
3
+ Manages multiple LLM providers with automatic fallback when one fails.
4
+ """
5
+
6
+ import asyncio
7
+ import time
8
+ from typing import Optional, List, Dict, Any, Tuple
9
+ from dataclasses import dataclass, field
10
+ from enum import Enum
11
+
12
+ from .base import BaseLLM
13
+ from .zai import ZAIClient
14
+ from .openrouter import OpenRouterClient
15
+ from .ollama import OllamaClient
16
+
17
+
18
+ class ProviderStatus(Enum):
19
+ """Status of an LLM provider"""
20
+ UNKNOWN = "unknown"
21
+ AVAILABLE = "available"
22
+ UNAVAILABLE = "unavailable"
23
+ RATE_LIMITED = "rate_limited"
24
+ ERROR = "error"
25
+
26
+
27
+ @dataclass
28
+ class ProviderInfo:
29
+ """Information about a provider in the fallback chain"""
30
+ name: str
31
+ client: BaseLLM
32
+ status: ProviderStatus = ProviderStatus.UNKNOWN
33
+ last_success: float = 0
34
+ last_failure: float = 0
35
+ consecutive_failures: int = 0
36
+ total_requests: int = 0
37
+ successful_requests: int = 0
38
+
39
+
40
+ class UnifiedLLM(BaseLLM):
41
+ """
42
+ Unified LLM interface with automatic fallback chain.
43
+
44
+ Tries providers in order until one succeeds:
45
+ 1. ZAI (primary)
46
+ 2. OpenRouter (fallback)
47
+ 3. Ollama (local fallback)
48
+
49
+ Features:
50
+ - Automatic failover on errors or empty responses
51
+ - Provider health tracking
52
+ - Configurable timeouts and retries
53
+ - Detailed logging
54
+ - Compatible with BaseLLM interface
55
+ """
56
+
57
+ # Default configuration
58
+ DEFAULT_CONFIG = {
59
+ "enabled": True,
60
+ "order": ["zai", "openrouter"],
61
+ "timeout_seconds": 60,
62
+ "retry_on_empty": True,
63
+ "max_consecutive_failures": 3,
64
+ "backoff_seconds": 30,
65
+ "ollama_url": "http://172.17.0.1:11434",
66
+ "ollama_model": "phi4:latest",
67
+ }
68
+
69
+ def __init__(self, config: dict = None, settings_getter=None):
70
+ """
71
+ Initialize the unified LLM with fallback chain.
72
+
73
+ Args:
74
+ config: Configuration dictionary (merged with defaults)
75
+ settings_getter: Function to get settings from settings.json
76
+ """
77
+ # BaseLLM requires api_key and model - use placeholder values
78
+ super().__init__("unified", "fallback-chain")
79
+ self.config = {**self.DEFAULT_CONFIG, **(config or {})}
80
+ self._settings_getter = settings_getter
81
+ self._providers: Dict[str, ProviderInfo] = {}
82
+ self._active_provider: Optional[str] = None
83
+ self._initialized = False
84
+
85
+ # Import os for env vars
86
+ import os
87
+ self._os = os
88
+
89
+ def _get_setting(self, key: str, default: Any = None) -> Any:
90
+ """Get a setting from config or settings.json"""
91
+ # Try config first
92
+ if key in self.config:
93
+ return self.config[key]
94
+
95
+ # Try settings getter
96
+ if self._settings_getter:
97
+ value = self._settings_getter(key)
98
+ if value is not None:
99
+ return value
100
+
101
+ # Try LLM_FALLBACK nested config
102
+ if self._settings_getter:
103
+ all_settings = self._settings_getter("LLM_FALLBACK") or {}
104
+ if key.upper() in all_settings:
105
+ return all_settings[key.upper()]
106
+
107
+ return default
108
+
109
+ def _initialize_providers(self):
110
+ """Initialize all configured providers"""
111
+ if self._initialized:
112
+ return
113
+
114
+ order = self._get_setting("order", self.config["order"])
115
+
116
+ for provider_name in order:
117
+ client = self._create_provider(provider_name)
118
+ if client:
119
+ self._providers[provider_name] = ProviderInfo(
120
+ name=provider_name,
121
+ client=client
122
+ )
123
+ print(f"[UnifiedLLM] Initialized provider: {provider_name}")
124
+
125
+ self._initialized = True
126
+
127
+ def _create_provider(self, name: str) -> Optional[BaseLLM]:
128
+ """Create a provider client by name"""
129
+ name = name.lower()
130
+
131
+ if name == "zai":
132
+ api_key = self._get_setting("ZAI_API_KEY") or self._os.environ.get("ZAI_API_KEY", "")
133
+ model = self._get_setting("ZAI_MODEL_MAIN") or self._os.environ.get("ZAI_MODEL_MAIN", "glm-4.6v")
134
+ if api_key:
135
+ return ZAIClient(api_key, model)
136
+
137
+ elif name == "openrouter":
138
+ api_key = self._get_setting("OPENROUTER_API_KEY") or self._os.environ.get("OPENROUTER_API_KEY", "")
139
+ model = self._get_setting("OPENROUTER_MODEL_MAIN") or self._os.environ.get("OPENROUTER_MODEL_MAIN", "anthropic/claude-3.5-sonnet")
140
+ if api_key:
141
+ return OpenRouterClient(api_key, model)
142
+
143
+ elif name == "ollama":
144
+ url = self._get_setting("OLLAMA_URL") or self._get_setting("ollama_url", "http://172.17.0.1:11434")
145
+ model = self._get_setting("OLLAMA_MODEL") or self._get_setting("ollama_model", "phi4:latest")
146
+ # Ollama doesn't require an API key
147
+ return OllamaClient("", model, url)
148
+
149
+ return None
150
+
151
+ async def _check_provider_available(self, name: str) -> bool:
152
+ """Check if a provider is available for use"""
153
+ if name not in self._providers:
154
+ return False
155
+
156
+ info = self._providers[name]
157
+
158
+ # Check if we're in backoff due to consecutive failures
159
+ max_failures = self._get_setting("max_consecutive_failures", 3)
160
+ backoff = self._get_setting("backoff_seconds", 30)
161
+
162
+ if info.consecutive_failures >= max_failures:
163
+ if time.time() - info.last_failure < backoff:
164
+ print(f"[UnifiedLLM] Provider {name} in backoff ({info.consecutive_failures} failures)")
165
+ return False
166
+ else:
167
+ # Reset after backoff period
168
+ info.consecutive_failures = 0
169
+
170
+ # Check actual availability if provider supports it
171
+ client = info.client
172
+ if hasattr(client, 'is_available'):
173
+ try:
174
+ available = await client.is_available()
175
+ info.status = ProviderStatus.AVAILABLE if available else ProviderStatus.UNAVAILABLE
176
+ return available
177
+ except Exception as e:
178
+ print(f"[UnifiedLLM] Error checking {name} availability: {e}")
179
+ info.status = ProviderStatus.ERROR
180
+ return False
181
+
182
+ # Assume available if we can't check
183
+ return True
184
+
185
+ async def chat(
186
+ self,
187
+ messages: List[Dict[str, str]],
188
+ max_tokens: int = 500,
189
+ temperature: float = None
190
+ ) -> Optional[str]:
191
+ """
192
+ Send a chat request through the fallback chain.
193
+
194
+ This method is compatible with BaseLLM interface - returns just the response string.
195
+
196
+ Args:
197
+ messages: List of message dictionaries with 'role' and 'content'
198
+ max_tokens: Maximum tokens to generate
199
+ temperature: Sampling temperature (None = use default)
200
+
201
+ Returns:
202
+ Response text string, or None if all providers fail
203
+ """
204
+ response, _ = await self.chat_with_provider(messages, max_tokens, temperature)
205
+ return response
206
+
207
+ async def chat_with_provider(
208
+ self,
209
+ messages: List[Dict[str, str]],
210
+ max_tokens: int = 500,
211
+ temperature: float = None,
212
+ provider_hint: str = None
213
+ ) -> Tuple[Optional[str], str]:
214
+ """
215
+ Send a chat request through the fallback chain, returning provider info.
216
+
217
+ Args:
218
+ messages: List of message dictionaries with 'role' and 'content'
219
+ max_tokens: Maximum tokens to generate
220
+ temperature: Sampling temperature (None = use default)
221
+ provider_hint: Optional hint for which provider to try first
222
+
223
+ Returns:
224
+ Tuple of (response_text, provider_name_used)
225
+ Returns (None, "") if all providers fail
226
+ """
227
+ if not self._get_setting("enabled", True):
228
+ # Fallback mode disabled, use only first provider
229
+ return await self._try_single_provider(
230
+ list(self._providers.keys())[0] if self._providers else None,
231
+ messages, max_tokens, temperature
232
+ )
233
+
234
+ self._initialize_providers()
235
+
236
+ if not self._providers:
237
+ print("[UnifiedLLM] No providers configured!")
238
+ return None, ""
239
+
240
+ # Determine order to try providers
241
+ order = list(self._providers.keys())
242
+
243
+ # If we have an active provider that recently succeeded, try it first
244
+ if self._active_provider and self._active_provider in order:
245
+ order.remove(self._active_provider)
246
+ order.insert(0, self._active_provider)
247
+
248
+ # Apply provider hint
249
+ if provider_hint and provider_hint in order:
250
+ order.remove(provider_hint)
251
+ order.insert(0, provider_hint)
252
+
253
+ # Try each provider in order
254
+ last_error = None
255
+ for provider_name in order:
256
+ # Check if provider is available
257
+ if not await self._check_provider_available(provider_name):
258
+ continue
259
+
260
+ try:
261
+ response = await self._try_single_provider(
262
+ provider_name, messages, max_tokens, temperature
263
+ )
264
+
265
+ if response:
266
+ return response, provider_name
267
+ except Exception as e:
268
+ last_error = e
269
+ print(f"[UnifiedLLM] Provider {provider_name} failed: {e}")
270
+
271
+ # All providers failed
272
+ print(f"[UnifiedLLM] All providers failed. Last error: {last_error}")
273
+ return None, ""
274
+
275
+ async def _try_single_provider(
276
+ self,
277
+ provider_name: str,
278
+ messages: List[Dict[str, str]],
279
+ max_tokens: int,
280
+ temperature: float
281
+ ) -> Optional[str]:
282
+ """Try to get a response from a single provider"""
283
+ if not provider_name or provider_name not in self._providers:
284
+ return None
285
+
286
+ info = self._providers[provider_name]
287
+ client = info.client
288
+ timeout = self._get_setting("timeout_seconds", 60)
289
+ retry_on_empty = self._get_setting("retry_on_empty", True)
290
+
291
+ info.total_requests += 1
292
+
293
+ try:
294
+ # Add timeout wrapper
295
+ response = await asyncio.wait_for(
296
+ client.chat(messages, max_tokens=max_tokens, temperature=temperature),
297
+ timeout=timeout
298
+ )
299
+
300
+ # Check for empty response
301
+ if not response or not response.strip():
302
+ if retry_on_empty:
303
+ print(f"[UnifiedLLM] Empty response from {provider_name}, retrying...")
304
+ # One retry with different temperature
305
+ response = await asyncio.wait_for(
306
+ client.chat(messages, max_tokens=max_tokens, temperature=0.7),
307
+ timeout=timeout
308
+ )
309
+
310
+ if response and response.strip():
311
+ # Success!
312
+ info.last_success = time.time()
313
+ info.consecutive_failures = 0
314
+ info.successful_requests += 1
315
+ info.status = ProviderStatus.AVAILABLE
316
+ self._active_provider = provider_name
317
+ return response
318
+
319
+ # Empty response after retry
320
+ info.last_failure = time.time()
321
+ info.consecutive_failures += 1
322
+ return None
323
+
324
+ except asyncio.TimeoutError:
325
+ print(f"[UnifiedLLM] Timeout from {provider_name} after {timeout}s")
326
+ info.last_failure = time.time()
327
+ info.consecutive_failures += 1
328
+ info.status = ProviderStatus.ERROR
329
+ return None
330
+
331
+ except Exception as e:
332
+ print(f"[UnifiedLLM] Error from {provider_name}: {e}")
333
+ info.last_failure = time.time()
334
+ info.consecutive_failures += 1
335
+ info.status = ProviderStatus.ERROR
336
+ return None
337
+
338
+ def get_active_provider(self) -> Optional[str]:
339
+ """Get the name of the currently active provider"""
340
+ return self._active_provider
341
+
342
+ def get_provider_status(self) -> Dict[str, dict]:
343
+ """Get status of all providers"""
344
+ return {
345
+ name: {
346
+ "status": info.status.value,
347
+ "last_success": info.last_success,
348
+ "last_failure": info.last_failure,
349
+ "consecutive_failures": info.consecutive_failures,
350
+ "total_requests": info.total_requests,
351
+ "successful_requests": info.successful_requests,
352
+ "success_rate": info.successful_requests / max(1, info.total_requests)
353
+ }
354
+ for name, info in self._providers.items()
355
+ }
356
+
357
+ def reset_provider(self, name: str):
358
+ """Reset a provider's failure count"""
359
+ if name in self._providers:
360
+ self._providers[name].consecutive_failures = 0
361
+ self._providers[name].status = ProviderStatus.UNKNOWN
362
+ print(f"[UnifiedLLM] Reset provider: {name}")
363
+
364
+ def set_provider_config(self, name: str, **kwargs):
365
+ """Update configuration for a provider"""
366
+ if name in self._providers:
367
+ client = self._providers[name].client
368
+ for key, value in kwargs.items():
369
+ if hasattr(client, key):
370
+ setattr(client, key, value)
371
+ print(f"[UnifiedLLM] Updated {name}.{key} = {value}")
372
+
373
+ async def close(self):
374
+ """Close all provider sessions"""
375
+ for name, info in self._providers.items():
376
+ try:
377
+ if hasattr(info.client, 'close'):
378
+ await info.client.close()
379
+ except Exception as e:
380
+ print(f"[UnifiedLLM] Error closing {name}: {e}")
381
+
382
+ def __repr__(self):
383
+ providers = list(self._providers.keys())
384
+ active = self._active_provider or "none"
385
+ return f"<UnifiedLLM providers={providers} active={active}>"
386
+
387
+
388
+ # Singleton instance
389
+ _unified_llm: Optional[UnifiedLLM] = None
390
+
391
+
392
+ def get_unified_llm(config: dict = None, settings_getter=None) -> UnifiedLLM:
393
+ """
394
+ Get the global UnifiedLLM instance.
395
+
396
+ Args:
397
+ config: Configuration (only used on first call)
398
+ settings_getter: Function to get settings from settings.json
399
+
400
+ Returns:
401
+ The UnifiedLLM singleton
402
+ """
403
+ global _unified_llm
404
+
405
+ if _unified_llm is None:
406
+ # Get settings getter if not provided
407
+ if settings_getter is None:
408
+ try:
409
+ from core.settings import get as settings_get
410
+ settings_getter = settings_get
411
+ except ImportError:
412
+ pass
413
+
414
+ _unified_llm = UnifiedLLM(config, settings_getter)
415
+ _unified_llm._initialize_providers()
416
+
417
+ return _unified_llm
418
+
419
+
420
+ def reset_unified_llm():
421
+ """Reset the singleton (for testing)"""
422
+ global _unified_llm
423
+ _unified_llm = None
@@ -0,0 +1,169 @@
1
+ """
2
+ Brain: LLM - ZAI API Client
3
+ Uses OpenAI-compatible endpoint for ZAI Coding Plan
4
+ """
5
+
6
+ import aiohttp
7
+ import asyncio
8
+ import time
9
+ from typing import Optional, List, Dict
10
+ from .base import BaseLLM
11
+
12
+
13
+ class ZAIClient(BaseLLM):
14
+ """ZAI API client (OpenAI-compatible)"""
15
+
16
+ # ZAI Coding Plan uses OpenAI-compatible endpoint
17
+ BASE_URL = "https://api.z.ai/api/coding/paas/v4"
18
+
19
+ def __init__(self, api_key: str, model: str = "glm-4.6v"):
20
+ super().__init__(api_key, model)
21
+ self.session: Optional[aiohttp.ClientSession] = None
22
+ self._available: Optional[bool] = None
23
+ self._last_check: float = 0
24
+
25
+ async def _get_session(self) -> aiohttp.ClientSession:
26
+ if self.session is None or self.session.closed:
27
+ self.session = aiohttp.ClientSession()
28
+ return self.session
29
+
30
+ async def is_available(self) -> bool:
31
+ """Check if ZAI API is accessible and configured"""
32
+ # Cache availability for 60 seconds
33
+ if self._available is not None and time.time() - self._last_check < 60:
34
+ return self._available
35
+
36
+ if not self.api_key:
37
+ self._available = False
38
+ self._last_check = time.time()
39
+ return False
40
+
41
+ try:
42
+ session = await self._get_session()
43
+
44
+ # Simple models list check to verify API is working
45
+ async with session.get(
46
+ f"{self.BASE_URL}/models",
47
+ headers={"Authorization": f"Bearer {self.api_key}"},
48
+ timeout=aiohttp.ClientTimeout(total=10)
49
+ ) as resp:
50
+ if resp.status == 200:
51
+ self._available = True
52
+ self._last_check = time.time()
53
+ return True
54
+ elif resp.status == 401:
55
+ print("[ZAI] Invalid API key")
56
+ self._available = False
57
+ self._last_check = time.time()
58
+ return False
59
+ except Exception as e:
60
+ print(f"[ZAI] Availability check failed: {e}")
61
+
62
+ self._available = False
63
+ self._last_check = time.time()
64
+ return False
65
+
66
+ async def chat(
67
+ self,
68
+ messages: List[Dict[str, str]],
69
+ max_tokens: int = 500,
70
+ temperature: float = None
71
+ ) -> Optional[str]:
72
+ """Send chat completion request (OpenAI format)"""
73
+ import os
74
+ # Use passed temperature, or environment variable, or default high value
75
+ if temperature is None:
76
+ temperature = float(os.environ.get("LLM_TEMPERATURE", "0.95"))
77
+
78
+ session = await self._get_session()
79
+
80
+ # OpenAI API format with Bearer token
81
+ headers = {
82
+ "Authorization": f"Bearer {self.api_key}",
83
+ "Content-Type": "application/json"
84
+ }
85
+
86
+ payload = {
87
+ "model": self.model,
88
+ "messages": messages,
89
+ "max_tokens": max_tokens,
90
+ "temperature": temperature,
91
+ "frequency_penalty": 0.8,
92
+ "presence_penalty": 0.6,
93
+ "thinking": {
94
+ "type": "disabled"
95
+ },
96
+ }
97
+
98
+ # Try up to 2 times
99
+ for attempt in range(2):
100
+ try:
101
+ async with session.post(
102
+ f"{self.BASE_URL}/chat/completions",
103
+ headers=headers,
104
+ json=payload,
105
+ timeout=aiohttp.ClientTimeout(total=60)
106
+ ) as resp:
107
+ if resp.status != 200:
108
+ error = await resp.text()
109
+ print(f"[ZAI] Error {resp.status}: {error[:300]}")
110
+ self._available = False
111
+ return None
112
+
113
+ data = await resp.json()
114
+ # Check for error response
115
+ if "error" in data:
116
+ print(f"[ZAI] API Error: {data['error']}")
117
+ self._available = False
118
+ return None
119
+ # Log token usage
120
+ if "usage" in data:
121
+ usage = data["usage"]
122
+ print(f"[LLM] Tokens - Input: {usage.get('prompt_tokens', '?')} | Output: {usage.get('completion_tokens', '?')} | Total: {usage.get('total_tokens', '?')}")
123
+ # OpenAI response format
124
+ if "choices" not in data or not data["choices"]:
125
+ print(f"[ZAI] No choices in response: {list(data.keys())}")
126
+ return None
127
+
128
+ message = data["choices"][0].get("message", {})
129
+ content = message.get("content", "")
130
+
131
+ # Log if reasoning_content present (thinking mode leak)
132
+ if message.get("reasoning_content"):
133
+ print(f"[ZAI] Note: reasoning_content present but ignored (internal thinking)")
134
+
135
+ # If still empty, retry with intimate instruction
136
+ if not content or not content.strip():
137
+ if attempt == 0:
138
+ print(f"[ZAI] Empty content on first attempt, retrying with intimate instruction...")
139
+ # Add intimate instruction to output dialogue
140
+ retry_messages = messages.copy()
141
+ retry_messages.append({
142
+ "role": "system",
143
+ "content": "IMPORTANT: You must respond with actual dialogue that can be spoken. Do not just think - say something out loud."
144
+ })
145
+ payload["messages"] = retry_messages
146
+ continue
147
+ else:
148
+ print(f"[ZAI] Empty content after retry")
149
+ return None
150
+
151
+ print(f"[ZAI] Response: {content[:80]}...")
152
+ # Mark as available since we got a response
153
+ self._available = True
154
+ self._last_check = time.time()
155
+ return content
156
+
157
+ except asyncio.TimeoutError:
158
+ print(f"[ZAI] Timeout (60s)")
159
+ return None
160
+ except Exception as e:
161
+ print(f"[ZAI] Exception: {e}")
162
+ if attempt == 1:
163
+ return None
164
+
165
+ return None
166
+
167
+ async def close(self):
168
+ if self.session and not self.session.closed:
169
+ await self.session.close()
@@ -0,0 +1,23 @@
1
+ # Brain - Intelligence Center
2
+
3
+ Multi-provider LLM, memory systems, and subconscious processing.
4
+
5
+ ## Modules
6
+ - `memory/` - Token-efficient memory with vector search
7
+ - `vector_store.py` - Redis-based semantic memory
8
+ - `episodic.py` - Conversation memory
9
+ - `working.py` - Working memory (RAM)
10
+ - `llm/` - Multi-provider LLM support (see llm/manifest.md)
11
+ - `subconscious/` - 24/7 living brain (see subconscious/manifest.md)
12
+ - `embeddings/` - Sentence transformers for semantic search
13
+ - `stt/` - Speech-to-text (Google, Whisper)
14
+
15
+ ## Key Files
16
+ - `__init__.py` - Main Memory class
17
+ - `index.py` - Fast memory index
18
+
19
+ ## Features
20
+ - Multi-provider: ZAI or OpenRouter
21
+ - Semantic search via embeddings (all-MiniLM-L6-v2)
22
+ - Redis vector storage with archiving
23
+ - Subconscious impulse generation