webagents 0.1.13__py3-none-any.whl → 0.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- webagents/__init__.py +1 -1
- webagents/__main__.py +55 -0
- webagents/agents/__init__.py +1 -1
- webagents/agents/core/__init__.py +1 -1
- webagents/agents/core/base_agent.py +15 -15
- webagents/agents/core/handoffs.py +1 -1
- webagents/agents/skills/__init__.py +11 -11
- webagents/agents/skills/base.py +1 -1
- webagents/agents/skills/core/llm/litellm/__init__.py +1 -1
- webagents/agents/skills/core/llm/litellm/skill.py +1 -1
- webagents/agents/skills/core/mcp/README.md +2 -2
- webagents/agents/skills/core/mcp/skill.py +2 -2
- webagents/agents/skills/core/memory/long_term_memory/memory_skill.py +14 -14
- webagents/agents/skills/core/memory/short_term_memory/__init__.py +1 -1
- webagents/agents/skills/core/memory/short_term_memory/skill.py +1 -1
- webagents/agents/skills/core/memory/vector_memory/skill.py +6 -6
- webagents/agents/skills/core/planning/__init__.py +1 -1
- webagents/agents/skills/ecosystem/crewai/__init__.py +3 -1
- webagents/agents/skills/ecosystem/crewai/skill.py +158 -0
- webagents/agents/skills/ecosystem/database/__init__.py +3 -1
- webagents/agents/skills/ecosystem/database/skill.py +522 -0
- webagents/agents/skills/ecosystem/google/calendar/skill.py +1 -1
- webagents/agents/skills/ecosystem/mongodb/__init__.py +3 -0
- webagents/agents/skills/ecosystem/mongodb/skill.py +428 -0
- webagents/agents/skills/ecosystem/n8n/README.md +287 -0
- webagents/agents/skills/ecosystem/n8n/__init__.py +3 -0
- webagents/agents/skills/ecosystem/n8n/skill.py +341 -0
- webagents/agents/skills/ecosystem/x_com/README.md +401 -0
- webagents/agents/skills/ecosystem/x_com/__init__.py +3 -0
- webagents/agents/skills/ecosystem/x_com/skill.py +1048 -0
- webagents/agents/skills/ecosystem/zapier/README.md +363 -0
- webagents/agents/skills/ecosystem/zapier/__init__.py +3 -0
- webagents/agents/skills/ecosystem/zapier/skill.py +337 -0
- webagents/agents/skills/robutler/__init__.py +2 -2
- webagents/agents/skills/robutler/auth/__init__.py +3 -3
- webagents/agents/skills/robutler/auth/skill.py +16 -16
- webagents/agents/skills/robutler/crm/__init__.py +2 -2
- webagents/agents/skills/robutler/crm/skill.py +5 -5
- webagents/agents/skills/robutler/discovery/README.md +5 -5
- webagents/agents/skills/robutler/discovery/__init__.py +2 -2
- webagents/agents/skills/robutler/discovery/skill.py +21 -21
- webagents/agents/skills/robutler/message_history/__init__.py +2 -2
- webagents/agents/skills/robutler/message_history/skill.py +5 -5
- webagents/agents/skills/robutler/nli/__init__.py +1 -1
- webagents/agents/skills/robutler/nli/skill.py +9 -9
- webagents/agents/skills/robutler/payments/__init__.py +3 -3
- webagents/agents/skills/robutler/payments/exceptions.py +1 -1
- webagents/agents/skills/robutler/payments/skill.py +23 -23
- webagents/agents/skills/robutler/storage/__init__.py +2 -2
- webagents/agents/skills/robutler/storage/files/__init__.py +2 -2
- webagents/agents/skills/robutler/storage/files/skill.py +4 -4
- webagents/agents/skills/robutler/storage/json/__init__.py +1 -1
- webagents/agents/skills/robutler/storage/json/skill.py +3 -3
- webagents/agents/skills/robutler/storage/kv/skill.py +3 -3
- webagents/agents/skills/robutler/storage.py +6 -6
- webagents/agents/tools/decorators.py +12 -12
- webagents/server/__init__.py +3 -3
- webagents/server/context/context_vars.py +2 -2
- webagents/server/core/app.py +13 -13
- webagents/server/core/middleware.py +3 -3
- webagents/server/core/models.py +1 -1
- webagents/server/core/monitoring.py +2 -2
- webagents/server/middleware.py +1 -1
- webagents/server/models.py +2 -2
- webagents/server/monitoring.py +15 -15
- webagents/utils/logging.py +20 -20
- webagents-0.2.2.dist-info/METADATA +266 -0
- webagents-0.2.2.dist-info/RECORD +105 -0
- webagents-0.2.2.dist-info/licenses/LICENSE +20 -0
- webagents/api/__init__.py +0 -17
- webagents/api/client.py +0 -1207
- webagents/api/types.py +0 -253
- webagents-0.1.13.dist-info/METADATA +0 -32
- webagents-0.1.13.dist-info/RECORD +0 -96
- webagents-0.1.13.dist-info/licenses/LICENSE +0 -1
- {webagents-0.1.13.dist-info → webagents-0.2.2.dist-info}/WHEEL +0 -0
- {webagents-0.1.13.dist-info → webagents-0.2.2.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()
|