From 48251f8babfbb25187184ff386e55d2f6986c458 Mon Sep 17 00:00:00 2001 From: agentmoose Date: Wed, 25 Feb 2026 10:00:10 -0700 Subject: [PATCH 1/3] salesagent docs --- _data/dropdown_v2.yml | 11 +- _data/sidebar.yml | 483 ++++++++++++- _layouts/home.html | 2 +- agents/salesagent.md | 113 +-- agents/salesagent/architecture.md | 411 +++++++++++ .../deployment/deployment-overview.md | 106 +++ agents/salesagent/deployment/fly-io.md | 327 +++++++++ agents/salesagent/deployment/gcp.md | 380 ++++++++++ agents/salesagent/deployment/multi-tenant.md | 280 ++++++++ agents/salesagent/deployment/single-tenant.md | 350 ++++++++++ agents/salesagent/developers/contributing.md | 220 ++++++ agents/salesagent/developers/dev-setup.md | 330 +++++++++ agents/salesagent/developers/migrations.md | 238 +++++++ .../developers/source-architecture.md | 448 ++++++++++++ agents/salesagent/developers/testing.md | 202 ++++++ .../getting-started/buy-side-integration.md | 293 ++++++++ .../getting-started/configuration.md | 347 +++++++++ .../getting-started/publisher-onboarding.md | 279 ++++++++ .../salesagent/getting-started/quickstart.md | 197 ++++++ agents/salesagent/glossary.md | 58 ++ .../salesagent/integrations/adcp-ecosystem.md | 139 ++++ .../salesagent/integrations/custom-adapter.md | 430 ++++++++++++ agents/salesagent/integrations/gam.md | 332 +++++++++ .../salesagent/integrations/mock-adapter.md | 246 +++++++ agents/salesagent/operations/admin-ui.md | 310 +++++++++ agents/salesagent/operations/monitoring.md | 290 ++++++++ agents/salesagent/operations/security.md | 299 ++++++++ agents/salesagent/overview.md | 133 ++++ agents/salesagent/protocols.md | 180 +++++ .../reference/adcp-cross-references.md | 125 ++++ agents/salesagent/reference/error-codes.md | 276 ++++++++ agents/salesagent/schemas/adcp-types.md | 217 ++++++ agents/salesagent/schemas/api-schemas.md | 475 +++++++++++++ agents/salesagent/schemas/database-models.md | 207 ++++++ agents/salesagent/tools/content-standards.md | 34 + agents/salesagent/tools/create-media-buy.md | 203 ++++++ .../salesagent/tools/get-adcp-capabilities.md | 111 +++ .../tools/get-media-buy-delivery.md | 190 +++++ agents/salesagent/tools/get-media-buys.md | 130 ++++ agents/salesagent/tools/get-products.md | 166 +++++ .../tools/list-authorized-properties.md | 116 +++ .../salesagent/tools/list-creative-formats.md | 168 +++++ agents/salesagent/tools/list-creatives.md | 153 ++++ agents/salesagent/tools/log-event.md | 30 + .../tools/provide-performance-feedback.md | 30 + agents/salesagent/tools/sync-catalogs.md | 30 + agents/salesagent/tools/sync-creatives.md | 219 ++++++ agents/salesagent/tools/sync-event-sources.md | 30 + agents/salesagent/tools/tool-reference.md | 108 +++ agents/salesagent/tools/update-media-buy.md | 177 +++++ .../tools/update-performance-index.md | 116 +++ agents/salesagent/tools/workflow-tools.md | 301 ++++++++ .../tutorials/campaign-lifecycle.md | 658 ++++++++++++++++++ agents/salesagent/tutorials/catalog-setup.md | 221 ++++++ agents/salesagent/tutorials/mock-testing.md | 200 ++++++ 55 files changed, 12011 insertions(+), 114 deletions(-) create mode 100644 agents/salesagent/architecture.md create mode 100644 agents/salesagent/deployment/deployment-overview.md create mode 100644 agents/salesagent/deployment/fly-io.md create mode 100644 agents/salesagent/deployment/gcp.md create mode 100644 agents/salesagent/deployment/multi-tenant.md create mode 100644 agents/salesagent/deployment/single-tenant.md create mode 100644 agents/salesagent/developers/contributing.md create mode 100644 agents/salesagent/developers/dev-setup.md create mode 100644 agents/salesagent/developers/migrations.md create mode 100644 agents/salesagent/developers/source-architecture.md create mode 100644 agents/salesagent/developers/testing.md create mode 100644 agents/salesagent/getting-started/buy-side-integration.md create mode 100644 agents/salesagent/getting-started/configuration.md create mode 100644 agents/salesagent/getting-started/publisher-onboarding.md create mode 100644 agents/salesagent/getting-started/quickstart.md create mode 100644 agents/salesagent/glossary.md create mode 100644 agents/salesagent/integrations/adcp-ecosystem.md create mode 100644 agents/salesagent/integrations/custom-adapter.md create mode 100644 agents/salesagent/integrations/gam.md create mode 100644 agents/salesagent/integrations/mock-adapter.md create mode 100644 agents/salesagent/operations/admin-ui.md create mode 100644 agents/salesagent/operations/monitoring.md create mode 100644 agents/salesagent/operations/security.md create mode 100644 agents/salesagent/overview.md create mode 100644 agents/salesagent/protocols.md create mode 100644 agents/salesagent/reference/adcp-cross-references.md create mode 100644 agents/salesagent/reference/error-codes.md create mode 100644 agents/salesagent/schemas/adcp-types.md create mode 100644 agents/salesagent/schemas/api-schemas.md create mode 100644 agents/salesagent/schemas/database-models.md create mode 100644 agents/salesagent/tools/content-standards.md create mode 100644 agents/salesagent/tools/create-media-buy.md create mode 100644 agents/salesagent/tools/get-adcp-capabilities.md create mode 100644 agents/salesagent/tools/get-media-buy-delivery.md create mode 100644 agents/salesagent/tools/get-media-buys.md create mode 100644 agents/salesagent/tools/get-products.md create mode 100644 agents/salesagent/tools/list-authorized-properties.md create mode 100644 agents/salesagent/tools/list-creative-formats.md create mode 100644 agents/salesagent/tools/list-creatives.md create mode 100644 agents/salesagent/tools/log-event.md create mode 100644 agents/salesagent/tools/provide-performance-feedback.md create mode 100644 agents/salesagent/tools/sync-catalogs.md create mode 100644 agents/salesagent/tools/sync-creatives.md create mode 100644 agents/salesagent/tools/sync-event-sources.md create mode 100644 agents/salesagent/tools/tool-reference.md create mode 100644 agents/salesagent/tools/update-media-buy.md create mode 100644 agents/salesagent/tools/update-performance-index.md create mode 100644 agents/salesagent/tools/workflow-tools.md create mode 100644 agents/salesagent/tutorials/campaign-lifecycle.md create mode 100644 agents/salesagent/tutorials/catalog-setup.md create mode 100644 agents/salesagent/tutorials/mock-testing.md diff --git a/_data/dropdown_v2.yml b/_data/dropdown_v2.yml index 8fb1b03039..57d0bd4990 100644 --- a/_data/dropdown_v2.yml +++ b/_data/dropdown_v2.yml @@ -194,10 +194,19 @@ sectionName: Product title: Multi-Format link: /formats/multiformat.html - needsDivider: 0 + needsDivider: 1 isHeader: 0 isSubSectionStart: 0 + - subsectionId: 4 + sectionId: 1 + sectionName: Product + title: Prebid Sales Agent + link: /agents/salesagent/overview.html + needsDivider: 0 + isHeader: 0 + isSubSectionStart: 1 + #-------------_Support Section--------------- - sectionId: 2 diff --git a/_data/sidebar.yml b/_data/sidebar.yml index 4b5d6df225..1f48137bbd 100644 --- a/_data/sidebar.yml +++ b/_data/sidebar.yml @@ -2055,6 +2055,8 @@ subgroup: 1000 sbCollapseId: prebid-agents +#---- General ----| + - sbSecId: 10 title: General link: @@ -2065,8 +2067,485 @@ subgroup: 0 - sbSecId: 10 - title: Salesagent - link: /agents/salesagent.html + title: Overview + link: /agents/salesagent/overview.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: Architecture & Protocols + link: /agents/salesagent/architecture.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: "Protocols: MCP vs A2A" + link: /agents/salesagent/protocols.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: Glossary + link: /agents/salesagent/glossary.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +#---- Getting Started ----| + +- sbSecId: 10 + title: Getting Started + link: + isHeader: 1 + headerId: agents-getting-started + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: Quick Start + link: /agents/salesagent/getting-started/quickstart.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: Publisher Onboarding + link: /agents/salesagent/getting-started/publisher-onboarding.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: Buy-Side Integration + link: /agents/salesagent/getting-started/buy-side-integration.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: Configuration Reference + link: /agents/salesagent/getting-started/configuration.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +#---- Tools & Schemas ----| + +- sbSecId: 10 + title: Tools & Schemas + link: + isHeader: 1 + headerId: agents-tools + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: Tool Reference + link: /agents/salesagent/tools/tool-reference.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: get_adcp_capabilities + link: /agents/salesagent/tools/get-adcp-capabilities.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: get_products + link: /agents/salesagent/tools/get-products.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: list_creative_formats + link: /agents/salesagent/tools/list-creative-formats.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: create_media_buy + link: /agents/salesagent/tools/create-media-buy.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: update_media_buy + link: /agents/salesagent/tools/update-media-buy.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: get_media_buys + link: /agents/salesagent/tools/get-media-buys.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: sync_creatives + link: /agents/salesagent/tools/sync-creatives.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: list_creatives + link: /agents/salesagent/tools/list-creatives.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: get_media_buy_delivery + link: /agents/salesagent/tools/get-media-buy-delivery.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: list_authorized_properties + link: /agents/salesagent/tools/list-authorized-properties.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: update_performance_index + link: /agents/salesagent/tools/update-performance-index.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: Workflow Tools + link: /agents/salesagent/tools/workflow-tools.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: sync_catalogs (Planned) + link: /agents/salesagent/tools/sync-catalogs.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: provide_performance_feedback (Planned) + link: /agents/salesagent/tools/provide-performance-feedback.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: sync_event_sources (Planned) + link: /agents/salesagent/tools/sync-event-sources.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: log_event (Planned) + link: /agents/salesagent/tools/log-event.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: Content Standards (Planned) + link: /agents/salesagent/tools/content-standards.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: API Schema Reference + link: /agents/salesagent/schemas/api-schemas.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: Database Models + link: /agents/salesagent/schemas/database-models.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: AdCP Protocol Types + link: /agents/salesagent/schemas/adcp-types.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +#---- Deployment & Operations ----| + +- sbSecId: 10 + title: Deployment & Operations + link: + isHeader: 1 + headerId: agents-deployment + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: Deployment Overview + link: /agents/salesagent/deployment/deployment-overview.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: Single-Tenant Deployment + link: /agents/salesagent/deployment/single-tenant.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: Multi-Tenant Deployment + link: /agents/salesagent/deployment/multi-tenant.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: Deploy to Fly.io + link: /agents/salesagent/deployment/fly-io.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: Deploy to GCP + link: /agents/salesagent/deployment/gcp.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: Admin UI Guide + link: /agents/salesagent/operations/admin-ui.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: Security Model + link: /agents/salesagent/operations/security.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: Monitoring & Audit Logging + link: /agents/salesagent/operations/monitoring.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +#---- Integrations ----| + +- sbSecId: 10 + title: Integrations + link: + isHeader: 1 + headerId: agents-integrations + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: GAM Adapter + link: /agents/salesagent/integrations/gam.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: Custom Adapter + link: /agents/salesagent/integrations/custom-adapter.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: Mock Adapter + link: /agents/salesagent/integrations/mock-adapter.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: AdCP Ecosystem + link: /agents/salesagent/integrations/adcp-ecosystem.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +#---- Tutorials ----| + +- sbSecId: 10 + title: Tutorials + link: + isHeader: 1 + headerId: agents-tutorials + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: Campaign Lifecycle + link: /agents/salesagent/tutorials/campaign-lifecycle.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: Catalog Setup + link: /agents/salesagent/tutorials/catalog-setup.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: Testing with Mock Adapter + link: /agents/salesagent/tutorials/mock-testing.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +#---- Developers ----| + +- sbSecId: 10 + title: Developers + link: + isHeader: 1 + headerId: agents-developers + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: Dev Environment Setup + link: /agents/salesagent/developers/dev-setup.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: Source Architecture + link: /agents/salesagent/developers/source-architecture.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: Testing Guide + link: /agents/salesagent/developers/testing.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: Database Migrations + link: /agents/salesagent/developers/migrations.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: Contributing + link: /agents/salesagent/developers/contributing.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +#---- Reference ----| + +- sbSecId: 10 + title: Reference + link: + isHeader: 1 + headerId: agents-reference + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: Error Codes + link: /agents/salesagent/reference/error-codes.html + isHeader: 0 + isSectionHeader: 0 + sectionTitle: + subgroup: 0 + +- sbSecId: 10 + title: AdCP Cross-References + link: /agents/salesagent/reference/adcp-cross-references.html isHeader: 0 isSectionHeader: 0 sectionTitle: diff --git a/_layouts/home.html b/_layouts/home.html index 4300227a8e..3d8c1524fa 100644 --- a/_layouts/home.html +++ b/_layouts/home.html @@ -59,7 +59,7 @@

Prebid Documentation

Prebid Sales Agent Icon

Prebid Sales Agent

Expose inventory to AI agents with AdCP-ready workflows.

- Setup Prebid Sales Agent + View Docs diff --git a/agents/salesagent.md b/agents/salesagent.md index 4d8d3646f9..f10cc33326 100644 --- a/agents/salesagent.md +++ b/agents/salesagent.md @@ -1,116 +1,9 @@ --- layout: page_v2 -title: Salesagent +title: Prebid Sales Agent description: A media sales agent that implements the AdCP Media Buy protocol sidebarType: 10 +redirect_to: /agents/salesagent/overview.html --- -# Prebid Sales Agent - -The Prebid Sales Agent is a server that exposes advertising inventory to AI agents via the Model Context Protocol (MCP) and Agent-to-Agent (A2A) protocol. It is designed to integrate with ad servers like Google Ad Manager and provides tools for managing inventory and campaigns throughout their lifecycle. - - - -## Key Features - -### For AI Agents - -- **Product Discovery**: Natural language search for advertising products. -- **Campaign Creation**: Automated media buying with targeting capabilities. -- **Creative Management**: Streamlined upload and approval workflows. -- **Performance Monitoring**: Real-time access to campaign metrics. - -### For Publishers - -- **Multi-Tenant System**: Isolates data per publisher for security and organization. -- **Adapter Pattern**: Supports multiple ad servers (e.g., Google Ad Manager). -- **Real-time Dashboard**: Live activity feed powered by Server-Sent Events (SSE). -- **Workflow Management**: Unified system for human-in-the-loop approvals. -- **Admin Interface**: Web UI with Google OAuth for easy management. - -### For Developers - -- **MCP Protocol**: Standard interface for AI agents. -- **A2A Protocol**: Agent-to-Agent communication via JSON-RPC 2.0. -- **REST API**: Programmatic tenant management. -- **Docker Deployment**: Easy setup for both local and production environments. - -## Getting Started - -### Quick Start (Evaluation) - -You can try the sales agent locally using Docker: - -```bash -# Clone and start -git clone https://github.com/prebid/salesagent.git -cd salesagent -docker compose up -d - -# Test the MCP interface -uvx adcp http://localhost:8000/mcp/ --auth test-token list_tools -``` - -Access services at [http://localhost:8000](http://localhost:8000): - -- **Admin UI**: `/admin` (Test credentials: `test123`) -- **MCP Server**: `/mcp/` -- **A2A Server**: `/a2a` - -### Production Deployment - -For production, publishers can deploy their own sales agent instance. The repository provides guides for various deployment methods, including Docker and cloud platforms. - -## The AdContext Protocol (AdCP) - -The Sales Agent is built on the **AdContext Protocol (AdCP)**, an open standard designed to standardize how AI agents interact with advertising platforms. - - - -### Protocol Architecture - -AdCP operates as a layer on top of standard AI interaction protocols: - -- **MCP (Model Context Protocol)**: Facilitates direct integration with AI assistants (e.g., Claude Desktop). -- **A2A (Agent-to-Agent Protocol)**: Enables complex, autonomous workflows and collaboration between agents using JSON-RPC 2.0. - -### Core Concepts - -AdCP abstracts complex advertising operations into standardized domains: - -1. **Inventory Discovery** (`get_products`): Agents can search for ad products using natural language criteria (e.g., "video ads in North America") rather than specific line item IDs. -2. **Media Buying** (`create_media_buy`): A normalized workflow for proposal, negotiation, and booking that works consistently across different ad servers. -3. **Creative Management** (`build_creative`): Standardized handling of creative assets, allowing agents to generate or upload assets that match publisher specifications. -4. **Signal Activation** (`get_signals`, `activate_signal`): Mechanisms for passing context and identity signals to improve targeting and campaign performance. - -### Workflow Example - -A typical AI-driven campaign flow using AdCP might look like this: - -1. **Discovery**: Expected outcome is a list of available "Products" matching the agent's intent. -2. **Planning**: The agent uses `create_media_buy` to submit a proposal. -3. **Review**: The Sales Agent (and potentially a human publisher) reviews the proposal. -4. **Execution**: Once approved, the Sales Agent pushes the orders to the underlying ad server (e.g., GAM). - -## Architecture - -The project follows a clean structure isolating core MCP components, business logic services, and ad server adapters. - -```text -salesagent/ -├── src/ -│ ├── core/ # Core MCP server components -│ ├── services/ # Business logic services -│ ├── adapters/ # Ad server integrations (e.g., GAM) -│ └── admin/ # Admin UI (Flask) -├── scripts/ # Utility and deployment scripts -└── tests/ # Comprehensive test suite -``` - -## Contributing - -Contributions are welcome! Please refer to the [Development Guide](https://github.com/prebid/salesagent/blob/main/docs/development/README.md) in the repository for details on setting up your environment and creating pull requests. +This page has moved to the [Prebid Sales Agent Overview](/agents/salesagent/overview.html). diff --git a/agents/salesagent/architecture.md b/agents/salesagent/architecture.md new file mode 100644 index 0000000000..1bb54a4f08 --- /dev/null +++ b/agents/salesagent/architecture.md @@ -0,0 +1,411 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Architecture & Protocols +description: System architecture, protocol design, database schema, and integration patterns for the Prebid Sales Agent +sidebarType: 10 +--- + +# Architecture & Protocols +{: .no_toc} + +The Prebid Sales Agent follows a layered architecture that cleanly separates protocol handling, business logic, and ad server integrations. This page describes the system design, protocol comparison, multi-tenancy model, database schema, adapter pattern, and authentication layers. + +- TOC +{:toc} + +## Four-Layer Architecture + +The system is organized into four distinct layers. Each layer has a single responsibility and communicates only with its immediate neighbors. + +```text +┌──────────────────────────────────────────────────────────────────┐ +│ Layer 1: Admin UI (Flask) │ +│ Web dashboard, SSE activity feed, approval queue │ +└──────────────────────────┬───────────────────────────────────────┘ + │ +┌──────────────────────────┴───────────────────────────────────────┐ +│ Layer 2: MCP / A2A Protocol Servers │ +│ FastMCP (HTTP/SSE) at /mcp/ │ JSON-RPC 2.0 at /a2a │ +│ Tool registration & routing │ Agent card discovery │ +└──────────────────────────┬───────────────────────────────────────┘ + │ +┌──────────────────────────┴───────────────────────────────────────┐ +│ Layer 3: Service Layer │ +│ ProductService │ MediaBuyService │ CreativeService │ +│ PrincipalService │ ReportingService │ AuditService │ +│ ToolContext (tenant isolation) │ +└──────────────┬───────────────────┬───────────────────────────────┘ + │ │ +┌──────────────┴──────┐ ┌────────┴───────────────────────────────┐ +│ Layer 4: Adapters │ │ Layer 4: Database │ +│ GAMAdapter │ │ PostgreSQL + SQLAlchemy ORM │ +│ MockAdapter │ │ Alembic auto-migrations │ +│ (future adapters) │ │ Composite unique constraints │ +└─────────────────────┘ └────────────────────────────────────────┘ +``` + +### Layer Responsibilities + +{: .table .table-bordered .table-striped } +| Layer | Component | Responsibility | +|-------|-----------|----------------| +| **1. Admin UI** | Flask web app | Dashboard, approval queue, SSE activity feed, OAuth login | +| **2. Protocol** | FastMCP server | MCP tool registration, HTTP/SSE transport, token auth | +| **2. Protocol** | A2A server | JSON-RPC 2.0 endpoint, agent card at `/.well-known/agent.json` | +| **3. Services** | Business logic | Validation, orchestration, tenant isolation via ToolContext | +| **4. Adapters** | Ad server plugins | Translate service calls into ad server API operations | +| **4. Database** | PostgreSQL | Persistent storage with SQLAlchemy ORM and Alembic migrations | + +## Data Flow + +Every AI agent request follows the same path through the system, regardless of whether it arrives via MCP or A2A. + +```text +AI Agent (Claude, GPT, custom) + │ + │ HTTP POST with auth token + ▼ +┌─────────────────────────────┐ +│ Nginx Reverse Proxy (:8000)│ +│ Routes /mcp/ or /a2a │ +└──────────┬──────────────────┘ + │ + ▼ +┌─────────────────────────────┐ +│ MCP Server (:8080) │ ┌─────────────────────────────┐ +│ or A2A Server │──▶│ ToolContext │ +│ (protocol parsing) │ │ - tenant_id │ +└──────────┬──────────────────┘ │ - principal_id │ + │ │ - db_session │ + ▼ │ - adapter instance │ +┌─────────────────────────────┐ └─────────────────────────────┘ +│ Service Layer │◀─── uses ToolContext for isolation +│ (business logic) │ +└──────────┬──────────────────┘ + │ + ┌─────┴─────┐ + ▼ ▼ +┌──────────┐ ┌──────────────┐ +│ Database │ │ Ad Server │ +│ (CRUD) │ │ Adapter │ +└──────────┘ │ (GAM, Mock) │ + └──────────────┘ +``` + +### Request Lifecycle + +1. **Authentication** -- The protocol layer validates the incoming token (MCP `x-adcp-auth` header or A2A `Authorization: Bearer` header) and resolves it to a principal and tenant. +2. **ToolContext creation** -- A `ToolContext` object is constructed containing the `tenant_id`, `principal_id`, database session, and the correct ad server adapter for that tenant. +3. **Service dispatch** -- The appropriate service method is called with the ToolContext, ensuring all operations are scoped to the authenticated tenant. +4. **Adapter execution** -- When ad server operations are needed (campaign creation, delivery reporting), the service delegates to the tenant's configured adapter. +5. **Response** -- Results flow back through the protocol layer and are serialized as MCP tool results or A2A JSON-RPC responses. + +## MCP vs A2A Protocol Comparison + +The Sales Agent exposes identical business functionality through both protocols. The choice between them depends on your integration pattern. + +{: .table .table-bordered .table-striped } +| Aspect | MCP (Model Context Protocol) | A2A (Agent-to-Agent Protocol) | +|--------|------------------------------|-------------------------------| +| **Transport** | HTTP with SSE (Server-Sent Events) | HTTP with JSON-RPC 2.0 | +| **Endpoint** | `/mcp/` | `/a2a` | +| **Authentication** | `x-adcp-auth` header token | `Authorization: Bearer` header | +| **Discovery** | Tool listing via `list_tools` | Agent card at `/.well-known/agent.json` | +| **Streaming** | Native SSE streaming | JSON-RPC response batching | +| **Best for** | AI assistants (Claude, GPT) that natively support MCP | Multi-agent orchestration systems | +| **Agent card** | Not applicable | Published at `/.well-known/agent.json` with capabilities | +| **Library** | FastMCP Python SDK | Custom JSON-RPC handler | + +### When to Use MCP + +Use MCP when your AI assistant natively supports the Model Context Protocol. This is the primary interface and provides the most natural integration for single-agent workflows: + +```bash +# Connect Claude Desktop to the Sales Agent +uvx adcp http://localhost:8000/mcp/ --auth your-token list_tools +``` + +### When to Use A2A + +Use A2A when building multi-agent systems where agents need to discover and communicate with each other programmatically. A2A provides structured agent discovery via the agent card: + +```bash +# Discover agent capabilities +curl http://localhost:8000/.well-known/agent.json + +# Send a JSON-RPC request +curl -X POST http://localhost:8000/a2a \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your-token" \ + -d '{"jsonrpc":"2.0","method":"get_products","params":{"brief":"video"},"id":1}' +``` + +{: .alert.alert-info :} +Both protocols expose the same set of tools and return identical data structures. See the [Tool Reference](/agents/salesagent/tools/tool-reference.html) for the complete catalog of available operations. For a deeper comparison of protocol design philosophies, see the [AdCP Protocol Comparison](https://docs.adcontextprotocol.org/docs/building/understanding/protocol-comparison). + +## Multi-Tenancy Model + +The Sales Agent is designed for multi-tenant operation, where each publisher operates in complete isolation from others. + +### Tenant Isolation via ToolContext + +Every incoming request is resolved to a specific tenant. The `ToolContext` object carries this tenant scope through the entire request lifecycle: + +```python +class ToolContext: + tenant_id: int # Resolved from auth token + principal_id: int # The authenticated agent/user + db_session: Session # SQLAlchemy session + adapter: AdServerAdapter # Tenant's configured ad server adapter +``` + +All database queries are automatically scoped by `tenant_id`. A principal (AI agent or human user) in Tenant A can never see or modify data belonging to Tenant B. + +### Composite Identity Model + +Principals (agents and users) are identified by a composite unique constraint of `(tenant_id, email)`. This means the same email address can exist as separate principals in different tenants, each with independent permissions and audit histories. + +```text +┌──────────────────────────────────────────────────────┐ +│ Tenants │ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ Publisher A │ │ Publisher B │ │ Publisher C │ │ +│ │ │ │ │ │ │ │ +│ │ Principals │ │ Principals │ │ Principals │ │ +│ │ Products │ │ Products │ │ Products │ │ +│ │ Media Buys │ │ Media Buys │ │ Media Buys │ │ +│ │ Creatives │ │ Creatives │ │ Creatives │ │ +│ │ Audit Logs │ │ Audit Logs │ │ Audit Logs │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ │ +│ Complete data isolation per tenant │ +└──────────────────────────────────────────────────────┘ +``` + +## Database Architecture + +The Sales Agent uses PostgreSQL as its primary data store, with SQLAlchemy ORM for data access and Alembic for schema migrations. + +### Technology Stack + +{: .table .table-bordered .table-striped } +| Component | Technology | Purpose | +|-----------|-----------|---------| +| **Database** | PostgreSQL | ACID-compliant relational storage | +| **ORM** | SQLAlchemy 2.x | Python object-relational mapping | +| **Migrations** | Alembic | Auto-generated schema migrations | +| **Connection** | Connection pooling | Managed by SQLAlchemy engine | + +### Auto-Migrations + +Alembic is configured in auto-generate mode. When SQLAlchemy models change, Alembic detects the differences and creates migration scripts automatically: + +```bash +# Generate a new migration after model changes +alembic revision --autogenerate -m "add new column" + +# Apply pending migrations +alembic upgrade head +``` + +Migrations run automatically on container startup in Docker deployments. + +### Core Tables + +{: .table .table-bordered .table-striped } +| Table | Purpose | Key Fields | +|-------|---------|------------| +| **tenants** | Publisher accounts | `id`, `name`, `slug`, `ad_server_type`, `config` | +| **principals** | AI agents and human users | `id`, `tenant_id`, `email`, `name`, `token_hash`, `role` | +| **products** | Advertising inventory items | `id`, `tenant_id`, `name`, `description`, `pricing`, `targeting` | +| **media_buys** | Campaign orders from buyers | `id`, `tenant_id`, `principal_id`, `product_id`, `budget`, `status`, `flight_dates` | +| **creatives** | Ad creative assets | `id`, `tenant_id`, `media_buy_id`, `format`, `content`, `approval_status` | +| **audit_logs** | Complete operational history | `id`, `tenant_id`, `principal_id`, `action`, `entity_type`, `entity_id`, `details`, `timestamp` | + +### Entity Relationships + +```text +tenants + │ + ├── principals (many) ── composite unique: (tenant_id, email) + │ │ + │ └── media_buys (many) + │ │ + │ └── creatives (many) + │ + ├── products (many) + │ │ + │ └── media_buys (many) ── references product_id + │ + └── audit_logs (many) ── references principal_id, entity_type, entity_id +``` + +All tables include `tenant_id` as a foreign key to enforce data isolation at the database level. + +## Adapter Pattern + +The Sales Agent uses the adapter pattern to support multiple ad server backends. Each adapter implements the `AdServerAdapter` abstract base class, providing a uniform interface regardless of the underlying ad server. + +### AdServerAdapter Interface + +```python +class AdServerAdapter(ABC): + """Abstract base class for ad server integrations.""" + + @abstractmethod + async def create_order(self, media_buy: MediaBuy) -> str: + """Create an order in the ad server. Returns external order ID.""" + + @abstractmethod + async def create_line_item(self, media_buy: MediaBuy, order_id: str) -> str: + """Create a line item under an order. Returns external line item ID.""" + + @abstractmethod + async def upload_creative(self, creative: Creative) -> str: + """Upload a creative asset. Returns external creative ID.""" + + @abstractmethod + async def get_delivery_report(self, order_id: str) -> DeliveryReport: + """Fetch delivery metrics for an order.""" + + @abstractmethod + async def sync_creative(self, creative: Creative) -> str: + """Sync creative status from the ad server.""" +``` + +### Available Adapters + +{: .table .table-bordered .table-striped } +| Adapter | Class | Purpose | +|---------|-------|---------| +| **Google Ad Manager** | `GAMAdapter` | Production integration with Google Ad Manager API | +| **Mock** | `MockAdapter` | Development and testing with simulated responses | + +### Adding a New Adapter + +To support a new ad server, create a class that implements `AdServerAdapter`: + +1. Create a new file in the adapters directory (e.g., `xandr_adapter.py`). +2. Implement all abstract methods from `AdServerAdapter`. +3. Register the adapter type in the tenant configuration. +4. Tenants select their adapter via the `ad_server_type` field. + +{: .alert.alert-info :} +For detailed source code organization and module-level documentation, see [Source Architecture](/agents/salesagent/developers/source-architecture.html). + +## Port Allocation + +The Docker deployment exposes services on the following ports: + +{: .table .table-bordered .table-striped } +| Port | Service | Description | +|------|---------|-------------| +| **8000** | Nginx reverse proxy | Public entry point; routes to internal services | +| **8080** | MCP Server (FastMCP) | Primary AI agent interface via HTTP/SSE | +| **8001** | Admin UI (Flask) | Web dashboard and approval workflows | +| **8091** | A2A Server (alternate) | Direct A2A access when not using the proxy | + +In production, all traffic enters through the Nginx proxy on port 8000, which routes requests based on path: + +```text +http://localhost:8000 + │ + ├── /mcp/* ──▶ MCP Server (:8080) + ├── /a2a ──▶ A2A Server (:8080) + ├── /admin/* ──▶ Admin UI (:8001) + ├── /health ──▶ Health endpoint + └── /.well-known/agent.json ──▶ A2A agent card +``` + +{: .alert.alert-info :} +For local development without Docker, services can be started individually on their native ports. See the [Quick Start](/agents/salesagent/getting-started/quickstart.html) guide for details. + +## Authentication Layers + +The Sales Agent implements multiple authentication mechanisms depending on the access context. + +### Authentication Methods + +{: .table .table-bordered .table-striped } +| Layer | Method | Scope | Use Case | +|-------|--------|-------|----------| +| **Super Admin** | `SUPER_ADMIN_TOKEN` env var | System-wide | Tenant provisioning, system configuration | +| **OAuth** | Google OAuth 2.0 | Admin UI | Human publishers accessing the dashboard | +| **Tenant Admin** | Token-based | Per-tenant | Publisher admin operations via API | +| **Principal Token** | `x-adcp-auth` or `Bearer` header | Per-tenant | AI agent authentication for MCP/A2A | + +### Token Resolution Flow + +```text +Incoming Request + │ + ├── Has x-adcp-auth header? + │ └── Yes ──▶ Look up principal by token hash + │ └── Resolve tenant_id, principal_id + │ + ├── Has Authorization: Bearer header? + │ └── Yes ──▶ Check if super admin token + │ ├── Yes ──▶ Super admin context + │ └── No ──▶ Look up principal by token hash + │ └── Resolve tenant_id, principal_id + │ + └── Has OAuth session cookie? + └── Yes ──▶ Resolve from session + └── Admin UI context +``` + +Principal tokens are stored as hashed values in the database. The raw token is only shown once at creation time. Each principal can have multiple tokens for rotation without downtime. + +{: .alert.alert-info :} +For complete security documentation, including credential management, network isolation, and production hardening, see [Security & Operations](/agents/salesagent/operations/security.html). + +## AI Integration + +The Sales Agent integrates Google Gemini for intelligent product discovery, enabling AI agents to search advertising inventory using natural language. + +### Product Discovery with Gemini + +When an AI agent calls `get_products` with a natural language query, the service uses Gemini to match the query against the tenant's product catalog: + +```text +AI Agent: "Find video advertising products for sports content" + │ + ▼ +┌─────────────────────────────┐ +│ ProductService.get_products│ +│ │ +│ 1. Load tenant's products │ +│ 2. Send to Gemini with │ +│ query context │ +│ 3. Gemini ranks and │ +│ filters matches │ +│ 4. Return scored results │ +└─────────────────────────────┘ + │ + ▼ +Matched products with relevance scores +``` + +### Configuration + +Gemini integration requires a Google AI API key set in the environment: + +```yaml +# docker-compose.yml excerpt +environment: + - GEMINI_API_KEY=your-api-key +``` + +When no API key is configured, product discovery falls back to keyword-based matching against product names and descriptions. + +{: .alert.alert-info :} +Product discovery is one of several tools available to AI agents. See the [Tool Reference](/agents/salesagent/tools/tool-reference.html) for the complete list of operations including media buy management, creative handling, and reporting. + +## Further Reading + +- [Tool Reference](/agents/salesagent/tools/tool-reference.html) -- Complete catalog of MCP/A2A tools +- [Security & Operations](/agents/salesagent/operations/security.html) -- Authentication, credential management, and production hardening +- [Source Architecture](/agents/salesagent/developers/source-architecture.html) -- Codebase organization and module documentation +- [AdCP Protocol Comparison](https://docs.adcontextprotocol.org/docs/building/understanding/protocol-comparison) -- MCP vs A2A design philosophy +- [AdCP Media Buy Protocol](https://docs.adcontextprotocol.org/docs/media-buy) -- Standard media buy lifecycle specification diff --git a/agents/salesagent/deployment/deployment-overview.md b/agents/salesagent/deployment/deployment-overview.md new file mode 100644 index 0000000000..d736fb3dcb --- /dev/null +++ b/agents/salesagent/deployment/deployment-overview.md @@ -0,0 +1,106 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Deployment - Deployment Overview +description: Comparison of deployment options for the Prebid Sales Agent including Docker, Fly.io, and Google Cloud Run +sidebarType: 10 +--- + +# Deployment Overview +{: .no_toc} + +This page helps you choose the right deployment model and platform for the Prebid Sales Agent. Whether you are a single publisher evaluating the system or a platform operator serving multiple publishers, there is a deployment path designed for your use case. + +- TOC +{:toc} + +## Deployment Options + +The Sales Agent supports multiple deployment platforms. Choose based on your infrastructure requirements, operational expertise, and time constraints. + +{: .table .table-bordered .table-striped } +| Platform | Setup Time | Difficulty | Database | Best For | +|----------|-----------|------------|----------|----------| +| **Docker** (local/on-prem) | ~2 min | Easy | Bundled PostgreSQL via Compose | Evaluation, small publishers, on-premises requirements | +| **Fly.io** (cloud) | 10-15 min | Medium | Separate DB setup needed (Fly Postgres or external) | Cloud hosting with edge deployment support | +| **Google Cloud Run** | 15-20 min | Medium | Cloud SQL or external PostgreSQL | GCP-native environments, auto-scaling, production workloads | + +{: .alert.alert-info :} +**Recommendation:** Start with the Docker Compose deployment for evaluation and testing. It bundles PostgreSQL and requires no cloud accounts or external database configuration. + +## Single-Tenant vs Multi-Tenant + +The Sales Agent supports two deployment models depending on how many publishers share a single instance. + +### Single-Tenant + +A single-tenant deployment runs one publisher per Sales Agent instance. This is the recommended model for most publishers. + +- Simpler configuration -- only one tenant to manage +- Isolated resources -- no shared database or compute with other publishers +- Straightforward admin UI -- no tenant switching or subdomain routing +- Easier to reason about security boundaries + +### Multi-Tenant + +A multi-tenant deployment hosts multiple publishers on a single Sales Agent instance. This model is designed for platform operators and managed service providers. + +- Multiple publishers share one deployment with isolated data per tenant +- Subdomain routing maps requests to the correct tenant (e.g., `publisher-a.yourdomain.com`) +- Super admin controls cross-tenant administration via environment variable whitelist +- Self-signup enables new tenants to provision themselves at `/signup` +- Composite identity model ensures strict data isolation between tenants + +{: .table .table-bordered .table-striped } +| Aspect | Single-Tenant | Multi-Tenant | +|--------|--------------|--------------| +| Publishers per instance | 1 | Many | +| Configuration complexity | Low | Medium | +| Subdomain routing | Not required | Required | +| Super admin role | Not required | Required | +| Self-signup | Not applicable | Optional | +| Recommended for | Individual publishers | Platform operators, managed services | + +## Infrastructure Requirements + +All deployment options share the following infrastructure requirements: + +{: .table .table-bordered .table-striped } +| Component | Requirement | Notes | +|-----------|------------|-------| +| **PostgreSQL** | 16+ | Primary data store; bundled in Docker Compose or provisioned externally | +| **Docker** | 20.10+ | Container runtime for all deployment methods | +| **Nginx** (optional) | Latest stable | Reverse proxy for custom domains, SSL termination, rate limiting | +| **ENCRYPTION_KEY** | Fernet key | Required for encrypting API keys and sensitive adapter configuration | + +{: .alert.alert-warning :} +PostgreSQL 16 or later is required. Earlier versions are not tested and may not support all features used by the Sales Agent. + +## Deployment Guides + +Detailed step-by-step instructions are available for each deployment model: + +- [Single-Tenant Deployment](/agents/salesagent/deployment/single-tenant.html) -- Docker Compose setup with bundled PostgreSQL, recommended for most publishers + +{: .alert.alert-info :} +Fly.io and Google Cloud Run deployment guides are coming in Phase 2. For now, both platforms follow standard container deployment patterns using the `ghcr.io/prebid/salesagent:latest` image with an externally provisioned PostgreSQL database. + +## Post-Deployment Steps + +After your Sales Agent is running, complete the following steps to make it operational: + +1. **Configure your ad server** -- Set up the adapter for your ad server (Google Ad Manager or Mock for testing) via the Admin UI at `/admin` under Settings +2. **Set up products** -- Define the advertising products you want to sell, including pricing, targeting, and format configuration +3. **Add advertisers** -- Create principals (advertiser accounts) and generate API tokens so buying agents can authenticate +4. **Configure SSO** -- Replace the test password with OAuth authentication (Google, Microsoft, Okta, Auth0, or Keycloak) for the Admin UI +5. **Set up a custom domain** -- Configure your public-facing domain and SSL/TLS certificates + +{: .alert.alert-info :} +For a guided walkthrough of these post-deployment steps, see the [Admin UI Guide](/agents/salesagent/operations/admin-ui.html) and [Publisher Onboarding](/agents/salesagent/getting-started/publisher-onboarding.html). + +## Further Reading + +- [Single-Tenant Deployment](/agents/salesagent/deployment/single-tenant.html) -- Step-by-step Docker Compose deployment +- [Quick Start](/agents/salesagent/getting-started/quickstart.html) -- Get running in 2 minutes +- [Security Model](/agents/salesagent/operations/security.html) -- Authentication, encryption, and access control +- [Admin UI Guide](/agents/salesagent/operations/admin-ui.html) -- Managing products, advertisers, and settings +- [AdCP Deployment Best Practices](https://docs.adcontextprotocol.org/docs/deployment) -- Protocol-level deployment guidance diff --git a/agents/salesagent/deployment/fly-io.md b/agents/salesagent/deployment/fly-io.md new file mode 100644 index 0000000000..fdbfa226c1 --- /dev/null +++ b/agents/salesagent/deployment/fly-io.md @@ -0,0 +1,327 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Deployment - Deploy to Fly.io +description: Step-by-step guide for deploying the Prebid Sales Agent to Fly.io +sidebarType: 10 +--- + +# Deploy to Fly.io +{: .no_toc} + +This guide walks you through deploying the Prebid Sales Agent to Fly.io, a platform that runs Docker containers on servers close to your users. Fly.io is a good option for teams that want a managed platform without the complexity of Kubernetes. + +- TOC +{:toc} + +## Prerequisites + +{: .table .table-bordered .table-striped } +| Requirement | Notes | +|-------------|-------| +| Fly.io account | [Sign up at fly.io](https://fly.io) | +| `flyctl` CLI | [Install instructions](https://fly.io/docs/hands-on/install-flyctl/) | +| Authenticated CLI | Run `fly auth login` | + +## Step 1: Create the Fly App + +Create a new Fly app in your preferred region: + +```bash +fly apps create sales-agent --org personal +``` + +{: .alert.alert-info :} +Replace `personal` with your Fly.io organization name if applicable. Run `fly regions list` to see available regions. + +## Step 2: Provision a Database + +The Sales Agent requires PostgreSQL. Fly.io offers two options: + +### Option A: Fly Managed Postgres (Recommended) + +Fly's fully managed Postgres service includes automatic backups, failover, and monitoring: + +```bash +fly postgres create --name sales-agent-db --region ord --vm-size shared-cpu-1x --volume-size 10 +``` + +Attach the database to your app: + +```bash +fly postgres attach sales-agent-db --app sales-agent +``` + +This automatically sets the `DATABASE_URL` secret on your app. + +{: .table .table-bordered .table-striped } +| Resource | Estimated Cost | +|----------|---------------| +| Managed Postgres (shared-cpu-1x, 10GB) | ~$38/month | +| Automatic daily backups | Included | + +### Option B: Fly Postgres (Self-Managed) + +For lower cost, use Fly's self-managed Postgres option. This runs Postgres as a regular Fly app without managed backups or failover: + +```bash +fly postgres create --name sales-agent-db --region ord --vm-size shared-cpu-1x --volume-size 1 --initial-cluster-size 1 +``` + +Attach to your app: + +```bash +fly postgres attach sales-agent-db --app sales-agent +``` + +{: .table .table-bordered .table-striped } +| Resource | Estimated Cost | +|----------|---------------| +| Self-managed Postgres (shared-cpu-1x, 1GB) | ~$5-7/month | +| Backups | Manual (your responsibility) | + +{: .alert.alert-warning :} +With self-managed Postgres, you are responsible for backups. Set up `pg_dump` on a schedule or use Fly's volume snapshots. + +## Step 3: Set Secrets + +Configure the required secrets for your deployment: + +```bash +# Generate and set the encryption key +fly secrets set ENCRYPTION_KEY="$(openssl rand -base64 32)" --app sales-agent + +# Enable test mode for initial setup (remove later) +fly secrets set ADCP_AUTH_TEST_MODE="true" --app sales-agent +``` + +{: .alert.alert-danger :} +Record the `ENCRYPTION_KEY` value securely before proceeding. If lost, all encrypted data (API keys, adapter credentials) becomes unrecoverable. Run `fly secrets list --app sales-agent` to verify the secret is set, but note that Fly does not display secret values after creation. + +## Step 4: Deploy + +### From the Docker Image (Recommended) + +Deploy using the published container image: + +```bash +fly deploy --image docker.io/prebid/salesagent:latest --app sales-agent +``` + +### From Source + +If you need to customize the build, clone the repository and deploy from source: + +```bash +git clone https://github.com/prebid/salesagent.git +cd salesagent +fly deploy --app sales-agent +``` + +The repository includes a `Dockerfile` that Fly uses automatically. + +## Step 5: Initial Setup + +Once the deployment completes, open the Admin UI: + +```bash +fly open /admin --app sales-agent +``` + +Or navigate to `https://sales-agent.fly.dev/admin` in your browser. + +### Setup Mode + +On first launch with no tenants configured, the Sales Agent enters Setup Mode. This provides a guided workflow: + +1. Navigate to the Admin UI +2. Log in with the test password `test123` (since `ADCP_AUTH_TEST_MODE=true`) +3. Complete the setup wizard to create your first tenant +4. Configure your publisher name, domain, and ad server adapter + +### Test Credentials + +When `ADCP_AUTH_TEST_MODE=true`, the following credentials are available: + +{: .table .table-bordered .table-striped } +| Credential | Value | Purpose | +|------------|-------|---------| +| Admin UI password | `test123` | Access the admin dashboard | +| MCP auth token | `test-token` | Authenticate MCP tool calls | +| A2A bearer token | `test-token` | Authenticate A2A requests | + +{: .alert.alert-warning :} +Disable test mode before production use. Run `fly secrets unset ADCP_AUTH_TEST_MODE --app sales-agent` and configure SSO instead. + +## Step 6: Configure SSO + +For production authentication, configure an SSO provider: + +1. In your identity provider (Google, Microsoft, Okta, Auth0, or Keycloak), create an OAuth application +2. Set the redirect URI to: + +```text +https://sales-agent.fly.dev/auth/oidc/callback +``` + +3. In the Admin UI, go to **Settings > SSO Configuration** +4. Enter the Client ID and Client Secret +5. Save and test the login flow +6. Disable test mode: + +```bash +fly secrets unset ADCP_AUTH_TEST_MODE --app sales-agent +``` + +## Database Migrations + +Database migrations run automatically on each application startup. No manual intervention is needed for routine deployments. + +To run migrations manually (e.g., for troubleshooting), open an SSH console: + +```bash +fly ssh console --app sales-agent +``` + +Inside the console: + +```bash +python -m alembic upgrade head +``` + +## Monitoring + +### Viewing Logs + +```bash +# Stream live logs +fly logs --app sales-agent + +# View recent logs +fly logs --app sales-agent --no-tail +``` + +### Checking Status + +```bash +# View app status and running instances +fly status --app sales-agent + +# Check the health endpoint +curl https://sales-agent.fly.dev/health +``` + +### SSH Access + +```bash +# Open an interactive shell +fly ssh console --app sales-agent + +# Run a one-off command +fly ssh console --app sales-agent -C "python -m scripts.diagnostics" +``` + +## Scaling + +### Horizontal Scaling (Instances) + +Add more instances to handle increased traffic: + +```bash +# Scale to 2 instances +fly scale count 2 --app sales-agent +``` + +### Vertical Scaling (CPU and Memory) + +Increase resources per instance: + +```bash +# Scale to 1 dedicated CPU and 1GB memory +fly scale vm shared-cpu-2x --memory 1024 --app sales-agent +``` + +{: .table .table-bordered .table-striped } +| VM Size | CPU | Memory | Estimated Cost | +|---------|-----|--------|---------------| +| `shared-cpu-1x` | Shared 1x | 256MB | ~$5/month | +| `shared-cpu-2x` | Shared 2x | 512MB | ~$10/month | +| `performance-1x` | Dedicated 1x | 2GB | ~$30/month | + +## Custom Domain + +To serve the Sales Agent on your own domain: + +```bash +# Add a certificate for your domain +fly certs create ads.yourpublisher.com --app sales-agent +``` + +Then create a CNAME record in your DNS provider: + +```text +ads.yourpublisher.com. IN CNAME sales-agent.fly.dev. +``` + +Fly.io automatically provisions and renews the TLS certificate. + +## Cost Estimate + +{: .table .table-bordered .table-striped } +| Component | Managed Postgres | Self-Managed Postgres | +|-----------|------------------|-----------------------| +| App (shared-cpu-1x) | ~$5/month | ~$5/month | +| Postgres | ~$38/month | ~$5-7/month | +| Bandwidth (typical) | ~$2-5/month | ~$2-5/month | +| **Total** | **~$45-50/month** | **~$12-17/month** | + +## Troubleshooting + +### App Fails to Start + +```bash +fly logs --app sales-agent +``` + +{: .table .table-bordered .table-striped } +| Symptom | Cause | Fix | +|---------|-------|-----| +| `ENCRYPTION_KEY not set` | Missing secret | Run `fly secrets set ENCRYPTION_KEY="$(openssl rand -base64 32)" --app sales-agent` | +| `connection refused` on database | Postgres not attached | Run `fly postgres attach sales-agent-db --app sales-agent` | +| OOM killed | Insufficient memory | Scale up: `fly scale vm shared-cpu-1x --memory 512 --app sales-agent` | +| Health check failures | App still starting | Wait 30-60 seconds; check logs for migration progress | + +### Database Connection Issues + +If the app cannot reach the database: + +```bash +# Verify the database is running +fly status --app sales-agent-db + +# Check the DATABASE_URL secret is set +fly secrets list --app sales-agent +``` + +If the database was recreated, you may need to re-attach it: + +```bash +fly postgres attach sales-agent-db --app sales-agent +``` + +### Deployment Stuck + +If a deployment hangs during health checks: + +```bash +# Check what's happening +fly logs --app sales-agent + +# Cancel and retry +fly deploy --image docker.io/prebid/salesagent:latest --app sales-agent --strategy immediate +``` + +## Next Steps + +- [Deployment Overview](/agents/salesagent/deployment/deployment-overview.html) -- Compare all deployment options +- [Configuration Reference](/agents/salesagent/deployment/configuration-reference.html) -- Full list of environment variables +- [Admin UI Guide](/agents/salesagent/operations/admin-ui.html) -- Configure products, advertisers, and settings diff --git a/agents/salesagent/deployment/gcp.md b/agents/salesagent/deployment/gcp.md new file mode 100644 index 0000000000..cb04c1998c --- /dev/null +++ b/agents/salesagent/deployment/gcp.md @@ -0,0 +1,380 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Deployment - Deploy to Google Cloud Run +description: Step-by-step guide for deploying the Prebid Sales Agent to Google Cloud Run with Cloud SQL +sidebarType: 10 +--- + +# Deploy to Google Cloud Run +{: .no_toc} + +This guide walks you through deploying the Prebid Sales Agent to Google Cloud Run backed by Cloud SQL for PostgreSQL. Cloud Run is a serverless container platform that scales automatically and charges per use. + +- TOC +{:toc} + +## Prerequisites + +{: .table .table-bordered .table-striped } +| Requirement | Notes | +|-------------|-------| +| Google Cloud project | With billing enabled | +| `gcloud` CLI | [Install instructions](https://cloud.google.com/sdk/docs/install) | +| Authenticated CLI | Run `gcloud auth login` | + +## Step 1: Enable Required APIs + +Enable the APIs needed for Cloud Run and Cloud SQL: + +```bash +gcloud services enable sqladmin.googleapis.com +gcloud services enable sql-component.googleapis.com +gcloud services enable run.googleapis.com +``` + +## Step 2: Create a Cloud SQL Instance + +Create a PostgreSQL instance. The `db-f1-micro` tier is sufficient for most deployments: + +```bash +gcloud sql instances create sales-agent-db \ + --database-version=POSTGRES_16 \ + --tier=db-f1-micro \ + --region=us-central1 \ + --storage-size=10GB \ + --storage-auto-increase +``` + +Create the database and user: + +```bash +# Create the database +gcloud sql databases create salesagent --instance=sales-agent-db + +# Set a password for the postgres user +gcloud sql users set-password postgres \ + --instance=sales-agent-db \ + --password=YOUR_SECURE_PASSWORD +``` + +{: .alert.alert-danger :} +Replace `YOUR_SECURE_PASSWORD` with a strong password. If the password contains special characters (`@`, `#`, `%`, etc.), you must URL-encode them in the `DATABASE_URL` later. For example, `p@ss` becomes `p%40ss`. + +Note the instance connection name for later: + +```bash +gcloud sql instances describe sales-agent-db --format="value(connectionName)" +``` + +This returns a value like `your-project:us-central1:sales-agent-db`. + +## Step 3: Deploy to Cloud Run + +Deploy the Sales Agent container with the required configuration: + +```bash +gcloud run deploy sales-agent \ + --image=docker.io/prebid/salesagent:latest \ + --platform=managed \ + --region=us-central1 \ + --allow-unauthenticated \ + --add-cloudsql-instances=YOUR_PROJECT:us-central1:sales-agent-db \ + --memory=1Gi \ + --cpu=1 \ + --min-instances=1 \ + --no-cpu-throttling \ + --set-env-vars="DATABASE_URL=postgresql://postgres:YOUR_SECURE_PASSWORD@/salesagent?host=/cloudsql/YOUR_PROJECT:us-central1:sales-agent-db" \ + --set-env-vars="ENCRYPTION_KEY=$(openssl rand -base64 32)" \ + --set-env-vars="ADCP_AUTH_TEST_MODE=true" +``` + +{: .alert.alert-danger :} +**Two flags are mandatory for the Sales Agent to function on Cloud Run:** +- `--no-cpu-throttling` -- The Sales Agent runs background services (MCP server, A2A server, scheduled tasks). Without this flag, Cloud Run throttles the CPU between requests, causing these services to stall. +- `--min-instances=1` -- Keeps at least one instance warm at all times. Without this, cold starts cause MCP clients and A2A integrations to time out. + +Omitting either flag results in intermittent 502 errors and unresponsive background services. + +{: .alert.alert-warning :} +Record the `ENCRYPTION_KEY` value before deploying. Run `echo $(openssl rand -base64 32)` first, save the output, then use it in the deploy command. If lost, all encrypted data becomes unrecoverable. + +### DATABASE_URL Format + +Cloud Run connects to Cloud SQL through a Unix domain socket. The `DATABASE_URL` format for this connection is: + +```text +postgresql://USER:PASSWORD@/DATABASE?host=/cloudsql/PROJECT:REGION:INSTANCE +``` + +{: .table .table-bordered .table-striped } +| Component | Value | Example | +|-----------|-------|---------| +| `USER` | Database username | `postgres` | +| `PASSWORD` | URL-encoded password | `p%40ssword` | +| `DATABASE` | Database name | `salesagent` | +| `PROJECT` | GCP project ID | `my-project-123` | +| `REGION` | Cloud SQL region | `us-central1` | +| `INSTANCE` | Cloud SQL instance name | `sales-agent-db` | + +### Password URL Encoding + +If your database password contains special characters, URL-encode them: + +{: .table .table-bordered .table-striped } +| Character | Encoded | +|-----------|---------| +| `@` | `%40` | +| `#` | `%23` | +| `%` | `%25` | +| `/` | `%2F` | +| `:` | `%3A` | +| `?` | `%3F` | +| `=` | `%3D` | + +For example, if your password is `my@pass#123`, the `DATABASE_URL` would use `my%40pass%23123`. + +## Step 4: Verify the Deployment + +After the deploy command completes, it prints the service URL. Verify the deployment: + +```bash +# Get the service URL +SERVICE_URL=$(gcloud run services describe sales-agent \ + --region=us-central1 \ + --format="value(status.url)") + +# Check the health endpoint +curl $SERVICE_URL/health +``` + +Expected response: + +```json +{"status": "ok"} +``` + +{: .table .table-bordered .table-striped } +| Service | URL | Expected Result | +|---------|-----|-----------------| +| Admin UI | `$SERVICE_URL/admin` | Login page or setup wizard | +| MCP Server | `$SERVICE_URL/mcp/` | MCP endpoint (requires auth) | +| A2A Server | `$SERVICE_URL/a2a` | A2A endpoint (requires auth) | +| Health Check | `$SERVICE_URL/health` | `{"status": "ok"}` | +| Agent Card | `$SERVICE_URL/.well-known/agent.json` | JSON agent descriptor | + +## Step 5: Initial Setup + +### Setup Mode + +On first launch with no tenants configured, the Sales Agent enters Setup Mode: + +1. Navigate to `$SERVICE_URL/admin` +2. Log in with the test password `test123` (since `ADCP_AUTH_TEST_MODE=true`) +3. Complete the setup wizard to create your first tenant +4. Configure your publisher name, domain, and ad server adapter + +### Test Credentials + +When `ADCP_AUTH_TEST_MODE=true`, the following credentials are available: + +{: .table .table-bordered .table-striped } +| Credential | Value | Purpose | +|------------|-------|---------| +| Admin UI password | `test123` | Access the admin dashboard | +| MCP auth token | `test-token` | Authenticate MCP tool calls | +| A2A bearer token | `test-token` | Authenticate A2A requests | + +{: .alert.alert-warning :} +Disable test mode before production use. Update the environment variable: `gcloud run services update sales-agent --region=us-central1 --remove-env-vars=ADCP_AUTH_TEST_MODE` + +## Step 6: Configure SSO + +For production authentication, configure an SSO provider: + +1. In your identity provider (Google, Microsoft, Okta, Auth0, or Keycloak), create an OAuth application +2. Set the redirect URI to: + +```text +https://sales-agent-HASH-uc.a.run.app/auth/oidc/callback +``` + +Replace the URL with your actual Cloud Run service URL. + +3. In the Admin UI, go to **Settings > SSO Configuration** +4. Enter the Client ID and Client Secret +5. Save and test the login flow +6. Disable test mode: + +```bash +gcloud run services update sales-agent \ + --region=us-central1 \ + --remove-env-vars=ADCP_AUTH_TEST_MODE +``` + +## Database Migrations + +Database migrations run automatically on each application startup. No manual intervention is needed for routine deployments. + +To run migrations manually, use Cloud Run Jobs or exec into a running instance: + +```bash +gcloud run services exec sales-agent \ + --region=us-central1 \ + -- python -m alembic upgrade head +``` + +## Custom Domain + +To serve the Sales Agent on your own domain: + +```bash +gcloud beta run domain-mappings create \ + --service=sales-agent \ + --domain=ads.yourpublisher.com \ + --region=us-central1 +``` + +The command outputs DNS records to create. Add the specified records in your DNS provider, then wait for the mapping to become active: + +```bash +gcloud beta run domain-mappings describe \ + --domain=ads.yourpublisher.com \ + --region=us-central1 +``` + +Google Cloud automatically provisions and renews the TLS certificate for mapped domains. + +{: .alert.alert-info :} +Custom domain mapping can take up to 15 minutes to provision the SSL certificate. During this time, HTTPS requests may return certificate errors. + +## Monitoring + +### Viewing Logs + +```bash +# Stream live logs +gcloud run services logs tail sales-agent --region=us-central1 + +# View recent logs in the console +gcloud run services logs read sales-agent --region=us-central1 --limit=100 +``` + +Logs are also available in the [Cloud Run console](https://console.cloud.google.com/run) and Cloud Logging. + +### Checking Status + +```bash +# View service details +gcloud run services describe sales-agent --region=us-central1 + +# Check revision status +gcloud run revisions list --service=sales-agent --region=us-central1 +``` + +## Scaling + +Cloud Run scales automatically based on request concurrency. The key configuration options: + +{: .table .table-bordered .table-striped } +| Parameter | Default | Recommended | Description | +|-----------|---------|-------------|-------------| +| `--min-instances` | 0 | 1 | Minimum warm instances (required for background services) | +| `--max-instances` | 100 | 5-10 | Maximum instances | +| `--memory` | 512Mi | 1Gi | Memory per instance | +| `--cpu` | 1 | 1 | CPUs per instance | +| `--concurrency` | 80 | 80 | Max concurrent requests per instance | + +Update scaling parameters: + +```bash +gcloud run services update sales-agent \ + --region=us-central1 \ + --min-instances=1 \ + --max-instances=5 \ + --memory=1Gi +``` + +## Cost Estimate + +{: .table .table-bordered .table-striped } +| Component | Estimated Monthly Cost | +|-----------|----------------------| +| Cloud SQL (db-f1-micro, 10GB) | ~$10/month | +| Cloud Run (1 vCPU, 1Gi, always-allocated) | ~$30-35/month | +| Egress (typical) | ~$1-5/month | +| **Total** | **~$40-50/month** | + +{: .alert.alert-info :} +Cloud Run charges are higher when using `--no-cpu-throttling` and `--min-instances=1` because the CPU is always allocated rather than billed per-request. This is required for the Sales Agent's background services to function. + +## Troubleshooting + +### 502 Bad Gateway Errors + +{: .table .table-bordered .table-striped } +| Cause | Fix | +|-------|-----| +| Missing `--no-cpu-throttling` | Redeploy with `--no-cpu-throttling` flag | +| Missing `--min-instances=1` | Update with `gcloud run services update sales-agent --region=us-central1 --min-instances=1` | +| Container crashing | Check logs: `gcloud run services logs read sales-agent --region=us-central1 --limit=50` | + +{: .alert.alert-danger :} +The most common cause of 502 errors on Cloud Run is the missing `--no-cpu-throttling` flag. Without it, Cloud Run throttles CPU between requests, causing the Sales Agent's background services (MCP, A2A, scheduled tasks) to stall and time out. + +### Database Connection Errors + +```bash +# Verify the Cloud SQL instance is running +gcloud sql instances describe sales-agent-db --format="value(state)" + +# Check the Cloud SQL connection is attached +gcloud run services describe sales-agent \ + --region=us-central1 \ + --format="value(spec.template.metadata.annotations['run.googleapis.com/cloudsql-instances'])" +``` + +Common database connection issues: + +{: .table .table-bordered .table-striped } +| Symptom | Cause | Fix | +|---------|-------|-----| +| `could not connect to server: No such file or directory` | Cloud SQL instance not attached | Add `--add-cloudsql-instances` to the deploy command | +| `password authentication failed` | Incorrect password in `DATABASE_URL` | Check password URL encoding; update env var | +| `database "salesagent" does not exist` | Database not created | Run `gcloud sql databases create salesagent --instance=sales-agent-db` | + +### Password Encoding Issues + +If your database password contains special characters and the app cannot connect: + +1. Verify the password works directly: + +```bash +gcloud sql connect sales-agent-db --user=postgres +``` + +2. URL-encode all special characters in the `DATABASE_URL` +3. Update the environment variable: + +```bash +gcloud run services update sales-agent \ + --region=us-central1 \ + --set-env-vars="DATABASE_URL=postgresql://postgres:ENCODED_PASSWORD@/salesagent?host=/cloudsql/YOUR_PROJECT:us-central1:sales-agent-db" +``` + +### Insufficient Memory + +If the container is OOM-killed: + +```bash +gcloud run services update sales-agent \ + --region=us-central1 \ + --memory=2Gi +``` + +The Sales Agent requires a minimum of 1Gi memory. If running with large datasets or concurrent users, 2Gi is recommended. + +## Next Steps + +- [Deployment Overview](/agents/salesagent/deployment/deployment-overview.html) -- Compare all deployment options +- [Configuration Reference](/agents/salesagent/deployment/configuration-reference.html) -- Full list of environment variables +- [Admin UI Guide](/agents/salesagent/operations/admin-ui.html) -- Configure products, advertisers, and settings diff --git a/agents/salesagent/deployment/multi-tenant.md b/agents/salesagent/deployment/multi-tenant.md new file mode 100644 index 0000000000..4a9c41f852 --- /dev/null +++ b/agents/salesagent/deployment/multi-tenant.md @@ -0,0 +1,280 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Deployment - Multi-Tenant Deployment +description: Deploying the Prebid Sales Agent for multiple publishers on a single instance +sidebarType: 10 +--- + +# Multi-Tenant Deployment +{: .no_toc} + +This guide covers deploying a single Prebid Sales Agent instance that serves multiple publishers, each isolated in their own tenant with separate data, credentials, and configuration. + +- TOC +{:toc} + +## When to Use Multi-Tenant + +Multi-tenant mode is designed for organizations that manage ad operations on behalf of multiple publishers. Typical use cases include: + +- **Platform operators** running a shared Sales Agent for all publishers on their platform +- **Managed service providers** offering Sales Agent as a hosted service +- **Publisher networks** centralizing ad operations tooling across properties + +In multi-tenant mode, each publisher gets an isolated tenant with its own subdomain, data, ad server credentials, and user access. A single deployment handles all tenants, reducing infrastructure overhead. + +If you are deploying for a single publisher, see the [Single-Tenant Deployment](/agents/salesagent/deployment/single-tenant.html) guide instead. + +## Architecture + +Multi-tenant mode adds a tenant resolution layer on top of the standard Sales Agent architecture. Incoming requests are routed to the correct tenant based on the subdomain in the request: + +```text +┌──────────────────────────────────────────────────────────────┐ +│ DNS / Load Balancer │ +│ *.sales-agent.yourdomain.com → Sales Agent │ +└──────────────────────────┬───────────────────────────────────┘ + │ +┌──────────────────────────┴───────────────────────────────────┐ +│ Tenant Resolution Middleware │ +│ Host header → subdomain → tenant context │ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ Tenant A │ │ Tenant B │ │ Tenant C │ │ +│ │ pub-a.sales │ │ pub-b.sales │ │ pub-c.sales │ │ +│ │ -agent.co │ │ -agent.co │ │ -agent.co │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +└──────────────────────────┬───────────────────────────────────┘ + │ +┌──────────────────────────┴───────────────────────────────────┐ +│ PostgreSQL (shared) │ +│ All tenants share one database instance │ +│ Data isolation enforced at application layer │ +└──────────────────────────────────────────────────────────────┘ +``` + +## Enable Multi-Tenant Mode + +Set the following environment variable to enable multi-tenant mode: + +```bash +ADCP_MULTI_TENANT=true +``` + +When this flag is set, the Sales Agent expects every incoming request to resolve to a specific tenant. Requests that cannot be mapped to a tenant are rejected. + +## Domain Configuration + +Multi-tenant mode requires several domain-related environment variables: + +{: .table .table-bordered .table-striped } +| Variable | Required | Example | Description | +|----------|----------|---------|-------------| +| `BASE_DOMAIN` | Yes | `yourdomain.com` | The root domain of your deployment | +| `SALES_AGENT_DOMAIN` | Yes | `sales-agent.yourdomain.com` | Base domain for tenant subdomains | +| `ADMIN_DOMAIN` | No | `admin.yourdomain.com` | Dedicated domain for the platform admin UI | +| `SUPER_ADMIN_DOMAIN` | No | `super.yourdomain.com` | Dedicated domain for super-admin operations | + +With the configuration above, individual tenants are accessible at: + +```text +https://publisher-a.sales-agent.yourdomain.com +https://publisher-b.sales-agent.yourdomain.com +``` + +## Wildcard DNS Setup + +Multi-tenant mode requires a wildcard DNS record so that all tenant subdomains resolve to your Sales Agent instance. + +Create a wildcard A or CNAME record: + +```text +*.sales-agent.yourdomain.com. IN A 203.0.113.10 +``` + +Or, if using a CNAME: + +```text +*.sales-agent.yourdomain.com. IN CNAME your-lb.example.com. +``` + +{: .alert.alert-info :} +If you are using a cloud provider's load balancer, the CNAME approach is typically preferred since the underlying IP address may change. + +### Wildcard SSL Certificate + +You also need a wildcard TLS certificate for `*.sales-agent.yourdomain.com`. Options include: + +- **Let's Encrypt** with DNS-01 challenge (required for wildcards) +- **Cloud-managed certificates** (AWS ACM, GCP-managed SSL, Cloudflare) +- **Purchased wildcard certificate** from a certificate authority + +## Subdomain Routing + +The Sales Agent resolves the tenant for each incoming request using the following priority order: + +{: .table .table-bordered .table-striped } +| Priority | Method | Header / Source | Example | +|----------|--------|-----------------|---------| +| 1 | Explicit override | `x-adcp-tenant` request header | `x-adcp-tenant: publisher-a` | +| 2 | Platform proxy header | `Apx-Incoming-Host` header | `Apx-Incoming-Host: publisher-a.sales-agent.yourdomain.com` | +| 3 | Host header (default) | `Host` header | `Host: publisher-a.sales-agent.yourdomain.com` | + +In most deployments, the Host header is sufficient. The `Apx-Incoming-Host` header is useful when a CDN or reverse proxy rewrites the Host header. The `x-adcp-tenant` header allows programmatic tenant selection from API clients. + +### Session Cookie Scoping + +Session cookies are scoped to the `SALES_AGENT_DOMAIN` value. This means: + +- Cookies are set with `Domain=.sales-agent.yourdomain.com` +- A user session on `publisher-a.sales-agent.yourdomain.com` does not carry over to `publisher-b.sales-agent.yourdomain.com` +- Each tenant has fully isolated sessions + +## Creating Tenants + +### Via the Admin UI + +1. Navigate to the admin or super-admin domain +2. Go to **Tenants > Create Tenant** +3. Fill in the publisher name, slug (used as the subdomain), and initial configuration +4. Save the tenant + +The tenant is immediately accessible at `https://.sales-agent.yourdomain.com`. + +### Via the CLI + +Use the tenant setup script for scripted or bulk tenant creation: + +```bash +python -m scripts.setup.setup_tenant \ + --name "Publisher A" \ + --slug "publisher-a" \ + --domain "publisher-a.sales-agent.yourdomain.com" +``` + +{: .alert.alert-info :} +The `--slug` value becomes the subdomain prefix. It must be unique, lowercase, and contain only alphanumeric characters and hyphens. + +## Per-Tenant GAM Service Account Setup + +Each tenant that integrates with Google Ad Manager (GAM) needs its own service account credentials. This keeps credentials isolated and allows different publishers to connect to different GAM networks. + +To configure a tenant's GAM credentials: + +1. In the Google Cloud Console, create a service account for the publisher +2. Download the JSON key file +3. In the Admin UI, navigate to the tenant's **Settings > Ad Server > GAM** +4. Upload the service account JSON key +5. Enter the GAM network code + +The credentials are encrypted at rest using the deployment's `ENCRYPTION_KEY`. + +{: .alert.alert-warning :} +Each service account must be granted access to its respective GAM network by the publisher. Share the service account email address with the publisher and have them add it in the GAM Admin console under **Global Settings > Network > API Access**. + +## MCP Client Configuration + +MCP clients connecting to a multi-tenant deployment must route requests to the correct tenant subdomain. The MCP server URL includes the tenant subdomain: + +```json +{ + "mcpServers": { + "salesagent": { + "url": "https://publisher-a.sales-agent.yourdomain.com/mcp/", + "headers": { + "x-adcp-auth": "YOUR_MCP_TOKEN" + } + } + } +} +``` + +Each tenant has its own MCP authentication token, configured in the Admin UI under **Settings > API Access**. + +To connect to a different tenant, change the subdomain in the URL: + +```json +{ + "mcpServers": { + "salesagent": { + "url": "https://publisher-b.sales-agent.yourdomain.com/mcp/", + "headers": { + "x-adcp-auth": "PUBLISHER_B_MCP_TOKEN" + } + } + } +} +``` + +## Self-Signup + +When self-signup is enabled, new publishers can register their own tenant at the `/signup` endpoint: + +```text +https://sales-agent.yourdomain.com/signup +``` + +Self-signup creates a new tenant with a subdomain based on the publisher's chosen slug. The registering user becomes the tenant admin. + +{: .alert.alert-warning :} +Self-signup is disabled by default. Enable it only if your deployment model supports open registration. Review the [Security Model](/agents/salesagent/operations/security.html) documentation before enabling. + +## Troubleshooting + +### Tenant Context Errors + +**Symptom:** Requests return `400 Bad Request` with a message about missing tenant context. + +**Cause:** The Sales Agent could not determine which tenant the request belongs to. + +Check the following: + +1. Verify `ADCP_MULTI_TENANT=true` is set +2. Confirm the subdomain in the URL matches an existing tenant slug +3. If behind a reverse proxy, ensure the Host header is being forwarded: + +```bash +curl -v https://publisher-a.sales-agent.yourdomain.com/health +``` + +Look for the `Host` header in the request. If the proxy is stripping it, configure the proxy to pass `Apx-Incoming-Host` or `x-adcp-tenant` instead. + +### Custom Domain Issues + +**Symptom:** A tenant's custom domain does not resolve or returns a different tenant. + +Check: + +1. DNS for the custom domain points to the Sales Agent instance +2. The custom domain is registered in the tenant's settings in the Admin UI +3. The TLS certificate covers the custom domain (wildcard certificates only cover one level of subdomains) + +### Cookie and Session Problems + +**Symptom:** Users are logged out when navigating between pages, or sessions are shared across tenants. + +Verify: + +1. `SALES_AGENT_DOMAIN` is set correctly +2. The domain value matches the actual domain tenants are served from +3. Cookies are being set with the correct `Domain` attribute (check browser developer tools) + +### Tenant Not Found After Creation + +**Symptom:** A newly created tenant returns 404. + +The tenant slug must match the subdomain exactly. Verify: + +```bash +# Check if the tenant exists +curl https://admin.yourdomain.com/api/tenants +``` + +Ensure the slug in the database matches the subdomain being requested. + +## Next Steps + +- [Single-Tenant Deployment](/agents/salesagent/deployment/single-tenant.html) -- Simpler deployment for a single publisher +- [Deployment Overview](/agents/salesagent/deployment/deployment-overview.html) -- Compare all deployment options +- [Security Model](/agents/salesagent/operations/security.html) -- Authentication, encryption, and access control +- [Admin UI Guide](/agents/salesagent/operations/admin-ui.html) -- Configure tenants, products, and settings diff --git a/agents/salesagent/deployment/single-tenant.md b/agents/salesagent/deployment/single-tenant.md new file mode 100644 index 0000000000..6da37bd275 --- /dev/null +++ b/agents/salesagent/deployment/single-tenant.md @@ -0,0 +1,350 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Deployment - Single-Tenant Deployment +description: Step-by-step guide for deploying the Prebid Sales Agent as a single-tenant Docker Compose stack +sidebarType: 10 +--- + +# Single-Tenant Deployment +{: .no_toc} + +This guide walks you through deploying the Prebid Sales Agent for a single publisher using Docker Compose. The stack bundles PostgreSQL, the Sales Agent application, and an optional Nginx reverse proxy -- everything you need to run in production. + +- TOC +{:toc} + +## Architecture + +The Docker Compose stack includes four services that work together: + +```text +┌──────────────────────────────────────────────────────────┐ +│ Nginx (reverse proxy) │ +│ Port 80/443 → upstream :8000 │ +└──────────────────────────┬───────────────────────────────┘ + │ +┌──────────────────────────┴───────────────────────────────┐ +│ adcp-server (Sales Agent) │ +│ Port 8000 │ +│ ┌──────────┐ ┌──────────┐ ┌────────────────────┐ │ +│ │ Admin UI │ │MCP Server│ │ A2A Server │ │ +│ │ (Flask) │ │(FastMCP) │ │ (JSON-RPC) │ │ +│ └──────────┘ └──────────┘ └────────────────────┘ │ +└──────────────────────────┬───────────────────────────────┘ + │ +┌──────────────────────────┴───────────────────────────────┐ +│ PostgreSQL 16 │ +│ Port 5432 │ +│ Volume: postgres_data │ +└──────────────────────────────────────────────────────────┘ +``` + +{: .table .table-bordered .table-striped } +| Service | Role | Port | Notes | +|---------|------|------|-------| +| `postgres` | Database | 5432 (internal) | PostgreSQL 16, persistent volume | +| `adcp-server` | Sales Agent application | 8000 | MCP, A2A, Admin UI, health check | +| `nginx` | Reverse proxy | 80, 443 | SSL termination, custom domain routing | + +## Prerequisites + +{: .table .table-bordered .table-striped } +| Requirement | Minimum Version | Notes | +|-------------|----------------|-------| +| Docker | 20.10+ | [Install Docker](https://docs.docker.com/get-docker/) | +| Docker Compose | 2.0+ | Included with Docker Desktop | +| Git | 2.0+ | To clone the repository | + +{: .alert.alert-info :} +No Python, Node.js, or other runtime is needed. Everything runs inside Docker containers. + +## Step 1: Clone and Configure + +Clone the repository and create your environment configuration: + +```bash +git clone https://github.com/prebid/salesagent.git +cd salesagent +cp .env.template .env +``` + +### Essential Environment Variables + +Edit the `.env` file with your configuration. The following variables are the most important: + +{: .table .table-bordered .table-striped } +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `DATABASE_URL` | Auto-configured | `postgresql://salesagent:salesagent@postgres:5432/salesagent` | Set automatically by Docker Compose; override only if using an external database | +| `ENCRYPTION_KEY` | Yes | None | Fernet symmetric key for encrypting API keys and adapter credentials | +| `ADCP_AUTH_TEST_MODE` | No | `false` | Set to `true` for evaluation only; enables test credentials | +| `CREATE_DEMO_TENANT` | No | `false` | Creates a demo tenant with sample data on first boot | + +Generate an encryption key: + +```bash +python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" +``` + +Or if you do not have Python installed locally: + +```bash +docker run --rm python:3.12-slim python -c " +from cryptography.fernet import Fernet +print(Fernet.generate_key().decode()) +" +``` + +{: .alert.alert-danger :} +Store your `ENCRYPTION_KEY` securely. If lost, all encrypted data (API keys, adapter credentials) becomes unrecoverable. Back it up outside of the deployment directory. + +## Step 2: Start the Stack + +Launch all services in the background: + +```bash +docker compose up -d +``` + +Docker Compose will: +1. Pull the latest `ghcr.io/prebid/salesagent` image +2. Start PostgreSQL and wait for it to become healthy +3. Run database migrations automatically +4. Start the Sales Agent application + +First startup may take 30-60 seconds while the database initializes and migrations run. + +## Step 3: Verify Services + +Confirm that all services are running and healthy: + +```bash +# Check container status +docker compose ps + +# Verify health endpoint +curl http://localhost:8000/health +``` + +Expected health response: + +```json +{"status": "ok"} +``` + +Verify all endpoints are accessible: + +{: .table .table-bordered .table-striped } +| Service | URL | Expected Result | +|---------|-----|-----------------| +| Admin UI | `http://localhost:8000/admin` | Login page or setup wizard | +| MCP Server | `http://localhost:8000/mcp/` | MCP endpoint (requires auth) | +| A2A Server | `http://localhost:8000/a2a` | A2A endpoint (requires auth) | +| Health Check | `http://localhost:8000/health` | `{"status": "ok"}` | +| Agent Card | `http://localhost:8000/.well-known/agent.json` | JSON agent descriptor | + +## Step 4: First-Time Setup + +### Setup Mode + +On first launch with no tenants configured, the Sales Agent enters **Setup Mode**. This provides a guided workflow to create your first publisher tenant. + +1. Navigate to `http://localhost:8000/admin` +2. If `ADCP_AUTH_TEST_MODE=true`, log in with password `test123` +3. The setup wizard walks you through creating your first tenant +4. Configure your publisher name, domain, and ad server adapter + +### Test Credentials + +When `ADCP_AUTH_TEST_MODE=true`, the following credentials are available for evaluation: + +{: .table .table-bordered .table-striped } +| Credential | Value | Purpose | +|------------|-------|---------| +| Admin UI password | `test123` | Access the admin dashboard | +| MCP auth token | `test-token` | Authenticate MCP tool calls | +| A2A bearer token | `test-token` | Authenticate A2A requests | + +{: .alert.alert-warning :} +Test credentials must be disabled before going to production. Remove `ADCP_AUTH_TEST_MODE` from your `.env` file and configure SSO authentication instead. + +### Configure SSO + +For production use, replace test credentials with OAuth: + +1. Go to `/admin` and navigate to **Settings > SSO Configuration** +2. Choose your identity provider (Google, Microsoft, Okta, Auth0, or Keycloak) +3. Enter the Client ID and Client Secret from your IdP +4. Set the callback URL to `https://yourdomain.com/admin/auth/callback` +5. Save and test the login flow + +See the [Admin UI Guide](/agents/salesagent/operations/admin-ui.html) for detailed SSO configuration instructions. + +## Step 5: Custom Domain Configuration + +To serve the Sales Agent on your own domain (e.g., `ads.yourpublisher.com`): + +1. Point your domain's DNS to the server running the Sales Agent (A record or CNAME) +2. Update the Nginx configuration in `nginx.conf` with your domain: + +```nginx +server { + listen 80; + server_name ads.yourpublisher.com; + + location / { + proxy_pass http://adcp-server:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +3. Update the tenant's custom domain in the Admin UI under **Settings > Custom Domain** + +## Step 6: SSL/TLS Setup + +{: .alert.alert-danger :} +HTTPS is mandatory for production deployments. The Sales Agent enforces HTTPS for all non-localhost connections. + +### Using Let's Encrypt with Certbot + +The recommended approach for SSL is Let's Encrypt with automatic certificate renewal: + +```bash +# Install certbot (example for Ubuntu) +sudo apt install certbot python3-certbot-nginx + +# Obtain and configure certificate +sudo certbot --nginx -d ads.yourpublisher.com +``` + +Certbot will automatically update your Nginx configuration to handle SSL termination and redirect HTTP to HTTPS. + +### Using a Custom Certificate + +If you have an existing certificate, configure Nginx manually: + +```nginx +server { + listen 443 ssl; + server_name ads.yourpublisher.com; + + ssl_certificate /etc/nginx/ssl/fullchain.pem; + ssl_certificate_key /etc/nginx/ssl/privkey.pem; + + location / { + proxy_pass http://adcp-server:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + } +} +``` + +## Common Operations + +### Viewing Logs + +```bash +# Follow all service logs +docker compose logs -f + +# View only the Sales Agent logs +docker compose logs -f adcp-server + +# View only PostgreSQL logs +docker compose logs -f postgres + +# View last 100 lines +docker compose logs --tail=100 adcp-server +``` + +### Stopping and Starting + +```bash +# Stop all services (data preserved) +docker compose down + +# Start all services +docker compose up -d + +# Restart only the Sales Agent (e.g., after config change) +docker compose restart adcp-server +``` + +### Rebuilding + +When updating to a new version: + +```bash +# Pull latest images +docker compose pull + +# Restart with new images +docker compose up -d +``` + +Migrations run automatically on startup, so database schema updates are applied when the new image boots. + +### Resetting the Database + +{: .alert.alert-danger :} +This destroys all data including tenants, products, advertisers, campaigns, and audit logs. Use only for fresh starts during evaluation. + +```bash +# Stop services and remove data volumes +docker compose down -v + +# Start fresh +docker compose up -d +``` + +## Troubleshooting + +### Container Fails to Start + +```bash +docker compose logs adcp-server +``` + +Common issues: + +{: .table .table-bordered .table-striped } +| Symptom | Cause | Fix | +|---------|-------|-----| +| `connection refused` on database | PostgreSQL not ready | Wait 30 seconds and retry; check `docker compose ps` | +| `ENCRYPTION_KEY not set` | Missing environment variable | Add `ENCRYPTION_KEY` to `.env` file | +| Port 8000 already in use | Another service on that port | Stop the conflicting service or change the port in `docker-compose.yml` | +| Migration errors | Database schema conflict | Reset with `docker compose down -v && docker compose up -d` | + +### MCP Returns 401 Unauthorized + +Ensure you are passing the auth token in the correct header format: + +```bash +# MCP uses x-adcp-auth header +curl -H "x-adcp-auth: test-token" http://localhost:8000/mcp/ + +# A2A uses Authorization: Bearer header +curl -H "Authorization: Bearer test-token" http://localhost:8000/a2a +``` + +### Health Check Returns Unhealthy + +The `/health` endpoint checks database connectivity. Verify PostgreSQL is running: + +```bash +docker compose ps postgres +docker compose logs postgres +``` + +## Next Steps + +- [Admin UI Guide](/agents/salesagent/operations/admin-ui.html) -- Configure products, advertisers, and settings +- [Publisher Onboarding](/agents/salesagent/getting-started/publisher-onboarding.html) -- End-to-end publisher setup +- [Security Model](/agents/salesagent/operations/security.html) -- Authentication, encryption, and access control +- [Deployment Overview](/agents/salesagent/deployment/deployment-overview.html) -- Compare all deployment options diff --git a/agents/salesagent/developers/contributing.md b/agents/salesagent/developers/contributing.md new file mode 100644 index 0000000000..484f534c1d --- /dev/null +++ b/agents/salesagent/developers/contributing.md @@ -0,0 +1,220 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Developers - Contributing +description: How to contribute to the Prebid Sales Agent project +sidebarType: 10 +--- + +# Contributing +{: .no_toc} + +This guide covers the contribution workflow, commit conventions, CI pipeline, and release process for the Prebid Sales Agent. + +- TOC +{:toc} + +## Getting Started + +1. Fork the [salesagent repository](https://github.com/prebid/salesagent) on GitHub. +2. Set up your [development environment](/agents/salesagent/developers/dev-setup.html). +3. Create a feature branch from `main`. +4. Make your changes and add tests. +5. Run the [test suite](/agents/salesagent/developers/testing.html). +6. Open a pull request against `main`. + +{: .alert.alert-warning :} +All contributions are subject to the [IPR Policy](https://github.com/prebid/salesagent/blob/main/IPR_POLICY.md). Read it before submitting your first pull request. The `ipr-agreement.yml` GitHub Action checks that contributors have accepted the policy. + +## Branching Strategy + +Create feature branches from `main` using a descriptive name: + +```bash +git checkout main +git pull origin main +git checkout -b feat/add-frequency-capping +``` + +Branch name conventions: + +{: .table .table-bordered .table-striped } +| Prefix | Use Case | +|--------|----------| +| `feat/` | New features | +| `fix/` | Bug fixes | +| `docs/` | Documentation changes | +| `refactor/` | Code refactoring | +| `chore/` | Maintenance and tooling | + +## Conventional Commits + +Pull request titles must follow the [Conventional Commits](https://www.conventionalcommits.org/) format. This is enforced by the `pr-title-check.yml` GitHub Action -- pull requests with non-conforming titles will fail CI. + +### Format + +``` +type(scope): description +``` + +### Types + +{: .table .table-bordered .table-striped } +| Type | Description | Example | +|------|-------------|---------| +| `feat` | New feature | `feat(catalog): add frequency capping to products` | +| `fix` | Bug fix | `fix(media-buy): correct total spend calculation` | +| `docs` | Documentation | `docs(api): update tool parameter descriptions` | +| `refactor` | Code refactoring | `refactor(adapters): extract common validation logic` | +| `perf` | Performance improvement | `perf(search): add index for product name lookups` | +| `chore` | Maintenance | `chore(deps): update sqlalchemy to 2.0.30` | +| `test` | Test additions or changes | `test(unit): add coverage for pricing edge cases` | +| `ci` | CI/CD changes | `ci: add PostgreSQL 17 to test matrix` | + +### Scope + +The scope is optional but recommended. Use the module or area being changed (e.g., `catalog`, `media-buy`, `adapters`, `auth`, `admin`). + +### Breaking Changes + +Append `!` after the type/scope for breaking changes: + +``` +feat(api)!: rename get_products to search_products +``` + +## Code Style + +### Python Version + +The project requires Python 3.12 or later. Use features available in that version, including type parameter syntax and `match` statements where appropriate. + +### Dependency Management + +Use `uv` for all dependency management: + +```bash +# Add a runtime dependency +uv add package-name + +# Add a development dependency +uv add --dev package-name + +# Sync dependencies from pyproject.toml +uv sync +``` + +### Pre-commit Hooks + +Install pre-commit hooks before your first commit: + +```bash +./scripts/setup_hooks.sh +``` + +The hooks run automatically on each commit and check for: + +- Code formatting (ruff) +- Import sorting (ruff) +- Type errors (mypy) +- Route conflicts between MCP tools +- AdCP contract compliance + +See [Development Environment Setup](/agents/salesagent/developers/dev-setup.html) for the full list of hooks and how to run them manually. + +### Type Hints + +Type hints are required for all public API functions, MCP tool parameters, and return values: + +```python +async def create_product( + name: str, + delivery_type: DeliveryType, + cpm: Decimal | None = None, +) -> Product: + ... +``` + +## Pull Request Guidelines + +### Before Submitting + +1. Run the full unit test suite: `pytest tests/unit/ -v` +2. Run pre-commit hooks on all files: `uv run pre-commit run --all-files` +3. Verify your changes work with integration tests if they touch database logic: `pytest tests/integration/ -v` +4. Update documentation if your change affects tool behavior, configuration, or APIs. + +### PR Description + +Include in your pull request description: + +- **What** the change does +- **Why** the change is needed +- **How** to test it +- Any **migration** steps required + +### Review Process + +- At least one approving review is required before merging. +- CI must pass (tests, linting, type checking, IPR check). +- PRs are squash-merged to keep the commit history clean. + +## CI/CD Pipeline + +The project uses GitHub Actions for continuous integration and release automation. + +{: .table .table-bordered .table-striped } +| Workflow | Trigger | Steps | +|----------|---------|-------| +| `test.yml` | Push to `main`, PR to `main` | Security audit (`uv-secure`), smoke tests, full test suite with PostgreSQL | +| `release-please.yml` | Push to `main` | Automated versioning, CHANGELOG generation, release creation | +| `pr-title-check.yml` | PR opened/edited | Validates conventional commit format in PR title | +| `ipr-agreement.yml` | PR opened | Checks that the contributor has accepted the IPR policy | + +### Test Workflow Details + +The `test.yml` workflow runs in this order: + +1. **Security audit** -- `uv-secure` scans dependencies for known vulnerabilities. +2. **Smoke tests** -- Runs tests marked with `@pytest.mark.smoke` to catch critical regressions early. +3. **Full test suite** -- Runs unit and integration tests against a PostgreSQL service container. + +{: .alert.alert-info :} +If the security audit step fails, check `uv-secure` output for the affected package and version. Update the dependency or add an exception in the security configuration if the vulnerability does not apply. + +## Release Process + +Releases are automated through [release-please](https://github.com/googleapis/release-please). The tool reads conventional commit messages to determine version bumps and generate changelogs. + +### How It Works + +1. Conventional commits are merged into `main`. +2. `release-please` opens (or updates) a release PR that bumps the version and updates `CHANGELOG.md`. +3. When the release PR is merged, a GitHub Release is created with the generated notes. + +### Version Bump Rules + +{: .table .table-bordered .table-striped } +| Commit Type | Version Bump | CHANGELOG Section | +|-------------|-------------|-------------------| +| `feat` | Minor | Features | +| `fix` | Patch | Bug Fixes | +| `perf` | Patch | Performance Improvements | +| `refactor` | -- | Code Refactoring | +| `docs` | -- | Documentation | +| `chore` | -- | Hidden | + +Breaking changes (indicated by `!` in the commit type) trigger a major version bump regardless of the commit type. + +### Configuration + +Release-please is configured through `release-please-config.json` in the repository root. This file controls: + +- Package name and release type +- Changelog sections and ordering +- Version file locations + +## Further Reading + +- [Development Environment Setup](/agents/salesagent/developers/dev-setup.html) -- Prerequisites, setup, and common commands +- [Testing Guide](/agents/salesagent/developers/testing.html) -- Test organization, fixtures, and coverage +- [Source Architecture](/agents/salesagent/developers/source-architecture.html) -- Codebase organization and module documentation diff --git a/agents/salesagent/developers/dev-setup.md b/agents/salesagent/developers/dev-setup.md new file mode 100644 index 0000000000..3ff7f215e3 --- /dev/null +++ b/agents/salesagent/developers/dev-setup.md @@ -0,0 +1,330 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Developers - Development Environment Setup +description: How to set up the Prebid Sales Agent for local development including prerequisites, testing, hooks, and IDE configuration +sidebarType: 10 +--- + +# Development Environment Setup +{: .no_toc} + +This guide walks you through setting up a local development environment for the Prebid Sales Agent. It covers prerequisites, project setup, testing, pre-commit hooks, IDE configuration, and common development commands. + +- TOC +{:toc} + +## Prerequisites + +Before you begin, install the following tools: + +{: .table .table-bordered .table-striped } +| Requirement | Minimum Version | Installation | Notes | +|-------------|----------------|--------------|-------| +| **Python** | 3.12+ | [python.org](https://www.python.org/downloads/) or `brew install python@3.12` | Required for running the application | +| **uv** | Latest | `curl -LsSf https://astral.sh/uv/install.sh \| sh` | Fast Python package manager (replaces pip/venv) | +| **Docker** | 20.10+ | [Install Docker](https://docs.docker.com/get-docker/) | Required for PostgreSQL and E2E tests | +| **Docker Compose** | 2.0+ | Included with Docker Desktop | Orchestrates multi-container environments | +| **PostgreSQL** | 16+ | Via Docker (recommended) | Primary data store; no local install needed | + +{: .alert.alert-info :} +PostgreSQL runs inside Docker, so you do not need to install it locally. The `docker compose` configuration handles database provisioning, migration, and port mapping automatically. + +## Clone and Setup + +### Step 1: Clone the Repository + +```bash +git clone https://github.com/prebid/salesagent.git +cd salesagent +``` + +### Step 2: Configure Environment Variables + +```bash +cp .env.template .env +``` + +Edit `.env` to set any required values. The template includes sensible defaults for local development. Key variables: + +{: .table .table-bordered .table-striped } +| Variable | Default | Description | +|----------|---------|-------------| +| `DATABASE_URL` | `postgresql://salesagent:salesagent@localhost:5432/salesagent` | PostgreSQL connection string | +| `ENCRYPTION_KEY` | (generated) | Fernet key for encrypting sensitive data | +| `ADCP_AUTH_TEST_MODE` | `true` | Enables test credentials for development | +| `CREATE_DEMO_TENANT` | `true` | Creates demo tenant with sample data on startup | +| `ENVIRONMENT` | `development` | Controls logging level and debug features | + +### Step 3: Install Python Dependencies + +```bash +uv sync +``` + +This creates a virtual environment and installs all dependencies from `pyproject.toml`, including dev dependencies. + +### Step 4: Start Infrastructure + +```bash +docker compose build +docker compose up -d +``` + +This starts PostgreSQL and the Sales Agent. Database migrations run automatically on first startup. + +### Step 5: Verify Setup + +```bash +# Check services are running +docker compose ps + +# Test MCP connectivity +uvx adcp http://localhost:8000/mcp/ --auth test-token list_tools +``` + +## Hot Reload + +In development mode, the Sales Agent builds from source and supports hot reload. Changes to Python files are detected and the server restarts automatically. + +To run in development mode with hot reload: + +```bash +# Start only the database container +docker compose up -d db + +# Run the application locally with auto-reload +uv run uvicorn src.core.main:app --reload --host 0.0.0.0 --port 8080 +``` + +{: .alert.alert-info :} +When running locally (outside Docker), ensure your `.env` file has `DATABASE_URL` pointing to `localhost:5432` instead of the Docker internal hostname. + +## Running Tests + +The test suite is organized into three tiers: unit, integration, and end-to-end. + +### Unit Tests + +Unit tests run without external dependencies and are the fastest to execute: + +```bash +uv run pytest tests/unit/ -x +``` + +The `-x` flag stops on the first failure, which is useful during development. + +### Integration Tests + +Integration tests require a running PostgreSQL database: + +```bash +# Ensure PostgreSQL is running +docker compose up -d db + +# Run integration tests +uv run pytest tests/integration/ +``` + +Integration tests validate database operations, adapter behavior, and service-layer logic against real infrastructure. + +### End-to-End (E2E) Tests + +E2E tests run the full Sales Agent stack inside Docker and test the MCP and A2A protocols from the outside: + +```bash +uv run pytest tests/e2e/ +``` + +{: .alert.alert-warning :} +E2E tests build and start Docker containers, so they take longer to run. They are typically run before opening a pull request or as part of CI. + +### Running Specific Tests + +```bash +# Run a specific test file +uv run pytest tests/unit/test_schemas.py -x + +# Run a specific test function +uv run pytest tests/unit/test_schemas.py::test_product_validation -x + +# Run tests matching a keyword +uv run pytest tests/ -k "media_buy" -x + +# Run with verbose output +uv run pytest tests/unit/ -x -v +``` + +### Test Coverage + +```bash +uv run pytest tests/unit/ --cov=src --cov-report=term-missing +``` + +## Pre-commit Hooks + +The project uses pre-commit hooks to catch issues before they reach CI. + +### Setup + +```bash +# Install hooks +./setup_hooks.sh +``` + +This configures Git hooks from `.pre-commit-config.yaml` to run automatically on each commit. + +### What the Hooks Check + +{: .table .table-bordered .table-striped } +| Hook | Description | Auto-fix | +|------|-------------|----------| +| **Route conflict detection** | Ensures no two MCP tools register the same route name | No -- must be resolved manually | +| **AdCP contract compliance** | Validates that tool signatures match the AdCP protocol specification | No -- must be resolved manually | +| **Code formatting** (ruff) | Enforces consistent Python code style | Yes -- auto-formats on commit | +| **Import sorting** (ruff) | Sorts and organizes import statements | Yes -- auto-formats on commit | +| **Type checking** (mypy) | Static type analysis for Python | No -- must be resolved manually | +| **YAML/JSON validation** | Checks configuration files for syntax errors | No -- must be resolved manually | + +### Running Hooks Manually + +```bash +# Run all hooks on staged files +uv run pre-commit run + +# Run all hooks on all files +uv run pre-commit run --all-files + +# Run a specific hook +uv run pre-commit run ruff --all-files +``` + +## IDE Setup + +The project includes configuration for several development environments. + +### Claude Code + +The repository includes a `.claude/` directory with project-level context files that help Claude Code understand the codebase structure, conventions, and architecture patterns. No additional configuration is needed -- open the project in Claude Code and the context is loaded automatically. + +### Cursor / GitHub Copilot + +An `AGENTS.md` file at the project root provides architecture context and coding conventions for Cursor and GitHub Copilot. These tools read the file automatically to improve code suggestions. + +### MCP Configuration + +The project includes `.mcp.json` for MCP-aware tools and editors. This file describes the server configuration and enables direct tool invocation from supported editors. + +### Recommended VS Code Extensions + +{: .table .table-bordered .table-striped } +| Extension | Purpose | +|-----------|---------| +| **Python** (ms-python) | Language support, IntelliSense, debugging | +| **Ruff** (charliermarsh.ruff) | Linting and formatting integration | +| **Mypy** | Type checking integration | +| **Docker** (ms-azuretools.vscode-docker) | Container management | +| **SQLAlchemy** | Model highlighting and autocomplete | + +## Database Access Patterns + +The Sales Agent enforces a consistent pattern for database access. All database operations must use the `get_db_session()` context manager. + +### Required Pattern + +```python +from src.core.database.database_session import get_db_session + +async def my_function(): + async with get_db_session() as session: + # All database operations within this context + result = await session.execute(query) + await session.commit() +``` + +{: .alert.alert-warning :} +Never create database connections manually or use raw connection strings. Always use `get_db_session()` to ensure proper connection pooling, transaction management, and cleanup. + +### Why This Matters + +- **Connection pooling** -- Sessions are drawn from and returned to the SQLAlchemy connection pool automatically +- **Transaction safety** -- Each context manager scope is a transaction boundary with automatic rollback on exceptions +- **Tenant isolation** -- The `ToolContext` passes the session with tenant scoping already applied +- **Testing** -- Tests can inject mock sessions without changing business logic + +### In MCP Tools + +MCP tool functions receive database access through the `ToolContext` dependency: + +```python +@mcp.tool() +async def my_tool( + param: str, + ctx: ToolContext = Depends(get_tool_context), +) -> MyResponse: + # ctx.db is already a scoped session + products = await ctx.db.execute( + select(Product).where(Product.tenant_id == ctx.tenant_id) + ) + ... +``` + +## Common Development Commands + +{: .table .table-bordered .table-striped } +| Command | Description | +|---------|-------------| +| `docker compose up -d` | Start all services (database + Sales Agent) | +| `docker compose down` | Stop all services | +| `docker compose logs -f` | Follow live logs for all services | +| `docker compose logs -f salesagent` | Follow Sales Agent logs only | +| `docker compose restart salesagent` | Restart the Sales Agent container | +| `docker compose down -v` | Stop and destroy all data volumes (full reset) | +| `docker compose build --no-cache` | Rebuild containers from scratch | +| `uv sync` | Install/update Python dependencies | +| `uv run pytest tests/unit/ -x` | Run unit tests, stop on first failure | +| `uv run pytest tests/integration/` | Run integration tests | +| `uv run pytest tests/e2e/` | Run E2E tests (requires Docker) | +| `uv run ruff check src/` | Run linter | +| `uv run ruff format src/` | Auto-format code | +| `uv run mypy src/` | Run type checker | +| `uv run pre-commit run --all-files` | Run all pre-commit hooks | + +### Database Management + +```bash +# Access the PostgreSQL shell +docker compose exec db psql -U salesagent -d salesagent + +# Generate a new Alembic migration +uv run alembic revision --autogenerate -m "description of change" + +# Apply pending migrations +uv run alembic upgrade head + +# Roll back the last migration +uv run alembic downgrade -1 + +# Reset the database (destroy and recreate) +docker compose down -v && docker compose up -d +``` + +### Viewing Logs + +```bash +# All service logs +docker compose logs -f + +# Sales Agent only, last 100 lines +docker compose logs -f --tail 100 salesagent + +# Database logs +docker compose logs -f db +``` + +## Further Reading + +- [Source Architecture](/agents/salesagent/developers/source-architecture.html) -- Codebase organization and module documentation +- [API Schema Reference](/agents/salesagent/schemas/api-schemas.html) -- Pydantic model definitions and validation +- [Quick Start](/agents/salesagent/getting-started/quickstart.html) -- Docker-based quickstart guide +- [Architecture & Protocols](/agents/salesagent/architecture.html) -- System design and data flow +- [Tool Reference](/agents/salesagent/tools/tool-reference.html) -- Complete catalog of MCP tools diff --git a/agents/salesagent/developers/migrations.md b/agents/salesagent/developers/migrations.md new file mode 100644 index 0000000000..e1d2b37791 --- /dev/null +++ b/agents/salesagent/developers/migrations.md @@ -0,0 +1,238 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Developers - Database Migrations +description: Alembic migration configuration, creating migrations, and migration patterns for the Prebid Sales Agent +sidebarType: 10 +--- + +# Database Migrations +{: .no_toc} + +The Sales Agent uses Alembic for database schema migrations with SQLAlchemy ORM models. Migrations run automatically on startup in Docker deployments. + +- TOC +{:toc} + +## Overview + +All database schema changes are managed through Alembic migration scripts. Each migration captures a discrete schema change with both `upgrade()` and `downgrade()` functions, allowing the schema to move forward or roll back to any point in its history. + +The migration system supports: + +- Automatic migration generation from SQLAlchemy model changes +- Sequential numeric revision IDs for clear ordering +- Automatic execution on container startup in Docker deployments +- Manual execution for local development and production deployments + +## Configuration + +Alembic is configured through two files in the project root: + +{: .table .table-bordered .table-striped } +| File | Purpose | +|------|---------| +| `alembic.ini` | Alembic configuration including database URL and script location | +| `alembic/env.py` | Migration environment setup, model imports, and connection handling | + +Migration scripts are stored in `alembic/versions/` and are committed to version control. + +## Migration File Structure + +Each migration file follows a standard pattern: + +```python +"""Description of migration + +Revision ID: 001 +Revises: initial_schema +Create Date: YYYY-MM-DD HH:MM:SS +""" +revision: str = "001" +down_revision: str | None = "initial_schema" + +def upgrade() -> None: + # Apply changes + pass + +def downgrade() -> None: + # Revert changes + pass +``` + +Key fields: + +- **`revision`** -- A numeric string identifier for this migration (e.g., `"001"`, `"002"`). +- **`down_revision`** -- The revision this migration builds on. Set to `None` for the initial migration. +- **`upgrade()`** -- Contains the SQL or ORM operations to apply the schema change. +- **`downgrade()`** -- Contains the operations to revert the schema change. + +## Running Migrations + +{: .table .table-bordered .table-striped } +| Environment | Command | +|-------------|---------| +| Docker Compose | Automatic on startup | +| Docker manual | `docker-compose exec adcp-server python scripts/ops/migrate.py` | +| Fly.io | `fly ssh console -C "cd /app && python scripts/ops/migrate.py"` | +| Local | `alembic upgrade head` | +| Rollback one step | `alembic downgrade -1` | +| Rollback to specific | `alembic downgrade ` | +| Check current revision | `alembic current` | +| View migration history | `alembic history` | + +### Applying Migrations Locally + +```bash +# Apply all pending migrations +uv run alembic upgrade head + +# Apply migrations up to a specific revision +uv run alembic upgrade 005 + +# Check which revision the database is on +uv run alembic current +``` + +### Rolling Back + +```bash +# Roll back the most recent migration +uv run alembic downgrade -1 + +# Roll back to a specific revision +uv run alembic downgrade 003 + +# Roll back all migrations (reset to empty schema) +uv run alembic downgrade base +``` + +{: .alert.alert-warning :} +Rolling back migrations in production requires caution. Data migrations that transform or delete data may not be fully reversible. Always back up the database before rolling back in a production environment. + +## Creating New Migrations + +### Autogenerate from Model Changes + +After modifying SQLAlchemy models, generate a migration automatically: + +```bash +uv run alembic revision --autogenerate -m "add_new_column" +``` + +This compares the current database schema against the SQLAlchemy model definitions and generates the appropriate `upgrade()` and `downgrade()` operations. + +{: .alert.alert-info :} +Always review autogenerated migrations before applying them. Alembic autogenerate does not detect all change types -- for example, it cannot detect changes to column names, changes to constraints on existing columns, or data migrations. + +### Manual Migration + +For changes that autogenerate cannot handle, create an empty migration and write the operations manually: + +```bash +uv run alembic revision -m "migrate_legacy_data" +``` + +Then edit the generated file in `alembic/versions/` to add your operations. + +### Naming Conventions + +- **Revision IDs** -- Sequential numeric strings: `001`, `002`, `003`, and so on. +- **Messages** -- Use snake_case descriptions that summarize the change: `add_policy_compliance_fields`, `create_gam_inventory_tables`. + +## Existing Migrations + +The project includes 18+ migrations covering the full schema evolution. Key categories include: + +{: .table .table-bordered .table-striped } +| Category | Description | +|----------|-------------| +| Schema foundation | Initial tables, format definitions, and core relationships | +| Policy compliance | Fields for regulatory and business policy enforcement | +| Super admin config | Configuration tables for platform-level administration | +| GAM inventory | Google Ad Manager inventory tables and sync tracking | +| Principal/advertiser mapping | Relationship tables between principals and advertiser accounts | +| Context persistence | Workflow state and conversation context storage | +| JSON validation | Partial schema handling and JSON column validation | + +## Best Practices + +### Always Include Both Directions + +Every migration must implement both `upgrade()` and `downgrade()`. If a change is genuinely irreversible, document that in the `downgrade()` function and raise an exception: + +```python +def downgrade() -> None: + raise RuntimeError( + "This migration cannot be reversed. " + "Data was transformed in a lossy operation." + ) +``` + +### Test Against Fresh and Existing Databases + +Run migrations against both a clean database (from `base` to `head`) and a database at the previous revision: + +```bash +# Fresh database +uv run alembic downgrade base +uv run alembic upgrade head + +# Single step from current +uv run alembic upgrade +1 +``` + +### Make Data Migrations Idempotent + +Data migrations (as opposed to schema migrations) should be safe to run multiple times without side effects. Use conditional logic or upserts instead of raw inserts: + +```python +def upgrade() -> None: + conn = op.get_bind() + # Only insert if the row does not already exist + existing = conn.execute( + sa.text("SELECT 1 FROM config WHERE key = 'default_currency'") + ).fetchone() + if not existing: + conn.execute( + sa.text("INSERT INTO config (key, value) VALUES ('default_currency', 'USD')") + ) +``` + +### Review Autogenerated SQL + +Autogenerate is a starting point, not a final product. Review the generated operations for: + +- Correct column types and nullability +- Proper index and constraint definitions +- Foreign key cascading behavior +- Default values and server defaults + +## Troubleshooting + +### Migration Head Conflicts + +If two developers create migrations with the same `down_revision`, Alembic reports a "multiple heads" error: + +```bash +# Check for multiple heads +uv run alembic heads + +# Merge heads into a single migration +uv run alembic merge -m "merge_heads" +``` + +### Database Out of Sync + +If the database schema does not match the migration history (common after manual schema changes), stamp the database to mark it at a known revision without running migrations: + +```bash +uv run alembic stamp +``` + +{: .alert.alert-danger :} +Stamping does not modify the database schema. It only updates the Alembic version table. Use this only when you are certain the schema matches the specified revision. + +## Further Reading + +- [Development Environment Setup](/agents/salesagent/developers/dev-setup.html) -- Database setup and common development commands +- [Source Architecture](/agents/salesagent/developers/source-architecture.html) -- Codebase organization and module documentation diff --git a/agents/salesagent/developers/source-architecture.md b/agents/salesagent/developers/source-architecture.md new file mode 100644 index 0000000000..76090d1dd3 --- /dev/null +++ b/agents/salesagent/developers/source-architecture.md @@ -0,0 +1,448 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Developers - Source Code Architecture +description: Deep-dive into the Prebid Sales Agent source code structure, core modules, tools, database layer, services, and key patterns +sidebarType: 10 +--- + +# Source Code Architecture +{: .no_toc} + +This page provides a detailed walkthrough of the Prebid Sales Agent source code. It covers the project directory structure, core modules, tool implementations, database layer, services, adapters, and the key architectural patterns that tie everything together. + +- TOC +{:toc} + +## Project Structure + +The source code lives under the `src/` directory, organized by architectural layer: + +```text +src/ +├── core/ # Core application logic +│ ├── main.py # FastMCP server, route mounting, entry point +│ ├── schemas.py # Pydantic request/response models +│ ├── tool_context.py # ToolContext dependency injection +│ ├── config_loader.py # Environment variable loading, feature flags +│ ├── audit_logger.py # Operation logging, security event tracking +│ ├── database/ # Database layer +│ │ ├── models.py # SQLAlchemy ORM models +│ │ ├── database.py # Engine initialization, pool config +│ │ └── database_session.py # get_db_session() context manager +│ └── tools/ # MCP tool implementations +│ ├── capabilities.py # get_adcp_capabilities +│ ├── products.py # get_products +│ ├── creative_formats.py # list_creative_formats +│ ├── media_buy_create.py # create_media_buy +│ ├── media_buy_update.py # update_media_buy +│ ├── media_buy_delivery.py # get_media_buy_delivery +│ ├── media_buy_list.py # get_media_buys +│ ├── properties.py # list_authorized_properties +│ ├── performance.py # update_performance_index +│ └── creatives/ # Creative management tools +│ ├── sync_wrappers.py # sync_creatives +│ └── listing.py # list_creatives +├── services/ # Business logic services +│ ├── ai_product_service.py # Gemini AI product discovery +│ ├── targeting_capabilities.py # Targeting taxonomy management +│ └── gam_inventory_service.py # GAM inventory synchronization +├── adapters/ # Ad server adapters +│ ├── base.py # AdServerAdapter abstract base class +│ ├── google_ad_manager.py # GAM API integration +│ └── mock_ad_server.py # Mock adapter for testing +└── admin/ # Admin UI + ├── app.py # Flask application factory + └── blueprints/ # Flask blueprints + ├── tenants.py # Tenant management views + └── activity_stream.py # SSE activity feed +``` + +Supporting directories outside `src/`: + +```text +tests/ +├── unit/ # Fast, no-dependency tests +├── integration/ # Requires PostgreSQL +└── e2e/ # Full Docker-based tests + +alembic/ # Database migration scripts +docker/ # Dockerfiles and compose configs +.claude/ # Claude Code project context +``` + +## Core Modules + +### main.py -- Application Entry Point + +`src/core/main.py` is the entry point for the entire application. It creates the FastMCP server instance, mounts the Admin UI as a sub-application, and registers all MCP tools. + +```python +from mcp.server.fastmcp import FastMCP + +# Create the FastMCP server +mcp = FastMCP("salesagent") + +# Mount the Flask admin UI +mcp.mount("/admin", admin_app) + +# Tools are registered via @mcp.tool() decorators in the tools/ directory +# The import of tool modules triggers their registration +``` + +Key responsibilities: + +- Creates and configures the `FastMCP` server instance +- Mounts the Admin UI Flask application at `/admin` +- Imports all tool modules, triggering `@mcp.tool()` registration +- Configures HTTP/SSE transport and authentication middleware +- Starts the ASGI server (Uvicorn) when run directly + +### schemas.py -- Pydantic Validation Layer + +`src/core/schemas.py` defines all request and response models as Pydantic `BaseModel` classes. Every piece of data entering or leaving the system passes through these schemas for validation. + +```python +from pydantic import BaseModel, Field +from enum import Enum +from typing import Optional +from datetime import datetime + +class PricingModel(str, Enum): + cpm = "cpm" + vcpm = "vcpm" + cpc = "cpc" + cpcv = "cpcv" + cpv = "cpv" + cpp = "cpp" + flat_rate = "flat_rate" + +class Product(BaseModel): + product_id: str + name: str + description: str + delivery_type: DeliveryType + format_ids: list[str] + pricing_options: list[PricingOption] + estimated_exposures: Optional[ExposureEstimate] = None + channels: list[str] + countries: list[str] +``` + +See the [API Schema Reference](/agents/salesagent/schemas/api-schemas.html) for the complete model catalog. + +### tool_context.py -- Dependency Injection + +`src/core/tool_context.py` defines the `ToolContext` class, which is the primary mechanism for dependency injection in MCP tools. Every tool that needs database access, adapter access, or tenant scoping receives a `ToolContext` instance. + +```python +class ToolContext: + tenant_id: int # Resolved from auth token + principal_id: int # The authenticated agent/user + db_session: Session # SQLAlchemy session (scoped to tenant) + adapter: AdServerAdapter # Tenant's configured ad server adapter + config: Config # Server configuration and feature flags +``` + +The `get_tool_context` dependency resolver: + +1. Extracts the auth token from the request headers +2. Looks up the principal and tenant from the database +3. Loads the tenant's adapter configuration +4. Constructs a `ToolContext` with all resolved dependencies +5. Injects it into the tool function via FastAPI-style `Depends()` + +### config_loader.py -- Configuration Management + +`src/core/config_loader.py` handles loading configuration from environment variables, resolving tenant-specific settings, and managing feature flags. + +{: .table .table-bordered .table-striped } +| Responsibility | Description | +|---------------|-------------| +| **Environment loading** | Reads `.env` file and environment variables | +| **Tenant resolution** | Maps hostnames and tokens to tenant configurations | +| **Feature flags** | Controls optional features (AI search, approval workflows) | +| **Validation** | Ensures required variables are set at startup | + +### audit_logger.py -- Audit Logging + +`src/core/audit_logger.py` records all significant operations for compliance and debugging. Every media buy creation, creative upload, status change, and administrative action is logged with: + +- Timestamp +- Tenant ID and principal ID +- Action type and entity references +- Request details and outcome +- Security-relevant events (failed auth, permission denials) + +## Tools Directory + +The `src/core/tools/` directory contains one file per MCP tool (or group of related tools). Each tool is registered with the FastMCP server via the `@mcp.tool()` decorator. + +### Tool Registration Pattern + +All tools follow a consistent pattern: + +```python +from src.core.main import mcp +from src.core.tool_context import ToolContext, get_tool_context +from fastapi import Depends + +@mcp.tool() +async def tool_name( + param1: str, + param2: int = 0, + ctx: ToolContext = Depends(get_tool_context), +) -> ToolResponse: + """Tool description shown to AI agents.""" + return await _tool_name_impl(param1, param2, ctx) +``` + +### Tool Catalog + +{: .table .table-bordered .table-striped } +| File | Tool(s) | Category | Description | +|------|---------|----------|-------------| +| `capabilities.py` | `get_adcp_capabilities` | Discovery | Protocol capability discovery and agent metadata | +| `products.py` | `get_products` | Discovery | AI-powered product search with Gemini integration | +| `creative_formats.py` | `list_creative_formats` | Discovery | Creative format specifications and constraints | +| `media_buy_create.py` | `create_media_buy` | Execution | Campaign creation with targeting and budget | +| `media_buy_update.py` | `update_media_buy` | Execution | Campaign modification (budget, dates, status) | +| `media_buy_delivery.py` | `get_media_buy_delivery` | Performance | Delivery metrics from the ad server | +| `media_buy_list.py` | `get_media_buys` | Execution | Query and list media buys with filters | +| `creatives/sync_wrappers.py` | `sync_creatives` | Creative | Upload and sync creative assets with the ad server | +| `creatives/listing.py` | `list_creatives` | Creative | Search and filter the creative library | +| `properties.py` | `list_authorized_properties` | Governance | Publisher domain authorization | +| `performance.py` | `update_performance_index` | Performance | Package-level performance data aggregation | + +### Error Handling with with_error_logging + +Tools use a `with_error_logging` wrapper that provides consistent error handling and audit logging: + +```python +@mcp.tool() +async def create_media_buy( + params: CreateMediaBuyRequest, + ctx: ToolContext = Depends(get_tool_context), +) -> CreateMediaBuyResponse: + """Create a new media buy campaign.""" + async with with_error_logging(ctx, "create_media_buy"): + # Business logic here + # Errors are caught, logged, and returned as structured error responses + ... +``` + +This wrapper: + +- Logs the tool invocation with parameters +- Catches and classifies exceptions (validation, auth, not found, internal) +- Records the outcome in the audit log +- Returns structured error objects to the AI agent + +### raw_functions.py for A2A + +Some tools expose their core logic through `raw_functions.py` modules, which provide a plain Python interface without MCP decorators. These are used by the A2A (Agent-to-Agent) JSON-RPC handler to invoke the same business logic through a different protocol. + +## Database Layer + +The database layer lives in `src/core/database/` and uses SQLAlchemy 2.x with async support. + +### models.py -- ORM Models + +`src/core/database/models.py` defines SQLAlchemy declarative models for all database tables: + +{: .table .table-bordered .table-striped } +| Model | Table | Key Fields | Relationships | +|-------|-------|------------|---------------| +| `Tenant` | `tenants` | `id`, `name`, `slug`, `ad_server_type`, `config` | Has many: principals, products, media_buys, audit_logs | +| `Principal` | `principals` | `id`, `tenant_id`, `email`, `name`, `token_hash`, `role` | Belongs to: tenant. Has many: media_buys | +| `Product` | `products` | `id`, `tenant_id`, `name`, `description`, `pricing`, `targeting` | Belongs to: tenant. Has many: media_buys | +| `MediaBuy` | `media_buys` | `id`, `tenant_id`, `principal_id`, `product_id`, `budget`, `status`, `flight_dates` | Belongs to: tenant, principal, product. Has many: creatives | +| `Creative` | `creatives` | `id`, `tenant_id`, `media_buy_id`, `format`, `content`, `approval_status` | Belongs to: tenant, media_buy | +| `AuditLog` | `audit_logs` | `id`, `tenant_id`, `principal_id`, `action`, `entity_type`, `entity_id`, `details`, `timestamp` | Belongs to: tenant, principal | + +All models include `tenant_id` as a foreign key with a composite unique constraint to enforce data isolation. + +### database.py -- Engine Initialization + +`src/core/database/database.py` creates and configures the SQLAlchemy async engine with connection pooling: + +```python +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession + +engine = create_async_engine( + DATABASE_URL, + pool_size=10, + max_overflow=20, + pool_recycle=3600, +) +``` + +### database_session.py -- Session Context Manager + +`src/core/database/database_session.py` provides the `get_db_session()` context manager, which is the only sanctioned way to obtain a database session: + +```python +from contextlib import asynccontextmanager + +@asynccontextmanager +async def get_db_session(): + async with AsyncSession(engine) as session: + try: + yield session + await session.commit() + except Exception: + await session.rollback() + raise +``` + +{: .alert.alert-warning :} +All database access must go through `get_db_session()`. Never create connections manually or use raw connection strings. This ensures proper connection pooling, transaction management, and cleanup. + +## Services + +The `src/services/` directory contains business logic that is shared across multiple tools. + +### ai_product_service.py -- Gemini AI Integration + +`src/services/ai_product_service.py` implements the AI-powered product discovery pipeline. When an AI agent calls `get_products` with a natural language brief: + +1. Loads the tenant's product catalog from the database +2. Sends the products and search query to the Google Gemini API +3. Gemini ranks products by relevance to the brief +4. Returns scored and filtered results + +When no Gemini API key is configured, the service falls back to keyword-based matching against product names and descriptions. + +### targeting_capabilities.py -- Targeting Taxonomy + +`src/services/targeting_capabilities.py` manages geographic and custom targeting taxonomies. It resolves targeting identifiers (country codes, DMA codes, custom key-values) into ad server-specific targeting criteria. + +### gam_inventory_service.py -- GAM Inventory Sync + +`src/services/gam_inventory_service.py` synchronizes inventory data between the Sales Agent's local product catalog and GAM. It handles: + +- Fetching ad unit hierarchies from GAM +- Mapping GAM placements to local products +- Syncing delivery forecasts and availability data + +## Adapters + +The `src/adapters/` directory implements the adapter pattern for ad server integrations. + +### base.py -- Abstract Interface + +`src/adapters/base.py` defines the `AdServerAdapter` abstract base class. All adapters must implement these methods: + +```python +class AdServerAdapter(ABC): + @abstractmethod + async def create_order(self, media_buy: MediaBuy) -> str: ... + + @abstractmethod + async def create_line_item(self, media_buy: MediaBuy, order_id: str) -> str: ... + + @abstractmethod + async def upload_creative(self, creative: Creative) -> str: ... + + @abstractmethod + async def get_delivery_report(self, order_id: str) -> DeliveryReport: ... + + @abstractmethod + async def sync_creative(self, creative: Creative) -> str: ... +``` + +### google_ad_manager.py -- GAM Adapter + +`src/adapters/google_ad_manager.py` implements the `AdServerAdapter` interface for Google Ad Manager. It uses the GAM API client library to translate AdCP operations into GAM API calls. See the [Google Ad Manager Integration](/agents/salesagent/integrations/gam.html) guide for configuration details. + +### mock_ad_server.py -- Mock Adapter + +`src/adapters/mock_ad_server.py` provides a mock implementation that returns simulated responses. It is used for: + +- Local development without GAM credentials +- Unit and integration testing +- Demo and evaluation environments + +## Admin UI + +The `src/admin/` directory contains a Flask web application that provides the publisher dashboard. + +### app.py -- Flask Application + +`src/admin/app.py` creates the Flask application, configures session management, and mounts it as a sub-application of the FastMCP server at `/admin`. + +### Blueprints + +{: .table .table-bordered .table-striped } +| Blueprint | File | Routes | Description | +|-----------|------|--------|-------------| +| **Tenants** | `blueprints/tenants.py` | `/admin/tenants/*` | Tenant management, product CRUD, principal management | +| **Activity Stream** | `blueprints/activity_stream.py` | `/admin/activity/*` | Real-time SSE feed of system events | + +The Admin UI supports OAuth login, role-based access control, and a human-in-the-loop approval queue for media buys and creatives. + +## Key Patterns + +### Pattern 1: @mcp.tool() + with_error_logging + +Every MCP tool combines the `@mcp.tool()` decorator for registration with the `with_error_logging` context manager for consistent error handling and audit logging: + +```python +@mcp.tool() +async def tool_name( + params: RequestModel, + ctx: ToolContext = Depends(get_tool_context), +) -> ResponseModel: + """Tool description.""" + async with with_error_logging(ctx, "tool_name"): + # Business logic + return ResponseModel(...) +``` + +### Pattern 2: ToolContext Injection + +All tenant-scoped operations receive a `ToolContext` via FastAPI-style dependency injection. This ensures every database query, adapter call, and audit log entry is automatically scoped to the correct tenant: + +```python +ctx: ToolContext = Depends(get_tool_context) +# ctx.tenant_id, ctx.principal_id, ctx.db, ctx.adapter are all available +``` + +### Pattern 3: get_db_session() Context Manager + +Database access is always mediated by the `get_db_session()` context manager. This enforces connection pooling, transaction boundaries, and automatic rollback on errors. + +### Pattern 4: Adapter Pattern + +Business logic never calls ad server APIs directly. Instead, it delegates to the adapter instance from `ToolContext`, which implements the `AdServerAdapter` interface. This makes the entire service layer ad-server-agnostic. + +### Pattern 5: raw_functions.py for A2A + +Tools expose their core logic through plain Python functions (without MCP decorators) in `raw_functions.py` modules. The A2A JSON-RPC handler calls these functions directly, ensuring both protocols execute identical business logic. + +## Entry Point Summary + +The application starts from `src/core/main.py`: + +1. `FastMCP("salesagent")` creates the MCP server instance +2. Tool modules are imported, triggering `@mcp.tool()` registration for all 14 tools +3. The Flask admin app is mounted at `/admin` +4. Authentication middleware is configured for token resolution +5. The ASGI server (Uvicorn) starts and listens for connections + +```text +src/core/main.py + │ + ├── Creates FastMCP server + ├── Imports src/core/tools/*.py → Registers 14 MCP tools + ├── Mounts src/admin/app.py → Admin UI at /admin + ├── Configures auth middleware → Token → ToolContext resolution + └── Starts Uvicorn → HTTP/SSE on :8080 +``` + +## Further Reading + +- [API Schema Reference](/agents/salesagent/schemas/api-schemas.html) -- Complete Pydantic model documentation +- [Development Environment Setup](/agents/salesagent/developers/dev-setup.html) -- Local dev setup and testing +- [Google Ad Manager Integration](/agents/salesagent/integrations/gam.html) -- GAM adapter configuration +- [Architecture & Protocols](/agents/salesagent/architecture.html) -- High-level system design +- [Tool Reference](/agents/salesagent/tools/tool-reference.html) -- MCP tool catalog with parameters and examples diff --git a/agents/salesagent/developers/testing.md b/agents/salesagent/developers/testing.md new file mode 100644 index 0000000000..2b24433425 --- /dev/null +++ b/agents/salesagent/developers/testing.md @@ -0,0 +1,202 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Developers - Testing Guide +description: Test organization, running tests, fixtures, and coverage for the Prebid Sales Agent +sidebarType: 10 +--- + +# Testing Guide +{: .no_toc} + +The Prebid Sales Agent test suite is organized into four tiers with distinct speed, dependency, and coverage characteristics. + +- TOC +{:toc} + +## Test Organization + +The test suite is split into directories by scope. Each tier has different external dependency requirements and execution time expectations. + +{: .table .table-bordered .table-striped } +| Level | Directory | Speed | Dependencies | Command | +|-------|-----------|-------|-------------|---------| +| Unit | `tests/unit/` | <1s per test | None | `pytest tests/unit/ -v` | +| Integration | `tests/integration/` | <5s per test | PostgreSQL | `pytest tests/integration/ -v` | +| E2E | `tests/e2e/` | <30s per test | Full stack | `pytest tests/e2e/ -v` | +| UI | `tests/ui/` | <10s per test | Flask app | `pytest tests/ui/ -v` | + +- **Unit tests** run without any external services and validate individual functions, schema validation, and business logic in isolation. +- **Integration tests** require a running PostgreSQL database and test service-layer logic, adapter behavior, and database operations against real infrastructure. +- **E2E tests** exercise the full Sales Agent stack inside Docker, testing MCP and A2A protocols from the outside. +- **UI tests** validate the Flask-based admin interface, including form submissions, page rendering, and authentication flows. + +## Test Markers + +Pytest markers control which tests run in a given context. Apply them as decorators on test functions or classes. + +{: .table .table-bordered .table-striped } +| Marker | Description | +|--------|-------------| +| `@pytest.mark.unit` | Unit-level test with no external dependencies | +| `@pytest.mark.integration` | Requires PostgreSQL and service infrastructure | +| `@pytest.mark.e2e` | End-to-end test against the full running stack | +| `@pytest.mark.requires_db` | Needs a database with tables provisioned | +| `@pytest.mark.requires_server` | Needs a running MCP server | +| `@pytest.mark.slow` | Execution time exceeds 5 seconds | +| `@pytest.mark.ai` | Tests AI/LLM features (Gemini) | +| `@pytest.mark.smoke` | Critical-path tests that must always pass | +| `@pytest.mark.gam` | Requires Google Ad Manager credentials | +| `@pytest.mark.skip_ci` | Skipped in CI environments | + +Use markers to filter test runs: + +```bash +# Run only unit tests +pytest -m unit + +# Run smoke tests across all tiers +pytest -m smoke + +# Exclude slow and AI tests +pytest -m "not slow and not ai" +``` + +## Pytest Configuration + +The project configures pytest through `pytest.ini` in the repository root. + +{: .table .table-bordered .table-striped } +| Setting | Value | Purpose | +|---------|-------|---------| +| `asyncio_mode` | `auto` | Automatically handles async test functions without requiring explicit `@pytest.mark.asyncio` | +| `testpaths` | `tests` | Default directory for test discovery | + +## Common Fixtures + +Shared fixtures are defined in `conftest.py` files at each tier of the test directory. These fixtures handle setup and teardown of test infrastructure. + +{: .table .table-bordered .table-striped } +| Fixture | Scope | Description | +|---------|-------|-------------| +| `test_db` | session | Provides an async database session backed by SQLite (unit) or PostgreSQL (integration) | +| `test_tenant` | function | Creates an isolated tenant with default configuration for the test | +| `test_principal` | function | Creates a principal (user) associated with the test tenant | +| `mock_gam_client` | function | Returns a mock Google Ad Manager client that records calls without making real API requests | + +### Using Fixtures + +Fixtures are injected by name as function parameters: + +```python +@pytest.mark.unit +async def test_product_creation(test_db, test_tenant): + """Verify that a product can be created with valid data.""" + product = Product( + tenant_id=test_tenant.id, + name="Display Banner 300x250", + delivery_type="non_guaranteed", + ) + test_db.add(product) + await test_db.flush() + + assert product.id is not None + assert product.tenant_id == test_tenant.id +``` + +## Running Tests + +### Quick Run (SQLite, No External Dependencies) + +```bash +./run_all_tests.sh quick +``` + +This runs unit tests against an in-memory SQLite database. No Docker or PostgreSQL required. + +### Full CI Suite (PostgreSQL) + +```bash +./run_all_tests.sh ci +``` + +This runs the complete test suite against PostgreSQL, matching the CI environment. + +### Specific Tests + +```bash +# All unit tests +pytest tests/unit/ -v + +# Integration tests matching a keyword +pytest tests/integration/ -k "test_media_buy" -v + +# A single test file +pytest tests/unit/test_schemas.py -v + +# A single test function +pytest tests/unit/test_schemas.py::test_product_validation -v +``` + +### With Docker + +```bash +# Unit tests inside the container +docker-compose exec adcp-server pytest tests/unit/ + +# Full suite with coverage report +docker-compose exec adcp-server pytest --cov=. --cov-report=html +``` + +The HTML coverage report is written to `htmlcov/` inside the container. Mount a volume or copy the output to view it locally. + +## Test Structure + +All tests follow the Arrange-Act-Assert pattern: + +```python +@pytest.mark.unit +async def test_media_buy_total_calculation(test_db, test_tenant): + """Total spend should equal unit price multiplied by quantity.""" + # Arrange + product = await create_test_product(test_db, test_tenant, cpm=12.50) + line_item = LineItem(product_id=product.id, impressions=100_000) + + # Act + total = line_item.calculate_total() + + # Assert + assert total == Decimal("1250.00") +``` + +Each test should validate a single behavior. Use descriptive test names that state the expected outcome. + +## Coverage Targets + +The project enforces minimum coverage thresholds per test tier. + +{: .table .table-bordered .table-striped } +| Tier | Target | Rationale | +|------|--------|-----------| +| Unit | 85% | Core business logic and schema validation | +| Integration | 80% | Database operations and service-layer interactions | +| E2E | 50% | Critical user paths through the full stack | +| UI | 60% | Admin interface pages and form handling | + +Run coverage reports locally: + +```bash +# Terminal summary with missing lines +pytest tests/unit/ --cov=src --cov-report=term-missing + +# HTML report for detailed analysis +pytest tests/unit/ --cov=src --cov-report=html +``` + +{: .alert.alert-info :} +Coverage targets are enforced in CI. A pull request that drops coverage below the threshold will fail the `test.yml` workflow. + +## Further Reading + +- [Development Environment Setup](/agents/salesagent/developers/dev-setup.html) -- Prerequisites and local setup +- [Testing with the Mock Adapter](/agents/salesagent/tutorials/mock-testing.html) -- Tutorial for testing campaign workflows without a real ad server +- [Contributing](/agents/salesagent/developers/contributing.html) -- Code style, commit conventions, and CI pipeline diff --git a/agents/salesagent/getting-started/buy-side-integration.md b/agents/salesagent/getting-started/buy-side-integration.md new file mode 100644 index 0000000000..ed40bbc37e --- /dev/null +++ b/agents/salesagent/getting-started/buy-side-integration.md @@ -0,0 +1,293 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Getting Started - Buy-Side Integration +description: How to connect an AI buying agent to a Prebid Sales Agent via MCP and A2A protocols +sidebarType: 10 +--- + +# Buy-Side Integration +{: .no_toc} + +This guide explains how to connect an AI buying agent to a Prebid Sales Agent instance. Whether you are building a custom agent or configuring Claude Desktop, you can use either the MCP (Model Context Protocol) or A2A (Agent-to-Agent) protocol to discover inventory, create media buys, manage creatives, and monitor delivery. + +- TOC +{:toc} + +## MCP Integration + +The MCP interface is the primary way for AI agents to interact with the Sales Agent. It uses [FastMCP](https://github.com/jlowin/fastmcp) with StreamableHTTP transport. + +### Python Client Setup + +Install the FastMCP client library: + +```bash +pip install fastmcp +``` + +Connect to the Sales Agent using `StreamableHttpTransport`: + +```python +import asyncio +from fastmcp import Client +from fastmcp.client.transports import StreamableHttpTransport + +# Configure the transport with your Sales Agent URL and auth token +transport = StreamableHttpTransport( + "http://localhost:8000/mcp/", + headers={"x-adcp-auth": "your-auth-token"} +) + +async def main(): + async with Client(transport) as client: + # List all available tools + tools = await client.list_tools() + for tool in tools: + print(f" {tool.name}: {tool.description}") + +asyncio.run(main()) +``` + +{: .alert.alert-info :} +The MCP endpoint URL must include the trailing slash: `http://localhost:8000/mcp/` + +### Authentication + +MCP requests are authenticated via the `x-adcp-auth` header. Each advertiser receives a unique token from the publisher (see [Publisher Onboarding](/agents/salesagent/getting-started/publisher-onboarding.html)). + +```python +transport = StreamableHttpTransport( + "https://sales.publisher.com/mcp/", + headers={"x-adcp-auth": "advertiser-api-token"} +) +``` + +{: .alert.alert-warning :} +In test mode (`ADCP_AUTH_TEST_MODE=true`), the token `test-token` is accepted. Never use test mode in production. + +### Calling Tools + +Use `client.call_tool()` to invoke any MCP tool: + +```python +async with Client(transport) as client: + # Search products by natural language brief + products = await client.call_tool("get_products", { + "brief": "video ads for sports audience" + }) + + # Get full product details + product = await client.call_tool("get_product_details", { + "product_id": "prod_abc123" + }) + + # List supported creative formats + formats = await client.call_tool("list_creative_formats", {}) +``` + +{: .alert.alert-info :} +For the complete tool catalog with parameters and response schemas, see the [Tool Reference](/agents/salesagent/tools/tool-reference.html). + +## A2A Integration + +The A2A (Agent-to-Agent) protocol provides an alternative interface using JSON-RPC 2.0. It is designed for complex multi-agent workflows and orchestration platforms. + +### Agent Discovery + +A2A agents discover Sales Agent capabilities via the standard agent card: + +```bash +curl https://sales.publisher.com/.well-known/agent.json +``` + +The agent card returns a JSON descriptor with the agent's name, description, supported capabilities, and endpoint URL: + +```json +{ + "name": "Publisher Sales Agent", + "description": "AI-driven advertising sales for publisher inventory", + "url": "https://sales.publisher.com/a2a", + "version": "1.0.0", + "capabilities": { + "streaming": false, + "pushNotifications": false + }, + "skills": [ + { + "id": "media-buying", + "name": "Media Buying", + "description": "Discover products, create and manage media buys" + } + ], + "authentication": { + "schemes": ["bearer"] + } +} +``` + +### JSON-RPC 2.0 Requests + +A2A uses standard JSON-RPC 2.0 over HTTP POST: + +```bash +curl -X POST https://sales.publisher.com/a2a \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your-auth-token" \ + -d '{ + "jsonrpc": "2.0", + "id": "1", + "method": "tasks/send", + "params": { + "id": "task-001", + "message": { + "role": "user", + "parts": [ + { + "type": "text", + "text": "Show me available video advertising products" + } + ] + } + } + }' +``` + +### A2A Authentication + +A2A requests use standard Bearer token authentication in the `Authorization` header: + +```bash +Authorization: Bearer advertiser-api-token +``` + +## Complete Workflow Example + +The following example demonstrates a full media buying workflow from product discovery through delivery monitoring: + +```python +import asyncio +from fastmcp import Client +from fastmcp.client.transports import StreamableHttpTransport + +transport = StreamableHttpTransport( + "http://localhost:8000/mcp/", + headers={"x-adcp-auth": "your-auth-token"} +) + +async def run_campaign(): + async with Client(transport) as client: + + # 1. Discover capabilities + capabilities = await client.call_tool("get_adcp_capabilities", {}) + print("Capabilities:", capabilities) + + # 2. Search for products matching the campaign brief + products = await client.call_tool("get_products", { + "brief": "homepage banner ads for tech audience" + }) + print("Found products:", products) + + # 3. Check supported creative formats + formats = await client.call_tool("list_creative_formats", {}) + print("Creative formats:", formats) + + # 4. Create a media buy + media_buy = await client.call_tool("create_media_buy", { + "product_id": "prod_abc123", + "advertiser_id": "adv_xyz789", + "name": "Q2 Homepage Takeover", + "budget_cents": 100000, + "start_date": "2025-04-01", + "end_date": "2025-06-30" + }) + print("Media buy created:", media_buy) + media_buy_id = media_buy[0].text # Extract the media buy ID + + # 5. Sync creatives to the media buy + creative = await client.call_tool("sync_creatives", { + "media_buy_id": media_buy_id, + "creatives": [ + { + "name": "Spring Sale Banner", + "format": "display", + "width": 728, + "height": 90, + "click_url": "https://advertiser.com/spring-sale", + "asset_url": "https://cdn.advertiser.com/banners/spring-728x90.jpg" + } + ] + }) + print("Creatives synced:", creative) + + # 6. Monitor delivery + delivery = await client.call_tool("get_media_buy_delivery", { + "media_buy_id": media_buy_id + }) + print("Delivery report:", delivery) + +asyncio.run(run_campaign()) +``` + +### Workflow Steps Explained + +{: .table .table-bordered .table-striped } +| Step | Tool | Purpose | +|------|------|---------| +| 1 | `get_adcp_capabilities` | Discover what the Sales Agent supports | +| 2 | `get_products` | Search inventory matching a campaign brief | +| 3 | `list_creative_formats` | Check accepted creative formats and specs | +| 4 | `create_media_buy` | Book a campaign with budget and flight dates | +| 5 | `sync_creatives` | Attach creative assets to the media buy | +| 6 | `get_media_buy_delivery` | Monitor impressions, clicks, and spend | + +## Claude Desktop Integration + +You can connect Claude Desktop directly to a Sales Agent by adding it as an MCP server in your configuration. + +Edit your Claude Desktop config file: + +- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` +- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` + +Add the Sales Agent as an MCP server: + +```json +{ + "mcpServers": { + "salesagent": { + "command": "uvx", + "args": [ + "adcp", + "http://localhost:8000/mcp/", + "--auth", + "your-auth-token" + ] + } + } +} +``` + +After saving the config, restart Claude Desktop. The Sales Agent tools will appear in Claude's tool list, allowing you to interact with advertising inventory through natural conversation. + +{: .alert.alert-info :} +Claude Desktop will show the Sales Agent tools with their descriptions. You can ask Claude to search for products, create campaigns, and check delivery using natural language. + +## Error Handling + +When integrating with the Sales Agent, handle common error scenarios: + +{: .table .table-bordered .table-striped } +| Error | Cause | Resolution | +|-------|-------|------------| +| `401 Unauthorized` | Invalid or missing auth token | Verify the token with the publisher | +| `403 Forbidden` | Token lacks required permissions | Request appropriate access from the publisher | +| `404 Not Found` | Invalid resource ID | Check the ID returned from previous tool calls | +| `422 Validation Error` | Invalid parameters | Review the tool's parameter schema | +| `429 Rate Limited` | Too many requests | Implement exponential backoff | + +## Further Reading + +- [Tool Reference](/agents/salesagent/tools/tool-reference.html) -- Complete catalog of MCP tools with parameters +- [Publisher Onboarding](/agents/salesagent/getting-started/publisher-onboarding.html) -- Set up your own Sales Agent +- [AdCP Schemas](https://docs.adcontextprotocol.org/docs/reference/schemas) -- Protocol schema definitions +- [AdCP SDKs](https://docs.adcontextprotocol.org/docs/sdks) -- Official client libraries diff --git a/agents/salesagent/getting-started/configuration.md b/agents/salesagent/getting-started/configuration.md new file mode 100644 index 0000000000..4959299b6d --- /dev/null +++ b/agents/salesagent/getting-started/configuration.md @@ -0,0 +1,347 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Getting Started - Configuration +description: Complete configuration reference for the Prebid Sales Agent including environment variables, Docker Compose, and Nginx setup +sidebarType: 10 +--- + +# Configuration Reference +{: .no_toc} + +This page documents all configuration options for the Prebid Sales Agent, including environment variables, Docker Compose settings, Nginx reverse proxy configuration, and runtime feature flags. + +- TOC +{:toc} + +## Environment Variables + +The Sales Agent is configured primarily through environment variables. These can be set in a `.env` file, passed directly to Docker, or configured in your orchestration platform. + +### Core Settings + +{: .table .table-bordered .table-striped } +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `DATABASE_URL` | Yes | -- | PostgreSQL connection string. Format: `postgresql://user:pass@host:5432/dbname` | +| `ENCRYPTION_KEY` | Yes | -- | Secret key for encrypting sensitive data at rest. Must be 32+ characters. Generate with `openssl rand -hex 32` | +| `SALES_AGENT_DOMAIN` | Yes (prod) | `localhost:8000` | Public domain of the Sales Agent. Used for OAuth callbacks and agent card URLs | +| `ENVIRONMENT` | No | `development` | Runtime environment. Set to `production` for production logging, security headers, and error handling | + +{: .alert.alert-danger :} +The `ENCRYPTION_KEY` is used to encrypt ad server credentials and API tokens in the database. Losing this key means losing access to all encrypted data. Back it up securely. + +### Multi-Tenant Configuration + +{: .table .table-bordered .table-striped } +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `ADCP_MULTI_TENANT` | No | `true` | Enable multi-tenant mode. When `false`, the system operates as a single publisher | + +### Authentication and Access Control + +{: .table .table-bordered .table-striped } +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `SUPER_ADMIN_EMAILS` | No | -- | Comma-separated list of email addresses granted super admin access. Example: `admin@pub.com,ops@pub.com` | +| `SUPER_ADMIN_DOMAINS` | No | -- | Comma-separated list of email domains. All users with matching email domains get super admin access. Example: `publisher.com` | +| `ADCP_AUTH_TEST_MODE` | No | `false` | Enable test authentication mode. Accepts `test-token` for all endpoints. **Never enable in production** | + +{: .alert.alert-warning :} +`ADCP_AUTH_TEST_MODE=true` bypasses all authentication. This is intended only for local development and testing. + +### OAuth / SSO Providers + +Configure one or more OAuth providers for admin UI authentication. + +#### Google + +{: .table .table-bordered .table-striped } +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `OAUTH_GOOGLE_CLIENT_ID` | No | -- | Google OAuth 2.0 client ID | +| `OAUTH_GOOGLE_CLIENT_SECRET` | No | -- | Google OAuth 2.0 client secret | + +#### Microsoft + +{: .table .table-bordered .table-striped } +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `OAUTH_MICROSOFT_CLIENT_ID` | No | -- | Microsoft / Azure AD application ID | +| `OAUTH_MICROSOFT_CLIENT_SECRET` | No | -- | Microsoft / Azure AD client secret | + +#### Okta + +{: .table .table-bordered .table-striped } +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `OAUTH_OKTA_CLIENT_ID` | No | -- | Okta OAuth client ID | +| `OAUTH_OKTA_CLIENT_SECRET` | No | -- | Okta OAuth client secret | +| `OAUTH_OKTA_DOMAIN` | No | -- | Okta organization domain. Example: `your-org.okta.com` | + +#### Auth0 + +{: .table .table-bordered .table-striped } +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `OAUTH_AUTH0_CLIENT_ID` | No | -- | Auth0 application client ID | +| `OAUTH_AUTH0_CLIENT_SECRET` | No | -- | Auth0 application client secret | +| `OAUTH_AUTH0_DOMAIN` | No | -- | Auth0 tenant domain. Example: `your-tenant.auth0.com` | + +#### Keycloak + +{: .table .table-bordered .table-striped } +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `OAUTH_KEYCLOAK_CLIENT_ID` | No | -- | Keycloak client ID | +| `OAUTH_KEYCLOAK_CLIENT_SECRET` | No | -- | Keycloak client secret | +| `OAUTH_KEYCLOAK_URL` | No | -- | Keycloak realm URL. Example: `https://keycloak.example.com/realms/myrealm` | + +### Google Ad Manager (GAM) Integration + +{: .table .table-bordered .table-striped } +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `GAM_OAUTH_CLIENT_ID` | No | -- | GAM API OAuth client ID | +| `GAM_OAUTH_CLIENT_SECRET` | No | -- | GAM API OAuth client secret | +| `GAM_OAUTH_REFRESH_TOKEN` | No | -- | GAM API OAuth refresh token for offline access | +| `GAM_NETWORK_CODE` | No | -- | GAM network code (numeric ID) | + +{: .alert.alert-info :} +When GAM credentials are not configured, the Sales Agent falls back to the mock adapter, which simulates ad server responses for development and testing. + +### AI and Search + +{: .table .table-bordered .table-striped } +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `GEMINI_API_KEY` | No | -- | Google Gemini API key for AI-powered product search and RAG capabilities | + +### Development and Testing + +{: .table .table-bordered .table-striped } +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `CREATE_DEMO_TENANT` | No | `false` | Automatically create a demo publisher tenant with sample products and advertisers on startup | +| `ADCP_AUTH_TEST_MODE` | No | `false` | Accept `test-token` for authentication on all endpoints | + +## Example .env File + +A minimal `.env` file for local development: + +```bash +# Database +DATABASE_URL=postgresql://salesagent:salesagent@localhost:5432/salesagent + +# Security +ENCRYPTION_KEY=your-random-32-character-encryption-key-here + +# Domain +SALES_AGENT_DOMAIN=localhost:8000 + +# Development +CREATE_DEMO_TENANT=true +ADCP_AUTH_TEST_MODE=true +ENVIRONMENT=development +``` + +A production `.env` file: + +```bash +# Database +DATABASE_URL=postgresql://user:securepassword@db.internal:5432/salesagent + +# Security +ENCRYPTION_KEY=a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2 + +# Domain +SALES_AGENT_DOMAIN=sales.publisher.com +ENVIRONMENT=production + +# Multi-tenant +ADCP_MULTI_TENANT=true + +# Access control +SUPER_ADMIN_EMAILS=admin@publisher.com +SUPER_ADMIN_DOMAINS=publisher.com + +# OAuth (Google) +OAUTH_GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com +OAUTH_GOOGLE_CLIENT_SECRET=your-client-secret + +# Google Ad Manager +GAM_OAUTH_CLIENT_ID=your-gam-client-id +GAM_OAUTH_CLIENT_SECRET=your-gam-secret +GAM_OAUTH_REFRESH_TOKEN=your-refresh-token +GAM_NETWORK_CODE=12345678 + +# AI +GEMINI_API_KEY=your-gemini-api-key +``` + +{: .alert.alert-danger :} +Never commit `.env` files to version control. Add `.env` to your `.gitignore` file. + +## Docker Compose Configuration + +The default `docker-compose.yml` starts both the Sales Agent and a PostgreSQL database: + +```yaml +services: + salesagent: + image: ghcr.io/prebid/salesagent:latest + ports: + - "8000:8000" + env_file: + - .env + depends_on: + db: + condition: service_healthy + restart: unless-stopped + + db: + image: postgres:16 + environment: + POSTGRES_USER: salesagent + POSTGRES_PASSWORD: salesagent + POSTGRES_DB: salesagent + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U salesagent"] + interval: 5s + timeout: 5s + retries: 5 + restart: unless-stopped + +volumes: + pgdata: +``` + +### Key Docker Compose Options + +{: .table .table-bordered .table-striped } +| Setting | Description | +|---------|-------------| +| `env_file` | Path to your `.env` file with environment variables | +| `ports` | Map container port 8000 to a host port | +| `depends_on` with `service_healthy` | Ensures the database is ready before starting the Sales Agent | +| `volumes: pgdata` | Persistent storage for PostgreSQL data | +| `restart: unless-stopped` | Automatically restart on failure or host reboot | + +### Custom Port Mapping + +To run the Sales Agent on a different port: + +```yaml +ports: + - "9000:8000" # Access at http://localhost:9000 +``` + +### External Database + +To use an external PostgreSQL database, remove the `db` service and set `DATABASE_URL` directly: + +```yaml +services: + salesagent: + image: ghcr.io/prebid/salesagent:latest + ports: + - "8000:8000" + environment: + DATABASE_URL: postgresql://user:pass@external-db:5432/salesagent + ENCRYPTION_KEY: your-encryption-key + restart: unless-stopped +``` + +## Nginx Reverse Proxy + +For production deployments, place the Sales Agent behind an Nginx reverse proxy to handle SSL termination and request routing. + +### Basic Nginx Configuration + +```nginx +server { + listen 443 ssl http2; + server_name sales.publisher.com; + + ssl_certificate /etc/letsencrypt/live/sales.publisher.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/sales.publisher.com/privkey.pem; + + # MCP and A2A endpoints + location / { + proxy_pass http://127.0.0.1:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # SSE support for real-time dashboard + location /events { + proxy_pass http://127.0.0.1:8000; + proxy_set_header Host $host; + proxy_set_header Connection ''; + proxy_http_version 1.1; + chunked_transfer_encoding off; + proxy_buffering off; + proxy_cache off; + } + + # MCP streaming support + location /mcp/ { + proxy_pass http://127.0.0.1:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_http_version 1.1; + proxy_buffering off; + proxy_cache off; + } +} + +server { + listen 80; + server_name sales.publisher.com; + return 301 https://$server_name$request_uri; +} +``` + +{: .alert.alert-info :} +The SSE and MCP locations require `proxy_buffering off` to support streaming responses. Without this, real-time events and MCP transport will not work correctly. + +## Feature Flags and Runtime Config + +Some configuration options can be changed at runtime through the Admin UI without restarting the Sales Agent. + +### Admin UI Settings + +Navigate to **Admin UI > Settings** to configure: + +{: .table .table-bordered .table-striped } +| Setting | Description | +|---------|-------------| +| Approval Workflows | Enable/disable human-in-the-loop approval for media buys and creatives | +| Notification Preferences | Configure email or webhook notifications for new campaigns | +| Rate Limits | Set per-advertiser request rate limits | +| Product Visibility | Toggle products as active/inactive without deleting them | + +### Tenant-Level Configuration + +In multi-tenant mode, each publisher tenant can independently configure: + +- Active ad server adapter +- Product catalog and pricing +- Advertiser accounts and tokens +- Approval workflow rules +- Notification settings + +{: .alert.alert-info :} +Runtime configuration changes take effect immediately without a restart. Environment variable changes require a container restart. + +## Further Reading + +- [Quick Start](/agents/salesagent/getting-started/quickstart.html) -- Get running with Docker +- [Publisher Onboarding](/agents/salesagent/getting-started/publisher-onboarding.html) -- Step-by-step publisher setup +- [Deployment Guide](/agents/salesagent/deployment/single-tenant.html) -- Production deployment best practices +- [AdCP Configuration](https://docs.adcontextprotocol.org/docs/reference/configuration) -- Protocol-level configuration options diff --git a/agents/salesagent/getting-started/publisher-onboarding.md b/agents/salesagent/getting-started/publisher-onboarding.md new file mode 100644 index 0000000000..7ac79a8ce3 --- /dev/null +++ b/agents/salesagent/getting-started/publisher-onboarding.md @@ -0,0 +1,279 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Getting Started - Publisher Onboarding +description: End-to-end guide for publishers to set up the Prebid Sales Agent with SSO, products, advertisers, and ad server integration +sidebarType: 10 +--- + +# Publisher Onboarding +{: .no_toc} + +This guide walks publishers through the complete onboarding process -- from initial deployment to going live with AI-driven ad sales. Each step builds on the previous one, resulting in a fully operational Sales Agent connected to your ad server. + +- TOC +{:toc} + +## Step 1: Deploy the Sales Agent + +Before configuring your publisher environment, you need a running Sales Agent instance. + +Follow the [Quick Start](/agents/salesagent/getting-started/quickstart.html) guide to deploy with Docker. For production environments, see the [Deployment Guide](/agents/salesagent/deployment/single-tenant.html). + +{: .alert.alert-info :} +For local evaluation, use `CREATE_DEMO_TENANT=true` to pre-populate sample data while you learn the system. + +## Step 2: Configure Single Sign-On (SSO) + +The Sales Agent supports OAuth 2.0 / OpenID Connect for admin authentication. Configure your preferred identity provider to control who can access the publisher dashboard. + +### Supported Providers + +{: .table .table-bordered .table-striped } +| Provider | Environment Variables | Notes | +|----------|----------------------|-------| +| Google | `OAUTH_GOOGLE_CLIENT_ID`, `OAUTH_GOOGLE_CLIENT_SECRET` | Google Workspace or consumer accounts | +| Microsoft | `OAUTH_MICROSOFT_CLIENT_ID`, `OAUTH_MICROSOFT_CLIENT_SECRET` | Azure AD / Entra ID | +| Okta | `OAUTH_OKTA_CLIENT_ID`, `OAUTH_OKTA_CLIENT_SECRET`, `OAUTH_OKTA_DOMAIN` | Requires Okta domain | +| Auth0 | `OAUTH_AUTH0_CLIENT_ID`, `OAUTH_AUTH0_CLIENT_SECRET`, `OAUTH_AUTH0_DOMAIN` | Requires Auth0 domain | +| Keycloak | `OAUTH_KEYCLOAK_CLIENT_ID`, `OAUTH_KEYCLOAK_CLIENT_SECRET`, `OAUTH_KEYCLOAK_URL` | Self-hosted identity provider | + +### Google SSO Setup + +1. Go to the [Google Cloud Console](https://console.cloud.google.com/apis/credentials) +2. Create an OAuth 2.0 Client ID (Web application) +3. Add authorized redirect URI: `https://your-domain.com/auth/google/callback` +4. Set the environment variables: + +```bash +OAUTH_GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com +OAUTH_GOOGLE_CLIENT_SECRET=your-client-secret +``` + +### Microsoft SSO Setup + +1. Register an application in [Azure Portal](https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps) +2. Add a Web redirect URI: `https://your-domain.com/auth/microsoft/callback` +3. Create a client secret under Certificates & Secrets +4. Set the environment variables: + +```bash +OAUTH_MICROSOFT_CLIENT_ID=your-application-id +OAUTH_MICROSOFT_CLIENT_SECRET=your-client-secret +``` + +### Okta SSO Setup + +1. Create a new Web application in your Okta admin console +2. Set the sign-in redirect URI: `https://your-domain.com/auth/okta/callback` +3. Set the environment variables: + +```bash +OAUTH_OKTA_CLIENT_ID=your-client-id +OAUTH_OKTA_CLIENT_SECRET=your-client-secret +OAUTH_OKTA_DOMAIN=your-org.okta.com +``` + +### Auth0 SSO Setup + +1. Create a Regular Web Application in your Auth0 dashboard +2. Add the callback URL: `https://your-domain.com/auth/auth0/callback` +3. Set the environment variables: + +```bash +OAUTH_AUTH0_CLIENT_ID=your-client-id +OAUTH_AUTH0_CLIENT_SECRET=your-client-secret +OAUTH_AUTH0_DOMAIN=your-tenant.auth0.com +``` + +### Keycloak SSO Setup + +1. Create a client in your Keycloak realm +2. Set the Valid Redirect URI: `https://your-domain.com/auth/keycloak/callback` +3. Set the environment variables: + +```bash +OAUTH_KEYCLOAK_CLIENT_ID=your-client-id +OAUTH_KEYCLOAK_CLIENT_SECRET=your-client-secret +OAUTH_KEYCLOAK_URL=https://keycloak.your-domain.com/realms/your-realm +``` + +### Super Admin Access + +Control which users have super admin privileges using email addresses or domain patterns: + +```bash +SUPER_ADMIN_EMAILS=admin@yourpub.com,ops@yourpub.com +SUPER_ADMIN_DOMAINS=yourpub.com +``` + +{: .alert.alert-warning :} +Super admins have full access to all tenants and system settings. Restrict this to a small set of trusted users. + +## Step 3: Set Up Product Catalog + +Products define the advertising inventory you offer to AI buying agents. Each product specifies format, targeting, pricing, and availability. + +### Creating Products + +Navigate to **Admin UI > Products** and create products with the following attributes: + +{: .table .table-bordered .table-striped } +| Field | Description | Example | +|-------|-------------|---------| +| Name | Human-readable product name | "Homepage Leaderboard - Desktop" | +| Description | Detailed description for AI agents | "728x90 display banner on homepage above the fold, desktop only" | +| Ad Unit | Ad server placement identifier | `/12345/homepage/leaderboard` | +| Format | Creative format (display, video, native, audio) | `display` | +| Dimensions | Width x Height for display | `728x90` | +| Base Rate | CPM floor price | `$15.00` | + +### Pricing Options + +Products support multiple pricing models: + +{: .table .table-bordered .table-striped } +| Pricing Model | Description | Use Case | +|---------------|-------------|----------| +| CPM | Cost per thousand impressions | Standard display and video | +| CPC | Cost per click | Performance campaigns | +| Flat Rate | Fixed cost per day/week/month | Sponsorships and takeovers | +| Tiered | Volume-based pricing with breakpoints | High-volume buyers | + +### Product Best Practices + +- Write detailed, natural-language descriptions -- AI agents use these to match buyer intent +- Include targeting capabilities (geo, device, audience segments) in the description +- Set competitive base rates as CPM floors; actual pricing can negotiate upward +- Group related placements into product bundles where appropriate + +{: .alert.alert-info :} +Products are discoverable by AI agents via the `get_products` MCP tool. The better your descriptions, the more accurately agents can match buyer requests to your inventory. + +## Step 4: Create Advertiser Accounts + +Advertisers represent the buying entities that interact with your Sales Agent. Each advertiser gets isolated API credentials. + +### Creating an Advertiser + +Navigate to **Admin UI > Advertisers** to create advertiser accounts: + +{: .table .table-bordered .table-striped } +| Field | Description | Example | +|-------|-------------|---------| +| Name | Advertiser or agency name | "Acme Corp" | +| Contact Email | Primary contact for notifications | "media@acme.com" | +| Status | Active or paused | `active` | + +### Generating API Tokens + +Each advertiser needs an authentication token to access the MCP and A2A endpoints: + +1. Select the advertiser in the Admin UI +2. Navigate to the **API Tokens** section +3. Click **Generate Token** +4. Copy the token and share it securely with the advertiser + +{: .alert.alert-danger :} +API tokens provide full buying access to your inventory. Treat them as secrets -- transmit securely and rotate regularly. + +### Token Authentication + +Advertisers use their token to authenticate with the Sales Agent: + +- **MCP**: Pass the token in the `x-adcp-auth` header +- **A2A**: Pass the token as a `Bearer` token in the `Authorization` header + +## Step 5: Connect Ad Server + +The Sales Agent uses an adapter pattern to integrate with ad servers. The adapter translates AdCP operations into platform-specific API calls. + +### Google Ad Manager (GAM) Adapter + +To connect to GAM, configure the following environment variables: + +```bash +GAM_OAUTH_CLIENT_ID=your-gam-client-id +GAM_OAUTH_CLIENT_SECRET=your-gam-client-secret +GAM_OAUTH_REFRESH_TOKEN=your-refresh-token +GAM_NETWORK_CODE=12345678 +``` + +Setup steps: + +1. Create a service account or OAuth credentials in the [Google API Console](https://console.cloud.google.com/) +2. Enable the Google Ad Manager API +3. Link the credentials to your GAM network +4. Set the environment variables above +5. Restart the Sales Agent + +{: .alert.alert-info :} +The GAM adapter maps AdCP media buys to GAM Orders, Line Items, and Creatives. See the [AdCP GAM Mapping](https://docs.adcontextprotocol.org/docs/reference/gam-mapping) documentation for details. + +### Adapter Configuration + +Select the active adapter via the Admin UI or environment configuration. The adapter determines how media buys, creatives, and delivery data are handled. + +{: .table .table-bordered .table-striped } +| Adapter | Status | Description | +|---------|--------|-------------| +| Google Ad Manager | Production | Full campaign lifecycle management | +| Mock | Testing | Simulates ad server responses for development | + +## Step 6: Test with Mock Adapter + +Before connecting a live ad server, validate your setup using the mock adapter: + +1. Ensure the Sales Agent is running with `ADCP_AUTH_TEST_MODE=true` +2. The mock adapter is active by default when no GAM credentials are configured +3. Test the full workflow: + +```bash +# List available products +uvx adcp http://localhost:8000/mcp/ --auth test-token get_products '{"brief":"all"}' + +# Create a media buy +uvx adcp http://localhost:8000/mcp/ --auth test-token create_media_buy '{ + "product_id": "your-product-id", + "advertiser_id": "your-advertiser-id", + "budget_cents": 50000, + "start_date": "2025-04-01", + "end_date": "2025-04-30", + "name": "Test Campaign" +}' + +# Check delivery +uvx adcp http://localhost:8000/mcp/ --auth test-token get_media_buy_delivery '{ + "media_buy_id": "your-media-buy-id" +}' +``` + +The mock adapter returns realistic simulated responses, allowing you to verify the full campaign lifecycle without affecting live inventory. + +## Step 7: Go-Live Checklist + +Before enabling production traffic, verify the following: + +{: .table .table-bordered .table-striped } +| Item | Status | Details | +|------|--------|---------| +| SSL/TLS configured | Required | HTTPS on your `SALES_AGENT_DOMAIN` | +| SSO configured | Required | At least one OAuth provider enabled | +| `ADCP_AUTH_TEST_MODE` disabled | Required | Must be `false` in production | +| `ENCRYPTION_KEY` set | Required | Unique, random 32+ character key | +| Ad server adapter connected | Required | GAM or other production adapter | +| Products created | Required | At least one product with pricing | +| Advertiser accounts created | Required | With production API tokens | +| `ENVIRONMENT` set to `production` | Recommended | Enables production logging and security | +| Nginx reverse proxy configured | Recommended | See [Configuration](/agents/salesagent/getting-started/configuration.html) | +| Backup strategy in place | Recommended | PostgreSQL backup schedule | +| Monitoring configured | Recommended | Health check endpoint polling | + +{: .alert.alert-danger :} +Never run with `ADCP_AUTH_TEST_MODE=true` in production. This disables authentication and allows unrestricted access. + +## Further Reading + +- [Quick Start](/agents/salesagent/getting-started/quickstart.html) -- Docker deployment guide +- [Configuration Reference](/agents/salesagent/getting-started/configuration.html) -- All environment variables +- [Buy-Side Integration](/agents/salesagent/getting-started/buy-side-integration.html) -- Connect AI buying agents +- [AdCP Quick Start](https://docs.adcontextprotocol.org/docs/quickstart) -- Protocol-level getting started guide diff --git a/agents/salesagent/getting-started/quickstart.md b/agents/salesagent/getting-started/quickstart.md new file mode 100644 index 0000000000..aecfc4dffe --- /dev/null +++ b/agents/salesagent/getting-started/quickstart.md @@ -0,0 +1,197 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Getting Started - Quick Start +description: Docker quickstart guide for deploying the Prebid Sales Agent in minutes +sidebarType: 10 +--- + +# Quick Start +{: .no_toc} + +Get the Prebid Sales Agent running locally in minutes using Docker. This guide covers two deployment options, first-time setup, and verifying your installation. + +- TOC +{:toc} + +## Prerequisites + +Before you begin, ensure you have the following installed: + +{: .table .table-bordered .table-striped } +| Requirement | Minimum Version | Notes | +|-------------|----------------|-------| +| Docker | 20.10+ | [Install Docker](https://docs.docker.com/get-docker/) | +| Docker Compose | 2.0+ | Included with Docker Desktop | + +{: .alert.alert-info :} +No Python, Node.js, or other runtime is needed -- everything runs inside Docker containers. + +## Option 1: Docker Run (Existing Database) + +If you already have a PostgreSQL database, you can run the Sales Agent container directly: + +```bash +docker run -d \ + --name salesagent \ + -p 8000:8000 \ + -e DATABASE_URL=postgresql://user:pass@host:5432/salesagent \ + -e ENCRYPTION_KEY=your-encryption-key \ + -e CREATE_DEMO_TENANT=true \ + -e ADCP_AUTH_TEST_MODE=true \ + ghcr.io/prebid/salesagent:latest +``` + +{: .alert.alert-warning :} +`ADCP_AUTH_TEST_MODE=true` enables test credentials and should never be used in production. See [Configuration](/agents/salesagent/getting-started/configuration.html) for production settings. + +## Option 2: Clone and Run (Includes PostgreSQL) + +This is the recommended approach for local development and evaluation. It starts both the Sales Agent and a bundled PostgreSQL database: + +```bash +git clone https://github.com/prebid/salesagent.git +cd salesagent +docker compose up -d +``` + +Docker Compose will start: +- **PostgreSQL** on port 5432 (internal) +- **Sales Agent** on port 8000 (exposed) + +Database migrations run automatically on first startup. + +## First-Time Setup + +### Setup Mode + +On first launch with no tenants configured, the Sales Agent enters **Setup Mode**. This provides a guided workflow to create your first publisher tenant. + +1. Navigate to `http://localhost:8000/admin` +2. The setup wizard will walk you through creating your first tenant +3. Configure your publisher name and domain + +### Test Credentials + +When `ADCP_AUTH_TEST_MODE=true` is set, the following test credentials are available: + +{: .table .table-bordered .table-striped } +| Credential | Value | Purpose | +|------------|-------|---------| +| Admin UI password | `test123` | Access the admin dashboard | +| MCP auth token | `test-token` | Authenticate MCP tool calls | +| A2A bearer token | `test-token` | Authenticate A2A requests | + +### Local Testing with Demo Tenant + +For quick evaluation, set `CREATE_DEMO_TENANT=true` to automatically create a demo publisher tenant with sample products and advertisers: + +```bash +CREATE_DEMO_TENANT=true docker compose up -d +``` + +This populates the system with realistic demo data so you can immediately test the MCP and A2A interfaces without manual configuration. + +## Verify Your Installation + +After startup, verify that all services are running: + +{: .table .table-bordered .table-striped } +| Service | URL | Expected Result | +|---------|-----|-----------------| +| Admin UI | `http://localhost:8000/admin` | Login page or setup wizard | +| MCP Server | `http://localhost:8000/mcp/` | MCP endpoint (requires auth) | +| A2A Server | `http://localhost:8000/a2a` | A2A endpoint (requires auth) | +| Health Check | `http://localhost:8000/health` | `{"status": "ok"}` | +| Agent Card | `http://localhost:8000/.well-known/agent.json` | JSON agent descriptor | + +### Test with CLI Commands + +Use the `adcp` CLI tool to verify MCP connectivity: + +```bash +# List available tools +uvx adcp http://localhost:8000/mcp/ --auth test-token list_tools + +# Search for products +uvx adcp http://localhost:8000/mcp/ --auth test-token get_products '{"brief":"video"}' + +# Check capabilities +uvx adcp http://localhost:8000/mcp/ --auth test-token get_adcp_capabilities +``` + +## Connecting an AI Agent + +Once your Sales Agent is running, you can connect an AI buying agent using the MCP Python client: + +```python +from fastmcp import Client +from fastmcp.client.transports import StreamableHttpTransport + +transport = StreamableHttpTransport( + "http://localhost:8000/mcp/", + headers={"x-adcp-auth": "test-token"} +) + +async with Client(transport) as client: + # Discover available tools + tools = await client.list_tools() + print(f"Available tools: {[t.name for t in tools]}") + + # Get products + result = await client.call_tool("get_products", {"brief": "homepage banner"}) + print(result) +``` + +{: .alert.alert-info :} +For complete buy-side integration details, see [Buy-Side Integration](/agents/salesagent/getting-started/buy-side-integration.html). + +## Common Docker Commands + +{: .table .table-bordered .table-striped } +| Command | Description | +|---------|-------------| +| `docker compose up -d` | Start all services in background | +| `docker compose down` | Stop all services | +| `docker compose logs -f` | Follow live logs | +| `docker compose logs salesagent` | View Sales Agent logs only | +| `docker compose restart salesagent` | Restart the Sales Agent | +| `docker compose down -v` | Stop and remove all data volumes | +| `docker compose pull` | Pull latest images | + +## Troubleshooting + +### Container fails to start + +Check the logs for error details: + +```bash +docker compose logs salesagent +``` + +Common issues: +- **Database connection refused** -- Ensure PostgreSQL is running and `DATABASE_URL` is correct +- **Port 8000 already in use** -- Stop the conflicting service or change the port mapping in `docker-compose.yml` +- **Migration errors** -- Try removing volumes and restarting: `docker compose down -v && docker compose up -d` + +### MCP endpoint returns 401 + +Ensure you are passing the auth token in the correct header: +- MCP: `x-adcp-auth: test-token` +- A2A: `Authorization: Bearer test-token` + +### Health check returns unhealthy + +The health endpoint checks database connectivity. Verify your PostgreSQL instance is accessible: + +```bash +docker compose exec salesagent python -c "from app.database import engine; print('DB OK')" +``` + +{: .alert.alert-danger :} +If you encounter persistent issues, check the [GitHub Issues](https://github.com/prebid/salesagent/issues) page or open a new issue with your Docker logs. + +## Next Steps + +- [Publisher Onboarding](/agents/salesagent/getting-started/publisher-onboarding.html) -- Configure SSO, products, and advertisers +- [Configuration Reference](/agents/salesagent/getting-started/configuration.html) -- Full environment variable reference +- [Deployment Guide](/agents/salesagent/deployment/single-tenant.html) -- Production deployment with Nginx and HTTPS diff --git a/agents/salesagent/glossary.md b/agents/salesagent/glossary.md new file mode 100644 index 0000000000..0b93827700 --- /dev/null +++ b/agents/salesagent/glossary.md @@ -0,0 +1,58 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Glossary +description: Definitions of key terms used in the Prebid Sales Agent documentation +sidebarType: 10 +--- + +# Glossary +{: .no_toc} + +Definitions of key terms used throughout the Prebid Sales Agent documentation, organized into Sales Agent-specific concepts and AdCP protocol terms. + +- TOC +{:toc} + +## Sales Agent Terms + +These terms are specific to the Prebid Sales Agent implementation. + +{: .table .table-bordered .table-striped } +| Term | Definition | +|------|------------| +| **ToolContext** | Dependency-injected object providing tenant context, database session, adapter instance, and authentication state to MCP tools. Populated via `Depends(get_tool_context)` on each tool invocation. | +| **Adapter** | Pluggable integration layer connecting the Sales Agent to an ad server. Each adapter translates AdCP operations into platform-specific API calls. Current adapters include Google Ad Manager and a mock adapter for testing. | +| **Tenant** | An isolated publisher instance within a multi-tenant deployment. Each tenant has its own products, advertisers, configuration, and data. Tenant isolation is enforced at the database and authentication layers. | +| **Principal** | An authenticated entity (AI buying agent or human user) that interacts with the Sales Agent via MCP or A2A. Principals are identified by their access token and granted scopes. | +| **Workflow** | Human-in-the-loop approval process for media buys and creatives. When a buying agent creates or modifies a campaign, the operation may be queued for publisher review in the Admin UI before taking effect in the ad server. | +| **Setup Mode** | Initial state of a new deployment before any tenants are configured. The Admin UI presents a guided setup wizard that walks the publisher through tenant creation, adapter configuration, and product catalog setup. | +| **Demo Tenant** | Auto-generated publisher tenant with sample products, advertisers, and configuration. Created when the environment variable `CREATE_DEMO_TENANT=true` is set. Useful for testing and evaluation without manual setup. | + +## AdCP Protocol Terms + +These terms are defined by the [Ad Context Protocol (AdCP)](https://adcontextprotocol.org) specification. The definitions below are brief summaries; see the [AdCP Glossary](https://docs.adcontextprotocol.org/docs/reference/glossary) for canonical definitions. + +{: .table .table-bordered .table-striped } +| Term | Definition | +|------|------------| +| **Brief** | A natural language description of an advertiser's campaign goals, used by buying agents to search for matching products. Passed to `get_products` as the `brief` parameter. See [AdCP Glossary](https://docs.adcontextprotocol.org/docs/reference/glossary). | +| **Product** | An advertising offering from a publisher, including placement details, pricing options, creative format requirements, and targeting capabilities. See [AdCP Glossary](https://docs.adcontextprotocol.org/docs/reference/glossary). | +| **Package** | A bundled collection of products offered as a single purchasable unit, often with combined pricing or volume discounts. See [AdCP Glossary](https://docs.adcontextprotocol.org/docs/reference/glossary). | +| **Media Buy** | A committed purchase of advertising inventory, specifying products, budget, flight dates, and targeting parameters. The central transactional object in AdCP. See [AdCP Glossary](https://docs.adcontextprotocol.org/docs/reference/glossary). | +| **Creative** | An advertising asset (image, video, HTML) that is associated with a media buy and rendered to users. Must conform to the format specifications of the purchased products. See [AdCP Glossary](https://docs.adcontextprotocol.org/docs/reference/glossary). | +| **Format ID** | A standardized identifier for a creative format (e.g., display banner dimensions, video duration). Returned by `list_creative_formats` and referenced in product and creative objects. See [AdCP Glossary](https://docs.adcontextprotocol.org/docs/reference/glossary). | +| **Pricing Option** | A pricing model attached to a product, specifying the rate type (CPM, CPC, flat rate), currency, and amount. Multiple pricing options may be available for a single product. See [AdCP Glossary](https://docs.adcontextprotocol.org/docs/reference/glossary). | +| **Brand Manifest** | A structured description of a brand's identity, category, audience, and compliance requirements. Provided by buying agents to enable personalized product recommendations. See [AdCP Glossary](https://docs.adcontextprotocol.org/docs/reference/glossary). | +| **Targeting Overlay** | Additional targeting constraints applied to a media buy beyond the defaults inherited from the product. Allows buyers to narrow delivery by geography, audience segment, or other dimensions. See [AdCP Glossary](https://docs.adcontextprotocol.org/docs/reference/glossary). | +| **Orchestrator** | A coordinating agent that manages workflows across multiple sales agents on behalf of a buyer. Orchestrators use A2A protocol for multi-agent communication. See [AdCP Glossary](https://docs.adcontextprotocol.org/docs/reference/glossary). | +| **Sales Agent** | A server-side agent representing a publisher that exposes advertising inventory to AI buying agents. The Prebid Sales Agent is the reference implementation. See [AdCP Glossary](https://docs.adcontextprotocol.org/docs/reference/glossary). | + +{: .alert.alert-info :} +For the canonical definitions of AdCP protocol terms, see the [AdCP Glossary](https://docs.adcontextprotocol.org/docs/reference/glossary). + +## Further Reading + +- [Overview](/agents/salesagent/overview.html) -- Introduction to the Prebid Sales Agent +- [Architecture & Protocols](/agents/salesagent/architecture.html) -- System design and protocol details +- [Tool Reference](/agents/salesagent/tools/tool-reference.html) -- Complete catalog of MCP tools +- [AdCP Introduction](https://docs.adcontextprotocol.org/docs/intro) -- Protocol specification and concepts diff --git a/agents/salesagent/integrations/adcp-ecosystem.md b/agents/salesagent/integrations/adcp-ecosystem.md new file mode 100644 index 0000000000..9eddac103a --- /dev/null +++ b/agents/salesagent/integrations/adcp-ecosystem.md @@ -0,0 +1,139 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Integrations - AdCP Ecosystem Integration +description: How the Prebid Sales Agent interacts with other AdCP agents and protocols +sidebarType: 10 +--- + +# AdCP Ecosystem Integration +{: .no_toc} + +The Ad Context Protocol defines multiple agent types that work together in AI-driven advertising workflows. This page describes how the Prebid Sales Agent (Publisher role) interacts with buying agents, signal agents, creative agents, governance agents, and orchestration platforms. + +- TOC +{:toc} + +## Agent Interaction Model + +```text + ┌─────────────────────┐ + │ Orchestration │ + │ Platform │ + └──────────┬──────────┘ + │ A2A + ┌────────────────────┼────────────────────┐ + │ │ │ +┌─────────▼─────────┐ ┌──────▼──────────┐ ┌──────▼──────────┐ +│ Signal Agent │ │ Buying Agent │ │ Creative Agent │ +│ (audiences) │ │ (media buys) │ │ (assets) │ +└─────────┬─────────┘ └──────┬──────────┘ └──────┬──────────┘ + │ │ MCP / A2A │ + │ ┌───────▼───────┐ │ + └───────────▶│ Sales Agent │◀────────────┘ + │ (Publisher) │ + └───────┬───────┘ + │ + ┌───────▼───────┐ + │ Ad Server │ + │ (GAM, etc.) │ + └───────────────┘ +``` + +## Buying Agents + +Buying agents discover inventory and execute media buys on behalf of advertisers. This is the primary interaction pattern for the Sales Agent. + +**Interaction flow:** + +1. `get_adcp_capabilities` -- Discover what the Sales Agent supports +2. `get_products` -- Search inventory with a natural language brief +3. `list_creative_formats` -- Check format requirements +4. `create_media_buy` -- Submit a campaign +5. `sync_creatives` -- Upload creative assets +6. `get_media_buy_delivery` -- Monitor performance + +Buying agents connect via MCP (for direct AI assistant integration) or A2A (for multi-agent workflows). Both protocols expose identical tool functionality. + +{: .alert.alert-info :} +For a step-by-step integration guide, see [Buy-Side Integration](/agents/salesagent/getting-started/buy-side-integration.html). + +## Signal Agents + +Signal agents provide audience data and contextual signals that enrich product discovery and targeting. + +**How signals interact with the Sales Agent:** + +- Audience signals are passed as targeting parameters in `get_products` and `create_media_buy` +- The Sales Agent's AI-powered product search (via Gemini) uses these signals to improve matching +- Signal data does not flow through the Sales Agent directly -- buying agents aggregate signals before making requests + +{: .alert.alert-info :} +See the [Signals Protocol](https://docs.adcontextprotocol.org/docs/signals/overview) for the signal data specification. + +## Creative Agents + +Creative agents generate, optimize, and manage advertising assets. They interact with the Sales Agent through format negotiation and creative delivery. + +**Integration points:** + +- `list_creative_formats` -- Creative agents query format specifications (dimensions, file types, asset requirements) to generate compliant assets +- `sync_creatives` -- Deliver generated assets to the Sales Agent for trafficking to the ad server +- `list_creatives` -- Check the status of uploaded creatives (pending review, approved, rejected) + +The Sales Agent validates creative assets against format specifications before forwarding them to the ad server adapter. + +{: .alert.alert-info :} +See the [Creative Protocol](https://docs.adcontextprotocol.org/docs/creative) and [Creative Formats](https://docs.adcontextprotocol.org/docs/creative/formats) for format specifications and manifest structure. + +## Governance Agents + +Governance agents enforce content policies, brand safety rules, and property authorization across the advertising ecosystem. + +**Integration points:** + +- `get_adcp_capabilities` -- Returns content policy information when available +- `create_media_buy` -- Brand manifest validation against publisher content standards +- Content standards tools (planned) -- `get_content_standards`, `create_content_standards`, `update_content_standards`, `list_content_standards` + +{: .alert.alert-warning :} +Content standards tools are defined in the AdCP specification but not yet implemented in the Prebid Sales Agent. See [Content Standards Tools](/agents/salesagent/tools/content-standards.html) for status. + +{: .alert.alert-info :} +See the [Governance Protocol](https://docs.adcontextprotocol.org/docs/governance/overview) for content governance specifications. + +## Orchestration Platforms + +Orchestration platforms coordinate multi-agent workflows across multiple sales agents. They use the A2A protocol for agent-to-agent communication. + +**How orchestration works:** + +1. The orchestrator discovers sales agents via `/.well-known/agent.json` +2. It queries multiple sales agents' capabilities and inventory +3. It coordinates buying decisions across publishers +4. It manages the lifecycle of media buys across agents + +**A2A discovery:** The Sales Agent publishes an agent card at `/.well-known/agent.json` containing its identity, supported protocols, available tools, and authentication requirements. Orchestrators use this for automated discovery. + +{: .alert.alert-info :} +See the [Sponsored Intelligence](https://docs.adcontextprotocol.org/docs/sponsored-intelligence/overview) documentation for orchestration patterns. + +## Brand Protocol + +The Brand Protocol defines how advertiser identity and brand safety information flow through the AdCP ecosystem. + +**How it works with the Sales Agent:** + +- Buying agents include a `BrandManifest` in `create_media_buy` requests +- The manifest contains brand name, URL, logo, and category +- The Sales Agent uses this for compliance checks against publisher content standards +- Ad server adapters may use brand data for order naming and trafficking + +{: .alert.alert-info :} +See the [Brand Protocol / brand.json](https://docs.adcontextprotocol.org/docs/brand-protocol/brand-json) specification. + +## Further Reading + +- [Overview](/agents/salesagent/overview.html) -- Sales Agent role in the AdCP architecture +- [Protocols: MCP vs A2A](/agents/salesagent/protocols.html) -- Protocol comparison and usage guidance +- [AdCP Cross-Reference Index](/agents/salesagent/reference/adcp-cross-references.html) -- Links to all AdCP protocol documentation +- [AdCP Introduction](https://docs.adcontextprotocol.org/docs/intro) -- Protocol overview and concepts diff --git a/agents/salesagent/integrations/custom-adapter.md b/agents/salesagent/integrations/custom-adapter.md new file mode 100644 index 0000000000..d1493328a3 --- /dev/null +++ b/agents/salesagent/integrations/custom-adapter.md @@ -0,0 +1,430 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Integrations - Building a Custom Adapter +description: Guide to implementing a custom ad server adapter for the Prebid Sales Agent +sidebarType: 10 +--- + +# Building a Custom Adapter +{: .no_toc} + +The Sales Agent uses an adapter pattern to integrate with ad servers. This page documents the `AdServerAdapter` abstract base class and walks through the process of building a custom adapter for an ad server not already supported. + +- TOC +{:toc} + +## Adapter Pattern Overview + +Every ad server integration is a subclass of `AdServerAdapter`, located in `src/adapters/base.py`. The base class defines a set of abstract methods that the Sales Agent core calls to create campaigns, upload creatives, check delivery, and manage media buys. Your adapter translates these calls into the API operations of your ad server. + +```text +┌─────────────────────┐ ┌──────────────────────┐ ┌─────────────────┐ +│ Sales Agent Core │────▶│ AdServerAdapter │────▶│ Your Ad Server │ +│ (business logic) │ │ (abstract interface) │ │ (external API) │ +└─────────────────────┘ └──────────────────────┘ └─────────────────┘ + ▲ + │ + ┌────────┴────────┐ + │ YourAdapter() │ + │ (your impl) │ + └─────────────────┘ +``` + +The adapter is instantiated with the following constructor signature: + +```python +def __init__(self, config, principal, dry_run=False, creative_engine=None, tenant_id=None): +``` + +{: .table .table-bordered .table-striped } +| Parameter | Type | Description | +|-----------|------|-------------| +| `config` | `dict` | Adapter-specific configuration loaded from the database | +| `principal` | `Principal` | The authenticated principal (advertiser) making the request | +| `dry_run` | `bool` | When `True`, the adapter should simulate operations without making real API calls | +| `creative_engine` | `CreativeEngine` or `None` | Optional creative processing engine for format conversions | +| `tenant_id` | `str` or `None` | The tenant identifier for multi-tenant deployments | + +## Required Abstract Methods + +Your adapter must implement all seven of the following methods. The Sales Agent core calls these methods during the media buy lifecycle. + +### create_media_buy + +Creates a new media buy (campaign/order) in the ad server. + +```python +def create_media_buy(self, request, packages, start_time, end_time, package_pricing_info=None): +``` + +{: .table .table-bordered .table-striped } +| Parameter | Type | Description | +|-----------|------|-------------| +| `request` | `CreateMediaBuyRequest` | The media buy specification including advertiser, budget, and targeting | +| `packages` | `list` | Product packages to include in the media buy | +| `start_time` | `datetime` | Campaign start time | +| `end_time` | `datetime` | Campaign end time | +| `package_pricing_info` | `dict` or `None` | Optional pricing overrides per package | + +**Returns:** `CreateMediaBuyResponse` -- contains the media buy ID, line item IDs, and status. + +### add_creative_assets + +Uploads creative assets to the ad server and associates them with a media buy. + +```python +def add_creative_assets(self, media_buy_id, assets, today): +``` + +{: .table .table-bordered .table-striped } +| Parameter | Type | Description | +|-----------|------|-------------| +| `media_buy_id` | `str` | The media buy to associate creatives with | +| `assets` | `list` | Creative asset objects containing file data, dimensions, and format metadata | +| `today` | `date` | Current date for time-based logic | + +**Returns:** `list[AssetStatus]` -- status of each uploaded asset (success, pending review, or error). + +### associate_creatives + +Associates previously uploaded platform creatives with specific line items. + +```python +def associate_creatives(self, line_item_ids, platform_creative_ids): +``` + +{: .table .table-bordered .table-striped } +| Parameter | Type | Description | +|-----------|------|-------------| +| `line_item_ids` | `list` | Line item identifiers in the ad server | +| `platform_creative_ids` | `list` | Creative identifiers in the ad server | + +**Returns:** `list[dict]` -- association results for each line item / creative pair. + +### check_media_buy_status + +Retrieves the current status of a media buy from the ad server. + +```python +def check_media_buy_status(self, media_buy_id, today): +``` + +{: .table .table-bordered .table-striped } +| Parameter | Type | Description | +|-----------|------|-------------| +| `media_buy_id` | `str` | The media buy to check | +| `today` | `date` | Current date for time-based status calculations | + +**Returns:** `CheckMediaBuyStatusResponse` -- contains the overall status, per-line-item statuses, and any error messages. + +### get_media_buy_delivery + +Fetches delivery metrics (impressions, clicks, spend) for a media buy over a date range. + +```python +def get_media_buy_delivery(self, media_buy_id, date_range, today): +``` + +{: .table .table-bordered .table-striped } +| Parameter | Type | Description | +|-----------|------|-------------| +| `media_buy_id` | `str` | The media buy to query | +| `date_range` | `DateRange` | Start and end dates for the delivery report | +| `today` | `date` | Current date for relative calculations | + +**Returns:** `AdapterGetMediaBuyDeliveryResponse` -- delivery metrics including impressions, clicks, spend, and pacing data. + +### update_media_buy_performance_index + +Updates the performance index for packages within a media buy. The performance index is used by the Sales Agent to adjust pacing and optimization. + +```python +def update_media_buy_performance_index(self, media_buy_id, package_performance): +``` + +{: .table .table-bordered .table-striped } +| Parameter | Type | Description | +|-----------|------|-------------| +| `media_buy_id` | `str` | The media buy to update | +| `package_performance` | `dict` | Performance index values keyed by package ID | + +**Returns:** `bool` -- `True` if the update succeeded. + +### update_media_buy + +Modifies an existing media buy (pause, resume, cancel, adjust budget, etc.). + +```python +def update_media_buy(self, media_buy_id, buyer_ref, action, package_id, budget, today): +``` + +{: .table .table-bordered .table-striped } +| Parameter | Type | Description | +|-----------|------|-------------| +| `media_buy_id` | `str` | The media buy to modify | +| `buyer_ref` | `str` | Buyer reference identifier | +| `action` | `str` | The action to perform (e.g., `pause`, `resume`, `cancel`, `adjust_budget`) | +| `package_id` | `str` | The specific package/line item to target, if applicable | +| `budget` | `int` or `None` | New budget value in cents, if adjusting budget | +| `today` | `date` | Current date for time-based logic | + +**Returns:** `UpdateMediaBuyResponse` -- updated status and confirmation details. + +## Optional Override Methods + +The base class provides default implementations for these methods. Override them to expose additional capabilities. + +### get_supported_pricing_models + +Returns the set of pricing models your ad server supports. + +```python +def get_supported_pricing_models(self) -> set[str]: + # Default: {"cpm"} +``` + +Return a set of strings from the supported values: `cpm`, `vcpm`, `cpcv`, `cpp`, `cpc`, `cpv`, `flat_rate`. + +### get_targeting_capabilities + +Returns the targeting dimensions your ad server supports. + +```python +def get_targeting_capabilities(self) -> TargetingCapabilities: + # Default: geo countries only +``` + +The default implementation advertises country-level geographic targeting only. Override this to declare support for regions, DMAs, postal codes, or custom targeting dimensions. + +### get_packages_snapshot + +Returns near-real-time delivery snapshots for active packages. + +```python +def get_packages_snapshot(self): +``` + +Override this to provide live pacing data that the Sales Agent uses for in-flight optimization. + +### get_available_inventory + +Returns available inventory data for AI-driven product configuration. + +```python +def get_available_inventory(self): +``` + +Override this to allow the Sales Agent to query your ad server for available inventory when configuring products. + +### get_creative_formats + +Returns the creative formats your ad server accepts. + +```python +def get_creative_formats(self): +``` + +Override this to declare custom creative format support beyond the defaults. + +### validate_product_config + +Validates a product configuration against your ad server's constraints. + +```python +def validate_product_config(self): +``` + +Override this to add ad-server-specific validation when products are created or updated in the Admin UI. + +### get_config_ui_endpoint + +Returns a URL path for a custom configuration UI. + +```python +def get_config_ui_endpoint(self): +``` + +Override this if your adapter provides its own web-based configuration interface. + +### register_ui_routes + +Registers Flask routes for a custom adapter configuration UI. + +```python +def register_ui_routes(self, app): +``` + +Override this to mount custom Flask endpoints for adapter-specific settings pages. + +## Key Dataclasses + +### TargetingCapabilities + +Declares which targeting dimensions your adapter supports. + +{: .table .table-bordered .table-striped } +| Field | Type | Description | +|-------|------|-------------| +| `geo_countries` | `bool` | Country-level geographic targeting | +| `geo_regions` | `bool` | Region/state-level geographic targeting | +| `nielsen_dma` | `bool` | Nielsen DMA (Designated Market Area) targeting | +| `postal_codes_us` | `bool` | US postal code targeting | +| `postal_codes_ca` | `bool` | Canadian postal code targeting | +| `postal_codes_gb` | `bool` | UK postal code targeting | +| `postal_codes_de` | `bool` | German postal code targeting | +| `postal_codes_fr` | `bool` | French postal code targeting | +| `postal_codes_au` | `bool` | Australian postal code targeting | + +The `validate_geo_systems()` method on `TargetingCapabilities` checks that the declared targeting dimensions are internally consistent. + +### AdapterCapabilities + +Declares the overall feature set of your adapter. + +{: .table .table-bordered .table-striped } +| Field | Type | Description | +|-------|------|-------------| +| `supports_inventory_sync` | `bool` | Whether the adapter can sync inventory data from the ad server | +| `supports_custom_targeting` | `bool` | Whether the adapter supports key-value or custom targeting | +| `supports_geo_targeting` | `bool` | Whether the adapter supports geographic targeting | +| `supported_pricing_models` | `set[str]` | Set of supported pricing model strings | +| `supports_webhooks` | `bool` | Whether the adapter can receive webhook callbacks | +| `supports_realtime_reporting` | `bool` | Whether the adapter can provide real-time delivery data | + +### BaseConnectionConfig and BaseProductConfig + +Pydantic `BaseModel` subclasses for adapter configuration validation. Both are configured with `extra="forbid"` to reject unknown fields. + +- **`BaseConnectionConfig`** -- Extend this to define the connection parameters your adapter needs (API keys, network IDs, endpoint URLs). +- **`BaseProductConfig`** -- Extend this to define product-level configuration fields specific to your ad server. + +```python +from pydantic import BaseModel + +class MyConnectionConfig(BaseConnectionConfig): + api_key: str + endpoint_url: str + network_id: str + +class MyProductConfig(BaseProductConfig): + line_item_type: str = "standard" + priority: int = 8 +``` + +## Class Attributes + +Set these class-level attributes on your adapter subclass. + +{: .table .table-bordered .table-striped } +| Attribute | Type | Description | +|-----------|------|-------------| +| `adapter_name` | `str` | Unique identifier for the adapter (e.g., `"my_ad_server"`) | +| `default_channels` | `list` | Default advertising channels (e.g., `["display", "video"]`) | +| `capabilities` | `AdapterCapabilities` | Feature declaration for the adapter | +| `connection_config_class` | `type` | Pydantic model class for connection settings | +| `product_config_class` | `type` | Pydantic model class for product settings | + +## Helper Methods + +The base class provides utility methods available to all adapters. + +{: .table .table-bordered .table-striped } +| Method | Description | +|--------|-------------| +| `log()` | Returns a logger scoped to the adapter for structured logging | +| `get_adapter_id(principal)` | Resolves the adapter configuration ID for a given principal | +| `audit_logger` | Property that returns the `AuditLogger` instance for recording operations | + +## Skeleton Adapter Example + +The following skeleton shows the minimum structure for a custom adapter: + +```python +from src.adapters.base import ( + AdServerAdapter, + AdapterCapabilities, + BaseConnectionConfig, + BaseProductConfig, + TargetingCapabilities, +) + + +class MyConnectionConfig(BaseConnectionConfig): + api_key: str + endpoint_url: str + + +class MyProductConfig(BaseProductConfig): + line_item_type: str = "standard" + + +class MyAdServerAdapter(AdServerAdapter): + adapter_name = "my_ad_server" + default_channels = ["display", "video"] + capabilities = AdapterCapabilities( + supports_inventory_sync=False, + supports_custom_targeting=True, + supports_geo_targeting=True, + supported_pricing_models={"cpm", "cpc"}, + supports_webhooks=False, + supports_realtime_reporting=False, + ) + connection_config_class = MyConnectionConfig + product_config_class = MyProductConfig + + def create_media_buy(self, request, packages, start_time, end_time, package_pricing_info=None): + # Translate to your ad server's campaign/order creation API + order_id = self._call_api("POST", "/orders", { + "name": request.name, + "budget": request.budget_cents, + "start": start_time.isoformat(), + "end": end_time.isoformat(), + }) + self.audit_logger.log_operation( + operation="create_media_buy", + principal_name=self._principal.name, + principal_id=str(self._principal.id), + adapter_id=self.get_adapter_id(self._principal), + success=True, + details={"order_id": order_id}, + ) + return CreateMediaBuyResponse(media_buy_id=order_id, status="created") + + def add_creative_assets(self, media_buy_id, assets, today): + # Upload each asset to your ad server + ... + + def associate_creatives(self, line_item_ids, platform_creative_ids): + # Link creatives to line items in your ad server + ... + + def check_media_buy_status(self, media_buy_id, today): + # Query your ad server for campaign status + ... + + def get_media_buy_delivery(self, media_buy_id, date_range, today): + # Fetch delivery metrics from your ad server's reporting API + ... + + def update_media_buy_performance_index(self, media_buy_id, package_performance): + # Update performance indices on your ad server + ... + + def update_media_buy(self, media_buy_id, buyer_ref, action, package_id, budget, today): + # Modify campaign state (pause, resume, cancel, adjust budget) + ... + + def _call_api(self, method, path, payload): + """Internal helper to make authenticated API calls.""" + ... +``` + +{: .alert.alert-info :} +For a complete, working adapter implementation, see the [Mock Adapter Reference](/agents/salesagent/integrations/mock-adapter.html). The mock adapter implements every abstract and optional method and is a good starting point for a new integration. + +## Further Reading + +- [Google Ad Manager Integration](/agents/salesagent/integrations/gam.html) -- Production GAM adapter setup and configuration +- [Mock Adapter Reference](/agents/salesagent/integrations/mock-adapter.html) -- Complete reference adapter for testing +- [Source Architecture](/agents/salesagent/developers/source-architecture.html) -- Adapter source code organization and module layout +- [Architecture & Protocols](/agents/salesagent/architecture.html) -- System design and adapter pattern context diff --git a/agents/salesagent/integrations/gam.md b/agents/salesagent/integrations/gam.md new file mode 100644 index 0000000000..c9e2867939 --- /dev/null +++ b/agents/salesagent/integrations/gam.md @@ -0,0 +1,332 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Integrations - Google Ad Manager Integration +description: Complete guide to integrating the Prebid Sales Agent with Google Ad Manager including authentication, product mapping, and troubleshooting +sidebarType: 10 +--- + +# Google Ad Manager Integration +{: .no_toc} + +The Google Ad Manager (GAM) adapter connects the Prebid Sales Agent to your GAM network, translating AdCP operations into GAM API calls. This guide covers authentication setup, GCP provisioning, product-to-line-item mapping, and testing. + +- TOC +{:toc} + +## Overview + +The GAM adapter (`GAMAdapter`) is the production ad server integration for the Sales Agent. It implements the `AdServerAdapter` abstract interface and maps each AdCP operation to the corresponding Google Ad Manager API call. + +{: .table .table-bordered .table-striped } +| AdCP Operation | GAM API Mapping | Description | +|----------------|-----------------|-------------| +| `create_media_buy` | Create Order + Line Items | Creates a GAM Order with one or more Line Items based on the media buy specification | +| `update_media_buy` | Update Order / Line Item | Modifies flight dates, budget, targeting, or status on existing GAM entities | +| `get_media_buy_delivery` | Run ReportQuery | Fetches delivery metrics (impressions, clicks, spend) from the GAM Reporting API | +| `sync_creatives` | Create / Update Creative | Uploads creative assets to GAM and associates them with Line Items | +| `list_creatives` | Get Creatives by Statement | Queries GAM for creatives matching filter criteria | + +The adapter handles all GAM-specific concerns -- API authentication, rate limiting, pagination, error mapping, and retry logic -- so that the rest of the Sales Agent operates entirely in terms of AdCP abstractions. + +{: .alert.alert-info :} +The GAM adapter is one of several available adapters. For development and testing, the Mock adapter provides simulated responses without requiring GAM credentials. See the [Architecture & Protocols](/agents/salesagent/architecture.html) page for details on the adapter pattern. + +## Authentication Methods + +The GAM adapter supports two authentication methods. Choose based on your deployment context. + +{: .table .table-bordered .table-striped } +| Method | Use Case | Configuration | Security Level | +|--------|----------|---------------|----------------| +| **Service Account** (recommended) | Production deployments | JSON key uploaded via Admin UI | High -- no user interaction required, key stored encrypted | +| **OAuth 2.0 Client** | Development and testing | Environment variables | Medium -- requires initial user consent flow | + +### Service Account (Recommended for Production) + +Service accounts are the recommended authentication method for production. They provide non-interactive, server-to-server authentication with no manual consent flow. + +Service account credentials are uploaded and managed through the Admin UI at **Settings > Ad Server Configuration**. The JSON key file is encrypted at rest using the `ENCRYPTION_KEY` configured in your environment. + +### OAuth 2.0 Client (Development) + +For development environments, you can authenticate using OAuth 2.0 client credentials. This method requires an initial user consent flow to obtain a refresh token. + +Set the following environment variables: + +```bash +GAM_OAUTH_CLIENT_ID=your-client-id.apps.googleusercontent.com +GAM_OAUTH_CLIENT_SECRET=your-client-secret +GAM_OAUTH_REFRESH_TOKEN=your-refresh-token +GAM_NETWORK_CODE=12345678 +``` + +{: .alert.alert-warning :} +OAuth credentials in environment variables are suitable for development only. In production, use the service account method with credentials managed through the Admin UI. + +## Service Account Setup + +Follow these steps to provision a GCP service account and connect it to your GAM network. + +### Step 1: Create a GCP Project + +If you do not already have a GCP project for the Sales Agent: + +1. Go to the [Google Cloud Console](https://console.cloud.google.com/) +2. Click **Select a project** > **New Project** +3. Enter a project name (e.g., `salesagent-production`) +4. Select your billing account and organization +5. Click **Create** + +### Step 2: Enable the Google Ad Manager API + +1. In the GCP Console, navigate to **APIs & Services > Library** +2. Search for "Google Ad Manager API" (also known as the "Ad Manager API") +3. Click **Enable** + +{: .alert.alert-info :} +The API may take a few minutes to fully activate after enabling. + +### Step 3: Create a Service Account + +1. Navigate to **IAM & Admin > Service Accounts** +2. Click **Create Service Account** +3. Enter a name (e.g., `salesagent-gam`) and description +4. Click **Create and Continue** +5. Skip the optional role grants (the service account needs GAM-level permissions, not GCP-level) +6. Click **Done** + +### Step 4: Generate a JSON Key + +1. Click on your newly created service account +2. Go to the **Keys** tab +3. Click **Add Key > Create new key** +4. Select **JSON** format +5. Click **Create** -- the key file downloads automatically + +{: .alert.alert-warning :} +Store the JSON key file securely. It provides full API access to your GAM network. Never commit it to version control. + +### Step 5: Link the Service Account to GAM + +1. Log in to your [Google Ad Manager](https://admanager.google.com/) account +2. Navigate to **Admin > Global settings > Network settings** +3. Under **API access**, ensure API access is enabled +4. Go to **Admin > Global settings > Service account users** +5. Add the service account email (e.g., `salesagent-gam@your-project.iam.gserviceaccount.com`) +6. Assign the appropriate role (typically **Administrator** or a custom role with order/line item/creative/report permissions) + +### Step 6: Upload to the Admin UI + +1. Open the Sales Agent Admin UI at `https://your-domain.com/admin` +2. Navigate to **Settings > Ad Server Configuration** +3. Select **Google Ad Manager** as the adapter type +4. Enter your GAM Network Code +5. Upload the JSON key file +6. Click **Save and Test Connection** + +The Admin UI encrypts the key file using your `ENCRYPTION_KEY` before storing it in the database. + +### Step 7: Verify the Connection + +After saving, the Admin UI runs a connectivity test against the GAM API. A successful test confirms: + +- The service account credentials are valid +- The GAM API is enabled on your GCP project +- The service account has been added as a user in your GAM network +- The network code matches an accessible network + +```bash +# You can also verify via CLI +uvx adcp http://localhost:8000/mcp/ --auth your-token get_adcp_capabilities +``` + +The response should show `ad_server_type: "google_ad_manager"` with a connected status. + +## Product Configuration + +Products in the Sales Agent map to GAM line items when a media buy is created. This section explains how products translate into GAM entities and how to configure targeting templates. + +### Product-to-GAM Mapping + +When an AI agent creates a media buy for a product, the GAM adapter creates the following entities: + +{: .table .table-bordered .table-striped } +| AdCP Entity | GAM Entity | Relationship | +|-------------|------------|--------------| +| Media Buy | Order | One media buy creates one GAM Order | +| Product (within media buy) | Line Item | Each product in the buy becomes a Line Item under the Order | +| Creative | Creative | Creatives are uploaded and associated with Line Items | +| Targeting | Line Item Targeting | Targeting from the product and media buy is merged into Line Item targeting criteria | + +### Configuring Targeting Templates + +Each product can define a targeting template that specifies which GAM targeting criteria to apply when the product is purchased. Configure these in the Admin UI under **Products > Targeting**. + +{: .table .table-bordered .table-striped } +| Targeting Type | GAM Equivalent | Example Value | +|----------------|---------------|---------------| +| `geo_countries` | Geographic targeting (countries) | `["US", "GB", "CA"]` | +| `geo_regions` | Geographic targeting (regions/states) | `["US-CA", "US-NY"]` | +| `geo_metros` | DMA targeting | `["501", "803"]` (Nielsen DMA codes) | +| `custom_targeting` | Key-value targeting | `{"section": ["sports", "news"]}` | +| `ad_unit_ids` | Inventory targeting (ad units) | `["/12345/homepage/leaderboard"]` | +| `placement_ids` | Inventory targeting (placements) | `["456789"]` | + +### Format Mapping + +The adapter maps AdCP creative format categories to GAM creative types: + +{: .table .table-bordered .table-striped } +| AdCP Format Category | GAM Creative Type | Notes | +|---------------------|-------------------|-------| +| `display` | `ImageCreative` / `ThirdPartyCreative` | Standard IAB display sizes | +| `video` | `VideoCreative` / `VastRedirectCreative` | VAST-compliant video | +| `native` | `NativeCreative` | Native style templates | +| `audio` | `AudioCreative` | DAAST-compliant audio | + +### Pricing Model Mapping + +{: .table .table-bordered .table-striped } +| AdCP Pricing Model | GAM Cost Type | GAM Rate Type | +|-------------------|---------------|---------------| +| `cpm` | `CPM` | Standard | +| `vcpm` | `VCPM` | Viewable | +| `cpc` | `CPC` | Click-based | +| `flat_rate` | `CPD` | Per-day flat rate | +| `cpcv` | `CPM` (with viewability target) | Custom | + +## Testing + +Before going live with a GAM integration, validate your setup thoroughly. + +### Testing with the Mock Adapter + +Start by testing your product configuration and media buy workflow using the mock adapter: + +```bash +# Start with mock adapter (no GAM credentials needed) +docker compose up -d + +# Create a test media buy +uvx adcp http://localhost:8000/mcp/ --auth test-token create_media_buy '{ + "product_id": "your-product-id", + "advertiser_id": "your-advertiser-id", + "budget_cents": 50000, + "start_date": "2025-04-01", + "end_date": "2025-04-30", + "name": "Test Campaign" +}' +``` + +### Testing with GAM Test Network + +Google provides test networks for development. To test against a real GAM API without affecting production: + +1. Request a [GAM test network](https://developers.google.com/ad-manager/api/start#test_network) from Google +2. Configure the Sales Agent with your test network credentials +3. Run the full media buy lifecycle: + +```bash +# Discover products +uvx adcp http://localhost:8000/mcp/ --auth your-token get_products '{"brief":"all"}' + +# Create a media buy +uvx adcp http://localhost:8000/mcp/ --auth your-token create_media_buy '{ + "product_id": "prod_123", + "advertiser_id": "adv_456", + "budget_cents": 100000, + "start_date": "2025-05-01", + "end_date": "2025-05-31", + "name": "GAM Integration Test" +}' + +# Check delivery (may take time to populate in test network) +uvx adcp http://localhost:8000/mcp/ --auth your-token get_media_buy_delivery '{ + "media_buy_id": "mb_789" +}' +``` + +### Integration Test Checklist + +{: .table .table-bordered .table-striped } +| Test | Expected Result | Notes | +|------|----------------|-------| +| Connection test | Success in Admin UI | Validates credentials and API access | +| `get_adcp_capabilities` | Shows `google_ad_manager` adapter | Confirms adapter is loaded | +| `create_media_buy` | Returns `media_buy_id` + GAM order ID | Check GAM console for created Order | +| `sync_creatives` | Returns creative IDs | Check GAM console for uploaded Creatives | +| `get_media_buy_delivery` | Returns delivery metrics | May take 24h in test networks for data | +| `update_media_buy` (pause) | Status changes to `paused` | Verify Line Item is paused in GAM | + +## Troubleshooting + +### GAM API Errors + +{: .table .table-bordered .table-striped } +| Error | Cause | Resolution | +|-------|-------|------------| +| `AuthenticationError` | Invalid or expired credentials | Re-upload service account key or refresh OAuth token | +| `PermissionDenied` | Service account lacks GAM permissions | Add the service account as a user in GAM with appropriate role | +| `ApiVersionError` | Unsupported API version | Update the Sales Agent to the latest version | +| `NOT_FOUND` (Network) | Incorrect network code | Verify `GAM_NETWORK_CODE` matches your GAM network | +| `ServerError` | Transient GAM API issue | The adapter retries automatically; check again after a few minutes | + +### Rate Limiting + +The GAM API enforces rate limits per network. The adapter includes automatic retry with exponential backoff for rate-limited requests. + +{: .table .table-bordered .table-striped } +| Limit Type | Default Limit | Adapter Behavior | +|------------|--------------|------------------| +| Requests per second | Varies by network | Automatic throttling with backoff | +| Daily quota | Varies by account tier | Logged as warning when approaching 80% | +| Reporting queries | Varies | Queued and batched to stay within limits | + +If you consistently hit rate limits, consider: + +- Reducing the frequency of `get_media_buy_delivery` calls +- Batching creative sync operations +- Contacting Google to increase your API quota + +### Permission Issues + +If the adapter authenticates successfully but operations fail: + +1. Verify the service account has the correct role in GAM (**Admin > Service account users**) +2. Ensure the role includes permissions for: Orders, Line Items, Creatives, Reports +3. Check that the network code in your configuration matches the network where the service account is added + +### OAuth Token Errors + +For OAuth-based authentication (development): + +{: .table .table-bordered .table-striped } +| Error | Cause | Resolution | +|-------|-------|------------| +| `invalid_grant` | Refresh token expired or revoked | Re-run the OAuth consent flow to obtain a new refresh token | +| `invalid_client` | Wrong client ID or secret | Verify `GAM_OAUTH_CLIENT_ID` and `GAM_OAUTH_CLIENT_SECRET` | +| `access_denied` | User did not grant required scopes | Re-authorize with the `https://www.googleapis.com/auth/dfp` scope | + +### Common Issues + +**Order creation fails with targeting error**: Ensure the ad unit IDs in your product targeting templates exist in your GAM network. Ad unit paths are case-sensitive and must match exactly. + +**Creatives rejected by GAM**: Check that the creative dimensions match the Line Item's expected sizes. GAM enforces strict size validation for display creatives. + +**Delivery data returns empty**: GAM reporting data can take up to 24 hours to appear. For real-time approximate data, the adapter falls back to Line Item delivery indicators when available. + +**Connection test passes but operations fail**: This usually indicates the service account has API access but lacks the specific GAM role permissions needed for order/line item management. Check the service account's role in GAM Admin. + +## Custom Adapters for Other Ad Servers + +If you use an ad server other than Google Ad Manager, you can create a custom adapter by implementing the `AdServerAdapter` abstract base class. The adapter pattern ensures that all business logic remains ad-server-agnostic. + +For details on the adapter interface and implementation guidelines, see [Source Architecture](/agents/salesagent/developers/source-architecture.html). + +## Further Reading + +- [Architecture & Protocols](/agents/salesagent/architecture.html) -- Adapter pattern and system design +- [Publisher Onboarding](/agents/salesagent/getting-started/publisher-onboarding.html) -- End-to-end publisher setup +- [Source Architecture](/agents/salesagent/developers/source-architecture.html) -- Adapter source code organization +- [Tool Reference](/agents/salesagent/tools/tool-reference.html) -- Complete MCP tool catalog +- [Google Ad Manager API Reference](https://developers.google.com/ad-manager/api/reference) -- Official GAM API documentation diff --git a/agents/salesagent/integrations/mock-adapter.md b/agents/salesagent/integrations/mock-adapter.md new file mode 100644 index 0000000000..dc7a4e6c54 --- /dev/null +++ b/agents/salesagent/integrations/mock-adapter.md @@ -0,0 +1,246 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Integrations - Mock Adapter Reference +description: Reference for the mock ad server adapter used for testing and development +sidebarType: 10 +--- + +# Mock Adapter Reference +{: .no_toc} + +The mock adapter (`MockAdServer`) is a simulated ad server integration for testing, evaluation, and CI/CD pipelines. It implements the full `AdServerAdapter` interface without requiring a real ad server. + +- TOC +{:toc} + +## Purpose + +The mock adapter serves three primary use cases: + +1. **Evaluation** -- Run the Sales Agent end-to-end without provisioning ad server credentials. Evaluate MCP tool calls, A2A task flows, and Admin UI features with simulated delivery data. +2. **Development** -- Build and test new features against a predictable, controllable ad server simulation. No network calls, no rate limits, no API quotas. +3. **CI/CD** -- Run automated integration tests in pipelines where real ad server access is unavailable or impractical. + +The mock adapter is a drop-in replacement for any production adapter. All Sales Agent features -- media buy creation, creative management, delivery reporting, and campaign lifecycle operations -- work identically with the mock adapter. + +## Supported Channels and Capabilities + +The `MockAdServer` class implements `AdServerAdapter` and declares support for a broad set of channels and pricing models. + +**Supported channels:** + +{: .table .table-bordered .table-striped } +| Channel | Description | +|---------|-------------| +| `display` | Standard IAB display advertising | +| `olv` | Online video (pre-roll, mid-roll, out-stream) | +| `streaming_audio` | Streaming audio ad insertion | +| `social` | Social media advertising placements | + +**Supported pricing models:** + +{: .table .table-bordered .table-striped } +| Model | Description | +|-------|-------------| +| `cpm` | Cost per mille (thousand impressions) | +| `vcpm` | Viewable cost per mille | +| `cpcv` | Cost per completed view | +| `cpp` | Cost per point | +| `cpc` | Cost per click | +| `cpv` | Cost per view | +| `flat_rate` | Flat rate (fixed cost for a period) | + +All targeting dimensions are supported, including geographic (country, region, DMA, postal code), custom key-value targeting, and inventory targeting. + +## Product-Level Simulation Parameters + +Each product configured for the mock adapter accepts simulation parameters that control how delivery data is generated. + +{: .table .table-bordered .table-striped } +| Parameter | Default | Description | +|-----------|---------|-------------| +| `daily_impressions` | 10,000 | Simulated daily impression volume | +| `fill_rate` | 85% | Percentage of available inventory filled | +| `ctr` | 2% | Simulated click-through rate | +| `viewability` | 65% | Simulated viewability rate | +| `scenario` | `normal` | Simulation scenario (see below) | + +### Simulation Scenarios + +The `scenario` parameter controls the overall behavior of the mock adapter for a given product. + +{: .table .table-bordered .table-striped } +| Scenario | Behavior | +|----------|----------| +| `normal` | Standard delivery pacing with minor daily variance | +| `high_demand` | Accelerated delivery with higher fill rates and faster budget consumption | +| `degraded` | Reduced delivery with intermittent errors and lower fill rates | +| `outage` | Simulates an ad server outage -- all operations return errors after initial creation | + +Set the scenario in the product configuration through the Admin UI or during tenant setup. + +## Delivery Simulation + +The mock adapter generates time-accelerated campaign delivery data that simulates realistic pacing behavior. + +Key behaviors: + +- **Time acceleration** -- Delivery data accumulates faster than real time, allowing a 30-day campaign to produce meaningful metrics within minutes. +- **Pacing curves** -- Delivery follows configurable pacing curves (even, front-loaded, or back-loaded) to simulate real campaign behavior. +- **Daily variance** -- Random variance is applied to daily metrics to produce realistic-looking delivery reports. +- **Webhook notifications** -- The mock adapter fires webhook callbacks at configurable delivery milestones (25%, 50%, 75%, 100% pacing). + +## Human-in-the-Loop (HITL) Simulation + +The mock adapter can simulate human approval workflows, which is useful for testing campaigns that require manual review before going live. + +### HITL Modes + +{: .table .table-bordered .table-striped } +| Mode | Behavior | +|------|----------| +| **Sync** | Operations return after a configurable delay, simulating a human reviewer responding in real time. The delay is configurable per operation. | +| **Async** | Operations immediately return a `pending` status. A background process completes the approval after a configurable interval, triggering a webhook notification. | +| **Mixed** | Per-operation overrides allow some operations to use sync mode and others to use async mode. | + +### Approval Simulation + +The mock adapter simulates approval decisions with a configurable success rate. For example, setting the approval rate to 90% means roughly 1 in 10 submissions will be rejected. + +{: .table .table-bordered .table-striped } +| Configuration Key | Default | Description | +|-------------------|---------|-------------| +| `approval_rate` | 100% | Percentage of submissions that are approved | +| `sync_delay_seconds` | 2 | Delay before sync mode operations return | +| `async_completion_seconds` | 10 | Delay before async mode operations complete | + +### Configuration + +HITL simulation is configured via the `platform_mappings.mock.hitl_config` section in the tenant configuration: + +```json +{ + "platform_mappings": { + "mock": { + "hitl_config": { + "mode": "async", + "approval_rate": 90, + "sync_delay_seconds": 3, + "async_completion_seconds": 15, + "per_operation_overrides": { + "create_media_buy": {"mode": "sync"}, + "add_creative_assets": {"mode": "async", "approval_rate": 80} + } + } + } + } +} +``` + +## Interface Methods + +The mock adapter implements the full `AdServerAdapter` interface. + +### Required Methods + +{: .table .table-bordered .table-striped } +| Method | Mock Behavior | +|--------|---------------| +| `create_media_buy` | Creates a simulated order with generated IDs. Respects HITL mode if configured. | +| `add_creative_assets` | Accepts assets and returns success statuses. Simulates format validation. | +| `associate_creatives` | Records associations in memory. Returns success for all pairs. | +| `check_media_buy_status` | Returns status based on campaign dates and the configured scenario. | +| `get_media_buy_delivery` | Generates simulated delivery data using the product simulation parameters. | +| `update_media_buy` | Processes pause, resume, cancel, and budget adjustment actions. | +| `update_media_buy_performance_index` | Accepts performance index values and adjusts simulated pacing. | + +### Optional Methods + +{: .table .table-bordered .table-striped } +| Method | Mock Behavior | +|--------|---------------| +| `get_supported_pricing_models` | Returns all seven pricing models. | +| `get_targeting_capabilities` | Returns all targeting dimensions as supported. | +| `validate_product_config` | Validates simulation parameters (e.g., `fill_rate` between 0 and 100). | + +## Setup + +Set up a tenant with the mock adapter using the setup script: + +```bash +docker-compose exec adcp-server python -m scripts.setup.setup_tenant "Test Publisher" --adapter mock +``` + +This creates a tenant with: + +- The mock adapter configured as the ad server integration +- A default set of products across all supported channels +- A test principal with an API token printed to the console + +{: .alert.alert-info :} +The mock adapter requires no external credentials or API keys. It works immediately after tenant setup. + +## Testing Examples + +### MCP Client + +Use the `uvx` CLI to interact with the mock adapter through MCP: + +```bash +# Discover available products +uvx adcp http://localhost:8000/mcp/ --auth test-token get_products '{"brief":"all"}' + +# Create a media buy +uvx adcp http://localhost:8000/mcp/ --auth test-token create_media_buy '{ + "product_id": "prod_display_001", + "advertiser_id": "adv_test", + "budget_cents": 50000, + "start_date": "2025-04-01", + "end_date": "2025-04-30", + "name": "Mock Test Campaign" +}' + +# Check delivery +uvx adcp http://localhost:8000/mcp/ --auth test-token get_media_buy_delivery '{ + "media_buy_id": "mb_mock_001" +}' +``` + +### A2A Protocol + +Send tasks via the Agent-to-Agent protocol: + +```bash +curl -X POST http://localhost:8000/a2a \ + -H "Authorization: Bearer test-token" \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "method": "tasks/send", + "id": "1", + "params": { + "id": "task-001", + "message": { + "role": "user", + "parts": [{"type": "text", "text": "Create a $500 display campaign for April 2025"}] + } + } + }' +``` + +### Delivery Simulation Demo + +The repository includes an end-to-end delivery simulation script: + +```bash +docker-compose exec adcp-server python -m examples.delivery_simulation_demo +``` + +This script creates a media buy, advances simulated time, and prints delivery reports at each stage of the campaign lifecycle. + +## Further Reading + +- [Building a Custom Adapter](/agents/salesagent/integrations/custom-adapter.html) -- Implement your own ad server adapter +- [Testing Guide](/agents/salesagent/developers/testing.html) -- Automated testing strategies using the mock adapter +- [Campaign Lifecycle Tutorial](/agents/salesagent/getting-started/campaign-lifecycle.html) -- Step-by-step walkthrough of a campaign from creation to reporting +- [Google Ad Manager Integration](/agents/salesagent/integrations/gam.html) -- Production GAM adapter for comparison diff --git a/agents/salesagent/operations/admin-ui.md b/agents/salesagent/operations/admin-ui.md new file mode 100644 index 0000000000..575817bdbb --- /dev/null +++ b/agents/salesagent/operations/admin-ui.md @@ -0,0 +1,310 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Operations - Admin UI Guide +description: Complete guide to the Prebid Sales Agent admin interface for managing products, advertisers, creatives, and settings +sidebarType: 10 +--- + +# Admin UI Guide +{: .no_toc} + +The Prebid Sales Agent includes a web-based administration interface built with Flask, accessible at `/admin`. This guide covers every section of the Admin UI, from initial login through product configuration, advertiser management, and system settings. + +- TOC +{:toc} + +## Login and Authentication + +### Setup Mode + +On first launch with no tenants configured, the Sales Agent enters **Setup Mode**. This provides a guided wizard to create your first publisher tenant, configure the ad server adapter, and set initial authentication. + +Navigate to `http://localhost:8000/admin` to start the wizard. + +### Test Credentials (Development Only) + +When `ADCP_AUTH_TEST_MODE=true` is set, you can log in with the password `test123`. This mode is intended for local evaluation only. + +{: .alert.alert-danger :} +Never use `ADCP_AUTH_TEST_MODE=true` in production. It enables well-known test credentials that provide full access to the Admin UI and API endpoints. + +### SSO Configuration + +For production deployments, the Admin UI supports OAuth-based Single Sign-On with the following identity providers: + +{: .table .table-bordered .table-striped } +| Provider | Configuration Required | Notes | +|----------|----------------------|-------| +| **Google** | Client ID, Client Secret | Google Cloud Console OAuth 2.0 credentials | +| **Microsoft** | Client ID, Client Secret, Tenant ID | Azure AD / Entra ID app registration | +| **Okta** | Client ID, Client Secret, Domain | Okta developer application | +| **Auth0** | Client ID, Client Secret, Domain | Auth0 regular web application | +| **Keycloak** | Client ID, Client Secret, Realm, Server URL | Self-hosted identity provider | + +To configure SSO: + +1. Create an OAuth application in your identity provider +2. Set the callback URL to `https://yourdomain.com/admin/auth/callback` +3. In the Admin UI, go to **Settings > SSO Configuration** +4. Select your provider and enter the credentials +5. Save and test the login flow + +{: .alert.alert-info :} +After SSO is configured, restrict admin access to specific email addresses or domains using the `SUPER_ADMIN_EMAILS` or `SUPER_ADMIN_DOMAINS` environment variables. See [Security Model](/agents/salesagent/operations/security.html) for details. + +## Dashboard + +The Admin UI dashboard provides a tenant-level overview of your Sales Agent instance: + +- **Tenant summary** -- Active products, registered advertisers, pending approvals +- **Activity feed** -- Real-time stream of system events powered by Server-Sent Events (SSE), including media buy requests, creative submissions, and authentication events +- **Quick actions** -- Links to common tasks like creating products or generating advertiser tokens + +The activity feed updates in real time without page refreshes. Events include: +- New media buy requests +- Creative approval/rejection actions +- Advertiser authentication attempts +- Configuration changes + +## Product Management + +Products represent the advertising inventory that AI buying agents can discover and purchase through the MCP and A2A interfaces. + +### Creating a Product + +Navigate to **Products > Create Product** and configure the following: + +{: .table .table-bordered .table-striped } +| Field | Required | Description | +|-------|----------|-------------| +| Name | Yes | Human-readable product name (e.g., "Homepage Leaderboard") | +| Description | Yes | Detailed description used by AI agents for discovery | +| Format IDs | Yes | IAB format identifiers for the ad unit (e.g., banner, video) | +| Base Price | Yes | Starting price for the product | +| Currency | Yes | ISO 4217 currency code (e.g., USD, EUR, GBP) | + +### Pricing Options + +The Sales Agent supports multiple pricing models. Configure one or more pricing options per product: + +{: .table .table-bordered .table-striped } +| Pricing Model | Code | Description | +|---------------|------|-------------| +| Cost Per Mille | `cpm` | Price per 1,000 impressions | +| Viewable CPM | `vcpm` | Price per 1,000 viewable impressions | +| Cost Per Click | `cpc` | Price per click | +| Cost Per Completed View | `cpcv` | Price per completed video view | +| Cost Per View | `cpv` | Price per video view (any duration) | +| Cost Per Period | `cpp` | Fixed price for a calendar period | +| Flat Rate | `flat_rate` | Single fixed price for the placement | + +### Targeting Configuration + +Each product can include targeting parameters that constrain where and how ads are served: + +- **Geographic targeting** -- Country, region, metro, or city-level targeting +- **Device targeting** -- Desktop, mobile, tablet, or connected TV +- **Content targeting** -- Contextual categories, keywords, or URL patterns +- **Audience targeting** -- First-party audience segments (where supported by the ad server) + +Targeting options are defined in the product and communicated to buying agents during discovery. The ad server adapter translates these into platform-specific targeting criteria. + +### Format IDs + +Format IDs follow IAB standards and identify the creative format required for the product. Common formats include: + +{: .table .table-bordered .table-striped } +| Format | Description | Typical Size | +|--------|-------------|-------------| +| `banner` | Display banner | 300x250, 728x90, 160x600 | +| `video` | In-stream or out-stream video | Various aspect ratios | +| `native` | Native advertising unit | Varies by placement | +| `audio` | Audio ad unit | N/A | + +## Advertiser Management + +Advertisers (called **principals** in AdCP terminology) are the entities that buy advertising through AI agents. Each principal has credentials that authenticate their agent's requests. + +### Creating a Principal + +Navigate to **Advertisers > Create Advertiser** and provide: + +{: .table .table-bordered .table-striped } +| Field | Required | Description | +|-------|----------|-------------| +| Name | Yes | Organization or advertiser name | +| Contact Email | Yes | Primary contact email address | +| Description | No | Notes about the advertiser | + +### Generating API Tokens + +After creating a principal, generate an authentication token: + +1. Go to **Advertisers** and select the advertiser +2. Click **Generate Token** +3. Copy the token immediately -- it is shown only once +4. Share the token securely with the advertiser or their AI agent operator + +The token authenticates both MCP and A2A requests: + +```bash +# MCP authentication +curl -H "x-adcp-auth: " https://yourdomain.com/mcp/ + +# A2A authentication +curl -H "Authorization: Bearer " https://yourdomain.com/a2a +``` + +### Managing Access + +From the advertiser detail page, you can: + +- **View activity** -- See all media buy requests and creative submissions from this principal +- **Rotate tokens** -- Generate a new token and invalidate the previous one +- **Revoke access** -- Disable the principal's token, immediately blocking API access +- **Delete advertiser** -- Remove the principal and all associated data + +{: .alert.alert-warning :} +Token rotation generates a new token and invalidates the old one immediately. Coordinate with the advertiser's agent operator before rotating to avoid service interruption. + +## Creative Management + +Creatives are the ad assets (images, video, HTML) submitted by buying agents for use in campaigns. The Admin UI provides an approval workflow to review creatives before they go live. + +### Approval Workflow + +Creatives follow a three-state lifecycle: + +```text +┌─────────────────┐ ┌──────────┐ ┌──────────┐ +│ pending_review │────▶│ approved │ │ rejected │ +│ (submitted) │ └──────────┘ └──────────┘ +└────────┬────────┘ ▲ ▲ + │ │ │ + └────────────────────┴────────────────┘ + (admin review) +``` + +{: .table .table-bordered .table-striped } +| State | Description | API Visibility | +|-------|-------------|---------------| +| `pending_review` | Newly submitted, awaiting publisher review | Not served | +| `approved` | Reviewed and accepted by the publisher | Available for campaigns | +| `rejected` | Reviewed and declined by the publisher | Not served; reason provided | + +### Reviewing Creatives + +Navigate to **Creatives > Pending Review** to see all creatives awaiting approval: + +1. Preview the creative asset (image, video thumbnail, or HTML render) +2. Verify the creative meets your editorial and technical standards +3. Check format validation results (size, file type, IAB compliance) +4. Click **Approve** or **Reject** with an optional comment + +### Format Validation + +The system automatically validates creatives against the product's format requirements: + +- Image dimensions and file size +- Video duration and codec compatibility +- HTML5 creative structure and compliance +- VAST/VPAID tag validation for video creatives + +Validation results appear on the creative review page. Creatives that fail validation are flagged but can still be manually approved if the publisher chooses. + +## Workflow Management + +The workflow queue provides human-in-the-loop oversight for automated operations. When a buying agent submits a media buy request, it enters the approval queue for publisher review. + +### Media Buy Approval Queue + +Navigate to **Workflows > Pending Approvals** to review incoming media buy requests: + +{: .table .table-bordered .table-striped } +| Field | Description | +|-------|-------------| +| Advertiser | The principal requesting the buy | +| Product | The product being purchased | +| Budget | Total spend requested | +| Flight dates | Campaign start and end dates | +| Targeting | Any additional targeting parameters | +| Creatives | Associated creative assets | + +For each request, you can: +- **Approve** -- The campaign is created in the ad server +- **Reject** -- The buying agent receives a rejection with the reason +- **Request changes** -- Send feedback back to the buying agent + +{: .alert.alert-info :} +Workflow approvals can be configured to auto-approve for trusted principals or below certain budget thresholds. See the adapter configuration documentation for details. + +## Settings + +### Adapter Configuration + +The adapter connects the Sales Agent to your ad server. Navigate to **Settings > Adapter** to configure: + +{: .table .table-bordered .table-striped } +| Adapter | Description | Configuration Required | +|---------|-------------|----------------------| +| **Google Ad Manager (GAM)** | Production adapter for GAM | Network code, service account JSON key | +| **Mock** | Testing adapter that simulates an ad server | None (default for evaluation) | + +Select your adapter and provide the required credentials. The Mock adapter is selected by default and requires no configuration. + +{: .alert.alert-warning :} +Adapter credentials (such as GAM service account keys) are encrypted at rest using the `ENCRYPTION_KEY`. See [Security Model](/agents/salesagent/operations/security.html) for details on the encryption mechanism. + +### Custom Domain + +Under **Settings > Custom Domain**, configure the public-facing domain for your Sales Agent instance: + +1. Enter your custom domain (e.g., `ads.yourpublisher.com`) +2. Ensure DNS is pointed to your server +3. The domain is used in agent cards and protocol responses + +### General Settings + +General configuration options include: + +- **Publisher name** -- Displayed in the agent card and Admin UI +- **Default currency** -- ISO 4217 code for product pricing +- **Contact information** -- Publisher support email for buying agents + +### Tenant Deactivation + +{: .alert.alert-danger :} +Tenant deactivation is a soft delete. It immediately blocks all API access (MCP and A2A) for the tenant, but preserves all data in the database. + +To deactivate a tenant: + +1. Go to **Settings > Tenant** +2. Click **Deactivate Tenant** +3. Confirm the action + +Deactivated tenants: +- Cannot authenticate via MCP or A2A +- Are hidden from public discovery +- Retain all historical data (campaigns, creatives, audit logs) +- Can be reactivated by a super admin + +## Self-Signup + +In multi-tenant deployments, the Sales Agent supports self-service tenant provisioning at `/signup`. New publishers can: + +1. Navigate to `https://yourdomain.com/signup` +2. Provide their organization name, domain, and admin email +3. Complete OAuth authentication +4. Receive a fully provisioned tenant with default settings + +Self-signup creates an isolated tenant with its own data, products, and advertiser space. The super admin can review and manage all tenants from the Admin UI. + +{: .alert.alert-info :} +Self-signup is only relevant for multi-tenant deployments. Single-tenant deployments use Setup Mode for initial configuration. + +## Further Reading + +- [Security Model](/agents/salesagent/operations/security.html) -- Authentication, encryption, and access control +- [Deployment Overview](/agents/salesagent/deployment/deployment-overview.html) -- Compare deployment options +- [Publisher Onboarding](/agents/salesagent/getting-started/publisher-onboarding.html) -- End-to-end setup walkthrough +- [AdCP Advanced Topics: Principals & Security](https://docs.adcontextprotocol.org/docs/advanced/principals-security) -- Protocol-level identity model diff --git a/agents/salesagent/operations/monitoring.md b/agents/salesagent/operations/monitoring.md new file mode 100644 index 0000000000..a151a1a07c --- /dev/null +++ b/agents/salesagent/operations/monitoring.md @@ -0,0 +1,290 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Operations - Monitoring and Audit Logging +description: Audit logging, activity monitoring, and operational observability for the Prebid Sales Agent +sidebarType: 10 +--- + +# Monitoring and Audit Logging +{: .no_toc} + +The Sales Agent records all significant operations to an audit trail and provides operational monitoring through health endpoints, container logs, and a real-time activity stream. This page covers the `AuditLogger` implementation, storage locations, Slack notifications, security violation detection, and health monitoring. + +- TOC +{:toc} + +## AuditLogger + +The `AuditLogger` class is the central logging component for the Sales Agent. It records operations to the database, log files, and optional Slack channels. + +### Initialization + +```python +from src.core.audit_logger import AuditLogger, get_audit_logger + +# Direct instantiation +logger = AuditLogger(adapter_name="google_ad_manager", tenant_id="tenant_001") + +# Helper function (recommended) +logger = get_audit_logger(adapter_name="google_ad_manager", tenant_id="tenant_001") +``` + +The `get_audit_logger()` helper is the recommended way to obtain a logger instance. It handles initialization and is called automatically within the adapter `__init__()` method, so adapter implementations do not need to create a logger manually. + +{: .table .table-bordered .table-striped } +| Parameter | Type | Description | +|-----------|------|-------------| +| `adapter_name` | `str` | The name of the adapter that owns this logger (e.g., `"google_ad_manager"`, `"mock"`) | +| `tenant_id` | `str` or `None` | Tenant identifier for multi-tenant deployments. When set, all log entries are tagged with this tenant. | + +## Core Methods + +### log_operation + +Records a standard operation to the audit trail. + +```python +logger.log_operation( + operation="media_buy.created", + principal_name="Acme Corp", + principal_id="principal_abc123", + adapter_id="adapter_gam_001", + success=True, + details={"media_buy_id": "mb_789", "budget_cents": 500000}, + error=None, + tenant_id="tenant_001", +) +``` + +{: .table .table-bordered .table-striped } +| Parameter | Type | Description | +|-----------|------|-------------| +| `operation` | `str` | Operation identifier (e.g., `media_buy.created`, `creative.approved`) | +| `principal_name` | `str` | Display name of the principal performing the action | +| `principal_id` | `str` | Unique identifier of the principal | +| `adapter_id` | `str` | Identifier of the adapter handling the operation | +| `success` | `bool` | Whether the operation completed successfully | +| `details` | `dict` or `None` | Operation-specific data stored as JSONB | +| `error` | `str` or `None` | Error message if the operation failed | +| `tenant_id` | `str` or `None` | Tenant context (overrides the logger's default if provided) | + +This method writes to all three storage locations (database, file, console) and triggers Slack notifications for qualifying operations. + +### log_security_violation + +Records an unauthorized access attempt or security-relevant event. + +```python +logger.log_security_violation( + operation="cross_tenant_access", + principal_id="principal_abc123", + resource_id="tenant_other_002", + reason="Principal attempted to access a resource outside their tenant", + tenant_id="tenant_001", +) +``` + +{: .table .table-bordered .table-striped } +| Parameter | Type | Description | +|-----------|------|-------------| +| `operation` | `str` | The operation that triggered the violation | +| `principal_id` | `str` | Identifier of the principal involved | +| `resource_id` | `str` | The resource that was improperly accessed or targeted | +| `reason` | `str` | Human-readable description of the violation | +| `tenant_id` | `str` or `None` | Tenant context | + +Security violations are written to the database, the dedicated `security.jsonl` log file, and the console. They always trigger a Slack notification if Slack is configured. + +## Audit Trail Storage + +The audit trail is stored in three locations simultaneously for redundancy and flexibility. + +### 1. Database + +All audit entries are written to the `AuditLog` table in PostgreSQL. + +{: .table .table-bordered .table-striped } +| Column | Type | Description | +|--------|------|-------------| +| `id` | `UUID` | Primary key | +| `tenant_id` | `str` | Tenant that owns this entry | +| `timestamp` | `datetime` | UTC timestamp of the event | +| `operation` | `str` | Operation identifier | +| `principal_name` | `str` | Display name of the acting principal | +| `principal_id` | `str` | Unique identifier of the acting principal | +| `adapter_id` | `str` | Adapter that handled the operation | +| `success` | `bool` | Whether the operation succeeded | +| `error_message` | `str` or `null` | Error message on failure | +| `details` | `JSONB` | Operation-specific structured data | + +The database is the primary audit store and backs the Admin UI audit log viewer and the activity stream API. + +### 2. File Backup + +Audit entries are also written to log files on disk for backup and external log aggregation. + +{: .table .table-bordered .table-striped } +| File | Contents | Format | +|------|----------|--------| +| `logs/audit.log` | All operations (success and failure) | Human-readable text | +| `logs/error.log` | Failed operations only | Human-readable text | +| `logs/security.jsonl` | Security violations only | JSON Lines (one JSON object per line) | +| `logs/structured.jsonl` | All operations | JSON Lines (one JSON object per line) | + +{: .alert.alert-info :} +The `structured.jsonl` file is designed for ingestion by log aggregation systems (e.g., Elasticsearch, Datadog, Splunk). Each line is a self-contained JSON object with all audit fields. + +### 3. Console Output + +All audit entries are printed to the console (stdout/stderr) with structured formatting. In Docker deployments, console output is captured by the container runtime and accessible via `docker compose logs`. + +## Slack Notifications + +When a Slack webhook URL is configured, the audit logger sends notifications for high-impact operations. + +### Operations That Trigger Notifications + +{: .table .table-bordered .table-striped } +| Category | Operations | +|----------|------------| +| **Media buy** | Create, update, delete | +| **Creative** | Approve, reject, manual approval | +| **User management** | Add user, toggle active status, update role | +| **Tenant management** | Create, deactivate, reactivate, update settings | +| **Adapter configuration** | Setup, update connection or product config | +| **Principal management** | Create, update platform mappings | +| **High-value transactions** | Any operation where budget exceeds $10,000 | +| **Security violations** | All violations (always notified) | + +{: .alert.alert-warning :} +High-value transaction notifications use a $10,000 threshold. Any media buy creation or budget adjustment above this amount triggers a Slack alert regardless of the operation category. + +## Security Violation Detection + +The audit logger flags the following patterns as security violations: + +{: .table .table-bordered .table-striped } +| Violation Type | Description | Severity | +|---------------|-------------|----------| +| Repeated failed authentication | Multiple failed auth attempts from the same source within a time window | High | +| Invalid or expired tokens | Requests using tokens that do not exist or have been revoked | Medium | +| Cross-tenant access attempts | A principal attempting to access resources belonging to a different tenant | Critical | +| Unusual request patterns | Abnormal request volume, timing, or endpoint access that deviates from the principal's baseline | Medium | + +Security violations are: + +- Stored in the `AuditLog` database table with a `security_violation` flag +- Written to `logs/security.jsonl` for dedicated security log analysis +- Sent to Slack immediately (if configured) +- Surfaced in the Admin UI activity stream with a warning indicator + +## Activity Stream + +The Sales Agent provides a real-time activity stream via Server-Sent Events (SSE). The activity stream is implemented as a Flask blueprint and backed by the database audit log. + +### Endpoint + +```text +GET /api/activity-stream +``` + +The SSE endpoint pushes new audit log entries to connected clients in real time. The Admin UI uses this endpoint to display a live activity feed. + +### Event Format + +Each SSE event contains a JSON-serialized audit log entry: + +```json +{ + "id": "evt_abc123", + "timestamp": "2025-04-15T14:30:00Z", + "operation": "media_buy.created", + "principal_name": "Acme Corp", + "success": true, + "details": { + "media_buy_id": "mb_789", + "budget_cents": 500000 + } +} +``` + +{: .alert.alert-info :} +The activity stream requires an authenticated Admin UI session. It does not accept MCP or A2A tokens. + +## Docker Container Logs + +In Docker deployments, all console output from the Sales Agent is captured by the Docker logging driver. + +### Viewing Logs + +```bash +# Follow logs in real time +docker compose logs -f salesagent + +# Filter for errors +docker compose logs salesagent | grep ERROR + +# View logs for a specific time range (Docker timestamp filtering) +docker compose logs --since 1h salesagent + +# View logs from all services +docker compose logs -f +``` + +### Log Levels + +{: .table .table-bordered .table-striped } +| Level | Contents | +|-------|----------| +| `INFO` | Standard operations, startup messages, health checks | +| `WARNING` | Non-critical issues, approaching rate limits, deprecated feature usage | +| `ERROR` | Failed operations, adapter errors, unhandled exceptions | +| `CRITICAL` | Security violations, database connectivity loss, encryption failures | + +## Health Monitoring + +The Sales Agent exposes a health check endpoint for infrastructure monitoring. + +### Health Endpoint + +```bash +curl http://localhost:8000/health +``` + +Response: + +```json +{ + "status": "ok" +} +``` + +The health endpoint performs the following checks: + +{: .table .table-bordered .table-striped } +| Check | Description | Failure Behavior | +|-------|-------------|------------------| +| Database connectivity | Executes a lightweight query against PostgreSQL | Returns `{"status": "error", "detail": "database unavailable"}` | +| Adapter availability | Verifies the configured adapter can be instantiated | Returns `{"status": "degraded", "detail": "adapter unavailable"}` | + +{: .alert.alert-warning :} +The health endpoint does not require authentication. It is intended for use by load balancers and container orchestrators (e.g., Docker health checks, Kubernetes liveness probes). Do not expose it on a public network without IP restrictions. + +### Docker Health Check + +Configure a Docker health check in your `docker-compose.yml`: + +```bash +healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 15s +``` + +## Further Reading + +- [Security Model](/agents/salesagent/operations/security.html) -- Authentication, encryption, and access control +- [Admin UI Guide](/agents/salesagent/operations/admin-ui.html) -- Activity stream and audit log viewer in the Admin UI +- [Single-Tenant Deployment](/agents/salesagent/deployment/single-tenant.html) -- Docker Compose configuration and log management diff --git a/agents/salesagent/operations/security.md b/agents/salesagent/operations/security.md new file mode 100644 index 0000000000..23408b828f --- /dev/null +++ b/agents/salesagent/operations/security.md @@ -0,0 +1,299 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Operations - Security Model +description: Authentication, encryption, access control, and security architecture of the Prebid Sales Agent +sidebarType: 10 +--- + +# Security Model +{: .no_toc} + +This page documents the security architecture of the Prebid Sales Agent, covering authentication mechanisms, principal resolution, multi-tenant access control, encryption, audit logging, and operational security practices. + +- TOC +{:toc} + +## Authentication Mechanisms + +The Sales Agent uses three distinct authentication mechanisms depending on the interface being accessed. + +### MCP Authentication + +AI agents connecting via the Model Context Protocol authenticate using the `x-adcp-auth` header. Each principal (advertiser) receives a unique token generated through the Admin UI. + +```bash +curl -H "x-adcp-auth: " https://yourdomain.com/mcp/ +``` + +The token is a per-principal credential that maps the request to a specific advertiser and tenant. All MCP tool calls require this header. + +### A2A Authentication + +Agent-to-Agent protocol requests use standard `Authorization: Bearer` token authentication: + +```bash +curl -H "Authorization: Bearer " https://yourdomain.com/a2a +``` + +The same principal token works for both MCP and A2A authentication, but the header format differs. + +### Admin UI Authentication + +The Admin UI supports two authentication modes: + +{: .table .table-bordered .table-striped } +| Mode | Mechanism | Use Case | +|------|-----------|----------| +| **Setup Mode** | Password (`test123`) | Initial setup and evaluation only | +| **OAuth SSO** | Google, Microsoft, Okta, Auth0, Keycloak | Production deployments | + +{: .alert.alert-danger :} +Setup Mode password authentication (`ADCP_AUTH_TEST_MODE=true`) must never be used in production. It enables well-known credentials that grant full administrative access. + +Supported OAuth providers: + +{: .table .table-bordered .table-striped } +| Provider | Protocol | Configuration | +|----------|----------|---------------| +| Google | OAuth 2.0 / OpenID Connect | Client ID, Client Secret | +| Microsoft | OAuth 2.0 / OpenID Connect | Client ID, Client Secret, Tenant ID | +| Okta | OAuth 2.0 / OpenID Connect | Client ID, Client Secret, Domain | +| Auth0 | OAuth 2.0 / OpenID Connect | Client ID, Client Secret, Domain | +| Keycloak | OAuth 2.0 / OpenID Connect | Client ID, Client Secret, Realm, Server URL | + +## Principal Resolution Flow + +When an API request arrives, the Sales Agent resolves the principal through a chain that maps the token to a tenant and adapter configuration: + +```text +┌────────────────┐ ┌─────────────┐ ┌────────────┐ ┌─────────────┐ +│ Auth Token │───▶│ Principal │───▶│ Tenant │───▶│ Adapter │ +│ (header) │ │ (advertiser)│ │ (publisher) │ │ (ad server) │ +└────────────────┘ └─────────────┘ └────────────┘ └─────────────┘ +``` + +1. **Token** -- Extracted from the `x-adcp-auth` or `Authorization: Bearer` header +2. **Principal** -- Looked up in the database by token hash; identifies the advertiser +3. **Tenant** -- The publisher tenant that the principal belongs to; provides configuration context +4. **Adapter** -- The ad server integration configured for the tenant (e.g., GAM, Mock) + +This resolution chain ensures every API request is scoped to a specific advertiser operating within a specific publisher's context, using the correct ad server integration. + +{: .alert.alert-info :} +Tokens are stored as hashes in the database, not in plaintext. The original token value is shown only once at generation time. + +## Multi-Tenant Access Control + +In multi-tenant deployments, the Sales Agent enforces strict data isolation between publishers. + +### Composite Identity Constraint + +Tenant isolation is enforced at the database level through a composite `(tenant_id, email)` constraint. This ensures: + +- Each tenant's data is isolated from all other tenants +- A user with the same email across multiple tenants has separate, independent identities +- Database queries are automatically scoped to the authenticated tenant + +### Role-Based Permissions + +{: .table .table-bordered .table-striped } +| Role | Scope | Capabilities | +|------|-------|-------------| +| **Super Admin** | All tenants | Create/deactivate tenants, manage cross-tenant settings, view all activity | +| **Tenant Admin** | Single tenant | Manage products, advertisers, creatives, workflows, and settings for their tenant | +| **Principal** | Single tenant, limited | Submit media buys and creatives, view own campaigns and delivery data | + +### Cross-Tenant Protection + +The following safeguards prevent cross-tenant data access: + +- All database queries include `tenant_id` in their WHERE clause +- Principal tokens are scoped to a single tenant and cannot access other tenants' data +- Admin UI sessions are bound to a specific tenant context +- API responses never include data from other tenants +- Subdomain routing ensures requests are directed to the correct tenant + +## Encryption + +### Fernet Symmetric Encryption + +The Sales Agent uses [Fernet symmetric encryption](https://cryptography.io/en/latest/fernet/) (from the Python `cryptography` library) to protect sensitive data at rest. Fernet provides authenticated encryption using AES-128-CBC with HMAC-SHA256. + +Encrypted fields include: + +{: .table .table-bordered .table-striped } +| Data | Location | Purpose | +|------|----------|---------| +| Principal API tokens | `principals` table | Advertiser authentication credentials | +| Ad server credentials | `adapter_config` table | GAM service account keys, API keys | +| OAuth client secrets | `sso_config` table | Identity provider credentials | + +### Encryption Key Management + +The `ENCRYPTION_KEY` environment variable holds the Fernet key used for all encryption operations. + +```bash +# Generate a new Fernet key +python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" +``` + +{: .alert.alert-danger :} +The `ENCRYPTION_KEY` is the root secret for the entire deployment. If lost, all encrypted data becomes unrecoverable. Store it securely (e.g., in a secrets manager) and back it up separately from the deployment. + +Key rotation requires decrypting all data with the old key and re-encrypting with the new key. This is a manual operation -- contact the development team for guidance if key rotation is needed. + +## Super Admin + +Super admins have cross-tenant administrative privileges and are configured through environment variables. + +### Configuration + +{: .table .table-bordered .table-striped } +| Variable | Format | Description | +|----------|--------|-------------| +| `SUPER_ADMIN_EMAILS` | Comma-separated emails | Specific email addresses granted super admin access | +| `SUPER_ADMIN_DOMAINS` | Comma-separated domains | All users from these email domains are granted super admin access | + +Example: + +```bash +SUPER_ADMIN_EMAILS=admin@yourcompany.com,ops@yourcompany.com +SUPER_ADMIN_DOMAINS=yourcompany.com +``` + +{: .alert.alert-warning :} +Super admin access is a whitelist -- only emails or domains explicitly listed are granted elevated privileges. There is no default super admin. If neither variable is set, no user has cross-tenant access. + +### Super Admin Capabilities + +- Create and deactivate tenants +- View all tenants and their configurations +- Reactivate deactivated tenants +- Access any tenant's Admin UI +- View cross-tenant audit logs + +## HTTPS Requirements + +{: .alert.alert-danger :} +HTTPS is mandatory for all production deployments. The Sales Agent enforces HTTPS for all non-localhost connections to protect authentication tokens in transit. + +Recommended SSL/TLS configuration: + +- Terminate SSL at the Nginx reverse proxy layer +- Use Let's Encrypt for automatic certificate management +- Enforce TLS 1.2 or higher +- Enable HSTS headers + +For localhost development (`http://localhost:8000`), HTTPS enforcement is relaxed to allow evaluation without certificate setup. + +## Token Lifecycle + +Principal tokens follow a defined lifecycle managed entirely through the Admin UI: + +### Generation + +1. Navigate to **Advertisers** in the Admin UI +2. Select or create a principal +3. Click **Generate Token** +4. Copy the token immediately -- it is displayed only once + +The token is hashed before storage. The plaintext value cannot be retrieved after generation. + +### Rotation + +To rotate a token (e.g., if compromised or as part of regular security hygiene): + +1. Navigate to the principal in the Admin UI +2. Click **Rotate Token** +3. The old token is immediately invalidated +4. A new token is generated and displayed once +5. Share the new token with the advertiser's agent operator + +{: .alert.alert-warning :} +Token rotation is immediate. The old token stops working as soon as the new token is generated. Coordinate with the advertiser before rotating to avoid disrupting active campaigns. + +### Revocation + +To revoke access without generating a replacement token: + +1. Navigate to the principal in the Admin UI +2. Click **Revoke Token** +3. All API access for this principal is immediately blocked + +Revoked principals can have new tokens generated later if access needs to be restored. + +## Audit Logging + +The Sales Agent maintains a detailed audit log backed by the PostgreSQL database. Every significant operation is recorded for compliance, debugging, and security analysis. + +### Tracked Operations + +{: .table .table-bordered .table-striped } +| Category | Events | +|----------|--------| +| **Authentication** | Login attempts, token usage, failed authentication, SSO callbacks | +| **Media Buys** | Submissions, approvals, rejections, modifications | +| **Creatives** | Uploads, approval/rejection, format validation results | +| **Products** | Creation, modification, deletion, pricing changes | +| **Advertisers** | Principal creation, token generation/rotation/revocation | +| **Configuration** | Adapter changes, SSO updates, domain changes, tenant settings | +| **Security** | Cross-tenant access attempts, unauthorized requests, rate limit violations | + +### Log Fields + +Each audit log entry includes: + +{: .table .table-bordered .table-striped } +| Field | Description | +|-------|-------------| +| `timestamp` | UTC timestamp of the event | +| `tenant_id` | Tenant context for the operation | +| `principal_id` | The principal that performed the action (if applicable) | +| `user_email` | The admin user email (for Admin UI actions) | +| `operation` | The operation type (e.g., `media_buy.created`, `creative.approved`) | +| `details` | JSON payload with operation-specific data | +| `ip_address` | Source IP address of the request | + +### Security Violation Detection + +The audit log captures security-relevant events that can be used to detect unauthorized access patterns: + +- Repeated failed authentication attempts +- Requests with invalid or expired tokens +- Attempts to access resources outside the principal's tenant +- Unusual request patterns (volume, timing, endpoint access) + +## Tenant Deactivation + +Tenant deactivation is a soft delete mechanism that immediately blocks all access while preserving data for potential reactivation. + +### What Happens on Deactivation + +{: .table .table-bordered .table-striped } +| Aspect | Behavior | +|--------|----------| +| MCP/A2A API access | Immediately blocked for all principals | +| Admin UI access | Blocked for tenant admins | +| Data (campaigns, creatives, logs) | Preserved in database | +| Active campaigns | Stopped in the ad server (adapter-dependent) | +| Public discovery | Tenant removed from agent card listings | + +### Reactivation + +Only super admins can reactivate a deactivated tenant: + +1. Log in as a super admin +2. Navigate to the tenant management page +3. Select the deactivated tenant +4. Click **Reactivate** +5. All API access is restored with existing tokens + +{: .alert.alert-info :} +Deactivation does not delete or invalidate tokens. Upon reactivation, existing principal tokens work immediately without regeneration. + +## Further Reading + +- [Admin UI Guide](/agents/salesagent/operations/admin-ui.html) -- Managing authentication, products, and advertisers +- [Single-Tenant Deployment](/agents/salesagent/deployment/single-tenant.html) -- SSL/TLS and Nginx configuration +- [Deployment Overview](/agents/salesagent/deployment/deployment-overview.html) -- Infrastructure requirements +- [AdCP Advanced Topics: Principals & Security](https://docs.adcontextprotocol.org/docs/advanced/principals-security) -- Protocol-level security model diff --git a/agents/salesagent/overview.md b/agents/salesagent/overview.md new file mode 100644 index 0000000000..b87e6b0be9 --- /dev/null +++ b/agents/salesagent/overview.md @@ -0,0 +1,133 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Overview +description: Overview of the Prebid Sales Agent, a reference implementation of the AdCP Media Buy protocol for AI-driven advertising +sidebarType: 10 +--- + +# Prebid Sales Agent Overview +{: .no_toc} + +The Prebid Sales Agent is a server that exposes advertising inventory to AI agents via the Model Context Protocol (MCP) and Agent-to-Agent (A2A) protocol. It is the reference implementation of the [Ad Context Protocol (AdCP)](https://adcontextprotocol.org). + +- TOC +{:toc} + +## What is the Prebid Sales Agent? + +The Sales Agent sits between AI buying agents and publisher ad servers (such as Google Ad Manager), translating standardized AdCP requests into platform-specific operations. It handles the full campaign lifecycle — from product discovery to delivery reporting. + +{: .alert.alert-info :} +For the full source code and latest updates, visit the [prebid/salesagent repository](https://github.com/prebid/salesagent). + +### Three-Role Model + +AdCP defines three roles in AI-driven advertising: + +{: .table .table-bordered .table-striped } +| Role | Description | Example | +|------|-------------|---------| +| **Publisher (Sales Agent)** | Exposes inventory to AI agents, manages campaigns, integrates with ad servers | Prebid Sales Agent | +| **Buyer (Buying Agent)** | Discovers products and executes media buys on behalf of advertisers | Claude, custom AI agents, orchestrators | +| **Orchestrator** | Coordinates multi-agent workflows across multiple sales agents | Scope3, custom platforms | + +The Prebid Sales Agent serves the **Publisher** role. + +## Key Capabilities + +### For AI Agents + +- **Product Discovery** — Natural language search for advertising products using AI/RAG +- **Campaign Creation** — Automated media buying with targeting, budget, and flight dates +- **Creative Management** — Upload, sync, and manage creative assets with format validation +- **Performance Monitoring** — Real-time access to campaign delivery metrics and spend + +### For Publishers + +- **Multi-Tenant System** — Isolated data per publisher with composite identity model +- **Adapter Pattern** — Pluggable ad server integrations (Google Ad Manager, mock, and more) +- **Real-time Dashboard** — Live activity feed powered by Server-Sent Events (SSE) +- **Workflow Management** — Human-in-the-loop approval queue for media buys and creatives +- **Admin Interface** — Web UI with OAuth and role-based access control +- **Audit Logging** — Complete operational history for compliance and debugging + +### For Developers + +- **MCP Protocol** — FastMCP with HTTP/SSE streamable transport at `/mcp/` +- **A2A Protocol** — JSON-RPC 2.0 agent-to-agent communication at `/a2a` +- **Docker Deployment** — Production-ready containers with bundled PostgreSQL +- **Testing** — Unit, integration, and E2E test suites with mock adapter + +## Protocol Support + +The Sales Agent exposes identical functionality through two protocols: + +**MCP (Model Context Protocol)** — The primary interface for AI assistants. Uses FastMCP with HTTP/SSE streamable transport. Authentication via `x-adcp-auth` header token. + +**A2A (Agent-to-Agent Protocol)** — For complex multi-agent workflows. JSON-RPC 2.0 compliant with agent discovery at `/.well-known/agent.json`. Authentication via `Authorization: Bearer` header. + +{: .alert.alert-info :} +For details on protocol architecture and comparison, see [Architecture & Protocols](/agents/salesagent/architecture.html). + +## Quick Start + +Try the Sales Agent locally with Docker: + +```bash +git clone https://github.com/prebid/salesagent.git +cd salesagent +docker compose up -d +``` + +Access services at `http://localhost:8000`: + +{: .table .table-bordered .table-striped } +| Service | URL | Notes | +|---------|-----|-------| +| Admin UI | `/admin` | Test credentials: `test123` | +| MCP Server | `/mcp/` | Token auth required | +| A2A Server | `/a2a` | Bearer auth required | +| Health Check | `/health` | No auth needed | + +Test the MCP interface: + +```bash +uvx adcp http://localhost:8000/mcp/ --auth test-token list_tools +uvx adcp http://localhost:8000/mcp/ --auth test-token get_products '{"brief":"video"}' +``` + +{: .alert.alert-info :} +For the full setup guide, see [Quick Start](/agents/salesagent/getting-started/quickstart.html). + +## Architecture + +The project follows a four-layer architecture isolating protocol handling, business logic, and ad server integrations: + +```text +┌─────────────────────────────────────────────────────┐ +│ Admin UI (Flask) │ +└─────────────────────────────────────────────────────┘ + │ +┌─────────────────────────────────────────────────────┐ +│ MCP Server (FastMCP) │ +│ A2A Server (JSON-RPC) │ +└─────────────────────────────────────────────────────┘ + │ + ┌─────────────────┼─────────────────┐ + │ │ │ +┌───────────────┐ ┌──────────────┐ ┌──────────────┐ +│ PostgreSQL │ │ Ad Server │ │ Gemini │ +│ Database │ │ Adapters │ │ AI API │ +└───────────────┘ └──────────────┘ └──────────────┘ +``` + +{: .alert.alert-info :} +For the full architecture and system design details, see [Architecture & Protocols](/agents/salesagent/architecture.html). + +## Further Reading + +- [Quick Start](/agents/salesagent/getting-started/quickstart.html) — Get running in 2 minutes with Docker +- [Publisher Onboarding](/agents/salesagent/getting-started/publisher-onboarding.html) — End-to-end publisher setup guide +- [Buy-Side Integration](/agents/salesagent/getting-started/buy-side-integration.html) — Connect an AI buying agent +- [Tool Reference](/agents/salesagent/tools/tool-reference.html) — Complete catalog of MCP tools +- [AdCP Introduction](https://docs.adcontextprotocol.org/docs/intro) — Protocol specification and concepts diff --git a/agents/salesagent/protocols.md b/agents/salesagent/protocols.md new file mode 100644 index 0000000000..91311a77f1 --- /dev/null +++ b/agents/salesagent/protocols.md @@ -0,0 +1,180 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Protocols - MCP and A2A Compared +description: Comparison of MCP and A2A protocols supported by the Prebid Sales Agent +sidebarType: 10 +--- + +# Protocols: MCP and A2A Compared +{: .no_toc} + +The Prebid Sales Agent exposes identical functionality through two protocols: MCP (Model Context Protocol) and A2A (Agent-to-Agent). Both call the same underlying implementation functions and share authentication infrastructure. + +- TOC +{:toc} + +## Protocol Comparison + +{: .table .table-bordered .table-striped } +| Aspect | MCP | A2A | +|--------|-----|-----| +| Protocol | HTTP + tool-call model | JSON-RPC 2.0 | +| Library | `fastmcp` (`EnhancedMCPServer`) | `a2a-sdk` (official SDK) | +| Internal Port | 8080 | 8091 | +| Route | `/mcp/` | `/a2a` | +| Auth Header | `x-adcp-auth` | `x-adcp-auth` | +| Discovery | `tools/list` method | `/.well-known/agent.json` | +| Response Format | Pydantic models (JSON) | A2A Message/Task types | +| Streaming | StreamableHttpTransport (SSE) | A2A spec streaming | +| Best For | Direct AI assistant integration | Multi-agent workflows | + +## MCP (Model Context Protocol) + +The MCP server is the primary interface for AI assistants. It uses the `fastmcp` library and exposes tools through an `EnhancedMCPServer` instance with `StreamableHttpTransport` for server-sent events. + +### Transport and Routing + +- **External route:** `/mcp/` (behind nginx proxy on port 8000) +- **Internal port:** 8080 +- **Transport:** HTTP with `StreamableHttpTransport` (SSE-capable) + +### Tool Registration + +Tools are registered with the `@mcp.tool()` decorator. Each tool function receives a `ToolContext` through FastAPI-style dependency injection (`Depends(get_tool_context)`), which provides access to the tenant, principal, database session, adapter instance, and server configuration. + +```python +@mcp.tool() +async def get_products( + brief: str, + brand_manifest: BrandManifest | None = None, + ctx: ToolContext = Depends(get_tool_context), +) -> GetProductsResponse: + return await core_get_products_tool(brief, brand_manifest, ctx) +``` + +### Context Headers + +- **`x-adcp-auth`** — Access token for authentication (required for most tools, optional for discovery) +- **`x-context-id`** — Optional request tracking identifier for correlating related operations + +## A2A (Agent-to-Agent Protocol) + +The A2A server handles multi-agent workflows using the official `a2a-sdk`. It communicates via JSON-RPC 2.0 over Starlette and supports the full A2A task lifecycle. + +### Transport and Routing + +- **External route:** `/a2a` (behind nginx proxy on port 8000) +- **Internal port:** 8091 +- **Transport:** JSON-RPC 2.0 via Starlette + +### Agent Discovery + +A2A clients discover the agent by fetching `/.well-known/agent.json`, which returns an agent card describing the agent's capabilities, supported tasks, and endpoint URL. + +### Task Lifecycle + +A2A operations follow a status progression: + +1. **`submitted`** — Task received and queued +2. **`working`** — Task is being processed +3. **`completed`** — Task finished successfully (result in response) +4. **`failed`** — Task encountered an error +5. **`input-required`** — Task needs additional information from the caller + +## Shared Implementation + +Both protocols call the same core implementation functions. For example, `create_media_buy` in MCP and the equivalent A2A task both invoke `core_create_media_buy_tool()`. This guarantees identical behavior regardless of which protocol a client uses. + +Other shared core functions include `core_get_products_tool()`, `core_get_adcp_capabilities_tool()`, `core_sync_creatives_tool()`, and `core_get_media_buy_delivery_tool()`. + +{: .alert.alert-info :} +The shared core layer means that any bug fix or feature added to a tool is immediately available through both MCP and A2A without duplicate implementation work. + +## Authentication + +Both protocols use the same authentication mechanism: + +1. The client sends a token in the `x-adcp-auth` header. +2. The server resolves the token to a principal (buyer identity) and tenant (publisher). +3. The `ToolContext` is populated with the authenticated state and passed to the core function. + +Token scopes determine which tools the principal can access. Discovery tools (`get_adcp_capabilities`, `get_products`, `list_creative_formats`) work without authentication but return richer data when a token is provided. + +## When to Use Each Protocol + +### Use MCP When + +- Integrating a single AI assistant (e.g., Claude, GPT) with a publisher's inventory +- Building a direct tool-call interface where the assistant invokes tools by name +- Prototyping or testing with the `uvx adcp` CLI +- Streaming responses are needed via SSE + +### Use A2A When + +- Building multi-agent systems where agents coordinate across publishers +- Using an orchestrator that manages workflows across multiple sales agents +- The client expects JSON-RPC 2.0 semantics and task lifecycle management +- Agent discovery via `/.well-known/agent.json` is part of the integration pattern + +## Testing Each Protocol + +### Testing MCP + +Use the `uvx adcp` CLI to interact with the MCP server directly: + +```bash +# List available tools +uvx adcp http://localhost:8000/mcp/ --auth test-token list_tools + +# Discover capabilities +uvx adcp http://localhost:8000/mcp/ --auth test-token get_adcp_capabilities + +# Search products +uvx adcp http://localhost:8000/mcp/ --auth test-token get_products '{"brief": "video ads"}' +``` + +### Testing A2A + +Use `curl` to send JSON-RPC 2.0 requests to the A2A endpoint: + +```bash +# Send a task +curl -X POST http://localhost:8000/a2a \ + -H "Content-Type: application/json" \ + -H "x-adcp-auth: test-token" \ + -d '{ + "jsonrpc": "2.0", + "method": "tasks/send", + "id": "1", + "params": { + "message": { + "role": "user", + "parts": [{"text": "Find video advertising products"}] + } + } + }' +``` + +```bash +# Fetch agent card +curl http://localhost:8000/.well-known/agent.json +``` + +## Error Handling Differences + +Both protocols surface the same error codes (`unauthorized`, `not_found`, `validation_error`, etc.), but the transport format differs: + +{: .table .table-bordered .table-striped } +| Aspect | MCP | A2A | +|--------|-----|-----| +| Error envelope | Tool result with `is_error: true` | JSON-RPC error object or failed task | +| Error fields | `error_code` + `message` in response body | `code` + `message` in JSON-RPC error, or task status `failed` | +| HTTP status | Always 200 (errors in body) | Always 200 (errors in JSON-RPC response) | +| Partial results | Not supported | Possible via `input-required` status | + +## Further Reading + +- [Architecture & Protocols](/agents/salesagent/architecture.html) -- Full system architecture and layer design +- [Tool Reference](/agents/salesagent/tools/tool-reference.html) -- Complete catalog of available tools +- [Buy-Side Integration](/agents/salesagent/getting-started/buy-side-integration.html) -- Connect an AI buying agent +- [AdCP Protocol Comparison](https://docs.adcontextprotocol.org/docs/building/understanding/protocol-comparison) -- Canonical protocol comparison in the AdCP specification diff --git a/agents/salesagent/reference/adcp-cross-references.md b/agents/salesagent/reference/adcp-cross-references.md new file mode 100644 index 0000000000..51c8afb17b --- /dev/null +++ b/agents/salesagent/reference/adcp-cross-references.md @@ -0,0 +1,125 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Reference - AdCP Cross-Reference Index +description: Index of AdCP protocol documentation relevant to the Prebid Sales Agent +sidebarType: 10 +--- + +# AdCP Cross-Reference Index +{: .no_toc} + +This page indexes all AdCP protocol documentation relevant to the Prebid Sales Agent. Use it to find the canonical protocol specification for any feature or concept referenced in the Sales Agent documentation. + +- TOC +{:toc} + +## Media Buy Protocol + +The core protocol governing campaign lifecycle operations. + +{: .table .table-bordered .table-striped } +| Document | Description | +|----------|-------------| +| [Media Buy Overview](https://docs.adcontextprotocol.org/docs/media-buy) | Protocol specification for the campaign lifecycle | +| [Media Buy Specification](https://docs.adcontextprotocol.org/docs/media-buy/specification) | Detailed technical specification | +| [get_products](https://docs.adcontextprotocol.org/docs/media-buy/task-reference/get_products) | Product discovery task reference | +| [create_media_buy](https://docs.adcontextprotocol.org/docs/media-buy/task-reference/create_media_buy) | Campaign creation task reference | +| [update_media_buy](https://docs.adcontextprotocol.org/docs/media-buy/task-reference/update_media_buy) | Campaign modification task reference | +| [get_media_buy_delivery](https://docs.adcontextprotocol.org/docs/media-buy/task-reference/get_media_buy_delivery) | Delivery reporting task reference | +| [sync_creatives](https://docs.adcontextprotocol.org/docs/media-buy/task-reference/sync_creatives) | Creative management task reference | +| [list_creatives](https://docs.adcontextprotocol.org/docs/media-buy/task-reference/list_creatives) | Creative listing task reference | +| [sync_catalogs](https://docs.adcontextprotocol.org/docs/media-buy/task-reference/sync_catalogs) | Catalog synchronization task reference | +| [provide_performance_feedback](https://docs.adcontextprotocol.org/docs/media-buy/task-reference/provide_performance_feedback) | Performance feedback task reference | +| [sync_event_sources](https://docs.adcontextprotocol.org/docs/media-buy/task-reference/sync_event_sources) | Event source registration task reference | +| [log_event](https://docs.adcontextprotocol.org/docs/media-buy/task-reference/log_event) | Event logging task reference | +| [Advanced Topics](https://docs.adcontextprotocol.org/docs/media-buy/advanced-topics) | Principals, security, and edge cases | + +## Creative Protocol + +Specifications for creative assets, formats, and delivery. + +{: .table .table-bordered .table-striped } +| Document | Description | +|----------|-------------| +| [Creative Protocol](https://docs.adcontextprotocol.org/docs/creative) | Creative asset management overview | +| [Creative Formats](https://docs.adcontextprotocol.org/docs/creative/formats) | Format specifications (dimensions, media types, assets) | +| [Creative Manifests](https://docs.adcontextprotocol.org/docs/creative/creative-manifests) | Asset packaging and manifest structure | + +## Governance Protocol + +Content policies, property authorization, and brand safety. + +{: .table .table-bordered .table-striped } +| Document | Description | +|----------|-------------| +| [Governance Overview](https://docs.adcontextprotocol.org/docs/governance/overview) | Property governance and content standards | + +## Signals Protocol + +Audience data and contextual signal integration. + +{: .table .table-bordered .table-striped } +| Document | Description | +|----------|-------------| +| [Signals Overview](https://docs.adcontextprotocol.org/docs/signals/overview) | Audience signal specification | + +## Brand Protocol + +Advertiser identity and brand safety information. + +{: .table .table-bordered .table-striped } +| Document | Description | +|----------|-------------| +| [Brand Protocol / brand.json](https://docs.adcontextprotocol.org/docs/brand-protocol/brand-json) | Brand identity specification | + +## Sponsored Intelligence + +Multi-agent orchestration patterns. + +{: .table .table-bordered .table-striped } +| Document | Description | +|----------|-------------| +| [Sponsored Intelligence](https://docs.adcontextprotocol.org/docs/sponsored-intelligence/overview) | Orchestration platform integration | + +## Building with AdCP + +Implementation guides and developer resources. + +{: .table .table-bordered .table-striped } +| Document | Description | +|----------|-------------| +| [Protocol Comparison](https://docs.adcontextprotocol.org/docs/building/understanding/protocol-comparison) | MCP vs A2A protocol comparison | +| [Schemas and SDKs](https://docs.adcontextprotocol.org/docs/building/schemas-and-sdks) | Client libraries (`adcp` PyPI, `@adcp/client` npm) | +| [AdCP Quickstart](https://docs.adcontextprotocol.org/docs/quickstart) | Getting started with the protocol | +| [AdCP Introduction](https://docs.adcontextprotocol.org/docs/intro) | Protocol overview and concepts | +| [Deployment Best Practices](https://docs.adcontextprotocol.org/docs/deployment) | Protocol-level deployment guidance | +| [Configuration](https://docs.adcontextprotocol.org/docs/reference/configuration) | Protocol-level configuration options | + +## Reference + +Glossaries, taxonomies, and version information. + +{: .table .table-bordered .table-striped } +| Document | Description | +|----------|-------------| +| [Glossary](https://docs.adcontextprotocol.org/docs/reference/glossary) | Term definitions for the AdCP ecosystem | +| [Media Channel Taxonomy](https://docs.adcontextprotocol.org/docs/reference/media-channel-taxonomy) | Channel classifications (display, video, audio, native) | +| [GMSF](https://docs.adcontextprotocol.org/docs/reference/gmsf) | Global Media Standards Framework | +| [Release Notes](https://docs.adcontextprotocol.org/docs/reference/release-notes) | AdCP version history | +| [Roadmap](https://docs.adcontextprotocol.org/docs/reference/roadmap) | Planned features and timeline | +| [Implementor FAQ](https://docs.adcontextprotocol.org/docs/reference/implementor-faq) | Answers to common implementation questions | + +## Version Compatibility + +{: .table .table-bordered .table-striped } +| Sales Agent Version | AdCP Spec Version | `adcp` Library | Notes | +|--------------------|-------------------|----------------|-------| +| 1.x | 2.5.0+ | `adcp>=2.5.0` | Initial release | +| 1.x (latest) | 3.2.0+ | `adcp>=3.2.0` | Current | + +## Further Reading + +- [Overview](/agents/salesagent/overview.html) -- What is the Prebid Sales Agent +- [Glossary](/agents/salesagent/glossary.html) -- Sales Agent-specific term definitions +- [Protocols: MCP vs A2A](/agents/salesagent/protocols.html) -- Protocol comparison +- [AdCP Ecosystem Integration](/agents/salesagent/integrations/adcp-ecosystem.html) -- How the Sales Agent connects to other agents diff --git a/agents/salesagent/reference/error-codes.md b/agents/salesagent/reference/error-codes.md new file mode 100644 index 0000000000..6cb181844f --- /dev/null +++ b/agents/salesagent/reference/error-codes.md @@ -0,0 +1,276 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Reference - Error Code Reference +description: Catalog of error codes, error response formats, and troubleshooting guidance for the Prebid Sales Agent +sidebarType: 10 +--- + +# Error Code Reference +{: .no_toc} + +This page documents all error codes returned by the Prebid Sales Agent, their meanings, and how to resolve them. Errors follow a consistent structure across both MCP and A2A protocols, making it straightforward for buying agents to handle failures programmatically. + +- TOC +{:toc} + +## Error Response Structure + +All tool errors are returned as structured objects with a machine-readable `error_code` and a human-readable `message`. The exact format depends on the protocol being used. + +### MCP Error Format + +When a tool call fails over the MCP protocol, the error is returned as a `ToolError` within the MCP tool result. The response includes `isError: true` to distinguish it from a successful result: + +```json +{ + "isError": true, + "content": [ + { + "type": "text", + "text": "{\"error_code\": \"PRODUCT_NOT_FOUND\", \"message\": \"Product 'prod_invalid' does not exist in this publisher's catalog.\", \"details\": {}}" + } + ] +} +``` + +The `text` field contains a JSON-encoded error object with the following fields: + +{: .table .table-bordered .table-striped } +| Field | Type | Description | +|-------|------|-------------| +| `error_code` | `str` | Machine-readable error identifier (e.g., `PRODUCT_NOT_FOUND`) | +| `message` | `str` | Human-readable description of what went wrong | +| `details` | `object` | Optional additional context (validation errors, conflicting values, etc.) | + +### A2A Error Format + +When a request fails over the A2A protocol, the error follows the JSON-RPC 2.0 error format: + +```json +{ + "jsonrpc": "2.0", + "error": { + "code": -32000, + "message": "PRODUCT_NOT_FOUND: Product 'prod_invalid' does not exist in this publisher's catalog.", + "data": { + "error_code": "PRODUCT_NOT_FOUND", + "details": {} + } + }, + "id": 1 +} +``` + +{: .table .table-bordered .table-striped } +| JSON-RPC Field | Description | +|----------------|-------------| +| `error.code` | JSON-RPC error code. Application errors use `-32000` through `-32099`. | +| `error.message` | Combined error code and message string | +| `error.data.error_code` | The AdCP error code, identical to the MCP `error_code` field | +| `error.data.details` | Optional additional context | + +### Parsing Errors in Python + +Use the following pattern to handle errors consistently in your MCP client code: + +```python +from fastmcp import Client + +async with Client(transport) as client: + result = await client.call_tool("create_media_buy", params) + + # Check if the result is an error + if isinstance(result, dict) and result.get("isError"): + import json + error = json.loads(result["content"][0]["text"]) + error_code = error["error_code"] + message = error["message"] + + if error_code == "PRODUCT_NOT_FOUND": + # Re-fetch products and retry + pass + elif error_code == "VALIDATION_ERROR": + # Fix parameters and retry + print(f"Validation failed: {error.get('details', {})}") + else: + print(f"Error [{error_code}]: {message}") + else: + # Success - process the result + print(f"Media buy created: {result['media_buy_id']}") +``` + +## Error Code Catalog + +The following table lists all error codes that the Sales Agent may return. Error codes are consistent across MCP and A2A protocols. + +{: .table .table-bordered .table-striped } +| Error Code | HTTP Status | Description | Resolution | +|------------|-------------|-------------|------------| +| `AUTHENTICATION_REQUIRED` | 401 | Missing or invalid authentication token. The request did not include an `x-adcp-auth` header (MCP) or `Authorization: Bearer` header (A2A), or the provided token does not match any principal. | Provide a valid authentication token. In test mode, use `test-token`. In production, use a token generated via the Admin UI. | +| `TENANT_NOT_FOUND` | 404 | The tenant context could not be resolved from the authentication token. This typically means the token is valid but the associated tenant has been deleted or deactivated. | Verify the deployment configuration and ensure the tenant exists in the database. Contact the publisher administrator. | +| `PRODUCT_NOT_FOUND` | 404 | The referenced `product_id` does not exist in the publisher's catalog, or has been deactivated. | Call [`get_products`](/agents/salesagent/tools/get-products.html) to retrieve valid product IDs before creating a media buy. | +| `MEDIA_BUY_NOT_FOUND` | 404 | The referenced `media_buy_id` does not exist, or belongs to a different tenant or principal. | Call `get_media_buys` to list your active media buys and verify the ID. | +| `FORMAT_INCOMPATIBLE` | 400 | The creative format does not match the requirements of the target product. For example, uploading a display banner to a video-only product. | Call [`list_creative_formats`](/agents/salesagent/tools/list-creative-formats.html) to check which formats the product supports, and ensure your creative's `format_id` matches. | +| `BUDGET_INSUFFICIENT` | 400 | The specified budget is too low for the requested inventory. The budget does not meet the product's minimum spend requirement or cannot deliver meaningful volume at the selected pricing. | Increase the `budget_cents` value in the package, or select a different pricing option with a lower rate. | +| `TARGETING_TOO_NARROW` | 400 | The combination of targeting constraints (countries, devices, audience segments) excludes all available inventory for the selected product. | Broaden the targeting parameters. Remove restrictive filters or expand geographic and device targeting. | +| `POLICY_VIOLATION` | 403 | The request violates content policies or brand safety rules. This may be triggered by the brand category, creative content, or advertiser classification. | Review the publisher's content standards and brand safety requirements. Contact the publisher to understand which policies apply. | +| `INVALID_PRICING_OPTION` | 400 | The `pricing_option_id` specified in a package does not exist for the selected product, or has been discontinued. | Call [`get_products`](/agents/salesagent/tools/get-products.html) to retrieve current `pricing_options` for the product and use a valid `pricing_option_id`. | +| `VALIDATION_ERROR` | 400 | The request failed schema validation. One or more parameters have incorrect types, missing required fields, or invalid values. | Check the `details` field in the error response for specific validation failures. Correct the parameter types and required fields, then retry. See [Validation Errors](#validation-errors) below. | +| `CONFLICT` | 409 | The operation conflicts with the current state of the resource. For example, attempting to update a media buy that has already been cancelled or completed. | Check the current status of the resource with `get_media_buys` before attempting modifications. | +| `RATE_LIMITED` | 429 | Too many requests in a short time period. The server is rate-limiting your client. | Implement exponential backoff and retry after the duration specified in the `Retry-After` header or error details. | +| `INTERNAL_ERROR` | 500 | An unexpected server error occurred. This indicates a bug or infrastructure issue rather than a client error. | Retry the request. If the error persists, check the server logs and report the issue on the [GitHub Issues](https://github.com/prebid/salesagent/issues) page. | + +## Validation Errors + +When a request fails Pydantic schema validation, the error code is `VALIDATION_ERROR` and the `details` field contains structured information about each validation failure. + +### Validation Error Structure + +```json +{ + "error_code": "VALIDATION_ERROR", + "message": "Request validation failed: 2 errors", + "details": { + "errors": [ + { + "field": "packages.0.budget_cents", + "message": "value is not a valid integer", + "type": "int_parsing", + "input": "not-a-number" + }, + { + "field": "start_date", + "message": "invalid date format", + "type": "date_parsing", + "input": "July 1st" + } + ] + } +} +``` + +### Common Validation Failures + +{: .table .table-bordered .table-striped } +| Field | Common Issue | Expected Format | +|-------|-------------|-----------------| +| `budget_cents` | String instead of integer, or negative value | Positive integer (e.g., `500000` for $5,000.00) | +| `start_date` / `end_date` | Non-ISO date format | ISO 8601 date string (e.g., `"2025-07-01"`) | +| `product_id` | Empty string or missing field | Non-empty string matching a valid product | +| `packages` | Empty array | At least one package is required | +| `pricing_option_id` | Missing from package | Required string referencing a valid pricing option | +| `format_id` | Incorrect format identifier | String matching a format from `list_creative_formats` | +| `brief` | Empty string | Non-empty natural language description | +| `media_buy_id` | Missing or null | Non-empty string referencing an existing media buy | + +### How Pydantic Validation Works + +The Sales Agent uses [Pydantic v2](https://docs.pydantic.dev/) for request validation. When a tool receives parameters, Pydantic validates them against the tool's schema before any business logic executes. This means: + +1. **Type coercion is limited** -- Pydantic will attempt reasonable coercions (e.g., `"123"` to `123` for integer fields) but will reject clearly invalid types. +2. **Required fields are enforced** -- Missing required fields produce a `VALIDATION_ERROR` before the tool logic runs. +3. **Nested validation** -- Objects like `packages`, `brand_manifest`, and `targeting` are validated recursively. An error in a nested field reports the full dotted path (e.g., `packages.0.budget_cents`). + +## Troubleshooting Tips + +### "AUTHENTICATION_REQUIRED" on every request + +Verify that your transport is sending the correct header: + +```python +# Correct - lowercase header name +transport = StreamableHttpTransport( + "http://localhost:8000/mcp/", + headers={"x-adcp-auth": "test-token"} # MCP auth header +) +``` + +Common mistakes: +- Using `Authorization: Bearer` instead of `x-adcp-auth` for MCP connections (Bearer is for A2A) +- Forgetting to include the header in the transport constructor +- Using an expired or revoked token + +### "PRODUCT_NOT_FOUND" when creating a media buy + +Products are tenant-scoped. If you received a `product_id` from a different tenant or from documentation examples, it will not work. Always call `get_products` first to discover valid IDs for your authenticated tenant: + +```python +products = await client.call_tool("get_products", {"brief": "all products"}) +valid_ids = [p["product_id"] for p in products["products"]] +``` + +### "FORMAT_INCOMPATIBLE" when uploading creatives + +This error means the `format_id` on your creative does not match any format supported by the product in the media buy. Debug by checking both sides: + +```python +# Check what formats the product supports +product = products["products"][0] +print(f"Product formats: {product['format_ids']}") + +# Check format specifications +formats = await client.call_tool( + "list_creative_formats", + {"format_ids": product["format_ids"]} +) +for fmt in formats["formats"]: + print(f" {fmt['format_id']}: {fmt['media_type']} " + f"{fmt['width']}x{fmt['height']}") +``` + +### "VALIDATION_ERROR" with no obvious cause + +Check the `details.errors` array for the specific field and type. Common issues include: + +- **Date formats** -- Use ISO 8601 (`"2025-07-01"`), not locale-specific formats +- **Budget as float** -- `budget_cents` must be an integer, not a float (use `500000`, not `5000.00`) +- **Missing packages** -- `create_media_buy` requires at least one entry in the `packages` array +- **Nested objects** -- `brand_manifest`, `targeting`, and `metadata` must be objects, not strings + +### "TARGETING_TOO_NARROW" for valid-looking targeting + +The intersection of all targeting constraints must match available inventory. Try removing constraints one at a time to identify which filter is too restrictive: + +```python +# Start broad, then narrow +targeting_tests = [ + {}, # No targeting - should always work + {"countries": ["US"]}, + {"countries": ["US"], "devices": ["desktop"]}, + {"countries": ["US"], "devices": ["desktop"], "audiences": ["sports_fans"]}, +] + +for targeting in targeting_tests: + try: + result = await client.call_tool("create_media_buy", { + # ... other params ... + "packages": [{"targeting": targeting, ...}] + }) + print(f"OK with targeting: {targeting}") + break + except Exception as e: + print(f"Failed with targeting: {targeting}") +``` + +### "INTERNAL_ERROR" responses + +Internal errors indicate server-side issues rather than client mistakes. Steps to diagnose: + +1. Check the server logs: `docker compose logs salesagent` +2. Verify the database is accessible: `curl http://localhost:8000/health` +3. Retry the request -- transient errors (database timeouts, adapter failures) may resolve on retry +4. If using the mock adapter, ensure no test session state has become corrupted + +{: .alert.alert-info :} +If you encounter persistent `INTERNAL_ERROR` responses, file an issue on the [prebid/salesagent GitHub repository](https://github.com/prebid/salesagent/issues) with the full error response and server logs. + +## Further Reading + +- [Tool Reference](/agents/salesagent/tools/tool-reference.html) -- Complete catalog of MCP tools and their parameters +- [get_products](/agents/salesagent/tools/get-products.html) -- Product discovery tool reference +- [create_media_buy](/agents/salesagent/tools/create-media-buy.html) -- Media buy creation tool reference +- [update_media_buy](/agents/salesagent/tools/update-media-buy.html) -- Media buy modification tool reference +- [sync_creatives](/agents/salesagent/tools/sync-creatives.html) -- Creative management tool reference +- [Campaign Lifecycle Tutorial](/agents/salesagent/tutorials/campaign-lifecycle.html) -- End-to-end walkthrough with working code +- [Architecture & Protocols](/agents/salesagent/architecture.html) -- MCP vs A2A protocol details +- [AdCP Error Specification](https://docs.adcontextprotocol.org/docs/intro) -- Protocol-level error handling standards diff --git a/agents/salesagent/schemas/adcp-types.md b/agents/salesagent/schemas/adcp-types.md new file mode 100644 index 0000000000..22425cd0c5 --- /dev/null +++ b/agents/salesagent/schemas/adcp-types.md @@ -0,0 +1,217 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Schemas - AdCP Protocol Types +description: Reference for AdCP types imported from the adcp Python library +sidebarType: 10 +--- + +# AdCP Protocol Types +{: .no_toc} + +The Prebid Sales Agent imports protocol-level types from the `adcp` PyPI package (`adcp>=3.2.0`). These types define the canonical schemas for AdCP protocol messages and are used throughout the tool implementations. + +- TOC +{:toc} + +## Overview + +The `adcp` library provides Pydantic models that enforce the AdCP specification at the type level. The Sales Agent uses these types for request validation, response serialization, and interoperability with other AdCP-compliant agents. + +```python +# Example import in Sales Agent source +from adcp.types import BrandManifest, Product, Package, PricingOption +``` + +## Core Types + +### BrandManifest + +Brand metadata submitted by buying agents with media buy requests. Used for compliance checks and personalization. + +{: .table .table-bordered .table-striped } +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `brand_name` | `str` | Yes | Advertiser brand name | +| `brand_url` | `str` | No | Brand website URL | +| `logo_url` | `str` | No | Brand logo URL | +| `brand_category` | `str` | No | IAB brand category | + +### Product + +Advertising product definition returned by `get_products`. + +{: .table .table-bordered .table-striped } +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `product_id` | `str` | Yes | Unique product identifier | +| `name` | `str` | Yes | Product name | +| `description` | `str` | Yes | Description for AI-powered search | +| `delivery_type` | `DeliveryType` | Yes | `guaranteed` or `non_guaranteed` | +| `pricing_options` | `list[PricingOption]` | Yes | Available pricing configurations | +| `format_ids` | `list[str]` | Yes | Compatible creative format identifiers | +| `channels` | `list[str]` | Yes | Delivery channels | +| `countries` | `list[str]` | Yes | Geographic availability (ISO 3166-1 alpha-2) | + +### Package + +A line item within a media buy, bundling a product with targeting and budget. + +{: .table .table-bordered .table-striped } +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `package_id` | `str` | Yes | Unique package identifier | +| `product_id` | `str` | Yes | Reference to the purchased product | +| `budget` | `TotalBudget` | Yes | Budget specification | +| `targeting` | `TargetingOverlay` | No | Targeting criteria | +| `pricing` | `PricingOption` | Yes | Negotiated pricing | +| `start_date` | `date` | No | Package-level start date | +| `end_date` | `date` | No | Package-level end date | + +### PricingOption + +Available pricing configuration for a product or package. + +{: .table .table-bordered .table-striped } +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `model` | `PricingModel` | Yes | Pricing model type | +| `amount` | `float` | Yes | Price amount | +| `currency` | `str` | Yes | ISO 4217 currency code | +| `minimum_spend` | `float` | No | Minimum spend threshold | + +### CreativeAsset + +Creative file metadata for uploads via `sync_creatives`. + +{: .table .table-bordered .table-striped } +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `format_id` | `str` | Yes | Creative format identifier | +| `name` | `str` | Yes | Creative display name | +| `asset_url` | `str` | Yes | URL to the creative asset file | +| `click_through_url` | `str` | No | Landing page URL | +| `metadata` | `dict` | No | Additional creative metadata | + +### CreativeAssignment + +Maps creatives to packages within a media buy. + +{: .table .table-bordered .table-striped } +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `package_id` | `str` | Yes | Target package identifier | +| `creative_ids` | `list[str]` | Yes | Creative identifiers to assign | + +### TargetingOverlay + +Targeting criteria applied to packages or media buys. + +{: .table .table-bordered .table-striped } +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `geo_countries` | `list[str]` | No | ISO 3166-1 alpha-2 country codes | +| `geo_regions` | `list[str]` | No | Region/state codes | +| `geo_metros` | `list[str]` | No | Metro area / DMA codes | +| `devices` | `list[str]` | No | Device types | +| `content_categories` | `list[str]` | No | IAB content categories | +| `custom_targeting` | `dict[str, list[str]]` | No | Key-value targeting pairs | + +### FormatID + +Creative format identifier following the AdCP structure. + +{: .table .table-bordered .table-striped } +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `agent_url` | `str` | Yes | URL of the agent that defined this format | +| `id` | `str` | Yes | Format identifier within that agent | + +### TotalBudget + +Campaign or package budget specification. + +{: .table .table-bordered .table-striped } +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `amount` | `float` | Yes | Budget amount | +| `currency` | `str` | Yes | ISO 4217 currency code | + +### ReportingWebhook + +Webhook configuration for async notifications on campaign status changes. + +{: .table .table-bordered .table-striped } +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `url` | `str` | Yes | Webhook endpoint URL | +| `events` | `list[str]` | No | Event types to subscribe to | + +## Enums + +### PricingModel + +{: .table .table-bordered .table-striped } +| Value | Description | +|-------|-------------| +| `cpm` | Cost per mille (thousand impressions) | +| `vcpm` | Viewable cost per mille | +| `cpc` | Cost per click | +| `cpcv` | Cost per completed view (video) | +| `cpv` | Cost per view | +| `cpp` | Cost per point (reach-based) | +| `flat_rate` | Fixed cost per time period | + +### MediaBuyStatus + +{: .table .table-bordered .table-striped } +| Value | Description | +|-------|-------------| +| `pending` | Awaiting publisher approval | +| `approved` | Approved, ready to activate | +| `active` | Delivering impressions | +| `paused` | Temporarily stopped | +| `completed` | Campaign finished | +| `failed` | Creation or activation failed | + +### CreativeStatus + +{: .table .table-bordered .table-striped } +| Value | Description | +|-------|-------------| +| `pending_review` | Awaiting publisher review | +| `approved` | Passed review | +| `rejected` | Failed review | +| `processing` | Being processed by the ad server | + +### DeliveryType + +{: .table .table-bordered .table-striped } +| Value | Description | +|-------|-------------| +| `guaranteed` | Reserved inventory with delivery guarantees | +| `non_guaranteed` | Best-effort delivery without guarantees | + +### FormatCategory + +{: .table .table-bordered .table-striped } +| Value | Description | +|-------|-------------| +| `display` | Banner and rich media formats | +| `video` | Video ad formats (pre-roll, mid-roll, etc.) | +| `audio` | Audio ad formats | +| `native` | Native ad formats | + +## JSON Schema Validation + +The `adcp` library validates request and response data against JSON schemas. Schemas follow semantic versioning: + +- Versioned path: `/schemas/2.5.0/` +- Major version alias: `/schemas/v2/` + +When the Sales Agent receives a tool call, Pydantic validates the input against these schemas before any business logic executes. Invalid inputs produce a `VALIDATION_ERROR` response. + +## Further Reading + +- [API Schema Reference](/agents/salesagent/schemas/api-schemas.html) -- Pydantic request/response models +- [Database Models](/agents/salesagent/schemas/database-models.html) -- SQLAlchemy ORM models +- [AdCP Schemas and SDKs](https://docs.adcontextprotocol.org/docs/building/schemas-and-sdks) -- Upstream schema definitions and client libraries diff --git a/agents/salesagent/schemas/api-schemas.md b/agents/salesagent/schemas/api-schemas.md new file mode 100644 index 0000000000..c4696f8953 --- /dev/null +++ b/agents/salesagent/schemas/api-schemas.md @@ -0,0 +1,475 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Schemas - API Schema Reference +description: Complete reference for all Pydantic request/response models, data types, and enums in the Prebid Sales Agent +sidebarType: 10 +--- + +# API Schema Reference +{: .no_toc} + +All data entering and leaving the Prebid Sales Agent is validated through Pydantic models defined in `src/core/schemas.py`. This page provides a complete reference for request/response models, core data types, enums, and JSON examples. + +- TOC +{:toc} + +## Overview + +The Sales Agent uses Pydantic `BaseModel` classes as a validation and serialization layer between the protocol servers (MCP/A2A) and the business logic. Every tool request is validated against a request model before processing, and every response is serialized through a response model before being sent to the AI agent. + +This approach provides: + +- **Type safety** -- All fields are typed and validated at runtime +- **Documentation** -- Models are self-documenting with field descriptions +- **Serialization** -- Automatic conversion to/from JSON for protocol transport +- **Consistency** -- Both MCP and A2A use the same models, ensuring identical behavior + +## Request/Response Models by Tool + +Each MCP tool has a corresponding request model (for input validation) and response model (for output serialization). + +{: .table .table-bordered .table-striped } +| Tool | Request Model | Response Model | +|------|--------------|----------------| +| `get_adcp_capabilities` | `GetAdcpCapabilitiesRequest` | `GetAdcpCapabilitiesResponse` | +| `get_products` | `GetProductsRequest` | `GetProductsResponse` | +| `list_creative_formats` | `ListCreativeFormatsRequest` | `ListCreativeFormatsResponse` | +| `create_media_buy` | `CreateMediaBuyRequest` | `CreateMediaBuyResponse` | +| `update_media_buy` | `UpdateMediaBuyRequest` | `UpdateMediaBuyResponse` | +| `get_media_buys` | `GetMediaBuysRequest` | `GetMediaBuysResponse` | +| `get_media_buy_delivery` | `GetMediaBuyDeliveryRequest` | `GetMediaBuyDeliveryResponse` | +| `sync_creatives` | `SyncCreativesRequest` | `SyncCreativesResponse` | +| `list_creatives` | `ListCreativesRequest` | `ListCreativesResponse` | +| `list_authorized_properties` | `ListAuthorizedPropertiesRequest` | `ListAuthorizedPropertiesResponse` | +| `update_performance_index` | `UpdatePerformanceIndexRequest` | `UpdatePerformanceIndexResponse` | +| `list_tasks` | `ListTasksRequest` | `ListTasksResponse` | +| `get_task` | `GetTaskRequest` | `GetTaskResponse` | +| `complete_task` | `CompleteTaskRequest` | `CompleteTaskResponse` | + +## Core Data Models + +### Product + +Represents an advertising product in the publisher's catalog. Products are what AI buying agents discover and purchase. + +{: .table .table-bordered .table-striped } +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `product_id` | `str` | Yes | Unique product identifier | +| `name` | `str` | Yes | Human-readable product name | +| `description` | `str` | Yes | Detailed description used for AI-powered search matching | +| `delivery_type` | `DeliveryType` | Yes | `guaranteed` or `non_guaranteed` | +| `format_ids` | `list[str]` | Yes | Compatible creative format identifiers | +| `pricing_options` | `list[PricingOption]` | Yes | Available pricing models with rates | +| `estimated_exposures` | `ExposureEstimate` | No | Estimated impression/exposure volumes | +| `channels` | `list[str]` | Yes | Delivery channels (e.g., `display`, `video`, `audio`, `native`) | +| `countries` | `list[str]` | Yes | Geographic availability as ISO 3166-1 alpha-2 codes | + +### Creative + +Represents an advertising creative asset uploaded by a buying agent. + +{: .table .table-bordered .table-striped } +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `creative_id` | `str` | Yes | Unique creative identifier | +| `name` | `str` | Yes | Human-readable creative name | +| `format_id` | `str` | Yes | Creative format identifier (must match a supported format) | +| `assets` | `list[CreativeAsset]` | Yes | Creative asset files (images, video, HTML) | +| `status` | `CreativeStatus` | Yes | Processing status (`pending_review`, `approved`, `rejected`, `processing`) | +| `approval_status` | `str` | Yes | Publisher approval status | +| `created_at` | `datetime` | Yes | Creation timestamp (ISO 8601) | +| `tags` | `list[str]` | No | Optional tags for organization and filtering | + +### MediaPackage (Package) + +Represents a media buy package -- a bundle of one or more products with targeting and budget. + +{: .table .table-bordered .table-striped } +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `package_id` | `str` | Yes | Unique package identifier | +| `product_id` | `str` | Yes | Reference to the purchased product | +| `budget` | `BudgetSpec` | Yes | Budget specification (amount, currency) | +| `targeting` | `Targeting` | No | Targeting criteria for this package | +| `pricing` | `Pricing` | Yes | Negotiated pricing for this package | + +### Principal + +Represents an authenticated entity (AI agent or human user) that interacts with the Sales Agent. + +{: .table .table-bordered .table-striped } +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `principal_id` | `str` | Yes | Unique principal identifier | +| `name` | `str` | Yes | Display name of the agent or user | +| `type` | `str` | Yes | Principal type (e.g., `agent`, `human`, `service`) | +| `email` | `str` | Yes | Contact email (composite unique with tenant_id) | +| `ad_server_ids` | `dict[str, str]` | No | Mapping of ad server type to external advertiser ID | + +### Targeting + +Defines geographic and custom targeting criteria for media buys and packages. + +{: .table .table-bordered .table-striped } +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `geo_countries` | `list[str]` | No | Target countries (ISO 3166-1 alpha-2 codes, e.g., `["US", "GB"]`) | +| `geo_regions` | `list[str]` | No | Target regions/states (e.g., `["US-CA", "US-NY"]`) | +| `geo_metros` | `list[str]` | No | Target metro areas / DMAs (Nielsen DMA codes) | +| `geo_postal_areas` | `list[str]` | No | Target postal/ZIP codes | +| `custom_targeting` | `dict[str, list[str]]` | No | Key-value targeting pairs (e.g., `{"section": ["sports", "news"]}`) | + +### Pricing + +Specifies the pricing model and rate for a media buy or package. + +{: .table .table-bordered .table-striped } +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `pricing_model` | `PricingModel` | Yes | Pricing model enum value (see Enums section) | +| `bid_price` | `float` | Yes | Price amount in the specified currency | +| `currency` | `str` | Yes | ISO 4217 currency code (e.g., `USD`, `EUR`, `GBP`) | + +### PricingOption + +Represents one available pricing configuration for a product. + +{: .table .table-bordered .table-striped } +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `model` | `PricingModel` | Yes | Pricing model type | +| `amount` | `float` | Yes | Price amount | +| `currency` | `str` | Yes | ISO 4217 currency code | +| `minimum_spend` | `float` | No | Minimum spend threshold for this pricing tier | + +### ExposureEstimate + +Estimated impression or exposure volumes for a product. + +{: .table .table-bordered .table-striped } +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `min_impressions` | `int` | Yes | Lower bound of estimated impressions | +| `max_impressions` | `int` | Yes | Upper bound of estimated impressions | +| `period` | `str` | Yes | Time period for the estimate (e.g., `daily`, `weekly`, `monthly`) | + +### DeliveryReport + +Delivery metrics returned by the ad server adapter. + +{: .table .table-bordered .table-striped } +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `impressions` | `int` | Yes | Total impressions delivered | +| `clicks` | `int` | Yes | Total clicks | +| `spend` | `float` | Yes | Total spend in campaign currency | +| `ctr` | `float` | Yes | Click-through rate (clicks / impressions) | +| `start_date` | `datetime` | Yes | Report period start | +| `end_date` | `datetime` | Yes | Report period end | + +## Enums + +### PricingModel + +Defines the available pricing models for advertising products. + +{: .table .table-bordered .table-striped } +| Value | Description | +|-------|-------------| +| `cpm` | Cost per mille (thousand impressions) | +| `vcpm` | Viewable cost per mille (viewable impressions only) | +| `cpc` | Cost per click | +| `cpcv` | Cost per completed view (video) | +| `cpv` | Cost per view | +| `cpp` | Cost per point (reach-based) | +| `flat_rate` | Fixed cost per time period (day, week, month) | + +```python +class PricingModel(str, Enum): + cpm = "cpm" + vcpm = "vcpm" + cpc = "cpc" + cpcv = "cpcv" + cpv = "cpv" + cpp = "cpp" + flat_rate = "flat_rate" +``` + +### MediaBuyStatus + +Tracks the lifecycle state of a media buy. + +{: .table .table-bordered .table-striped } +| Value | Description | +|-------|-------------| +| `pending` | Created but awaiting publisher approval | +| `approved` | Approved by publisher, ready to activate | +| `active` | Currently delivering impressions | +| `paused` | Temporarily stopped by buyer or publisher | +| `completed` | Flight dates ended, campaign finished | +| `failed` | Creation or activation failed (see error details) | + +```python +class MediaBuyStatus(str, Enum): + pending = "pending" + approved = "approved" + active = "active" + paused = "paused" + completed = "completed" + failed = "failed" +``` + +### CreativeStatus + +Tracks the processing and approval state of a creative asset. + +{: .table .table-bordered .table-striped } +| Value | Description | +|-------|-------------| +| `pending_review` | Uploaded and awaiting publisher review | +| `approved` | Passed review and ready for delivery | +| `rejected` | Failed review (see rejection reason) | +| `processing` | Being processed by the ad server (transcoding, validation) | + +```python +class CreativeStatus(str, Enum): + pending_review = "pending_review" + approved = "approved" + rejected = "rejected" + processing = "processing" +``` + +### DeliveryType + +Specifies whether a product offers guaranteed or non-guaranteed delivery. + +{: .table .table-bordered .table-striped } +| Value | Description | +|-------|-------------| +| `guaranteed` | Publisher guarantees delivery of contracted impressions | +| `non_guaranteed` | Best-effort delivery with no volume commitment | + +```python +class DeliveryType(str, Enum): + guaranteed = "guaranteed" + non_guaranteed = "non_guaranteed" +``` + +### FormatCategory + +Top-level creative format categories. + +{: .table .table-bordered .table-striped } +| Value | Description | +|-------|-------------| +| `audio` | Audio ad formats (DAAST-compliant) | +| `video` | Video ad formats (VAST-compliant) | +| `display` | Standard IAB display banner formats | +| `native` | Native ad formats with structured content | + +```python +class FormatCategory(str, Enum): + audio = "audio" + video = "video" + display = "display" + native = "native" +``` + +## JSON Examples + +### Product Discovery (get_products) + +**Request:** + +```json +{ + "brief": "premium video ads targeting sports fans in the US", + "filters": { + "channels": ["video"], + "countries": ["US"] + } +} +``` + +**Response:** + +```json +{ + "products": [ + { + "product_id": "prod_abc123", + "name": "Premium Video - Sports Audience", + "description": "Pre-roll and mid-roll video inventory across sports content network", + "delivery_type": "guaranteed", + "format_ids": ["video_16x9_preroll", "video_16x9_midroll"], + "pricing_options": [ + { + "model": "cpm", + "amount": 25.00, + "currency": "USD" + }, + { + "model": "cpcv", + "amount": 0.08, + "currency": "USD" + } + ], + "estimated_exposures": { + "min_impressions": 100000, + "max_impressions": 500000, + "period": "monthly" + }, + "channels": ["video"], + "countries": ["US"] + } + ], + "total_results": 1 +} +``` + +### Campaign Creation (create_media_buy) + +**Request:** + +```json +{ + "product_id": "prod_abc123", + "advertiser_id": "adv_456", + "name": "Sports Video Campaign - Q2", + "budget_cents": 250000, + "start_date": "2025-04-01", + "end_date": "2025-06-30", + "targeting": { + "geo_countries": ["US"], + "geo_regions": ["US-CA", "US-NY", "US-TX"], + "custom_targeting": { + "section": ["sports"], + "device": ["mobile", "ctv"] + } + }, + "pricing": { + "pricing_model": "cpm", + "bid_price": 25.00, + "currency": "USD" + } +} +``` + +**Response:** + +```json +{ + "media_buy_id": "mb_789xyz", + "status": "pending", + "product_id": "prod_abc123", + "name": "Sports Video Campaign - Q2", + "budget_cents": 250000, + "start_date": "2025-04-01", + "end_date": "2025-06-30", + "created_at": "2025-03-15T14:30:00Z", + "ad_server_order_id": "gam_order_12345", + "approval_required": true +} +``` + +### Creative Upload (sync_creatives) + +**Request:** + +```json +{ + "media_buy_id": "mb_789xyz", + "creatives": [ + { + "name": "Sports Video 30s - Version A", + "format_id": "video_16x9_preroll", + "assets": [ + { + "type": "video", + "url": "https://cdn.example.com/creatives/sports_30s_v1.mp4", + "mime_type": "video/mp4", + "duration_seconds": 30 + } + ], + "tags": ["sports", "q2-campaign", "version-a"] + } + ] +} +``` + +**Response:** + +```json +{ + "creatives": [ + { + "creative_id": "cr_abc123", + "name": "Sports Video 30s - Version A", + "format_id": "video_16x9_preroll", + "status": "processing", + "approval_status": "pending_review", + "created_at": "2025-03-15T14:35:00Z", + "ad_server_creative_id": "gam_creative_67890" + } + ], + "sync_status": "submitted" +} +``` + +### Delivery Report (get_media_buy_delivery) + +**Request:** + +```json +{ + "media_buy_id": "mb_789xyz" +} +``` + +**Response:** + +```json +{ + "media_buy_id": "mb_789xyz", + "status": "active", + "delivery": { + "impressions": 47523, + "clicks": 891, + "spend": 1188.08, + "ctr": 0.0188, + "start_date": "2025-04-01T00:00:00Z", + "end_date": "2025-04-15T23:59:59Z" + }, + "budget_remaining_cents": 131192, + "pacing": "on_track", + "flight_progress_percent": 16.7 +} +``` + +## Validation Rules + +Pydantic enforces the following validation rules across all models: + +{: .table .table-bordered .table-striped } +| Rule | Applied To | Description | +|------|-----------|-------------| +| Required fields | All models | Fields without defaults must be provided | +| Type coercion | Numeric fields | Strings are coerced to numbers where possible | +| Enum validation | `PricingModel`, `MediaBuyStatus`, etc. | Values must match defined enum members | +| Date validation | `start_date`, `end_date` | Must be valid ISO 8601 date strings | +| Currency codes | `currency` fields | Should follow ISO 4217 format | +| Country codes | `countries`, `geo_countries` | Should follow ISO 3166-1 alpha-2 format | +| Non-empty strings | `name`, `brief`, `description` | Must contain at least one character | +| Positive amounts | `budget_cents`, `bid_price` | Must be greater than zero | + +{: .alert.alert-info :} +Validation errors are returned as structured objects with `error_code: "validation_error"` and a `details` field listing the specific field violations. + +## Further Reading + +- [Source Architecture](/agents/salesagent/developers/source-architecture.html) -- Where schemas fit in the codebase +- [Tool Reference](/agents/salesagent/tools/tool-reference.html) -- Tool parameters and response types +- [get_products](/agents/salesagent/tools/get-products.html) -- Product discovery with detailed schema examples +- [Architecture & Protocols](/agents/salesagent/architecture.html) -- Data flow through the validation layer +- [AdCP Protocol Types](https://docs.adcontextprotocol.org/docs/reference/types) -- Standard AdCP type definitions diff --git a/agents/salesagent/schemas/database-models.md b/agents/salesagent/schemas/database-models.md new file mode 100644 index 0000000000..e8cd0d94e9 --- /dev/null +++ b/agents/salesagent/schemas/database-models.md @@ -0,0 +1,207 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Schemas - Database Models +description: SQLAlchemy ORM model reference for the Prebid Sales Agent database schema +sidebarType: 10 +--- + +# Database Models +{: .no_toc} + +The Prebid Sales Agent uses PostgreSQL with SQLAlchemy ORM for data persistence. All models enforce tenant isolation through `tenant_id` foreign keys. Schema changes are managed via Alembic migrations. + +- TOC +{:toc} + +## Entity Relationships + +```text +tenants + ├── users (many) — composite unique: (tenant_id, email) + ├── principals (many) — advertiser/agent accounts + │ └── media_buys (many) + │ ├── packages (many) + │ └── creatives (many) + ├── products (many) — advertising inventory + ├── adapter_config (one) — ad server configuration + └── audit_logs (many) — operation history +``` + +## Tenant + +Represents a publisher entity. Each tenant operates in isolation with its own products, advertisers, and ad server configuration. + +{: .table .table-bordered .table-striped } +| Column | Type | Nullable | Description | +|--------|------|----------|-------------| +| `id` | UUID | No | Primary key | +| `name` | str | No | Publisher display name | +| `domain` | str | No | Publisher domain | +| `is_active` | bool | No | Whether the tenant is active (soft delete) | +| `adapter_type` | str | Yes | Ad server adapter type (`gam`, `mock`) | +| `created_at` | datetime | No | Creation timestamp | +| `updated_at` | datetime | No | Last update timestamp | + +## User + +Admin UI user account. Users are scoped to a single tenant via a composite unique constraint. + +{: .table .table-bordered .table-striped } +| Column | Type | Nullable | Description | +|--------|------|----------|-------------| +| `id` | UUID | No | Primary key | +| `tenant_id` | UUID (FK) | No | References `tenants.id` | +| `email` | str | No | User email (unique within tenant) | +| `name` | str | No | Display name | +| `role` | str | No | User role (`admin`, `viewer`) | +| `is_active` | bool | No | Whether the user is active | + +{: .alert.alert-info :} +The composite unique constraint `(tenant_id, email)` allows the same email address to exist independently in different tenants with separate identities. + +## Principal + +An authenticated entity (AI buying agent or human user) that interacts with the Sales Agent via MCP or A2A. + +{: .table .table-bordered .table-striped } +| Column | Type | Nullable | Description | +|--------|------|----------|-------------| +| `id` | UUID | No | Primary key | +| `tenant_id` | UUID (FK) | No | References `tenants.id` | +| `name` | str | No | Display name | +| `email` | str | No | Contact email | +| `token_hash` | str | Yes | Hashed authentication token | +| `ad_server_ids` | JSON | Yes | Mapping of ad server type to external advertiser ID | +| `is_active` | bool | No | Whether the principal is active | + +## Product + +An advertising product in the publisher's catalog. Products are what AI buying agents discover via `get_products` and purchase via `create_media_buy`. + +{: .table .table-bordered .table-striped } +| Column | Type | Nullable | Description | +|--------|------|----------|-------------| +| `id` | UUID | No | Primary key | +| `tenant_id` | UUID (FK) | No | References `tenants.id` | +| `name` | str | No | Product name | +| `description` | str | No | Description used for AI-powered search matching | +| `delivery_type` | str | No | `guaranteed` or `non_guaranteed` | +| `format_ids` | JSON | No | Compatible creative format identifiers | +| `pricing_options` | JSON | No | Available pricing models with rates | +| `channels` | JSON | No | Delivery channels (`display`, `video`, `audio`, `native`) | +| `countries` | JSON | No | Geographic availability (ISO 3166-1 alpha-2 codes) | +| `is_active` | bool | No | Whether the product is active | + +## MediaBuy + +A campaign created by a buying agent. Tracks the full lifecycle from submission through delivery. + +{: .table .table-bordered .table-striped } +| Column | Type | Nullable | Description | +|--------|------|----------|-------------| +| `id` | UUID | No | Primary key | +| `tenant_id` | UUID (FK) | No | References `tenants.id` | +| `principal_id` | UUID (FK) | No | References `principals.id` | +| `campaign_name` | str | No | Human-readable campaign name | +| `buyer_ref` | str | Yes | Buyer-side reference ID for reconciliation | +| `status` | str | No | Lifecycle status (`pending`, `approved`, `active`, `paused`, `completed`, `failed`) | +| `start_date` | date | No | Campaign start date | +| `end_date` | date | No | Campaign end date | +| `created_at` | datetime | No | Creation timestamp | + +## Package + +A line item within a media buy, bundling a product with targeting and budget. + +{: .table .table-bordered .table-striped } +| Column | Type | Nullable | Description | +|--------|------|----------|-------------| +| `id` | UUID | No | Primary key | +| `media_buy_id` | UUID (FK) | No | References `media_buys.id` | +| `product_id` | str | No | References a product in the catalog | +| `budget` | float | No | Package budget amount | +| `targeting` | JSON | Yes | Targeting criteria (geo, device, content) | +| `pricing` | JSON | No | Negotiated pricing for this package | + +## Creative + +A creative asset uploaded by a buying agent. + +{: .table .table-bordered .table-striped } +| Column | Type | Nullable | Description | +|--------|------|----------|-------------| +| `id` | UUID | No | Primary key | +| `tenant_id` | UUID (FK) | No | References `tenants.id` | +| `name` | str | No | Creative display name | +| `format_id` | str | No | Creative format identifier | +| `status` | str | No | Processing status (`pending_review`, `approved`, `rejected`, `processing`) | +| `assets` | JSON | No | Creative asset files (images, video, HTML) | +| `tags` | JSON | Yes | Optional tags for organization and filtering | + +## AdapterConfig + +Ad server configuration for a tenant. Connection credentials are encrypted at rest using Fernet. + +{: .table .table-bordered .table-striped } +| Column | Type | Nullable | Description | +|--------|------|----------|-------------| +| `id` | UUID | No | Primary key | +| `tenant_id` | UUID (FK) | No | References `tenants.id` | +| `adapter_type` | str | No | Adapter identifier (`gam`, `mock`) | +| `connection_config` | encrypted JSON | No | Encrypted adapter connection settings | +| `product_config` | JSON | Yes | Per-product adapter configuration | + +## AuditLog + +Operation log entry for compliance, debugging, and security analysis. + +{: .table .table-bordered .table-striped } +| Column | Type | Nullable | Description | +|--------|------|----------|-------------| +| `id` | UUID | No | Primary key | +| `tenant_id` | str | Yes | Tenant context | +| `timestamp` | datetime | No | Event timestamp (UTC) | +| `operation` | str | No | Operation type (e.g., `media_buy.created`) | +| `principal_name` | str | Yes | Name of the acting principal | +| `principal_id` | str | Yes | ID of the acting principal | +| `adapter_id` | str | Yes | Ad server adapter ID | +| `success` | bool | No | Whether the operation succeeded | +| `error_message` | str | Yes | Error message if operation failed | +| `details` | JSONB | Yes | Operation-specific metadata | + +## WorkflowStep + +Tracks human-in-the-loop approval tasks for media buys and creatives. + +{: .table .table-bordered .table-striped } +| Column | Type | Nullable | Description | +|--------|------|----------|-------------| +| `id` | UUID | No | Primary key | +| `tenant_id` | str | No | Tenant context | +| `context_id` | str | No | Associated workflow context | +| `status` | str | No | Task status (`pending`, `in_progress`, `completed`, `failed`, `requires_approval`) | +| `type` | str | No | Task type (`media_buy`, `creative`, `product`) | +| `tool_name` | str | No | Name of the MCP tool that created this task | +| `owner` | str | Yes | Assigned owner | +| `request_data` | JSON | Yes | Input data for the task | +| `response_data` | JSON | Yes | Output data from the task | +| `error_message` | str | Yes | Error message if task failed | + +## Tenant Isolation + +All database queries are automatically scoped by `tenant_id`. This ensures: + +- Each tenant's data is isolated from all other tenants +- A user with the same email across multiple tenants has separate, independent identities +- Principal tokens are scoped to a single tenant + +## Soft Delete + +Tenant deactivation sets `is_active = false` without removing data. All API access is blocked immediately, but data is preserved for potential reactivation by a super admin. + +## Further Reading + +- [Source Architecture](/agents/salesagent/developers/source-architecture.html) -- Code structure and module map +- [Database Migrations](/agents/salesagent/developers/migrations.html) -- Alembic migration patterns +- [Security Model](/agents/salesagent/operations/security.html) -- Access control and encryption +- [API Schema Reference](/agents/salesagent/schemas/api-schemas.html) -- Pydantic request/response models diff --git a/agents/salesagent/tools/content-standards.md b/agents/salesagent/tools/content-standards.md new file mode 100644 index 0000000000..56737247d6 --- /dev/null +++ b/agents/salesagent/tools/content-standards.md @@ -0,0 +1,34 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Tools - Content Standards Tools +description: Tools for managing publisher brand safety rules and content policies +sidebarType: 10 +--- + +# Content Standards Tools +{: .no_toc} + +Four tools for managing publisher brand safety rules and content policies: `get_content_standards`, `create_content_standards`, `update_content_standards`, and `list_content_standards`. These are part of the AdCP Governance protocol and allow buying agents to discover and comply with publisher content requirements before submitting creatives. + +{: .alert.alert-warning :} +These tools are defined in the AdCP specification but are not yet implemented in the Prebid Sales Agent. They are planned for a future release. + +## Tools in This Group + +{: .table .table-bordered .table-striped } +| Tool | Description | +|------|-------------| +| `get_content_standards` | Retrieves a specific content standards document by ID | +| `create_content_standards` | Creates a new set of content standards for a publisher | +| `update_content_standards` | Modifies an existing content standards document | +| `list_content_standards` | Lists all content standards documents for a publisher | + +## AdCP Specification + +These tools are part of the [AdCP Governance Protocol](https://docs.adcontextprotocol.org/docs/governance/overview). See the protocol specification for the canonical parameter definitions and expected behavior. + +## Related Tools + +- [Tool Reference](/agents/salesagent/tools/tool-reference.html) -- Complete tool catalog +- [get_adcp_capabilities](/agents/salesagent/tools/get-adcp-capabilities.html) -- Discover supported protocols and features +- [create_media_buy](/agents/salesagent/tools/create-media-buy.html) -- Create a campaign (subject to content standards) diff --git a/agents/salesagent/tools/create-media-buy.md b/agents/salesagent/tools/create-media-buy.md new file mode 100644 index 0000000000..f53972c773 --- /dev/null +++ b/agents/salesagent/tools/create-media-buy.md @@ -0,0 +1,203 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Tools - create_media_buy +description: Create advertising campaigns with packages, creatives, and targeting +sidebarType: 10 +--- + +# create_media_buy +{: .no_toc} + +Creates a new media buy (advertising campaign) with one or more packages, optional creative assets, and targeting configuration. + +- TOC +{:toc} + +## Description + +The `create_media_buy` tool is the primary execution tool for purchasing advertising inventory. It accepts a campaign definition including flight dates, packages (line items), and optional creative assignments, then orchestrates the creation process through the ad server adapter. + +This is an **asynchronous** tool. Depending on the publisher's configuration, media buy creation may require human-in-the-loop approval before the campaign goes live. The tool returns immediately with a `media_buy_id` and status, but the campaign may remain in a `pending_approval` state until a publisher administrator approves it through the workflow system. + +Authentication is **required**. The buyer's identity is derived from the authentication token. + +## Parameters + +{: .table .table-bordered .table-striped } +| Name | Type | Required | Description | Default | +|------|------|----------|-------------|---------| +| `campaign_name` | `str` | Yes | Human-readable name for the campaign. | -- | +| `media_buy_start_date` | `date` | Yes | Campaign start date (ISO 8601 format, e.g., `"2026-03-01"`). | -- | +| `media_buy_end_date` | `date` | Yes | Campaign end date (ISO 8601 format, e.g., `"2026-03-31"`). | -- | +| `packages` | `list[PackageRequest]` | Yes | One or more packages (line items) defining the media buy. See PackageRequest below. | -- | +| `brand_manifest` | `BrandManifest` | No | Brand metadata for compliance and personalization. See [get_products](/agents/salesagent/tools/get-products.html) for schema. | `None` | +| `buyer_ref` | `str` | No | Buyer-side reference ID for reconciliation. | `None` | +| `creatives` | `list[CreativeAsset]` | No | Inline creative assets to upload with the media buy. | `None` | +| `creative_ids` | `list[str]` | No | IDs of previously uploaded creatives to attach. | `None` | +| `assignments` | `dict[str, list[str]]` | No | Map of package identifiers to creative ID lists, defining which creatives serve on which packages. | `None` | +| `push_notification_config` | `PushNotificationConfig` | No | Configuration for async push notifications on status changes. | `None` | + +### PackageRequest + +Each package in the `packages` list defines a line item: + +{: .table .table-bordered .table-striped } +| Name | Type | Required | Description | +|------|------|----------|-------------| +| `product_id` | `str` | Yes | Product identifier from `get_products` | +| `name` | `str` | No | Package display name | +| `budget` | `float` | Yes | Package budget amount | +| `currency` | `str` | No | ISO 4217 currency code (e.g., `"USD"`) | +| `quantity` | `int` | No | Target impression quantity | +| `targeting` | `TargetingSpec` | No | Additional targeting overrides | +| `start_date` | `date` | No | Package-level start date (overrides campaign dates) | +| `end_date` | `date` | No | Package-level end date (overrides campaign dates) | + +### CreativeAsset + +Inline creatives included with the media buy: + +{: .table .table-bordered .table-striped } +| Name | Type | Required | Description | +|------|------|----------|-------------| +| `format_id` | `str` | Yes | Creative format identifier from `list_creative_formats` | +| `name` | `str` | Yes | Creative display name | +| `asset_url` | `str` | Yes | URL to the creative asset file | +| `click_through_url` | `str` | No | Landing page URL | +| `metadata` | `dict` | No | Additional creative metadata | + +## Response + +Returns one of two result types: + +### CreateMediaBuySuccess + +{: .table .table-bordered .table-striped } +| Field | Type | Description | +|-------|------|-------------| +| `media_buy_id` | `str` | Unique identifier for the created media buy | +| `status` | `str` | Current status (e.g., `active`, `pending_approval`, `draft`) | +| `campaign_name` | `str` | Confirmed campaign name | +| `packages` | `list[PackageResult]` | Created packages with assigned IDs | +| `creatives` | `list[CreativeResult]` | Created/attached creative results | +| `created_at` | `datetime` | Creation timestamp | + +### CreateMediaBuyError + +{: .table .table-bordered .table-striped } +| Field | Type | Description | +|-------|------|-------------| +| `error_code` | `str` | Machine-readable error code | +| `message` | `str` | Human-readable error description | +| `details` | `dict` | Additional error context | + +## Error Codes + +{: .table .table-bordered .table-striped } +| Error Code | Description | +|------------|-------------| +| `unauthorized` | Missing or invalid authentication token | +| `forbidden` | Token lacks media buy creation scope | +| `validation_error` | Invalid parameters (e.g., end date before start date, missing required fields) | +| `product_not_found` | Referenced product_id does not exist | +| `creative_format_mismatch` | Creative does not match the required format for the package | +| `budget_below_minimum` | Package budget is below the product's minimum | +| `date_out_of_range` | Flight dates outside the product's available window | +| `internal_error` | Unexpected server error | + +## Example + +```python +from mcp import ClientSession + +async with ClientSession(transport) as session: + await session.initialize() + + # Create a simple media buy with one package + result = await session.call_tool( + "create_media_buy", + arguments={ + "campaign_name": "Q1 Brand Awareness - Luxe Motors", + "media_buy_start_date": "2026-03-01", + "media_buy_end_date": "2026-03-31", + "buyer_ref": "luxe-q1-2026-001", + "packages": [ + { + "product_id": "prod_abc123", + "name": "Premium Video - US", + "budget": 50000.00, + "currency": "USD", + "quantity": 2000000, + } + ], + }, + ) + media_buy = result.content + print(f"Media Buy ID: {media_buy['media_buy_id']}") + print(f"Status: {media_buy['status']}") + + # Create with inline creatives and assignments + result = await session.call_tool( + "create_media_buy", + arguments={ + "campaign_name": "Spring Campaign", + "media_buy_start_date": "2026-04-01", + "media_buy_end_date": "2026-04-30", + "packages": [ + { + "product_id": "prod_def456", + "budget": 25000.00, + } + ], + "creatives": [ + { + "format_id": "video_16x9_preroll", + "name": "Spring 30s Spot", + "asset_url": "https://cdn.example.com/spring-30s.mp4", + "click_through_url": "https://luxemotors.example.com/spring", + } + ], + "assignments": { + "prod_def456": ["spring-30s-spot"], + }, + "brand_manifest": { + "brand_name": "Luxe Motors", + "category": "automotive", + }, + }, + ) +``` + +Example success response: + +```json +{ + "media_buy_id": "mb_xyz789", + "status": "pending_approval", + "campaign_name": "Q1 Brand Awareness - Luxe Motors", + "packages": [ + { + "package_id": "pkg_001", + "product_id": "prod_abc123", + "name": "Premium Video - US", + "budget": 50000.00, + "status": "pending" + } + ], + "creatives": [], + "created_at": "2026-02-25T10:30:00Z" +} +``` + +## Related Tools + +- [get_products](/agents/salesagent/tools/get-products.html) -- Discover products before creating a media buy +- [update_media_buy](/agents/salesagent/tools/update-media-buy.html) -- Modify an existing media buy +- [sync_creatives](/agents/salesagent/tools/sync-creatives.html) -- Upload creatives separately before or after creation +- [get_media_buy_delivery](/agents/salesagent/tools/get-media-buy-delivery.html) -- Monitor delivery after creation +- [Tool Reference](/agents/salesagent/tools/tool-reference.html) -- Complete tool catalog + +## Protocol Specification + +{: .alert.alert-info :} +The `create_media_buy` tool implements the AdCP `create_media_buy` task. See the [AdCP Specification](https://docs.adcontextprotocol.org/docs/intro) for the full protocol definition including `PackageRequest` schema, `BrandManifest` requirements, and the human-in-the-loop approval flow. diff --git a/agents/salesagent/tools/get-adcp-capabilities.md b/agents/salesagent/tools/get-adcp-capabilities.md new file mode 100644 index 0000000000..0a1512a3b0 --- /dev/null +++ b/agents/salesagent/tools/get-adcp-capabilities.md @@ -0,0 +1,111 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Tools - get_adcp_capabilities +description: Discover agent capabilities, supported protocols, and publisher metadata +sidebarType: 10 +--- + +# get_adcp_capabilities +{: .no_toc} + +Discovers the Sales Agent's capabilities, supported protocols, authorized publisher domains, and available tools. This is typically the first tool a buying agent calls to understand what the Sales Agent can do. + +- TOC +{:toc} + +## Description + +The `get_adcp_capabilities` tool returns a full snapshot of the Sales Agent's current state and capabilities. It reports the agent's identity, supported AdCP protocol versions, available tools and skills, geo targeting systems, and portfolio information. When called with authentication, it also returns publisher-specific data such as authorized domains and custom targeting taxonomies. + +This tool is **idempotent** and safe to call repeatedly. Response times are typically under 1 second. + +## Parameters + +{: .table .table-bordered .table-striped } +| Name | Type | Required | Description | Default | +|------|------|----------|-------------|---------| +| `protocols` | `list[str]` | No | Filter capabilities to specific protocols (e.g., `["adcp"]`). When omitted, all supported protocols are returned. | `None` | + +## Response + +Returns a `GetAdcpCapabilitiesResponse` object containing the following key fields: + +{: .table .table-bordered .table-striped } +| Field | Type | Description | +|-------|------|-------------| +| `agent_name` | `str` | Display name of the Sales Agent instance | +| `agent_version` | `str` | Semantic version of the Sales Agent | +| `adcp_versions` | `list[str]` | Supported AdCP protocol versions | +| `supported_tools` | `list[ToolDescriptor]` | List of available tools with their names, descriptions, and parameter schemas | +| `supported_skills` | `list[str]` | High-level capabilities (e.g., `product_discovery`, `media_buying`) | +| `authorized_domains` | `list[str]` | Publisher domains this agent is authorized to sell inventory for (requires auth) | +| `geo_targeting_systems` | `list[str]` | Supported geographic targeting taxonomies (e.g., `geonames`, `iso3166`) | +| `portfolio` | `PortfolioInfo` | Summary of the publisher's inventory portfolio | +| `creative_formats` | `list[str]` | Supported creative format categories | + +## Error Codes + +{: .table .table-bordered .table-striped } +| Error Code | Description | +|------------|-------------| +| `internal_error` | Unexpected server error during capability resolution | + +This tool does not require authentication and does not produce authorization errors. + +## Example + +```python +from mcp import ClientSession + +async with ClientSession(transport) as session: + await session.initialize() + + # Discover all capabilities + result = await session.call_tool("get_adcp_capabilities") + print(result) + + # Filter to AdCP protocol only + result = await session.call_tool( + "get_adcp_capabilities", + arguments={"protocols": ["adcp"]}, + ) + capabilities = result.content + print(f"Agent: {capabilities['agent_name']}") + print(f"AdCP versions: {capabilities['adcp_versions']}") + print(f"Tools: {[t['name'] for t in capabilities['supported_tools']]}") +``` + +Example response: + +```json +{ + "agent_name": "Prebid Sales Agent", + "agent_version": "1.0.0", + "adcp_versions": ["1.0.0"], + "supported_tools": [ + { + "name": "get_products", + "description": "AI-powered product search" + } + ], + "supported_skills": ["product_discovery", "media_buying", "creative_management"], + "authorized_domains": ["publisher.example.com"], + "geo_targeting_systems": ["geonames", "iso3166"], + "portfolio": { + "total_products": 42, + "channels": ["display", "video", "audio"] + }, + "creative_formats": ["display", "video", "audio", "native"] +} +``` + +## Related Tools + +- [get_products](/agents/salesagent/tools/get-products.html) -- Search advertising products after discovering capabilities +- [list_creative_formats](/agents/salesagent/tools/list-creative-formats.html) -- Get detailed format specifications +- [Tool Reference](/agents/salesagent/tools/tool-reference.html) -- Complete tool catalog + +## Protocol Specification + +{: .alert.alert-info :} +The `get_adcp_capabilities` tool implements the AdCP capability discovery flow. See the [AdCP Specification](https://docs.adcontextprotocol.org/docs/intro) for the full protocol definition of agent capability negotiation. diff --git a/agents/salesagent/tools/get-media-buy-delivery.md b/agents/salesagent/tools/get-media-buy-delivery.md new file mode 100644 index 0000000000..87b53e8997 --- /dev/null +++ b/agents/salesagent/tools/get-media-buy-delivery.md @@ -0,0 +1,190 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Tools - get_media_buy_delivery +description: Retrieve delivery metrics, spend data, and performance breakdowns for media buys +sidebarType: 10 +--- + +# get_media_buy_delivery +{: .no_toc} + +Retrieves delivery metrics and performance data for one or more media buys, including impressions, spend, click-through rates, and package-level breakdowns. + +- TOC +{:toc} + +## Description + +The `get_media_buy_delivery` tool provides access to campaign performance data. It supports querying by individual media buy ID, multiple IDs, buyer reference, or date range. Results include aggregate metrics at the media buy level and granular breakdowns at the package level. + +This is a **synchronous** tool. Data freshness depends on the ad server adapter's reporting latency -- typically near-real-time for the mock adapter and up to a few hours for production ad servers like Google Ad Manager. + +Authentication is **required**. The buyer can only access delivery data for media buys they own or have been granted access to. + +## Parameters + +All parameters are optional, but at least one identifier (`media_buy_id`, `media_buy_ids`, `buyer_ref`, or `buyer_refs`) must be provided. + +{: .table .table-bordered .table-striped } +| Name | Type | Required | Description | Default | +|------|------|----------|-------------|---------| +| `media_buy_id` | `str` | No | Single media buy identifier to query. | `None` | +| `media_buy_ids` | `list[str]` | No | Multiple media buy identifiers to query in a single request. | `None` | +| `buyer_ref` | `str` | No | Single buyer-side reference ID. Returns delivery for all media buys with this reference. | `None` | +| `buyer_refs` | `list[str]` | No | Multiple buyer-side reference IDs. | `None` | +| `start_date` | `date` | No | Filter delivery data to this start date (ISO 8601, e.g., `"2026-03-01"`). | `None` | +| `end_date` | `date` | No | Filter delivery data to this end date (ISO 8601, e.g., `"2026-03-31"`). | `None` | + +### Query Patterns + +The tool supports several common query patterns: + +- **Single media buy** -- Pass `media_buy_id` for a specific campaign +- **Batch query** -- Pass `media_buy_ids` for multiple campaigns in one request +- **By buyer reference** -- Pass `buyer_ref` or `buyer_refs` to find campaigns by your internal IDs +- **Date-filtered** -- Combine any identifier with `start_date` and `end_date` to scope the reporting window + +## Response + +Returns a `GetMediaBuyDeliveryResponse` object: + +{: .table .table-bordered .table-striped } +| Field | Type | Description | +|-------|------|-------------| +| `media_buys` | `list[MediaBuyDelivery]` | Delivery data for each matching media buy | + +Each `MediaBuyDelivery` contains: + +{: .table .table-bordered .table-striped } +| Field | Type | Description | +|-------|------|-------------| +| `media_buy_id` | `str` | Media buy identifier | +| `campaign_name` | `str` | Campaign display name | +| `status` | `str` | Current media buy status | +| `impressions` | `int` | Total impressions delivered | +| `clicks` | `int` | Total clicks recorded | +| `ctr` | `float` | Click-through rate (clicks / impressions) | +| `spend` | `float` | Total spend to date | +| `currency` | `str` | Spend currency (ISO 4217) | +| `budget` | `float` | Total campaign budget | +| `budget_utilization` | `float` | Percentage of budget spent (0.0 to 1.0) | +| `start_date` | `date` | Campaign start date | +| `end_date` | `date` | Campaign end date | +| `packages` | `list[PackageDelivery]` | Package-level delivery breakdowns | + +Each `PackageDelivery` contains: + +{: .table .table-bordered .table-striped } +| Field | Type | Description | +|-------|------|-------------| +| `package_id` | `str` | Package identifier | +| `product_id` | `str` | Associated product identifier | +| `name` | `str` | Package display name | +| `impressions` | `int` | Package-level impressions | +| `clicks` | `int` | Package-level clicks | +| `ctr` | `float` | Package-level click-through rate | +| `spend` | `float` | Package-level spend | +| `budget` | `float` | Package budget | +| `pacing` | `str` | Delivery pacing status: `on_track`, `ahead`, `behind`, `complete` | + +## Error Codes + +{: .table .table-bordered .table-striped } +| Error Code | Description | +|------------|-------------| +| `unauthorized` | Missing or invalid authentication token | +| `forbidden` | Token lacks permission to access this media buy's data | +| `not_found` | No media buys found matching the provided identifiers | +| `validation_error` | Invalid parameters (e.g., end_date before start_date, no identifiers provided) | +| `internal_error` | Unexpected server error | + +## Example + +```python +from mcp import ClientSession + +async with ClientSession(transport) as session: + await session.initialize() + + # Get delivery for a single media buy + result = await session.call_tool( + "get_media_buy_delivery", + arguments={"media_buy_id": "mb_xyz789"}, + ) + delivery = result.content["media_buys"][0] + print(f"Impressions: {delivery['impressions']:,}") + print(f"Spend: ${delivery['spend']:,.2f} / ${delivery['budget']:,.2f}") + print(f"CTR: {delivery['ctr']:.2%}") + + # Batch query multiple campaigns + result = await session.call_tool( + "get_media_buy_delivery", + arguments={ + "media_buy_ids": ["mb_xyz789", "mb_abc123", "mb_def456"], + }, + ) + + # Query by buyer reference with date range + result = await session.call_tool( + "get_media_buy_delivery", + arguments={ + "buyer_ref": "luxe-q1-2026-001", + "start_date": "2026-03-01", + "end_date": "2026-03-15", + }, + ) + + # Check package-level pacing + for mb in result.content["media_buys"]: + for pkg in mb["packages"]: + if pkg["pacing"] == "behind": + print(f"WARNING: {pkg['name']} is behind pace") +``` + +Example response: + +```json +{ + "media_buys": [ + { + "media_buy_id": "mb_xyz789", + "campaign_name": "Q1 Brand Awareness - Luxe Motors", + "status": "active", + "impressions": 850000, + "clicks": 12750, + "ctr": 0.015, + "spend": 21250.00, + "currency": "USD", + "budget": 50000.00, + "budget_utilization": 0.425, + "start_date": "2026-03-01", + "end_date": "2026-03-31", + "packages": [ + { + "package_id": "pkg_001", + "product_id": "prod_abc123", + "name": "Premium Video - US", + "impressions": 850000, + "clicks": 12750, + "ctr": 0.015, + "spend": 21250.00, + "budget": 50000.00, + "pacing": "on_track" + } + ] + } + ] +} +``` + +## Related Tools + +- [create_media_buy](/agents/salesagent/tools/create-media-buy.html) -- Create campaigns to monitor +- [update_media_buy](/agents/salesagent/tools/update-media-buy.html) -- Adjust campaigns based on delivery data +- [get_products](/agents/salesagent/tools/get-products.html) -- Look up product details for package product_ids +- [Tool Reference](/agents/salesagent/tools/tool-reference.html) -- Complete tool catalog + +## Protocol Specification + +{: .alert.alert-info :} +The `get_media_buy_delivery` tool implements the AdCP `get_media_buy_delivery` operation. See the [AdCP Specification](https://docs.adcontextprotocol.org/docs/intro) for the full protocol definition including delivery metric schemas and reporting granularity options. diff --git a/agents/salesagent/tools/get-media-buys.md b/agents/salesagent/tools/get-media-buys.md new file mode 100644 index 0000000000..83f857f6d4 --- /dev/null +++ b/agents/salesagent/tools/get-media-buys.md @@ -0,0 +1,130 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Tools - get_media_buys +description: Retrieve media buys with status, creative approval state, and optional delivery snapshots +sidebarType: 10 +--- + +# get_media_buys +{: .no_toc} + +Get media buys with status, creative approval state, and optional delivery snapshots. Returns a list of media buys matching the requested filters. + +- TOC +{:toc} + +## Description + +The `get_media_buys` tool retrieves media buys from the publisher's system, returning their current status, associated packages, and creative approval state. When no filters are provided, it returns all active media buys visible to the authenticated principal. + +Callers can narrow results by media buy IDs, buyer references, status, or account. Setting `include_snapshot` to `true` attaches near-real-time delivery statistics (impressions, spend, pacing) to each package in the response. This is useful for monitoring active campaigns without calling a separate delivery endpoint. + +Source: `src/core/tools/media_buy_list.py:196` + +## Parameters + +{: .table .table-bordered .table-striped } +| Name | Type | Required | Description | Default | +|------|------|----------|-------------|---------| +| `media_buy_ids` | `list[str]` | No | Array of publisher media buy IDs to retrieve. | `None` | +| `buyer_refs` | `list[str]` | No | Array of buyer reference IDs to retrieve. | `None` | +| `status_filter` | `MediaBuyStatus \| list[MediaBuyStatus]` | No | Filter by status — single status or array of values (`pending`, `approved`, `active`, `paused`, `completed`, `failed`). | `None` | +| `include_snapshot` | `bool` | No | When true, include near-real-time delivery stats per package. | `false` | +| `account_id` | `str` | No | Filter to a specific account. | `None` | +| `context` | `ContextObject` | No | Application level context object. | `None` | + +## Response + +Returns a `GetMediaBuysResponse` object containing: + +{: .table .table-bordered .table-striped } +| Field | Type | Description | +|-------|------|-------------| +| `media_buys` | `list[MediaBuy]` | List of matching media buys with status, packages, and optional delivery snapshots | +| `errors` | `list[object]` | Any errors encountered during retrieval | + +Each `MediaBuy` in the list contains the media buy's current status, its associated packages with creative assignments, and — when `include_snapshot` is `true` — delivery metrics such as impressions delivered, spend to date, and pacing percentage. + +## Error Codes + +{: .table .table-bordered .table-striped } +| Error Code | Description | +|------------|-------------| +| `principal_id_missing` | No principal ID was found in the request context. Authentication is required. | +| `principal_not_found` | The authenticated principal does not exist in the system. | +| `account_id_filtering_not_supported` | The `account_id` filter is not supported for this tenant configuration. | + +## Example + +```python +from mcp import ClientSession + +async with ClientSession(transport) as session: + await session.initialize() + + # Retrieve all active media buys + result = await session.call_tool( + "get_media_buys", + arguments={ + "status_filter": "active", + }, + ) + for buy in result.content["media_buys"]: + print(f"{buy['media_buy_id']}: {buy['status']}") + + # Retrieve specific media buys with delivery snapshots + result = await session.call_tool( + "get_media_buys", + arguments={ + "media_buy_ids": ["mb_abc123", "mb_def456"], + "include_snapshot": True, + }, + ) + for buy in result.content["media_buys"]: + for pkg in buy.get("packages", []): + snapshot = pkg.get("delivery_snapshot", {}) + print( + f" {pkg['package_id']}: " + f"{snapshot.get('impressions_delivered', 0)} impressions" + ) +``` + +Example response: + +```json +{ + "media_buys": [ + { + "media_buy_id": "mb_abc123", + "buyer_ref": "buyer_ref_001", + "status": "active", + "packages": [ + { + "package_id": "pkg_001", + "product_id": "prod_abc123", + "creative_status": "approved", + "delivery_snapshot": { + "impressions_delivered": 45200, + "spend_to_date": 1130.00, + "pacing_percentage": 0.72 + } + } + ] + } + ], + "errors": [] +} +``` + +## Related Tools + +- [create_media_buy](/agents/salesagent/tools/create-media-buy.html) -- Create a new media buy +- [update_media_buy](/agents/salesagent/tools/update-media-buy.html) -- Modify an existing media buy +- [get_media_buy_delivery](/agents/salesagent/tools/get-media-buy-delivery.html) -- Detailed delivery reporting for a single media buy +- [get_products](/agents/salesagent/tools/get-products.html) -- Discover products before creating media buys +- [Tool Reference](/agents/salesagent/tools/tool-reference.html) -- Complete tool catalog + +## Protocol Specification + +{: .alert.alert-info :} +The `get_media_buys` tool implements the AdCP `get_media_buys` task. See the [AdCP Specification](https://docs.adcontextprotocol.org/docs/intro) for the full protocol definition including `MediaBuy` schema, status lifecycle, and delivery snapshot format. diff --git a/agents/salesagent/tools/get-products.md b/agents/salesagent/tools/get-products.md new file mode 100644 index 0000000000..b5dd5c8cde --- /dev/null +++ b/agents/salesagent/tools/get-products.md @@ -0,0 +1,166 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Tools - get_products +description: AI-powered product discovery with natural language search and filtering +sidebarType: 10 +--- + +# get_products +{: .no_toc} + +Searches the publisher's advertising product catalog using AI-powered retrieval, returning products that match a natural language brief and optional structured filters. + +- TOC +{:toc} + +## Description + +The `get_products` tool is the primary entry point for product discovery. It accepts a natural language `brief` describing what the buyer is looking for (e.g., "premium video inventory targeting sports fans in the US") and uses AI/RAG (Retrieval-Augmented Generation) to find matching products from the publisher's catalog. + +Results include pricing options, available creative formats, estimated exposure volumes, supported channels, and geographic availability. When a `brand_manifest` is provided, the agent can further tailor recommendations to the brand's category, audience, and compliance requirements. + +Response times are typically a few seconds due to the AI-powered search pipeline. + +## Parameters + +{: .table .table-bordered .table-striped } +| Name | Type | Required | Description | Default | +|------|------|----------|-------------|---------| +| `brief` | `str` | Yes | Natural language description of desired advertising products. The AI search engine interprets this to find relevant matches. | -- | +| `brand_manifest` | `BrandManifest` | No | Brand metadata including category, audience profile, and compliance requirements. Enables personalized product recommendations. | `None` | +| `adcp_version` | `str` | No | AdCP protocol version for response formatting. | `"1.0.0"` | +| `filters` | `ProductFilters` | No | Structured filters to narrow results (see below). | `None` | +| `push_notification_config` | `PushNotificationConfig` | No | Configuration for async push notifications when results are ready. | `None` | +| `context` | `ContextObject` | No | Additional context about the buying agent's session and preferences. | `None` | + +### ProductFilters + +The `filters` parameter accepts a `ProductFilters` object with the following fields: + +{: .table .table-bordered .table-striped } +| Name | Type | Description | +|------|------|-------------| +| `channels` | `list[str]` | Filter by channel (e.g., `["display", "video"]`) | +| `countries` | `list[str]` | Filter by country code (ISO 3166-1 alpha-2) | +| `format_ids` | `list[str]` | Filter by creative format identifiers | +| `min_budget` | `float` | Minimum budget threshold | +| `max_budget` | `float` | Maximum budget threshold | + +### BrandManifest + +The `brand_manifest` parameter accepts a `BrandManifest` object that describes the advertiser: + +{: .table .table-bordered .table-striped } +| Name | Type | Description | +|------|------|-------------| +| `brand_name` | `str` | Name of the advertising brand | +| `category` | `str` | Industry category (e.g., "automotive", "retail") | +| `audience` | `AudienceProfile` | Target audience demographics and interests | +| `compliance` | `ComplianceRequirements` | Regulatory and brand safety requirements | + +## Response + +Returns a `GetProductsResponse` object containing a list of matching products: + +{: .table .table-bordered .table-striped } +| Field | Type | Description | +|-------|------|-------------| +| `products` | `list[Product]` | Matching products ranked by relevance | +| `total_results` | `int` | Total number of matching products | +| `search_metadata` | `SearchMetadata` | Information about the search execution | + +Each `Product` in the list contains: + +{: .table .table-bordered .table-striped } +| Field | Type | Description | +|-------|------|-------------| +| `product_id` | `str` | Unique product identifier | +| `name` | `str` | Human-readable product name | +| `description` | `str` | Detailed product description | +| `pricing_options` | `list[PricingOption]` | Available pricing models (CPM, CPC, flat rate, etc.) | +| `format_ids` | `list[str]` | Compatible creative format identifiers | +| `estimated_exposures` | `ExposureEstimate` | Estimated impression/exposure volumes | +| `channels` | `list[str]` | Delivery channels (display, video, audio, native) | +| `countries` | `list[str]` | Geographic availability (ISO 3166-1 alpha-2 codes) | + +## Error Codes + +{: .table .table-bordered .table-striped } +| Error Code | Description | +|------------|-------------| +| `validation_error` | Invalid parameters (e.g., empty brief) | +| `internal_error` | AI search pipeline failure | + +## Example + +```python +from mcp import ClientSession + +async with ClientSession(transport) as session: + await session.initialize() + + # Simple natural language search + result = await session.call_tool( + "get_products", + arguments={ + "brief": "premium video ads targeting tech professionals in the US" + }, + ) + products = result.content + for product in products["products"]: + print(f"{product['name']}: {product['pricing_options']}") + + # Search with brand context and filters + result = await session.call_tool( + "get_products", + arguments={ + "brief": "high-impact display placements for luxury automotive", + "brand_manifest": { + "brand_name": "Luxe Motors", + "category": "automotive", + }, + "filters": { + "channels": ["display"], + "countries": ["US", "GB"], + }, + }, + ) +``` + +Example response: + +```json +{ + "products": [ + { + "product_id": "prod_abc123", + "name": "Premium Video - Tech Audience", + "description": "Pre-roll and mid-roll video inventory across tech publisher network", + "pricing_options": [ + {"model": "CPM", "amount": 25.00, "currency": "USD"} + ], + "format_ids": ["video_16x9_preroll", "video_16x9_midroll"], + "estimated_exposures": { + "min_impressions": 100000, + "max_impressions": 500000, + "period": "monthly" + }, + "channels": ["video"], + "countries": ["US"] + } + ], + "total_results": 1 +} +``` + +## Related Tools + +- [get_adcp_capabilities](/agents/salesagent/tools/get-adcp-capabilities.html) -- Discover capabilities before searching products +- [list_creative_formats](/agents/salesagent/tools/list-creative-formats.html) -- Get details on format_ids returned in products +- [create_media_buy](/agents/salesagent/tools/create-media-buy.html) -- Buy a discovered product +- [Tool Reference](/agents/salesagent/tools/tool-reference.html) -- Complete tool catalog + +## Protocol Specification + +{: .alert.alert-info :} +The `get_products` tool implements the AdCP `get_products` task. See the [AdCP Specification](https://docs.adcontextprotocol.org/docs/intro) for the full protocol definition including `BrandManifest` schema and product response format. diff --git a/agents/salesagent/tools/list-authorized-properties.md b/agents/salesagent/tools/list-authorized-properties.md new file mode 100644 index 0000000000..05d79a0132 --- /dev/null +++ b/agents/salesagent/tools/list-authorized-properties.md @@ -0,0 +1,116 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Tools - list_authorized_properties +description: List publisher properties (domains) this agent is authorized to represent +sidebarType: 10 +--- + +# list_authorized_properties +{: .no_toc} + +Lists all publisher properties (domains) this agent is authorized to represent. Authentication is optional — without auth, returns public inventory; with auth, returns publisher-specific property data. + +- TOC +{:toc} + +## Description + +The `list_authorized_properties` tool returns the set of publisher domains and properties that the Sales Agent is authorized to sell inventory for. The response includes domain metadata and, when available, the publisher's advertising policies. + +When called without authentication, the tool returns publicly available inventory information. When called with a valid authentication context, it returns the full set of properties specific to the authenticated publisher, including any associated advertising policies and restrictions. + +Results can be narrowed using `property_tags` (e.g., `["premium", "sports"]`) or `publisher_domains` (e.g., `["example.com"]`) to retrieve a subset of the authorized properties. + +Source: `src/core/tools/properties.py:214` + +## Parameters + +{: .table .table-bordered .table-striped } +| Name | Type | Required | Description | Default | +|------|------|----------|-------------|---------| +| `property_tags` | `list[str]` | No | Filter to specific property tags. | `None` | +| `publisher_domains` | `list[str]` | No | Filter to specific publisher domains. | `None` | + +## Response + +Returns a `ListAuthorizedPropertiesResponse` object containing: + +{: .table .table-bordered .table-striped } +| Field | Type | Description | +|-------|------|-------------| +| `publisher_domains` | `list[PublisherDomain]` | List of authorized publisher domains with metadata | +| `advertising_policies` | `AdvertisingPolicies` | Publisher advertising policies and restrictions (present when authenticated) | +| `context` | `ContextObject` | Response context metadata | + +Each `PublisherDomain` in the list contains the domain name, associated tags, and property-level metadata such as content categories and supported ad formats. + +## Error Codes + +{: .table .table-bordered .table-striped } +| Error Code | Description | +|------------|-------------| +| `TENANT_ERROR` | Unable to resolve the tenant context for this request. | +| `PROPERTIES_ERROR` | Failed to retrieve property data from the underlying service. | + +## Example + +```python +from mcp import ClientSession + +async with ClientSession(transport) as session: + await session.initialize() + + # List all authorized properties + result = await session.call_tool("list_authorized_properties") + for domain in result.content["publisher_domains"]: + print(f"{domain['domain']}: {domain.get('tags', [])}") + + # Filter by tags and specific domains + result = await session.call_tool( + "list_authorized_properties", + arguments={ + "property_tags": ["premium"], + "publisher_domains": ["news.example.com"], + }, + ) + properties = result.content + if properties.get("advertising_policies"): + print(f"Policies: {properties['advertising_policies']}") +``` + +Example response: + +```json +{ + "publisher_domains": [ + { + "domain": "news.example.com", + "tags": ["premium", "news"], + "content_categories": ["news", "politics", "business"], + "supported_formats": ["display", "video", "native"] + }, + { + "domain": "sports.example.com", + "tags": ["premium", "sports"], + "content_categories": ["sports", "entertainment"], + "supported_formats": ["display", "video"] + } + ], + "advertising_policies": { + "blocked_categories": ["gambling", "tobacco"], + "require_creative_approval": true, + "max_ad_density": 0.3 + } +} +``` + +## Related Tools + +- [get_adcp_capabilities](/agents/salesagent/tools/get-adcp-capabilities.html) -- Discover agent capabilities including authorized domains +- [get_products](/agents/salesagent/tools/get-products.html) -- Search products available on authorized properties +- [Tool Reference](/agents/salesagent/tools/tool-reference.html) -- Complete tool catalog + +## Protocol Specification + +{: .alert.alert-info :} +The `list_authorized_properties` tool implements the AdCP `list_authorized_properties` task. See the [AdCP Specification](https://docs.adcontextprotocol.org/docs/intro) for the full protocol definition including publisher domain schema and advertising policy format. diff --git a/agents/salesagent/tools/list-creative-formats.md b/agents/salesagent/tools/list-creative-formats.md new file mode 100644 index 0000000000..52d41bd958 --- /dev/null +++ b/agents/salesagent/tools/list-creative-formats.md @@ -0,0 +1,168 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Tools - list_creative_formats +description: Query available creative format specifications with flexible filtering +sidebarType: 10 +--- + +# list_creative_formats +{: .no_toc} + +Returns creative format specifications supported by the publisher, with filtering by type, dimensions, asset types, and more. + +- TOC +{:toc} + +## Description + +The `list_creative_formats` tool provides detailed specifications for every creative format the publisher supports. Buying agents use this tool to understand size requirements, accepted asset types, and responsive behavior before uploading creatives or creating media buys. + +All filter parameters are optional and combinable. When multiple filters are provided, results must match all criteria (AND logic). This is a synchronous tool with response times typically under 1 second. + +## Parameters + +{: .table .table-bordered .table-striped } +| Name | Type | Required | Description | Default | +|------|------|----------|-------------|---------| +| `type` | `FormatCategory` | No | Filter by format category. One of: `audio`, `video`, `display`, `native`. | `None` | +| `format_ids` | `list[FormatId]` | No | Filter to specific format identifiers. | `None` | +| `is_responsive` | `bool` | No | Filter by responsive behavior. `true` returns only responsive formats; `false` returns only fixed-size formats. | `None` | +| `name_search` | `str` | No | Case-insensitive substring search on format names. | `None` | +| `asset_types` | `list[AssetContentType]` | No | Filter by accepted asset content types (e.g., `["image/png", "video/mp4"]`). | `None` | +| `min_width` | `int` | No | Minimum width in pixels. | `None` | +| `max_width` | `int` | No | Maximum width in pixels. | `None` | +| `min_height` | `int` | No | Minimum height in pixels. | `None` | +| `max_height` | `int` | No | Maximum height in pixels. | `None` | + +### FormatCategory Values + +{: .table .table-bordered .table-striped } +| Value | Description | +|-------|-------------| +| `audio` | Audio ad formats (e.g., podcast, streaming audio) | +| `video` | Video ad formats (e.g., pre-roll, mid-roll, outstream) | +| `display` | Display ad formats (e.g., banner, interstitial, billboard) | +| `native` | Native ad formats (e.g., in-feed, content recommendation) | + +## Response + +Returns a `ListCreativeFormatsResponse` object: + +{: .table .table-bordered .table-striped } +| Field | Type | Description | +|-------|------|-------------| +| `formats` | `list[CreativeFormat]` | List of matching creative format specifications | +| `total` | `int` | Total number of matching formats | + +Each `CreativeFormat` contains: + +{: .table .table-bordered .table-striped } +| Field | Type | Description | +|-------|------|-------------| +| `format_id` | `str` | Unique format identifier | +| `name` | `str` | Human-readable format name | +| `type` | `FormatCategory` | Format category (audio, video, display, native) | +| `width` | `int` | Width in pixels (0 for responsive or audio) | +| `height` | `int` | Height in pixels (0 for responsive or audio) | +| `is_responsive` | `bool` | Whether the format adapts to container size | +| `accepted_asset_types` | `list[str]` | MIME types accepted for this format | +| `max_file_size_bytes` | `int` | Maximum file size in bytes | +| `max_duration_seconds` | `float` | Maximum duration for video/audio formats | +| `specs` | `dict` | Additional format-specific requirements | + +## Error Codes + +{: .table .table-bordered .table-striped } +| Error Code | Description | +|------------|-------------| +| `validation_error` | Invalid filter parameters (e.g., negative dimensions) | + +## Example + +```python +from mcp import ClientSession + +async with ClientSession(transport) as session: + await session.initialize() + + # List all video formats + result = await session.call_tool( + "list_creative_formats", + arguments={"type": "video"}, + ) + for fmt in result.content["formats"]: + print(f"{fmt['name']}: {fmt['width']}x{fmt['height']}") + + # Find display formats in a specific size range + result = await session.call_tool( + "list_creative_formats", + arguments={ + "type": "display", + "min_width": 300, + "max_width": 728, + "min_height": 250, + "max_height": 250, + }, + ) + + # Search by name + result = await session.call_tool( + "list_creative_formats", + arguments={"name_search": "leaderboard"}, + ) + + # Filter by accepted asset types + result = await session.call_tool( + "list_creative_formats", + arguments={"asset_types": ["video/mp4", "video/webm"]}, + ) +``` + +Example response: + +```json +{ + "formats": [ + { + "format_id": "video_16x9_preroll", + "name": "Pre-Roll Video 16:9", + "type": "video", + "width": 1920, + "height": 1080, + "is_responsive": false, + "accepted_asset_types": ["video/mp4", "video/webm"], + "max_file_size_bytes": 52428800, + "max_duration_seconds": 30.0, + "specs": { + "aspect_ratio": "16:9", + "min_bitrate_kbps": 2000 + } + }, + { + "format_id": "display_300x250", + "name": "Medium Rectangle", + "type": "display", + "width": 300, + "height": 250, + "is_responsive": false, + "accepted_asset_types": ["image/png", "image/jpeg", "image/gif"], + "max_file_size_bytes": 153600, + "max_duration_seconds": null, + "specs": {} + } + ], + "total": 2 +} +``` + +## Related Tools + +- [get_products](/agents/salesagent/tools/get-products.html) -- Products reference format_ids from this tool +- [sync_creatives](/agents/salesagent/tools/sync-creatives.html) -- Upload creatives that conform to these formats +- [create_media_buy](/agents/salesagent/tools/create-media-buy.html) -- Create campaigns using products tied to these formats +- [Tool Reference](/agents/salesagent/tools/tool-reference.html) -- Complete tool catalog + +## Protocol Specification + +{: .alert.alert-info :} +The `list_creative_formats` tool implements the AdCP creative format discovery flow. See the [AdCP Specification](https://docs.adcontextprotocol.org/docs/intro) for the standard creative format schema and taxonomy. diff --git a/agents/salesagent/tools/list-creatives.md b/agents/salesagent/tools/list-creatives.md new file mode 100644 index 0000000000..0c1bcacf9e --- /dev/null +++ b/agents/salesagent/tools/list-creatives.md @@ -0,0 +1,153 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Tools - list_creatives +description: List and filter creative assets from the centralized library +sidebarType: 10 +--- + +# list_creatives +{: .no_toc} + +List and filter creative assets from the centralized library. Supports both flat parameters and nested objects for filtering, sorting, and pagination. + +- TOC +{:toc} + +## Description + +The `list_creatives` tool provides access to the publisher's creative asset library. It supports filtering by media buy, buyer reference, status, format, tags, and date range, as well as full-text search across creative names and descriptions. Results are paginated and sortable. + +When `include_performance` is set to `true`, each creative in the response includes aggregated performance metrics (impressions, clicks, CTR). When `include_assignments` is `true`, the response shows which packages each creative is assigned to. Both options increase response size and latency. + +As of AdCP 2.5, the tool accepts `media_buy_ids` and `buyer_refs` as arrays for batch filtering across multiple media buys or buyers in a single call. + +Source: `src/core/tools/creatives/listing.py:456` + +## Parameters + +{: .table .table-bordered .table-striped } +| Name | Type | Required | Description | Default | +|------|------|----------|-------------|---------| +| `media_buy_id` | `str` | No | Filter by single media buy ID. | `None` | +| `media_buy_ids` | `list[str]` | No | Filter by multiple media buy IDs (AdCP 2.5). | `None` | +| `buyer_ref` | `str` | No | Filter by single buyer reference. | `None` | +| `buyer_refs` | `list[str]` | No | Filter by multiple buyer references (AdCP 2.5). | `None` | +| `status` | `str` | No | Filter by creative status (`pending`, `approved`, `rejected`). | `None` | +| `format` | `str` | No | Filter by creative format. | `None` | +| `tags` | `list[str]` | No | Filter by tags. | `None` | +| `created_after` | `str` | No | Filter by creation date (ISO 8601). | `None` | +| `created_before` | `str` | No | Filter by creation date (ISO 8601). | `None` | +| `search` | `str` | No | Search in creative names and descriptions. | `None` | +| `include_performance` | `bool` | No | Include performance metrics per creative. | `false` | +| `include_assignments` | `bool` | No | Include package assignment data per creative. | `false` | +| `page` | `int` | No | Page number for pagination. | `1` | +| `limit` | `int` | No | Results per page (max 1000). | `50` | +| `sort_by` | `str` | No | Sort field (`created_date`, `name`, `status`). | `"created_date"` | +| `sort_order` | `str` | No | Sort order (`asc`, `desc`). | `"desc"` | + +## Response + +Returns a `ListCreativesResponse` object containing: + +{: .table .table-bordered .table-striped } +| Field | Type | Description | +|-------|------|-------------| +| `creatives` | `list[Creative]` | List of creative assets matching the filters | +| `query_summary` | `object` | Summary of the applied filters and search terms | +| `pagination` | `PaginationInfo` | Pagination metadata (`page`, `limit`, `total`, `total_pages`) | +| `format_summary` | `object` | Breakdown of results by creative format | +| `status_summary` | `object` | Breakdown of results by creative status | +| `context` | `ContextObject` | Response context metadata | + +## Error Codes + +{: .table .table-bordered .table-striped } +| Error Code | Description | +|------------|-------------| +| `authentication_required` | Creatives contain sensitive data and require authentication. | +| `no_tenant_context` | No tenant context available. The request must include a valid tenant. | +| `invalid_date_format` | The `created_after` or `created_before` value is not valid ISO 8601. | + +## Example + +```python +from mcp import ClientSession + +async with ClientSession(transport) as session: + await session.initialize() + + # List all approved creatives for a media buy + result = await session.call_tool( + "list_creatives", + arguments={ + "media_buy_id": "mb_abc123", + "status": "approved", + }, + ) + for creative in result.content["creatives"]: + print(f"{creative['name']} ({creative['format']})") + + # Search creatives with performance data across multiple buyers + result = await session.call_tool( + "list_creatives", + arguments={ + "buyer_refs": ["buyer_001", "buyer_002"], + "search": "holiday campaign", + "include_performance": True, + "sort_by": "name", + "sort_order": "asc", + "limit": 25, + }, + ) + pagination = result.content["pagination"] + print(f"Page {pagination['page']} of {pagination['total_pages']}") + for creative in result.content["creatives"]: + perf = creative.get("performance", {}) + print(f" {creative['name']}: {perf.get('impressions', 0)} impressions") +``` + +Example response: + +```json +{ + "creatives": [ + { + "creative_id": "cr_xyz789", + "name": "Holiday Banner 300x250", + "format": "display", + "status": "approved", + "created_date": "2026-01-15T10:30:00Z", + "tags": ["holiday", "q4"], + "performance": { + "impressions": 120000, + "clicks": 3600, + "ctr": 0.03 + } + } + ], + "query_summary": { + "filters_applied": ["buyer_refs", "search"], + "search_term": "holiday campaign" + }, + "pagination": { + "page": 1, + "limit": 25, + "total": 1, + "total_pages": 1 + }, + "format_summary": {"display": 1}, + "status_summary": {"approved": 1} +} +``` + +## Related Tools + +- [sync_creatives](/agents/salesagent/tools/sync-creatives.html) -- Upload or sync creative assets to the library +- [list_creative_formats](/agents/salesagent/tools/list-creative-formats.html) -- Discover supported creative formats +- [get_media_buys](/agents/salesagent/tools/get-media-buys.html) -- Retrieve media buys that reference creatives +- [Tool Reference](/agents/salesagent/tools/tool-reference.html) -- Complete tool catalog + +## Protocol Specification + +{: .alert.alert-info :} +The `list_creatives` tool implements the AdCP `list_creatives` task. See the [AdCP Specification](https://docs.adcontextprotocol.org/docs/intro) for the full protocol definition including creative status lifecycle and performance metric schemas. diff --git a/agents/salesagent/tools/log-event.md b/agents/salesagent/tools/log-event.md new file mode 100644 index 0000000000..876160b84f --- /dev/null +++ b/agents/salesagent/tools/log-event.md @@ -0,0 +1,30 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Tools - log_event +description: Logs individual conversion or attribution events for a media buy +sidebarType: 10 +--- + +# log_event +{: .no_toc} + +Logs individual conversion or attribution events associated with a media buy, enabling the Sales Agent to track campaign performance and feed data back into optimization loops. + +{: .alert.alert-warning :} +This tool is defined in the AdCP specification but is not yet implemented in the Prebid Sales Agent. It is planned for a future release. + +## Expected Behavior + +When implemented, `log_event` will accept individual event records — each tied to a media buy ID and an event source registered via `sync_event_sources`. Events typically represent conversions (purchases, sign-ups, app installs) or attribution signals (view-through, click-through). + +Each event includes a timestamp, event type, and optional metadata such as revenue value or custom dimensions. The Sales Agent will persist these events and make them available in delivery reporting through `get_media_buy_delivery`. + +## AdCP Specification + +This tool is part of the [AdCP Media Buy Protocol](https://docs.adcontextprotocol.org/docs/media-buy/task-reference/log_event). See the protocol specification for the canonical parameter definitions and expected behavior. + +## Related Tools + +- [Tool Reference](/agents/salesagent/tools/tool-reference.html) -- Complete tool catalog +- [sync_event_sources](/agents/salesagent/tools/sync-event-sources.html) -- Register event sources for conversion tracking +- [get_media_buy_delivery](/agents/salesagent/tools/get-media-buy-delivery.html) -- Retrieve delivery metrics for a media buy diff --git a/agents/salesagent/tools/provide-performance-feedback.md b/agents/salesagent/tools/provide-performance-feedback.md new file mode 100644 index 0000000000..b601291f97 --- /dev/null +++ b/agents/salesagent/tools/provide-performance-feedback.md @@ -0,0 +1,30 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Tools - provide_performance_feedback +description: Submits performance feedback for active media buys back to the Sales Agent +sidebarType: 10 +--- + +# provide_performance_feedback +{: .no_toc} + +Allows buying agents to submit performance feedback (conversion data, brand lift, ROI metrics) for active media buys back to the Sales Agent, enabling closed-loop optimization. + +{: .alert.alert-warning :} +This tool is defined in the AdCP specification but is not yet implemented in the Prebid Sales Agent. It is planned for a future release. + +## Expected Behavior + +When implemented, `provide_performance_feedback` will accept structured performance data tied to a specific media buy ID. Buying agents can report metrics such as post-click conversions, revenue attributed to the campaign, brand lift survey results, or custom KPIs. + +The Sales Agent will store this feedback and make it available to the publisher through the Admin UI and delivery reporting tools. This enables publishers to evaluate campaign effectiveness from the buyer's perspective alongside their own delivery data. + +## AdCP Specification + +This tool is part of the [AdCP Media Buy Protocol](https://docs.adcontextprotocol.org/docs/media-buy/task-reference/provide_performance_feedback). See the protocol specification for the canonical parameter definitions and expected behavior. + +## Related Tools + +- [Tool Reference](/agents/salesagent/tools/tool-reference.html) -- Complete tool catalog +- [get_media_buy_delivery](/agents/salesagent/tools/get-media-buy-delivery.html) -- Retrieve delivery metrics for a media buy +- [update_media_buy](/agents/salesagent/tools/update-media-buy.html) -- Modify an existing media buy diff --git a/agents/salesagent/tools/sync-catalogs.md b/agents/salesagent/tools/sync-catalogs.md new file mode 100644 index 0000000000..5fca6f1492 --- /dev/null +++ b/agents/salesagent/tools/sync-catalogs.md @@ -0,0 +1,30 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Tools - sync_catalogs +description: Synchronizes product catalog data between buying agents and the Sales Agent +sidebarType: 10 +--- + +# sync_catalogs +{: .no_toc} + +Synchronizes product catalog data (product feeds, store information, or inventory) between a buying agent and the Sales Agent. This tool is designed for commerce and retail media use cases where catalog alignment between buyer and seller is required before media buys can be created. + +{: .alert.alert-warning :} +This tool is defined in the AdCP specification but is not yet implemented in the Prebid Sales Agent. It is planned for a future release. + +## Expected Behavior + +When implemented, `sync_catalogs` will accept product feed data from a buying agent and reconcile it with the publisher's internal catalog. This supports retail media scenarios where an advertiser's product catalog (SKUs, prices, availability) must be aligned with the publisher's ad inventory before campaigns can target specific products. + +The tool will handle incremental updates, allowing buying agents to sync changed items rather than retransmitting the full catalog on each call. + +## AdCP Specification + +This tool is part of the [AdCP Media Buy Protocol](https://docs.adcontextprotocol.org/docs/media-buy/task-reference/sync_catalogs). See the protocol specification for the canonical parameter definitions and expected behavior. + +## Related Tools + +- [Tool Reference](/agents/salesagent/tools/tool-reference.html) -- Complete tool catalog +- [get_products](/agents/salesagent/tools/get-products.html) -- Search the publisher's product catalog +- [create_media_buy](/agents/salesagent/tools/create-media-buy.html) -- Create a campaign from discovered products diff --git a/agents/salesagent/tools/sync-creatives.md b/agents/salesagent/tools/sync-creatives.md new file mode 100644 index 0000000000..26a151f93d --- /dev/null +++ b/agents/salesagent/tools/sync-creatives.md @@ -0,0 +1,219 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Tools - sync_creatives +description: Upload, validate, and manage creative assets with format compliance checking +sidebarType: 10 +--- + +# sync_creatives +{: .no_toc} + +Uploads, validates, and synchronizes creative assets with the publisher's ad server, supporting bulk operations, format validation, and dry-run previews. + +- TOC +{:toc} + +## Description + +The `sync_creatives` tool manages the lifecycle of creative assets. It accepts a list of creative definitions, validates them against the publisher's format specifications, and synchronizes them with the ad server. The tool supports several operational modes: + +- **Upload new creatives** -- Provide creative definitions to upload and register +- **Update existing creatives** -- Sync changes to previously uploaded creatives +- **Delete missing creatives** -- Optionally remove creatives from the server that are not in the current sync payload +- **Dry run** -- Validate creatives without making changes to the ad server +- **Assignment mapping** -- Associate creatives with specific packages in existing media buys + +This is an **asynchronous** tool. Large creative uploads may take several seconds depending on asset sizes and ad server latency. + +Authentication is **required**. + +## Parameters + +{: .table .table-bordered .table-striped } +| Name | Type | Required | Description | Default | +|------|------|----------|-------------|---------| +| `creatives` | `list[CreativeAsset]` | Yes | List of creative assets to sync. See CreativeAsset below. | -- | +| `assignments` | `dict[str, list[str]]` | No | Map of package IDs or media buy IDs to creative ID lists. Defines which creatives serve on which packages. | `None` | +| `creative_ids` | `list[str]` | No | IDs of existing creatives to include in the sync scope. Useful for managing assignments without re-uploading. | `None` | +| `delete_missing` | `bool` | No | When `true`, creatives on the server that are not in the current `creatives` list will be removed. Use with caution. | `false` | +| `dry_run` | `bool` | No | When `true`, validates all creatives and returns what would change without making actual modifications. | `false` | +| `validation_mode` | `ValidationMode` | No | Controls validation strictness. See ValidationMode below. | `None` | + +### CreativeAsset + +Each creative in the `creatives` list: + +{: .table .table-bordered .table-striped } +| Name | Type | Required | Description | +|------|------|----------|-------------| +| `format_id` | `str` | Yes | Creative format identifier from `list_creative_formats` | +| `name` | `str` | Yes | Human-readable creative name | +| `asset_url` | `str` | Yes | URL to the creative asset file | +| `click_through_url` | `str` | No | Landing page URL | +| `creative_id` | `str` | No | Existing creative ID (for updates). Omit for new uploads. | +| `metadata` | `dict` | No | Additional creative metadata (alt text, tracking pixels, etc.) | + +### ValidationMode + +Controls how strictly creative assets are validated: + +{: .table .table-bordered .table-striped } +| Value | Description | +|-------|-------------| +| `strict` | All format requirements must be met. Rejects non-conforming creatives. | +| `lenient` | Warns about format mismatches but allows the sync to proceed. | +| `none` | Skips validation entirely (not recommended for production). | + +## Response + +Returns a `SyncCreativesResponse` object: + +{: .table .table-bordered .table-striped } +| Field | Type | Description | +|-------|------|-------------| +| `results` | `list[CreativeSyncResult]` | Per-creative sync results | +| `summary` | `SyncSummary` | Aggregate counts of created, updated, deleted, and failed operations | + +Each `CreativeSyncResult` contains: + +{: .table .table-bordered .table-striped } +| Field | Type | Description | +|-------|------|-------------| +| `creative_id` | `str` | The creative's unique identifier (assigned on creation) | +| `name` | `str` | Creative name | +| `status` | `str` | Sync result: `created`, `updated`, `deleted`, `unchanged`, `failed` | +| `validation_errors` | `list[str]` | Any validation issues encountered | +| `warnings` | `list[str]` | Non-blocking warnings | + +The `SyncSummary` contains: + +{: .table .table-bordered .table-striped } +| Field | Type | Description | +|-------|------|-------------| +| `created` | `int` | Number of new creatives uploaded | +| `updated` | `int` | Number of existing creatives modified | +| `deleted` | `int` | Number of creatives removed | +| `unchanged` | `int` | Number of creatives with no changes | +| `failed` | `int` | Number of creatives that failed to sync | + +## Error Codes + +{: .table .table-bordered .table-striped } +| Error Code | Description | +|------------|-------------| +| `unauthorized` | Missing or invalid authentication token | +| `forbidden` | Token lacks creative management scope | +| `validation_error` | One or more creatives failed format validation (in strict mode) | +| `format_not_found` | Referenced format_id does not exist | +| `asset_unreachable` | Creative asset URL could not be fetched | +| `file_too_large` | Asset exceeds the format's maximum file size | +| `internal_error` | Unexpected server error | + +## Example + +```python +from mcp import ClientSession + +async with ClientSession(transport) as session: + await session.initialize() + + # Upload new creatives + result = await session.call_tool( + "sync_creatives", + arguments={ + "creatives": [ + { + "format_id": "display_300x250", + "name": "Spring Banner 300x250", + "asset_url": "https://cdn.example.com/spring-300x250.png", + "click_through_url": "https://brand.example.com/spring", + }, + { + "format_id": "video_16x9_preroll", + "name": "Spring Video 30s", + "asset_url": "https://cdn.example.com/spring-30s.mp4", + "click_through_url": "https://brand.example.com/spring", + }, + ], + }, + ) + summary = result.content["summary"] + print(f"Created: {summary['created']}, Failed: {summary['failed']}") + + # Dry run to validate before uploading + result = await session.call_tool( + "sync_creatives", + arguments={ + "creatives": [ + { + "format_id": "display_728x90", + "name": "Leaderboard Ad", + "asset_url": "https://cdn.example.com/leaderboard.png", + } + ], + "dry_run": True, + "validation_mode": "strict", + }, + ) + + # Sync with assignments and cleanup + result = await session.call_tool( + "sync_creatives", + arguments={ + "creatives": [ + { + "format_id": "display_300x250", + "name": "Updated Spring Banner", + "asset_url": "https://cdn.example.com/spring-v2-300x250.png", + "creative_id": "cr_existing_001", + } + ], + "assignments": { + "pkg_001": ["cr_existing_001"], + }, + "delete_missing": True, + }, + ) +``` + +Example response: + +```json +{ + "results": [ + { + "creative_id": "cr_new_001", + "name": "Spring Banner 300x250", + "status": "created", + "validation_errors": [], + "warnings": [] + }, + { + "creative_id": "cr_new_002", + "name": "Spring Video 30s", + "status": "created", + "validation_errors": [], + "warnings": ["Asset bitrate (1500kbps) is below recommended minimum (2000kbps)"] + } + ], + "summary": { + "created": 2, + "updated": 0, + "deleted": 0, + "unchanged": 0, + "failed": 0 + } +} +``` + +## Related Tools + +- [list_creative_formats](/agents/salesagent/tools/list-creative-formats.html) -- Check format requirements before syncing +- [create_media_buy](/agents/salesagent/tools/create-media-buy.html) -- Create campaigns with inline creatives +- [get_products](/agents/salesagent/tools/get-products.html) -- Discover products and their required formats +- [Tool Reference](/agents/salesagent/tools/tool-reference.html) -- Complete tool catalog + +## Protocol Specification + +{: .alert.alert-info :} +The `sync_creatives` tool implements the AdCP `sync_creatives` operation. See the [AdCP Specification](https://docs.adcontextprotocol.org/docs/intro) for the full protocol definition including creative manifest schemas and validation requirements. diff --git a/agents/salesagent/tools/sync-event-sources.md b/agents/salesagent/tools/sync-event-sources.md new file mode 100644 index 0000000000..49cab9b26b --- /dev/null +++ b/agents/salesagent/tools/sync-event-sources.md @@ -0,0 +1,30 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Tools - sync_event_sources +description: Registers event sources with the Sales Agent for conversion tracking and attribution +sidebarType: 10 +--- + +# sync_event_sources +{: .no_toc} + +Registers event sources (pixels, server-to-server endpoints) with the Sales Agent for conversion tracking and attribution, enabling buyers to connect their measurement infrastructure to active campaigns. + +{: .alert.alert-warning :} +This tool is defined in the AdCP specification but is not yet implemented in the Prebid Sales Agent. It is planned for a future release. + +## Expected Behavior + +When implemented, `sync_event_sources` will accept event source definitions from a buying agent — such as tracking pixel URLs, server-to-server callback endpoints, or postback configurations — and register them with the Sales Agent for a given media buy. + +Once registered, these event sources enable the publisher's ad server to fire conversion signals back to the buyer's measurement system. The `log_event` tool can then be used to send individual events through the registered sources. + +## AdCP Specification + +This tool is part of the [AdCP Media Buy Protocol](https://docs.adcontextprotocol.org/docs/media-buy/task-reference/sync_event_sources). See the protocol specification for the canonical parameter definitions and expected behavior. + +## Related Tools + +- [Tool Reference](/agents/salesagent/tools/tool-reference.html) -- Complete tool catalog +- [log_event](/agents/salesagent/tools/log-event.html) -- Log individual conversion or attribution events +- [get_media_buy_delivery](/agents/salesagent/tools/get-media-buy-delivery.html) -- Retrieve delivery metrics for a media buy diff --git a/agents/salesagent/tools/tool-reference.md b/agents/salesagent/tools/tool-reference.md new file mode 100644 index 0000000000..fd2d7e3438 --- /dev/null +++ b/agents/salesagent/tools/tool-reference.md @@ -0,0 +1,108 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Tools - Tool Reference +description: Complete catalog of MCP tools exposed by the Prebid Sales Agent +sidebarType: 10 +--- + +# Tool Reference +{: .no_toc} + +The Prebid Sales Agent exposes 14 tools through both MCP and A2A protocols. Each tool maps to an AdCP operation and follows a consistent registration, authentication, and error-handling pattern. + +- TOC +{:toc} + +## Tool Registration + +Every tool is registered with the FastMCP server using the `@mcp.tool()` decorator. This decorator maps a Python function to an MCP tool name, making it callable by any connected AI agent. + +```python +from mcp.server.fastmcp import FastMCP + +mcp = FastMCP("salesagent") + +@mcp.tool() +async def get_adcp_capabilities( + protocols: list[str] | None = None, + ctx: ToolContext = Depends(get_tool_context), +) -> GetAdcpCapabilitiesResponse: + """Discover agent capabilities and supported protocols.""" + ... +``` + +### ToolContext Dependency Injection + +Tools that need access to the database, ad server adapter, or authentication state receive a `ToolContext` object through FastAPI-style dependency injection (`Depends(get_tool_context)`). The `ToolContext` carries: + +- **`publisher`** -- The resolved publisher identity for multi-tenant isolation +- **`auth`** -- Authentication state including token scopes and buyer identity +- **`db`** -- Database session for queries and persistence +- **`adapter`** -- The ad server adapter instance (e.g., Google Ad Manager, mock) +- **`config`** -- Server configuration and feature flags + +Tools that do not require authentication (marked "Optional" below) can still receive a `ToolContext` when a token is provided, providing access to additional publisher-specific data. + +## Tool Categories + +Tools are organized into five categories based on their role in the advertising workflow: + +- **Discovery** -- Read-only tools for exploring inventory, formats, and agent capabilities +- **Execution** -- Tools that create or modify media buys (campaigns) +- **Creative** -- Tools for managing creative assets and format validation +- **Performance** -- Tools for delivery metrics and performance data +- **Workflow** -- Tools for human-in-the-loop task management +- **Governance** -- Tools for authorization and publisher domain management + +## Tool Summary + +{: .table .table-bordered .table-striped } +| Tool | Category | Sync/Async | Auth Required | Description | +|------|----------|------------|---------------|-------------| +| [get_adcp_capabilities](/agents/salesagent/tools/get-adcp-capabilities.html) | Discovery | Async | Optional | Protocol capability discovery | +| [get_products](/agents/salesagent/tools/get-products.html) | Discovery | Async | Optional | AI-powered product search | +| [list_creative_formats](/agents/salesagent/tools/list-creative-formats.html) | Discovery | Sync | Optional | Creative format specifications | +| [create_media_buy](/agents/salesagent/tools/create-media-buy.html) | Execution | Async | Required | Campaign creation | +| [update_media_buy](/agents/salesagent/tools/update-media-buy.html) | Execution | Async | Required | Campaign modification | +| get_media_buys | Execution | Sync | Required | Query media buys | +| [get_media_buy_delivery](/agents/salesagent/tools/get-media-buy-delivery.html) | Performance | Sync | Required | Delivery metrics | +| [sync_creatives](/agents/salesagent/tools/sync-creatives.html) | Creative | Async | Required | Upload and manage creatives | +| list_creatives | Creative | Sync | Required | Search creative library | +| list_authorized_properties | Governance | Sync | Optional | Publisher domains | +| update_performance_index | Performance | Sync | Required | Package performance data | +| list_tasks | Workflow | Sync | Required | List workflow tasks | +| get_task | Workflow | Sync | Required | Get task details | +| complete_task | Workflow | Sync | Required | Complete or fail a task | + +## Authentication + +Tools marked **Auth Required** expect a valid token in the `x-adcp-auth` header (MCP) or `Authorization: Bearer` header (A2A). Without a valid token, these tools return a `401 Unauthorized` error. + +Tools marked **Auth Optional** work without authentication but return richer, publisher-specific data when a token is provided. + +## Error Handling + +All tools follow a consistent error pattern. Errors are returned as structured objects with machine-readable `error_code` fields and human-readable `message` fields. Common error codes include: + +{: .table .table-bordered .table-striped } +| Error Code | HTTP Equivalent | Description | +|------------|-----------------|-------------| +| `unauthorized` | 401 | Missing or invalid authentication token | +| `forbidden` | 403 | Token lacks required scopes | +| `not_found` | 404 | Requested resource does not exist | +| `validation_error` | 422 | Invalid parameters | +| `conflict` | 409 | Operation conflicts with current state | +| `internal_error` | 500 | Unexpected server error | + +## AdCP Media Buy Protocol + +The tools in this reference implement the [AdCP Media Buy Protocol](https://docs.adcontextprotocol.org/docs/intro), which defines the standard operations for AI-driven advertising. Each tool page includes a cross-reference to the relevant section of the protocol specification. + +{: .alert.alert-info :} +For the full AdCP specification, see [docs.adcontextprotocol.org](https://docs.adcontextprotocol.org). For protocol architecture details, see [Architecture & Protocols](/agents/salesagent/architecture.html). + +## Further Reading + +- [Architecture & Protocols](/agents/salesagent/architecture.html) -- MCP and A2A transport details +- [Quick Start](/agents/salesagent/getting-started/quickstart.html) -- Try the tools locally +- [Buy-Side Integration](/agents/salesagent/getting-started/buy-side-integration.html) -- Connect an AI buying agent diff --git a/agents/salesagent/tools/update-media-buy.md b/agents/salesagent/tools/update-media-buy.md new file mode 100644 index 0000000000..57be86876b --- /dev/null +++ b/agents/salesagent/tools/update-media-buy.md @@ -0,0 +1,177 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Tools - update_media_buy +description: Modify, pause, resume, or cancel an existing media buy +sidebarType: 10 +--- + +# update_media_buy +{: .no_toc} + +Modifies an existing media buy by changing its status (pause, resume, cancel) or updating package-level details. + +- TOC +{:toc} + +## Description + +The `update_media_buy` tool allows buying agents to manage the lifecycle of an active media buy. It supports status transitions (pause, resume, cancel) as well as package-level modifications such as budget adjustments and flight date changes. + +This is an **asynchronous** tool. Depending on the nature of the update and the publisher's configuration, changes may require human-in-the-loop approval before taking effect. + +Authentication is **required**. The buyer must own or have access to the media buy being modified. + +## Parameters + +{: .table .table-bordered .table-striped } +| Name | Type | Required | Description | Default | +|------|------|----------|-------------|---------| +| `media_buy_id` | `str` | Yes | Unique identifier of the media buy to update. | -- | +| `action` | `str` | Yes | The action to perform on the media buy. One of: `pause`, `resume`, `cancel`, `update`. | -- | +| `buyer_ref` | `str` | No | Buyer-side reference ID for reconciliation. | `None` | +| `packages` | `list[UpdatePackage]` | No | Package-level modifications. Required when `action` is `update`. | `None` | + +### Action Values + +{: .table .table-bordered .table-striped } +| Action | Description | Valid From States | +|--------|-------------|-------------------| +| `pause` | Temporarily halt delivery on all packages | `active` | +| `resume` | Resume delivery on a paused media buy | `paused` | +| `cancel` | Permanently cancel the media buy | `active`, `paused`, `pending_approval` | +| `update` | Modify package details (budget, dates, targeting) | `active`, `paused`, `draft` | + +### UpdatePackage + +When `action` is `update`, the `packages` list specifies which packages to modify: + +{: .table .table-bordered .table-striped } +| Name | Type | Required | Description | +|------|------|----------|-------------| +| `package_id` | `str` | Yes | Identifier of the package to update | +| `budget` | `float` | No | Updated budget amount | +| `quantity` | `int` | No | Updated target impression count | +| `start_date` | `date` | No | Updated start date | +| `end_date` | `date` | No | Updated end date | +| `targeting` | `TargetingSpec` | No | Updated targeting overrides | +| `status` | `str` | No | Package-level status change | + +## Response + +Returns one of two result types: + +### UpdateMediaBuyRequest (Success) + +{: .table .table-bordered .table-striped } +| Field | Type | Description | +|-------|------|-------------| +| `media_buy_id` | `str` | The updated media buy identifier | +| `status` | `str` | New status after the update | +| `action` | `str` | The action that was performed | +| `packages` | `list[PackageResult]` | Updated package states | +| `updated_at` | `datetime` | Timestamp of the update | + +### UpdateMediaBuyError + +{: .table .table-bordered .table-striped } +| Field | Type | Description | +|-------|------|-------------| +| `error_code` | `str` | Machine-readable error code | +| `message` | `str` | Human-readable error description | +| `details` | `dict` | Additional error context | + +## Error Codes + +{: .table .table-bordered .table-striped } +| Error Code | Description | +|------------|-------------| +| `unauthorized` | Missing or invalid authentication token | +| `forbidden` | Token lacks permission to modify this media buy | +| `not_found` | Media buy with the given ID does not exist | +| `invalid_state_transition` | The requested action is not valid for the current media buy status | +| `validation_error` | Invalid parameters (e.g., missing packages for `update` action) | +| `conflict` | Concurrent modification conflict | +| `internal_error` | Unexpected server error | + +## Example + +```python +from mcp import ClientSession + +async with ClientSession(transport) as session: + await session.initialize() + + # Pause a running campaign + result = await session.call_tool( + "update_media_buy", + arguments={ + "media_buy_id": "mb_xyz789", + "action": "pause", + }, + ) + print(f"Status: {result.content['status']}") + + # Resume a paused campaign + result = await session.call_tool( + "update_media_buy", + arguments={ + "media_buy_id": "mb_xyz789", + "action": "resume", + }, + ) + + # Update package budgets + result = await session.call_tool( + "update_media_buy", + arguments={ + "media_buy_id": "mb_xyz789", + "action": "update", + "buyer_ref": "luxe-q1-2026-001", + "packages": [ + { + "package_id": "pkg_001", + "budget": 75000.00, + "end_date": "2026-04-15", + } + ], + }, + ) + + # Cancel a campaign + result = await session.call_tool( + "update_media_buy", + arguments={ + "media_buy_id": "mb_xyz789", + "action": "cancel", + }, + ) +``` + +Example success response: + +```json +{ + "media_buy_id": "mb_xyz789", + "status": "paused", + "action": "pause", + "packages": [ + { + "package_id": "pkg_001", + "status": "paused", + "budget": 50000.00 + } + ], + "updated_at": "2026-02-25T14:00:00Z" +} +``` + +## Related Tools + +- [create_media_buy](/agents/salesagent/tools/create-media-buy.html) -- Create a media buy before updating it +- [get_media_buy_delivery](/agents/salesagent/tools/get-media-buy-delivery.html) -- Check delivery before making changes +- [Tool Reference](/agents/salesagent/tools/tool-reference.html) -- Complete tool catalog + +## Protocol Specification + +{: .alert.alert-info :} +The `update_media_buy` tool implements the AdCP `update_media_buy` task. See the [AdCP Specification](https://docs.adcontextprotocol.org/docs/intro) for the full protocol definition including valid state transitions and the update approval flow. diff --git a/agents/salesagent/tools/update-performance-index.md b/agents/salesagent/tools/update-performance-index.md new file mode 100644 index 0000000000..c129faef21 --- /dev/null +++ b/agents/salesagent/tools/update-performance-index.md @@ -0,0 +1,116 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Tools - update_performance_index +description: Update performance index data for a media buy's packages +sidebarType: 10 +--- + +# update_performance_index +{: .no_toc} + +Updates performance index data for a media buy's packages. Buying agents use this to provide performance signals that inform optimization. + +- TOC +{:toc} + +## Description + +The `update_performance_index` tool allows buying agents to submit performance signals for packages within a media buy. Each performance data entry maps a `product_id` to a `performance_index` value where `1.0` represents baseline performance. Values above `1.0` indicate above-average performance; values below `1.0` indicate underperformance. + +The publisher's Sales Agent uses these signals to optimize delivery — for example, shifting impressions toward higher-performing packages or adjusting pacing. An optional `confidence_score` (0.0 to 1.0) indicates how reliable the performance signal is, allowing the optimization system to weigh signals appropriately. + +This tool requires authentication. Only the buying agent that owns the media buy can submit performance data. + +Source: `src/core/tools/performance.py:125` + +## Parameters + +{: .table .table-bordered .table-striped } +| Name | Type | Required | Description | Default | +|------|------|----------|-------------|---------| +| `media_buy_id` | `str` | Yes | ID of the media buy to update. | -- | +| `performance_data` | `list[PerformanceEntry]` | Yes | List of performance data objects (see below). | -- | + +### PerformanceEntry + +Each object in the `performance_data` array contains: + +{: .table .table-bordered .table-striped } +| Name | Type | Required | Description | +|------|------|----------|-------------| +| `product_id` | `str` | Yes | The product ID within the media buy to report on. | +| `performance_index` | `float` | Yes | Performance index value. `1.0` = baseline performance. | +| `confidence_score` | `float` | No | Confidence in the signal, from `0.0` (low) to `1.0` (high). | + +## Response + +Returns an `UpdatePerformanceIndexResponse` object containing: + +{: .table .table-bordered .table-striped } +| Field | Type | Description | +|-------|------|-------------| +| `status` | `str` | Result status: `"success"` or `"failed"` | +| `detail` | `str` | Human-readable message describing the outcome | +| `context` | `ContextObject` | Response context metadata | + +## Error Codes + +{: .table .table-bordered .table-striped } +| Error Code | Description | +|------------|-------------| +| `principal_id_missing` | No principal ID was found in the request context. Authentication is required. | +| `principal_not_found` | The authenticated principal does not exist in the system. | +| `context_required` | A context object is required for this operation. | +| `validation_error` | Invalid parameters (e.g., missing `media_buy_id`, empty `performance_data`, out-of-range values). | + +## Example + +```python +from mcp import ClientSession + +async with ClientSession(transport) as session: + await session.initialize() + + # Submit performance signals for two products + result = await session.call_tool( + "update_performance_index", + arguments={ + "media_buy_id": "mb_abc123", + "performance_data": [ + { + "product_id": "prod_video_001", + "performance_index": 1.35, + "confidence_score": 0.9, + }, + { + "product_id": "prod_display_002", + "performance_index": 0.72, + "confidence_score": 0.85, + }, + ], + }, + ) + print(f"Status: {result.content['status']}") + print(f"Detail: {result.content['detail']}") +``` + +Example response: + +```json +{ + "status": "success", + "detail": "Performance index updated for 2 products in media buy mb_abc123." +} +``` + +## Related Tools + +- [get_media_buys](/agents/salesagent/tools/get-media-buys.html) -- Retrieve media buys to identify product IDs for reporting +- [get_media_buy_delivery](/agents/salesagent/tools/get-media-buy-delivery.html) -- View delivery data that informs performance signals +- [update_media_buy](/agents/salesagent/tools/update-media-buy.html) -- Modify media buy parameters based on performance +- [Tool Reference](/agents/salesagent/tools/tool-reference.html) -- Complete tool catalog + +## Protocol Specification + +{: .alert.alert-info :} +The `update_performance_index` tool implements the AdCP `update_performance_index` task. See the [AdCP Specification](https://docs.adcontextprotocol.org/docs/intro) for the full protocol definition including performance index semantics and optimization signal handling. diff --git a/agents/salesagent/tools/workflow-tools.md b/agents/salesagent/tools/workflow-tools.md new file mode 100644 index 0000000000..8716472a90 --- /dev/null +++ b/agents/salesagent/tools/workflow-tools.md @@ -0,0 +1,301 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Tools - Workflow Tools +description: Manage human-in-the-loop approval tasks for media buys and creatives +sidebarType: 10 +--- + +# Workflow Tools +{: .no_toc} + +Workflow tools manage the human-in-the-loop approval process for media buys and creatives. When a buying agent submits a media buy or creative, it may enter a pending approval state. These tools allow agents to monitor and interact with the approval workflow. + +- TOC +{:toc} + +## Overview + +The workflow system tracks tasks that require human approval or asynchronous processing. A media buy submission, for example, may create a task with status `requires_approval` that must be completed before the buy becomes active. These three tools — `list_tasks`, `get_task`, and `complete_task` — provide full visibility and control over the task lifecycle. + +All three tools require authentication. Without a valid tenant context, each returns a `no_tenant_context` error. + +Source: `src/core/main.py:690,799,866` + +## list_tasks + +List workflow tasks with optional filtering by status, object type, or object ID. Results are paginated. + +### Parameters + +{: .table .table-bordered .table-striped } +| Name | Type | Required | Description | Default | +|------|------|----------|-------------|---------| +| `status` | `str` | No | Filter by task status: `pending`, `in_progress`, `completed`, `failed`, `requires_approval`. | `None` | +| `object_type` | `str` | No | Filter by associated object type: `media_buy`, `creative`, `product`. | `None` | +| `object_id` | `str` | No | Filter by associated object ID. | `None` | +| `limit` | `int` | No | Maximum number of tasks to return. | `20` | +| `offset` | `int` | No | Number of tasks to skip for pagination. | `0` | + +### Response + +{: .table .table-bordered .table-striped } +| Field | Type | Description | +|-------|------|-------------| +| `tasks` | `list[Task]` | List of workflow tasks matching the filters | +| `total` | `int` | Total number of matching tasks | +| `offset` | `int` | Current pagination offset | +| `limit` | `int` | Current page size | +| `has_more` | `bool` | Whether additional pages of results exist | + +### Error Codes + +{: .table .table-bordered .table-striped } +| Error Code | Description | +|------------|-------------| +| `no_tenant_context` | No tenant context available. Authentication is required. | + +### Example + +```python +from mcp import ClientSession + +async with ClientSession(transport) as session: + await session.initialize() + + # List all tasks pending approval + result = await session.call_tool( + "list_tasks", + arguments={ + "status": "requires_approval", + }, + ) + for task in result.content["tasks"]: + print(f"{task['task_id']}: {task['type']} — {task['status']}") + print(f"Total: {result.content['total']}") + + # List tasks for a specific media buy + result = await session.call_tool( + "list_tasks", + arguments={ + "object_type": "media_buy", + "object_id": "mb_abc123", + "limit": 10, + }, + ) +``` + +Example response: + +```json +{ + "tasks": [ + { + "task_id": "task_001", + "type": "media_buy_approval", + "status": "requires_approval", + "tool_name": "create_media_buy", + "owner": "buyer_agent_001", + "created_at": "2026-02-20T14:30:00Z", + "associated_objects": [ + {"type": "media_buy", "id": "mb_abc123"} + ] + } + ], + "total": 1, + "offset": 0, + "limit": 20, + "has_more": false +} +``` + +--- + +## get_task + +Get detailed information about a specific workflow task, including its request and response data. + +### Parameters + +{: .table .table-bordered .table-striped } +| Name | Type | Required | Description | Default | +|------|------|----------|-------------|---------| +| `task_id` | `str` | Yes | ID of the task to retrieve. | -- | + +### Response + +{: .table .table-bordered .table-striped } +| Field | Type | Description | +|-------|------|-------------| +| `task_id` | `str` | Unique task identifier | +| `context_id` | `str` | ID of the conversation or session context | +| `status` | `str` | Current task status | +| `type` | `str` | Task type (e.g., `media_buy_approval`, `creative_review`) | +| `tool_name` | `str` | Name of the tool that created this task | +| `owner` | `str` | Identifier of the agent or user that owns the task | +| `created_at` | `str` | ISO 8601 timestamp of task creation | +| `request_data` | `object` | Original request parameters that triggered the task | +| `response_data` | `object` | Response data (populated after completion) | +| `error_message` | `str` | Error message (populated if the task failed) | +| `associated_objects` | `list[object]` | Objects related to this task (media buys, creatives, products) | + +### Error Codes + +{: .table .table-bordered .table-striped } +| Error Code | Description | +|------------|-------------| +| `no_tenant_context` | No tenant context available. Authentication is required. | +| `task_not_found` | No task exists with the specified `task_id`. | + +### Example + +```python +from mcp import ClientSession + +async with ClientSession(transport) as session: + await session.initialize() + + # Get details for a specific task + result = await session.call_tool( + "get_task", + arguments={"task_id": "task_001"}, + ) + task = result.content + print(f"Task: {task['task_id']}") + print(f"Status: {task['status']}") + print(f"Type: {task['type']}") + print(f"Created: {task['created_at']}") + if task.get("request_data"): + print(f"Request: {task['request_data']}") +``` + +Example response: + +```json +{ + "task_id": "task_001", + "context_id": "ctx_session_abc", + "status": "requires_approval", + "type": "media_buy_approval", + "tool_name": "create_media_buy", + "owner": "buyer_agent_001", + "created_at": "2026-02-20T14:30:00Z", + "request_data": { + "media_buy_id": "mb_abc123", + "packages": [ + {"product_id": "prod_001", "budget": 5000} + ] + }, + "response_data": null, + "error_message": null, + "associated_objects": [ + {"type": "media_buy", "id": "mb_abc123"} + ] +} +``` + +--- + +## complete_task + +Complete a pending workflow task. This simulates human approval or records an async completion. Only tasks in a pending or approval-required state can be completed. + +### Parameters + +{: .table .table-bordered .table-striped } +| Name | Type | Required | Description | Default | +|------|------|----------|-------------|---------| +| `task_id` | `str` | Yes | ID of the task to complete. | -- | +| `status` | `str` | No | Completion status. Must be `completed` or `failed`. | `"completed"` | +| `response_data` | `dict` | No | Arbitrary response data to attach to the completed task. | `None` | +| `error_message` | `str` | No | Error message when completing with `failed` status. | `None` | + +### Response + +{: .table .table-bordered .table-striped } +| Field | Type | Description | +|-------|------|-------------| +| `task_id` | `str` | ID of the completed task | +| `status` | `str` | Final task status (`completed` or `failed`) | +| `message` | `str` | Human-readable completion message | +| `completed_at` | `str` | ISO 8601 timestamp of completion | +| `completed_by` | `str` | Identifier of the principal that completed the task | + +### Error Codes + +{: .table .table-bordered .table-striped } +| Error Code | Description | +|------------|-------------| +| `no_tenant_context` | No tenant context available. Authentication is required. | +| `task_not_found` | No task exists with the specified `task_id`. | +| `task_already_completed` | The task has already been completed and cannot be modified. | +| `invalid_status` | The `status` parameter must be `completed` or `failed`. | + +### Example + +```python +from mcp import ClientSession + +async with ClientSession(transport) as session: + await session.initialize() + + # Approve a pending task + result = await session.call_tool( + "complete_task", + arguments={ + "task_id": "task_001", + "status": "completed", + "response_data": { + "approved_by": "publisher_ops", + "notes": "Creative assets verified.", + }, + }, + ) + print(f"Task {result.content['task_id']}: {result.content['status']}") + print(f"Completed at: {result.content['completed_at']}") + + # Reject a task + result = await session.call_tool( + "complete_task", + arguments={ + "task_id": "task_002", + "status": "failed", + "error_message": "Creative does not meet brand safety requirements.", + }, + ) +``` + +Example response (approval): + +```json +{ + "task_id": "task_001", + "status": "completed", + "message": "Task completed successfully.", + "completed_at": "2026-02-20T15:45:00Z", + "completed_by": "publisher_ops" +} +``` + +Example response (rejection): + +```json +{ + "task_id": "task_002", + "status": "failed", + "message": "Task marked as failed.", + "completed_at": "2026-02-20T15:50:00Z", + "completed_by": "publisher_ops" +} +``` + +## Related Tools + +- [create_media_buy](/agents/salesagent/tools/create-media-buy.html) -- Creates media buys that may produce approval tasks +- [sync_creatives](/agents/salesagent/tools/sync-creatives.html) -- Syncs creatives that may produce review tasks +- [get_media_buys](/agents/salesagent/tools/get-media-buys.html) -- Check media buy status after task completion +- [Tool Reference](/agents/salesagent/tools/tool-reference.html) -- Complete tool catalog + +## Protocol Specification + +{: .alert.alert-info :} +The `list_tasks`, `get_task`, and `complete_task` tools implement the AdCP workflow management layer. See the [AdCP Specification](https://docs.adcontextprotocol.org/docs/intro) for the full protocol definition including task state machine, approval semantics, and webhook notification format. diff --git a/agents/salesagent/tutorials/campaign-lifecycle.md b/agents/salesagent/tutorials/campaign-lifecycle.md new file mode 100644 index 0000000000..f983e0c17b --- /dev/null +++ b/agents/salesagent/tutorials/campaign-lifecycle.md @@ -0,0 +1,658 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Tutorials - End-to-End Campaign Lifecycle Tutorial +description: Complete step-by-step tutorial walking through an entire campaign lifecycle using the mock adapter and Python FastMCP client +sidebarType: 10 +--- + +# End-to-End Campaign Lifecycle Tutorial +{: .no_toc} + +This tutorial walks through the complete lifecycle of an advertising campaign using the Prebid Sales Agent's MCP interface and the mock adapter. You will discover capabilities, search for products, create a media buy, upload creatives, and track delivery -- all from Python code using the FastMCP client. + +- TOC +{:toc} + +## Prerequisites + +Before starting this tutorial, ensure you have the following: + +{: .table .table-bordered .table-striped } +| Requirement | Details | +|-------------|---------| +| Running Sales Agent | Docker Compose with `CREATE_DEMO_TENANT=true` and `ADCP_AUTH_TEST_MODE=true`. See [Quick Start](/agents/salesagent/getting-started/quickstart.html). | +| Python 3.11+ | Required for async/await and type hints | +| fastmcp package | `pip install fastmcp` | +| httpx package | `pip install httpx` (dependency of fastmcp) | + +{: .alert.alert-info :} +This tutorial uses the **mock adapter**, so all ad server responses are simulated. No real ad server connection is required. The mock adapter returns realistic but synthetic data, making it safe to experiment freely. + +## Step 1: Connect to the Server + +Establish a connection to the Sales Agent's MCP endpoint using `StreamableHttpTransport`. The transport handles HTTP/SSE communication and passes your authentication token with every request. + +```python +import asyncio +import json +from fastmcp import Client +from fastmcp.client.transports import StreamableHttpTransport + +async def main(): + # Create transport with auth token + transport = StreamableHttpTransport( + "http://localhost:8000/mcp/", + headers={"x-adcp-auth": "test-token"} + ) + + async with Client(transport) as client: + print("Connected to Sales Agent MCP server") + + # All subsequent steps happen inside this context manager + # ... + +asyncio.run(main()) +``` + +The `x-adcp-auth` header carries your authentication token. In test mode, `test-token` is accepted. In production, this would be a token generated through the Admin UI for your advertiser account. + +## Step 2: Discover Capabilities + +The first tool a buying agent should call is [`get_adcp_capabilities`](/agents/salesagent/tools/get-adcp-capabilities.html). This returns the agent's identity, supported protocol versions, available tools, and targeting systems. + +```python +async with Client(transport) as client: + # Discover what the Sales Agent can do + result = await client.call_tool("get_adcp_capabilities") + capabilities = result + + print(f"Agent: {capabilities['agent_name']}") + print(f"Version: {capabilities['agent_version']}") + print(f"AdCP versions: {capabilities['adcp_versions']}") + print(f"Skills: {capabilities['supported_skills']}") + print(f"Tools: {[t['name'] for t in capabilities['supported_tools']]}") + print(f"Geo systems: {capabilities['geo_targeting_systems']}") +``` + +Expected response: + +```json +{ + "agent_name": "Prebid Sales Agent", + "agent_version": "1.0.0", + "adcp_versions": ["1.0.0"], + "supported_skills": [ + "product_discovery", + "media_buying", + "creative_management", + "delivery_reporting" + ], + "supported_tools": [ + {"name": "get_adcp_capabilities", "description": "Protocol capability discovery"}, + {"name": "get_products", "description": "AI-powered product search"}, + {"name": "create_media_buy", "description": "Campaign creation"}, + {"name": "sync_creatives", "description": "Upload and manage creatives"}, + {"name": "get_media_buy_delivery", "description": "Delivery metrics"}, + {"name": "get_media_buys", "description": "Query media buys"} + ], + "geo_targeting_systems": ["geonames", "iso3166"], + "authorized_domains": ["demo-publisher.example.com"], + "creative_formats": ["display", "video", "audio", "native"] +} +``` + +Use this response to confirm which tools are available and what targeting systems are supported before proceeding. + +## Step 3: Search Products + +Use [`get_products`](/agents/salesagent/tools/get-products.html) to search the publisher's advertising catalog with a natural language brief. The AI-powered search engine interprets your request and returns matching products with pricing, formats, and availability. + +```python +async with Client(transport) as client: + # Search for video advertising products + result = await client.call_tool( + "get_products", + { + "brief": "video ads for sports content", + "brand_manifest": { + "brand_name": "SportsBrand Co", + "category": "sporting_goods" + } + } + ) + products = result + + print(f"Found {products['total_results']} products:") + for product in products["products"]: + print(f" - {product['name']} (ID: {product['product_id']})") + print(f" Channels: {product['channels']}") + print(f" Pricing: {product['pricing_options']}") + print(f" Formats: {product['format_ids']}") +``` + +Expected response: + +```json +{ + "products": [ + { + "product_id": "prod_sports_video_001", + "name": "Premium Sports Video - Pre-Roll", + "description": "15s and 30s pre-roll video across sports editorial content", + "pricing_options": [ + {"model": "CPM", "amount": 28.00, "currency": "USD", "id": "price_cpm_28"} + ], + "format_ids": ["video_16x9_preroll_15s", "video_16x9_preroll_30s"], + "estimated_exposures": { + "min_impressions": 200000, + "max_impressions": 800000, + "period": "monthly" + }, + "channels": ["video"], + "countries": ["US", "GB", "CA"] + } + ], + "total_results": 1 +} +``` + +Note the `product_id` and `pricing_options[].id` values -- you will need these when creating a media buy. + +## Step 4: Check Creative Formats + +Before creating a campaign, verify what creative specifications the chosen product requires by calling [`list_creative_formats`](/agents/salesagent/tools/list-creative-formats.html). + +```python +async with Client(transport) as client: + # Get creative format specs for the product's format IDs + result = await client.call_tool( + "list_creative_formats", + { + "format_ids": ["video_16x9_preroll_15s", "video_16x9_preroll_30s"] + } + ) + formats = result + + for fmt in formats["formats"]: + print(f"Format: {fmt['format_id']}") + print(f" Type: {fmt['media_type']}") + print(f" Dimensions: {fmt['width']}x{fmt['height']}") + print(f" Max file size: {fmt['max_file_size_bytes']} bytes") + print(f" Accepted MIME types: {fmt['mime_types']}") + print(f" Duration: {fmt.get('duration_seconds', 'N/A')}s") +``` + +Expected response: + +```json +{ + "formats": [ + { + "format_id": "video_16x9_preroll_15s", + "media_type": "video", + "width": 1920, + "height": 1080, + "max_file_size_bytes": 15728640, + "mime_types": ["video/mp4", "video/webm"], + "duration_seconds": 15, + "aspect_ratio": "16:9" + }, + { + "format_id": "video_16x9_preroll_30s", + "media_type": "video", + "width": 1920, + "height": 1080, + "max_file_size_bytes": 31457280, + "mime_types": ["video/mp4", "video/webm"], + "duration_seconds": 30, + "aspect_ratio": "16:9" + } + ] +} +``` + +Ensure your creative assets match these specifications before uploading. Mismatched formats will result in a `FORMAT_INCOMPATIBLE` error. + +## Step 5: Create a Media Buy + +With a product selected and format specs confirmed, create a media buy using [`create_media_buy`](/agents/salesagent/tools/create-media-buy.html). This is the core execution step that creates a campaign. + +```python +async with Client(transport) as client: + # Create a media buy (campaign) + result = await client.call_tool( + "create_media_buy", + { + "brand_manifest": { + "brand_name": "SportsBrand Co", + "category": "sporting_goods", + "website": "https://sportsbrand.example.com" + }, + "campaign_name": "SportsBrand Q2 Video Campaign", + "start_date": "2025-07-01", + "end_date": "2025-07-31", + "packages": [ + { + "product_id": "prod_sports_video_001", + "budget_cents": 500000, + "pricing_option_id": "price_cpm_28", + "targeting": { + "countries": ["US"], + "devices": ["desktop", "mobile"] + } + } + ] + } + ) + media_buy = result + + print(f"Media Buy ID: {media_buy['media_buy_id']}") + print(f"Status: {media_buy['status']}") + print(f"Total Budget: ${media_buy['total_budget_cents'] / 100:.2f}") +``` + +Expected response: + +```json +{ + "media_buy_id": "mb_abc123xyz", + "status": "pending_approval", + "campaign_name": "SportsBrand Q2 Video Campaign", + "total_budget_cents": 500000, + "start_date": "2025-07-01", + "end_date": "2025-07-31", + "packages": [ + { + "package_id": "pkg_001", + "product_id": "prod_sports_video_001", + "budget_cents": 500000, + "pricing_option_id": "price_cpm_28", + "status": "pending" + } + ], + "created_at": "2025-06-15T14:30:00Z" +} +``` + +{: .alert.alert-info :} +The `create_media_buy` tool is **asynchronous**. The response confirms that the media buy has been accepted, but actual provisioning in the ad server happens in the background. The initial status is typically `pending_approval`, meaning a human publisher must approve the buy before it goes live. Poll with `get_media_buys` to track status changes. + +### Understanding the Packages Array + +Each package in the `packages` array represents a line item within the media buy: + +{: .table .table-bordered .table-striped } +| Field | Type | Description | +|-------|------|-------------| +| `product_id` | `str` | The product to purchase (from `get_products` results) | +| `budget_cents` | `int` | Budget for this package in cents (e.g., 500000 = $5,000.00) | +| `pricing_option_id` | `str` | Which pricing model to use (from the product's `pricing_options`) | +| `targeting` | `object` | Optional targeting overrides (countries, devices, audience segments) | + +A single media buy can contain multiple packages targeting different products, enabling multi-placement campaigns in a single request. + +## Step 6: Upload Creatives + +Once the media buy exists, upload creative assets using [`sync_creatives`](/agents/salesagent/tools/sync-creatives.html). The creative format must match the specifications from Step 4. + +```python +async with Client(transport) as client: + # Upload creatives for the media buy + result = await client.call_tool( + "sync_creatives", + { + "media_buy_id": "mb_abc123xyz", + "creatives": [ + { + "name": "SportsBrand 15s Pre-Roll", + "format_id": "video_16x9_preroll_15s", + "asset_url": "https://cdn.sportsbrand.example.com/ads/q2_preroll_15s.mp4", + "click_through_url": "https://sportsbrand.example.com/summer-sale", + "metadata": { + "duration_seconds": 15, + "mime_type": "video/mp4" + } + }, + { + "name": "SportsBrand 30s Pre-Roll", + "format_id": "video_16x9_preroll_30s", + "asset_url": "https://cdn.sportsbrand.example.com/ads/q2_preroll_30s.mp4", + "click_through_url": "https://sportsbrand.example.com/summer-sale", + "metadata": { + "duration_seconds": 30, + "mime_type": "video/mp4" + } + } + ] + } + ) + creative_result = result + + for creative in creative_result["creatives"]: + print(f"Creative: {creative['name']}") + print(f" ID: {creative['creative_id']}") + print(f" Status: {creative['status']}") +``` + +Expected response: + +```json +{ + "media_buy_id": "mb_abc123xyz", + "creatives": [ + { + "creative_id": "cr_001", + "name": "SportsBrand 15s Pre-Roll", + "format_id": "video_16x9_preroll_15s", + "status": "pending_review", + "asset_url": "https://cdn.sportsbrand.example.com/ads/q2_preroll_15s.mp4" + }, + { + "creative_id": "cr_002", + "name": "SportsBrand 30s Pre-Roll", + "format_id": "video_16x9_preroll_30s", + "status": "pending_review", + "asset_url": "https://cdn.sportsbrand.example.com/ads/q2_preroll_30s.mp4" + } + ], + "synced_at": "2025-06-15T14:35:00Z" +} +``` + +{: .alert.alert-warning :} +Creative uploads are also asynchronous. The `pending_review` status means the publisher must approve the creative assets before they are associated with live ad serving. Ensure your `format_id` matches a format supported by the product, or you will receive a `FORMAT_INCOMPATIBLE` error. + +## Step 7: Track Delivery + +Once the campaign is approved and running, monitor delivery metrics using [`get_media_buy_delivery`](/agents/salesagent/tools/get-media-buy-delivery.html). + +```python +async with Client(transport) as client: + # Get delivery metrics + result = await client.call_tool( + "get_media_buy_delivery", + { + "media_buy_id": "mb_abc123xyz" + } + ) + delivery = result + + print(f"Media Buy: {delivery['media_buy_id']}") + print(f"Status: {delivery['status']}") + print(f"Impressions: {delivery['impressions']:,}") + print(f"Clicks: {delivery['clicks']:,}") + print(f"CTR: {delivery['ctr']:.2%}") + print(f"Spend: ${delivery['spend_cents'] / 100:.2f}") + print(f"Budget remaining: ${delivery['budget_remaining_cents'] / 100:.2f}") +``` + +Expected response: + +```json +{ + "media_buy_id": "mb_abc123xyz", + "status": "delivering", + "impressions": 142857, + "clicks": 1286, + "ctr": 0.009, + "spend_cents": 400000, + "budget_remaining_cents": 100000, + "start_date": "2025-07-01", + "end_date": "2025-07-31", + "packages": [ + { + "package_id": "pkg_001", + "product_id": "prod_sports_video_001", + "impressions": 142857, + "clicks": 1286, + "spend_cents": 400000 + } + ], + "last_updated": "2025-07-18T12:00:00Z" +} +``` + +{: .alert.alert-info :} +With the mock adapter, delivery data is simulated. In production with a real ad server adapter (e.g., Google Ad Manager), these numbers reflect actual delivery metrics from the ad server. + +## Step 8: Check Campaign Status + +Use `get_media_buys` to list all your media buys and check their current status. This is useful for monitoring multiple campaigns at once. + +```python +async with Client(transport) as client: + # List all media buys + result = await client.call_tool("get_media_buys", {}) + media_buys = result + + print(f"Total media buys: {len(media_buys['media_buys'])}") + for mb in media_buys["media_buys"]: + print(f" [{mb['status']}] {mb['campaign_name']} " + f"(ID: {mb['media_buy_id']}, " + f"Budget: ${mb['total_budget_cents'] / 100:.2f})") +``` + +Expected response: + +```json +{ + "media_buys": [ + { + "media_buy_id": "mb_abc123xyz", + "campaign_name": "SportsBrand Q2 Video Campaign", + "status": "delivering", + "total_budget_cents": 500000, + "start_date": "2025-07-01", + "end_date": "2025-07-31", + "created_at": "2025-06-15T14:30:00Z" + } + ] +} +``` + +### Media Buy Status Values + +{: .table .table-bordered .table-striped } +| Status | Description | +|--------|-------------| +| `pending_approval` | Awaiting publisher approval | +| `approved` | Approved but not yet delivering (before start date) | +| `delivering` | Actively serving ads | +| `paused` | Temporarily paused by publisher or buyer | +| `completed` | Flight dates ended; delivery finished | +| `rejected` | Publisher rejected the media buy | +| `cancelled` | Cancelled by the buyer | + +## Complete Working Example + +Here is the complete tutorial code in a single runnable script: + +```python +import asyncio +import json +from fastmcp import Client +from fastmcp.client.transports import StreamableHttpTransport + + +async def run_campaign_lifecycle(): + """Complete campaign lifecycle using the mock adapter.""" + + transport = StreamableHttpTransport( + "http://localhost:8000/mcp/", + headers={"x-adcp-auth": "test-token"} + ) + + async with Client(transport) as client: + # Step 1: Discover capabilities + print("=== Step 1: Discover Capabilities ===") + capabilities = await client.call_tool("get_adcp_capabilities") + print(f"Agent: {capabilities['agent_name']}") + print(f"Tools: {[t['name'] for t in capabilities['supported_tools']]}") + print() + + # Step 2: Search for products + print("=== Step 2: Search Products ===") + products_result = await client.call_tool( + "get_products", + { + "brief": "video ads for sports content", + "brand_manifest": { + "brand_name": "SportsBrand Co", + "category": "sporting_goods" + } + } + ) + product = products_result["products"][0] + print(f"Selected: {product['name']} ({product['product_id']})") + print(f"Pricing: {product['pricing_options']}") + print() + + # Step 3: Check creative formats + print("=== Step 3: Check Creative Formats ===") + formats_result = await client.call_tool( + "list_creative_formats", + {"format_ids": product["format_ids"]} + ) + for fmt in formats_result["formats"]: + print(f" {fmt['format_id']}: {fmt['width']}x{fmt['height']} " + f"({fmt['media_type']}, {fmt.get('duration_seconds', 'N/A')}s)") + print() + + # Step 4: Create a media buy + print("=== Step 4: Create Media Buy ===") + pricing_id = product["pricing_options"][0]["id"] + media_buy = await client.call_tool( + "create_media_buy", + { + "brand_manifest": { + "brand_name": "SportsBrand Co", + "category": "sporting_goods", + "website": "https://sportsbrand.example.com" + }, + "campaign_name": "SportsBrand Q2 Video Campaign", + "start_date": "2025-07-01", + "end_date": "2025-07-31", + "packages": [ + { + "product_id": product["product_id"], + "budget_cents": 500000, + "pricing_option_id": pricing_id, + "targeting": { + "countries": ["US"], + "devices": ["desktop", "mobile"] + } + } + ] + } + ) + media_buy_id = media_buy["media_buy_id"] + print(f"Created: {media_buy_id} (Status: {media_buy['status']})") + print() + + # Step 5: Upload creatives + print("=== Step 5: Upload Creatives ===") + format_id = product["format_ids"][0] + creative_result = await client.call_tool( + "sync_creatives", + { + "media_buy_id": media_buy_id, + "creatives": [ + { + "name": "SportsBrand Pre-Roll", + "format_id": format_id, + "asset_url": "https://cdn.sportsbrand.example.com/ads/preroll.mp4", + "click_through_url": "https://sportsbrand.example.com/sale", + "metadata": { + "duration_seconds": 15, + "mime_type": "video/mp4" + } + } + ] + } + ) + for cr in creative_result["creatives"]: + print(f" Creative: {cr['name']} ({cr['creative_id']}) - {cr['status']}") + print() + + # Step 6: Track delivery + print("=== Step 6: Track Delivery ===") + delivery = await client.call_tool( + "get_media_buy_delivery", + {"media_buy_id": media_buy_id} + ) + print(f"Impressions: {delivery.get('impressions', 0):,}") + print(f"Clicks: {delivery.get('clicks', 0):,}") + print(f"Spend: ${delivery.get('spend_cents', 0) / 100:.2f}") + print() + + # Step 7: Check status + print("=== Step 7: Check Campaign Status ===") + all_buys = await client.call_tool("get_media_buys", {}) + for mb in all_buys["media_buys"]: + print(f" [{mb['status']}] {mb['campaign_name']}") + + +if __name__ == "__main__": + asyncio.run(run_campaign_lifecycle()) +``` + +## Testing Tips + +The mock adapter supports several special HTTP headers for advanced testing scenarios. Pass these headers in the `StreamableHttpTransport` constructor or add them per-request. + +### X-Dry-Run Header + +Set `X-Dry-Run: true` to validate a request without persisting any changes. Useful for testing `create_media_buy` calls without creating actual campaigns: + +```python +transport = StreamableHttpTransport( + "http://localhost:8000/mcp/", + headers={ + "x-adcp-auth": "test-token", + "X-Dry-Run": "true" + } +) +``` + +### X-Mock-Time Header + +Override the server's current time to test time-dependent behavior such as flight date validation and delivery pacing: + +```python +transport = StreamableHttpTransport( + "http://localhost:8000/mcp/", + headers={ + "x-adcp-auth": "test-token", + "X-Mock-Time": "2025-07-15T12:00:00Z" + } +) +``` + +### X-Test-Session-ID Header + +Isolate test data by providing a session ID. All data created within a test session is automatically cleaned up when the session ends: + +```python +transport = StreamableHttpTransport( + "http://localhost:8000/mcp/", + headers={ + "x-adcp-auth": "test-token", + "X-Test-Session-ID": "my-test-session-001" + } +) +``` + +{: .alert.alert-warning :} +These testing headers are only available when the server is running with the mock adapter. They have no effect in production deployments with real ad server adapters. + +## Further Reading + +- [Tool Reference](/agents/salesagent/tools/tool-reference.html) -- Complete catalog of all 14 MCP tools +- [get_adcp_capabilities](/agents/salesagent/tools/get-adcp-capabilities.html) -- Capability discovery details +- [get_products](/agents/salesagent/tools/get-products.html) -- Product search parameters and filtering +- [create_media_buy](/agents/salesagent/tools/create-media-buy.html) -- Media buy creation reference +- [sync_creatives](/agents/salesagent/tools/sync-creatives.html) -- Creative upload and management +- [get_media_buy_delivery](/agents/salesagent/tools/get-media-buy-delivery.html) -- Delivery metrics reference +- [Error Code Reference](/agents/salesagent/reference/error-codes.html) -- Troubleshooting error responses +- [Quick Start](/agents/salesagent/getting-started/quickstart.html) -- Server setup and Docker deployment +- [Architecture & Protocols](/agents/salesagent/architecture.html) -- How MCP and A2A protocols work diff --git a/agents/salesagent/tutorials/catalog-setup.md b/agents/salesagent/tutorials/catalog-setup.md new file mode 100644 index 0000000000..ef149a0871 --- /dev/null +++ b/agents/salesagent/tutorials/catalog-setup.md @@ -0,0 +1,221 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Tutorials - Publisher Catalog Setup +description: Step-by-step tutorial for creating advertising products with pricing, targeting, and creative formats +sidebarType: 10 +--- + +# Publisher Catalog Setup Tutorial +{: .no_toc} + +This tutorial walks through creating a product catalog that the Sales Agent uses to match advertiser briefs to your inventory. + +- TOC +{:toc} + +## Prerequisites + +Before starting this tutorial, you need: + +- A running Sales Agent instance (see [Quick Start](/agents/salesagent/getting-started/quickstart.html)) +- Admin access to the publisher tenant +- At least one configured ad server adapter (or the [mock adapter](/agents/salesagent/tutorials/mock-testing.html) for testing) + +## Step 1: Plan Your Products + +Map your ad inventory to products that advertisers can purchase. Each product represents a distinct advertising opportunity with its own pricing, targeting, and creative requirements. + +Common product types: + +{: .table .table-bordered .table-striped } +| Product Type | Channel | Typical Pricing | Example | +|-------------|---------|-----------------|---------| +| Display Banner | Web | CPM | 300x250 banner on sports section | +| Video Pre-roll | Web, CTV | CPCV | 15-second pre-roll on video content | +| Audio Spot | Audio | CPM | 30-second mid-roll on podcast feed | +| Native | Web, App | CPC | In-feed sponsored content card | +| Interstitial | App | CPM | Full-screen ad between app screens | +| Homepage Takeover | Web | Flat rate | 24-hour homepage sponsorship | + +{: .alert.alert-info :} +Product descriptions are used by the AI search engine when matching advertiser briefs to inventory. Write descriptions that include the content vertical, audience characteristics, and placement context. For example: "Premium video pre-roll on live sports streaming content reaching 18-34 male demographics" is more discoverable than "Video ad." + +## Step 2: Access the Admin UI + +Navigate to the Admin UI: + +``` +http://localhost:8000/admin +``` + +Log in with your admin credentials. Select **Products** from the navigation menu. + +## Step 3: Create a Product + +Click **New Product** and fill in the required fields: + +{: .table .table-bordered .table-striped } +| Field | Required | Description | +|-------|----------|-------------| +| Name | Yes | Display name for the product (e.g., "Display Banner - Sports Section") | +| Description | Yes | Detailed description used for AI search matching | +| Delivery Type | Yes | `guaranteed` (reserved inventory) or `non_guaranteed` (auction-based) | +| Channels | Yes | One or more: `web`, `app`, `ctv`, `audio` | +| Countries | Yes | ISO 3166-1 alpha-2 country codes where the product is available | + +### Delivery Types + +{: .table .table-bordered .table-striped } +| Type | Behavior | Use Case | +|------|----------|----------| +| `guaranteed` | Inventory is reserved for the buyer at a fixed price | Direct-sold campaigns, sponsorships | +| `non_guaranteed` | Inventory is available on a best-effort basis, may compete in auction | Programmatic campaigns, remnant inventory | + +## Step 4: Configure Pricing Options + +Each product can have multiple pricing options to support different buying models. Navigate to the **Pricing** tab for your product. + +### Supported Pricing Models + +{: .table .table-bordered .table-striped } +| Model | Unit | Description | +|-------|------|-------------| +| CPM | Cost per 1,000 impressions | Standard display and video pricing | +| vCPM | Cost per 1,000 viewable impressions | Viewability-guaranteed pricing | +| CPC | Cost per click | Performance-oriented pricing | +| CPCV | Cost per completed view | Video completion pricing | +| Flat rate | Cost per time period | Sponsorships and takeovers | + +For each pricing option, configure: + +- **Rate** -- The unit price (e.g., `$12.50` CPM) +- **Currency** -- ISO 4217 currency code (e.g., `USD`, `EUR`, `GBP`) +- **Minimum spend** -- The minimum total spend required to book this product at this rate + +``` +Example: + Model: CPM + Rate: $12.50 + Currency: USD + Minimum Spend: $5,000 +``` + +{: .alert.alert-warning :} +Pricing options are visible to the Sales Agent during media buy creation. Set rates that reflect your actual rate card. The agent uses these rates when generating proposals and calculating campaign totals. + +## Step 5: Set Up Targeting + +Navigate to the **Targeting** tab. Targeting options define what inventory segments the product covers and what additional targeting the buyer can apply. + +### Geographic Targeting + +{: .table .table-bordered .table-striped } +| Level | Examples | Notes | +|-------|----------|-------| +| Countries | `US`, `GB`, `DE` | ISO 3166-1 alpha-2 | +| Regions | `US-CA`, `US-NY` | ISO 3166-2 | +| Metros / DMAs | `501` (New York), `803` (Los Angeles) | Nielsen DMA codes | +| Postal codes | `10001`, `SW1A 1AA` | Country-specific format | + +### Device Targeting + +Select one or more device types: + +- Desktop +- Mobile +- Tablet +- Connected TV +- Set-top box + +### Content Targeting + +Assign IAB content categories to associate the product with specific content verticals (e.g., `IAB17` for Sports, `IAB1` for Arts & Entertainment). + +### Custom Key-Value Targeting + +Define custom key-value pairs for targeting dimensions specific to your inventory: + +``` +Key: section +Values: sports, news, entertainment, lifestyle + +Key: position +Values: above_fold, below_fold, sidebar +``` + +## Step 6: Configure Creative Formats + +Navigate to the **Creatives** tab. Link the product to supported creative formats by referencing format IDs from the `list_creative_formats` tool. + +To see available formats: + +```bash +uvx adcp http://localhost:8000/mcp/ --auth YOUR_TOKEN list_creative_formats '{}' +``` + +For each linked format, specify: + +{: .table .table-bordered .table-striped } +| Field | Description | Example | +|-------|-------------|---------| +| Format ID | Reference to a creative format definition | `banner_300x250` | +| Accepted media types | MIME types the ad server accepts | `image/jpeg`, `image/png`, `text/html` | +| Max file size | Maximum creative file size in bytes | `150000` (150 KB) | +| Dimensions | Width x height in pixels (for display) | `300x250` | + +## Step 7: Test with AI Search + +After creating your products, verify that the AI search engine can discover them. The `get_products` tool accepts a natural-language brief and returns matching products: + +```bash +uvx adcp http://localhost:8000/mcp/ --auth YOUR_TOKEN \ + get_products '{"brief":"video ads for sports content"}' +``` + +Expected output includes your video products with sports-related descriptions, ranked by relevance. + +If your products are not returned: + +1. Check that the product description includes relevant keywords. +2. Verify the product is not disabled or archived. +3. Confirm the product's channels and countries match the implicit or explicit brief criteria. + +## Step 8: Verify with a Test Media Buy + +Create a test campaign to validate the full flow from product selection through order creation: + +```bash +uvx adcp http://localhost:8000/mcp/ --auth YOUR_TOKEN \ + create_media_buy '{ + "product_id": "YOUR_PRODUCT_ID", + "advertiser_name": "Test Advertiser", + "campaign_name": "Catalog Validation Test", + "start_date": "2026-03-01", + "end_date": "2026-03-31", + "impressions": 100000 + }' +``` + +{: .alert.alert-info :} +Using the [mock adapter](/agents/salesagent/tutorials/mock-testing.html) for this step avoids creating real orders in your ad server. The mock adapter simulates the full campaign lifecycle without external dependencies. + +Verify the media buy was created: + +```bash +uvx adcp http://localhost:8000/mcp/ --auth YOUR_TOKEN \ + get_media_buy '{"media_buy_id": "RETURNED_ID"}' +``` + +Confirm that: + +- The product ID and pricing match your catalog configuration +- Targeting defaults are applied correctly +- The campaign dates and impression goals are set as specified + +## Further Reading + +- [Publisher Onboarding](/agents/salesagent/getting-started/publisher-onboarding.html) -- Full onboarding guide for new publishers +- [Admin UI Guide](/agents/salesagent/overview/admin-ui.html) -- Admin interface reference +- [get_products](/agents/salesagent/tools/tool-reference.html) -- Tool reference for product search +- [list_creative_formats](/agents/salesagent/tools/tool-reference.html) -- Tool reference for creative format listing +- [Campaign Lifecycle Tutorial](/agents/salesagent/tutorials/campaign-lifecycle.html) -- End-to-end campaign workflow diff --git a/agents/salesagent/tutorials/mock-testing.md b/agents/salesagent/tutorials/mock-testing.md new file mode 100644 index 0000000000..edff3cf32b --- /dev/null +++ b/agents/salesagent/tutorials/mock-testing.md @@ -0,0 +1,200 @@ +--- +layout: page_v2 +title: Prebid Sales Agent - Tutorials - Testing with the Mock Adapter +description: Tutorial for using the mock adapter to test campaign workflows without a real ad server +sidebarType: 10 +--- + +# Testing with the Mock Adapter +{: .no_toc} + +This tutorial walks through using the mock adapter to test the full campaign lifecycle without connecting to a real ad server. The mock adapter simulates all ad server operations including delivery, creative processing, and approval workflows. + +- TOC +{:toc} + +## Prerequisites + +A running Sales Agent instance with Docker. See the [Quick Start](/agents/salesagent/getting-started/quickstart.html) guide if you have not deployed yet. + +## Step 1: Create a Test Tenant with Mock Adapter + +```bash +docker-compose exec adcp-server python -m scripts.setup.setup_tenant "Test Publisher" --adapter mock +``` + +This creates a tenant configured to use the mock adapter instead of a real ad server. + +## Step 2: Get Your Test Token + +Retrieve the principal token for API access: + +```bash +docker-compose exec postgres psql -U adcp_user -d adcp \ + -c "SELECT access_token FROM principals LIMIT 1;" +``` + +Save this token for use in subsequent commands. + +## Step 3: Discover Products + +```bash +uvx adcp http://localhost:8000/mcp/ --auth YOUR_TOKEN get_products '{"brief":"all products"}' +``` + +The mock adapter returns a set of pre-configured products spanning display, video, audio, and native channels. Note the `product_id` values for the next step. + +## Step 4: Create a Media Buy + +```bash +uvx adcp http://localhost:8000/mcp/ --auth YOUR_TOKEN create_media_buy '{ + "campaign_name": "Test Campaign", + "media_buy_start_date": "2026-03-01", + "media_buy_end_date": "2026-03-31", + "packages": [{ + "product_id": "PRODUCT_ID_FROM_STEP_3", + "budget": 5000, + "currency": "USD" + }] +}' +``` + +The mock adapter processes this immediately (or routes through approval, depending on HITL configuration). Note the `media_buy_id` from the response. + +## Step 5: Upload Creatives + +```bash +uvx adcp http://localhost:8000/mcp/ --auth YOUR_TOKEN sync_creatives '{ + "creatives": [{ + "name": "Test Banner", + "format_id": "FORMAT_ID", + "asset_url": "https://example.com/banner.jpg", + "click_through_url": "https://example.com/landing" + }], + "assignments": { + "PACKAGE_ID": ["CREATIVE_ID"] + } +}' +``` + +## Step 6: Check Delivery + +```bash +uvx adcp http://localhost:8000/mcp/ --auth YOUR_TOKEN get_media_buy_delivery '{ + "media_buy_id": "YOUR_MEDIA_BUY_ID" +}' +``` + +The mock adapter returns simulated delivery metrics based on the product's simulation parameters (daily impressions, fill rate, CTR, viewability). + +## Simulation Scenarios + +The mock adapter supports four simulation scenarios that control delivery behavior. Configure these through the product's simulation parameters in the Admin UI. + +{: .table .table-bordered .table-striped } +| Scenario | Behavior | +|----------|----------| +| `normal` | Standard delivery at configured rates | +| `high_demand` | Competitive inventory, higher CPMs, slower fill | +| `degraded` | Partial delivery failures, reduced fill rate | +| `outage` | Complete ad server failure, all operations return errors | + +### Product Simulation Parameters + +{: .table .table-bordered .table-striped } +| Parameter | Default | Description | +|-----------|---------|-------------| +| `daily_impressions` | 10,000 | Simulated daily impression volume | +| `fill_rate` | 85% | Percentage of available inventory filled | +| `ctr` | 2% | Simulated click-through rate | +| `viewability` | 65% | Simulated viewability rate | +| `scenario` | `normal` | Active simulation scenario | + +## Testing HITL Approval Workflows + +The mock adapter can simulate human-in-the-loop (HITL) approval flows where media buys or creatives require manual approval before activation. + +### Sync Mode + +In sync mode, the mock adapter delays responses to simulate human review time: + +1. Submit a media buy via `create_media_buy` +2. The response returns with status `pending_approval` +3. Use `list_tasks` to see the pending approval task +4. Use `complete_task` to approve or reject + +```python +# List pending tasks +tasks = await client.call_tool("list_tasks", {"status": "requires_approval"}) + +# Approve the task +await client.call_tool("complete_task", { + "task_id": tasks["tasks"][0]["task_id"], + "status": "completed" +}) +``` + +### Async Mode + +In async mode, tasks enter a pending state and can be completed via webhook or manual intervention: + +1. Submit a media buy — returns immediately with a task ID +2. Poll `get_task` or wait for a webhook notification +3. Complete the task when ready + +### Mixed Mode + +Mixed mode applies per-operation overrides. For example, media buys may require approval while creative uploads process automatically. + +Configure HITL mode through the adapter's `platform_mappings.mock.hitl_config` setting in the Admin UI. + +## Python Client Example + +```python +from fastmcp import Client +from fastmcp.client.transports import StreamableHttpTransport + +transport = StreamableHttpTransport( + "http://localhost:8000/mcp/", + headers={"x-adcp-auth": "YOUR_TOKEN"} +) + +async with Client(transport) as client: + # Discover products + products = await client.call_tool("get_products", {"brief": "display banner"}) + product_id = products["products"][0]["product_id"] + + # Create media buy + result = await client.call_tool("create_media_buy", { + "campaign_name": "Mock Test", + "media_buy_start_date": "2026-03-01", + "media_buy_end_date": "2026-03-31", + "packages": [{ + "product_id": product_id, + "budget": 5000, + "currency": "USD" + }] + }) + media_buy_id = result["media_buy_id"] + + # Check delivery + delivery = await client.call_tool("get_media_buy_delivery", { + "media_buy_id": media_buy_id + }) + print(f"Impressions: {delivery['impressions']}") + print(f"Spend: ${delivery['spend']:.2f}") +``` + +## Delivery Simulation + +The mock adapter supports time-accelerated delivery simulation with webhook notifications. This allows testing of pacing, budget depletion, and campaign completion without waiting for real calendar time. + +{: .alert.alert-info :} +For a detailed walkthrough of delivery simulation with working code, see `examples/delivery_simulation_demo.py` in the [salesagent repository](https://github.com/prebid/salesagent). + +## Further Reading + +- [Mock Adapter Reference](/agents/salesagent/integrations/mock-adapter.html) -- Full mock adapter documentation +- [Campaign Lifecycle Tutorial](/agents/salesagent/tutorials/campaign-lifecycle.html) -- End-to-end campaign walkthrough +- [Testing Guide](/agents/salesagent/developers/testing.html) -- Running the test suite +- [Workflow Tools](/agents/salesagent/tools/workflow-tools.html) -- HITL task management From 6917b3a694bc682ad3553770c89fc71fb8f2f24c Mon Sep 17 00:00:00 2001 From: agentmoose Date: Wed, 25 Feb 2026 10:11:23 -0700 Subject: [PATCH 2/3] fix errors --- agents/salesagent/deployment/fly-io.md | 8 ++++---- agents/salesagent/deployment/gcp.md | 13 +++++++------ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/agents/salesagent/deployment/fly-io.md b/agents/salesagent/deployment/fly-io.md index fdbfa226c1..1886e0b42a 100644 --- a/agents/salesagent/deployment/fly-io.md +++ b/agents/salesagent/deployment/fly-io.md @@ -163,10 +163,10 @@ For production authentication, configure an SSO provider: https://sales-agent.fly.dev/auth/oidc/callback ``` -3. In the Admin UI, go to **Settings > SSO Configuration** -4. Enter the Client ID and Client Secret -5. Save and test the login flow -6. Disable test mode: +1. In the Admin UI, go to **Settings > SSO Configuration** +1. Enter the Client ID and Client Secret +1. Save and test the login flow +1. Disable test mode: ```bash fly secrets unset ADCP_AUTH_TEST_MODE --app sales-agent diff --git a/agents/salesagent/deployment/gcp.md b/agents/salesagent/deployment/gcp.md index cb04c1998c..4e965d64e9 100644 --- a/agents/salesagent/deployment/gcp.md +++ b/agents/salesagent/deployment/gcp.md @@ -90,6 +90,7 @@ gcloud run deploy sales-agent \ {: .alert.alert-danger :} **Two flags are mandatory for the Sales Agent to function on Cloud Run:** + - `--no-cpu-throttling` -- The Sales Agent runs background services (MCP server, A2A server, scheduled tasks). Without this flag, Cloud Run throttles the CPU between requests, causing these services to stall. - `--min-instances=1` -- Keeps at least one instance warm at all times. Without this, cold starts cause MCP clients and A2A integrations to time out. @@ -200,10 +201,10 @@ https://sales-agent-HASH-uc.a.run.app/auth/oidc/callback Replace the URL with your actual Cloud Run service URL. -3. In the Admin UI, go to **Settings > SSO Configuration** -4. Enter the Client ID and Client Secret -5. Save and test the login flow -6. Disable test mode: +1. In the Admin UI, go to **Settings > SSO Configuration** +1. Enter the Client ID and Client Secret +1. Save and test the login flow +1. Disable test mode: ```bash gcloud run services update sales-agent \ @@ -352,8 +353,8 @@ If your database password contains special characters and the app cannot connect gcloud sql connect sales-agent-db --user=postgres ``` -2. URL-encode all special characters in the `DATABASE_URL` -3. Update the environment variable: +1. URL-encode all special characters in the `DATABASE_URL` +1. Update the environment variable: ```bash gcloud run services update sales-agent \ From 86771df839c770b3d3f283d1df8f2a295e5e7466 Mon Sep 17 00:00:00 2001 From: agentmoose Date: Wed, 25 Feb 2026 11:28:30 -0700 Subject: [PATCH 3/3] Fix markdownlint violations in salesagent docs - Add blank lines before lists (MD032) - Add language specifiers to fenced code blocks (MD040) - Wrap bare email in backticks (MD034) - Indent code block under list item to fix ordered list prefix (MD029) Co-Authored-By: Claude Opus 4.6 --- agents/salesagent/deployment/single-tenant.md | 27 ++++++++++--------- agents/salesagent/developers/contributing.md | 4 +-- .../getting-started/publisher-onboarding.md | 2 +- .../salesagent/getting-started/quickstart.md | 3 +++ agents/salesagent/operations/admin-ui.md | 3 +++ agents/salesagent/reference/error-codes.md | 1 + agents/salesagent/tutorials/catalog-setup.md | 6 ++--- 7 files changed, 27 insertions(+), 19 deletions(-) diff --git a/agents/salesagent/deployment/single-tenant.md b/agents/salesagent/deployment/single-tenant.md index 6da37bd275..f82f6a70e2 100644 --- a/agents/salesagent/deployment/single-tenant.md +++ b/agents/salesagent/deployment/single-tenant.md @@ -107,6 +107,7 @@ docker compose up -d ``` Docker Compose will: + 1. Pull the latest `ghcr.io/prebid/salesagent` image 2. Start PostgreSQL and wait for it to become healthy 3. Run database migrations automatically @@ -187,20 +188,20 @@ To serve the Sales Agent on your own domain (e.g., `ads.yourpublisher.com`): 1. Point your domain's DNS to the server running the Sales Agent (A record or CNAME) 2. Update the Nginx configuration in `nginx.conf` with your domain: -```nginx -server { - listen 80; - server_name ads.yourpublisher.com; - - location / { - proxy_pass http://adcp-server:8000; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + ```nginx + server { + listen 80; + server_name ads.yourpublisher.com; + + location / { + proxy_pass http://adcp-server:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } } -} -``` + ``` 3. Update the tenant's custom domain in the Admin UI under **Settings > Custom Domain** diff --git a/agents/salesagent/developers/contributing.md b/agents/salesagent/developers/contributing.md index 484f534c1d..f07748f07b 100644 --- a/agents/salesagent/developers/contributing.md +++ b/agents/salesagent/developers/contributing.md @@ -52,7 +52,7 @@ Pull request titles must follow the [Conventional Commits](https://www.conventio ### Format -``` +```text type(scope): description ``` @@ -78,7 +78,7 @@ The scope is optional but recommended. Use the module or area being changed (e.g Append `!` after the type/scope for breaking changes: -``` +```text feat(api)!: rename get_products to search_products ``` diff --git a/agents/salesagent/getting-started/publisher-onboarding.md b/agents/salesagent/getting-started/publisher-onboarding.md index 7ac79a8ce3..0648c376e7 100644 --- a/agents/salesagent/getting-started/publisher-onboarding.md +++ b/agents/salesagent/getting-started/publisher-onboarding.md @@ -161,7 +161,7 @@ Navigate to **Admin UI > Advertisers** to create advertiser accounts: | Field | Description | Example | |-------|-------------|---------| | Name | Advertiser or agency name | "Acme Corp" | -| Contact Email | Primary contact for notifications | "media@acme.com" | +| Contact Email | Primary contact for notifications | `media@acme.com` | | Status | Active or paused | `active` | ### Generating API Tokens diff --git a/agents/salesagent/getting-started/quickstart.md b/agents/salesagent/getting-started/quickstart.md index aecfc4dffe..1a6c3d3e74 100644 --- a/agents/salesagent/getting-started/quickstart.md +++ b/agents/salesagent/getting-started/quickstart.md @@ -55,6 +55,7 @@ docker compose up -d ``` Docker Compose will start: + - **PostgreSQL** on port 5432 (internal) - **Sales Agent** on port 8000 (exposed) @@ -169,6 +170,7 @@ docker compose logs salesagent ``` Common issues: + - **Database connection refused** -- Ensure PostgreSQL is running and `DATABASE_URL` is correct - **Port 8000 already in use** -- Stop the conflicting service or change the port mapping in `docker-compose.yml` - **Migration errors** -- Try removing volumes and restarting: `docker compose down -v && docker compose up -d` @@ -176,6 +178,7 @@ Common issues: ### MCP endpoint returns 401 Ensure you are passing the auth token in the correct header: + - MCP: `x-adcp-auth: test-token` - A2A: `Authorization: Bearer test-token` diff --git a/agents/salesagent/operations/admin-ui.md b/agents/salesagent/operations/admin-ui.md index 575817bdbb..e11fbd400d 100644 --- a/agents/salesagent/operations/admin-ui.md +++ b/agents/salesagent/operations/admin-ui.md @@ -61,6 +61,7 @@ The Admin UI dashboard provides a tenant-level overview of your Sales Agent inst - **Quick actions** -- Links to common tasks like creating products or generating advertiser tokens The activity feed updates in real time without page refreshes. Events include: + - New media buy requests - Creative approval/rejection actions - Advertiser authentication attempts @@ -231,6 +232,7 @@ Navigate to **Workflows > Pending Approvals** to review incoming media buy reque | Creatives | Associated creative assets | For each request, you can: + - **Approve** -- The campaign is created in the ad server - **Reject** -- The buying agent receives a rejection with the reason - **Request changes** -- Send feedback back to the buying agent @@ -283,6 +285,7 @@ To deactivate a tenant: 3. Confirm the action Deactivated tenants: + - Cannot authenticate via MCP or A2A - Are hidden from public discovery - Retain all historical data (campaigns, creatives, audit logs) diff --git a/agents/salesagent/reference/error-codes.md b/agents/salesagent/reference/error-codes.md index 6cb181844f..e765070d38 100644 --- a/agents/salesagent/reference/error-codes.md +++ b/agents/salesagent/reference/error-codes.md @@ -186,6 +186,7 @@ transport = StreamableHttpTransport( ``` Common mistakes: + - Using `Authorization: Bearer` instead of `x-adcp-auth` for MCP connections (Bearer is for A2A) - Forgetting to include the header in the transport constructor - Using an expired or revoked token diff --git a/agents/salesagent/tutorials/catalog-setup.md b/agents/salesagent/tutorials/catalog-setup.md index ef149a0871..7afb2c9386 100644 --- a/agents/salesagent/tutorials/catalog-setup.md +++ b/agents/salesagent/tutorials/catalog-setup.md @@ -44,7 +44,7 @@ Product descriptions are used by the AI search engine when matching advertiser b Navigate to the Admin UI: -``` +```text http://localhost:8000/admin ``` @@ -92,7 +92,7 @@ For each pricing option, configure: - **Currency** -- ISO 4217 currency code (e.g., `USD`, `EUR`, `GBP`) - **Minimum spend** -- The minimum total spend required to book this product at this rate -``` +```text Example: Model: CPM Rate: $12.50 @@ -135,7 +135,7 @@ Assign IAB content categories to associate the product with specific content ver Define custom key-value pairs for targeting dimensions specific to your inventory: -``` +```text Key: section Values: sports, news, entertainment, lifestyle