Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/adcp/decisioning/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,41 @@ def advertised_tools_for_instance(self) -> frozenset[str]:
serving |= set(tools)
return frozenset(serving)

def get_advertised_tools(self, *, advertise_all: bool | None = None) -> frozenset[str]:
"""Names ``tools/list`` will return when this handler is served.

The class-level :attr:`advertised_tools` set is the *universe*
of tools the handler base supports across all specialisms (~50
entries on :class:`PlatformHandler`). What buyers actually see
on the wire is narrower:

1. Per-instance specialism filter — :meth:`advertised_tools_for_instance`
intersects the universe with the platform's claimed
specialisms (a sales-only adopter drops audience/governance
tools).
2. Override-detection filter — tools whose handler method is
still the SDK's ``not_supported`` default are dropped
(``advertise_all=False``, the spec-aligned default).

This method runs the same pipeline :func:`adcp.server.serve`
runs at boot, so adopters can inspect the effective set without
standing up a network port. The default ``advertise_all`` value
is whatever was configured on
:func:`adcp.decisioning.create_adcp_server_from_platform`
(``False`` when not set).

:param advertise_all: Override the configured value for this
call. ``True`` returns the per-specialism set without the
override filter; ``False`` applies the full filter.
:returns: Frozen set of tool names.
"""
from adcp.server.mcp_tools import get_tools_for_handler

effective = self._advertise_all if advertise_all is None else advertise_all
return frozenset(
tool["name"] for tool in get_tools_for_handler(self, advertise_all=effective)
)

