Module adcp.server.proposal

Proposal generation helpers.

Provides utilities for building ADCP Proposals in get_products responses. Proposals represent recommended media plans with budget allocations across products.

Functions

def proposals_not_supported(reason: str = 'This agent does not generate proposals') ‑> ProposalNotSupported
Expand source code
def proposals_not_supported(
    reason: str = "This agent does not generate proposals",
) -> ProposalNotSupported:
    """Create a response indicating proposals are not supported.

    Args:
        reason: Human-readable explanation

    Returns:
        ProposalNotSupported response
    """
    return ProposalNotSupported(
        proposals_supported=False,
        reason=reason,
        error=Error(
            code="PROPOSALS_NOT_SUPPORTED",
            message=reason,
        ),
    )

Create a response indicating proposals are not supported.

Args

reason
Human-readable explanation

Returns

ProposalNotSupported response

Classes

class AllocationBuilder (product_id: str, allocation_percentage: float)
Expand source code
class AllocationBuilder:
    """Builder for product allocations within a proposal."""

    def __init__(
        self,
        product_id: str,
        allocation_percentage: float,
    ):
        """Create an allocation builder.

        Args:
            product_id: ID of the product (must match a product in the response)
            allocation_percentage: Percentage of budget (0-100)
        """
        self._data: dict[str, Any] = {
            "product_id": product_id,
            "allocation_percentage": allocation_percentage,
        }

    def with_pricing_option(self, pricing_option_id: str) -> AllocationBuilder:
        """Specify which pricing option to use.

        Args:
            pricing_option_id: ID from the product's pricing_options array
        """
        self._data["pricing_option_id"] = pricing_option_id
        return self

    def with_rationale(self, rationale: str) -> AllocationBuilder:
        """Add explanation for this allocation.

        Args:
            rationale: Why this product/allocation is recommended
        """
        self._data["rationale"] = rationale
        return self

    def with_sequence(self, sequence: int) -> AllocationBuilder:
        """Set ordering hint for multi-line-item plans.

        Args:
            sequence: 1-based ordering position
        """
        self._data["sequence"] = sequence
        return self

    def with_tags(self, tags: list[str]) -> AllocationBuilder:
        """Add categorical tags.

        Args:
            tags: Tags like 'desktop', 'mobile', 'german'
        """
        self._data["tags"] = tags
        return self

    def build(self) -> dict[str, Any]:
        """Build the allocation dict."""
        return self._data.copy()

Builder for product allocations within a proposal.

Create an allocation builder.

Args

product_id
ID of the product (must match a product in the response)
allocation_percentage
Percentage of budget (0-100)

Methods

def build(self) ‑> dict[str, typing.Any]
Expand source code
def build(self) -> dict[str, Any]:
    """Build the allocation dict."""
    return self._data.copy()

Build the allocation dict.

def with_pricing_option(self, pricing_option_id: str) ‑> AllocationBuilder
Expand source code
def with_pricing_option(self, pricing_option_id: str) -> AllocationBuilder:
    """Specify which pricing option to use.

    Args:
        pricing_option_id: ID from the product's pricing_options array
    """
    self._data["pricing_option_id"] = pricing_option_id
    return self

Specify which pricing option to use.

Args

pricing_option_id
ID from the product's pricing_options array
def with_rationale(self, rationale: str) ‑> AllocationBuilder
Expand source code
def with_rationale(self, rationale: str) -> AllocationBuilder:
    """Add explanation for this allocation.

    Args:
        rationale: Why this product/allocation is recommended
    """
    self._data["rationale"] = rationale
    return self

Add explanation for this allocation.

Args

rationale
Why this product/allocation is recommended
def with_sequence(self, sequence: int) ‑> AllocationBuilder
Expand source code
def with_sequence(self, sequence: int) -> AllocationBuilder:
    """Set ordering hint for multi-line-item plans.

    Args:
        sequence: 1-based ordering position
    """
    self._data["sequence"] = sequence
    return self

Set ordering hint for multi-line-item plans.

Args

sequence
1-based ordering position
def with_tags(self, tags: list[str]) ‑> AllocationBuilder
Expand source code
def with_tags(self, tags: list[str]) -> AllocationBuilder:
    """Add categorical tags.

    Args:
        tags: Tags like 'desktop', 'mobile', 'german'
    """
    self._data["tags"] = tags
    return self

Add categorical tags.

Args

