webagents 0.1.0__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 (94) hide show
  1. webagents/__init__.py +18 -0
  2. webagents/__main__.py +55 -0
  3. webagents/agents/__init__.py +13 -0
  4. webagents/agents/core/__init__.py +19 -0
  5. webagents/agents/core/base_agent.py +1834 -0
  6. webagents/agents/core/handoffs.py +293 -0
  7. webagents/agents/handoffs/__init__.py +0 -0
  8. webagents/agents/interfaces/__init__.py +0 -0
  9. webagents/agents/lifecycle/__init__.py +0 -0
  10. webagents/agents/skills/__init__.py +109 -0
  11. webagents/agents/skills/base.py +136 -0
  12. webagents/agents/skills/core/__init__.py +8 -0
  13. webagents/agents/skills/core/guardrails/__init__.py +0 -0
  14. webagents/agents/skills/core/llm/__init__.py +0 -0
  15. webagents/agents/skills/core/llm/anthropic/__init__.py +1 -0
  16. webagents/agents/skills/core/llm/litellm/__init__.py +10 -0
  17. webagents/agents/skills/core/llm/litellm/skill.py +538 -0
  18. webagents/agents/skills/core/llm/openai/__init__.py +1 -0
  19. webagents/agents/skills/core/llm/xai/__init__.py +1 -0
  20. webagents/agents/skills/core/mcp/README.md +375 -0
  21. webagents/agents/skills/core/mcp/__init__.py +15 -0
  22. webagents/agents/skills/core/mcp/skill.py +731 -0
  23. webagents/agents/skills/core/memory/__init__.py +11 -0
  24. webagents/agents/skills/core/memory/long_term_memory/__init__.py +10 -0
  25. webagents/agents/skills/core/memory/long_term_memory/memory_skill.py +639 -0
  26. webagents/agents/skills/core/memory/short_term_memory/__init__.py +9 -0
  27. webagents/agents/skills/core/memory/short_term_memory/skill.py +341 -0
  28. webagents/agents/skills/core/memory/vector_memory/skill.py +447 -0
  29. webagents/agents/skills/core/planning/__init__.py +9 -0
  30. webagents/agents/skills/core/planning/planner.py +343 -0
  31. webagents/agents/skills/ecosystem/__init__.py +0 -0
  32. webagents/agents/skills/ecosystem/crewai/__init__.py +1 -0
  33. webagents/agents/skills/ecosystem/database/__init__.py +1 -0
  34. webagents/agents/skills/ecosystem/filesystem/__init__.py +0 -0
  35. webagents/agents/skills/ecosystem/google/__init__.py +0 -0
  36. webagents/agents/skills/ecosystem/google/calendar/__init__.py +6 -0
  37. webagents/agents/skills/ecosystem/google/calendar/skill.py +306 -0
  38. webagents/agents/skills/ecosystem/n8n/__init__.py +0 -0
  39. webagents/agents/skills/ecosystem/openai_agents/__init__.py +0 -0
  40. webagents/agents/skills/ecosystem/web/__init__.py +0 -0
  41. webagents/agents/skills/ecosystem/zapier/__init__.py +0 -0
  42. webagents/agents/skills/robutler/__init__.py +11 -0
  43. webagents/agents/skills/robutler/auth/README.md +63 -0
  44. webagents/agents/skills/robutler/auth/__init__.py +17 -0
  45. webagents/agents/skills/robutler/auth/skill.py +354 -0
  46. webagents/agents/skills/robutler/crm/__init__.py +18 -0
  47. webagents/agents/skills/robutler/crm/skill.py +368 -0
  48. webagents/agents/skills/robutler/discovery/README.md +281 -0
  49. webagents/agents/skills/robutler/discovery/__init__.py +16 -0
  50. webagents/agents/skills/robutler/discovery/skill.py +230 -0
  51. webagents/agents/skills/robutler/kv/__init__.py +6 -0
  52. webagents/agents/skills/robutler/kv/skill.py +80 -0
  53. webagents/agents/skills/robutler/message_history/__init__.py +9 -0
  54. webagents/agents/skills/robutler/message_history/skill.py +270 -0
  55. webagents/agents/skills/robutler/messages/__init__.py +0 -0
  56. webagents/agents/skills/robutler/nli/__init__.py +13 -0
  57. webagents/agents/skills/robutler/nli/skill.py +687 -0
  58. webagents/agents/skills/robutler/notifications/__init__.py +5 -0
  59. webagents/agents/skills/robutler/notifications/skill.py +141 -0
  60. webagents/agents/skills/robutler/payments/__init__.py +41 -0
  61. webagents/agents/skills/robutler/payments/exceptions.py +255 -0
  62. webagents/agents/skills/robutler/payments/skill.py +610 -0
  63. webagents/agents/skills/robutler/storage/__init__.py +10 -0
  64. webagents/agents/skills/robutler/storage/files/__init__.py +9 -0
  65. webagents/agents/skills/robutler/storage/files/skill.py +445 -0
  66. webagents/agents/skills/robutler/storage/json/__init__.py +9 -0
  67. webagents/agents/skills/robutler/storage/json/skill.py +336 -0
  68. webagents/agents/skills/robutler/storage/kv/skill.py +88 -0
  69. webagents/agents/skills/robutler/storage.py +389 -0
  70. webagents/agents/tools/__init__.py +0 -0
  71. webagents/agents/tools/decorators.py +426 -0
  72. webagents/agents/tracing/__init__.py +0 -0
  73. webagents/agents/workflows/__init__.py +0 -0
  74. webagents/scripts/__init__.py +0 -0
  75. webagents/server/__init__.py +28 -0
  76. webagents/server/context/__init__.py +0 -0
  77. webagents/server/context/context_vars.py +121 -0
  78. webagents/server/core/__init__.py +0 -0
  79. webagents/server/core/app.py +843 -0
  80. webagents/server/core/middleware.py +69 -0
  81. webagents/server/core/models.py +98 -0
  82. webagents/server/core/monitoring.py +59 -0
  83. webagents/server/endpoints/__init__.py +0 -0
  84. webagents/server/interfaces/__init__.py +0 -0
  85. webagents/server/middleware.py +330 -0
  86. webagents/server/models.py +92 -0
  87. webagents/server/monitoring.py +659 -0
  88. webagents/utils/__init__.py +0 -0
  89. webagents/utils/logging.py +359 -0
  90. webagents-0.1.0.dist-info/METADATA +230 -0
  91. webagents-0.1.0.dist-info/RECORD +94 -0
  92. webagents-0.1.0.dist-info/WHEEL +4 -0
  93. webagents-0.1.0.dist-info/entry_points.txt +2 -0
  94. webagents-0.1.0.dist-info/licenses/LICENSE +20 -0
