extremis 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 (43) hide show
  1. extremis/__init__.py +15 -0
  2. extremis/api.py +277 -0
  3. extremis/client.py +248 -0
  4. extremis/config.py +67 -0
  5. extremis/consolidation/__init__.py +3 -0
  6. extremis/consolidation/consolidator.py +168 -0
  7. extremis/consolidation/prompts.py +41 -0
  8. extremis/embeddings/__init__.py +3 -0
  9. extremis/embeddings/openai.py +55 -0
  10. extremis/embeddings/sentence_transformers.py +34 -0
  11. extremis/interfaces.py +80 -0
  12. extremis/mcp/__init__.py +3 -0
  13. extremis/mcp/server.py +433 -0
  14. extremis/migrate.py +233 -0
  15. extremis/migrations/001_initial.sql +69 -0
  16. extremis/migrations/001_initial_sqlite.sql +91 -0
  17. extremis/observer/__init__.py +3 -0
  18. extremis/observer/observer.py +145 -0
  19. extremis/scorer/__init__.py +3 -0
  20. extremis/scorer/attention.py +198 -0
  21. extremis/server/__init__.py +0 -0
  22. extremis/server/app.py +156 -0
  23. extremis/server/auth.py +95 -0
  24. extremis/server/deps.py +56 -0
  25. extremis/server/routes/__init__.py +0 -0
  26. extremis/server/routes/health.py +37 -0
  27. extremis/server/routes/kg.py +54 -0
  28. extremis/server/routes/memories.py +98 -0
  29. extremis/storage/__init__.py +5 -0
  30. extremis/storage/chroma.py +237 -0
  31. extremis/storage/kg.py +250 -0
  32. extremis/storage/log.py +87 -0
  33. extremis/storage/pinecone_store.py +192 -0
  34. extremis/storage/postgres.py +254 -0
  35. extremis/storage/recall_reason.py +56 -0
  36. extremis/storage/score_index.py +56 -0
  37. extremis/storage/sqlite.py +251 -0
  38. extremis/types.py +145 -0
  39. extremis-0.1.0.dist-info/METADATA +801 -0
  40. extremis-0.1.0.dist-info/RECORD +43 -0
  41. extremis-0.1.0.dist-info/WHEEL +4 -0
  42. extremis-0.1.0.dist-info/entry_points.txt +4 -0
  43. extremis-0.1.0.dist-info/licenses/LICENSE +21 -0
