@geravant/sinain 1.13.0 → 1.14.0

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.
Files changed (67) hide show
  1. package/.env.example +4 -2
  2. package/config-shared.js +1 -0
  3. package/package.json +4 -1
  4. package/sinain-agent/run.sh +36 -4
  5. package/sinain-core/src/buffers/feed-buffer.ts +6 -4
  6. package/sinain-core/src/index.ts +50 -19
  7. package/sinain-memory/graph_query.py +12 -3
  8. package/sinain-memory/knowledge_integrator.py +194 -10
  9. package/sinain-memory/__pycache__/common.cpython-312.pyc +0 -0
  10. package/sinain-memory/__pycache__/embed_client.cpython-312.pyc +0 -0
  11. package/sinain-memory/__pycache__/graph_query.cpython-312.pyc +0 -0
  12. package/sinain-memory/__pycache__/knowledge_integrator.cpython-312.pyc +0 -0
  13. package/sinain-memory/__pycache__/session_distiller.cpython-312.pyc +0 -0
  14. package/sinain-memory/__pycache__/triplestore.cpython-312.pyc +0 -0
  15. package/sinain-memory/eval/__init__.py +0 -0
  16. package/sinain-memory/eval/__pycache__/__init__.cpython-312.pyc +0 -0
  17. package/sinain-memory/eval/assertions.py +0 -267
  18. package/sinain-memory/eval/benchmarks/__init__.py +0 -0
  19. package/sinain-memory/eval/benchmarks/__pycache__/__init__.cpython-312.pyc +0 -0
  20. package/sinain-memory/eval/benchmarks/__pycache__/base_adapter.cpython-312.pyc +0 -0
  21. package/sinain-memory/eval/benchmarks/__pycache__/config.cpython-312.pyc +0 -0
  22. package/sinain-memory/eval/benchmarks/__pycache__/evaluate.cpython-312.pyc +0 -0
  23. package/sinain-memory/eval/benchmarks/__pycache__/ingest.cpython-312.pyc +0 -0
  24. package/sinain-memory/eval/benchmarks/__pycache__/longmemeval_adapter.cpython-312.pyc +0 -0
  25. package/sinain-memory/eval/benchmarks/__pycache__/meeting_adapter.cpython-312.pyc +0 -0
  26. package/sinain-memory/eval/benchmarks/__pycache__/meeting_runner.cpython-312.pyc +0 -0
  27. package/sinain-memory/eval/benchmarks/__pycache__/query.cpython-312.pyc +0 -0
  28. package/sinain-memory/eval/benchmarks/__pycache__/report.cpython-312.pyc +0 -0
  29. package/sinain-memory/eval/benchmarks/__pycache__/runner.cpython-312.pyc +0 -0
  30. package/sinain-memory/eval/benchmarks/base_adapter.py +0 -43
  31. package/sinain-memory/eval/benchmarks/config.py +0 -23
  32. package/sinain-memory/eval/benchmarks/evaluate.py +0 -146
  33. package/sinain-memory/eval/benchmarks/ingest.py +0 -152
  34. package/sinain-memory/eval/benchmarks/judges/__init__.py +0 -0
  35. package/sinain-memory/eval/benchmarks/judges/__pycache__/__init__.cpython-312.pyc +0 -0
  36. package/sinain-memory/eval/benchmarks/judges/__pycache__/qa_judge.cpython-312.pyc +0 -0
  37. package/sinain-memory/eval/benchmarks/judges/qa_judge.py +0 -81
  38. package/sinain-memory/eval/benchmarks/longmemeval_adapter.py +0 -177
  39. package/sinain-memory/eval/benchmarks/meeting_adapter.py +0 -81
  40. package/sinain-memory/eval/benchmarks/meeting_runner.py +0 -230
  41. package/sinain-memory/eval/benchmarks/query.py +0 -193
  42. package/sinain-memory/eval/benchmarks/report.py +0 -87
  43. package/sinain-memory/eval/benchmarks/run_meeting_bench.sh +0 -318
  44. package/sinain-memory/eval/benchmarks/runner.py +0 -283
  45. package/sinain-memory/eval/judges/__init__.py +0 -0
  46. package/sinain-memory/eval/judges/base_judge.py +0 -61
  47. package/sinain-memory/eval/judges/curation_judge.py +0 -46
  48. package/sinain-memory/eval/judges/insight_judge.py +0 -48
  49. package/sinain-memory/eval/judges/mining_judge.py +0 -42
  50. package/sinain-memory/eval/judges/signal_judge.py +0 -45
  51. package/sinain-memory/eval/retrieval_benchmark.jsonl +0 -12
  52. package/sinain-memory/eval/retrieval_evaluator.py +0 -186
  53. package/sinain-memory/eval/schemas.py +0 -247
  54. package/sinain-memory/tests/__init__.py +0 -0
  55. package/sinain-memory/tests/conftest.py +0 -189
  56. package/sinain-memory/tests/test_curator_helpers.py +0 -94
  57. package/sinain-memory/tests/test_embedder.py +0 -210
  58. package/sinain-memory/tests/test_extract_json.py +0 -124
  59. package/sinain-memory/tests/test_feedback_computation.py +0 -121
  60. package/sinain-memory/tests/test_miner_helpers.py +0 -71
  61. package/sinain-memory/tests/test_module_management.py +0 -458
  62. package/sinain-memory/tests/test_parsers.py +0 -96
  63. package/sinain-memory/tests/test_tick_evaluator.py +0 -430
  64. package/sinain-memory/tests/test_triple_extractor.py +0 -255
  65. package/sinain-memory/tests/test_triple_ingest.py +0 -191
  66. package/sinain-memory/tests/test_triple_migrate.py +0 -138
  67. package/sinain-memory/tests/test_triplestore.py +0 -248
