webagents 0.1.13__py3-none-any.whl → 0.2.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 (62) hide show
  1. webagents/__init__.py +1 -1
  2. webagents/__main__.py +55 -0
  3. webagents/agents/__init__.py +1 -1
  4. webagents/agents/core/__init__.py +1 -1
  5. webagents/agents/core/base_agent.py +15 -15
  6. webagents/agents/core/handoffs.py +1 -1
  7. webagents/agents/skills/__init__.py +11 -11
  8. webagents/agents/skills/base.py +1 -1
  9. webagents/agents/skills/core/llm/litellm/__init__.py +1 -1
  10. webagents/agents/skills/core/llm/litellm/skill.py +1 -1
  11. webagents/agents/skills/core/mcp/README.md +2 -2
  12. webagents/agents/skills/core/mcp/skill.py +2 -2
  13. webagents/agents/skills/core/memory/long_term_memory/memory_skill.py +14 -14
  14. webagents/agents/skills/core/memory/short_term_memory/__init__.py +1 -1
  15. webagents/agents/skills/core/memory/short_term_memory/skill.py +1 -1
  16. webagents/agents/skills/core/memory/vector_memory/skill.py +6 -6
  17. webagents/agents/skills/core/planning/__init__.py +1 -1
  18. webagents/agents/skills/ecosystem/google/calendar/skill.py +1 -1
  19. webagents/agents/skills/robutler/__init__.py +2 -2
  20. webagents/agents/skills/robutler/auth/__init__.py +3 -3
  21. webagents/agents/skills/robutler/auth/skill.py +16 -16
  22. webagents/agents/skills/robutler/crm/__init__.py +2 -2
  23. webagents/agents/skills/robutler/crm/skill.py +5 -5
  24. webagents/agents/skills/robutler/discovery/README.md +5 -5
  25. webagents/agents/skills/robutler/discovery/__init__.py +2 -2
  26. webagents/agents/skills/robutler/discovery/skill.py +21 -21
  27. webagents/agents/skills/robutler/message_history/__init__.py +2 -2
  28. webagents/agents/skills/robutler/message_history/skill.py +5 -5
  29. webagents/agents/skills/robutler/nli/__init__.py +1 -1
  30. webagents/agents/skills/robutler/nli/skill.py +9 -9
  31. webagents/agents/skills/robutler/payments/__init__.py +3 -3
  32. webagents/agents/skills/robutler/payments/exceptions.py +1 -1
  33. webagents/agents/skills/robutler/payments/skill.py +23 -23
  34. webagents/agents/skills/robutler/storage/__init__.py +2 -2
  35. webagents/agents/skills/robutler/storage/files/__init__.py +2 -2
  36. webagents/agents/skills/robutler/storage/files/skill.py +4 -4
  37. webagents/agents/skills/robutler/storage/json/__init__.py +1 -1
  38. webagents/agents/skills/robutler/storage/json/skill.py +3 -3
  39. webagents/agents/skills/robutler/storage/kv/skill.py +3 -3
  40. webagents/agents/skills/robutler/storage.py +6 -6
  41. webagents/agents/tools/decorators.py +12 -12
  42. webagents/server/__init__.py +3 -3
  43. webagents/server/context/context_vars.py +2 -2
  44. webagents/server/core/app.py +13 -13
  45. webagents/server/core/middleware.py +3 -3
  46. webagents/server/core/models.py +1 -1
  47. webagents/server/core/monitoring.py +2 -2
  48. webagents/server/middleware.py +1 -1
  49. webagents/server/models.py +2 -2
  50. webagents/server/monitoring.py +15 -15
  51. webagents/utils/logging.py +20 -20
  52. webagents-0.2.0.dist-info/METADATA +242 -0
  53. webagents-0.2.0.dist-info/RECORD +94 -0
  54. webagents-0.2.0.dist-info/licenses/LICENSE +20 -0
  55. webagents/api/__init__.py +0 -17
  56. webagents/api/client.py +0 -1207
  57. webagents/api/types.py +0 -253
  58. webagents-0.1.13.dist-info/METADATA +0 -32
  59. webagents-0.1.13.dist-info/RECORD +0 -96
  60. webagents-0.1.13.dist-info/licenses/LICENSE +0 -1
  61. {webagents-0.1.13.dist-info → webagents-0.2.0.dist-info}/WHEEL +0 -0
  62. {webagents-0.1.13.dist-info → webagents-0.2.0.dist-info}/entry_points.txt +0 -0
