webagents 0.2.0__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.
- webagents/__init__.py +9 -0
- webagents/agents/core/base_agent.py +865 -69
- webagents/agents/core/handoffs.py +14 -6
- webagents/agents/skills/base.py +33 -2
- webagents/agents/skills/core/llm/litellm/skill.py +906 -27
- webagents/agents/skills/core/memory/vector_memory/skill.py +8 -16
- webagents/agents/skills/ecosystem/crewai/__init__.py +3 -1
- webagents/agents/skills/ecosystem/crewai/skill.py +158 -0
- webagents/agents/skills/ecosystem/database/__init__.py +3 -1
- webagents/agents/skills/ecosystem/database/skill.py +522 -0
- webagents/agents/skills/ecosystem/mongodb/__init__.py +3 -0
- webagents/agents/skills/ecosystem/mongodb/skill.py +428 -0
- webagents/agents/skills/ecosystem/n8n/README.md +287 -0
- webagents/agents/skills/ecosystem/n8n/__init__.py +3 -0
- webagents/agents/skills/ecosystem/n8n/skill.py +341 -0
- webagents/agents/skills/ecosystem/openai/__init__.py +6 -0
- webagents/agents/skills/ecosystem/openai/skill.py +867 -0
- webagents/agents/skills/ecosystem/replicate/README.md +440 -0
- webagents/agents/skills/ecosystem/replicate/__init__.py +10 -0
- webagents/agents/skills/ecosystem/replicate/skill.py +517 -0
- webagents/agents/skills/ecosystem/x_com/README.md +401 -0
- webagents/agents/skills/ecosystem/x_com/__init__.py +3 -0
- webagents/agents/skills/ecosystem/x_com/skill.py +1048 -0
- webagents/agents/skills/ecosystem/zapier/README.md +363 -0
- webagents/agents/skills/ecosystem/zapier/__init__.py +3 -0
- webagents/agents/skills/ecosystem/zapier/skill.py +337 -0
- webagents/agents/skills/examples/__init__.py +6 -0
- webagents/agents/skills/examples/music_player.py +329 -0
- webagents/agents/skills/robutler/handoff/__init__.py +6 -0
- webagents/agents/skills/robutler/handoff/skill.py +191 -0
- webagents/agents/skills/robutler/nli/skill.py +180 -24
- webagents/agents/skills/robutler/payments/exceptions.py +27 -7
- webagents/agents/skills/robutler/payments/skill.py +64 -14
- webagents/agents/skills/robutler/storage/files/skill.py +2 -2
- webagents/agents/tools/decorators.py +243 -47
- webagents/agents/widgets/__init__.py +6 -0
- webagents/agents/widgets/renderer.py +150 -0
- webagents/server/core/app.py +130 -15
- webagents/server/core/models.py +1 -1
- webagents/utils/logging.py +13 -1
- {webagents-0.2.0.dist-info → webagents-0.2.3.dist-info}/METADATA +16 -9
- {webagents-0.2.0.dist-info → webagents-0.2.3.dist-info}/RECORD +45 -24
- webagents/agents/skills/ecosystem/openai_agents/__init__.py +0 -0
- {webagents-0.2.0.dist-info → webagents-0.2.3.dist-info}/WHEEL +0 -0
- {webagents-0.2.0.dist-info → webagents-0.2.3.dist-info}/entry_points.txt +0 -0
- {webagents-0.2.0.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
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
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":
|
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.
|
370
|
+
self.logger.info(f"🔐 ✅ Forwarding payment token for agent-to-agent communication: {payment_token[:20]}...")
|
354
371
|
else:
|
355
|
-
self.logger.
|
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
|
-
|
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
|
-
|
443
|
-
|
444
|
-
|
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=
|
132
|
+
error_code=error_code,
|
123
133
|
context=context,
|
124
|
-
user_message=
|
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
|
217
|
-
|
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 "
|
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
|
-
|
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.
|
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
|
-
|
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
|
-
|
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};
|
469
|
-
to_charge =
|
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 =
|
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
|
272
|
+
async def get_my_public_content_urls(
|
273
273
|
self,
|
274
274
|
scope: Optional[str] = None
|
275
275
|
) -> str:
|