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.
- extremis/__init__.py +15 -0
- extremis/api.py +277 -0
- extremis/client.py +248 -0
- extremis/config.py +67 -0
- extremis/consolidation/__init__.py +3 -0
- extremis/consolidation/consolidator.py +168 -0
- extremis/consolidation/prompts.py +41 -0
- extremis/embeddings/__init__.py +3 -0
- extremis/embeddings/openai.py +55 -0
- extremis/embeddings/sentence_transformers.py +34 -0
- extremis/interfaces.py +80 -0
- extremis/mcp/__init__.py +3 -0
- extremis/mcp/server.py +433 -0
- extremis/migrate.py +233 -0
- extremis/migrations/001_initial.sql +69 -0
- extremis/migrations/001_initial_sqlite.sql +91 -0
- extremis/observer/__init__.py +3 -0
- extremis/observer/observer.py +145 -0
- extremis/scorer/__init__.py +3 -0
- extremis/scorer/attention.py +198 -0
- extremis/server/__init__.py +0 -0
- extremis/server/app.py +156 -0
- extremis/server/auth.py +95 -0
- extremis/server/deps.py +56 -0
- extremis/server/routes/__init__.py +0 -0
- extremis/server/routes/health.py +37 -0
- extremis/server/routes/kg.py +54 -0
- extremis/server/routes/memories.py +98 -0
- extremis/storage/__init__.py +5 -0
- extremis/storage/chroma.py +237 -0
- extremis/storage/kg.py +250 -0
- extremis/storage/log.py +87 -0
- extremis/storage/pinecone_store.py +192 -0
- extremis/storage/postgres.py +254 -0
- extremis/storage/recall_reason.py +56 -0
- extremis/storage/score_index.py +56 -0
- extremis/storage/sqlite.py +251 -0
- extremis/types.py +145 -0
- extremis-0.1.0.dist-info/METADATA +801 -0
- extremis-0.1.0.dist-info/RECORD +43 -0
- extremis-0.1.0.dist-info/WHEEL +4 -0
- extremis-0.1.0.dist-info/entry_points.txt +4 -0
- 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"
|