File size: 9,042 Bytes
f67cae0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
#!/usr/bin/env python3
"""Build per-question E2E v3 structure shards for a static viewer."""

from __future__ import annotations

import argparse
import base64
import gzip
import json
import shutil
from collections import defaultdict
from pathlib import Path
from typing import Any, Iterator


ROOT = Path(__file__).resolve().parent.parent
DEFAULT_SHAPES = [
    "tabular_records",
    "chronology_and_timeline_indexes",
    "claim_and_theme_summaries",
    "qa_shortcuts_and_templates",
    "relation_graphs_and_mappings",
]


def iter_jsonl(path: Path) -> Iterator[dict[str, Any]]:
    with path.open(encoding="utf-8") as handle:
        for line_number, line in enumerate(handle, 1):
            if not line.strip():
                continue
            value = json.loads(line)
            if not isinstance(value, dict):
                raise ValueError(f"{path}:{line_number}: expected an object")
            yield value


def load_shape_index(
    scaffolds_dir: Path, shape: str
) -> tuple[str, dict[str, list[dict[str, str]]]]:
    index_path = scaffolds_dir / shape / "_index.json"
    if not index_path.exists():
        raise FileNotFoundError(f"missing shape index: {index_path}")
    payload = json.loads(index_path.read_text(encoding="utf-8"))
    files: dict[str, list[dict[str, str]]] = defaultdict(list)
    for entry in payload.get("entries", []):
        if entry.get("doc_id") is None or not entry.get("file"):
            continue
        files[str(entry["doc_id"])].append(
            {
                "file": str(entry["file"]),
                "unit_name": str(entry.get("unit_name") or ""),
                "unit_description": str(entry.get("unit_description") or ""),
            }
        )
    return str(payload.get("description") or ""), dict(files)


def load_bundle_map(path: Path | None) -> dict[str, str]:
    if path is None:
        return {}
    mapping: dict[str, str] = {}
    for row in iter_jsonl(path):
        physical_id = str(row["physical_id"])
        for source_id in row.get("source_doc_ids", []):
            source_id = str(source_id)
            if source_id in mapping:
                raise ValueError(f"duplicate bundle mapping for {source_id}")
            mapping[source_id] = physical_id
    return mapping


def structure_format(filename: str) -> str:
    suffixes = Path(filename).suffixes
    if filename.endswith(".timeline.json"):
        return "json"
    if filename.endswith(".edges.jsonl"):
        return "jsonl"
    return suffixes[-1].lstrip(".").lower() if suffixes else "txt"


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--dataset", required=True)
    parser.add_argument("--unified", type=Path, required=True)
    parser.add_argument("--scaffolds-dir", type=Path, required=True)
    parser.add_argument("--bundle-manifest", type=Path)
    parser.add_argument("--out", type=Path, default=ROOT / "e2e_structures_v3")
    parser.add_argument("--label", default="E2E Structures v3")
    parser.add_argument("--shapes", default=",".join(DEFAULT_SHAPES))
    return parser.parse_args()


