webagents 0.2.2__py3-none-any.whl → 0.2.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. webagents/__init__.py +9 -0
  2. webagents/agents/core/base_agent.py +865 -69
  3. webagents/agents/core/handoffs.py +14 -6
  4. webagents/agents/skills/base.py +33 -2
  5. webagents/agents/skills/core/llm/litellm/skill.py +906 -27
  6. webagents/agents/skills/core/memory/vector_memory/skill.py +8 -16
  7. webagents/agents/skills/ecosystem/openai/__init__.py +6 -0
  8. webagents/agents/skills/ecosystem/openai/skill.py +867 -0
  9. webagents/agents/skills/ecosystem/replicate/README.md +440 -0
  10. webagents/agents/skills/ecosystem/replicate/__init__.py +10 -0
  11. webagents/agents/skills/ecosystem/replicate/skill.py +517 -0
  12. webagents/agents/skills/examples/__init__.py +6 -0
  13. webagents/agents/skills/examples/music_player.py +329 -0
  14. webagents/agents/skills/robutler/handoff/__init__.py +6 -0
  15. webagents/agents/skills/robutler/handoff/skill.py +191 -0
  16. webagents/agents/skills/robutler/nli/skill.py +180 -24
  17. webagents/agents/skills/robutler/payments/exceptions.py +27 -7
  18. webagents/agents/skills/robutler/payments/skill.py +64 -14
  19. webagents/agents/skills/robutler/storage/files/skill.py +2 -2
  20. webagents/agents/tools/decorators.py +243 -47
  21. webagents/agents/widgets/__init__.py +6 -0
  22. webagents/agents/widgets/renderer.py +150 -0
  23. webagents/server/core/app.py +130 -15
  24. webagents/server/core/models.py +1 -1
  25. webagents/utils/logging.py +13 -1
  26. {webagents-0.2.2.dist-info → webagents-0.2.3.dist-info}/METADATA +8 -25
  27. {webagents-0.2.2.dist-info → webagents-0.2.3.dist-info}/RECORD +30 -20
  28. webagents/agents/skills/ecosystem/openai_agents/__init__.py +0 -0
  29. {webagents-0.2.2.dist-info → webagents-0.2.3.dist-info}/WHEEL +0 -0
  30. {webagents-0.2.2.dist-info → webagents-0.2.3.dist-info}/entry_points.txt +0 -0
  31. {webagents-0.2.2.dist-info → webagents-0.2.3.dist-info}/licenses/LICENSE +0 -0
@@ -232,14 +232,13 @@ class NLISkill(Skill):
232
232
  @prompt(priority=20, scope="all")
233
233
  def nli_general_prompt(self, context: Any = None) -> str:
234
234
  base_url = self.agent_base_url.rstrip('/')
235
- return f"""You are part of a network of AI agents working for their owners. Each agent has their own name and address.
236
-
237
- CRITICAL: When you need to communicate with another agent:
238
- 1. Convert @agentname to the full URL: {base_url}/agents/agentname
239
- 2. Use the nli_tool to send your message to that URL
240
- 3. DO NOT just store or retrieve agent names - actually communicate with them
241
-
242
- """
235
+ agent_name = self.agent.name if self.agent and hasattr(self.agent, 'name') else None
236
+
237
+ prompt = f"Agents: Convert @name to {base_url}/agents/name, use nli_tool. DON'T call yourself.\n"
238
+ if agent_name:
239
+ prompt += f"You are @{agent_name}. NEVER call {base_url}/agents/{agent_name} via NLI!\n"
240
+
241
+ return prompt
243
242
 
244
243
  @tool(description="Communicate with other WebAgents agents via natural language", scope="all")
