predacore 0.1.0__py3-none-any.whl

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 (193) hide show
  1. predacore/__init__.py +12 -0
  2. predacore/_vendor/__init__.py +0 -0
  3. predacore/_vendor/common/__init__.py +1 -0
  4. predacore/_vendor/common/embedding.py +442 -0
  5. predacore/_vendor/common/errors.py +247 -0
  6. predacore/_vendor/common/llm.py +592 -0
  7. predacore/_vendor/common/logging_config.py +213 -0
  8. predacore/_vendor/common/logging_utils.py +73 -0
  9. predacore/_vendor/common/memory_service.py +850 -0
  10. predacore/_vendor/common/metrics.py +246 -0
  11. predacore/_vendor/common/models.py +309 -0
  12. predacore/_vendor/common/notifications.py +459 -0
  13. predacore/_vendor/common/protos/__init__.py +32 -0
  14. predacore/_vendor/common/protos/csc_pb2.py +55 -0
  15. predacore/_vendor/common/protos/csc_pb2_grpc.py +192 -0
  16. predacore/_vendor/common/protos/daf_pb2.py +77 -0
  17. predacore/_vendor/common/protos/daf_pb2_grpc.py +419 -0
  18. predacore/_vendor/common/protos/egm_pb2.py +62 -0
  19. predacore/_vendor/common/protos/egm_pb2_grpc.py +278 -0
  20. predacore/_vendor/common/protos/knowledge_nexus_pb2.py +64 -0
  21. predacore/_vendor/common/protos/knowledge_nexus_pb2_grpc.py +324 -0
  22. predacore/_vendor/common/protos/wil_pb2.py +55 -0
  23. predacore/_vendor/common/protos/wil_pb2_grpc.py +192 -0
  24. predacore/_vendor/common/skill_collective.py +587 -0
  25. predacore/_vendor/common/skill_evolution.py +497 -0
  26. predacore/_vendor/common/skill_genome.py +391 -0
  27. predacore/_vendor/common/skill_scanner.py +522 -0
  28. predacore/_vendor/common/structured_output.py +32 -0
  29. predacore/_vendor/common/vector_store.py +205 -0
  30. predacore/_vendor/core_strategic_engine/__init__.py +3 -0
  31. predacore/_vendor/core_strategic_engine/llm_planner.py +204 -0
  32. predacore/_vendor/core_strategic_engine/plan_cache.py +81 -0
  33. predacore/_vendor/core_strategic_engine/planner.py +730 -0
  34. predacore/_vendor/core_strategic_engine/planner_enhancements.py +368 -0
  35. predacore/_vendor/core_strategic_engine/planner_mcts.py +873 -0
  36. predacore/_vendor/ethical_governance_module/__init__.py +3 -0
  37. predacore/_vendor/ethical_governance_module/audit_logger.py +95 -0
  38. predacore/_vendor/ethical_governance_module/persistent_audit.py +465 -0
  39. predacore/_vendor/ethical_governance_module/rule_engine.py +225 -0
  40. predacore/_vendor/ethical_governance_module/service.py +356 -0
  41. predacore/_vendor/knowledge_nexus/__init__.py +12 -0
  42. predacore/_vendor/knowledge_nexus/faiss_vector_index.py +208 -0
  43. predacore/_vendor/knowledge_nexus/health.py +16 -0
  44. predacore/_vendor/knowledge_nexus/service.py +531 -0
  45. predacore/_vendor/knowledge_nexus/storage.py +558 -0
  46. predacore/_vendor/knowledge_nexus/vector_index.py +42 -0
  47. predacore/_vendor/user_modeling_engine/__init__.py +5 -0
  48. predacore/_vendor/user_modeling_engine/service.py +145 -0
  49. predacore/agents/__init__.py +0 -0
  50. predacore/agents/autonomy.py +790 -0
  51. predacore/agents/collaboration.py +402 -0
  52. predacore/agents/daf/__init__.py +39 -0
  53. predacore/agents/daf/agent_process.py +629 -0
  54. predacore/agents/daf/agent_registry.py +320 -0
  55. predacore/agents/daf/health.py +22 -0
  56. predacore/agents/daf/health_api.py +21 -0
  57. predacore/agents/daf/metrics_util.py +30 -0
  58. predacore/agents/daf/scheduler.py +139 -0
  59. predacore/agents/daf/self_optimization.py +456 -0
  60. predacore/agents/daf/service.py +1135 -0
  61. predacore/agents/daf/task_store.py +115 -0
  62. predacore/agents/daf_bridge.py +404 -0
  63. predacore/agents/engine.py +1089 -0
  64. predacore/agents/meta_cognition.py +381 -0
  65. predacore/agents/self_improvement.py +481 -0
  66. predacore/auth/__init__.py +0 -0
  67. predacore/auth/middleware.py +431 -0
  68. predacore/auth/sandbox.py +782 -0
  69. predacore/auth/security.py +423 -0
  70. predacore/bootstrap.py +471 -0
  71. predacore/channels/__init__.py +12 -0
  72. predacore/channels/discord.py +171 -0
  73. predacore/channels/email.py +338 -0
  74. predacore/channels/health.py +461 -0
  75. predacore/channels/imessage.py +269 -0
  76. predacore/channels/registry.py +220 -0
  77. predacore/channels/signal.py +219 -0
  78. predacore/channels/slack.py +239 -0
  79. predacore/channels/telegram.py +512 -0
  80. predacore/channels/webchat.py +1100 -0
  81. predacore/channels/whatsapp.py +242 -0
  82. predacore/cli.py +2420 -0
  83. predacore/config.py +871 -0
  84. predacore/core.py +1959 -0
  85. predacore/errors.py +208 -0
  86. predacore/gateway.py +833 -0
  87. predacore/identity/EVENT_HORIZON.md +236 -0
  88. predacore/identity/SOUL_SEED.md +92 -0
  89. predacore/identity/__init__.py +29 -0
  90. predacore/identity/beliefs.py +444 -0
  91. predacore/identity/defaults/HEARTBEAT.md +52 -0
  92. predacore/identity/defaults/IDENTITY.md +56 -0
  93. predacore/identity/defaults/JOURNAL.md +12 -0
  94. predacore/identity/defaults/MEMORY.md +21 -0
  95. predacore/identity/defaults/REFLECTION.md +21 -0
  96. predacore/identity/defaults/SOUL.md +50 -0
  97. predacore/identity/defaults/TOOLS.md +28 -0
  98. predacore/identity/defaults/USER.md +24 -0
  99. predacore/identity/engine.py +816 -0
  100. predacore/identity/heartbeat.py +242 -0
  101. predacore/identity/reflection.py +411 -0
  102. predacore/llm_providers/__init__.py +20 -0
  103. predacore/llm_providers/_anthropic_wire.py +291 -0
  104. predacore/llm_providers/anthropic.py +168 -0
  105. predacore/llm_providers/base.py +65 -0
  106. predacore/llm_providers/circuit_breaker.py +98 -0
  107. predacore/llm_providers/claude_models.py +29 -0
  108. predacore/llm_providers/gemini.py +296 -0
  109. predacore/llm_providers/gemini_cli.py +120 -0
  110. predacore/llm_providers/message_validator.py +263 -0
  111. predacore/llm_providers/openai.py +392 -0
  112. predacore/llm_providers/predacore_sdk.py +324 -0
  113. predacore/llm_providers/router.py +377 -0
  114. predacore/llm_providers/text_tool_adapter.py +152 -0
  115. predacore/memory/__init__.py +33 -0
  116. predacore/memory/consolidator.py +718 -0
  117. predacore/memory/retriever.py +390 -0
  118. predacore/memory/store.py +2070 -0
  119. predacore/operators/__init__.py +98 -0
  120. predacore/operators/android.py +1873 -0
  121. predacore/operators/base.py +340 -0
  122. predacore/operators/browser_bridge.py +2019 -0
  123. predacore/operators/desktop.py +856 -0
  124. predacore/operators/enums.py +285 -0
  125. predacore/operators/mock.py +356 -0
  126. predacore/operators/native_service.py +1589 -0
  127. predacore/operators/ocr_fallback.py +413 -0
  128. predacore/operators/retry.py +151 -0
  129. predacore/operators/screen_vision.py +948 -0
  130. predacore/operators/smart_input.py +1227 -0
  131. predacore/prompts.py +332 -0
  132. predacore/services/__init__.py +0 -0
  133. predacore/services/alerting.py +524 -0
  134. predacore/services/code_index.py +1332 -0
  135. predacore/services/config_watcher.py +236 -0
  136. predacore/services/cron.py +415 -0
  137. predacore/services/daemon.py +710 -0
  138. predacore/services/db_adapter.py +225 -0
  139. predacore/services/db_client.py +358 -0
  140. predacore/services/db_server.py +312 -0
  141. predacore/services/embedding.py +510 -0
  142. predacore/services/git_integration.py +675 -0
  143. predacore/services/identity_service.py +325 -0
  144. predacore/services/lane_queue.py +380 -0
  145. predacore/services/mcp_client.py +373 -0
  146. predacore/services/mcp_registry.py +321 -0
  147. predacore/services/outcome_store.py +620 -0
  148. predacore/services/plugins.py +508 -0
  149. predacore/services/rate_limiter.py +449 -0
  150. predacore/services/transcripts.py +259 -0
  151. predacore/services/voice.py +669 -0
  152. predacore/sessions.py +659 -0
  153. predacore/tools/__init__.py +14 -0
  154. predacore/tools/dispatcher.py +585 -0
  155. predacore/tools/enums.py +197 -0
  156. predacore/tools/executor.py +295 -0
  157. predacore/tools/handlers/__init__.py +283 -0
  158. predacore/tools/handlers/_context.py +290 -0
  159. predacore/tools/handlers/agent.py +993 -0
  160. predacore/tools/handlers/apis.py +323 -0
  161. predacore/tools/handlers/channels.py +481 -0
  162. predacore/tools/handlers/collective_intelligence.py +249 -0
  163. predacore/tools/handlers/creative.py +284 -0
  164. predacore/tools/handlers/cron.py +253 -0
  165. predacore/tools/handlers/desktop.py +608 -0
  166. predacore/tools/handlers/file_ops.py +173 -0
  167. predacore/tools/handlers/git.py +73 -0
  168. predacore/tools/handlers/identity.py +100 -0
  169. predacore/tools/handlers/marketplace.py +96 -0
  170. predacore/tools/handlers/mcp.py +362 -0
  171. predacore/tools/handlers/memory.py +298 -0
  172. predacore/tools/handlers/pipeline_handler.py +299 -0
  173. predacore/tools/handlers/shell.py +323 -0
  174. predacore/tools/handlers/stats.py +132 -0
  175. predacore/tools/handlers/voice.py +225 -0
  176. predacore/tools/handlers/web.py +403 -0
  177. predacore/tools/health.py +233 -0
  178. predacore/tools/marketplace.py +684 -0
  179. predacore/tools/middleware.py +498 -0
  180. predacore/tools/pipeline.py +841 -0
  181. predacore/tools/registry.py +2249 -0
  182. predacore/tools/resilience.py +429 -0
  183. predacore/tools/skill_marketplace.py +503 -0
  184. predacore/tools/subsystem_init.py +351 -0
  185. predacore/tools/trust_policy.py +344 -0
  186. predacore/utils/__init__.py +1 -0
  187. predacore/utils/cache.py +121 -0
  188. predacore-0.1.0.dist-info/METADATA +267 -0
  189. predacore-0.1.0.dist-info/RECORD +193 -0
  190. predacore-0.1.0.dist-info/WHEEL +5 -0
  191. predacore-0.1.0.dist-info/entry_points.txt +12 -0
  192. predacore-0.1.0.dist-info/licenses/LICENSE +190 -0
  193. predacore-0.1.0.dist-info/top_level.txt +1 -0
