minima-cli 0.4.9__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.
- minima/__init__.py +5 -0
- minima/api/__init__.py +1 -0
- minima/api/auth.py +39 -0
- minima/api/errors.py +40 -0
- minima/api/routers/__init__.py +1 -0
- minima/api/routers/calibration.py +50 -0
- minima/api/routers/feedback.py +279 -0
- minima/api/routers/health.py +50 -0
- minima/api/routers/models.py +42 -0
- minima/api/routers/recommend.py +66 -0
- minima/api/routers/savings.py +55 -0
- minima/api/routers/strategies.py +33 -0
- minima/catalog/__init__.py +1 -0
- minima/catalog/data/capability_priors.json +210 -0
- minima/catalog/data/model_aliases.json +12 -0
- minima/catalog/merge.py +69 -0
- minima/catalog/refresh.py +54 -0
- minima/catalog/sources/__init__.py +1 -0
- minima/catalog/sources/litellm.py +19 -0
- minima/catalog/sources/openrouter.py +25 -0
- minima/catalog/store.py +86 -0
- minima/config.py +288 -0
- minima/deps.py +35 -0
- minima/llm/__init__.py +1 -0
- minima/llm/anthropic.py +106 -0
- minima/llm/base.py +196 -0
- minima/llm/gemini.py +124 -0
- minima/llm/registry.py +54 -0
- minima/logging.py +28 -0
- minima/main.py +109 -0
- minima/memory/__init__.py +1 -0
- minima/memory/adapter.py +572 -0
- minima/memory/keys.py +83 -0
- minima/memory/records.py +190 -0
- minima/memory/threadpool.py +41 -0
- minima/metrics/__init__.py +1 -0
- minima/metrics/calibration.py +415 -0
- minima/metrics/report.py +116 -0
- minima/metrics/savings.py +98 -0
- minima/recommender/__init__.py +1 -0
- minima/recommender/_pg_pool.py +38 -0
- minima/recommender/_redis_client.py +32 -0
- minima/recommender/aggregate.py +157 -0
- minima/recommender/classify.py +165 -0
- minima/recommender/decisionlog.py +505 -0
- minima/recommender/durablerefs.py +312 -0
- minima/recommender/engine.py +997 -0
- minima/recommender/escalation.py +83 -0
- minima/recommender/propensity.py +189 -0
- minima/recommender/recstore.py +368 -0
- minima/recommender/score.py +318 -0
- minima/recommender/types.py +166 -0
- minima/schemas/__init__.py +1 -0
- minima/schemas/common.py +73 -0
- minima/schemas/feedback.py +34 -0
- minima/schemas/models_catalog.py +36 -0
- minima/schemas/recommend.py +104 -0
- minima/schemas/savings.py +39 -0
- minima/schemas/strategies.py +57 -0
- minima/schemas/workflow.py +43 -0
- minima/seeding/__init__.py +1 -0
- minima/seeding/items.py +42 -0
- minima/seeding/llmrouterbench.py +232 -0
- minima/seeding/routerbench.py +141 -0
- minima/seeding/run_seed.py +56 -0
- minima/seeding/synthetic.py +70 -0
- minima/tenancy/__init__.py +8 -0
- minima/tenancy/context.py +37 -0
- minima/tenancy/passthrough.py +110 -0
- minima/version.py +3 -0
- minima_cli-0.4.9.dist-info/METADATA +275 -0
- minima_cli-0.4.9.dist-info/RECORD +161 -0
- minima_cli-0.4.9.dist-info/WHEEL +4 -0
- minima_cli-0.4.9.dist-info/entry_points.txt +5 -0
- minima_cli-0.4.9.dist-info/licenses/LICENSE +295 -0
- minima_client/__init__.py +19 -0
- minima_client/autocapture.py +101 -0
- minima_client/client.py +301 -0
- minima_client/errors.py +23 -0
- minima_harness/LICENSE_PI +32 -0
- minima_harness/__init__.py +16 -0
- minima_harness/agent/__init__.py +72 -0
- minima_harness/agent/agent.py +276 -0
- minima_harness/agent/events.py +124 -0
- minima_harness/agent/loop.py +311 -0
- minima_harness/agent/state.py +79 -0
- minima_harness/agent/tools.py +97 -0
- minima_harness/ai/__init__.py +66 -0
- minima_harness/ai/compat.py +71 -0
- minima_harness/ai/errors.py +96 -0
- minima_harness/ai/events.py +117 -0
- minima_harness/ai/openrouter_catalog.py +153 -0
- minima_harness/ai/provider_catalog.py +299 -0
- minima_harness/ai/provider_quirks.py +37 -0
- minima_harness/ai/providers/__init__.py +75 -0
- minima_harness/ai/providers/_common.py +48 -0
- minima_harness/ai/providers/anthropic.py +290 -0
- minima_harness/ai/providers/base.py +65 -0
- minima_harness/ai/providers/faux.py +173 -0
- minima_harness/ai/providers/google.py +221 -0
- minima_harness/ai/providers/openai_compat.py +278 -0
- minima_harness/ai/registry.py +184 -0
- minima_harness/ai/stream.py +82 -0
- minima_harness/ai/tools.py +51 -0
- minima_harness/ai/types.py +204 -0
- minima_harness/ai/usage.py +41 -0
- minima_harness/minima/__init__.py +40 -0
- minima_harness/minima/cache.py +102 -0
- minima_harness/minima/config.py +85 -0
- minima_harness/minima/goals.py +226 -0
- minima_harness/minima/judge.py +144 -0
- minima_harness/minima/mapping.py +147 -0
- minima_harness/minima/meter.py +143 -0
- minima_harness/minima/router.py +220 -0
- minima_harness/minima/runtime.py +544 -0
- minima_harness/minima/signals.py +195 -0
- minima_harness/session/__init__.py +14 -0
- minima_harness/session/format.py +35 -0
- minima_harness/session/store.py +236 -0
- minima_harness/tasks/__init__.py +17 -0
- minima_harness/tasks/task_set.py +78 -0
- minima_harness/tools/__init__.py +7 -0
- minima_harness/tools/_io.py +34 -0
- minima_harness/tools/bash.py +70 -0
- minima_harness/tools/builtin.py +23 -0
- minima_harness/tools/edit.py +50 -0
- minima_harness/tools/find.py +38 -0
- minima_harness/tools/grep.py +73 -0
- minima_harness/tools/ls.py +35 -0
- minima_harness/tools/read.py +38 -0
- minima_harness/tools/tasks.py +75 -0
- minima_harness/tools/write.py +36 -0
- minima_harness/tui/__init__.py +3 -0
- minima_harness/tui/analytics.py +111 -0
- minima_harness/tui/app.py +1927 -0
- minima_harness/tui/bridge.py +103 -0
- minima_harness/tui/cli.py +227 -0
- minima_harness/tui/clipboard.py +60 -0
- minima_harness/tui/commands.py +49 -0
- minima_harness/tui/compaction.py +17 -0
- minima_harness/tui/config_cli.py +141 -0
- minima_harness/tui/config_store.py +237 -0
- minima_harness/tui/context.py +93 -0
- minima_harness/tui/customize.py +95 -0
- minima_harness/tui/diff.py +53 -0
- minima_harness/tui/editor.py +43 -0
- minima_harness/tui/extensions.py +84 -0
- minima_harness/tui/extra_models.py +52 -0
- minima_harness/tui/history.py +71 -0
- minima_harness/tui/mubit.py +295 -0
- minima_harness/tui/overlays.py +593 -0
- minima_harness/tui/packages.py +59 -0
- minima_harness/tui/run_modes.py +66 -0
- minima_harness/tui/theme.py +77 -0
- minima_harness/tui/welcome.py +83 -0
- minima_harness/tui/widgets/__init__.py +3 -0
- minima_harness/tui/widgets/banner.py +38 -0
- minima_harness/tui/widgets/editor.py +83 -0
- minima_harness/tui/widgets/footer.py +73 -0
- minima_harness/tui/widgets/messages.py +151 -0
- minima_harness/tui/widgets/status.py +57 -0
minima/memory/adapter.py
ADDED
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
"""The single integration point with the Mubit SDK.
|
|
2
|
+
|
|
3
|
+
Everything Minima knows about Mubit lives here. The recommender depends only on the
|
|
4
|
+
``Memory`` protocol, so tests can swap in a fake. All SDK calls are synchronous and
|
|
5
|
+
run in a worker thread; the recall path is latency-bounded.
|
|
6
|
+
|
|
7
|
+
Mubit run scoping: Minima uses the memory *lane* string as the run_id, so a namespace
|
|
8
|
+
maps to one stable run. That keeps ``upsert_key`` (scoped to run_id + user_id) stable
|
|
9
|
+
across requests, and recall over the same run finds the accumulated outcomes.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import time
|
|
15
|
+
from collections.abc import Mapping, Sequence
|
|
16
|
+
from typing import Any, Protocol, runtime_checkable
|
|
17
|
+
|
|
18
|
+
import anyio
|
|
19
|
+
import httpx
|
|
20
|
+
from mubit import Client, TransportError
|
|
21
|
+
|
|
22
|
+
from minima.config import Settings
|
|
23
|
+
from minima.logging import get_logger
|
|
24
|
+
from minima.memory import threadpool
|
|
25
|
+
from minima.memory.records import OutcomeRecord, RecalledEvidence, RecallResult
|
|
26
|
+
|
|
27
|
+
log = get_logger("minima.memory")
|
|
28
|
+
|
|
29
|
+
# Lowercase LTM entry-type tags for Mubit's query filter. Recall deliberately does
|
|
30
|
+
# NOT filter by type (seeds land as "fact", feedback as "observation"); Minima outcomes
|
|
31
|
+
# are selected by metadata kind instead. Used by get_context only.
|
|
32
|
+
CONTEXT_ENTRY_TYPES = ["observation", "lesson", "fact"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _f(value: object, default: float = 0.0) -> float:
|
|
36
|
+
try:
|
|
37
|
+
return float(value) # type: ignore[arg-type]
|
|
38
|
+
except (TypeError, ValueError):
|
|
39
|
+
return default
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _parse_evidence(ev: Mapping[str, Any]) -> RecalledEvidence:
|
|
43
|
+
return RecalledEvidence(
|
|
44
|
+
entry_id=str(ev.get("id", "")),
|
|
45
|
+
reference_id=(ev.get("reference_id") or None),
|
|
46
|
+
score=_f(ev.get("score")),
|
|
47
|
+
knowledge_confidence=_f(ev.get("knowledge_confidence")),
|
|
48
|
+
is_stale=bool(ev.get("is_stale", False)),
|
|
49
|
+
content=str(ev.get("content", "")),
|
|
50
|
+
record=OutcomeRecord.from_metadata(ev.get("metadata_json")),
|
|
51
|
+
referenceable=bool(ev.get("referenceable", False)),
|
|
52
|
+
entry_type=str(ev.get("entry_type", "")),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _parse_lookup_record(item: Mapping[str, Any]) -> RecalledEvidence | None:
|
|
57
|
+
"""Parse a single LookupResponse item (from POST /v2/core/lookup) into RecalledEvidence.
|
|
58
|
+
|
|
59
|
+
The lookup response shape differs from recall: no ANN scores, metadata is a dict
|
|
60
|
+
(not a JSON string), and id is a numeric node ID. Score and knowledge_confidence are
|
|
61
|
+
set to 1.0 — an exact keyed match is maximally certain.
|
|
62
|
+
"""
|
|
63
|
+
raw_id = item.get("id")
|
|
64
|
+
if raw_id is None:
|
|
65
|
+
return None
|
|
66
|
+
record = OutcomeRecord.from_metadata(item.get("metadata"))
|
|
67
|
+
if record is None:
|
|
68
|
+
return None
|
|
69
|
+
entry_id = str(raw_id)
|
|
70
|
+
return RecalledEvidence(
|
|
71
|
+
entry_id=entry_id,
|
|
72
|
+
reference_id=entry_id,
|
|
73
|
+
score=1.0,
|
|
74
|
+
knowledge_confidence=1.0,
|
|
75
|
+
is_stale=False,
|
|
76
|
+
content="",
|
|
77
|
+
record=record,
|
|
78
|
+
referenceable=True,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _log_explain(lane: str, raw: object) -> None:
|
|
83
|
+
"""Diagnostic: per-evidence score components (server-side ExplainInfo)."""
|
|
84
|
+
if not isinstance(raw, Mapping):
|
|
85
|
+
return
|
|
86
|
+
components = []
|
|
87
|
+
for ev in raw.get("evidence") or []:
|
|
88
|
+
if not isinstance(ev, Mapping):
|
|
89
|
+
continue
|
|
90
|
+
info = ev.get("explain_info")
|
|
91
|
+
if isinstance(info, Mapping):
|
|
92
|
+
components.append(
|
|
93
|
+
{
|
|
94
|
+
"id": str(ev.get("id", ""))[:12],
|
|
95
|
+
"semantic": _f(info.get("semantic_score")),
|
|
96
|
+
"lexical": _f(info.get("lexical_score")),
|
|
97
|
+
"recency": _f(info.get("recency_score")),
|
|
98
|
+
"decay": _f(info.get("temporal_decay_factor"), 1.0),
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
if components:
|
|
102
|
+
log.info(
|
|
103
|
+
"recall_explain",
|
|
104
|
+
lane=lane,
|
|
105
|
+
rank_by_mode=raw.get("rank_by_mode"),
|
|
106
|
+
n=len(components),
|
|
107
|
+
components=components,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@runtime_checkable
|
|
112
|
+
class Memory(Protocol):
|
|
113
|
+
async def recall(
|
|
114
|
+
self,
|
|
115
|
+
*,
|
|
116
|
+
query: str,
|
|
117
|
+
lane: str,
|
|
118
|
+
user_id: str | None = None,
|
|
119
|
+
limit: int = 25,
|
|
120
|
+
entry_types: Sequence[str] | None = None,
|
|
121
|
+
env_tags: Sequence[str] | None = None,
|
|
122
|
+
timeout_ms: int | None = None,
|
|
123
|
+
) -> RecallResult: ...
|
|
124
|
+
|
|
125
|
+
async def remember_outcome(
|
|
126
|
+
self,
|
|
127
|
+
*,
|
|
128
|
+
content: str,
|
|
129
|
+
record: OutcomeRecord,
|
|
130
|
+
lane: str,
|
|
131
|
+
upsert_key: str,
|
|
132
|
+
idempotency_key: str,
|
|
133
|
+
user_id: str | None = None,
|
|
134
|
+
env_tags: Sequence[str] | None = None,
|
|
135
|
+
importance: str = "medium",
|
|
136
|
+
source: str = "human",
|
|
137
|
+
) -> str | None: ...
|
|
138
|
+
|
|
139
|
+
async def record_outcome(
|
|
140
|
+
self,
|
|
141
|
+
*,
|
|
142
|
+
lane: str,
|
|
143
|
+
reference_id: str,
|
|
144
|
+
outcome: str,
|
|
145
|
+
signal: float,
|
|
146
|
+
entry_ids: Sequence[str] | None = None,
|
|
147
|
+
user_id: str | None = None,
|
|
148
|
+
verified_in_production: bool = False,
|
|
149
|
+
idempotency_key: str | None = None,
|
|
150
|
+
rationale: str = "",
|
|
151
|
+
) -> dict: ...
|
|
152
|
+
|
|
153
|
+
async def remember_lesson(
|
|
154
|
+
self,
|
|
155
|
+
*,
|
|
156
|
+
content: str,
|
|
157
|
+
lane: str,
|
|
158
|
+
upsert_key: str,
|
|
159
|
+
user_id: str | None = None,
|
|
160
|
+
lesson_type: str = "success",
|
|
161
|
+
lesson_scope: str = "session",
|
|
162
|
+
importance: str = "high",
|
|
163
|
+
env_tags: Sequence[str] | None = None,
|
|
164
|
+
metadata: Mapping[str, Any] | None = None,
|
|
165
|
+
idempotency_key: str | None = None,
|
|
166
|
+
) -> str | None: ...
|
|
167
|
+
|
|
168
|
+
async def batch_insert(
|
|
169
|
+
self, *, run_id: str, items: list[dict], deduplicate: bool = True
|
|
170
|
+
) -> dict: ...
|
|
171
|
+
|
|
172
|
+
async def lookup(
|
|
173
|
+
self,
|
|
174
|
+
*,
|
|
175
|
+
lane: str,
|
|
176
|
+
match: list[dict],
|
|
177
|
+
limit: int = 256,
|
|
178
|
+
) -> list[RecalledEvidence]: ...
|
|
179
|
+
|
|
180
|
+
async def dereference(
|
|
181
|
+
self, *, lane: str, reference_id: str
|
|
182
|
+
) -> RecalledEvidence | None: ...
|
|
183
|
+
|
|
184
|
+
async def get_context(
|
|
185
|
+
self,
|
|
186
|
+
*,
|
|
187
|
+
query: str,
|
|
188
|
+
lane: str,
|
|
189
|
+
user_id: str | None = None,
|
|
190
|
+
entry_types: Sequence[str] | None = None,
|
|
191
|
+
max_token_budget: int = 1500,
|
|
192
|
+
) -> str: ...
|
|
193
|
+
|
|
194
|
+
async def reflect(
|
|
195
|
+
self, *, lane: str, user_id: str | None = None, include_linked_runs: bool = False
|
|
196
|
+
) -> dict: ...
|
|
197
|
+
|
|
198
|
+
async def surface_strategies(
|
|
199
|
+
self, *, lane: str, lesson_types: Sequence[str] | None = None, max_strategies: int = 5
|
|
200
|
+
) -> dict: ...
|
|
201
|
+
|
|
202
|
+
async def health(self) -> dict: ...
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class MubitMemory:
|
|
206
|
+
"""Concrete ``Memory`` backed by the Mubit SDK Client."""
|
|
207
|
+
|
|
208
|
+
def __init__(
|
|
209
|
+
self,
|
|
210
|
+
settings: Settings,
|
|
211
|
+
*,
|
|
212
|
+
endpoint: str | None = None,
|
|
213
|
+
api_key: str | None = None,
|
|
214
|
+
transport: str | None = None,
|
|
215
|
+
):
|
|
216
|
+
# endpoint/api_key/transport override settings so one process can hold a distinct
|
|
217
|
+
# client per org (multi-tenancy). They default to the single-tenant env config.
|
|
218
|
+
self._settings = settings
|
|
219
|
+
self._endpoint = endpoint or settings.mubit_endpoint
|
|
220
|
+
self._transport = transport or settings.mubit_transport
|
|
221
|
+
resolved_key = api_key if api_key is not None else settings.mubit_api_key
|
|
222
|
+
self._api_key = resolved_key
|
|
223
|
+
kwargs: dict[str, Any] = {
|
|
224
|
+
"endpoint": self._endpoint,
|
|
225
|
+
"timeout_ms": settings.mubit_timeout_ms,
|
|
226
|
+
}
|
|
227
|
+
if resolved_key:
|
|
228
|
+
kwargs["api_key"] = resolved_key
|
|
229
|
+
if self._transport:
|
|
230
|
+
kwargs["transport"] = self._transport
|
|
231
|
+
self._client = Client(**kwargs)
|
|
232
|
+
|
|
233
|
+
# ---- reads -----------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
async def recall(
|
|
236
|
+
self,
|
|
237
|
+
*,
|
|
238
|
+
query: str,
|
|
239
|
+
lane: str,
|
|
240
|
+
user_id: str | None = None,
|
|
241
|
+
limit: int = 25,
|
|
242
|
+
entry_types: Sequence[str] | None = None,
|
|
243
|
+
env_tags: Sequence[str] | None = None,
|
|
244
|
+
timeout_ms: int | None = None,
|
|
245
|
+
) -> RecallResult:
|
|
246
|
+
settings = self._settings
|
|
247
|
+
budget_ms = (
|
|
248
|
+
timeout_ms if timeout_ms is not None else settings.minima_memory_recall_timeout_ms
|
|
249
|
+
)
|
|
250
|
+
# Low-level control query (the typed recall() wrapper drops rank_by / timestamps /
|
|
251
|
+
# budget / explain). Default entry_types covers both intake paths: seed records
|
|
252
|
+
# (batch_insert) land as "fact", feedback records (remember intent=observation)
|
|
253
|
+
# as "observation"; Minima outcomes are still authoritatively selected by metadata
|
|
254
|
+
# kind. prefer_current_run keeps everything in this lane's run while skipping
|
|
255
|
+
# cross-run global-lesson overlays from other actors.
|
|
256
|
+
resolved_types = (
|
|
257
|
+
list(entry_types)
|
|
258
|
+
if entry_types
|
|
259
|
+
else [t.strip() for t in settings.minima_recall_entry_types.split(",") if t.strip()]
|
|
260
|
+
)
|
|
261
|
+
payload: dict[str, Any] = {
|
|
262
|
+
"run_id": lane,
|
|
263
|
+
"query": query,
|
|
264
|
+
"mode": settings.minima_recall_mode,
|
|
265
|
+
"limit": limit,
|
|
266
|
+
"include_working_memory": False,
|
|
267
|
+
"prefer_current_run": True,
|
|
268
|
+
"lane_filter": lane,
|
|
269
|
+
}
|
|
270
|
+
if user_id:
|
|
271
|
+
payload["user_id"] = user_id
|
|
272
|
+
if resolved_types:
|
|
273
|
+
payload["entry_types"] = resolved_types
|
|
274
|
+
if env_tags:
|
|
275
|
+
payload["env_tags"] = list(env_tags)
|
|
276
|
+
if settings.minima_recall_rank_by:
|
|
277
|
+
payload["rank_by"] = settings.minima_recall_rank_by
|
|
278
|
+
if settings.minima_recall_budget:
|
|
279
|
+
payload["budget"] = settings.minima_recall_budget
|
|
280
|
+
if settings.minima_recall_max_age_days > 0:
|
|
281
|
+
payload["min_timestamp"] = int(
|
|
282
|
+
time.time() - settings.minima_recall_max_age_days * 86_400
|
|
283
|
+
)
|
|
284
|
+
if settings.minima_recall_explain:
|
|
285
|
+
payload["explain"] = True
|
|
286
|
+
try:
|
|
287
|
+
with anyio.move_on_after(budget_ms / 1000.0) as scope:
|
|
288
|
+
raw = await threadpool.run_cancellable(self._client._control.query, payload)
|
|
289
|
+
if scope.cancelled_caught:
|
|
290
|
+
log.warning("recall_timeout", lane=lane, budget_ms=budget_ms)
|
|
291
|
+
return RecallResult(evidence=[], degraded=True, timed_out=True)
|
|
292
|
+
result = self._parse_recall(raw)
|
|
293
|
+
if settings.minima_recall_explain:
|
|
294
|
+
_log_explain(lane, raw)
|
|
295
|
+
return result
|
|
296
|
+
except TransportError as exc:
|
|
297
|
+
log.warning("recall_transport_error", lane=lane, code=exc.args[0] if exc.args else "")
|
|
298
|
+
return RecallResult(evidence=[], degraded=True, error=str(exc))
|
|
299
|
+
except Exception as exc: # noqa: BLE001 — recall must never break a recommendation
|
|
300
|
+
log.warning("recall_error", lane=lane, error=str(exc))
|
|
301
|
+
return RecallResult(evidence=[], degraded=True, error=str(exc))
|
|
302
|
+
|
|
303
|
+
def _parse_recall(self, raw: object) -> RecallResult:
|
|
304
|
+
data: Mapping[str, Any] = raw if isinstance(raw, Mapping) else {}
|
|
305
|
+
evidence: list[RecalledEvidence] = []
|
|
306
|
+
for ev in data.get("evidence") or []:
|
|
307
|
+
if not isinstance(ev, Mapping):
|
|
308
|
+
continue
|
|
309
|
+
evidence.append(_parse_evidence(ev))
|
|
310
|
+
return RecallResult(
|
|
311
|
+
evidence=evidence,
|
|
312
|
+
degraded=bool(data.get("degraded", False)),
|
|
313
|
+
raw_confidence=_f(data.get("confidence")),
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
async def lookup(
|
|
317
|
+
self,
|
|
318
|
+
*,
|
|
319
|
+
lane: str,
|
|
320
|
+
match: list[dict],
|
|
321
|
+
limit: int = 256,
|
|
322
|
+
) -> list[RecalledEvidence]:
|
|
323
|
+
"""Deterministic keyed lookup via POST /v2/core/lookup.
|
|
324
|
+
|
|
325
|
+
Returns all non-deleted outcome records for the given (lane, match) filters
|
|
326
|
+
without touching ANN — results are stable across identical calls. Use for
|
|
327
|
+
(cluster, model) keyed reads where recall flicker is unacceptable.
|
|
328
|
+
"""
|
|
329
|
+
try:
|
|
330
|
+
raw = await threadpool.run(
|
|
331
|
+
self._client.lookup,
|
|
332
|
+
session_id=lane,
|
|
333
|
+
match=match,
|
|
334
|
+
limit=limit,
|
|
335
|
+
)
|
|
336
|
+
except Exception as exc: # noqa: BLE001 — lookup is additive; must not block a recommend
|
|
337
|
+
log.warning("lookup_error", lane=lane, error=str(exc))
|
|
338
|
+
return []
|
|
339
|
+
if not isinstance(raw, list):
|
|
340
|
+
return []
|
|
341
|
+
results: list[RecalledEvidence] = []
|
|
342
|
+
for item in raw:
|
|
343
|
+
if not isinstance(item, dict):
|
|
344
|
+
continue
|
|
345
|
+
parsed = _parse_lookup_record(item)
|
|
346
|
+
if parsed is not None:
|
|
347
|
+
results.append(parsed)
|
|
348
|
+
return results
|
|
349
|
+
|
|
350
|
+
async def dereference(self, *, lane: str, reference_id: str) -> RecalledEvidence | None:
|
|
351
|
+
"""Exact re-read of a known durable record (the (cluster, model) outcome upsert).
|
|
352
|
+
|
|
353
|
+
Returns None on any failure — the fast path is strictly additive to ANN recall.
|
|
354
|
+
"""
|
|
355
|
+
try:
|
|
356
|
+
raw = await threadpool.run(
|
|
357
|
+
self._client.dereference, reference_id=reference_id, session_id=lane
|
|
358
|
+
)
|
|
359
|
+
except Exception as exc: # noqa: BLE001 — fast path must never break a recommendation
|
|
360
|
+
log.warning("dereference_error", lane=lane, error=str(exc))
|
|
361
|
+
return None
|
|
362
|
+
if not isinstance(raw, Mapping) or raw.get("found") is False:
|
|
363
|
+
return None
|
|
364
|
+
ev = raw.get("evidence")
|
|
365
|
+
if not isinstance(ev, Mapping) or not ev:
|
|
366
|
+
return None
|
|
367
|
+
parsed = _parse_evidence(ev)
|
|
368
|
+
# Exact identity fetch: similarity is 1.0 by construction (same cluster key).
|
|
369
|
+
parsed.score = 1.0
|
|
370
|
+
if not parsed.reference_id:
|
|
371
|
+
parsed.reference_id = reference_id
|
|
372
|
+
return parsed
|
|
373
|
+
|
|
374
|
+
async def get_context(
|
|
375
|
+
self,
|
|
376
|
+
*,
|
|
377
|
+
query: str,
|
|
378
|
+
lane: str,
|
|
379
|
+
user_id: str | None = None,
|
|
380
|
+
entry_types: Sequence[str] | None = None,
|
|
381
|
+
max_token_budget: int = 1500,
|
|
382
|
+
) -> str:
|
|
383
|
+
try:
|
|
384
|
+
raw = await threadpool.run(
|
|
385
|
+
self._client.get_context,
|
|
386
|
+
query=query,
|
|
387
|
+
session_id=lane,
|
|
388
|
+
user_id=user_id,
|
|
389
|
+
entry_types=list(entry_types or CONTEXT_ENTRY_TYPES),
|
|
390
|
+
include_working_memory=False,
|
|
391
|
+
max_token_budget=max_token_budget,
|
|
392
|
+
format="structured",
|
|
393
|
+
mode="full",
|
|
394
|
+
)
|
|
395
|
+
except Exception as exc: # noqa: BLE001
|
|
396
|
+
log.warning("get_context_error", lane=lane, error=str(exc))
|
|
397
|
+
return ""
|
|
398
|
+
if isinstance(raw, Mapping):
|
|
399
|
+
return str(raw.get("context_block", ""))
|
|
400
|
+
return ""
|
|
401
|
+
|
|
402
|
+
# ---- writes ----------------------------------------------------------------
|
|
403
|
+
|
|
404
|
+
async def remember_outcome(
|
|
405
|
+
self,
|
|
406
|
+
*,
|
|
407
|
+
content: str,
|
|
408
|
+
record: OutcomeRecord,
|
|
409
|
+
lane: str,
|
|
410
|
+
upsert_key: str,
|
|
411
|
+
idempotency_key: str,
|
|
412
|
+
user_id: str | None = None,
|
|
413
|
+
env_tags: Sequence[str] | None = None,
|
|
414
|
+
importance: str = "medium",
|
|
415
|
+
source: str = "human",
|
|
416
|
+
) -> str | None:
|
|
417
|
+
raw = await threadpool.run(
|
|
418
|
+
self._client.remember,
|
|
419
|
+
content=content,
|
|
420
|
+
session_id=lane,
|
|
421
|
+
agent_id="minima",
|
|
422
|
+
intent="observation",
|
|
423
|
+
metadata=record.to_metadata(),
|
|
424
|
+
user_id=user_id,
|
|
425
|
+
upsert_key=upsert_key,
|
|
426
|
+
importance=importance,
|
|
427
|
+
source=source,
|
|
428
|
+
lane=lane,
|
|
429
|
+
idempotency_key=idempotency_key,
|
|
430
|
+
env_tags=list(env_tags) if env_tags else None,
|
|
431
|
+
wait=True,
|
|
432
|
+
)
|
|
433
|
+
return _extract_record_id(raw)
|
|
434
|
+
|
|
435
|
+
async def record_outcome(
|
|
436
|
+
self,
|
|
437
|
+
*,
|
|
438
|
+
lane: str,
|
|
439
|
+
reference_id: str,
|
|
440
|
+
outcome: str,
|
|
441
|
+
signal: float,
|
|
442
|
+
entry_ids: Sequence[str] | None = None,
|
|
443
|
+
user_id: str | None = None,
|
|
444
|
+
verified_in_production: bool = False,
|
|
445
|
+
idempotency_key: str | None = None,
|
|
446
|
+
rationale: str = "",
|
|
447
|
+
) -> dict:
|
|
448
|
+
# Low-level control op so we can pass idempotency_key (the typed
|
|
449
|
+
# client.record_outcome wrapper drops it).
|
|
450
|
+
payload = {
|
|
451
|
+
"run_id": lane,
|
|
452
|
+
"reference_id": reference_id,
|
|
453
|
+
"outcome": outcome,
|
|
454
|
+
"signal": signal,
|
|
455
|
+
"rationale": rationale,
|
|
456
|
+
"user_id": user_id,
|
|
457
|
+
"verified_in_production": verified_in_production or None,
|
|
458
|
+
"entry_ids": list(entry_ids) if entry_ids else None,
|
|
459
|
+
"idempotency_key": idempotency_key,
|
|
460
|
+
}
|
|
461
|
+
payload = {k: v for k, v in payload.items() if v is not None}
|
|
462
|
+
raw = await threadpool.run(self._client._control.record_outcome, payload)
|
|
463
|
+
return raw if isinstance(raw, dict) else {}
|
|
464
|
+
|
|
465
|
+
async def remember_lesson(
|
|
466
|
+
self,
|
|
467
|
+
*,
|
|
468
|
+
content: str,
|
|
469
|
+
lane: str,
|
|
470
|
+
upsert_key: str,
|
|
471
|
+
user_id: str | None = None,
|
|
472
|
+
lesson_type: str = "success",
|
|
473
|
+
lesson_scope: str = "session",
|
|
474
|
+
importance: str = "high",
|
|
475
|
+
env_tags: Sequence[str] | None = None,
|
|
476
|
+
metadata: Mapping[str, Any] | None = None,
|
|
477
|
+
idempotency_key: str | None = None,
|
|
478
|
+
) -> str | None:
|
|
479
|
+
# A Lesson entry (intent="lesson") goes through the server's validation gate and
|
|
480
|
+
# feeds reflect()/surface_strategies rule promotion. upsert_key keeps one durable
|
|
481
|
+
# lesson per (cluster, model) so repeated wins reinforce rather than flood LTM.
|
|
482
|
+
raw = await threadpool.run(
|
|
483
|
+
self._client.remember,
|
|
484
|
+
content=content,
|
|
485
|
+
session_id=lane,
|
|
486
|
+
agent_id="minima",
|
|
487
|
+
intent="lesson",
|
|
488
|
+
lesson_type=lesson_type,
|
|
489
|
+
lesson_scope=lesson_scope,
|
|
490
|
+
lesson_importance=importance,
|
|
491
|
+
metadata=dict(metadata) if metadata else None,
|
|
492
|
+
user_id=user_id,
|
|
493
|
+
upsert_key=upsert_key,
|
|
494
|
+
importance=importance,
|
|
495
|
+
source="human",
|
|
496
|
+
lane=lane,
|
|
497
|
+
idempotency_key=idempotency_key,
|
|
498
|
+
env_tags=list(env_tags) if env_tags else None,
|
|
499
|
+
wait=True,
|
|
500
|
+
)
|
|
501
|
+
return _extract_record_id(raw)
|
|
502
|
+
|
|
503
|
+
async def batch_insert(
|
|
504
|
+
self, *, run_id: str, items: list[dict], deduplicate: bool = True
|
|
505
|
+
) -> dict:
|
|
506
|
+
# Control batch_insert (/v2/control/batch_insert) is run-scoped and takes
|
|
507
|
+
# {run_id, deduplicate, items}. The core route expects a bare array.
|
|
508
|
+
raw = await threadpool.run(
|
|
509
|
+
self._client._control.batch_insert,
|
|
510
|
+
{"run_id": run_id, "deduplicate": deduplicate, "items": items},
|
|
511
|
+
)
|
|
512
|
+
return raw if isinstance(raw, dict) else {}
|
|
513
|
+
|
|
514
|
+
async def reflect(
|
|
515
|
+
self, *, lane: str, user_id: str | None = None, include_linked_runs: bool = False
|
|
516
|
+
) -> dict:
|
|
517
|
+
raw = await threadpool.run(
|
|
518
|
+
self._client.reflect,
|
|
519
|
+
session_id=lane,
|
|
520
|
+
user_id=user_id,
|
|
521
|
+
include_linked_runs=include_linked_runs,
|
|
522
|
+
)
|
|
523
|
+
return raw if isinstance(raw, dict) else {}
|
|
524
|
+
|
|
525
|
+
async def surface_strategies(
|
|
526
|
+
self, *, lane: str, lesson_types: Sequence[str] | None = None, max_strategies: int = 5
|
|
527
|
+
) -> dict:
|
|
528
|
+
raw = await threadpool.run(
|
|
529
|
+
self._client.surface_strategies,
|
|
530
|
+
session_id=lane,
|
|
531
|
+
lesson_types=list(lesson_types) if lesson_types else None,
|
|
532
|
+
max_strategies=max_strategies,
|
|
533
|
+
)
|
|
534
|
+
return raw if isinstance(raw, dict) else {}
|
|
535
|
+
|
|
536
|
+
@property
|
|
537
|
+
def endpoint(self) -> str:
|
|
538
|
+
return self._endpoint
|
|
539
|
+
|
|
540
|
+
async def health(self) -> dict:
|
|
541
|
+
"""Liveness probe via Mubit's core health route (no embedding, fast)."""
|
|
542
|
+
base = self._endpoint.rstrip("/")
|
|
543
|
+
url = f"{base}/v2/core/health"
|
|
544
|
+
headers = {"Authorization": f"Bearer {self._api_key}"} if self._api_key else {}
|
|
545
|
+
try:
|
|
546
|
+
headers = {"Authorization": f"Bearer {self._api_key}"} if self._api_key else {}
|
|
547
|
+
async with httpx.AsyncClient(timeout=3.0) as http:
|
|
548
|
+
resp = await http.get(url, headers=headers)
|
|
549
|
+
return {
|
|
550
|
+
"reachable": resp.status_code == 200,
|
|
551
|
+
"transport": self._transport,
|
|
552
|
+
"status_code": resp.status_code,
|
|
553
|
+
}
|
|
554
|
+
except Exception as exc: # noqa: BLE001
|
|
555
|
+
return {
|
|
556
|
+
"reachable": False,
|
|
557
|
+
"transport": self._transport,
|
|
558
|
+
"error": str(exc),
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _extract_record_id(raw: object) -> str | None:
|
|
563
|
+
if not isinstance(raw, Mapping):
|
|
564
|
+
return None
|
|
565
|
+
traces = raw.get("traces") or []
|
|
566
|
+
if not traces or not isinstance(traces[0], Mapping):
|
|
567
|
+
return None
|
|
568
|
+
writes = traces[0].get("writes") or []
|
|
569
|
+
if not writes or not isinstance(writes[0], Mapping):
|
|
570
|
+
return None
|
|
571
|
+
record_id = writes[0].get("record_id")
|
|
572
|
+
return str(record_id) if record_id else None
|
minima/memory/keys.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Deterministic builders for Mubit keys, lanes, fingerprints, and content gists."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def normalize_task_text(text: str, max_chars: int = 512) -> str:
|
|
10
|
+
"""Collapse whitespace and truncate, so paraphrases embed similarly."""
|
|
11
|
+
collapsed = " ".join(text.split())
|
|
12
|
+
return collapsed[:max_chars]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def task_fingerprint(text: str) -> str:
|
|
16
|
+
"""Stable hash of the normalized task text (non-cryptographic use)."""
|
|
17
|
+
norm = " ".join(text.lower().split())
|
|
18
|
+
return hashlib.sha1(norm.encode("utf-8")).hexdigest() # noqa: S324
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Common low-signal tokens dropped before building a fine-cluster signature, so the
|
|
22
|
+
# bucket reflects the task's salient nouns/verbs rather than filler.
|
|
23
|
+
_STOPWORDS = frozenset(
|
|
24
|
+
"""
|
|
25
|
+
a an and are as at be by can could do does for from given has have how i if in
|
|
26
|
+
into is it its me my of on or please that the their then there these this to
|
|
27
|
+
use using want was we what when where which who why will with would you your
|
|
28
|
+
""".split()
|
|
29
|
+
)
|
|
30
|
+
_WORD = re.compile(r"[a-z0-9]+")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def salient_signature(text: str, max_tokens: int = 4) -> str:
|
|
34
|
+
"""A short, stable bucket id derived from a task's most salient tokens.
|
|
35
|
+
|
|
36
|
+
Lowercases, drops stopwords and very short tokens, keeps the longest distinct
|
|
37
|
+
tokens (longer words carry more topic signal), sorts for order-independence, and
|
|
38
|
+
hashes. Paraphrases that share salient vocabulary land in the same bucket; this
|
|
39
|
+
is a deterministic, embedding-free approximation of a topic cluster.
|
|
40
|
+
"""
|
|
41
|
+
tokens = [t for t in _WORD.findall(text.lower()) if len(t) >= 4 and t not in _STOPWORDS]
|
|
42
|
+
if not tokens:
|
|
43
|
+
return "general"
|
|
44
|
+
# Distinct, longest-first, then alphabetical for a stable top-k selection.
|
|
45
|
+
ranked = sorted(set(tokens), key=lambda t: (-len(t), t))[:max_tokens]
|
|
46
|
+
key = " ".join(sorted(ranked))
|
|
47
|
+
return hashlib.sha1(key.encode("utf-8")).hexdigest()[:8] # noqa: S324
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def task_cluster(task_type: str, difficulty: str, signature: str | None = None) -> str:
|
|
51
|
+
"""Cluster used as the upsert grouping key, e.g. ``code:hard`` (coarse) or
|
|
52
|
+
``code:hard:1a2b3c4d`` (fine, when a keyword signature is supplied)."""
|
|
53
|
+
base = f"{task_type}:{difficulty}"
|
|
54
|
+
return f"{base}:{signature}" if signature else base
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def build_content(task_type: str, difficulty: str, text: str, max_chars: int = 512) -> str:
|
|
58
|
+
"""The text Mubit embeds: a task gist prefixed with type/difficulty tags."""
|
|
59
|
+
return f"[{task_type}/{difficulty}] {normalize_task_text(text, max_chars)}"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def outcome_upsert_key(cluster: str, model_id: str) -> str:
|
|
63
|
+
"""One durable outcome record per (task-cluster, model)."""
|
|
64
|
+
return f"minima:om:{cluster}:{model_id}"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def outcome_idempotency_key(recommendation_id: str, model_id: str) -> str:
|
|
68
|
+
raw = f"{recommendation_id}:{model_id}"
|
|
69
|
+
return "oc:" + hashlib.sha1(raw.encode("utf-8")).hexdigest()[:16] # noqa: S324
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def lesson_upsert_key(cluster: str, model_id: str) -> str:
|
|
73
|
+
"""One durable lesson per (task-cluster, model) so repeated verified-prod wins
|
|
74
|
+
reinforce a single lesson rather than flooding LTM."""
|
|
75
|
+
return f"minima:lesson:{cluster}:{model_id}"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def build_lesson_content(cluster: str, model_id: str, quality: float) -> str:
|
|
79
|
+
"""A compact NL lesson gist, embedded so reflect()/surface_strategies can cluster it."""
|
|
80
|
+
return (
|
|
81
|
+
f"For {cluster} tasks, {model_id} is a reliable, cost-effective choice "
|
|
82
|
+
f"(verified in production at ~{quality:.0%} quality)."
|
|
83
|
+
)
|