tags
Tags like 'desktop', 'mobile', 'german'
class ProposalBuilder (name: str, proposal_id: str | None = None)
Expand source code
class ProposalBuilder:
    """Builder for ADCP Proposals.

    Helps construct valid proposals for get_products responses. Proposals
    represent recommended media plans with budget allocations.

    Example:
        proposal = (
            ProposalBuilder("Q1 Brand Campaign")
            .with_description("Balanced awareness campaign")
            .add_allocation("product-1", 60)
                .with_rationale("High-impact display")
            .add_allocation("product-2", 40)
                .with_rationale("Contextual targeting")
            .with_budget_guidance(min=10000, recommended=25000, max=50000)
            .build()
        )
    """

    def __init__(self, name: str, proposal_id: str | None = None):
        """Create a new proposal builder.

        Args:
            name: Human-readable name for the proposal
            proposal_id: Unique ID (auto-generated if not provided)
        """
        self._name = name
        self._proposal_id = proposal_id or f"proposal-{uuid4().hex[:8]}"
        self._description: str | None = None
        self._brief_alignment: str | None = None
        self._expires_at: datetime | None = None
        self._allocations: list[dict[str, Any]] = []
        self._budget_guidance: dict[str, Any] | None = None
        self._current_allocation: AllocationBuilder | None = None
        self._ext: dict[str, Any] | None = None

    def with_description(self, description: str) -> ProposalBuilder:
        """Add description explaining the proposal strategy.

        Args:
            description: What the proposal achieves
        """
        self._finalize_allocation()
        self._description = description
        return self

    def with_brief_alignment(self, alignment: str) -> ProposalBuilder:
        """Explain how proposal aligns with campaign brief.

        Args:
            alignment: Alignment explanation
        """
        self._finalize_allocation()
        self._brief_alignment = alignment
        return self

    def expires_in(self, days: int = 7) -> ProposalBuilder:
        """Set expiration relative to now.

        Args:
            days: Number of days until expiration
        """
        self._finalize_allocation()
        self._expires_at = datetime.now(timezone.utc) + timedelta(days=days)
        return self

    def expires_at(self, expires: datetime) -> ProposalBuilder:
        """Set absolute expiration time.

        Args:
            expires: When the proposal expires
        """
        self._finalize_allocation()
        self._expires_at = expires
        return self

    def add_allocation(
        self,
        product_id: str,
        allocation_percentage: float,
    ) -> ProposalBuilder:
        """Add a product allocation.

        After calling this, chain allocation methods (with_rationale, etc.)
        before adding another allocation or calling build().

        Args:
            product_id: ID of the product
            allocation_percentage: Percentage of budget (0-100)

        Returns:
            Self for method chaining
        """
        self._finalize_allocation()
        self._current_allocation = AllocationBuilder(product_id, allocation_percentage)
        return self

    def with_pricing_option(self, pricing_option_id: str) -> ProposalBuilder:
        """Set pricing option for current allocation."""
        if self._current_allocation:
            self._current_allocation.with_pricing_option(pricing_option_id)
        return self

    def with_rationale(self, rationale: str) -> ProposalBuilder:
        """Add rationale for current allocation."""
        if self._current_allocation:
            self._current_allocation.with_rationale(rationale)
        return self

    def with_sequence(self, sequence: int) -> ProposalBuilder:
        """Set sequence for current allocation."""
        if self._current_allocation:
            self._current_allocation.with_sequence(sequence)
        return self

    def with_tags(self, tags: list[str]) -> ProposalBuilder:
        """Add tags for current allocation."""
        if self._current_allocation:
            self._current_allocation.with_tags(tags)
        return self

    def with_budget_guidance(
        self,
        *,
        min: float | None = None,
        recommended: float | None = None,
        max: float | None = None,
        currency: str = "USD",
    ) -> ProposalBuilder:
        """Add budget guidance for the proposal.

        Args:
            min: Minimum recommended budget
            recommended: Optimal budget
            max: Maximum before diminishing returns
            currency: ISO 4217 currency code
        """
        self._finalize_allocation()
        self._budget_guidance = {
            "currency": currency,
        }
        if min is not None:
            self._budget_guidance["min"] = min
        if recommended is not None:
            self._budget_guidance["recommended"] = recommended
        if max is not None:
            self._budget_guidance["max"] = max
        return self

    def with_extension(self, ext: dict[str, Any]) -> ProposalBuilder:
        """Add extension data.

        Args:
            ext: Extension object
        """
        self._finalize_allocation()
        self._ext = ext
        return self

    def _finalize_allocation(self) -> None:
        """Finalize current allocation and add to list."""
        if self._current_allocation:
            self._allocations.append(self._current_allocation.build())
            self._current_allocation = None

    def build(self) -> dict[str, Any]:
        """Build the proposal dict.

        Returns:
            Proposal as a dict ready for use in get_products response

        Raises:
            ValueError: If allocations don't sum to 100
        """
        self._finalize_allocation()

        if not self._allocations:
            raise ValueError("Proposal must have at least one allocation")

        total = sum(a["allocation_percentage"] for a in self._allocations)
        if abs(total - 100.0) > 0.01:
            raise ValueError(f"Allocation percentages must sum to 100, got {total}")

        proposal: dict[str, Any] = {
            "proposal_id": self._proposal_id,
            "name": self._name,
            "allocations": self._allocations,
        }

        if self._description:
            proposal["description"] = self._description
        if self._brief_alignment:
            proposal["brief_alignment"] = self._brief_alignment
        if self._expires_at:
            proposal["expires_at"] = self._expires_at.isoformat()
        if self._budget_guidance:
            proposal["total_budget_guidance"] = self._budget_guidance
        if self._ext:
            proposal["ext"] = self._ext

        return proposal

    def validate(self) -> list[str]:
        """Validate the proposal without building.

        Returns:
            List of validation errors (empty if valid)
        """
        errors: list[str] = []

        if self._current_allocation:
            allocations = self._allocations + [self._current_allocation.build()]
        else:
            allocations = self._allocations

        if not allocations:
            errors.append("Proposal must have at least one allocation")
        else:
            total = sum(a["allocation_percentage"] for a in allocations)
            if abs(total - 100.0) > 0.01:
                errors.append(f"Allocation percentages must sum to 100, got {total}")

        return errors

