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.
Files changed (125) hide show
  1. sma/__init__.py +5 -0
  2. sma/__main__.py +5 -0
  3. sma/agent/__init__.py +5 -0
  4. sma/agent/adapter_draft.py +217 -0
  5. sma/agent/api.py +67 -0
  6. sma/agent/comparison.py +591 -0
  7. sma/agent/llm.py +280 -0
  8. sma/agent/policies.py +21 -0
  9. sma/agent/service.py +95 -0
  10. sma/cli.py +65 -0
  11. sma/encoders/__init__.py +38 -0
  12. sma/encoders/agentobs.py +27 -0
  13. sma/encoders/base.py +23 -0
  14. sma/encoders/code_treesitter.py +64 -0
  15. sma/encoders/coverage.py +80 -0
  16. sma/encoders/draft_adapter.py +183 -0
  17. sma/encoders/healthcare.py +207 -0
  18. sma/encoders/logs_drain.py +142 -0
  19. sma/encoders/prose_tier1.py +57 -0
  20. sma/encoders/structured.py +57 -0
  21. sma/encoders/traces.py +45 -0
  22. sma/eval/__init__.py +2 -0
  23. sma/eval/agentic/__init__.py +35 -0
  24. sma/eval/agentic/arms/__init__.py +0 -0
  25. sma/eval/agentic/arms/cyber.py +48 -0
  26. sma/eval/agentic/arms/discovery.py +35 -0
  27. sma/eval/agentic/arms/finance.py +38 -0
  28. sma/eval/agentic/arms/legal.py +74 -0
  29. sma/eval/agentic/arms/medicine.py +45 -0
  30. sma/eval/agentic/harness.py +275 -0
  31. sma/eval/agentic/memories.py +308 -0
  32. sma/eval/agentic/metrics.py +82 -0
  33. sma/eval/agentic_qa/__init__.py +27 -0
  34. sma/eval/agentic_qa/agent.py +383 -0
  35. sma/eval/agentic_qa/metrics.py +239 -0
  36. sma/eval/agentic_qa/pools.py +197 -0
  37. sma/eval/arn.py +65 -0
  38. sma/eval/baselines/__init__.py +6 -0
  39. sma/eval/baselines/bge_dense.py +54 -0
  40. sma/eval/baselines/bm25.py +18 -0
  41. sma/eval/baselines/dense.py +42 -0
  42. sma/eval/baselines/hipporag.py +235 -0
  43. sma/eval/baselines/hybrid_rrf.py +30 -0
  44. sma/eval/baselines/longcontext_llm.py +124 -0
  45. sma/eval/baselines/rerank.py +41 -0
  46. sma/eval/baselines/splade.py +77 -0
  47. sma/eval/baselines/wl_kernel.py +163 -0
  48. sma/eval/bugsinpy.py +358 -0
  49. sma/eval/bugsinpy_families.py +164 -0
  50. sma/eval/crossdomain.py +89 -0
  51. sma/eval/diabetes.py +61 -0
  52. sma/eval/drift_env.py +26 -0
  53. sma/eval/drift_metrics.py +24 -0
  54. sma/eval/family_labels.py +167 -0
  55. sma/eval/fraud_elliptic/__init__.py +29 -0
  56. sma/eval/fraud_elliptic/encoder.py +279 -0
  57. sma/eval/fraud_elliptic/eval.py +269 -0
  58. sma/eval/fraud_elliptic/test_encoder.py +123 -0
  59. sma/eval/ieee_cis.py +66 -0
  60. sma/eval/loghub.py +16 -0
  61. sma/eval/loghub_eval.py +480 -0
  62. sma/eval/longmemeval.py +51 -0
  63. sma/eval/memory_backends/__init__.py +2 -0
  64. sma/eval/memory_backends/base.py +22 -0
  65. sma/eval/memory_backends/context_only.py +14 -0
  66. sma/eval/memory_backends/rag_notes.py +17 -0
  67. sma/eval/memory_backends/shared_llm.py +30 -0
  68. sma/eval/memory_backends/sma_memory.py +54 -0
  69. sma/eval/memory_backends/zep_graphiti.py +33 -0
  70. sma/eval/metrics.py +32 -0
  71. sma/eval/ontology_bench.py +219 -0
  72. sma/eval/report.py +573 -0
  73. sma/eval/ssb_eval.py +216 -0
  74. sma/eval/ssb_generator.py +116 -0
  75. sma/eval/stats.py +108 -0
  76. sma/eval/transfer_eval.py +844 -0
  77. sma/index/__init__.py +15 -0
  78. sma/index/ann.py +21 -0
  79. sma/index/content_vectors.py +60 -0
  80. sma/index/inverted.py +63 -0
  81. sma/index/macfac.py +174 -0
  82. sma/ir/__init__.py +22 -0
  83. sma/ir/canon.py +106 -0
  84. sma/ir/schema.py +165 -0
  85. sma/ir/sexpr.py +86 -0
  86. sma/ir/signatures.py +76 -0
  87. sma/match/__init__.py +20 -0
  88. sma/match/conflicts.py +46 -0
  89. sma/match/engine.py +60 -0
  90. sma/match/explain.py +59 -0
  91. sma/match/infer.py +54 -0
  92. sma/match/kernels.py +54 -0
  93. sma/match/mdl.py +30 -0
  94. sma/match/merge_cpsat.py +77 -0
  95. sma/match/merge_greedy.py +15 -0
  96. sma/match/mh.py +177 -0
  97. sma/match/ses.py +84 -0
  98. sma/match/types.py +115 -0
  99. sma/match/verifier.py +27 -0
  100. sma/ontology/__init__.py +45 -0
  101. sma/ontology/attack.py +134 -0
  102. sma/ontology/cpc.py +69 -0
  103. sma/ontology/graph.py +58 -0
  104. sma/ontology/loader.py +262 -0
  105. sma/ontology/mitre_xml.py +67 -0
  106. sma/ontology/mount.py +101 -0
  107. sma/ontology/rdf_loader.py +75 -0
  108. sma/ontology/registry.py +115 -0
  109. sma/ontology/router.py +69 -0
  110. sma/ontology/usgaap.py +73 -0
  111. sma/sage/__init__.py +6 -0
  112. sma/sage/assimilate.py +12 -0
  113. sma/sage/pools.py +105 -0
  114. sma/sage/probabilities.py +10 -0
  115. sma/store/__init__.py +6 -0
  116. sma/store/lmdb_store.py +78 -0
  117. sma/store/registry.py +26 -0
  118. sma/store/wal.py +26 -0
  119. sma/ui/app.py +642 -0
  120. structuremappingmemory-1.0.0.dist-info/METADATA +190 -0
  121. structuremappingmemory-1.0.0.dist-info/RECORD +125 -0
  122. structuremappingmemory-1.0.0.dist-info/WHEEL +5 -0
  123. structuremappingmemory-1.0.0.dist-info/entry_points.txt +2 -0
  124. structuremappingmemory-1.0.0.dist-info/licenses/LICENSE +204 -0
  125. 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
+
@@ -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
+
@@ -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
+