dispatch_agents 0.12.2__tar.gz → 0.13.3__tar.gz
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.
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/PKG-INFO +1 -1
- dispatch_agents-0.13.3/RELEASE_NOTES.md +2 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/dispatch_agents/__init__.py +3 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/dispatch_agents/config.py +165 -2
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/dispatch_agents/integrations/github/__init__.py +31 -13
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/dispatch_agents/models.py +11 -4
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/company-researcher/dispatch.yaml +0 -1
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/conversational-agent/dispatch.yaml +0 -1
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/daily-digest/dispatch.yaml +0 -1
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/deep-research/dispatch.yaml +0 -1
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/hello_world/agent.py +105 -12
- dispatch_agents-0.13.3/examples/hello_world/dispatch.yaml +16 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/hello_world/uv.lock +50 -49
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/knowledge-base-query/dispatch.yaml +0 -1
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/multi-framework/dispatch.yaml +0 -1
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/weather-assistant/dispatch.yaml +0 -1
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/weather-service/dispatch.yaml +0 -1
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/pyproject.toml +2 -1
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/tests/test_config.py +240 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/tests/test_github_integration.py +384 -429
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/tests/test_github_schema_compliance.py +93 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/uv.lock +367 -328
- dispatch_agents-0.12.2/RELEASE_NOTES.md +0 -1
- dispatch_agents-0.12.2/examples/hello_world/dispatch.yaml +0 -9
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/.claude-plugin/marketplace.json +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/.github/scripts/change_scope.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/.github/scripts/ci_git.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/.github/scripts/version_policy.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/.github/tests/test_change_scope.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/.github/tests/test_ci_git.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/.github/tests/test_version_policy.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/.github/workflows/ci-reusable.yml +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/.github/workflows/feature-branch.yml +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/.github/workflows/release.yml +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/.github/workflows/version-policy-reusable.yml +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/.gitignore +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/CONTRIBUTING.md +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/LICENSE +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/LICENSE-3rdparty.csv +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/NOTICE +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/README.md +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/agentservice/__init__.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/agentservice/py.typed +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/agentservice/v1/__init__.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/agentservice/v1/message_pb2.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/agentservice/v1/message_pb2.pyi +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/agentservice/v1/message_pb2_grpc.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/agentservice/v1/request_response_pb2.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/agentservice/v1/request_response_pb2.pyi +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/agentservice/v1/request_response_pb2_grpc.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/agentservice/v1/service_pb2.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/agentservice/v1/service_pb2.pyi +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/agentservice/v1/service_pb2_grpc.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/dispatch_agents/agent_service.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/dispatch_agents/contrib/__init__.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/dispatch_agents/contrib/claude/__init__.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/dispatch_agents/contrib/openai/__init__.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/dispatch_agents/events.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/dispatch_agents/grpc_server.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/dispatch_agents/instrument.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/dispatch_agents/integrations/__init__.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/dispatch_agents/integrations/github/README.md +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/dispatch_agents/integrations/github/client.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/dispatch_agents/invocation.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/dispatch_agents/llm.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/dispatch_agents/llm_langchain.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/dispatch_agents/logging_config.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/dispatch_agents/mcp.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/dispatch_agents/memory.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/dispatch_agents/proxy/__init__.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/dispatch_agents/proxy/server.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/dispatch_agents/proxy/sse_utils.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/dispatch_agents/py.typed +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/dispatch_agents/resources.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/dispatch_agents/version.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/README.md +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/company-researcher/.gitignore +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/company-researcher/AGENTS.md +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/company-researcher/README.md +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/company-researcher/agent.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/company-researcher/pyproject.toml +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/company-researcher/uv.lock +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/conversational-agent/README.md +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/conversational-agent/agent.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/conversational-agent/pyproject.toml +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/daily-digest/README.md +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/daily-digest/agent.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/daily-digest/pyproject.toml +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/deep-research/README.md +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/deep-research/agent.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/deep-research/configuration.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/deep-research/deep_researcher.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/deep-research/prompts.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/deep-research/pyproject.toml +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/deep-research/state.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/deep-research/tools.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/hello_world/.gitignore +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/hello_world/AGENTS.md +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/hello_world/pyproject.toml +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/hello_world/test_agent.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/knowledge-base-query/.env.example +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/knowledge-base-query/.gitignore +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/knowledge-base-query/AGENTS.md +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/knowledge-base-query/agent.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/knowledge-base-query/pyproject.toml +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/multi-framework/README.md +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/multi-framework/agent.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/multi-framework/pyproject.toml +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/pyproject.toml +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/uv.lock +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/weather-assistant/.gitignore +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/weather-assistant/AGENTS.md +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/weather-assistant/agent.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/weather-assistant/pyproject.toml +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/weather-assistant/uv.lock +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/weather-service/.gitignore +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/weather-service/AGENTS.md +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/weather-service/agent.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/weather-service/pyproject.toml +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/examples/weather-service/uv.lock +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/internal/py.typed +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/plugins/README.md +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/tests/__init__.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/tests/e2e_claude_mcp_proxy.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/tests/schemas/README.md +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/tests/schemas/octokit-webhooks.json +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/tests/test.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/tests/test_agent_service.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/tests/test_agent_uid.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/tests/test_contrib_claude.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/tests/test_contrib_openai.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/tests/test_dev_mode_isolation.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/tests/test_extra_headers.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/tests/test_fn_decorator.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/tests/test_github_client.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/tests/test_grpc_server.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/tests/test_init.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/tests/test_instrument.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/tests/test_llm_langchain.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/tests/test_llm_logging.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/tests/test_logging_config.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/tests/test_mcp.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/tests/test_memory.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/tests/test_proxy_e2e.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/tests/test_proxy_server.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/tests/test_resources.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/tests/test_sse_utils.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/tests/test_trace_context.py +0 -0
- {dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/tests/test_typed_events.py +0 -0
|
@@ -162,6 +162,7 @@ def _dev_mode_audit_hook(event: str, args: tuple) -> None:
|
|
|
162
162
|
|
|
163
163
|
|
|
164
164
|
from .agent_service import AgentServiceClient
|
|
165
|
+
from .config import _runtime as config
|
|
165
166
|
from .events import (
|
|
166
167
|
HANDLER_METADATA,
|
|
167
168
|
REGISTERED_HANDLERS,
|
|
@@ -208,6 +209,8 @@ from .models import (
|
|
|
208
209
|
)
|
|
209
210
|
|
|
210
211
|
__all__ = [
|
|
212
|
+
# runtime config
|
|
213
|
+
"config",
|
|
211
214
|
# storage and dev mode isolation
|
|
212
215
|
"get_data_dir",
|
|
213
216
|
"DisallowedWriteError",
|
|
@@ -433,7 +433,6 @@ class DispatchConfig(BaseModel):
|
|
|
433
433
|
namespace: skunkworks
|
|
434
434
|
agent_name: my-agent
|
|
435
435
|
entrypoint: agent.py
|
|
436
|
-
base_image: python:3.13-slim
|
|
437
436
|
env:
|
|
438
437
|
LOG_LEVEL: debug
|
|
439
438
|
MY_APP_MODE: production
|
|
@@ -464,7 +463,10 @@ class DispatchConfig(BaseModel):
|
|
|
464
463
|
)
|
|
465
464
|
base_image: str | None = Field(
|
|
466
465
|
default=None,
|
|
467
|
-
description=
|
|
466
|
+
description=(
|
|
467
|
+
"Currently only python:3.13-slim is supported; defaults to it "
|
|
468
|
+
"when omitted. Other values are rejected at deploy time."
|
|
469
|
+
),
|
|
468
470
|
)
|
|
469
471
|
system_packages: list[str] | None = Field(
|
|
470
472
|
default=None,
|
|
@@ -500,6 +502,49 @@ class DispatchConfig(BaseModel):
|
|
|
500
502
|
raise ValueError(f"All env values must be strings. {examples}")
|
|
501
503
|
return v
|
|
502
504
|
|
|
505
|
+
vars: dict[str, Any] | None = Field(
|
|
506
|
+
default=None,
|
|
507
|
+
description="Configuration variables accessible at runtime via dispatch_agents.config.vars. "
|
|
508
|
+
"Unlike env, these are NOT injected as environment variables. "
|
|
509
|
+
"Supports any YAML-serializable type. Use {value: <any>, description: <str>} "
|
|
510
|
+
"to attach descriptions for the UI.",
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
@field_validator("vars", mode="before")
|
|
514
|
+
@classmethod
|
|
515
|
+
def _validate_vars(cls, v: dict | None) -> dict[str, Any] | None:
|
|
516
|
+
"""Validate vars format.
|
|
517
|
+
|
|
518
|
+
Non-dict values (str, int, float, bool, list, None) are allowed as-is.
|
|
519
|
+
Dict values must use the described-var shape:
|
|
520
|
+
``{value: <any>, description?: <str>}``. Other dicts are rejected
|
|
521
|
+
because ``value`` and ``description`` are reserved keywords — silently
|
|
522
|
+
treating arbitrary dicts as plain values would let a caller
|
|
523
|
+
accidentally collide with the described-var schema.
|
|
524
|
+
"""
|
|
525
|
+
if not v or not isinstance(v, dict):
|
|
526
|
+
return v
|
|
527
|
+
_DESCRIBED_KEYS = {"value", "description"}
|
|
528
|
+
for key, val in v.items():
|
|
529
|
+
if not isinstance(val, dict):
|
|
530
|
+
continue
|
|
531
|
+
if "value" not in val:
|
|
532
|
+
raise ValueError(
|
|
533
|
+
f"Var '{key}' is a dict without a 'value' key. Dict values "
|
|
534
|
+
"must use the described-var shape "
|
|
535
|
+
"{value: <any>, description?: <str>}. To store structured "
|
|
536
|
+
"data, nest it under 'value'."
|
|
537
|
+
)
|
|
538
|
+
extra = set(val.keys()) - _DESCRIBED_KEYS
|
|
539
|
+
if extra:
|
|
540
|
+
raise ValueError(
|
|
541
|
+
f"Var '{key}' has unexpected keys {sorted(extra)}. Dict "
|
|
542
|
+
"values may only use the described-var shape "
|
|
543
|
+
"{value: <any>, description?: <str>}. To store structured "
|
|
544
|
+
"data, nest it under 'value'."
|
|
545
|
+
)
|
|
546
|
+
return v
|
|
547
|
+
|
|
503
548
|
secrets: list[SecretConfig] | None = Field(
|
|
504
549
|
default=None,
|
|
505
550
|
description="Secrets to inject as environment variables",
|
|
@@ -573,10 +618,14 @@ class DispatchConfig(BaseModel):
|
|
|
573
618
|
result["local_dependencies"] = self.local_dependencies
|
|
574
619
|
if self.env:
|
|
575
620
|
result["env"] = dict(self.env)
|
|
621
|
+
if self.vars:
|
|
622
|
+
result["vars"] = dict(self.vars)
|
|
576
623
|
if self.secrets:
|
|
577
624
|
result["secrets"] = [
|
|
578
625
|
{"name": s.name, "secret_id": s.secret_id} for s in self.secrets
|
|
579
626
|
]
|
|
627
|
+
if self.mcp_servers:
|
|
628
|
+
result["mcp_servers"] = [{"server": m.server} for m in self.mcp_servers]
|
|
580
629
|
if self.volumes:
|
|
581
630
|
result["volumes"] = [
|
|
582
631
|
{"name": v.name, "mountPath": v.mount_path, "mode": v.mode.value}
|
|
@@ -602,3 +651,117 @@ class DispatchConfig(BaseModel):
|
|
|
602
651
|
return result
|
|
603
652
|
|
|
604
653
|
model_config = {"populate_by_name": True}
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
# ---------------------------------------------------------------------------
|
|
657
|
+
# Runtime config singleton
|
|
658
|
+
# ---------------------------------------------------------------------------
|
|
659
|
+
|
|
660
|
+
import functools as _functools
|
|
661
|
+
|
|
662
|
+
_DISPATCH_YAML_PATH = "/app/dispatch.yaml"
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
@_functools.lru_cache(maxsize=1)
|
|
666
|
+
def _load_runtime_config() -> "DispatchConfig":
|
|
667
|
+
"""Load dispatch.yaml once and cache the parsed DispatchConfig.
|
|
668
|
+
|
|
669
|
+
Falls back to an empty DispatchConfig when the file is absent
|
|
670
|
+
(dev mode or non-containerized runs). Namespace and agent_name
|
|
671
|
+
additionally fall back to DISPATCH_NAMESPACE / DISPATCH_AGENT_NAME
|
|
672
|
+
env vars so local ``dispatch agent dev`` runs work transparently.
|
|
673
|
+
"""
|
|
674
|
+
import yaml as _yaml
|
|
675
|
+
|
|
676
|
+
path = os.environ.get("DISPATCH_CONFIG_PATH", _DISPATCH_YAML_PATH)
|
|
677
|
+
raw: dict = {}
|
|
678
|
+
if os.path.exists(path):
|
|
679
|
+
with open(path, encoding="utf-8") as f:
|
|
680
|
+
raw = _yaml.safe_load(f) or {}
|
|
681
|
+
|
|
682
|
+
cfg = DispatchConfig.model_validate(raw)
|
|
683
|
+
|
|
684
|
+
# Env-var fallbacks for identity fields used outside containers
|
|
685
|
+
updates: dict[str, Any] = {}
|
|
686
|
+
if cfg.namespace is None:
|
|
687
|
+
ns = os.environ.get("DISPATCH_NAMESPACE")
|
|
688
|
+
if ns:
|
|
689
|
+
updates["namespace"] = ns
|
|
690
|
+
if cfg.agent_name is None:
|
|
691
|
+
name = os.environ.get("DISPATCH_AGENT_NAME")
|
|
692
|
+
if name:
|
|
693
|
+
updates["agent_name"] = name
|
|
694
|
+
if updates:
|
|
695
|
+
cfg = cfg.model_copy(update=updates)
|
|
696
|
+
|
|
697
|
+
return cfg
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
_DESCRIBED_VAR_KEYS = frozenset({"value", "description"})
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
def _is_described_var(val: Any) -> bool:
|
|
704
|
+
"""Check if a value is a described var ({value, description}) vs a plain dict."""
|
|
705
|
+
return (
|
|
706
|
+
isinstance(val, dict)
|
|
707
|
+
and "value" in val
|
|
708
|
+
and set(val.keys()) <= _DESCRIBED_VAR_KEYS
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def _unwrap_vars(raw: dict[str, Any] | None) -> dict[str, Any]:
|
|
713
|
+
"""Unwrap described vars ({value, description}) to plain values.
|
|
714
|
+
|
|
715
|
+
Plain dicts (with keys outside {value, description}) are passed through as-is.
|
|
716
|
+
"""
|
|
717
|
+
if not raw:
|
|
718
|
+
return {}
|
|
719
|
+
result: dict[str, Any] = {}
|
|
720
|
+
for key, val in raw.items():
|
|
721
|
+
if _is_described_var(val):
|
|
722
|
+
result[key] = val["value"]
|
|
723
|
+
else:
|
|
724
|
+
result[key] = val
|
|
725
|
+
return result
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def _extract_var_descriptions(raw: dict[str, Any] | None) -> dict[str, str]:
|
|
729
|
+
"""Extract description strings from described vars."""
|
|
730
|
+
if not raw:
|
|
731
|
+
return {}
|
|
732
|
+
result: dict[str, str] = {}
|
|
733
|
+
for key, val in raw.items():
|
|
734
|
+
if _is_described_var(val) and "description" in val:
|
|
735
|
+
result[key] = val["description"]
|
|
736
|
+
return result
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
class _RuntimeConfig:
|
|
740
|
+
"""Proxy that exposes dispatch.yaml fields at runtime.
|
|
741
|
+
|
|
742
|
+
Usage::
|
|
743
|
+
|
|
744
|
+
from dispatch_agents import config
|
|
745
|
+
|
|
746
|
+
config.namespace # str | None
|
|
747
|
+
config.agent_name # str | None
|
|
748
|
+
config.vars["temperature"] # 0.7 (unwrapped)
|
|
749
|
+
config.vars.get("missing") # None
|
|
750
|
+
config.vars_descriptions["max_turns"] # "Maximum agent turns"
|
|
751
|
+
"""
|
|
752
|
+
|
|
753
|
+
@property
|
|
754
|
+
def vars(self) -> dict[str, Any]:
|
|
755
|
+
"""Return unwrapped vars dict (described vars return just the value)."""
|
|
756
|
+
return _unwrap_vars(_load_runtime_config().vars)
|
|
757
|
+
|
|
758
|
+
@property
|
|
759
|
+
def vars_descriptions(self) -> dict[str, str]:
|
|
760
|
+
"""Return descriptions for vars that have them."""
|
|
761
|
+
return _extract_var_descriptions(_load_runtime_config().vars)
|
|
762
|
+
|
|
763
|
+
def __getattr__(self, name: str) -> Any:
|
|
764
|
+
return getattr(_load_runtime_config(), name)
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
_runtime = _RuntimeConfig()
|
{dispatch_agents-0.12.2 → dispatch_agents-0.13.3}/dispatch_agents/integrations/github/__init__.py
RENAMED
|
@@ -83,9 +83,9 @@ GitHub Topics:
|
|
|
83
83
|
|
|
84
84
|
from __future__ import annotations
|
|
85
85
|
|
|
86
|
-
from typing import Any, ClassVar, Literal
|
|
86
|
+
from typing import Any, ClassVar, Literal, Self
|
|
87
87
|
|
|
88
|
-
from pydantic import ConfigDict, Field
|
|
88
|
+
from pydantic import ConfigDict, Field, StrictInt, model_validator
|
|
89
89
|
|
|
90
90
|
from dispatch_agents.events import BasePayload
|
|
91
91
|
from dispatch_agents.integrations.github.client import GitHubAppToken
|
|
@@ -209,12 +209,12 @@ class GitHubRepository(GitHubModel):
|
|
|
209
209
|
description: str | None = Field(default=None, description="Repository description")
|
|
210
210
|
fork: bool = Field(default=False, description="Whether this is a fork")
|
|
211
211
|
url: str | None = Field(default=None, description="API URL")
|
|
212
|
-
created_at:
|
|
213
|
-
|
|
212
|
+
created_at: StrictInt | str = Field(
|
|
213
|
+
description="Creation timestamp (Unix epoch int or ISO8601 string)",
|
|
214
214
|
)
|
|
215
215
|
updated_at: str | None = Field(default=None, description="ISO8601 update timestamp")
|
|
216
|
-
pushed_at: str | None = Field(
|
|
217
|
-
|
|
216
|
+
pushed_at: StrictInt | str | None = Field(
|
|
217
|
+
description="Last push timestamp (Unix epoch int, ISO8601 string, or null)",
|
|
218
218
|
)
|
|
219
219
|
homepage: str | None = Field(default=None, description="Homepage URL")
|
|
220
220
|
size: int = Field(default=0, description="Repository size in KB")
|
|
@@ -490,7 +490,7 @@ class GitHubCommitUser(GitHubModel):
|
|
|
490
490
|
"""Git user information (author/committer)."""
|
|
491
491
|
|
|
492
492
|
name: str = Field(description="Git user name")
|
|
493
|
-
email: str = Field(description="Git user email")
|
|
493
|
+
email: str | None = Field(description="Git user email")
|
|
494
494
|
username: str | None = Field(default=None, description="GitHub username if linked")
|
|
495
495
|
date: str | None = Field(default=None, description="ISO8601 timestamp")
|
|
496
496
|
|
|
@@ -906,20 +906,38 @@ class PullRequestUnassigned(PullRequestBase):
|
|
|
906
906
|
assignee: GitHubUser = Field(description="User who was unassigned")
|
|
907
907
|
|
|
908
908
|
|
|
909
|
-
class
|
|
909
|
+
class PullRequestReviewTargetBase(PullRequestBase):
|
|
910
|
+
"""Base payload for pull_request review target events."""
|
|
911
|
+
|
|
912
|
+
requested_reviewer: GitHubUser | None = Field(
|
|
913
|
+
default=None,
|
|
914
|
+
description="User requested for review (absent when a team is requested)",
|
|
915
|
+
)
|
|
916
|
+
requested_team: GitHubTeam | None = Field(
|
|
917
|
+
default=None,
|
|
918
|
+
description="Team requested for review (absent when a user is requested)",
|
|
919
|
+
)
|
|
920
|
+
|
|
921
|
+
@model_validator(mode="after")
|
|
922
|
+
def validate_review_target(self) -> Self:
|
|
923
|
+
"""Require exactly one review target field."""
|
|
924
|
+
if (self.requested_reviewer is None) == (self.requested_team is None):
|
|
925
|
+
raise ValueError(
|
|
926
|
+
"Exactly one of requested_reviewer or requested_team must be provided"
|
|
927
|
+
)
|
|
928
|
+
return self
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
class PullRequestReviewRequested(PullRequestReviewTargetBase):
|
|
910
932
|
"""Payload for github.pull_request.review_requested events."""
|
|
911
933
|
|
|
912
934
|
_dispatch_topic: ClassVar[str] = "github.pull_request.review_requested"
|
|
913
|
-
requested_reviewer: GitHubUser = Field(description="User requested for review")
|
|
914
935
|
|
|
915
936
|
|
|
916
|
-
class PullRequestReviewRequestRemoved(
|
|
937
|
+
class PullRequestReviewRequestRemoved(PullRequestReviewTargetBase):
|
|
917
938
|
"""Payload for github.pull_request.review_request_removed events."""
|
|
918
939
|
|
|
919
940
|
_dispatch_topic: ClassVar[str] = "github.pull_request.review_request_removed"
|
|
920
|
-
requested_reviewer: GitHubUser = Field(
|
|
921
|
-
description="User whose review request was removed"
|
|
922
|
-
)
|
|
923
941
|
|
|
924
942
|
|
|
925
943
|
class PullRequestReadyForReview(PullRequestBase):
|
|
@@ -19,6 +19,13 @@ from typing import Annotated, Any, Literal, TypeAlias
|
|
|
19
19
|
|
|
20
20
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
21
21
|
|
|
22
|
+
_IDENTIFIER_PATTERN = r"^[a-zA-Z0-9._-]+$"
|
|
23
|
+
_IDENTIFIER_MAX_LENGTH = 128
|
|
24
|
+
Identifier: TypeAlias = Annotated[
|
|
25
|
+
str,
|
|
26
|
+
Field(min_length=1, max_length=_IDENTIFIER_MAX_LENGTH, pattern=_IDENTIFIER_PATTERN),
|
|
27
|
+
]
|
|
28
|
+
|
|
22
29
|
|
|
23
30
|
def get_now_utc() -> str:
|
|
24
31
|
"""Get the current UTC time in ISO8601 format."""
|
|
@@ -391,7 +398,7 @@ class AgentFunction(StrictBaseModel):
|
|
|
391
398
|
the triggers that can invoke it (topics, schedules, etc.).
|
|
392
399
|
"""
|
|
393
400
|
|
|
394
|
-
name:
|
|
401
|
+
name: Identifier = Field(description="Handler function name")
|
|
395
402
|
description: str | None = Field(
|
|
396
403
|
default=None, description="Handler docstring or description"
|
|
397
404
|
)
|
|
@@ -627,9 +634,9 @@ class PublishEventBody(StrictBaseModel):
|
|
|
627
634
|
class SubscriptionBody(StrictBaseModel):
|
|
628
635
|
"""Request body for agent subscription management."""
|
|
629
636
|
|
|
630
|
-
topics: list[
|
|
631
|
-
agent_name:
|
|
632
|
-
functions: list[AgentFunction]
|
|
637
|
+
topics: list[Identifier] = Field(default_factory=list)
|
|
638
|
+
agent_name: Identifier
|
|
639
|
+
functions: list[AgentFunction] = Field(min_length=1)
|
|
633
640
|
|
|
634
641
|
|
|
635
642
|
class EventRequest(StrictBaseModel):
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
"""Generated agent entrypoint."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
4
6
|
|
|
5
7
|
import aiohttp
|
|
6
8
|
import dispatch_agents
|
|
7
9
|
from dispatch_agents import BasePayload, fn, on
|
|
8
|
-
from dispatch_agents.integrations.github import
|
|
10
|
+
from dispatch_agents.integrations.github import (
|
|
11
|
+
CheckSuiteCompleted,
|
|
12
|
+
PullRequestReviewCommentCreated,
|
|
13
|
+
)
|
|
9
14
|
from pydantic import Field, PositiveInt
|
|
10
15
|
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
11
18
|
|
|
12
19
|
class GreetingPayload(BasePayload):
|
|
13
20
|
"""Input payload for greeting requests."""
|
|
@@ -30,7 +37,7 @@ async def greet(payload: GreetingPayload) -> GreetingResponse:
|
|
|
30
37
|
- Typed output serialization (returns GreetingResponse)
|
|
31
38
|
- Automatic schema extraction for API documentation
|
|
32
39
|
"""
|
|
33
|
-
|
|
40
|
+
logger.info("Handling greet request for: %s", payload.subject)
|
|
34
41
|
|
|
35
42
|
# Validate that subject field exists - ValueError is non-retryable
|
|
36
43
|
if not payload.subject:
|
|
@@ -60,13 +67,13 @@ class SleepResponse(BasePayload):
|
|
|
60
67
|
@dispatch_agents.on(topic="sleep")
|
|
61
68
|
async def sleep(payload: SleepRequest) -> SleepResponse:
|
|
62
69
|
"""Sleep for the specified duration, logging countdown progress."""
|
|
63
|
-
|
|
70
|
+
logger.info("Starting sleep for %s seconds", payload.duration_seconds)
|
|
64
71
|
|
|
65
72
|
for remaining in range(payload.duration_seconds, 0, -1):
|
|
66
|
-
|
|
73
|
+
logger.info("Countdown: %s seconds remaining", remaining)
|
|
67
74
|
await asyncio.sleep(1)
|
|
68
75
|
|
|
69
|
-
|
|
76
|
+
logger.info("Sleep completed")
|
|
70
77
|
return SleepResponse(seconds_slept=payload.duration_seconds)
|
|
71
78
|
|
|
72
79
|
|
|
@@ -83,9 +90,9 @@ async def on_pr_review_comment(
|
|
|
83
90
|
event: PullRequestReviewCommentCreated,
|
|
84
91
|
) -> PRReviewCommentResponse:
|
|
85
92
|
"""Handle GitHub PR review comment created events."""
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
93
|
+
logger.info("Received PR review comment from %s", event.comment.user.login)
|
|
94
|
+
logger.info("Comment body: %.100s...", event.comment.body)
|
|
95
|
+
logger.info("PR: %s", event.pull_request.title)
|
|
89
96
|
|
|
90
97
|
return PRReviewCommentResponse(
|
|
91
98
|
repo=event.repository.full_name,
|
|
@@ -94,6 +101,34 @@ async def on_pr_review_comment(
|
|
|
94
101
|
)
|
|
95
102
|
|
|
96
103
|
|
|
104
|
+
class CheckSuiteCompletedResponse(BasePayload):
|
|
105
|
+
"""Response for check_suite.completed events."""
|
|
106
|
+
|
|
107
|
+
repo: str | None = Field(description="Repository full name (owner/repo)")
|
|
108
|
+
head_sha: str = Field(description="Head commit SHA of the suite")
|
|
109
|
+
conclusion: str | None = Field(
|
|
110
|
+
description="Suite conclusion (success, failure, ...)"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@on(github_event=CheckSuiteCompleted)
|
|
115
|
+
async def on_check_suite_completed(
|
|
116
|
+
event: CheckSuiteCompleted,
|
|
117
|
+
) -> CheckSuiteCompletedResponse:
|
|
118
|
+
"""Handle GitHub check_suite.completed events."""
|
|
119
|
+
logger.info(
|
|
120
|
+
"Check suite completed: repo=%s sha=%s conclusion=%s",
|
|
121
|
+
event.repository.full_name if event.repository else None,
|
|
122
|
+
event.check_suite.head_sha,
|
|
123
|
+
event.check_suite.conclusion,
|
|
124
|
+
)
|
|
125
|
+
return CheckSuiteCompletedResponse(
|
|
126
|
+
repo=event.repository.full_name if event.repository else None,
|
|
127
|
+
head_sha=event.check_suite.head_sha,
|
|
128
|
+
conclusion=event.check_suite.conclusion,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
97
132
|
class ReverseRequest(BasePayload):
|
|
98
133
|
"""Input for the reverse function."""
|
|
99
134
|
|
|
@@ -109,10 +144,68 @@ class ReverseResponse(BasePayload):
|
|
|
109
144
|
@fn()
|
|
110
145
|
async def reverse(payload: ReverseRequest) -> ReverseResponse:
|
|
111
146
|
"""Reverse the provided text string."""
|
|
112
|
-
|
|
147
|
+
logger.info("Reversing: %r", payload.text)
|
|
113
148
|
return ReverseResponse(reversed_text=payload.text[::-1])
|
|
114
149
|
|
|
115
150
|
|
|
151
|
+
class StorageWriteRequest(BasePayload):
|
|
152
|
+
"""Input for writing to persistent storage."""
|
|
153
|
+
|
|
154
|
+
key: str = Field(description="Filename to write")
|
|
155
|
+
value: str = Field(description="Content to write")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class StorageWriteResponse(BasePayload):
|
|
159
|
+
"""Output of storage write."""
|
|
160
|
+
|
|
161
|
+
path: str = Field(description="Full path of the written file")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _safe_data_path(key: str) -> str:
|
|
165
|
+
"""Resolve a key to a path under /data, rejecting traversal attempts."""
|
|
166
|
+
path = os.path.abspath(os.path.join("/data", key))
|
|
167
|
+
if not path.startswith("/data/"):
|
|
168
|
+
raise ValueError("Invalid key: must resolve within /data")
|
|
169
|
+
return path
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@fn()
|
|
173
|
+
async def storage_write(payload: StorageWriteRequest) -> StorageWriteResponse:
|
|
174
|
+
"""Write a value to persistent storage at /data."""
|
|
175
|
+
path = _safe_data_path(payload.key)
|
|
176
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
177
|
+
with open(path, "w") as f:
|
|
178
|
+
f.write(payload.value)
|
|
179
|
+
logger.info("Wrote %s bytes to %s", len(payload.value), path)
|
|
180
|
+
return StorageWriteResponse(path=path)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class StorageReadRequest(BasePayload):
|
|
184
|
+
"""Input for reading from persistent storage."""
|
|
185
|
+
|
|
186
|
+
key: str = Field(description="Filename to read")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class StorageReadResponse(BasePayload):
|
|
190
|
+
"""Output of storage read."""
|
|
191
|
+
|
|
192
|
+
value: str | None = Field(description="File content, or null if not found")
|
|
193
|
+
exists: bool = Field(description="Whether the file exists")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@fn()
|
|
197
|
+
async def storage_read(payload: StorageReadRequest) -> StorageReadResponse:
|
|
198
|
+
"""Read a value from persistent storage at /data."""
|
|
199
|
+
path = _safe_data_path(payload.key)
|
|
200
|
+
if os.path.exists(path):
|
|
201
|
+
with open(path) as f:
|
|
202
|
+
value = f.read()
|
|
203
|
+
logger.info("Read %s bytes from %s", len(value), path)
|
|
204
|
+
return StorageReadResponse(value=value, exists=True)
|
|
205
|
+
logger.info("File not found: %s", path)
|
|
206
|
+
return StorageReadResponse(value=None, exists=False)
|
|
207
|
+
|
|
208
|
+
|
|
116
209
|
class EgressTestRequest(BasePayload):
|
|
117
210
|
"""Input for the egress test function."""
|
|
118
211
|
|
|
@@ -140,21 +233,21 @@ async def test_egress(payload: EgressTestRequest) -> EgressTestResponse:
|
|
|
140
233
|
configured, this request will be blocked unless the target domain is
|
|
141
234
|
in allow_domains.
|
|
142
235
|
"""
|
|
143
|
-
|
|
236
|
+
logger.info("Testing egress to: %s", payload.url)
|
|
144
237
|
try:
|
|
145
238
|
async with aiohttp.ClientSession(
|
|
146
239
|
timeout=aiohttp.ClientTimeout(total=10)
|
|
147
240
|
) as session:
|
|
148
241
|
async with session.get(payload.url) as resp:
|
|
149
242
|
body = await resp.text()
|
|
150
|
-
|
|
243
|
+
logger.info("Response: %s (%s bytes)", resp.status, len(body))
|
|
151
244
|
return EgressTestResponse(
|
|
152
245
|
success=True,
|
|
153
246
|
status_code=resp.status,
|
|
154
247
|
body=body[:1000],
|
|
155
248
|
)
|
|
156
249
|
except Exception as e:
|
|
157
|
-
|
|
250
|
+
logger.info("Request failed: %s: %s", type(e).__name__, e)
|
|
158
251
|
return EgressTestResponse(
|
|
159
252
|
success=False,
|
|
160
253
|
body=f"{type(e).__name__}: {e}",
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
namespace: examples
|
|
2
|
+
entrypoint: agent.py
|
|
3
|
+
system_packages: []
|
|
4
|
+
agent_name: hello-world
|
|
5
|
+
volumes:
|
|
6
|
+
- name: data
|
|
7
|
+
mountPath: /data
|
|
8
|
+
mode: read_write_many
|
|
9
|
+
resources:
|
|
10
|
+
limits:
|
|
11
|
+
cpu: 250m
|
|
12
|
+
memory: 2Gi
|
|
13
|
+
network:
|
|
14
|
+
egress:
|
|
15
|
+
allow_domains:
|
|
16
|
+
- match_name: jsonplaceholder.typicode.com
|