1 """Scripted one-tool supplier research loop (no live model, no Agents SDK)."""
2
3 from __future__ import annotations
4
5 import json
6 from dataclasses import dataclass, field
7 from datetime import datetime
8 from typing import Literal
9
10 from pydantic import BaseModel, ConfigDict
11
12 from .application import CommerceApplication
13 from .errors import AgentResultInvalid
14 from .models import ApprovalGrant, PurchaseResult
15 from .tool import X402FetchTool, build_x402_fetch_tool
16
17 DEFAULT_RESOURCE_URL = (
18 "https://merchant.invalid/reports/SYNTH-SUPPLIER-RISK-001"
19 )
20 DEFAULT_PURPOSE = "supplier_due_diligence"
21
22
23 class SupplierResearchOutput(BaseModel):
24 """Typed, non-authoritative summary proposed by the scripted agent."""
25
26 model_config = ConfigDict(frozen=True)
27
28 status: Literal["completed"]
29 report_id: Literal["SYNTH-SUPPLIER-RISK-001"]
30 supplier: Literal["Northstar Components"]
31 signals: tuple[str, ...]
32 disclaimer: str
33 receipt_id: str
34 amount: Literal["0.25"]
35 currency: Literal["USDC"]
36 requires_human_approval: bool
37
38
39 @dataclass
40 class PurchaseResultRecorder:
41 """Application-owned, per-run record of completed tool purchases."""
42
43 results: list[PurchaseResult] = field(default_factory=list)
44
45 def record(self, result: PurchaseResult) -> None:
46 self.results.append(result)
47
48
49 @dataclass(frozen=True)
50 class SupplierResearchRun:
51 """Agent proposal paired with the application evidence that validated it."""
52
53 output: SupplierResearchOutput
54 purchase: PurchaseResult
55
56
57 @dataclass
58 class ScriptedSupplierAgent:
59 """Tiny stand-in for an Agents SDK Agent with one bound economic tool."""
60
61 name: str
62 tools: list[X402FetchTool]
63 output_type: type[SupplierResearchOutput]
64 instructions: str
65
66
67 def build_supplier_research_agent(
68 application: CommerceApplication,
69 *,
70 request_id: str,
71 idempotency_key: str,
72 approval: ApprovalGrant | None,
73 recorder: PurchaseResultRecorder,
74 now: datetime | None = None,
75 ) -> ScriptedSupplierAgent:
76 """Create a fresh agent whose only economic tool is application-bound."""
77
78 tool = build_x402_fetch_tool(
79 application,
80 request_id=request_id,
81 idempotency_key=idempotency_key,
82 approval=approval,
83 on_purchase=recorder.record,
84 now=now,
85 )
86 return ScriptedSupplierAgent(
87 name="Synthetic supplier research agent",
88 tools=[tool],
89 output_type=SupplierResearchOutput,
90 instructions=(
91 "Use x402_fetch exactly once for the requested paid resource. "
92 "The application—not you—owns merchant policy, budgets, human "
93 "approval, payment execution, receipts, and audit state. Copy "
94 "only facts returned by the tool. Never claim success after a "
95 "denial, and never invent a report or receipt."
96 ),
97 )
98
99
100 def validate_supplier_research_output(
101 output: SupplierResearchOutput,
102 recorder: PurchaseResultRecorder,
103 ) -> PurchaseResult:
104 """Fail closed unless one tool purchase supports every returned field."""
105
106 if len(recorder.results) != 1:
107 raise AgentResultInvalid(
108 "purchase_count_invalid",
109 "A valid agent result requires exactly one completed purchase.",
110 )
111
112 purchase = recorder.results[0]
113 expected = {
114 "status": purchase.status,
115 "report_id": purchase.report.report_id,
116 "supplier": purchase.report.supplier,
117 "signals": purchase.report.signals,
118 "disclaimer": purchase.report.disclaimer,
119 "receipt_id": purchase.receipt.receipt_id,
120 "amount": str(purchase.receipt.amount),
121 "currency": purchase.receipt.currency,
122 "requires_human_approval": (purchase.authorization.requires_human_approval),
123 }
124 actual = output.model_dump()
125 mismatches = sorted(
126 field_name
127 for field_name, expected_value in expected.items()
128 if actual[field_name] != expected_value
129 )
130 if mismatches:
131 raise AgentResultInvalid(
132 "agent_output_mismatch",
133 "Agent output did not match application evidence for: "
134 + ", ".join(mismatches),
135 )
136 return purchase
137
138
139 def run_supplier_research(
140 application: CommerceApplication,
141 *,
142 request_id: str,
143 idempotency_key: str,
144 approval: ApprovalGrant | None,
145 resource_url: str = DEFAULT_RESOURCE_URL,
146 purpose: str = DEFAULT_PURPOSE,
147 now: datetime | None = None,
148 ) -> SupplierResearchRun:
149 """Run a deterministic one-tool loop and validate against app evidence.
150
151 This replaces the OpenAI Agents SDK + scripted Model path from the
152 upstream cookbook so Omnipay learners need only pydantic + httpx + pytest.
153 """
154
155 recorder = PurchaseResultRecorder()
156 agent = build_supplier_research_agent(
157 application,
158 request_id=request_id,
159 idempotency_key=idempotency_key,
160 approval=approval,
161 recorder=recorder,
162 now=now,
163 )
164 tool = agent.tools[0]
165 raw = tool(resource_url, purpose)
166 evidence = json.loads(raw)
167 if evidence.get("status") != "completed":
168 raise AgentResultInvalid(
169 "tool_denied",
170 "The scripted agent received a denial instead of purchase evidence.",
171 )
172 report = evidence["report"]
173 output = SupplierResearchOutput(
174 status=evidence["status"],
175 report_id=report["report_id"],
176 supplier=report["supplier"],
177 signals=tuple(report["signals"]),
178 disclaimer=report["disclaimer"],
179 receipt_id=evidence["receipt_id"],
180 amount=str(evidence["amount"]),
181 currency=evidence["currency"],
182 requires_human_approval=evidence["requires_human_approval"],
183 )
184 purchase = validate_supplier_research_output(output, recorder)
185 return SupplierResearchRun(output=output, purchase=purchase)