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.
@@ -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()