webagents/api/client.py DELETED
@@ -1,1207 +0,0 @@
1
- """
2
- Robutler API Client - Robutler V2.0
3
-
4
- HTTP client for integrating with Robutler Platform services.
5
- Provides authentication, user management, payment, and other platform APIs.
6
- """
7
-
8
- import os
9
- import asyncio
10
- import aiohttp
11
- import json
12
- from datetime import datetime
13
- from typing import Dict, Any, List, Optional, Union
14
- from decimal import Decimal
15
-
16
- from .types import (
17
- User, ApiKey, Integration, CreditTransaction,
18
- AuthResponse, ApiResponse,
19
- UserRole, SubscriptionStatus, TransactionType
20
- )
21
-
22
-
23
- class RobutlerAPIError(Exception):
24
- """Raised when Robutler API returns an error"""
25
- def __init__(self, message: str, status_code: int = 400, response_data: Optional[Dict] = None):
26
- super().__init__(message)
27
- self.status_code = status_code
28
- self.response_data = response_data or {}
29
-
30
-
31
- class Agent:
32
- """Agent model with attributes for clean access"""
33
-
34
- def __init__(self, data: Dict[str, Any]):
35
- # Handle nested agent data structure: {'agent': {...}} or flat {...}
36
- if 'agent' in data:
37
- agent_data = data['agent']
38
- self._raw_data = data # Keep full response structure
39
- else:
40
- agent_data = data
41
- self._raw_data = data
42
-
43
- # Extract all agent properties from the agent_data
44
- self.id: str = agent_data.get("id", "") or agent_data.get("agentId", "")
45
- self.name: str = agent_data.get("name", "")
46
- self.instructions: str = agent_data.get("instructions", "")
47
- self.model: str = agent_data.get("model", "gpt-4o-mini")
48
- self.intents: List[str] = agent_data.get("intents", [])
49
- self.can_use_other_agents: bool = agent_data.get("canTalkToOtherAgents", False)
50
- self.other_agents_can_talk: bool = agent_data.get("otherAgentsCanTalk", False)
51
- self.credits_per_token: Optional[float] = self._parse_float(agent_data.get("creditsPerToken"))
52
- self.minimum_balance: Optional[float] = self._parse_float(agent_data.get("minimumBalance"))
53
- self.agent_pricing_percent: Optional[float] = self._parse_float(agent_data.get("agentPricingPercent"))
54
- self.is_public: bool = agent_data.get("isPublic", False)
55
- self.description: str = agent_data.get("description", "")
56
- self.avatar_url: Optional[str] = agent_data.get("avatarUrl")
57
- self.greeting_message: Optional[str] = agent_data.get("greetingMessage")
58
- self.suggested_actions: List[str] = agent_data.get("suggestedActions", [])
59
- self.greeting_image_mobile: Optional[str] = agent_data.get("greetingImageMobile")
60
- self.greeting_image_desktop: Optional[str] = agent_data.get("greetingImageDesktop")
61
- self.skills: Optional[Dict[str, Any]] = agent_data.get("skills")
62
- self.api_key_encrypted: Optional[str] = agent_data.get("apiKey") # This is encrypted
63
-
64
- def _parse_float(self, value) -> Optional[float]:
65
- """Parse float value from string or number"""
66
- if value is None:
67
- return None
68
- try:
69
- return float(value)
70
- except (ValueError, TypeError):
71
- return None
72
-
73
- def to_dict(self) -> Dict[str, Any]:
74
- """Convert back to dictionary format - returns the agent data only"""
75
- if 'agent' in self._raw_data:
76
- return self._raw_data['agent']
77
- return self._raw_data
78
-
79
- def get_full_data(self) -> Dict[str, Any]:
80
- """Get the full raw data structure"""
81
- return self._raw_data
82
-
83
- def __repr__(self) -> str:
84
- return f"Agent(id='{self.id}', name='{self.name}', model='{self.model}')"
85
-
86
-
87
- class ContentFile:
88
- """Content file model with attributes for clean access"""
89
-
90
- def __init__(self, data: Dict[str, Any]):
91
- self.id: str = data.get("id", "")
92
- self.name: str = data.get("originalFileName", "")
93
- self.size: int = data.get("size", 0)
94
- self.url: str = data.get("url", "")
95
- self.visibility: str = data.get("visibility", "private")
96
- self._raw_data = data
97
-
98
- def to_dict(self) -> Dict[str, Any]:
99
- """Convert back to dictionary format"""
100
- return self._raw_data
101
-
102
- @property
103
- def size_formatted(self) -> str:
104
- """Get formatted file size"""
105
- if self.size > 1024 * 1024: # MB
106
- return f"{self.size / (1024 * 1024):.1f}MB"
107
- elif self.size > 1024: # KB
108
- return f"{self.size // 1024}KB"
109
- else: # Bytes
110
- return f"{self.size}B"
111
-
112
- def __repr__(self) -> str:
113
- return f"ContentFile(name='{self.name}', size='{self.size_formatted}')"
114
-
115
-
116
- class UserProfile:
117
- """User profile model with attributes for clean access"""
118
-
119
- def __init__(self, data: Dict[str, Any]):
120
- self.id: str = data.get("id", "")
121
- self.name: str = data.get("name", "")
122
- self.email: str = data.get("email", "")
123
- self.role: str = data.get("role", "user")
124
- self.plan_name: str = data.get("planName", "")
125
- self.total_credits: Decimal = Decimal(str(data.get("totalCredits", "0")))
126
- self.used_credits: Decimal = Decimal(str(data.get("usedCredits", "0")))
127
- self.available_credits: Decimal = self.total_credits - self.used_credits
128
- self.referral_code: str = data.get("referralCode", "")
129
- self._raw_data = data
130
-
131
- def to_dict(self) -> Dict[str, Any]:
132
- """Convert back to dictionary format"""
133
- return self._raw_data
134
-
135
- def __repr__(self) -> str:
136
- return f"UserProfile(name='{self.name}', email='{self.email}', plan='{self.plan_name}')"
137
-
138
-
139
- class ApiKeyInfo:
140
- """API Key info model with attributes for clean access"""
141
-
142
- def __init__(self, data: Dict[str, Any]):
143
- self.id: str = data.get("id", "")
144
- self.name: str = data.get("name", "")
145
- self.key: str = data.get("key", "")
146
- self.created_at: str = data.get("createdAt", "")
147
- self.last_used: Optional[str] = data.get("lastUsed")
148
- self.permissions: Dict[str, Any] = data.get("permissions", {})
149
- self._raw_data = data
150
-
151
- def to_dict(self) -> Dict[str, Any]:
152
- """Convert back to dictionary format"""
153
- return self._raw_data
154
-
155
- def __repr__(self) -> str:
156
- return f"ApiKeyInfo(name='{self.name}', id='{self.id}')"
157
-
158
-
159
- class TransactionInfo:
160
- """Transaction info model with attributes for clean access"""
161
-
162
- def __init__(self, data: Dict[str, Any]):
163
- self.id: str = data.get("id", "")
164
- self.type: str = data.get("type", "")
165
- self.amount: Decimal = Decimal(str(data.get("amount", "0")))
166
- self.description: str = data.get("description", "")
167
- self.created_at: str = data.get("createdAt", "")
168
- self.status: str = data.get("status", "")
169
- self._raw_data = data
170
-
171
- def to_dict(self) -> Dict[str, Any]:
172
- """Convert back to dictionary format"""
173
- return self._raw_data
174
-
175
- def __repr__(self) -> str:
176
- return f"TransactionInfo(type='{self.type}', amount={self.amount}, status='{self.status}')"
177
-
178
-
179
- class ChatCompletionResult:
180
- """Chat completion result model with attributes for clean access"""
181
-
182
- def __init__(self, data: Dict[str, Any]):
183
- self.id: str = data.get("id", "")
184
- self.choices: List[Dict[str, Any]] = data.get("choices", [])
185
- self.usage: Dict[str, Any] = data.get("usage", {})
186
- self.model: str = data.get("model", "")
187
- self._raw_data = data
188
-
189
- @property
190
- def content(self) -> str:
191
- """Get the main response content"""
192
- if self.choices:
193
- return self.choices[0].get("message", {}).get("content", "")
194
- return ""
195
-
196
- def to_dict(self) -> Dict[str, Any]:
197
- """Convert back to dictionary format"""
198
- return self._raw_data
199
-
200
- def __repr__(self) -> str:
201
- return f"ChatCompletionResult(model='{self.model}', choices={len(self.choices)})"
202
-
203
-
204
- class AgentsResource:
205
- """Agents resource for hierarchical API access"""
206
-
207
- def __init__(self, client):
208
- self._client = client
209
-
210
- async def list(self) -> List[Agent]:
211
- """List user's agents - returns list of Agent objects"""
212
- response = await self._client._make_request('GET', '/agents')
213
- if not response.success:
214
- raise RobutlerAPIError(f"Failed to list agents: {response.status_code}", response.status_code, response.data)
215
-
216
- agents_data = response.data.get("agents", [])
217
- return [Agent(agent_data) for agent_data in agents_data]
218
-
219
- async def get_by_name(self, name: str) -> Agent:
220
- """Get agent by name - returns Agent object"""
221
- response = await self._client._make_request('GET', f'/agents/by-name/{name}')
222
- if not response.success:
223
- raise RobutlerAPIError(f"Failed to get agent by name '{name}': {response.status_code}", response.status_code, response.data)
224
- return Agent(response.data)
225
-
226
- async def get_by_id(self, agent_id: str) -> Agent:
227
- """Get agent by ID - returns Agent object"""
228
- response = await self._client._make_request('GET', f'/agents/{agent_id}')
229
- if not response.success:
230
- raise RobutlerAPIError(f"Failed to get agent by ID '{agent_id}': {response.status_code}", response.status_code, response.data)
231
- return Agent(response.data)
232
-
233
- async def get(self, agent_id: str) -> 'AgentResource':
234
- """Get agent resource by ID"""
235
- return AgentResource(self._client, agent_id)
236
-
237
- async def create(self, agent_data: Dict[str, Any]) -> Agent:
238
- """Create a new agent - returns created Agent object"""
239
- response = await self._client._make_request('POST', '/agents', data=agent_data)
240
- if not response.success:
241
- raise RobutlerAPIError(f"Failed to create agent: {response.status_code}", response.status_code, response.data)
242
- return Agent(response.data)
243
-
244
- async def update(self, agent_id: str, agent_data: Dict[str, Any]) -> Agent:
245
- """Update an existing agent - returns updated Agent object"""
246
- response = await self._client._make_request('PUT', f'/agents/{agent_id}', data=agent_data)
247
- if not response.success:
248
- raise RobutlerAPIError(f"Failed to update agent {agent_id}: {response.status_code}", response.status_code, response.data)
249
- return Agent(response.data)
250
-
251
- async def delete(self, agent_id: str) -> bool:
252
- """Delete an agent - returns True if successful"""
253
- response = await self._client._make_request('DELETE', f'/agents/{agent_id}')
254
- if not response.success:
255
- raise RobutlerAPIError(f"Failed to delete agent {agent_id}: {response.status_code}", response.status_code, response.data)
256
- return True
257
-
258
- async def search(self, query: str, max_results: int = 10, mode: str = 'semantic', min_similarity: float = 0.7) -> List[Dict[str, Any]]:
259
- """Search for agents - returns list of agent results"""
260
- search_data = {
261
- 'query': query,
262
- 'fields': ['name', 'description', 'intents']
263
- }
264
-
265
- response = await self._client._make_request('POST', '/agents/search', data=search_data)
266
- if not response.success:
267
- raise RobutlerAPIError(f"Failed to search agents: {response.message}", response.status_code, response.data)
268
-
269
- return response.data.get('agents', [])
270
-
271
- async def discover(self, capabilities: List[str], max_results: int = 10) -> List[Dict[str, Any]]:
272
- """Discover agents by capabilities - returns list of agent results"""
273
- discovery_params = {
274
- 'capabilities': capabilities,
275
- 'limit': max_results
276
- }
277
-
278
- response = await self._client._make_request('GET', '/agents/discover', params=discovery_params)
279
- if not response.success:
280
- raise RobutlerAPIError(f"Failed to discover agents: {response.message}", response.status_code, response.data)
281
-
282
- return response.data.get('agents', [])
283
-
284
- async def find_similar(self, agent_id: str, max_results: int = 10) -> List[Dict[str, Any]]:
285
- """Find similar agents - returns list of agent results"""
286
- response = await self._client._make_request('GET', f'/agents/{agent_id}/similar', params={'limit': max_results})
287
- if not response.success:
288
- raise RobutlerAPIError(f"Failed to find similar agents: {response.message}", response.status_code, response.data)
289
-
290
- return response.data.get('agents', [])
291
-
292
-
293
- class AgentResource:
294
- """Individual agent resource"""
295
-
296
- def __init__(self, client, agent_id: str):
297
- self._client = client
298
- self.agent_id = agent_id
299
-
300
- async def get(self) -> Agent:
301
- """Get agent details - returns Agent object"""
302
- response = await self._client._make_request('GET', f'/agents/{self.agent_id}')
303
- if not response.success:
304
- raise RobutlerAPIError(f"Failed to get agent {self.agent_id}: {response.status_code}", response.status_code, response.data)
305
- return Agent(response.data)
306
-
307
- async def api_key(self) -> str:
308
- """Get API key for this agent - returns the API key string"""
309
- response = await self._client._make_request('GET', f'/agents/{self.agent_id}/api-key')
310
- if not response.success:
311
- raise RobutlerAPIError(f"Failed to get API key for agent {self.agent_id}: {response.status_code}", response.status_code, response.data)
312
-
313
- api_key = response.data.get("apiKey")
314
- if not api_key:
315
- raise RobutlerAPIError(f"No API key found for agent {self.agent_id}")
316
- return api_key
317
-
318
- async def chat_completion(self, data: Dict[str, Any]) -> ChatCompletionResult:
319
- """Send chat completion request to this agent - returns ChatCompletionResult"""
320
- response = await self._client._make_request('POST', f'/agents/{self.agent_id}/chat/completions', data=data)
321
- if not response.success:
322
- raise RobutlerAPIError(f"Chat completion failed for agent {self.agent_id}: {response.status_code}", response.status_code, response.data)
323
- return ChatCompletionResult(response.data)
324
-
325
- async def get_intents(self) -> List[Dict[str, Any]]:
326
- """Get published intents for this agent - returns list of intent objects"""
327
- response = await self._client._make_request('GET', f'/agents/{self.agent_id}/intents')
328
- if not response.success:
329
- raise RobutlerAPIError(f"Failed to get intents for agent {self.agent_id}: {response.message}", response.status_code, response.data)
330
-
331
- return response.data.get('intents', [])
332
-
333
-
334
- class IntentsResource:
335
- """Intents resource for hierarchical API access"""
336
-
337
- def __init__(self, client):
338
- self._client = client
339
-
340
- async def publish(self, intents_data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
341
- """Publish intents - returns list of publish results"""
342
- data = {'intents': intents_data}
343
- response = await self._client._make_request('POST', '/intents/publish', data=data)
344
- if not response.success:
345
- raise RobutlerAPIError(f"Failed to publish intents: {response.message}", response.status_code, response.data)
346
-
347
- return response.data.get('results', [])
348
-
349
-
350
- class ContentResource:
351
- """Content resource for hierarchical API access"""
352
-
353
- def __init__(self, client):
354
- self._client = client
355
-
356
- async def agent_access(self, visibility: str = 'public') -> List[ContentFile]:
357
- """Get agent-accessible content - returns list of ContentFile objects
358
-
359
- Agent ID is automatically inferred from the API key used for authentication.
360
- """
361
- params = {'visibility': visibility}
362
-
363
- # Debug logging
364
- api_key_prefix = self._client.api_key[:20] + "..." if self._client.api_key and len(self._client.api_key) > 20 else self._client.api_key
365
- print(f"🌐 API Request: GET /content/agent-access?visibility={visibility}")
366
- print(f"🔑 Using API key: {api_key_prefix}")
367
- print(f"🏢 Base URL: {self._client.base_url}")
368
-
369
- response = await self._client._make_request('GET', '/content/agent-access', params=params)
370
-
371
- print(f"📡 Response: status={response.status_code}, success={response.success}")
372
- if response.data:
373
- files_count = len(response.data.get('files', []))
374
- print(f"📁 Files in response: {files_count}")
375
-
376
- # Debug: show first few files if any
377
- files_data = response.data.get('files', [])
378
- for i, file_data in enumerate(files_data[:3]): # Show first 3 files
379
- print(f" File {i+1}: {file_data.get('originalFileName', 'unknown')} - tags: {file_data.get('tags', [])}")
380
- else:
381
- print(f"📁 No data in response")
382
-
383
- if not response.success:
384
- print(f"❌ Error: {response.message}")
385
- raise RobutlerAPIError(f"Failed to get agent content: {response.status_code}", response.status_code, response.data)
386
-
387
- files_data = response.data.get('files', [])
388
- # Rewrite URLs for public serving via chat
389
- for file_data in files_data:
390
- if isinstance(file_data, dict) and 'url' in file_data:
391
- file_data['url'] = self._client._rewrite_public_url(file_data.get('url'))
392
- return [ContentFile(file_data) for file_data in files_data]
393
-
394
- async def list(self,
395
- visibility: Optional[str] = None,
396
- tags: Optional[str] = None,
397
- limit: Optional[int] = None,
398
- offset: Optional[int] = None) -> List[ContentFile]:
399
- """List user's content files - returns list of ContentFile objects"""
400
- params = {}
401
- if visibility:
402
- params['visibility'] = visibility
403
- if tags:
404
- params['tags'] = tags
405
- if limit:
406
- params['limit'] = limit
407
- if offset:
408
- params['offset'] = offset
409
-
410
- response = await self._client._make_request('GET', '/content', params=params)
411
- if not response.success:
412
- raise RobutlerAPIError(f"Failed to list content: {response.status_code}", response.status_code, response.data)
413
-
414
- files_data = response.data.get('files', [])
415
- for file_data in files_data:
416
- if isinstance(file_data, dict) and 'url' in file_data:
417
- file_data['url'] = self._client._rewrite_public_url(file_data.get('url'))
418
- return [ContentFile(file_data) for file_data in files_data]
419
-
420
- async def upload(self, file_data: bytes, filename: str, visibility: str = 'private') -> ContentFile:
421
- """Upload content file - returns ContentFile object"""
422
- data = {
423
- 'file': file_data,
424
- 'filename': filename,
425
- 'visibility': visibility
426
- }
427
- response = await self._client._make_request('POST', '/content', data=data)
428
- if not response.success:
429
- raise RobutlerAPIError(f"Failed to upload file: {response.status_code}", response.status_code, response.data)
430
- return ContentFile(response.data)
431
-
432
- async def delete(self, file_id: str) -> bool:
433
- """Delete content file - returns True if successful"""
434
- response = await self._client._make_request('DELETE', f'/content/{file_id}')
435
- if not response.success:
436
- raise RobutlerAPIError(f"Failed to delete file {file_id}: {response.status_code}", response.status_code, response.data)
437
- return True
438
-
439
-
440
- class UserResource:
441
- """User resource for hierarchical API access"""
442
-
443
- def __init__(self, client):
444
- self._client = client
445
-
446
- async def get(self) -> UserProfile:
447
- """Get current user profile - returns UserProfile object"""
448
- response = await self._client._make_request('GET', '/user')
449
- if not response.success:
450
- raise RobutlerAPIError(f"Failed to get user: {response.status_code}", response.status_code, response.data)
451
- # API returns shape { user: { ... } }
452
- user_data = response.data.get('user', response.data or {})
453
- return UserProfile(user_data)
454
-
455
- async def credits(self) -> Decimal:
456
- """Get user's available credits - returns Decimal"""
457
- response = await self._client._make_request('GET', '/user/credits')
458
- if not response.success:
459
- raise RobutlerAPIError(f"Failed to get credits: {response.status_code}", response.status_code, response.data)
460
-
461
- # Prefer server-computed availableCredits (includes plan_credits if server supports it)
462
- if isinstance(response.data, dict) and 'availableCredits' in response.data:
463
- try:
464
- return Decimal(str(response.data.get('availableCredits', '0')))
465
- except Exception:
466
- pass
467
-
468
- # Fallback to local calculation if server didn't provide it
469
- total_credits = Decimal(str(response.data.get('totalCredits', '0'))) if isinstance(response.data, dict) else Decimal('0')
470
- used_credits = Decimal(str(response.data.get('usedCredits', '0'))) if isinstance(response.data, dict) else Decimal('0')
471
- return total_credits - used_credits
472
-
473
- async def transactions(self, limit: int = 50) -> List[TransactionInfo]:
474
- """Get user's transaction history - returns list of TransactionInfo objects"""
475
- response = await self._client._make_request('GET', f'/user/transactions?limit={limit}')
476
- if not response.success:
477
- raise RobutlerAPIError(f"Failed to get transactions: {response.status_code}", response.status_code, response.data)
478
-
479
- transactions_data = response.data.get('transactions', [])
480
- return [TransactionInfo(transaction_data) for transaction_data in transactions_data]
481
-
482
-
483
- class ApiKeysResource:
484
- """API Keys resource for hierarchical API access"""
485
-
486
- def __init__(self, client):
487
- self._client = client
488
-
489
- async def list(self) -> List[ApiKeyInfo]:
490
- """List user's API keys - returns list of ApiKeyInfo objects"""
491
- response = await self._client._make_request('GET', '/api-keys')
492
- if not response.success:
493
- raise RobutlerAPIError(f"Failed to list API keys: {response.status_code}", response.status_code, response.data)
494
-
495
- keys_data = response.data.get('keys', [])
496
- return [ApiKeyInfo(key_data) for key_data in keys_data]
497
-
498
- async def create(self, name: str, permissions: Optional[Dict[str, Any]] = None) -> ApiKeyInfo:
499
- """Create new API key - returns ApiKeyInfo object"""
500
- data = {'name': name}
501
- if permissions:
502
- data['permissions'] = permissions
503
-
504
- response = await self._client._make_request('POST', '/api-keys', data=data)
505
- if not response.success:
506
- raise RobutlerAPIError(f"Failed to create API key: {response.status_code}", response.status_code, response.data)
507
- return ApiKeyInfo(response.data)
508
-
509
- async def delete(self, key_id: str) -> bool:
510
- """Delete API key - returns True if successful"""
511
- response = await self._client._make_request('DELETE', f'/api-keys/{key_id}')
512
- if not response.success:
513
- raise RobutlerAPIError(f"Failed to delete API key {key_id}: {response.status_code}", response.status_code, response.data)
514
- return True
515
-
516
-
517
- class TokensResource:
518
- """Payment tokens resource for hierarchical API access"""
519
-
520
- def __init__(self, client):
521
- self._client = client
522
-
523
- async def validate(self, token: str) -> bool:
524
- """Validate payment token - returns True if valid"""
525
- response = await self._client._make_request('GET', '/tokens/validate', params={'token': token})
526
- if not response.success:
527
- return False
528
- return response.data.get('valid', False)
529
-
530
- async def get_balance(self, token: str) -> float:
531
- """Get payment token balance - returns balance as float"""
532
- # Use validate endpoint; it returns availableAmount
533
- response = await self._client._make_request('GET', '/tokens/validate', params={'token': token})
534
- if not response.success:
535
- raise RobutlerAPIError(
536
- f"Failed to get token balance: {response.message}",
537
- response.status_code,
538
- response.data,
539
- )
540
- amount_value = response.data.get('availableAmount', response.data.get('balance', 0.0))
541
- try:
542
- return float(amount_value)
543
- except (TypeError, ValueError):
544
- return 0.0
545
-
546
- async def validate_with_balance(self, token: str) -> Dict[str, Any]:
547
- """Validate token and get balance - returns dict with valid and balance"""
548
- response = await self._client._make_request('GET', '/tokens/validate', params={'token': token})
549
- if not response.success:
550
- error_msg = response.message or 'Validation failed'
551
- return {'valid': False, 'error': error_msg, 'balance': 0.0}
552
- valid = response.data.get('valid', False)
553
- amount_value = response.data.get('availableAmount', response.data.get('balance', 0.0))
554
- try:
555
- balance = float(amount_value)
556
- except (TypeError, ValueError):
557
- balance = 0.0
558
- return {'valid': valid, 'balance': balance}
559
-
560
- async def redeem(self, token: str, amount: Union[str, float], api_key_id: Optional[str] = None) -> bool:
561
- """Redeem/charge payment token - returns True if successful"""
562
- data = {
563
- 'token': token,
564
- 'amount': str(amount)
565
- }
566
- if api_key_id:
567
- data['apiKeyId'] = api_key_id
568
- response = await self._client._make_request('PUT', '/tokens/redeem', data=data)
569
- if not response.success:
570
- raise RobutlerAPIError(f"Failed to redeem token: {response.message}", response.status_code, response.data)
571
- return response.success
572
-
573
-
574
- class RobutlerClient:
575
- """
576
- Robutler Platform API Client
577
-
578
- Provides hierarchical access to:
579
- - client.agents.list() - Agent management
580
- - client.content.agent_access() - Content access
581
- - client.user.get() - User management
582
- - client.api_keys.list() - API key management
583
- """
584
-
585
- def __init__(self,
586
- api_key: Optional[str] = None,
587
- base_url: Optional[str] = None,
588
- timeout: int = 30,
589
- max_retries: int = 3):
590
- """
591
- Initialize Robutler API client
592
-
593
- Args:
594
- api_key: Robutler API key (or set ROBUTLER_API_KEY env var)
595
- base_url: Base URL for Robutler API (or set ROBUTLER_API_URL env var)
596
- timeout: Request timeout in seconds
597
- max_retries: Maximum number of retries for failed requests
598
- """
599
- self.api_key = api_key or os.getenv('ROBUTLER_API_KEY')
600
- # Prefer internal cluster URL in production, then public URL, then hosted default, then localhost
601
- resolved_base = (
602
- base_url
603
- or os.getenv('ROBUTLER_INTERNAL_API_URL')
604
- or os.getenv('ROBUTLER_API_URL')
605
- or 'https://robutler.ai'
606
- or 'http://localhost:3000'
607
- )
608
- self.base_url = resolved_base.rstrip('/')
609
- # Public content base URL used by chat frontend (e.g., http://localhost:3001)
610
- self.public_base_url = (os.getenv('ROBUTLER_CHAT_URL') or 'http://localhost:3001').rstrip('/')
611
- self.timeout = timeout
612
- self.max_retries = max_retries
613
-
614
- if not self.api_key:
615
- raise ValueError("Robutler API key is required. Set ROBUTLER_API_KEY environment variable or provide api_key parameter.")
616
-
617
- # Session for connection pooling
618
- self._session: Optional[aiohttp.ClientSession] = None
619
-
620
- # Initialize hierarchical resources
621
- self.agents = AgentsResource(self)
622
- self.content = ContentResource(self)
623
- self.user = UserResource(self)
624
- self.api_keys = ApiKeysResource(self)
625
- self.tokens = TokensResource(self)
626
- self.intents = IntentsResource(self)
627
-
628
- def _rewrite_public_url(self, url: Optional[str]) -> Optional[str]:
629
- """Rewrite portal public content URLs to the chat public base URL.
630
- - http(s)://<portal>/api/content/public/... -> http(s)://<chat>/api/content/public/...
631
- - /api/content/public/... -> <chat>/api/content/public/...
632
- """
633
- if not url:
634
- return url
635
- try:
636
- if url.startswith('/api/content/public'):
637
- return f"{self.public_base_url}{url}"
638
- portal_prefix = f"{self.base_url}/api/content/public"
639
- if url.startswith(portal_prefix):
640
- return url.replace(self.base_url, self.public_base_url, 1)
641
- except Exception:
642
- return url
643
- return url
644
-
645
- async def _get_session(self) -> aiohttp.ClientSession:
646
- """Get or create aiohttp session"""
647
- if self._session is None or self._session.closed:
648
- timeout = aiohttp.ClientTimeout(total=self.timeout)
649
- self._session = aiohttp.ClientSession(timeout=timeout)
650
- return self._session
651
-
652
- async def close(self):
653
- """Close the HTTP session"""
654
- if self._session and not self._session.closed:
655
- await self._session.close()
656
-
657
- async def __aenter__(self):
658
- """Async context manager entry"""
659
- return self
660
-
661
- async def __aexit__(self, exc_type, exc_val, exc_tb):
662
- """Async context manager exit"""
663
- await self.close()
664
-
665
- def _get_headers(self, additional_headers: Optional[Dict[str, str]] = None) -> Dict[str, str]:
666
- """Get request headers with authentication"""
667
- headers = {
668
- 'Content-Type': 'application/json',
669
- 'Authorization': f'Bearer {self.api_key}',
670
- 'X-API-Key': self.api_key,
671
- 'User-Agent': 'Robutler-V2-Client/1.0'
672
- }
673
-
674
- if additional_headers:
675
- headers.update(additional_headers)
676
-
677
- return headers
678
-
679
- async def _make_request(self,
680
- method: str,
681
- endpoint: str,
682
- data: Optional[Dict[str, Any]] = None,
683
- params: Optional[Dict[str, Any]] = None,
684
- headers: Optional[Dict[str, str]] = None) -> ApiResponse:
685
- """
686
- Make HTTP request to Robutler API with retries and error handling
687
-
688
- Args:
689
- method: HTTP method (GET, POST, PUT, DELETE)
690
- endpoint: API endpoint (without base URL)
691
- data: Request body data
692
- params: Query parameters
693
- headers: Additional headers
694
-
695
- Returns:
696
- ApiResponse with success/error information
697
- """
698
- url = f"{self.base_url}/api{endpoint}"
699
- request_headers = self._get_headers(headers)
700
-
701
- session = await self._get_session()
702
-
703
- for attempt in range(self.max_retries + 1):
704
- try:
705
- async with session.request(
706
- method=method,
707
- url=url,
708
- json=data if data else None,
709
- params=params,
710
- headers=request_headers
711
- ) as response:
712
-
713
- # Get response text
714
- response_text = await response.text()
715
-
716
- # Try to parse as JSON
717
- try:
718
- response_data = json.loads(response_text) if response_text else {}
719
- except json.JSONDecodeError:
720
- response_data = {'message': response_text}
721
-
722
- # Handle different status codes
723
- if response.status == 200:
724
- return ApiResponse(
725
- success=True,
726
- data=response_data,
727
- status_code=response.status
728
- )
729
- elif response.status == 401:
730
- return ApiResponse(
731
- success=False,
732
- error='Authentication failed',
733
- message=response_data.get('message', 'Invalid API key or token'),
734
- status_code=response.status
735
- )
736
- elif response.status == 403:
737
- return ApiResponse(
738
- success=False,
739
- error='Authorization failed',
740
- message=response_data.get('message', 'Insufficient permissions'),
741
- status_code=response.status
742
- )
743
- elif response.status == 404:
744
- return ApiResponse(
745
- success=False,
746
- error='Not found',
747
- message=response_data.get('message', 'Resource not found'),
748
- status_code=response.status
749
- )
750
- elif response.status >= 500:
751
- # Server error - retry
752
- if attempt < self.max_retries:
753
- await asyncio.sleep(2 ** attempt) # Exponential backoff
754
- continue
755
-
756
- return ApiResponse(
757
- success=False,
758
- error='Server error',
759
- message=response_data.get('message', 'Internal server error'),
760
- status_code=response.status
761
- )
762
- else:
763
- return ApiResponse(
764
- success=False,
765
- error='Request failed',
766
- message=response_data.get('message', f'HTTP {response.status}'),
767
- status_code=response.status
768
- )
769
-
770
- except aiohttp.ClientError as e:
771
- if attempt < self.max_retries:
772
- await asyncio.sleep(2 ** attempt)
773
- continue
774
-
775
- return ApiResponse(
776
- success=False,
777
- error='Network error',
778
- message=str(e),
779
- status_code=0
780
- )
781
- except Exception as e:
782
- return ApiResponse(
783
- success=False,
784
- error='Unexpected error',
785
- message=str(e),
786
- status_code=0
787
- )
788
-
789
- return ApiResponse(
790
- success=False,
791
- error='Max retries exceeded',
792
- message=f'Failed after {self.max_retries + 1} attempts',
793
- status_code=0
794
- )
795
-
796
- # ===== USER MANAGEMENT METHODS =====
797
-
798
- async def get_user(self) -> AuthResponse:
799
- """Get current user information"""
800
- try:
801
- response = await self._make_request('GET', '/user')
802
-
803
- if not response.success:
804
- return AuthResponse(
805
- success=False,
806
- error=response.error,
807
- message=response.message
808
- )
809
-
810
- user_data = response.data.get('user', {}) if response.data else {}
811
-
812
- # Convert to User object
813
- user = User(
814
- id=user_data.get('id', ''),
815
- name=user_data.get('name'),
816
- email=user_data.get('email', ''),
817
- role=UserRole(user_data.get('role', 'user')),
818
- google_id=user_data.get('googleId'),
819
- avatar_url=user_data.get('avatarUrl'),
820
- stripe_customer_id=user_data.get('stripeCustomerId'),
821
- stripe_subscription_id=user_data.get('stripeSubscriptionId'),
822
- plan_name=user_data.get('planName'),
823
- total_credits=Decimal(user_data.get('totalCredits', '0')),
824
- used_credits=Decimal(user_data.get('usedCredits', '0')),
825
- referral_code=user_data.get('referralCode'),
826
- referred_by=user_data.get('referredBy'),
827
- referral_count=user_data.get('referralCount', 0)
828
- )
829
-
830
- return AuthResponse(success=True, user=user)
831
-
832
- except Exception as e:
833
- return AuthResponse(
834
- success=False,
835
- error='Failed to get user',
836
- message=str(e)
837
- )
838
-
839
- async def validate_api_key(self, api_key: str) -> AuthResponse:
840
- """Validate API key and get associated user"""
841
- try:
842
- # Create temporary client with the API key to test
843
- temp_headers = {
844
- 'Authorization': f'Bearer {api_key}',
845
- 'X-API-Key': api_key
846
- }
847
-
848
- response = await self._make_request('GET', '/user', headers=temp_headers)
849
-
850
- if not response.success:
851
- return AuthResponse(
852
- success=False,
853
- error='Invalid API key',
854
- message=response.message
855
- )
856
-
857
- user_data = response.data.get('user', {}) if response.data else {}
858
-
859
- # Convert to User object
860
- user = User(
861
- id=user_data.get('id', ''),
862
- name=user_data.get('name'),
863
- email=user_data.get('email', ''),
864
- role=UserRole(user_data.get('role', 'user'))
865
- )
866
-
867
- return AuthResponse(success=True, user=user)
868
-
869
- except Exception as e:
870
- return AuthResponse(
871
- success=False,
872
- error='API key validation failed',
873
- message=str(e)
874
- )
875
-
876
- async def get_user_credits(self) -> ApiResponse:
877
- """Get user credit information"""
878
- return await self._make_request('GET', '/user/credits')
879
-
880
- async def get_user_transactions(self, limit: int = 50, offset: int = 0) -> ApiResponse:
881
- """Get user transaction history"""
882
- params = {'limit': limit, 'offset': offset}
883
- return await self._make_request('GET', '/user/transactions', params=params)
884
-
885
- # ===== API KEY MANAGEMENT METHODS =====
886
-
887
- async def list_api_keys(self) -> ApiResponse:
888
- """List user's API keys"""
889
- return await self._make_request('GET', '/api-keys')
890
-
891
- async def create_api_key(self, name: str, permissions: Optional[Dict[str, Any]] = None) -> ApiResponse:
892
- """Create a new API key"""
893
- data = {
894
- 'name': name,
895
- 'permissions': permissions or {}
896
- }
897
- return await self._make_request('POST', '/api-keys', data=data)
898
-
899
- async def delete_api_key(self, api_key_id: str) -> ApiResponse:
900
- """Delete an API key"""
901
- return await self._make_request('DELETE', f'/api-keys/{api_key_id}')
902
-
903
- # ===== INTEGRATION METHODS =====
904
-
905
- async def list_integrations(self) -> ApiResponse:
906
- """List user's integrations"""
907
- return await self._make_request('GET', '/user/integrations')
908
-
909
- async def create_integration(self,
910
- name: str,
911
- integration_type: str = "api",
912
- protocol: str = "http") -> ApiResponse:
913
- """Create a new integration"""
914
- data = {
915
- 'name': name,
916
- 'type': integration_type,
917
- 'protocol': protocol
918
- }
919
- return await self._make_request('POST', '/user/integrations', data=data)
920
-
921
- # ===== CREDIT/PAYMENT METHODS =====
922
-
923
- async def track_usage(self,
924
- amount: Union[str, Decimal, float],
925
- description: str = "API usage",
926
- source: str = "api_usage",
927
- integration_id: Optional[str] = None) -> ApiResponse:
928
- """Track credit usage"""
929
- data = {
930
- 'amount': str(amount),
931
- 'type': 'usage',
932
- 'description': description,
933
- 'source': source
934
- }
935
- if integration_id:
936
- data['integration_id'] = integration_id
937
-
938
- return await self._make_request('POST', '/user/transactions', data=data)
939
-
940
- # ===== HEALTH/STATUS METHODS =====
941
-
942
- async def health_check(self) -> ApiResponse:
943
- """Check API health status"""
944
- return await self._make_request('GET', '/health')
945
-
946
- async def get_config(self) -> ApiResponse:
947
- """Get API configuration"""
948
- return await self._make_request('GET', '/config')
949
-
950
- # ===== UTILITY METHODS =====
951
-
952
- def _parse_user_data(self, user_data: Dict[str, Any]) -> User:
953
- """Parse user data from API response into User object"""
954
- return User(
955
- id=user_data.get('id', ''),
956
- name=user_data.get('name'),
957
- email=user_data.get('email', ''),
958
- role=UserRole(user_data.get('role', 'user')),
959
- google_id=user_data.get('googleId'),
960
- avatar_url=user_data.get('avatarUrl'),
961
- created_at=self._parse_datetime(user_data.get('createdAt')),
962
- updated_at=self._parse_datetime(user_data.get('updatedAt')),
963
- stripe_customer_id=user_data.get('stripeCustomerId'),
964
- stripe_subscription_id=user_data.get('stripeSubscriptionId'),
965
- stripe_product_id=user_data.get('stripeProductId'),
966
- plan_name=user_data.get('planName'),
967
- total_credits=Decimal(user_data.get('totalCredits', '0')),
968
- used_credits=Decimal(user_data.get('usedCredits', '0')),
969
- referral_code=user_data.get('referralCode'),
970
- referred_by=user_data.get('referredBy'),
971
- referral_count=user_data.get('referralCount', 0)
972
- )
973
-
974
- def _parse_datetime(self, date_str: Optional[str]) -> Optional[datetime]:
975
- """Parse datetime string from API response"""
976
- if not date_str:
977
- return None
978
-
979
- try:
980
- return datetime.fromisoformat(date_str.replace('Z', '+00:00'))
981
- except ValueError:
982
- return None
983
-
984
- # ===== AGENT MANAGEMENT METHODS =====
985
-
986
- # Legacy methods for backward compatibility - these wrap the new hierarchical methods
987
- async def list_agents(self) -> List[Agent]:
988
- """List user's agents (legacy - use client.agents.list())"""
989
- return await self.agents.list()
990
-
991
- async def get_agent_api_key(self, agent_id: str) -> str:
992
- """Get API key for an agent (legacy - use client.agents.get(id).api_key())"""
993
- agent_resource = await self.agents.get(agent_id)
994
- return await agent_resource.api_key()
995
-
996
- async def get_agent_content(self, visibility: str = 'public') -> List[ContentFile]:
997
- """Get agent-accessible content (legacy - use client.content.agent_access())"""
998
- return await self.content.agent_access(visibility)
999
-
1000
- # ===== CONTENT STORAGE METHODS =====
1001
-
1002
- async def list_content(self,
1003
- visibility: Optional[str] = None,
1004
- tags: Optional[str] = None,
1005
- limit: Optional[int] = None,
1006
- offset: Optional[int] = None) -> ApiResponse:
1007
- """List user's content files"""
1008
- params = {}
1009
- if visibility:
1010
- params['visibility'] = visibility
1011
- if tags:
1012
- params['tags'] = tags
1013
- if limit:
1014
- params['limit'] = limit
1015
- if offset:
1016
- params['offset'] = offset
1017
-
1018
- response = await self._make_request('GET', '/content', params=params)
1019
- if not response.success:
1020
- raise RobutlerAPIError(f"Failed to list content: {response.status_code}", response.status_code, response.data)
1021
-
1022
- files_data = response.data.get('files', [])
1023
- for file_data in files_data:
1024
- if isinstance(file_data, dict) and 'url' in file_data:
1025
- file_data['url'] = self._rewrite_public_url(file_data.get('url'))
1026
- return [ContentFile(file_data) for file_data in files_data]
1027
-
1028
- async def upload_content(self,
1029
- filename: str,
1030
- content_data: bytes,
1031
- content_type: str = 'application/json',
1032
- visibility: str = 'private',
1033
- description: Optional[str] = None,
1034
- tags: Optional[List[str]] = None) -> ApiResponse:
1035
- """Upload content to user's storage area"""
1036
- session = await self._get_session()
1037
- url = f"{self.base_url}/api/content"
1038
-
1039
- # Prepare form data
1040
- form_data = aiohttp.FormData()
1041
- form_data.add_field('file', content_data, filename=filename, content_type=content_type)
1042
- form_data.add_field('visibility', visibility)
1043
-
1044
- if description:
1045
- form_data.add_field('description', description)
1046
- if tags:
1047
- form_data.add_field('tags', json.dumps(tags))
1048
-
1049
- headers = {
1050
- 'Authorization': f'Bearer {self.api_key}',
1051
- 'X-API-Key': self.api_key,
1052
- 'User-Agent': 'Robutler-V2-Client/1.0'
1053
- }
1054
-
1055
- try:
1056
- async with session.post(url, data=form_data, headers=headers) as response:
1057
- response_text = await response.text()
1058
-
1059
- try:
1060
- response_data = json.loads(response_text) if response_text else {}
1061
- except json.JSONDecodeError:
1062
- response_data = {'message': response_text}
1063
-
1064
- if response.status == 200:
1065
- # Rewrite returned URL if present
1066
- if isinstance(response_data, dict) and 'url' in response_data:
1067
- response_data['url'] = self._rewrite_public_url(response_data.get('url'))
1068
- return ApiResponse(
1069
- success=True,
1070
- data=response_data,
1071
- status_code=response.status
1072
- )
1073
- else:
1074
- return ApiResponse(
1075
- success=False,
1076
- error='Upload failed',
1077
- message=response_data.get('error', f'HTTP {response.status}'),
1078
- status_code=response.status
1079
- )
1080
-
1081
- except Exception as e:
1082
- return ApiResponse(
1083
- success=False,
1084
- error='Upload error',
1085
- message=str(e),
1086
- status_code=0
1087
- )
1088
-
1089
- async def get_content(self, filename: str) -> ApiResponse:
1090
- """Get content file by filename"""
1091
- # First list content to find the file
1092
- list_response = await self.list_content(visibility='private')
1093
- if not list_response.success:
1094
- return list_response
1095
-
1096
- files = list_response.data.get('files', []) if list_response.data else []
1097
- target_file = None
1098
-
1099
- for file_info in files:
1100
- if file_info.get('fileName') == filename or file_info.get('originalFileName') == filename:
1101
- target_file = file_info
1102
- break
1103
-
1104
- if not target_file:
1105
- return ApiResponse(
1106
- success=False,
1107
- error='File not found',
1108
- message=f"File '{filename}' not found",
1109
- status_code=404
1110
- )
1111
-
1112
- # Get the file content from the URL
1113
- file_url = target_file.get('url')
1114
- if not file_url:
1115
- return ApiResponse(
1116
- success=False,
1117
- error='File URL not available',
1118
- message=f"No URL available for file '{filename}'",
1119
- status_code=404
1120
- )
1121
-
1122
- session = await self._get_session()
1123
- headers = self._get_headers()
1124
-
1125
- try:
1126
- async with session.get(file_url, headers=headers) as response:
1127
- if response.status == 200:
1128
- content_text = await response.text()
1129
-
1130
- # Try to parse as JSON
1131
- try:
1132
- content_data = json.loads(content_text)
1133
- except json.JSONDecodeError:
1134
- content_data = content_text
1135
-
1136
- return ApiResponse(
1137
- success=True,
1138
- data={
1139
- 'filename': target_file.get('fileName'),
1140
- 'content': content_data,
1141
- 'metadata': {
1142
- 'size': target_file.get('size'),
1143
- 'uploadedAt': target_file.get('uploadedAt'),
1144
- 'description': target_file.get('description'),
1145
- 'tags': target_file.get('tags', [])
1146
- }
1147
- },
1148
- status_code=response.status
1149
- )
1150
- else:
1151
- return ApiResponse(
1152
- success=False,
1153
- error='Failed to retrieve content',
1154
- message=f'HTTP {response.status}',
1155
- status_code=response.status
1156
- )
1157
-
1158
- except Exception as e:
1159
- return ApiResponse(
1160
- success=False,
1161
- error='Content retrieval error',
1162
- message=str(e),
1163
- status_code=0
1164
- )
1165
-
1166
- async def delete_content(self, filename: str) -> ApiResponse:
1167
- """Delete content file by filename"""
1168
- params = {'fileName': filename}
1169
- return await self._make_request('DELETE', '/content', params=params)
1170
-
1171
- async def update_content(self,
1172
- filename: str,
1173
- content_data: bytes,
1174
- content_type: str = 'application/json',
1175
- description: Optional[str] = None,
1176
- tags: Optional[List[str]] = None) -> ApiResponse:
1177
- """Update existing content file"""
1178
- # Delete old version first
1179
- delete_result = await self.delete_content(filename)
1180
- # Continue even if delete fails (file might not exist)
1181
-
1182
- # Upload new version
1183
- return await self.upload_content(
1184
- filename=filename,
1185
- content_data=content_data,
1186
- content_type=content_type,
1187
- visibility='private',
1188
- description=description,
1189
- tags=tags
1190
- )
1191
-
1192
- def __repr__(self) -> str:
1193
- """String representation of the client"""
1194
- return f"RobutlerClient(base_url='{self.base_url}', api_key='***{self.api_key[-4:] if self.api_key else None}')"
1195
-
1196
-
1197
- # ===== CONVENIENCE FUNCTIONS =====
1198
-
1199
- async def create_client(api_key: Optional[str] = None, base_url: Optional[str] = None) -> RobutlerClient:
1200
- """Create and return a Robutler API client"""
1201
- return RobutlerClient(api_key=api_key, base_url=base_url)
1202
-
1203
-
1204
- async def validate_api_key(api_key: str, base_url: Optional[str] = None) -> AuthResponse:
1205
- """Validate API key using a temporary client"""
1206
- async with RobutlerClient(api_key=api_key, base_url=base_url) as client:
1207
- return await client.get_user()