@@ -0,0 +1,445 @@
1
+ """
2
+ RobutlerFilesSkill - File Management with Harmonized API
3
+ Uses the new harmonized content API for cleaner and more efficient operations.
4
+ """
5
+
6
+ import json
7
+ import os
8
+ import base64
9
+ import aiohttp
10
+ from typing import Dict, List, Any, Optional, Union
11
+ from datetime import datetime
12
+
13
+ from ....base import Skill
14
+ from webagents.agents.tools.decorators import tool
15
+ from robutler.api.client import RobutlerClient
16
+ from webagents.agents.skills.robutler.payments import pricing, PricingInfo
17
+
18
+ class RobutlerFilesSkill(Skill):
19
+ """
20
+ WebAgents portal file management skill using harmonized API.
21
+
22
+ Features:
23
+ - Download and store files from URLs
24
+ - Store files from base64 data
25
+ - List files with agent-based access
26
+ - Agent access is automatically handled by the API
27
+
28
+ Uses the new /api/content/agent endpoints for agent operations.
29
+ """
30
+
31
+ def __init__(self, config: Optional[Dict[str, Any]] = None):
32
+ super().__init__(config)
33
+ self.portal_url = config.get('portal_url', 'http://localhost:3000') if config else 'http://localhost:3000'
34
+ # Base URL used by the chat frontend to serve public content
35
+ self.chat_base_url = (config.get('chat_base_url') if config else None) or os.getenv('ROBUTLER_CHAT_URL', 'http://localhost:3001')
36
+ self.api_key = config.get('api_key', os.getenv('ROBUTLER_API_KEY', 'rok_testapikey')) if config else os.getenv('ROBUTLER_API_KEY', 'rok_testapikey')
37
+
38
+ # Initialize RobutlerClient
39
+ self.client = RobutlerClient(
40
+ api_key=self.api_key,
41
+ base_url=self.portal_url
42
+ )
43
+
44
+ async def initialize(self, agent_reference):
45
+ """Initialize with agent reference"""
46
+ await super().initialize(agent_reference)
47
+ self.agent = agent_reference
48
+
49
+ # Check if agent has its own API key
50
+ if hasattr(agent_reference, 'api_key') and agent_reference.api_key:
51
+ self.agent_api_key = agent_reference.api_key
52
+
53
+ # Debug logging for agent API key
54
+ agent_key_prefix = self.agent_api_key[:20] + "..." if len(self.agent_api_key) > 20 else self.agent_api_key
55
+ print(f"🔑 Storage skill using agent API key: {agent_key_prefix}")
56
+
57
+ # Create a separate client for agent operations
58
+ self.agent_client = RobutlerClient(
59
+ api_key=self.agent_api_key,
60
+ base_url=self.portal_url
61
+ )
62
+ else:
63
+ # Fall back to user API key
64
+ self.agent_api_key = self.api_key
65
+ self.agent_client = self.client
66
+
67
+ # Debug logging for fallback
68
+ user_key_prefix = self.api_key[:20] + "..." if len(self.api_key) > 20 else self.api_key
69
+ print(f"🔑 Storage skill using user API key (fallback): {user_key_prefix}")
70
+
71
+ async def cleanup(self):
72
+ """Cleanup method to close client sessions"""
73
+ if self.client:
74
+ await self.client.close()
75
+
76
+ # Close agent client if it's different from the main client
77
+ if hasattr(self, 'agent_client') and self.agent_client != self.client:
78
+ await self.agent_client.close()
79
+
80
+ def _get_agent_name_from_context(self) -> str:
81
+ """
82
+ Get the current agent name from context.
83
+
84
+ Returns:
85
+ Agent name (e.g., 'van-gogh') or empty string if not found
86
+ """
87
+ try:
88
+ from webagents.server.context.context_vars import get_agent_name
89
+
90
+ # Use the utility function
91
+ agent_name = get_agent_name()
92
+ if agent_name:
93
+ return agent_name
94
+
95
+ # Fallback: try to get from agent instance
96
+ if hasattr(self, 'agent') and hasattr(self.agent, 'name'):
97
+ return self.agent.name or ''
98
+
99
+ return '' # Default to empty string
100
+ except Exception:
101
+ return '' # Fallback to empty string
102
+
103
+ def _rewrite_public_url(self, url: Optional[str]) -> Optional[str]:
104
+ """Rewrite portal public content URLs to chat base URL.
105
+ Examples:
106
+ http://localhost:3000/api/content/public/.. -> http://localhost:3001/api/content/public/..
107
+ /api/content/public/... stays relative and gets chat base prefixed when rendered client-side
108
+ """
109
+ if not url:
110
+ return url
111
+ try:
112
+ if url.startswith('/api/content/public'):
113
+ # Already relative; prefix with chat base for clarity
114
+ return f"{self.chat_base_url}{url}"
115
+ portal_prefix = f"{self.portal_url}/api/content/public"
116
+ if url.startswith(portal_prefix):
117
+ return url.replace(self.portal_url, self.chat_base_url, 1)
118
+ except Exception:
119
+ return url
120
+ return url
121
+
122
+ # @tool(scope="owner")
123
+ async def store_file_from_url(
124
+ self,
125
+ url: str,
126
+ filename: Optional[str] = None,
127
+ description: Optional[str] = None,
128
+ tags: Optional[List[str]] = None,
129
+ visibility: str = "private"
130
+ ) -> str:
131
+ """
132
+ A tool for downloading and storing a file from a URL. Never use this tool for files that you already own, e.g. URLs returned by list_files.
133
+
134
+ Args:
135
+ url: URL to download file from
136
+ filename: Optional custom filename (auto-detected if not provided)
137
+ description: Optional description of the file
138
+ tags: Optional list of tags for the file
139
+ visibility: File visibility - "public", "private", or "shared" (default: "private")
140
+
141
+ Returns:
142
+ JSON string with storage result
143
+ """
144
+ try:
145
+ # Download file from URL
146
+ async with aiohttp.ClientSession() as session:
147
+ async with session.get(url) as response:
148
+ if response.status != 200:
149
+ return json.dumps({
150
+ "success": False,
151
+ "error": f"Failed to download file: HTTP {response.status}"
152
+ })
153
+
154
+ content_data = await response.read()
155
+ content_type = response.headers.get('content-type', 'application/octet-stream')
156
+
157
+ # Auto-detect filename if not provided
158
+ if not filename:
159
+ filename = url.split('/')[-1] or 'downloaded_file'
160
+ # Remove query parameters
161
+ filename = filename.split('?')[0]
162
+
163
+ # Get agent name for filename prefixing
164
+ agent_name = self._get_agent_name_from_context()
165
+
166
+ # Prefix filename with agent name if available
167
+ if agent_name and not filename.startswith(f"{agent_name}_"):
168
+ filename = f"{agent_name}_{filename}"
169
+
170
+ # Store file using RobutlerClient with new API
171
+ response = await self.client.upload_content(
172
+ filename=filename,
173
+ content_data=content_data,
174
+ content_type=content_type,
175
+ visibility=visibility,
176
+ description=description or f"File downloaded from {url} by {agent_name or 'agent'}",
177
+ tags=tags
178
+ )
179
+
180
+ if response.success and response.data:
181
+ return json.dumps({
182
+ "success": True,
183
+ "id": response.data.get('id'),
184
+ "filename": response.data.get('fileName'),
185
+ "url": self._rewrite_public_url(response.data.get('url')),
186
+ "size": response.data.get('size'),
187
+ "content_type": content_type,
188
+ "visibility": visibility,
189
+ "source_url": url
190
+ }, indent=2)
191
+ else:
192
+ return json.dumps({
193
+ "success": False,
194
+ "error": f"Upload failed: {response.error or response.message}"
195
+ })
196
+
197
+ except Exception as e:
198
+ return json.dumps({
199
+ "success": False,
200
+ "error": f"Failed to store file from URL: {str(e)}"
201
+ })
202
+
203
+ # @tool(scope="owner")
204
+ async def store_file_from_base64(
205
+ self,
206
+ filename: str,
207
+ base64_data: str,
208
+ content_type: str = "application/octet-stream",
209
+ description: Optional[str] = None,
210
+ tags: Optional[List[str]] = None,
211
+ visibility: str = "private"
212
+ ) -> str:
213
+ """
214
+ A tool for storing a file from base64 encoded data.
215
+
216
+ Args:
217
+ filename: Name of the file
218
+ base64_data: Base64 encoded file content
219
+ content_type: MIME type of the file
220
+ description: Optional description of the file
221
+ tags: Optional list of tags for the file
222
+ visibility: File visibility - "public", "private", or "shared" (default: "private")
223
+
224
+ Returns:
225
+ JSON string with storage result
226
+ """
227
+ try:
228
+ # Decode base64 data
229
+ content_data = base64.b64decode(base64_data)
230
+
231
+ # Get agent name for filename prefixing
232
+ agent_name = self._get_agent_name_from_context()
233
+
234
+ # Prefix filename with agent name if available
235
+ if agent_name and not filename.startswith(f"{agent_name}_"):
236
+ filename = f"{agent_name}_{filename}"
237
+
238
+ # Store file using RobutlerClient with new API
239
+ response = await self.client.upload_content(
240
+ filename=filename,
241
+ content_data=content_data,
242
+ content_type=content_type,
243
+ visibility=visibility,
244
+ description=description or f"File uploaded from base64 data by {agent_name or 'agent'}",
245
+ tags=tags
246
+ )
247
+
248
+ if response.success and response.data:
249
+ return json.dumps({
250
+ "success": True,
251
+ "id": response.data.get('id'),
252
+ "filename": response.data.get('fileName'),
253
+ "url": self._rewrite_public_url(response.data.get('url')),
254
+ "size": response.data.get('size'),
255
+ "content_type": content_type,
256
+ "visibility": visibility
257
+ }, indent=2)
258
+ else:
259
+ return json.dumps({
260
+ "success": False,
261
+ "error": f"Upload failed: {response.error or response.message}"
262
+ })
263
+
264
+ except Exception as e:
265
+ return json.dumps({
266
+ "success": False,
267
+ "error": f"Failed to store file from base64: {str(e)}"
268
+ })
269
+
270
+ @tool
271
+ @pricing(credits_per_call=0.005)
272
+ async def list_files(
273
+ self,
274
+ scope: Optional[str] = None
275
+ ) -> str:
276
+ """
277
+ List files accessible by the current agent with scope-based filtering.
278
+
279
+ The behavior depends on who is calling:
280
+ - Agent owner calling "show all files" (scope=None): Returns all private + public agent files
281
+ - Agent owner calling "show public files" (scope="public"): Returns only public agent files
282
+ - Agent owner calling "show private files" (scope="private"): Returns only private agent files
283
+ - Non-owner calling: Always returns only public agent files regardless of scope
284
+
285
+ Args:
286
+ scope: Optional scope filter - "public", "private", or None (all files for owner)
287
+
288
+ Returns:
289
+ JSON string with file list based on scope and ownership
290
+ """
291
+ try:
292
+ from webagents.server.context.context_vars import get_context
293
+ from webagents.utils.logging import get_logger
294
+ logger = get_logger('webagents_files')
295
+
296
+ # Get context for agent information and auth
297
+ context = get_context()
298
+ if not context:
299
+ return json.dumps({
300
+ "success": False,
301
+ "error": "Agent context not available"
302
+ })
303
+
304
+ # Debug context attributes
305
+ print(f"🔍 DEBUG: Context available, type: {type(context)}")
306
+ print(f"🔍 DEBUG: Context attributes: {[attr for attr in dir(context) if not attr.startswith('_')]}")
307
+ if hasattr(context, 'custom_data'):
308
+ print(f"🔍 DEBUG: Context custom_data keys: {list(context.custom_data.keys())}")
309
+
310
+ agent_name = self._get_agent_name_from_context()
311
+
312
+ # Determine if current user is the actual owner of the agent
313
+ # SECURITY: Only actual owners should see private content, not just ADMINs
314
+ is_owner = False
315
+ try:
316
+ print(f"🔍 DEBUG: Determining actual ownership...")
317
+
318
+ # Check if we have auth context with user info
319
+ current_user_id = None
320
+ if context.auth and hasattr(context.auth, 'user_id'):
321
+ current_user_id = context.auth.user_id
322
+ print(f"🔍 DEBUG: Current user ID from auth: {current_user_id}")
323
+
324
+ # Get agent owner ID
325
+ agent_owner_id = None
326
+ if hasattr(self.agent, 'owner_user_id'):
327
+ agent_owner_id = self.agent.owner_user_id
328
+ print(f"🔍 DEBUG: Agent owner ID: {agent_owner_id}")
329
+ elif hasattr(self.agent, 'userId'):
330
+ agent_owner_id = self.agent.userId
331
+ print(f"🔍 DEBUG: Agent userId: {agent_owner_id}")
332
+
333
+ # Check if current user is the actual owner
334
+ if current_user_id and agent_owner_id:
335
+ is_owner = current_user_id == agent_owner_id
336
+ print(f"🔍 DEBUG: Ownership check: {current_user_id} == {agent_owner_id} = {is_owner}")
337
+ else:
338
+ print(f"🔍 DEBUG: Missing user ID or agent owner ID, defaulting to non-owner")
339
+ is_owner = False
340
+
341
+ print(f"🔍 DEBUG: Final isOwner determination: {is_owner}")
342
+ except Exception as e:
343
+ print(f"🔍 DEBUG: Error determining ownership: {e}")
344
+ is_owner = False
345
+
346
+ # Build URL with query parameters
347
+ url = f"{self.portal_url}/api/content/agent"
348
+ params = []
349
+
350
+ # Add isOwner parameter for security filtering
351
+ params.append(f"isOwner={str(is_owner).lower()}")
352
+
353
+ # Add scope parameter for filtering based on ownership and visibility
354
+ if scope:
355
+ params.append(f"scope={scope}")
356
+
357
+ if params:
358
+ url += "?" + "&".join(params)
359
+
360
+ # Make request to new agent content endpoint
361
+ api_key_prefix = self.agent_api_key[:20] + "..." if len(self.agent_api_key) > 20 else self.agent_api_key
362
+ print(f"🔍 DEBUG: Calling /api/content/agent using API key: {api_key_prefix}")
363
+ print(f"🔍 DEBUG: Final URL: {url}")
364
+ print(f"🔍 DEBUG: isOwner parameter being sent: {is_owner}")
365
+
366
+ # Make request with agent API key
367
+ async with aiohttp.ClientSession() as session:
368
+ headers = {
369
+ "Authorization": f"Bearer {self.agent_api_key}",
370
+ "Content-Type": "application/json"
371
+ }
372
+
373
+ async with session.get(url, headers=headers) as response:
374
+ if response.status != 200:
375
+ error_text = await response.text()
376
+ logger.error(f"Agent content API error: {response.status} - {error_text}")
377
+ return json.dumps({
378
+ "success": False,
379
+ "error": f"Failed to list files: HTTP {response.status}"
380
+ })
381
+
382
+ data = await response.json()
383
+ logger.debug(f"API Response status: {response.status}")
384
+ logger.debug(f"API Response content count: {len(data.get('content', []))}")
385
+ logger.debug(f"API Response scope info: {data.get('scope', {})}")
386
+
387
+ # Log each file for debugging
388
+ for item in data.get('content', []):
389
+ logger.debug(f"File: {item.get('fileName')} - Visibility: {item.get('visibility')}")
390
+
391
+ # Extract relevant fields from response
392
+ files = []
393
+ for item in data.get('content', []):
394
+ files.append({
395
+ "id": item.get('id'),
396
+ "filename": item.get('fileName'),
397
+ "original_filename": item.get('originalFileName'),
398
+ "size": item.get('size'),
399
+ "uploaded_at": item.get('uploadedAt'),
400
+ "description": item.get('description'),
401
+ "content_type": item.get('contentType'),
402
+ "url": self._rewrite_public_url(item.get('url')),
403
+ "visibility": item.get('visibility'),
404
+ "tags": item.get('tags', [])
405
+ })
406
+
407
+ return json.dumps({
408
+ "success": True,
409
+ "agent_name": data.get('agent', {}).get('name', agent_name),
410
+ "total_files": len(files),
411
+ "files": files
412
+ }, indent=2)
413
+
414
+ except Exception as e:
415
+ logger.error(f"Error in list_files: {e}")
416
+ return json.dumps({
417
+ "success": False,
418
+ "error": f"Failed to list files: {str(e)}"
419
+ })
420
+
421
+ def get_skill_info(self) -> Dict[str, Any]:
422
+ """Get comprehensive skill information"""
423
+ return {
424
+ "name": "RobutlerFilesSkill",
425
+ "description": "File management using harmonized content API",
426
+ "version": "1.2.0",
427
+ "capabilities": [
428
+ "Download and store files from URLs (owner scope only)",
429
+ "Store files from base64 data (owner scope only)",
430
+ "List agent-accessible files using new API",
431
+ "Automatic agent name prefixing for uploaded files",
432
+ "Integration with harmonized content API",
433
+ "Simplified agent access management"
434
+ ],
435
+ "tools": [
436
+ "store_file_from_url",
437
+ "store_file_from_base64",
438
+ "list_files"
439
+ ],
440
+ "config": {
441
+ "portal_url": self.portal_url,
442
+ "api_key_configured": bool(self.api_key),
443
+ "api_version": "harmonized"
444
+ }
445
+ }
@@ -0,0 +1,9 @@
1
+ """
2
+ WebAgents JSON Storage Skill
3
+
4
+ JSON data storage capabilities for long-term memory and agent data persistence.
5
+ """
6
+
7
+ from .skill import RobutlerJSONSkill
8
+
9
+ __all__ = ['RobutlerJSONSkill']