structuremappingmemory 1.0.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.
- sma/__init__.py +5 -0
- sma/__main__.py +5 -0
- sma/agent/__init__.py +5 -0
- sma/agent/adapter_draft.py +217 -0
- sma/agent/api.py +67 -0
- sma/agent/comparison.py +591 -0
- sma/agent/llm.py +280 -0
- sma/agent/policies.py +21 -0
- sma/agent/service.py +95 -0
- sma/cli.py +65 -0
- sma/encoders/__init__.py +38 -0
- sma/encoders/agentobs.py +27 -0
- sma/encoders/base.py +23 -0
- sma/encoders/code_treesitter.py +64 -0
- sma/encoders/coverage.py +80 -0
- sma/encoders/draft_adapter.py +183 -0
- sma/encoders/healthcare.py +207 -0
- sma/encoders/logs_drain.py +142 -0
- sma/encoders/prose_tier1.py +57 -0
- sma/encoders/structured.py +57 -0
- sma/encoders/traces.py +45 -0
- sma/eval/__init__.py +2 -0
- sma/eval/agentic/__init__.py +35 -0
- sma/eval/agentic/arms/__init__.py +0 -0
- sma/eval/agentic/arms/cyber.py +48 -0
- sma/eval/agentic/arms/discovery.py +35 -0
- sma/eval/agentic/arms/finance.py +38 -0
- sma/eval/agentic/arms/legal.py +74 -0
- sma/eval/agentic/arms/medicine.py +45 -0
- sma/eval/agentic/harness.py +275 -0
- sma/eval/agentic/memories.py +308 -0
- sma/eval/agentic/metrics.py +82 -0
- sma/eval/agentic_qa/__init__.py +27 -0
- sma/eval/agentic_qa/agent.py +383 -0
- sma/eval/agentic_qa/metrics.py +239 -0
- sma/eval/agentic_qa/pools.py +197 -0
- sma/eval/arn.py +65 -0
- sma/eval/baselines/__init__.py +6 -0
- sma/eval/baselines/bge_dense.py +54 -0
- sma/eval/baselines/bm25.py +18 -0
- sma/eval/baselines/dense.py +42 -0
- sma/eval/baselines/hipporag.py +235 -0
- sma/eval/baselines/hybrid_rrf.py +30 -0
- sma/eval/baselines/longcontext_llm.py +124 -0
- sma/eval/baselines/rerank.py +41 -0
- sma/eval/baselines/splade.py +77 -0
- sma/eval/baselines/wl_kernel.py +163 -0
- sma/eval/bugsinpy.py +358 -0
- sma/eval/bugsinpy_families.py +164 -0
- sma/eval/crossdomain.py +89 -0
- sma/eval/diabetes.py +61 -0
- sma/eval/drift_env.py +26 -0
- sma/eval/drift_metrics.py +24 -0
- sma/eval/family_labels.py +167 -0
- sma/eval/fraud_elliptic/__init__.py +29 -0
- sma/eval/fraud_elliptic/encoder.py +279 -0
- sma/eval/fraud_elliptic/eval.py +269 -0
- sma/eval/fraud_elliptic/test_encoder.py +123 -0
- sma/eval/ieee_cis.py +66 -0
- sma/eval/loghub.py +16 -0
- sma/eval/loghub_eval.py +480 -0
- sma/eval/longmemeval.py +51 -0
- sma/eval/memory_backends/__init__.py +2 -0
- sma/eval/memory_backends/base.py +22 -0
- sma/eval/memory_backends/context_only.py +14 -0
- sma/eval/memory_backends/rag_notes.py +17 -0
- sma/eval/memory_backends/shared_llm.py +30 -0
- sma/eval/memory_backends/sma_memory.py +54 -0
- sma/eval/memory_backends/zep_graphiti.py +33 -0
- sma/eval/metrics.py +32 -0
- sma/eval/ontology_bench.py +219 -0
- sma/eval/report.py +573 -0
- sma/eval/ssb_eval.py +216 -0
- sma/eval/ssb_generator.py +116 -0
- sma/eval/stats.py +108 -0
- sma/eval/transfer_eval.py +844 -0
- sma/index/__init__.py +15 -0
- sma/index/ann.py +21 -0
- sma/index/content_vectors.py +60 -0
- sma/index/inverted.py +63 -0
- sma/index/macfac.py +174 -0
- sma/ir/__init__.py +22 -0
- sma/ir/canon.py +106 -0
- sma/ir/schema.py +165 -0
- sma/ir/sexpr.py +86 -0
- sma/ir/signatures.py +76 -0
- sma/match/__init__.py +20 -0
- sma/match/conflicts.py +46 -0
- sma/match/engine.py +60 -0
- sma/match/explain.py +59 -0
- sma/match/infer.py +54 -0
- sma/match/kernels.py +54 -0
- sma/match/mdl.py +30 -0
- sma/match/merge_cpsat.py +77 -0
- sma/match/merge_greedy.py +15 -0
- sma/match/mh.py +177 -0
- sma/match/ses.py +84 -0
- sma/match/types.py +115 -0
- sma/match/verifier.py +27 -0
- sma/ontology/__init__.py +45 -0
- sma/ontology/attack.py +134 -0
- sma/ontology/cpc.py +69 -0
- sma/ontology/graph.py +58 -0
- sma/ontology/loader.py +262 -0
- sma/ontology/mitre_xml.py +67 -0
- sma/ontology/mount.py +101 -0
- sma/ontology/rdf_loader.py +75 -0
- sma/ontology/registry.py +115 -0
- sma/ontology/router.py +69 -0
- sma/ontology/usgaap.py +73 -0
- sma/sage/__init__.py +6 -0
- sma/sage/assimilate.py +12 -0
- sma/sage/pools.py +105 -0
- sma/sage/probabilities.py +10 -0
- sma/store/__init__.py +6 -0
- sma/store/lmdb_store.py +78 -0
- sma/store/registry.py +26 -0
- sma/store/wal.py +26 -0
- sma/ui/app.py +642 -0
- structuremappingmemory-1.0.0.dist-info/METADATA +190 -0
- structuremappingmemory-1.0.0.dist-info/RECORD +125 -0
- structuremappingmemory-1.0.0.dist-info/WHEEL +5 -0
- structuremappingmemory-1.0.0.dist-info/entry_points.txt +2 -0
- structuremappingmemory-1.0.0.dist-info/licenses/LICENSE +204 -0
- structuremappingmemory-1.0.0.dist-info/top_level.txt +1 -0
sma/agent/llm.py
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""LLM orchestration layer.
|
|
2
|
+
|
|
3
|
+
The LLM is intentionally downstream of extraction and retrieval. It receives a
|
|
4
|
+
mode, a user question, and already-retrieved evidence, then verbalizes an
|
|
5
|
+
answer. It cannot write facts to memory or affect candidate generation.
|
|
6
|
+
|
|
7
|
+
Two interchangeable backends:
|
|
8
|
+
- LocalOrchestrator: quantized Qwen GGUF via llama-cpp (CPU, offline)
|
|
9
|
+
- DeepSeekOrchestrator: DeepSeek's OpenAI-compatible API (needs
|
|
10
|
+
SMA_DEEPSEEK_API_KEY in the environment or repo .env)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
DEFAULT_MODEL_REPO = "Qwen/Qwen2.5-0.5B-Instruct-GGUF"
|
|
21
|
+
DEFAULT_MODEL_FILE = "qwen2.5-0.5b-instruct-q4_k_m.gguf"
|
|
22
|
+
DEFAULT_MODEL_PATH = "models/qwen2.5-0.5b-instruct-q4_k_m.gguf"
|
|
23
|
+
|
|
24
|
+
DEEPSEEK_BASE_URL = "https://api.deepseek.com"
|
|
25
|
+
DEEPSEEK_MODEL = "deepseek-chat"
|
|
26
|
+
DEEPSEEK_KEY_ENV = "SMA_DEEPSEEK_API_KEY"
|
|
27
|
+
|
|
28
|
+
SYSTEM_PROMPT = (
|
|
29
|
+
"You are the answer writer for SMA-1, an agentic memory system. Retrieval is already "
|
|
30
|
+
"complete; you only verbalize. The evidence items are PAST incidents retrieved from memory "
|
|
31
|
+
"as candidate precedents for the user's input. If the input is itself a new incident (e.g. "
|
|
32
|
+
"raw log lines), do NOT look for those literal entries - hostnames, dates and IDs will "
|
|
33
|
+
"always differ. Instead say which precedent has the most similar failure pattern (the "
|
|
34
|
+
"sequence/causal shape of events), what happened in it, and what that suggests here, citing "
|
|
35
|
+
"items like [1]. Only when no precedent shares even the failure pattern, say so plainly. "
|
|
36
|
+
"For ordinary questions, answer strictly from the evidence. Reply in 2-5 short sentences. "
|
|
37
|
+
"Never repeat yourself."
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
MAX_HISTORY_TURNS = 8
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def build_messages(
|
|
45
|
+
question: str, mode: str, evidence: list[dict], history: list[dict] | None = None
|
|
46
|
+
) -> list[dict]:
|
|
47
|
+
# Plain numbered texts only: provenance hashes and scores are for the UI
|
|
48
|
+
# and audit trail, not the verbalizer — small models parrot them back.
|
|
49
|
+
# 900 chars/item: the H3 judge pass showed a 400-char cap truncates exactly
|
|
50
|
+
# the anomaly lines questions ask about (abstention artifacts); 900 x 5
|
|
51
|
+
# items stays within the local model's 4k context alongside chat history.
|
|
52
|
+
def _item(i: int, row: dict) -> str:
|
|
53
|
+
why = row.get("alignment")
|
|
54
|
+
head = f"[{i + 1}]" + (f" (why retrieved: {why})" if why else "")
|
|
55
|
+
return f"{head} {row.get('text', '')[:900]}"
|
|
56
|
+
|
|
57
|
+
# Coverage warnings (blueprint 12-R3) ride along as pseudo-rows with a
|
|
58
|
+
# "warning" key and no text; surface them as caveats, not evidence items.
|
|
59
|
+
warnings = [row["warning"] for row in evidence if row.get("warning")]
|
|
60
|
+
rows = [row for row in evidence if not row.get("warning")]
|
|
61
|
+
evidence_text = "\n".join(_item(i, row) for i, row in enumerate(rows[:8]))
|
|
62
|
+
warning_text = "".join(f"\nCaveat: {w}." for w in warnings)
|
|
63
|
+
window_caveat = (
|
|
64
|
+
"\nCaveat: each evidence item is one bounded session window. Events outside a "
|
|
65
|
+
"window are not recorded in it - the absence of an event in the evidence is NOT "
|
|
66
|
+
"evidence that it did not happen. Never infer outcomes from absence."
|
|
67
|
+
if rows
|
|
68
|
+
else ""
|
|
69
|
+
)
|
|
70
|
+
user = (
|
|
71
|
+
f"Evidence retrieved by the '{mode}' memory for the latest question:\n"
|
|
72
|
+
f"{evidence_text or '(none retrieved)'}{window_caveat}{warning_text}\n\n"
|
|
73
|
+
f"Question: {question}"
|
|
74
|
+
)
|
|
75
|
+
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
|
|
76
|
+
for turn in (history or [])[-MAX_HISTORY_TURNS * 2:]:
|
|
77
|
+
role = turn.get("role")
|
|
78
|
+
content = turn.get("content")
|
|
79
|
+
if role in ("user", "assistant") and isinstance(content, str) and content:
|
|
80
|
+
messages.append({"role": role, "content": content})
|
|
81
|
+
messages.append({"role": "user", "content": user})
|
|
82
|
+
return messages
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _env_key(name: str) -> str | None:
|
|
86
|
+
"""Read a key from the environment, falling back to the repo .env file."""
|
|
87
|
+
value = os.environ.get(name)
|
|
88
|
+
if value:
|
|
89
|
+
return value
|
|
90
|
+
env_path = Path(".env")
|
|
91
|
+
if env_path.exists():
|
|
92
|
+
for line in env_path.read_text(encoding="utf-8").splitlines():
|
|
93
|
+
line = line.strip()
|
|
94
|
+
if line.startswith(f"{name}="):
|
|
95
|
+
return line.split("=", 1)[1].strip()
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass(frozen=True)
|
|
100
|
+
class OrchestratorConfig:
|
|
101
|
+
model_path: str = DEFAULT_MODEL_PATH
|
|
102
|
+
n_ctx: int = 4096
|
|
103
|
+
n_threads: int = 8
|
|
104
|
+
temperature: float = 0.2
|
|
105
|
+
max_tokens: int = 220
|
|
106
|
+
repeat_penalty: float = 1.25
|
|
107
|
+
top_p: float = 0.9
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class LocalOrchestrator:
|
|
111
|
+
name = "local"
|
|
112
|
+
|
|
113
|
+
def __init__(self, config: OrchestratorConfig | None = None):
|
|
114
|
+
self.config = config or OrchestratorConfig(
|
|
115
|
+
model_path=os.environ.get("SMA_LLM_MODEL", DEFAULT_MODEL_PATH)
|
|
116
|
+
)
|
|
117
|
+
self._llm = None
|
|
118
|
+
self._load_error: str | None = None
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def status(self) -> dict:
|
|
122
|
+
if self._llm is not None:
|
|
123
|
+
return {
|
|
124
|
+
"backend": "llama_cpp",
|
|
125
|
+
"model": Path(self.config.model_path).name,
|
|
126
|
+
"loaded": True,
|
|
127
|
+
}
|
|
128
|
+
path = Path(self.config.model_path)
|
|
129
|
+
return {
|
|
130
|
+
"backend": "deterministic_fallback" if not path.exists() else "llama_cpp",
|
|
131
|
+
"model": Path(self.config.model_path).name,
|
|
132
|
+
"loaded": False,
|
|
133
|
+
"load_error": self._load_error or ("model file missing" if not path.exists() else ""),
|
|
134
|
+
"recommended_model": f"{DEFAULT_MODEL_REPO}/{DEFAULT_MODEL_FILE}",
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
def _ensure_loaded(self) -> bool:
|
|
138
|
+
if self._llm is not None:
|
|
139
|
+
return True
|
|
140
|
+
path = Path(self.config.model_path)
|
|
141
|
+
if not path.exists():
|
|
142
|
+
self._load_error = "model file missing; run scripts/fetch_model.py"
|
|
143
|
+
return False
|
|
144
|
+
try:
|
|
145
|
+
from llama_cpp import Llama
|
|
146
|
+
|
|
147
|
+
self._llm = Llama(
|
|
148
|
+
model_path=str(path),
|
|
149
|
+
n_ctx=self.config.n_ctx,
|
|
150
|
+
n_threads=self.config.n_threads,
|
|
151
|
+
verbose=False,
|
|
152
|
+
)
|
|
153
|
+
return True
|
|
154
|
+
except Exception as exc: # pragma: no cover - depends on optional runtime
|
|
155
|
+
self._load_error = str(exc)
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
def answer(
|
|
159
|
+
self, question: str, mode: str, evidence: list[dict], history: list[dict] | None = None
|
|
160
|
+
) -> str:
|
|
161
|
+
if not self._ensure_loaded():
|
|
162
|
+
return fallback_answer(question, mode, evidence, self.status)
|
|
163
|
+
# Chat completion uses the model's own instruct template (raw completion
|
|
164
|
+
# makes small Qwen models ramble); repeat_penalty + tight max_tokens
|
|
165
|
+
# stop the looping/repetition failure mode of 0.5B quantized models.
|
|
166
|
+
response = self._llm.create_chat_completion( # type: ignore[union-attr]
|
|
167
|
+
messages=build_messages(question, mode, evidence, history),
|
|
168
|
+
max_tokens=self.config.max_tokens,
|
|
169
|
+
temperature=self.config.temperature,
|
|
170
|
+
top_p=self.config.top_p,
|
|
171
|
+
repeat_penalty=self.config.repeat_penalty,
|
|
172
|
+
)
|
|
173
|
+
text = (response["choices"][0]["message"]["content"] or "").strip()
|
|
174
|
+
return text or fallback_answer(question, mode, evidence, self.status)
|
|
175
|
+
|
|
176
|
+
def complete(self, messages: list[dict], max_tokens: int = 600, temperature: float = 0.0) -> str:
|
|
177
|
+
"""Raw chat completion for non-verbalizer callers (e.g. adapter drafting).
|
|
178
|
+
|
|
179
|
+
Raises RuntimeError when the local model is unavailable - drafting has
|
|
180
|
+
no deterministic fallback, by design (rules either come from a model or
|
|
181
|
+
a human, never from a heuristic pretending to be one).
|
|
182
|
+
"""
|
|
183
|
+
if not self._ensure_loaded():
|
|
184
|
+
raise RuntimeError(f"local model unavailable: {self._load_error}")
|
|
185
|
+
response = self._llm.create_chat_completion( # type: ignore[union-attr]
|
|
186
|
+
messages=messages,
|
|
187
|
+
max_tokens=max_tokens,
|
|
188
|
+
temperature=temperature,
|
|
189
|
+
top_p=self.config.top_p,
|
|
190
|
+
repeat_penalty=self.config.repeat_penalty,
|
|
191
|
+
)
|
|
192
|
+
return (response["choices"][0]["message"]["content"] or "").strip()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class DeepSeekOrchestrator:
|
|
196
|
+
name = "deepseek"
|
|
197
|
+
|
|
198
|
+
def __init__(self, model: str = DEEPSEEK_MODEL, api_key: str | None = None):
|
|
199
|
+
self.model = model
|
|
200
|
+
self._api_key = api_key or _env_key(DEEPSEEK_KEY_ENV)
|
|
201
|
+
self._last_error: str | None = None
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def status(self) -> dict:
|
|
205
|
+
return {
|
|
206
|
+
"backend": "deepseek_api",
|
|
207
|
+
"model": self.model,
|
|
208
|
+
"key_present": bool(self._api_key),
|
|
209
|
+
"last_error": self._last_error or "",
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
def answer(
|
|
213
|
+
self, question: str, mode: str, evidence: list[dict], history: list[dict] | None = None
|
|
214
|
+
) -> str:
|
|
215
|
+
if not self._api_key:
|
|
216
|
+
self._last_error = f"{DEEPSEEK_KEY_ENV} not set"
|
|
217
|
+
return fallback_answer(question, mode, evidence, self.status)
|
|
218
|
+
try:
|
|
219
|
+
import httpx
|
|
220
|
+
|
|
221
|
+
response = httpx.post(
|
|
222
|
+
f"{DEEPSEEK_BASE_URL}/chat/completions",
|
|
223
|
+
headers={"Authorization": f"Bearer {self._api_key}"},
|
|
224
|
+
json={
|
|
225
|
+
"model": self.model,
|
|
226
|
+
"messages": build_messages(question, mode, evidence, history),
|
|
227
|
+
"temperature": 0.3,
|
|
228
|
+
"max_tokens": 400,
|
|
229
|
+
},
|
|
230
|
+
timeout=60.0,
|
|
231
|
+
)
|
|
232
|
+
response.raise_for_status()
|
|
233
|
+
self._last_error = None
|
|
234
|
+
text = (response.json()["choices"][0]["message"]["content"] or "").strip()
|
|
235
|
+
return text or fallback_answer(question, mode, evidence, self.status)
|
|
236
|
+
except Exception as exc:
|
|
237
|
+
self._last_error = f"{type(exc).__name__}: {exc}"
|
|
238
|
+
return fallback_answer(question, mode, evidence, self.status)
|
|
239
|
+
|
|
240
|
+
def complete(self, messages: list[dict], max_tokens: int = 600, temperature: float = 0.0) -> str:
|
|
241
|
+
"""Raw chat completion for non-verbalizer callers (e.g. adapter drafting)."""
|
|
242
|
+
if not self._api_key:
|
|
243
|
+
raise RuntimeError(f"{DEEPSEEK_KEY_ENV} not set")
|
|
244
|
+
import httpx
|
|
245
|
+
|
|
246
|
+
response = httpx.post(
|
|
247
|
+
f"{DEEPSEEK_BASE_URL}/chat/completions",
|
|
248
|
+
headers={"Authorization": f"Bearer {self._api_key}"},
|
|
249
|
+
json={
|
|
250
|
+
"model": self.model,
|
|
251
|
+
"messages": messages,
|
|
252
|
+
"temperature": temperature,
|
|
253
|
+
"max_tokens": max_tokens,
|
|
254
|
+
},
|
|
255
|
+
timeout=60.0,
|
|
256
|
+
)
|
|
257
|
+
response.raise_for_status()
|
|
258
|
+
return (response.json()["choices"][0]["message"]["content"] or "").strip()
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def fallback_answer(question: str, mode: str, evidence: list[dict], status: dict) -> str:
|
|
262
|
+
evidence = [row for row in evidence if not row.get("warning")]
|
|
263
|
+
if not evidence:
|
|
264
|
+
return (
|
|
265
|
+
f"No evidence was retrieved for `{mode}`. Local LLM status: "
|
|
266
|
+
f"{status.get('load_error') or status.get('last_error') or status.get('backend')}."
|
|
267
|
+
)
|
|
268
|
+
top = evidence[0]
|
|
269
|
+
lines = [
|
|
270
|
+
f"Mode `{mode}` retrieved {len(evidence)} evidence item(s).",
|
|
271
|
+
f"Top evidence: {top.get('text', '')}",
|
|
272
|
+
f"Provenance: {top.get('provenance', top.get('source_id', ''))}",
|
|
273
|
+
"Local LLM is not available, so this is a deterministic evidence summary "
|
|
274
|
+
f"rather than generated prose ({status.get('load_error') or status.get('last_error') or 'no backend'}).",
|
|
275
|
+
]
|
|
276
|
+
return "\n".join(lines)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
default_orchestrator = LocalOrchestrator()
|
|
280
|
+
default_deepseek = DeepSeekOrchestrator()
|
sma/agent/policies.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Hard API policies for SMA agent memory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def reject_free_text_facts(annotation) -> None:
|
|
7
|
+
if isinstance(annotation, str):
|
|
8
|
+
raise ValueError("free-text facts are rejected; route annotations through encode()")
|
|
9
|
+
if isinstance(annotation, dict) and "sexpr" not in annotation and "case_id" not in annotation:
|
|
10
|
+
raise ValueError("annotations must be encoded cases or canonical S-expressions")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def require_provenance(claims: list[dict]) -> list[dict]:
|
|
14
|
+
checked: list[dict] = []
|
|
15
|
+
for claim in claims:
|
|
16
|
+
if not claim.get("provenance"):
|
|
17
|
+
claim = dict(claim)
|
|
18
|
+
claim["status"] = "unsupported-by-memory"
|
|
19
|
+
checked.append(claim)
|
|
20
|
+
return checked
|
|
21
|
+
|
sma/agent/service.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""In-process SMA memory service used by CLI, API, and UI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import asdict
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from sma.encoders import get_encoder
|
|
9
|
+
from sma.index.macfac import MacFacIndex
|
|
10
|
+
from sma.ir.schema import Case, make_case
|
|
11
|
+
from sma.ir.sexpr import canonical_case_text, loads_case
|
|
12
|
+
from sma.match.engine import match_cases
|
|
13
|
+
from sma.match.explain import correspondence_table, explain_text
|
|
14
|
+
from sma.match.infer import candidate_inferences
|
|
15
|
+
from sma.match.types import MatchConfig
|
|
16
|
+
from sma.match.verifier import verify_inference
|
|
17
|
+
from sma.sage.pools import SagePool
|
|
18
|
+
from sma.store import CaseStore
|
|
19
|
+
|
|
20
|
+
from .policies import reject_free_text_facts
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class MemoryService:
|
|
24
|
+
def __init__(self, store_path: str | Path = "data/processed/store"):
|
|
25
|
+
self.store = CaseStore(store_path)
|
|
26
|
+
self.config = MatchConfig(gamma=0.25, rho=0.5, delta=2)
|
|
27
|
+
self.index = MacFacIndex(self.config)
|
|
28
|
+
self.gmaps = {}
|
|
29
|
+
self.pools: dict[str, SagePool] = {}
|
|
30
|
+
for case in self.store.iter_cases():
|
|
31
|
+
self.index.add(case)
|
|
32
|
+
|
|
33
|
+
def encode(self, artifact: str, adapter_id: str, **kwargs) -> dict:
|
|
34
|
+
result = get_encoder(adapter_id).encode(artifact, **kwargs)
|
|
35
|
+
self.store.put(result.case)
|
|
36
|
+
self.index.add(result.case)
|
|
37
|
+
return {
|
|
38
|
+
"case_id": result.case.case_id,
|
|
39
|
+
"n_statements": len(result.case.statements),
|
|
40
|
+
"warnings": list(result.warnings),
|
|
41
|
+
"sexpr": canonical_case_text(result.case.statements),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
def inline_case(self, sexpr: str) -> Case:
|
|
45
|
+
return make_case(loads_case(sexpr), {"adapter": "inline"})
|
|
46
|
+
|
|
47
|
+
def retrieve(self, case_id: str | None = None, inline_case: str | None = None, k: int = 10) -> list[dict]:
|
|
48
|
+
query = self.store.get(case_id) if case_id else self.inline_case(inline_case or "")
|
|
49
|
+
return [asdict(result) for result in self.index.retrieve(query, k=k)]
|
|
50
|
+
|
|
51
|
+
def map(self, base_id: str, target_id: str, scorer: str = "ses") -> dict:
|
|
52
|
+
config = MatchConfig(
|
|
53
|
+
gamma=self.config.gamma,
|
|
54
|
+
rho=self.config.rho,
|
|
55
|
+
delta=self.config.delta,
|
|
56
|
+
scorer=scorer,
|
|
57
|
+
)
|
|
58
|
+
gmap = match_cases(self.store.get(base_id), self.store.get(target_id), config=config)
|
|
59
|
+
gmap_id = f"{base_id[:8]}_{target_id[:8]}_{scorer}"
|
|
60
|
+
self.gmaps[gmap_id] = gmap
|
|
61
|
+
return {
|
|
62
|
+
"gmap_id": gmap_id,
|
|
63
|
+
"correspondences": gmap.correspondences,
|
|
64
|
+
"SES_n": gmap.normalized_score,
|
|
65
|
+
"gap": gmap.optimality_gap,
|
|
66
|
+
"kernels_used": len(gmap.kernels),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
def project(self, gmap_id: str) -> list[dict]:
|
|
70
|
+
return [asdict(inference) for inference in candidate_inferences(self.gmaps[gmap_id])]
|
|
71
|
+
|
|
72
|
+
def verify(self, inference: str) -> dict:
|
|
73
|
+
result = verify_inference(inference)
|
|
74
|
+
return asdict(result)
|
|
75
|
+
|
|
76
|
+
def store_annotations(self, case_id: str, outcome_annotations) -> dict:
|
|
77
|
+
reject_free_text_facts(outcome_annotations)
|
|
78
|
+
return {"status": "ok", "case_id": case_id}
|
|
79
|
+
|
|
80
|
+
def generalize(self, pool_id: str, case_id: str) -> dict:
|
|
81
|
+
pool = self.pools.setdefault(pool_id, SagePool(pool_id=pool_id, config=self.config))
|
|
82
|
+
assignment = pool.assimilate(self.store.get(case_id))
|
|
83
|
+
return {"pool_id": pool_id, "assignment": assignment}
|
|
84
|
+
|
|
85
|
+
def pool_stats(self, pool_id: str) -> dict:
|
|
86
|
+
pool = self.pools.setdefault(pool_id, SagePool(pool_id=pool_id, config=self.config))
|
|
87
|
+
return pool.stats()
|
|
88
|
+
|
|
89
|
+
def explain(self, gmap_id: str) -> dict:
|
|
90
|
+
gmap = self.gmaps[gmap_id]
|
|
91
|
+
return {"text": explain_text(gmap), "table": correspondence_table(gmap)}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
default_service = MemoryService()
|
|
95
|
+
|
sma/cli.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""CLI for SMA MVP."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from sma.agent.service import MemoryService
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
13
|
+
parser = argparse.ArgumentParser(prog="sma")
|
|
14
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
15
|
+
|
|
16
|
+
enc = sub.add_parser("encode")
|
|
17
|
+
enc.add_argument("adapter_id")
|
|
18
|
+
enc.add_argument("artifact_file")
|
|
19
|
+
enc.add_argument("--store", default="data/processed/store")
|
|
20
|
+
|
|
21
|
+
ret = sub.add_parser("retrieve")
|
|
22
|
+
ret.add_argument("case_id")
|
|
23
|
+
ret.add_argument("--k", type=int, default=10)
|
|
24
|
+
ret.add_argument("--store", default="data/processed/store")
|
|
25
|
+
|
|
26
|
+
mp = sub.add_parser("map")
|
|
27
|
+
mp.add_argument("base_id")
|
|
28
|
+
mp.add_argument("target_id")
|
|
29
|
+
mp.add_argument("--scorer", default="ses")
|
|
30
|
+
mp.add_argument("--store", default="data/processed/store")
|
|
31
|
+
|
|
32
|
+
report = sub.add_parser("report")
|
|
33
|
+
report.add_argument("--out", default="reports/report.html")
|
|
34
|
+
|
|
35
|
+
sub.add_parser("ui")
|
|
36
|
+
return parser
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def main(argv: list[str] | None = None) -> None:
|
|
40
|
+
args = build_parser().parse_args(argv)
|
|
41
|
+
if args.cmd == "report":
|
|
42
|
+
from sma.eval.report import main as report_main
|
|
43
|
+
|
|
44
|
+
raise SystemExit(report_main(["--out", args.out]))
|
|
45
|
+
if args.cmd == "ui":
|
|
46
|
+
from sma.ui.app import main as ui_main
|
|
47
|
+
|
|
48
|
+
raise SystemExit(ui_main([]))
|
|
49
|
+
|
|
50
|
+
service = MemoryService(getattr(args, "store", "data/processed/store"))
|
|
51
|
+
if args.cmd == "encode":
|
|
52
|
+
artifact = open(args.artifact_file, encoding="utf-8").read()
|
|
53
|
+
print(json.dumps(service.encode(artifact, args.adapter_id), indent=2))
|
|
54
|
+
elif args.cmd == "retrieve":
|
|
55
|
+
print(json.dumps(service.retrieve(case_id=args.case_id, k=args.k), indent=2))
|
|
56
|
+
elif args.cmd == "map":
|
|
57
|
+
print(json.dumps(service.map(args.base_id, args.target_id, args.scorer), indent=2))
|
|
58
|
+
else:
|
|
59
|
+
print(f"unknown command: {args.cmd}", file=sys.stderr)
|
|
60
|
+
raise SystemExit(2)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
main()
|
|
65
|
+
|
sma/encoders/__init__.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from .agentobs import AgentObservationEncoder
|
|
2
|
+
from .base import EncodeResult
|
|
3
|
+
from .code_treesitter import CodeEncoder
|
|
4
|
+
from .logs_drain import LogEncoder
|
|
5
|
+
from .prose_tier1 import ProseTier1Encoder
|
|
6
|
+
from .structured import StructuredEncoder
|
|
7
|
+
from .healthcare import HealthcareEncoder
|
|
8
|
+
from .traces import TraceEncoder
|
|
9
|
+
|
|
10
|
+
ENCODERS = {
|
|
11
|
+
"logs": LogEncoder,
|
|
12
|
+
"code": CodeEncoder,
|
|
13
|
+
"traces": TraceEncoder,
|
|
14
|
+
"structured": StructuredEncoder,
|
|
15
|
+
"healthcare": HealthcareEncoder,
|
|
16
|
+
"agentobs": AgentObservationEncoder,
|
|
17
|
+
"prose_tier1": ProseTier1Encoder,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_encoder(adapter_id: str):
|
|
22
|
+
if adapter_id not in ENCODERS:
|
|
23
|
+
raise KeyError(f"unknown adapter: {adapter_id}")
|
|
24
|
+
return ENCODERS[adapter_id]()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"AgentObservationEncoder",
|
|
29
|
+
"CodeEncoder",
|
|
30
|
+
"ENCODERS",
|
|
31
|
+
"EncodeResult",
|
|
32
|
+
"LogEncoder",
|
|
33
|
+
"ProseTier1Encoder",
|
|
34
|
+
"StructuredEncoder",
|
|
35
|
+
"TraceEncoder",
|
|
36
|
+
"get_encoder",
|
|
37
|
+
]
|
|
38
|
+
|
sma/encoders/agentobs.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Agent observation encoder."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from sma.ir.schema import entity, make_case, stmt
|
|
6
|
+
|
|
7
|
+
from .base import EncodeResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AgentObservationEncoder:
|
|
11
|
+
adapter_id = "agentobs"
|
|
12
|
+
version = "0.1.0"
|
|
13
|
+
|
|
14
|
+
def encode(self, artifact: str, **kwargs) -> EncodeResult:
|
|
15
|
+
command = kwargs.get("command", "tool")
|
|
16
|
+
exit_code = str(kwargs.get("exit_code", 0))
|
|
17
|
+
obs = entity("obs_0", "observation")
|
|
18
|
+
statements = [
|
|
19
|
+
stmt("toolOutput", obs, entity(command, "command")),
|
|
20
|
+
stmt("exitCode", obs, entity(exit_code, "integer")),
|
|
21
|
+
]
|
|
22
|
+
if "error" in artifact.lower() or exit_code != "0":
|
|
23
|
+
statements.append(stmt("failureEvent", obs, entity(command, "command")))
|
|
24
|
+
if artifact.strip():
|
|
25
|
+
statements.append(stmt("outputDigest", obs, entity(str(abs(hash(artifact))), "digest")))
|
|
26
|
+
return EncodeResult(make_case(statements, {"adapter": self.adapter_id, "tier": 0}), ())
|
|
27
|
+
|
sma/encoders/base.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Encoder base classes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Protocol
|
|
7
|
+
|
|
8
|
+
from sma.ir.schema import Case
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class EncodeResult:
|
|
13
|
+
case: Case
|
|
14
|
+
warnings: tuple[str, ...] = ()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Encoder(Protocol):
|
|
18
|
+
adapter_id: str
|
|
19
|
+
version: str
|
|
20
|
+
|
|
21
|
+
def encode(self, artifact: str, **kwargs) -> EncodeResult:
|
|
22
|
+
...
|
|
23
|
+
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Code and bug encoder with Python AST fallback."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
from sma.ir.schema import Statement, entity, make_case, stmt
|
|
9
|
+
|
|
10
|
+
from .base import EncodeResult
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CodeEncoder:
|
|
14
|
+
adapter_id = "code"
|
|
15
|
+
version = "0.1.0"
|
|
16
|
+
|
|
17
|
+
def encode(self, artifact: str, **kwargs) -> EncodeResult:
|
|
18
|
+
language = kwargs.get("language", "python")
|
|
19
|
+
if language == "python":
|
|
20
|
+
return self._encode_python(artifact)
|
|
21
|
+
return self._encode_regex(artifact)
|
|
22
|
+
|
|
23
|
+
def _encode_python(self, artifact: str) -> EncodeResult:
|
|
24
|
+
statements: list[Statement] = []
|
|
25
|
+
try:
|
|
26
|
+
tree = ast.parse(artifact)
|
|
27
|
+
except SyntaxError as exc:
|
|
28
|
+
return EncodeResult(
|
|
29
|
+
make_case([stmt("syntaxError", entity(str(exc.lineno or 0), "line"))], {"adapter": self.adapter_id, "tier": 0}),
|
|
30
|
+
(str(exc),),
|
|
31
|
+
)
|
|
32
|
+
for node in ast.walk(tree):
|
|
33
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
34
|
+
kind = "class" if isinstance(node, ast.ClassDef) else "function"
|
|
35
|
+
statements.append(stmt("defines", entity(kind, "kind"), entity(node.name, kind)))
|
|
36
|
+
elif isinstance(node, ast.Call):
|
|
37
|
+
name = call_name(node.func)
|
|
38
|
+
if name:
|
|
39
|
+
statements.append(stmt("calls", entity("module", "scope"), entity(name, "callable")))
|
|
40
|
+
elif isinstance(node, (ast.Import, ast.ImportFrom)):
|
|
41
|
+
for alias in node.names:
|
|
42
|
+
statements.append(stmt("imports", entity("module", "scope"), entity(alias.name, "module")))
|
|
43
|
+
elif isinstance(node, ast.Raise):
|
|
44
|
+
statements.append(stmt("throws", entity("module", "scope"), entity(type(node).__name__, "exception")))
|
|
45
|
+
elif isinstance(node, ast.ExceptHandler):
|
|
46
|
+
exc = getattr(node.type, "id", "Exception") if node.type is not None else "Exception"
|
|
47
|
+
statements.append(stmt("catches", entity("module", "scope"), entity(exc, "exception")))
|
|
48
|
+
return EncodeResult(make_case(statements or [stmt("emptyCode", entity("module"))], {"adapter": self.adapter_id, "tier": 0}), ())
|
|
49
|
+
|
|
50
|
+
def _encode_regex(self, artifact: str) -> EncodeResult:
|
|
51
|
+
statements: list[Statement] = []
|
|
52
|
+
for name in re.findall(r"\b(?:function|def|class)\s+([A-Za-z_]\w*)", artifact):
|
|
53
|
+
statements.append(stmt("defines", entity("symbol", "kind"), entity(name, "symbol")))
|
|
54
|
+
return EncodeResult(make_case(statements or [stmt("rawCode", entity("module"))], {"adapter": self.adapter_id, "tier": 0}), ())
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def call_name(node: ast.AST) -> str | None:
|
|
58
|
+
if isinstance(node, ast.Name):
|
|
59
|
+
return node.id
|
|
60
|
+
if isinstance(node, ast.Attribute):
|
|
61
|
+
parent = call_name(node.value)
|
|
62
|
+
return f"{parent}.{node.attr}" if parent else node.attr
|
|
63
|
+
return None
|
|
64
|
+
|