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.
Files changed (46) 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/crewai/__init__.py +3 -1
  8. webagents/agents/skills/ecosystem/crewai/skill.py +158 -0
  9. webagents/agents/skills/ecosystem/database/__init__.py +3 -1
  10. webagents/agents/skills/ecosystem/database/skill.py +522 -0
  11. webagents/agents/skills/ecosystem/mongodb/__init__.py +3 -0
  12. webagents/agents/skills/ecosystem/mongodb/skill.py +428 -0
  13. webagents/agents/skills/ecosystem/n8n/README.md +287 -0
  14. webagents/agents/skills/ecosystem/n8n/__init__.py +3 -0
  15. webagents/agents/skills/ecosystem/n8n/skill.py +341 -0
  16. webagents/agents/skills/ecosystem/openai/__init__.py +6 -0
  17. webagents/agents/skills/ecosystem/openai/skill.py +867 -0
  18. webagents/agents/skills/ecosystem/replicate/README.md +440 -0
  19. webagents/agents/skills/ecosystem/replicate/__init__.py +10 -0
  20. webagents/agents/skills/ecosystem/replicate/skill.py +517 -0
  21. webagents/agents/skills/ecosystem/x_com/README.md +401 -0
  22. webagents/agents/skills/ecosystem/x_com/__init__.py +3 -0
  23. webagents/agents/skills/ecosystem/x_com/skill.py +1048 -0
  24. webagents/agents/skills/ecosystem/zapier/README.md +363 -0
  25. webagents/agents/skills/ecosystem/zapier/__init__.py +3 -0
  26. webagents/agents/skills/ecosystem/zapier/skill.py +337 -0
  27. webagents/agents/skills/examples/__init__.py +6 -0
  28. webagents/agents/skills/examples/music_player.py +329 -0
  29. webagents/agents/skills/robutler/handoff/__init__.py +6 -0
  30. webagents/agents/skills/robutler/handoff/skill.py +191 -0
  31. webagents/agents/skills/robutler/nli/skill.py +180 -24
  32. webagents/agents/skills/robutler/payments/exceptions.py +27 -7
  33. webagents/agents/skills/robutler/payments/skill.py +64 -14
  34. webagents/agents/skills/robutler/storage/files/skill.py +2 -2
  35. webagents/agents/tools/decorators.py +243 -47
  36. webagents/agents/widgets/__init__.py +6 -0
  37. webagents/agents/widgets/renderer.py +150 -0
  38. webagents/server/core/app.py +130 -15
  39. webagents/server/core/models.py +1 -1
  40. webagents/utils/logging.py +13 -1
  41. {webagents-0.2.0.dist-info → webagents-0.2.3.dist-info}/METADATA +16 -9
  42. {webagents-0.2.0.dist-info → webagents-0.2.3.dist-info}/RECORD +45 -24
  43. webagents/agents/skills/ecosystem/openai_agents/__init__.py +0 -0
  44. {webagents-0.2.0.dist-info → webagents-0.2.3.dist-info}/WHEEL +0 -0
  45. {webagents-0.2.0.dist-info → webagents-0.2.3.dist-info}/entry_points.txt +0 -0
  46. {webagents-0.2.0.dist-info → webagents-0.2.3.dist-info}/licenses/LICENSE +0 -0
@@ -247,74 +247,104 @@ def prompt(priority: int = 50, scope: Union[str, List[str]] = "all"):
247
247
  return decorator
248
248
 
249
249
 
250
- def handoff(name: Optional[str] = None, handoff_type: str = "agent", description: Optional[str] = None,
251
- scope: Union[str, List[str]] = "all"):
252
- """Decorator to mark functions as handoffs for automatic registration
250
+ def handoff(
251
+ name: Optional[str] = None,
252
+ prompt: Optional[str] = None,
253
+ scope: Union[str, List[str]] = "all",
254
+ priority: int = 50,
255
+ auto_tool: bool = False,
256
+ auto_tool_description: Optional[str] = None
257
+ ):
258
+ """Decorator to mark functions as handoff handlers for automatic registration
259
+
260
+ Handoff functions handle chat completions and can be:
261
+ 1. Async generators (streaming native) - work in both streaming and non-streaming modes
262
+ 2. Regular async functions (non-streaming native) - work in both streaming and non-streaming modes
253
263
 
