Skip to content

Commit ce36dbf

Browse files
authored
Merge pull request #194 from veithly/fix/x420-demo
Enhance X402ReactAgent with default URL handling and prompt management
2 parents 4f45016 + 7a27d7c commit ce36dbf

File tree

1 file changed

+58
-10
lines changed

1 file changed

+58
-10
lines changed

examples/x402_agent_demo.py

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
from spoon_ai.chat import ChatBot # noqa: E402
2626
from spoon_ai.schema import Message, Role # noqa: E402
2727
from spoon_ai.payments import X402PaymentReceipt, X402PaymentService # noqa: E402
28+
from pydantic import Field # noqa: E402
29+
from spoon_ai.prompts.spoon_react import NEXT_STEP_PROMPT_TEMPLATE # noqa: E402
2830
from spoon_ai.tools.base import BaseTool, ToolResult # noqa: E402
2931
from spoon_ai.tools.tool_manager import ToolManager # noqa: E402
3032
from spoon_ai.tools.x402_payment import X402PaywalledRequestTool # noqa: E402
@@ -42,7 +44,11 @@ class HttpProbeTool(BaseTool):
4244
parameters: Dict[str, Any] = {
4345
"type": "object",
4446
"properties": {
45-
"url": {"type": "string", "description": "Resource URL to fetch"},
47+
"url": {
48+
"type": "string",
49+
"description": "Resource URL to fetch",
50+
"default": PAYWALLED_URL,
51+
},
4652
"timeout": {
4753
"type": "number",
4854
"description": "Timeout in seconds for the request",
@@ -60,12 +66,14 @@ class HttpProbeTool(BaseTool):
6066

6167
async def execute(
6268
self,
63-
url: str,
69+
url: str = PAYWALLED_URL,
6470
timeout: float = 20.0,
6571
headers: Optional[Dict[str, str]] = None,
6672
) -> ToolResult:
73+
# Fall back to the demo URL so the agent never fails on a missing argument.
74+
target = url or PAYWALLED_URL
6775
async with httpx.AsyncClient(timeout=timeout) as client:
68-
response = await client.get(url, headers=headers)
76+
response = await client.get(target, headers=headers)
6977
try:
7078
body = response.json()
7179
except json.JSONDecodeError:
@@ -125,6 +133,25 @@ def decode_receipt(header_value: str) -> Dict[str, Any]:
125133
return payload.model_dump()
126134

127135

136+
def extract_music_url(content: str) -> Optional[str]:
137+
"""Extract SoundCloud (or other) music link from HTML/text."""
138+
if not content:
139+
return None
140+
cleaned = html.unescape(content)
141+
# Look for SoundCloud embed first
142+
match = re.search(r"https?://w\.soundcloud\.com/player/\?[^\s\"'<>]+", cleaned)
143+
if match:
144+
return match.group(0)
145+
match = re.search(r"https?://soundcloud\.com/[^\s\"'<>]+", cleaned)
146+
if match:
147+
return match.group(0)
148+
# Fallback: first http(s) link
149+
match = re.search(r"https?://[^\s\"'<>]+", cleaned)
150+
if match:
151+
return match.group(0)
152+
return None
153+
154+
128155
def parse_tool_output(raw: str) -> Any:
129156
segment: Optional[str] = None
130157
if "Output:" in raw:
@@ -198,6 +225,8 @@ def print_conversation(messages: list[Message]) -> None:
198225
class X402ReactAgent(SpoonReactAI):
199226
name: str = "x402_react_agent"
200227
description: str = "ReAct agent that pays x402 invoices to reach protected resources"
228+
target_url: str = PAYWALLED_URL
229+
service: X402PaymentService = Field(default_factory=X402PaymentService, exclude=True)
201230

202231
template_system_prompt: str = (
203232
"You are an autonomous ReAct agent with tool access."
@@ -213,23 +242,31 @@ class X402ReactAgent(SpoonReactAI):
213242
)
214243

215244
def __init__(self, service: X402PaymentService, url: str, **kwargs: Any) -> None:
216-
super().__init__(**kwargs)
217-
self.service = service
218-
self.target_url = url
245+
super().__init__(service=service, target_url=url, **kwargs)
219246
self.http_probe_tool = HttpProbeTool()
220247
self.payment_tool: Optional[X402PaywalledRequestTool] = None
221-
self.system_prompt = self.template_system_prompt.format(
222-
target_url=self.target_url,
223-
amount=str(PAYMENT_USDC),
224-
)
225248
self.max_steps = 6
226249
self.x402_enabled = False # prevent base class from auto-attaching duplicate tools
227250
self.available_tools = ToolManager([])
251+
self._refresh_prompts()
228252

229253
async def initialize(self) -> None:
230254
ensure_wallet_configuration(self.service)
231255
self.payment_tool = X402PaywalledRequestTool(service=self.service)
232256
self.available_tools = ToolManager([self.http_probe_tool, self.payment_tool])
257+
self._refresh_prompts()
258+
259+
def _refresh_prompts(self) -> None:
260+
"""Keep the customised x402 playbook while still listing current tools."""
261+
tool_list = self._build_tool_list()
262+
self.system_prompt = (
263+
self.template_system_prompt.format(
264+
target_url=self.target_url,
265+
amount=str(PAYMENT_USDC),
266+
)
267+
+ f"\n\nAvailable tools:\n{tool_list}"
268+
)
269+
self.next_step_prompt = NEXT_STEP_PROMPT_TEMPLATE.format(tool_list=tool_list)
233270

234271

235272
async def main() -> None:
@@ -276,13 +313,21 @@ async def main() -> None:
276313
http_probe_result = extract_tool_payload(messages, "http_probe")
277314
payment_result = extract_tool_payload(messages, "x402_paywalled_request")
278315
assistant_summary = extract_last_assistant(messages)
316+
music_url: Optional[str] = None
279317

280318
if not assistant_summary and payment_result:
281319
body = payment_result.get("body")
282320
if isinstance(body, dict):
283321
assistant_summary = summarise_text(json.dumps(body, ensure_ascii=False))
284322
elif isinstance(body, str):
285323
assistant_summary = summarise_text(body)
324+
music_url = extract_music_url(body)
325+
326+
# Attempt music URL extraction even if assistant summary already exists.
327+
if music_url is None and payment_result:
328+
body = payment_result.get("body")
329+
if isinstance(body, str):
330+
music_url = extract_music_url(body)
286331

287332
if http_probe_result:
288333
preview_body = http_probe_result.get("body")
@@ -310,6 +355,9 @@ async def main() -> None:
310355
if assistant_summary:
311356
rprint("\n[bold green]Agent Final Summary[/]")
312357
rprint(assistant_summary)
358+
if music_url:
359+
rprint("\n[bold green]Music URL[/]")
360+
rprint(music_url)
313361

314362
if payment_header:
315363
rprint("\n[bold blue]Signed X-PAYMENT Header[/]")

0 commit comments

Comments
 (0)