245
244
  async def nli_tool(self,
@@ -270,6 +269,15 @@ CRITICAL: When you need to communicate with another agent:
270
269
  """
271
270
  start_time = datetime.utcnow()
272
271
 
272
+ # CRITICAL: Prevent self-calling via NLI
273
+ if self.agent and hasattr(self.agent, 'name'):
274
+ agent_name = self.agent.name
275
+ # Check if the URL contains the agent's own name
276
+ if f"/agents/{agent_name}" in agent_url or agent_url.endswith(f"/{agent_name}"):
277
+ error_msg = f"❌ ERROR: Cannot use nli_tool to call yourself (@{agent_name})! You should execute your own tasks directly instead of delegating to yourself via NLI."
278
+ self.logger.error(error_msg)
279
+ return error_msg
280
+
273
281
  # Validate and normalize parameters
274
282
  if authorized_amount is None:
275
283
  authorized_amount = self.default_authorization
@@ -295,7 +303,7 @@ CRITICAL: When you need to communicate with another agent:
295
303
  "content": message
296
304
  }
297
305
  ],
298
- "stream": False,
306
+ "stream": True, # Enable streaming to work around LiteLLM bug (non-streaming drops image data)
299
307
  "temperature": 0.7,
300
308
  "max_tokens": 2048
301
309
  }
@@ -323,36 +331,45 @@ CRITICAL: When you need to communicate with another agent:
323
331
  # CRITICAL: Forward payment token from current context to enable agent-to-agent billing
324
332
  payment_token = None
325
333
 
334
+ self.logger.debug(f"🔐 Token lookup: ctx={ctx is not None}")
335
+ if ctx:
336
+ self.logger.debug(f"🔐 Token lookup: ctx attrs={list(vars(ctx).keys())[:10]}")
337
+
326
338
  # Method 1: Check if payment_token is directly available in context
327
339
  if ctx and hasattr(ctx, 'payment_token') and ctx.payment_token:
328
340
  payment_token = ctx.payment_token
329
- self.logger.debug(f"🔐 Found payment token in context.payment_token")
341
+ self.logger.debug(f"🔐 Found payment token in context.payment_token: {payment_token[:20]}...")
330
342
 
331
343
  # Method 2: Extract from request headers (most common case)
332
344
  elif ctx and hasattr(ctx, 'request') and ctx.request:
333
345
  request_headers = getattr(ctx.request, 'headers', {})
346
+ self.logger.debug(f"🔐 Token lookup: request_headers type={type(request_headers)}")
334
347
  if hasattr(request_headers, 'get'):
348
+ self.logger.debug(f"🔐 Token lookup: checking headers for payment token")
335
349
  payment_token = (
336
350
  request_headers.get('X-Payment-Token') or
337
351
  request_headers.get('x-payment-token') or
338
352
  request_headers.get('payment_token')
339
353
  )
340
354
  if payment_token:
341
- self.logger.debug(f"🔐 Found payment token in request headers")
355
+ self.logger.debug(f"🔐 Found payment token in request headers: {payment_token[:20]}...")
356
+ else:
357
+ self.logger.debug(f"🔐 No payment token in request headers")
342
358
 
343
359
  # Method 3: Check custom_data for payment context (fallback)
344
360
  elif ctx and hasattr(ctx, 'custom_data') and ctx.custom_data:
361
+ self.logger.debug(f"🔐 Token lookup: checking custom_data")
345
362
  payment_context = ctx.custom_data.get('payment_context')
346
363
  if payment_context and hasattr(payment_context, 'payment_token'):
347
364
  payment_token = payment_context.payment_token
348
- self.logger.debug(f"🔐 Found payment token in custom_data.payment_context")
365
+ self.logger.debug(f"🔐 Found payment token in custom_data.payment_context: {payment_token[:20]}...")
349
366
 
350
367
  # Forward the payment token if found
351
368
  if payment_token:
352
369
  headers["X-Payment-Token"] = payment_token
353
- self.logger.debug(f"🔐 Forwarding payment token for agent-to-agent communication: {payment_token[:20]}...")
370
+ self.logger.info(f"🔐 Forwarding payment token for agent-to-agent communication: {payment_token[:20]}...")
354
371
  else:
355
- self.logger.debug(f"🔐 No payment token found to forward - target agent may require payment")
372
+ self.logger.warning(f"🔐 No payment token found to forward - target agent may require payment")
356
373
  except Exception:
357
374
  acting_user_id = None
358
375
 
@@ -437,17 +454,31 @@ CRITICAL: When you need to communicate with another agent:
437
454
  duration_ms = (datetime.utcnow() - start_time).total_seconds() * 1000
438
455
 
439
456
  if response.status_code == 200:
440
- response_data = response.json()
457
+ # Handle streaming response (SSE format)
458
+ agent_response = ""
459
+ async for line in response.aiter_lines():
460
+ if not line or line.strip() == "":
461
+ continue
462
+ if line.startswith("data: "):
463
+ data_str = line[6:] # Remove "data: " prefix
464
+ if data_str == "[DONE]":
465
+ break
466
+ try:
467
+ chunk_data = json.loads(data_str)
468
+ if 'choices' in chunk_data and len(chunk_data['choices']) > 0:
469
+ choice = chunk_data['choices'][0]
470
+ # Check delta (streaming) or message (non-streaming)
471
+ for msg_key in ['delta', 'message']:
472
+ if msg_key in choice:
473
+ msg = choice[msg_key]
474
+ if 'content' in msg and msg['content']:
475
+ agent_response += msg['content']
476
+ except json.JSONDecodeError:
477
+ pass # Skip malformed chunks
441
478
 
442
- # Extract message content from OpenAI-compatible response
443
- if 'choices' in response_data and len(response_data['choices']) > 0:
444
- choice = response_data['choices'][0]
445
- if 'message' in choice and 'content' in choice['message']:
446
- agent_response = choice['message']['content']
447
- else:
448
- agent_response = str(response_data)
449
- else:
450
- agent_response = str(response_data)
479
+ if not agent_response:
480
+ # Fallback: maybe it's actually not streaming despite request?
481
+ agent_response = str(response_data) if 'response_data' in locals() else "No response"
451
482
 
452
483
  # Track successful communication
453
484
  communication = NLICommunication(
@@ -547,6 +578,131 @@ CRITICAL: When you need to communicate with another agent:
547
578
 
548
579
  return f"❌ Communication error: {error_msg}"
549
580
 
581
+ async def stream_message(
582
+ self,
583
+ agent_url: str,
584
+ messages: List[Dict[str, Any]],
585
+ tools: Optional[List[Dict[str, Any]]] = None,
586
+ authorized_amount: float = None,
587
+ timeout: float = None
588
+ ):
589
+ """Stream response from remote agent via NLI
590
+
591
+ Used by AgentHandoffSkill for streaming remote agent responses.
592
+
593
+ Args:
594
+ agent_url: Full URL of target agent
595
+ messages: Conversation messages to send
596
+ tools: Tools to pass to remote agent
597
+ authorized_amount: Maximum cost authorization in USD
598
+ timeout: Request timeout in seconds
599
+
600
+ Yields:
601
+ OpenAI-compatible streaming chunks from remote agent
602
+ """
603
+ # Validate parameters
604
+ if authorized_amount is None:
605
+ authorized_amount = self.default_authorization
606
+
607
+ if authorized_amount > self.max_authorization:
608
+ raise ValueError(f"Authorized amount ${authorized_amount:.2f} exceeds maximum ${self.max_authorization:.2f}")
609
+
610
+ if timeout is None:
611
+ timeout = self.default_timeout
612
+
613
+ if not HTTPX_AVAILABLE:
614
+ raise ValueError("HTTP client not available - install httpx")
615
+
616
+ # Prepare streaming request payload
617
+ payload = {
618
+ "model": self.agent.name if self.agent else "unknown",
619
+ "messages": messages,
620
+ "stream": True,
621
+ "temperature": 0.7,
622
+ "max_tokens": 4096
623
+ }
624
+
625
+ if tools:
626
+ payload["tools"] = tools
627
+
628
+ # Prepare headers
629
+ headers = {
630
+ "Content-Type": "application/json",
631
+ "User-Agent": f"WebAgents-NLI/{self.agent.name if self.agent else 'unknown'}",
632
+ "X-Authorization-Amount": str(authorized_amount),
633
+ "X-Origin-Agent": self.agent.name if self.agent else "unknown",
634
+ }
635
+
636
+ # Include Authorization if available
637
+ bearer = os.getenv('WEBAGENTS_API_KEY') or os.getenv('SERVICE_TOKEN')
638
+ if bearer:
639
+ headers["Authorization"] = f"Bearer {bearer}"
640
+ headers["X-API-Key"] = bearer
641
+
642
+ # Forward payment token if available
643
+ try:
644
+ from webagents.server.context.context_vars import get_context as _gc
645
+ ctx = _gc()
646
+ payment_token = None
647
+
648
+ if ctx and hasattr(ctx, 'payment_token') and ctx.payment_token:
649
+ payment_token = ctx.payment_token
650
+ elif ctx and hasattr(ctx, 'request') and ctx.request:
651
+ request_headers = getattr(ctx.request, 'headers', {})
652
+ if hasattr(request_headers, 'get'):
653
+ payment_token = (
654
+ request_headers.get('X-Payment-Token') or
655
+ request_headers.get('x-payment-token')
656
+ )
657
+
658
+ if payment_token:
659
+ headers["X-Payment-Token"] = payment_token
660
+ self.logger.info(f"🔐 Forwarding payment token for streaming handoff")
661
+ except Exception:
662
+ pass
663
+
664
+ # Stream from remote agent
665
+ self.logger.info(f"🌊 Starting streaming handoff to: {agent_url}")
666
+
667
+ try:
668
+ async with httpx.AsyncClient(timeout=timeout) as client:
669
+ async with client.stream(
670
+ "POST",
671
+ f"{agent_url}/chat/completions",
672
+ json=payload,
673
+ headers=headers
674
+ ) as response:
675
+ response.raise_for_status()
676
+
677
+ # Parse SSE stream
678
+ async for line in response.aiter_lines():
679
+ if not line or line.startswith(':'):
680
+ continue
681
+
682
+ if line.startswith('data: '):
683
+ data = line[6:] # Remove 'data: ' prefix
684
+
685
+ if data == '[DONE]':
686
+ self.logger.debug("🌊 Stream completed: [DONE]")
687
+ break
688
+
689
+ try:
690
+ chunk = json.loads(data)
691
+ yield chunk
692
+ except json.JSONDecodeError as e:
693
+ self.logger.warning(f"Invalid JSON chunk: {data[:100]}")
694
+ continue
695
+
696
+ except httpx.HTTPStatusError as e:
697
+ self.logger.error(f"Remote agent HTTP error: {e.response.status_code}")
698
+ raise
699
+ except httpx.TimeoutException:
700
+ self.logger.error(f"Remote agent timeout after {timeout}s")
701
+ raise
702
+ except Exception as e:
703
+ self.logger.error(f"Remote agent streaming error: {e}")
704
+ raise
705
+
550
706
  # @tool(description="List known agent endpoints and their statistics", scope="owner")
551
707
  async def list_known_agents(self, context=None) -> str:
552
708
  """
@@ -103,12 +103,13 @@ class PaymentTokenInvalidError(PaymentError):
103
103
 
104
104
 
105
105
  class InsufficientBalanceError(PaymentError):
106
- """Raised when payment token balance is insufficient"""
106
+ """Raised when payment token or account balance is insufficient"""
107
107
 
108
108
  def __init__(self,
109
109
  current_balance: float,
110
110
  required_balance: float,
111
- token_prefix: Optional[str] = None):
111
+ token_prefix: Optional[str] = None,
112
+ is_token_balance: bool = True):
112
113
  context = {
113
114
  'current_balance': current_balance,
114
115
  'required_balance': required_balance,
@@ -116,12 +117,21 @@ class InsufficientBalanceError(PaymentError):
116
117
  }