254
264
  Args:
255
- name: Optional override for handoff name (defaults to function name)
256
- handoff_type: Type of handoff - "agent", "llm", "pipeline", etc.
257
- description: Handoff description (defaults to function docstring)
265
+ name: Handoff identifier (defaults to function name)
266
+ prompt: Description/guidance about when to use this handoff
267
+ - Used as handoff description for discovery
268
+ - Added to dynamic prompts to guide the LLM
258
269
  scope: Access scope - "all", "owner", "admin", or list of scopes
270
+ priority: Execution priority - lower numbers = higher priority (determines default)
271
+ First handoff with lowest priority becomes the default completion handler
272
+ auto_tool: If True, automatically create a tool to invoke this handoff dynamically
273
+ auto_tool_description: Description for the auto-generated tool (defaults to generic description)
259
274
 
260
- Handoff functions should return HandoffResult:
261
-
262
- @handoff(handoff_type="agent", scope=["admin"])
263
- async def escalate_to_admin(self, issue: str, context: Context = None) -> HandoffResult:
264
- # Process handoff
265
- return HandoffResult(result="escalated", handoff_type="agent")
275
+ Examples:
276
+ # Local LLM handoff (non-streaming)
277
+ @handoff(name="gpt4", prompt="Primary LLM for general reasoning", priority=10)
278
+ async def chat_completion(self, messages, tools=None, **kwargs) -> Dict[str, Any]:
279
+ return await llm_api_call(messages, tools)
280
+
281
+ # Remote agent handoff (streaming)
282
+ @handoff(name="specialist", prompt="Hand off to specialist for complex tasks", priority=20)
283
+ async def remote_handoff(self, messages, tools=None, **kwargs) -> AsyncGenerator[Dict, None]:
284
+ async for chunk in nli_stream(agent_url, messages):
285
+ yield chunk
286
+
287
+ # With context injection
288
+ @handoff(name="custom", priority=15)
289
+ async def my_handoff(self, messages, tools=None, context=None, **kwargs) -> Dict:
290
+ api_key = context.get("api_key")
291
+ return await custom_llm_call(messages, api_key)
266
292
  """
267
293
  def decorator(func: Callable) -> Callable:
268
- # Mark function with metadata for BaseAgent discovery
269
- func._webagents_is_handoff = True
270
- func._handoff_type = handoff_type
271
- func._handoff_scope = scope
272
- func._handoff_name = name or func.__name__
273
- func._handoff_description = description or func.__doc__ or f"Handoff: {func.__name__}"
294
+ # Validate function type
295
+ is_async_gen = inspect.isasyncgenfunction(func)
296
+ is_async = inspect.iscoroutinefunction(func)
297
+
298
+ if not is_async_gen and not is_async:
299
+ raise ValueError(
300
+ f"Handoff '{func.__name__}' must be async function or async generator. "
301
+ f"Got: {type(func).__name__}"
302
+ )
274
303
 
275
304
  # Check if function expects context injection
276
305
  sig = inspect.signature(func)
277
306
  has_context_param = 'context' in sig.parameters
278
307
 
308
+ # Create wrapper if context injection needed
279
309
  if has_context_param:
280
- @functools.wraps(func)
281
- async def async_wrapper(*args, **kwargs):
282
- # Inject context if requested and not provided
283
- if 'context' not in kwargs:
284
- from ...server.context.context_vars import get_context
285
- context = get_context()
286
- kwargs['context'] = context
310
+ if is_async_gen:
311
+ # Async generator with context
312
+ @functools.wraps(func)
313
+ async def async_gen_wrapper(*args, **kwargs):
314
+ if 'context' not in kwargs:
315
+ from ...server.context.context_vars import get_context
316
+ context = get_context()
317
+ kwargs['context'] = context
318
+
319
+ async for item in func(*args, **kwargs):
320
+ yield item
287
321
 
288
- # Call original function
289
- if inspect.iscoroutinefunction(func):
322
+ wrapper = async_gen_wrapper
323
+ else:
324
+ # Regular async function with context
325
+ @functools.wraps(func)
326
+ async def async_wrapper(*args, **kwargs):
327
+ if 'context' not in kwargs:
328
+ from ...server.context.context_vars import get_context
329
+ context = get_context()
330
+ kwargs['context'] = context
331
+
290
332
  return await func(*args, **kwargs)
291
- else:
292
- return func(*args, **kwargs)
293
-
294
- @functools.wraps(func)
295
- def sync_wrapper(*args, **kwargs):
296
- # Inject context if requested and not provided
297
- if 'context' not in kwargs:
298
- from ...server.context.context_vars import get_context
299
- context = get_context()
300
- kwargs['context'] = context
301
333
 
302
- return func(*args, **kwargs)
303
-
304
- # Return appropriate wrapper
305
- if inspect.iscoroutinefunction(func):
306
334
  wrapper = async_wrapper
307
- else:
308
- wrapper = sync_wrapper
309
335
  else:
336
+ # No context injection needed
310
337
  wrapper = func
311
338
 
312
- # Copy metadata to wrapper
339
+ # Mark function with metadata for BaseAgent discovery
313
340
  wrapper._webagents_is_handoff = True
314
- wrapper._handoff_type = handoff_type
315
- wrapper._handoff_scope = scope
316
341
  wrapper._handoff_name = name or func.__name__
317
- wrapper._handoff_description = description or func.__doc__ or f"Handoff: {func.__name__}"
342
+ wrapper._handoff_prompt = prompt or func.__doc__ or f"Handoff: {func.__name__}"
343
+ wrapper._handoff_scope = scope
344
+ wrapper._handoff_priority = priority
345
+ wrapper._handoff_is_generator = is_async_gen
346
+ wrapper._handoff_auto_tool = auto_tool
347
+ wrapper._handoff_auto_tool_description = auto_tool_description or f"Switch to {name or func.__name__} handoff"
318
348
 
319
349
  return wrapper
320
350
 
@@ -423,4 +453,170 @@ def http(subpath: str, method: str = "get", scope: Union[str, List[str]] = "all"
423
453
 
424
454
  return wrapper
425
455
 
456
+ return decorator
457
+
458
+
459
+ def widget(func: Optional[Callable] = None, *, name: Optional[str] = None, description: Optional[str] = None, template: Optional[str] = None, scope: Union[str, List[str]] = "all", auto_escape: bool = True):
460
+ """Decorator to mark functions as widgets for automatic registration
461
+
462
+ Widgets are interactive HTML components rendered in sandboxed iframes on the frontend.
463
+ They support both Jinja2 templates and inline HTML strings.
464
+
465
+ Can be used as:
466
+ @widget
467
+ def my_widget(self, param: str) -> str: ...
468
+
469
+ Or:
470
+ @widget(name="custom", description="Custom widget", scope="owner")
471
+ def my_widget(self, param: str) -> str: ...
472
+
473
+ Args:
474
+ name: Optional override for widget name (defaults to function name)
475
+ description: Widget description (defaults to function docstring)
476
+ template: Optional path to Jinja2 template file
477
+ scope: Access scope - "all", "owner", "admin", or list of scopes
478
+ auto_escape: Automatically HTML-escape string arguments (default: True)
479
+ Set to False if you're passing pre-rendered safe HTML
480
+
481
+ The decorated function should return HTML wrapped in <widget> tags:
482
+
483
+ @widget(scope="all") # auto_escape=True by default
484
+ def my_widget(self, param: str, context: Context = None) -> str:
485
+ # param is automatically escaped! No need for html.escape()
486
+ html = f"<div>{param}</div>"
487
+ return f'<widget kind="webagents" id="my_widget">{html}</widget>'
488
+
489
+ For pre-rendered HTML, disable auto-escaping:
490
+
491
+ @widget(scope="all", auto_escape=False)
492
+ def unsafe_widget(self, raw_html: str) -> str:
493
+ # raw_html is NOT escaped - use with caution!
494
+ return f'<widget kind="webagents" id="unsafe">{raw_html}</widget>'
495
+
496
+ Widgets are only included in LLM context for browser requests (detected via User-Agent).
497
+ """
498
+ def decorator(f: Callable) -> Callable:
499
+ # Generate widget schema for LLM awareness
500
+ sig = inspect.signature(f)
501
+ parameters = {}
502
+ required = []
503
+
504
+ for param_name, param in sig.parameters.items():
505
+ # Skip 'self' and 'context' parameters from schema
506
+ if param_name in ('self', 'context'):
507
+ continue
508
+
509
+ param_type = "string" # Default type
510
+ param_desc = f"Parameter {param_name}"
511
+
512
+ # Try to infer type from annotation
513
+ if param.annotation != inspect.Parameter.empty:
514
+ if param.annotation == int:
515
+ param_type = "integer"
516
+ elif param.annotation == float:
517
+ param_type = "number"
518
+ elif param.annotation == bool:
519
+ param_type = "boolean"
520
+ elif param.annotation == list:
521
+ param_type = "array"
522
+ elif param.annotation == dict:
523
+ param_type = "object"
524
+
525
+ parameters[param_name] = {
526
+ "type": param_type,
527
+ "description": param_desc
528
+ }
529
+
530
+ # Mark as required if no default value
531
+ if param.default == inspect.Parameter.empty:
532
+ required.append(param_name)
533
+
534
+ # Create widget schema (similar to tool schema for LLM)
535
+ widget_schema = {
536
+ "type": "function",
537
+ "function": {
538
+ "name": name or f.__name__,
539
+ "description": description or f.__doc__ or f"Widget: {f.__name__}",
540
+ "parameters": {
541
+ "type": "object",
542
+ "properties": parameters,
543
+ "required": required
544
+ }
545
+ }
546
+ }
547
+
548
+ # Mark function with metadata for BaseAgent discovery
549
+ f._webagents_is_widget = True
550
+ f._webagents_widget_definition = widget_schema
551
+ f._widget_scope = scope
552
+ f._widget_scope_was_set = func is None # If func is None, decorator was called with params
553
+ f._widget_name = name or f.__name__
554
+ f._widget_description = description or f.__doc__ or f"Widget: {f.__name__}"
555
+ f._widget_template = template
556
+
557
+ # Check if function expects context injection
558
+ has_context_param = 'context' in sig.parameters
559
+
560
+ @functools.wraps(f)
561
+ async def async_wrapper(*args, **kwargs):
562
+ # Inject context if function expects it
563
+ if has_context_param and 'context' not in kwargs:
564
+ from webagents.server.context.context_vars import get_context
565
+ kwargs['context'] = get_context()
566
+
567
+ # Auto-escape string arguments if enabled
568
+ if auto_escape:
569
+ import html
570
+ # Escape all string kwargs (except 'context' and 'self')
571
+ escaped_kwargs = {}
572
+ for key, value in kwargs.items():
573
+ if key not in ('context', 'self') and isinstance(value, str):
574
+ escaped_kwargs[key] = html.escape(value)
575
+ else:
576
+ escaped_kwargs[key] = value
577
+ kwargs = escaped_kwargs
578
+
579
+ # Call original function
580
+ if inspect.iscoroutinefunction(f):
581
+ return await f(*args, **kwargs)
582
+ else:
583
+ return f(*args, **kwargs)
584
+
585
+ @functools.wraps(f)
586
+ def sync_wrapper(*args, **kwargs):
587
+ # Inject context if function expects it
588
+ if has_context_param and 'context' not in kwargs:
589
+ from webagents.server.context.context_vars import get_context
590
+ kwargs['context'] = get_context()
591
+
592
+ # Auto-escape string arguments if enabled
593
+ if auto_escape:
594
+ import html
595
+ # Escape all string kwargs (except 'context' and 'self')
596
+ escaped_kwargs = {}
597
+ for key, value in kwargs.items():
598
+ if key not in ('context', 'self') and isinstance(value, str):
599
+ escaped_kwargs[key] = html.escape(value)
600
+ else:
601
+ escaped_kwargs[key] = value
602
+ kwargs = escaped_kwargs
603
+
604
+ # Call original function
605
+ return f(*args, **kwargs)
606
+
607
+ # Preserve metadata on wrapper
608
+ wrapper = async_wrapper if inspect.iscoroutinefunction(f) else sync_wrapper
609
+ wrapper._webagents_is_widget = True
610
+ wrapper._webagents_widget_definition = widget_schema
611
+ wrapper._widget_scope = scope
612
+ wrapper._widget_scope_was_set = func is None
613
+ wrapper._widget_name = name or f.__name__
614
+ wrapper._widget_description = description or f.__doc__ or f"Widget: {f.__name__}"
615
+ wrapper._widget_template = template
616
+
617
+ return wrapper
618
+
619
+ # Support both @widget and @widget()
620
+ if func is not None:
621
+ return decorator(func)
426
622
  return decorator
@@ -0,0 +1,6 @@
1
+ """Widget system for rendering interactive HTML components"""
2
+
3
+ from .renderer import WidgetTemplateRenderer, ChatKitRenderer
4
+
5
+ __all__ = ['WidgetTemplateRenderer', 'ChatKitRenderer'] # ChatKitRenderer is deprecated alias
6
+
@@ -0,0 +1,150 @@
1
+ """Widget template renderer for HTML templates with Jinja2 support"""
2
+
3
+ import os
4
+ import warnings
5
+ from typing import Dict, Any, Optional
6
+ from jinja2 import Environment, FileSystemLoader, Template, TemplateNotFound
7
+ import html
8
+
9
+
10
+ class WidgetTemplateRenderer:
11
+ """Renders WebAgents widget HTML from Jinja2 templates or inline HTML strings
12
+
13
+ This renderer is specifically for WebAgents HTML widgets (kind="webagents"),
14
+ NOT for OpenAI ChatKit widgets (kind="openai").
15
+
16
+ Supports both file-based templates and inline HTML with variable substitution.
17
+ Ensures proper HTML escaping for data attributes to prevent XSS.
18
+
19
+ Example:
20
+ # File-based template
21
+ renderer = WidgetTemplateRenderer(template_dir="widgets")
22
+ html = renderer.render("music_player.html", {"title": "Song Name"})
23
+
24
+ # Inline HTML
25
+ renderer = WidgetTemplateRenderer()
26
+ html = renderer.render_inline("<div>{{ title }}</div>", {"title": "Song Name"})
27
+ """
28
+
29
+ def __init__(self, template_dir: Optional[str] = None):
30
+ """Initialize widget renderer
31
+
32
+ Args:
33
+ template_dir: Directory containing Jinja2 template files (optional)
34
+ """
35
+ self.template_dir = template_dir or "widgets"
36
+
37
+ # Initialize Jinja2 environment if template directory exists
38
+ if os.path.exists(self.template_dir):
39
+ self.env = Environment(
40
+ loader=FileSystemLoader(self.template_dir),
41
+ autoescape=True # Auto-escape HTML for security
42
+ )
43
+ else:
44
+ self.env = None
45
+
46
+ def render(self, template_name: str, context: Dict[str, Any]) -> str:
47
+ """Render widget HTML from a template file
48
+
49
+ Args:
50
+ template_name: Name of template file (e.g., "music_player.html")
51
+ context: Dictionary of variables to pass to template
52
+
53
+ Returns:
54
+ Rendered HTML string
55
+
56
+ Raises:
57
+ TemplateNotFound: If template file doesn't exist
58
+ ValueError: If template directory not configured
59
+ """
60
+ if not self.env:
61
+ raise ValueError(f"Template directory '{self.template_dir}' not found")
62
+
63
+ template = self.env.get_template(template_name)
64
+ return template.render(**context)
65
+
66
+ def render_inline(self, html_string: str, context: Dict[str, Any]) -> str:
67
+ """Render widget HTML from an inline string
68
+
69
+ Args:
70
+ html_string: HTML string with Jinja2 template syntax
71
+ context: Dictionary of variables to pass to template
72
+
73
+ Returns:
74
+ Rendered HTML string
75
+ """
76
+ template = Template(html_string, autoescape=True)
77
+ return template.render(**context)
78
+
79
+ @staticmethod
80
+ def escape_data(data: Any) -> str:
81
+ """Escape data for safe inclusion in HTML attributes
82
+
83
+ Args:
84
+ data: Data to escape (will be converted to string)
85
+
86
+ Returns:
87
+ HTML-escaped string safe for attribute values
88
+ """
89
+ return html.escape(str(data), quote=True)
90
+
91
+ @staticmethod
92
+ def inject_tailwind_cdn(html_content: str) -> str:
93
+ """Inject Tailwind CSS CDN if not already present
94
+
95
+ Args:
96
+ html_content: HTML content to check and modify
97
+
98
+ Returns:
99
+ HTML content with Tailwind CDN injected in <head>
100
+ """
101
+ # Check if Tailwind is already present
102
+ if 'tailwindcss.com' in html_content:
103
+ return html_content
104
+
105
+ # Check if there's a <head> tag
106
+ if '<head>' in html_content:
107
+ # Inject after <head>
108
+ return html_content.replace(
109
+ '<head>',
110
+ '<head>\n <script src="https://cdn.tailwindcss.com"></script>',
111
+ 1 # Only replace first occurrence
112
+ )
113
+ elif '<html>' in html_content:
114
+ # Inject after <html>
115
+ return html_content.replace(
116
+ '<html>',
117
+ '<html>\n<head>\n <script src="https://cdn.tailwindcss.com"></script>\n</head>',
118
+ 1
119
+ )
120
+ else:
121
+ # No HTML structure, wrap in basic HTML with Tailwind
122
+ return f"""<!DOCTYPE html>
123
+ <html>
124
+ <head>
125
+ <script src="https://cdn.tailwindcss.com"></script>
126
+ </head>
127
+ <body>
128
+ {html_content}
129
+ </body>
130
+ </html>"""
131
+
132
+
133
+ # Backward compatibility alias with deprecation warning
134
+ class ChatKitRenderer(WidgetTemplateRenderer):
135
+ """Deprecated: Use WidgetTemplateRenderer instead.
136
+
137
+ This class was misnamed - it renders WebAgents HTML widgets using Jinja2,
138
+ NOT OpenAI ChatKit widgets. Use WidgetTemplateRenderer for clarity.
139
+ """
140
+
141
+ def __init__(self, *args, **kwargs):
142
+ warnings.warn(
143
+ "ChatKitRenderer is deprecated and will be removed in a future version. "
144
+ "Use WidgetTemplateRenderer instead. Note: This class renders WebAgents HTML widgets, "
145
+ "not OpenAI ChatKit widgets.",
146
+ DeprecationWarning,
147
+ stacklevel=2
148
+ )
149
+ super().__init__(*args, **kwargs)
150
+