def main() -> None:
    args = parse_args()
    shapes = [value.strip() for value in args.shapes.split(",") if value.strip()]
    if not shapes:
        raise ValueError("--shapes must contain at least one shape")

    shape_descriptions: dict[str, str] = {}
    shape_files: dict[str, dict[str, list[dict[str, str]]]] = {}
    for shape in shapes:
        description, files = load_shape_index(args.scaffolds_dir, shape)
        shape_descriptions[shape] = description
        shape_files[shape] = files
        print(f"{shape:38s} {len(files):>6} extraction docs")

    bundle_map = load_bundle_map(args.bundle_manifest)
    records_dir = args.out / "records"
    if records_dir.exists():
        shutil.rmtree(records_dir)
    records_dir.mkdir(parents=True, exist_ok=True)

    index_rows: list[dict[str, Any]] = []
    extraction_docs_seen: set[str] = set()
    source_docs_seen: set[str] = set()
    source_docs_unmapped: set[str] = set()
    extraction_docs_without_structures: set[str] = set()
    total_structures = 0

    for row in iter_jsonl(args.unified):
        grouped: dict[str, list[dict[str, Any]]] = {}
        group_order: list[str] = []
        for doc in row.get("docs", []) or []:
            source_id = str(doc.get("id"))
            source_docs_seen.add(source_id)
            extraction_id = bundle_map.get(source_id, source_id)
            if bundle_map and source_id not in bundle_map:
                source_docs_unmapped.add(source_id)
            if extraction_id not in grouped:
                grouped[extraction_id] = []
                group_order.append(extraction_id)
            grouped[extraction_id].append(
                {"doc_id": source_id, "contents": str(doc.get("contents") or "")}
            )

        per_doc: list[dict[str, Any]] = []
        question_structures = 0
        for extraction_id in group_order:
            extraction_docs_seen.add(extraction_id)
            structures: list[dict[str, Any]] = []
            for shape in shapes:
                for entry in shape_files[shape].get(extraction_id, []):
                    filename = entry["file"]
                    path = args.scaffolds_dir / shape / filename
                    structures.append(
                        {
                            "shape_id": shape,
                            "description": shape_descriptions[shape],
                            "file": filename,
                            "unit_name": entry["unit_name"],
                            "unit_description": entry["unit_description"],
                            "format": structure_format(filename),
                            "content": path.read_text(encoding="utf-8")
                            if path.exists()
                            else "",
                        }
                    )
            if not structures:
                extraction_docs_without_structures.add(extraction_id)
            question_structures += len(structures)
            source_docs = grouped[extraction_id]
            per_doc.append(
                {
                    "doc_id": extraction_id,
                    "source_doc_ids": [doc["doc_id"] for doc in source_docs],
                    "source_docs": source_docs,
                    "is_supporting": True,
                    "n_structures": len(structures),
                    "contents": "\n\n".join(
                        f"===== {doc['doc_id']} =====\n{doc['contents']}"
                        for doc in source_docs
                    ),
                    "structures": structures,
                }
            )

        qid = str(row["qid"])
        record = {
            "qid": qid,
            "dataset": args.dataset,
            "question": row.get("question"),
            "gold_answers": row.get("answers", []),
            "n_docs": len(per_doc),
            "n_source_docs": sum(len(doc["source_doc_ids"]) for doc in per_doc),
            "n_structures": question_structures,
            "docs": per_doc,
        }
        raw = json.dumps(record, ensure_ascii=False, separators=(",", ":")).encode()
        encoded = base64.b64encode(gzip.compress(raw, compresslevel=9)).decode("ascii")
        relative_path = f"records/{qid}.json.gzip.b64"
        (args.out / relative_path).write_text(encoded, encoding="ascii")
        index_rows.append(
            {
                "qid": qid,
                "question": row.get("question"),
                "n_docs": len(per_doc),
                "n_source_docs": record["n_source_docs"],
                "n_structures": question_structures,
                "doc_ids": [doc["doc_id"] for doc in per_doc],
                "source_doc_ids": [
                    source_id
                    for doc in per_doc
                    for source_id in doc["source_doc_ids"]
                ],
                "path": relative_path,
            }
        )
        total_structures += question_structures

    meta = {
        "label": args.label,
        "dataset": args.dataset,
        "n_qids": len(index_rows),
        "n_extraction_docs": len(extraction_docs_seen),
        "n_source_docs": len(source_docs_seen),
        "n_source_docs_unmapped": len(source_docs_unmapped),
        "n_extraction_docs_without_structures": len(
            extraction_docs_without_structures
        ),
        "n_structures_total": total_structures,
        "shapes": shapes,
        "shape_descriptions": shape_descriptions,
        "scaffolds_dir": str(args.scaffolds_dir),
        "unified": str(args.unified),
        "bundle_manifest": str(args.bundle_manifest) if args.bundle_manifest else None,
    }
    args.out.mkdir(parents=True, exist_ok=True)
    (args.out / "index.json").write_text(
        json.dumps({"meta": meta, "rows": index_rows}, ensure_ascii=False),
        encoding="utf-8",
    )
    print(json.dumps(meta, indent=2))


if __name__ == "__main__":
    main()