@@ -1,81 +0,0 @@
1
- """LLM-as-Judge: QA answer quality evaluator (LongMemEval-compatible, 1-5 scale).
2
-
3
- Uses GPT-4o via OpenRouter for comparability with published results.
4
- """
5
-
6
- from __future__ import annotations
7
-
8
- import sys
9
- from pathlib import Path
10
-
11
- # Add sinain-memory to path for common imports
12
- _koog_dir = str(Path(__file__).resolve().parent.parent.parent.parent)
13
- if _koog_dir not in sys.path:
14
- sys.path.insert(0, _koog_dir)
15
-
16
- from common import LLMError, call_llm, extract_json # noqa: E402
17
-
18
- SYSTEM_PROMPT = """\
19
- You are evaluating whether a predicted answer correctly answers a question.
20
- The gold (reference) answer is provided.
21
-
22
- Score on a scale of 1-5:
23
- 5: Perfect — captures all key information from the gold answer, no errors
24
- 4: Mostly correct — minor omissions or imprecision, main point is right
25
- 3: Partially correct — captures some key points but misses important details
26
- 2: Related but mostly wrong — touches the topic but answer is largely incorrect
27
- 1: Completely wrong, contradicts the gold answer, or says "I don't know" when the answer exists
28
-
29
- Special cases:
30
- - If the gold answer indicates abstention is correct (e.g. "I don't know" or "not mentioned"),
31
- then a predicted "I don't know" scores 5.
32
- - Numeric answers within 10% of gold = full credit.
33
- - Getting the gist right but missing specifics = 3-4 depending on importance.
34
-
35
- Respond with ONLY a JSON object: {"score": <1-5>, "reasoning": "brief explanation"}"""
36
-
37
-
38
- def judge_qa(
39
- question: str,
40
- gold_answer: str,
41
- predicted_answer: str,
42
- *,
43
- condition: str = "",
44
- model: str | None = None,
45
- ) -> dict | None:
46
- """Score a QA answer. Returns {"score": 1-5, "reasoning": str} or None on failure."""
47
- user_parts = [
48
- f"## Question\n{question}",
49
- f"\n## Gold Answer\n{gold_answer}",
50
- f"\n## Predicted Answer\n{predicted_answer}",
51
- ]
52
- if condition:
53
- user_parts.append(f"\n## Context Condition: {condition}")
54
-
55
- try:
56
- kwargs: dict = {
57
- "system_prompt": SYSTEM_PROMPT,
58
- "user_prompt": "\n".join(user_parts),
59
- "max_tokens": 200,
60
- "json_mode": True,
61
- }
62
- if model:
63
- kwargs["model"] = model
64
- else:
65
- kwargs["script"] = "meeting_benchmark"
66
-
67
- raw = call_llm(**kwargs)
68
- result = extract_json(raw)
69
-
70
- score = result.get("score")
71
- reasoning = result.get("reasoning", "")
72
-
73
- if not isinstance(score, (int, float)) or not (1 <= score <= 5):
74
- print(f"[warn] qa_judge returned invalid score: {score}", file=sys.stderr)
75
- return None
76
-
77
- return {"score": int(score), "reasoning": str(reasoning)[:300]}
78
-
79
- except (ValueError, LLMError, KeyError) as e:
80
- print(f"[warn] qa_judge call failed: {e}", file=sys.stderr)
81
- return None
@@ -1,177 +0,0 @@
1
- """LongMemEval (ICLR 2025) adapter — download + parse into sinain format.
2
-
3
- Dataset: https://huggingface.co/datasets/xiaowu0162/longmemeval-cleaned
4
- Paper: https://arxiv.org/abs/2410.10813
5
-
6
- Fields per item:
7
- question_id, question_type, question, answer, question_date,
8
- haystack_session_ids, haystack_dates, haystack_sessions, answer_session_ids
9
-
10
- haystack_sessions entries: {"role": "user"/"assistant", "content": "...", "has_answer": bool}
11
- """
12
-
13
- from __future__ import annotations
14
-
15
- import hashlib
16
- import json
17
- from datetime import datetime, timedelta, timezone
18
- from pathlib import Path
19
-
20
- from .base_adapter import BenchmarkAdapter, BenchmarkInstance, BenchmarkQuestion
21
-
22
-
23
- def _download_dataset(data_dir: Path) -> Path:
24
- """Download LongMemEval from HuggingFace if not cached."""
25
- cache_path = data_dir / "longmemeval" / "longmemeval_s_cleaned.json"
26
- if cache_path.exists():
27
- return cache_path
28
-
29
- cache_path.parent.mkdir(parents=True, exist_ok=True)
30
-
31
- url = "https://huggingface.co/datasets/xiaowu0162/longmemeval-cleaned/resolve/main/longmemeval_s_cleaned.json"
32
- print(f"[longmemeval] downloading from {url} ...")
33
-
34
- # Use curl to avoid macOS Python SSL cert issues
35
- import subprocess
36
- result = subprocess.run(
37
- ["curl", "-fSL", "-o", str(cache_path), url],
38
- capture_output=True, text=True, timeout=120,
39
- )
40
- if result.returncode != 0:
41
- raise RuntimeError(f"Download failed: {result.stderr[:200]}")
42
- print(f"[longmemeval] saved to {cache_path} ({cache_path.stat().st_size} bytes)")
43
- return cache_path
44
-
45
-
46
- def _session_hash(sessions: list[dict]) -> str:
47
- """Content hash for a haystack (for grouping questions with shared context)."""
48
- raw = json.dumps(sessions, sort_keys=True, ensure_ascii=False)
49
- return hashlib.sha256(raw.encode()).hexdigest()[:16]
50
-
51
-
52
- def _sessions_to_feed_items(
53
- haystack_sessions: list[list[dict]],
54
- haystack_session_ids: list[str],
55
- haystack_dates: list[str],
56
- ) -> list[list[dict]]:
57
- """Convert LongMemEval haystack into sinain feed item sessions.
58
-
59
- haystack_sessions is a list of sessions, each a list of turn dicts:
60
- sessions[i][j] = {"role": "user"/"assistant", "content": "..."}
61
-
62
- Each session becomes a list of feed items with synthesized timestamps.
63
- User turns → source: "audio", assistant turns → source: "agent".
64
- """
65
- result: list[list[dict]] = []
66
-
67
- for i, session_turns in enumerate(haystack_sessions):
68
- if not session_turns:
69
- continue
70
-
71
- base_ts = haystack_dates[i] if i < len(haystack_dates) else "2025-01-01T10:00:00Z"
72
- base_dt = _parse_date(base_ts)
73
-
74
- items = []
75
- for j, turn in enumerate(session_turns):
76
- ts = (base_dt + timedelta(seconds=30 * j)).isoformat()
77
- source = "audio" if turn.get("role") == "user" else "agent"
78
- items.append({
79
- "source": source,
80
- "text": turn.get("content", ""),
81
- "ts": ts,
82
- "channel": "benchmark",
83
- })
84
- if items:
85
- result.append(items)
86
-
87
- return result
88
-
89
-
90
- def _parse_date(s: str) -> datetime:
91
- """Best-effort date parsing."""
92
- for fmt in ("%Y-%m-%dT%H:%M:%SZ", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d", "%m/%d/%Y"):
93
- try:
94
- return datetime.strptime(s, fmt).replace(tzinfo=timezone.utc)
95
- except (ValueError, TypeError):
96
- continue
97
- return datetime(2025, 1, 1, 10, 0, tzinfo=timezone.utc)
98
-
99
-
100
- class LongMemEvalAdapter(BenchmarkAdapter):
101
- """Adapter for LongMemEval (ICLR 2025) benchmark."""
102
-
103
- @property
104
- def name(self) -> str:
105
- return "longmemeval"
106
-
107
- def load_dataset(self, data_dir: str) -> list[BenchmarkInstance]:
108
- """Download and parse LongMemEval, grouping questions by shared haystack."""
109
- path = _download_dataset(Path(data_dir))
110
- with open(path) as f:
111
- raw_items = json.load(f)
112
-
113
- # Group questions by haystack content hash
114
- groups: dict[str, dict] = {}
115
- for item in raw_items:
116
- h = _session_hash(item.get("haystack_sessions", []))
117
- if h not in groups:
118
- groups[h] = {
119
- "haystack_sessions": item["haystack_sessions"],
120
- "haystack_session_ids": item.get("haystack_session_ids", []),
121
- "haystack_dates": item.get("haystack_dates", []),
122
- "questions": [],
123
- }
124
- groups[h]["questions"].append(item)
125
-
126
- instances = []
127
- for h, group in groups.items():
128
- feed_sessions = _sessions_to_feed_items(
129
- group["haystack_sessions"],
130
- group["haystack_session_ids"],
131
- group["haystack_dates"],
132
- )
133
-
134
- questions = []
135
- for item in group["questions"]:
136
- questions.append(BenchmarkQuestion(
137
- id=item["question_id"],
138
- text=item["question"],
139
- gold_answer=str(item["answer"]),
140
- category=item.get("question_type", "unknown"),
141
- evidence_session_ids=item.get("answer_session_ids", []),
142
- metadata={
143
- "question_date": item.get("question_date", ""),
144
- },
145
- ))
146
-
147
- instances.append(BenchmarkInstance(
148
- id=f"lme-{h}",
149
- sessions=feed_sessions,
150
- questions=questions,
151
- raw_sessions=group["haystack_sessions"],
152
- metadata={
153
- "haystack_hash": h,
154
- "num_sessions": len(feed_sessions),
155
- "num_turns": len(group["haystack_sessions"]),
156
- },
157
- ))
158
-
159
- print(f"[longmemeval] loaded {sum(len(i.questions) for i in instances)} questions "
160
- f"across {len(instances)} unique haystacks")
161
- return instances
162
-
163
- def format_full_context(self, instance: BenchmarkInstance) -> str:
164
- """Render the full conversation history for the baseline condition."""
165
- lines = []
166
- for session in instance.raw_sessions:
167
- if isinstance(session, list):
168
- for turn in session:
169
- role = turn.get("role", "unknown").capitalize()
170
- content = turn.get("content", "")
171
- lines.append(f"{role}: {content}")
172
- lines.append("---") # session separator
173
- elif isinstance(session, dict):
174
- role = session.get("role", "unknown").capitalize()
175
- content = session.get("content", "")
176
- lines.append(f"{role}: {content}")
177
- return "\n\n".join(lines)
@@ -1,81 +0,0 @@
1
- """Meeting Memory adapter — loads QA pairs + ground-truth transcript.
2
-
3
- Unlike LongMemEval, this adapter does NOT ingest. The real sinain pipeline
4
- (start-local.sh) produces the knowledge-graph.db during a live capture run.
5
- This adapter only loads QA pairs and provides the full-context baseline.
6
-
7
- Data layout:
8
- eval/benchmarks/data/meeting/
9
- <name>.txt — ground-truth transcript
10
- <name>_qa.json — gold QA pairs
11
- """
12
-
13
- from __future__ import annotations
14
-
15
- import json
16
- from pathlib import Path
17
-
18
- from .base_adapter import BenchmarkAdapter, BenchmarkInstance, BenchmarkQuestion
19
-
20
-
21
- class MeetingMemoryAdapter(BenchmarkAdapter):
22
- """Adapter for meeting memory benchmarks (real pipeline capture)."""
23
-
24
- @property
25
- def name(self) -> str:
26
- return "meeting"
27
-
28
- def load_dataset(self, data_dir: str) -> list[BenchmarkInstance]:
29
- """Load meeting QA pairs and transcripts from data/meeting/."""
30
- meeting_dir = Path(data_dir) / "meeting"
31
- if not meeting_dir.exists():
32
- raise FileNotFoundError(f"Meeting data not found: {meeting_dir}")
33
-
34
- instances = []
35
- for qa_path in sorted(meeting_dir.glob("*_qa.json")):
36
- # Derive transcript path: foo_qa.json → foo.txt
37
- stem = qa_path.stem.replace("_qa", "")
38
- transcript_path = qa_path.parent / f"{stem}.txt"
39
- if not transcript_path.exists():
40
- print(f"[meeting] warning: no transcript for {qa_path.name}, skipping")
41
- continue
42
-
43
- # Load QA pairs
44
- with open(qa_path) as f:
45
- raw_questions = json.load(f)
46
-
47
- questions = []
48
- for item in raw_questions:
49
- questions.append(BenchmarkQuestion(
50
- id=item["id"],
51
- text=item["question"],
52
- gold_answer=item["gold_answer"],
53
- category=item.get("category", "unknown"),
54
- evidence_session_ids=item.get("evidence_timestamps", []),
55
- metadata={
56
- "evidence_timestamps": item.get("evidence_timestamps", []),
57
- },
58
- ))
59
-
60
- # Load transcript
61
- transcript_text = transcript_path.read_text(encoding="utf-8")
62
-
63
- instances.append(BenchmarkInstance(
64
- id=f"meeting-{stem}",
65
- sessions=[], # Not used — real pipeline does ingestion
66
- questions=questions,
67
- raw_sessions=[],
68
- metadata={
69
- "transcript_path": str(transcript_path),
70
- "raw_transcript": transcript_text,
71
- "stem": stem,
72
- },
73
- ))
74
-
75
- total_q = sum(len(i.questions) for i in instances)
76
- print(f"[meeting] loaded {total_q} questions across {len(instances)} meetings")
77
- return instances
78
-
79
- def format_full_context(self, instance: BenchmarkInstance) -> str:
80
- """Return the raw transcript verbatim for the full-context baseline."""
81
- return instance.metadata.get("raw_transcript", "")
@@ -1,230 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Meeting Memory Benchmark runner — evaluates a captured knowledge-graph.db.
3
-
4
- Standalone script: does NOT modify or import runner.py.
5
- Reuses shared infrastructure: query, judge, evaluate, report.
6
-
7
- Usage:
8
- python3 eval/benchmarks/meeting_runner.py \
9
- --db /tmp/sinain-bench-XXXX/knowledge-graph.db \
10
- --conditions sinain-memory,full-context
11
-
12
- # Quick test (3 questions):
13
- python3 eval/benchmarks/meeting_runner.py \
14
- --db /tmp/sinain-bench-XXXX/knowledge-graph.db \
15
- --subset 3
16
- """
17
-
18
- from __future__ import annotations
19
-
20
- import argparse
21
- import json
22
- import sys
23
- from pathlib import Path
24
-
25
- # Add sinain-memory to path
26
- _koog_dir = str(Path(__file__).resolve().parent.parent.parent)
27
- if _koog_dir not in sys.path:
28
- sys.path.insert(0, _koog_dir)
29
-
30
- from eval.benchmarks.config import DATA_DIR, RESULTS_DIR, QA_MODEL, JUDGE_MODEL
31
- from eval.benchmarks.meeting_adapter import MeetingMemoryAdapter
32
- from eval.benchmarks.query import answer_question, _get_retrieved_facts, compute_content_recall
33
- from eval.benchmarks.evaluate import token_f1, aggregate_results
34
- from eval.benchmarks.judges.qa_judge import judge_qa
35
- from eval.benchmarks.report import generate_markdown, generate_json
36
-
37
-
38
- def run_meeting_benchmark(
39
- db_path: str,
40
- conditions: list[str],
41
- *,
42
- subset: int | None = None,
43
- meeting_filter: str | None = None,
44
- qa_model: str = QA_MODEL,
45
- judge_model: str = JUDGE_MODEL,
46
- output_dir: Path = RESULTS_DIR,
47
- data_dir: Path = DATA_DIR,
48
- resume: bool = False,
49
- ) -> tuple[dict, list[dict]]:
50
- """Run meeting benchmark against a captured knowledge-graph.db."""
51
-
52
- adapter = MeetingMemoryAdapter()
53
- instances = adapter.load_dataset(str(data_dir))
54
-
55
- if meeting_filter:
56
- instances = [i for i in instances if meeting_filter in i.id]
57
- print(f"[meeting] filtered to {len(instances)} meeting(s) matching '{meeting_filter}'")
58
-
59
- if not instances:
60
- print("[meeting] no meeting data found — check data/meeting/ directory")
61
- return {"error": "no data"}, []
62
-
63
- # Validate DB exists for sinain-memory condition
64
- if "sinain-memory" in conditions:
65
- if not db_path or not Path(db_path).exists():
66
- print(f"[meeting] ERROR: --db path does not exist: {db_path}")
67
- print(" Run the capture pipeline first (see plan Part 1)")
68
- sys.exit(1)
69
-
70
- print(f"\n{'='*60}")
71
- print(f" Meeting Memory Benchmark")
72
- print(f" Conditions: {', '.join(conditions)}")
73
- print(f" DB: {db_path or '(none)'}")
74
- print(f" QA model: {qa_model}")
75
- print(f" Judge model: {judge_model}")
76
- print(f"{'='*60}\n")
77
-
78
- # Flatten questions
79
- all_questions = []
80
- for inst in instances:
81
- for q in inst.questions:
82
- all_questions.append((inst, q))
83
-
84
- if subset:
85
- all_questions = all_questions[:subset]
86
-
87
- total = len(all_questions)
88
- print(f"[meeting] evaluating {total} questions\n")
89
-
90
- # Resume support
91
- resume_path = output_dir / "meeting_progress.jsonl"
92
- completed: dict[str, dict] = {}
93
- if resume and resume_path.exists():
94
- for line in resume_path.read_text().strip().split("\n"):
95
- if line:
96
- entry = json.loads(line)
97
- completed[entry["id"]] = entry
98
-
99
- output_dir.mkdir(parents=True, exist_ok=True)
100
- details: list[dict] = []
101
-
102
- for idx, (inst, question) in enumerate(all_questions):
103
- qid = question.id
104
-
105
- if qid in completed:
106
- details.append(completed[qid])
107
- continue
108
-
109
- print(f"[{idx+1}/{total}] {qid} [{question.category}]")
110
- print(f" Q: {question.text[:80]}...")
111
-
112
- full_context = adapter.format_full_context(inst)
113
-
114
- # Retrieval metrics
115
- retrieval = {}
116
- if db_path and "sinain-memory" in conditions:
117
- retrieved_facts = _get_retrieved_facts(db_path, question.text)
118
- retrieval = compute_content_recall(retrieved_facts, question.gold_answer)
119
-
120
- # Generate answers per condition
121
- answers = {}
122
- for cond in conditions:
123
- if cond == "sinain-memory" and not db_path:
124
- answers[cond] = {"text": "(no DB)", "score": 1, "f1": 0.0}
125
- continue
126
-
127
- print(f" [{cond}] generating answer...")
128
- answer_text = answer_question(
129
- question, cond,
130
- db_path=db_path,
131
- full_context=full_context,
132
- model=qa_model,
133
- )
134
-
135
- f1 = token_f1(answer_text, question.gold_answer)
136
-
137
- judge_result = judge_qa(
138
- question.text, question.gold_answer, answer_text,
139
- condition=cond, model=judge_model,
140
- )
141
- score = judge_result["score"] if judge_result else None
142
- reasoning = judge_result["reasoning"] if judge_result else None
143
-
144
- answers[cond] = {
145
- "text": answer_text[:500],
146
- "score": score,
147
- "f1": round(f1, 4),
148
- "reasoning": reasoning,
149
- }
150
- print(f" score={score}/5 f1={f1:.2f}")
151
-
152
- entry = {
153
- "id": qid,
154
- "question": question.text,
155
- "gold_answer": question.gold_answer,
156
- "category": question.category,
157
- "retrieval": retrieval,
158
- "answers": answers,
159
- }
160
- details.append(entry)
161
-
162
- # Save progress
163
- with open(resume_path, "a") as f:
164
- f.write(json.dumps(entry, ensure_ascii=False) + "\n")
165
-
166
- summary = aggregate_results(details)
167
- return summary, details
168
-
169
-
170
- def main() -> None:
171
- parser = argparse.ArgumentParser(description="Meeting Memory Benchmark")
172
- parser.add_argument("--db", required=False, default=None,
173
- help="Path to knowledge-graph.db from capture run")
174
- parser.add_argument("--conditions", default="sinain-memory,full-context",
175
- help="Comma-separated conditions (sinain-memory, full-context)")
176
- parser.add_argument("--subset", type=int, default=None,
177
- help="Run only first N questions")
178
- parser.add_argument("--meeting", default=None,
179
- help="Filter to specific meeting by stem (e.g. al-futaim-prep-5min)")
180
- parser.add_argument("--qa-model", default=QA_MODEL)
181
- parser.add_argument("--judge-model", default=JUDGE_MODEL)
182
- parser.add_argument("--output-dir", type=Path, default=RESULTS_DIR)
183
- parser.add_argument("--data-dir", type=Path, default=DATA_DIR)
184
- parser.add_argument("--format", default="json,markdown")
185
- parser.add_argument("--resume", action="store_true")
186
- args = parser.parse_args()
187
-
188
- conditions = [c.strip() for c in args.conditions.split(",")]
189
- formats = [f.strip() for f in args.format.split(",")]
190
-
191
- summary, details = run_meeting_benchmark(
192
- args.db, conditions,
193
- subset=args.subset,
194
- meeting_filter=args.meeting,
195
- qa_model=args.qa_model,
196
- judge_model=args.judge_model,
197
- output_dir=args.output_dir,
198
- data_dir=args.data_dir,
199
- resume=args.resume,
200
- )
201
-
202
- # Write outputs
203
- args.output_dir.mkdir(parents=True, exist_ok=True)
204
-
205
- if "json" in formats:
206
- json_path = args.output_dir / "meeting_results.json"
207
- json_path.write_text(generate_json("meeting", summary, details))
208
- print(f"\n[output] JSON: {json_path}")
209
-
210
- if "markdown" in formats:
211
- md_path = args.output_dir / "meeting_results.md"
212
- md_path.write_text(generate_markdown("meeting", summary, details))
213
- print(f"[output] Markdown: {md_path}")
214
-
215
- # Print summary
216
- print(f"\n{'='*60}")
217
- print(f" Meeting Memory Benchmark — Summary")
218
- print(f"{'='*60}")
219
- ipr = summary.get("ipr")
220
- if ipr:
221
- print(f" IPR: {ipr:.1%}")
222
- for cond, data in summary.get("conditions", {}).items():
223
- print(f" {cond}: {data['mean_score']:.2f}/5 (n={data['n']})")
224
- for k, v in summary.get("retrieval", {}).items():
225
- print(f" {k}: {v:.1%}")
226
- print()
227
-
228
-
229
- if __name__ == "__main__":
230
- main()