differix 0.1.0a1__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.
- agents/editor.py +455 -0
- agents/researcher.py +405 -0
- agents/reviewer.py +374 -0
- cli/model_cli.py +430 -0
- config.py +557 -0
- differix-0.1.0a1.dist-info/METADATA +123 -0
- differix-0.1.0a1.dist-info/RECORD +62 -0
- differix-0.1.0a1.dist-info/WHEEL +5 -0
- differix-0.1.0a1.dist-info/entry_points.txt +2 -0
- differix-0.1.0a1.dist-info/top_level.txt +6 -0
- main.py +2796 -0
- memory/__init__.py +59 -0
- memory/embeddings/__init__.py +29 -0
- memory/embeddings/factory.py +178 -0
- memory/embeddings/providers.py +259 -0
- memory/manager.py +186 -0
- memory/migrator.py +253 -0
- memory/ranker.py +203 -0
- memory/schemas.py +127 -0
- memory/storage.py +552 -0
- tools/agents_chat.py +152 -0
- tools/asset_extractor.py +545 -0
- tools/asset_manager.py +115 -0
- tools/atomic_changeset.py +264 -0
- tools/backup_manager.py +122 -0
- tools/changelog_engine.py +261 -0
- tools/coherence_checker.py +771 -0
- tools/compiler.py +531 -0
- tools/component_normalizer.py +130 -0
- tools/context_assembler.py +631 -0
- tools/contract_validator.py +478 -0
- tools/cost_predictor.py +141 -0
- tools/drift_guard.py +196 -0
- tools/file_manager.py +119 -0
- tools/handshake.py +351 -0
- tools/lesson_manager.py +570 -0
- tools/lesson_memory_bridge.py +398 -0
- tools/llm_client.py +103 -0
- tools/llm_selector.py +362 -0
- tools/model_ranker.py +535 -0
- tools/palette_extractor.py +1043 -0
- tools/plugin_manager.py +204 -0
- tools/project_context_gatherer.py +404 -0
- tools/project_scanner.py +41 -0
- tools/project_structure.py +244 -0
- tools/reasoning_logger.py +78 -0
- tools/reflection.py +452 -0
- tools/sandbox_utils.py +56 -0
- tools/security/__init__.py +0 -0
- tools/security/behavior_guard.py +83 -0
- tools/security/verification_firewall.py +113 -0
- tools/security_filter.py +213 -0
- tools/session_adr.py +311 -0
- tools/skill_manager.py +129 -0
- tools/state_manager.py +383 -0
- tools/structure_index.py +570 -0
- tools/test_schema_consistency.py +19 -0
- tools/verification_firewall.py +113 -0
- tools/visual_capture.py +65 -0
- tools/visual_diff.py +220 -0
- tools/visual_reviewer.py +79 -0
- tools/web_search.py +371 -0
memory/__init__.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
VegeX memory subsystem — public API.
|
|
3
|
+
|
|
4
|
+
Lesson nodes are MemoryNodes. Ranking is A-Score (blueprint §3).
|
|
5
|
+
SQLite is the runtime ranking index; lessons/*.json is the source of truth.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from memory.manager import MemoryManager
|
|
9
|
+
from memory.ranker import (
|
|
10
|
+
activation_score,
|
|
11
|
+
cosine_similarity,
|
|
12
|
+
rank_memory_nodes,
|
|
13
|
+
rank_memory_nodes_scored,
|
|
14
|
+
)
|
|
15
|
+
from memory.schemas import (
|
|
16
|
+
DimensionMismatchError,
|
|
17
|
+
MemoryNode,
|
|
18
|
+
MemoryNotFoundError,
|
|
19
|
+
MemoryScope,
|
|
20
|
+
MemoryStorageError,
|
|
21
|
+
MemoryValidationError,
|
|
22
|
+
RankingWeights,
|
|
23
|
+
ScoredMemoryNode,
|
|
24
|
+
)
|
|
25
|
+
from memory.storage import SQLiteMemoryStorage
|
|
26
|
+
from memory.migrator import check_and_migrate, MigrationReport
|
|
27
|
+
from memory.embeddings import (
|
|
28
|
+
BaseEmbeddingProvider,
|
|
29
|
+
HashEmbeddingProvider,
|
|
30
|
+
LocalEmbeddingProvider,
|
|
31
|
+
OpenRouterEmbeddingProvider,
|
|
32
|
+
build_embedding_provider,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
__version__ = "0.4.0"
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"MemoryManager",
|
|
39
|
+
"SQLiteMemoryStorage",
|
|
40
|
+
"MemoryNode",
|
|
41
|
+
"MemoryScope",
|
|
42
|
+
"RankingWeights",
|
|
43
|
+
"ScoredMemoryNode",
|
|
44
|
+
"activation_score",
|
|
45
|
+
"cosine_similarity",
|
|
46
|
+
"rank_memory_nodes",
|
|
47
|
+
"rank_memory_nodes_scored",
|
|
48
|
+
"MemoryStorageError",
|
|
49
|
+
"DimensionMismatchError",
|
|
50
|
+
"MemoryNotFoundError",
|
|
51
|
+
"MemoryValidationError",
|
|
52
|
+
"BaseEmbeddingProvider",
|
|
53
|
+
"HashEmbeddingProvider",
|
|
54
|
+
"LocalEmbeddingProvider",
|
|
55
|
+
"OpenRouterEmbeddingProvider",
|
|
56
|
+
"build_embedding_provider",
|
|
57
|
+
"check_and_migrate",
|
|
58
|
+
"MigrationReport",
|
|
59
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Embedding provider package for the VegeX memory subsystem.
|
|
3
|
+
|
|
4
|
+
Providers implement a minimal interface:
|
|
5
|
+
.dim -> int
|
|
6
|
+
.signature -> (name, model, dim)
|
|
7
|
+
.embed(text) -> list[float]
|
|
8
|
+
.embed_batch(texts) -> list[list[float]]
|
|
9
|
+
|
|
10
|
+
Use `build_embedding_provider(config)` to select one. Never call provider
|
|
11
|
+
classes directly from storage or ranking code — those layers only care
|
|
12
|
+
about vector length.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from memory.embeddings.providers import (
|
|
16
|
+
BaseEmbeddingProvider,
|
|
17
|
+
HashEmbeddingProvider,
|
|
18
|
+
LocalEmbeddingProvider,
|
|
19
|
+
OpenRouterEmbeddingProvider,
|
|
20
|
+
)
|
|
21
|
+
from memory.embeddings.factory import build_embedding_provider
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"BaseEmbeddingProvider",
|
|
25
|
+
"HashEmbeddingProvider",
|
|
26
|
+
"LocalEmbeddingProvider",
|
|
27
|
+
"OpenRouterEmbeddingProvider",
|
|
28
|
+
"build_embedding_provider",
|
|
29
|
+
]
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Provider selection with health probing.
|
|
3
|
+
|
|
4
|
+
Priority order for `auto`:
|
|
5
|
+
1. OpenRouter (if OPENROUTER_API_KEY is set and prefer_local is False)
|
|
6
|
+
2. Local (fastembed, if installed)
|
|
7
|
+
3. Hash (zero-dependency fallback)
|
|
8
|
+
|
|
9
|
+
Every candidate is probed with a cheap embed call before being accepted.
|
|
10
|
+
A broken or placeholder key never gets past the probe.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
from memory.embeddings.providers import (
|
|
19
|
+
BaseEmbeddingProvider,
|
|
20
|
+
HashEmbeddingProvider,
|
|
21
|
+
LocalEmbeddingProvider,
|
|
22
|
+
OpenRouterEmbeddingProvider,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
_PROBE_STRING = "vegex embedding probe"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _probe(provider: BaseEmbeddingProvider) -> bool:
|
|
31
|
+
"""
|
|
32
|
+
Return True iff `provider.embed()` succeeds on a trivial input AND
|
|
33
|
+
returns a vector of the expected dimension.
|
|
34
|
+
"""
|
|
35
|
+
try:
|
|
36
|
+
vec = provider.embed(_PROBE_STRING)
|
|
37
|
+
except Exception as exc:
|
|
38
|
+
logger.warning(
|
|
39
|
+
"Provider %s failed probe: %s",
|
|
40
|
+
getattr(provider, "signature", "unknown"),
|
|
41
|
+
exc,
|
|
42
|
+
)
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
if not isinstance(vec, list) or len(vec) != provider.dim:
|
|
46
|
+
logger.warning(
|
|
47
|
+
"Provider %s returned vector of unexpected shape: "
|
|
48
|
+
"expected %d, got %s",
|
|
49
|
+
getattr(provider, "signature", "unknown"),
|
|
50
|
+
provider.dim,
|
|
51
|
+
len(vec) if isinstance(vec, list) else type(vec).__name__,
|
|
52
|
+
)
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def build_embedding_provider(
|
|
59
|
+
config,
|
|
60
|
+
prefer_local: Optional[bool] = None,
|
|
61
|
+
force_provider: Optional[str] = None,
|
|
62
|
+
skip_probe: bool = False,
|
|
63
|
+
) -> BaseEmbeddingProvider:
|
|
64
|
+
"""
|
|
65
|
+
Build and probe the embedding provider for this process.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
config: the `config` module (or any object with the same attributes).
|
|
69
|
+
prefer_local: override config.EMBEDDING_PREFER_LOCAL.
|
|
70
|
+
force_provider: "openrouter" | "local" | "hash" — raises if
|
|
71
|
+
unavailable.
|
|
72
|
+
skip_probe: bypass the health check (testing only).
|
|
73
|
+
"""
|
|
74
|
+
provider_choice = (
|
|
75
|
+
force_provider
|
|
76
|
+
or getattr(config, "EMBEDDING_PROVIDER", "auto")
|
|
77
|
+
).lower()
|
|
78
|
+
|
|
79
|
+
if prefer_local is None:
|
|
80
|
+
prefer_local = bool(getattr(config, "EMBEDDING_PREFER_LOCAL", False))
|
|
81
|
+
|
|
82
|
+
dim = int(getattr(config, "EMBEDDING_DIM", 512))
|
|
83
|
+
api_key = getattr(config, "OPENROUTER_API_KEY", None)
|
|
84
|
+
model = getattr(
|
|
85
|
+
config, "OPENROUTER_EMBEDDING_MODEL", "openai/text-embedding-3-small"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# ------------------------------------------------------------------ #
|
|
89
|
+
# Explicit provider
|
|
90
|
+
# ------------------------------------------------------------------ #
|
|
91
|
+
if provider_choice == "hash":
|
|
92
|
+
provider = HashEmbeddingProvider(dim=dim)
|
|
93
|
+
if not skip_probe and not _probe(provider):
|
|
94
|
+
raise RuntimeError("HashEmbeddingProvider failed its own probe.")
|
|
95
|
+
logger.info("Embedding provider forced: hash (%s)", provider.signature)
|
|
96
|
+
return provider
|
|
97
|
+
|
|
98
|
+
if provider_choice == "local":
|
|
99
|
+
try:
|
|
100
|
+
provider = LocalEmbeddingProvider()
|
|
101
|
+
except Exception as exc:
|
|
102
|
+
raise RuntimeError(
|
|
103
|
+
f"provider='local' requested but LocalEmbeddingProvider "
|
|
104
|
+
f"could not be constructed: {exc}. "
|
|
105
|
+
f"Install with: pip install '.[embeddings]'"
|
|
106
|
+
) from exc
|
|
107
|
+
if not skip_probe and not _probe(provider):
|
|
108
|
+
raise RuntimeError(
|
|
109
|
+
"provider='local' requested but the probe failed. "
|
|
110
|
+
"Check fastembed installation and model download access."
|
|
111
|
+
)
|
|
112
|
+
logger.info("Embedding provider forced: local (%s)", provider.signature)
|
|
113
|
+
return provider
|
|
114
|
+
|
|
115
|
+
if provider_choice in ("openrouter", "openai"):
|
|
116
|
+
if not api_key:
|
|
117
|
+
raise RuntimeError(
|
|
118
|
+
f"provider='{provider_choice}' requested but "
|
|
119
|
+
f"OPENROUTER_API_KEY is not set."
|
|
120
|
+
)
|
|
121
|
+
provider = OpenRouterEmbeddingProvider(
|
|
122
|
+
api_key=api_key, model=model, dim=dim
|
|
123
|
+
)
|
|
124
|
+
if not skip_probe and not _probe(provider):
|
|
125
|
+
raise RuntimeError(
|
|
126
|
+
f"provider='{provider_choice}' requested but the probe "
|
|
127
|
+
f"failed. Verify OPENROUTER_API_KEY is valid at "
|
|
128
|
+
f"https://openrouter.ai/keys"
|
|
129
|
+
)
|
|
130
|
+
logger.info("Embedding provider forced: openrouter (%s)", provider.signature)
|
|
131
|
+
return provider
|
|
132
|
+
|
|
133
|
+
# ------------------------------------------------------------------ #
|
|
134
|
+
# auto — probe each tier in priority order
|
|
135
|
+
# ------------------------------------------------------------------ #
|
|
136
|
+
if api_key and not prefer_local:
|
|
137
|
+
try:
|
|
138
|
+
provider = OpenRouterEmbeddingProvider(
|
|
139
|
+
api_key=api_key, model=model, dim=dim
|
|
140
|
+
)
|
|
141
|
+
if skip_probe or _probe(provider):
|
|
142
|
+
logger.info(
|
|
143
|
+
"Embedding provider (auto): openrouter (%s)",
|
|
144
|
+
provider.signature,
|
|
145
|
+
)
|
|
146
|
+
return provider
|
|
147
|
+
logger.warning(
|
|
148
|
+
"OpenRouter provider failed probe — falling back. "
|
|
149
|
+
"Check that OPENROUTER_API_KEY is valid and that your "
|
|
150
|
+
"account has access to openai/text-embedding-3-small."
|
|
151
|
+
)
|
|
152
|
+
except Exception as exc:
|
|
153
|
+
logger.warning("OpenRouter provider init failed: %s", exc)
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
provider = LocalEmbeddingProvider()
|
|
157
|
+
if skip_probe or _probe(provider):
|
|
158
|
+
logger.info(
|
|
159
|
+
"Embedding provider (auto): local (%s)", provider.signature
|
|
160
|
+
)
|
|
161
|
+
return provider
|
|
162
|
+
logger.warning("Local provider failed probe.")
|
|
163
|
+
except Exception as exc:
|
|
164
|
+
logger.debug("Local provider unavailable: %s", exc)
|
|
165
|
+
|
|
166
|
+
logger.warning(
|
|
167
|
+
"No working semantic embedding provider available. "
|
|
168
|
+
"Falling back to HashEmbeddingProvider (dim=%d). "
|
|
169
|
+
"Retrieval quality will be limited.",
|
|
170
|
+
dim,
|
|
171
|
+
)
|
|
172
|
+
fallback = HashEmbeddingProvider(dim=dim)
|
|
173
|
+
if not skip_probe and not _probe(fallback):
|
|
174
|
+
raise RuntimeError("HashEmbeddingProvider failed its own probe.")
|
|
175
|
+
return fallback
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
__all__ = ["build_embedding_provider"]
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Concrete embedding providers.
|
|
3
|
+
|
|
4
|
+
Providers are stateless from the caller's perspective. The `signature`
|
|
5
|
+
attribute is the identity used by the migrator to detect provider changes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import logging
|
|
12
|
+
import math
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
from abc import ABC, abstractmethod
|
|
16
|
+
from typing import List, Optional, Sequence, Tuple
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Base
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
class BaseEmbeddingProvider(ABC):
|
|
25
|
+
"""Interface every provider must satisfy."""
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def dim(self) -> int:
|
|
30
|
+
"""Length of vectors this provider produces."""
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def signature(self) -> Tuple[str, str, int]:
|
|
35
|
+
"""
|
|
36
|
+
Stable identity for migration comparison.
|
|
37
|
+
Return (provider_name, model_name, dim).
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def embed(self, text: str) -> List[float]:
|
|
42
|
+
"""Produce a single vector."""
|
|
43
|
+
|
|
44
|
+
def embed_batch(self, texts: Sequence[str]) -> List[List[float]]:
|
|
45
|
+
"""Default: sequential. Override for API providers."""
|
|
46
|
+
return [self.embed(t) for t in texts]
|
|
47
|
+
|
|
48
|
+
def is_available(self) -> bool:
|
|
49
|
+
"""Whether this provider can currently produce embeddings."""
|
|
50
|
+
return True
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# Hash — zero-dependency fallback
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
class HashEmbeddingProvider(BaseEmbeddingProvider):
|
|
57
|
+
"""
|
|
58
|
+
Deterministic bag-of-words hashing.
|
|
59
|
+
|
|
60
|
+
NOT semantically deep. Correct for distinguishing lessons that share
|
|
61
|
+
surface vocabulary; useless for synonymy or paraphrase. This is the
|
|
62
|
+
emergency fallback — if it is your active provider, retrieval quality
|
|
63
|
+
will be mediocre.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(self, dim: int = 256) -> None:
|
|
67
|
+
if dim <= 0:
|
|
68
|
+
raise ValueError(f"Embedding dim must be > 0, got {dim}")
|
|
69
|
+
self._dim = dim
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def dim(self) -> int:
|
|
73
|
+
return self._dim
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def signature(self) -> Tuple[str, str, int]:
|
|
77
|
+
return ("hash", f"bow-sha256-{self._dim}", self._dim)
|
|
78
|
+
|
|
79
|
+
def is_available(self) -> bool:
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
def embed(self, text: str) -> List[float]:
|
|
83
|
+
vec = [0.0] * self._dim
|
|
84
|
+
tokens = re.findall(r"[a-z0-9_]+", (text or "").lower())
|
|
85
|
+
if not tokens:
|
|
86
|
+
return vec
|
|
87
|
+
for tok in tokens:
|
|
88
|
+
digest = hashlib.sha256(tok.encode("utf-8")).digest()
|
|
89
|
+
bucket = int.from_bytes(digest[:4], "big") % self._dim
|
|
90
|
+
vec[bucket] += 1.0
|
|
91
|
+
norm = math.sqrt(sum(v * v for v in vec))
|
|
92
|
+
if norm > 0.0:
|
|
93
|
+
vec = [v / norm for v in vec]
|
|
94
|
+
return vec
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
# OpenRouter — primary provider (uses existing OPENROUTER_API_KEY)
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
class OpenRouterEmbeddingProvider(BaseEmbeddingProvider):
|
|
101
|
+
"""
|
|
102
|
+
Uses OpenRouter's /v1/embeddings endpoint with the existing OpenRouter
|
|
103
|
+
API key. This is the same key used for chat completions, so no new
|
|
104
|
+
secret is required.
|
|
105
|
+
|
|
106
|
+
Endpoint: https://openrouter.ai/api/v1/embeddings
|
|
107
|
+
Model ID: provider-prefixed, e.g. "openai/text-embedding-3-small"
|
|
108
|
+
|
|
109
|
+
Batching: OpenRouter's embeddings endpoint accepts multiple inputs per
|
|
110
|
+
request. 128 is a safe practical default.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
BATCH_SIZE = 128
|
|
114
|
+
|
|
115
|
+
def __init__(
|
|
116
|
+
self,
|
|
117
|
+
api_key: Optional[str] = None,
|
|
118
|
+
model: str = "openai/text-embedding-3-small",
|
|
119
|
+
dim: int = 512,
|
|
120
|
+
base_url: str = "https://openrouter.ai/api/v1",
|
|
121
|
+
timeout: float = 60.0,
|
|
122
|
+
) -> None:
|
|
123
|
+
self._api_key = api_key or os.getenv("OPENROUTER_API_KEY")
|
|
124
|
+
if not self._api_key:
|
|
125
|
+
raise ValueError(
|
|
126
|
+
"OpenRouterEmbeddingProvider requires OPENROUTER_API_KEY. "
|
|
127
|
+
"Set it in .env or pass api_key=."
|
|
128
|
+
)
|
|
129
|
+
self._model = model
|
|
130
|
+
self._dim = dim
|
|
131
|
+
self._base_url = base_url
|
|
132
|
+
self._timeout = timeout
|
|
133
|
+
self._client = None # lazy init
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def dim(self) -> int:
|
|
137
|
+
return self._dim
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def signature(self) -> Tuple[str, str, int]:
|
|
141
|
+
return ("openrouter", self._model, self._dim)
|
|
142
|
+
|
|
143
|
+
def is_available(self) -> bool:
|
|
144
|
+
return bool(self._api_key)
|
|
145
|
+
|
|
146
|
+
def _get_client(self):
|
|
147
|
+
if self._client is None:
|
|
148
|
+
try:
|
|
149
|
+
from openai import OpenAI
|
|
150
|
+
except ImportError as exc:
|
|
151
|
+
raise RuntimeError(
|
|
152
|
+
"openai package is required for OpenRouterEmbeddingProvider"
|
|
153
|
+
) from exc
|
|
154
|
+
self._client = OpenAI(
|
|
155
|
+
api_key=self._api_key,
|
|
156
|
+
base_url=self._base_url,
|
|
157
|
+
timeout=self._timeout,
|
|
158
|
+
default_headers={
|
|
159
|
+
# OpenRouter requires these for rate-limit attribution.
|
|
160
|
+
# They can be any string; the values below identify VegeX.
|
|
161
|
+
"HTTP-Referer": "https://github.com/tdahq8/vegex",
|
|
162
|
+
"X-Title": "VegeX",
|
|
163
|
+
},
|
|
164
|
+
)
|
|
165
|
+
return self._client
|
|
166
|
+
|
|
167
|
+
def embed(self, text: str) -> List[float]:
|
|
168
|
+
return self.embed_batch([text])[0]
|
|
169
|
+
|
|
170
|
+
def embed_batch(self, texts: Sequence[str]) -> List[List[float]]:
|
|
171
|
+
if not texts:
|
|
172
|
+
return []
|
|
173
|
+
client = self._get_client()
|
|
174
|
+
results: List[List[float]] = []
|
|
175
|
+
for i in range(0, len(texts), self.BATCH_SIZE):
|
|
176
|
+
chunk = list(texts[i: i + self.BATCH_SIZE])
|
|
177
|
+
try:
|
|
178
|
+
response = client.embeddings.create(
|
|
179
|
+
model=self._model,
|
|
180
|
+
input=chunk,
|
|
181
|
+
dimensions=self._dim,
|
|
182
|
+
)
|
|
183
|
+
ordered = sorted(response.data, key=lambda d: d.index)
|
|
184
|
+
for item in ordered:
|
|
185
|
+
results.append(list(item.embedding))
|
|
186
|
+
except Exception as exc:
|
|
187
|
+
logger.warning(
|
|
188
|
+
"OpenRouter embedding batch %d failed: %s",
|
|
189
|
+
i // self.BATCH_SIZE, exc,
|
|
190
|
+
)
|
|
191
|
+
# Re-raise — the caller decides whether to skip or abort.
|
|
192
|
+
raise
|
|
193
|
+
return results
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# ---------------------------------------------------------------------------
|
|
197
|
+
# Local — fastembed (ONNX) offline provider
|
|
198
|
+
# ---------------------------------------------------------------------------
|
|
199
|
+
class LocalEmbeddingProvider(BaseEmbeddingProvider):
|
|
200
|
+
"""
|
|
201
|
+
Offline provider using fastembed (ONNX runtime, ~50MB model download).
|
|
202
|
+
|
|
203
|
+
Deliberately avoids `sentence-transformers` (which pulls PyTorch,
|
|
204
|
+
~2GB). If fastembed is not installed, `is_available()` returns False
|
|
205
|
+
and construction raises — the factory handles fallback.
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
DEFAULT_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
|
|
209
|
+
DEFAULT_DIM = 384
|
|
210
|
+
|
|
211
|
+
def __init__(self, model: str = DEFAULT_MODEL) -> None:
|
|
212
|
+
try:
|
|
213
|
+
from fastembed import TextEmbedding
|
|
214
|
+
except ImportError as exc:
|
|
215
|
+
raise RuntimeError(
|
|
216
|
+
"fastembed not installed. Install with: pip install '.[embeddings]'"
|
|
217
|
+
) from exc
|
|
218
|
+
self._model_name = model
|
|
219
|
+
self._dim = self.DEFAULT_DIM
|
|
220
|
+
self._text_embedding_cls = TextEmbedding
|
|
221
|
+
self._engine = None # lazy
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def dim(self) -> int:
|
|
225
|
+
return self._dim
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def signature(self) -> Tuple[str, str, int]:
|
|
229
|
+
return ("local", self._model_name, self._dim)
|
|
230
|
+
|
|
231
|
+
def is_available(self) -> bool:
|
|
232
|
+
try:
|
|
233
|
+
from fastembed import TextEmbedding # noqa: F401
|
|
234
|
+
return True
|
|
235
|
+
except ImportError:
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
def _get_engine(self):
|
|
239
|
+
if self._engine is None:
|
|
240
|
+
self._engine = self._text_embedding_cls(model_name=self._model_name)
|
|
241
|
+
return self._engine
|
|
242
|
+
|
|
243
|
+
def embed(self, text: str) -> List[float]:
|
|
244
|
+
return self.embed_batch([text])[0]
|
|
245
|
+
|
|
246
|
+
def embed_batch(self, texts: Sequence[str]) -> List[List[float]]:
|
|
247
|
+
if not texts:
|
|
248
|
+
return []
|
|
249
|
+
engine = self._get_engine()
|
|
250
|
+
vectors = list(engine.embed(list(texts)))
|
|
251
|
+
return [list(map(float, v)) for v in vectors]
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
__all__ = [
|
|
255
|
+
"BaseEmbeddingProvider",
|
|
256
|
+
"HashEmbeddingProvider",
|
|
257
|
+
"OpenRouterEmbeddingProvider",
|
|
258
|
+
"LocalEmbeddingProvider",
|
|
259
|
+
]
|