117
118
  if token_prefix:
118
119
  context['token_prefix'] = token_prefix
120
+
121
+ # Distinguish between token balance (can retry with new token)
122
+ # and account balance (user needs to add credits)
123
+ if is_token_balance:
124
+ error_code = "INSUFFICIENT_TOKEN_BALANCE"
125
+ user_message = f"Payment token has insufficient balance (${current_balance:.2f} < ${required_balance:.2f}). Requesting fresh token..."
126
+ else:
127
+ error_code = "INSUFFICIENT_ACCOUNT_BALANCE"
128
+ user_message = f"Your account has insufficient credits. You need ${required_balance:.2f} but only have ${current_balance:.2f}. Please add more credits."
119
129
 
120
130
  super().__init__(
121
131
  message=f"Insufficient balance: ${current_balance:.2f} < ${required_balance:.2f} required",
122
- error_code="INSUFFICIENT_BALANCE",
132
+ error_code=error_code,
123
133
  context=context,
124
- user_message=f"Insufficient credits. You have ${current_balance:.2f} but need ${required_balance:.2f}. Please add more credits to your account."
134
+ user_message=user_message
125
135
  )
126
136
 
127
137
 
@@ -213,12 +223,22 @@ def create_token_invalid_error(token_prefix: Optional[str] = None,
213
223
 
214
224
  def create_insufficient_balance_error(current_balance: float,
215
225
  required_balance: float,
216
- token_prefix: Optional[str] = None) -> InsufficientBalanceError:
217
- """Create an insufficient balance error"""
226
+ token_prefix: Optional[str] = None,
227
+ is_token_balance: bool = True) -> InsufficientBalanceError:
228
+ """Create an insufficient balance error
229
+
230
+ Args:
231
+ current_balance: Current balance available
232
+ required_balance: Minimum balance required
233
+ token_prefix: Optional token identifier prefix
234
+ is_token_balance: True if checking token balance (can retry with fresh token),
235
+ False if checking account balance (user needs to add credits)
236
+ """
218
237
  return InsufficientBalanceError(
219
238
  current_balance=current_balance,
220
239
  required_balance=required_balance,
221
- token_prefix=token_prefix
240
+ token_prefix=token_prefix,
241
+ is_token_balance=is_token_balance
222
242
  )
