hermes-benchmark 0.0.5__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.
- hermes_benchmark/__init__.py +0 -0
- hermes_benchmark/capacity/__init__.py +16 -0
- hermes_benchmark/capacity/estimates.py +309 -0
- hermes_benchmark/capacity/models.py +142 -0
- hermes_benchmark/capacity/pilot.py +133 -0
- hermes_benchmark/capacity/profiler.py +387 -0
- hermes_benchmark/cli.py +519 -0
- hermes_benchmark/config.py +209 -0
- hermes_benchmark/enrichment/__init__.py +21 -0
- hermes_benchmark/enrichment/builder.py +556 -0
- hermes_benchmark/enrichment/clustering.py +56 -0
- hermes_benchmark/enrichment/models.py +18 -0
- hermes_benchmark/enrichment/providers.py +96 -0
- hermes_benchmark/io.py +38 -0
- hermes_benchmark/language_templates.py +81 -0
- hermes_benchmark/ner_ontology.py +124 -0
- hermes_benchmark/pipeline.py +1142 -0
- hermes_benchmark/progress.py +46 -0
- hermes_benchmark/tasks/__init__.py +70 -0
- hermes_benchmark/tasks/classification.py +1381 -0
- hermes_benchmark/tasks/classification_provider.py +261 -0
- hermes_benchmark/tasks/extractive_qa.py +1318 -0
- hermes_benchmark/tasks/google_client.py +26 -0
- hermes_benchmark/tasks/knowledge_base.py +429 -0
- hermes_benchmark/tasks/models.py +110 -0
- hermes_benchmark/tasks/ner.py +808 -0
- hermes_benchmark/tasks/output_names.py +118 -0
- hermes_benchmark/tasks/paragraphs.py +926 -0
- hermes_benchmark/tasks/qa_provider.py +290 -0
- hermes_benchmark/tasks/reranking.py +745 -0
- hermes_benchmark/templates/ar.yaml +116 -0
- hermes_benchmark/templates/az.yaml +116 -0
- hermes_benchmark/templates/de.yaml +117 -0
- hermes_benchmark/templates/en.yaml +116 -0
- hermes_benchmark/templates/es.yaml +116 -0
- hermes_benchmark/templates/fr.yaml +116 -0
- hermes_benchmark/templates/pt.yaml +116 -0
- hermes_benchmark/templates/ru.yaml +116 -0
- hermes_benchmark/templates/tr.yaml +116 -0
- hermes_benchmark/templates/zh.yaml +109 -0
- hermes_benchmark/utils/__init__.py +1 -0
- hermes_benchmark/utils/hashing.py +18 -0
- hermes_benchmark/wikipedia/__init__.py +15 -0
- hermes_benchmark/wikipedia/corpus.py +371 -0
- hermes_benchmark/wikipedia/dump.py +159 -0
- hermes_benchmark/wikipedia/models.py +49 -0
- hermes_benchmark/wikipedia/text.py +235 -0
- hermes_benchmark-0.0.5.dist-info/METADATA +137 -0
- hermes_benchmark-0.0.5.dist-info/RECORD +52 -0
- hermes_benchmark-0.0.5.dist-info/WHEEL +4 -0
- hermes_benchmark-0.0.5.dist-info/entry_points.txt +2 -0
- hermes_benchmark-0.0.5.dist-info/licenses/LICENSE +19 -0
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Wikipedia dump profiling and benchmark capacity estimation."""
|
|
2
|
+
|
|
3
|
+
from .estimates import estimate_task_capacities
|
|
4
|
+
from .models import CapacityReport, CapacityRange, CorpusProfile, TaskCapacity
|
|
5
|
+
from .pilot import run_pilot
|
|
6
|
+
from .profiler import profile_dump
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"CapacityRange",
|
|
10
|
+
"CapacityReport",
|
|
11
|
+
"CorpusProfile",
|
|
12
|
+
"TaskCapacity",
|
|
13
|
+
"estimate_task_capacities",
|
|
14
|
+
"profile_dump",
|
|
15
|
+
"run_pilot",
|
|
16
|
+
]
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""Task-capacity formulas compatible with the legacy 10K pipeline."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Iterable, List, Mapping, Optional, Tuple
|
|
4
|
+
|
|
5
|
+
from hermes_benchmark.config import ServicesConfig, TaskConfig
|
|
6
|
+
|
|
7
|
+
from .models import (
|
|
8
|
+
CapacityRange,
|
|
9
|
+
CapacityReport,
|
|
10
|
+
CorpusProfile,
|
|
11
|
+
PoolConstraint,
|
|
12
|
+
TaskCapacity,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_STATUS_PRIORITY = {
|
|
17
|
+
"disabled": 0,
|
|
18
|
+
"sufficient": 1,
|
|
19
|
+
"uncertain": 2,
|
|
20
|
+
"insufficient": 3,
|
|
21
|
+
"blocked": 4,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _task_status(values: Iterable[CapacityRange]) -> str:
|
|
26
|
+
statuses = [value.status for value in values]
|
|
27
|
+
return max(statuses, key=lambda value: _STATUS_PRIORITY[value])
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _missing_services(
|
|
31
|
+
services: ServicesConfig,
|
|
32
|
+
required: Iterable[str],
|
|
33
|
+
) -> Tuple[str, ...]:
|
|
34
|
+
missing = []
|
|
35
|
+
for name in required:
|
|
36
|
+
if getattr(services, name) is None:
|
|
37
|
+
missing.append(name)
|
|
38
|
+
return tuple(missing)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _option(task: TaskConfig, name: str, default: Optional[int]) -> Optional[int]:
|
|
42
|
+
if name not in task.options:
|
|
43
|
+
return default
|
|
44
|
+
value = task.options[name]
|
|
45
|
+
if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
|
|
46
|
+
raise ValueError(f"Task option {name!r} must be a positive integer")
|
|
47
|
+
return value
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _structural_range(
|
|
51
|
+
profile: CorpusProfile,
|
|
52
|
+
requested: int,
|
|
53
|
+
observed: int,
|
|
54
|
+
estimated: Optional[int],
|
|
55
|
+
upper: Optional[int],
|
|
56
|
+
basis: str,
|
|
57
|
+
missing: Tuple[str, ...] = (),
|
|
58
|
+
) -> CapacityRange:
|
|
59
|
+
if missing:
|
|
60
|
+
status = "blocked"
|
|
61
|
+
elif observed >= requested:
|
|
62
|
+
status = "sufficient"
|
|
63
|
+
elif profile.complete:
|
|
64
|
+
status = "insufficient"
|
|
65
|
+
else:
|
|
66
|
+
status = "uncertain"
|
|
67
|
+
return CapacityRange(
|
|
68
|
+
requested=requested,
|
|
69
|
+
observed_candidates=observed,
|
|
70
|
+
lower_bound=observed,
|
|
71
|
+
estimated=estimated,
|
|
72
|
+
upper_bound=observed if profile.complete else upper,
|
|
73
|
+
confidence="exact" if profile.complete else "sample_projection",
|
|
74
|
+
status=status,
|
|
75
|
+
basis=basis,
|
|
76
|
+
dependencies=missing,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _proxy_range(
|
|
81
|
+
profile: CorpusProfile,
|
|
82
|
+
requested: int,
|
|
83
|
+
observed: int,
|
|
84
|
+
projected_upper: Optional[int],
|
|
85
|
+
basis: str,
|
|
86
|
+
missing: Tuple[str, ...],
|
|
87
|
+
) -> CapacityRange:
|
|
88
|
+
upper = observed if profile.complete else projected_upper
|
|
89
|
+
if missing:
|
|
90
|
+
status = "blocked"
|
|
91
|
+
elif upper is not None and upper < requested and profile.complete:
|
|
92
|
+
status = "insufficient"
|
|
93
|
+
else:
|
|
94
|
+
status = "uncertain"
|
|
95
|
+
return CapacityRange(
|
|
96
|
+
requested=requested,
|
|
97
|
+
observed_candidates=observed,
|
|
98
|
+
lower_bound=0,
|
|
99
|
+
estimated=None,
|
|
100
|
+
upper_bound=upper,
|
|
101
|
+
confidence="proxy_upper_bound",
|
|
102
|
+
status=status,
|
|
103
|
+
basis=basis,
|
|
104
|
+
dependencies=missing,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _article_upper(profile: CorpusProfile, multiplier: int = 1) -> Optional[int]:
|
|
109
|
+
if profile.complete:
|
|
110
|
+
return profile.eligible_articles * multiplier
|
|
111
|
+
limits = [
|
|
112
|
+
value
|
|
113
|
+
for value in (profile.article_limit, profile.max_pages)
|
|
114
|
+
if value is not None
|
|
115
|
+
]
|
|
116
|
+
return min(limits) * multiplier if limits else None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _disabled(name: str, task: TaskConfig) -> TaskCapacity:
|
|
120
|
+
return TaskCapacity(
|
|
121
|
+
name=name,
|
|
122
|
+
enabled=False,
|
|
123
|
+
requested=task.target,
|
|
124
|
+
status="disabled",
|
|
125
|
+
variants={},
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def estimate_task_capacities(
|
|
130
|
+
profile: CorpusProfile,
|
|
131
|
+
tasks: Mapping[str, TaskConfig],
|
|
132
|
+
services: ServicesConfig,
|
|
133
|
+
) -> CapacityReport:
|
|
134
|
+
"""Estimate each configured task without invoking external services."""
|
|
135
|
+
|
|
136
|
+
results: Dict[str, TaskCapacity] = {}
|
|
137
|
+
report_warnings: List[str] = list(profile.warnings)
|
|
138
|
+
embeddings = _missing_services(services, ("embeddings",))
|
|
139
|
+
knowledge_base = _missing_services(services, ("knowledge_base",))
|
|
140
|
+
llm_and_embeddings = _missing_services(services, ("llm", "embeddings"))
|
|
141
|
+
|
|
142
|
+
for name, task in tasks.items():
|
|
143
|
+
if not task.enabled:
|
|
144
|
+
results[name] = _disabled(name, task)
|
|
145
|
+
continue
|
|
146
|
+
variants: Dict[str, CapacityRange]
|
|
147
|
+
warnings: Tuple[str, ...] = ()
|
|
148
|
+
if name in {
|
|
149
|
+
"paragraph_coherence",
|
|
150
|
+
"paragraph_ordering",
|
|
151
|
+
"article_membership",
|
|
152
|
+
"next_paragraph",
|
|
153
|
+
}:
|
|
154
|
+
missing = () if name == "paragraph_ordering" else embeddings
|
|
155
|
+
value = _structural_range(
|
|
156
|
+
profile,
|
|
157
|
+
task.target,
|
|
158
|
+
profile.articles_four_plus,
|
|
159
|
+
profile.estimated_articles_four_plus,
|
|
160
|
+
_article_upper(profile),
|
|
161
|
+
"articles with at least four selected paragraphs",
|
|
162
|
+
missing,
|
|
163
|
+
)
|
|
164
|
+
variants = {"default": value}
|
|
165
|
+
elif name == "reranking":
|
|
166
|
+
candidate_count = _option(task, "candidate_count", 20)
|
|
167
|
+
query_per_title = _option(task, "query_per_title", 1)
|
|
168
|
+
assert candidate_count is not None
|
|
169
|
+
assert query_per_title is not None
|
|
170
|
+
observed = (
|
|
171
|
+
profile.reranking_articles * query_per_title
|
|
172
|
+
if profile.reranking_articles >= candidate_count
|
|
173
|
+
else 0
|
|
174
|
+
)
|
|
175
|
+
projected_articles = profile.estimated_reranking_articles
|
|
176
|
+
estimated = (
|
|
177
|
+
projected_articles * query_per_title
|
|
178
|
+
if projected_articles is not None
|
|
179
|
+
and projected_articles >= candidate_count
|
|
180
|
+
else (0 if projected_articles is not None else None)
|
|
181
|
+
)
|
|
182
|
+
variants = {
|
|
183
|
+
"type1": _structural_range(
|
|
184
|
+
profile,
|
|
185
|
+
task.target,
|
|
186
|
+
observed,
|
|
187
|
+
estimated,
|
|
188
|
+
_article_upper(profile, query_per_title),
|
|
189
|
+
(
|
|
190
|
+
"reranking-ready articles; at least "
|
|
191
|
+
f"{candidate_count} distinct titles required"
|
|
192
|
+
),
|
|
193
|
+
embeddings,
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
elif name == "classification":
|
|
197
|
+
candidate_total = _option(task, "candidate_total", None)
|
|
198
|
+
observed = profile.category_articles
|
|
199
|
+
projected = profile.estimated_category_articles
|
|
200
|
+
if candidate_total is not None:
|
|
201
|
+
observed = min(observed, candidate_total)
|
|
202
|
+
projected = (
|
|
203
|
+
min(projected, candidate_total) if projected is not None else None
|
|
204
|
+
)
|
|
205
|
+
value = _proxy_range(
|
|
206
|
+
profile,
|
|
207
|
+
task.target,
|
|
208
|
+
observed,
|
|
209
|
+
projected,
|
|
210
|
+
"category-bearing article proxy before clustering and LLM review",
|
|
211
|
+
llm_and_embeddings,
|
|
212
|
+
)
|
|
213
|
+
variants = {name: value for name in ("type1", "type2", "type3")}
|
|
214
|
+
elif name == "ner":
|
|
215
|
+
context = _proxy_range(
|
|
216
|
+
profile,
|
|
217
|
+
task.target,
|
|
218
|
+
profile.entity_contexts,
|
|
219
|
+
profile.estimated_entity_contexts,
|
|
220
|
+
"internal-link context proxy before Wikidata typing",
|
|
221
|
+
knowledge_base,
|
|
222
|
+
)
|
|
223
|
+
mention = _proxy_range(
|
|
224
|
+
profile,
|
|
225
|
+
task.target,
|
|
226
|
+
profile.entity_mentions,
|
|
227
|
+
profile.estimated_entity_mentions,
|
|
228
|
+
"internal-link mention proxy before Wikidata typing",
|
|
229
|
+
knowledge_base,
|
|
230
|
+
)
|
|
231
|
+
variants = {"type1": context, "type2": context, "type3": mention}
|
|
232
|
+
elif name == "extractive_qa":
|
|
233
|
+
qa_service_names = ["knowledge_base"]
|
|
234
|
+
if task.options.get("generation_method", "template") == "hybrid":
|
|
235
|
+
qa_service_names.append("llm")
|
|
236
|
+
qa_services = _missing_services(services, tuple(qa_service_names))
|
|
237
|
+
variants = {
|
|
238
|
+
"type1": _proxy_range(
|
|
239
|
+
profile,
|
|
240
|
+
task.target,
|
|
241
|
+
profile.qa_capped_candidates,
|
|
242
|
+
profile.estimated_qa_candidates,
|
|
243
|
+
("entity-bearing QA context proxy with per-label " "candidate cap"),
|
|
244
|
+
qa_services,
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
else:
|
|
248
|
+
value = CapacityRange(
|
|
249
|
+
requested=task.target,
|
|
250
|
+
observed_candidates=0,
|
|
251
|
+
lower_bound=0,
|
|
252
|
+
estimated=None,
|
|
253
|
+
upper_bound=None,
|
|
254
|
+
confidence="unsupported",
|
|
255
|
+
status="blocked",
|
|
256
|
+
basis="unsupported task",
|
|
257
|
+
)
|
|
258
|
+
variants = {"default": value}
|
|
259
|
+
warnings = (f"No capacity model is registered for task {name!r}.",)
|
|
260
|
+
report_warnings.extend(warnings)
|
|
261
|
+
|
|
262
|
+
results[name] = TaskCapacity(
|
|
263
|
+
name=name,
|
|
264
|
+
enabled=True,
|
|
265
|
+
requested=task.target,
|
|
266
|
+
status=_task_status(variants.values()),
|
|
267
|
+
variants=variants,
|
|
268
|
+
warnings=warnings,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
constraints = []
|
|
272
|
+
for constraint_name, pair in (
|
|
273
|
+
(
|
|
274
|
+
"multiple_choice_disjoint_articles",
|
|
275
|
+
("paragraph_coherence", "paragraph_ordering"),
|
|
276
|
+
),
|
|
277
|
+
(
|
|
278
|
+
"other_disjoint_articles",
|
|
279
|
+
("article_membership", "next_paragraph"),
|
|
280
|
+
),
|
|
281
|
+
):
|
|
282
|
+
if not all(name in tasks and tasks[name].enabled for name in pair):
|
|
283
|
+
continue
|
|
284
|
+
requested = sum(tasks[name].target for name in pair)
|
|
285
|
+
if profile.articles_four_plus >= requested:
|
|
286
|
+
status = "sufficient"
|
|
287
|
+
elif profile.complete:
|
|
288
|
+
status = "insufficient"
|
|
289
|
+
else:
|
|
290
|
+
status = "uncertain"
|
|
291
|
+
constraints.append(
|
|
292
|
+
PoolConstraint(
|
|
293
|
+
name=constraint_name,
|
|
294
|
+
tasks=pair,
|
|
295
|
+
requested=requested,
|
|
296
|
+
observed_available=profile.articles_four_plus,
|
|
297
|
+
estimated_available=profile.estimated_articles_four_plus,
|
|
298
|
+
status=status,
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
return CapacityReport(
|
|
303
|
+
schema_version=1,
|
|
304
|
+
run_status="complete",
|
|
305
|
+
profile=profile,
|
|
306
|
+
tasks=results,
|
|
307
|
+
constraints=tuple(constraints),
|
|
308
|
+
warnings=tuple(dict.fromkeys(report_warnings)),
|
|
309
|
+
)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Public models for dump profiles and task-capacity reports."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict, dataclass
|
|
4
|
+
from typing import Any, Dict, Mapping, Optional, Tuple
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class CorpusProfile:
|
|
9
|
+
"""Observed and projected properties of a configured corpus scan."""
|
|
10
|
+
|
|
11
|
+
mode: str
|
|
12
|
+
dump_path: str
|
|
13
|
+
complete: bool
|
|
14
|
+
completion_reason: str
|
|
15
|
+
pages_scanned: int
|
|
16
|
+
main_namespace_pages: int
|
|
17
|
+
redirects_skipped: int
|
|
18
|
+
empty_pages_skipped: int
|
|
19
|
+
title_suffixes_skipped: int
|
|
20
|
+
insufficient_paragraphs_skipped: int
|
|
21
|
+
eligible_articles: int
|
|
22
|
+
paragraphs_available: int
|
|
23
|
+
paragraphs_selected: int
|
|
24
|
+
articles_four_plus: int
|
|
25
|
+
category_articles: int
|
|
26
|
+
entity_contexts: int
|
|
27
|
+
entity_mentions: int
|
|
28
|
+
qa_candidate_contexts: int
|
|
29
|
+
qa_capped_candidates: int
|
|
30
|
+
reranking_articles: int
|
|
31
|
+
bytes_read: int
|
|
32
|
+
total_bytes: int
|
|
33
|
+
estimated_pages: Optional[int]
|
|
34
|
+
estimated_eligible_articles: Optional[int]
|
|
35
|
+
estimated_articles_four_plus: Optional[int]
|
|
36
|
+
estimated_category_articles: Optional[int]
|
|
37
|
+
estimated_entity_contexts: Optional[int]
|
|
38
|
+
estimated_entity_mentions: Optional[int]
|
|
39
|
+
estimated_qa_candidates: Optional[int]
|
|
40
|
+
estimated_reranking_articles: Optional[int]
|
|
41
|
+
article_limit: Optional[int]
|
|
42
|
+
max_pages: Optional[int]
|
|
43
|
+
warnings: Tuple[str, ...] = ()
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def fraction_read(self) -> Optional[float]:
|
|
47
|
+
if self.total_bytes <= 0:
|
|
48
|
+
return None
|
|
49
|
+
return min(1.0, self.bytes_read / self.total_bytes)
|
|
50
|
+
|
|
51
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
52
|
+
value = asdict(self)
|
|
53
|
+
value["warnings"] = list(self.warnings)
|
|
54
|
+
value["fraction_read"] = self.fraction_read
|
|
55
|
+
return value
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class CapacityRange:
|
|
60
|
+
"""Capacity information for one concrete output variant."""
|
|
61
|
+
|
|
62
|
+
requested: int
|
|
63
|
+
observed_candidates: int
|
|
64
|
+
lower_bound: int
|
|
65
|
+
estimated: Optional[int]
|
|
66
|
+
upper_bound: Optional[int]
|
|
67
|
+
confidence: str
|
|
68
|
+
status: str
|
|
69
|
+
basis: str
|
|
70
|
+
dependencies: Tuple[str, ...] = ()
|
|
71
|
+
|
|
72
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
73
|
+
value = asdict(self)
|
|
74
|
+
value["dependencies"] = list(self.dependencies)
|
|
75
|
+
return value
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True)
|
|
79
|
+
class TaskCapacity:
|
|
80
|
+
"""Capacity result for one configured task and its variants."""
|
|
81
|
+
|
|
82
|
+
name: str
|
|
83
|
+
enabled: bool
|
|
84
|
+
requested: int
|
|
85
|
+
status: str
|
|
86
|
+
variants: Mapping[str, CapacityRange]
|
|
87
|
+
warnings: Tuple[str, ...] = ()
|
|
88
|
+
|
|
89
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
90
|
+
return {
|
|
91
|
+
"name": self.name,
|
|
92
|
+
"enabled": self.enabled,
|
|
93
|
+
"requested": self.requested,
|
|
94
|
+
"status": self.status,
|
|
95
|
+
"variants": {
|
|
96
|
+
name: value.to_dict() for name, value in self.variants.items()
|
|
97
|
+
},
|
|
98
|
+
"warnings": list(self.warnings),
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass(frozen=True)
|
|
103
|
+
class PoolConstraint:
|
|
104
|
+
"""A legacy disjoint-selection constraint shared by multiple tasks."""
|
|
105
|
+
|
|
106
|
+
name: str
|
|
107
|
+
tasks: Tuple[str, ...]
|
|
108
|
+
requested: int
|
|
109
|
+
observed_available: int
|
|
110
|
+
estimated_available: Optional[int]
|
|
111
|
+
status: str
|
|
112
|
+
|
|
113
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
114
|
+
value = asdict(self)
|
|
115
|
+
value["tasks"] = list(self.tasks)
|
|
116
|
+
return value
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass(frozen=True)
|
|
120
|
+
class CapacityReport:
|
|
121
|
+
"""Complete, serializable capacity analysis result."""
|
|
122
|
+
|
|
123
|
+
schema_version: int
|
|
124
|
+
run_status: str
|
|
125
|
+
profile: CorpusProfile
|
|
126
|
+
tasks: Mapping[str, TaskCapacity]
|
|
127
|
+
constraints: Tuple[PoolConstraint, ...] = ()
|
|
128
|
+
warnings: Tuple[str, ...] = ()
|
|
129
|
+
output_dir: Optional[str] = None
|
|
130
|
+
|
|
131
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
132
|
+
return {
|
|
133
|
+
"schema_version": self.schema_version,
|
|
134
|
+
"run_status": self.run_status,
|
|
135
|
+
"profile": self.profile.to_dict(),
|
|
136
|
+
"tasks": {
|
|
137
|
+
name: value.to_dict() for name, value in self.tasks.items()
|
|
138
|
+
},
|
|
139
|
+
"constraints": [value.to_dict() for value in self.constraints],
|
|
140
|
+
"warnings": list(self.warnings),
|
|
141
|
+
"output_dir": self.output_dir,
|
|
142
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Safe, isolated pilot corpus construction."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import tempfile
|
|
7
|
+
import uuid
|
|
8
|
+
from dataclasses import replace
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from hermes_benchmark.config import GenerationConfig
|
|
12
|
+
from hermes_benchmark.progress import progress_bar
|
|
13
|
+
from hermes_benchmark.wikipedia import build_corpus
|
|
14
|
+
|
|
15
|
+
from .estimates import estimate_task_capacities
|
|
16
|
+
from .models import CapacityReport
|
|
17
|
+
from .profiler import profile_dump
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _bounded(requested: int, configured: int) -> int:
|
|
21
|
+
return min(requested, configured)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def run_pilot(
|
|
25
|
+
config: GenerationConfig,
|
|
26
|
+
*,
|
|
27
|
+
article_limit: int = 100,
|
|
28
|
+
max_pages: int = 10_000,
|
|
29
|
+
) -> CapacityReport:
|
|
30
|
+
"""Build and publish an isolated pilot corpus plus its capacity report."""
|
|
31
|
+
|
|
32
|
+
if article_limit <= 0:
|
|
33
|
+
raise ValueError("article_limit must be greater than zero")
|
|
34
|
+
if max_pages <= 0:
|
|
35
|
+
raise ValueError("max_pages must be greater than zero")
|
|
36
|
+
|
|
37
|
+
effective_articles = article_limit
|
|
38
|
+
if config.corpus.article_limit is not None:
|
|
39
|
+
effective_articles = _bounded(
|
|
40
|
+
effective_articles,
|
|
41
|
+
config.corpus.article_limit,
|
|
42
|
+
)
|
|
43
|
+
effective_pages = max_pages
|
|
44
|
+
if config.corpus.max_pages is not None:
|
|
45
|
+
effective_pages = _bounded(effective_pages, config.corpus.max_pages)
|
|
46
|
+
|
|
47
|
+
stage_progress = progress_bar(total=2, desc="Pilot", unit="stage")
|
|
48
|
+
try:
|
|
49
|
+
stage_progress.set_postfix_str("profiling", refresh=False)
|
|
50
|
+
profile = profile_dump(
|
|
51
|
+
config.dump.path,
|
|
52
|
+
config.corpus,
|
|
53
|
+
page_limit=effective_pages,
|
|
54
|
+
tasks=config.tasks,
|
|
55
|
+
mode="pilot",
|
|
56
|
+
)
|
|
57
|
+
base_report = estimate_task_capacities(
|
|
58
|
+
profile,
|
|
59
|
+
config.tasks,
|
|
60
|
+
config.services,
|
|
61
|
+
)
|
|
62
|
+
stage_progress.update(1)
|
|
63
|
+
stage_progress.set_postfix_str("building corpus", refresh=False)
|
|
64
|
+
|
|
65
|
+
output_root = Path(config.output_dir)
|
|
66
|
+
final_dir = output_root / "pilot"
|
|
67
|
+
output_root.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
staging_dir = Path(
|
|
69
|
+
tempfile.mkdtemp(prefix=".pilot.", dir=str(output_root))
|
|
70
|
+
)
|
|
71
|
+
backup_dir = output_root / f".pilot.backup.{uuid.uuid4().hex}"
|
|
72
|
+
warnings = list(base_report.warnings)
|
|
73
|
+
pilot_corpus = config.corpus.model_copy(
|
|
74
|
+
update={
|
|
75
|
+
"article_limit": effective_articles,
|
|
76
|
+
"max_pages": effective_pages,
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
stats = build_corpus(
|
|
80
|
+
config.dump.path,
|
|
81
|
+
staging_dir,
|
|
82
|
+
pilot_corpus,
|
|
83
|
+
)
|
|
84
|
+
if stats.articles_written == 0:
|
|
85
|
+
run_status = "fail"
|
|
86
|
+
warnings.append("The pilot did not produce any eligible articles.")
|
|
87
|
+
elif stats.articles_written < effective_articles:
|
|
88
|
+
run_status = "pass_with_warnings"
|
|
89
|
+
warnings.append(
|
|
90
|
+
"The pilot reached the dump/page limit before the requested "
|
|
91
|
+
f"{effective_articles} articles; {stats.articles_written} were written."
|
|
92
|
+
)
|
|
93
|
+
else:
|
|
94
|
+
run_status = "pass"
|
|
95
|
+
|
|
96
|
+
report = replace(
|
|
97
|
+
base_report,
|
|
98
|
+
run_status=run_status,
|
|
99
|
+
warnings=tuple(dict.fromkeys(warnings)),
|
|
100
|
+
output_dir=str(final_dir),
|
|
101
|
+
)
|
|
102
|
+
report_path = staging_dir / "report.json"
|
|
103
|
+
with report_path.open("w", encoding="utf-8") as report_file:
|
|
104
|
+
json.dump(
|
|
105
|
+
report.to_dict(),
|
|
106
|
+
report_file,
|
|
107
|
+
ensure_ascii=False,
|
|
108
|
+
indent=2,
|
|
109
|
+
sort_keys=True,
|
|
110
|
+
)
|
|
111
|
+
report_file.write("\n")
|
|
112
|
+
|
|
113
|
+
moved_old = False
|
|
114
|
+
try:
|
|
115
|
+
if final_dir.exists():
|
|
116
|
+
os.replace(final_dir, backup_dir)
|
|
117
|
+
moved_old = True
|
|
118
|
+
os.replace(staging_dir, final_dir)
|
|
119
|
+
except BaseException:
|
|
120
|
+
if moved_old and backup_dir.exists() and not final_dir.exists():
|
|
121
|
+
os.replace(backup_dir, final_dir)
|
|
122
|
+
raise
|
|
123
|
+
if backup_dir.exists():
|
|
124
|
+
shutil.rmtree(backup_dir)
|
|
125
|
+
stage_progress.update(1)
|
|
126
|
+
stage_progress.set_postfix_str("complete", refresh=False)
|
|
127
|
+
return report
|
|
128
|
+
except BaseException:
|
|
129
|
+
if "staging_dir" in locals() and staging_dir.exists():
|
|
130
|
+
shutil.rmtree(staging_dir)
|
|
131
|
+
raise
|
|
132
|
+
finally:
|
|
133
|
+
stage_progress.close()
|