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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .beads/PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,4 @@
| \[x] | buyer-5x7 | Template & Reporting MCP Tools | P2 | buyer-mw9 | 2026-03-25 |

---
*Last updated: 2026-04-04 15:59 UTC — auto-generated by beads*
*Last updated: 2026-04-13 19:33 UTC — auto-generated by beads*
11 changes: 10 additions & 1 deletion .beads/generate_progress.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down
35 changes: 26 additions & 9 deletions .beads/test_generate_progress.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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()

Expand Down Expand Up @@ -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"


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 6 additions & 6 deletions docs/architecture/agent-hierarchy.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ graph TB
CTV["CTV Specialist<br/>(streaming)"]
MOB["Mobile App Specialist<br/>(app install)"]
PERF["Performance Specialist<br/>(remarketing)"]
DSP["DSP Specialist<br/>(deal discovery)"]
DSP["Buyer Deal Specialist<br/>(deal discovery)"]
end

subgraph Level3["Level 3 — Functional Agents"]
Expand Down Expand Up @@ -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.

Expand All @@ -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.

---

Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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) |

Expand All @@ -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, ...)
Expand All @@ -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
Expand Down Expand Up @@ -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`
Expand All @@ -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
Expand All @@ -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
```

Expand All @@ -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 |
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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",
Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion docs/architecture/deal-library.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand Down
6 changes: 3 additions & 3 deletions docs/architecture/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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.

Expand Down Expand Up @@ -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
Expand Down
Loading
Loading