223
243
 
224
244
 
@@ -253,7 +253,7 @@ class PaymentSkill(Skill):
253
253
  # ===== CONNECTION LIFECYCLE HOOKS =====
254
254
  @prompt(priority=20, scope="all")
255
255
  def payments_general_prompt(self, context: Any = None) -> str:
256
- return "You are part of a network of AI agents working for their owners. You pay each other for services and help the owners make money."
256
+ return "Agent network: Pay each other for services, help owners earn."
257
257
 
258
258
 
259
259
  @hook("on_connection", priority=10)
@@ -419,6 +419,10 @@ class PaymentSkill(Skill):
419
419
  usage_records = getattr(context, 'usage', []) or []
420
420
  llm_cost_usd = 0.0
421
421
  tool_cost_usd = 0.0
422
+
423
+ # Track detailed breakdown for logging
424
+ llm_breakdown = []
425
+ tool_breakdown = []
422
426
 
423
427
  for record in usage_records:
424
428
  if not isinstance(record, dict):
@@ -428,6 +432,7 @@ class PaymentSkill(Skill):
428
432
  model = record.get('model')
429
433
  prompt_tokens = int(record.get('prompt_tokens') or 0)
430
434
  completion_tokens = int(record.get('completion_tokens') or 0)
435
+ self.logger.info(f"💰 PAYMENT: Processing LLM usage - model={model}, tokens={prompt_tokens}+{completion_tokens}")
431
436
  try:
