graph-mind-memory 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- associative_memory.py +507 -0
- automatic_capture.py +288 -0
- automatic_capture_cli.py +72 -0
- brain_log.py +501 -0
- coding_memory.py +438 -0
- concept_plan_benchmark_v071.py +248 -0
- conversation_memory.py +476 -0
- development_memory.py +450 -0
- development_paths.py +40 -0
- embedding_warmup.py +171 -0
- entity_timeline.py +130 -0
- executable_memory.py +312 -0
- executable_plan_v2_diagnostic.py +272 -0
- graph_mind_mcp_server.py +864 -0
- graph_mind_memory-0.1.0.dist-info/METADATA +252 -0
- graph_mind_memory-0.1.0.dist-info/RECORD +43 -0
- graph_mind_memory-0.1.0.dist-info/WHEEL +5 -0
- graph_mind_memory-0.1.0.dist-info/entry_points.txt +4 -0
- graph_mind_memory-0.1.0.dist-info/licenses/LICENSE +661 -0
- graph_mind_memory-0.1.0.dist-info/top_level.txt +37 -0
- grounded_concept_bridge_v072.py +511 -0
- ingestion_gate.py +113 -0
- ingestion_synonyms.py +165 -0
- install.py +198 -0
- llm_concept_planner.py +170 -0
- local_brain.py +354 -0
- longmemeval_adapter.py +120 -0
- longmemeval_ingestion_calibration.py +366 -0
- longmemeval_ingestion_preflight.py +188 -0
- longmemeval_ingestion_v7.py +356 -0
- memory_algebra.py +155 -0
- memory_contract.py +154 -0
- memory_normalization.py +13 -0
- memory_vocabulary.py +34 -0
- semantic_grounding_v073.py +501 -0
- semantic_llm_extractor.py +215 -0
- semantic_memory.py +155 -0
- semantic_recall.py +100 -0
- state_relation_contract.py +158 -0
- temporal_resolver.py +272 -0
- temporal_state.py +210 -0
- universal_personal_memory.py +234 -0
- vector_cache.py +194 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""Freeze, capture, and evaluate target-blind LLM concept plans."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from argparse import ArgumentParser
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from statistics import median
|
|
7
|
+
from time import perf_counter
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
from associative_memory import AssociativeMemoryIndex
|
|
12
|
+
from development_paths import default_development_db
|
|
13
|
+
from llm_concept_planner import (
|
|
14
|
+
DEFAULT_MODEL, INSTRUCTIONS, MINIMUM_CONCEPT_COVERAGE, PLAN_SCHEMA,
|
|
15
|
+
OpenAIResponsesConceptPlanner, digest,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
RUN_NAME = "graph-v3.0-llm-concept-plan-v0.7.1"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _read(path):
|
|
23
|
+
return json.loads(Path(path).read_text(encoding="utf-8"))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _write(path, value):
|
|
27
|
+
Path(path).write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n",
|
|
28
|
+
encoding="utf-8")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def prepare(root, cases_path, model):
|
|
32
|
+
root = Path(root)
|
|
33
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
cases = _read(cases_path)
|
|
35
|
+
questions = [{"id": row["id"], "query": row["query"]}
|
|
36
|
+
for row in cases]
|
|
37
|
+
manifest = {
|
|
38
|
+
"run": RUN_NAME,
|
|
39
|
+
"model": model,
|
|
40
|
+
"questions_sha256": digest(questions),
|
|
41
|
+
"prompt_sha256": digest(INSTRUCTIONS),
|
|
42
|
+
"schema_sha256": digest(PLAN_SCHEMA),
|
|
43
|
+
"minimum_concept_coverage": MINIMUM_CONCEPT_COVERAGE,
|
|
44
|
+
"planner_sees": ["id", "query"],
|
|
45
|
+
"planner_never_sees": ["style", "expected", "concept_cues", "memory_contents"],
|
|
46
|
+
}
|
|
47
|
+
_write(root / "questions.json", questions)
|
|
48
|
+
_write(root / "manifest.json", manifest)
|
|
49
|
+
return manifest
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _verify_frozen(root, model=None):
|
|
53
|
+
root = Path(root)
|
|
54
|
+
questions, manifest = _read(root / "questions.json"), _read(root / "manifest.json")
|
|
55
|
+
checks = {
|
|
56
|
+
"questions_sha256": digest(questions),
|
|
57
|
+
"prompt_sha256": digest(INSTRUCTIONS),
|
|
58
|
+
"schema_sha256": digest(PLAN_SCHEMA),
|
|
59
|
+
"minimum_concept_coverage": MINIMUM_CONCEPT_COVERAGE,
|
|
60
|
+
}
|
|
61
|
+
for key, actual in checks.items():
|
|
62
|
+
if manifest.get(key) != actual:
|
|
63
|
+
raise RuntimeError("FROZEN_CONCEPT_PLAN_CONTRACT_CHANGED:" + key)
|
|
64
|
+
if model is not None and manifest.get("model") != model:
|
|
65
|
+
raise RuntimeError("FROZEN_CONCEPT_PLAN_MODEL_CHANGED")
|
|
66
|
+
return questions, manifest
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _records(path):
|
|
70
|
+
records = {}
|
|
71
|
+
path = Path(path)
|
|
72
|
+
if not path.exists():
|
|
73
|
+
return records
|
|
74
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
75
|
+
if not line.strip():
|
|
76
|
+
continue
|
|
77
|
+
row = json.loads(line)
|
|
78
|
+
previous = records.get(row["id"])
|
|
79
|
+
# A browser double-submit can produce two valid captures. Preserve the
|
|
80
|
+
# first success so evaluation cannot cherry-pick between generations.
|
|
81
|
+
if previous and previous.get("status") == "ok":
|
|
82
|
+
continue
|
|
83
|
+
records[row["id"]] = row
|
|
84
|
+
return records
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def capture(root, *, limit=0):
|
|
88
|
+
root = Path(root)
|
|
89
|
+
questions, manifest = _verify_frozen(root)
|
|
90
|
+
if not os.environ.get("OPENAI_API_KEY"):
|
|
91
|
+
raise RuntimeError("OPENAI_API_KEY_REQUIRED_NO_REQUEST_SENT")
|
|
92
|
+
planner = OpenAIResponsesConceptPlanner(manifest["model"])
|
|
93
|
+
output = root / "responses.jsonl"
|
|
94
|
+
existing = _records(output)
|
|
95
|
+
pending = [row for row in questions
|
|
96
|
+
if existing.get(row["id"], {}).get("status") != "ok"]
|
|
97
|
+
if limit:
|
|
98
|
+
pending = pending[:limit]
|
|
99
|
+
completed = 0
|
|
100
|
+
with output.open("a", encoding="utf-8") as handle:
|
|
101
|
+
for question in pending:
|
|
102
|
+
try:
|
|
103
|
+
plan = planner.plan(question["query"])
|
|
104
|
+
row = {"id": question["id"], "status": "ok", "query_sha256":
|
|
105
|
+
digest(question["query"]), "plan": plan}
|
|
106
|
+
except RuntimeError as exc:
|
|
107
|
+
row = {"id": question["id"], "status": "api_error",
|
|
108
|
+
"query_sha256": digest(question["query"]),
|
|
109
|
+
"error": str(exc).split(":")[:3]}
|
|
110
|
+
handle.write(json.dumps(row, ensure_ascii=False) + "\n")
|
|
111
|
+
handle.flush()
|
|
112
|
+
completed += 1
|
|
113
|
+
if row["status"] != "ok":
|
|
114
|
+
break
|
|
115
|
+
current = _records(output)
|
|
116
|
+
successful = sum(row.get("status") == "ok" for row in current.values())
|
|
117
|
+
return {"attempted": completed, "successful": successful,
|
|
118
|
+
"total_questions": len(questions),
|
|
119
|
+
"remaining": len(questions) - successful}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _metric(ranking, expected):
|
|
123
|
+
if not expected:
|
|
124
|
+
return {"false_accept": bool(ranking), "hit_at_1": None,
|
|
125
|
+
"hit_at_5": None, "reciprocal_rank": None}
|
|
126
|
+
rank = next((i for i, item in enumerate(ranking, 1) if item in set(expected)), None)
|
|
127
|
+
return {"false_accept": None, "hit_at_1": rank == 1,
|
|
128
|
+
"hit_at_5": rank is not None and rank <= 5,
|
|
129
|
+
"reciprocal_rank": 1.0 / rank if rank else 0.0}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _pct(values, p):
|
|
133
|
+
return sorted(values)[round((len(values) - 1) * p)] if values else 0.0
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def evaluate(root, cases_path, db_path, *, limit=8):
|
|
137
|
+
root = Path(root)
|
|
138
|
+
questions, manifest = _verify_frozen(root)
|
|
139
|
+
records, cases = _records(root / "responses.jsonl"), _read(cases_path)
|
|
140
|
+
question_by_id = {row["id"]: row for row in questions}
|
|
141
|
+
case_by_id = {row["id"]: row for row in cases}
|
|
142
|
+
if set(question_by_id) != set(case_by_id):
|
|
143
|
+
raise RuntimeError("QUESTION_AND_LABEL_IDS_DIFFER")
|
|
144
|
+
details, retrieval_ms, planner_ms = [], [], []
|
|
145
|
+
with AssociativeMemoryIndex(db_path) as index:
|
|
146
|
+
index.sync_sources()
|
|
147
|
+
for item_id, question in question_by_id.items():
|
|
148
|
+
case = case_by_id[item_id]
|
|
149
|
+
if question["query"] != case["query"]:
|
|
150
|
+
raise RuntimeError("QUESTION_TEXT_CHANGED:" + item_id)
|
|
151
|
+
record = records.get(item_id)
|
|
152
|
+
plan = (record or {}).get("plan", {}) if (record or {}).get("status") == "ok" else {}
|
|
153
|
+
cues = plan.get("concept_cues", []) if plan.get("status") == "plan" else []
|
|
154
|
+
started = perf_counter()
|
|
155
|
+
if cues:
|
|
156
|
+
result = index.activate(question["query"], concept_cues=cues,
|
|
157
|
+
seed_limit=12, hops=2, fanout=8, max_nodes=24, limit=limit,
|
|
158
|
+
minimum_concept_coverage=manifest["minimum_concept_coverage"])
|
|
159
|
+
else:
|
|
160
|
+
result = {"status": "UNKNOWN", "reason": "PLANNER_ABSTAINED",
|
|
161
|
+
"results": [], "maximum_concept_coverage": 0.0}
|
|
162
|
+
elapsed = (perf_counter() - started) * 1000
|
|
163
|
+
ranking = [row["engram_id"] for row in result["results"]]
|
|
164
|
+
metric = _metric(ranking, case["expected"])
|
|
165
|
+
retrieval_ms.append(elapsed)
|
|
166
|
+
if plan.get("latency_ms") is not None:
|
|
167
|
+
planner_ms.append(float(plan["latency_ms"]))
|
|
168
|
+
details.append({"id": item_id, "style": case["style"],
|
|
169
|
+
"query": question["query"], "expected": case["expected"],
|
|
170
|
+
"generated_plan": {"status": plan.get("status", "missing"),
|
|
171
|
+
"concept_cues": cues},
|
|
172
|
+
"ranking": ranking, "metrics": metric,
|
|
173
|
+
"trace": {"status": result.get("status"),
|
|
174
|
+
"reason": result.get("reason"),
|
|
175
|
+
"maximum_concept_coverage": result.get(
|
|
176
|
+
"maximum_concept_coverage", 0.0)}})
|
|
177
|
+
positives = [row for row in details if row["expected"]]
|
|
178
|
+
controls = [row for row in details if not row["expected"]]
|
|
179
|
+
vague = [row for row in positives if row["style"] == "VAGUE"]
|
|
180
|
+
exact = [row for row in positives if row["style"] == "EXACT"]
|
|
181
|
+
def aggregate(rows):
|
|
182
|
+
return {"count": len(rows),
|
|
183
|
+
"hit_at_1": sum(r["metrics"]["hit_at_1"] for r in rows) / len(rows),
|
|
184
|
+
"hit_at_5": sum(r["metrics"]["hit_at_5"] for r in rows) / len(rows),
|
|
185
|
+
"mrr": sum(r["metrics"]["reciprocal_rank"] for r in rows) / len(rows)}
|
|
186
|
+
usage = [records[row["id"]]["plan"].get("usage", {}) for row in details
|
|
187
|
+
if row["id"] in records and records[row["id"]].get("status") == "ok"]
|
|
188
|
+
successful_plans = sum(row.get("status") == "ok" for row in records.values())
|
|
189
|
+
result = {"benchmark": RUN_NAME, "evaluation_status":
|
|
190
|
+
"COMPLETE" if successful_plans == len(details) else "INCOMPLETE",
|
|
191
|
+
"case_count": len(details), "successful_plans": len(usage),
|
|
192
|
+
"contract": manifest,
|
|
193
|
+
"summary": {"positive": aggregate(positives), "exact": aggregate(exact),
|
|
194
|
+
"vague": aggregate(vague), "control_count": len(controls),
|
|
195
|
+
"false_association_rate": sum(r["metrics"]["false_accept"]
|
|
196
|
+
for r in controls) / len(controls),
|
|
197
|
+
"planner_latency_ms_median": median(planner_ms) if planner_ms else 0.0,
|
|
198
|
+
"planner_latency_ms_p95": _pct(planner_ms, 0.95),
|
|
199
|
+
"retrieval_latency_ms_median": median(retrieval_ms),
|
|
200
|
+
"retrieval_latency_ms_p95": _pct(retrieval_ms, 0.95),
|
|
201
|
+
"input_tokens": sum(x.get("input_tokens", 0) for x in usage),
|
|
202
|
+
"output_tokens": sum(x.get("output_tokens", 0) for x in usage)},
|
|
203
|
+
"details": details}
|
|
204
|
+
_write(root / "report.json", result)
|
|
205
|
+
(root / "RESULTS.md").write_text(render(result), encoding="utf-8")
|
|
206
|
+
return result
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def render(result):
|
|
210
|
+
s = result["summary"]
|
|
211
|
+
return "\n".join([
|
|
212
|
+
"# Target-blind LLM Concept Planner v0.7.1", "",
|
|
213
|
+
f"Status: **{result['evaluation_status']}**", "",
|
|
214
|
+
"| Slice | Hit@1 | Hit@5 | MRR |", "|---|---:|---:|---:|",
|
|
215
|
+
f"| Exact | {s['exact']['hit_at_1']:.2%} | {s['exact']['hit_at_5']:.2%} | {s['exact']['mrr']:.3f} |",
|
|
216
|
+
f"| Vague | {s['vague']['hit_at_1']:.2%} | {s['vague']['hit_at_5']:.2%} | {s['vague']['mrr']:.3f} |",
|
|
217
|
+
f"| All positive | {s['positive']['hit_at_1']:.2%} | {s['positive']['hit_at_5']:.2%} | {s['positive']['mrr']:.3f} |",
|
|
218
|
+
"", f"- Control false association: {s['false_association_rate']:.2%}",
|
|
219
|
+
f"- Planner latency median / p95: {s['planner_latency_ms_median']:.2f} / {s['planner_latency_ms_p95']:.2f} ms",
|
|
220
|
+
f"- Graph retrieval latency median / p95: {s['retrieval_latency_ms_median']:.2f} / {s['retrieval_latency_ms_p95']:.2f} ms",
|
|
221
|
+
f"- Planner input / output tokens: {s['input_tokens']} / {s['output_tokens']}",
|
|
222
|
+
"", "## 연구 경계", "",
|
|
223
|
+
"- API planner에는 query만 전달했다. style, expected, oracle cue, memory content는 전달하지 않았다.",
|
|
224
|
+
"- 현재 24문항은 이전 v0.7 진단에 사용된 development set이므로 final blind 성능이 아니다.",
|
|
225
|
+
"- prompt, schema, model, 0.75 coverage threshold는 manifest hash로 동결했다.",
|
|
226
|
+
"- 다음 단계는 이 계약을 바꾸지 않고 새 untouched 질문을 한 번 평가하는 것이다.", ""])
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def main():
|
|
230
|
+
parser = ArgumentParser()
|
|
231
|
+
parser.add_argument("action", choices=["prepare", "capture", "evaluate", "all"])
|
|
232
|
+
parser.add_argument("--root", default="runs/" + RUN_NAME)
|
|
233
|
+
parser.add_argument("--cases", default="associative_hybrid_benchmark_v07_cases.json")
|
|
234
|
+
parser.add_argument("--db", default=str(default_development_db()))
|
|
235
|
+
parser.add_argument("--model", default=DEFAULT_MODEL)
|
|
236
|
+
parser.add_argument("--limit", type=int, default=0)
|
|
237
|
+
args = parser.parse_args()
|
|
238
|
+
if args.action in {"prepare", "all"}:
|
|
239
|
+
prepare(args.root, args.cases, args.model)
|
|
240
|
+
if args.action in {"capture", "all"}:
|
|
241
|
+
print(json.dumps(capture(args.root, limit=args.limit), ensure_ascii=False))
|
|
242
|
+
if args.action in {"evaluate", "all"}:
|
|
243
|
+
result = evaluate(args.root, args.cases, Path(args.db))
|
|
244
|
+
print(json.dumps(result["summary"], ensure_ascii=False, indent=2))
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
if __name__ == "__main__":
|
|
248
|
+
main()
|