def __init__(
self,
platform: DecisioningPlatform,
Expand All @@ -709,6 +744,7 @@ def __init__(
buyer_agent_registry: BuyerAgentRegistry | None = None,
config_store: ProductConfigStore | None = None,
property_list_fetcher: PropertyListFetcher | None = None,
advertise_all: bool = False,
) -> None:
super().__init__()
self._platform = platform
Expand All @@ -722,6 +758,7 @@ def __init__(
self._buyer_agent_registry = buyer_agent_registry
self._config_store = config_store
self._property_list_fetcher = property_list_fetcher
self._advertise_all = advertise_all

# Cache whether the platform's create_media_buy accepts 'configs'
# so we only pay the inspect.signature cost at construction time.
Expand Down
12 changes: 12 additions & 0 deletions src/adcp/decisioning/serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ def create_adcp_server_from_platform(
buyer_agent_registry: BuyerAgentRegistry | None = None,
config_store: ProductConfigStore | None = None,
property_list_fetcher: PropertyListFetcher | None = None,
advertise_all: bool = False,
) -> tuple[PlatformHandler, ThreadPoolExecutor, TaskRegistry]:
"""Build the :class:`PlatformHandler` + supporting wiring from a
:class:`DecisioningPlatform`.
Expand Down Expand Up @@ -176,6 +177,15 @@ def create_adcp_server_from_platform(
(avoid duplicate delivery; idempotency-key dedup at the
receiver would handle it but explicit suppression matches the
v5 manual-emit posture for adopters mid-migration).
:param advertise_all: Mirror of the same flag on :func:`serve` —
controls how :meth:`PlatformHandler.get_advertised_tools` and
the eventual ``tools/list`` response filter the handler's tool
universe. ``False`` (default, spec-aligned) drops tools whose
method is still the SDK's ``not_supported`` shim; ``True``
advertises every tool the platform's claimed specialisms cover
regardless of override status. Stored on the returned handler
so adopters can call ``handler.get_advertised_tools()`` to
inspect the effective set without standing up a server.

To wire a :class:`ProposalManager` (v1 two-platform composition),
pass it on a :class:`PlatformRouter` via
Expand Down Expand Up @@ -286,6 +296,7 @@ def create_adcp_server_from_platform(
buyer_agent_registry=buyer_agent_registry,
config_store=config_store,
property_list_fetcher=property_list_fetcher,
advertise_all=advertise_all,
)

# Boot-time fail-fast: property_list_filtering declared but no fetcher wired.
Expand Down Expand Up @@ -428,6 +439,7 @@ def serve(
buyer_agent_registry=buyer_agent_registry,
config_store=config_store,
property_list_fetcher=property_list_fetcher,
advertise_all=advertise_all,
)

# Phase 1 sandbox-authority — wire the comply controller's account
Expand Down
80 changes: 80 additions & 0 deletions tests/test_decisioning_serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -504,3 +504,83 @@ def test_serve_does_not_fire_gate_for_platform_without_webhook_eligible_tools()
handler, executor, _ = create_adcp_server_from_platform(platform)
assert handler._webhook_sender is None
executor.shutdown(wait=True)


# ---- advertise_all kwarg + get_advertised_tools method ----


def test_get_advertised_tools_filters_to_claimed_specialisms() -> None:
"""``handler.get_advertised_tools()`` returns the effective set
``serve()`` would advertise — per-instance specialism filter +
protocol/discovery always-ons. Materially smaller than the
handler's class-level tool universe."""
platform = _SalesPlatformWithRequiredMethods()
handler, executor, _ = create_adcp_server_from_platform(
platform, auto_emit_completion_webhooks=False
)
advertised = handler.get_advertised_tools()
# The five overridden sales methods appear.
for tool in (
"get_products",
"create_media_buy",
"update_media_buy",
"sync_creatives",
"get_media_buy_delivery",
):
assert tool in advertised
# ``get_adcp_capabilities`` is always-on (protocol discovery).
assert "get_adcp_capabilities" in advertised
# Tools from specialisms the platform didn't claim are filtered out.
assert "build_creative" not in advertised # creative-builder
assert "acquire_rights" not in advertised # brand-rights
# Effective set is materially smaller than the class-level universe.
assert len(advertised) < len(type(handler).advertised_tools)
executor.shutdown(wait=True)


def test_get_advertised_tools_per_call_override_wins_over_configured_default() -> None:
"""The ``advertise_all`` kwarg on the method overrides the value
configured at factory time. Lets adopters inspect both modes from
a single handler."""
platform = _SalesPlatformWithRequiredMethods()
handler, executor, _ = create_adcp_server_from_platform(
platform, advertise_all=False, auto_emit_completion_webhooks=False
)
forced_universe = handler.get_advertised_tools(advertise_all=True)
forced_filtered = handler.get_advertised_tools(advertise_all=False)
assert forced_universe >= forced_filtered
executor.shutdown(wait=True)


def test_get_advertised_tools_returns_frozenset() -> None:
"""API guarantee: ``get_advertised_tools()`` returns a frozenset so
callers can intersect/union with other sets without worrying about
mutation."""
platform = _SalesPlatformWithRequiredMethods()
handler, executor, _ = create_adcp_server_from_platform(
platform, auto_emit_completion_webhooks=False
)
advertised = handler.get_advertised_tools()
assert isinstance(advertised, frozenset)
executor.shutdown(wait=True)


def test_create_adcp_server_from_platform_stores_advertise_all_on_handler() -> None:
"""``advertise_all=True`` on the factory threads to the handler's
configured default for :meth:`get_advertised_tools`."""
platform = _SalesPlatformWithRequiredMethods()
handler, executor, _ = create_adcp_server_from_platform(
platform, advertise_all=True, auto_emit_completion_webhooks=False
)
assert handler._advertise_all is True
executor.shutdown(wait=True)


def test_create_adcp_server_from_platform_advertise_all_default_false() -> None:
"""``advertise_all`` defaults to False, matching :func:`serve`."""
platform = _SalesPlatformWithRequiredMethods()
handler, executor, _ = create_adcp_server_from_platform(
platform, auto_emit_completion_webhooks=False
)
assert handler._advertise_all is False
executor.shutdown(wait=True)
Loading