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.
Files changed (62) hide show
  1. agents/editor.py +455 -0
  2. agents/researcher.py +405 -0
  3. agents/reviewer.py +374 -0
  4. cli/model_cli.py +430 -0
  5. config.py +557 -0
  6. differix-0.1.0a1.dist-info/METADATA +123 -0
  7. differix-0.1.0a1.dist-info/RECORD +62 -0
  8. differix-0.1.0a1.dist-info/WHEEL +5 -0
  9. differix-0.1.0a1.dist-info/entry_points.txt +2 -0
  10. differix-0.1.0a1.dist-info/top_level.txt +6 -0
  11. main.py +2796 -0
  12. memory/__init__.py +59 -0
  13. memory/embeddings/__init__.py +29 -0
  14. memory/embeddings/factory.py +178 -0
  15. memory/embeddings/providers.py +259 -0
  16. memory/manager.py +186 -0
  17. memory/migrator.py +253 -0
  18. memory/ranker.py +203 -0
  19. memory/schemas.py +127 -0
  20. memory/storage.py +552 -0
  21. tools/agents_chat.py +152 -0
  22. tools/asset_extractor.py +545 -0
  23. tools/asset_manager.py +115 -0
  24. tools/atomic_changeset.py +264 -0
  25. tools/backup_manager.py +122 -0
  26. tools/changelog_engine.py +261 -0
  27. tools/coherence_checker.py +771 -0
  28. tools/compiler.py +531 -0
  29. tools/component_normalizer.py +130 -0
  30. tools/context_assembler.py +631 -0
  31. tools/contract_validator.py +478 -0
  32. tools/cost_predictor.py +141 -0
  33. tools/drift_guard.py +196 -0
  34. tools/file_manager.py +119 -0
  35. tools/handshake.py +351 -0
  36. tools/lesson_manager.py +570 -0
  37. tools/lesson_memory_bridge.py +398 -0
  38. tools/llm_client.py +103 -0
  39. tools/llm_selector.py +362 -0
  40. tools/model_ranker.py +535 -0
  41. tools/palette_extractor.py +1043 -0
  42. tools/plugin_manager.py +204 -0
  43. tools/project_context_gatherer.py +404 -0
  44. tools/project_scanner.py +41 -0
  45. tools/project_structure.py +244 -0
  46. tools/reasoning_logger.py +78 -0
  47. tools/reflection.py +452 -0
  48. tools/sandbox_utils.py +56 -0
  49. tools/security/__init__.py +0 -0
  50. tools/security/behavior_guard.py +83 -0
  51. tools/security/verification_firewall.py +113 -0
  52. tools/security_filter.py +213 -0
  53. tools/session_adr.py +311 -0
  54. tools/skill_manager.py +129 -0
  55. tools/state_manager.py +383 -0
  56. tools/structure_index.py +570 -0
  57. tools/test_schema_consistency.py +19 -0
  58. tools/verification_firewall.py +113 -0
  59. tools/visual_capture.py +65 -0
  60. tools/visual_diff.py +220 -0
  61. tools/visual_reviewer.py +79 -0
  62. 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
+ ]