extremis/__init__.py ADDED
@@ -0,0 +1,15 @@
1
+ from .api import Extremis
2
+ from .client import HostedClient
3
+ from .config import Config
4
+ from .types import FeedbackSignal, LogEntry, Memory, MemoryLayer, RecallResult
5
+
6
+ __all__ = [
7
+ "Extremis",
8
+ "HostedClient",
9
+ "Config",
10
+ "Memory",
11
+ "MemoryLayer",
12
+ "LogEntry",
13
+ "RecallResult",
14
+ "FeedbackSignal",
15
+ ]
extremis/api.py ADDED
@@ -0,0 +1,277 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timezone
4
+ from typing import Optional
5
+ from uuid import UUID
6
+
7
+ from .config import Config
8
+ from .embeddings.sentence_transformers import SentenceTransformerEmbedder
9
+ from .interfaces import Embedder, LogStore, MemoryStore
10
+ from .observer.observer import HeuristicObserver
11
+ from .scorer.attention import AttentionScorer
12
+ from .storage.kg import SQLiteKGStore
13
+ from .storage.log import FileLogStore
14
+ from .storage.sqlite import SQLiteMemoryStore
15
+ from .types import (
16
+ AttentionResult,
17
+ EntityResult,
18
+ EntityType,
19
+ FeedbackSignal,
20
+ LogEntry,
21
+ Memory,
22
+ MemoryLayer,
23
+ Observation,
24
+ RecallResult,
25
+ )
26
+
27
+ _NEGATIVE_WEIGHT_MULTIPLIER = 1.5 # match friday-saas asymmetric RL weighting
28
+
29
+
30
+ def _build_store(config: Config) -> MemoryStore:
31
+ """Select and initialise the memory store from config."""
32
+ if config.store == "postgres":
33
+ if not config.postgres_url:
34
+ raise ValueError("EXTREMIS_STORE=postgres requires EXTREMIS_POSTGRES_URL to be set.")
35
+ from .storage.postgres import PostgresMemoryStore
36
+
37
+ return PostgresMemoryStore(config.postgres_url, config)
38
+ if config.store == "chroma":
39
+ from .storage.chroma import ChromaMemoryStore
40
+
41
+ return ChromaMemoryStore(config.resolved_chroma_path(), config)
42
+ if config.store == "pinecone":
43
+ if not config.pinecone_api_key:
44
+ raise ValueError("EXTREMIS_STORE=pinecone requires EXTREMIS_PINECONE_API_KEY to be set.")
45
+ from .storage.pinecone_store import PineconeMemoryStore
46
+
47
+ return PineconeMemoryStore(
48
+ config.pinecone_api_key,
49
+ config.pinecone_index,
50
+ config,
51
+ score_db_path=config.resolved_pinecone_score_db(),
52
+ )
53
+ return SQLiteMemoryStore(config.resolved_local_db_path(), config)
54
+
55
+
56
+ def _build_embedder(config: Config) -> Embedder:
57
+ """Select embedder based on model name."""
58
+ if config.embedder.startswith("text-embedding"):
59
+ from .embeddings.openai import OpenAIEmbedder
60
+
61
+ return OpenAIEmbedder(config.embedder, config.openai_api_key or None)
62
+ return SentenceTransformerEmbedder(config.embedder)
63
+
64
+
65
+ class Extremis:
66
+ """
67
+ The three methods agents actually call: remember, recall, report_outcome.
68
+ Plus remember_now for time-sensitive direct writes.
69
+ """
70
+
71
+ def __init__(
72
+ self,
73
+ config: Optional[Config] = None,
74
+ log: Optional[LogStore] = None,
75
+ local: Optional[MemoryStore] = None,
76
+ embedder: Optional[Embedder] = None,
77
+ ) -> None:
78
+ self._config = config or Config()
79
+ self._log = log or FileLogStore(
80
+ self._config.resolved_log_dir(),
81
+ namespace=self._config.namespace,
82
+ )
83
+ self._local = local or _build_store(self._config)
84
+ self._embedder = embedder or _build_embedder(self._config)
85
+ self._kg = SQLiteKGStore(self._config.resolved_local_db_path(), self._config)
86
+ self._observer = HeuristicObserver(namespace=self._config.namespace)
87
+ self._attention = AttentionScorer(self._config)
88
+
89
+ def remember(
90
+ self,
91
+ content: str,
92
+ role: str = "user",
93
+ conversation_id: str = "default",
94
+ metadata: Optional[dict] = None,
95
+ ) -> None:
96
+ """
97
+ Append to the log. Cheap path — no LLM, no embedding.
98
+ Also writes an episodic memory to the local store for immediate recall.
99
+ """
100
+ entry = LogEntry(
101
+ role=role,
102
+ content=content,
103
+ conversation_id=conversation_id,
104
+ metadata=metadata or {},
105
+ )
106
+ self._log.append(entry)
107
+
108
+ embedding = self._embedder.embed(content)
109
+ memory = Memory(
110
+ layer=MemoryLayer.EPISODIC,
111
+ content=content,
112
+ embedding=embedding,
113
+ metadata={"conversation_id": conversation_id, "role": role, **(metadata or {})},
114
+ validity_start=datetime.now(tz=timezone.utc),
115
+ )
116
+ self._local.store(memory)
117
+
118
+ def recall(
119
+ self,
120
+ query: str,
121
+ limit: int = 10,
122
+ layers: Optional[list[MemoryLayer]] = None,
123
+ min_score: float = 0.0,
124
+ ) -> list[RecallResult]:
125
+ """
126
+ Layered retrieval:
127
+ - Identity layer always included (who the user is, invariants)
128
+ - Procedural layer always included (behavioral rules)
129
+ - Semantic + Episodic ranked by relevance × utility × recency
130
+ """
131
+ query_embedding = self._embedder.embed(query)
132
+
133
+ # Always pull identity + procedural regardless of layer filter
134
+ pinned_layers = [MemoryLayer.IDENTITY, MemoryLayer.PROCEDURAL]
135
+ pinned = self._local.search(
136
+ query_embedding,
137
+ layers=pinned_layers,
138
+ limit=5,
139
+ min_score=min_score,
140
+ )
141
+
142
+ # Ranked recall for semantic + episodic (or custom filter)
143
+ search_layers = layers or [MemoryLayer.SEMANTIC, MemoryLayer.EPISODIC]
144
+ search_layers = [layer for layer in search_layers if layer not in pinned_layers]
145
+
146
+ ranked = self._local.search(
147
+ query_embedding,
148
+ layers=search_layers,
149
+ limit=limit,
150
+ min_score=min_score,
151
+ )
152
+
153
+ # Merge: pinned first (deduped), then ranked
154
+ seen: set[UUID] = set()
155
+ results: list[RecallResult] = []
156
+ for r in pinned + ranked:
157
+ if r.memory.id not in seen:
158
+ seen.add(r.memory.id)
159
+ results.append(r)
160
+
161
+ return results[:limit]
162
+
163
+ def report_outcome(
164
+ self,
165
+ memory_ids: list[UUID],
166
+ success: bool,
167
+ weight: float = 1.0,
168
+ ) -> None:
169
+ """
170
+ RL signal. Adjusts utility scores on the referenced memories.
171
+ Negative signals are amplified by 1.5× (mirrors human memory asymmetry).
172
+ """
173
+ FeedbackSignal(memory_ids=memory_ids, success=success, weight=weight)
174
+ delta = weight if success else -(weight * _NEGATIVE_WEIGHT_MULTIPLIER)
175
+ for mid in memory_ids:
176
+ self._local.update_score(mid, delta)
177
+
178
+ def remember_now(
179
+ self,
180
+ content: str,
181
+ layer: MemoryLayer,
182
+ expires_at: Optional[datetime] = None,
183
+ confidence: float = 0.9,
184
+ metadata: Optional[dict] = None,
185
+ ) -> Memory:
186
+ """
187
+ Skip the log; write directly to structured memory.
188
+ Use for time-sensitive facts ('flight Thursday 6am') or high-confidence
189
+ identity/procedural rules that don't need to be derived from logs.
190
+ """
191
+ embedding = self._embedder.embed(content)
192
+ memory = Memory(
193
+ layer=layer,
194
+ content=content,
195
+ embedding=embedding,
196
+ confidence=confidence,
197
+ metadata=metadata or {},
198
+ validity_start=datetime.now(tz=timezone.utc),
199
+ validity_end=expires_at,
200
+ )
201
+ return self._local.store(memory)
202
+
203
+ # ------------------------------------------------------------------ #
204
+ # Knowledge graph
205
+ # ------------------------------------------------------------------ #
206
+
207
+ def kg_add_entity(
208
+ self,
209
+ name: str,
210
+ type: EntityType,
211
+ metadata: Optional[dict] = None,
212
+ ):
213
+ return self._kg.add_entity(name, type, metadata)
214
+
215
+ def kg_add_relationship(
216
+ self,
217
+ from_entity: str,
218
+ to_entity: str,
219
+ rel_type: str,
220
+ weight: float = 1.0,
221
+ metadata: Optional[dict] = None,
222
+ ):
223
+ return self._kg.add_relationship(from_entity, to_entity, rel_type, weight, metadata)
224
+
225
+ def kg_add_attribute(self, entity: str, key: str, value: str):
226
+ return self._kg.add_attribute(entity, key, value)
227
+
228
+ def kg_query(self, name: str) -> Optional[EntityResult]:
229
+ return self._kg.query_entity(name)
230
+
231
+ def kg_traverse(self, name: str, depth: int = 2) -> list[EntityResult]:
232
+ return self._kg.traverse(name, depth)
233
+
234
+ # ------------------------------------------------------------------ #
235
+ # Observer
236
+ # ------------------------------------------------------------------ #
237
+
238
+ def observe(self, conversation_id: str = "default") -> list[Observation]:
239
+ """Compress recent log entries for conversation_id into priority observations."""
240
+ all_entries = self._log.read_since(None)
241
+ entries = [e for e in all_entries if e.conversation_id == conversation_id]
242
+ return self._observer.compress(entries)
243
+
244
+ # ------------------------------------------------------------------ #
245
+ # Attention scoring
246
+ # ------------------------------------------------------------------ #
247
+
248
+ def score_attention(
249
+ self,
250
+ message: str,
251
+ sender: str = "",
252
+ channel: str = "dm",
253
+ owner_ids: Optional[set[str]] = None,
254
+ allowlist: Optional[set[str]] = None,
255
+ context: Optional[dict] = None,
256
+ ) -> AttentionResult:
257
+ return self._attention.score(
258
+ message,
259
+ sender=sender,
260
+ channel=channel,
261
+ owner_ids=owner_ids,
262
+ allowlist=allowlist,
263
+ context=context,
264
+ )
265
+
266
+ # ------------------------------------------------------------------ #
267
+ # Internal accessors (used by consolidator, MCP server, tests)
268
+ # ------------------------------------------------------------------ #
269
+
270
+ def get_local_store(self) -> MemoryStore:
271
+ return self._local
272
+
273
+ def get_log(self) -> LogStore:
274
+ return self._log
275
+
276
+ def get_kg(self) -> SQLiteKGStore:
277
+ return self._kg
extremis/client.py ADDED
@@ -0,0 +1,248 @@
1
+ """
2
+ HostedClient — drop-in replacement for Memory that talks to the extremis hosted API.
3
+
4
+ All computation (embedding, search, RL scoring, KG, consolidation) runs server-side.
5
+ No local database. No 90 MB model download.
6
+
7
+ Usage:
8
+ from extremis import HostedClient
9
+
10
+ mem = HostedClient(api_key="extremis_sk_...")
11
+
12
+ # Exact same API as Memory
13
+ mem.remember("User is building a WhatsApp AI", conversation_id="conv_001")
14
+ results = mem.recall("WhatsApp product")
15
+ mem.report_outcome([r.memory.id for r in results], success=True)
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from datetime import datetime
21
+ from typing import Optional
22
+ from uuid import UUID
23
+
24
+ from .types import (
25
+ AttentionResult,
26
+ EntityResult,
27
+ EntityType,
28
+ Memory,
29
+ MemoryLayer,
30
+ Observation,
31
+ RecallResult,
32
+ )
33
+
34
+ _CLOUD_URL = "https://api.extremis.com" # not yet live — self-host with extremis-server
35
+
36
+
37
+ class HostedClient:
38
+ """
39
+ Stateless HTTP client. Every call is a round-trip to a extremis server.
40
+
41
+ Self-host:
42
+ extremis-server serve --host 0.0.0.0 --port 8000
43
+ extremis-server create-key --namespace alice
44
+ mem = HostedClient(api_key="extremis_sk_...", base_url="http://localhost:8000")
45
+
46
+ Cloud (coming soon — join the waitlist at github.com/ashwanijha04/extremis):
47
+ mem = HostedClient(api_key="extremis_sk_...")
48
+
49
+ Install: pip install "extremis[client]"
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ api_key: str,
55
+ base_url: str = _CLOUD_URL,
56
+ timeout: float = 30.0,
57
+ ) -> None:
58
+ try:
59
+ import httpx
60
+ except ImportError:
61
+ raise ImportError("HostedClient requires httpx: pip install 'extremis[client]'") from None
62
+
63
+ self._base = base_url.rstrip("/")
64
+ self._http = httpx.Client(
65
+ base_url=self._base,
66
+ headers={"Authorization": f"Bearer {api_key}"},
67
+ timeout=timeout,
68
+ )
69
+
70
+ # ── Core memory ─────────────────────────────────────────────────────────
71
+
72
+ def remember(
73
+ self,
74
+ content: str,
75
+ role: str = "user",
76
+ conversation_id: str = "default",
77
+ metadata: Optional[dict] = None,
78
+ ) -> None:
79
+ self._post(
80
+ "/v1/memories/remember",
81
+ {
82
+ "content": content,
83
+ "role": role,
84
+ "conversation_id": conversation_id,
85
+ "metadata": metadata or {},
86
+ },
87
+ )
88
+
89
+ def recall(
90
+ self,
91
+ query: str,
92
+ limit: int = 10,
93
+ layers: Optional[list[MemoryLayer]] = None,
94
+ min_score: float = 0.0,
95
+ ) -> list[RecallResult]:
96
+ data = self._post(
97
+ "/v1/memories/recall",
98
+ {
99
+ "query": query,
100
+ "limit": limit,
101
+ "layers": [layer.value for layer in layers] if layers else None,
102
+ "min_score": min_score,
103
+ },
104
+ )
105
+ return [RecallResult(**r) for r in data["results"]]
106
+
107
+ def report_outcome(
108
+ self,
109
+ memory_ids: list[UUID],
110
+ success: bool,
111
+ weight: float = 1.0,
112
+ ) -> None:
113
+ self._post(
114
+ "/v1/memories/report",
115
+ {
116
+ "memory_ids": [str(m) for m in memory_ids],
117
+ "success": success,
118
+ "weight": weight,
119
+ },
120
+ )
121
+
122
+ def remember_now(
123
+ self,
124
+ content: str,
125
+ layer: MemoryLayer,
126
+ expires_at: Optional[datetime] = None,
127
+ confidence: float = 0.9,
128
+ metadata: Optional[dict] = None,
129
+ ) -> Memory:
130
+ data = self._post(
131
+ "/v1/memories/store",
132
+ {
133
+ "content": content,
134
+ "layer": layer.value,
135
+ "confidence": confidence,
136
+ "expires_at": expires_at.isoformat() if expires_at else None,
137
+ "metadata": metadata or {},
138
+ },
139
+ )
140
+ return Memory(**data)
141
+
142
+ def observe(self, conversation_id: str = "default") -> list[Observation]:
143
+ data = self._get("/v1/memories/observe", {"conversation_id": conversation_id})
144
+ return [Observation(**o) for o in data["observations"]]
145
+
146
+ def consolidate(self) -> dict:
147
+ return self._post("/v1/memories/consolidate", {})
148
+
149
+ # ── Knowledge graph ──────────────────────────────────────────────────────
150
+
151
+ def kg_add_entity(self, name: str, type: EntityType, metadata: Optional[dict] = None):
152
+ return self._post(
153
+ "/v1/kg/write",
154
+ {
155
+ "operation": "add_entity",
156
+ "name": name,
157
+ "entity_type": type.value,
158
+ "metadata": metadata or {},
159
+ },
160
+ )
161
+
162
+ def kg_add_relationship(
163
+ self,
164
+ from_entity: str,
165
+ to_entity: str,
166
+ rel_type: str,
167
+ weight: float = 1.0,
168
+ metadata: Optional[dict] = None,
169
+ ):
170
+ return self._post(
171
+ "/v1/kg/write",
172
+ {
173
+ "operation": "add_relationship",
174
+ "from_entity": from_entity,
175
+ "to_entity": to_entity,
176
+ "rel_type": rel_type,
177
+ "weight": weight,
178
+ "metadata": metadata or {},
179
+ },
180
+ )
181
+
182
+ def kg_add_attribute(self, entity: str, key: str, value: str):
183
+ return self._post(
184
+ "/v1/kg/write",
185
+ {
186
+ "operation": "add_attribute",
187
+ "name": entity,
188
+ "key": key,
189
+ "value": value,
190
+ },
191
+ )
192
+
193
+ def kg_query(self, name: str) -> Optional[EntityResult]:
194
+ data = self._post("/v1/kg/query", {"name": name, "traverse_depth": 0})
195
+ if data.get("result") is None:
196
+ return None
197
+ return EntityResult(**data["result"])
198
+
199
+ def kg_traverse(self, name: str, depth: int = 2) -> list[EntityResult]:
200
+ data = self._post("/v1/kg/query", {"name": name, "traverse_depth": depth})
201
+ return [EntityResult(**r) for r in data.get("results", [])]
202
+
203
+ # ── Attention ────────────────────────────────────────────────────────────
204
+
205
+ def score_attention(
206
+ self,
207
+ message: str,
208
+ sender: str = "",
209
+ channel: str = "dm",
210
+ owner_ids: Optional[set[str]] = None,
211
+ allowlist: Optional[set[str]] = None,
212
+ context: Optional[dict] = None,
213
+ ) -> AttentionResult:
214
+ ctx = context or {}
215
+ data = self._get(
216
+ "/v1/attention/score",
217
+ {
218
+ "message": message,
219
+ "sender": sender,
220
+ "channel": channel,
221
+ "owner_ids": ",".join(owner_ids or []),
222
+ "allowlist": ",".join(allowlist or []),
223
+ "ongoing": str(ctx.get("ongoing", False)).lower(),
224
+ "already_answered": str(ctx.get("already_answered", False)).lower(),
225
+ },
226
+ )
227
+ return AttentionResult(**data)
228
+
229
+ # ── HTTP helpers ─────────────────────────────────────────────────────────
230
+
231
+ def _post(self, path: str, body: dict) -> dict:
232
+ resp = self._http.post(path, json=body)
233
+ resp.raise_for_status()
234
+ return resp.json() if resp.content else {}
235
+
236
+ def _get(self, path: str, params: dict) -> dict:
237
+ resp = self._http.get(path, params=params)
238
+ resp.raise_for_status()
239
+ return resp.json()
240
+
241
+ def close(self) -> None:
242
+ self._http.close()
243
+
244
+ def __enter__(self):
245
+ return self
246
+
247
+ def __exit__(self, *_):
248
+ self.close()
extremis/config.py ADDED
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic_settings import BaseSettings, SettingsConfigDict
4
+
5
+
6
+ class Config(BaseSettings):
7
+ model_config = SettingsConfigDict(env_prefix="EXTREMIS_", env_file=".env")
8
+
9
+ # ── Backend ─────────────────────────────────────────────────────
10
+ # "sqlite" | "postgres" | "chroma" | "pinecone"
11
+ store: str = "sqlite"
12
+
13
+ # ── Namespace ────────────────────────────────────────────────────
14
+ # Isolates one user/agent's memories from another.
15
+ # EXTREMIS_NAMESPACE=user_123 for per-user isolation on a shared server.
16
+ namespace: str = "default"
17
+
18
+ # ── SQLite ───────────────────────────────────────────────────────
19
+ extremis_home: str = "~/.extremis"
20
+ log_dir: str = "" # defaults to {extremis_home}/log
21
+ local_db_path: str = "" # defaults to {extremis_home}/local.db
22
+
23
+ # ── Postgres ─────────────────────────────────────────────────────
24
+ postgres_url: str = ""
25
+
26
+ # ── Chroma ───────────────────────────────────────────────────────
27
+ chroma_path: str = "" # defaults to {extremis_home}/chroma
28
+
29
+ # ── Pinecone ─────────────────────────────────────────────────────
30
+ pinecone_api_key: str = ""
31
+ pinecone_index: str = "extremis"
32
+ pinecone_score_db: str = "" # defaults to {extremis_home}/pinecone_scores.db
33
+
34
+ # ── Embeddings ───────────────────────────────────────────────────
35
+ # sentence-transformers model name OR OpenAI model name
36
+ # e.g. "all-MiniLM-L6-v2" or "text-embedding-3-small"
37
+ embedder: str = "all-MiniLM-L6-v2"
38
+ embedding_dim: int = 384
39
+ openai_api_key: str = "" # used when embedder = "text-embedding-*"
40
+
41
+ # ── Consolidation ────────────────────────────────────────────────
42
+ consolidation_idle_minutes: int = 30
43
+ consolidation_daily_hour: int = 4
44
+ consolidation_model: str = "claude-haiku-4-5-20251001"
45
+ consolidation_hard_model: str = "claude-sonnet-4-6"
46
+
47
+ # ── Retrieval ranking ────────────────────────────────────────────
48
+ rl_alpha: float = 0.5
49
+ recency_half_life_days: int = 90
50
+
51
+ # ── Attention scorer ─────────────────────────────────────────────
52
+ attention_full_threshold: int = 75
53
+ attention_standard_threshold: int = 50
54
+ attention_minimal_threshold: int = 25
55
+
56
+ # ── Resolved paths ───────────────────────────────────────────────
57
+ def resolved_log_dir(self) -> str:
58
+ return self.log_dir or f"{self.extremis_home}/log"
59
+
60
+ def resolved_local_db_path(self) -> str:
61
+ return self.local_db_path or f"{self.extremis_home}/local.db"
62
+
63
+ def resolved_chroma_path(self) -> str:
64
+ return self.chroma_path or f"{self.extremis_home}/chroma"
65
+
66
+ def resolved_pinecone_score_db(self) -> str:
67
+ return self.pinecone_score_db or f"{self.extremis_home}/pinecone_scores.db"
@@ -0,0 +1,3 @@
1
+ from .consolidator import LLMConsolidator
2
+
3
+ __all__ = ["LLMConsolidator"]