432
437
  if LITELLM_AVAILABLE and cost_per_token and model:
433
438
  p_cost, c_cost = cost_per_token(
@@ -435,23 +440,44 @@ class PaymentSkill(Skill):
435
440
  prompt_tokens=prompt_tokens,
436
441
  completion_tokens=completion_tokens
437
442
  )
438
- llm_cost_usd += float((p_cost or 0.0) + (c_cost or 0.0))
443
+ record_cost = float((p_cost or 0.0) + (c_cost or 0.0))
444
+ llm_cost_usd += record_cost
445
+ self.logger.info(f"💰 PAYMENT: Calculated cost ${record_cost:.6f} for {model}")
446
+ llm_breakdown.append({
447
+ 'model': model,
448
+ 'prompt_tokens': prompt_tokens,
449
+ 'completion_tokens': completion_tokens,
450
+ 'cost_usd': record_cost
451
+ })
439
452
  except Exception as e:
440
- self.logger.debug(f"LLM cost_per_token failed for model {model}: {e}")
453
+ self.logger.warning(f"💰 PAYMENT: cost_per_token failed for model {model}: {e}")
441
454
  continue
442
455
  elif record_type == 'tool':
443
456
  pricing = record.get('pricing') or {}
444
457
  credits = pricing.get('credits')
