@moleculeagora/cli 0.1.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.
- package/README.md +31 -0
- package/dist/index.js +30368 -0
- package/dist/index.js.map +1 -0
- package/dist/python-v1/agora_runtime.py +282 -0
- package/dist/python-v1/answer-set-metric.py +264 -0
- package/dist/python-v1/assertion-set-evaluation.py +879 -0
- package/dist/python-v1/exact-match.py +60 -0
- package/dist/python-v1/l4-composition.py +435 -0
- package/dist/python-v1/multi-output-tabular-metric.py +392 -0
- package/dist/python-v1/panel-ranking-metric.py +622 -0
- package/dist/python-v1/project-test.py +256 -0
- package/dist/python-v1/protein-binder-assay-metric.py +600 -0
- package/dist/python-v1/public-tool-metric.py +161 -0
- package/dist/python-v1/ranking-metric.py +426 -0
- package/dist/python-v1/reference-artifact-assertion.py +532 -0
- package/dist/python-v1/rubric-validation.py +246 -0
- package/dist/python-v1/solver-python-stdio-test.py +160 -0
- package/dist/python-v1/statistical-endpoint-test-v2.py +629 -0
- package/dist/python-v1/statistical-endpoint-test.py +442 -0
- package/dist/python-v1/table-metric.py +1291 -0
- package/dist/release-metadata.json +7 -0
- package/package.json +67 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from agora_runtime import (
|
|
4
|
+
fail_runtime,
|
|
5
|
+
load_json_file,
|
|
6
|
+
load_runtime_context,
|
|
7
|
+
resolve_evaluation_artifact,
|
|
8
|
+
resolve_scoring_asset,
|
|
9
|
+
resolve_submission_artifact,
|
|
10
|
+
write_score,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def require_string(config, key):
|
|
15
|
+
value = config.get(key)
|
|
16
|
+
if not isinstance(value, str) or not value.strip():
|
|
17
|
+
fail_runtime(f"compiled_config.{key} must be a non-empty string.")
|
|
18
|
+
return value.strip()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def read_bytes(path: Path, label: str) -> bytes:
|
|
22
|
+
try:
|
|
23
|
+
return path.read_bytes()
|
|
24
|
+
except FileNotFoundError:
|
|
25
|
+
fail_runtime(f"Missing {label} at {path}.")
|
|
26
|
+
except OSError as error:
|
|
27
|
+
fail_runtime(f"Unable to read {label}: {error}.")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def main():
|
|
31
|
+
runtime_context = load_runtime_context()
|
|
32
|
+
config_path = resolve_scoring_asset(
|
|
33
|
+
runtime_context,
|
|
34
|
+
"compiled_config",
|
|
35
|
+
kind="config",
|
|
36
|
+
)
|
|
37
|
+
config = load_json_file(config_path, label="compiled_config")
|
|
38
|
+
evaluation_role = require_string(config, "evaluation_role")
|
|
39
|
+
submission_role = require_string(config, "submission_role")
|
|
40
|
+
score_key = require_string(config, "final_score_key")
|
|
41
|
+
evaluation_path = resolve_evaluation_artifact(runtime_context, evaluation_role)
|
|
42
|
+
submission_path = resolve_submission_artifact(runtime_context, submission_role)
|
|
43
|
+
matched = read_bytes(
|
|
44
|
+
evaluation_path,
|
|
45
|
+
f"evaluation artifact {evaluation_role}",
|
|
46
|
+
) == read_bytes(
|
|
47
|
+
submission_path,
|
|
48
|
+
f"submission artifact {submission_role}",
|
|
49
|
+
)
|
|
50
|
+
score = 1.0 if matched else 0.0
|
|
51
|
+
write_score(
|
|
52
|
+
score=score,
|
|
53
|
+
details={
|
|
54
|
+
score_key: score,
|
|
55
|
+
},
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
main()
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import math
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from agora_runtime import (
|
|
11
|
+
fail_runtime,
|
|
12
|
+
load_json_file,
|
|
13
|
+
load_score_output,
|
|
14
|
+
load_runtime_context,
|
|
15
|
+
reject_submission,
|
|
16
|
+
resolve_scoring_asset,
|
|
17
|
+
write_score,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
INPUT_ROOT = Path(os.environ.get("AGORA_RUNTIME_INPUT_ROOT", "/input"))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def require_string(value, label):
|
|
24
|
+
if not isinstance(value, str) or not value.strip():
|
|
25
|
+
fail_runtime(f"{label} must be a non-empty string.")
|
|
26
|
+
return value.strip()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def require_number(value, label):
|
|
30
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
31
|
+
fail_runtime(f"{label} must be a number.")
|
|
32
|
+
return float(value)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def require_positive_integer(value, label):
|
|
36
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < 1:
|
|
37
|
+
fail_runtime(f"{label} must be a positive integer.")
|
|
38
|
+
return value
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def validate_unit_score(value, label):
|
|
42
|
+
if not math.isfinite(value) or value < 0 or value > 1:
|
|
43
|
+
fail_runtime(f"{label} must be a finite number in [0, 1].")
|
|
44
|
+
return value
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def require_object(value, label):
|
|
48
|
+
if not isinstance(value, dict):
|
|
49
|
+
fail_runtime(f"{label} must be an object.")
|
|
50
|
+
return value
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def require_list(value, label):
|
|
54
|
+
if not isinstance(value, list):
|
|
55
|
+
fail_runtime(f"{label} must be an array.")
|
|
56
|
+
return value
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def write_json(path, value):
|
|
60
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
path.write_text(json.dumps(value, sort_keys=True, indent=2), encoding="utf-8")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def symlink_or_copy(source, target):
|
|
65
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
if not source.exists():
|
|
67
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
return
|
|
69
|
+
try:
|
|
70
|
+
target.symlink_to(source, target_is_directory=source.is_dir())
|
|
71
|
+
except OSError:
|
|
72
|
+
if source.is_dir():
|
|
73
|
+
shutil.copytree(source, target, dirs_exist_ok=True)
|
|
74
|
+
else:
|
|
75
|
+
shutil.copy2(source, target)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def asset_file_metadata(path):
|
|
79
|
+
data = path.read_bytes()
|
|
80
|
+
import hashlib
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
"size_bytes": len(data),
|
|
84
|
+
"sha256": hashlib.sha256(data).hexdigest(),
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def resolve_profile_determinism_env(runtime_context):
|
|
89
|
+
profile = require_object(runtime_context.get("runtime_profile"), "runtime_profile")
|
|
90
|
+
determinism_env = require_object(
|
|
91
|
+
profile.get("determinism_env"),
|
|
92
|
+
"runtime_profile.determinism_env",
|
|
93
|
+
)
|
|
94
|
+
resolved = {}
|
|
95
|
+
for name, value in determinism_env.items():
|
|
96
|
+
resolved[require_string(name, "runtime_profile.determinism_env key")] = require_string(
|
|
97
|
+
value,
|
|
98
|
+
f"runtime_profile.determinism_env.{name}",
|
|
99
|
+
)
|
|
100
|
+
return resolved
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def assert_deterministic_child_env(env, determinism_env):
|
|
104
|
+
for name, expected in determinism_env.items():
|
|
105
|
+
if env.get(name) != expected:
|
|
106
|
+
fail_runtime(
|
|
107
|
+
f"component subprocess env {name} must be {expected!r}."
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def build_component_env(runtime_context, temp_root, temp_output):
|
|
112
|
+
determinism_env = resolve_profile_determinism_env(runtime_context)
|
|
113
|
+
env = os.environ.copy()
|
|
114
|
+
sdk_dir = str(temp_root / "scoring_assets" / "python_v1_runtime_sdk")
|
|
115
|
+
env.update(determinism_env)
|
|
116
|
+
env["PYTHONPATH"] = os.pathsep.join(
|
|
117
|
+
[sdk_dir, env.get("PYTHONPATH", "")]
|
|
118
|
+
).strip(os.pathsep)
|
|
119
|
+
env["AGORA_RUNTIME_INPUT_ROOT"] = str(temp_root)
|
|
120
|
+
env["AGORA_RUNTIME_OUTPUT_ROOT"] = str(temp_output)
|
|
121
|
+
assert_deterministic_child_env(env, determinism_env)
|
|
122
|
+
return env
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def build_component_manifest(runtime_context, component, temp_root, program_path, config_path):
|
|
126
|
+
sdk_path = resolve_scoring_asset(
|
|
127
|
+
runtime_context,
|
|
128
|
+
"python_v1_runtime_sdk",
|
|
129
|
+
kind="document",
|
|
130
|
+
)
|
|
131
|
+
temp_program_path = temp_root / "scoring_assets" / "compiled_program" / "score.py"
|
|
132
|
+
temp_config_path = temp_root / "scoring_assets" / "compiled_config" / "score-config.json"
|
|
133
|
+
temp_sdk_path = temp_root / "scoring_assets" / "python_v1_runtime_sdk" / "agora_runtime.py"
|
|
134
|
+
|
|
135
|
+
symlink_or_copy(program_path, temp_program_path)
|
|
136
|
+
symlink_or_copy(config_path, temp_config_path)
|
|
137
|
+
symlink_or_copy(sdk_path, temp_sdk_path)
|
|
138
|
+
symlink_or_copy(INPUT_ROOT / "evaluation", temp_root / "evaluation")
|
|
139
|
+
symlink_or_copy(INPUT_ROOT / "submission", temp_root / "submission")
|
|
140
|
+
|
|
141
|
+
program_metadata = asset_file_metadata(temp_program_path)
|
|
142
|
+
config_metadata = asset_file_metadata(temp_config_path)
|
|
143
|
+
sdk_metadata = asset_file_metadata(temp_sdk_path)
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
"kind": "runtime_manifest",
|
|
147
|
+
"runtime_profile": runtime_context["runtime_profile"],
|
|
148
|
+
"artifact_contract": runtime_context["artifact_contract"],
|
|
149
|
+
"evaluation_bindings": runtime_context.get("evaluation_bindings", []),
|
|
150
|
+
"artifacts": runtime_context["artifacts"],
|
|
151
|
+
"scoring_assets": [
|
|
152
|
+
{
|
|
153
|
+
"role": "compiled_program",
|
|
154
|
+
"kind": "program",
|
|
155
|
+
"artifact_id": f"component-program-{component['id']}",
|
|
156
|
+
"abi_version": "python-v1",
|
|
157
|
+
"entrypoint": "score.py",
|
|
158
|
+
"relative_path": "scoring_assets/compiled_program/score.py",
|
|
159
|
+
"file_name": "score.py",
|
|
160
|
+
**program_metadata,
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"role": "compiled_config",
|
|
164
|
+
"kind": "config",
|
|
165
|
+
"artifact_id": f"component-config-{component['id']}",
|
|
166
|
+
"relative_path": "scoring_assets/compiled_config/score-config.json",
|
|
167
|
+
"file_name": "score-config.json",
|
|
168
|
+
**config_metadata,
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"role": "python_v1_runtime_sdk",
|
|
172
|
+
"kind": "document",
|
|
173
|
+
"artifact_id": f"component-sdk-{component['id']}",
|
|
174
|
+
"relative_path": "scoring_assets/python_v1_runtime_sdk/agora_runtime.py",
|
|
175
|
+
"file_name": "agora_runtime.py",
|
|
176
|
+
**sdk_metadata,
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
"objective": component["objective"],
|
|
180
|
+
"final_score_key": component["final_score_key"],
|
|
181
|
+
"scorer_result_schema": runtime_context["scorer_result_schema"],
|
|
182
|
+
"policies": runtime_context["policies"],
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def run_component(runtime_context, component):
|
|
187
|
+
program_role = require_string(component.get("program_role"), f"components.{component.get('id')}.program_role")
|
|
188
|
+
program_path = resolve_scoring_asset(runtime_context, program_role, kind="document")
|
|
189
|
+
with tempfile.TemporaryDirectory(prefix=f"agora-l4-{component['id']}-") as temp_dir:
|
|
190
|
+
temp_root = Path(temp_dir) / "input"
|
|
191
|
+
temp_output = Path(temp_dir) / "output"
|
|
192
|
+
temp_output.mkdir(parents=True, exist_ok=True)
|
|
193
|
+
config_path = temp_root / "component-config.json"
|
|
194
|
+
write_json(config_path, component["config"])
|
|
195
|
+
manifest = build_component_manifest(
|
|
196
|
+
runtime_context,
|
|
197
|
+
component,
|
|
198
|
+
temp_root,
|
|
199
|
+
program_path,
|
|
200
|
+
config_path,
|
|
201
|
+
)
|
|
202
|
+
write_json(temp_root / "runtime-manifest.json", manifest)
|
|
203
|
+
env = build_component_env(runtime_context, temp_root, temp_output)
|
|
204
|
+
completed = subprocess.run(
|
|
205
|
+
[sys.executable, str(temp_root / "scoring_assets" / "compiled_program" / "score.py")],
|
|
206
|
+
env=env,
|
|
207
|
+
stdout=subprocess.PIPE,
|
|
208
|
+
stderr=subprocess.PIPE,
|
|
209
|
+
text=True,
|
|
210
|
+
timeout=300,
|
|
211
|
+
check=False,
|
|
212
|
+
)
|
|
213
|
+
output_path = temp_output / "score.json"
|
|
214
|
+
if not output_path.exists():
|
|
215
|
+
fail_runtime(
|
|
216
|
+
f"component {component['id']} did not write score.json. stderr: {completed.stderr.strip()}"
|
|
217
|
+
)
|
|
218
|
+
try:
|
|
219
|
+
payload = load_score_output(
|
|
220
|
+
output_path,
|
|
221
|
+
label=f"component {component['id']} score output",
|
|
222
|
+
)
|
|
223
|
+
except RuntimeError as error:
|
|
224
|
+
fail_runtime(str(error))
|
|
225
|
+
if completed.returncode != 0:
|
|
226
|
+
fail_runtime(
|
|
227
|
+
f"component {component['id']} failed with exit code {completed.returncode}: {payload.get('error') or completed.stderr.strip()}"
|
|
228
|
+
)
|
|
229
|
+
if payload.get("ok") is not True:
|
|
230
|
+
reject_submission(
|
|
231
|
+
f"component {component['id']} rejected submission: {payload.get('error') or 'component returned ok=false'}",
|
|
232
|
+
details={"component": component["id"]},
|
|
233
|
+
)
|
|
234
|
+
return require_number(payload.get("score"), f"component {component['id']} score")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def normalize_components(config):
|
|
238
|
+
components = []
|
|
239
|
+
seen = set()
|
|
240
|
+
for index, value in enumerate(require_list(config.get("components"), "compiled_config.components")):
|
|
241
|
+
component = require_object(value, f"compiled_config.components[{index}]")
|
|
242
|
+
component_id = require_string(component.get("id"), f"compiled_config.components[{index}].id")
|
|
243
|
+
if component_id in seen:
|
|
244
|
+
fail_runtime(f"compiled_config.components repeats component id {component_id}.")
|
|
245
|
+
seen.add(component_id)
|
|
246
|
+
component["id"] = component_id
|
|
247
|
+
component["objective"] = require_string(component.get("objective"), f"compiled_config.components[{index}].objective")
|
|
248
|
+
component["final_score_key"] = require_string(component.get("final_score_key"), f"compiled_config.components[{index}].final_score_key")
|
|
249
|
+
component["config"] = require_object(component.get("config"), f"compiled_config.components[{index}].config")
|
|
250
|
+
components.append(component)
|
|
251
|
+
return components
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def weighted_sum_score(components_by_id, scores, aggregation):
|
|
255
|
+
terms = require_list(aggregation.get("terms"), "compiled_config.aggregation.terms")
|
|
256
|
+
weights = {}
|
|
257
|
+
for index, term_value in enumerate(terms):
|
|
258
|
+
term = require_object(term_value, f"compiled_config.aggregation.terms[{index}]")
|
|
259
|
+
component = require_string(term.get("component"), f"compiled_config.aggregation.terms[{index}].component")
|
|
260
|
+
if component not in components_by_id:
|
|
261
|
+
fail_runtime(f"compiled_config.aggregation.terms[{index}].component references unknown component {component}.")
|
|
262
|
+
if component in weights:
|
|
263
|
+
fail_runtime(f"compiled_config.aggregation.terms repeats component {component}.")
|
|
264
|
+
weights[component] = require_number(term.get("weight"), f"compiled_config.aggregation.terms[{index}].weight")
|
|
265
|
+
if set(weights) != set(components_by_id):
|
|
266
|
+
fail_runtime("compiled_config.aggregation.terms must exactly match compiled_config.components ids.")
|
|
267
|
+
if bool(aggregation.get("normalize_weights", False)):
|
|
268
|
+
total_weight = sum(abs(weight) for weight in weights.values())
|
|
269
|
+
if total_weight == 0:
|
|
270
|
+
fail_runtime("compiled_config.aggregation.terms weights must not sum to zero when normalize_weights is true.")
|
|
271
|
+
weights = {component: weight / total_weight for component, weight in weights.items()}
|
|
272
|
+
return sum(scores[component] * weight for component, weight in weights.items()), weights
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def threshold_passes(value, comparator, threshold):
|
|
276
|
+
if comparator == "gte":
|
|
277
|
+
return value >= threshold
|
|
278
|
+
if comparator == "gt":
|
|
279
|
+
return value > threshold
|
|
280
|
+
if comparator == "lte":
|
|
281
|
+
return value <= threshold
|
|
282
|
+
if comparator == "lt":
|
|
283
|
+
return value < threshold
|
|
284
|
+
fail_runtime(f"compiled_config.aggregation.comparator={comparator} is unsupported.")
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def threshold_gate_score(components_by_id, scores, aggregation):
|
|
288
|
+
score_component = require_string(
|
|
289
|
+
aggregation.get("score_component"),
|
|
290
|
+
"compiled_config.aggregation.score_component",
|
|
291
|
+
)
|
|
292
|
+
gate_component = require_string(
|
|
293
|
+
aggregation.get("gate_component"),
|
|
294
|
+
"compiled_config.aggregation.gate_component",
|
|
295
|
+
)
|
|
296
|
+
if score_component not in components_by_id:
|
|
297
|
+
fail_runtime("compiled_config.aggregation.score_component references an unknown component.")
|
|
298
|
+
if gate_component not in components_by_id:
|
|
299
|
+
fail_runtime("compiled_config.aggregation.gate_component references an unknown component.")
|
|
300
|
+
comparator = require_string(aggregation.get("comparator", "gte"), "compiled_config.aggregation.comparator")
|
|
301
|
+
threshold = require_number(aggregation.get("threshold"), "compiled_config.aggregation.threshold")
|
|
302
|
+
fail_score = require_number(aggregation.get("fail_score", 0), "compiled_config.aggregation.fail_score")
|
|
303
|
+
passed = threshold_passes(scores[gate_component], comparator, threshold)
|
|
304
|
+
return (scores[score_component] if passed else fail_score), {
|
|
305
|
+
"kind": "threshold_gate",
|
|
306
|
+
"score_component": score_component,
|
|
307
|
+
"gate_component": gate_component,
|
|
308
|
+
"gate_score": scores[gate_component],
|
|
309
|
+
"comparator": comparator,
|
|
310
|
+
"threshold": threshold,
|
|
311
|
+
"passed": passed,
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def k_of_n_score(components_by_id, scores, aggregation):
|
|
316
|
+
terms = require_list(aggregation.get("terms"), "compiled_config.aggregation.terms")
|
|
317
|
+
term_components = set()
|
|
318
|
+
term_details = []
|
|
319
|
+
for index, term_value in enumerate(terms):
|
|
320
|
+
term = require_object(term_value, f"compiled_config.aggregation.terms[{index}]")
|
|
321
|
+
component = require_string(term.get("component"), f"compiled_config.aggregation.terms[{index}].component")
|
|
322
|
+
if component not in components_by_id:
|
|
323
|
+
fail_runtime(f"compiled_config.aggregation.terms[{index}].component references unknown component {component}.")
|
|
324
|
+
if component in term_components:
|
|
325
|
+
fail_runtime(f"compiled_config.aggregation.terms repeats component {component}.")
|
|
326
|
+
term_components.add(component)
|
|
327
|
+
pass_threshold = require_number(
|
|
328
|
+
term.get("pass_threshold"),
|
|
329
|
+
f"compiled_config.aggregation.terms[{index}].pass_threshold",
|
|
330
|
+
)
|
|
331
|
+
if pass_threshold < 0 or pass_threshold > 1:
|
|
332
|
+
fail_runtime(
|
|
333
|
+
f"compiled_config.aggregation.terms[{index}].pass_threshold must be in [0, 1]."
|
|
334
|
+
)
|
|
335
|
+
component_score = scores[component]
|
|
336
|
+
passed = component_score >= pass_threshold
|
|
337
|
+
term_details.append(
|
|
338
|
+
{
|
|
339
|
+
"component": component,
|
|
340
|
+
"score": component_score,
|
|
341
|
+
"pass_threshold": pass_threshold,
|
|
342
|
+
"passed": passed,
|
|
343
|
+
}
|
|
344
|
+
)
|
|
345
|
+
if set(term_components) != set(components_by_id):
|
|
346
|
+
fail_runtime("compiled_config.aggregation.terms must exactly match compiled_config.components ids.")
|
|
347
|
+
minimum_pass_count = require_positive_integer(
|
|
348
|
+
aggregation.get("minimum_pass_count"),
|
|
349
|
+
"compiled_config.aggregation.minimum_pass_count",
|
|
350
|
+
)
|
|
351
|
+
if minimum_pass_count > len(terms):
|
|
352
|
+
fail_runtime("compiled_config.aggregation.minimum_pass_count must be less than or equal to terms.length.")
|
|
353
|
+
passed_count = sum(1 for term in term_details if term["passed"])
|
|
354
|
+
total_count = len(term_details)
|
|
355
|
+
score = 1.0 if passed_count >= minimum_pass_count else 0.0
|
|
356
|
+
validate_unit_score(score, "k_of_n final score")
|
|
357
|
+
return score, {
|
|
358
|
+
"kind": "k_of_n",
|
|
359
|
+
"passed_count": passed_count,
|
|
360
|
+
"required_count": minimum_pass_count,
|
|
361
|
+
"total_count": total_count,
|
|
362
|
+
"terms": term_details,
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def worst_group_score(components_by_id, scores, aggregation):
|
|
367
|
+
terms = require_list(aggregation.get("terms"), "compiled_config.aggregation.terms")
|
|
368
|
+
term_scores = {}
|
|
369
|
+
for index, term_value in enumerate(terms):
|
|
370
|
+
term = require_object(term_value, f"compiled_config.aggregation.terms[{index}]")
|
|
371
|
+
component = require_string(term.get("component"), f"compiled_config.aggregation.terms[{index}].component")
|
|
372
|
+
if component not in components_by_id:
|
|
373
|
+
fail_runtime(f"compiled_config.aggregation.terms[{index}].component references unknown component {component}.")
|
|
374
|
+
if component in term_scores:
|
|
375
|
+
fail_runtime(f"compiled_config.aggregation.terms repeats component {component}.")
|
|
376
|
+
term_scores[component] = scores[component]
|
|
377
|
+
if len(term_scores) < 2:
|
|
378
|
+
fail_runtime("compiled_config.aggregation.terms must include at least two components.")
|
|
379
|
+
if set(term_scores) != set(components_by_id):
|
|
380
|
+
fail_runtime("compiled_config.aggregation.terms must exactly match compiled_config.components ids.")
|
|
381
|
+
term_details = [
|
|
382
|
+
{"component": component, "score": term_scores[component]}
|
|
383
|
+
for component in sorted(term_scores)
|
|
384
|
+
]
|
|
385
|
+
worst_term = min(term_details, key=lambda term: (term["score"], term["component"]))
|
|
386
|
+
return worst_term["score"], {
|
|
387
|
+
"kind": "worst_group",
|
|
388
|
+
"worst_component": worst_term["component"],
|
|
389
|
+
"terms": term_details,
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def main():
|
|
394
|
+
runtime_context = load_runtime_context()
|
|
395
|
+
config_path = resolve_scoring_asset(runtime_context, "compiled_config", kind="config")
|
|
396
|
+
config = load_json_file(config_path, label="compiled_config")
|
|
397
|
+
mode = require_string(config.get("mode"), "compiled_config.mode")
|
|
398
|
+
final_score_key = require_string(runtime_context.get("final_score_key"), "runtime_context.final_score_key")
|
|
399
|
+
if final_score_key != require_string(config.get("final_score_key"), "compiled_config.final_score_key"):
|
|
400
|
+
fail_runtime("compiled_config.final_score_key must match runtime_context.final_score_key.")
|
|
401
|
+
components = normalize_components(config)
|
|
402
|
+
components_by_id = {component["id"]: component for component in components}
|
|
403
|
+
scores = {
|
|
404
|
+
component["id"]: run_component(runtime_context, component)
|
|
405
|
+
for component in components
|
|
406
|
+
}
|
|
407
|
+
aggregation = require_object(config.get("aggregation"), "compiled_config.aggregation")
|
|
408
|
+
aggregation_kind = require_string(aggregation.get("kind"), "compiled_config.aggregation.kind")
|
|
409
|
+
if mode != aggregation_kind:
|
|
410
|
+
fail_runtime("compiled_config.mode must match compiled_config.aggregation.kind.")
|
|
411
|
+
if aggregation_kind == "weighted_sum":
|
|
412
|
+
score, aggregation_details = weighted_sum_score(components_by_id, scores, aggregation)
|
|
413
|
+
elif aggregation_kind == "threshold_gate":
|
|
414
|
+
score, aggregation_details = threshold_gate_score(components_by_id, scores, aggregation)
|
|
415
|
+
elif aggregation_kind == "k_of_n":
|
|
416
|
+
score, aggregation_details = k_of_n_score(components_by_id, scores, aggregation)
|
|
417
|
+
elif aggregation_kind == "worst_group":
|
|
418
|
+
score, aggregation_details = worst_group_score(components_by_id, scores, aggregation)
|
|
419
|
+
else:
|
|
420
|
+
fail_runtime(f"compiled_config.aggregation.kind={aggregation_kind} is unsupported.")
|
|
421
|
+
details = {
|
|
422
|
+
final_score_key: score,
|
|
423
|
+
"aggregation": aggregation_kind,
|
|
424
|
+
"components": scores,
|
|
425
|
+
"aggregation_details": aggregation_details,
|
|
426
|
+
}
|
|
427
|
+
if aggregation_kind == "k_of_n":
|
|
428
|
+
details["passed_count"] = aggregation_details["passed_count"]
|
|
429
|
+
details["required_count"] = aggregation_details["required_count"]
|
|
430
|
+
details["total_count"] = aggregation_details["total_count"]
|
|
431
|
+
write_score(score=score, details=details)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
if __name__ == "__main__":
|
|
435
|
+
main()
|