Builder for ADCP Proposals.

Helps construct valid proposals for get_products responses. Proposals represent recommended media plans with budget allocations.

Example

proposal = ( ProposalBuilder("Q1 Brand Campaign") .with_description("Balanced awareness campaign") .add_allocation("product-1", 60) .with_rationale("High-impact display") .add_allocation("product-2", 40) .with_rationale("Contextual targeting") .with_budget_guidance(min=10000, recommended=25000, max=50000) .build() )

Create a new proposal builder.

Args

name
Human-readable name for the proposal
proposal_id
Unique ID (auto-generated if not provided)

Methods

def add_allocation(self, product_id: str, allocation_percentage: float) ‑> ProposalBuilder
Expand source code
def add_allocation(
    self,
    product_id: str,
    allocation_percentage: float,
) -> ProposalBuilder:
    """Add a product allocation.

    After calling this, chain allocation methods (with_rationale, etc.)
    before adding another allocation or calling build().

    Args:
        product_id: ID of the product
        allocation_percentage: Percentage of budget (0-100)

    Returns:
        Self for method chaining
    """
    self._finalize_allocation()
    self._current_allocation = AllocationBuilder(product_id, allocation_percentage)
    return self

Add a product allocation.

After calling this, chain allocation methods (with_rationale, etc.) before adding another allocation or calling build().

Args

product_id
ID of the product
allocation_percentage
Percentage of budget (0-100)

Returns

Self for method chaining

def build(self) ‑> dict[str, typing.Any]
Expand source code
def build(self) -> dict[str, Any]:
    """Build the proposal dict.

    Returns:
        Proposal as a dict ready for use in get_products response

    Raises:
        ValueError: If allocations don't sum to 100
    """
    self._finalize_allocation()

    if not self._allocations:
        raise ValueError("Proposal must have at least one allocation")

    total = sum(a["allocation_percentage"] for a in self._allocations)
    if abs(total - 100.0) > 0.01:
        raise ValueError(f"Allocation percentages must sum to 100, got {total}")

    proposal: dict[str, Any] = {
        "proposal_id": self._proposal_id,
        "name": self._name,
        "allocations": self._allocations,
    }

    if self._description:
        proposal["description"] = self._description
    if self._brief_alignment:
        proposal["brief_alignment"] = self._brief_alignment
    if self._expires_at:
        proposal["expires_at"] = self._expires_at.isoformat()
    if self._budget_guidance:
        proposal["total_budget_guidance"] = self._budget_guidance
    if self._ext:
        proposal["ext"] = self._ext

    return proposal

Build the proposal dict.

Returns

Proposal as a dict ready for use in get_products response

Raises

ValueError
If allocations don't sum to 100
def expires_at(self, expires: datetime) ‑> ProposalBuilder
Expand source code
def expires_at(self, expires: datetime) -> ProposalBuilder:
    """Set absolute expiration time.

    Args:
        expires: When the proposal expires
    """
    self._finalize_allocation()
    self._expires_at = expires
    return self

Set absolute expiration time.

Args

expires
When the proposal expires
def expires_in(self, days: int = 7) ‑> ProposalBuilder
Expand source code
def expires_in(self, days: int = 7) -> ProposalBuilder:
    """Set expiration relative to now.

    Args:
        days: Number of days until expiration
    """
    self._finalize_allocation()
    self._expires_at = datetime.now(timezone.utc) + timedelta(days=days)
    return self

Set expiration relative to now.

