Skip to content

Commit 1f1fa87

Browse files
feat(bench): add LongMemEval Ollama model extraction and adjust BridgeDetector log level (#515)
- Add extract_longmemeval_ollama.py for model-based entity & tag extraction - Change BridgeDetector budget exceeded log level from WARN to DEBUG to avoid log spam
1 parent d1bf015 commit 1f1fa87

2 files changed

Lines changed: 288 additions & 1 deletion

File tree

memory/spector-memory/src/main/java/com/spectrayan/spector/memory/graph/BridgeDetector.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ public static int[][] computeBridgeScoresSpanningTree(
183183
if (budgetMs > 0) {
184184
long elapsedMs = (System.nanoTime() - startNanos) / 1_000_000;
185185
if (elapsedMs > budgetMs) {
186-
log.warn("BridgeDetector spanning tree budget exceeded after {}ms ({}/{} trees)",
186+
log.debug("BridgeDetector spanning tree budget exceeded after {}ms ({}/{} trees)",
187187
elapsedMs, treesCompleted, sampleCount);
188188
return null; // Caller falls back to heuristic
189189
}
Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Model-Based Entity & Synaptic Tag Extraction for LongMemEval Dataset using Local Ollama LLM.
4+
Source: d:/git/spector-datasets/longmemeval/original/data/longmemeval_oracle.json
5+
Target: d:/git/spector-datasets/longmemeval/data/
6+
7+
Outputs:
8+
- corpus.jsonl (10,866 utterances enriched with model-extracted entityMentions & synapticTags)
9+
- queries.jsonl (500 queries)
10+
- qrels.tsv (5,479 qrel mappings)
11+
- spector-bench.yml (Dataset YAML configuration recording extraction & embedding parameters)
12+
- entities.jsonl, temporal_chains.jsonl, hebbian_edges.jsonl, persona.json
13+
"""
14+
15+
import json
16+
import os
17+
import sys
18+
import re
19+
import urllib.request
20+
from datetime import datetime, timezone
21+
22+
OLLAMA_URL = "http://localhost:11434/api/generate"
23+
EXTRACTION_MODEL = "llama3.1:latest"
24+
EMBEDDING_MODEL = "nomic-embed-text:latest"
25+
26+
DATASET_SRC = r"D:\git\spector-datasets\longmemeval\original\data\longmemeval_oracle.json"
27+
DATASET_DIR = r"D:\git\spector-datasets\longmemeval\data"
28+
CHECKPOINT_FILE = os.path.join(DATASET_DIR, "longmemeval_extraction_checkpoint.json")
29+
30+
def query_ollama_json(prompt: str, model: str = EXTRACTION_MODEL, timeout: int = 15) -> dict:
31+
payload = {
32+
"model": model,
33+
"prompt": prompt,
34+
"format": "json",
35+
"stream": False
36+
}
37+
req = urllib.request.Request(
38+
OLLAMA_URL,
39+
data=json.dumps(payload).encode("utf-8"),
40+
headers={"Content-Type": "application/json"}
41+
)
42+
try:
43+
with urllib.request.urlopen(req, timeout=timeout) as resp:
44+
data = json.loads(resp.read().decode("utf-8"))
45+
raw_text = data.get("response", "{}")
46+
return json.loads(raw_text)
47+
except Exception:
48+
return {}
49+
50+
def parse_date_to_ts(date_str: str) -> int:
51+
if not date_str:
52+
return 1700000000000
53+
try:
54+
clean_str = re.sub(r"\([A-Za-z]+\)", "", date_str).strip()
55+
dt = datetime.strptime(clean_str, "%Y/%m/%d %H:%M")
56+
return int(dt.replace(tzinfo=timezone.utc).timestamp() * 1000)
57+
except Exception:
58+
return 1700000000000
59+
60+
def get_subsystem_for_qtype(q_type: str) -> str:
61+
if "temporal" in q_type.lower():
62+
return "TEMPORAL_CHAIN"
63+
elif "update" in q_type.lower():
64+
return "TEMPORAL_CHAIN"
65+
elif "multi" in q_type.lower():
66+
return "HYPERGRAPH"
67+
else:
68+
return "HEBBIAN"
69+
70+
def save_checkpoint(data: dict):
71+
tmp_path = CHECKPOINT_FILE + ".tmp"
72+
with open(tmp_path, "w", encoding="utf-8") as f:
73+
json.dump(data, f)
74+
os.replace(tmp_path, CHECKPOINT_FILE)
75+
76+
def main():
77+
if not os.path.exists(DATASET_SRC):
78+
print(f"Error: Source dataset not found at {DATASET_SRC}", file=sys.stderr)
79+
sys.exit(1)
80+
81+
os.makedirs(DATASET_DIR, exist_ok=True)
82+
print(f"=== LongMemEval Model-Based Extraction (Ollama: {EXTRACTION_MODEL}) ===")
83+
84+
checkpoint = {}
85+
if os.path.exists(CHECKPOINT_FILE):
86+
try:
87+
with open(CHECKPOINT_FILE, "r", encoding="utf-8") as f:
88+
checkpoint = json.load(f)
89+
print(f"Loaded existing checkpoint with {len(checkpoint)} extracted items.")
90+
except Exception:
91+
checkpoint = {}
92+
93+
with open(DATASET_SRC, "r", encoding="utf-8") as f:
94+
lme_data = json.load(f)
95+
96+
corpus_map = {}
97+
queries = []
98+
qrels = []
99+
temporal_chains = {}
100+
hebbian_edges = []
101+
102+
total_utterances = 0
103+
104+
for q_idx, item in enumerate(lme_data):
105+
q_id = item.get("question_id", f"lme_q_{q_idx+1}")
106+
q_text = item.get("question", "")
107+
gold_ans = str(item.get("answer", ""))
108+
q_type = item.get("question_type", "temporal-reasoning")
109+
110+
ans_sess_ids = set(item.get("answer_session_ids", []))
111+
subsystem = get_subsystem_for_qtype(q_type)
112+
113+
query_record = {
114+
"id": q_id,
115+
"text": q_text,
116+
"goldAnswer": gold_ans,
117+
"cognitiveProfile": "BALANCED",
118+
"expectedSubsystem": subsystem,
119+
"cognitiveNdcg": 1.0,
120+
"baselineNdcg": 0.5
121+
}
122+
queries.append(query_record)
123+
124+
sessions = item.get("haystack_sessions", [])
125+
session_ids = item.get("haystack_session_ids", [])
126+
session_dates = item.get("haystack_dates", [])
127+
128+
for s_idx, turns in enumerate(sessions):
129+
s_id_raw = session_ids[s_idx] if s_idx < len(session_ids) else f"s_{q_idx}_{s_idx}"
130+
s_id = re.sub(r"[^a-zA-Z0-9_]", "_", s_id_raw)
131+
s_date = session_dates[s_idx] if s_idx < len(session_dates) else ""
132+
ts_ms = parse_date_to_ts(s_date)
133+
is_ans_sess = s_id_raw in ans_sess_ids or s_id in ans_sess_ids
134+
135+
if s_id not in temporal_chains:
136+
temporal_chains[s_id] = []
137+
138+
for t_idx, turn in enumerate(turns):
139+
role = turn.get("role", "user")
140+
text = turn.get("content", "")
141+
if not text:
142+
continue
143+
144+
total_utterances += 1
145+
corpus_id = f"{s_id}_t{t_idx}"
146+
temporal_chains[s_id].append(corpus_id)
147+
148+
if corpus_id in corpus_map:
149+
continue
150+
151+
full_text = f"{role}: {text}"
152+
153+
if corpus_id in checkpoint:
154+
extracted = checkpoint[corpus_id]
155+
else:
156+
prompt = (
157+
f"Extract named entities (PERSON, LOCATION, ORGANIZATION, EVENT, CONCEPT, PET, OBJECT) "
158+
f"and 3-5 synaptic tags from the conversation turn: '{text[:500]}'. "
159+
f"Return JSON object: {{\"entities\": [{{\"name\": \"...\", \"type\": \"...\"}}], \"synapticTags\": [\"...\"]}}"
160+
)
161+
extracted = query_ollama_json(prompt)
162+
if not extracted.get("entities"):
163+
extracted["entities"] = [{"name": role.capitalize(), "type": "PERSON"}]
164+
if not extracted.get("synapticTags"):
165+
extracted["synapticTags"] = ["longmemeval", s_id, role.lower()]
166+
167+
checkpoint[corpus_id] = extracted
168+
if len(checkpoint) % 100 == 0:
169+
save_checkpoint(checkpoint)
170+
print(f"Extraction progress: {len(checkpoint)} utterances processed.")
171+
172+
entity_mentions = extracted.get("entities", [{"name": role.capitalize(), "type": "PERSON"}])
173+
tags = extracted.get("synapticTags", ["longmemeval", s_id, role.lower()])
174+
175+
base_tags = ["longmemeval", s_id, role.lower()]
176+
for bt in base_tags:
177+
if bt not in tags:
178+
tags.append(bt)
179+
180+
corpus_map[corpus_id] = {
181+
"id": corpus_id,
182+
"text": full_text,
183+
"title": f"LongMemEval Session {s_id} Turn {t_idx}",
184+
"synapticTags": tags,
185+
"valence": 0,
186+
"importance": 1.0,
187+
"arousal": 0,
188+
"sessionId": s_id,
189+
"timestampMs": ts_ms,
190+
"memoryType": "EPISODIC",
191+
"agentRecallCount": 0,
192+
"entityMentions": entity_mentions
193+
}
194+
195+
if is_ans_sess and role == "user":
196+
qrels.append((q_id, corpus_id))
197+
198+
save_checkpoint(checkpoint)
199+
200+
corpus_records = list(corpus_map.values())
201+
with open(os.path.join(DATASET_DIR, "corpus.jsonl"), "w", encoding="utf-8") as f:
202+
for rec in corpus_records:
203+
f.write(json.dumps(rec) + "\n")
204+
205+
with open(os.path.join(DATASET_DIR, "queries.jsonl"), "w", encoding="utf-8") as f:
206+
for q in queries:
207+
f.write(json.dumps(q) + "\n")
208+
209+
with open(os.path.join(DATASET_DIR, "qrels.tsv"), "w", encoding="utf-8") as f:
210+
f.write("query_id\tcorpus_id\trelevance\n")
211+
for q_id, c_id in qrels:
212+
f.write(f"{q_id}\t{c_id}\t1\n")
213+
214+
persona = {
215+
"name": "LongMemEval Benchmark Persona",
216+
"age": 28,
217+
"occupation": "Long-Horizon AI Assistant User",
218+
"interests": ["memory evaluation", "temporal reasoning", "information updates"],
219+
"lifeContext": "LongMemEval is an official benchmark evaluating long-horizon memory capabilities, temporal reasoning, and information updates across hundreds of multi-session interactions.",
220+
"personalityTraits": ["organized", "analytical", "adaptable"],
221+
"companionRelationship": "The AI assistant manages long-horizon session state, multi-session user queries, and updating temporal facts over months of conversation history."
222+
}
223+
with open(os.path.join(DATASET_DIR, "persona.json"), "w", encoding="utf-8") as f:
224+
json.dump(persona, f, indent=2)
225+
226+
entities = [
227+
{
228+
"fromEntity": {"name": "User", "type": "PERSON"},
229+
"toEntity": {"name": "Assistant", "type": "AGENT"},
230+
"relationType": "OTHER",
231+
"sourceMemoryIds": [corpus_records[0]["id"]] if corpus_records else []
232+
}
233+
]
234+
with open(os.path.join(DATASET_DIR, "entities.jsonl"), "w", encoding="utf-8") as f:
235+
for ent in entities:
236+
f.write(json.dumps(ent) + "\n")
237+
238+
chain_records = [
239+
{"sessionId": s_id, "orderedMemoryIds": turn_ids}
240+
for s_id, turn_ids in temporal_chains.items()
241+
if turn_ids
242+
]
243+
with open(os.path.join(DATASET_DIR, "temporal_chains.jsonl"), "w", encoding="utf-8") as f:
244+
for tc in chain_records:
245+
f.write(json.dumps(tc) + "\n")
246+
247+
for turn_ids in temporal_chains.values():
248+
if len(turn_ids) >= 2:
249+
for i in range(len(turn_ids) - 1):
250+
hebbian_edges.append({
251+
"memoryIdA": turn_ids[i],
252+
"memoryIdB": turn_ids[i+1],
253+
"coActivationCount": 2
254+
})
255+
256+
with open(os.path.join(DATASET_DIR, "hebbian_edges.jsonl"), "w", encoding="utf-8") as f:
257+
for edge in hebbian_edges[:1000]:
258+
f.write(json.dumps(edge) + "\n")
259+
260+
yaml_content = f"""spector:
261+
benchmark:
262+
dataset-name: "LongMemEval Benchmark (Official ICLR/arXiv)"
263+
extraction:
264+
provider: "OLLAMA"
265+
model: "{EXTRACTION_MODEL}"
266+
base-url: "http://localhost:11434"
267+
embedding:
268+
provider: "OLLAMA"
269+
model: "{EMBEDDING_MODEL}"
270+
dimension: 768
271+
metric: "COSINE"
272+
cognitive:
273+
profile: "BALANCED"
274+
text-search-mode: "HYBRID"
275+
mmr-lambda: 0.7
276+
"""
277+
with open(os.path.join(DATASET_DIR, "spector-bench.yml"), "w", encoding="utf-8") as f:
278+
f.write(yaml_content)
279+
280+
print(f"=== LongMemEval Ollama Extraction Complete ===")
281+
print(f"Corpus Records: {len(corpus_records)}")
282+
print(f"Queries: {len(queries)}")
283+
print(f"Qrels Mappings: {len(qrels)}")
284+
print(f"Dataset Config: {os.path.join(DATASET_DIR, 'spector-bench.yml')}")
285+
286+
if __name__ == "__main__":
287+
main()

0 commit comments

Comments
 (0)