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,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
|
package/brain/llm/zai.py
ADDED
|
@@ -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
|