starfriend commited on
Commit
716b5a1
·
verified ·
1 Parent(s): b035174

Upload summaries.py

Browse files
Files changed (1) hide show
  1. metadata_search/summaries.py +172 -0
metadata_search/summaries.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import re
6
+ from pathlib import Path
7
+
8
+ from huggingface_hub import hf_hub_download
9
+ from local_rag.config import LocalRagConfig
10
+
11
+ TITLE_PATTERN = re.compile(r"\*\*Title:\*\*\s*(.+)", re.MULTILINE)
12
+ _FILENAME_TITLE = re.compile(r"^.+?\s-\s\d{4}\s-\s(.+)$")
13
+ _FILENAME_YEAR = re.compile(r"^.+?\s-\s(\d{4})\s-\s")
14
+
15
+ HF_DATASET_REPO = os.getenv("WATERSCOPE_DATASET_REPO", "EnvironmentalAI/WaterScopeAI-Data")
16
+ HF_SUMMARY_PREFIX = os.getenv("WATERSCOPE_SUMMARY_PREFIX", "Summary")
17
+ HF_SUMMARY_CACHE = Path(
18
+ os.getenv("WATERSCOPE_SUMMARY_CACHE", "/tmp/waterscope-summary-cache")
19
+ ).expanduser().resolve()
20
+ HF_SUMMARY_ON_DEMAND = os.getenv("WATERSCOPE_SUMMARY_ON_DEMAND", "1").lower() in {
21
+ "1", "true", "yes"
22
+ }
23
+
24
+
25
+ def _year_from_filename(file_name: str) -> str | None:
26
+ match = _FILENAME_YEAR.match(Path(file_name).name)
27
+ return match.group(1) if match else None
28
+
29
+
30
+ def _download_cloud_summary(file_name: str) -> Path | None:
31
+ """Download one summary from the HF Dataset and cache it locally."""
32
+ if not HF_SUMMARY_ON_DEMAND:
33
+ return None
34
+
35
+ year = _year_from_filename(file_name)
36
+ if not year:
37
+ return None
38
+
39
+ repo_file = f"{HF_SUMMARY_PREFIX}/{year}/{Path(file_name).name}"
40
+ try:
41
+ downloaded = hf_hub_download(
42
+ repo_id=HF_DATASET_REPO,
43
+ repo_type="dataset",
44
+ filename=repo_file,
45
+ local_dir=str(HF_SUMMARY_CACHE),
46
+ )
47
+ except Exception:
48
+ return None
49
+
50
+ path = Path(downloaded)
51
+ return path if path.is_file() else None
52
+
53
+
54
+ def summary_search_dirs() -> list[Path]:
55
+ """Directories to search for summary .txt files (first match wins)."""
56
+ dirs: list[Path] = []
57
+ for raw in (os.getenv("PAPER_SUMMARIES_DIRS") or "").split(":"):
58
+ raw = raw.strip()
59
+ if raw:
60
+ dirs.append(Path(raw).expanduser().resolve())
61
+
62
+ override = os.getenv("PAPER_SUMMARIES_DIR")
63
+ if override:
64
+ dirs.append(Path(override).expanduser().resolve())
65
+
66
+ project_root = Path(__file__).resolve().parents[1]
67
+ dirs.append(project_root / "paper_summaries")
68
+
69
+ unique: list[Path] = []
70
+ seen: set[str] = set()
71
+ for path in dirs:
72
+ key = str(path)
73
+ if key in seen:
74
+ continue
75
+ seen.add(key)
76
+ unique.append(path)
77
+ return unique
78
+
79
+
80
+ def summaries_dir() -> Path:
81
+ for path in summary_search_dirs():
82
+ if path.exists():
83
+ return path
84
+ return Path(__file__).resolve().parents[1] / "paper_summaries"
85
+
86
+
87
+ def _manifest_path(config: LocalRagConfig | None = None) -> Path:
88
+ cfg = config or LocalRagConfig.from_env()
89
+ return cfg.manifest_path
90
+
91
+
92
+ def load_manifest(config: LocalRagConfig | None = None) -> dict:
93
+ path = _manifest_path(config)
94
+ if not path.exists():
95
+ raise FileNotFoundError(f"Manifest not found: {path}. Rebuild local_rag_data/manifest.json first.")
96
+ return json.loads(path.read_text(encoding="utf-8"))
97
+
98
+
99
+ def title_from_filename(file_name: str) -> str:
100
+ stem = Path(file_name).stem
101
+ match = _FILENAME_TITLE.match(stem)
102
+ if match:
103
+ return match.group(1).strip()
104
+ return stem
105
+
106
+
107
+ def extract_title(file_name: str, summary: str | None = None) -> str:
108
+ if summary:
109
+ match = TITLE_PATTERN.search(summary)
110
+ if match:
111
+ return match.group(1).strip()
112
+ return title_from_filename(file_name)
113
+
114
+
115
+ def summary_path(file_name: str) -> Path | None:
116
+ # Preserve existing local behavior first.
117
+ for root in summary_search_dirs():
118
+ candidate = root / file_name
119
+ if candidate.exists():
120
+ return candidate
121
+
122
+ # Cloud production fallback: fetch only the requested summary.
123
+ return _download_cloud_summary(file_name)
124
+
125
+
126
+ def load_summary(file_name: str) -> str | None:
127
+ path = summary_path(file_name)
128
+ if path is None:
129
+ return None
130
+ return path.read_text(encoding="utf-8")
131
+
132
+
133
+ def list_papers(config: LocalRagConfig | None = None) -> list[dict]:
134
+ """List indexed papers without reading every summary file into memory."""
135
+ manifest = load_manifest(config)
136
+ papers: list[dict] = []
137
+ for entry in manifest.get("files", []):
138
+ file_name = entry["file_name"]
139
+ papers.append(
140
+ {
141
+ "document_id": entry["document_id"],
142
+ "file_name": file_name,
143
+ "title": title_from_filename(file_name),
144
+ "chunk_count": entry.get("chunk_count", 0),
145
+ "has_summary": summary_path(file_name) is not None,
146
+ }
147
+ )
148
+ papers.sort(key=lambda item: item["title"].lower())
149
+ return papers
150
+
151
+
152
+ def summaries_for_documents(document_ids: list[str], config: LocalRagConfig | None = None) -> list[dict]:
153
+ manifest = load_manifest(config)
154
+ by_id = {entry["document_id"]: entry for entry in manifest.get("files", [])}
155
+ selected: list[dict] = []
156
+ for document_id in document_ids:
157
+ entry = by_id.get(document_id)
158
+ if not entry:
159
+ continue
160
+ file_name = entry["file_name"]
161
+ summary = load_summary(file_name)
162
+ if not summary:
163
+ continue
164
+ selected.append(
165
+ {
166
+ "document_id": document_id,
167
+ "title": extract_title(file_name, summary),
168
+ "file_name": file_name,
169
+ "text": summary,
170
+ }
171
+ )
172
+ return selected