445
458
  if credits is not None:
446
459
  try:
447
- tool_cost_usd += float(credits)
460
+ record_cost = float(credits)
461
+ tool_cost_usd += record_cost
462
+ tool_breakdown.append({
463
+ 'tool_name': record.get('tool_name', 'unknown'),
464
+ 'reason': pricing.get('reason', 'Tool usage'),
465
+ 'credits': record_cost,
466
+ 'metadata': pricing.get('metadata', {})
467
+ })
448
468
  except Exception:
449
469
  continue
450
470
 
451
471
  # Calculate total to charge using external calculator if provided, else default formula
452
- if callable(self.amount_calculator):
472
+ # Compute default totals
473
+ subtotal = (llm_cost_usd + tool_cost_usd)
474
+ default_total = subtotal * (1.0 + (self.agent_pricing_percent / 100.0))
475
+
476
+ # Track if using custom calculator (revenue-sharing model)
477
+ using_custom_calculator = callable(self.amount_calculator)
478
+
479
+ if using_custom_calculator:
453
480
  try:
454
- # Log inputs and client auth fingerprint used for downstream calls
455
481
  self.logger.debug(
456
482
  f"🧮 Amount calculator input | llm_cost_usd={llm_cost_usd:.6f} "
457
483
  f"tool_cost_usd={tool_cost_usd:.6f} agent_pricing_percent={self.agent_pricing_percent:.2f}% "
@@ -459,20 +485,41 @@ class PaymentSkill(Skill):
459
485
  f"client_key_fp={getattr(self.client, '_api_key_fingerprint', 'na')}"
460
486
  )
461
487
  result = self.amount_calculator(llm_cost_usd, tool_cost_usd, self.agent_pricing_percent)
462
- if inspect.isawaitable(result):
463
- to_charge = float(await result)
464
- else:
465
- to_charge = float(result)
466
- self.logger.debug(f"🧮 Amount calculator output | to_charge={to_charge:.6f}")
488
+ to_charge = float(await result) if inspect.isawaitable(result) else float(result)
467
489
  except Exception as e:
468
- self.logger.error(f"Amount calculator failed: {e}; falling back to default formula")
469
- to_charge = (llm_cost_usd + tool_cost_usd) * (1.0 + (self.agent_pricing_percent / 100.0))
490
+ self.logger.error(f"Amount calculator failed: {e}; using default total")
491
+ to_charge = default_total
492
+ using_custom_calculator = False
470
493
  else:
471
- to_charge = (llm_cost_usd + tool_cost_usd) * (1.0 + (self.agent_pricing_percent / 100.0))
494
+ to_charge = default_total
472
495
 
473
496
  if to_charge <= 0:
474
497
  return context
475
498
 
