uenv-sdk 0.1.0__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.
- uenv/sdk/__init__.py +413 -0
- uenv/sdk/artifact_io.py +26 -0
- uenv/sdk/component_registry.py +339 -0
- uenv/sdk/content.py +57 -0
- uenv/sdk/data_preparation.py +239 -0
- uenv/sdk/data_publication.py +460 -0
- uenv/sdk/errors.py +12 -0
- uenv/sdk/hub.py +610 -0
- uenv/sdk/modeling.py +206 -0
- uenv/sdk/package_loader.py +518 -0
- uenv/sdk/package_release.py +689 -0
- uenv/sdk/public_types.py +321 -0
- uenv/sdk/py.typed +0 -0
- uenv/sdk/resources/backends.json +50 -0
- uenv/sdk/resources/extensions/EmptyConfig.schema.json +9 -0
- uenv/sdk/resources/extensions/EvaluationPlan.schema.json +55 -0
- uenv/sdk/resources/extensions/PlainAgentConfig.schema.json +27 -0
- uenv/sdk/resources/extensions/ReadFileArgs.schema.json +16 -0
- uenv/sdk/resources/extensions/TerminalArgs.schema.json +25 -0
- uenv/sdk/resources/extensions/ToolConfig.schema.json +25 -0
- uenv/sdk/resources/extensions/WriteFileArgs.schema.json +21 -0
- uenv/sdk/resources/uenv.schema.json +5674 -0
- uenv/sdk/run_config.py +95 -0
- uenv/sdk/runtime_resources.py +50 -0
- uenv/sdk/schema_registry.py +178 -0
- uenv/sdk/service_auth.py +65 -0
- uenv/sdk/source_files.py +93 -0
- uenv/sdk/source_preparation.py +160 -0
- uenv/sdk/tls.py +31 -0
- uenv/sdk/tools.py +150 -0
- uenv/sdk/training_records.py +23 -0
- uenv/sdk/trajectory.py +41 -0
- uenv/sdk/workspace.py +221 -0
- uenv/v1/__init__.py +1 -0
- uenv/v1/annotations_pb2.py +35 -0
- uenv/v1/annotations_pb2_grpc.py +24 -0
- uenv/v1/common_pb2.py +187 -0
- uenv/v1/common_pb2_grpc.py +24 -0
- uenv/v1/execution_pb2.py +315 -0
- uenv/v1/execution_pb2_grpc.py +24 -0
- uenv/v1/hub_pb2.py +325 -0
- uenv/v1/hub_pb2_grpc.py +24 -0
- uenv/v1/interaction_pb2.py +166 -0
- uenv/v1/interaction_pb2_grpc.py +24 -0
- uenv/v1/management_pb2.py +381 -0
- uenv/v1/management_pb2_grpc.py +441 -0
- uenv/v1/package_pb2.py +119 -0
- uenv/v1/package_pb2_grpc.py +24 -0
- uenv/v1/run_pb2.py +197 -0
- uenv/v1/run_pb2_grpc.py +24 -0
- uenv/v1/scoring_pb2.py +82 -0
- uenv/v1/scoring_pb2_grpc.py +24 -0
- uenv/v1/services_pb2.py +182 -0
- uenv/v1/services_pb2_grpc.py +972 -0
- uenv/v1/task_pb2.py +78 -0
- uenv/v1/task_pb2_grpc.py +24 -0
- uenv/v1/trajectory_pb2.py +107 -0
- uenv/v1/trajectory_pb2_grpc.py +24 -0
- uenv_sdk-0.1.0.dist-info/METADATA +20 -0
- uenv_sdk-0.1.0.dist-info/RECORD +62 -0
- uenv_sdk-0.1.0.dist-info/WHEEL +5 -0
- uenv_sdk-0.1.0.dist-info/top_level.txt +1 -0
uenv/sdk/__init__.py
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
"""Unified Python component SDK; Worker owns process and resource isolation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from copy import deepcopy
|
|
6
|
+
from dataclasses import dataclass, field, fields
|
|
7
|
+
from typing import BinaryIO, Callable, Iterable
|
|
8
|
+
import json
|
|
9
|
+
import hashlib
|
|
10
|
+
import warnings
|
|
11
|
+
from os import PathLike
|
|
12
|
+
from .modeling import (
|
|
13
|
+
ArtifactRef,
|
|
14
|
+
ContentPart,
|
|
15
|
+
EvaluationPlan,
|
|
16
|
+
Field,
|
|
17
|
+
UEnvModel,
|
|
18
|
+
bind_schema,
|
|
19
|
+
model_from_envelope,
|
|
20
|
+
model_json_schema,
|
|
21
|
+
model_to_envelope,
|
|
22
|
+
)
|
|
23
|
+
from .schema_registry import SchemaRegistry
|
|
24
|
+
from .workspace import ArtifactStore, CommandResult, RemoteWorkspace, Workspace
|
|
25
|
+
from .public_types import (
|
|
26
|
+
ArtifactRef as ArtifactReference, ContentPart as ContentItem, DatasetRef,
|
|
27
|
+
RuntimeSpec, TrajectoryEvent, GenerationEvent, Metric, ScoreResultGenerationRewardsItem as GenerationReward,
|
|
28
|
+
)
|
|
29
|
+
from .content import normalize_content
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def to_wire(value):
|
|
33
|
+
if isinstance(value, UEnvModel):
|
|
34
|
+
return model_to_envelope(value)
|
|
35
|
+
if hasattr(value, "__dataclass_fields__"):
|
|
36
|
+
return {
|
|
37
|
+
f.name: to_wire(getattr(value, f.name))
|
|
38
|
+
for f in fields(value)
|
|
39
|
+
if getattr(value, f.name) is not None or f.name in {"success", "reward"}
|
|
40
|
+
}
|
|
41
|
+
if isinstance(value, dict):
|
|
42
|
+
return {k: to_wire(v) for k, v in value.items()}
|
|
43
|
+
if isinstance(value, (tuple, list)):
|
|
44
|
+
return [to_wire(v) for v in value]
|
|
45
|
+
return value
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class PreparedSample:
|
|
50
|
+
sample_id: str
|
|
51
|
+
input: UEnvModel
|
|
52
|
+
private_data: UEnvModel | None = None
|
|
53
|
+
runtime: RuntimeSpec | None = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class TaskSpec:
|
|
58
|
+
"""Typed component view; transport-only digest and envelope stay hidden."""
|
|
59
|
+
|
|
60
|
+
# 这是数据集作者在 reset(task, seed=...) 中收到的任务对象,不需要作者自行构造。
|
|
61
|
+
# Adapter 先产生 PreparedSample,SDK 组装传输数据,Worker 校验后还原成本对象。
|
|
62
|
+
# 把它当作只读任务输入使用;标准答案、隐藏测试等 private_data 不在这里。
|
|
63
|
+
# 最常用的是 task.input;其具体类型由本数据集 models.py 的公开输入模型决定。
|
|
64
|
+
|
|
65
|
+
# 数据来源标识:id(数据集 ID)、revision(固定数据版本)、split(划分)、
|
|
66
|
+
# subset(子集,未指定时为空字符串)。由准备流程传入,不在 reset 中重新选择。
|
|
67
|
+
dataset: DatasetRef
|
|
68
|
+
# Adapter 提供的样本编号,用于定位该数据版本中的一条题目。
|
|
69
|
+
sample_id: str
|
|
70
|
+
# Adapter 填写的公开输入。例:GSM8K 为 Gsm8kInput,SWE-Pro 为 SweProInput。
|
|
71
|
+
# 使用 task.input.instruction 等模型属性,不需要手动解开传输层的 schema_ref/data。
|
|
72
|
+
input: UEnvModel
|
|
73
|
+
# 可选的样本运行资源声明;当前有值时为 {"image": "镜像名@sha256:摘要"}。
|
|
74
|
+
# None 表示样本未单独指定。实际资源选择由系统完成,这不是启动 Backend 的接口。
|
|
75
|
+
runtime: RuntimeSpec | None = None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def task_from_wire(value: dict, input_model: type[UEnvModel]) -> TaskSpec:
|
|
79
|
+
return TaskSpec(
|
|
80
|
+
dataset=deepcopy(value["dataset"]),
|
|
81
|
+
sample_id=value["sample_id"],
|
|
82
|
+
input=model_from_envelope(input_model, value["input"]),
|
|
83
|
+
runtime=deepcopy(value.get("runtime")),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class DatasetAdapter(ABC):
|
|
88
|
+
def bind_preparation(self, *, upload_file=None, run_spec=None):
|
|
89
|
+
"""System-owned preparation capabilities; never give the Adapter Hub credentials."""
|
|
90
|
+
self._upload_file = upload_file
|
|
91
|
+
self._preparation_run_spec = deepcopy(run_spec)
|
|
92
|
+
return self
|
|
93
|
+
|
|
94
|
+
def store_text(self, value: str | ArtifactReference, *, media_type: str = "text/plain") -> ArtifactReference:
|
|
95
|
+
"""Convert source text to the ordinary immutable file reference."""
|
|
96
|
+
if isinstance(value, dict):
|
|
97
|
+
return deepcopy(value)
|
|
98
|
+
if not isinstance(value, str):
|
|
99
|
+
raise TypeError("Source material must be text or ArtifactRef")
|
|
100
|
+
import tempfile
|
|
101
|
+
from pathlib import Path
|
|
102
|
+
|
|
103
|
+
with tempfile.TemporaryDirectory(prefix="uenv-prepare-") as directory:
|
|
104
|
+
path = Path(directory) / "material"
|
|
105
|
+
path.write_bytes(value.encode("utf-8"))
|
|
106
|
+
return self.store_file(path, media_type=media_type)
|
|
107
|
+
|
|
108
|
+
def store_file(self, path: str | PathLike[str], *, media_type: str = "application/octet-stream") -> ArtifactReference:
|
|
109
|
+
"""Store an explicitly selected image/audio/archive/test file.
|
|
110
|
+
|
|
111
|
+
The caller provides the upload capability: publication uploads to Hub,
|
|
112
|
+
direct task preparation uploads to Server. Dataset code needs neither
|
|
113
|
+
service addresses nor credentials and returns the same ArtifactRef.
|
|
114
|
+
"""
|
|
115
|
+
upload = getattr(self, "_upload_file", None)
|
|
116
|
+
if upload is None:
|
|
117
|
+
raise ValueError("MATERIAL_UPLOAD_REQUIRED")
|
|
118
|
+
return upload(path, media_type=media_type)
|
|
119
|
+
|
|
120
|
+
@staticmethod
|
|
121
|
+
def read_rows(path: str | PathLike[str]) -> Iterable[dict]:
|
|
122
|
+
"""Read JSONL or Parquet; override for a dataset-specific source layout.
|
|
123
|
+
|
|
124
|
+
Runs only during trusted preparation. Hub samples remain standardized
|
|
125
|
+
JSONL, and Worker never reads the original source file.
|
|
126
|
+
"""
|
|
127
|
+
from pathlib import Path
|
|
128
|
+
|
|
129
|
+
if Path(path).suffix.lower() == ".parquet":
|
|
130
|
+
try:
|
|
131
|
+
import pyarrow.parquet as parquet
|
|
132
|
+
except ImportError as exc:
|
|
133
|
+
raise RuntimeError(
|
|
134
|
+
"Install uenv-sdk[parquet] in the preparation environment"
|
|
135
|
+
) from exc
|
|
136
|
+
# Only file decoding is shared; business fields remain in normalize().
|
|
137
|
+
with parquet.ParquetFile(path) as source:
|
|
138
|
+
for batch in source.iter_batches(batch_size=128):
|
|
139
|
+
yield from batch.to_pylist()
|
|
140
|
+
return
|
|
141
|
+
with open(path, encoding="utf-8") as source:
|
|
142
|
+
for line in source:
|
|
143
|
+
if line.strip():
|
|
144
|
+
yield json.loads(line)
|
|
145
|
+
|
|
146
|
+
@abstractmethod
|
|
147
|
+
def normalize(self, row: dict) -> PreparedSample:
|
|
148
|
+
"""Deterministically convert one source row; never choose backend or agent."""
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass(frozen=True)
|
|
152
|
+
class Observation:
|
|
153
|
+
"""Agent-visible text, file references or package-owned structured data.
|
|
154
|
+
|
|
155
|
+
Authors pass a string, a file reference, a UEnvModel, or an ordered list of
|
|
156
|
+
these values. SDK normalization keeps the internal ContentPart[] protocol.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
content: str | UEnvModel | dict | list = field(default_factory=list)
|
|
160
|
+
terminated: bool = False
|
|
161
|
+
episode_truncated: bool = False
|
|
162
|
+
|
|
163
|
+
def __post_init__(self):
|
|
164
|
+
object.__setattr__(self, "content", normalize_content(self.content))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class Environment(ABC):
|
|
168
|
+
def __init__(self, *, workspace: Workspace, artifacts: ArtifactStore):
|
|
169
|
+
# 系统创建环境时提供执行能力;每道任务的数据在 reset(task, seed=...) 中传入。
|
|
170
|
+
# 这里不保存 task、seed 或截止时间;它们属于每一次调用。
|
|
171
|
+
# 已准备好的工作区接口:workspace、read_file、write_file、run。
|
|
172
|
+
# 它操作当前后端内的任务工作区,不负责选择或创建容器。
|
|
173
|
+
self.workspace = workspace
|
|
174
|
+
# 系统管理的文件引用:read_artifact(ref)、copy_artifact(ref, output)、
|
|
175
|
+
# put_bytes(content, media_type)。只允许访问本任务公开材料,不含私有答案。
|
|
176
|
+
self.artifacts = artifacts
|
|
177
|
+
|
|
178
|
+
@abstractmethod
|
|
179
|
+
def reset(self, task: TaskSpec, *, seed: int) -> Observation:
|
|
180
|
+
"""Initialize this task and return the Agent's first observation."""
|
|
181
|
+
# 【必须实现】task.input 是本数据集公开输入模型;seed 是本次随机种子。
|
|
182
|
+
# 纯问答返回题目;需准备工作区时调用 self.workspace / self.artifacts。
|
|
183
|
+
# self.workspace.run(argv) 自动使用本次调用剩余预算,不要求作者计算截止时间。
|
|
184
|
+
# Worker 负责输入/输出校验和运行限制;这里不调用模型、评分或调度后端。
|
|
185
|
+
|
|
186
|
+
def finalize(self, task: TaskSpec, final_answer: list[dict]) -> list[dict]:
|
|
187
|
+
"""Collect the submission after interaction, within the finalization reserve.
|
|
188
|
+
|
|
189
|
+
Default: preserve the Agent answer. Dataset environments may snapshot
|
|
190
|
+
their public workspace here. No model calls or private scoring inputs
|
|
191
|
+
are available. Errors fail the attempt; they must not become empty answers.
|
|
192
|
+
"""
|
|
193
|
+
return deepcopy(final_answer)
|
|
194
|
+
|
|
195
|
+
def close(self):
|
|
196
|
+
"""Optional plugin cleanup; Worker still owns underlying resources."""
|
|
197
|
+
# 【可选实现】释放作者自己创建的连接等;底层容器和进程仍由系统回收。
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class AgentRuntimeError(RuntimeError):
|
|
201
|
+
"""SDK representation of the existing Worker ErrorRecord, not a new wire type."""
|
|
202
|
+
|
|
203
|
+
def __init__(self, error: dict):
|
|
204
|
+
self.error = deepcopy(error)
|
|
205
|
+
super().__init__(error["code"])
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def code(self):
|
|
209
|
+
return self.error["code"]
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class AgentContext(ABC):
|
|
213
|
+
"""Worker-provided capability view; it never owns scheduling or budgets."""
|
|
214
|
+
|
|
215
|
+
task: TaskSpec
|
|
216
|
+
observation: Observation
|
|
217
|
+
tools: tuple[dict, ...]
|
|
218
|
+
seed: int
|
|
219
|
+
artifacts: object # Task-authorized file reads and writes supplied by Worker.
|
|
220
|
+
tool_services: dict[str, str] # Worker-created endpoints by selected tool name.
|
|
221
|
+
# observation is the current reset/step result, including end flags.
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def mcp_config(self) -> dict:
|
|
225
|
+
"""Worker-managed MCP connection for selected Python tools.
|
|
226
|
+
|
|
227
|
+
Pass this to the framework's MCP client. Calls must preserve arguments
|
|
228
|
+
returned by generate(); URLs and credentials must not be persisted.
|
|
229
|
+
An empty tool selection returns an empty configuration.
|
|
230
|
+
"""
|
|
231
|
+
raise NotImplementedError("The Worker supplies the MCP connection")
|
|
232
|
+
|
|
233
|
+
@abstractmethod
|
|
234
|
+
async def generate(self, messages: list[dict], *, enable_tools: bool = True) -> dict:
|
|
235
|
+
"""Request one generation through Rust within the original budget and trace.
|
|
236
|
+
|
|
237
|
+
enable_tools=False requests text-only generation without changing selected
|
|
238
|
+
bindings or execution permission. Denials raise AgentRuntimeError.
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
@abstractmethod
|
|
242
|
+
async def call_tool(self, call: dict, *, execute: Callable | None = None) -> Observation:
|
|
243
|
+
"""Return public feedback after Worker admission.
|
|
244
|
+
|
|
245
|
+
Framework-native tools may supply execute(arguments, context), returning
|
|
246
|
+
Observation (or an awaitable). It runs locally only after admission;
|
|
247
|
+
omitting it uses the selected tool definition. Native objects stay local.
|
|
248
|
+
"""
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class AgentRunner(ABC):
|
|
252
|
+
def __init__(self, config: dict):
|
|
253
|
+
if not isinstance(config, dict):
|
|
254
|
+
raise TypeError("AgentRunner config must be an object")
|
|
255
|
+
self.config = deepcopy(config)
|
|
256
|
+
|
|
257
|
+
def prepare(
|
|
258
|
+
self, context: AgentContext, bindings: list[dict], native_factories: dict,
|
|
259
|
+
python_definitions: dict[str, ToolDefinition], workspace: str, model_id: str,
|
|
260
|
+
) -> dict[str, ToolDefinition]:
|
|
261
|
+
"""Synchronously prepare once per attempt, returning selected native tools.
|
|
262
|
+
|
|
263
|
+
Worker supplies the resolved tool bindings, selected native factories,
|
|
264
|
+
Python definitions keyed by selected name, workspace and model identity.
|
|
265
|
+
No task is bound to context yet; do not generate or execute tools here.
|
|
266
|
+
Return exactly the selected native ToolDefinitions keyed by public binding
|
|
267
|
+
name, never implementation ID. Python definitions stay Worker-owned.
|
|
268
|
+
The default returns no native tools (for example PlainAgent). Publishing
|
|
269
|
+
checks signatures only; it never calls prepare. On failure, close must
|
|
270
|
+
still release any resources acquired by this Agent.
|
|
271
|
+
"""
|
|
272
|
+
return {}
|
|
273
|
+
|
|
274
|
+
def close(self):
|
|
275
|
+
"""Release Agent-owned framework resources, including partial setup.
|
|
276
|
+
|
|
277
|
+
Called after tool freeze, even when setup/run fails. Must tolerate an
|
|
278
|
+
unprepared or already closed Agent. Worker still owns backend resources.
|
|
279
|
+
"""
|
|
280
|
+
|
|
281
|
+
@abstractmethod
|
|
282
|
+
async def run(self, context: AgentContext) -> list[dict]:
|
|
283
|
+
"""Own the interaction loop; receive only visible capabilities."""
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class ToolExecutor(ABC):
|
|
287
|
+
"""Deprecated SDK base; removed in uenv-sdk 0.2.0.
|
|
288
|
+
|
|
289
|
+
Use @tool returning Observation, or ToolDefinition for native integration.
|
|
290
|
+
The package loader does not discover ToolExecutor subclasses. This is not
|
|
291
|
+
openhands.sdk.tool.ToolExecutor, which remains the framework's own interface.
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
def __init_subclass__(cls, **kwargs):
|
|
295
|
+
super().__init_subclass__(**kwargs)
|
|
296
|
+
warnings.warn(
|
|
297
|
+
"uenv.sdk.ToolExecutor is deprecated and will be removed in 0.2.0; "
|
|
298
|
+
"use @tool returning Observation or ToolDefinition instead",
|
|
299
|
+
DeprecationWarning,
|
|
300
|
+
stacklevel=2,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
def __init__(self, config: dict):
|
|
304
|
+
if not isinstance(config, dict):
|
|
305
|
+
raise TypeError("ToolExecutor config must be an object")
|
|
306
|
+
self.config = deepcopy(config)
|
|
307
|
+
|
|
308
|
+
@abstractmethod
|
|
309
|
+
async def execute(self, arguments: dict, context) -> dict:
|
|
310
|
+
"""Execute one selected tool; input and output follow its published schemas."""
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
@dataclass(frozen=True)
|
|
314
|
+
class ScoreInput:
|
|
315
|
+
task: TaskSpec
|
|
316
|
+
final_answer: list[ContentItem]
|
|
317
|
+
trajectory_ref: ArtifactReference
|
|
318
|
+
private_data: UEnvModel | None = None
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@dataclass(frozen=True)
|
|
322
|
+
class ScoreResult:
|
|
323
|
+
success: bool | None = None
|
|
324
|
+
metrics: list[Metric] = field(default_factory=list)
|
|
325
|
+
reward: float | None = None
|
|
326
|
+
evidence: list[ArtifactReference] = field(default_factory=list)
|
|
327
|
+
generation_rewards: list[GenerationReward] | None = None
|
|
328
|
+
|
|
329
|
+
# Only the Rust Worker fills these fields. Scorer-returned values are rejected.
|
|
330
|
+
status: str | None = None
|
|
331
|
+
scorer: dict | None = None
|
|
332
|
+
error: dict | None = None
|
|
333
|
+
|
|
334
|
+
@classmethod
|
|
335
|
+
def binary(cls, passed):
|
|
336
|
+
if type(passed) is not bool:
|
|
337
|
+
raise TypeError("Binary score requires an actual bool")
|
|
338
|
+
value = float(passed)
|
|
339
|
+
return cls(
|
|
340
|
+
passed,
|
|
341
|
+
[
|
|
342
|
+
{
|
|
343
|
+
"name": "accuracy",
|
|
344
|
+
"value": value,
|
|
345
|
+
"unit": "ratio",
|
|
346
|
+
"direction": "higher",
|
|
347
|
+
}
|
|
348
|
+
],
|
|
349
|
+
value,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
binary_score = ScoreResult.binary
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
class Scorer(ABC):
|
|
357
|
+
def __init__(
|
|
358
|
+
self,
|
|
359
|
+
*,
|
|
360
|
+
workspace: Workspace,
|
|
361
|
+
workspace_factory: Callable,
|
|
362
|
+
read_artifact: Callable,
|
|
363
|
+
read_trajectory: Callable,
|
|
364
|
+
copy_artifact: Callable | None = None,
|
|
365
|
+
):
|
|
366
|
+
# Worker 创建评分器时提供能力;普通作者沿用基类构造函数即可。
|
|
367
|
+
# 这些接口只在本次评分期间有效,不提供 Agent 的可写工作区或容器管理权限。
|
|
368
|
+
self.workspace = workspace
|
|
369
|
+
self._workspace_factory = workspace_factory
|
|
370
|
+
self._read_artifact = read_artifact
|
|
371
|
+
self._read_trajectory = read_trajectory
|
|
372
|
+
self._copy_artifact = copy_artifact
|
|
373
|
+
|
|
374
|
+
@abstractmethod
|
|
375
|
+
def score(self, request: ScoreInput) -> ScoreResult:
|
|
376
|
+
"""Score the submitted answer and trajectory using optional reference data."""
|
|
377
|
+
# 【必须实现】只接收本次评分数据,不再接收系统 context。
|
|
378
|
+
# 简单任务直接比较答案;测试评分或过程评分按需使用下面的系统方法。
|
|
379
|
+
# 超时、取消、协议校验和文件授权由 Worker/Runtime 负责。
|
|
380
|
+
|
|
381
|
+
def create_workspace(self) -> RemoteWorkspace:
|
|
382
|
+
"""Create an additional isolated workspace; use self.workspace for ordinary scoring."""
|
|
383
|
+
return RemoteWorkspace(self._workspace_factory)
|
|
384
|
+
|
|
385
|
+
def read_artifact(self, reference: ArtifactReference) -> bytes:
|
|
386
|
+
"""Read a file authorized for this scoring request, including private material."""
|
|
387
|
+
return self._read_artifact(reference)
|
|
388
|
+
|
|
389
|
+
def copy_artifact(self, reference: ArtifactReference, output: BinaryIO) -> None:
|
|
390
|
+
"""Stream an authorized artifact to a binary writer without exposing private files."""
|
|
391
|
+
if self._copy_artifact is None:
|
|
392
|
+
raise RuntimeError("SCORER_ARTIFACT_STREAM_UNAVAILABLE")
|
|
393
|
+
self._copy_artifact(reference, output)
|
|
394
|
+
|
|
395
|
+
def read_trajectory(self, request: ScoreInput) -> list[TrajectoryEvent]:
|
|
396
|
+
"""Read and validate this task's recorded scoring checkpoint events."""
|
|
397
|
+
# 返回统一轨迹事件,可按 generation_id 计算过程奖励,不生成新回答。
|
|
398
|
+
return self._read_trajectory(request)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def read_reference_text(request: ScoreInput) -> tuple[str, dict]:
|
|
402
|
+
"""Opt-in extraction for rules requiring text and private reference material."""
|
|
403
|
+
if request.private_data is None:
|
|
404
|
+
raise ValueError("This scoring rule requires reference material")
|
|
405
|
+
if any(part["kind"] != "text" for part in request.final_answer):
|
|
406
|
+
raise ValueError("Text-only scorer cannot discard non-text answer parts")
|
|
407
|
+
return "".join(
|
|
408
|
+
part["text"] for part in request.final_answer
|
|
409
|
+
), request.private_data.model_dump()
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
# User-defined tools use the same authoring API in independent and dataset packages.
|
|
413
|
+
from .tools import SandboxToolContext, ToolContext, ToolDefinition, tool
|
uenv/sdk/artifact_io.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Internal chunk transport; authors continue to use the same ArtifactRef."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def artifact_chunks(peer, method, reference):
|
|
7
|
+
size = reference["size_bytes"]
|
|
8
|
+
offset, hasher = 0, hashlib.sha256()
|
|
9
|
+
while offset < size or offset == 0:
|
|
10
|
+
data = bytes(
|
|
11
|
+
peer.request(method, {"artifact_ref": reference, "offset": offset})
|
|
12
|
+
)
|
|
13
|
+
expected = min(256 * 1024, size - offset)
|
|
14
|
+
if len(data) != expected:
|
|
15
|
+
raise ValueError("ARTIFACT_STREAM_LENGTH_MISMATCH")
|
|
16
|
+
hasher.update(data)
|
|
17
|
+
offset += len(data)
|
|
18
|
+
yield data
|
|
19
|
+
if offset == size:
|
|
20
|
+
break
|
|
21
|
+
if "sha256:" + hasher.hexdigest() != reference["digest"]:
|
|
22
|
+
raise ValueError("ARTIFACT_STREAM_DIGEST_MISMATCH")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def read_artifact(peer, method, reference):
|
|
26
|
+
return b"".join(artifact_chunks(peer, method, reference))
|