Args

days
Number of days until expiration
def validate(self) ‑> list[str]
Expand source code
def validate(self) -> list[str]:
    """Validate the proposal without building.

    Returns:
        List of validation errors (empty if valid)
    """
    errors: list[str] = []

    if self._current_allocation:
        allocations = self._allocations + [self._current_allocation.build()]
    else:
        allocations = self._allocations

    if not allocations:
        errors.append("Proposal must have at least one allocation")
    else:
        total = sum(a["allocation_percentage"] for a in allocations)
        if abs(total - 100.0) > 0.01:
            errors.append(f"Allocation percentages must sum to 100, got {total}")

    return errors

Validate the proposal without building.

Returns

List of validation errors (empty if valid)

def with_brief_alignment(self, alignment: str) ‑> ProposalBuilder
Expand source code
def with_brief_alignment(self, alignment: str) -> ProposalBuilder:
    """Explain how proposal aligns with campaign brief.

    Args:
        alignment: Alignment explanation
    """
    self._finalize_allocation()
    self._brief_alignment = alignment
    return self

Explain how proposal aligns with campaign brief.

Args

alignment
Alignment explanation
def with_budget_guidance(self,
*,
min: float | None = None,
recommended: float | None = None,
max: float | None = None,
currency: str = 'USD') ‑> ProposalBuilder
Expand source code
def with_budget_guidance(
    self,
    *,
    min: float | None = None,
    recommended: float | None = None,
    max: float | None = None,
    currency: str = "USD",
) -> ProposalBuilder:
    """Add budget guidance for the proposal.

    Args:
        min: Minimum recommended budget
        recommended: Optimal budget
        max: Maximum before diminishing returns
        currency: ISO 4217 currency code
    """
    self._finalize_allocation()
    self._budget_guidance = {
        "currency": currency,
    }
    if min is not None:
        self._budget_guidance["min"] = min
    if recommended is not None:
        self._budget_guidance["recommended"] = recommended
    if max is not None:
        self._budget_guidance["max"] = max
    return self

Add budget guidance for the proposal.

Args

min
Minimum recommended budget
recommended
Optimal budget
max
Maximum before diminishing returns
currency
ISO 4217 currency code
def with_description(self, description: str) ‑> ProposalBuilder
Expand source code
def with_description(self, description: str) -> ProposalBuilder:
    """Add description explaining the proposal strategy.

    Args:
        description: What the proposal achieves
    """
    self._finalize_allocation()
    self._description = description
    return self

Add description explaining the proposal strategy.

Args

description
What the proposal achieves
def with_extension(self, ext: dict[str, Any]) ‑> ProposalBuilder
Expand source code
def with_extension(self, ext: dict[str, Any]) -> ProposalBuilder:
    """Add extension data.

    Args:
        ext: Extension object
    """
    self._finalize_allocation()
    self._ext = ext
    return self

Add extension data.

Args

ext
Extension object
def with_pricing_option(self, pricing_option_id: str) ‑> ProposalBuilder
Expand source code
def with_pricing_option(self, pricing_option_id: str) -> ProposalBuilder:
    """Set pricing option for current allocation."""
    if self._current_allocation:
        self._current_allocation.with_pricing_option(pricing_option_id)
    return self

Set pricing option for current allocation.

def with_rationale(self, rationale: str) ‑> ProposalBuilder
Expand source code
def with_rationale(self, rationale: str) -> ProposalBuilder:
    """Add rationale for current allocation."""
    if self._current_allocation:
        self._current_allocation.with_rationale(rationale)
    return self

Add rationale for current allocation.

def with_sequence(self, sequence: int) ‑> ProposalBuilder
Expand source code
def with_sequence(self, sequence: int) -> ProposalBuilder:
    """Set sequence for current allocation."""
    if self._current_allocation:
        self._current_allocation.with_sequence(sequence)
    return self

Set sequence for current allocation.

def with_tags(self, tags: list[str]) ‑> ProposalBuilder
Expand source code
def with_tags(self, tags: list[str]) -> ProposalBuilder:
    """Add tags for current allocation."""
    if self._current_allocation:
        self._current_allocation.with_tags(tags)
    return self

Add tags for current allocation.

class ProposalNotSupported (**data: Any)
Expand source code
class ProposalNotSupported(BaseModel):
    """Response indicating proposal generation is not supported.

    Use this when your agent supports get_products but not proposal generation.
    """

    proposals_supported: bool = False
    reason: str = "This agent does not generate proposals"
    error: Error | None = None

Response indicating proposal generation is not supported.

Use this when your agent supports get_products but not proposal generation.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • pydantic.main.BaseModel

Class variables

var errorError | None
var model_config
var proposals_supported : bool
var reason : str