gatewaysdk 0.3.2__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.
- gatewaysdk/__init__.py +540 -0
- gatewaysdk/adapter/__init__.py +15 -0
- gatewaysdk/adapter/base.py +94 -0
- gatewaysdk/adapter/messages.py +270 -0
- gatewaysdk/adapter/triplet.py +1026 -0
- gatewaysdk/algorithm/__init__.py +39 -0
- gatewaysdk/algorithm/apo/__init__.py +5 -0
- gatewaysdk/algorithm/apo/apo.py +898 -0
- gatewaysdk/algorithm/apo/prompts/apply_edit_variant01.poml +22 -0
- gatewaysdk/algorithm/apo/prompts/apply_edit_variant02.poml +18 -0
- gatewaysdk/algorithm/apo/prompts/text_gradient_variant01.poml +18 -0
- gatewaysdk/algorithm/apo/prompts/text_gradient_variant02.poml +16 -0
- gatewaysdk/algorithm/apo/prompts/text_gradient_variant03.poml +107 -0
- gatewaysdk/algorithm/base.py +262 -0
- gatewaysdk/algorithm/decorator.py +264 -0
- gatewaysdk/algorithm/evals/__init__.py +7 -0
- gatewaysdk/algorithm/evals/evals.py +217 -0
- gatewaysdk/algorithm/fast.py +250 -0
- gatewaysdk/algorithm/gepa/__init__.py +61 -0
- gatewaysdk/algorithm/gepa/adapter.py +495 -0
- gatewaysdk/algorithm/gepa/gepa.py +570 -0
- gatewaysdk/algorithm/gepa/lib/__init__.py +18 -0
- gatewaysdk/algorithm/gepa/lib/adapters/README.md +12 -0
- gatewaysdk/algorithm/gepa/lib/adapters/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/adapters/anymaths_adapter/README.md +341 -0
- gatewaysdk/algorithm/gepa/lib/adapters/anymaths_adapter/__init__.py +1 -0
- gatewaysdk/algorithm/gepa/lib/adapters/anymaths_adapter/anymaths_adapter.py +174 -0
- gatewaysdk/algorithm/gepa/lib/adapters/anymaths_adapter/requirements.txt +1 -0
- gatewaysdk/algorithm/gepa/lib/adapters/default_adapter/README.md +0 -0
- gatewaysdk/algorithm/gepa/lib/adapters/default_adapter/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/adapters/default_adapter/default_adapter.py +209 -0
- gatewaysdk/algorithm/gepa/lib/adapters/dspy_adapter/README.md +7 -0
- gatewaysdk/algorithm/gepa/lib/adapters/dspy_adapter/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/adapters/dspy_adapter/dspy_adapter.py +307 -0
- gatewaysdk/algorithm/gepa/lib/adapters/dspy_full_program_adapter/README.md +99 -0
- gatewaysdk/algorithm/gepa/lib/adapters/dspy_full_program_adapter/dspy_program_proposal_signature.py +137 -0
- gatewaysdk/algorithm/gepa/lib/adapters/dspy_full_program_adapter/full_program_adapter.py +268 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/GEPA_RAG.md +621 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/__init__.py +56 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/evaluation_metrics.py +226 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/generic_rag_adapter.py +496 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/rag_pipeline.py +238 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_store_interface.py +212 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/__init__.py +2 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/chroma_store.py +196 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/lancedb_store.py +422 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/milvus_store.py +409 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/qdrant_store.py +368 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/weaviate_store.py +418 -0
- gatewaysdk/algorithm/gepa/lib/adapters/mcp_adapter/README.md +552 -0
- gatewaysdk/algorithm/gepa/lib/adapters/mcp_adapter/__init__.py +37 -0
- gatewaysdk/algorithm/gepa/lib/adapters/mcp_adapter/mcp_adapter.py +699 -0
- gatewaysdk/algorithm/gepa/lib/adapters/mcp_adapter/mcp_client.py +364 -0
- gatewaysdk/algorithm/gepa/lib/adapters/terminal_bench_adapter/README.md +9 -0
- gatewaysdk/algorithm/gepa/lib/adapters/terminal_bench_adapter/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/adapters/terminal_bench_adapter/terminal_bench_adapter.py +217 -0
- gatewaysdk/algorithm/gepa/lib/api.py +382 -0
- gatewaysdk/algorithm/gepa/lib/core/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/core/adapter.py +180 -0
- gatewaysdk/algorithm/gepa/lib/core/data_loader.py +74 -0
- gatewaysdk/algorithm/gepa/lib/core/engine.py +379 -0
- gatewaysdk/algorithm/gepa/lib/core/result.py +233 -0
- gatewaysdk/algorithm/gepa/lib/core/state.py +636 -0
- gatewaysdk/algorithm/gepa/lib/examples/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/examples/aime.py +24 -0
- gatewaysdk/algorithm/gepa/lib/examples/anymaths-bench/eval_default.py +111 -0
- gatewaysdk/algorithm/gepa/lib/examples/anymaths-bench/prompt-templates/instruction_prompt.txt +9 -0
- gatewaysdk/algorithm/gepa/lib/examples/anymaths-bench/prompt-templates/optimal_prompt.txt +24 -0
- gatewaysdk/algorithm/gepa/lib/examples/anymaths-bench/train_anymaths.py +177 -0
- gatewaysdk/algorithm/gepa/lib/examples/dspy_full_program_evolution/arc_agi.ipynb +25705 -0
- gatewaysdk/algorithm/gepa/lib/examples/dspy_full_program_evolution/example.ipynb +348 -0
- gatewaysdk/algorithm/gepa/lib/examples/mcp_adapter/__init__.py +4 -0
- gatewaysdk/algorithm/gepa/lib/examples/mcp_adapter/mcp_optimization_example.py +456 -0
- gatewaysdk/algorithm/gepa/lib/examples/rag_adapter/RAG_GUIDE.md +613 -0
- gatewaysdk/algorithm/gepa/lib/examples/rag_adapter/__init__.py +9 -0
- gatewaysdk/algorithm/gepa/lib/examples/rag_adapter/rag_optimization.py +820 -0
- gatewaysdk/algorithm/gepa/lib/examples/rag_adapter/requirements-rag.txt +29 -0
- gatewaysdk/algorithm/gepa/lib/examples/terminal-bench/prompt-templates/instruction_prompt.txt +16 -0
- gatewaysdk/algorithm/gepa/lib/examples/terminal-bench/prompt-templates/terminus.txt +9 -0
- gatewaysdk/algorithm/gepa/lib/examples/terminal-bench/train_terminus.py +161 -0
- gatewaysdk/algorithm/gepa/lib/gepa_utils.py +117 -0
- gatewaysdk/algorithm/gepa/lib/logging/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/logging/experiment_tracker.py +187 -0
- gatewaysdk/algorithm/gepa/lib/logging/logger.py +75 -0
- gatewaysdk/algorithm/gepa/lib/logging/utils.py +103 -0
- gatewaysdk/algorithm/gepa/lib/proposer/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/proposer/base.py +31 -0
- gatewaysdk/algorithm/gepa/lib/proposer/merge.py +357 -0
- gatewaysdk/algorithm/gepa/lib/proposer/reflective_mutation/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/proposer/reflective_mutation/base.py +49 -0
- gatewaysdk/algorithm/gepa/lib/proposer/reflective_mutation/reflective_mutation.py +176 -0
- gatewaysdk/algorithm/gepa/lib/py.typed +0 -0
- gatewaysdk/algorithm/gepa/lib/strategies/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/strategies/batch_sampler.py +77 -0
- gatewaysdk/algorithm/gepa/lib/strategies/candidate_selector.py +50 -0
- gatewaysdk/algorithm/gepa/lib/strategies/component_selector.py +36 -0
- gatewaysdk/algorithm/gepa/lib/strategies/eval_policy.py +64 -0
- gatewaysdk/algorithm/gepa/lib/strategies/instruction_proposal.py +126 -0
- gatewaysdk/algorithm/gepa/lib/utils/__init__.py +10 -0
- gatewaysdk/algorithm/gepa/lib/utils/stop_condition.py +196 -0
- gatewaysdk/algorithm/gepa/tracing.py +105 -0
- gatewaysdk/algorithm/utils.py +177 -0
- gatewaysdk/algorithm/verl/__init__.py +5 -0
- gatewaysdk/algorithm/verl/interface.py +202 -0
- gatewaysdk/automations.py +111 -0
- gatewaysdk/benchmark_hub/README.md +88 -0
- gatewaysdk/benchmark_hub/__init__.py +119 -0
- gatewaysdk/benchmark_hub/_archive.py +189 -0
- gatewaysdk/benchmark_hub/_tar.py +6 -0
- gatewaysdk/benchmark_hub/client.py +992 -0
- gatewaysdk/benchmark_hub/dispatch_shard.py +34 -0
- gatewaysdk/benchmark_hub/eval_config.py +20 -0
- gatewaysdk/benchmark_hub/evals.py +351 -0
- gatewaysdk/benchmark_hub/harbor_adapter.py +673 -0
- gatewaysdk/benchmark_hub/path_utils.py +81 -0
- gatewaysdk/benchmark_hub/save_utils.py +101 -0
- gatewaysdk/benchmark_hub/verifiers_adapter.py +327 -0
- gatewaysdk/benchmark_hub/versioning.py +72 -0
- gatewaysdk/build.py +515 -0
- gatewaysdk/cli/__init__.py +58 -0
- gatewaysdk/cli/agent_runner.py +132 -0
- gatewaysdk/cli/http_client.py +115 -0
- gatewaysdk/cli/platform.py +4086 -0
- gatewaysdk/cli/prometheus.py +115 -0
- gatewaysdk/cli/release_gate.py +215 -0
- gatewaysdk/cli/store.py +131 -0
- gatewaysdk/cli/vllm.py +29 -0
- gatewaysdk/client.py +406 -0
- gatewaysdk/config.py +348 -0
- gatewaysdk/connectors/__init__.py +25 -0
- gatewaysdk/connectors/client.py +203 -0
- gatewaysdk/connectors/skill.py +25 -0
- gatewaysdk/connectors/template.py +106 -0
- gatewaysdk/context.py +606 -0
- gatewaysdk/emitter/__init__.py +43 -0
- gatewaysdk/emitter/annotation.py +370 -0
- gatewaysdk/emitter/exception.py +54 -0
- gatewaysdk/emitter/message.py +61 -0
- gatewaysdk/emitter/object.py +117 -0
- gatewaysdk/emitter/reward.py +320 -0
- gatewaysdk/env_var.py +156 -0
- gatewaysdk/environment/__init__.py +108 -0
- gatewaysdk/environment/_bundle.py +281 -0
- gatewaysdk/environment/_harbor.py +276 -0
- gatewaysdk/environment/_materialize.py +97 -0
- gatewaysdk/environment/_tar.py +33 -0
- gatewaysdk/environment/_world.py +346 -0
- gatewaysdk/environment/core.py +527 -0
- gatewaysdk/environment/errors.py +58 -0
- gatewaysdk/environment/runtime.py +150 -0
- gatewaysdk/environment/schema/__init__.py +35 -0
- gatewaysdk/environment/schema/__main__.py +225 -0
- gatewaysdk/environment/schema/_slack_fidelity.py +116 -0
- gatewaysdk/environment/schema/_slack_scenario.py +183 -0
- gatewaysdk/environment/schema/api.py +1628 -0
- gatewaysdk/environment/schema/batch.py +755 -0
- gatewaysdk/environment/schema/compiler.py +615 -0
- gatewaysdk/environment/schema/conform.py +428 -0
- gatewaysdk/environment/schema/connector.py +777 -0
- gatewaysdk/environment/schema/connectors/apple-business-manager/handlers.py +17 -0
- gatewaysdk/environment/schema/connectors/apple-business-manager/parity.json +28 -0
- gatewaysdk/environment/schema/connectors/apple-business-manager/provenance.json +233 -0
- gatewaysdk/environment/schema/connectors/apple-business-manager/scope.toml +112 -0
- gatewaysdk/environment/schema/connectors/apple-business-manager/world.json +1380 -0
- gatewaysdk/environment/schema/connectors/base.json +28 -0
- gatewaysdk/environment/schema/connectors/custom/handlers.py +18 -0
- gatewaysdk/environment/schema/connectors/custom/world.json +123 -0
- gatewaysdk/environment/schema/connectors/github/handlers.py +81 -0
- gatewaysdk/environment/schema/connectors/github/parity.json +37 -0
- gatewaysdk/environment/schema/connectors/github/provenance.json +134 -0
- gatewaysdk/environment/schema/connectors/github/scope.toml +158 -0
- gatewaysdk/environment/schema/connectors/github/world.json +5869 -0
- gatewaysdk/environment/schema/connectors/google-calendar/handlers.py +23 -0
- gatewaysdk/environment/schema/connectors/google-calendar/world.json +209 -0
- gatewaysdk/environment/schema/connectors/google-drive/handlers.py +18 -0
- gatewaysdk/environment/schema/connectors/google-drive/world.json +286 -0
- gatewaysdk/environment/schema/connectors/jamf/handlers.py +34 -0
- gatewaysdk/environment/schema/connectors/jamf/parity.json +49 -0
- gatewaysdk/environment/schema/connectors/jamf/provenance.json +181 -0
- gatewaysdk/environment/schema/connectors/jamf/scope.toml +128 -0
- gatewaysdk/environment/schema/connectors/jamf/world.json +2218 -0
- gatewaysdk/environment/schema/connectors/jira/handlers.py +28 -0
- gatewaysdk/environment/schema/connectors/jira/world.json +389 -0
- gatewaysdk/environment/schema/connectors/kandji/handlers.py +38 -0
- gatewaysdk/environment/schema/connectors/kandji/parity.json +29 -0
- gatewaysdk/environment/schema/connectors/kandji/provenance.json +152 -0
- gatewaysdk/environment/schema/connectors/kandji/scope.toml +120 -0
- gatewaysdk/environment/schema/connectors/kandji/world.json +765 -0
- gatewaysdk/environment/schema/connectors/linear/handlers.py +20 -0
- gatewaysdk/environment/schema/connectors/linear/world.json +363 -0
- gatewaysdk/environment/schema/connectors/microsoft-teams/fidelity.json +45 -0
- gatewaysdk/environment/schema/connectors/microsoft-teams/handlers.py +1 -0
- gatewaysdk/environment/schema/connectors/microsoft-teams/provenance.json +851 -0
- gatewaysdk/environment/schema/connectors/microsoft-teams/scope.md +231 -0
- gatewaysdk/environment/schema/connectors/microsoft-teams/teams-types.json +1710 -0
- gatewaysdk/environment/schema/connectors/microsoft-teams/world.json +858 -0
- gatewaysdk/environment/schema/connectors/netsuite/handlers.py +32 -0
- gatewaysdk/environment/schema/connectors/netsuite/parity.json +50 -0
- gatewaysdk/environment/schema/connectors/netsuite/provenance.json +137 -0
- gatewaysdk/environment/schema/connectors/netsuite/scope.toml +78 -0
- gatewaysdk/environment/schema/connectors/netsuite/world.json +16243 -0
- gatewaysdk/environment/schema/connectors/salesforce/handlers.py +806 -0
- gatewaysdk/environment/schema/connectors/salesforce/parity.json +46 -0
- gatewaysdk/environment/schema/connectors/salesforce/provenance.json +143 -0
- gatewaysdk/environment/schema/connectors/salesforce/scope.toml +174 -0
- gatewaysdk/environment/schema/connectors/salesforce/world.json +4695 -0
- gatewaysdk/environment/schema/connectors/slack/MODEL.md +236 -0
- gatewaysdk/environment/schema/connectors/slack/capabilities.json +1039 -0
- gatewaysdk/environment/schema/connectors/slack/handlers.py +762 -0
- gatewaysdk/environment/schema/connectors/slack/parity.json +65 -0
- gatewaysdk/environment/schema/connectors/slack/provenance.json +294 -0
- gatewaysdk/environment/schema/connectors/slack/scope.toml +44 -0
- gatewaysdk/environment/schema/connectors/slack/world.json +4993 -0
- gatewaysdk/environment/schema/connectors/workday/handlers.py +75 -0
- gatewaysdk/environment/schema/connectors/workday/parity.json +36 -0
- gatewaysdk/environment/schema/connectors/workday/provenance.json +179 -0
- gatewaysdk/environment/schema/connectors/workday/scope.toml +180 -0
- gatewaysdk/environment/schema/connectors/workday/world.json +806 -0
- gatewaysdk/environment/schema/data.py +593 -0
- gatewaysdk/environment/schema/extract.py +263 -0
- gatewaysdk/environment/schema/host.py +2437 -0
- gatewaysdk/environment/schema/host_surfaces.py +275 -0
- gatewaysdk/environment/schema/platform.py +1272 -0
- gatewaysdk/environment/schema/scaffold.py +250 -0
- gatewaysdk/environment/schema/skill.py +118 -0
- gatewaysdk/environment/schema/skills/agents/connector-template-author.md +202 -0
- gatewaysdk/environment/schema/skills/connector-schema-authoring/CONNECTORS.md +331 -0
- gatewaysdk/environment/schema/skills/connector-schema-authoring/CONVENTIONS.md +100 -0
- gatewaysdk/environment/schema/skills/connector-schema-authoring/SKILL.md +313 -0
- gatewaysdk/environment/schema/skills/world-data-ingestion/ROWS.md +223 -0
- gatewaysdk/environment/schema/skills/world-data-ingestion/SKILL.md +293 -0
- gatewaysdk/environment/schema/skills/worlds-getting-started/SKILL.md +390 -0
- gatewaysdk/environment/schema/slack.py +757 -0
- gatewaysdk/environment/schema/snapshot.py +479 -0
- gatewaysdk/environment/schema/store.py +904 -0
- gatewaysdk/environment/schema/tasks.py +525 -0
- gatewaysdk/environment/schema/tools_world.py +322 -0
- gatewaysdk/environment/schema/validation.py +577 -0
- gatewaysdk/environment/schema/worker.py +78 -0
- gatewaysdk/environment/schema/workers.py +500 -0
- gatewaysdk/environment/schema/world_tests.py +336 -0
- gatewaysdk/environments/__init__.py +24 -0
- gatewaysdk/environments/client.py +486 -0
- gatewaysdk/environments/types.py +318 -0
- gatewaysdk/execution/__init__.py +15 -0
- gatewaysdk/execution/base.py +64 -0
- gatewaysdk/execution/client_server.py +443 -0
- gatewaysdk/execution/events.py +69 -0
- gatewaysdk/execution/inter_process.py +16 -0
- gatewaysdk/execution/shared_memory.py +282 -0
- gatewaysdk/experiments/__init__.py +90 -0
- gatewaysdk/experiments/assignment.py +177 -0
- gatewaysdk/experiments/client.py +877 -0
- gatewaysdk/experiments/exposure.py +222 -0
- gatewaysdk/experiments/types.py +81 -0
- gatewaysdk/importers/__init__.py +49 -0
- gatewaysdk/importers/_normalize.py +91 -0
- gatewaysdk/importers/builder.py +173 -0
- gatewaysdk/importers/client.py +95 -0
- gatewaysdk/importers/langsmith.py +196 -0
- gatewaysdk/importers/recipes.py +249 -0
- gatewaysdk/instrumentation/__init__.py +300 -0
- gatewaysdk/instrumentation/agentops.py +314 -0
- gatewaysdk/instrumentation/agentops_langchain.py +45 -0
- gatewaysdk/instrumentation/base.py +119 -0
- gatewaysdk/instrumentation/litellm.py +83 -0
- gatewaysdk/instrumentation/registry.py +273 -0
- gatewaysdk/instrumentation/vllm.py +81 -0
- gatewaysdk/instrumentation/weave.py +500 -0
- gatewaysdk/integrations/__init__.py +15 -0
- gatewaysdk/integrations/gateway/__init__.py +11 -0
- gatewaysdk/integrations/gateway/client.py +171 -0
- gatewaysdk/integrations/tool_access.py +549 -0
- gatewaysdk/litagent/__init__.py +11 -0
- gatewaysdk/litagent/decorator.py +536 -0
- gatewaysdk/litagent/litagent.py +252 -0
- gatewaysdk/llm_proxy.py +1742 -0
- gatewaysdk/logging.py +370 -0
- gatewaysdk/memory.py +278 -0
- gatewaysdk/personas.py +186 -0
- gatewaysdk/platform/__init__.py +17 -0
- gatewaysdk/platform/builder.py +221 -0
- gatewaysdk/platform/compatibility.py +67 -0
- gatewaysdk/platform/manifest.py +185 -0
- gatewaysdk/platform/orchestrator.py +901 -0
- gatewaysdk/platform/registry.py +122 -0
- gatewaysdk/platform/worker.py +864 -0
- gatewaysdk/replay/__init__.py +1032 -0
- gatewaysdk/replay/pytest.py +56 -0
- gatewaysdk/reward.py +7 -0
- gatewaysdk/run.py +1781 -0
- gatewaysdk/runner/__init__.py +11 -0
- gatewaysdk/runner/agent.py +878 -0
- gatewaysdk/runner/base.py +182 -0
- gatewaysdk/runner/legacy.py +309 -0
- gatewaysdk/security.py +700 -0
- gatewaysdk/semconv.py +170 -0
- gatewaysdk/server.py +399 -0
- gatewaysdk/sessions.py +282 -0
- gatewaysdk/store/__init__.py +45 -0
- gatewaysdk/store/base.py +908 -0
- gatewaysdk/store/client_server.py +2093 -0
- gatewaysdk/store/collection/__init__.py +30 -0
- gatewaysdk/store/collection/base.py +587 -0
- gatewaysdk/store/collection/memory.py +970 -0
- gatewaysdk/store/collection/mongo.py +1412 -0
- gatewaysdk/store/collection_based.py +1823 -0
- gatewaysdk/store/gateway.py +983 -0
- gatewaysdk/store/gateway_listener.py +465 -0
- gatewaysdk/store/listener.py +58 -0
- gatewaysdk/store/memory.py +396 -0
- gatewaysdk/store/mongo.py +165 -0
- gatewaysdk/store/redis_stream.py +517 -0
- gatewaysdk/store/sqlite.py +3 -0
- gatewaysdk/store/threading.py +370 -0
- gatewaysdk/store/utils.py +142 -0
- gatewaysdk/tracer/__init__.py +14 -0
- gatewaysdk/tracer/base.py +286 -0
- gatewaysdk/tracer/dummy.py +106 -0
- gatewaysdk/tracer/otel.py +559 -0
- gatewaysdk/tracing/__init__.py +110 -0
- gatewaysdk/tracing/api.py +808 -0
- gatewaysdk/tracing/attributes.py +9 -0
- gatewaysdk/tracing/context.py +272 -0
- gatewaysdk/tracing/exporters/__init__.py +10 -0
- gatewaysdk/tracing/exporters/gateway.py +228 -0
- gatewaysdk/tracing/identity.py +288 -0
- gatewaysdk/tracing/init.py +620 -0
- gatewaysdk/tracing/instrumentors/__init__.py +15 -0
- gatewaysdk/tracing/instrumentors/claude_agent_sdk.py +766 -0
- gatewaysdk/tracing/instrumentors/instrumentation_principles.md +294 -0
- gatewaysdk/tracing/instrumentors/registry.py +352 -0
- gatewaysdk/tracing/mapping.py +729 -0
- gatewaysdk/tracing/processors.py +393 -0
- gatewaysdk/tracing/push.py +617 -0
- gatewaysdk/tracing/push_models.py +247 -0
- gatewaysdk/tracing/semconv.py +294 -0
- gatewaysdk/tracing/span_builder.py +356 -0
- gatewaysdk/trainer/__init__.py +6 -0
- gatewaysdk/trainer/init_utils.py +263 -0
- gatewaysdk/trainer/legacy.py +359 -0
- gatewaysdk/trainer/registry.py +12 -0
- gatewaysdk/trainer/trainer.py +638 -0
- gatewaysdk/types/__init__.py +63 -0
- gatewaysdk/types/core.py +556 -0
- gatewaysdk/types/resources.py +204 -0
- gatewaysdk/types/tracer.py +515 -0
- gatewaysdk/types/tracing.py +162 -0
- gatewaysdk/users.py +251 -0
- gatewaysdk/utils/__init__.py +1 -0
- gatewaysdk/utils/id.py +18 -0
- gatewaysdk/utils/metrics.py +1025 -0
- gatewaysdk/utils/otel.py +550 -0
- gatewaysdk/utils/otlp.py +556 -0
- gatewaysdk/utils/redact.py +22 -0
- gatewaysdk/utils/server_launcher.py +1045 -0
- gatewaysdk/utils/system_snapshot.py +90 -0
- gatewaysdk/verl/__init__.py +8 -0
- gatewaysdk/verl/__main__.py +6 -0
- gatewaysdk/verl/async_server.py +46 -0
- gatewaysdk/verl/config.yaml +27 -0
- gatewaysdk/verl/daemon.py +1154 -0
- gatewaysdk/verl/dataset.py +44 -0
- gatewaysdk/verl/entrypoint.py +248 -0
- gatewaysdk/verl/trainer.py +549 -0
- gatewaysdk/world_browser.py +748 -0
- gatewaysdk/world_data.py +453 -0
- gatewaysdk/world_sessions.py +978 -0
- gatewaysdk/world_tasks.py +285 -0
- gatewaysdk/world_tools.py +115 -0
- gatewaysdk/worlds.py +639 -0
- gatewaysdk-0.3.2.dist-info/METADATA +236 -0
- gatewaysdk-0.3.2.dist-info/RECORD +376 -0
- gatewaysdk-0.3.2.dist-info/WHEEL +4 -0
- gatewaysdk-0.3.2.dist-info/entry_points.txt +5 -0
- gatewaysdk-0.3.2.dist-info/licenses/LICENSE +19 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
"""Session tasks: a task supplied at open instead of read from the bundle.
|
|
2
|
+
|
|
3
|
+
A bundled task is ``scenarios/<name>/`` in the version (instruction, seed
|
|
4
|
+
overlay, ``checks/verify.py``). A session task is the same three things sent
|
|
5
|
+
with the open, for one session, so one published api world serves any number
|
|
6
|
+
of end users each with their own goal, starting rows and pass criteria.
|
|
7
|
+
|
|
8
|
+
This module is the host's half of the contract whose other half is
|
|
9
|
+
``gateway/packages/shared/src/domain/worldTasks.ts`` (the platform validates
|
|
10
|
+
with Zod, the host validates again here: it trusts nothing it did not check).
|
|
11
|
+
Keep the two in step by hand; the limits and the check grammar are the same.
|
|
12
|
+
|
|
13
|
+
* :func:`parse_task_spec` — the spec, checked and normalised.
|
|
14
|
+
* :func:`write_task_dir` — the spec on disk under the session
|
|
15
|
+
(``task.json``, ``instruction.md``, ``checks/verify.py``), where a grade
|
|
16
|
+
and a restart find it.
|
|
17
|
+
* :func:`evaluate_assertions` — the declarative grader, pure Python over
|
|
18
|
+
the state dump, so a check cannot read outside its session.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import re
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any, Iterable
|
|
28
|
+
|
|
29
|
+
from .validation import fail
|
|
30
|
+
|
|
31
|
+
# The caps, as the platform's WORLD_TASK_LIMITS.
|
|
32
|
+
NAME_CHARS = 120
|
|
33
|
+
INSTRUCTION_BYTES = 32 * 1024
|
|
34
|
+
METADATA_BYTES = 4 * 1024
|
|
35
|
+
SEED_ROWS = 5_000
|
|
36
|
+
SEED_BYTES = 4 * 1024 * 1024
|
|
37
|
+
CHECKS = 200
|
|
38
|
+
PYTHON_SOURCE_BYTES = 64 * 1024
|
|
39
|
+
PYTHON_TIMEOUT_S = 30.0
|
|
40
|
+
RUBRIC_BYTES = 16 * 1024
|
|
41
|
+
RUBRIC_CRITERIA = 50
|
|
42
|
+
RUBRIC_CRITERION_BYTES = 2 * 1024
|
|
43
|
+
TOOLS = 200
|
|
44
|
+
TOOL_NAME_CHARS = 200
|
|
45
|
+
|
|
46
|
+
SEED_MODES = ("append", "replace")
|
|
47
|
+
ASSERTIONS = ("exists", "absent", "count", "all", "any")
|
|
48
|
+
FIELD_OPS = ("eq", "neq", "in", "gt", "gte", "lt", "lte", "contains")
|
|
49
|
+
_NAME = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]*$")
|
|
50
|
+
_CHECK_ID = re.compile(r"^[A-Za-z0-9_.-]+$")
|
|
51
|
+
_CRITERION_ID = re.compile(r"^[A-Za-z0-9_][A-Za-z0-9_.-]*$")
|
|
52
|
+
_TOOL_NAME = re.compile(r"^[A-Za-z0-9_][A-Za-z0-9_.:-]*$")
|
|
53
|
+
|
|
54
|
+
TASK_DIR = "task"
|
|
55
|
+
TASK_FILE = "task.json"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _bytes(value: Any) -> int:
|
|
59
|
+
text = value if isinstance(value, str) else json.dumps(value, separators=(",", ":"))
|
|
60
|
+
return len(text.encode("utf-8"))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _scalar(value: Any) -> bool:
|
|
64
|
+
return value is None or isinstance(value, (str, bool, int, float))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class TaskSpec:
|
|
69
|
+
"""A validated session task. ``raw`` is the normalised spec as a dict: what
|
|
70
|
+
the platform sent, defaults filled, and what is written to ``task.json``
|
|
71
|
+
and compared when an open is retried."""
|
|
72
|
+
|
|
73
|
+
instruction: str
|
|
74
|
+
name: str | None
|
|
75
|
+
seed: dict | None
|
|
76
|
+
grader: dict | None
|
|
77
|
+
metadata: dict
|
|
78
|
+
raw: dict
|
|
79
|
+
#: The tools the session may call, or None for every tool the world serves.
|
|
80
|
+
tools: tuple[str, ...] | None = None
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def grader_kind(self) -> str | None:
|
|
84
|
+
return self.grader["kind"] if self.grader else None
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def graded_on_platform(self) -> bool:
|
|
88
|
+
"""A rubric task is judged by the platform's inference path, never here."""
|
|
89
|
+
return self.grader_kind == "rubric"
|
|
90
|
+
|
|
91
|
+
def allows_tool(self, tool: str) -> bool:
|
|
92
|
+
return self.tools is None or tool in self.tools
|
|
93
|
+
|
|
94
|
+
def entities(self) -> list[str]:
|
|
95
|
+
"""Every entity the task names, seed rows and assertion checks alike, in
|
|
96
|
+
order of first mention. A world that defines none of a name cannot start
|
|
97
|
+
or grade this task: a seed row has no table, and a check over a table
|
|
98
|
+
that does not exist selects no rows, which an ``absent`` or ``count:
|
|
99
|
+
eq 0`` check would pass on a typo."""
|
|
100
|
+
named: list[str] = []
|
|
101
|
+
if self.seed is not None:
|
|
102
|
+
named.extend(self.seed["rows"])
|
|
103
|
+
if self.grader is not None and self.grader["kind"] == "assertions":
|
|
104
|
+
named.extend(check["entity"] for check in self.grader["checks"])
|
|
105
|
+
return list(dict.fromkeys(named))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def unknown_task_entities(spec: TaskSpec, defined: Iterable[str]) -> list[str]:
|
|
109
|
+
"""The entities ``spec`` names that ``defined`` (a world contract's) lacks."""
|
|
110
|
+
known = set(defined)
|
|
111
|
+
return [entity for entity in spec.entities() if entity not in known]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def parse_task_spec(value: Any) -> TaskSpec:
|
|
115
|
+
"""The spec checked for shape and size; every refusal names its path."""
|
|
116
|
+
if not isinstance(value, dict):
|
|
117
|
+
fail("task", "a task is a json object", "/task")
|
|
118
|
+
unknown = set(value) - {"name", "instruction", "seed", "grader", "tools", "metadata"}
|
|
119
|
+
if unknown:
|
|
120
|
+
fail("task", f"unknown task field(s): {', '.join(sorted(unknown))}", "/task")
|
|
121
|
+
name = value.get("name")
|
|
122
|
+
if name is not None and (not isinstance(name, str) or not _NAME.match(name) or len(name) > NAME_CHARS):
|
|
123
|
+
fail("task", "a task name is letters, digits, _ . - (leading letter or digit)", "/task/name")
|
|
124
|
+
instruction = value.get("instruction")
|
|
125
|
+
if not isinstance(instruction, str) or not instruction:
|
|
126
|
+
fail("task", "a task needs an instruction", "/task/instruction")
|
|
127
|
+
if _bytes(instruction) > INSTRUCTION_BYTES:
|
|
128
|
+
fail("task", f"an instruction is at most {INSTRUCTION_BYTES} bytes", "/task/instruction")
|
|
129
|
+
seed = _parse_seed(value.get("seed"))
|
|
130
|
+
grader = _parse_grader(value.get("grader"))
|
|
131
|
+
tools = _parse_tools(value.get("tools"))
|
|
132
|
+
metadata = value.get("metadata")
|
|
133
|
+
if metadata is None:
|
|
134
|
+
metadata = {}
|
|
135
|
+
if not isinstance(metadata, dict) or any(not isinstance(k, str) or not k for k in metadata):
|
|
136
|
+
fail("task", "metadata is a json object with string keys", "/task/metadata")
|
|
137
|
+
if _bytes(metadata) > METADATA_BYTES:
|
|
138
|
+
fail("task", f"task metadata is at most {METADATA_BYTES} bytes of json", "/task/metadata")
|
|
139
|
+
raw: dict = {"instruction": instruction}
|
|
140
|
+
if name is not None:
|
|
141
|
+
raw["name"] = name
|
|
142
|
+
if seed is not None:
|
|
143
|
+
raw["seed"] = seed
|
|
144
|
+
if grader is not None:
|
|
145
|
+
raw["grader"] = grader
|
|
146
|
+
if tools is not None:
|
|
147
|
+
raw["tools"] = list(tools)
|
|
148
|
+
if metadata:
|
|
149
|
+
raw["metadata"] = metadata
|
|
150
|
+
return TaskSpec(
|
|
151
|
+
instruction=instruction, name=name, seed=seed, grader=grader, metadata=metadata, raw=raw, tools=tools
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _parse_tools(tools: Any) -> tuple[str, ...] | None:
|
|
156
|
+
if tools is None:
|
|
157
|
+
return None
|
|
158
|
+
if not isinstance(tools, list) or not tools:
|
|
159
|
+
fail("task", "tools is a non-empty list of tool names", "/task/tools")
|
|
160
|
+
if len(tools) > TOOLS:
|
|
161
|
+
fail("task", f"a task allows at most {TOOLS} tools", "/task/tools")
|
|
162
|
+
for i, tool in enumerate(tools):
|
|
163
|
+
if not isinstance(tool, str) or not _TOOL_NAME.match(tool) or len(tool) > TOOL_NAME_CHARS:
|
|
164
|
+
fail("task", "a tool name is letters, digits, _ . : -", f"/task/tools/{i}")
|
|
165
|
+
if len(set(tools)) != len(tools):
|
|
166
|
+
fail("task", "a tool is listed once", "/task/tools")
|
|
167
|
+
return tuple(tools)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def unknown_task_tools(spec: TaskSpec, served: Iterable[str]) -> list[str]:
|
|
171
|
+
"""The tools ``spec`` allows that the world does not serve. A name the
|
|
172
|
+
world never had would make the allow-list smaller than the author meant
|
|
173
|
+
without anyone noticing, so it is refused at open."""
|
|
174
|
+
if spec.tools is None:
|
|
175
|
+
return []
|
|
176
|
+
known = set(served)
|
|
177
|
+
return [tool for tool in spec.tools if tool not in known]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _parse_seed(seed: Any) -> dict | None:
|
|
181
|
+
if seed is None:
|
|
182
|
+
return None
|
|
183
|
+
if not isinstance(seed, dict) or set(seed) - {"mode", "rows"}:
|
|
184
|
+
fail("task", "seed is {mode?: append|replace, rows: {entity: [row, ...]}}", "/task/seed")
|
|
185
|
+
mode = seed.get("mode", "append")
|
|
186
|
+
if mode not in SEED_MODES:
|
|
187
|
+
fail("task", f"seed mode must be one of {', '.join(SEED_MODES)}", "/task/seed/mode")
|
|
188
|
+
rows = seed.get("rows")
|
|
189
|
+
if not isinstance(rows, dict):
|
|
190
|
+
fail("task", "seed.rows is {entity: [row, ...]}", "/task/seed/rows")
|
|
191
|
+
total = 0
|
|
192
|
+
for entity, entity_rows in rows.items():
|
|
193
|
+
if not isinstance(entity, str) or not entity or len(entity) > 120:
|
|
194
|
+
fail("task", "an entity name is a string of up to 120 characters", "/task/seed/rows")
|
|
195
|
+
if not isinstance(entity_rows, list) or any(not isinstance(r, dict) for r in entity_rows):
|
|
196
|
+
fail("task", f"seed.rows.{entity} is a list of row objects", f"/task/seed/rows/{entity}")
|
|
197
|
+
total += len(entity_rows)
|
|
198
|
+
if total > SEED_ROWS:
|
|
199
|
+
fail("task", f"a task seed carries at most {SEED_ROWS} rows (got {total})", "/task/seed/rows")
|
|
200
|
+
if _bytes(rows) > SEED_BYTES:
|
|
201
|
+
fail("task", f"a task seed is at most {SEED_BYTES} bytes of json", "/task/seed/rows")
|
|
202
|
+
return {"mode": mode, "rows": rows}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _parse_grader(grader: Any) -> dict | None:
|
|
206
|
+
if grader is None:
|
|
207
|
+
return None
|
|
208
|
+
if not isinstance(grader, dict):
|
|
209
|
+
fail("task", "grader is a json object with a kind", "/task/grader")
|
|
210
|
+
kind = grader.get("kind")
|
|
211
|
+
if kind == "python":
|
|
212
|
+
if set(grader) - {"kind", "source", "timeoutSeconds"}:
|
|
213
|
+
fail("task", "a python grader is {kind, source, timeoutSeconds?}", "/task/grader")
|
|
214
|
+
source = grader.get("source")
|
|
215
|
+
if not isinstance(source, str) or not source:
|
|
216
|
+
fail("task", "a python grader needs its verify.py source", "/task/grader/source")
|
|
217
|
+
if _bytes(source) > PYTHON_SOURCE_BYTES:
|
|
218
|
+
fail("task", f"a python grader is at most {PYTHON_SOURCE_BYTES} bytes", "/task/grader/source")
|
|
219
|
+
out: dict = {"kind": "python", "source": source}
|
|
220
|
+
timeout = grader.get("timeoutSeconds")
|
|
221
|
+
if timeout is not None:
|
|
222
|
+
if (
|
|
223
|
+
isinstance(timeout, bool)
|
|
224
|
+
or not isinstance(timeout, (int, float))
|
|
225
|
+
or not 0 < timeout <= PYTHON_TIMEOUT_S
|
|
226
|
+
):
|
|
227
|
+
fail("task", f"timeoutSeconds is a number in (0, {PYTHON_TIMEOUT_S:g}]", "/task/grader/timeoutSeconds")
|
|
228
|
+
out["timeoutSeconds"] = float(timeout)
|
|
229
|
+
return out
|
|
230
|
+
if kind == "assertions":
|
|
231
|
+
if set(grader) - {"kind", "checks"}:
|
|
232
|
+
fail("task", "an assertions grader is {kind, checks}", "/task/grader")
|
|
233
|
+
checks = grader.get("checks")
|
|
234
|
+
if not isinstance(checks, list) or not checks:
|
|
235
|
+
fail("task", "an assertions grader needs at least one check", "/task/grader/checks")
|
|
236
|
+
if len(checks) > CHECKS:
|
|
237
|
+
fail("task", f"an assertions grader has at most {CHECKS} checks", "/task/grader/checks")
|
|
238
|
+
parsed = [_parse_check(check, f"/task/grader/checks/{i}") for i, check in enumerate(checks)]
|
|
239
|
+
# Every check owns one reward key, its id or its position; the keys are
|
|
240
|
+
# distinct as a set, so an explicit `check_2` cannot shadow the third
|
|
241
|
+
# check's positional one. Mirrors WorldTaskAssertionsGraderSchema.
|
|
242
|
+
seen: set[str] = set()
|
|
243
|
+
for i, check in enumerate(parsed):
|
|
244
|
+
key = check_key(check, i)
|
|
245
|
+
if key in seen:
|
|
246
|
+
if "id" in check:
|
|
247
|
+
fail(
|
|
248
|
+
"task",
|
|
249
|
+
f"check id '{key}' is used twice (an id of the form check_<n> is also the positional key of check n)",
|
|
250
|
+
f"/task/grader/checks/{i}/id",
|
|
251
|
+
)
|
|
252
|
+
fail(
|
|
253
|
+
"task",
|
|
254
|
+
f"check {i} has no id and its positional key '{key}' is taken by another check's id",
|
|
255
|
+
f"/task/grader/checks/{i}/id",
|
|
256
|
+
)
|
|
257
|
+
seen.add(key)
|
|
258
|
+
return {"kind": "assertions", "checks": parsed}
|
|
259
|
+
if kind == "rubric":
|
|
260
|
+
return _parse_rubric(grader)
|
|
261
|
+
fail("task", "grader.kind must be assertions, python or rubric", "/task/grader/kind")
|
|
262
|
+
raise AssertionError # fail() never returns
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _parse_rubric(grader: dict) -> dict:
|
|
266
|
+
"""An LLM judge's brief. Checked here for shape only: the host stores it
|
|
267
|
+
with the task and never runs it; the platform grades it."""
|
|
268
|
+
if set(grader) - {"kind", "rubric", "criteria", "model"}:
|
|
269
|
+
fail("task", "a rubric grader is {kind, rubric, criteria?, model?}", "/task/grader")
|
|
270
|
+
rubric = grader.get("rubric")
|
|
271
|
+
if not isinstance(rubric, str) or not rubric:
|
|
272
|
+
fail("task", "a rubric grader needs its rubric text", "/task/grader/rubric")
|
|
273
|
+
if _bytes(rubric) > RUBRIC_BYTES:
|
|
274
|
+
fail("task", f"a rubric is at most {RUBRIC_BYTES} bytes", "/task/grader/rubric")
|
|
275
|
+
out: dict = {"kind": "rubric", "rubric": rubric}
|
|
276
|
+
criteria = grader.get("criteria")
|
|
277
|
+
if criteria is not None:
|
|
278
|
+
if not isinstance(criteria, list) or not criteria:
|
|
279
|
+
fail("task", "criteria is a non-empty list", "/task/grader/criteria")
|
|
280
|
+
if len(criteria) > RUBRIC_CRITERIA:
|
|
281
|
+
fail("task", f"a rubric has at most {RUBRIC_CRITERIA} criteria", "/task/grader/criteria")
|
|
282
|
+
parsed = []
|
|
283
|
+
seen: set[str] = set()
|
|
284
|
+
for i, criterion in enumerate(criteria):
|
|
285
|
+
at = f"/task/grader/criteria/{i}"
|
|
286
|
+
if not isinstance(criterion, dict) or set(criterion) - {"id", "description", "weight"}:
|
|
287
|
+
fail("task", "a criterion is {id, description, weight?}", at)
|
|
288
|
+
criterion_id = criterion.get("id")
|
|
289
|
+
if not isinstance(criterion_id, str) or not _CRITERION_ID.match(criterion_id) or len(criterion_id) > 120:
|
|
290
|
+
fail("task", "a criterion id is letters, digits, _ . -", f"{at}/id")
|
|
291
|
+
if criterion_id in seen:
|
|
292
|
+
fail("task", f"criterion id '{criterion_id}' is used twice", f"{at}/id")
|
|
293
|
+
seen.add(criterion_id)
|
|
294
|
+
description = criterion.get("description")
|
|
295
|
+
if not isinstance(description, str) or not description:
|
|
296
|
+
fail("task", "a criterion needs a description", f"{at}/description")
|
|
297
|
+
if _bytes(description) > RUBRIC_CRITERION_BYTES:
|
|
298
|
+
fail("task", f"a criterion is at most {RUBRIC_CRITERION_BYTES} bytes", f"{at}/description")
|
|
299
|
+
item: dict = {"id": criterion_id, "description": description}
|
|
300
|
+
weight = criterion.get("weight")
|
|
301
|
+
if weight is not None:
|
|
302
|
+
if isinstance(weight, bool) or not isinstance(weight, (int, float)) or not weight > 0:
|
|
303
|
+
fail("task", "weight is a positive number", f"{at}/weight")
|
|
304
|
+
item["weight"] = float(weight)
|
|
305
|
+
parsed.append(item)
|
|
306
|
+
out["criteria"] = parsed
|
|
307
|
+
model = grader.get("model")
|
|
308
|
+
if model is not None:
|
|
309
|
+
if not isinstance(model, dict) or set(model) != {"provider", "model"}:
|
|
310
|
+
fail("task", "model is {provider, model}", "/task/grader/model")
|
|
311
|
+
provider, name = model["provider"], model["model"]
|
|
312
|
+
if not isinstance(provider, str) or not 0 < len(provider) <= 120:
|
|
313
|
+
fail("task", "model.provider is a string of up to 120 characters", "/task/grader/model/provider")
|
|
314
|
+
if not isinstance(name, str) or not 0 < len(name) <= 200:
|
|
315
|
+
fail("task", "model.model is a string of up to 200 characters", "/task/grader/model/model")
|
|
316
|
+
out["model"] = {"provider": provider, "model": name}
|
|
317
|
+
return out
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def check_key(check: dict, index: int) -> str:
|
|
321
|
+
"""The reward key of a check: its id, else its position as ``check_<i>``."""
|
|
322
|
+
return check.get("id") or f"check_{index}"
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _parse_check(check: Any, at: str) -> dict:
|
|
326
|
+
if not isinstance(check, dict):
|
|
327
|
+
fail("task", "a check is a json object", at)
|
|
328
|
+
unknown = set(check) - {"id", "weight", "entity", "where", "assert", "count", "field", "op", "value"}
|
|
329
|
+
if unknown:
|
|
330
|
+
fail("task", f"unknown check field(s): {', '.join(sorted(unknown))}", at)
|
|
331
|
+
out: dict = {}
|
|
332
|
+
check_id = check.get("id")
|
|
333
|
+
if check_id is not None:
|
|
334
|
+
if not isinstance(check_id, str) or not _CHECK_ID.match(check_id) or len(check_id) > 120:
|
|
335
|
+
fail("task", "a check id is letters, digits, _ . -", f"{at}/id")
|
|
336
|
+
out["id"] = check_id
|
|
337
|
+
weight = check.get("weight", 1)
|
|
338
|
+
if isinstance(weight, bool) or not isinstance(weight, (int, float)) or not weight > 0 or weight == float("inf"):
|
|
339
|
+
fail("task", "weight is a positive number", f"{at}/weight")
|
|
340
|
+
out["weight"] = float(weight)
|
|
341
|
+
entity = check.get("entity")
|
|
342
|
+
if not isinstance(entity, str) or not entity or len(entity) > 120:
|
|
343
|
+
fail("task", "a check names an entity", f"{at}/entity")
|
|
344
|
+
out["entity"] = entity
|
|
345
|
+
where = check.get("where")
|
|
346
|
+
if where is not None:
|
|
347
|
+
if not isinstance(where, dict) or any(
|
|
348
|
+
not isinstance(k, str) or not k or not _scalar(v) for k, v in where.items()
|
|
349
|
+
):
|
|
350
|
+
fail("task", "where is {field: scalar, ...}", f"{at}/where")
|
|
351
|
+
out["where"] = where
|
|
352
|
+
assertion = check.get("assert", "exists")
|
|
353
|
+
if assertion not in ASSERTIONS:
|
|
354
|
+
fail("task", f"assert must be one of {', '.join(ASSERTIONS)}", f"{at}/assert")
|
|
355
|
+
out["assert"] = assertion
|
|
356
|
+
count = check.get("count")
|
|
357
|
+
if assertion == "count":
|
|
358
|
+
if not isinstance(count, dict) or set(count) - {"eq", "gte", "lte"} or not count:
|
|
359
|
+
fail("task", "assert: count needs a count bound {eq|gte|lte: n}", f"{at}/count")
|
|
360
|
+
for key, bound in count.items():
|
|
361
|
+
if isinstance(bound, bool) or not isinstance(bound, int) or bound < 0:
|
|
362
|
+
fail("task", f"count.{key} is a non-negative integer", f"{at}/count/{key}")
|
|
363
|
+
out["count"] = count
|
|
364
|
+
elif count is not None:
|
|
365
|
+
fail("task", "count only goes with assert: count", f"{at}/count")
|
|
366
|
+
field = check.get("field")
|
|
367
|
+
op = check.get("op", "eq")
|
|
368
|
+
value = check.get("value")
|
|
369
|
+
if assertion in ("all", "any"):
|
|
370
|
+
if not isinstance(field, str) or not field:
|
|
371
|
+
fail("task", f"assert: {assertion} needs a field to test", f"{at}/field")
|
|
372
|
+
if op not in FIELD_OPS:
|
|
373
|
+
fail("task", f"op must be one of {', '.join(FIELD_OPS)}", f"{at}/op")
|
|
374
|
+
if "value" not in check:
|
|
375
|
+
fail("task", f"assert: {assertion} needs a value to compare with", f"{at}/value")
|
|
376
|
+
if op == "in":
|
|
377
|
+
if not isinstance(value, list) or any(not _scalar(v) for v in value):
|
|
378
|
+
fail("task", "op: in compares against a list of scalars", f"{at}/value")
|
|
379
|
+
elif not _scalar(value):
|
|
380
|
+
fail("task", f"op: {op} compares against one scalar", f"{at}/value")
|
|
381
|
+
out["field"], out["op"], out["value"] = field, op, value
|
|
382
|
+
elif field is not None or "value" in check or "op" in check:
|
|
383
|
+
fail("task", "field/op/value only go with assert: all or any", f"{at}/field")
|
|
384
|
+
return out
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
# --------------------------------------------------------------------------- #
|
|
388
|
+
# On disk
|
|
389
|
+
# --------------------------------------------------------------------------- #
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def write_task_dir(spec: TaskSpec, session_dir: Path) -> Path:
|
|
393
|
+
"""The task under the session: ``task.json`` (the whole spec),
|
|
394
|
+
``instruction.md``, and for a python grader ``checks/verify.py`` where
|
|
395
|
+
the bundled layout has it, so the one verifier path runs both."""
|
|
396
|
+
root = Path(session_dir) / TASK_DIR
|
|
397
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
398
|
+
(root / TASK_FILE).write_text(json.dumps(spec.raw, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
399
|
+
(root / "instruction.md").write_text(spec.instruction, encoding="utf-8")
|
|
400
|
+
if spec.grader_kind == "python":
|
|
401
|
+
checks = root / "checks"
|
|
402
|
+
checks.mkdir(exist_ok=True)
|
|
403
|
+
(checks / "verify.py").write_text(spec.grader["source"], encoding="utf-8")
|
|
404
|
+
return root
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def read_task_dir(session_dir: Path) -> TaskSpec | None:
|
|
408
|
+
"""The task a session directory holds, or None for a bundled-task session."""
|
|
409
|
+
path = Path(session_dir) / TASK_DIR / TASK_FILE
|
|
410
|
+
if not path.is_file():
|
|
411
|
+
return None
|
|
412
|
+
return parse_task_spec(json.loads(path.read_text(encoding="utf-8")))
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
# --------------------------------------------------------------------------- #
|
|
416
|
+
# The assertions grader
|
|
417
|
+
# --------------------------------------------------------------------------- #
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _lookup(row: Any, path: str) -> tuple[bool, Any]:
|
|
421
|
+
"""``(found, value)`` of a dotted path into a row."""
|
|
422
|
+
current = row
|
|
423
|
+
for part in path.split("."):
|
|
424
|
+
if not isinstance(current, dict) or part not in current:
|
|
425
|
+
return False, None
|
|
426
|
+
current = current[part]
|
|
427
|
+
return True, current
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _same(actual: Any, expected: Any) -> bool:
|
|
431
|
+
"""JSON equality: ``true`` is not ``1``, as it is not in the schema."""
|
|
432
|
+
if isinstance(actual, bool) or isinstance(expected, bool):
|
|
433
|
+
return isinstance(actual, bool) and isinstance(expected, bool) and actual is expected
|
|
434
|
+
return actual == expected
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _matches_where(row: dict, where: dict | None) -> bool:
|
|
438
|
+
for path, expected in (where or {}).items():
|
|
439
|
+
found, actual = _lookup(row, path)
|
|
440
|
+
if not found or not _same(actual, expected):
|
|
441
|
+
return False
|
|
442
|
+
return True
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _ordered(actual: Any, expected: Any) -> bool:
|
|
446
|
+
"""Whether the two compare with ``<``: both numbers (not bools) or both strings."""
|
|
447
|
+
numbers = (int, float)
|
|
448
|
+
if isinstance(actual, bool) or isinstance(expected, bool):
|
|
449
|
+
return False
|
|
450
|
+
if isinstance(actual, numbers) and isinstance(expected, numbers):
|
|
451
|
+
return True
|
|
452
|
+
return isinstance(actual, str) and isinstance(expected, str)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _predicate(row: dict, field: str, op: str, expected: Any) -> bool:
|
|
456
|
+
found, actual = _lookup(row, field)
|
|
457
|
+
if op == "eq":
|
|
458
|
+
return found and _same(actual, expected)
|
|
459
|
+
if op == "neq":
|
|
460
|
+
return not found or not _same(actual, expected)
|
|
461
|
+
if op == "in":
|
|
462
|
+
return found and any(_same(actual, option) for option in expected)
|
|
463
|
+
if op == "contains":
|
|
464
|
+
if not found:
|
|
465
|
+
return False
|
|
466
|
+
if isinstance(actual, str):
|
|
467
|
+
return isinstance(expected, str) and expected in actual
|
|
468
|
+
if isinstance(actual, list):
|
|
469
|
+
return any(_same(item, expected) for item in actual)
|
|
470
|
+
return False
|
|
471
|
+
if not found or not _ordered(actual, expected):
|
|
472
|
+
return False
|
|
473
|
+
if op == "gt":
|
|
474
|
+
return actual > expected
|
|
475
|
+
if op == "gte":
|
|
476
|
+
return actual >= expected
|
|
477
|
+
if op == "lt":
|
|
478
|
+
return actual < expected
|
|
479
|
+
return actual <= expected # lte
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _rows_of(state: dict, entity: str) -> list[dict]:
|
|
483
|
+
"""The rows of one entity from a state dump: ``{entity: {key: row}}`` (the
|
|
484
|
+
verifier's shape) or ``{entity: [row, ...]}``."""
|
|
485
|
+
held = state.get(entity)
|
|
486
|
+
if isinstance(held, dict):
|
|
487
|
+
return [row for row in held.values() if isinstance(row, dict)]
|
|
488
|
+
if isinstance(held, list):
|
|
489
|
+
return [row for row in held if isinstance(row, dict)]
|
|
490
|
+
return []
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def evaluate_check(check: dict, state: dict) -> bool:
|
|
494
|
+
selected = [row for row in _rows_of(state, check["entity"]) if _matches_where(row, check.get("where"))]
|
|
495
|
+
assertion = check["assert"]
|
|
496
|
+
if assertion == "exists":
|
|
497
|
+
return bool(selected)
|
|
498
|
+
if assertion == "absent":
|
|
499
|
+
return not selected
|
|
500
|
+
if assertion == "count":
|
|
501
|
+
bound = check["count"]
|
|
502
|
+
n = len(selected)
|
|
503
|
+
return all(
|
|
504
|
+
(key != "eq" or n == value) and (key != "gte" or n >= value) and (key != "lte" or n <= value)
|
|
505
|
+
for key, value in bound.items()
|
|
506
|
+
)
|
|
507
|
+
tests = (_predicate(row, check["field"], check["op"], check["value"]) for row in selected)
|
|
508
|
+
if assertion == "all":
|
|
509
|
+
return bool(selected) and all(tests)
|
|
510
|
+
return any(tests) # any
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def evaluate_assertions(checks: list[dict], state: dict) -> dict:
|
|
514
|
+
"""The grade of an assertions grader over a state dump: the weighted share
|
|
515
|
+
of checks that passed as ``reward``, each check's 0/1 under its id (or its
|
|
516
|
+
position) in ``rewards``. The runner's shape, as ``verify.py`` prints it."""
|
|
517
|
+
rewards: dict[str, float] = {}
|
|
518
|
+
total = 0.0
|
|
519
|
+
passed = 0.0
|
|
520
|
+
for i, check in enumerate(checks):
|
|
521
|
+
ok = evaluate_check(check, state)
|
|
522
|
+
rewards[check_key(check, i)] = 1.0 if ok else 0.0
|
|
523
|
+
total += check["weight"]
|
|
524
|
+
passed += check["weight"] if ok else 0.0
|
|
525
|
+
return {"reward": (passed / total) if total else None, "rewards": rewards}
|