|
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) |