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,272 @@
|
|
|
1
|
+
"""Question-only diagnostic for the machine-executable Graph-MIND plan contract."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
import getpass
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import time
|
|
12
|
+
from urllib.error import HTTPError, URLError
|
|
13
|
+
from urllib.request import Request, urlopen
|
|
14
|
+
|
|
15
|
+
from longmemeval_adapter import load_instances
|
|
16
|
+
from longmemeval_ingestion_calibration import MODEL, RELATIONS, digest, response_text
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
DATA = Path("external/longmemeval/longmemeval_s_cleaned.json")
|
|
20
|
+
OUTPUT = Path("runs/graph-v2.1-executable-plan-v2.2")
|
|
21
|
+
QUESTION_IDS = (
|
|
22
|
+
"gpt4_70e84552_abs", "gpt4_d31cdae3", # compare
|
|
23
|
+
"80ec1f4f_abs", "b6019101", "gpt4_7fce9456", # count
|
|
24
|
+
"60bf93ed_abs", "gpt4_4edbafa2", "07741c45", # date/earliest/latest
|
|
25
|
+
"09ba9854_abs", "720133ac", # subtract/sum
|
|
26
|
+
)
|
|
27
|
+
EXPECTED_OPERATORS = {
|
|
28
|
+
"gpt4_70e84552_abs": "COMPARE", "gpt4_d31cdae3": "COMPARE",
|
|
29
|
+
"80ec1f4f_abs": "COUNT", "b6019101": "COUNT", "gpt4_7fce9456": "COUNT",
|
|
30
|
+
"60bf93ed_abs": "DATE_DIFF", "gpt4_4edbafa2": "EARLIEST",
|
|
31
|
+
"07741c45": "LATEST", "09ba9854_abs": "SUBTRACT", "720133ac": "SUM",
|
|
32
|
+
}
|
|
33
|
+
OPERATORS = ("LOOKUP", "COUNT", "SUM", "SUBTRACT", "DATE_DIFF", "COMPARE",
|
|
34
|
+
"LATEST", "EARLIEST")
|
|
35
|
+
VALUE_FIELDS = ("OBJECT_TEXT", "NUMERIC_VALUE", "DATE_VALUE", "SESSION_DATE", "EVENT_COUNT")
|
|
36
|
+
|
|
37
|
+
PROMPT = """Compile one long-term-memory question into a machine-executable query plan.
|
|
38
|
+
The memory, answer, evidence labels, and official task type are unavailable. Do not answer.
|
|
39
|
+
|
|
40
|
+
Use only controlled ingestion relations. Put every value needed by the top-level operator in a
|
|
41
|
+
separate operand. Each operand is a typed filter over the complete memory index. Normalize explicit
|
|
42
|
+
calendar ranges to ISO start inclusive/end exclusive using question_date. A constraint relative to
|
|
43
|
+
another memory event, such as "before making an offer", must use time_relation and
|
|
44
|
+
time_reference_operand; never put symbolic text in an ISO field. Use null when the question does not
|
|
45
|
+
specify a value. relation_details, entity_terms, and category_tags are lexical constraints,
|
|
46
|
+
not guessed answers. For COUNT, choose a distinct key and set index_scope_required true. For
|
|
47
|
+
SUBTRACT, DATE_DIFF, and COMPARE emit operands in left/right order. For SUM, operands may describe
|
|
48
|
+
separate stored values or one repeated event class. Missing required operands must cause UNKNOWN.
|
|
49
|
+
The words current/latest/most recent require top-level LATEST; first/earliest require EARLIEST.
|
|
50
|
+
Questions about I/my/the narrator use USER_MEMORY. Use ASSISTANT_HISTORY only when the question
|
|
51
|
+
explicitly asks what the assistant previously said or recommended. For COUNT, aggregate_operand must
|
|
52
|
+
name exactly the operand being counted, and that operand needs a non-NONE distinct_by. If
|
|
53
|
+
empty_result is ZERO_IF_COMPLETE, no abstain_if rule may treat zero matches as missing evidence.
|
|
54
|
+
Return only the requested JSON."""
|
|
55
|
+
|
|
56
|
+
NULL_STRING = {"type": ["string", "null"]}
|
|
57
|
+
OPERAND = {"type": "object", "properties": {
|
|
58
|
+
"name": {"type": "string"},
|
|
59
|
+
"source_scope": {"type": "string", "enum": ["USER_MEMORY", "ASSISTANT_HISTORY", "BOTH"]},
|
|
60
|
+
"relations": {"type": "array", "items": {"type": "string", "enum": list(RELATIONS)}},
|
|
61
|
+
"relation_details": {"type": "array", "items": {"type": "string"}},
|
|
62
|
+
"entity_terms": {"type": "array", "items": {"type": "string"}},
|
|
63
|
+
"category_tags": {"type": "array", "items": {"type": "string"}},
|
|
64
|
+
"value_field": {"type": "string", "enum": list(VALUE_FIELDS)},
|
|
65
|
+
"value_type": {"type": "string", "enum": [
|
|
66
|
+
"ENTITY", "TEXT", "NUMBER", "MONEY", "DURATION", "DATE", "BOOLEAN"]},
|
|
67
|
+
"unit": NULL_STRING, "selector": {"type": "string", "enum": [
|
|
68
|
+
"ALL", "UNIQUE", "LATEST", "EARLIEST"]},
|
|
69
|
+
"time_start": NULL_STRING, "time_end_exclusive": NULL_STRING,
|
|
70
|
+
"time_relation": {"type": "string", "enum": [
|
|
71
|
+
"ANY", "BEFORE", "AFTER", "ON_OR_BEFORE", "ON_OR_AFTER"]},
|
|
72
|
+
"time_reference_operand": NULL_STRING,
|
|
73
|
+
"distinct_by": {"type": "string", "enum": [
|
|
74
|
+
"NONE", "CANONICAL_ENTITY_ID", "OCCURRENCE_ID", "OBJECT_TEXT", "EVENT_ID"]},
|
|
75
|
+
"required": {"type": "boolean"}},
|
|
76
|
+
"required": ["name", "source_scope", "relations", "relation_details", "entity_terms",
|
|
77
|
+
"category_tags", "value_field", "value_type", "unit", "selector", "time_start",
|
|
78
|
+
"time_end_exclusive", "time_relation", "time_reference_operand", "distinct_by",
|
|
79
|
+
"required"], "additionalProperties": False}
|
|
80
|
+
SCHEMA = {"type": "object", "properties": {
|
|
81
|
+
"operator": {"type": "string", "enum": list(OPERATORS)},
|
|
82
|
+
"operands": {"type": "array", "items": OPERAND},
|
|
83
|
+
"aggregate_operand": NULL_STRING,
|
|
84
|
+
"result_type": {"type": "string", "enum": [
|
|
85
|
+
"ENTITY", "TEXT", "NUMBER", "MONEY", "DURATION", "DATE", "BOOLEAN"]},
|
|
86
|
+
"result_unit": NULL_STRING,
|
|
87
|
+
"empty_result": {"type": "string", "enum": ["ZERO_IF_COMPLETE", "EMPTY_LIST", "UNKNOWN"]},
|
|
88
|
+
"index_scope_required": {"type": "boolean"},
|
|
89
|
+
"abstain_if": {"type": "array", "items": {"type": "string"}},
|
|
90
|
+
"confidence": {"type": "string", "enum": ["HIGH", "MEDIUM", "LOW"]}},
|
|
91
|
+
"required": ["operator", "operands", "aggregate_operand", "result_type", "result_unit", "empty_result",
|
|
92
|
+
"index_scope_required", "abstain_if", "confidence"],
|
|
93
|
+
"additionalProperties": False}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def questions() -> list[dict]:
|
|
97
|
+
mapping = {row["question_id"]: row for row in load_instances(DATA)}
|
|
98
|
+
return [{"question_id": question_id, "question": mapping[question_id]["question"],
|
|
99
|
+
"question_date": mapping[question_id]["question_date"]}
|
|
100
|
+
for question_id in QUESTION_IDS]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def payload(row: dict) -> dict:
|
|
104
|
+
supplied = {"question": row["question"], "question_date": row["question_date"]}
|
|
105
|
+
return {"model": MODEL, "store": False, "reasoning": {"effort": "low"},
|
|
106
|
+
"input": [{"role": "system", "content": PROMPT},
|
|
107
|
+
{"role": "user", "content": json.dumps(supplied, ensure_ascii=False)}],
|
|
108
|
+
"text": {"format": {"type": "json_schema", "name": "executable_plan_v2",
|
|
109
|
+
"strict": True, "schema": SCHEMA}},
|
|
110
|
+
"max_output_tokens": 8192}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def call_api(key: str, body: dict) -> tuple[dict, float]:
|
|
114
|
+
started = time.perf_counter()
|
|
115
|
+
request = Request("https://api.openai.com/v1/responses",
|
|
116
|
+
data=json.dumps(body).encode("utf-8"),
|
|
117
|
+
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"})
|
|
118
|
+
with urlopen(request, timeout=120) as response:
|
|
119
|
+
return json.load(response), time.perf_counter() - started
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def load_responses() -> dict[str, dict]:
|
|
123
|
+
result = {}
|
|
124
|
+
path = OUTPUT / "responses.jsonl"
|
|
125
|
+
if path.exists():
|
|
126
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
127
|
+
if line.strip():
|
|
128
|
+
row = json.loads(line)
|
|
129
|
+
result[row["question_id"]] = row
|
|
130
|
+
return result
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def send(key: str, row: dict) -> dict:
|
|
134
|
+
body = payload(row)
|
|
135
|
+
record = {"question_id": row["question_id"], "request_sha256": digest(body),
|
|
136
|
+
"timestamp": datetime.now(timezone.utc).isoformat()}
|
|
137
|
+
started = time.perf_counter()
|
|
138
|
+
try:
|
|
139
|
+
raw, seconds = call_api(key, body)
|
|
140
|
+
record.update(status="ok", plan=json.loads(response_text(raw)), usage=raw.get("usage", {}),
|
|
141
|
+
response_id=raw.get("id"), model=raw.get("model"), seconds=seconds)
|
|
142
|
+
except HTTPError as error:
|
|
143
|
+
record.update(status="transport_error", http_status=error.code,
|
|
144
|
+
seconds=time.perf_counter() - started)
|
|
145
|
+
except (URLError, TimeoutError) as error:
|
|
146
|
+
record.update(status="transport_error", error=type(error).__name__,
|
|
147
|
+
seconds=time.perf_counter() - started)
|
|
148
|
+
except (ValueError, KeyError, TypeError) as error:
|
|
149
|
+
record.update(status="parse_failure", error=str(error),
|
|
150
|
+
usage=locals().get("raw", {}).get("usage", {}),
|
|
151
|
+
seconds=time.perf_counter() - started)
|
|
152
|
+
return record
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def run(key: str, workers: int) -> None:
|
|
156
|
+
OUTPUT.mkdir(parents=True, exist_ok=True)
|
|
157
|
+
path = OUTPUT / "responses.jsonl"
|
|
158
|
+
previous = load_responses()
|
|
159
|
+
pending = [row for row in questions()
|
|
160
|
+
if previous.get(row["question_id"], {}).get("status") != "ok"]
|
|
161
|
+
with path.open("a", encoding="utf-8") as stream:
|
|
162
|
+
with ThreadPoolExecutor(max_workers=workers) as pool:
|
|
163
|
+
futures = [pool.submit(send, key, row) for row in pending]
|
|
164
|
+
for future in as_completed(futures):
|
|
165
|
+
record = future.result()
|
|
166
|
+
stream.write(json.dumps(record, ensure_ascii=False) + "\n")
|
|
167
|
+
stream.flush()
|
|
168
|
+
print(f"{record['question_id']} {record['status']}", flush=True)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def structural_audit(plan: dict, expected: str) -> list[str]:
|
|
172
|
+
errors = []
|
|
173
|
+
if plan["operator"] != expected:
|
|
174
|
+
errors.append("OPERATOR_MISMATCH")
|
|
175
|
+
if not plan["operands"]:
|
|
176
|
+
errors.append("NO_OPERANDS")
|
|
177
|
+
if expected in {"SUBTRACT", "DATE_DIFF", "COMPARE"} and len(plan["operands"]) != 2:
|
|
178
|
+
errors.append("BINARY_OPERAND_COUNT")
|
|
179
|
+
if expected == "COUNT":
|
|
180
|
+
if not plan["index_scope_required"]:
|
|
181
|
+
errors.append("COUNT_SCOPE_NOT_REQUIRED")
|
|
182
|
+
operands_by_name = {operand["name"]: operand for operand in plan["operands"]}
|
|
183
|
+
aggregate = operands_by_name.get(plan["aggregate_operand"])
|
|
184
|
+
if aggregate is None or aggregate["distinct_by"] == "NONE":
|
|
185
|
+
errors.append("COUNT_DISTINCT_KEY_MISSING")
|
|
186
|
+
if (plan["empty_result"] == "ZERO_IF_COMPLETE"
|
|
187
|
+
and any("NO_MATCH" in rule.upper() for rule in plan["abstain_if"])):
|
|
188
|
+
errors.append("COUNT_EMPTY_CONTRADICTION")
|
|
189
|
+
elif plan["aggregate_operand"] is not None:
|
|
190
|
+
errors.append("UNEXPECTED_AGGREGATE_OPERAND")
|
|
191
|
+
operand_names = {operand["name"] for operand in plan["operands"]}
|
|
192
|
+
for operand in plan["operands"]:
|
|
193
|
+
if operand["source_scope"] != "USER_MEMORY":
|
|
194
|
+
errors.append(f"{operand['name']}:SOURCE_SCOPE_LEAK")
|
|
195
|
+
if not operand["relations"]:
|
|
196
|
+
errors.append(f"{operand['name']}:NO_RELATION")
|
|
197
|
+
if operand["time_start"] and not re_iso(operand["time_start"]):
|
|
198
|
+
errors.append(f"{operand['name']}:BAD_TIME_START")
|
|
199
|
+
if operand["time_end_exclusive"] and not re_iso(operand["time_end_exclusive"]):
|
|
200
|
+
errors.append(f"{operand['name']}:BAD_TIME_END")
|
|
201
|
+
if operand["time_relation"] != "ANY" and not operand["time_reference_operand"]:
|
|
202
|
+
errors.append(f"{operand['name']}:TIME_REFERENCE_MISSING")
|
|
203
|
+
if (operand["time_reference_operand"] is not None
|
|
204
|
+
and operand["time_reference_operand"] not in operand_names):
|
|
205
|
+
errors.append(f"{operand['name']}:UNKNOWN_TIME_REFERENCE")
|
|
206
|
+
return errors
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def re_iso(value: str) -> bool:
|
|
210
|
+
try:
|
|
211
|
+
datetime.fromisoformat(value)
|
|
212
|
+
return True
|
|
213
|
+
except ValueError:
|
|
214
|
+
return False
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def summarize() -> dict:
|
|
218
|
+
responses = load_responses()
|
|
219
|
+
audits = []
|
|
220
|
+
for question_id in QUESTION_IDS:
|
|
221
|
+
record = responses.get(question_id, {})
|
|
222
|
+
errors = structural_audit(record["plan"], EXPECTED_OPERATORS[question_id]) \
|
|
223
|
+
if record.get("status") == "ok" else [record.get("status", "MISSING")]
|
|
224
|
+
audits.append({"question_id": question_id, "expected_operator": EXPECTED_OPERATORS[question_id],
|
|
225
|
+
"actual_operator": record.get("plan", {}).get("operator"),
|
|
226
|
+
"errors": errors, "passed": not errors})
|
|
227
|
+
inp = sum(row.get("usage", {}).get("input_tokens", 0) for row in responses.values())
|
|
228
|
+
out = sum(row.get("usage", {}).get("output_tokens", 0) for row in responses.values())
|
|
229
|
+
report = {"role": "question-only executable-plan structural diagnostic",
|
|
230
|
+
"questions": len(QUESTION_IDS),
|
|
231
|
+
"api_success": sum(row.get("status") == "ok" for row in responses.values()),
|
|
232
|
+
"structural_pass": sum(row["passed"] for row in audits), "audits": audits,
|
|
233
|
+
"usage": {"input_tokens": inp, "output_tokens": out,
|
|
234
|
+
"cost_usd": inp / 1e6 * .25 + out / 1e6 * 2},
|
|
235
|
+
"claim_limit": "No plan gold or execution result; structural executability only."}
|
|
236
|
+
OUTPUT.mkdir(parents=True, exist_ok=True)
|
|
237
|
+
(OUTPUT / "report.json").write_text(
|
|
238
|
+
json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
239
|
+
(OUTPUT / "RESULTS.md").write_text(
|
|
240
|
+
"# Executable plan v2 diagnostic\n\n"
|
|
241
|
+
"> question과 question date만 사용한 구조 진단이다. plan 정확도나 executor 정답률이 아니다.\n\n"
|
|
242
|
+
f"- API 성공: **{report['api_success']}/{len(QUESTION_IDS)}**\n"
|
|
243
|
+
f"- structural pass: **{report['structural_pass']}/{len(QUESTION_IDS)}**\n"
|
|
244
|
+
f"- 비용: **${report['usage']['cost_usd']:.4f}**\n",
|
|
245
|
+
encoding="utf-8")
|
|
246
|
+
return report
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def main() -> None:
|
|
250
|
+
parser = argparse.ArgumentParser()
|
|
251
|
+
parser.add_argument("--run", action="store_true")
|
|
252
|
+
parser.add_argument("--key-stdin", action="store_true")
|
|
253
|
+
parser.add_argument("--workers", type=int, default=5)
|
|
254
|
+
args = parser.parse_args()
|
|
255
|
+
OUTPUT.mkdir(parents=True, exist_ok=True)
|
|
256
|
+
(OUTPUT / "questions.json").write_text(
|
|
257
|
+
json.dumps(questions(), ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
258
|
+
(OUTPUT / "freeze.json").write_text(json.dumps({
|
|
259
|
+
"question_ids": list(QUESTION_IDS), "expected_operators": EXPECTED_OPERATORS,
|
|
260
|
+
"prompt_sha256": digest(PROMPT), "schema_sha256": digest(SCHEMA),
|
|
261
|
+
"model_input_excludes": ["memory", "answer", "evidence", "question_type"],
|
|
262
|
+
"claim_limit": "Development structural diagnostic"}, indent=2) + "\n", encoding="utf-8")
|
|
263
|
+
if args.run:
|
|
264
|
+
key = getpass.getpass("OpenAI API key: ") if args.key_stdin else os.environ.get("OPENAI_API_KEY", "")
|
|
265
|
+
if not key:
|
|
266
|
+
raise RuntimeError("OPENAI_API_KEY is absent; no requests sent")
|
|
267
|
+
run(key, args.workers)
|
|
268
|
+
print(json.dumps(summarize(), ensure_ascii=False, indent=2))
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
if __name__ == "__main__":
|
|
272
|
+
main()
|