predacore/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ """
2
+ PredaCore — The unified PredaCore AI agent.
3
+
4
+ Wraps the entire PredaCore stack (CSC, DAF, WIL, EGM, KN) into a
5
+ single-process conversational agent that can be run with:
6
+
7
+ predacore chat – interactive terminal session
8
+ predacore start – 24/7 daemon mode
9
+ predacore setup – guided onboarding wizard
10
+ """
11
+
12
+ __version__ = "0.1.0"
File without changes
@@ -0,0 +1 @@
1
+ # This file makes 'src/common' a Python package.
@@ -0,0 +1,442 @@
1
+ """
2
+ Embedding client abstraction and a simple in-memory vector index.
3
+
4
+ Uses provider-agnostic env config; falls back to a hashing embedding when
5
+ no provider is configured.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import math
11
+ import re
12
+ import threading
13
+ from typing import Any, Optional
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class EmbeddingClient:
19
+ async def embed(self, texts: list[str]) -> list[list[float]]:
20
+ raise NotImplementedError
21
+
22
+
23
+ class OpenAIEmbeddingClient(EmbeddingClient):
24
+ def __init__(
25
+ self,
26
+ api_key: str,
27
+ model: str = "text-embedding-3-small",
28
+ base_url: str | None = None,
29
+ ):
30
+ self.api_key = api_key
31
+ self.model = model
32
+ self.base_url = (base_url or "https://api.openai.com").rstrip("/")
33
+ self.endpoint = f"{self.base_url}/v1/embeddings"
34
+
35
+ async def embed(self, texts: list[str]) -> list[list[float]]:
36
+ import httpx
37
+
38
+ headers = {
39
+ "Authorization": f"Bearer {self.api_key}",
40
+ "Content-Type": "application/json",
41
+ }
42
+ async with httpx.AsyncClient(timeout=60) as client:
43
+ resp = await client.post(
44
+ self.endpoint,
45
+ json={"model": self.model, "input": texts},
46
+ headers=headers,
47
+ )
48
+ resp.raise_for_status()
49
+ data = resp.json()
50
+ return [item["embedding"] for item in data["data"]]
51
+
52
+
53
+ class GeminiEmbeddingClient(EmbeddingClient):
54
+ """Google Gemini embedding client using the Generative Language API."""
55
+
56
+ def __init__(
57
+ self,
58
+ api_key: str,
59
+ model: str = "models/gemini-embedding-001",
60
+ base_url: str | None = None,
61
+ ):
62
+ self.api_key = api_key
63
+ self.model = model if model.startswith("models/") else f"models/{model}"
64
+ self.base_url = (
65
+ base_url or "https://generativelanguage.googleapis.com"
66
+ ).rstrip("/")
67
+ self.single_endpoint = f"{self.base_url}/v1beta/{self.model}:embedContent"
68
+
69
+ async def embed(self, texts: list[str]) -> list[list[float]]:
70
+ import httpx
71
+
72
+ if not texts:
73
+ return []
74
+
75
+ headers = {
76
+ "Content-Type": "application/json",
77
+ "x-goog-api-key": self.api_key,
78
+ }
79
+
80
+ def _extract_embedding(payload: dict[str, Any]) -> list[float] | None:
81
+ if isinstance(payload.get("values"), list):
82
+ return [float(v) for v in payload["values"]]
83
+ emb = payload.get("embedding")
84
+ if isinstance(emb, dict) and isinstance(emb.get("values"), list):
85
+ return [float(v) for v in emb["values"]]
86
+ if isinstance(emb, list):
87
+ return [float(v) for v in emb]
88
+ return None
89
+
90
+ async with httpx.AsyncClient(timeout=60) as client:
91
+ vectors = []
92
+ for text in texts:
93
+ resp = await client.post(
94
+ self.single_endpoint,
95
+ headers=headers,
96
+ json={
97
+ "model": self.model,
98
+ "content": {"parts": [{"text": text}]},
99
+ },
100
+ )
101
+ resp.raise_for_status()
102
+ data = resp.json()
103
+ vec = _extract_embedding(data if isinstance(data, dict) else {})
104
+ if not vec:
105
+ raise RuntimeError("Unexpected Gemini embedding response format")
106
+ vectors.append(vec)
107
+ return vectors
108
+
109
+
110
+ class ZhiPuEmbeddingClient(EmbeddingClient):
111
+ """ZhiPu BigModel embedding client (OpenAI-compatible API)."""
112
+
113
+ def __init__(
114
+ self,
115
+ api_key: str,
116
+ model: str = "embedding-3",
117
+ base_url: str = "https://open.bigmodel.cn/api/paas/v4",
118
+ ):
119
+ self.api_key = api_key
120
+ self.model = model
121
+ self.base_url = base_url.rstrip("/")
122
+ self.endpoint = f"{self.base_url}/embeddings"
123
+
124
+ async def embed(self, texts: list[str]) -> list[list[float]]:
125
+ import httpx
126
+
127
+ headers = {
128
+ "Authorization": f"Bearer {self.api_key}",
129
+ "Content-Type": "application/json",
130
+ }
131
+ async with httpx.AsyncClient(timeout=60) as client:
132
+ resp = await client.post(
133
+ self.endpoint,
134
+ json={"model": self.model, "input": texts},
135
+ headers=headers,
136
+ )
137
+ resp.raise_for_status()
138
+ data = resp.json()
139
+ return [item["embedding"] for item in data["data"]]
140
+
141
+
142
+ class LocalEmbeddingClient(EmbeddingClient):
143
+ """
144
+ Local semantic embedding using sentence-transformers models via HuggingFace.
145
+
146
+ Runs entirely offline — no API key needed. Uses GTE-small (384-dim)
147
+ by default: fast, lightweight (~67MB), and produces real semantic vectors.
148
+ GTE-small scores ~61.4 on MTEB vs MiniLM's ~56.3 — same dim, better quality.
149
+ """
150
+
151
+ _model_cache: dict[str, Any] = {}
152
+ _model_lock = threading.Lock()
153
+
154
+ def __init__(self, model_name: str = "thenlper/gte-small"):
155
+ self.model_name = model_name
156
+ self._tokenizer: Any = None
157
+ self._model: Any = None
158
+
159
+ def _load(self) -> None:
160
+ if self._model is not None:
161
+ return
162
+ with LocalEmbeddingClient._model_lock:
163
+ # Double-check after acquiring lock
164
+ if self._model is not None:
165
+ return
166
+ if self.model_name in LocalEmbeddingClient._model_cache:
167
+ cached = LocalEmbeddingClient._model_cache[self.model_name]
168
+ self._tokenizer = cached["tokenizer"]
169
+ self._model = cached["model"]
170
+ return
171
+ from transformers import AutoModel, AutoTokenizer
172
+
173
+ self._tokenizer = AutoTokenizer.from_pretrained(self.model_name)
174
+ self._model = AutoModel.from_pretrained(self.model_name)
175
+ self._model.eval()
176
+ LocalEmbeddingClient._model_cache[self.model_name] = {
177
+ "tokenizer": self._tokenizer,
178
+ "model": self._model,
179
+ }
180
+ logger.info("LocalEmbeddingClient: loaded %s", self.model_name)
181
+
182
+ async def embed(self, texts: list[str]) -> list[list[float]]:
183
+ import asyncio
184
+
185
+ return await asyncio.to_thread(self._embed_sync, texts)
186
+
187
+ def _embed_sync(self, texts: list[str]) -> list[list[float]]:
188
+ import torch
189
+
190
+ self._load()
191
+ encoded = self._tokenizer(
192
+ texts, padding=True, truncation=True, max_length=256, return_tensors="pt"
193
+ )
194
+ with torch.no_grad():
195
+ outputs = self._model(**encoded)
196
+ mask = encoded["attention_mask"]
197
+ token_emb = outputs.last_hidden_state
198
+ mask_expanded = mask.unsqueeze(-1).expand(token_emb.size()).float()
199
+ summed = torch.sum(token_emb * mask_expanded, 1)
200
+ counts = torch.clamp(mask_expanded.sum(1), min=1e-9)
201
+ embeddings = torch.nn.functional.normalize(summed / counts, p=2, dim=1)
202
+ return embeddings.tolist()
203
+
204
+
205
+ class HashingEmbeddingClient(EmbeddingClient):
206
+ def __init__(self, dim: int = 256):
207
+ self.dim = dim
208
+
209
+ def _hash(self, s: str) -> list[float]:
210
+ # Very crude deterministic embedding for offline use
211
+ v = [0] * self.dim
212
+ for i, ch in enumerate(s.encode("utf-8")):
213
+ v[i % self.dim] = (v[i % self.dim] + ch) % 1000
214
+ # L2 normalize
215
+ norm = math.sqrt(sum(x * x for x in v)) or 1.0
216
+ return [x / norm for x in v]
217
+
218
+ async def embed(self, texts: list[str]) -> list[list[float]]:
219
+ return [self._hash(t) for t in texts]
220
+
221
+
222
+ class ResilientEmbeddingClient(EmbeddingClient):
223
+ """
224
+ Wrapper that falls back to hash embeddings if the primary provider fails.
225
+ """
226
+
227
+ def __init__(
228
+ self, primary: EmbeddingClient, fallback: EmbeddingClient | None = None
229
+ ):
230
+ self.primary = primary
231
+ self.fallback = fallback or HashingEmbeddingClient()
232
+
233
+ @staticmethod
234
+ def _sanitize_error(exc: Exception) -> str:
235
+ msg = str(exc)
236
+ msg = re.sub(r"(key=)[^&\s]+", r"\1***", msg)
237
+ msg = re.sub(r"(Bearer\s+)[A-Za-z0-9._-]+", r"\1***", msg)
238
+ return msg
239
+
240
+ async def embed(self, texts: list[str]) -> list[list[float]]:
241
+ try:
242
+ return await self.primary.embed(texts)
243
+ except Exception as exc:
244
+ safe_err = self._sanitize_error(exc)
245
+ logger.warning(
246
+ "Primary embedding provider failed (%s); using hash fallback.",
247
+ safe_err,
248
+ )
249
+ return await self.fallback.embed(texts)
250
+
251
+
252
+ def _local_embedding_available() -> bool:
253
+ """Check if transformers + torch are installed for local embeddings."""
254
+ try:
255
+ import torch # noqa: F401
256
+ import transformers # noqa: F401
257
+
258
+ return True
259
+ except ImportError:
260
+ return False
261
+
262
+
263
+ def _best_fallback() -> EmbeddingClient:
264
+ """Return LocalEmbeddingClient if torch is available, else HashingEmbeddingClient."""
265
+ if _local_embedding_available():
266
+ return LocalEmbeddingClient()
267
+ return HashingEmbeddingClient()
268
+
269
+
270
+ def get_default_embedding_client() -> EmbeddingClient:
271
+ import os
272
+
273
+ provider = (os.getenv("EMBED_PROVIDER") or "auto").lower()
274
+ openai_key = os.getenv("OPENAI_API_KEY")
275
+ gemini_key = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
276
+ zhipu_key = os.getenv("ZHIPU_API_KEY")
277
+ llm_provider = (os.getenv("LLM_PROVIDER") or "").lower()
278
+ prefer_gemini = llm_provider.startswith("gemini")
279
+
280
+ fallback = _best_fallback()
281
+
282
+ def _openai_client() -> EmbeddingClient:
283
+ model = os.getenv("EMBED_MODEL") or "text-embedding-3-small"
284
+ base_url = os.getenv("OPENAI_BASE_URL")
285
+ primary = OpenAIEmbeddingClient(
286
+ api_key=openai_key or "", model=model, base_url=base_url
287
+ )
288
+ return ResilientEmbeddingClient(primary=primary, fallback=fallback)
289
+
290
+ def _gemini_client() -> EmbeddingClient:
291
+ model = os.getenv("EMBED_MODEL") or "models/gemini-embedding-001"
292
+ base_url = os.getenv("GEMINI_BASE_URL")
293
+ primary = GeminiEmbeddingClient(
294
+ api_key=gemini_key or "", model=model, base_url=base_url
295
+ )
296
+ return ResilientEmbeddingClient(primary=primary, fallback=fallback)
297
+
298
+ def _zhipu_client() -> EmbeddingClient:
299
+ model = os.getenv("EMBED_MODEL") or "embedding-3"
300
+ primary = ZhiPuEmbeddingClient(api_key=zhipu_key or "", model=model)
301
+ return ResilientEmbeddingClient(primary=primary, fallback=fallback)
302
+
303
+ # When using Claude/Anthropic as LLM, prefer local embeddings (free, fast, offline)
304
+ # Only use API embeddings if explicitly configured via EMBED_PROVIDER
305
+ prefer_local = llm_provider in ("anthropic", "claude", "")
306
+
307
+ if provider == "auto":
308
+ # Prefer local model when using Claude — no need for OpenAI key
309
+ if prefer_local and _local_embedding_available():
310
+ logger.info("Using local embeddings (GTE-small, 384-dim)")
311
+ return ResilientEmbeddingClient(primary=LocalEmbeddingClient(), fallback=fallback)
312
+ # Follow the active LLM family, then fall back by availability
313
+ if prefer_gemini and gemini_key:
314
+ return _gemini_client()
315
+ if openai_key:
316
+ return _openai_client()
317
+ if gemini_key:
318
+ return _gemini_client()
319
+ if zhipu_key:
320
+ return _zhipu_client()
321
+
322
+ if provider == "openai" and openai_key:
323
+ return _openai_client()
324
+
325
+ if provider in {"gemini", "google"} and gemini_key:
326
+ return _gemini_client()
327
+
328
+ if provider == "zhipu" and zhipu_key:
329
+ return _zhipu_client()
330
+
331
+ if provider == "local" or provider == "auto":
332
+ return fallback
333
+
334
+ # Final fallback — always prefer local over hash
335
+ return fallback
336
+
337
+
338
+ class InMemoryVectorIndex:
339
+ """
340
+ In-memory vector index with numpy-optimized search.
341
+ Uses vectorized matrix operations for fast cosine similarity.
342
+ """
343
+
344
+ def __init__(self):
345
+ self._ids: list[str] = []
346
+ self._metas: list[dict[str, Any]] = []
347
+ self._vectors: Any | None = None # numpy array, lazy init
348
+ self._dim: int | None = None
349
+
350
+ def add(
351
+ self, item_id: str, vector: list[float], meta: dict[str, Any] | None = None
352
+ ):
353
+ try:
354
+ import numpy as np
355
+
356
+ vec = np.array(vector, dtype=np.float32)
357
+
358
+ # Normalize the vector for cosine similarity
359
+ norm = np.linalg.norm(vec)
360
+ if norm > 1e-12:
361
+ vec = vec / norm
362
+
363
+ self._ids.append(item_id)
364
+ self._metas.append(meta or {})
365
+
366
+ if self._vectors is None:
367
+ self._dim = len(vector)
368
+ self._vectors = vec.reshape(1, -1)
369
+ else:
370
+ self._vectors = np.vstack([self._vectors, vec.reshape(1, -1)])
371
+ except ImportError:
372
+ # Fallback: store as list if numpy not available
373
+ if not hasattr(self, "_items"):
374
+ self._items = []
375
+ self._items.append((item_id, vector, meta or {}))
376
+
377
+ def search(
378
+ self, query_vec: list[float], top_k: int = 5
379
+ ) -> list[tuple[str, float, dict[str, Any]]]:
380
+ if not self._ids:
381
+ return []
382
+
383
+ try:
384
+ import numpy as np
385
+
386
+ # Normalize query vector
387
+ query = np.array(query_vec, dtype=np.float32)
388
+ norm = np.linalg.norm(query)
389
+ if norm > 1e-12:
390
+ query = query / norm
391
+
392
+ # Vectorized cosine similarity: matrix @ vector
393
+ # Since vectors are normalized, dot product = cosine similarity
394
+ scores = self._vectors @ query # Shape: (n_items,)
395
+
396
+ # Get top-k indices
397
+ if len(scores) <= top_k:
398
+ top_indices = np.argsort(scores)[::-1]
399
+ else:
400
+ # Use argpartition for efficiency when k << n
401
+ top_indices = np.argpartition(scores, -top_k)[-top_k:]
402
+ top_indices = top_indices[np.argsort(scores[top_indices])[::-1]]
403
+
404
+ return [
405
+ (self._ids[i], float(scores[i]), self._metas[i]) for i in top_indices
406
+ ]
407
+
408
+ except ImportError:
409
+ # Fallback to pure Python if numpy not available
410
+ return self._search_fallback(query_vec, top_k)
411
+
412
+ def _search_fallback(
413
+ self, query_vec: list[float], top_k: int
414
+ ) -> list[tuple[str, float, dict[str, Any]]]:
415
+ """Pure Python fallback for when numpy is not available."""
416
+
417
+ def cosine(a: list[float], b: list[float]) -> float:
418
+ s = sum(x * y for x, y in zip(a, b, strict=False))
419
+ na = math.sqrt(sum(x * x for x in a)) or 1.0
420
+ nb = math.sqrt(sum(y * y for y in b)) or 1.0
421
+ return s / (na * nb)
422
+
423
+ # Check both storage paths: _items (list fallback) and _ids+_metas (numpy path)
424
+ if hasattr(self, "_items") and self._items:
425
+ scored = [
426
+ (iid, cosine(vec, query_vec), meta) for (iid, vec, meta) in self._items
427
+ ]
428
+ scored.sort(key=lambda x: x[1], reverse=True)
429
+ return scored[:top_k]
430
+ # Fallback: reconstruct from numpy storage if _items not populated
431
+ if self._ids and self._vectors is not None:
432
+ try:
433
+ vecs_list = self._vectors.tolist() if hasattr(self._vectors, 'tolist') else []
434
+ scored = [
435
+ (self._ids[i], cosine(vecs_list[i], query_vec), self._metas[i])
436
+ for i in range(len(self._ids))
437
+ ]
438
+ scored.sort(key=lambda x: x[1], reverse=True)
439
+ return scored[:top_k]
440
+ except (IndexError, TypeError):
441
+ pass
442
+ return []
@@ -0,0 +1,247 @@
1
+ """
2
+ Prometheus Structured Error Hierarchy.
3
+
4
+ Every error in the system inherits from PrometheusError, which carries:
5
+ - error_code: machine-readable string (e.g. "TOOL_EXEC_FAILED")
6
+ - context: dict of debug info (tool name, args, trace_id, etc.)
7
+ - recoverable: whether the caller should retry or bail
8
+
9
+ Usage:
10
+ try:
11
+ await executor.execute("shell_exec", {"command": "rm -rf /"})
12
+ except ToolExecutionError as e:
13
+ logger.error(f"[{e.error_code}] {e}", extra=e.context)
14
+ if e.recoverable:
15
+ await retry(...)
16
+ """
17
+ from __future__ import annotations
18
+
19
+ from typing import Any, Optional
20
+
21
+
22
+ class PrometheusError(Exception):
23
+ """
24
+ Base exception for all Prometheus errors.
25
+
26
+ Attributes:
27
+ error_code: Machine-readable error identifier.
28
+ context: Structured debug information.
29
+ recoverable: Hint to callers whether retry might help.
30
+ """
31
+
32
+ error_code: str = "PREDACORE_ERROR"
33
+ recoverable: bool = False
34
+
35
+ def __init__(
36
+ self,
37
+ message: str = "",
38
+ *,
39
+ error_code: str | None = None,
40
+ context: dict[str, Any] | None = None,
41
+ recoverable: bool | None = None,
42
+ ):
43
+ super().__init__(message)
44
+ if error_code is not None:
45
+ self.error_code = error_code
46
+ self.context: dict[str, Any] = context or {}
47
+ if recoverable is not None:
48
+ self.recoverable = recoverable
49
+
50
+ def __str__(self) -> str:
51
+ base = super().__str__()
52
+ if self.context:
53
+ ctx = ", ".join(f"{k}={v!r}" for k, v in self.context.items())
54
+ return f"[{self.error_code}] {base} ({ctx})"
55
+ return f"[{self.error_code}] {base}"
56
+
57
+
58
+ # ── Tool Errors ──────────────────────────────────────────────────────
59
+
60
+
61
+ class ToolExecutionError(PrometheusError):
62
+ """A tool call failed during execution."""
63
+
64
+ error_code = "TOOL_EXEC_FAILED"
65
+ recoverable = True
66
+
67
+
68
+ class ToolNotFoundError(PrometheusError):
69
+ """Requested tool does not exist."""
70
+
71
+ error_code = "TOOL_NOT_FOUND"
72
+ recoverable = False
73
+
74
+
75
+ class ToolPermissionError(PrometheusError):
76
+ """Tool blocked by trust policy or EGM."""
77
+
78
+ error_code = "TOOL_PERMISSION_DENIED"
79
+ recoverable = False
80
+
81
+
82
+ class ToolTimeoutError(ToolExecutionError):
83
+ """Tool execution exceeded time limit."""
84
+
85
+ error_code = "TOOL_TIMEOUT"
86
+ recoverable = True
87
+
88
+
89
+ # ── LLM Provider Errors ─────────────────────────────────────────────
90
+
91
+
92
+ class LLMProviderError(PrometheusError):
93
+ """An LLM API call failed."""
94
+
95
+ error_code = "LLM_PROVIDER_ERROR"
96
+ recoverable = True
97
+
98
+
99
+ class LLMRateLimitError(LLMProviderError):
100
+ """Provider rate limit hit — retry with backoff."""
101
+
102
+ error_code = "LLM_RATE_LIMIT"
103
+ recoverable = True
104
+
105
+
106
+ class LLMContextLengthError(LLMProviderError):
107
+ """Message exceeds provider context window."""
108
+
109
+ error_code = "LLM_CONTEXT_TOO_LONG"
110
+ recoverable = False
111
+
112
+
113
+ class LLMAllProvidersFailedError(LLMProviderError):
114
+ """Every provider in the failover chain has failed."""
115
+
116
+ error_code = "LLM_ALL_PROVIDERS_FAILED"
117
+ recoverable = False
118
+
119
+
120
+ # ── Auth & Security Errors ───────────────────────────────────────────
121
+
122
+
123
+ class AuthenticationError(PrometheusError):
124
+ """Authentication failed — bad token, expired, etc."""
125
+
126
+ error_code = "AUTH_FAILED"
127
+ recoverable = False
128
+
129
+
130
+ class AuthorizationError(PrometheusError):
131
+ """User lacks permission for this action."""
132
+
133
+ error_code = "AUTH_FORBIDDEN"
134
+ recoverable = False
135
+
136
+
137
+ # ── Sandbox Errors ───────────────────────────────────────────────────
138
+
139
+
140
+ class SandboxError(PrometheusError):
141
+ """Docker sandbox operation failed."""
142
+
143
+ error_code = "SANDBOX_ERROR"
144
+ recoverable = True
145
+
146
+
147
+ class SandboxNotAvailableError(SandboxError):
148
+ """Docker daemon not running or sandbox image not built."""
149
+
150
+ error_code = "SANDBOX_NOT_AVAILABLE"
151
+ recoverable = False
152
+
153
+
154
+ class SandboxTimeoutError(SandboxError):
155
+ """Sandbox execution exceeded time limit."""
156
+
157
+ error_code = "SANDBOX_TIMEOUT"
158
+ recoverable = True
159
+
160
+
161
+ # ── Memory & Persistence Errors ──────────────────────────────────────
162
+
163
+
164
+ class MemoryServiceError(PrometheusError):
165
+ """Memory storage or retrieval failed."""
166
+
167
+ error_code = "MEMORY_ERROR"
168
+ recoverable = True
169
+
170
+
171
+ class MemoryNotFoundError(MemoryServiceError):
172
+ """Requested memory ID does not exist."""
173
+
174
+ error_code = "MEMORY_NOT_FOUND"
175
+ recoverable = False
176
+
177
+
178
+ class PersistenceError(PrometheusError):
179
+ """Database or file I/O failure."""
180
+
181
+ error_code = "PERSISTENCE_ERROR"
182
+ recoverable = True
183
+
184
+
185
+ # ── Channel Errors ───────────────────────────────────────────────────
186
+
187
+
188
+ class ChannelError(PrometheusError):
189
+ """A messaging channel encountered an error."""
190
+
191
+ error_code = "CHANNEL_ERROR"
192
+ recoverable = True
193
+
194
+
195
+ class ChannelConnectionError(ChannelError):
196
+ """Failed to connect to a messaging platform."""
197
+
198
+ error_code = "CHANNEL_CONNECT_FAILED"
199
+ recoverable = True
200
+
201
+
202
+ class ChannelRateLimitError(ChannelError):
203
+ """Platform rate limit exceeded."""
204
+
205
+ error_code = "CHANNEL_RATE_LIMIT"
206
+ recoverable = True
207
+
208
+
209
+ class ChannelMessageTooLongError(ChannelError):
210
+ """Message exceeds platform character limit."""
211
+
212
+ error_code = "CHANNEL_MSG_TOO_LONG"
213
+ recoverable = False
214
+
215
+
216
+ # ── Planning & Agent Errors ──────────────────────────────────────────
217
+
218
+
219
+ class PlanningError(PrometheusError):
220
+ """Plan generation failed."""
221
+
222
+ error_code = "PLANNING_ERROR"
223
+ recoverable = True
224
+
225
+
226
+ class AgentLifecycleError(PrometheusError):
227
+ """Agent spawn/terminate/collaborate failure."""
228
+
229
+ error_code = "AGENT_LIFECYCLE_ERROR"
230
+ recoverable = True
231
+
232
+
233
+ class EGMViolationError(PrometheusError):
234
+ """Action blocked by Ethical Governance Module."""
235
+
236
+ error_code = "EGM_VIOLATION"
237
+ recoverable = False
238
+
239
+
240
+ # ── Configuration Errors ─────────────────────────────────────────────
241
+
242
+
243
+ class ConfigurationError(PrometheusError):
244
+ """Invalid or missing configuration."""
245
+
246
+ error_code = "CONFIG_ERROR"
247
+ recoverable = False