2525from spoon_ai .chat import ChatBot # noqa: E402
2626from spoon_ai .schema import Message , Role # noqa: E402
2727from 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
2830from spoon_ai .tools .base import BaseTool , ToolResult # noqa: E402
2931from spoon_ai .tools .tool_manager import ToolManager # noqa: E402
3032from 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+
128155def 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:
198225class 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 \n Available tools:\n { tool_list } "
268+ )
269+ self .next_step_prompt = NEXT_STEP_PROMPT_TEMPLATE .format (tool_list = tool_list )
233270
234271
235272async 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