From 842b9f1b77029c7f22a217d1158040077d5e8bbe Mon Sep 17 00:00:00 2001 From: ATC964 <82515918+atc964@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:34:01 -0400 Subject: [PATCH 1/4] Add BEAD_PREFIX filter to PROGRESS.md generator to exclude non-buyer beads The generator had no prefix-based filtering, relying solely on the JSONL file containing only buyer- beads. This adds an explicit BEAD_PREFIX = "buyer-" guard in load_issues() so ar- or seller- beads are rejected even if they appear in the JSONL. Also regenerates PROGRESS.md and fixes stale Phase 4 assertions in end-to-end tests. bead: ar-sl73 Co-Authored-By: Claude Opus 4.6 (1M context) --- .beads/PROGRESS.md | 151 +++++++++++++++++++++++-------- .beads/generate_progress.py | 11 ++- .beads/test_generate_progress.py | 35 +++++-- 3 files changed, 149 insertions(+), 48 deletions(-) diff --git a/.beads/PROGRESS.md b/.beads/PROGRESS.md index 2e13ba9..eb67d64 100644 --- a/.beads/PROGRESS.md +++ b/.beads/PROGRESS.md @@ -1,8 +1,8 @@ -# Buyer Agent v2 — Progress +# Buyer Agent V2 — Progress -**0 open** | **0 in progress** | **54 closed** | **0 blocked** | 54 total +**1 open** | **1 in progress** | **106 closed** | **0 blocked** | 108 total -`[████████████████████] 100% (54/54)` +`[████████████████████] 98% (106/108)` ## Phase 1 — Seller Interoperability @@ -23,7 +23,6 @@ | | ID | Task | Priority | Blockers | Done | |---|---|---|---|---|---| -| \[x] | buyer-an0 | Phase 2: Campaign Automation | P1 | — | 2026-03-19 | | \[x] | buyer-8ih | 2A: Multi-Seller Deal Orchestration | P2 | — | 2026-03-19 | | \[x] | buyer-u8l | 2B: Campaign Brief to Deal Pipeline | P2 | — | 2026-03-19 | | \[x] | buyer-9zz | 2C: Budget Pacing & Reallocation | P2 | — | 2026-03-19 | @@ -42,47 +41,123 @@ | \[x] | buyer-lna | Pacing snapshot storage | P1 | — | 2026-03-19 | | \[x] | buyer-lae | Quote normalization logic | P1 | — | 2026-03-19 | -## Phase 3 — Buyer Agent AI Assistant (MCP Platform) +## Phase 3 — Platform & Infrastructure | | ID | Task | Priority | Blockers | Done | |---|---|---|---|---|---| -| \[ ] | buyer-mw9 | EPIC: Buyer Agent AI Assistant | P1 | — | (open — children done) | -| \[x] | buyer-fvd | MCP Server Foundation | P1 | — | 2026-03-25 | -| \[x] | buyer-j95 | Infrastructure-as-Code Deployment | P3 | — | 2026-03-25 | -| \[x] | buyer-nz9 | Order Status & Audit API Integration | P2 | — | 2026-03-25 | -| \[x] | buyer-3w3 | Campaign Management MCP Tools | P1 | — | 2026-03-25 | -| \[x] | buyer-4ds | Deal Library MCP Tools | P1 | — | 2026-03-25 | -| \[x] | buyer-byk | Setup Wizard Service | P1 | — | 2026-03-25 | -| \[x] | buyer-nob | Seller Discovery MCP Tools | P2 | — | 2026-03-25 | -| \[x] | buyer-r0j | Negotiation & Orders MCP Tools | P2 | — | 2026-03-25 | -| \[x] | buyer-5x7 | Template & Reporting MCP Tools | P2 | — | 2026-03-25 | -| \[x] | buyer-j7f | Approval & API Key MCP Tools | P2 | — | 2026-03-25 | -| \[x] | buyer-q6t | Claude Desktop Setup Guide | P2 | — | 2026-03-25 | -| \[x] | buyer-1o3 | Deployment & Operations Guide | P3 | — | 2026-03-25 | -| \[x] | buyer-02h | ChatGPT & Multi-Client Setup Guide | P3 | — | 2026-03-25 | +| \[x] | buyer-4bg | 3A: DSP Integration | P2 | seller-dcd | 2026-03-25 | +| \[x] | buyer-8s2 |   ↳ 3A-1: FreeWheel Buyer Cloud (Beeswax) Integration | P2 | — | 2026-03-25 | +| \[x] | buyer-mek |   ↳ 3A-2: Amazon DSP Integration | P2 | — | 2026-03-25 | +| \[x] | buyer-a8k |   ↳ 3A-3: The Trade Desk (TTD) Integration | P2 | — | 2026-03-25 | +| \[x] | buyer-ppg |   ↳ 3A-4: DV360 (Google Display & Video 360) Integration | P2 | — | 2026-03-25 | +| \[x] | buyer-kyo | 3B: Order Lifecycle State Machine | P2 | seller-awh | 2026-03-25 | +| \[x] | buyer-zzq | 3C: API & SDK Documentation | P3 | — | 2026-03-25 | +| \[x] | buyer-je8 | 3D: Mediaocean Order Management Integration | P2 | — | 2026-03-25 | +| \[x] | buyer-k2i |   ↳ 3D-Phase1: Mediaocean Prisma (Digital/Programmatic) | P2 | — | 2026-03-25 | +| \[x] | buyer-wwf |   ↳ 3D-Phase2: Mediaocean Lumina (Linear TV) | P2 | — | 2026-03-25 | +| \[x] | buyer-w5c | 3E: Builder Guides for Vertical Customization | P2 | — | 2026-03-25 | +| \[x] | buyer-1o3 | 3F: Deployment & Operations Guide | P3 | — | 2026-03-25 | +| \[x] | buyer-j95 | 3G: Infrastructure-as-Code Deployment (CloudFormation/Terraform) | P3 | — | 2026-03-25 | ## Phase 4 — Deal Library & External Hooks +### Phase 4A — Deal Library MVP + +| | ID | Task | Priority | Blockers | Done | +|---|---|---|---|---|---| +| \[x] | buyer-te6b.1.4 | Add manual deal entry | P1 | — | 2026-03-18 | +| \[x] | buyer-te6b.1.5 | Build portfolio inspection tools | P1 | — | 2026-03-18 | +| \[x] | buyer-te6b.1.2 | Create DealJockey L2 agent in buyer hierarchy | P1 | — | 2026-03-18 | +| \[x] | buyer-te6b.1.12 | Deal library CRUD operations | P1 | — | 2026-03-18 | +| \[x] | buyer-te6b.1.7 | Define DealJockey event types (Phase 1) | P1 | — | 2026-03-18 | +| \[x] | buyer-te6b.1.11 | Extend DealStore schema | P1 | — | 2026-03-18 | +| \[x] | buyer-te6b.1.3 | Implement CSV deal import parser | P1 | — | 2026-03-18 | + +### Phase 4B — Templates & Seller Integration + | | ID | Task | Priority | Blockers | Done | |---|---|---|---|---|---| -| \[x] | buyer-te6b.1.11 | Extend DealStore schema | P1 | — | 2026-03-25 | -| \[x] | buyer-te6b.1.12 | Deal library CRUD operations | P1 | — | 2026-03-25 | -| \[x] | buyer-te6b.1.3 | CSV deal import parser | P1 | — | 2026-03-25 | -| \[x] | buyer-te6b.1.4 | Manual deal entry | P1 | — | 2026-03-25 | -| \[x] | buyer-te6b.1.5 | Portfolio inspection tools | P1 | — | 2026-03-25 | -| \[x] | buyer-te6b.1.7 | Deal library event types | P1 | — | 2026-03-25 | -| \[x] | buyer-te6b.1.6 | Organize deal-booking modules | P1 | — | 2026-03-25 | -| \[x] | buyer-te6b.1.1 | Seller API contract | P1 | — | 2026-03-25 | -| \[x] | buyer-te6b.1.10 | GET /deals/{id}/performance endpoint | P1 | — | 2026-03-25 | -| \[x] | buyer-te6b.1.8 | GET /supply-chain endpoint | P1 | — | 2026-03-25 | -| \[x] | buyer-te6b.1.9 | POST /deals/from-template endpoint | P1 | — | 2026-03-25 | -| \[x] | buyer-te6b.1.13 | Deal template and supply path template CRUD | P1 | — | 2026-03-25 | -| \[x] | buyer-xa5 | SSP Integration Planning (PubMatic, Magnite, Index Exchange) | P1 | — | 2026-03-25 | -| \[x] | buyer-vj3x | SSP Connector base class and interface | P1 | — | 2026-03-25 | -| \[x] | buyer-udd5 | PubMatic SSP connector for deal import | P1 | — | 2026-03-25 | -| \[x] | buyer-1ia3 | Magnite SSP connector for deal import | P2 | — | 2026-03-25 | -| \[x] | buyer-qo6b | Index Exchange SSP connector for deal import | P2 | — | 2026-03-25 | -| \[x] | buyer-sozw | SSP connector MCP tools (import, list, test) | P1 | — | 2026-03-25 | +| \[x] | buyer-te6b.1.10 | Add GET /api/v1/deals/{id}/performance endpoint [dj4] | P1 | — | 2026-03-19 | +| \[x] | buyer-te6b.1.8 | Add GET /api/v1/supply-chain endpoint [dj2] | P1 | — | 2026-03-19 | +| \[x] | buyer-te6b.1.9 | Add POST /api/v1/deals/from-template endpoint [dj3] | P1 | — | 2026-03-19 | +| \[x] | buyer-te6b.2.7 | AnalyzeSupplyPathTool [dj6] | P2 | — | 2026-03-19 | +| \[x] | buyer-te6b.1.13 | Deal template and supply path template CRUD [dj5] | P1 | — | 2026-03-19 | +| \[x] | buyer-te6b.2.8 | InstantiateDealFromTemplateTool [dj7] | P2 | — | 2026-03-19 | +| \[x] | buyer-te6b.1.6 | Organize internal deal-booking modules (consolidate per ar-fad) | P1 | — | 2026-03-19 | +| \[x] | buyer-te6b.1.1 | Write DealJockey seller API contract (supply-chain, from-template, bulk, performance) | P1 | — | 2026-03-19 | + +### Phase 4C — Portfolio Intelligence + +| | ID | Task | Priority | Blockers | Done | +|---|---|---|---|---|---| +| \[x] | buyer-te6b.2.14 | Add POST /api/v1/deals/bulk endpoint [dj6] | P2 | — | 2026-03-25 | +| \[x] | buyer-te6b.2.2 | Build cross-path price comparison tool | P2 | — | 2026-03-25 | +| \[x] | buyer-te6b.2.4 | Build deal deprecation analysis and execution | P2 | — | 2026-03-25 | +| \[x] | buyer-te6b.2.6 | Build human instructions adapter for manual deal migration | P2 | — | 2026-03-25 | +| \[x] | buyer-te6b.2.9 | BulkDealOperationTool [dj8] | P2 | — | 2026-03-25 | +| \[x] | buyer-te6b.2.11 | Deal migration tool [dj12] | P2 | — | 2026-03-25 | +| \[x] | buyer-te6b.2.12 | Define DealJockey event types (Phase 2) | P2 | — | 2026-03-25 | +| \[x] | buyer-te6b.2.13 | Enhanced supply-chain with sellers.json and schain [dj5] | P2 | — | 2026-03-25 | +| \[x] | buyer-te6b.2.10 | GetDealPerformanceTool [dj9] | P2 | — | 2026-03-25 | +| \[x] | buyer-te6b.2.3 | Implement deal duplication for new advertisers | P2 | — | 2026-03-25 | +| \[x] | buyer-te6b.2.1 | Implement deal portfolio gap analysis | P2 | — | 2026-03-25 | +| \[x] | buyer-te6b.2.5 | Implement portfolio health reporting | P2 | — | 2026-03-25 | + +### Phase 4D — Platform Integrations + +| | ID | Task | Priority | Blockers | Done | +|---|---|---|---|---|---| +| \[x] | buyer-te6b.3.4 | Amazon DSP API connector for deal import | P3 | — | 2026-03-25 | +| \[x] | buyer-te6b.3.7 | Cross-platform deal activation tracker [dj11] | P3 | — | 2026-03-25 | +| \[x] | buyer-te6b.3.8 | Cross-platform deal deduplication | P3 | — | 2026-03-25 | +| \[x] | buyer-te6b.3.2 | DV360 API connector for deal import | P3 | — | 2026-03-25 | +| \[x] | buyer-te6b.3.6 | Mediaocean Lumina export parser | P3 | — | 2026-03-25 | +| \[x] | buyer-te6b.3.5 | Mediaocean Prisma export parser | P3 | — | 2026-03-25 | +| \[x] | buyer-te6b.3.1 | TTD API connector for deal import | P3 | — | 2026-03-25 | + +### Phase 4E — External Model Integration + +| | ID | Task | Priority | Blockers | Done | +|---|---|---|---|---|---| +| \[x] | buyer-te6b.4.3 | Curator awareness in SPO [dj16] | P3 | — | 2026-03-25 | +| \[x] | buyer-te6b.4.6 | Curator support (OpenDirect 3.0) [dj7] | P3 | — | 2026-03-25 | +| \[x] | buyer-te6b.4.1 | Event system (Phase 4: optimization events) [dj14] | P3 | — | 2026-03-25 | +| \[ ] | buyer-te6b.4.4 | External optimization hooks for deal library | P3 | — | | +| \[x] | buyer-te6b.4.5 | ML-tuned supply path scoring | P3 | — | 2026-03-25 | +| \[x] | buyer-te6b.4.2 | Receive IAB Deals API v1.0 push updates [dj15] | P3 | — | 2026-03-25 | + +## Other + +| | ID | Task | Priority | Blockers | Done | +|---|---|---|---|---|---| +| \[x] | buyer-6dq | Add code standards document for buyer agent | P2 | — | 2026-03-25 | +| \[x] | buyer-j7f | Approval & API Key MCP Tools | P2 | buyer-mw9 | 2026-03-25 | +| \[x] | buyer-op3 | Architecture review: Buyer Agent v2 Replan | P1 | — | 2026-03-25 | +| \[x] | buyer-3w3 | Campaign Management MCP Tools | P1 | buyer-mw9 | 2026-03-25 | +| \[x] | buyer-02h | ChatGPT & Multi-Client Setup Guide | P3 | buyer-mw9 | 2026-03-25 | +| \[x] | buyer-390 | Cherry-pick deal library code from test/dealjockey-v1-e2e to main | P1 | — | 2026-03-25 | +| \[x] | buyer-q6t | Claude Desktop Setup Guide | P2 | buyer-mw9 | 2026-03-25 | +| \[x] | buyer-dxa | Code review: Buyer Agent v2 Replan impact assessment | P1 | — | 2026-03-25 | +| \[x] | buyer-bqz | Create run-demo.sh in ad_buyer_system for one-command campaign demo startup | P1 | — | 2026-03-25 | +| \[x] | buyer-4ds | Deal Library MCP Tools | P1 | buyer-mw9 | 2026-03-25 | +| \[x] | buyer-td0 | DealJockey v1 end-to-end testing (4A+4B) | P1 | — | 2026-03-25 | +| \[x] | buyer-bei | Fill Campaign Automation guide stubs with content | P2 | — | 2026-03-25 | +| \[x] | buyer-4xu | Filter PROGRESS.md generator to only include repo-specific beads | P1 | — | 2026-03-25 | +| \[x] | buyer-eab | Implement persistent event storage (StorageEventBus) | P1 | — | 2026-03-20 | +| \[x] | buyer-fvd | MCP Server Foundation | P1 | buyer-mw9 | 2026-03-25 | +| \[x] | buyer-r0j | Negotiation & Orders MCP Tools | P2 | buyer-mw9 | 2026-03-25 | +| \[x] | buyer-nz9 | Order Status & Audit API Integration | P2 | — | 2026-03-25 | +| \[x] | buyer-bds | Quinn: Full test suite verification before code quality PR | P1 | — | 2026-03-25 | +| \[x] | buyer-1t8 | Rename all DealJockey references in buyer agent codebase | P1 | — | 2026-03-25 | +| \[x] | buyer-atc | Research: Agent Range GitHub org audit — repos, code, issues, PRs | P2 | — | 2026-03-25 | +| \[x] | buyer-nrx | Run ruff auto-fix on buyer codebase (715 auto-fixable violations) | P2 | — | 2026-03-25 | +| \[~] | buyer-xa5 | SSP Integration Planning (PubMatic, Magnite, Index Exchange) | P2 | — | | +| \[x] | buyer-nob | Seller Discovery MCP Tools | P2 | buyer-mw9 | 2026-03-25 | +| \[x] | buyer-0la | Session pickup 2026-03-22: DealJockey 4CDE prep + docs planning | P1 | — | 2026-03-25 | +| \[x] | buyer-cln | Session pickup: DealJockey v1 PR-ready, 8 branches queued for PRs | P1 | — | 2026-03-25 | +| \[x] | buyer-byk | Setup Wizard Service | P1 | buyer-mw9 | 2026-03-25 | +| \[x] | buyer-c8e | Task: Run live smoke tests for DealJockey seller endpoints | P1 | — | 2026-03-19 | +| \[x] | buyer-5x7 | Template & Reporting MCP Tools | P2 | buyer-mw9 | 2026-03-25 | --- -*Last updated: 2026-03-25 — Phase 4 complete (19/19), all phases complete (54/54)* +*Last updated: 2026-04-13 19:33 UTC — auto-generated by beads* diff --git a/.beads/generate_progress.py b/.beads/generate_progress.py index 407e2f9..083b737 100644 --- a/.beads/generate_progress.py +++ b/.beads/generate_progress.py @@ -21,6 +21,11 @@ JSONL_PATH = BEADS_DIR / "issues.jsonl" OUTPUT_PATH = BEADS_DIR / "PROGRESS.md" +# Only include beads whose ID starts with this prefix. +# Prevents parent-repo (ar-) or other-repo beads from leaking into this +# repo's PROGRESS.md when the JSONL contains cross-repo entries. +BEAD_PREFIX = "buyer-" + # Cross-repo blockers that can't be tracked as formal bd dependencies. CROSS_REPO_BLOCKERS = { "buyer-4bg": ["seller-dcd"], @@ -54,7 +59,7 @@ def refresh_jsonl(): def load_issues(): - """Load issues from JSONL, filtering tombstones and LEGACY beads.""" + """Load issues from JSONL, filtering tombstones, LEGACY, and non-repo beads.""" issues = [] if not JSONL_PATH.exists(): return issues @@ -68,6 +73,10 @@ def load_issues(): continue if "[LEGACY]" in issue.get("title", ""): continue + # Only include beads belonging to this repo (buyer- prefix). + # Parent-repo (ar-) or other-repo beads should not appear. + if BEAD_PREFIX and not issue.get("id", "").startswith(BEAD_PREFIX): + continue issues.append(issue) return issues diff --git a/.beads/test_generate_progress.py b/.beads/test_generate_progress.py index bafb930..e0024d4 100644 --- a/.beads/test_generate_progress.py +++ b/.beads/test_generate_progress.py @@ -296,9 +296,10 @@ def test_sub_phases_render_as_h3(self): for issue in issues: f.write(json.dumps(issue) + "\n") - # Patch paths and refresh + # Patch paths, refresh, and disable prefix filter (synthetic IDs) with patch.object(gp, "JSONL_PATH", jsonl_path), \ patch.object(gp, "OUTPUT_PATH", output_path), \ + patch.object(gp, "BEAD_PREFIX", ""), \ patch.object(gp, "refresh_jsonl", lambda: None): gp.generate() @@ -319,8 +320,10 @@ def test_flat_phase_no_h3(self): for issue in issues: f.write(json.dumps(issue) + "\n") + # Disable prefix filter for synthetic test IDs with patch.object(gp, "JSONL_PATH", jsonl_path), \ patch.object(gp, "OUTPUT_PATH", output_path), \ + patch.object(gp, "BEAD_PREFIX", ""), \ patch.object(gp, "refresh_jsonl", lambda: None): gp.generate() @@ -365,26 +368,40 @@ def test_filters_tombstones(self): with tempfile.TemporaryDirectory() as tmpdir: jsonl_path = Path(tmpdir) / "issues.jsonl" with open(jsonl_path, "w") as f: - f.write(json.dumps(make_issue("live", "Active bead")) + "\n") - f.write(json.dumps(make_issue("dead", "Tombstoned", + f.write(json.dumps(make_issue("buyer-live", "Active bead")) + "\n") + f.write(json.dumps(make_issue("buyer-dead", "Tombstoned", status="tombstone")) + "\n") with patch.object(gp, "JSONL_PATH", jsonl_path): issues = gp.load_issues() assert len(issues) == 1 - assert issues[0]["id"] == "live" + assert issues[0]["id"] == "buyer-live" def test_filters_legacy(self): with tempfile.TemporaryDirectory() as tmpdir: jsonl_path = Path(tmpdir) / "issues.jsonl" with open(jsonl_path, "w") as f: - f.write(json.dumps(make_issue("new", "Active bead")) + "\n") - f.write(json.dumps(make_issue("old", "[LEGACY] Old bead")) + "\n") + f.write(json.dumps(make_issue("buyer-new", "Active bead")) + "\n") + f.write(json.dumps(make_issue("buyer-old", "[LEGACY] Old bead")) + "\n") with patch.object(gp, "JSONL_PATH", jsonl_path): issues = gp.load_issues() assert len(issues) == 1 - assert issues[0]["id"] == "new" + assert issues[0]["id"] == "buyer-new" + + def test_filters_wrong_prefix(self): + """Beads with non-buyer prefixes are excluded by the prefix filter.""" + with tempfile.TemporaryDirectory() as tmpdir: + jsonl_path = Path(tmpdir) / "issues.jsonl" + with open(jsonl_path, "w") as f: + f.write(json.dumps(make_issue("buyer-ok", "Buyer bead")) + "\n") + f.write(json.dumps(make_issue("ar-xyz", "Parent repo bead")) + "\n") + f.write(json.dumps(make_issue("seller-abc", "Seller bead")) + "\n") + + with patch.object(gp, "JSONL_PATH", jsonl_path): + issues = gp.load_issues() + assert len(issues) == 1 + assert issues[0]["id"] == "buyer-ok" # --------------------------------------------------------------------------- @@ -438,8 +455,8 @@ def test_full_generate_with_real_data(self): # Phase 3 exists assert "## Phase 3" in content # Phase 4 with sub-phases - assert "## Phase 4 — DealJockey" in content - assert "### Phase 4A — MVP DealJockey" in content + assert "## Phase 4 — Deal Library & External Hooks" in content + assert "### Phase 4A — Deal Library MVP" in content assert "### Phase 4B — Templates & Seller Integration" in content # All Campaign Automation gap beads are present From 19a733a86fb7bef880553a8c4d5eac7f98c08857 Mon Sep 17 00:00:00 2001 From: ATC964 <82515918+atc964@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:38:21 -0400 Subject: [PATCH 2/4] Rename misnamed DSP references to BuyerDealFlow File renames: - dsp_agent.py -> buyer_deal_specialist_agent.py - dsp_deal_flow.py -> buyer_deal_flow.py - tools/dsp/ -> tools/buyer_deals/ Class/function renames: - DSPDealFlow -> BuyerDealFlow - DSPFlowState -> BuyerDealFlowState - DSPFlowStatus -> BuyerDealFlowStatus - create_dsp_agent() -> create_buyer_deal_specialist_agent() - run_dsp_deal_flow() -> run_buyer_deal_flow() Agent role updated from "DSP Deal Discovery Specialist" to "Buyer Deal Specialist". Backstory updated to remove "DSP specialist" language. All imports, __init__.py exports, docstrings, test assertions, and @patch targets updated across 22 files. Legitimate DSP references (platform names, seat IDs, activation instructions) preserved. All 2672 tests pass. bead: ar-v6ah Co-Authored-By: Claude Opus 4.6 (1M context) --- examples/dsp_deal_discovery.py | 2 +- src/ad_buyer/agents/level2/__init__.py | 4 +- ...gent.py => buyer_deal_specialist_agent.py} | 16 +-- src/ad_buyer/booking/deal_id.py | 2 +- src/ad_buyer/booking/pricing.py | 4 +- src/ad_buyer/flows/__init__.py | 10 +- .../{dsp_deal_flow.py => buyer_deal_flow.py} | 58 ++++---- src/ad_buyer/models/state_machine.py | 4 +- .../tools/{dsp => buyer_deals}/__init__.py | 2 +- .../discover_inventory.py | 2 +- .../tools/{dsp => buyer_deals}/get_pricing.py | 2 +- .../{dsp => buyer_deals}/request_deal.py | 2 +- tests/integration/test_flow_persistence.py | 28 ++-- .../test_identity_pricing_deal_pipeline.py | 4 +- tests/unit/test_agent_hierarchy.py | 34 ++--- tests/unit/test_booking/test_pricing.py | 4 +- tests/unit/test_dsp_deal_flow.py | 132 +++++++++--------- tests/unit/test_dsp_discovery_pricing.py | 94 ++++++------- tests/unit/test_dsp_tools.py | 2 +- tests/unit/test_negotiation_enabled_flag.py | 2 +- tests/unit/test_no_hardcoded_urls.py | 2 +- tests/unit/test_state_machine.py | 2 +- 22 files changed, 206 insertions(+), 206 deletions(-) rename src/ad_buyer/agents/level2/{dsp_agent.py => buyer_deal_specialist_agent.py} (89%) rename src/ad_buyer/flows/{dsp_deal_flow.py => buyer_deal_flow.py} (91%) rename src/ad_buyer/tools/{dsp => buyer_deals}/__init__.py (77%) rename src/ad_buyer/tools/{dsp => buyer_deals}/discover_inventory.py (99%) rename src/ad_buyer/tools/{dsp => buyer_deals}/get_pricing.py (99%) rename src/ad_buyer/tools/{dsp => buyer_deals}/request_deal.py (99%) diff --git a/examples/dsp_deal_discovery.py b/examples/dsp_deal_discovery.py index 9fc3cde..701677f 100644 --- a/examples/dsp_deal_discovery.py +++ b/examples/dsp_deal_discovery.py @@ -29,7 +29,7 @@ BuyerIdentity, DealType, ) - from ad_buyer.tools.dsp import DiscoverInventoryTool, GetPricingTool, RequestDealTool + from ad_buyer.tools.buyer_deals import DiscoverInventoryTool, GetPricingTool, RequestDealTool except ImportError as e: print(f"Error: {e}") print("\nPlease install the ad_buyer package first:") diff --git a/src/ad_buyer/agents/level2/__init__.py b/src/ad_buyer/agents/level2/__init__.py index afc1b4a..812c871 100644 --- a/src/ad_buyer/agents/level2/__init__.py +++ b/src/ad_buyer/agents/level2/__init__.py @@ -6,7 +6,7 @@ from .branding_agent import create_branding_agent from .ctv_agent import create_ctv_agent from .deal_library_agent import create_deal_library_agent -from .dsp_agent import create_dsp_agent +from .buyer_deal_specialist_agent import create_buyer_deal_specialist_agent from .linear_tv_agent import create_linear_tv_agent from .mobile_app_agent import create_mobile_app_agent from .performance_agent import create_performance_agent @@ -17,6 +17,6 @@ "create_ctv_agent", "create_linear_tv_agent", "create_performance_agent", - "create_dsp_agent", + "create_buyer_deal_specialist_agent", "create_deal_library_agent", ] diff --git a/src/ad_buyer/agents/level2/dsp_agent.py b/src/ad_buyer/agents/level2/buyer_deal_specialist_agent.py similarity index 89% rename from src/ad_buyer/agents/level2/dsp_agent.py rename to src/ad_buyer/agents/level2/buyer_deal_specialist_agent.py index 53f2f9f..5ed5643 100644 --- a/src/ad_buyer/agents/level2/dsp_agent.py +++ b/src/ad_buyer/agents/level2/buyer_deal_specialist_agent.py @@ -1,7 +1,7 @@ # Author: Green Mountain Systems AI Inc. # Donated to IAB Tech Lab -"""DSP (Demand Side Platform) Deal Discovery Specialist agent.""" +"""Buyer Deal Specialist agent for deal discovery and Deal ID creation.""" from typing import Any @@ -10,13 +10,13 @@ from ...config.settings import settings -def create_dsp_agent( +def create_buyer_deal_specialist_agent( tools: list[Any] | None = None, verbose: bool = True, ) -> Agent: - """Create the DSP Deal Discovery Specialist agent. + """Create the Buyer Deal Specialist agent. - The DSP Specialist focuses on: + The Buyer Deal Specialist focuses on: - Discovering available inventory from sellers - Presenting buyer identity for tiered pricing - Negotiating and requesting Deal IDs @@ -31,15 +31,15 @@ def create_dsp_agent( verbose: Whether to enable verbose logging Returns: - Configured DSP Deal Discovery Specialist Agent + Configured Buyer Deal Specialist Agent """ return Agent( - role="DSP Deal Discovery Specialist", + role="Buyer Deal Specialist", goal="""Discover premium advertising inventory, secure optimal tiered pricing based on buyer identity, and obtain Deal IDs that can be activated in traditional DSP platforms for programmatic media buying.""", - backstory="""You are a programmatic advertising expert specializing in DSP -(Demand Side Platform) operations and private marketplace deals. You understand + backstory="""You are a programmatic advertising expert specializing in buyer deal +workflows and private marketplace deals. You understand the nuances of deal structures and how to leverage buyer identity for better pricing. Your expertise includes: diff --git a/src/ad_buyer/booking/deal_id.py b/src/ad_buyer/booking/deal_id.py index 5a207e4..26829e4 100644 --- a/src/ad_buyer/booking/deal_id.py +++ b/src/ad_buyer/booking/deal_id.py @@ -5,7 +5,7 @@ Extracts the deal ID generation logic previously duplicated in: - unified_client.py (request_deal method) -- tools/dsp/request_deal.py (_generate_deal_id method) +- tools/buyer_deals/request_deal.py (_generate_deal_id method) Deal IDs have the format: DEAL-XXXXXXXX where XXXXXXXX is 8 uppercase hex characters derived from diff --git a/src/ad_buyer/booking/pricing.py b/src/ad_buyer/booking/pricing.py index be9fe7d..19bad34 100644 --- a/src/ad_buyer/booking/pricing.py +++ b/src/ad_buyer/booking/pricing.py @@ -5,8 +5,8 @@ Extracts the duplicated pricing logic from: - unified_client.py (get_pricing, request_deal methods) -- tools/dsp/request_deal.py (_create_deal_response) -- tools/dsp/get_pricing.py (_format_pricing) +- tools/buyer_deals/request_deal.py (_create_deal_response) +- tools/buyer_deals/get_pricing.py (_format_pricing) Pricing tiers: Public: 0% discount diff --git a/src/ad_buyer/flows/__init__.py b/src/ad_buyer/flows/__init__.py index ceb8dd2..87b1492 100644 --- a/src/ad_buyer/flows/__init__.py +++ b/src/ad_buyer/flows/__init__.py @@ -4,12 +4,12 @@ """Workflow flows for the Ad Buyer System.""" from .deal_booking_flow import DealBookingFlow -from .dsp_deal_flow import DSPDealFlow, DSPFlowState, DSPFlowStatus, run_dsp_deal_flow +from .buyer_deal_flow import BuyerDealFlow, BuyerDealFlowState, BuyerDealFlowStatus, run_buyer_deal_flow __all__ = [ "DealBookingFlow", - "DSPDealFlow", - "DSPFlowState", - "DSPFlowStatus", - "run_dsp_deal_flow", + "BuyerDealFlow", + "BuyerDealFlowState", + "BuyerDealFlowStatus", + "run_buyer_deal_flow", ] diff --git a/src/ad_buyer/flows/dsp_deal_flow.py b/src/ad_buyer/flows/buyer_deal_flow.py similarity index 91% rename from src/ad_buyer/flows/dsp_deal_flow.py rename to src/ad_buyer/flows/buyer_deal_flow.py index 6f7a5d1..330e7f5 100644 --- a/src/ad_buyer/flows/dsp_deal_flow.py +++ b/src/ad_buyer/flows/buyer_deal_flow.py @@ -1,7 +1,7 @@ # Author: Green Mountain Systems AI Inc. # Donated to IAB Tech Lab -"""DSP Deal Discovery Flow - workflow for obtaining Deal IDs for programmatic activation.""" +"""Buyer Deal Flow - workflow for obtaining Deal IDs for programmatic activation.""" import logging import sqlite3 @@ -13,7 +13,7 @@ from crewai.flow.flow import Flow, listen, start from pydantic import BaseModel, Field -from ..agents.level2.dsp_agent import create_dsp_agent +from ..agents.level2.buyer_deal_specialist_agent import create_buyer_deal_specialist_agent from ..clients.unified_client import UnifiedClient from ..models.buyer_identity import ( AccessTier, @@ -27,13 +27,13 @@ from ..events.models import EventType from ..models.state_machine import BuyerDealStatus, DealStateMachine, InvalidTransitionError from ..storage.deal_store import DealStore -from ..tools.dsp import DiscoverInventoryTool, GetPricingTool, RequestDealTool +from ..tools.buyer_deals import DiscoverInventoryTool, GetPricingTool, RequestDealTool logger = logging.getLogger(__name__) -class DSPFlowStatus(str, Enum): - """Status values for the DSP deal flow.""" +class BuyerDealFlowStatus(str, Enum): + """Status values for the buyer deal flow.""" INITIALIZED = "initialized" REQUEST_RECEIVED = "request_received" @@ -58,8 +58,8 @@ class DiscoveredProduct(BaseModel): score: float = Field(default=0.0, description="Match score for the request") -class DSPFlowState(BaseModel): - """State model for the DSP deal discovery flow.""" +class BuyerDealFlowState(BaseModel): + """State model for the buyer deal flow.""" # Input request: str = Field(default="", description="Natural language deal request") @@ -113,8 +113,8 @@ class DSPFlowState(BaseModel): ) # Execution tracking - status: DSPFlowStatus = Field( - default=DSPFlowStatus.INITIALIZED, + status: BuyerDealFlowStatus = Field( + default=BuyerDealFlowStatus.INITIALIZED, description="Current flow status", ) errors: list[str] = Field(default_factory=list) @@ -124,10 +124,10 @@ class DSPFlowState(BaseModel): updated_at: datetime = Field(default_factory=datetime.utcnow) -class DSPDealFlow(Flow[DSPFlowState]): - """Event-driven flow for DSP deal discovery and Deal ID creation. +class BuyerDealFlow(Flow[BuyerDealFlowState]): + """Event-driven flow for buyer deal discovery and Deal ID creation. - This flow enables the DSP use case where: + This flow enables the buyer deal use case where: 1. Buyer discovers available inventory with identity-based pricing 2. Buyer selects inventory and requests a Deal ID 3. Deal ID is returned for activation in traditional DSPs @@ -228,13 +228,13 @@ def receive_request(self) -> dict[str, Any]: if not request: self.state.errors.append("No deal request provided") - self.state.status = DSPFlowStatus.FAILED + self.state.status = BuyerDealFlowStatus.FAILED return {"status": "failed", "errors": self.state.errors} # Store buyer context in state self.state.buyer_context = self._buyer_context.model_dump() - self.state.status = DSPFlowStatus.REQUEST_RECEIVED + self.state.status = BuyerDealFlowStatus.REQUEST_RECEIVED self.state.updated_at = datetime.utcnow() # Emit quote.requested event @@ -281,7 +281,7 @@ def discover_inventory(self, request_result: dict[str, Any]) -> dict[str, Any]: return request_result try: - self.state.status = DSPFlowStatus.DISCOVERING_INVENTORY + self.state.status = BuyerDealFlowStatus.DISCOVERING_INVENTORY # Extract filters from request discovery_result = self._discover_tool._run( @@ -308,14 +308,14 @@ def discover_inventory(self, request_result: dict[str, Any]) -> dict[str, Any]: except Exception as e: # noqa: BLE001 - flow step must capture any failure from CrewAI self.state.errors.append(f"Inventory discovery failed: {e}") - self.state.status = DSPFlowStatus.FAILED + self.state.status = BuyerDealFlowStatus.FAILED return {"status": "failed", "error": str(e)} @listen(discover_inventory) def evaluate_and_select(self, discovery_result: dict[str, Any]) -> dict[str, Any]: """Evaluate discovered products and select best match. - In a full implementation, this would use the DSP agent to + In a full implementation, this would use the buyer deal specialist agent to intelligently select the best product. For now, we use a simplified selection based on the first available product. """ @@ -323,10 +323,10 @@ def evaluate_and_select(self, discovery_result: dict[str, Any]) -> dict[str, Any return discovery_result try: - self.state.status = DSPFlowStatus.EVALUATING_PRICING + self.state.status = BuyerDealFlowStatus.EVALUATING_PRICING # Create crew for intelligent selection - dsp_agent = create_dsp_agent( + deal_agent = create_buyer_deal_specialist_agent( tools=[self._discover_tool, self._pricing_tool], ) @@ -344,11 +344,11 @@ def evaluate_and_select(self, discovery_result: dict[str, Any]) -> dict[str, Any Return the product_id of the best matching product and explain why.""", expected_output="Product ID and selection rationale", - agent=dsp_agent, + agent=deal_agent, ) crew = Crew( - agents=[dsp_agent], + agents=[deal_agent], tasks=[selection_task], verbose=True, ) @@ -391,7 +391,7 @@ def evaluate_and_select(self, discovery_result: dict[str, Any]) -> dict[str, Any except Exception as e: # noqa: BLE001 - flow step must capture any failure from CrewAI self.state.errors.append(f"Product selection failed: {e}") - self.state.status = DSPFlowStatus.FAILED + self.state.status = BuyerDealFlowStatus.FAILED return {"status": "failed", "error": str(e)} def _extract_product_id(self, text: str) -> Optional[str]: @@ -422,11 +422,11 @@ def request_deal_id(self, selection_result: dict[str, Any]) -> dict[str, Any]: product_id = self.state.selected_product_id if not product_id: self.state.errors.append("No product selected for deal creation") - self.state.status = DSPFlowStatus.FAILED + self.state.status = BuyerDealFlowStatus.FAILED return {"status": "failed", "error": "No product selected"} try: - self.state.status = DSPFlowStatus.REQUESTING_DEAL + self.state.status = BuyerDealFlowStatus.REQUESTING_DEAL deal_result = self._deal_tool._run( product_id=product_id, @@ -438,7 +438,7 @@ def request_deal_id(self, selection_result: dict[str, Any]) -> dict[str, Any]: # Store deal response self.state.deal_response = {"raw": deal_result} - self.state.status = DSPFlowStatus.DEAL_CREATED + self.state.status = BuyerDealFlowStatus.DEAL_CREATED self.state.updated_at = datetime.utcnow() # Persist deal creation status @@ -462,7 +462,7 @@ def request_deal_id(self, selection_result: dict[str, Any]) -> dict[str, Any]: except Exception as e: # noqa: BLE001 - flow step must capture any failure from CrewAI self.state.errors.append(f"Deal request failed: {e}") - self.state.status = DSPFlowStatus.FAILED + self.state.status = BuyerDealFlowStatus.FAILED self._persist_deal_status("failed") return {"status": "failed", "error": str(e)} @@ -488,7 +488,7 @@ def get_status(self) -> dict[str, Any]: } -async def run_dsp_deal_flow( +async def run_buyer_deal_flow( request: str, buyer_identity: BuyerIdentity, deal_type: DealType = DealType.PREFERRED_DEAL, @@ -499,7 +499,7 @@ async def run_dsp_deal_flow( base_url: Optional[str] = None, store: Optional[DealStore] = None, ) -> dict[str, Any]: - """Convenience function to run the DSP deal flow. + """Convenience function to run the buyer deal flow. Args: request: Natural language deal request @@ -530,7 +530,7 @@ async def run_dsp_deal_flow( # Create client async with UnifiedClient(base_url=base_url) as client: # Create and run flow - flow = DSPDealFlow( + flow = BuyerDealFlow( client=client, buyer_context=buyer_context, store=store, diff --git a/src/ad_buyer/models/state_machine.py b/src/ad_buyer/models/state_machine.py index bc91172..799e71a 100644 --- a/src/ad_buyer/models/state_machine.py +++ b/src/ad_buyer/models/state_machine.py @@ -16,7 +16,7 @@ PAUSED/PACING_HOLD distinction, and validate_transition() method - Linear TV extensions: makegood_pending, partially_canceled -Existing code continues to work: ExecutionStatus and DSPFlowStatus are +Existing code continues to work: ExecutionStatus and BuyerDealFlowStatus are preserved and mapped into the new enums where flows need the machine. Pure Pydantic + stdlib -- no external dependencies. @@ -637,5 +637,5 @@ def from_execution_status(value: str) -> BuyerCampaignStatus: def from_dsp_flow_status(value: str) -> BuyerDealStatus: - """Map a legacy DSPFlowStatus value to BuyerDealStatus.""" + """Map a legacy BuyerDealFlowStatus value to BuyerDealStatus.""" return _DSP_FLOW_STATUS_MAP.get(value, BuyerDealStatus.QUOTED) diff --git a/src/ad_buyer/tools/dsp/__init__.py b/src/ad_buyer/tools/buyer_deals/__init__.py similarity index 77% rename from src/ad_buyer/tools/dsp/__init__.py rename to src/ad_buyer/tools/buyer_deals/__init__.py index f9733c6..2352bf4 100644 --- a/src/ad_buyer/tools/dsp/__init__.py +++ b/src/ad_buyer/tools/buyer_deals/__init__.py @@ -1,7 +1,7 @@ # Author: Green Mountain Systems AI Inc. # Donated to IAB Tech Lab -"""DSP (Demand Side Platform) tools for discovery, pricing, and deal management.""" +"""Buyer deal tools for discovery, pricing, and deal management.""" from .discover_inventory import DiscoverInventoryTool from .get_pricing import GetPricingTool diff --git a/src/ad_buyer/tools/dsp/discover_inventory.py b/src/ad_buyer/tools/buyer_deals/discover_inventory.py similarity index 99% rename from src/ad_buyer/tools/dsp/discover_inventory.py rename to src/ad_buyer/tools/buyer_deals/discover_inventory.py index 63042d8..17537ca 100644 --- a/src/ad_buyer/tools/dsp/discover_inventory.py +++ b/src/ad_buyer/tools/buyer_deals/discover_inventory.py @@ -1,7 +1,7 @@ # Author: Green Mountain Systems AI Inc. # Donated to IAB Tech Lab -"""Inventory discovery tool for DSP workflows.""" +"""Inventory discovery tool for buyer deal workflows.""" from typing import Any diff --git a/src/ad_buyer/tools/dsp/get_pricing.py b/src/ad_buyer/tools/buyer_deals/get_pricing.py similarity index 99% rename from src/ad_buyer/tools/dsp/get_pricing.py rename to src/ad_buyer/tools/buyer_deals/get_pricing.py index e04e46a..db2af50 100644 --- a/src/ad_buyer/tools/dsp/get_pricing.py +++ b/src/ad_buyer/tools/buyer_deals/get_pricing.py @@ -1,7 +1,7 @@ # Author: Green Mountain Systems AI Inc. # Donated to IAB Tech Lab -"""Tiered pricing tool for DSP workflows.""" +"""Tiered pricing tool for buyer deal workflows.""" from typing import Any diff --git a/src/ad_buyer/tools/dsp/request_deal.py b/src/ad_buyer/tools/buyer_deals/request_deal.py similarity index 99% rename from src/ad_buyer/tools/dsp/request_deal.py rename to src/ad_buyer/tools/buyer_deals/request_deal.py index 6e79eed..cdaccf5 100644 --- a/src/ad_buyer/tools/dsp/request_deal.py +++ b/src/ad_buyer/tools/buyer_deals/request_deal.py @@ -1,7 +1,7 @@ # Author: Green Mountain Systems AI Inc. # Donated to IAB Tech Lab -"""Deal ID request tool for DSP workflows.""" +"""Deal ID request tool for buyer deal workflows.""" from datetime import datetime, timedelta, timezone from typing import Any diff --git a/tests/integration/test_flow_persistence.py b/tests/integration/test_flow_persistence.py index d0d8d4a..061eca3 100644 --- a/tests/integration/test_flow_persistence.py +++ b/tests/integration/test_flow_persistence.py @@ -6,7 +6,7 @@ These tests verify that: 1. DealBookingFlow with store=None works unchanged (backward compatibility) 2. DealBookingFlow with a store persists deal and booking data -3. DSPDealFlow with a store persists deal data +3. BuyerDealFlow with a store persists deal data 4. API job tracking writes to the store via _persist_job """ @@ -15,7 +15,7 @@ import pytest from ad_buyer.flows.deal_booking_flow import DealBookingFlow -from ad_buyer.flows.dsp_deal_flow import DSPDealFlow, DSPFlowStatus +from ad_buyer.flows.buyer_deal_flow import BuyerDealFlow, BuyerDealFlowStatus from ad_buyer.models.buyer_identity import ( BuyerContext, BuyerIdentity, @@ -334,43 +334,43 @@ def test_store_failure_does_not_break_flow(self, mock_opendirect_client): # ----------------------------------------------------------------------- -# DSPDealFlow backward compatibility (store=None) +# BuyerDealFlow backward compatibility (store=None) # ----------------------------------------------------------------------- -class TestDSPDealFlowNoStore: - """Verify DSPDealFlow works identically when store=None.""" +class TestBuyerDealFlowNoStore: + """Verify BuyerDealFlow works identically when store=None.""" def test_init_without_store(self, mock_unified_client, buyer_context): """Flow can be created without a store argument.""" - flow = DSPDealFlow(mock_unified_client, buyer_context) + flow = BuyerDealFlow(mock_unified_client, buyer_context) assert flow._store is None def test_receive_request_no_store(self, mock_unified_client, buyer_context): """Request reception works without a store.""" - flow = DSPDealFlow(mock_unified_client, buyer_context) + flow = BuyerDealFlow(mock_unified_client, buyer_context) flow.state.request = "Premium video inventory for Q2" result = flow.receive_request() assert result["status"] == "success" - assert flow.state.status == DSPFlowStatus.REQUEST_RECEIVED + assert flow.state.status == BuyerDealFlowStatus.REQUEST_RECEIVED # ----------------------------------------------------------------------- -# DSPDealFlow with store +# BuyerDealFlow with store # ----------------------------------------------------------------------- -class TestDSPDealFlowWithStore: - """Verify DSPDealFlow persists data when store is provided.""" +class TestBuyerDealFlowWithStore: + """Verify BuyerDealFlow persists data when store is provided.""" def test_init_with_store(self, mock_unified_client, buyer_context, deal_store): """Flow accepts and stores the DealStore reference.""" - flow = DSPDealFlow(mock_unified_client, buyer_context, store=deal_store) + flow = BuyerDealFlow(mock_unified_client, buyer_context, store=deal_store) assert flow._store is deal_store def test_receive_request_persists_deal(self, mock_unified_client, buyer_context, deal_store): """Request reception creates a draft deal in the store.""" - flow = DSPDealFlow(mock_unified_client, buyer_context, store=deal_store) + flow = BuyerDealFlow(mock_unified_client, buyer_context, store=deal_store) flow.state.request = "Premium video inventory for Q2" flow.state.deal_type = DealType.PREFERRED_DEAL flow.state.impressions = 500000 @@ -399,7 +399,7 @@ def test_store_failure_does_not_break_dsp_flow(self, mock_unified_client, buyer_ broken_store.connect() broken_store.disconnect() - flow = DSPDealFlow(mock_unified_client, buyer_context, store=broken_store) + flow = BuyerDealFlow(mock_unified_client, buyer_context, store=broken_store) flow.state.request = "Premium video inventory" # Should not raise diff --git a/tests/integration/test_identity_pricing_deal_pipeline.py b/tests/integration/test_identity_pricing_deal_pipeline.py index 411bffa..818d867 100644 --- a/tests/integration/test_identity_pricing_deal_pipeline.py +++ b/tests/integration/test_identity_pricing_deal_pipeline.py @@ -26,8 +26,8 @@ BuyerIdentity, DealType, ) -from ad_buyer.tools.dsp.get_pricing import GetPricingTool -from ad_buyer.tools.dsp.request_deal import RequestDealTool +from ad_buyer.tools.buyer_deals.get_pricing import GetPricingTool +from ad_buyer.tools.buyer_deals.request_deal import RequestDealTool class TestIdentityToPricingPipeline: diff --git a/tests/unit/test_agent_hierarchy.py b/tests/unit/test_agent_hierarchy.py index 759c773..e02ac3f 100644 --- a/tests/unit/test_agent_hierarchy.py +++ b/tests/unit/test_agent_hierarchy.py @@ -29,7 +29,7 @@ from ad_buyer.agents.level1.portfolio_manager import create_portfolio_manager from ad_buyer.agents.level2.branding_agent import create_branding_agent from ad_buyer.agents.level2.ctv_agent import create_ctv_agent -from ad_buyer.agents.level2.dsp_agent import create_dsp_agent +from ad_buyer.agents.level2.buyer_deal_specialist_agent import create_buyer_deal_specialist_agent from ad_buyer.agents.level2.mobile_app_agent import create_mobile_app_agent from ad_buyer.agents.level2.performance_agent import create_performance_agent from ad_buyer.agents.level3.audience_planner_agent import create_audience_planner_agent @@ -343,25 +343,25 @@ def test_llm_temperature(self): assert agent.llm.temperature == 0.5 -class TestDSPAgent: - """Tests for the L2 DSP Deal Discovery Specialist (previously untested).""" +class TestBuyerDealSpecialistAgent: + """Tests for the L2 Buyer Deal Specialist (previously untested).""" def test_creation(self): - agent = create_dsp_agent(verbose=False) + agent = create_buyer_deal_specialist_agent(verbose=False) assert isinstance(agent, Agent) def test_role_name(self): - agent = create_dsp_agent(verbose=False) - assert agent.role == "DSP Deal Discovery Specialist" + agent = create_buyer_deal_specialist_agent(verbose=False) + assert agent.role == "Buyer Deal Specialist" def test_goal_focuses_on_deals(self): - agent = create_dsp_agent(verbose=False) + agent = create_buyer_deal_specialist_agent(verbose=False) goal_lower = agent.goal.lower() assert "deal" in goal_lower or "inventory" in goal_lower def test_backstory_mentions_deal_types(self): """DSP agent backstory should mention PG, PD, PA deal types.""" - agent = create_dsp_agent(verbose=False) + agent = create_buyer_deal_specialist_agent(verbose=False) backstory = agent.backstory assert "PG" in backstory or "Programmatic Guaranteed" in backstory assert "PD" in backstory or "Preferred Deal" in backstory @@ -369,7 +369,7 @@ def test_backstory_mentions_deal_types(self): def test_backstory_mentions_tiered_pricing(self): """DSP agent backstory should cover tiered pricing tiers.""" - agent = create_dsp_agent(verbose=False) + agent = create_buyer_deal_specialist_agent(verbose=False) backstory = agent.backstory assert "Public" in backstory assert "Seat" in backstory @@ -377,23 +377,23 @@ def test_backstory_mentions_tiered_pricing(self): assert "Advertiser" in backstory def test_delegation_enabled(self): - agent = create_dsp_agent(verbose=False) + agent = create_buyer_deal_specialist_agent(verbose=False) assert agent.allow_delegation is True def test_memory_enabled(self): - agent = create_dsp_agent(verbose=False) + agent = create_buyer_deal_specialist_agent(verbose=False) assert agent.memory def test_default_no_tools(self): - agent = create_dsp_agent(verbose=False) + agent = create_buyer_deal_specialist_agent(verbose=False) assert agent.tools == [] def test_custom_tools(self, mock_tools): - agent = create_dsp_agent(tools=mock_tools, verbose=False) + agent = create_buyer_deal_specialist_agent(tools=mock_tools, verbose=False) assert len(agent.tools) == 3 def test_llm_temperature(self): - agent = create_dsp_agent(verbose=False) + agent = create_buyer_deal_specialist_agent(verbose=False) assert agent.llm.temperature == 0.5 @@ -573,7 +573,7 @@ class TestHierarchyInvariants: L2_FACTORIES = [ create_branding_agent, create_ctv_agent, - create_dsp_agent, + create_buyer_deal_specialist_agent, create_mobile_app_agent, create_performance_agent, ] @@ -819,7 +819,7 @@ def test_level2_init_exports(self): from ad_buyer.agents.level2 import ( create_branding_agent, create_ctv_agent, - create_dsp_agent, + create_buyer_deal_specialist_agent, create_mobile_app_agent, create_performance_agent, ) @@ -827,7 +827,7 @@ def test_level2_init_exports(self): for fn in [ create_branding_agent, create_ctv_agent, - create_dsp_agent, + create_buyer_deal_specialist_agent, create_mobile_app_agent, create_performance_agent, ]: diff --git a/tests/unit/test_booking/test_pricing.py b/tests/unit/test_booking/test_pricing.py index 7355cb9..8544e9a 100644 --- a/tests/unit/test_booking/test_pricing.py +++ b/tests/unit/test_booking/test_pricing.py @@ -6,8 +6,8 @@ These tests verify that the PricingCalculator produces identical results to the duplicated pricing logic previously in: - unified_client.py (get_pricing, request_deal) -- tools/dsp/request_deal.py (_create_deal_response) -- tools/dsp/get_pricing.py (_format_pricing) +- tools/buyer_deals/request_deal.py (_create_deal_response) +- tools/buyer_deals/get_pricing.py (_format_pricing) """ import pytest diff --git a/tests/unit/test_dsp_deal_flow.py b/tests/unit/test_dsp_deal_flow.py index eaf894e..c7e8ecc 100644 --- a/tests/unit/test_dsp_deal_flow.py +++ b/tests/unit/test_dsp_deal_flow.py @@ -1,7 +1,7 @@ # Author: Green Mountain Systems AI Inc. # Donated to IAB Tech Lab -"""Tests for DSPDealFlow - DSP deal discovery and Deal ID creation workflow. +"""Tests for BuyerDealFlow - buyer deal discovery and Deal ID creation workflow. Covers: - Request validation (empty request, valid request) @@ -9,7 +9,7 @@ - Product evaluation and selection (crew-based, product ID extraction) - Deal ID request (success, no product selected, tool failure) - Status reporting -- run_dsp_deal_flow convenience function +- run_buyer_deal_flow convenience function - Edge cases: missing fields, state transitions """ @@ -18,12 +18,12 @@ import pytest -from ad_buyer.flows.dsp_deal_flow import ( +from ad_buyer.flows.buyer_deal_flow import ( DiscoveredProduct, - DSPDealFlow, - DSPFlowState, - DSPFlowStatus, - run_dsp_deal_flow, + BuyerDealFlow, + BuyerDealFlowState, + BuyerDealFlowStatus, + run_buyer_deal_flow, ) from ad_buyer.models.buyer_identity import ( AccessTier, @@ -79,8 +79,8 @@ def public_buyer_context(): @pytest.fixture def dsp_flow(mock_unified_client, agency_buyer_context): - """Create a DSPDealFlow with mocked client and agency context.""" - return DSPDealFlow(client=mock_unified_client, buyer_context=agency_buyer_context) + """Create a BuyerDealFlow with mocked client and agency context.""" + return BuyerDealFlow(client=mock_unified_client, buyer_context=agency_buyer_context) @pytest.fixture @@ -100,24 +100,24 @@ def dsp_flow_with_request(dsp_flow): # =========================================================================== -class TestDSPFlowModels: +class TestBuyerDealFlowModels: """Tests for DSP flow data models.""" def test_dsp_flow_state_defaults(self): - """DSPFlowState initializes with correct defaults.""" - state = DSPFlowState() + """BuyerDealFlowState initializes with correct defaults.""" + state = BuyerDealFlowState() assert state.request == "" assert state.deal_type == DealType.PREFERRED_DEAL assert state.impressions is None assert state.max_cpm is None - assert state.status == DSPFlowStatus.INITIALIZED + assert state.status == BuyerDealFlowStatus.INITIALIZED assert state.errors == [] assert state.discovered_products == [] def test_dsp_flow_state_with_values(self): - """DSPFlowState can be created with custom values.""" - state = DSPFlowState( + """BuyerDealFlowState can be created with custom values.""" + state = BuyerDealFlowState( request="CTV inventory", deal_type=DealType.PROGRAMMATIC_GUARANTEED, impressions=5_000_000, @@ -151,14 +151,14 @@ def test_discovered_product_model(self): assert product.score == 0.85 def test_dsp_flow_status_enum(self): - """DSPFlowStatus enum has all expected values.""" - assert DSPFlowStatus.INITIALIZED.value == "initialized" - assert DSPFlowStatus.REQUEST_RECEIVED.value == "request_received" - assert DSPFlowStatus.DISCOVERING_INVENTORY.value == "discovering_inventory" - assert DSPFlowStatus.EVALUATING_PRICING.value == "evaluating_pricing" - assert DSPFlowStatus.REQUESTING_DEAL.value == "requesting_deal" - assert DSPFlowStatus.DEAL_CREATED.value == "deal_created" - assert DSPFlowStatus.FAILED.value == "failed" + """BuyerDealFlowStatus enum has all expected values.""" + assert BuyerDealFlowStatus.INITIALIZED.value == "initialized" + assert BuyerDealFlowStatus.REQUEST_RECEIVED.value == "request_received" + assert BuyerDealFlowStatus.DISCOVERING_INVENTORY.value == "discovering_inventory" + assert BuyerDealFlowStatus.EVALUATING_PRICING.value == "evaluating_pricing" + assert BuyerDealFlowStatus.REQUESTING_DEAL.value == "requesting_deal" + assert BuyerDealFlowStatus.DEAL_CREATED.value == "deal_created" + assert BuyerDealFlowStatus.FAILED.value == "failed" # =========================================================================== @@ -176,7 +176,7 @@ def test_valid_request(self, dsp_flow_with_request): assert result["status"] == "success" assert result["request"] == dsp_flow_with_request.state.request assert result["access_tier"] == AccessTier.AGENCY.value - assert dsp_flow_with_request.state.status == DSPFlowStatus.REQUEST_RECEIVED + assert dsp_flow_with_request.state.status == BuyerDealFlowStatus.REQUEST_RECEIVED def test_empty_request_fails(self, dsp_flow): """Empty request string fails.""" @@ -185,7 +185,7 @@ def test_empty_request_fails(self, dsp_flow): result = dsp_flow.receive_request() assert result["status"] == "failed" - assert dsp_flow.state.status == DSPFlowStatus.FAILED + assert dsp_flow.state.status == BuyerDealFlowStatus.FAILED assert len(dsp_flow.state.errors) > 0 def test_buyer_context_stored_in_state(self, dsp_flow_with_request): @@ -198,7 +198,7 @@ def test_buyer_context_stored_in_state(self, dsp_flow_with_request): def test_advertiser_tier_access(self, mock_unified_client, advertiser_buyer_context): """Advertiser tier is correctly reported.""" - flow = DSPDealFlow(client=mock_unified_client, buyer_context=advertiser_buyer_context) + flow = BuyerDealFlow(client=mock_unified_client, buyer_context=advertiser_buyer_context) flow.state.request = "Premium inventory" result = flow.receive_request() @@ -207,7 +207,7 @@ def test_advertiser_tier_access(self, mock_unified_client, advertiser_buyer_cont def test_public_tier_access(self, mock_unified_client, public_buyer_context): """Public tier is correctly reported.""" - flow = DSPDealFlow(client=mock_unified_client, buyer_context=public_buyer_context) + flow = BuyerDealFlow(client=mock_unified_client, buyer_context=public_buyer_context) flow.state.request = "Any inventory" result = flow.receive_request() @@ -229,7 +229,7 @@ def test_skips_on_failed_request(self, dsp_flow): assert result["status"] == "failed" - @patch.object(DSPDealFlow, "__init__", lambda self, **kw: None) + @patch.object(BuyerDealFlow, "__init__", lambda self, **kw: None) def test_discovery_success(self, dsp_flow_with_request): """Successful discovery returns results and updates status.""" dsp_flow_with_request._discover_tool = MagicMock() @@ -239,7 +239,7 @@ def test_discovery_success(self, dsp_flow_with_request): assert result["status"] == "success" assert "discovery_result" in result - assert dsp_flow_with_request.state.status == DSPFlowStatus.DISCOVERING_INVENTORY + assert dsp_flow_with_request.state.status == BuyerDealFlowStatus.DISCOVERING_INVENTORY def test_discovery_tool_exception(self, dsp_flow_with_request): """Exception in discovery tool sets FAILED status.""" @@ -250,7 +250,7 @@ def test_discovery_tool_exception(self, dsp_flow_with_request): result = dsp_flow_with_request.discover_inventory({"status": "success"}) assert result["status"] == "failed" - assert dsp_flow_with_request.state.status == DSPFlowStatus.FAILED + assert dsp_flow_with_request.state.status == BuyerDealFlowStatus.FAILED assert len(dsp_flow_with_request.state.errors) > 0 def test_discovery_passes_filters(self, dsp_flow_with_request): @@ -322,9 +322,9 @@ def test_skips_on_failed_discovery(self, dsp_flow_with_request): ) assert result["status"] == "failed" - @patch("ad_buyer.flows.dsp_deal_flow.Task") - @patch("ad_buyer.flows.dsp_deal_flow.Crew") - @patch("ad_buyer.flows.dsp_deal_flow.create_dsp_agent") + @patch("ad_buyer.flows.buyer_deal_flow.Task") + @patch("ad_buyer.flows.buyer_deal_flow.Crew") + @patch("ad_buyer.flows.buyer_deal_flow.create_buyer_deal_specialist_agent") def test_successful_selection( self, mock_agent, mock_crew_cls, mock_task, dsp_flow_with_request ): @@ -346,9 +346,9 @@ def test_successful_selection( assert dsp_flow_with_request.state.selected_product_id == "ctv_001" assert dsp_flow_with_request.state.pricing_details is not None - @patch("ad_buyer.flows.dsp_deal_flow.Task") - @patch("ad_buyer.flows.dsp_deal_flow.Crew") - @patch("ad_buyer.flows.dsp_deal_flow.create_dsp_agent") + @patch("ad_buyer.flows.buyer_deal_flow.Task") + @patch("ad_buyer.flows.buyer_deal_flow.Crew") + @patch("ad_buyer.flows.buyer_deal_flow.create_buyer_deal_specialist_agent") def test_no_product_id_extracted( self, mock_agent, mock_crew_cls, mock_task, dsp_flow_with_request ): @@ -364,9 +364,9 @@ def test_no_product_id_extracted( assert result["status"] == "success" assert result["selected_product_id"] is None - @patch("ad_buyer.flows.dsp_deal_flow.Task") - @patch("ad_buyer.flows.dsp_deal_flow.Crew") - @patch("ad_buyer.flows.dsp_deal_flow.create_dsp_agent") + @patch("ad_buyer.flows.buyer_deal_flow.Task") + @patch("ad_buyer.flows.buyer_deal_flow.Crew") + @patch("ad_buyer.flows.buyer_deal_flow.create_buyer_deal_specialist_agent") def test_evaluation_exception( self, mock_agent, mock_crew_cls, mock_task, dsp_flow_with_request ): @@ -378,7 +378,7 @@ def test_evaluation_exception( ) assert result["status"] == "failed" - assert dsp_flow_with_request.state.status == DSPFlowStatus.FAILED + assert dsp_flow_with_request.state.status == BuyerDealFlowStatus.FAILED # =========================================================================== @@ -404,7 +404,7 @@ def test_fails_with_no_product_selected(self, dsp_flow_with_request): assert result["status"] == "failed" assert "No product selected" in result["error"] - assert dsp_flow_with_request.state.status == DSPFlowStatus.FAILED + assert dsp_flow_with_request.state.status == BuyerDealFlowStatus.FAILED def test_successful_deal_creation(self, dsp_flow_with_request): """Successful deal request stores deal response and sets DEAL_CREATED.""" @@ -416,7 +416,7 @@ def test_successful_deal_creation(self, dsp_flow_with_request): result = dsp_flow_with_request.request_deal_id({"status": "success"}) assert result["status"] == "success" - assert dsp_flow_with_request.state.status == DSPFlowStatus.DEAL_CREATED + assert dsp_flow_with_request.state.status == BuyerDealFlowStatus.DEAL_CREATED assert dsp_flow_with_request.state.deal_response is not None assert "raw" in dsp_flow_with_request.state.deal_response @@ -430,7 +430,7 @@ def test_deal_tool_exception(self, dsp_flow_with_request): result = dsp_flow_with_request.request_deal_id({"status": "success"}) assert result["status"] == "failed" - assert dsp_flow_with_request.state.status == DSPFlowStatus.FAILED + assert dsp_flow_with_request.state.status == BuyerDealFlowStatus.FAILED assert len(dsp_flow_with_request.state.errors) > 0 def test_deal_passes_flight_dates(self, dsp_flow_with_request): @@ -453,7 +453,7 @@ def test_deal_passes_flight_dates(self, dsp_flow_with_request): # =========================================================================== -class TestDSPFlowGetStatus: +class TestBuyerDealFlowGetStatus: """Tests for the DSP flow status method.""" def test_initial_status(self, dsp_flow): @@ -469,7 +469,7 @@ def test_initial_status(self, dsp_flow): def test_status_with_request(self, dsp_flow_with_request): """Status reflects configured request.""" - dsp_flow_with_request.state.status = DSPFlowStatus.REQUEST_RECEIVED + dsp_flow_with_request.state.status = BuyerDealFlowStatus.REQUEST_RECEIVED status = dsp_flow_with_request.get_status() assert status["status"] == "request_received" @@ -478,7 +478,7 @@ def test_status_with_request(self, dsp_flow_with_request): def test_status_after_deal_creation(self, dsp_flow_with_request): """Status reflects deal creation.""" - dsp_flow_with_request.state.status = DSPFlowStatus.DEAL_CREATED + dsp_flow_with_request.state.status = BuyerDealFlowStatus.DEAL_CREATED dsp_flow_with_request.state.selected_product_id = "ctv_001" dsp_flow_with_request.state.deal_response = {"raw": "DEAL-ABC123"} @@ -509,12 +509,12 @@ def test_status_with_errors(self, dsp_flow): # =========================================================================== -class TestDSPFlowInitialization: +class TestBuyerDealFlowInitialization: """Tests for DSP flow construction.""" def test_flow_creates_tools(self, mock_unified_client, agency_buyer_context): """Flow creates discover, pricing, and deal tools on init.""" - flow = DSPDealFlow(client=mock_unified_client, buyer_context=agency_buyer_context) + flow = BuyerDealFlow(client=mock_unified_client, buyer_context=agency_buyer_context) assert flow._discover_tool is not None assert flow._pricing_tool is not None @@ -526,7 +526,7 @@ def test_flow_state_is_initialized(self, dsp_flow): """Flow state is initialized with defaults.""" # crewai wraps the state model in a StateWithId subclass, # so we check attributes rather than exact type. - assert dsp_flow.state.status == DSPFlowStatus.INITIALIZED + assert dsp_flow.state.status == BuyerDealFlowStatus.INITIALIZED assert dsp_flow.state.request == "" assert dsp_flow.state.errors == [] @@ -536,14 +536,14 @@ def test_flow_state_is_initialized(self, dsp_flow): # =========================================================================== -class TestDSPFlowStateTransitions: +class TestBuyerDealFlowStateTransitions: """Tests verifying status transitions through the flow.""" def test_request_received_transition(self, dsp_flow_with_request): """receive_request transitions INITIALIZED -> REQUEST_RECEIVED.""" - assert dsp_flow_with_request.state.status == DSPFlowStatus.INITIALIZED + assert dsp_flow_with_request.state.status == BuyerDealFlowStatus.INITIALIZED dsp_flow_with_request.receive_request() - assert dsp_flow_with_request.state.status == DSPFlowStatus.REQUEST_RECEIVED + assert dsp_flow_with_request.state.status == BuyerDealFlowStatus.REQUEST_RECEIVED def test_discovering_inventory_transition(self, dsp_flow_with_request): """discover_inventory transitions to DISCOVERING_INVENTORY.""" @@ -551,11 +551,11 @@ def test_discovering_inventory_transition(self, dsp_flow_with_request): dsp_flow_with_request.discover_inventory({"status": "success"}) - assert dsp_flow_with_request.state.status == DSPFlowStatus.DISCOVERING_INVENTORY + assert dsp_flow_with_request.state.status == BuyerDealFlowStatus.DISCOVERING_INVENTORY - @patch("ad_buyer.flows.dsp_deal_flow.Task") - @patch("ad_buyer.flows.dsp_deal_flow.Crew") - @patch("ad_buyer.flows.dsp_deal_flow.create_dsp_agent") + @patch("ad_buyer.flows.buyer_deal_flow.Task") + @patch("ad_buyer.flows.buyer_deal_flow.Crew") + @patch("ad_buyer.flows.buyer_deal_flow.create_buyer_deal_specialist_agent") def test_evaluating_pricing_transition( self, mock_agent, mock_crew_cls, mock_task, dsp_flow_with_request ): @@ -569,7 +569,7 @@ def test_evaluating_pricing_transition( {"status": "success", "discovery_result": "results"} ) - assert dsp_flow_with_request.state.status == DSPFlowStatus.EVALUATING_PRICING + assert dsp_flow_with_request.state.status == BuyerDealFlowStatus.EVALUATING_PRICING def test_deal_created_transition(self, dsp_flow_with_request): """request_deal_id transitions to DEAL_CREATED on success.""" @@ -578,34 +578,34 @@ def test_deal_created_transition(self, dsp_flow_with_request): dsp_flow_with_request.request_deal_id({"status": "success"}) - assert dsp_flow_with_request.state.status == DSPFlowStatus.DEAL_CREATED + assert dsp_flow_with_request.state.status == BuyerDealFlowStatus.DEAL_CREATED def test_failed_transition_on_empty_request(self, dsp_flow): """Empty request transitions to FAILED.""" dsp_flow.state.request = "" dsp_flow.receive_request() - assert dsp_flow.state.status == DSPFlowStatus.FAILED + assert dsp_flow.state.status == BuyerDealFlowStatus.FAILED def test_failed_transition_on_discovery_error(self, dsp_flow_with_request): """Discovery failure transitions to FAILED.""" dsp_flow_with_request._discover_tool._run = MagicMock(side_effect=RuntimeError("error")) dsp_flow_with_request.discover_inventory({"status": "success"}) - assert dsp_flow_with_request.state.status == DSPFlowStatus.FAILED + assert dsp_flow_with_request.state.status == BuyerDealFlowStatus.FAILED # =========================================================================== -# run_dsp_deal_flow convenience function +# run_buyer_deal_flow convenience function # =========================================================================== -class TestRunDspDealFlowConvenience: - """Tests for the run_dsp_deal_flow helper function.""" +class TestRunBuyerDealFlowConvenience: + """Tests for the run_buyer_deal_flow helper function.""" def test_function_signature(self): - """run_dsp_deal_flow has the expected parameters.""" + """run_buyer_deal_flow has the expected parameters.""" import inspect - sig = inspect.signature(run_dsp_deal_flow) + sig = inspect.signature(run_buyer_deal_flow) params = list(sig.parameters.keys()) assert "request" in params @@ -621,7 +621,7 @@ def test_default_deal_type(self): """Default deal type is PREFERRED_DEAL.""" import inspect - sig = inspect.signature(run_dsp_deal_flow) + sig = inspect.signature(run_buyer_deal_flow) deal_type_default = sig.parameters["deal_type"].default assert deal_type_default == DealType.PREFERRED_DEAL diff --git a/tests/unit/test_dsp_discovery_pricing.py b/tests/unit/test_dsp_discovery_pricing.py index c123324..a53c0d5 100644 --- a/tests/unit/test_dsp_discovery_pricing.py +++ b/tests/unit/test_dsp_discovery_pricing.py @@ -7,7 +7,7 @@ - DiscoverInventoryTool: filters, formatting, edge cases - GetPricingTool: tier calculations, volume discounts, cost projections - RequestDealTool: deal creation, negotiation, validation, deal ID generation -- DSPDealFlow: state machine, flow steps, error propagation +- BuyerDealFlow: state machine, flow steps, error propagation - UnifiedClient DSP methods: discover_inventory, get_pricing, request_deal - Cross-tier pricing consistency across tools and client """ @@ -18,11 +18,11 @@ import pytest from ad_buyer.clients.unified_client import UnifiedClient -from ad_buyer.flows.dsp_deal_flow import ( +from ad_buyer.flows.buyer_deal_flow import ( DiscoveredProduct, - DSPDealFlow, - DSPFlowState, - DSPFlowStatus, + BuyerDealFlow, + BuyerDealFlowState, + BuyerDealFlowStatus, ) from ad_buyer.models.buyer_identity import ( AccessTier, @@ -32,7 +32,7 @@ DealResponse, DealType, ) -from ad_buyer.tools.dsp import DiscoverInventoryTool, GetPricingTool, RequestDealTool +from ad_buyer.tools.buyer_deals import DiscoverInventoryTool, GetPricingTool, RequestDealTool # ============================================================================= # Shared fixtures @@ -885,17 +885,17 @@ def test_get_activation_case_insensitive(self): # ============================================================================= -# DSPFlowState model tests +# BuyerDealFlowState model tests # ============================================================================= -class TestDSPFlowState: - """Tests for DSPFlowState model.""" +class TestBuyerDealFlowState: + """Tests for BuyerDealFlowState model.""" def test_default_state(self): """Default state should be initialized with sensible defaults.""" - state = DSPFlowState() - assert state.status == DSPFlowStatus.INITIALIZED + state = BuyerDealFlowState() + assert state.status == BuyerDealFlowStatus.INITIALIZED assert state.request == "" assert state.deal_type == DealType.PREFERRED_DEAL assert state.impressions is None @@ -905,7 +905,7 @@ def test_default_state(self): def test_state_with_values(self): """State should accept all field values.""" - state = DSPFlowState( + state = BuyerDealFlowState( request="CTV inventory", deal_type=DealType.PROGRAMMATIC_GUARANTEED, impressions=5_000_000, @@ -955,61 +955,61 @@ def test_discovered_product_defaults(self): # ============================================================================= -# DSPDealFlow - State Machine Tests +# BuyerDealFlow - State Machine Tests # ============================================================================= -class TestDSPDealFlowInit: - """Tests for DSPDealFlow initialization.""" +class TestBuyerDealFlowInit: + """Tests for BuyerDealFlow initialization.""" def test_flow_creates_all_tools(self, mock_client, agency_context): """Flow should initialize all three DSP tools.""" - flow = DSPDealFlow(client=mock_client, buyer_context=agency_context) + flow = BuyerDealFlow(client=mock_client, buyer_context=agency_context) assert flow._discover_tool is not None assert flow._pricing_tool is not None assert flow._deal_tool is not None def test_flow_initial_state(self, mock_client, agency_context): """Flow state should start as INITIALIZED.""" - flow = DSPDealFlow(client=mock_client, buyer_context=agency_context) - assert flow.state.status == DSPFlowStatus.INITIALIZED + flow = BuyerDealFlow(client=mock_client, buyer_context=agency_context) + assert flow.state.status == BuyerDealFlowStatus.INITIALIZED -class TestDSPDealFlowReceiveRequest: +class TestBuyerDealFlowReceiveRequest: """Tests for the receive_request step.""" def test_empty_request_fails(self, mock_client, agency_context): """Empty request should set status to FAILED.""" - flow = DSPDealFlow(client=mock_client, buyer_context=agency_context) + flow = BuyerDealFlow(client=mock_client, buyer_context=agency_context) flow.state.request = "" result = flow.receive_request() assert result["status"] == "failed" - assert flow.state.status == DSPFlowStatus.FAILED + assert flow.state.status == BuyerDealFlowStatus.FAILED assert len(flow.state.errors) > 0 def test_valid_request_succeeds(self, mock_client, agency_context): """Valid request should set status to REQUEST_RECEIVED.""" - flow = DSPDealFlow(client=mock_client, buyer_context=agency_context) + flow = BuyerDealFlow(client=mock_client, buyer_context=agency_context) flow.state.request = "CTV inventory under $25" result = flow.receive_request() assert result["status"] == "success" - assert flow.state.status == DSPFlowStatus.REQUEST_RECEIVED + assert flow.state.status == BuyerDealFlowStatus.REQUEST_RECEIVED assert result["access_tier"] == "agency" def test_request_stores_buyer_context(self, mock_client, advertiser_context): """receive_request should store serialized buyer context.""" - flow = DSPDealFlow(client=mock_client, buyer_context=advertiser_context) + flow = BuyerDealFlow(client=mock_client, buyer_context=advertiser_context) flow.state.request = "Display ads" flow.receive_request() assert flow.state.buyer_context is not None -class TestDSPDealFlowGetStatus: +class TestBuyerDealFlowGetStatus: """Tests for the get_status method.""" def test_get_status_initial(self, mock_client, agency_context): """get_status should reflect current flow state.""" - flow = DSPDealFlow(client=mock_client, buyer_context=agency_context) + flow = BuyerDealFlow(client=mock_client, buyer_context=agency_context) flow.state.request = "test" status = flow.get_status() assert status["status"] == "initialized" @@ -1018,7 +1018,7 @@ def test_get_status_initial(self, mock_client, agency_context): def test_get_status_after_failure(self, mock_client, agency_context): """get_status should show failure state.""" - flow = DSPDealFlow(client=mock_client, buyer_context=agency_context) + flow = BuyerDealFlow(client=mock_client, buyer_context=agency_context) flow.state.request = "" flow.receive_request() status = flow.get_status() @@ -1026,12 +1026,12 @@ def test_get_status_after_failure(self, mock_client, agency_context): assert len(status["errors"]) > 0 -class TestDSPDealFlowDiscoverInventory: +class TestBuyerDealFlowDiscoverInventory: """Tests for the discover_inventory step.""" def test_discover_skips_on_failed_request(self, mock_client, agency_context): """discover_inventory should pass through failure.""" - flow = DSPDealFlow(client=mock_client, buyer_context=agency_context) + flow = BuyerDealFlow(client=mock_client, buyer_context=agency_context) failed_result = {"status": "failed", "errors": ["bad request"]} result = flow.discover_inventory(failed_result) assert result["status"] == "failed" @@ -1039,7 +1039,7 @@ def test_discover_skips_on_failed_request(self, mock_client, agency_context): def test_discover_calls_tool_run(self, mock_client, agency_context): """discover_inventory should call the discover tool's _run method.""" mock_client.search_products.return_value = MagicMock(success=True, data=[_product()]) - flow = DSPDealFlow(client=mock_client, buyer_context=agency_context) + flow = BuyerDealFlow(client=mock_client, buyer_context=agency_context) flow.state.request = "CTV inventory" flow.state.max_cpm = 30.0 flow.state.impressions = 2_000_000 @@ -1047,12 +1047,12 @@ def test_discover_calls_tool_run(self, mock_client, agency_context): result = flow.discover_inventory({"status": "success"}) assert result["status"] == "success" assert "discovery_result" in result - assert flow.state.status == DSPFlowStatus.DISCOVERING_INVENTORY + assert flow.state.status == BuyerDealFlowStatus.DISCOVERING_INVENTORY def test_discover_handles_tool_exception(self, mock_client, agency_context): """Exception in discover tool should be caught and recorded.""" mock_client.search_products.side_effect = ConnectionError("network down") - flow = DSPDealFlow(client=mock_client, buyer_context=agency_context) + flow = BuyerDealFlow(client=mock_client, buyer_context=agency_context) flow.state.request = "anything" result = flow.discover_inventory({"status": "success"}) @@ -1062,73 +1062,73 @@ def test_discover_handles_tool_exception(self, mock_client, agency_context): assert result["status"] in ("success", "failed") -class TestDSPDealFlowRequestDealId: +class TestBuyerDealFlowRequestDealId: """Tests for the request_deal_id step.""" def test_request_deal_skips_on_failure(self, mock_client, agency_context): """request_deal_id should pass through failure.""" - flow = DSPDealFlow(client=mock_client, buyer_context=agency_context) + flow = BuyerDealFlow(client=mock_client, buyer_context=agency_context) result = flow.request_deal_id({"status": "failed", "error": "no products"}) assert result["status"] == "failed" def test_request_deal_no_product_selected(self, mock_client, agency_context): """request_deal_id with no selected product should fail.""" - flow = DSPDealFlow(client=mock_client, buyer_context=agency_context) + flow = BuyerDealFlow(client=mock_client, buyer_context=agency_context) flow.state.selected_product_id = None result = flow.request_deal_id({"status": "success"}) assert result["status"] == "failed" assert "No product selected" in result.get("error", "") - assert flow.state.status == DSPFlowStatus.FAILED + assert flow.state.status == BuyerDealFlowStatus.FAILED def test_request_deal_creates_deal(self, mock_client, agency_context): """request_deal_id with valid product should create a deal.""" mock_client.get_product.return_value = MagicMock(success=True, data=_product()) - flow = DSPDealFlow(client=mock_client, buyer_context=agency_context) + flow = BuyerDealFlow(client=mock_client, buyer_context=agency_context) flow.state.selected_product_id = "prod_001" flow.state.deal_type = DealType.PREFERRED_DEAL flow.state.impressions = 1_000_000 result = flow.request_deal_id({"status": "success"}) assert result["status"] == "success" - assert flow.state.status == DSPFlowStatus.DEAL_CREATED + assert flow.state.status == BuyerDealFlowStatus.DEAL_CREATED assert flow.state.deal_response is not None -class TestDSPDealFlowExtractProductId: +class TestBuyerDealFlowExtractProductId: """Tests for _extract_product_id helper.""" def test_extract_from_product_id_format(self, mock_client, agency_context): """Should extract from 'product_id: xxx' format.""" - flow = DSPDealFlow(client=mock_client, buyer_context=agency_context) + flow = BuyerDealFlow(client=mock_client, buyer_context=agency_context) result = flow._extract_product_id("product_id: ctv_premium_001") assert result == "ctv_premium_001" def test_extract_from_product_id_colon(self, mock_client, agency_context): """Should extract from 'Product ID: xxx' format.""" - flow = DSPDealFlow(client=mock_client, buyer_context=agency_context) + flow = BuyerDealFlow(client=mock_client, buyer_context=agency_context) result = flow._extract_product_id("The best option is Product ID: display_001") assert result == "display_001" def test_extract_returns_none_when_not_found(self, mock_client, agency_context): """Should return None if no product ID pattern found.""" - flow = DSPDealFlow(client=mock_client, buyer_context=agency_context) + flow = BuyerDealFlow(client=mock_client, buyer_context=agency_context) result = flow._extract_product_id("This text has no product reference at all.") assert result is None def test_extract_from_id_format(self, mock_client, agency_context): """Should extract from generic 'id: xxx' format.""" - flow = DSPDealFlow(client=mock_client, buyer_context=agency_context) + flow = BuyerDealFlow(client=mock_client, buyer_context=agency_context) result = flow._extract_product_id("id: prod_abc") assert result == "prod_abc" # ============================================================================= -# DSPFlowStatus enum tests +# BuyerDealFlowStatus enum tests # ============================================================================= -class TestDSPFlowStatus: - """Tests for DSPFlowStatus enum values.""" +class TestBuyerDealFlowStatus: + """Tests for BuyerDealFlowStatus enum values.""" def test_all_status_values(self): """All expected status values should be defined.""" @@ -1141,7 +1141,7 @@ def test_all_status_values(self): "deal_created", "failed", } - actual = {s.value for s in DSPFlowStatus} + actual = {s.value for s in BuyerDealFlowStatus} assert actual == expected diff --git a/tests/unit/test_dsp_tools.py b/tests/unit/test_dsp_tools.py index 5c85a6d..e23acce 100644 --- a/tests/unit/test_dsp_tools.py +++ b/tests/unit/test_dsp_tools.py @@ -11,7 +11,7 @@ BuyerContext, BuyerIdentity, ) -from ad_buyer.tools.dsp import DiscoverInventoryTool, GetPricingTool, RequestDealTool +from ad_buyer.tools.buyer_deals import DiscoverInventoryTool, GetPricingTool, RequestDealTool @pytest.fixture diff --git a/tests/unit/test_negotiation_enabled_flag.py b/tests/unit/test_negotiation_enabled_flag.py index 0fcf367..c9200d3 100644 --- a/tests/unit/test_negotiation_enabled_flag.py +++ b/tests/unit/test_negotiation_enabled_flag.py @@ -23,7 +23,7 @@ ) from ad_buyer.negotiation.client import NegotiationClient from ad_buyer.negotiation.strategies.simple_threshold import SimpleThresholdStrategy -from ad_buyer.tools.dsp import GetPricingTool, RequestDealTool +from ad_buyer.tools.buyer_deals import GetPricingTool, RequestDealTool # -- Fixtures ---------------------------------------------------------------- diff --git a/tests/unit/test_no_hardcoded_urls.py b/tests/unit/test_no_hardcoded_urls.py index 1e463fb..fcc5ea6 100644 --- a/tests/unit/test_no_hardcoded_urls.py +++ b/tests/unit/test_no_hardcoded_urls.py @@ -13,7 +13,7 @@ "src/ad_buyer/clients/unified_client.py", "src/ad_buyer/clients/mcp_client.py", "src/ad_buyer/clients/a2a_client.py", - "src/ad_buyer/flows/dsp_deal_flow.py", + "src/ad_buyer/flows/buyer_deal_flow.py", ] # The URL pattern that should not appear as a default parameter value diff --git a/tests/unit/test_state_machine.py b/tests/unit/test_state_machine.py index cc612f7..197f013 100644 --- a/tests/unit/test_state_machine.py +++ b/tests/unit/test_state_machine.py @@ -432,7 +432,7 @@ def test_from_execution_status_unknown_returns_initialized(self): assert result == BuyerCampaignStatus.INITIALIZED def test_from_dsp_flow_status_deal_created(self): - """Map DSPFlowStatus values to deal states.""" + """Map BuyerDealFlowStatus values to deal states.""" result = from_dsp_flow_status("deal_created") assert result == BuyerDealStatus.BOOKED From 20fbed3328658aadb89e45e1ff9c31e3a812f8ea Mon Sep 17 00:00:00 2001 From: ATC964 <82515918+atc964@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:41:58 -0400 Subject: [PATCH 3/4] Rename test files from dsp to buyer_deal to match code rename - test_dsp_deal_flow.py -> test_buyer_deal_flow.py - test_dsp_tools.py -> test_buyer_deal_tools.py - test_dsp_discovery_pricing.py -> test_buyer_deal_discovery_pricing.py - Updated internal fixture names (dsp_flow -> buyer_flow) - Updated docstrings and class names referencing DSP bead: ar-k8ho Co-Authored-By: Claude Opus 4.6 (1M context) --- ...y => test_buyer_deal_discovery_pricing.py} | 16 +- ...p_deal_flow.py => test_buyer_deal_flow.py} | 274 +++++++++--------- ..._dsp_tools.py => test_buyer_deal_tools.py} | 6 +- 3 files changed, 148 insertions(+), 148 deletions(-) rename tests/unit/{test_dsp_discovery_pricing.py => test_buyer_deal_discovery_pricing.py} (99%) rename tests/unit/{test_dsp_deal_flow.py => test_buyer_deal_flow.py} (64%) rename tests/unit/{test_dsp_tools.py => test_buyer_deal_tools.py} (98%) diff --git a/tests/unit/test_dsp_discovery_pricing.py b/tests/unit/test_buyer_deal_discovery_pricing.py similarity index 99% rename from tests/unit/test_dsp_discovery_pricing.py rename to tests/unit/test_buyer_deal_discovery_pricing.py index a53c0d5..469c3fc 100644 --- a/tests/unit/test_dsp_discovery_pricing.py +++ b/tests/unit/test_buyer_deal_discovery_pricing.py @@ -1,14 +1,14 @@ # Author: Green Mountain Systems AI Inc. # Donated to IAB Tech Lab -"""Comprehensive tests for DSP discovery and pricing flows. +"""Comprehensive tests for buyer deal discovery and pricing flows. Covers: - DiscoverInventoryTool: filters, formatting, edge cases - GetPricingTool: tier calculations, volume discounts, cost projections - RequestDealTool: deal creation, negotiation, validation, deal ID generation - BuyerDealFlow: state machine, flow steps, error propagation -- UnifiedClient DSP methods: discover_inventory, get_pricing, request_deal +- UnifiedClient buyer deal methods: discover_inventory, get_pricing, request_deal - Cross-tier pricing consistency across tools and client """ @@ -740,8 +740,8 @@ async def test_deal_id_format(self, mock_client, agency_context): assert match is not None, f"Expected DEAL-XXXXXXXX in output: {result}" @pytest.mark.asyncio - async def test_deal_includes_all_dsp_platforms(self, mock_client, agency_context): - """Deal should include activation instructions for all major DSPs.""" + async def test_deal_includes_all_buyer_platforms(self, mock_client, agency_context): + """Deal should include activation instructions for all major buyer platforms.""" mock_client.get_product.return_value = MagicMock(success=True, data=_product()) tool = RequestDealTool(client=mock_client, buyer_context=agency_context) result = await tool._arun(product_id="prod_001") @@ -963,7 +963,7 @@ class TestBuyerDealFlowInit: """Tests for BuyerDealFlow initialization.""" def test_flow_creates_all_tools(self, mock_client, agency_context): - """Flow should initialize all three DSP tools.""" + """Flow should initialize all three buyer deal tools.""" flow = BuyerDealFlow(client=mock_client, buyer_context=agency_context) assert flow._discover_tool is not None assert flow._pricing_tool is not None @@ -1146,12 +1146,12 @@ def test_all_status_values(self): # ============================================================================= -# UnifiedClient DSP methods +# UnifiedClient buyer deal methods # ============================================================================= -class TestUnifiedClientDSPMethods: - """Test DSP-specific methods on UnifiedClient.""" +class TestUnifiedClientBuyerDealMethods: + """Test buyer deal-specific methods on UnifiedClient.""" def test_set_buyer_identity(self, advertiser_identity): """set_buyer_identity should store the identity.""" diff --git a/tests/unit/test_dsp_deal_flow.py b/tests/unit/test_buyer_deal_flow.py similarity index 64% rename from tests/unit/test_dsp_deal_flow.py rename to tests/unit/test_buyer_deal_flow.py index c7e8ecc..d477fda 100644 --- a/tests/unit/test_dsp_deal_flow.py +++ b/tests/unit/test_buyer_deal_flow.py @@ -78,21 +78,21 @@ def public_buyer_context(): @pytest.fixture -def dsp_flow(mock_unified_client, agency_buyer_context): +def buyer_flow(mock_unified_client, agency_buyer_context): """Create a BuyerDealFlow with mocked client and agency context.""" return BuyerDealFlow(client=mock_unified_client, buyer_context=agency_buyer_context) @pytest.fixture -def dsp_flow_with_request(dsp_flow): - """DSP flow with a valid request already set.""" - dsp_flow.state.request = "CTV inventory for sports audiences under $30 CPM" - dsp_flow.state.deal_type = DealType.PREFERRED_DEAL - dsp_flow.state.impressions = 1_000_000 - dsp_flow.state.max_cpm = 30.0 - dsp_flow.state.flight_start = "2026-04-01" - dsp_flow.state.flight_end = "2026-04-30" - return dsp_flow +def buyer_flow_with_request(buyer_flow): + """Buyer deal flow with a valid request already set.""" + buyer_flow.state.request = "CTV inventory for sports audiences under $30 CPM" + buyer_flow.state.deal_type = DealType.PREFERRED_DEAL + buyer_flow.state.impressions = 1_000_000 + buyer_flow.state.max_cpm = 30.0 + buyer_flow.state.flight_start = "2026-04-01" + buyer_flow.state.flight_end = "2026-04-30" + return buyer_flow # =========================================================================== @@ -101,9 +101,9 @@ def dsp_flow_with_request(dsp_flow): class TestBuyerDealFlowModels: - """Tests for DSP flow data models.""" + """Tests for buyer deal flow data models.""" - def test_dsp_flow_state_defaults(self): + def test_buyer_flow_state_defaults(self): """BuyerDealFlowState initializes with correct defaults.""" state = BuyerDealFlowState() @@ -115,7 +115,7 @@ def test_dsp_flow_state_defaults(self): assert state.errors == [] assert state.discovered_products == [] - def test_dsp_flow_state_with_values(self): + def test_buyer_flow_state_with_values(self): """BuyerDealFlowState can be created with custom values.""" state = BuyerDealFlowState( request="CTV inventory", @@ -150,7 +150,7 @@ def test_discovered_product_model(self): assert len(product.targeting) == 2 assert product.score == 0.85 - def test_dsp_flow_status_enum(self): + def test_buyer_flow_status_enum(self): """BuyerDealFlowStatus enum has all expected values.""" assert BuyerDealFlowStatus.INITIALIZED.value == "initialized" assert BuyerDealFlowStatus.REQUEST_RECEIVED.value == "request_received" @@ -169,31 +169,31 @@ def test_dsp_flow_status_enum(self): class TestReceiveRequest: """Tests for the request validation entry point.""" - def test_valid_request(self, dsp_flow_with_request): + def test_valid_request(self, buyer_flow_with_request): """Valid request transitions to REQUEST_RECEIVED.""" - result = dsp_flow_with_request.receive_request() + result = buyer_flow_with_request.receive_request() assert result["status"] == "success" - assert result["request"] == dsp_flow_with_request.state.request + assert result["request"] == buyer_flow_with_request.state.request assert result["access_tier"] == AccessTier.AGENCY.value - assert dsp_flow_with_request.state.status == BuyerDealFlowStatus.REQUEST_RECEIVED + assert buyer_flow_with_request.state.status == BuyerDealFlowStatus.REQUEST_RECEIVED - def test_empty_request_fails(self, dsp_flow): + def test_empty_request_fails(self, buyer_flow): """Empty request string fails.""" - dsp_flow.state.request = "" + buyer_flow.state.request = "" - result = dsp_flow.receive_request() + result = buyer_flow.receive_request() assert result["status"] == "failed" - assert dsp_flow.state.status == BuyerDealFlowStatus.FAILED - assert len(dsp_flow.state.errors) > 0 + assert buyer_flow.state.status == BuyerDealFlowStatus.FAILED + assert len(buyer_flow.state.errors) > 0 - def test_buyer_context_stored_in_state(self, dsp_flow_with_request): + def test_buyer_context_stored_in_state(self, buyer_flow_with_request): """Buyer context is serialized and stored in state.""" - dsp_flow_with_request.receive_request() + buyer_flow_with_request.receive_request() - assert dsp_flow_with_request.state.buyer_context is not None - ctx = dsp_flow_with_request.state.buyer_context + assert buyer_flow_with_request.state.buyer_context is not None + ctx = buyer_flow_with_request.state.buyer_context assert ctx["identity"]["agency_id"] == "agency-123" def test_advertiser_tier_access(self, mock_unified_client, advertiser_buyer_context): @@ -223,43 +223,43 @@ def test_public_tier_access(self, mock_unified_client, public_buyer_context): class TestDiscoverInventory: """Tests for the inventory discovery step.""" - def test_skips_on_failed_request(self, dsp_flow): + def test_skips_on_failed_request(self, buyer_flow): """Discovery passes through upstream failure.""" - result = dsp_flow.discover_inventory({"status": "failed", "errors": ["bad"]}) + result = buyer_flow.discover_inventory({"status": "failed", "errors": ["bad"]}) assert result["status"] == "failed" @patch.object(BuyerDealFlow, "__init__", lambda self, **kw: None) - def test_discovery_success(self, dsp_flow_with_request): + def test_discovery_success(self, buyer_flow_with_request): """Successful discovery returns results and updates status.""" - dsp_flow_with_request._discover_tool = MagicMock() - dsp_flow_with_request._discover_tool._run.return_value = "Found 3 CTV products" + buyer_flow_with_request._discover_tool = MagicMock() + buyer_flow_with_request._discover_tool._run.return_value = "Found 3 CTV products" - result = dsp_flow_with_request.discover_inventory({"status": "success"}) + result = buyer_flow_with_request.discover_inventory({"status": "success"}) assert result["status"] == "success" assert "discovery_result" in result - assert dsp_flow_with_request.state.status == BuyerDealFlowStatus.DISCOVERING_INVENTORY + assert buyer_flow_with_request.state.status == BuyerDealFlowStatus.DISCOVERING_INVENTORY - def test_discovery_tool_exception(self, dsp_flow_with_request): + def test_discovery_tool_exception(self, buyer_flow_with_request): """Exception in discovery tool sets FAILED status.""" - dsp_flow_with_request._discover_tool._run = MagicMock( + buyer_flow_with_request._discover_tool._run = MagicMock( side_effect=RuntimeError("Connection refused") ) - result = dsp_flow_with_request.discover_inventory({"status": "success"}) + result = buyer_flow_with_request.discover_inventory({"status": "success"}) assert result["status"] == "failed" - assert dsp_flow_with_request.state.status == BuyerDealFlowStatus.FAILED - assert len(dsp_flow_with_request.state.errors) > 0 + assert buyer_flow_with_request.state.status == BuyerDealFlowStatus.FAILED + assert len(buyer_flow_with_request.state.errors) > 0 - def test_discovery_passes_filters(self, dsp_flow_with_request): + def test_discovery_passes_filters(self, buyer_flow_with_request): """Discovery passes max_cpm and impressions to the tool.""" - dsp_flow_with_request._discover_tool._run = MagicMock(return_value="Results") + buyer_flow_with_request._discover_tool._run = MagicMock(return_value="Results") - dsp_flow_with_request.discover_inventory({"status": "success"}) + buyer_flow_with_request.discover_inventory({"status": "success"}) - call_kwargs = dsp_flow_with_request._discover_tool._run.call_args + call_kwargs = buyer_flow_with_request._discover_tool._run.call_args assert call_kwargs.kwargs.get("max_cpm") == 30.0 or call_kwargs[1].get("max_cpm") == 30.0 @@ -271,39 +271,39 @@ def test_discovery_passes_filters(self, dsp_flow_with_request): class TestExtractProductId: """Tests for product ID extraction from agent text.""" - def test_product_id_colon_format(self, dsp_flow): + def test_product_id_colon_format(self, buyer_flow): """Extracts product_id from 'product_id: xxx' format.""" text = "I recommend product_id: ctv_premium_001 because it matches." - result = dsp_flow._extract_product_id(text) + result = buyer_flow._extract_product_id(text) assert result == "ctv_premium_001" - def test_product_id_json_format(self, dsp_flow): + def test_product_id_json_format(self, buyer_flow): """Extracts product_id from JSON-like format.""" text = '{"product_id": "prod_abc_123", "name": "Test"}' - result = dsp_flow._extract_product_id(text) + result = buyer_flow._extract_product_id(text) assert result == "prod_abc_123" - def test_product_id_title_case(self, dsp_flow): + def test_product_id_title_case(self, buyer_flow): """Extracts from 'Product ID: xxx' format.""" text = "The best option is Product ID: stream-hd-42" - result = dsp_flow._extract_product_id(text) + result = buyer_flow._extract_product_id(text) assert result == "stream-hd-42" - def test_no_product_id_returns_none(self, dsp_flow): + def test_no_product_id_returns_none(self, buyer_flow): """Returns None when no product ID pattern is found.""" text = "I could not find any matching products." - result = dsp_flow._extract_product_id(text) + result = buyer_flow._extract_product_id(text) assert result is None - def test_empty_string(self, dsp_flow): + def test_empty_string(self, buyer_flow): """Empty string returns None.""" - result = dsp_flow._extract_product_id("") + result = buyer_flow._extract_product_id("") assert result is None - def test_camel_case_format(self, dsp_flow): + def test_camel_case_format(self, buyer_flow): """Extracts from 'productId: xxx' format.""" text = "The productId: test_prod_99 is the best." - result = dsp_flow._extract_product_id(text) + result = buyer_flow._extract_product_id(text) assert result == "test_prod_99" @@ -315,9 +315,9 @@ def test_camel_case_format(self, dsp_flow): class TestEvaluateAndSelect: """Tests for product evaluation and selection step.""" - def test_skips_on_failed_discovery(self, dsp_flow_with_request): + def test_skips_on_failed_discovery(self, buyer_flow_with_request): """Evaluation passes through upstream failure.""" - result = dsp_flow_with_request.evaluate_and_select( + result = buyer_flow_with_request.evaluate_and_select( {"status": "failed", "error": "no results"} ) assert result["status"] == "failed" @@ -326,7 +326,7 @@ def test_skips_on_failed_discovery(self, dsp_flow_with_request): @patch("ad_buyer.flows.buyer_deal_flow.Crew") @patch("ad_buyer.flows.buyer_deal_flow.create_buyer_deal_specialist_agent") def test_successful_selection( - self, mock_agent, mock_crew_cls, mock_task, dsp_flow_with_request + self, mock_agent, mock_crew_cls, mock_task, buyer_flow_with_request ): """Successful selection stores product_id and pricing.""" # Mock the crew @@ -335,29 +335,29 @@ def test_successful_selection( mock_crew_cls.return_value = mock_crew_instance # Mock pricing tool - dsp_flow_with_request._pricing_tool._run = MagicMock(return_value="$18/CPM") + buyer_flow_with_request._pricing_tool._run = MagicMock(return_value="$18/CPM") - result = dsp_flow_with_request.evaluate_and_select( + result = buyer_flow_with_request.evaluate_and_select( {"status": "success", "discovery_result": "3 products found"} ) assert result["status"] == "success" assert result["selected_product_id"] == "ctv_001" - assert dsp_flow_with_request.state.selected_product_id == "ctv_001" - assert dsp_flow_with_request.state.pricing_details is not None + assert buyer_flow_with_request.state.selected_product_id == "ctv_001" + assert buyer_flow_with_request.state.pricing_details is not None @patch("ad_buyer.flows.buyer_deal_flow.Task") @patch("ad_buyer.flows.buyer_deal_flow.Crew") @patch("ad_buyer.flows.buyer_deal_flow.create_buyer_deal_specialist_agent") def test_no_product_id_extracted( - self, mock_agent, mock_crew_cls, mock_task, dsp_flow_with_request + self, mock_agent, mock_crew_cls, mock_task, buyer_flow_with_request ): """When agent response has no product ID, selected_product_id is None.""" mock_crew_instance = MagicMock() mock_crew_instance.kickoff.return_value = "No suitable products found." mock_crew_cls.return_value = mock_crew_instance - result = dsp_flow_with_request.evaluate_and_select( + result = buyer_flow_with_request.evaluate_and_select( {"status": "success", "discovery_result": "some results"} ) @@ -368,17 +368,17 @@ def test_no_product_id_extracted( @patch("ad_buyer.flows.buyer_deal_flow.Crew") @patch("ad_buyer.flows.buyer_deal_flow.create_buyer_deal_specialist_agent") def test_evaluation_exception( - self, mock_agent, mock_crew_cls, mock_task, dsp_flow_with_request + self, mock_agent, mock_crew_cls, mock_task, buyer_flow_with_request ): """Exception during evaluation sets FAILED status.""" mock_crew_cls.side_effect = RuntimeError("Crew creation failed") - result = dsp_flow_with_request.evaluate_and_select( + result = buyer_flow_with_request.evaluate_and_select( {"status": "success", "discovery_result": "results"} ) assert result["status"] == "failed" - assert dsp_flow_with_request.state.status == BuyerDealFlowStatus.FAILED + assert buyer_flow_with_request.state.status == BuyerDealFlowStatus.FAILED # =========================================================================== @@ -389,58 +389,58 @@ def test_evaluation_exception( class TestRequestDealId: """Tests for the Deal ID request step.""" - def test_skips_on_failed_selection(self, dsp_flow_with_request): + def test_skips_on_failed_selection(self, buyer_flow_with_request): """Deal request passes through upstream failure.""" - result = dsp_flow_with_request.request_deal_id( + result = buyer_flow_with_request.request_deal_id( {"status": "failed", "error": "nothing selected"} ) assert result["status"] == "failed" - def test_fails_with_no_product_selected(self, dsp_flow_with_request): + def test_fails_with_no_product_selected(self, buyer_flow_with_request): """Fails when no product has been selected.""" - dsp_flow_with_request.state.selected_product_id = None + buyer_flow_with_request.state.selected_product_id = None - result = dsp_flow_with_request.request_deal_id({"status": "success"}) + result = buyer_flow_with_request.request_deal_id({"status": "success"}) assert result["status"] == "failed" assert "No product selected" in result["error"] - assert dsp_flow_with_request.state.status == BuyerDealFlowStatus.FAILED + assert buyer_flow_with_request.state.status == BuyerDealFlowStatus.FAILED - def test_successful_deal_creation(self, dsp_flow_with_request): + def test_successful_deal_creation(self, buyer_flow_with_request): """Successful deal request stores deal response and sets DEAL_CREATED.""" - dsp_flow_with_request.state.selected_product_id = "ctv_001" - dsp_flow_with_request._deal_tool._run = MagicMock( + buyer_flow_with_request.state.selected_product_id = "ctv_001" + buyer_flow_with_request._deal_tool._run = MagicMock( return_value="Deal DEAL-ABC123 created for ctv_001" ) - result = dsp_flow_with_request.request_deal_id({"status": "success"}) + result = buyer_flow_with_request.request_deal_id({"status": "success"}) assert result["status"] == "success" - assert dsp_flow_with_request.state.status == BuyerDealFlowStatus.DEAL_CREATED - assert dsp_flow_with_request.state.deal_response is not None - assert "raw" in dsp_flow_with_request.state.deal_response + assert buyer_flow_with_request.state.status == BuyerDealFlowStatus.DEAL_CREATED + assert buyer_flow_with_request.state.deal_response is not None + assert "raw" in buyer_flow_with_request.state.deal_response - def test_deal_tool_exception(self, dsp_flow_with_request): + def test_deal_tool_exception(self, buyer_flow_with_request): """Exception in deal tool sets FAILED status.""" - dsp_flow_with_request.state.selected_product_id = "ctv_001" - dsp_flow_with_request._deal_tool._run = MagicMock( + buyer_flow_with_request.state.selected_product_id = "ctv_001" + buyer_flow_with_request._deal_tool._run = MagicMock( side_effect=RuntimeError("Server unavailable") ) - result = dsp_flow_with_request.request_deal_id({"status": "success"}) + result = buyer_flow_with_request.request_deal_id({"status": "success"}) assert result["status"] == "failed" - assert dsp_flow_with_request.state.status == BuyerDealFlowStatus.FAILED - assert len(dsp_flow_with_request.state.errors) > 0 + assert buyer_flow_with_request.state.status == BuyerDealFlowStatus.FAILED + assert len(buyer_flow_with_request.state.errors) > 0 - def test_deal_passes_flight_dates(self, dsp_flow_with_request): + def test_deal_passes_flight_dates(self, buyer_flow_with_request): """Deal request passes flight dates to the tool.""" - dsp_flow_with_request.state.selected_product_id = "ctv_001" - dsp_flow_with_request._deal_tool._run = MagicMock(return_value="Deal created") + buyer_flow_with_request.state.selected_product_id = "ctv_001" + buyer_flow_with_request._deal_tool._run = MagicMock(return_value="Deal created") - dsp_flow_with_request.request_deal_id({"status": "success"}) + buyer_flow_with_request.request_deal_id({"status": "success"}) - call_kwargs = dsp_flow_with_request._deal_tool._run.call_args + call_kwargs = buyer_flow_with_request._deal_tool._run.call_args kwargs = call_kwargs.kwargs if call_kwargs.kwargs else {} # The tool should receive flight dates if kwargs: @@ -454,11 +454,11 @@ def test_deal_passes_flight_dates(self, dsp_flow_with_request): class TestBuyerDealFlowGetStatus: - """Tests for the DSP flow status method.""" + """Tests for the buyer deal flow status method.""" - def test_initial_status(self, dsp_flow): + def test_initial_status(self, buyer_flow): """Fresh flow reports INITIALIZED status.""" - status = dsp_flow.get_status() + status = buyer_flow.get_status() assert status["status"] == "initialized" assert status["request"] == "" @@ -467,39 +467,39 @@ def test_initial_status(self, dsp_flow): assert status["deal_response"] is None assert status["errors"] == [] - def test_status_with_request(self, dsp_flow_with_request): + def test_status_with_request(self, buyer_flow_with_request): """Status reflects configured request.""" - dsp_flow_with_request.state.status = BuyerDealFlowStatus.REQUEST_RECEIVED - status = dsp_flow_with_request.get_status() + buyer_flow_with_request.state.status = BuyerDealFlowStatus.REQUEST_RECEIVED + status = buyer_flow_with_request.get_status() assert status["status"] == "request_received" assert "CTV" in status["request"] assert status["access_tier"] == "agency" - def test_status_after_deal_creation(self, dsp_flow_with_request): + def test_status_after_deal_creation(self, buyer_flow_with_request): """Status reflects deal creation.""" - dsp_flow_with_request.state.status = BuyerDealFlowStatus.DEAL_CREATED - dsp_flow_with_request.state.selected_product_id = "ctv_001" - dsp_flow_with_request.state.deal_response = {"raw": "DEAL-ABC123"} + buyer_flow_with_request.state.status = BuyerDealFlowStatus.DEAL_CREATED + buyer_flow_with_request.state.selected_product_id = "ctv_001" + buyer_flow_with_request.state.deal_response = {"raw": "DEAL-ABC123"} - status = dsp_flow_with_request.get_status() + status = buyer_flow_with_request.get_status() assert status["status"] == "deal_created" assert status["selected_product_id"] == "ctv_001" assert status["deal_response"] is not None - def test_status_includes_updated_at(self, dsp_flow): + def test_status_includes_updated_at(self, buyer_flow): """Status includes ISO-formatted updated_at.""" - status = dsp_flow.get_status() + status = buyer_flow.get_status() assert "updated_at" in status # Should be a valid ISO datetime string datetime.fromisoformat(status["updated_at"]) - def test_status_with_errors(self, dsp_flow): + def test_status_with_errors(self, buyer_flow): """Status reflects accumulated errors.""" - dsp_flow.state.errors = ["Error A", "Error B"] - status = dsp_flow.get_status() + buyer_flow.state.errors = ["Error A", "Error B"] + status = buyer_flow.get_status() assert len(status["errors"]) == 2 @@ -510,7 +510,7 @@ def test_status_with_errors(self, dsp_flow): class TestBuyerDealFlowInitialization: - """Tests for DSP flow construction.""" + """Tests for buyer deal flow construction.""" def test_flow_creates_tools(self, mock_unified_client, agency_buyer_context): """Flow creates discover, pricing, and deal tools on init.""" @@ -522,13 +522,13 @@ def test_flow_creates_tools(self, mock_unified_client, agency_buyer_context): assert flow._client is mock_unified_client assert flow._buyer_context is agency_buyer_context - def test_flow_state_is_initialized(self, dsp_flow): + def test_flow_state_is_initialized(self, buyer_flow): """Flow state is initialized with defaults.""" # crewai wraps the state model in a StateWithId subclass, # so we check attributes rather than exact type. - assert dsp_flow.state.status == BuyerDealFlowStatus.INITIALIZED - assert dsp_flow.state.request == "" - assert dsp_flow.state.errors == [] + assert buyer_flow.state.status == BuyerDealFlowStatus.INITIALIZED + assert buyer_flow.state.request == "" + assert buyer_flow.state.errors == [] # =========================================================================== @@ -539,58 +539,58 @@ def test_flow_state_is_initialized(self, dsp_flow): class TestBuyerDealFlowStateTransitions: """Tests verifying status transitions through the flow.""" - def test_request_received_transition(self, dsp_flow_with_request): + def test_request_received_transition(self, buyer_flow_with_request): """receive_request transitions INITIALIZED -> REQUEST_RECEIVED.""" - assert dsp_flow_with_request.state.status == BuyerDealFlowStatus.INITIALIZED - dsp_flow_with_request.receive_request() - assert dsp_flow_with_request.state.status == BuyerDealFlowStatus.REQUEST_RECEIVED + assert buyer_flow_with_request.state.status == BuyerDealFlowStatus.INITIALIZED + buyer_flow_with_request.receive_request() + assert buyer_flow_with_request.state.status == BuyerDealFlowStatus.REQUEST_RECEIVED - def test_discovering_inventory_transition(self, dsp_flow_with_request): + def test_discovering_inventory_transition(self, buyer_flow_with_request): """discover_inventory transitions to DISCOVERING_INVENTORY.""" - dsp_flow_with_request._discover_tool._run = MagicMock(return_value="found stuff") + buyer_flow_with_request._discover_tool._run = MagicMock(return_value="found stuff") - dsp_flow_with_request.discover_inventory({"status": "success"}) + buyer_flow_with_request.discover_inventory({"status": "success"}) - assert dsp_flow_with_request.state.status == BuyerDealFlowStatus.DISCOVERING_INVENTORY + assert buyer_flow_with_request.state.status == BuyerDealFlowStatus.DISCOVERING_INVENTORY @patch("ad_buyer.flows.buyer_deal_flow.Task") @patch("ad_buyer.flows.buyer_deal_flow.Crew") @patch("ad_buyer.flows.buyer_deal_flow.create_buyer_deal_specialist_agent") def test_evaluating_pricing_transition( - self, mock_agent, mock_crew_cls, mock_task, dsp_flow_with_request + self, mock_agent, mock_crew_cls, mock_task, buyer_flow_with_request ): """evaluate_and_select transitions to EVALUATING_PRICING.""" mock_crew_instance = MagicMock() mock_crew_instance.kickoff.return_value = "product_id: x" mock_crew_cls.return_value = mock_crew_instance - dsp_flow_with_request._pricing_tool._run = MagicMock(return_value="$20") + buyer_flow_with_request._pricing_tool._run = MagicMock(return_value="$20") - dsp_flow_with_request.evaluate_and_select( + buyer_flow_with_request.evaluate_and_select( {"status": "success", "discovery_result": "results"} ) - assert dsp_flow_with_request.state.status == BuyerDealFlowStatus.EVALUATING_PRICING + assert buyer_flow_with_request.state.status == BuyerDealFlowStatus.EVALUATING_PRICING - def test_deal_created_transition(self, dsp_flow_with_request): + def test_deal_created_transition(self, buyer_flow_with_request): """request_deal_id transitions to DEAL_CREATED on success.""" - dsp_flow_with_request.state.selected_product_id = "prod_1" - dsp_flow_with_request._deal_tool._run = MagicMock(return_value="Deal created") + buyer_flow_with_request.state.selected_product_id = "prod_1" + buyer_flow_with_request._deal_tool._run = MagicMock(return_value="Deal created") - dsp_flow_with_request.request_deal_id({"status": "success"}) + buyer_flow_with_request.request_deal_id({"status": "success"}) - assert dsp_flow_with_request.state.status == BuyerDealFlowStatus.DEAL_CREATED + assert buyer_flow_with_request.state.status == BuyerDealFlowStatus.DEAL_CREATED - def test_failed_transition_on_empty_request(self, dsp_flow): + def test_failed_transition_on_empty_request(self, buyer_flow): """Empty request transitions to FAILED.""" - dsp_flow.state.request = "" - dsp_flow.receive_request() - assert dsp_flow.state.status == BuyerDealFlowStatus.FAILED + buyer_flow.state.request = "" + buyer_flow.receive_request() + assert buyer_flow.state.status == BuyerDealFlowStatus.FAILED - def test_failed_transition_on_discovery_error(self, dsp_flow_with_request): + def test_failed_transition_on_discovery_error(self, buyer_flow_with_request): """Discovery failure transitions to FAILED.""" - dsp_flow_with_request._discover_tool._run = MagicMock(side_effect=RuntimeError("error")) - dsp_flow_with_request.discover_inventory({"status": "success"}) - assert dsp_flow_with_request.state.status == BuyerDealFlowStatus.FAILED + buyer_flow_with_request._discover_tool._run = MagicMock(side_effect=RuntimeError("error")) + buyer_flow_with_request.discover_inventory({"status": "success"}) + assert buyer_flow_with_request.state.status == BuyerDealFlowStatus.FAILED # =========================================================================== diff --git a/tests/unit/test_dsp_tools.py b/tests/unit/test_buyer_deal_tools.py similarity index 98% rename from tests/unit/test_dsp_tools.py rename to tests/unit/test_buyer_deal_tools.py index e23acce..adbfd15 100644 --- a/tests/unit/test_dsp_tools.py +++ b/tests/unit/test_buyer_deal_tools.py @@ -1,7 +1,7 @@ # Author: Green Mountain Systems AI Inc. # Donated to IAB Tech Lab -"""Tests for DSP tools.""" +"""Tests for buyer deal tools.""" from unittest.mock import AsyncMock, MagicMock @@ -328,7 +328,7 @@ async def test_request_deal_creates_deal_id(self, mock_client, agency_context): @pytest.mark.asyncio async def test_request_deal_includes_activation_instructions(self, mock_client, agency_context): - """Test that deal includes DSP activation instructions.""" + """Test that deal includes buyer deal activation instructions.""" mock_client.get_product.return_value = MagicMock( success=True, data={"id": "prod_1", "name": "Test", "basePrice": 20.00}, @@ -424,7 +424,7 @@ async def test_request_deal_invalid_type(self, mock_client, agency_context): class TestToolIntegration: - """Integration tests for DSP tools working together.""" + """Integration tests for buyer deal tools working together.""" @pytest.mark.asyncio async def test_discover_then_price_then_deal(self, mock_client, advertiser_context): From 613c48865e7f48746904457301495d38df2683ae Mon Sep 17 00:00:00 2001 From: ATC964 <82515918+atc964@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:44:08 -0400 Subject: [PATCH 4/4] Rename DSP agent/flow/tool references to Buyer Deal naming Renames documentation references from DSP-centric naming to buyer-centric naming to better reflect the agent's role: - DSP Deal Flow -> Buyer Deal Flow - DSPDealFlow -> BuyerDealFlow - DSP Specialist -> Buyer Deal Specialist - DSP Tools -> Buyer Deal Tools - tools/dsp/ -> tools/buyer_deals/ - dsp_deal_flow -> buyer_deal_flow - dsp_agent -> buyer_deal_specialist_agent Legitimate DSP platform references (DSP activation, DSP seat IDs, DSP platforms like TTD/DV360/Amazon DSP) are preserved. bead: ar-j7on Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 2 +- docs/architecture/agent-hierarchy.md | 12 +++--- .../{dsp-deal-flow.md => buyer-deal-flow.md} | 42 +++++++++---------- docs/architecture/deal-library.md | 2 +- docs/architecture/overview.md | 6 +-- docs/architecture/tools.md | 28 ++++++------- docs/event-bus/overview.md | 14 +++---- docs/guides/deal-booking.md | 16 +++---- docs/guides/overview.md | 2 +- docs/state-machines/order-lifecycle.md | 2 +- mkdocs.yml | 2 +- 11 files changed, 64 insertions(+), 64 deletions(-) rename docs/architecture/{dsp-deal-flow.md => buyer-deal-flow.md} (83%) diff --git a/README.md b/README.md index f492aa3..90b9d61 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Campaign Brief ──→ Portfolio Manager (Opus) ┌──────────────┼──────────────┐ ▼ ▼ ▼ Channel Specialists (Sonnet) - Branding │ CTV │ Mobile │ Performance │ DSP + Branding │ CTV │ Mobile │ Performance │ Deals │ │ │ ▼ ▼ ▼ Functional Agents (Sonnet) diff --git a/docs/architecture/agent-hierarchy.md b/docs/architecture/agent-hierarchy.md index d73903b..98d7ba3 100644 --- a/docs/architecture/agent-hierarchy.md +++ b/docs/architecture/agent-hierarchy.md @@ -17,7 +17,7 @@ graph TB CTV["CTV Specialist
(streaming)"] MOB["Mobile App Specialist
(app install)"] PERF["Performance Specialist
(remarketing)"] - DSP["DSP Specialist
(deal discovery)"] + DSP["Buyer Deal Specialist
(deal discovery)"] end subgraph Level3["Level 3 — Functional Agents"] @@ -158,9 +158,9 @@ Maximizes conversions and ROAS through lower-funnel tactics. | Creative | Dynamic creative optimization, A/B testing | | Tracking | Pixel implementation, cross-device attribution | -### DSP Deal Discovery Specialist +### Buyer Deal Discovery Specialist -**File:** `src/ad_buyer/agents/level2/dsp_agent.py` +**File:** `src/ad_buyer/agents/level2/buyer_deal_specialist_agent.py` Discovers inventory and obtains Deal IDs for activation in traditional DSP platforms. @@ -171,8 +171,8 @@ Discovers inventory and obtains Deal IDs for activation in traditional DSP platf | Pricing | Identity-based tiered pricing, volume discounts | | Negotiation | Price negotiation for agency/advertiser tiers | -!!! tip "DSP vs. other specialists" - The DSP Specialist works alongside the channel specialists, not in place of them. Channel specialists decide *what* inventory to buy; the DSP Specialist handles the mechanics of obtaining Deal IDs for programmatic activation. See [DSP Deal Flow](dsp-deal-flow.md) for the full workflow. +!!! tip "Buyer Deal Specialist vs. other specialists" + The Buyer Deal Specialist works alongside the channel specialists, not in place of them. Channel specialists decide *what* inventory to buy; the Buyer Deal Specialist handles the mechanics of obtaining Deal IDs for programmatic activation. See [Buyer Deal Flow](buyer-deal-flow.md) for the full workflow. --- @@ -379,6 +379,6 @@ sequenceDiagram - [Architecture Overview](overview.md) --- Full system architecture - [Tools Reference](tools.md) --- All CrewAI tools available to agents -- [DSP Deal Flow](dsp-deal-flow.md) --- DSP-specific deal discovery workflow +- [Buyer Deal Flow](buyer-deal-flow.md) --- Buyer deal discovery workflow - [Booking Flow](booking-flow.md) --- Detailed booking sequence - [Configuration](../guides/configuration.md) --- LLM and agent settings diff --git a/docs/architecture/dsp-deal-flow.md b/docs/architecture/buyer-deal-flow.md similarity index 83% rename from docs/architecture/dsp-deal-flow.md rename to docs/architecture/buyer-deal-flow.md index 8f8925f..9ae1a6e 100644 --- a/docs/architecture/dsp-deal-flow.md +++ b/docs/architecture/buyer-deal-flow.md @@ -1,14 +1,14 @@ -# DSP Deal Flow +# Buyer Deal Flow -The `DSPDealFlow` is a CrewAI event-driven flow that discovers seller inventory, evaluates products, and obtains Deal IDs for activation in traditional DSP platforms. It is distinct from the [`DealBookingFlow`](booking-flow.md), which handles the full campaign booking lifecycle through [OpenDirect](https://iabtechlab.com/standards/opendirect/). +The `BuyerDealFlow` is a CrewAI event-driven flow that discovers seller inventory, evaluates products, and obtains Deal IDs for activation in traditional DSP platforms. It is distinct from the [`DealBookingFlow`](booking-flow.md), which handles the full campaign booking lifecycle through [OpenDirect](https://iabtechlab.com/standards/opendirect/). -**Key file:** `src/ad_buyer/flows/dsp_deal_flow.py` +**Key file:** `src/ad_buyer/flows/buyer_deal_flow.py` ## When to Use This Flow | Scenario | Use | |----------|-----| -| Obtain a Deal ID for a DSP platform (TTD, DV360, etc.) | DSP Deal Flow | +| Obtain a Deal ID for a DSP platform (TTD, DV360, etc.) | Buyer Deal Flow | | Book a full campaign with orders and line items | [DealBookingFlow](booking-flow.md) | | Negotiate pricing with a seller | [NegotiationClient](../guides/negotiation.md) | @@ -19,9 +19,9 @@ The `DSPDealFlow` is a CrewAI event-driven flow that discovers seller inventory, ```mermaid sequenceDiagram participant Caller - participant Flow as DSPDealFlow - participant Tools as DSP Tools - participant Crew as DSP Agent Crew + participant Flow as BuyerDealFlow + participant Tools as Buyer Deal Tools + participant Crew as Buyer Deal Specialist Crew participant Seller as Seller Agent Caller->>Flow: kickoff(request, deal_type, ...) @@ -33,7 +33,7 @@ sequenceDiagram Seller-->>Tools: Available inventory with tiered pricing Note over Flow: discover_inventory() - Flow->>Crew: DSP Agent evaluates products + Flow->>Crew: Buyer Deal Specialist evaluates products Crew->>Tools: GetPricingTool._run(product_id) Tools->>Seller: Get detailed pricing Seller-->>Tools: Tier-specific pricing breakdown @@ -80,14 +80,14 @@ def discover_inventory(self, request_result) -> dict[str, Any]: ### Step 3: Evaluate and Select -A CrewAI crew with the DSP Agent intelligently selects the best product. +A CrewAI crew with the Buyer Deal Specialist intelligently selects the best product. ```python @listen(discover_inventory) def evaluate_and_select(self, discovery_result) -> dict[str, Any]: ``` -- Creates a `Crew` with the DSP Agent and the `DiscoverInventoryTool` + `GetPricingTool` +- Creates a `Crew` with the Buyer Deal Specialist and the `DiscoverInventoryTool` + `GetPricingTool` - The agent analyzes discovery results against the request criteria (deal type, max CPM, volume) - Extracts the selected `product_id` from the agent's response - Fetches detailed pricing for the selected product via `GetPricingTool` @@ -111,10 +111,10 @@ def request_deal_id(self, selection_result) -> dict[str, Any]: ## Flow State -The `DSPFlowState` Pydantic model tracks the entire lifecycle: +The `BuyerDealFlowState` Pydantic model tracks the entire lifecycle: ```python -class DSPFlowState(BaseModel): +class BuyerDealFlowState(BaseModel): # Input request: str # Natural language deal request deal_type: DealType # PG, PD, or PA @@ -137,7 +137,7 @@ class DSPFlowState(BaseModel): deal_response: Optional[dict] # Created deal info # Execution tracking - status: DSPFlowStatus # Current flow status + status: BuyerDealFlowStatus # Current flow status errors: list[str] # Error messages ``` @@ -148,7 +148,7 @@ class DSPFlowState(BaseModel): | `INITIALIZED` | Flow created, not yet started | | `REQUEST_RECEIVED` | Request validated, buyer context stored | | `DISCOVERING_INVENTORY` | Querying sellers for available inventory | -| `EVALUATING_PRICING` | DSP Agent selecting product and getting pricing | +| `EVALUATING_PRICING` | Buyer Deal Specialist selecting product and getting pricing | | `REQUESTING_DEAL` | Deal ID request sent to seller | | `DEAL_CREATED` | Deal ID obtained successfully | | `FAILED` | An error occurred at any step | @@ -160,7 +160,7 @@ class DSPFlowState(BaseModel): ### Direct Flow Instantiation ```python -from ad_buyer.flows.dsp_deal_flow import DSPDealFlow +from ad_buyer.flows.buyer_deal_flow import BuyerDealFlow from ad_buyer.clients.unified_client import UnifiedClient from ad_buyer.models.buyer_identity import ( BuyerContext, BuyerIdentity, DealType, @@ -182,7 +182,7 @@ store = DealStore("sqlite:///./ad_buyer.db") store.connect() # Create and configure flow -flow = DSPDealFlow( +flow = BuyerDealFlow( client=client, buyer_context=buyer_context, store=store, @@ -205,13 +205,13 @@ print(f"Deal: {status['deal_response']}") ### Convenience Function -The `run_dsp_deal_flow()` async function handles client setup and flow configuration in a single call: +The `run_buyer_deal_flow()` async function handles client setup and flow configuration in a single call: ```python -from ad_buyer.flows.dsp_deal_flow import run_dsp_deal_flow +from ad_buyer.flows.buyer_deal_flow import run_buyer_deal_flow from ad_buyer.models.buyer_identity import BuyerIdentity, DealType -result = await run_dsp_deal_flow( +result = await run_buyer_deal_flow( request="Premium CTV sports inventory for Q3", buyer_identity=BuyerIdentity( seat_id="ttd-seat-123", @@ -279,8 +279,8 @@ if status["status"] == "failed": ## Related -- [Agent Hierarchy](agent-hierarchy.md) --- DSP Specialist role in the hierarchy -- [Tools Reference](tools.md) --- DSP tools used by this flow +- [Agent Hierarchy](agent-hierarchy.md) --- Buyer Deal Specialist role in the hierarchy +- [Tools Reference](tools.md) --- Buyer deal tools used by this flow - [Deals API](../api/deals.md) --- REST API for quote-then-book deals - [Booking Flow](booking-flow.md) --- Alternative flow for full campaign booking - [Identity Strategy](../guides/identity.md) --- How buyer identity affects pricing tiers diff --git a/docs/architecture/deal-library.md b/docs/architecture/deal-library.md index 918a39d..e9758c8 100644 --- a/docs/architecture/deal-library.md +++ b/docs/architecture/deal-library.md @@ -84,7 +84,7 @@ Instantiation: ### Supply Path Templates -Supply path templates (`supply_path_templates` table) codify SPO (supply path optimization) routing preferences. They record scoring weights, preferred SSPs, blocked SSPs, and maximum reseller hop counts. Supply path templates are used by the DSP deal flow to evaluate and rank inventory sources. +Supply path templates (`supply_path_templates` table) codify SPO (supply path optimization) routing preferences. They record scoring weights, preferred SSPs, blocked SSPs, and maximum reseller hop counts. Supply path templates are used by the buyer deal flow to evaluate and rank inventory sources. --- diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 8231a63..58961f8 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -74,7 +74,7 @@ The Architecture section covers these topics: |-------|---------------| | **[Agent Hierarchy](agent-hierarchy.md)** | Three-level agent structure: portfolio manager, channel specialists, and tool-level agents | | **[Booking Flow](booking-flow.md)** | Detailed sequence diagram of the DealBookingFlow --- the campaign-level orchestration | -| **[DSP Deal Flow](dsp-deal-flow.md)** | Single-deal flow for direct DSP integration without multi-channel orchestration | +| **[Buyer Deal Flow](buyer-deal-flow.md)** | Single-deal flow for direct DSP integration without multi-channel orchestration | | **[Order State Machine](../state-machines/order-lifecycle.md)** | 12 deal states and 9 campaign states with guard conditions and audit trail | | **[Event Bus](../event-bus/overview.md)** | 13 event types providing structured observability across all flows | | **[Deal Store](deal-store.md)** | SQLite persistence for deals, events, and session state | @@ -86,7 +86,7 @@ The Architecture section covers these topics: The buyer has two distinct flow entry points, depending on the use case: - **DealBookingFlow** (campaign flow) --- Starts from a campaign brief. The portfolio manager allocates budget across channels, channel specialists research inventory in parallel, recommendations are built and approved, then deals are booked. This is the multi-channel, orchestrated path. -- **DSPDealFlow** (deal flow) --- Starts from a single deal request. Discovers inventory, evaluates pricing, and books one deal directly. This is the lightweight, single-deal path used for DSP integration. +- **BuyerDealFlow** (deal flow) --- Starts from a single deal request. Discovers inventory, evaluates pricing, and books one deal directly. This is the lightweight, single-deal path used for DSP integration. Both flows share the same deal state machine, event bus, and DealStore persistence --- they differ in scope and orchestration, not in how individual deals are managed. @@ -149,7 +149,7 @@ See also: [Seller Agent Architecture](https://iabtechlab.github.io/seller-agent/ ## Related - [Booking Flow](booking-flow.md) --- detailed sequence diagram of the campaign-level DealBookingFlow -- [DSP Deal Flow](dsp-deal-flow.md) --- single-deal flow for direct DSP integration +- [Buyer Deal Flow](buyer-deal-flow.md) --- single-deal flow for direct DSP integration - [Order State Machine](../state-machines/order-lifecycle.md) --- deal and campaign lifecycle enforcement - [Event Bus](../event-bus/overview.md) --- structured observability across all flows - [Models](models.md) --- data model reference diff --git a/docs/architecture/tools.md b/docs/architecture/tools.md index 33cb093..4419288 100644 --- a/docs/architecture/tools.md +++ b/docs/architecture/tools.md @@ -2,7 +2,7 @@ The buyer agent provides 13 tools that agents use to interact with sellers, plan audiences, execute bookings, and retrieve performance data. All tools extend CrewAI's `BaseTool` and expose both synchronous (`_run`) and asynchronous (`_arun`) interfaces. -Tools are how AI agents take action in the real world. While agents reason about strategy and make decisions, tools are the concrete operations they invoke --- searching a seller's product catalog, checking inventory availability, or confirming a booking. Each tool wraps a single, well-defined API call so that agents do not need to construct raw HTTP requests. Tools fall into four categories: **Audience** (understanding who to target), **Research** (finding what to buy), **DSP** (obtaining Deal IDs for programmatic activation), and **Execution** (placing and confirming orders). +Tools are how AI agents take action in the real world. While agents reason about strategy and make decisions, tools are the concrete operations they invoke --- searching a seller's product catalog, checking inventory availability, or confirming a booking. Each tool wraps a single, well-defined API call so that agents do not need to construct raw HTTP requests. Tools fall into four categories: **Audience** (understanding who to target), **Research** (finding what to buy), **Buyer Deals** (obtaining Deal IDs for programmatic activation), and **Execution** (placing and confirming orders). ### Typical Tool Flow @@ -11,7 +11,7 @@ In a standard booking, tools are used in this order: ``` 1. AudienceDiscovery / AudienceMatching / CoverageEstimation (plan audience) 2. ProductSearch / AvailsCheck (find inventory) -3. DiscoverInventory / GetPricing / RequestDeal (DSP deal path, if applicable) +3. DiscoverInventory / GetPricing / RequestDeal (buyer deal path, if applicable) 4. CreateOrder → CreateLine → ReserveLine → BookLine (execute booking) 5. GetStats (monitor delivery, coming soon) ``` @@ -26,7 +26,7 @@ graph LR CE[CoverageEstimationTool] end - subgraph DSP["DSP Tools"] + subgraph DSP["Buyer Deal Tools"] DI[DiscoverInventoryTool] GP[GetPricingTool] RD[RequestDealTool] @@ -52,7 +52,7 @@ graph LR | Category | Tools | Used By | |----------|-------|---------| | **Audience** | AudienceDiscovery, AudienceMatching, CoverageEstimation | Audience Planner, Research Agent | -| **DSP** | DiscoverInventory, GetPricing, RequestDeal | DSP Specialist | +| **Buyer Deals** | DiscoverInventory, GetPricing, RequestDeal | Buyer Deal Specialist | | **Execution** | CreateOrder, CreateLine, ReserveLine, BookLine | Execution Agent | | **Research** | ProductSearch, AvailsCheck | Research Agent | | **Reporting** | GetStats | Reporting Agent (Coming Soon) | @@ -183,11 +183,11 @@ result = tool._run( --- -## DSP Tools +## Buyer Deal Tools -DSP tools enable programmatic deal workflows where Deal IDs are obtained from sellers and activated in traditional DSP platforms. All three tools require a `UnifiedClient` and `BuyerContext` at initialization. +Buyer deal tools enable programmatic deal workflows where Deal IDs are obtained from sellers and activated in traditional DSP platforms. All three tools require a `UnifiedClient` and `BuyerContext` at initialization. -**Package:** `src/ad_buyer/tools/dsp/` +**Package:** `src/ad_buyer/tools/buyer_deals/` ### DiscoverInventoryTool @@ -207,7 +207,7 @@ Queries sellers for available inventory with identity-based access and tiered pr **Returns:** List of products with tiered pricing based on buyer identity, availability, and targeting capabilities. ```python -from ad_buyer.tools.dsp import DiscoverInventoryTool +from ad_buyer.tools.buyer_deals import DiscoverInventoryTool from ad_buyer.clients.unified_client import UnifiedClient from ad_buyer.models.buyer_identity import BuyerContext, BuyerIdentity @@ -250,7 +250,7 @@ Retrieves tier-specific pricing for a product, including volume discounts and de **Returns:** Full pricing breakdown with base CPM, tier discount, volume discount, final CPM, cost projection, and available deal types. ```python -from ad_buyer.tools.dsp import GetPricingTool +from ad_buyer.tools.buyer_deals import GetPricingTool tool = GetPricingTool(client=client, buyer_context=buyer_context) result = tool._run( @@ -289,7 +289,7 @@ Requests a Deal ID from a seller for programmatic activation in a DSP platform. **Returns:** Deal ID, pricing details, and platform-specific activation instructions for The Trade Desk, DV360, Amazon DSP, Xandr, and Yahoo DSP. ```python -from ad_buyer.tools.dsp import RequestDealTool +from ad_buyer.tools.buyer_deals import RequestDealTool tool = RequestDealTool(client=client, buyer_context=buyer_context) result = tool._run( @@ -508,12 +508,12 @@ audience_tools = [ CoverageEstimationTool(), ] -# DSP tools -- require UnifiedClient + BuyerContext -from ad_buyer.tools.dsp import ( +# Buyer deal tools -- require UnifiedClient + BuyerContext +from ad_buyer.tools.buyer_deals import ( DiscoverInventoryTool, GetPricingTool, RequestDealTool, ) -dsp_tools = [ +buyer_deal_tools = [ DiscoverInventoryTool(client=unified_client, buyer_context=buyer_ctx), GetPricingTool(client=unified_client, buyer_context=buyer_ctx), RequestDealTool(client=unified_client, buyer_context=buyer_ctx), @@ -525,6 +525,6 @@ dsp_tools = [ ## Related - [Agent Hierarchy](agent-hierarchy.md) --- Which agents use which tools -- [DSP Deal Flow](dsp-deal-flow.md) --- How DSP tools work together in a flow +- [Buyer Deal Flow](buyer-deal-flow.md) --- How buyer deal tools work together in a flow - [Booking Flow](booking-flow.md) --- How execution tools drive the booking lifecycle - [Configuration](../guides/configuration.md) --- Tool-related settings diff --git a/docs/event-bus/overview.md b/docs/event-bus/overview.md index 3dc6904..0870931 100644 --- a/docs/event-bus/overview.md +++ b/docs/event-bus/overview.md @@ -23,7 +23,7 @@ The diagram below shows how events flow from the buyer's deal and campaign flows ```mermaid graph TB subgraph Flows - DSP["DSPDealFlow"] + DSP["BuyerDealFlow"] DBF["DealBookingFlow"] end @@ -69,7 +69,7 @@ Every event is a Pydantic `Event` instance with the following fields: | `event_type` | `EventType` | *(required)* | Enum value identifying what happened | | `timestamp` | `datetime` | `datetime.now(timezone.utc)` | When the event was created | | `flow_id` | `str` | `""` | ID of the flow instance that produced this event | -| `flow_type` | `str` | `""` | Type of flow (e.g., `"dsp_deal"`, `"deal_booking"`) | +| `flow_type` | `str` | `""` | Type of flow (e.g., `"buyer_deal"`, `"deal_booking"`) | | `deal_id` | `str` | `""` | Associated deal ID, if applicable | | `session_id` | `str` | `""` | Associated session ID, if applicable | | `payload` | `dict[str, Any]` | `{}` | Event-specific data (see per-type examples below) | @@ -218,7 +218,7 @@ from ad_buyer.events.models import EventType event = await emit_event( EventType.DEAL_BOOKED, flow_id="flow-abc-123", - flow_type="dsp_deal", + flow_type="buyer_deal", deal_id="deal-456", payload={ "product_id": "prod-ctv-sports-001", @@ -358,7 +358,7 @@ List events with optional filters. "event_type": "deal.booked", "timestamp": "2026-03-11T14:30:00Z", "flow_id": "flow-abc-123", - "flow_type": "dsp_deal", + "flow_type": "buyer_deal", "deal_id": "deal-456", "session_id": "", "payload": {"product_id": "prod-001", "final_cpm": 14.50}, @@ -390,11 +390,11 @@ Retrieve a single event by ID. ## Event Flow Diagram -This sequence diagram shows how events flow through the system during a typical DSP deal: +This sequence diagram shows how events flow through the system during a typical buyer deal: ```mermaid sequenceDiagram - participant Flow as DSPDealFlow + participant Flow as BuyerDealFlow participant Helper as emit_event_sync() participant Bus as InMemoryEventBus participant Sub as Subscribers @@ -437,7 +437,7 @@ def negotiate_price(self): emit_event_sync( EventType.NEGOTIATION_ROUND, flow_id=self.state.flow_id, - flow_type="dsp_deal", + flow_type="buyer_deal", deal_id=self.state.deal_id, payload={ "round": round_number, diff --git a/docs/guides/deal-booking.md b/docs/guides/deal-booking.md index 2614f06..71311ef 100644 --- a/docs/guides/deal-booking.md +++ b/docs/guides/deal-booking.md @@ -353,21 +353,21 @@ curl -X POST http://localhost:8001/bookings/{job_id}/approve-all --- -## Using DSPDealFlow (Single-Deal, Direct Mode) +## Using BuyerDealFlow (Single-Deal, Direct Mode) -`DSPDealFlow` is for when you know roughly what you want and just need a Deal ID. It discovers inventory, picks the best match, and requests a deal --- all in one shot. +`BuyerDealFlow` is for when you know roughly what you want and just need a Deal ID. It discovers inventory, picks the best match, and requests a deal --- all in one shot. Use this for **single-deal, targeted acquisition** rather than full-campaign planning. ```python -from ad_buyer.flows.dsp_deal_flow import run_dsp_deal_flow +from ad_buyer.flows.buyer_deal_flow import run_buyer_deal_flow from ad_buyer.models.buyer_identity import BuyerIdentity, DealType from ad_buyer.storage.deal_store import DealStore store = DealStore("sqlite:///./deals.db") store.connect() -result = await run_dsp_deal_flow( +result = await run_buyer_deal_flow( request="Premium sports video inventory for Q3 awareness campaign", buyer_identity=BuyerIdentity( seat_id="ttd-seat-123", @@ -389,16 +389,16 @@ print(f"Deal: {result['status']['deal_response']}") 1. **Receive request** --- Validates the natural-language request and buyer context. 2. **Discover inventory** --- Searches the seller's catalog for matches. -3. **Evaluate and select** --- Uses a DSP agent (CrewAI) to pick the best product. +3. **Evaluate and select** --- Uses a Buyer Deal Specialist (CrewAI) to pick the best product. 4. **Request Deal ID** --- Calls the seller's deal endpoint for the selected product. **Key difference from DealBookingFlow:** -| | DealBookingFlow | DSPDealFlow | +| | DealBookingFlow | BuyerDealFlow | |---|---|---| | **Scope** | Full campaign, multiple channels | Single deal | | **Input** | Campaign brief with budget | Natural language request | -| **Selection** | Multi-channel specialists | Single DSP agent | +| **Selection** | Multi-channel specialists | Single Buyer Deal Specialist | | **Approval** | Human checkpoint | Automatic | | **Output** | Multiple booked lines | One Deal ID | @@ -482,7 +482,7 @@ The `DealsClient` and flow classes treat persistence as **best-effort**. If the **Book promptly.** Quotes expire. If you're running a multi-step workflow (browse, quote, negotiate, book), don't let the quote sit too long. The seller may have reserved inventory capacity that times out. -**Use `DealBookingFlow` for campaigns, `DSPDealFlow` for spot buys.** If you have a campaign brief with a budget to allocate across channels, use `DealBookingFlow`. If you just need a single Deal ID for a specific product, use `DSPDealFlow` or the `DealsClient` directly. +**Use `DealBookingFlow` for campaigns, `BuyerDealFlow` for spot buys.** If you have a campaign brief with a budget to allocate across channels, use `DealBookingFlow`. If you just need a single Deal ID for a specific product, use `BuyerDealFlow` or the `DealsClient` directly. **Always use async context managers.** The `DealsClient` holds an HTTP connection pool. Use `async with` to ensure clean shutdown: diff --git a/docs/guides/overview.md b/docs/guides/overview.md index fdad313..a772f9f 100644 --- a/docs/guides/overview.md +++ b/docs/guides/overview.md @@ -36,7 +36,7 @@ flowchart LR The buyer supports two distinct entry points depending on your use case: - **DealBookingFlow** (campaign flow) --- The full multi-channel path. Starts from a campaign brief, allocates budget, researches across channels in parallel, builds recommendations, and books multiple deals after approval. This is the primary workflow for campaign managers. -- **DSPDealFlow** (deal flow) --- The lightweight single-deal path. Discovers inventory and books one deal directly. Designed for programmatic DSP integration where the campaign planning happens externally. +- **BuyerDealFlow** (deal flow) --- The lightweight single-deal path. Discovers inventory and books one deal directly. Designed for programmatic DSP integration where the campaign planning happens externally. Both flows share the same [deal state machine](../state-machines/order-lifecycle.md), [event bus](../event-bus/overview.md), and DealStore persistence. For architectural details, see [Architecture Overview](../architecture/overview.md). diff --git a/docs/state-machines/order-lifecycle.md b/docs/state-machines/order-lifecycle.md index ce33f0f..47e7096 100644 --- a/docs/state-machines/order-lifecycle.md +++ b/docs/state-machines/order-lifecycle.md @@ -425,7 +425,7 @@ Two helper functions bridge the old enum values to the new `BuyerDealStatus` and ### `from_dsp_flow_status(value) -> BuyerDealStatus` -Maps legacy `DSPFlowStatus` values used in `DSPDealFlow`: +Maps legacy `BuyerDealFlowStatus` values used in `BuyerDealFlow`: | Legacy Value | Maps To | |-------------|---------| diff --git a/mkdocs.yml b/mkdocs.yml index e447d48..e34225b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -72,7 +72,7 @@ nav: - Overview: architecture/overview.md - Agent Hierarchy: architecture/agent-hierarchy.md - Booking Flow: architecture/booking-flow.md - - DSP Deal Flow: architecture/dsp-deal-flow.md + - Buyer Deal Flow: architecture/buyer-deal-flow.md - Deal Store: architecture/deal-store.md - Models: architecture/models.md - Tools Reference: architecture/tools.md