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,748 @@
|
|
|
1
|
+
"""A browser on a world's UI: the computer-use half of a live session.
|
|
2
|
+
|
|
3
|
+
A world session answers on SURFACES. ``tools`` is always there (the world's
|
|
4
|
+
own operations over the rendezvous). An api-type world may also answer on
|
|
5
|
+
``api`` (the service itself), ``ui`` (its dashboard, over the same state) and
|
|
6
|
+
``browser`` — a headless Chromium the platform runs NEXT TO that UI, which an
|
|
7
|
+
agent drives through Playwright. This module is the Python client for the
|
|
8
|
+
last two:
|
|
9
|
+
|
|
10
|
+
with open_session("payments-like", "refund-dispute", surfaces=["ui", "browser"]) as s:
|
|
11
|
+
s.ready()
|
|
12
|
+
with s.browser() as b:
|
|
13
|
+
b.goto() # the UI's entry page
|
|
14
|
+
b.click("text=Refund")
|
|
15
|
+
png = b.screenshot()
|
|
16
|
+
cu = s.computer_use_tools() # the same actions as model tools
|
|
17
|
+
my_agent(cu.impls, cu.schemas, s.instruction)
|
|
18
|
+
s.grade() # sees what the clicks did
|
|
19
|
+
|
|
20
|
+
Playwright is an OPTIONAL dependency (``pip install playwright`` and
|
|
21
|
+
``playwright install chromium`` for local mode); a hosted browser needs only
|
|
22
|
+
the ``playwright`` package. Nothing here imports it until a browser is opened,
|
|
23
|
+
so sessions that never touch a UI never pay for it.
|
|
24
|
+
|
|
25
|
+
MODES. ``hosted`` connects to the session's own browser (``surfaces.browser``
|
|
26
|
+
— a Playwright server the pod exposes over an authenticated websocket).
|
|
27
|
+
``local`` launches Chromium on this machine and points it at ``surfaces.ui``;
|
|
28
|
+
it is what a laptop uses against a hosted twin, and what tests use. ``auto``
|
|
29
|
+
picks hosted when the session has one. Asking for a mode the session cannot
|
|
30
|
+
serve raises :class:`WorldSurfaceUnavailable` — never a silent fallback to a
|
|
31
|
+
different browser than the one the episode will be judged on.
|
|
32
|
+
|
|
33
|
+
OBSERVABILITY. Every action a :class:`WorldBrowser` takes (goto, click, type,
|
|
34
|
+
press, scroll, back) is reported to the platform as one event — what was done,
|
|
35
|
+
whether it worked, where the page ended up, and a JPEG of the screen after —
|
|
36
|
+
through a :class:`BrowserActionRecorder`. Reporting runs on a background
|
|
37
|
+
thread and never fails the action; the platform renders it as the session's
|
|
38
|
+
browser timeline. Typed text into secret-looking fields is redacted before it
|
|
39
|
+
leaves the process.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import base64
|
|
45
|
+
import importlib
|
|
46
|
+
import logging
|
|
47
|
+
import queue
|
|
48
|
+
import re
|
|
49
|
+
import threading
|
|
50
|
+
import time
|
|
51
|
+
import urllib.parse
|
|
52
|
+
from dataclasses import dataclass, field
|
|
53
|
+
from datetime import datetime, timezone
|
|
54
|
+
from typing import Any, Callable, Dict, List, Optional, Protocol
|
|
55
|
+
|
|
56
|
+
from .world_tools import (
|
|
57
|
+
WorldSessionError,
|
|
58
|
+
WorldToolkit,
|
|
59
|
+
WorldToolSpec,
|
|
60
|
+
schema_from_signature,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
DEFAULT_VIEWPORT = {"width": 1280, "height": 800}
|
|
64
|
+
UI_TOKEN_QUERY = "token"
|
|
65
|
+
SETTLE_TIMEOUT_MS = 5_000
|
|
66
|
+
SETTLE_QUIET_MS = 150
|
|
67
|
+
SETTLE_POLL_MS = 25
|
|
68
|
+
SURFACE_KINDS = ("tools", "api", "ui", "browser")
|
|
69
|
+
|
|
70
|
+
EVENT_BATCH_SIZE = 10
|
|
71
|
+
EVENT_FLUSH_INTERVAL_SEC = 1.0
|
|
72
|
+
EVENT_MAX_RETRIES = 3
|
|
73
|
+
EVENT_SCREENSHOT_QUALITY = 60
|
|
74
|
+
EVENT_MAX_STRING = 500
|
|
75
|
+
|
|
76
|
+
_SECRET_KEY = re.compile(
|
|
77
|
+
r"pass(word|wd)?|secret|token|api[-_]?key|authorization|cookie"
|
|
78
|
+
r"|(^|[-_ #.\[\"'=])(otp|pin|cvv|ssn)([-_ \]\"']|$)",
|
|
79
|
+
re.IGNORECASE,
|
|
80
|
+
)
|
|
81
|
+
_SECRET_VALUE = re.compile(r"^((sk|pk|ghp|xox[abp])[-_][-_A-Za-z0-9]{8,}|AKIA[0-9A-Z]{12,}|Bearer [-_.A-Za-z0-9]{8,})")
|
|
82
|
+
|
|
83
|
+
log = logging.getLogger(__name__)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class WorldSurfaceUnavailable(WorldSessionError):
|
|
87
|
+
"""The session was not opened with (or the world does not have) this surface."""
|
|
88
|
+
|
|
89
|
+
def __init__(self, surface: str, reason: str) -> None:
|
|
90
|
+
super().__init__(f"World surface '{surface}' is unavailable: {reason}")
|
|
91
|
+
self.surface = surface
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass(frozen=True)
|
|
95
|
+
class ApiSurface:
|
|
96
|
+
url: str
|
|
97
|
+
#: Per-session credential of a World Host api surface, presented as
|
|
98
|
+
#: ``Authorization: Bearer``. ``None`` for a pod or a customer-hosted twin,
|
|
99
|
+
#: which answer with the world's own declared auth.
|
|
100
|
+
token: Optional[str] = None
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def headers(self) -> Dict[str, str]:
|
|
104
|
+
"""Headers every request to :attr:`url` needs."""
|
|
105
|
+
return {"Authorization": f"Bearer {self.token}"} if self.token else {}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass(frozen=True)
|
|
109
|
+
class UiSurface:
|
|
110
|
+
url: str
|
|
111
|
+
entry: str = "/"
|
|
112
|
+
#: Same-origin prefix the UI proxies to the world API (``/__world-api``),
|
|
113
|
+
#: or ``None`` when the UI only talks to the API server-side.
|
|
114
|
+
api_path: Optional[str] = None
|
|
115
|
+
#: Per-session credential of a World Host UI. A browser navigation cannot
|
|
116
|
+
#: carry a header, so the first page load presents it once as ``?token=``
|
|
117
|
+
#: and the host answers with an HttpOnly cookie. ``None`` for pod UIs.
|
|
118
|
+
token: Optional[str] = None
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def entry_url(self) -> str:
|
|
122
|
+
return self.url_for(self.entry or "/")
|
|
123
|
+
|
|
124
|
+
def url_for(self, path: str) -> str:
|
|
125
|
+
"""The URL of ``path`` on this surface, carrying the session token when
|
|
126
|
+
there is one. ``path`` may already have a query string."""
|
|
127
|
+
rel = path if path.startswith("/") else "/" + path
|
|
128
|
+
full = self.url.rstrip("/") + rel
|
|
129
|
+
if not self.token:
|
|
130
|
+
return full
|
|
131
|
+
before_hash, hash_sep, fragment = full.partition("#")
|
|
132
|
+
joiner = "&" if "?" in before_hash else "?"
|
|
133
|
+
query = urllib.parse.urlencode({UI_TOKEN_QUERY: self.token})
|
|
134
|
+
return before_hash + joiner + query + hash_sep + fragment
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def api_url(self) -> Optional[str]:
|
|
138
|
+
"""The API as reachable through the UI's origin, if it proxies one."""
|
|
139
|
+
if not self.api_path:
|
|
140
|
+
return None
|
|
141
|
+
return self.url.rstrip("/") + self.api_path
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def without_ui_token(url: str) -> str:
|
|
145
|
+
"""``url`` without its ``token`` query parameter: what may be logged."""
|
|
146
|
+
parts = urllib.parse.urlsplit(url)
|
|
147
|
+
if not parts.query:
|
|
148
|
+
return url
|
|
149
|
+
kept = [(k, v) for k, v in urllib.parse.parse_qsl(parts.query, keep_blank_values=True) if k != UI_TOKEN_QUERY]
|
|
150
|
+
return urllib.parse.urlunsplit(parts._replace(query=urllib.parse.urlencode(kept)))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass(frozen=True)
|
|
154
|
+
class BrowserSurface:
|
|
155
|
+
"""``protocol`` is ``cdp`` for the platform's Chromium relay (any number of
|
|
156
|
+
agents share one browser, each in its own context) or ``playwright`` for a
|
|
157
|
+
``playwright run-server`` endpoint."""
|
|
158
|
+
|
|
159
|
+
ws_url: str
|
|
160
|
+
protocol: str = "cdp"
|
|
161
|
+
viewport: Dict[str, int] = field(default_factory=lambda: dict(DEFAULT_VIEWPORT))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@dataclass(frozen=True)
|
|
165
|
+
class SessionSurfaces:
|
|
166
|
+
"""Parsed ``surfaces`` from the session descriptor."""
|
|
167
|
+
|
|
168
|
+
api: Optional[ApiSurface] = None
|
|
169
|
+
ui: Optional[UiSurface] = None
|
|
170
|
+
browser: Optional[BrowserSurface] = None
|
|
171
|
+
|
|
172
|
+
@classmethod
|
|
173
|
+
def from_descriptor(cls, raw: Optional[Dict[str, Any]]) -> "SessionSurfaces":
|
|
174
|
+
raw = raw or {}
|
|
175
|
+
api = raw.get("api")
|
|
176
|
+
ui = raw.get("ui")
|
|
177
|
+
browser = raw.get("browser")
|
|
178
|
+
return cls(
|
|
179
|
+
api=(ApiSurface(url=api["url"], token=api.get("token") or None) if api else None),
|
|
180
|
+
ui=(
|
|
181
|
+
UiSurface(
|
|
182
|
+
url=ui["url"],
|
|
183
|
+
entry=ui.get("entry") or "/",
|
|
184
|
+
api_path=ui.get("apiPath") or None,
|
|
185
|
+
token=ui.get("token") or None,
|
|
186
|
+
)
|
|
187
|
+
if ui
|
|
188
|
+
else None
|
|
189
|
+
),
|
|
190
|
+
browser=(
|
|
191
|
+
BrowserSurface(
|
|
192
|
+
ws_url=browser["wsUrl"],
|
|
193
|
+
protocol=browser.get("protocol") or "cdp",
|
|
194
|
+
viewport=dict(browser.get("viewport") or DEFAULT_VIEWPORT),
|
|
195
|
+
)
|
|
196
|
+
if browser
|
|
197
|
+
else None
|
|
198
|
+
),
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def kinds(self) -> List[str]:
|
|
203
|
+
kinds = ["tools"]
|
|
204
|
+
if self.api is not None:
|
|
205
|
+
kinds.append("api")
|
|
206
|
+
if self.ui is not None:
|
|
207
|
+
kinds.append("ui")
|
|
208
|
+
if self.browser is not None:
|
|
209
|
+
kinds.append("browser")
|
|
210
|
+
return kinds
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class _Keyboard(Protocol):
|
|
214
|
+
def press(self, key: str, **kwargs: Any) -> Any: ...
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class _Mouse(Protocol):
|
|
218
|
+
def wheel(self, delta_x: float, delta_y: float) -> Any: ...
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class _Page(Protocol):
|
|
222
|
+
"""The slice of a Playwright ``Page`` this module uses."""
|
|
223
|
+
|
|
224
|
+
url: str
|
|
225
|
+
keyboard: _Keyboard
|
|
226
|
+
mouse: _Mouse
|
|
227
|
+
|
|
228
|
+
def goto(self, url: str, **kwargs: Any) -> Any: ...
|
|
229
|
+
def click(self, selector: str, **kwargs: Any) -> Any: ...
|
|
230
|
+
def fill(self, selector: str, value: str, **kwargs: Any) -> Any: ...
|
|
231
|
+
def screenshot(self, **kwargs: Any) -> bytes: ...
|
|
232
|
+
def inner_text(self, selector: str, **kwargs: Any) -> str: ...
|
|
233
|
+
def content(self) -> str: ...
|
|
234
|
+
def go_back(self, **kwargs: Any) -> Any: ...
|
|
235
|
+
def wait_for_load_state(self, state: str = "load", **kwargs: Any) -> Any: ...
|
|
236
|
+
def wait_for_timeout(self, timeout: float) -> Any: ...
|
|
237
|
+
def title(self) -> str: ...
|
|
238
|
+
def on(self, event: str, handler: Callable[..., Any]) -> Any: ...
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# ── action events ────────────────────────────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def redact_action_args(action: str, args: Dict[str, Any]) -> Dict[str, Any]:
|
|
245
|
+
"""What of an action's arguments may leave the process. Selectors, paths
|
|
246
|
+
and keys are what a reviewer needs; anything keyed like a credential, or
|
|
247
|
+
typed into a field whose selector names one, or shaped like a known key,
|
|
248
|
+
is replaced. Strings are capped, nested values dropped."""
|
|
249
|
+
out: Dict[str, Any] = {}
|
|
250
|
+
secret_field = action == "browser_type" and bool(_SECRET_KEY.search(str(args.get("selector", ""))))
|
|
251
|
+
for key, value in args.items():
|
|
252
|
+
if value is None or isinstance(value, (bool, int, float)):
|
|
253
|
+
out[key] = value
|
|
254
|
+
elif isinstance(value, str):
|
|
255
|
+
if _SECRET_KEY.search(key) or _SECRET_VALUE.match(value) or (secret_field and key == "text"):
|
|
256
|
+
out[key] = "[redacted]"
|
|
257
|
+
else:
|
|
258
|
+
out[key] = value if len(value) <= EVENT_MAX_STRING else value[:EVENT_MAX_STRING] + "…"
|
|
259
|
+
else:
|
|
260
|
+
out[key] = "[omitted]"
|
|
261
|
+
return out
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class BrowserActionRecorder:
|
|
265
|
+
"""Ships browser action events to the platform, off the action's hot path.
|
|
266
|
+
|
|
267
|
+
``post`` receives a batch of wire-shaped events (the body of
|
|
268
|
+
``POST /world-sessions/{id}/browser-events``). Events are numbered here,
|
|
269
|
+
so ordering survives batching and retries — the platform dedups on
|
|
270
|
+
``(agentId, seq)``. A batch that keeps failing is dropped with a warning:
|
|
271
|
+
telemetry must never stall or fail the agent.
|
|
272
|
+
|
|
273
|
+
Numbering is durable across recorders: :meth:`next_seq` hands out a
|
|
274
|
+
position within this recorder, and the first send adds the base
|
|
275
|
+
``resume_from`` reports — the platform's next free seq for this agent —
|
|
276
|
+
so an agent id reopened in a later process appends to its timeline
|
|
277
|
+
instead of colliding with (and being deduplicated against) what it
|
|
278
|
+
already recorded. The base is fixed once resolved, which keeps retries
|
|
279
|
+
idempotent.
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
def __init__(
|
|
283
|
+
self,
|
|
284
|
+
post: Callable[[List[Dict[str, Any]]], None],
|
|
285
|
+
agent_id: str,
|
|
286
|
+
*,
|
|
287
|
+
resume_from: Optional[Callable[[], int]] = None,
|
|
288
|
+
batch_size: int = EVENT_BATCH_SIZE,
|
|
289
|
+
flush_interval: float = EVENT_FLUSH_INTERVAL_SEC,
|
|
290
|
+
max_retries: int = EVENT_MAX_RETRIES,
|
|
291
|
+
) -> None:
|
|
292
|
+
self.agent_id = agent_id
|
|
293
|
+
self._post = post
|
|
294
|
+
self._resume_from = resume_from
|
|
295
|
+
self._base: Optional[int] = None
|
|
296
|
+
self._batch_size = batch_size
|
|
297
|
+
self._flush_interval = flush_interval
|
|
298
|
+
self._max_retries = max_retries
|
|
299
|
+
self._queue: "queue.Queue[Optional[Dict[str, Any]]]" = queue.Queue()
|
|
300
|
+
self._seq = 0
|
|
301
|
+
self._pending = 0
|
|
302
|
+
self._cond = threading.Condition()
|
|
303
|
+
self.sent = 0
|
|
304
|
+
self.dropped = 0
|
|
305
|
+
self._thread = threading.Thread(target=self._run, name="gateway-browser-events", daemon=True)
|
|
306
|
+
self._thread.start()
|
|
307
|
+
|
|
308
|
+
def next_seq(self) -> int:
|
|
309
|
+
"""This recorder's next position; the wire seq is ``resume_from() + this``."""
|
|
310
|
+
with self._cond:
|
|
311
|
+
seq = self._seq
|
|
312
|
+
self._seq += 1
|
|
313
|
+
return seq
|
|
314
|
+
|
|
315
|
+
def _resolve_base(self) -> int:
|
|
316
|
+
if self._base is None:
|
|
317
|
+
base = self._resume_from() if self._resume_from is not None else 0
|
|
318
|
+
if not isinstance(base, int) or isinstance(base, bool) or base < 0:
|
|
319
|
+
raise ValueError(f"browser event base seq must be a non-negative integer, got {base!r}")
|
|
320
|
+
self._base = base
|
|
321
|
+
return self._base
|
|
322
|
+
|
|
323
|
+
def record(self, event: Dict[str, Any]) -> None:
|
|
324
|
+
with self._cond:
|
|
325
|
+
self._pending += 1
|
|
326
|
+
self._queue.put(event)
|
|
327
|
+
|
|
328
|
+
def flush(self, timeout: float = 15.0) -> bool:
|
|
329
|
+
"""Block until everything recorded so far has been posted (or dropped)."""
|
|
330
|
+
with self._cond:
|
|
331
|
+
return self._cond.wait_for(lambda: self._pending == 0, timeout)
|
|
332
|
+
|
|
333
|
+
def close(self, timeout: float = 15.0) -> None:
|
|
334
|
+
self.flush(timeout)
|
|
335
|
+
self._queue.put(None)
|
|
336
|
+
self._thread.join(timeout)
|
|
337
|
+
|
|
338
|
+
def _run(self) -> None:
|
|
339
|
+
while True:
|
|
340
|
+
item = self._queue.get()
|
|
341
|
+
if item is None:
|
|
342
|
+
return
|
|
343
|
+
batch: List[Dict[str, Any]] = [item]
|
|
344
|
+
deadline = time.monotonic() + self._flush_interval
|
|
345
|
+
stop = False
|
|
346
|
+
while len(batch) < self._batch_size:
|
|
347
|
+
remaining = deadline - time.monotonic()
|
|
348
|
+
if remaining <= 0:
|
|
349
|
+
break
|
|
350
|
+
try:
|
|
351
|
+
item = self._queue.get(timeout=remaining)
|
|
352
|
+
except queue.Empty:
|
|
353
|
+
break
|
|
354
|
+
if item is None:
|
|
355
|
+
stop = True
|
|
356
|
+
break
|
|
357
|
+
batch.append(item)
|
|
358
|
+
self._send(batch)
|
|
359
|
+
if stop:
|
|
360
|
+
return
|
|
361
|
+
|
|
362
|
+
def _send(self, batch: List[Dict[str, Any]]) -> None:
|
|
363
|
+
try:
|
|
364
|
+
for attempt in range(self._max_retries):
|
|
365
|
+
try:
|
|
366
|
+
base = self._resolve_base()
|
|
367
|
+
self._post(batch if base == 0 else [{**e, "seq": base + e["seq"]} for e in batch])
|
|
368
|
+
self.sent += len(batch)
|
|
369
|
+
return
|
|
370
|
+
except Exception as exc: # noqa: BLE001 — telemetry must not raise
|
|
371
|
+
if attempt + 1 == self._max_retries:
|
|
372
|
+
self.dropped += len(batch)
|
|
373
|
+
log.warning("dropping %d browser events after %d attempts: %s", len(batch), attempt + 1, exc)
|
|
374
|
+
return
|
|
375
|
+
time.sleep(0.5 * (2**attempt))
|
|
376
|
+
finally:
|
|
377
|
+
with self._cond:
|
|
378
|
+
self._pending -= len(batch)
|
|
379
|
+
self._cond.notify_all()
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _playwright_module() -> Any:
|
|
383
|
+
try:
|
|
384
|
+
return importlib.import_module("playwright.sync_api")
|
|
385
|
+
except ImportError as error: # pragma: no cover - depends on the install
|
|
386
|
+
raise WorldSessionError(
|
|
387
|
+
"Driving a world UI needs Playwright: pip install playwright "
|
|
388
|
+
"(and `playwright install chromium` for local mode)."
|
|
389
|
+
) from error
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
class WorldBrowser:
|
|
393
|
+
"""One Playwright page on the session's UI.
|
|
394
|
+
|
|
395
|
+
Thin on purpose: the point is to hand the agent the SAME browser the
|
|
396
|
+
platform records, not to re-invent Playwright. ``page`` is the real
|
|
397
|
+
Playwright page for anything this wrapper does not cover.
|
|
398
|
+
"""
|
|
399
|
+
|
|
400
|
+
def __init__(
|
|
401
|
+
self,
|
|
402
|
+
page: _Page,
|
|
403
|
+
ui: Optional[UiSurface],
|
|
404
|
+
*,
|
|
405
|
+
mode: str,
|
|
406
|
+
close: Callable[[], None],
|
|
407
|
+
viewport: Optional[Dict[str, int]] = None,
|
|
408
|
+
recorder: Optional[BrowserActionRecorder] = None,
|
|
409
|
+
) -> None:
|
|
410
|
+
self.page = page
|
|
411
|
+
self.ui = ui
|
|
412
|
+
self.mode = mode
|
|
413
|
+
self.viewport = dict(viewport or DEFAULT_VIEWPORT)
|
|
414
|
+
self.recorder = recorder
|
|
415
|
+
self._close = close
|
|
416
|
+
self._inflight = 0
|
|
417
|
+
self._last_network = time.monotonic()
|
|
418
|
+
page.on("request", self._on_request)
|
|
419
|
+
page.on("requestfinished", self._on_request_done)
|
|
420
|
+
page.on("requestfailed", self._on_request_done)
|
|
421
|
+
|
|
422
|
+
@property
|
|
423
|
+
def agent_id(self) -> Optional[str]:
|
|
424
|
+
return self.recorder.agent_id if self.recorder else None
|
|
425
|
+
|
|
426
|
+
def _act(self, action: str, args: Dict[str, Any], fn: Callable[[], Any]) -> Any:
|
|
427
|
+
"""Run one action and report it: the event carries what was asked,
|
|
428
|
+
whether it worked, and the screen after — for failures too, so a
|
|
429
|
+
reviewer sees what the agent saw when the selector did not match."""
|
|
430
|
+
if self.recorder is None:
|
|
431
|
+
return fn()
|
|
432
|
+
started = time.monotonic()
|
|
433
|
+
ok, error = True, None
|
|
434
|
+
try:
|
|
435
|
+
return fn()
|
|
436
|
+
except Exception as exc:
|
|
437
|
+
ok, error = False, f"{type(exc).__name__}: {exc}"[:4000]
|
|
438
|
+
raise
|
|
439
|
+
finally:
|
|
440
|
+
event: Dict[str, Any] = {
|
|
441
|
+
"agentId": self.recorder.agent_id,
|
|
442
|
+
"seq": self.recorder.next_seq(),
|
|
443
|
+
"ts": datetime.now(timezone.utc).isoformat(),
|
|
444
|
+
"action": action,
|
|
445
|
+
"args": redact_action_args(action, args),
|
|
446
|
+
"ok": ok,
|
|
447
|
+
"error": error,
|
|
448
|
+
"durationMs": int((time.monotonic() - started) * 1000),
|
|
449
|
+
"mode": self.mode,
|
|
450
|
+
"viewport": dict(self.viewport),
|
|
451
|
+
}
|
|
452
|
+
event.update(self._observe_for_event())
|
|
453
|
+
self.recorder.record(event)
|
|
454
|
+
|
|
455
|
+
def _observe_for_event(self) -> Dict[str, Any]:
|
|
456
|
+
out: Dict[str, Any] = {"url": None, "title": None, "screenshot": None}
|
|
457
|
+
try:
|
|
458
|
+
out["url"] = self.url
|
|
459
|
+
out["title"] = self.page.title()
|
|
460
|
+
jpeg = self.page.screenshot(type="jpeg", quality=EVENT_SCREENSHOT_QUALITY)
|
|
461
|
+
out["screenshot"] = base64.b64encode(jpeg).decode("ascii")
|
|
462
|
+
except Exception as exc: # noqa: BLE001 — a closed page still gets its event
|
|
463
|
+
log.debug("browser event observation failed: %s", exc)
|
|
464
|
+
return out
|
|
465
|
+
|
|
466
|
+
# ── navigation ─────────────────────────────────────────────────────────
|
|
467
|
+
def goto(self, path_or_url: Optional[str] = None) -> str:
|
|
468
|
+
"""Open a UI path (``/customers``), a full URL, or — with no argument —
|
|
469
|
+
the UI's declared entry page."""
|
|
470
|
+
if path_or_url is None:
|
|
471
|
+
if self.ui is None:
|
|
472
|
+
raise WorldSurfaceUnavailable("ui", "this session has no UI to navigate to")
|
|
473
|
+
target = self.ui.entry_url
|
|
474
|
+
elif path_or_url.startswith(("http://", "https://")):
|
|
475
|
+
target = path_or_url
|
|
476
|
+
else:
|
|
477
|
+
if self.ui is None:
|
|
478
|
+
raise WorldSurfaceUnavailable("ui", "relative paths need a UI surface")
|
|
479
|
+
target = self.ui.url_for(path_or_url)
|
|
480
|
+
|
|
481
|
+
def run() -> str:
|
|
482
|
+
self.page.goto(target, wait_until="load")
|
|
483
|
+
self._settle()
|
|
484
|
+
return self.url
|
|
485
|
+
|
|
486
|
+
return self._act(
|
|
487
|
+
"browser_goto",
|
|
488
|
+
{"path": path_or_url, "url": without_ui_token(target)},
|
|
489
|
+
run,
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
def back(self) -> str:
|
|
493
|
+
def run() -> str:
|
|
494
|
+
self.page.go_back(wait_until="load")
|
|
495
|
+
self._settle()
|
|
496
|
+
return self.url
|
|
497
|
+
|
|
498
|
+
return self._act("browser_back", {}, run)
|
|
499
|
+
|
|
500
|
+
def _on_request(self, _request: Any) -> None:
|
|
501
|
+
self._inflight += 1
|
|
502
|
+
self._last_network = time.monotonic()
|
|
503
|
+
|
|
504
|
+
def _on_request_done(self, _request: Any) -> None:
|
|
505
|
+
self._inflight = max(0, self._inflight - 1)
|
|
506
|
+
self._last_network = time.monotonic()
|
|
507
|
+
|
|
508
|
+
def _settle(self) -> None:
|
|
509
|
+
"""Wait for the page to finish reacting to an action.
|
|
510
|
+
|
|
511
|
+
``load`` alone is not enough for client-rendered UIs (React et al.): a
|
|
512
|
+
click returns before the fetch that re-renders the page, so an agent
|
|
513
|
+
would read stale text. Playwright's ``networkidle`` is per navigation
|
|
514
|
+
and resolves at once after the first one, so this tracks requests
|
|
515
|
+
itself: done when nothing is in flight and the network has been quiet
|
|
516
|
+
for ``SETTLE_QUIET_MS``, bounded so long-polling UIs cannot hang us.
|
|
517
|
+
"""
|
|
518
|
+
self.page.wait_for_load_state("load")
|
|
519
|
+
deadline = time.monotonic() + SETTLE_TIMEOUT_MS / 1000
|
|
520
|
+
while time.monotonic() < deadline:
|
|
521
|
+
quiet = (time.monotonic() - self._last_network) * 1000 >= SETTLE_QUIET_MS
|
|
522
|
+
if self._inflight == 0 and quiet:
|
|
523
|
+
return
|
|
524
|
+
self.page.wait_for_timeout(SETTLE_POLL_MS)
|
|
525
|
+
|
|
526
|
+
# ── acting ─────────────────────────────────────────────────────────────
|
|
527
|
+
def click(self, selector: str) -> None:
|
|
528
|
+
def run() -> None:
|
|
529
|
+
self.page.click(selector)
|
|
530
|
+
self._settle()
|
|
531
|
+
|
|
532
|
+
self._act("browser_click", {"selector": selector}, run)
|
|
533
|
+
|
|
534
|
+
def type(self, selector: str, text: str) -> None:
|
|
535
|
+
self._act("browser_type", {"selector": selector, "text": text}, lambda: self.page.fill(selector, text))
|
|
536
|
+
|
|
537
|
+
def press(self, key: str) -> None:
|
|
538
|
+
def run() -> None:
|
|
539
|
+
self.page.keyboard.press(key)
|
|
540
|
+
self._settle()
|
|
541
|
+
|
|
542
|
+
self._act("browser_press", {"key": key}, run)
|
|
543
|
+
|
|
544
|
+
def scroll(self, dy: int) -> None:
|
|
545
|
+
self._act("browser_scroll", {"dy": dy}, lambda: self.page.mouse.wheel(0, dy))
|
|
546
|
+
|
|
547
|
+
# ── observing ──────────────────────────────────────────────────────────
|
|
548
|
+
def screenshot(self, full_page: bool = False) -> bytes:
|
|
549
|
+
return self.page.screenshot(type="png", full_page=full_page)
|
|
550
|
+
|
|
551
|
+
def screenshot_b64(self, full_page: bool = False) -> str:
|
|
552
|
+
return base64.b64encode(self.screenshot(full_page)).decode("ascii")
|
|
553
|
+
|
|
554
|
+
def text(self, selector: str = "body") -> str:
|
|
555
|
+
return self.page.inner_text(selector)
|
|
556
|
+
|
|
557
|
+
def html(self) -> str:
|
|
558
|
+
return self.page.content()
|
|
559
|
+
|
|
560
|
+
@property
|
|
561
|
+
def url(self) -> str:
|
|
562
|
+
"""Where the page is, less the session token a World Host landing
|
|
563
|
+
carries: the host trades it for a cookie and redirects, but a page that
|
|
564
|
+
never got that far (host down, wrong origin) would still show it."""
|
|
565
|
+
return without_ui_token(self.page.url)
|
|
566
|
+
|
|
567
|
+
def title(self) -> str:
|
|
568
|
+
return self.page.title()
|
|
569
|
+
|
|
570
|
+
# ── lifecycle ──────────────────────────────────────────────────────────
|
|
571
|
+
def close(self) -> None:
|
|
572
|
+
try:
|
|
573
|
+
self._close()
|
|
574
|
+
finally:
|
|
575
|
+
if self.recorder is not None:
|
|
576
|
+
self.recorder.close()
|
|
577
|
+
|
|
578
|
+
def __enter__(self) -> "WorldBrowser":
|
|
579
|
+
return self
|
|
580
|
+
|
|
581
|
+
def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
|
|
582
|
+
self.close()
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def open_browser(
|
|
586
|
+
surfaces: SessionSurfaces,
|
|
587
|
+
*,
|
|
588
|
+
mode: str = "auto",
|
|
589
|
+
headless: bool = True,
|
|
590
|
+
recorder: Optional[BrowserActionRecorder] = None,
|
|
591
|
+
) -> WorldBrowser:
|
|
592
|
+
"""Open a browser on the session's UI in ``hosted``, ``local`` or ``auto`` mode."""
|
|
593
|
+
if mode not in ("auto", "hosted", "local"):
|
|
594
|
+
raise ValueError("mode must be 'auto', 'hosted' or 'local'")
|
|
595
|
+
if surfaces.ui is None and surfaces.browser is None:
|
|
596
|
+
raise WorldSurfaceUnavailable("ui", "open the session with surfaces=['ui'] on a world that declares [ui]")
|
|
597
|
+
resolved = mode
|
|
598
|
+
if mode == "auto":
|
|
599
|
+
resolved = "hosted" if surfaces.browser is not None else "local"
|
|
600
|
+
if resolved == "hosted" and surfaces.browser is None:
|
|
601
|
+
raise WorldSurfaceUnavailable("browser", "open the session with surfaces=['browser'] to get a hosted browser")
|
|
602
|
+
|
|
603
|
+
sync_api = _playwright_module()
|
|
604
|
+
pw = sync_api.sync_playwright().start()
|
|
605
|
+
viewport = surfaces.browser.viewport if surfaces.browser else dict(DEFAULT_VIEWPORT)
|
|
606
|
+
try:
|
|
607
|
+
if resolved == "hosted":
|
|
608
|
+
assert surfaces.browser is not None
|
|
609
|
+
if surfaces.browser.protocol == "playwright":
|
|
610
|
+
browser = pw.chromium.connect(surfaces.browser.ws_url)
|
|
611
|
+
else:
|
|
612
|
+
browser = pw.chromium.connect_over_cdp(surfaces.browser.ws_url)
|
|
613
|
+
else:
|
|
614
|
+
browser = pw.chromium.launch(headless=headless)
|
|
615
|
+
context = browser.new_context(viewport=viewport)
|
|
616
|
+
page = context.new_page()
|
|
617
|
+
except Exception:
|
|
618
|
+
pw.stop()
|
|
619
|
+
raise
|
|
620
|
+
|
|
621
|
+
def close() -> None:
|
|
622
|
+
try:
|
|
623
|
+
context.close()
|
|
624
|
+
browser.close()
|
|
625
|
+
finally:
|
|
626
|
+
pw.stop()
|
|
627
|
+
|
|
628
|
+
return WorldBrowser(page, surfaces.ui, mode=resolved, close=close, viewport=viewport, recorder=recorder)
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
# ── computer-use tools ───────────────────────────────────────────────────────
|
|
632
|
+
|
|
633
|
+
COMPUTER_USE_SPECS: List[WorldToolSpec] = [
|
|
634
|
+
WorldToolSpec(
|
|
635
|
+
"browser_goto",
|
|
636
|
+
"browser_goto(path=None)",
|
|
637
|
+
"Open a page of the world's UI. `path` is a UI path like /customers, a full URL, or omitted for the entry page. Returns the URL and page text.",
|
|
638
|
+
),
|
|
639
|
+
WorldToolSpec(
|
|
640
|
+
"browser_click",
|
|
641
|
+
"browser_click(selector)",
|
|
642
|
+
"Click an element by Playwright selector (CSS, `text=Refund`, `role=button[name=Save]`). Returns the page text after the click.",
|
|
643
|
+
),
|
|
644
|
+
WorldToolSpec(
|
|
645
|
+
"browser_type",
|
|
646
|
+
"browser_type(selector, text)",
|
|
647
|
+
"Replace the value of an input/textarea found by selector with `text`.",
|
|
648
|
+
),
|
|
649
|
+
WorldToolSpec(
|
|
650
|
+
"browser_press",
|
|
651
|
+
"browser_press(key)",
|
|
652
|
+
"Press a key (Enter, Tab, Escape, ArrowDown, ...). Returns the page text after.",
|
|
653
|
+
),
|
|
654
|
+
WorldToolSpec(
|
|
655
|
+
"browser_scroll",
|
|
656
|
+
"browser_scroll(dy=600)",
|
|
657
|
+
"Scroll the page vertically by `dy` pixels (negative scrolls up).",
|
|
658
|
+
),
|
|
659
|
+
WorldToolSpec(
|
|
660
|
+
"browser_screenshot",
|
|
661
|
+
"browser_screenshot(full_page=False)",
|
|
662
|
+
"Take a PNG screenshot; returns {'image_base64': ..., 'url': ...}.",
|
|
663
|
+
),
|
|
664
|
+
WorldToolSpec(
|
|
665
|
+
"browser_read",
|
|
666
|
+
"browser_read(selector='body')",
|
|
667
|
+
"Return the visible text of an element (default: whole page) plus the URL and title.",
|
|
668
|
+
),
|
|
669
|
+
WorldToolSpec(
|
|
670
|
+
"browser_back",
|
|
671
|
+
"browser_back()",
|
|
672
|
+
"Go back one page in history.",
|
|
673
|
+
),
|
|
674
|
+
]
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def computer_use_toolkit(browser: WorldBrowser, max_text: int = 20000) -> WorldToolkit:
|
|
678
|
+
"""The browser as model tools, in the same :class:`WorldToolkit` shape
|
|
679
|
+
the world's own tools come in — so an agent loop that already dispatches
|
|
680
|
+
``toolkit().impls`` merges ``computer_use_toolkit(b).impls`` and gains a
|
|
681
|
+
screen without a second dispatch path."""
|
|
682
|
+
|
|
683
|
+
def observe(selector: str = "body") -> Dict[str, Any]:
|
|
684
|
+
text = browser.text(selector)
|
|
685
|
+
return {
|
|
686
|
+
"url": browser.url,
|
|
687
|
+
"title": browser.title(),
|
|
688
|
+
"text": text[:max_text],
|
|
689
|
+
"truncated": len(text) > max_text,
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
def goto(path: Optional[str] = None) -> Dict[str, Any]:
|
|
693
|
+
browser.goto(path)
|
|
694
|
+
return observe()
|
|
695
|
+
|
|
696
|
+
def click(selector: str) -> Dict[str, Any]:
|
|
697
|
+
browser.click(selector)
|
|
698
|
+
return observe()
|
|
699
|
+
|
|
700
|
+
def type_(selector: str, text: str) -> Dict[str, Any]:
|
|
701
|
+
browser.type(selector, str(text))
|
|
702
|
+
return {"ok": True, "url": browser.url}
|
|
703
|
+
|
|
704
|
+
def press(key: str) -> Dict[str, Any]:
|
|
705
|
+
browser.press(key)
|
|
706
|
+
return observe()
|
|
707
|
+
|
|
708
|
+
def scroll(dy: int = 600) -> Dict[str, Any]:
|
|
709
|
+
browser.scroll(int(dy))
|
|
710
|
+
return {"ok": True, "url": browser.url}
|
|
711
|
+
|
|
712
|
+
def screenshot(full_page: bool = False) -> Dict[str, Any]:
|
|
713
|
+
return {"image_base64": browser.screenshot_b64(bool(full_page)), "url": browser.url}
|
|
714
|
+
|
|
715
|
+
def read(selector: str = "body") -> Dict[str, Any]:
|
|
716
|
+
return observe(selector)
|
|
717
|
+
|
|
718
|
+
def back() -> Dict[str, Any]:
|
|
719
|
+
browser.back()
|
|
720
|
+
return observe()
|
|
721
|
+
|
|
722
|
+
raw_impls: Dict[str, Callable[..., Any]] = {
|
|
723
|
+
"browser_goto": goto,
|
|
724
|
+
"browser_click": click,
|
|
725
|
+
"browser_type": type_,
|
|
726
|
+
"browser_press": press,
|
|
727
|
+
"browser_scroll": scroll,
|
|
728
|
+
"browser_screenshot": screenshot,
|
|
729
|
+
"browser_read": read,
|
|
730
|
+
"browser_back": back,
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
def bind(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
734
|
+
# Callable either way, like the world's own tools: impl({"selector": s})
|
|
735
|
+
# or impl(selector=s).
|
|
736
|
+
def impl(args: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Any:
|
|
737
|
+
payload = dict(args or {})
|
|
738
|
+
payload.update(kwargs)
|
|
739
|
+
return fn(**payload)
|
|
740
|
+
|
|
741
|
+
impl.__name__ = fn.__name__
|
|
742
|
+
return impl
|
|
743
|
+
|
|
744
|
+
return WorldToolkit(
|
|
745
|
+
schemas=[schema_from_signature(spec) for spec in COMPUTER_USE_SPECS],
|
|
746
|
+
impls={name: bind(fn) for name, fn in raw_impls.items()},
|
|
747
|
+
specs=list(COMPUTER_USE_SPECS),
|
|
748
|
+
)
|