Skip to content
Open
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
1 change: 1 addition & 0 deletions examples/dsp_deal_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ async def demo_unified_client_dsp_methods():
# Discover inventory
print("\n--- Discovering Inventory ---")
result = await client.discover_inventory(
query="display inventory",
channel="display",
max_cpm=25.0,
)
Expand Down
24 changes: 13 additions & 11 deletions src/ad_buyer/clients/unified_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -485,28 +485,30 @@ async def discover_inventory(
Returns:
UnifiedResult with discovered inventory
"""
# Build filters including identity context
filters = self._get_identity_context()
product_filters = {}
if channel:
filters["channel"] = channel
product_filters["channel"] = channel
if max_cpm is not None:
filters["maxPrice"] = max_cpm
product_filters["maxPrice"] = max_cpm
if min_impressions is not None:
filters["minImpressions"] = min_impressions
product_filters["minImpressions"] = min_impressions
if targeting:
filters["targeting"] = targeting
product_filters["targeting"] = targeting
if publisher:
filters["publisher"] = publisher
product_filters["publisher"] = publisher

if product_filters and not query:
raise ValueError(
"Product filters require a query; list_products does not support filters"
)

filters = {**self._get_identity_context(), **product_filters}
args = {"filters": filters}
if query:
args["query"] = query

# Try search_products first, fall back to list_products
if query:
return await self.call_tool("search_products", args, protocol=protocol)
else:
return await self.call_tool("list_products", protocol=protocol)
return await self.call_tool("list_products", protocol=protocol)

async def get_pricing(
self,
Expand Down
22 changes: 14 additions & 8 deletions src/ad_buyer/tools/buyer_deals/discover_inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,21 +153,27 @@ async def _arun(
) -> str:
"""Discover inventory with buyer identity context."""
try:
# Build filters
filters = {}
# Product filters are separate from identity. list_products cannot apply them.
product_filters = {}
if channel:
filters["channel"] = channel
product_filters["channel"] = channel
if max_cpm is not None:
filters["maxPrice"] = max_cpm
product_filters["maxPrice"] = max_cpm
if min_impressions is not None:
filters["minImpressions"] = min_impressions
product_filters["minImpressions"] = min_impressions
if targeting:
filters["targeting"] = targeting
product_filters["targeting"] = targeting
if publisher:
filters["publisher"] = publisher
product_filters["publisher"] = publisher

if product_filters and not query:
return (
"Error discovering inventory: Product filters require a query; "
"list_products does not support filters"
)

# Add identity context to filters
identity_context = self._buyer_context.identity.to_context_dict()
filters = dict(product_filters)
filters["buyer_context"] = identity_context

# Execute search
Expand Down
102 changes: 101 additions & 1 deletion tests/unit/test_buyer_deal_discovery_pricing.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import pytest

from ad_buyer.clients.unified_client import UnifiedClient
from ad_buyer.clients.unified_client import Protocol, UnifiedClient, UnifiedResult
from ad_buyer.models.buyer_identity import (
AccessTier,
BuyerContext,
Expand Down Expand Up @@ -197,6 +197,27 @@ async def test_discover_includes_identity_context(self, mock_client, advertiser_
assert "buyer_context" in filters
assert filters["buyer_context"]["access_tier"] == "advertiser"

@pytest.mark.asyncio
@pytest.mark.parametrize(
"filter_kwargs",
[
{"channel": "ctv"},
{"max_cpm": 20.0},
{"min_impressions": 5_000_000},
{"targeting": ["household"]},
{"publisher": "hulu"},
],
)
async def test_discover_rejects_product_filters_without_query(
self, mock_client, agency_context, filter_kwargs
):
"""Agents call this tool, so a filter without a query must fail here too."""
tool = DiscoverInventoryTool(client=mock_client, buyer_context=agency_context)
result = await tool._arun(**filter_kwargs)
assert "Product filters require a query" in result
mock_client.search_products.assert_not_called()
mock_client.list_products.assert_not_called()


class TestDiscoverInventoryFormatting:
"""Test output formatting in DiscoverInventoryTool."""
Expand Down Expand Up @@ -886,6 +907,85 @@ def test_get_activation_case_insensitive(self):
class TestUnifiedClientBuyerDealMethods:
"""Test buyer deal-specific methods on UnifiedClient."""

@pytest.mark.asyncio
async def test_discover_inventory_searches_with_query_and_filters(self, agency_identity):
client = UnifiedClient(base_url="http://localhost:8080", buyer_identity=agency_identity)
expected_result = UnifiedResult(data=[{"id": "ctv-1"}])
client.call_tool = AsyncMock(return_value=expected_result)

result = await client.discover_inventory(
query="CTV inventory",
channel="ctv",
max_cpm=30,
min_impressions=1000,
targeting=["household"],
publisher="example.test",
protocol=Protocol.A2A,
)

assert result is expected_result
client.call_tool.assert_awaited_once_with(
"search_products",
{
"query": "CTV inventory",
"filters": {
**agency_identity.to_context_dict(),
"channel": "ctv",
"maxPrice": 30,
"minImpressions": 1000,
"targeting": ["household"],
"publisher": "example.test",
},
},
protocol=Protocol.A2A,
)

@pytest.mark.asyncio
@pytest.mark.parametrize(
"filter_kwargs",
[
{"channel": "ctv"},
{"max_cpm": 30},
{"min_impressions": 1000},
{"targeting": ["household"]},
{"publisher": "example.test"},
],
)
async def test_discover_inventory_rejects_filters_without_query(
self, agency_identity, filter_kwargs
):
client = UnifiedClient(base_url="http://localhost:8080", buyer_identity=agency_identity)
client.call_tool = AsyncMock()

with pytest.raises(ValueError, match="Product filters require a query"):
await client.discover_inventory(**filter_kwargs)

client.call_tool.assert_not_awaited()

@pytest.mark.asyncio
async def test_discover_inventory_query_without_product_filters_searches(self, agency_identity):
client = UnifiedClient(base_url="http://localhost:8080", buyer_identity=agency_identity)
client.call_tool = AsyncMock(return_value=UnifiedResult(data=[]))

await client.discover_inventory(query="CTV inventory")

client.call_tool.assert_awaited_once_with(
"search_products",
{"query": "CTV inventory", "filters": agency_identity.to_context_dict()},
protocol=None,
)

@pytest.mark.asyncio
async def test_discover_inventory_without_query_or_product_filters_lists_products(
self, agency_identity
):
client = UnifiedClient(base_url="http://localhost:8080", buyer_identity=agency_identity)
client.call_tool = AsyncMock(return_value=UnifiedResult(data=[]))

await client.discover_inventory()

client.call_tool.assert_awaited_once_with("list_products", protocol=None)

def test_set_buyer_identity(self, advertiser_identity):
"""set_buyer_identity should store the identity."""
client = UnifiedClient(base_url="http://localhost:8080")
Expand Down
Loading