marin-taskcompendium 0.2.160.dev38062449553__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.
- marin_taskcompendium-0.2.160.dev38062449553.dist-info/METADATA +16 -0
- marin_taskcompendium-0.2.160.dev38062449553.dist-info/RECORD +81 -0
- marin_taskcompendium-0.2.160.dev38062449553.dist-info/WHEEL +4 -0
- taskcompendium/__init__.py +2 -0
- taskcompendium/chat.py +93 -0
- taskcompendium/convert/__init__.py +4 -0
- taskcompendium/convert/answers.py +210 -0
- taskcompendium/convert/code.py +78 -0
- taskcompendium/convert/conversation.py +44 -0
- taskcompendium/convert/delivery.py +34 -0
- taskcompendium/convert/environment.py +16 -0
- taskcompendium/convert/executable.py +348 -0
- taskcompendium/convert/json_schema.py +32 -0
- taskcompendium/convert/nemotron_ultra.py +225 -0
- taskcompendium/convert/preference.py +87 -0
- taskcompendium/convert/script_grader.py +61 -0
- taskcompendium/convert/tasktrove.py +184 -0
- taskcompendium/convert/tasktrove_code_contests.py +99 -0
- taskcompendium/convert/tasktrove_codeforces.py +117 -0
- taskcompendium/convert/tasktrove_converted_task.py +88 -0
- taskcompendium/convert/tasktrove_json_schemas.py +133 -0
- taskcompendium/convert/tasktrove_nemotron_data.py +25 -0
- taskcompendium/convert/tasktrove_nemotron_structured_outputs.py +284 -0
- taskcompendium/convert/tasktrove_nl2bash.py +168 -0
- taskcompendium/convert/tasktrove_python_unit_tests.py +100 -0
- taskcompendium/convert/tasktrove_stdio_cases.py +91 -0
- taskcompendium/convert/tasktrove_taco.py +88 -0
- taskcompendium/convert/verifyit_build.py +62 -0
- taskcompendium/direct_chat.py +40 -0
- taskcompendium/grader.py +42 -0
- taskcompendium/grading.py +118 -0
- taskcompendium/grading_result.py +37 -0
- taskcompendium/harbor/__init__.py +4 -0
- taskcompendium/harbor/compare.py +255 -0
- taskcompendium/harbor/export.py +644 -0
- taskcompendium/harbor/records.py +79 -0
- taskcompendium/harbor/snapshots.py +122 -0
- taskcompendium/harbor/tasktrove.py +72 -0
- taskcompendium/importers/__init__.py +4 -0
- taskcompendium/importers/nemo_predicted_action.py +200 -0
- taskcompendium/importers/swe.py +78 -0
- taskcompendium/importers/tasktrove/__init__.py +4 -0
- taskcompendium/importers/tasktrove/convert.py +70 -0
- taskcompendium/importers/tasktrove/mcqa.py +80 -0
- taskcompendium/importers/tasktrove/models.py +30 -0
- taskcompendium/models.py +891 -0
- taskcompendium/pipeline/README.md +223 -0
- taskcompendium/pipeline/__init__.py +4 -0
- taskcompendium/pipeline/audit_schema.py +119 -0
- taskcompendium/pipeline/chat_requests.py +100 -0
- taskcompendium/pipeline/controls.py +270 -0
- taskcompendium/pipeline/conversion.py +148 -0
- taskcompendium/pipeline/environment_inventory.py +49 -0
- taskcompendium/pipeline/execution_telemetry.py +141 -0
- taskcompendium/pipeline/filtering.py +37 -0
- taskcompendium/pipeline/fingerprints.py +110 -0
- taskcompendium/pipeline/inputs.py +98 -0
- taskcompendium/pipeline/models.py +325 -0
- taskcompendium/pipeline/query_cache.py +259 -0
- taskcompendium/pipeline/review.py +785 -0
- taskcompendium/pipeline/review_requests.py +285 -0
- taskcompendium/pipeline/sampling.py +44 -0
- taskcompendium/pipeline/shard_outputs.py +58 -0
- taskcompendium/pipeline/source_processing.py +1072 -0
- taskcompendium/pipeline/source_quality.py +283 -0
- taskcompendium/pipeline/source_verification.py +634 -0
- taskcompendium/pipeline/sources.py +210 -0
- taskcompendium/pipeline/stages.py +845 -0
- taskcompendium/pipeline/transforms.py +221 -0
- taskcompendium/pipeline/verification.py +192 -0
- taskcompendium/runtime/__init__.py +7 -0
- taskcompendium/runtime/environment.py +27 -0
- taskcompendium/runtime/episode.py +98 -0
- taskcompendium/runtime/grading.py +649 -0
- taskcompendium/runtime/local.py +219 -0
- taskcompendium/runtime/models.py +82 -0
- taskcompendium/runtime/output_capture.py +113 -0
- taskcompendium/runtime/resources.py +18 -0
- taskcompendium/runtime/shell.py +286 -0
- taskcompendium/runtime/task_grading.py +61 -0
- taskcompendium/submission.py +188 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: marin-taskcompendium
|
|
3
|
+
Version: 0.2.160.dev38062449553
|
|
4
|
+
Summary: Semantic task contracts, submission extraction, and shared grading
|
|
5
|
+
Requires-Python: <3.14,>=3.12
|
|
6
|
+
Requires-Dist: marin-rigging==0.2.160.dev38062449553
|
|
7
|
+
Requires-Dist: marin-shellbox==0.2.160.dev38062449553
|
|
8
|
+
Requires-Dist: marin-verifyit[judge,math,reasoning-gym,schema]==0.2.160.dev38062449553
|
|
9
|
+
Requires-Dist: pydantic>=2.0
|
|
10
|
+
Requires-Dist: tomlkit>=0.13
|
|
11
|
+
Provides-Extra: pipeline
|
|
12
|
+
Requires-Dist: datasets<5.0.0,>=3.1.0; extra == 'pipeline'
|
|
13
|
+
Requires-Dist: marin-finestore==0.2.160.dev38062449553; extra == 'pipeline'
|
|
14
|
+
Requires-Dist: marin-zephyr==0.2.160.dev38062449553; extra == 'pipeline'
|
|
15
|
+
Requires-Dist: numpy>=1.26; extra == 'pipeline'
|
|
16
|
+
Requires-Dist: pyarrow>=15.0; extra == 'pipeline'
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
taskcompendium/__init__.py,sha256=YCa8ajV8oMVGzdM-0ADm_Y7gL7bP-E4DDjDz4V7pBA0,68
|
|
2
|
+
taskcompendium/chat.py,sha256=IrkvB-hvvZzI4E39NKkJzJnjBJ5pTmzZux883o9_A4g,3194
|
|
3
|
+
taskcompendium/direct_chat.py,sha256=BRQYWrXl7NTH9abN_NovNSgLv6zKj1S8Wjg5RcZFK5c,1788
|
|
4
|
+
taskcompendium/grader.py,sha256=oOqvfBzw_vPOiFRvzfTtL0CEF1HZXKXerJVCUb-oFXE,1613
|
|
5
|
+
taskcompendium/grading.py,sha256=AL0uK07DN4UO8_sb3VOUOWgxkNVH8z6VYaeGfZN1jyY,5497
|
|
6
|
+
taskcompendium/grading_result.py,sha256=ZOc3_YsACp_IqnN2iVn9PMYQDh5jlFMWLJ7nKGul_q8,935
|
|
7
|
+
taskcompendium/models.py,sha256=UIqffR2yzu0VjB1ynND-7zmx3crYZaJsNI6NHt6eYSI,34910
|
|
8
|
+
taskcompendium/submission.py,sha256=l96cBGXc4qJysitVlUx4f6_VhMTuuMAnFWkEd0kLeZs,7539
|
|
9
|
+
taskcompendium/convert/__init__.py,sha256=AuRWrOcOqcm2s5NNLnBcPkluFLRcSxv9UnmesSeEaXM,146
|
|
10
|
+
taskcompendium/convert/answers.py,sha256=04G-n8VdVGeVntkr6D58hXF9imEG14vHSSnkweBe2XY,7965
|
|
11
|
+
taskcompendium/convert/code.py,sha256=XK1X24br7hZeo8UPQSARkEVOZcn6-lDgV5fZwcKKtxs,3846
|
|
12
|
+
taskcompendium/convert/conversation.py,sha256=55S2jGI52XFxGRQ_zPvyfP7O0YiH-HdSBov_8tQUgwM,1349
|
|
13
|
+
taskcompendium/convert/delivery.py,sha256=dLyrMXlC5ZH-dkDvT4QDtk0UmvKiIqCNv0EuSujrdyc,1418
|
|
14
|
+
taskcompendium/convert/environment.py,sha256=zKSh8aIzx4Y9CkyPVDhdpp2wRY8ngidZmsH00VI0nxU,618
|
|
15
|
+
taskcompendium/convert/executable.py,sha256=oRaBaMaHwkNKree_ZCxS8aVkAv6csgdbCBNH2-7e4WA,14309
|
|
16
|
+
taskcompendium/convert/json_schema.py,sha256=6jdI79YTVeX0jRAhFAEwtduR5J-63xIyJVk6TULVcSo,1332
|
|
17
|
+
taskcompendium/convert/nemotron_ultra.py,sha256=GKNEojj4aoh8GaGrurqwh4chZaIR0cMtDaz-pctPsbQ,8961
|
|
18
|
+
taskcompendium/convert/preference.py,sha256=QfqfjrX4EexIKtFfLCUMAZi0jAYSOBvlGWqeILzf3Bk,3942
|
|
19
|
+
taskcompendium/convert/script_grader.py,sha256=3uwzujkg3glbzVBXXl5KKHh_ZrQT48dB99iKEIZsVng,2420
|
|
20
|
+
taskcompendium/convert/tasktrove.py,sha256=zojE0PfC3Hd9EWpgLBeepB0cYZfvfIGL5rCPKmn8kLY,7537
|
|
21
|
+
taskcompendium/convert/tasktrove_code_contests.py,sha256=x8cXcpeWnbWSR28fgSVfywXeo4nci0nc3i0-vBKBMKo,3786
|
|
22
|
+
taskcompendium/convert/tasktrove_codeforces.py,sha256=1YH6x_LaVWFYoMEJ39x0aWzV8G5wG5KzvUOYlZHoFZc,4217
|
|
23
|
+
taskcompendium/convert/tasktrove_converted_task.py,sha256=Aq2v5xF-LTore-9y9HqCfZU_C0aspMLT5rIWmokeqD0,3542
|
|
24
|
+
taskcompendium/convert/tasktrove_json_schemas.py,sha256=eYgzfinpVg-TUY-XxD0ZHhdWyqJqrhtD0BwHmZA-JvA,5284
|
|
25
|
+
taskcompendium/convert/tasktrove_nemotron_data.py,sha256=FM3fJXSo3Z6HCM8tea7IxguoWergmo7XSIYS7T5VhQk,827
|
|
26
|
+
taskcompendium/convert/tasktrove_nemotron_structured_outputs.py,sha256=4Jm93oo1yeuZzvggyZU1cbxSUEtWsdz7SrerSNo0mE0,12239
|
|
27
|
+
taskcompendium/convert/tasktrove_nl2bash.py,sha256=Eevf7G96sPA2MUdA-xZ7yOMLs8LAMv5UWbFwQejK3D4,7028
|
|
28
|
+
taskcompendium/convert/tasktrove_python_unit_tests.py,sha256=b_tI3wfjz3vgORE0E4klemSYSQ3YpNNOv66Gxs8rZ7E,3841
|
|
29
|
+
taskcompendium/convert/tasktrove_stdio_cases.py,sha256=EF4u7t-4eSoE2A7uk3AJ7PhlWuH-F6qFaFWFhWt9-DY,4146
|
|
30
|
+
taskcompendium/convert/tasktrove_taco.py,sha256=e2ynxAbYBJZQO906fMJaGBy8tH9IDkaiQBZVP2rwvbY,3757
|
|
31
|
+
taskcompendium/convert/verifyit_build.py,sha256=WYxrr7ycjNsym1nAiN5RXVmowhMz_0J7QkLDXsF61DE,2817
|
|
32
|
+
taskcompendium/harbor/__init__.py,sha256=FGKy0KOvEejGv3g0Ov1LgJx1RHqPo8o8MYprTPahN8Q,118
|
|
33
|
+
taskcompendium/harbor/compare.py,sha256=CafzeTfgPSkin8BNziETdoQQk3CKd9ruyEUdkZh3-WE,10771
|
|
34
|
+
taskcompendium/harbor/export.py,sha256=j71GQtaSB9W9AAGK3gmbpd5nSsOaGm6XGTo1yOSwnkc,28192
|
|
35
|
+
taskcompendium/harbor/records.py,sha256=KFO8XTnvblnOLXwV09ut4AiH-YFn7ziTBoMc_xVJvwk,3328
|
|
36
|
+
taskcompendium/harbor/snapshots.py,sha256=C5mhctpFg4A6qkAkiMu_JdqKBk3oBXQ58yncyN29f5A,4011
|
|
37
|
+
taskcompendium/harbor/tasktrove.py,sha256=8SW2PGEFOhKxIbSyBic9lExj_RVv-sac9y1BHRXXi_4,3426
|
|
38
|
+
taskcompendium/importers/__init__.py,sha256=Sx3uJyho-XnsgGKm4f3oBceCu6N7NzB8lkY3-sH134Y,142
|
|
39
|
+
taskcompendium/importers/nemo_predicted_action.py,sha256=CEPAEQzAdt5KKiQBT9dmqHR41xdL9PetYebBow4nenA,9627
|
|
40
|
+
taskcompendium/importers/swe.py,sha256=KicdqOUbs8Y8Uz-VwclrpeOZm5Y4prVrZLwl7c4Wq4E,2730
|
|
41
|
+
taskcompendium/importers/tasktrove/__init__.py,sha256=zIJ4TMWYbzc3cs7aDWIAaw4RjB-ikKwek5GN3Smf7f0,128
|
|
42
|
+
taskcompendium/importers/tasktrove/convert.py,sha256=WN5nYZv1F1QzmyIFJW0con9wk4LU9IUHmHNzxSQ8B7s,3018
|
|
43
|
+
taskcompendium/importers/tasktrove/mcqa.py,sha256=nGXGLgDeBh33i8UfNkZsC3DQqdW7iofoWgXYgf_XMEo,3611
|
|
44
|
+
taskcompendium/importers/tasktrove/models.py,sha256=1BLxl8tn-Gjz_4QEx4sSowgAU2uKxXDfROxCS0tdLtg,755
|
|
45
|
+
taskcompendium/pipeline/README.md,sha256=yXoaVqf0jvdDoGkN44FKV7AW1C1F_zPcDeVHDfGe9wQ,13711
|
|
46
|
+
taskcompendium/pipeline/__init__.py,sha256=EVYEzJis0ke3RzUcOEYCC2O-am3_o5vdRMMkrDFzyVI,135
|
|
47
|
+
taskcompendium/pipeline/audit_schema.py,sha256=5ignj28PQr-oVsqu91icVsVil2W-pz02l7euCCBS674,4781
|
|
48
|
+
taskcompendium/pipeline/chat_requests.py,sha256=VUC3K_anr-gfOT_837KRI4TngETHOePeFxkPl-QhB_k,3875
|
|
49
|
+
taskcompendium/pipeline/controls.py,sha256=qr1dQgrYKkv8y3Dk17pGtyx_cJkEluXvPDjvSh1t22I,10897
|
|
50
|
+
taskcompendium/pipeline/conversion.py,sha256=Md2sMl49Y65fIeGwiOG8xTsWd2l_mPOOQqDL-ILMT4M,5920
|
|
51
|
+
taskcompendium/pipeline/environment_inventory.py,sha256=67kkb0KxD8bFu5tv3DbPaUTt-UDLz-nfwLToyr9yRDk,1879
|
|
52
|
+
taskcompendium/pipeline/execution_telemetry.py,sha256=bD5Kf_jgqKlz_qC0i2PdAlT4t9wj8c1REbAtHCboj58,4651
|
|
53
|
+
taskcompendium/pipeline/filtering.py,sha256=WKIK_7MXHdjpkc4LMawRhjkfsmzXtYcQlqDcnz5ylU0,1817
|
|
54
|
+
taskcompendium/pipeline/fingerprints.py,sha256=4kLN5XYuK8imZrrF7ZrdIdJyfrpOPVsNKBUh6DtJCWg,4531
|
|
55
|
+
taskcompendium/pipeline/inputs.py,sha256=136QHYa4ZWU01uDArHUoVLfnRsu2Ifrw6Hf1mZ0eDgE,3625
|
|
56
|
+
taskcompendium/pipeline/models.py,sha256=vm2HIZZhhWJSCqXIkuP6CjCaHnjq3IboLNV5NP507ZM,9205
|
|
57
|
+
taskcompendium/pipeline/query_cache.py,sha256=1hld3ddGs60KWpHPuXSLDOEE0BtDv6-cbPmmrWjFgAk,9633
|
|
58
|
+
taskcompendium/pipeline/review.py,sha256=eRB2pRf-HnI9d90yPyCxUkXCMIXh0CUBOg0HaBa-9jY,34450
|
|
59
|
+
taskcompendium/pipeline/review_requests.py,sha256=P9x6k5ZiLe5hiht5Dpob2_CHMnzTO3D3GGuI8MLu-4U,10359
|
|
60
|
+
taskcompendium/pipeline/sampling.py,sha256=Zxai94IJctH6QrpeCFzBYJoISkRHXBM0FPXrfy53G7U,1416
|
|
61
|
+
taskcompendium/pipeline/shard_outputs.py,sha256=Iov0RWinu0MVG35KZfUIyATCUY5oM3U_UZR7dfG1wTY,2205
|
|
62
|
+
taskcompendium/pipeline/source_processing.py,sha256=PLDKMuHrBnVR7NQfYIOnZnc-UKw4Ta-4wAM_DxEttQo,43793
|
|
63
|
+
taskcompendium/pipeline/source_quality.py,sha256=QAtdeLt22wfXySIWug0vURwTH_VAhWI_8OhITEwlv68,11553
|
|
64
|
+
taskcompendium/pipeline/source_verification.py,sha256=YLNADkr47D8dZ17qSrY72_axIJ56mR-7YeQ46EhQFqc,25644
|
|
65
|
+
taskcompendium/pipeline/sources.py,sha256=irN4Rv66yOGAGmwEQb-1g09N9R2FybWaGhgPgHS2OvU,9026
|
|
66
|
+
taskcompendium/pipeline/stages.py,sha256=nQXzG1OjWpYRExjad5B5h9ykBXKyT2w-gi9O4h8MHhA,34348
|
|
67
|
+
taskcompendium/pipeline/transforms.py,sha256=KbnvSu6zfU6lM4qxe3V83adGztH-Y5smO8O2zbWF7lw,8334
|
|
68
|
+
taskcompendium/pipeline/verification.py,sha256=UgfxdopckCkfcgXTJdo-c6qub09UFvzN4WIOFssOyqI,8480
|
|
69
|
+
taskcompendium/runtime/__init__.py,sha256=o67I1ai4r_ZEZSVdlrHdxWzabhAXKCw5N_IqYCqnj38,229
|
|
70
|
+
taskcompendium/runtime/environment.py,sha256=ts3uM0zp5DSqevXKiNneyD3-wdTaEorRTyA9He7O0Io,951
|
|
71
|
+
taskcompendium/runtime/episode.py,sha256=iBO6-rtNbELTIVzqgCjHW9QUqqgmGy7hSQJ9NLH3stI,3452
|
|
72
|
+
taskcompendium/runtime/grading.py,sha256=Adoc1qrIB86PuIaWfvRKNIQ4ssG8KkcGsdviPHBD4qc,28257
|
|
73
|
+
taskcompendium/runtime/local.py,sha256=UMJ2sVxHMIHAjzV3n7WXA-H9AdqQ1-XVBUKGhQ6V3Sw,8518
|
|
74
|
+
taskcompendium/runtime/models.py,sha256=4PeIzEMuNuwJgXpdHqGTqXjqwq4WiCFM3JSyMjYyduE,2258
|
|
75
|
+
taskcompendium/runtime/output_capture.py,sha256=nsc60DTTrpZVdr7hqo5xtZPopxZXtEUS_4xOAC527PQ,5314
|
|
76
|
+
taskcompendium/runtime/resources.py,sha256=-LgVikv0pjBLpu1y_BXEvCJC2trGqaEJgzGkcRqQDYk,618
|
|
77
|
+
taskcompendium/runtime/shell.py,sha256=gMUBDBz9Whnr4qac62sdXxG13xIHIDdmIib_Z05esw8,11696
|
|
78
|
+
taskcompendium/runtime/task_grading.py,sha256=GJKxkVShu2ZmZbKYhDt7b9zsl5HS4tMGJT_J40EPNLw,2252
|
|
79
|
+
marin_taskcompendium-0.2.160.dev38062449553.dist-info/METADATA,sha256=1bBUIOzI8lGveU4g7_DUz-oZaWR9sUQ1nwB0i68nz6s,770
|
|
80
|
+
marin_taskcompendium-0.2.160.dev38062449553.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
81
|
+
marin_taskcompendium-0.2.160.dev38062449553.dist-info/RECORD,,
|
taskcompendium/chat.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Copyright The Marin Authors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Convert OpenAI chat messages to typed conversation evidence."""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, ConfigDict, Field, JsonValue, field_validator
|
|
10
|
+
from verifyit.json_objects import unique_object
|
|
11
|
+
|
|
12
|
+
from taskcompendium.models import (
|
|
13
|
+
AssistantToolCalls,
|
|
14
|
+
ConversationEvent,
|
|
15
|
+
ConversationInput,
|
|
16
|
+
ConversationToolCall,
|
|
17
|
+
ConversationTrace,
|
|
18
|
+
TextMessage,
|
|
19
|
+
ToolResult,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ChatFunction(BaseModel):
|
|
24
|
+
model_config = ConfigDict(strict=True, allow_inf_nan=False)
|
|
25
|
+
|
|
26
|
+
name: str = Field(min_length=1)
|
|
27
|
+
arguments: dict[str, JsonValue]
|
|
28
|
+
|
|
29
|
+
@field_validator("arguments", mode="before")
|
|
30
|
+
@classmethod
|
|
31
|
+
def decode_arguments(cls, value: str) -> dict[str, JsonValue]:
|
|
32
|
+
if not isinstance(value, str):
|
|
33
|
+
raise ValueError("Provider tool arguments must be a JSON string")
|
|
34
|
+
return json.loads(value, object_pairs_hook=unique_object)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ChatToolCall(BaseModel):
|
|
38
|
+
model_config = ConfigDict(strict=True)
|
|
39
|
+
|
|
40
|
+
id: str = Field(min_length=1)
|
|
41
|
+
type: Literal["function"]
|
|
42
|
+
function: ChatFunction
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ChatAssistantMessage(BaseModel):
|
|
46
|
+
model_config = ConfigDict(strict=True)
|
|
47
|
+
|
|
48
|
+
role: Literal["assistant"]
|
|
49
|
+
content: str | None = None
|
|
50
|
+
tool_calls: list[ChatToolCall] | None = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def assistant_message(message: dict[str, Any]) -> TextMessage | AssistantToolCalls:
|
|
54
|
+
"""Validate chat wire data and return protocol-independent submission evidence.
|
|
55
|
+
|
|
56
|
+
Malformed protocol data raises at this ingestion boundary. A valid text reply,
|
|
57
|
+
wrong function name, or wrong arguments remain available for grading.
|
|
58
|
+
"""
|
|
59
|
+
validated = ChatAssistantMessage.model_validate(message)
|
|
60
|
+
if validated.tool_calls:
|
|
61
|
+
return AssistantToolCalls(
|
|
62
|
+
calls=tuple(
|
|
63
|
+
ConversationToolCall(call_id=call.id, name=call.function.name, arguments=call.function.arguments)
|
|
64
|
+
for call in validated.tool_calls
|
|
65
|
+
),
|
|
66
|
+
content=validated.content,
|
|
67
|
+
)
|
|
68
|
+
if validated.content is None:
|
|
69
|
+
raise ValueError("Chat response requires assistant content or function calls")
|
|
70
|
+
return TextMessage(role="assistant", content=validated.content)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _conversation_events(messages: list[dict[str, Any]]) -> tuple[ConversationEvent, ...]:
|
|
74
|
+
events: list[ConversationEvent] = []
|
|
75
|
+
for message in messages:
|
|
76
|
+
role = message.get("role")
|
|
77
|
+
if role == "assistant":
|
|
78
|
+
events.append(assistant_message(message))
|
|
79
|
+
elif role == "tool":
|
|
80
|
+
events.append(ToolResult(call_id=message["tool_call_id"], content=message["content"]))
|
|
81
|
+
else:
|
|
82
|
+
events.append(TextMessage.model_validate(message))
|
|
83
|
+
return tuple(events)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def chat_input(messages: list[dict[str, Any]]) -> ConversationInput:
|
|
87
|
+
"""Normalize a public task prefix before model inference."""
|
|
88
|
+
return ConversationInput(events=_conversation_events(messages))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def chat_conversation(messages: list[dict[str, Any]]) -> ConversationTrace:
|
|
92
|
+
"""Normalize a complete chat transcript for a TaskCompendium verifier."""
|
|
93
|
+
return ConversationTrace(events=_conversation_events(messages))
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# Copyright The Marin Authors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Conversation tasks whose final answer a verifyit mode grades in process.
|
|
5
|
+
|
|
6
|
+
Each helper takes the public prompt and reference values that a declaration extracts from its row,
|
|
7
|
+
checks that they are usable, and returns a task with its grader set, or a rejection naming the
|
|
8
|
+
defect. ``evidence`` is source material kept with the grader for review, never shown to the solver.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from collections.abc import Mapping
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from verifyit.modes.extract import extract_boxed
|
|
16
|
+
from verifyit.numeric import numeric_literal
|
|
17
|
+
from verifyit.spec import (
|
|
18
|
+
Constraint,
|
|
19
|
+
ExactSpec,
|
|
20
|
+
IfevalSpec,
|
|
21
|
+
JsonSchemaSpec,
|
|
22
|
+
MathSpec,
|
|
23
|
+
MathType,
|
|
24
|
+
McqSpec,
|
|
25
|
+
NumericSpec,
|
|
26
|
+
SchemaFormat,
|
|
27
|
+
Spec,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from taskcompendium.grader import verifyit_package
|
|
31
|
+
from taskcompendium.models import (
|
|
32
|
+
AnswerType,
|
|
33
|
+
ConversationInput,
|
|
34
|
+
EnvironmentRequirements,
|
|
35
|
+
PlainText,
|
|
36
|
+
ResourceGroups,
|
|
37
|
+
TaskResource,
|
|
38
|
+
TaskSpec,
|
|
39
|
+
TextMessage,
|
|
40
|
+
)
|
|
41
|
+
from taskcompendium.pipeline.models import ImportFailureKind, ImportRejection, RawRow
|
|
42
|
+
from taskcompendium.runtime.resources import inline_resource
|
|
43
|
+
|
|
44
|
+
EVIDENCE_PATH = "reference/source-evidence.json"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def source_defect(reason: str, detail: str) -> ImportRejection:
|
|
48
|
+
return ImportRejection(kind=ImportFailureKind.SOURCE_DEFECT, reason=reason, detail=detail)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def unsupported(reason: str, detail: str) -> ImportRejection:
|
|
52
|
+
return ImportRejection(kind=ImportFailureKind.UNSUPPORTED, reason=reason, detail=detail)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _text(value: Any) -> str | None:
|
|
56
|
+
return value.strip() if isinstance(value, str) and value.strip() else None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def evidence_resource(evidence: Mapping[str, Any]) -> TaskResource:
|
|
60
|
+
"""Source fields retained beside the grader for review and audit."""
|
|
61
|
+
return inline_resource(EVIDENCE_PATH, json.dumps(dict(evidence), ensure_ascii=False, sort_keys=True).encode())
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def answer_task(
|
|
65
|
+
row: RawRow,
|
|
66
|
+
*,
|
|
67
|
+
prompt: str,
|
|
68
|
+
spec: Spec,
|
|
69
|
+
answer_type: AnswerType = AnswerType.TEXT,
|
|
70
|
+
evidence: Mapping[str, Any] | None = None,
|
|
71
|
+
resources: tuple[TaskResource, ...] = (),
|
|
72
|
+
) -> TaskSpec:
|
|
73
|
+
"""A one-message conversation task graded in process by ``spec``."""
|
|
74
|
+
verifier = (*resources, *((evidence_resource(evidence),) if evidence else ()))
|
|
75
|
+
package = verifyit_package(spec, verifier)
|
|
76
|
+
return TaskSpec(
|
|
77
|
+
id=row.id,
|
|
78
|
+
source=row.source,
|
|
79
|
+
context=ConversationInput(events=(TextMessage(role="user", content=prompt),)),
|
|
80
|
+
environment_requirements=EnvironmentRequirements(),
|
|
81
|
+
resources=ResourceGroups(verifier=package.resources),
|
|
82
|
+
answer_type=answer_type,
|
|
83
|
+
answer_format=PlainText(),
|
|
84
|
+
grader=package.grader,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def math_type(expected: str) -> MathType:
|
|
89
|
+
"""Keep structured mathematical answers distinct from scalar values."""
|
|
90
|
+
if expected.startswith("[") and expected.endswith("]"):
|
|
91
|
+
return MathType.LIST
|
|
92
|
+
if expected.startswith(("[", "(")) and expected.endswith(("]", ")")) and "," in expected:
|
|
93
|
+
return MathType.TUPLE if expected.startswith("(") and expected.endswith(")") else MathType.INTERVAL
|
|
94
|
+
if expected.startswith((r"\{", "{")):
|
|
95
|
+
return MathType.SET
|
|
96
|
+
if "=" in expected or r"\approx" in expected:
|
|
97
|
+
return MathType.EQUATION
|
|
98
|
+
return MathType.SCALAR
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def math_answer_task(
|
|
102
|
+
row: RawRow, *, prompt: Any, answer: Any, evidence: Mapping[str, Any] | None = None
|
|
103
|
+
) -> TaskSpec | ImportRejection:
|
|
104
|
+
"""Grade a symbolic math answer; a boxed reference is unwrapped."""
|
|
105
|
+
problem, reference = _text(prompt), _text(answer)
|
|
106
|
+
if problem is None:
|
|
107
|
+
return source_defect("missing_prompt", "The problem must be a nonempty string")
|
|
108
|
+
if reference is None:
|
|
109
|
+
return source_defect("invalid_reference", "The reference answer must be a nonempty string")
|
|
110
|
+
expected = extract_boxed(reference) or reference
|
|
111
|
+
return answer_task(
|
|
112
|
+
row, prompt=problem, spec=MathSpec(expected=expected, math_type=math_type(expected)), evidence=evidence
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def numeric_answer_task(
|
|
117
|
+
row: RawRow,
|
|
118
|
+
*,
|
|
119
|
+
prompt: Any,
|
|
120
|
+
answer: Any,
|
|
121
|
+
tolerance_abs: float,
|
|
122
|
+
tolerance_rel: float,
|
|
123
|
+
evidence: Mapping[str, Any] | None = None,
|
|
124
|
+
) -> TaskSpec | ImportRejection:
|
|
125
|
+
"""Grade a numeric literal within absolute and relative tolerances."""
|
|
126
|
+
problem = _text(prompt)
|
|
127
|
+
if problem is None:
|
|
128
|
+
return source_defect("missing_prompt", "The problem must be a nonempty string")
|
|
129
|
+
reference = answer.strip() if isinstance(answer, str) else str(answer) if isinstance(answer, int | float) else None
|
|
130
|
+
try:
|
|
131
|
+
if reference is None:
|
|
132
|
+
raise ValueError("missing")
|
|
133
|
+
numeric_literal(reference)
|
|
134
|
+
except ValueError:
|
|
135
|
+
return source_defect("invalid_reference", f"The reference answer is not numeric: {answer!r}")
|
|
136
|
+
return answer_task(
|
|
137
|
+
row,
|
|
138
|
+
prompt=problem,
|
|
139
|
+
spec=NumericSpec(reference, tolerance_abs=tolerance_abs, tolerance_rel=tolerance_rel),
|
|
140
|
+
answer_type=AnswerType.NUMBER,
|
|
141
|
+
evidence=evidence,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def mcq_task(
|
|
146
|
+
row: RawRow, *, prompt: Any, answer: Any, options: int, evidence: Mapping[str, Any] | None = None
|
|
147
|
+
) -> TaskSpec | ImportRejection:
|
|
148
|
+
"""Grade an option letter; ``prompt`` already lists the ``options`` labeled choices."""
|
|
149
|
+
question, letter = _text(prompt), _text(answer)
|
|
150
|
+
if question is None:
|
|
151
|
+
return source_defect("missing_prompt", "The question must be a nonempty string")
|
|
152
|
+
if letter is None or len(letter) != 1 or not "A" <= letter.upper() < chr(65 + options):
|
|
153
|
+
return source_defect("invalid_reference", f"The key must be one of {options} option letters: {answer!r}")
|
|
154
|
+
return answer_task(row, prompt=question, spec=McqSpec(letter.upper(), options=options), evidence=evidence)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def exact_answer_task(
|
|
158
|
+
row: RawRow,
|
|
159
|
+
*,
|
|
160
|
+
prompt: Any,
|
|
161
|
+
answers: tuple[str, ...],
|
|
162
|
+
ignore_case: bool,
|
|
163
|
+
evidence: Mapping[str, Any] | None = None,
|
|
164
|
+
) -> TaskSpec | ImportRejection:
|
|
165
|
+
"""Grade an answer equal to one of ``answers`` after whitespace normalization."""
|
|
166
|
+
question = _text(prompt)
|
|
167
|
+
if question is None:
|
|
168
|
+
return source_defect("missing_prompt", "The question must be a nonempty string")
|
|
169
|
+
if not answers or not all(_text(item) for item in answers):
|
|
170
|
+
return source_defect("invalid_reference", "Accepted answers must be nonempty strings")
|
|
171
|
+
return answer_task(row, prompt=question, spec=ExactSpec(tuple(answers), ignore_case=ignore_case), evidence=evidence)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def ifeval_task(
|
|
175
|
+
row: RawRow,
|
|
176
|
+
*,
|
|
177
|
+
prompt: Any,
|
|
178
|
+
constraints: tuple[Constraint, ...],
|
|
179
|
+
evidence: Mapping[str, Any] | None = None,
|
|
180
|
+
) -> TaskSpec | ImportRejection:
|
|
181
|
+
"""Grade the fraction of instruction-following constraints the reply satisfies."""
|
|
182
|
+
instruction = _text(prompt)
|
|
183
|
+
if instruction is None:
|
|
184
|
+
return source_defect("missing_prompt", "The instruction must be a nonempty string")
|
|
185
|
+
if not constraints:
|
|
186
|
+
return unsupported("invalid_constraints", "At least one constraint is required")
|
|
187
|
+
return answer_task(row, prompt=instruction, spec=IfevalSpec(constraints=constraints), evidence=evidence)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def json_schema_task(
|
|
191
|
+
row: RawRow,
|
|
192
|
+
*,
|
|
193
|
+
prompt: Any,
|
|
194
|
+
schema: Any,
|
|
195
|
+
schema_format: SchemaFormat,
|
|
196
|
+
evidence: Mapping[str, Any] | None = None,
|
|
197
|
+
) -> TaskSpec | ImportRejection:
|
|
198
|
+
"""Grade a reply against ``schema`` (serialized schema text), shipped as the grader's ``schema.json``."""
|
|
199
|
+
instruction, document = _text(prompt), _text(schema)
|
|
200
|
+
if instruction is None:
|
|
201
|
+
return source_defect("missing_prompt", "The instruction must be a nonempty string")
|
|
202
|
+
if document is None:
|
|
203
|
+
return source_defect("invalid_schema", "The schema must be nonempty text")
|
|
204
|
+
return answer_task(
|
|
205
|
+
row,
|
|
206
|
+
prompt=instruction,
|
|
207
|
+
spec=JsonSchemaSpec(schema="schema.json", format=schema_format),
|
|
208
|
+
evidence=evidence,
|
|
209
|
+
resources=(inline_resource("schema.json", document.encode()),),
|
|
210
|
+
)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Copyright The Marin Authors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Checks and grading settings for code tasks whose hidden tests use the LiveCodeBench test-case layout."""
|
|
5
|
+
|
|
6
|
+
import ast
|
|
7
|
+
import re
|
|
8
|
+
from collections.abc import Mapping
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
CODE_GRADER_MEMORY_MB = 5120
|
|
12
|
+
"""The LiveCodeBench child process may use 4 GiB; the grading machine also needs room for its runtime."""
|
|
13
|
+
THREAD_ENVIRONMENT = {"OPENBLAS_NUM_THREADS": "1", "OMP_NUM_THREADS": "1"}
|
|
14
|
+
"""Run each test's numeric libraries on one thread."""
|
|
15
|
+
CODE_BLOCK = re.compile(r"```(?:\w+)?\n(.*?)```", re.DOTALL)
|
|
16
|
+
"""A fenced code block as the source scorers match one; they run the last block in a reply."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def validate_code_cases(cases: Mapping[str, Any] | list[Any]) -> None:
|
|
20
|
+
"""Reject decoded test layouts the LiveCodeBench scorer cannot normalize."""
|
|
21
|
+
if isinstance(cases, Mapping) and "test_cases" in cases:
|
|
22
|
+
if cases.get("language") not in (None, "python"):
|
|
23
|
+
raise ValueError("LCB supports only Python verification specs")
|
|
24
|
+
cases = cases["test_cases"]
|
|
25
|
+
elif isinstance(cases, Mapping):
|
|
26
|
+
inputs, outputs = cases.get("inputs"), cases.get("outputs")
|
|
27
|
+
if not isinstance(inputs, list) or not isinstance(outputs, list) or not inputs or len(inputs) != len(outputs):
|
|
28
|
+
raise ValueError("LCB requires aligned nonempty input and output lists")
|
|
29
|
+
kind = "functional" if cases.get("fn_name") is not None else "stdin"
|
|
30
|
+
cases = [
|
|
31
|
+
{"input": item, "output": result, "testtype": kind, "fn_name": cases.get("fn_name")}
|
|
32
|
+
for item, result in zip(inputs, outputs, strict=True)
|
|
33
|
+
]
|
|
34
|
+
if not isinstance(cases, list) or not cases or not all(isinstance(case, Mapping) for case in cases):
|
|
35
|
+
raise ValueError("LCB requires a nonempty list of test cases")
|
|
36
|
+
modes = set()
|
|
37
|
+
names = set()
|
|
38
|
+
for case in cases:
|
|
39
|
+
kind = case.get("testtype", case.get("type"))
|
|
40
|
+
if kind in ("stdin", "stdin_stdout"):
|
|
41
|
+
if not isinstance(case.get("input"), str) or not isinstance(case.get("output"), str):
|
|
42
|
+
raise ValueError("Standard-input LCB cases require string input and output")
|
|
43
|
+
modes.add("stdin")
|
|
44
|
+
elif kind in ("functional", "call_based"):
|
|
45
|
+
metadata = case.get("metadata")
|
|
46
|
+
name = case.get("fn_name") or (metadata.get("func_name") if isinstance(metadata, Mapping) else None)
|
|
47
|
+
if not isinstance(name, str) or not name:
|
|
48
|
+
raise ValueError("Functional LCB cases require a function name")
|
|
49
|
+
if not isinstance(case.get("input"), str | list):
|
|
50
|
+
raise ValueError("Functional LCB input must be a list or encoded string")
|
|
51
|
+
modes.add("functional")
|
|
52
|
+
names.add(name)
|
|
53
|
+
else:
|
|
54
|
+
raise ValueError("Unsupported LCB test-case type")
|
|
55
|
+
if len(modes) != 1 or len(names) > 1:
|
|
56
|
+
raise ValueError("LCB cases must share one execution mode and function name")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def has_code_block(response: str) -> bool:
|
|
60
|
+
"""Match a fenced code block the way the source scorers extract one."""
|
|
61
|
+
return CODE_BLOCK.search(response) is not None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def python_reply(solution: Any) -> str | None:
|
|
65
|
+
"""A source solution as a reply the scorers extract code from.
|
|
66
|
+
|
|
67
|
+
Returns ``None`` when there is no solution or when the program the scorers would run is not
|
|
68
|
+
Python 3. Some sources keep Python 2 solutions; the graders run Python 3, so such a solution
|
|
69
|
+
cannot serve as a known-correct submission.
|
|
70
|
+
"""
|
|
71
|
+
if not isinstance(solution, str) or not solution.strip():
|
|
72
|
+
return None
|
|
73
|
+
reply = solution if has_code_block(solution) else f"```python\n{solution}\n```"
|
|
74
|
+
try:
|
|
75
|
+
ast.parse(CODE_BLOCK.findall(reply)[-1].strip())
|
|
76
|
+
except SyntaxError: # Includes TabError from Python 2 tab indentation.
|
|
77
|
+
return None
|
|
78
|
+
return reply
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Copyright The Marin Authors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Conversation tasks whose final reply a packaged grader scores."""
|
|
5
|
+
|
|
6
|
+
from collections.abc import Mapping, Sequence
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from taskcompendium.convert.answers import evidence_resource
|
|
10
|
+
from taskcompendium.grader import GraderPackage
|
|
11
|
+
from taskcompendium.models import (
|
|
12
|
+
AnswerType,
|
|
13
|
+
ConversationEvent,
|
|
14
|
+
ConversationInput,
|
|
15
|
+
EnvironmentRequirements,
|
|
16
|
+
PlainText,
|
|
17
|
+
ResourceGroups,
|
|
18
|
+
TaskSpec,
|
|
19
|
+
)
|
|
20
|
+
from taskcompendium.pipeline.models import RawRow
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def conversation_task(
|
|
24
|
+
row: RawRow,
|
|
25
|
+
*,
|
|
26
|
+
events: Sequence[ConversationEvent],
|
|
27
|
+
package: GraderPackage,
|
|
28
|
+
evidence: Mapping[str, Any] | None = None,
|
|
29
|
+
) -> TaskSpec:
|
|
30
|
+
"""A task whose plain-text reply to ``events`` the packaged grader scores.
|
|
31
|
+
|
|
32
|
+
``evidence`` is source material kept beside the grader for review, never shown to the solver.
|
|
33
|
+
"""
|
|
34
|
+
verifier = (*package.resources, *((evidence_resource(evidence),) if evidence else ()))
|
|
35
|
+
return TaskSpec(
|
|
36
|
+
id=row.id,
|
|
37
|
+
source=row.source,
|
|
38
|
+
context=ConversationInput(events=tuple(events)),
|
|
39
|
+
environment_requirements=EnvironmentRequirements(),
|
|
40
|
+
resources=ResourceGroups(verifier=verifier),
|
|
41
|
+
answer_type=AnswerType.TEXT,
|
|
42
|
+
answer_format=PlainText(),
|
|
43
|
+
grader=package.grader,
|
|
44
|
+
)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Copyright The Marin Authors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Move a source's file-delivery wording into the assistant reply and record the rewrite.
|
|
5
|
+
|
|
6
|
+
Many sources were written for a shell agent that writes its answer to a file. A conversation task
|
|
7
|
+
asks for the same answer in its reply instead; the rewrite is kept as a normalization change, so the
|
|
8
|
+
audit retains the source's original wording.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from collections.abc import Sequence
|
|
12
|
+
|
|
13
|
+
from taskcompendium.models import TaskSpec, TextMessage
|
|
14
|
+
from taskcompendium.pipeline.models import NormalizationChange, NormalizedTask
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def replace_phrases(text: str, phrases: Sequence[tuple[str, str]]) -> str:
|
|
18
|
+
"""Apply each ``(original, replacement)`` rewrite in order."""
|
|
19
|
+
for original, replacement in phrases:
|
|
20
|
+
text = text.replace(original, replacement)
|
|
21
|
+
return text
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def rewritten_task(task: TaskSpec, *, original: str, reason: str) -> TaskSpec | NormalizedTask:
|
|
25
|
+
"""``task``, with a change record when its first message differs from the source's ``original``.
|
|
26
|
+
|
|
27
|
+
Stripping outer whitespace alone is not recorded as a change.
|
|
28
|
+
"""
|
|
29
|
+
prompt = task.context.events[0]
|
|
30
|
+
assert isinstance(prompt, TextMessage)
|
|
31
|
+
if prompt.content.strip() == original.strip():
|
|
32
|
+
return task
|
|
33
|
+
change = NormalizationChange(field="instruction", reason=reason, original=original, replacement=prompt.content)
|
|
34
|
+
return NormalizedTask(task, (change,))
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Copyright The Marin Authors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Environment requirements for graders that run an image in a fresh machine."""
|
|
5
|
+
|
|
6
|
+
from shellbox.machine import Backend
|
|
7
|
+
|
|
8
|
+
from taskcompendium.models import EnvironmentRequirements
|
|
9
|
+
|
|
10
|
+
IMAGE_BACKENDS = (Backend.GVISOR, Backend.DOCKER)
|
|
11
|
+
"""Backends that start a fresh machine from a digest-pinned image."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def grading_environment(image: str) -> EnvironmentRequirements:
|
|
15
|
+
"""A grader environment that runs the digest-pinned ``image`` under gVisor or Docker."""
|
|
16
|
+
return EnvironmentRequirements(docker_image=image, compatible_backends=IMAGE_BACKENDS)
|