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.
- predacore/__init__.py +12 -0
- predacore/_vendor/__init__.py +0 -0
- predacore/_vendor/common/__init__.py +1 -0
- predacore/_vendor/common/embedding.py +442 -0
- predacore/_vendor/common/errors.py +247 -0
- predacore/_vendor/common/llm.py +592 -0
- predacore/_vendor/common/logging_config.py +213 -0
- predacore/_vendor/common/logging_utils.py +73 -0
- predacore/_vendor/common/memory_service.py +850 -0
- predacore/_vendor/common/metrics.py +246 -0
- predacore/_vendor/common/models.py +309 -0
- predacore/_vendor/common/notifications.py +459 -0
- predacore/_vendor/common/protos/__init__.py +32 -0
- predacore/_vendor/common/protos/csc_pb2.py +55 -0
- predacore/_vendor/common/protos/csc_pb2_grpc.py +192 -0
- predacore/_vendor/common/protos/daf_pb2.py +77 -0
- predacore/_vendor/common/protos/daf_pb2_grpc.py +419 -0
- predacore/_vendor/common/protos/egm_pb2.py +62 -0
- predacore/_vendor/common/protos/egm_pb2_grpc.py +278 -0
- predacore/_vendor/common/protos/knowledge_nexus_pb2.py +64 -0
- predacore/_vendor/common/protos/knowledge_nexus_pb2_grpc.py +324 -0
- predacore/_vendor/common/protos/wil_pb2.py +55 -0
- predacore/_vendor/common/protos/wil_pb2_grpc.py +192 -0
- predacore/_vendor/common/skill_collective.py +587 -0
- predacore/_vendor/common/skill_evolution.py +497 -0
- predacore/_vendor/common/skill_genome.py +391 -0
- predacore/_vendor/common/skill_scanner.py +522 -0
- predacore/_vendor/common/structured_output.py +32 -0
- predacore/_vendor/common/vector_store.py +205 -0
- predacore/_vendor/core_strategic_engine/__init__.py +3 -0
- predacore/_vendor/core_strategic_engine/llm_planner.py +204 -0
- predacore/_vendor/core_strategic_engine/plan_cache.py +81 -0
- predacore/_vendor/core_strategic_engine/planner.py +730 -0
- predacore/_vendor/core_strategic_engine/planner_enhancements.py +368 -0
- predacore/_vendor/core_strategic_engine/planner_mcts.py +873 -0
- predacore/_vendor/ethical_governance_module/__init__.py +3 -0
- predacore/_vendor/ethical_governance_module/audit_logger.py +95 -0
- predacore/_vendor/ethical_governance_module/persistent_audit.py +465 -0
- predacore/_vendor/ethical_governance_module/rule_engine.py +225 -0
- predacore/_vendor/ethical_governance_module/service.py +356 -0
- predacore/_vendor/knowledge_nexus/__init__.py +12 -0
- predacore/_vendor/knowledge_nexus/faiss_vector_index.py +208 -0
- predacore/_vendor/knowledge_nexus/health.py +16 -0
- predacore/_vendor/knowledge_nexus/service.py +531 -0
- predacore/_vendor/knowledge_nexus/storage.py +558 -0
- predacore/_vendor/knowledge_nexus/vector_index.py +42 -0
- predacore/_vendor/user_modeling_engine/__init__.py +5 -0
- predacore/_vendor/user_modeling_engine/service.py +145 -0
- predacore/agents/__init__.py +0 -0
- predacore/agents/autonomy.py +790 -0
- predacore/agents/collaboration.py +402 -0
- predacore/agents/daf/__init__.py +39 -0
- predacore/agents/daf/agent_process.py +629 -0
- predacore/agents/daf/agent_registry.py +320 -0
- predacore/agents/daf/health.py +22 -0
- predacore/agents/daf/health_api.py +21 -0
- predacore/agents/daf/metrics_util.py +30 -0
- predacore/agents/daf/scheduler.py +139 -0
- predacore/agents/daf/self_optimization.py +456 -0
- predacore/agents/daf/service.py +1135 -0
- predacore/agents/daf/task_store.py +115 -0
- predacore/agents/daf_bridge.py +404 -0
- predacore/agents/engine.py +1089 -0
- predacore/agents/meta_cognition.py +381 -0
- predacore/agents/self_improvement.py +481 -0
- predacore/auth/__init__.py +0 -0
- predacore/auth/middleware.py +431 -0
- predacore/auth/sandbox.py +782 -0
- predacore/auth/security.py +423 -0
- predacore/bootstrap.py +471 -0
- predacore/channels/__init__.py +12 -0
- predacore/channels/discord.py +171 -0
- predacore/channels/email.py +338 -0
- predacore/channels/health.py +461 -0
- predacore/channels/imessage.py +269 -0
- predacore/channels/registry.py +220 -0
- predacore/channels/signal.py +219 -0
- predacore/channels/slack.py +239 -0
- predacore/channels/telegram.py +512 -0
- predacore/channels/webchat.py +1100 -0
- predacore/channels/whatsapp.py +242 -0
- predacore/cli.py +2420 -0
- predacore/config.py +871 -0
- predacore/core.py +1959 -0
- predacore/errors.py +208 -0
- predacore/gateway.py +833 -0
- predacore/identity/EVENT_HORIZON.md +236 -0
- predacore/identity/SOUL_SEED.md +92 -0
- predacore/identity/__init__.py +29 -0
- predacore/identity/beliefs.py +444 -0
- predacore/identity/defaults/HEARTBEAT.md +52 -0
- predacore/identity/defaults/IDENTITY.md +56 -0
- predacore/identity/defaults/JOURNAL.md +12 -0
- predacore/identity/defaults/MEMORY.md +21 -0
- predacore/identity/defaults/REFLECTION.md +21 -0
- predacore/identity/defaults/SOUL.md +50 -0
- predacore/identity/defaults/TOOLS.md +28 -0
- predacore/identity/defaults/USER.md +24 -0
- predacore/identity/engine.py +816 -0
- predacore/identity/heartbeat.py +242 -0
- predacore/identity/reflection.py +411 -0
- predacore/llm_providers/__init__.py +20 -0
- predacore/llm_providers/_anthropic_wire.py +291 -0
- predacore/llm_providers/anthropic.py +168 -0
- predacore/llm_providers/base.py +65 -0
- predacore/llm_providers/circuit_breaker.py +98 -0
- predacore/llm_providers/claude_models.py +29 -0
- predacore/llm_providers/gemini.py +296 -0
- predacore/llm_providers/gemini_cli.py +120 -0
- predacore/llm_providers/message_validator.py +263 -0
- predacore/llm_providers/openai.py +392 -0
- predacore/llm_providers/predacore_sdk.py +324 -0
- predacore/llm_providers/router.py +377 -0
- predacore/llm_providers/text_tool_adapter.py +152 -0
- predacore/memory/__init__.py +33 -0
- predacore/memory/consolidator.py +718 -0
- predacore/memory/retriever.py +390 -0
- predacore/memory/store.py +2070 -0
- predacore/operators/__init__.py +98 -0
- predacore/operators/android.py +1873 -0
- predacore/operators/base.py +340 -0
- predacore/operators/browser_bridge.py +2019 -0
- predacore/operators/desktop.py +856 -0
- predacore/operators/enums.py +285 -0
- predacore/operators/mock.py +356 -0
- predacore/operators/native_service.py +1589 -0
- predacore/operators/ocr_fallback.py +413 -0
- predacore/operators/retry.py +151 -0
- predacore/operators/screen_vision.py +948 -0
- predacore/operators/smart_input.py +1227 -0
- predacore/prompts.py +332 -0
- predacore/services/__init__.py +0 -0
- predacore/services/alerting.py +524 -0
- predacore/services/code_index.py +1332 -0
- predacore/services/config_watcher.py +236 -0
- predacore/services/cron.py +415 -0
- predacore/services/daemon.py +710 -0
- predacore/services/db_adapter.py +225 -0
- predacore/services/db_client.py +358 -0
- predacore/services/db_server.py +312 -0
- predacore/services/embedding.py +510 -0
- predacore/services/git_integration.py +675 -0
- predacore/services/identity_service.py +325 -0
- predacore/services/lane_queue.py +380 -0
- predacore/services/mcp_client.py +373 -0
- predacore/services/mcp_registry.py +321 -0
- predacore/services/outcome_store.py +620 -0
- predacore/services/plugins.py +508 -0
- predacore/services/rate_limiter.py +449 -0
- predacore/services/transcripts.py +259 -0
- predacore/services/voice.py +669 -0
- predacore/sessions.py +659 -0
- predacore/tools/__init__.py +14 -0
- predacore/tools/dispatcher.py +585 -0
- predacore/tools/enums.py +197 -0
- predacore/tools/executor.py +295 -0
- predacore/tools/handlers/__init__.py +283 -0
- predacore/tools/handlers/_context.py +290 -0
- predacore/tools/handlers/agent.py +993 -0
- predacore/tools/handlers/apis.py +323 -0
- predacore/tools/handlers/channels.py +481 -0
- predacore/tools/handlers/collective_intelligence.py +249 -0
- predacore/tools/handlers/creative.py +284 -0
- predacore/tools/handlers/cron.py +253 -0
- predacore/tools/handlers/desktop.py +608 -0
- predacore/tools/handlers/file_ops.py +173 -0
- predacore/tools/handlers/git.py +73 -0
- predacore/tools/handlers/identity.py +100 -0
- predacore/tools/handlers/marketplace.py +96 -0
- predacore/tools/handlers/mcp.py +362 -0
- predacore/tools/handlers/memory.py +298 -0
- predacore/tools/handlers/pipeline_handler.py +299 -0
- predacore/tools/handlers/shell.py +323 -0
- predacore/tools/handlers/stats.py +132 -0
- predacore/tools/handlers/voice.py +225 -0
- predacore/tools/handlers/web.py +403 -0
- predacore/tools/health.py +233 -0
- predacore/tools/marketplace.py +684 -0
- predacore/tools/middleware.py +498 -0
- predacore/tools/pipeline.py +841 -0
- predacore/tools/registry.py +2249 -0
- predacore/tools/resilience.py +429 -0
- predacore/tools/skill_marketplace.py +503 -0
- predacore/tools/subsystem_init.py +351 -0
- predacore/tools/trust_policy.py +344 -0
- predacore/utils/__init__.py +1 -0
- predacore/utils/cache.py +121 -0
- predacore-0.1.0.dist-info/METADATA +267 -0
- predacore-0.1.0.dist-info/RECORD +193 -0
- predacore-0.1.0.dist-info/WHEEL +5 -0
- predacore-0.1.0.dist-info/entry_points.txt +12 -0
- predacore-0.1.0.dist-info/licenses/LICENSE +190 -0
- 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
|