agentenv-framework 0.9.1254__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.
- agent_env/__init__.py +0 -0
- agent_env/a2a_agent/__init__.py +23 -0
- agent_env/a2a_agent/a2a_agent.py +600 -0
- agent_env/a2a_agent/conversation_store.py +356 -0
- agent_env/a2a_agent/object_transfer.py +609 -0
- agent_env/a2a_agent/protocol.py +238 -0
- agent_env/a2a_agent/store.py +201 -0
- agent_env/a2a_agent/validator.py +901 -0
- agent_env/artifact/__init__.py +48 -0
- agent_env/artifact/artifact.py +74 -0
- agent_env/artifact/artifacts/__init__.py +0 -0
- agent_env/artifact/artifacts/cli.py +83 -0
- agent_env/artifact/artifacts/docker_image.py +457 -0
- agent_env/artifact/artifacts/environment.py +89 -0
- agent_env/artifact/artifacts/environment_universe.py +156 -0
- agent_env/artifact/artifacts/file.py +214 -0
- agent_env/artifact/artifacts/file_artifact_universe.py +206 -0
- agent_env/artifact/artifacts/skill.py +206 -0
- agent_env/artifact/artifacts/vm_image.py +95 -0
- agent_env/artifact/ref.py +15 -0
- agent_env/artifact/registry.py +251 -0
- agent_env/artifact/store.py +295 -0
- agent_env/artifact/universe.py +38 -0
- agent_env/attribution.py +41 -0
- agent_env/bundle/__init__.py +4 -0
- agent_env/bundle/_fs.py +128 -0
- agent_env/bundle/authoring.py +239 -0
- agent_env/bundle/installed.py +175 -0
- agent_env/bundle/ledger.py +269 -0
- agent_env/bundle/materialize.py +204 -0
- agent_env/bundle/parse.py +594 -0
- agent_env/bundle/plan.py +414 -0
- agent_env/bundle/resolve.py +404 -0
- agent_env/bundle/run.py +361 -0
- agent_env/cli/__init__.py +69 -0
- agent_env/cli/__main__.py +10 -0
- agent_env/cli/_installers.py +576 -0
- agent_env/cli/_plugin_changes.py +947 -0
- agent_env/cli/a2a_agent/__init__.py +22 -0
- agent_env/cli/a2a_agent/add_skill.py +97 -0
- agent_env/cli/a2a_agent/deploy.py +60 -0
- agent_env/cli/a2a_agent/get.py +15 -0
- agent_env/cli/a2a_agent/get_instance.py +26 -0
- agent_env/cli/a2a_agent/put.py +78 -0
- agent_env/cli/a2a_agent/validate.py +56 -0
- agent_env/cli/artifact/__init__.py +20 -0
- agent_env/cli/artifact/cli.py +83 -0
- agent_env/cli/artifact/environment.py +44 -0
- agent_env/cli/artifact/environment_universe.py +163 -0
- agent_env/cli/artifact/file_artifact_universe.py +281 -0
- agent_env/cli/artifact/skill.py +128 -0
- agent_env/cli/banner.py +25 -0
- agent_env/cli/config.py +263 -0
- agent_env/cli/env/__init__.py +31 -0
- agent_env/cli/env/deploy.py +112 -0
- agent_env/cli/env/gateway.py +57 -0
- agent_env/cli/env/get_instance.py +40 -0
- agent_env/cli/env/mcp_server.py +410 -0
- agent_env/cli/env/multi.py +282 -0
- agent_env/cli/env/service_db.py +99 -0
- agent_env/cli/env/snapshot.py +35 -0
- agent_env/cli/env/state/__init__.py +6 -0
- agent_env/cli/env/state/init.py +108 -0
- agent_env/cli/env/state/teardown.py +44 -0
- agent_env/cli/env/website.py +216 -0
- agent_env/cli/env/website_browser.py +67 -0
- agent_env/cli/eval/__init__.py +16 -0
- agent_env/cli/eval/add_tasks.py +63 -0
- agent_env/cli/eval/create.py +49 -0
- agent_env/cli/eval/run.py +194 -0
- agent_env/cli/identity/__init__.py +5 -0
- agent_env/cli/identity/client_identity.py +37 -0
- agent_env/cli/plugin.py +438 -0
- agent_env/cli/run.py +285 -0
- agent_env/cli/task/__init__.py +21 -0
- agent_env/cli/task/create.py +109 -0
- agent_env/cli/task/get.py +17 -0
- agent_env/cli/task/get_instance.py +34 -0
- agent_env/cli/task/run.py +615 -0
- agent_env/cli/task/validate.py +31 -0
- agent_env/cli/up.py +123 -0
- agent_env/cli/utils.py +183 -0
- agent_env/config/__init__.py +50 -0
- agent_env/config/describe.py +1067 -0
- agent_env/config/errors.py +13 -0
- agent_env/config/loader.py +245 -0
- agent_env/config/model.py +154 -0
- agent_env/config/paths.py +25 -0
- agent_env/config/plugin_tables.py +101 -0
- agent_env/config/provenance.py +134 -0
- agent_env/config/runtime.py +1064 -0
- agent_env/config/snapshot.py +81 -0
- agent_env/entity_refs.py +186 -0
- agent_env/env/__init__.py +49 -0
- agent_env/env/env.py +433 -0
- agent_env/env/env_artifact_store.py +87 -0
- agent_env/env/envs/__init__.py +7 -0
- agent_env/env/envs/_deployment.py +281 -0
- agent_env/env/envs/gateway_server.py +32 -0
- agent_env/env/envs/mcp_server.py +387 -0
- agent_env/env/envs/multi_env.py +780 -0
- agent_env/env/envs/service_db/Dockerfile +20 -0
- agent_env/env/envs/service_db/Dockerfile.db-mcp +42 -0
- agent_env/env/envs/service_db/Dockerfile.db-web +8 -0
- agent_env/env/envs/service_db.py +92 -0
- agent_env/env/envs/website.py +294 -0
- agent_env/env/envs/website_browser/Dockerfile +40 -0
- agent_env/env/envs/website_browser/__init__.py +17 -0
- agent_env/env/envs/website_browser/entrypoint.sh +13 -0
- agent_env/env/gateway/Dockerfile +14 -0
- agent_env/env/gateway/__init__.py +37 -0
- agent_env/env/gateway/clock.py +184 -0
- agent_env/env/gateway/constants.py +190 -0
- agent_env/env/gateway/entrypoint.py +156 -0
- agent_env/env/gateway/gateway.py +1095 -0
- agent_env/env/gateway/get_time.py +60 -0
- agent_env/env/gateway/requirements.txt +4 -0
- agent_env/env/gateway/triggers.py +1193 -0
- agent_env/env/legacy_protocol.py +146 -0
- agent_env/env/registry.py +93 -0
- agent_env/env/snapshot_store.py +372 -0
- agent_env/env/store.py +411 -0
- agent_env/eval/__init__.py +14 -0
- agent_env/eval/eval.py +68 -0
- agent_env/eval/store.py +116 -0
- agent_env/examples/hello/README.md +9 -0
- agent_env/examples/hello/artifacts/greeting/check.sh +1 -0
- agent_env/examples/hello/artifacts/greeting/hello.txt +1 -0
- agent_env/examples/hello/tasks/hello.json +10 -0
- agent_env/explorer/__init__.py +12 -0
- agent_env/explorer/app.py +277 -0
- agent_env/explorer/openapi_docs.py +242 -0
- agent_env/explorer/plugin.py +95 -0
- agent_env/explorer/routers/__init__.py +1 -0
- agent_env/explorer/routers/common.py +174 -0
- agent_env/explorer/routers/conversations.py +25 -0
- agent_env/explorer/routers/objects.py +112 -0
- agent_env/explorer/routers/runs.py +475 -0
- agent_env/plugins/__init__.py +59 -0
- agent_env/plugins/_cli.py +244 -0
- agent_env/plugins/_discovery.py +97 -0
- agent_env/plugins/_inventory.py +262 -0
- agent_env/plugins/_registration.py +268 -0
- agent_env/plugins/_report.py +47 -0
- agent_env/plugins/_requirements.py +55 -0
- agent_env/providers/__init__.py +37 -0
- agent_env/providers/env_providers/__init__.py +12 -0
- agent_env/providers/env_providers/constants.py +13 -0
- agent_env/providers/env_providers/env_gateway_provider.py +1354 -0
- agent_env/providers/env_providers/env_provider.py +237 -0
- agent_env/providers/env_providers/env_server_provider.py +133 -0
- agent_env/providers/env_state/__init__.py +49 -0
- agent_env/providers/env_state/env_state_provider.py +554 -0
- agent_env/providers/env_state/local_postgres.py +390 -0
- agent_env/providers/env_state/store.py +112 -0
- agent_env/providers/sandbox_providers/__init__.py +29 -0
- agent_env/providers/sandbox_providers/chained_sandbox_provider.py +93 -0
- agent_env/providers/sandbox_providers/e2b/__init__.py +6 -0
- agent_env/providers/sandbox_providers/e2b/provider.py +376 -0
- agent_env/providers/sandbox_providers/e2b/sandbox.py +342 -0
- agent_env/providers/sandbox_providers/e2b/template.py +158 -0
- agent_env/providers/sandbox_providers/local_sandbox.py +408 -0
- agent_env/providers/sandbox_providers/modal_sandbox.py +500 -0
- agent_env/providers/sandbox_providers/modal_vm_sandbox.py +466 -0
- agent_env/providers/sandbox_providers/sandbox.py +387 -0
- agent_env/providers/sandbox_providers/sandbox_provider.py +614 -0
- agent_env/py.typed +0 -0
- agent_env/runner/__init__.py +16 -0
- agent_env/runner/local_runner.py +190 -0
- agent_env/runner/runner.py +127 -0
- agent_env/runner/store.py +94 -0
- agent_env/store/__init__.py +126 -0
- agent_env/store/_google.py +68 -0
- agent_env/store/base.py +33 -0
- agent_env/store/document_store/__init__.py +60 -0
- agent_env/store/document_store/document_store.py +519 -0
- agent_env/store/document_store/dynamodb_document_store.py +306 -0
- agent_env/store/document_store/evaluation.py +199 -0
- agent_env/store/document_store/firestore_mongo_document_store.py +242 -0
- agent_env/store/document_store/mongo_document_store.py +259 -0
- agent_env/store/document_store/sqlite_document_store.py +314 -0
- agent_env/store/ids.py +123 -0
- agent_env/store/image_store/__init__.py +34 -0
- agent_env/store/image_store/ecr_image_store.py +131 -0
- agent_env/store/image_store/google_credentials.py +113 -0
- agent_env/store/image_store/image_store.py +113 -0
- agent_env/store/image_store/local_registry_image_store.py +106 -0
- agent_env/store/image_store/oci_registry_credentials.py +196 -0
- agent_env/store/local_state.py +27 -0
- agent_env/store/object_store/__init__.py +24 -0
- agent_env/store/object_store/gcs_object_store.py +415 -0
- agent_env/store/object_store/local_object_store.py +143 -0
- agent_env/store/object_store/object_store.py +198 -0
- agent_env/store/object_store/s3_object_store.py +354 -0
- agent_env/store/query.py +112 -0
- agent_env/store/routing.py +609 -0
- agent_env/store/secret_store/__init__.py +19 -0
- agent_env/store/secret_store/aws_secrets_manager_secret_store.py +293 -0
- agent_env/store/secret_store/gcp_secret_manager_secret_store.py +319 -0
- agent_env/store/secret_store/local_secret_store.py +47 -0
- agent_env/store/secret_store/secret_store.py +19 -0
- agent_env/task/__init__.py +27 -0
- agent_env/task/interrupts.py +109 -0
- agent_env/task/registry.py +27 -0
- agent_env/task/step_journal.py +99 -0
- agent_env/task/store.py +940 -0
- agent_env/task/task.py +815 -0
- agent_env/task/teardown.py +142 -0
- agent_env/task_step/__init__.py +47 -0
- agent_env/task_step/context.py +195 -0
- agent_env/task_step/context_ops.py +224 -0
- agent_env/task_step/registry.py +182 -0
- agent_env/task_step/review_store.py +118 -0
- agent_env/task_step/snapshot_utils/__init__.py +2 -0
- agent_env/task_step/snapshot_utils/agent_state_capture.py +261 -0
- agent_env/task_step/snapshot_utils/snapshot_series.py +631 -0
- agent_env/task_step/store.py +129 -0
- agent_env/task_step/task_step.py +173 -0
- agent_env/task_step/task_steps/__init__.py +27 -0
- agent_env/task_step/task_steps/a2a_agent_validator/__init__.py +31 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/README.md +158 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.m4a +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.mp3 +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.mp4 +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.ogg +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.wav +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/document.pdf +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/red.gif +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/red.jpg +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/red.png +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_agent_card.py +103 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_agent_config_identity.py +69 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_agent_mcp.py +178 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_core_protocol.py +67 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_install.py +115 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_litellm_attribution.py +124 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_litellm_attribution_runtime.py +173 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_modalities.py +270 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_peer_agents.py +102 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_role.py +151 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_skill_config.py +194 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_snapshot.py +111 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_system_prompt.py +215 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_trajectory.py +164 -0
- agent_env/task_step/task_steps/add_skills.py +240 -0
- agent_env/task_step/task_steps/apply_server_config.py +288 -0
- agent_env/task_step/task_steps/collect_artifacts.py +940 -0
- agent_env/task_step/task_steps/deploy_agent.py +626 -0
- agent_env/task_step/task_steps/deploy_env.py +197 -0
- agent_env/task_step/task_steps/deploy_human_agent.py +100 -0
- agent_env/task_step/task_steps/deploy_sandbox.py +170 -0
- agent_env/task_step/task_steps/env_card_validator/__init__.py +4 -0
- agent_env/task_step/task_steps/env_card_validator/verify_env_card.py +109 -0
- agent_env/task_step/task_steps/env_card_validator/verify_env_core_protocol.py +108 -0
- agent_env/task_step/task_steps/install_agent.py +376 -0
- agent_env/task_step/task_steps/load_artifact.py +775 -0
- agent_env/task_step/task_steps/mcp_cli_builder/__init__.py +4 -0
- agent_env/task_step/task_steps/mcp_cli_builder/build_mcp_cli.py +201 -0
- agent_env/task_step/task_steps/mcp_cli_builder/codegen.py +627 -0
- agent_env/task_step/task_steps/mcp_env_validator/__init__.py +44 -0
- agent_env/task_step/task_steps/mcp_env_validator/validation_gate_aggregator.py +102 -0
- agent_env/task_step/task_steps/mcp_env_validator/verify_mcp_env_assessment.py +78 -0
- agent_env/task_step/task_steps/mcp_env_validator/verify_mcp_tool_schema.py +152 -0
- agent_env/task_step/task_steps/mcp_env_validator/verify_spec_conformance.py +486 -0
- agent_env/task_step/task_steps/modify_env_tool_access.py +80 -0
- agent_env/task_step/task_steps/multienv_validator/__init__.py +3 -0
- agent_env/task_step/task_steps/multienv_validator/combine_universe_verdicts.py +107 -0
- agent_env/task_step/task_steps/multienv_validator/universe_comparison.py +253 -0
- agent_env/task_step/task_steps/multienv_validator/verify_universe_agent_judge.py +180 -0
- agent_env/task_step/task_steps/multienv_validator/verify_universe_roundtrip.py +241 -0
- agent_env/task_step/task_steps/peer_agents.py +92 -0
- agent_env/task_step/task_steps/prompt_agent.py +900 -0
- agent_env/task_step/task_steps/register_agent_triggers.py +113 -0
- agent_env/task_step/task_steps/register_env_triggers.py +97 -0
- agent_env/task_step/task_steps/reset_env.py +71 -0
- agent_env/task_step/task_steps/review.py +152 -0
- agent_env/task_step/task_steps/run_code.py +461 -0
- agent_env/task_step/task_steps/run_code_runner.py +52 -0
- agent_env/task_step/task_steps/run_docker_container.py +380 -0
- agent_env/task_step/task_steps/sandbox_utils/__init__.py +0 -0
- agent_env/task_step/task_steps/sandbox_utils/sandbox_utils.py +79 -0
- agent_env/task_step/task_steps/snapshot_agent_state.py +205 -0
- agent_env/task_step/task_steps/snapshot_env.py +637 -0
- agent_env/task_step/task_steps/sync_env_clock.py +132 -0
- agent_env/task_step/task_steps/teardown_sandboxes.py +170 -0
- agent_env/task_step/task_steps/verifiers/__init__.py +23 -0
- agent_env/task_step/task_steps/verifiers/agent_prompt_response_verifier.py +166 -0
- agent_env/task_step/task_steps/verifiers/aggregate_verifiers.py +90 -0
- agent_env/task_step/task_steps/verifiers/env_outcome_verifier.py +127 -0
- agent_env/task_step/task_steps/verifiers/judge_utils/__init__.py +2 -0
- agent_env/task_step/task_steps/verifiers/judge_utils/frame_selection.py +377 -0
- agent_env/task_step/task_steps/verifiers/judge_utils/judge_output_format.py +1293 -0
- agent_env/task_step/task_steps/verifiers/judge_utils/trajectory_filter.py +514 -0
- agent_env/task_step/task_steps/verifiers/rubrics_verifier.py +1318 -0
- agent_env/task_step/task_steps/verifiers/run_container_unit_tests_verifier.py +434 -0
- agent_env/task_step/task_steps/verifiers/scoring.py +40 -0
- agent_env/task_step/task_steps/verifiers/verify_sandbox.py +308 -0
- agent_env/task_step/thread_work.py +61 -0
- agent_env/utils/__init__.py +0 -0
- agent_env/utils/card_naming.py +108 -0
- agent_env/utils/deprecation.py +109 -0
- agent_env/utils/docker_build.py +29 -0
- agent_env/utils/exec_retry.py +77 -0
- agent_env/utils/litellm_attribution.py +59 -0
- agent_env/utils/paths.py +20 -0
- agentenv_framework-0.9.1254.dist-info/METADATA +394 -0
- agentenv_framework-0.9.1254.dist-info/RECORD +312 -0
- agentenv_framework-0.9.1254.dist-info/WHEEL +4 -0
- agentenv_framework-0.9.1254.dist-info/entry_points.txt +5 -0
- agentenv_framework-0.9.1254.dist-info/licenses/LICENSE +202 -0
- agentenv_framework-0.9.1254.dist-info/licenses/NOTICE +4 -0
- agentenv_framework-0.9.1254.dist-info/licenses/THIRD_PARTY_NOTICES.md +7665 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Verify environment outcome using a dynamically loaded Python verifier script."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import importlib.util
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import tempfile
|
|
10
|
+
import uuid
|
|
11
|
+
from typing import ClassVar, Optional
|
|
12
|
+
|
|
13
|
+
from agent_env.task_step.context import TaskStepContext
|
|
14
|
+
from agent_env.entity_refs import EntityRef
|
|
15
|
+
from agent_env.task_step.task_step import TaskStep, TaskStepDependency
|
|
16
|
+
from agent_env.task_step.task_steps.verifiers.scoring import ScoreAggregator, aggregate_score
|
|
17
|
+
|
|
18
|
+
__all__ = ["EnvOutcomeVerifierTaskStep", "ScoreAggregator", "aggregate_score"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class EnvOutcomeVerifierTaskStep(TaskStep):
|
|
25
|
+
type: ClassVar[str] = "env_outcome_verifier"
|
|
26
|
+
entity_refs = (
|
|
27
|
+
EntityRef.env("env_id"),
|
|
28
|
+
EntityRef.artifact("file_artifact_id", version_field="file_artifact_version", artifact_type="file"),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
id: str,
|
|
34
|
+
version: Optional[int],
|
|
35
|
+
env_id: str,
|
|
36
|
+
file_artifact_id: str,
|
|
37
|
+
file_artifact_version: Optional[int] = None,
|
|
38
|
+
score_aggregator: Optional[ScoreAggregator] = None,
|
|
39
|
+
verifier_id: Optional[str] = None,
|
|
40
|
+
depends_on: Optional[list[TaskStepDependency]] = None,
|
|
41
|
+
fail_task_on_error: bool = True,
|
|
42
|
+
):
|
|
43
|
+
super().__init__(id, version, depends_on=depends_on, fail_task_on_error=fail_task_on_error)
|
|
44
|
+
self.verifier_id = verifier_id or uuid.uuid4().hex
|
|
45
|
+
self.env_id = env_id
|
|
46
|
+
self.file_artifact_id = file_artifact_id
|
|
47
|
+
self.file_artifact_version = file_artifact_version
|
|
48
|
+
if isinstance(score_aggregator, str):
|
|
49
|
+
score_aggregator = ScoreAggregator(score_aggregator)
|
|
50
|
+
self.score_aggregator = score_aggregator or ScoreAggregator.ALL_PASS
|
|
51
|
+
|
|
52
|
+
def to_dict(self) -> dict:
|
|
53
|
+
base = super().to_dict()
|
|
54
|
+
base["verifier_id"] = self.verifier_id
|
|
55
|
+
base["env_id"] = self.env_id
|
|
56
|
+
base["file_artifact_id"] = self.file_artifact_id
|
|
57
|
+
base["file_artifact_version"] = self.file_artifact_version
|
|
58
|
+
base["score_aggregator"] = self.score_aggregator.value
|
|
59
|
+
return base
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def put(cls, *, verify_script_file_path: Optional[str] = None, **kwargs) -> "EnvOutcomeVerifierTaskStep":
|
|
63
|
+
"""Store the step. With ``verify_script_file_path`` the script is uploaded as
|
|
64
|
+
``<id>-verifier-script`` first; otherwise the kwargs name an existing ``file_artifact_id``
|
|
65
|
+
(the shape ``agent-env task create`` builds from JSON)."""
|
|
66
|
+
from agent_env.task_step.store import get_task_step_store
|
|
67
|
+
|
|
68
|
+
if verify_script_file_path is not None:
|
|
69
|
+
from agent_env.artifact import FileArtifact
|
|
70
|
+
|
|
71
|
+
artifact = FileArtifact.put(
|
|
72
|
+
id=f"{kwargs['id']}-verifier-script",
|
|
73
|
+
description=f"Verifier script for {kwargs['id']}",
|
|
74
|
+
file_path=verify_script_file_path,
|
|
75
|
+
)
|
|
76
|
+
kwargs.update(file_artifact_id=artifact.id, file_artifact_version=artifact.version)
|
|
77
|
+
kwargs.setdefault("version", None)
|
|
78
|
+
return get_task_step_store().put_document(cls(**kwargs))
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def from_dict(cls, data: dict) -> EnvOutcomeVerifierTaskStep:
|
|
82
|
+
raw = data.get("score_aggregator")
|
|
83
|
+
return cls(
|
|
84
|
+
**cls._base_from_dict(data),
|
|
85
|
+
env_id=data["env_id"],
|
|
86
|
+
file_artifact_id=data["file_artifact_id"],
|
|
87
|
+
file_artifact_version=data.get("file_artifact_version"),
|
|
88
|
+
score_aggregator=ScoreAggregator(raw) if raw else None,
|
|
89
|
+
verifier_id=data.get("verifier_id"),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
async def execute(self, context: TaskStepContext) -> TaskStepContext:
|
|
93
|
+
from agent_env.artifact import FileArtifact
|
|
94
|
+
|
|
95
|
+
deployed = next((d for d in context.deployed_envs if d.env_id == self.env_id), None)
|
|
96
|
+
if deployed is None:
|
|
97
|
+
raise RuntimeError(f"Env '{self.env_id}' not found in context.deployed_envs")
|
|
98
|
+
|
|
99
|
+
artifact = FileArtifact.get(self.file_artifact_id, self.file_artifact_version)
|
|
100
|
+
verifier_bytes = await asyncio.to_thread(artifact.load)
|
|
101
|
+
|
|
102
|
+
tmp_fd, tmp_path = tempfile.mkstemp(suffix=".py")
|
|
103
|
+
try:
|
|
104
|
+
os.write(tmp_fd, verifier_bytes)
|
|
105
|
+
os.close(tmp_fd)
|
|
106
|
+
|
|
107
|
+
module_name = f"outcome_verifier_{self.id}"
|
|
108
|
+
spec = importlib.util.spec_from_file_location(module_name, tmp_path)
|
|
109
|
+
module = importlib.util.module_from_spec(spec)
|
|
110
|
+
spec.loader.exec_module(module)
|
|
111
|
+
|
|
112
|
+
logger.info(f"Running verify() from artifact '{self.file_artifact_id}' against {deployed.mcp_url}")
|
|
113
|
+
results = await module.verify(deployed.mcp_url)
|
|
114
|
+
|
|
115
|
+
score = aggregate_score(results, self.score_aggregator)
|
|
116
|
+
if "verifications" not in context.metadata:
|
|
117
|
+
context.metadata["verifications"] = {}
|
|
118
|
+
context.metadata["verifications"][self.verifier_id] = {
|
|
119
|
+
"results": results,
|
|
120
|
+
"score": score,
|
|
121
|
+
}
|
|
122
|
+
logger.info(f"Verification '{self.verifier_id}' complete: {len(results)} criteria, score={score}")
|
|
123
|
+
finally:
|
|
124
|
+
if os.path.exists(tmp_path):
|
|
125
|
+
os.unlink(tmp_path)
|
|
126
|
+
|
|
127
|
+
return context
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"""Per-criterion screenshot selection for a direct multimodal judge. Instead of the last N frames
|
|
2
|
+
(``compact_screenshot_trajectory``), the frame budget goes to the actions that matter: the actions the task
|
|
3
|
+
pins (``TrajectoryFilter.always_show_actions``), the action best matching each criterion plus the one after
|
|
4
|
+
it (an action's effect shows on the following screen), the final frames, and the rest spread over the run.
|
|
5
|
+
A run that fits the budget is sent whole. Every frame carries a label (``"[c3] action 17"``,
|
|
6
|
+
``"submitted action 4"``, ``"final action 41"``, several tags composed) the judge cites. Everything here is
|
|
7
|
+
trajectory-level and judge-agnostic; the verifier adapts ``LabeledFrame`` to its judge's message shape.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
from collections.abc import Iterable, Mapping
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
|
|
16
|
+
from agent_env.task_step.task_steps.verifiers.judge_utils.trajectory_filter import (
|
|
17
|
+
DEFAULT_ACTION_LOG_MAX_LINES,
|
|
18
|
+
DEFAULT_FRAME_BUDGET,
|
|
19
|
+
FINAL_FRAMES_RESERVED,
|
|
20
|
+
ImageFrame,
|
|
21
|
+
media_type_for_b64,
|
|
22
|
+
parse_json_attr,
|
|
23
|
+
strip_base64_images,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Cuts (with an ellipsis) applied to the rendered JSON of tool arguments — the same text the action log,
|
|
27
|
+
# the regex knobs and criterion matching see — tool results and agent messages.
|
|
28
|
+
MAX_ARGS_CHARS = 110
|
|
29
|
+
MAX_RESULT_CHARS = 160
|
|
30
|
+
MAX_AGENT_MESSAGE_CHARS = 200
|
|
31
|
+
# Replaces a base64 image payload in rendered text before any cut, so the cut lands on text.
|
|
32
|
+
_IMAGE_PLACEHOLDER = "<image>"
|
|
33
|
+
# A second candidate scoring at least this fraction of a criterion's best match is kept as a near-tie.
|
|
34
|
+
_NEAR_TIE_RATIO = 0.6
|
|
35
|
+
|
|
36
|
+
# Words that carry no evidence about WHICH action shows a criterion.
|
|
37
|
+
_CRITERION_STOPWORDS = frozenset({
|
|
38
|
+
"the", "and", "for", "that", "this", "with", "was", "were", "been", "being", "has", "have", "are",
|
|
39
|
+
"its", "his", "her", "not", "any", "all", "one", "two", "from", "into", "than", "then", "there",
|
|
40
|
+
"shows", "show", "shown", "screen", "state", "final", "trajectory", "agent", "task", "user", "app",
|
|
41
|
+
"exactly", "visible", "displayed", "confirm", "confirmed", "set", "least", "valid", "correct",
|
|
42
|
+
"matching", "either", "should", "must", "step", "steps", "run",
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class TrajectoryAction:
|
|
48
|
+
"""One ``execute_tool`` span of a raw OTel trajectory, in run order (``index`` is 1-based)."""
|
|
49
|
+
|
|
50
|
+
index: int
|
|
51
|
+
tool: str
|
|
52
|
+
args: dict
|
|
53
|
+
screenshot: str | None # raw base64 of the post-action screenshot, if the span had one
|
|
54
|
+
result: object = None # the tool result minus its `screenshot`; None if nothing is left
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class AgentMessage:
|
|
59
|
+
"""A ``text`` block of a ``chat`` span, placed after ``after_action`` tool calls (0 = before the first)."""
|
|
60
|
+
|
|
61
|
+
after_action: int
|
|
62
|
+
text: str
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class Transcript:
|
|
67
|
+
"""What the RAW trajectory records the agent doing (``actions``) and saying (``messages``)."""
|
|
68
|
+
|
|
69
|
+
actions: list[TrajectoryAction]
|
|
70
|
+
messages: list[AgentMessage]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class LabeledFrame:
|
|
75
|
+
label: str
|
|
76
|
+
frame: ImageFrame
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _drop_screenshots(value: object) -> object:
|
|
80
|
+
"""``value`` with every ``screenshot`` key removed, at any depth."""
|
|
81
|
+
if isinstance(value, dict):
|
|
82
|
+
return {k: _drop_screenshots(v) for k, v in value.items() if k != "screenshot"}
|
|
83
|
+
if isinstance(value, list):
|
|
84
|
+
return [_drop_screenshots(v) for v in value]
|
|
85
|
+
return value
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _without_screenshot(completion: object) -> object:
|
|
89
|
+
"""The tool result minus its ``screenshot`` keys; None when nothing is left at the TOP level. Nested
|
|
90
|
+
empty values are kept — an empty string inside a list is still what the tool returned."""
|
|
91
|
+
stripped = _drop_screenshots(completion)
|
|
92
|
+
return None if stripped in ("", None, {}) else stripped
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _text_blocks(completion: object) -> list[str]:
|
|
96
|
+
"""The non-empty ``text`` blocks of a chat completion — a bare block list, or ``{"content": [...]}``."""
|
|
97
|
+
blocks = completion.get("content") if isinstance(completion, dict) else completion
|
|
98
|
+
if not isinstance(blocks, list):
|
|
99
|
+
return []
|
|
100
|
+
return [b["text"] for b in blocks
|
|
101
|
+
if isinstance(b, dict) and b.get("type") == "text" and isinstance(b.get("text"), str) and b["text"].strip()]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def transcript_from_raw(raw_text: str) -> Transcript:
|
|
105
|
+
"""Every ``execute_tool`` span as a ``TrajectoryAction`` and every text block of a ``chat`` span as an
|
|
106
|
+
``AgentMessage``, in span order. The ``chain`` root is skipped: its completion is the final response the
|
|
107
|
+
judge already gets. Malformed input gives an empty transcript."""
|
|
108
|
+
try:
|
|
109
|
+
spans = json.loads(raw_text)
|
|
110
|
+
except (json.JSONDecodeError, TypeError):
|
|
111
|
+
return Transcript([], [])
|
|
112
|
+
if not isinstance(spans, list):
|
|
113
|
+
return Transcript([], [])
|
|
114
|
+
actions: list[TrajectoryAction] = []
|
|
115
|
+
messages: list[AgentMessage] = []
|
|
116
|
+
for span in spans:
|
|
117
|
+
attrs = span.get("attributes") if isinstance(span, dict) else None
|
|
118
|
+
if not isinstance(attrs, dict):
|
|
119
|
+
continue
|
|
120
|
+
operation = attrs.get("gen_ai.operation.name")
|
|
121
|
+
completion = parse_json_attr(attrs.get("gen_ai.completion", "{}"))
|
|
122
|
+
if operation == "chat":
|
|
123
|
+
messages.extend(AgentMessage(len(actions), text) for text in _text_blocks(completion))
|
|
124
|
+
elif operation == "execute_tool":
|
|
125
|
+
prompt = parse_json_attr(attrs.get("gen_ai.prompt", "{}"))
|
|
126
|
+
args = prompt.get("input", prompt) if isinstance(prompt, dict) else {}
|
|
127
|
+
if not isinstance(args, dict):
|
|
128
|
+
args = {}
|
|
129
|
+
# The tool name: harnesses that name every action span generically (e.g. "execute_tool")
|
|
130
|
+
# carry the real tool in the prompt attribute; prefer it, fall back to the span name.
|
|
131
|
+
tool = prompt.get("tool") if isinstance(prompt, dict) else None
|
|
132
|
+
shot = completion.get("screenshot") if isinstance(completion, dict) else None
|
|
133
|
+
actions.append(TrajectoryAction(
|
|
134
|
+
index=len(actions) + 1, tool=str(tool or span.get("name") or ""), args=args,
|
|
135
|
+
screenshot=shot if isinstance(shot, str) and shot else None,
|
|
136
|
+
result=_without_screenshot(completion),
|
|
137
|
+
))
|
|
138
|
+
return Transcript(actions, messages)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _cut(text: str, max_chars: int) -> str:
|
|
142
|
+
return text if len(text) <= max_chars else text[:max_chars] + "…"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _render_json(value: object, max_chars: int) -> str:
|
|
146
|
+
"""``value`` as compact JSON, images replaced by ``_IMAGE_PLACEHOLDER``, cut to ``max_chars``."""
|
|
147
|
+
return _cut(strip_base64_images(json.dumps(value, ensure_ascii=False), _IMAGE_PLACEHOLDER), max_chars)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def rendered_action(a: TrajectoryAction) -> str:
|
|
151
|
+
"""``"<tool> <args json>"``, the arguments cut to ``MAX_ARGS_CHARS`` — the text the regex knobs
|
|
152
|
+
(``evidence_exclude_pattern``, ``always_show_actions``) and criterion matching see, and what the action
|
|
153
|
+
log shows, so a value past the cut cannot be matched."""
|
|
154
|
+
return f"{a.tool} {_render_json(a.args, MAX_ARGS_CHARS)}"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _action_line(a: TrajectoryAction) -> str:
|
|
158
|
+
line = f"{a.index}. {rendered_action(a)}"
|
|
159
|
+
if a.result is not None:
|
|
160
|
+
line += f" -> {_render_json(a.result, MAX_RESULT_CHARS)}"
|
|
161
|
+
return line
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _message_line(m: AgentMessage) -> str:
|
|
165
|
+
# Images first, whitespace second: collapsing whitespace inside a base64 run would break it into
|
|
166
|
+
# pieces too short for the image pattern to catch.
|
|
167
|
+
text = " ".join(strip_base64_images(m.text, _IMAGE_PLACEHOLDER).split())
|
|
168
|
+
return f'agent said: "{_cut(text, MAX_AGENT_MESSAGE_CHARS)}"'
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def action_log(actions: Iterable[TrajectoryAction], *, messages: Iterable[AgentMessage] = (),
|
|
172
|
+
max_lines: int = DEFAULT_ACTION_LOG_MAX_LINES) -> str:
|
|
173
|
+
"""The run as text: ``"<i>. <tool> <args> -> <result>"`` per tool call and, in run order between them,
|
|
174
|
+
``'agent said: "..."'`` per agent message — the tool lines are facts, the agent's lines its claims.
|
|
175
|
+
Over ``max_lines`` the head and the tail are kept (half each, the head taking the odd line) around one
|
|
176
|
+
``"... (N lines not shown) ..."`` marker, so the final actions are always in the log."""
|
|
177
|
+
pending = sorted(messages, key=lambda m: m.after_action)
|
|
178
|
+
lines: list[str] = []
|
|
179
|
+
for a in actions:
|
|
180
|
+
while pending and pending[0].after_action < a.index:
|
|
181
|
+
lines.append(_message_line(pending.pop(0)))
|
|
182
|
+
lines.append(_action_line(a))
|
|
183
|
+
lines.extend(_message_line(m) for m in pending)
|
|
184
|
+
if len(lines) <= max_lines:
|
|
185
|
+
return "\n".join(lines)
|
|
186
|
+
tail = max_lines // 2
|
|
187
|
+
head = max_lines - tail
|
|
188
|
+
hidden = len(lines) - max_lines
|
|
189
|
+
return "\n".join([*lines[:head], f"... ({hidden} lines not shown) ...", *lines[-tail:]])
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _terms(text: object) -> set[str]:
|
|
193
|
+
return {w for w in re.findall(r"[a-z0-9]{3,}", str(text or "").lower()) if w not in _CRITERION_STOPWORDS}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _criterion_id(criterion: dict) -> str:
|
|
197
|
+
return str(criterion.get("id") or "?")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def criterion_matches(criteria: list[dict], actions: list[TrajectoryAction], *,
|
|
201
|
+
exclude_pattern: re.Pattern | None = None) -> list[tuple[str, TrajectoryAction]]:
|
|
202
|
+
"""``[(criterion_id, action), ...]`` in criteria order — each criterion's best-matching action, then a
|
|
203
|
+
near-tie (>= ``_NEAR_TIE_RATIO`` of the best score) if there is one.
|
|
204
|
+
|
|
205
|
+
Terms are weighted by inverse document frequency across this task's criteria. Candidates carrying a
|
|
206
|
+
term unique to the criterion are preferred; when none does, the shared-only matches are used (weaker
|
|
207
|
+
evidence is not a reason to send none). Actions whose rendering matches ``exclude_pattern`` are skipped."""
|
|
208
|
+
docs = [_terms(c.get("description")) | _terms(c.get("id")) for c in criteria]
|
|
209
|
+
df: dict[str, int] = {}
|
|
210
|
+
for d in docs:
|
|
211
|
+
for t in d:
|
|
212
|
+
df[t] = df.get(t, 0) + 1
|
|
213
|
+
out: list[tuple[str, TrajectoryAction]] = []
|
|
214
|
+
for c, terms in zip(criteria, docs, strict=True):
|
|
215
|
+
unique: list[tuple[float, int, TrajectoryAction]] = []
|
|
216
|
+
shared: list[tuple[float, int, TrajectoryAction]] = []
|
|
217
|
+
for a in actions:
|
|
218
|
+
rendered = rendered_action(a)
|
|
219
|
+
if exclude_pattern is not None and exclude_pattern.search(rendered):
|
|
220
|
+
continue
|
|
221
|
+
hit = terms & _terms(rendered)
|
|
222
|
+
if not hit:
|
|
223
|
+
continue
|
|
224
|
+
cand = (sum(1.0 / df.get(t, 1) for t in hit), a.index, a)
|
|
225
|
+
(unique if any(df.get(t, 9) == 1 for t in hit) else shared).append(cand)
|
|
226
|
+
scored = unique or shared
|
|
227
|
+
scored.sort(key=lambda x: (-x[0], -x[1]))
|
|
228
|
+
keep = scored[:1]
|
|
229
|
+
for cand in scored[1:2]:
|
|
230
|
+
if cand[0] >= _NEAR_TIE_RATIO * scored[0][0]:
|
|
231
|
+
keep.append(cand)
|
|
232
|
+
out.extend((_criterion_id(c), a) for _, _, a in keep)
|
|
233
|
+
return out
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@dataclass(frozen=True)
|
|
237
|
+
class _Want:
|
|
238
|
+
"""One frame a criterion wants attached."""
|
|
239
|
+
|
|
240
|
+
cid: str
|
|
241
|
+
index: int
|
|
242
|
+
rank: int # 0 = the criterion's best match, 1 = its near-tie
|
|
243
|
+
effect: bool # the frame AFTER the matching action, where the action's effect shows
|
|
244
|
+
|
|
245
|
+
@property
|
|
246
|
+
def tier(self) -> int:
|
|
247
|
+
"""Drop order under budget pressure: 0 = effect frames, 1 = near-tie matches, 2 = best matches."""
|
|
248
|
+
return 0 if self.effect else (1 if self.rank else 2)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _wants(criteria: list[dict], actions: list[TrajectoryAction], has_shot: set[int],
|
|
252
|
+
exclude_pattern: re.Pattern | None) -> list[_Want]:
|
|
253
|
+
rank: dict[str, int] = {}
|
|
254
|
+
out: list[_Want] = []
|
|
255
|
+
for cid, a in criterion_matches(criteria, actions, exclude_pattern=exclude_pattern):
|
|
256
|
+
r = rank.get(cid, 0)
|
|
257
|
+
rank[cid] = r + 1
|
|
258
|
+
for j, effect in ((a.index, False), (a.index + 1, True)):
|
|
259
|
+
if j in has_shot:
|
|
260
|
+
out.append(_Want(cid, j, r, effect))
|
|
261
|
+
return out
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _pins(shots: list[TrajectoryAction], always_show: Mapping[str, re.Pattern]) -> dict[int, list[str]]:
|
|
265
|
+
"""``{action index: [labels]}`` for every action with a frame whose rendering matches a pin, labels
|
|
266
|
+
in configuration order."""
|
|
267
|
+
out: dict[int, list[str]] = {}
|
|
268
|
+
for a in shots:
|
|
269
|
+
rendered = rendered_action(a)
|
|
270
|
+
labels = [label for label, pattern in always_show.items() if pattern.search(rendered)]
|
|
271
|
+
if labels:
|
|
272
|
+
out[a.index] = labels
|
|
273
|
+
return out
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _spread(items: list[int], k: int) -> list[int]:
|
|
277
|
+
"""Up to ``k`` of ``items``, evenly spaced from the first to the last (both always kept); ``k == 1``
|
|
278
|
+
keeps the last."""
|
|
279
|
+
n = len(items)
|
|
280
|
+
if k <= 0:
|
|
281
|
+
return []
|
|
282
|
+
if k >= n:
|
|
283
|
+
return list(items)
|
|
284
|
+
if k == 1:
|
|
285
|
+
return items[-1:]
|
|
286
|
+
return [items[round(i * (n - 1) / (k - 1))] for i in range(k)]
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _trim(picked: set[int], reserved: set[int], wants: list[_Want], cids: list[str],
|
|
290
|
+
max_frames: int) -> None:
|
|
291
|
+
"""Drop criterion frames from ``picked`` until it fits ``max_frames``: lower tiers first, one frame
|
|
292
|
+
per criterion per round, so no criterion loses its best match while another keeps an effect frame.
|
|
293
|
+
A frame wanted at several tiers is only droppable at its highest one."""
|
|
294
|
+
over = len(picked) - max_frames
|
|
295
|
+
value: dict[int, int] = {}
|
|
296
|
+
for w in wants:
|
|
297
|
+
value[w.index] = max(value.get(w.index, -1), w.tier)
|
|
298
|
+
for tier in (0, 1, 2):
|
|
299
|
+
queues = {
|
|
300
|
+
cid: [w.index for w in sorted(wants, key=lambda w: -w.rank) if w.cid == cid and w.tier == tier]
|
|
301
|
+
for cid in cids
|
|
302
|
+
}
|
|
303
|
+
while over > 0 and any(queues.values()):
|
|
304
|
+
for cid in cids:
|
|
305
|
+
if over <= 0:
|
|
306
|
+
break
|
|
307
|
+
queue = queues[cid]
|
|
308
|
+
while queue:
|
|
309
|
+
j = queue.pop(0)
|
|
310
|
+
if j in picked and j not in reserved and value[j] == tier:
|
|
311
|
+
picked.remove(j)
|
|
312
|
+
over -= 1
|
|
313
|
+
break
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _pick_within_budget(shots: list[TrajectoryAction], final: set[int], pins: Mapping[int, list[str]],
|
|
317
|
+
wants: list[_Want], cids: list[str], max_frames: int) -> set[int]:
|
|
318
|
+
"""The frame indices to keep when the run exceeds ``max_frames``: the final frames and the pinned
|
|
319
|
+
frames (at most half the budget left after the final frames, spread over the run) are reserved, the
|
|
320
|
+
criterion frames are trimmed to fit (see ``_trim``), and any room left is spread over the rest of the run."""
|
|
321
|
+
pin_cap = (max_frames - len(final)) // 2
|
|
322
|
+
reserved = final | set(_spread([p for p in sorted(pins) if p not in final], pin_cap))
|
|
323
|
+
picked = reserved | {w.index for w in wants}
|
|
324
|
+
if len(picked) > max_frames:
|
|
325
|
+
_trim(picked, reserved, wants, cids, max_frames)
|
|
326
|
+
room = max_frames - len(picked)
|
|
327
|
+
cands = [a.index for a in shots if a.index not in picked]
|
|
328
|
+
if room > 0 and cands:
|
|
329
|
+
step = max(1.0, len(cands) / room)
|
|
330
|
+
for k in range(room):
|
|
331
|
+
picked.add(cands[min(int(k * step), len(cands) - 1)])
|
|
332
|
+
return picked
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _labeled_frames(actions: list[TrajectoryAction], picked: set[int], wants: list[_Want], final: set[int],
|
|
336
|
+
pins: Mapping[int, list[str]], label_ids: Mapping[str, str]) -> list[LabeledFrame]:
|
|
337
|
+
"""``picked`` in run order, each labelled ``"[cA][cB] final <pin labels> action K"`` (parts present as
|
|
338
|
+
they apply); ``label_ids`` maps a criterion id to the key written in its tag."""
|
|
339
|
+
tags: dict[int, list[str]] = {}
|
|
340
|
+
for w in wants:
|
|
341
|
+
if w.cid not in tags.setdefault(w.index, []):
|
|
342
|
+
tags[w.index].append(w.cid)
|
|
343
|
+
by_index = {a.index: a for a in actions}
|
|
344
|
+
frames = []
|
|
345
|
+
for j in sorted(picked):
|
|
346
|
+
a = by_index[j]
|
|
347
|
+
assert a.screenshot is not None
|
|
348
|
+
parts = ["".join(f"[{label_ids.get(cid, cid)}]" for cid in tags.get(j, ()))]
|
|
349
|
+
if j in final:
|
|
350
|
+
parts.append("final")
|
|
351
|
+
parts.extend(pins.get(j, ()))
|
|
352
|
+
parts.append(f"action {j}")
|
|
353
|
+
frames.append(LabeledFrame(" ".join(part for part in parts if part),
|
|
354
|
+
ImageFrame(media_type=media_type_for_b64(a.screenshot), data=a.screenshot)))
|
|
355
|
+
return frames
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def select_frames_per_criterion(actions: list[TrajectoryAction], criteria: list[dict], *,
|
|
359
|
+
max_frames: int = DEFAULT_FRAME_BUDGET,
|
|
360
|
+
exclude_pattern: re.Pattern | None = None,
|
|
361
|
+
always_show: Mapping[str, re.Pattern] | None = None,
|
|
362
|
+
label_ids: Mapping[str, str] | None = None) -> list[LabeledFrame]:
|
|
363
|
+
"""The labelled frames to attach for grading ``criteria`` from ``actions``, in run order, never more
|
|
364
|
+
than ``max_frames``. The final-frame reservation is ``FINAL_FRAMES_RESERVED``, shrunk to half the
|
|
365
|
+
budget for tiny budgets. ``always_show`` maps a pin label to a compiled regex over ``rendered_action``;
|
|
366
|
+
``label_ids`` maps a criterion id to the key written in its frame tags (the verifier passes c1..cN)."""
|
|
367
|
+
shots = [a for a in actions if a.screenshot]
|
|
368
|
+
has_shot = {a.index for a in shots}
|
|
369
|
+
n_final = max(1, min(FINAL_FRAMES_RESERVED, max_frames // 2))
|
|
370
|
+
final = {a.index for a in shots[-n_final:]}
|
|
371
|
+
pins = _pins(shots, always_show or {})
|
|
372
|
+
wants = _wants(criteria, actions, has_shot, exclude_pattern)
|
|
373
|
+
if len(shots) <= max_frames:
|
|
374
|
+
picked = has_shot
|
|
375
|
+
else:
|
|
376
|
+
picked = _pick_within_budget(shots, final, pins, wants, [_criterion_id(c) for c in criteria], max_frames)
|
|
377
|
+
return _labeled_frames(actions, picked, wants, final, pins, label_ids or {})
|