@simbimbo/memory-ocmemog 0.1.4
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.
- package/CHANGELOG.md +59 -0
- package/LICENSE +21 -0
- package/README.md +223 -0
- package/brain/__init__.py +1 -0
- package/brain/runtime/__init__.py +13 -0
- package/brain/runtime/config.py +21 -0
- package/brain/runtime/inference.py +83 -0
- package/brain/runtime/instrumentation.py +17 -0
- package/brain/runtime/memory/__init__.py +13 -0
- package/brain/runtime/memory/api.py +152 -0
- package/brain/runtime/memory/artifacts.py +33 -0
- package/brain/runtime/memory/candidate.py +89 -0
- package/brain/runtime/memory/context_builder.py +87 -0
- package/brain/runtime/memory/conversation_state.py +1825 -0
- package/brain/runtime/memory/distill.py +198 -0
- package/brain/runtime/memory/embedding_engine.py +94 -0
- package/brain/runtime/memory/freshness.py +91 -0
- package/brain/runtime/memory/health.py +42 -0
- package/brain/runtime/memory/integrity.py +170 -0
- package/brain/runtime/memory/interaction_memory.py +57 -0
- package/brain/runtime/memory/memory_consolidation.py +60 -0
- package/brain/runtime/memory/memory_gate.py +38 -0
- package/brain/runtime/memory/memory_graph.py +54 -0
- package/brain/runtime/memory/memory_links.py +109 -0
- package/brain/runtime/memory/memory_salience.py +235 -0
- package/brain/runtime/memory/memory_synthesis.py +33 -0
- package/brain/runtime/memory/memory_taxonomy.py +35 -0
- package/brain/runtime/memory/person_identity.py +83 -0
- package/brain/runtime/memory/person_memory.py +138 -0
- package/brain/runtime/memory/pondering_engine.py +577 -0
- package/brain/runtime/memory/promote.py +237 -0
- package/brain/runtime/memory/provenance.py +356 -0
- package/brain/runtime/memory/reinforcement.py +73 -0
- package/brain/runtime/memory/retrieval.py +153 -0
- package/brain/runtime/memory/semantic_search.py +66 -0
- package/brain/runtime/memory/sentiment_memory.py +67 -0
- package/brain/runtime/memory/store.py +400 -0
- package/brain/runtime/memory/tool_catalog.py +68 -0
- package/brain/runtime/memory/unresolved_state.py +93 -0
- package/brain/runtime/memory/vector_index.py +270 -0
- package/brain/runtime/model_roles.py +11 -0
- package/brain/runtime/model_router.py +22 -0
- package/brain/runtime/providers.py +59 -0
- package/brain/runtime/security/__init__.py +3 -0
- package/brain/runtime/security/redaction.py +14 -0
- package/brain/runtime/state_store.py +25 -0
- package/brain/runtime/storage_paths.py +41 -0
- package/docs/architecture/memory.md +118 -0
- package/docs/release-checklist.md +34 -0
- package/docs/reports/ocmemog-code-audit-2026-03-14.md +155 -0
- package/docs/usage.md +223 -0
- package/index.ts +726 -0
- package/ocmemog/__init__.py +1 -0
- package/ocmemog/sidecar/__init__.py +1 -0
- package/ocmemog/sidecar/app.py +1068 -0
- package/ocmemog/sidecar/compat.py +74 -0
- package/ocmemog/sidecar/transcript_watcher.py +425 -0
- package/openclaw.plugin.json +18 -0
- package/package.json +60 -0
- package/scripts/install-ocmemog.sh +277 -0
- package/scripts/launchagents/com.openclaw.ocmemog.guard.plist +22 -0
- package/scripts/launchagents/com.openclaw.ocmemog.ponder.plist +22 -0
- package/scripts/launchagents/com.openclaw.ocmemog.sidecar.plist +27 -0
- package/scripts/ocmemog-context.sh +15 -0
- package/scripts/ocmemog-continuity-benchmark.py +178 -0
- package/scripts/ocmemog-demo.py +122 -0
- package/scripts/ocmemog-failover-test.sh +17 -0
- package/scripts/ocmemog-guard.sh +11 -0
- package/scripts/ocmemog-install.sh +93 -0
- package/scripts/ocmemog-load-test.py +106 -0
- package/scripts/ocmemog-ponder.sh +30 -0
- package/scripts/ocmemog-recall-test.py +58 -0
- package/scripts/ocmemog-reindex-vectors.py +14 -0
- package/scripts/ocmemog-reliability-soak.py +177 -0
- package/scripts/ocmemog-sidecar.sh +46 -0
- package/scripts/ocmemog-soak-report.py +58 -0
- package/scripts/ocmemog-soak-test.py +44 -0
- package/scripts/ocmemog-test-rig.py +345 -0
- package/scripts/ocmemog-transcript-append.py +45 -0
- package/scripts/ocmemog-transcript-watcher.py +8 -0
- package/scripts/ocmemog-transcript-watcher.sh +7 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Dict, Any
|
|
5
|
+
|
|
6
|
+
from brain.runtime.instrumentation import emit_event
|
|
7
|
+
from brain.runtime import state_store
|
|
8
|
+
from brain.runtime.memory import provenance, store
|
|
9
|
+
from brain.runtime import config
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
LOGFILE = state_store.reports_dir() / "brain_memory.log.jsonl"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _should_promote(confidence: float, threshold: float | None = None) -> bool:
|
|
16
|
+
threshold = config.OCMEMOG_PROMOTION_THRESHOLD if threshold is None else threshold
|
|
17
|
+
return confidence >= float(threshold)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _destination_table(summary: str) -> str:
|
|
21
|
+
lowered = summary.lower()
|
|
22
|
+
if "runbook" in lowered or "procedure" in lowered or "steps" in lowered:
|
|
23
|
+
return "runbooks"
|
|
24
|
+
if "lesson" in lowered or "postmortem" in lowered or "learned" in lowered:
|
|
25
|
+
return "lessons"
|
|
26
|
+
return "knowledge"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def promote_candidate(candidate: Dict[str, Any]) -> Dict[str, Any]:
|
|
30
|
+
emit_event(LOGFILE, "brain_memory_promote_start", status="ok")
|
|
31
|
+
confidence = float(candidate.get("confidence_score", 0.0))
|
|
32
|
+
decision = "promote" if _should_promote(confidence) else "reject"
|
|
33
|
+
candidate_id = str(candidate.get("candidate_id") or "")
|
|
34
|
+
|
|
35
|
+
candidate_metadata = provenance.normalize_metadata(candidate.get("metadata", {}), source="promote")
|
|
36
|
+
candidate_metadata["candidate_id"] = candidate_id
|
|
37
|
+
candidate_metadata["derived_from_candidate_id"] = candidate_id
|
|
38
|
+
candidate_metadata["derived_via"] = "promotion"
|
|
39
|
+
|
|
40
|
+
conn = store.connect()
|
|
41
|
+
promotion_id = None
|
|
42
|
+
destination = _destination_table(str(candidate.get("distilled_summary", "")))
|
|
43
|
+
if decision == "promote":
|
|
44
|
+
row = conn.execute(
|
|
45
|
+
"SELECT id FROM promotions WHERE source=? AND content=?",
|
|
46
|
+
(str(candidate.get("source_event_id")), candidate.get("distilled_summary", "")),
|
|
47
|
+
).fetchone()
|
|
48
|
+
if not row:
|
|
49
|
+
cur = conn.execute(
|
|
50
|
+
"""
|
|
51
|
+
INSERT INTO promotions (
|
|
52
|
+
candidate_id, source, confidence, status, decision_reason,
|
|
53
|
+
metadata_json, content, schema_version
|
|
54
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
55
|
+
""",
|
|
56
|
+
(
|
|
57
|
+
candidate_id,
|
|
58
|
+
str(candidate.get("source_event_id")),
|
|
59
|
+
confidence,
|
|
60
|
+
"promoted",
|
|
61
|
+
"confidence_threshold",
|
|
62
|
+
json.dumps(candidate_metadata, ensure_ascii=False),
|
|
63
|
+
candidate.get("distilled_summary", ""),
|
|
64
|
+
store.SCHEMA_VERSION,
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
memory_rec = conn.execute(
|
|
68
|
+
f"INSERT INTO {destination} (source, confidence, metadata_json, content, schema_version) VALUES (?, ?, ?, ?, ?)",
|
|
69
|
+
(
|
|
70
|
+
str(candidate.get("source_event_id")),
|
|
71
|
+
confidence,
|
|
72
|
+
json.dumps(candidate_metadata, ensure_ascii=False),
|
|
73
|
+
candidate.get("distilled_summary", ""),
|
|
74
|
+
store.SCHEMA_VERSION,
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
conn.execute(
|
|
78
|
+
"UPDATE candidates SET status='promoted', updated_at=datetime('now') WHERE candidate_id=?",
|
|
79
|
+
(candidate_id,),
|
|
80
|
+
)
|
|
81
|
+
conn.execute(
|
|
82
|
+
"INSERT INTO memory_events (event_type, source, details_json, schema_version) VALUES (?, ?, ?, ?)",
|
|
83
|
+
(
|
|
84
|
+
"candidate_promoted",
|
|
85
|
+
str(candidate.get("source_event_id")),
|
|
86
|
+
json.dumps({"candidate_id": candidate_id, "promotion_table": destination}),
|
|
87
|
+
store.SCHEMA_VERSION,
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
conn.commit()
|
|
91
|
+
promotion_id = cur.lastrowid
|
|
92
|
+
memory_id = memory_rec.lastrowid
|
|
93
|
+
else:
|
|
94
|
+
promotion_id = row[0]
|
|
95
|
+
memory_id = None
|
|
96
|
+
emit_event(LOGFILE, "brain_memory_promote_success", status="ok", destination=destination)
|
|
97
|
+
else:
|
|
98
|
+
conn.execute(
|
|
99
|
+
"UPDATE candidates SET status='rejected', updated_at=datetime('now') WHERE candidate_id=?",
|
|
100
|
+
(candidate_id,),
|
|
101
|
+
)
|
|
102
|
+
conn.execute(
|
|
103
|
+
"INSERT INTO promotions (candidate_id, source, confidence, status, decision_reason, metadata_json, content, schema_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
104
|
+
(
|
|
105
|
+
candidate_id,
|
|
106
|
+
str(candidate.get("source_event_id")),
|
|
107
|
+
confidence,
|
|
108
|
+
"rejected",
|
|
109
|
+
"below_threshold",
|
|
110
|
+
json.dumps(candidate_metadata, ensure_ascii=False),
|
|
111
|
+
candidate.get("distilled_summary", ""),
|
|
112
|
+
store.SCHEMA_VERSION,
|
|
113
|
+
),
|
|
114
|
+
)
|
|
115
|
+
conn.commit()
|
|
116
|
+
emit_event(LOGFILE, "brain_memory_promote_rejected", status="ok")
|
|
117
|
+
memory_id = None
|
|
118
|
+
conn.close()
|
|
119
|
+
|
|
120
|
+
if decision == "promote" and promotion_id is not None:
|
|
121
|
+
from brain.runtime.memory import reinforcement, vector_index
|
|
122
|
+
|
|
123
|
+
promoted_reference = f"{destination}:{memory_id}" if memory_id else ""
|
|
124
|
+
promotion_updates = {
|
|
125
|
+
**candidate_metadata,
|
|
126
|
+
"promotion_id": promotion_id,
|
|
127
|
+
"derived_from_promotion_id": promotion_id,
|
|
128
|
+
}
|
|
129
|
+
if promoted_reference:
|
|
130
|
+
provenance.update_memory_metadata(promoted_reference, promotion_updates)
|
|
131
|
+
reinforcement.log_experience(
|
|
132
|
+
task_id=str(candidate.get("candidate_id") or candidate.get("source_event_id") or ""),
|
|
133
|
+
outcome="promoted",
|
|
134
|
+
confidence=confidence,
|
|
135
|
+
reward_score=confidence,
|
|
136
|
+
memory_reference=promoted_reference or f"promotions:{promotion_id}",
|
|
137
|
+
experience_type="promotion",
|
|
138
|
+
source_module="memory_promote",
|
|
139
|
+
)
|
|
140
|
+
emit_event(LOGFILE, "brain_memory_reinforcement_created", status="ok")
|
|
141
|
+
if memory_id:
|
|
142
|
+
vector_index.insert_memory(memory_id, candidate.get("distilled_summary", ""), confidence)
|
|
143
|
+
|
|
144
|
+
return {"decision": decision, "confidence": confidence, "promotion_id": promotion_id, "destination": destination}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def promote_candidate_by_id(candidate_id: str) -> Dict[str, Any]:
|
|
148
|
+
conn = store.connect()
|
|
149
|
+
row = conn.execute(
|
|
150
|
+
"""
|
|
151
|
+
SELECT candidate_id, source_event_id, distilled_summary, verification_points,
|
|
152
|
+
confidence_score, metadata_json
|
|
153
|
+
FROM candidates WHERE candidate_id=?
|
|
154
|
+
""",
|
|
155
|
+
(candidate_id,),
|
|
156
|
+
).fetchone()
|
|
157
|
+
conn.close()
|
|
158
|
+
if not row:
|
|
159
|
+
emit_event(LOGFILE, "brain_memory_promote_error", status="error")
|
|
160
|
+
return {"decision": "error", "reason": "candidate_not_found"}
|
|
161
|
+
payload = dict(row)
|
|
162
|
+
try:
|
|
163
|
+
payload["metadata"] = json.loads(payload.get("metadata_json") or "{}")
|
|
164
|
+
except Exception:
|
|
165
|
+
payload["metadata"] = {}
|
|
166
|
+
return promote_candidate(payload)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def demote_memory(reference: str, reason: str = "low_confidence", new_confidence: float = 0.1) -> Dict[str, Any]:
|
|
170
|
+
table, sep, raw_id = reference.partition(":")
|
|
171
|
+
if not sep or not raw_id.isdigit():
|
|
172
|
+
return {"ok": False, "error": "invalid_reference"}
|
|
173
|
+
allowed = {"knowledge", "runbooks", "lessons", "directives", "reflections", "tasks"}
|
|
174
|
+
if table not in allowed:
|
|
175
|
+
return {"ok": False, "error": "unsupported_table"}
|
|
176
|
+
conn = store.connect()
|
|
177
|
+
row = conn.execute(
|
|
178
|
+
f"SELECT confidence, content, metadata_json FROM {table} WHERE id=?",
|
|
179
|
+
(int(raw_id),),
|
|
180
|
+
).fetchone()
|
|
181
|
+
if not row:
|
|
182
|
+
conn.close()
|
|
183
|
+
return {"ok": False, "error": "not_found"}
|
|
184
|
+
previous = float(row[0] or 0.0)
|
|
185
|
+
content = str(row[1] or "")
|
|
186
|
+
metadata_json = row[2] or "{}"
|
|
187
|
+
|
|
188
|
+
# archive into cold storage, then remove from hot table
|
|
189
|
+
conn.execute(
|
|
190
|
+
"INSERT INTO cold_storage (source_table, source_id, content, metadata_json, reason, schema_version) VALUES (?, ?, ?, ?, ?, ?)",
|
|
191
|
+
(table, int(raw_id), content, metadata_json, reason, store.SCHEMA_VERSION),
|
|
192
|
+
)
|
|
193
|
+
conn.execute(
|
|
194
|
+
f"DELETE FROM {table} WHERE id=?",
|
|
195
|
+
(int(raw_id),),
|
|
196
|
+
)
|
|
197
|
+
conn.execute(
|
|
198
|
+
"INSERT INTO demotions (memory_reference, previous_confidence, new_confidence, reason, schema_version) VALUES (?, ?, ?, ?, ?)",
|
|
199
|
+
(reference, previous, float(new_confidence), reason, store.SCHEMA_VERSION),
|
|
200
|
+
)
|
|
201
|
+
conn.commit()
|
|
202
|
+
conn.close()
|
|
203
|
+
emit_event(LOGFILE, "brain_memory_demoted", status="ok", reference=reference)
|
|
204
|
+
return {"ok": True, "reference": reference, "previous": previous, "new": float(new_confidence), "archived": True}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def demote_by_confidence(limit: int = 20, threshold: float | None = None, force: bool = False) -> Dict[str, Any]:
|
|
208
|
+
threshold = config.OCMEMOG_DEMOTION_THRESHOLD if threshold is None else threshold
|
|
209
|
+
tables = ("knowledge", "runbooks", "lessons", "directives", "reflections", "tasks")
|
|
210
|
+
conn = store.connect()
|
|
211
|
+
rows = []
|
|
212
|
+
for table in tables:
|
|
213
|
+
try:
|
|
214
|
+
rows.extend(
|
|
215
|
+
conn.execute(
|
|
216
|
+
f"SELECT '{table}' AS table_name, id, confidence FROM {table} ORDER BY confidence ASC LIMIT ?",
|
|
217
|
+
(limit,),
|
|
218
|
+
).fetchall()
|
|
219
|
+
)
|
|
220
|
+
except Exception:
|
|
221
|
+
continue
|
|
222
|
+
conn.close()
|
|
223
|
+
# sort by confidence and demote below threshold
|
|
224
|
+
ranked = sorted(rows, key=lambda r: float(r[2] or 0.0))
|
|
225
|
+
demoted = []
|
|
226
|
+
for row in ranked:
|
|
227
|
+
table = row[0]
|
|
228
|
+
memory_id = int(row[1])
|
|
229
|
+
confidence = float(row[2] or 0.0)
|
|
230
|
+
if confidence >= float(threshold) and not force:
|
|
231
|
+
continue
|
|
232
|
+
result = demote_memory(f"{table}:{memory_id}", reason="low_confidence", new_confidence=confidence * 0.5)
|
|
233
|
+
if result.get("ok"):
|
|
234
|
+
demoted.append(result)
|
|
235
|
+
if len(demoted) >= limit:
|
|
236
|
+
break
|
|
237
|
+
return {"ok": True, "threshold": float(threshold), "demoted": demoted, "count": len(demoted)}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict, Iterable, List, Optional, Set
|
|
6
|
+
|
|
7
|
+
from brain.runtime.memory import memory_links, store
|
|
8
|
+
|
|
9
|
+
_MEMORY_TABLES = {"knowledge", "reflections", "directives", "tasks", "runbooks", "lessons"}
|
|
10
|
+
_FETCHABLE_TABLES = _MEMORY_TABLES | {"promotions", "experiences", "conversation_turns", "conversation_checkpoints"}
|
|
11
|
+
_SYNTHETIC_PREFIXES = {"conversation", "session", "thread", "message", "label", "transcript"}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _load_json(value: Any, fallback: Any) -> Any:
|
|
15
|
+
try:
|
|
16
|
+
return json.loads(value or "")
|
|
17
|
+
except Exception:
|
|
18
|
+
return fallback
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _dedupe(values: Iterable[str]) -> List[str]:
|
|
22
|
+
seen: Set[str] = set()
|
|
23
|
+
items: List[str] = []
|
|
24
|
+
for value in values:
|
|
25
|
+
item = str(value or "").strip()
|
|
26
|
+
if not item or item in seen:
|
|
27
|
+
continue
|
|
28
|
+
seen.add(item)
|
|
29
|
+
items.append(item)
|
|
30
|
+
return items
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _transcript_target(path: str, start_line: Any = None, end_line: Any = None) -> str:
|
|
34
|
+
suffix = ""
|
|
35
|
+
try:
|
|
36
|
+
start = int(start_line) if start_line is not None else None
|
|
37
|
+
except Exception:
|
|
38
|
+
start = None
|
|
39
|
+
try:
|
|
40
|
+
end = int(end_line) if end_line is not None else None
|
|
41
|
+
except Exception:
|
|
42
|
+
end = None
|
|
43
|
+
if start and end and end >= start:
|
|
44
|
+
suffix = f"#L{start}-L{end}"
|
|
45
|
+
elif start:
|
|
46
|
+
suffix = f"#L{start}"
|
|
47
|
+
return f"transcript:{path}{suffix}"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def normalize_metadata(metadata: Optional[Dict[str, Any]], *, source: Optional[str] = None) -> Dict[str, Any]:
|
|
51
|
+
raw = dict(metadata or {})
|
|
52
|
+
existing = raw.get("provenance") if isinstance(raw.get("provenance"), dict) else {}
|
|
53
|
+
|
|
54
|
+
source_references = _dedupe(
|
|
55
|
+
[
|
|
56
|
+
*(existing.get("source_references") or []),
|
|
57
|
+
*(raw.get("source_references") or []),
|
|
58
|
+
existing.get("source_reference") or "",
|
|
59
|
+
raw.get("source_reference") or "",
|
|
60
|
+
existing.get("experience_reference") or "",
|
|
61
|
+
raw.get("experience_reference") or "",
|
|
62
|
+
]
|
|
63
|
+
)
|
|
64
|
+
source_labels = _dedupe(
|
|
65
|
+
[
|
|
66
|
+
*(existing.get("source_labels") or []),
|
|
67
|
+
*(raw.get("source_labels") or []),
|
|
68
|
+
existing.get("source_label") or "",
|
|
69
|
+
raw.get("source_label") or "",
|
|
70
|
+
]
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
conversation = dict(existing.get("conversation") or {})
|
|
74
|
+
for key in ("conversation_id", "session_id", "thread_id", "message_id", "role"):
|
|
75
|
+
if raw.get(key) is not None and conversation.get(key) is None:
|
|
76
|
+
conversation[key] = raw.get(key)
|
|
77
|
+
|
|
78
|
+
transcript_anchor = dict(existing.get("transcript_anchor") or {})
|
|
79
|
+
if raw.get("transcript_path") and not transcript_anchor.get("path"):
|
|
80
|
+
transcript_anchor = {
|
|
81
|
+
"path": raw.get("transcript_path"),
|
|
82
|
+
"start_line": raw.get("transcript_offset"),
|
|
83
|
+
"end_line": raw.get("transcript_end_offset"),
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
provenance: Dict[str, Any] = dict(existing)
|
|
87
|
+
if source_references:
|
|
88
|
+
provenance["source_references"] = source_references
|
|
89
|
+
provenance["source_reference"] = source_references[0]
|
|
90
|
+
if source_labels:
|
|
91
|
+
provenance["source_labels"] = source_labels
|
|
92
|
+
if conversation:
|
|
93
|
+
provenance["conversation"] = conversation
|
|
94
|
+
if transcript_anchor.get("path"):
|
|
95
|
+
provenance["transcript_anchor"] = transcript_anchor
|
|
96
|
+
if source:
|
|
97
|
+
provenance.setdefault("origin_source", source)
|
|
98
|
+
|
|
99
|
+
for key in (
|
|
100
|
+
"source_event_id",
|
|
101
|
+
"task_id",
|
|
102
|
+
"candidate_id",
|
|
103
|
+
"promotion_id",
|
|
104
|
+
"experience_reference",
|
|
105
|
+
"derived_from_candidate_id",
|
|
106
|
+
"derived_from_promotion_id",
|
|
107
|
+
"derived_via",
|
|
108
|
+
"kind",
|
|
109
|
+
):
|
|
110
|
+
if raw.get(key) is not None and provenance.get(key) is None:
|
|
111
|
+
provenance[key] = raw.get(key)
|
|
112
|
+
|
|
113
|
+
if provenance:
|
|
114
|
+
raw["provenance"] = provenance
|
|
115
|
+
if source_references:
|
|
116
|
+
raw["source_reference"] = source_references[0]
|
|
117
|
+
raw["source_references"] = source_references
|
|
118
|
+
if source_labels:
|
|
119
|
+
raw["source_labels"] = source_labels
|
|
120
|
+
raw.setdefault("source_label", source_labels[0])
|
|
121
|
+
return raw
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def preview_from_metadata(metadata: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
|
125
|
+
normalized = normalize_metadata(metadata)
|
|
126
|
+
provenance = normalized.get("provenance") if isinstance(normalized.get("provenance"), dict) else {}
|
|
127
|
+
return {
|
|
128
|
+
"source_references": provenance.get("source_references") or [],
|
|
129
|
+
"source_labels": provenance.get("source_labels") or [],
|
|
130
|
+
"conversation": provenance.get("conversation") or {},
|
|
131
|
+
"transcript_anchor": provenance.get("transcript_anchor") or None,
|
|
132
|
+
"origin_source": provenance.get("origin_source"),
|
|
133
|
+
"derived_via": provenance.get("derived_via"),
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _link_once(source_reference: str, link_type: str, target_reference: str) -> None:
|
|
138
|
+
if not source_reference or not target_reference:
|
|
139
|
+
return
|
|
140
|
+
existing = memory_links.get_memory_links(source_reference)
|
|
141
|
+
if any(item.get("link_type") == link_type and item.get("target_reference") == target_reference for item in existing):
|
|
142
|
+
return
|
|
143
|
+
memory_links.add_memory_link(source_reference, link_type, target_reference)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def apply_links(reference: str, metadata: Optional[Dict[str, Any]]) -> None:
|
|
147
|
+
normalized = normalize_metadata(metadata)
|
|
148
|
+
provenance = normalized.get("provenance") if isinstance(normalized.get("provenance"), dict) else {}
|
|
149
|
+
for source_reference in provenance.get("source_references") or []:
|
|
150
|
+
_link_once(reference, "derived_from", str(source_reference))
|
|
151
|
+
for label in provenance.get("source_labels") or []:
|
|
152
|
+
_link_once(reference, "source_label", f"label:{label}")
|
|
153
|
+
conversation = provenance.get("conversation") or {}
|
|
154
|
+
for key, link_type in (
|
|
155
|
+
("conversation_id", "conversation"),
|
|
156
|
+
("session_id", "session"),
|
|
157
|
+
("thread_id", "thread"),
|
|
158
|
+
("message_id", "message"),
|
|
159
|
+
):
|
|
160
|
+
value = str(conversation.get(key) or "").strip()
|
|
161
|
+
if value:
|
|
162
|
+
_link_once(reference, link_type, f"{link_type}:{value}")
|
|
163
|
+
transcript = provenance.get("transcript_anchor") or {}
|
|
164
|
+
if transcript.get("path"):
|
|
165
|
+
_link_once(
|
|
166
|
+
reference,
|
|
167
|
+
"transcript",
|
|
168
|
+
_transcript_target(
|
|
169
|
+
str(transcript.get("path")),
|
|
170
|
+
transcript.get("start_line"),
|
|
171
|
+
transcript.get("end_line"),
|
|
172
|
+
),
|
|
173
|
+
)
|
|
174
|
+
if provenance.get("experience_reference"):
|
|
175
|
+
_link_once(reference, "experience", str(provenance.get("experience_reference")))
|
|
176
|
+
if provenance.get("derived_from_candidate_id"):
|
|
177
|
+
_link_once(reference, "candidate", f"candidate:{provenance['derived_from_candidate_id']}")
|
|
178
|
+
if provenance.get("derived_from_promotion_id"):
|
|
179
|
+
_link_once(reference, "promotion", f"promotions:{provenance['derived_from_promotion_id']}")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def update_memory_metadata(reference: str, updates: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
183
|
+
table, sep, raw_id = reference.partition(":")
|
|
184
|
+
if not sep or table not in _MEMORY_TABLES or not raw_id.isdigit():
|
|
185
|
+
return None
|
|
186
|
+
conn = store.connect()
|
|
187
|
+
try:
|
|
188
|
+
row = conn.execute(f"SELECT metadata_json FROM {table} WHERE id = ?", (int(raw_id),)).fetchone()
|
|
189
|
+
if not row:
|
|
190
|
+
return None
|
|
191
|
+
current = _load_json(row["metadata_json"], {})
|
|
192
|
+
merged = normalize_metadata({**current, **updates})
|
|
193
|
+
conn.execute(
|
|
194
|
+
f"UPDATE {table} SET metadata_json = ? WHERE id = ?",
|
|
195
|
+
(json.dumps(merged, ensure_ascii=False), int(raw_id)),
|
|
196
|
+
)
|
|
197
|
+
conn.commit()
|
|
198
|
+
finally:
|
|
199
|
+
conn.close()
|
|
200
|
+
apply_links(reference, merged)
|
|
201
|
+
return merged
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def fetch_reference(reference: str) -> Optional[Dict[str, Any]]:
|
|
205
|
+
prefix, sep, raw_id = reference.partition(":")
|
|
206
|
+
if not sep or not prefix:
|
|
207
|
+
return None
|
|
208
|
+
if prefix in _SYNTHETIC_PREFIXES:
|
|
209
|
+
payload: Dict[str, Any] = {"reference": reference, "type": prefix, "value": raw_id}
|
|
210
|
+
if prefix == "transcript":
|
|
211
|
+
payload["path"] = raw_id
|
|
212
|
+
return payload
|
|
213
|
+
if prefix == "candidate":
|
|
214
|
+
return {"reference": reference, "type": "candidate", "candidate_id": raw_id}
|
|
215
|
+
if prefix not in _FETCHABLE_TABLES:
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
conn = store.connect()
|
|
219
|
+
try:
|
|
220
|
+
if prefix == "conversation_turns":
|
|
221
|
+
if not raw_id.isdigit():
|
|
222
|
+
return None
|
|
223
|
+
row = conn.execute("SELECT * FROM conversation_turns WHERE id = ?", (int(raw_id),)).fetchone()
|
|
224
|
+
if not row:
|
|
225
|
+
return None
|
|
226
|
+
payload = dict(row)
|
|
227
|
+
payload["reference"] = reference
|
|
228
|
+
payload["table"] = prefix
|
|
229
|
+
payload["id"] = int(raw_id)
|
|
230
|
+
payload["metadata"] = _load_json(payload.pop("metadata_json", "{}"), {})
|
|
231
|
+
return payload
|
|
232
|
+
if prefix == "conversation_checkpoints":
|
|
233
|
+
if not raw_id.isdigit():
|
|
234
|
+
return None
|
|
235
|
+
row = conn.execute("SELECT * FROM conversation_checkpoints WHERE id = ?", (int(raw_id),)).fetchone()
|
|
236
|
+
if not row:
|
|
237
|
+
return None
|
|
238
|
+
payload = dict(row)
|
|
239
|
+
payload["reference"] = reference
|
|
240
|
+
payload["table"] = prefix
|
|
241
|
+
payload["id"] = int(raw_id)
|
|
242
|
+
payload["metadata"] = _load_json(payload.pop("metadata_json", "{}"), {})
|
|
243
|
+
payload["open_loops"] = _load_json(payload.pop("open_loops_json", "[]"), [])
|
|
244
|
+
payload["pending_actions"] = _load_json(payload.pop("pending_actions_json", "[]"), [])
|
|
245
|
+
return payload
|
|
246
|
+
if prefix == "experiences":
|
|
247
|
+
if not raw_id.isdigit():
|
|
248
|
+
return None
|
|
249
|
+
row = conn.execute("SELECT * FROM experiences WHERE id = ?", (int(raw_id),)).fetchone()
|
|
250
|
+
if not row:
|
|
251
|
+
return None
|
|
252
|
+
payload = dict(row)
|
|
253
|
+
payload["reference"] = reference
|
|
254
|
+
payload["table"] = prefix
|
|
255
|
+
payload["id"] = int(raw_id)
|
|
256
|
+
payload["content"] = payload.get("outcome")
|
|
257
|
+
payload["metadata"] = _load_json(payload.pop("metadata_json", "{}"), {})
|
|
258
|
+
return payload
|
|
259
|
+
if prefix == "promotions":
|
|
260
|
+
if not raw_id.isdigit():
|
|
261
|
+
return None
|
|
262
|
+
row = conn.execute("SELECT * FROM promotions WHERE id = ?", (int(raw_id),)).fetchone()
|
|
263
|
+
if not row:
|
|
264
|
+
return None
|
|
265
|
+
payload = dict(row)
|
|
266
|
+
payload["reference"] = reference
|
|
267
|
+
payload["table"] = prefix
|
|
268
|
+
payload["id"] = int(raw_id)
|
|
269
|
+
payload["metadata"] = _load_json(payload.pop("metadata_json", "{}"), {})
|
|
270
|
+
return payload
|
|
271
|
+
if not raw_id.isdigit():
|
|
272
|
+
return None
|
|
273
|
+
row = conn.execute(f"SELECT * FROM {prefix} WHERE id = ?", (int(raw_id),)).fetchone()
|
|
274
|
+
if not row:
|
|
275
|
+
return None
|
|
276
|
+
payload = dict(row)
|
|
277
|
+
payload["reference"] = reference
|
|
278
|
+
payload["table"] = prefix
|
|
279
|
+
payload["id"] = int(raw_id)
|
|
280
|
+
payload["metadata"] = _load_json(payload.pop("metadata_json", "{}"), {})
|
|
281
|
+
return payload
|
|
282
|
+
finally:
|
|
283
|
+
conn.close()
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _hydrate_target(reference: str, depth: int, seen: Set[str]) -> Optional[Dict[str, Any]]:
|
|
287
|
+
if reference in seen:
|
|
288
|
+
return {"reference": reference, "cycle": True}
|
|
289
|
+
return hydrate_reference(reference, depth=depth, _seen=seen)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def hydrate_reference(reference: str, *, depth: int = 1, _seen: Optional[Set[str]] = None) -> Optional[Dict[str, Any]]:
|
|
293
|
+
seen = set(_seen or set())
|
|
294
|
+
if reference in seen:
|
|
295
|
+
return {"reference": reference, "cycle": True}
|
|
296
|
+
seen.add(reference)
|
|
297
|
+
payload = fetch_reference(reference)
|
|
298
|
+
if payload is None:
|
|
299
|
+
return None
|
|
300
|
+
|
|
301
|
+
metadata = payload.get("metadata") if isinstance(payload.get("metadata"), dict) else {}
|
|
302
|
+
preview = preview_from_metadata(metadata)
|
|
303
|
+
links = memory_links.get_memory_links(reference)
|
|
304
|
+
backlinks = memory_links.get_memory_links_for_target(reference)
|
|
305
|
+
|
|
306
|
+
payload["provenance_preview"] = preview
|
|
307
|
+
payload["links"] = links
|
|
308
|
+
payload["backlinks"] = backlinks
|
|
309
|
+
if depth <= 0:
|
|
310
|
+
return payload
|
|
311
|
+
|
|
312
|
+
payload["provenance"] = {
|
|
313
|
+
"outbound": [
|
|
314
|
+
{
|
|
315
|
+
**item,
|
|
316
|
+
"target": _hydrate_target(str(item.get("target_reference") or ""), depth - 1, seen),
|
|
317
|
+
}
|
|
318
|
+
for item in links
|
|
319
|
+
],
|
|
320
|
+
"inbound": [
|
|
321
|
+
{
|
|
322
|
+
**item,
|
|
323
|
+
"source": _hydrate_target(str(item.get("source_reference") or ""), depth - 1, seen),
|
|
324
|
+
}
|
|
325
|
+
for item in backlinks
|
|
326
|
+
],
|
|
327
|
+
}
|
|
328
|
+
return payload
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def collect_source_references(reference: str, *, depth: int = 2) -> List[str]:
|
|
332
|
+
pending = [reference]
|
|
333
|
+
seen: Set[str] = set()
|
|
334
|
+
collected: List[str] = []
|
|
335
|
+
remaining = max(0, int(depth))
|
|
336
|
+
while pending and remaining >= 0:
|
|
337
|
+
next_round: List[str] = []
|
|
338
|
+
for current in pending:
|
|
339
|
+
if current in seen:
|
|
340
|
+
continue
|
|
341
|
+
seen.add(current)
|
|
342
|
+
collected.append(current)
|
|
343
|
+
row = fetch_reference(current)
|
|
344
|
+
metadata = row.get("metadata") if isinstance((row or {}).get("metadata"), dict) else {}
|
|
345
|
+
preview = preview_from_metadata(metadata)
|
|
346
|
+
for source_ref in preview.get("source_references") or []:
|
|
347
|
+
if source_ref not in seen:
|
|
348
|
+
next_round.append(str(source_ref))
|
|
349
|
+
pending = next_round
|
|
350
|
+
remaining -= 1
|
|
351
|
+
return _dedupe(collected)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def source_references_only(reference: str, *, depth: int = 2) -> List[str]:
|
|
355
|
+
refs = collect_source_references(reference, depth=depth)
|
|
356
|
+
return refs[1:] if len(refs) > 1 else []
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any
|
|
4
|
+
|
|
5
|
+
from brain.runtime.instrumentation import emit_event
|
|
6
|
+
from brain.runtime import state_store
|
|
7
|
+
from brain.runtime.memory import store
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def log_experience(
|
|
11
|
+
task_id: str,
|
|
12
|
+
outcome: str,
|
|
13
|
+
confidence: float,
|
|
14
|
+
reward_score: float,
|
|
15
|
+
memory_reference: str,
|
|
16
|
+
experience_type: str,
|
|
17
|
+
source_module: str,
|
|
18
|
+
) -> Dict[str, Any]:
|
|
19
|
+
conn = store.connect()
|
|
20
|
+
row = conn.execute(
|
|
21
|
+
"SELECT id FROM experiences WHERE task_id=? AND memory_reference=? AND outcome=?",
|
|
22
|
+
(task_id, memory_reference, outcome),
|
|
23
|
+
).fetchone()
|
|
24
|
+
if row:
|
|
25
|
+
conn.close()
|
|
26
|
+
emit_event(state_store.reports_dir() / "brain_memory.log.jsonl", "brain_memory_experience_duplicate", status="ok")
|
|
27
|
+
return {"experience_id": row[0], "duplicate": True}
|
|
28
|
+
|
|
29
|
+
cur = conn.execute(
|
|
30
|
+
"INSERT INTO experiences (task_id, outcome, reward_score, confidence, memory_reference, experience_type, source_module, schema_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
31
|
+
(task_id, outcome, reward_score, confidence, memory_reference, experience_type, source_module, store.SCHEMA_VERSION),
|
|
32
|
+
)
|
|
33
|
+
conn.commit()
|
|
34
|
+
conn.close()
|
|
35
|
+
emit_event(state_store.reports_dir() / "brain_memory.log.jsonl", "brain_memory_experience_logged", status="ok")
|
|
36
|
+
return {"experience_id": cur.lastrowid, "duplicate": False, "experience_type": experience_type, "source_module": source_module}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def log_task_execution(
|
|
40
|
+
*,
|
|
41
|
+
task_id: str,
|
|
42
|
+
task_type: str,
|
|
43
|
+
agent_id: str,
|
|
44
|
+
tool_used: str,
|
|
45
|
+
success: bool,
|
|
46
|
+
duration_ms: int,
|
|
47
|
+
) -> Dict[str, Any]:
|
|
48
|
+
outcome_payload = {
|
|
49
|
+
"task_type": task_type,
|
|
50
|
+
"agent_id": agent_id,
|
|
51
|
+
"tool_used": tool_used,
|
|
52
|
+
"success": bool(success),
|
|
53
|
+
"duration_ms": duration_ms,
|
|
54
|
+
}
|
|
55
|
+
return log_experience(
|
|
56
|
+
task_id=task_id,
|
|
57
|
+
outcome=str(outcome_payload),
|
|
58
|
+
confidence=1.0,
|
|
59
|
+
reward_score=1.0 if success else 0.0,
|
|
60
|
+
memory_reference=f"tool:{tool_used}",
|
|
61
|
+
experience_type="task_execution",
|
|
62
|
+
source_module="task_engine",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def list_recent_experiences(limit: int = 20) -> Dict[str, int]:
|
|
67
|
+
conn = store.connect()
|
|
68
|
+
rows = conn.execute(
|
|
69
|
+
"SELECT experience_type, COUNT(*) as count FROM experiences GROUP BY experience_type ORDER BY count DESC LIMIT ?",
|
|
70
|
+
(limit,),
|
|
71
|
+
).fetchall()
|
|
72
|
+
conn.close()
|
|
73
|
+
return {row[0]: int(row[1]) for row in rows}
|