499
+ # Log detailed charge breakdown
500
+ self.logger.info(f"💰 Payment Breakdown for Agent '{getattr(self.agent, 'name', 'unknown')}':")
501
+ self.logger.info(f" 📊 LLM Costs: ${llm_cost_usd:.6f}")
502
+ for llm_record in llm_breakdown:
503
+ self.logger.info(f" - {llm_record['model']}: {llm_record['prompt_tokens']}+{llm_record['completion_tokens']} tokens = ${llm_record['cost_usd']:.6f}")
504
+
505
+ self.logger.info(f" 🛠️ Tool Costs: ${tool_cost_usd:.6f}")
506
+ for tool_record in tool_breakdown:
507
+ self.logger.info(f" - {tool_record['tool_name']}: ${tool_record['credits']:.6f} ({tool_record['reason']})")
508
+
509
+ # Display depends on pricing model
510
+ if using_custom_calculator:
511
+ # Revenue-sharing model: platform charges user, agent gets a share
512
+ import os
513
+ platform_markup = float(os.getenv('ROBUTLER_PLATFORM_MARKUP', '1.75'))
514
+ platform_charge = subtotal * platform_markup
515
+ self.logger.info(f" 🏦 Platform Charge to User: ${platform_charge:.6f} (base=${subtotal:.6f} × {platform_markup:.2f})")
516
+ self.logger.info(f" 💰 Agent Revenue Share: {self.agent_pricing_percent:.2f}% of ${platform_charge:.6f} = ${to_charge:.6f}")
517
+ else:
518
+ # Simple markup model
519
+ markup_dollars = to_charge - subtotal
520
+ self.logger.info(f" 📈 Agent Markup: {self.agent_pricing_percent:.2f}% (${markup_dollars:.6f})")
521
+ self.logger.info(f" 💵 Total Charge: ${to_charge:.6f} (subtotal=${subtotal:.6f} + markup=${markup_dollars:.6f})")
522
+
476
523
  # Charge payment token directly
477
524
  if payment_context.payment_token:
478
525
  success = await self._charge_payment_token(
@@ -481,10 +528,13 @@ class PaymentSkill(Skill):
481
528
  f"Agent {getattr(self.agent, 'name', 'unknown')} usage (margin {self.agent_pricing_percent:.2f}%)"
482
529
  )
483
530
  if success:
531
+ # Mark payment as successful for downstream finalize hooks (e.g., cashback)
532
+ setattr(payment_context, 'payment_successful', True)
484
533
  self.logger.info(
485
534
  f"💳 Payment finalized: ${to_charge:.6f} charged to token {payment_context.payment_token[:20]}..."
486
535
  )
487
536
  else:
537
+ setattr(payment_context, 'payment_successful', False)
488
538
  self.logger.error(
489
539
  f"💳 Payment failed: ${to_charge:.6f} could not charge token {payment_context.payment_token[:20]}..."
490
540
  )
@@ -267,9 +267,9 @@ class RobutlerFilesSkill(Skill):
267
267
  "error": f"Failed to store file from base64: {str(e)}"
268
268
  })
269
269
 
270
- @tool
270
+ @tool(description="Get public URLs of YOUR reference content. **WHEN TO USE**: Anytime you need to reference YOUR content/images/files in requests to other agents, you MUST call this tool FIRST to get actual URLs. DO NOT describe or invent file names - get real URLs. **USE CASES**: 1) Getting reference image URLs before image generation, 2) Finding style reference URLs, 3) Listing 'my content'/'my files'/'my public content'. **RETURNS**: Full public URLs (e.g., https://robutler.ai/api/content/public/abc123/image.png) that you pass to other agents. **IMPORTANT**: These URLs are for INTERNAL use (passing to other agents) - do NOT show raw URLs to users unless specifically asked or necessary for context.")
271
271
  @pricing(credits_per_call=0.005)
272
- async def list_files(
272
+ async def get_my_public_content_urls(
273
273
  self,
274
274
  scope: Optional[str] = None
275
275
  ) -> str: