superu 2025.11.7.1__py3-none-any.whl → 2026.2.5.1__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.
- superu/core.py +1074 -209
- superu/example.py +111 -657
- superu-2026.2.5.1.dist-info/METADATA +522 -0
- superu-2026.2.5.1.dist-info/RECORD +7 -0
- {superu-2025.11.7.1.dist-info → superu-2026.2.5.1.dist-info}/WHEEL +1 -1
- superu-2025.11.7.1.dist-info/METADATA +0 -303
- superu-2025.11.7.1.dist-info/RECORD +0 -7
- {superu-2025.11.7.1.dist-info → superu-2026.2.5.1.dist-info}/top_level.txt +0 -0
superu/core.py
CHANGED
|
@@ -4,8 +4,10 @@ import urllib.parse
|
|
|
4
4
|
import re
|
|
5
5
|
import uuid
|
|
6
6
|
import base64
|
|
7
|
+
from typing import List, Dict, Optional, Tuple, Union, Any
|
|
8
|
+
import os
|
|
7
9
|
|
|
8
|
-
server_url = 'https://voip-middlware.superu.ai'
|
|
10
|
+
server_url = os.getenv('superU_SERVER_URL', 'https://voip-middlware.superu.ai')
|
|
9
11
|
# server_url = 'http://localhost:5000'
|
|
10
12
|
|
|
11
13
|
class CallWrapper:
|
|
@@ -97,6 +99,29 @@ class CallWrapper:
|
|
|
97
99
|
json={'api_key': self.api_key, "call_uuid": call_uuid, "custom_fields": custom_fields}
|
|
98
100
|
)
|
|
99
101
|
return response.json()
|
|
102
|
+
|
|
103
|
+
def create_outbound_call(self, assistant_id, to : str, from_ : str = None, customer_name : str = 'Unknown', customer_id : str = 'Unknown', variable_values : dict = None , ):
|
|
104
|
+
if not assistant_id :
|
|
105
|
+
raise ValueError("assistant_id is required")
|
|
106
|
+
|
|
107
|
+
payload = {
|
|
108
|
+
'api_key': self.api_key,
|
|
109
|
+
'assistant_id': assistant_id,
|
|
110
|
+
'campaign_id': campaign_id,
|
|
111
|
+
'to': to,
|
|
112
|
+
'customer_name': customer_name,
|
|
113
|
+
'customer_id': customer_id
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if from_:
|
|
117
|
+
payload['from'] = from_
|
|
118
|
+
if variable_values:
|
|
119
|
+
payload['variable_values'] = variable_values
|
|
120
|
+
|
|
121
|
+
response = requests.post(f'{server_url}/campaign/outbound/create_call/superu', json=payload)
|
|
122
|
+
if response.status_code != 200:
|
|
123
|
+
raise Exception(f"Failed to create outbound call: {response.status_code}, {response.text}")
|
|
124
|
+
return response.json()
|
|
100
125
|
|
|
101
126
|
|
|
102
127
|
# def __getattr__(self, name):
|
|
@@ -106,249 +131,438 @@ class CallWrapper:
|
|
|
106
131
|
class AssistantWrapper:
|
|
107
132
|
def __init__(self, api_key):
|
|
108
133
|
self.api_key = api_key
|
|
109
|
-
|
|
110
|
-
def
|
|
111
|
-
|
|
112
|
-
|
|
134
|
+
|
|
135
|
+
def create_version(self, agent_id, version, assistant_data, knowledge_base=None, tools=None, call_forwarding=None):
|
|
136
|
+
if not agent_id or not version or not assistant_data:
|
|
137
|
+
raise ValueError("agent_id, version, and assistant_data are required")
|
|
138
|
+
|
|
113
139
|
payload = {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
**kwargs
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
response = requests.post(f'{server_url}/pypi_support/assistant_create', json={**payload , 'api_key': self.api_key})
|
|
122
|
-
if response.status_code != 200:
|
|
123
|
-
raise Exception(f"Failed to create assistant: {response.status_code}, {response.text}")
|
|
124
|
-
return response.json()
|
|
125
|
-
|
|
126
|
-
def create_basic(self, name, voice_id, first_message , system_prompt):
|
|
127
|
-
|
|
128
|
-
exmaple_json = {
|
|
129
|
-
"name": name,
|
|
130
|
-
"voice": {
|
|
131
|
-
"model": "eleven_flash_v2_5",
|
|
132
|
-
"voiceId": voice_id,
|
|
133
|
-
"provider": "11labs",
|
|
134
|
-
"stability": 0.9,
|
|
135
|
-
"similarityBoost": 0.9,
|
|
136
|
-
"useSpeakerBoost": True,
|
|
137
|
-
"inputMinCharacters": 5
|
|
138
|
-
},
|
|
139
|
-
"model": {
|
|
140
|
-
"model": "gpt-4o-mini",
|
|
141
|
-
"messages": [
|
|
142
|
-
{
|
|
143
|
-
"role": "system",
|
|
144
|
-
"content": system_prompt
|
|
145
|
-
}
|
|
146
|
-
],
|
|
147
|
-
"provider": "openai",
|
|
148
|
-
"temperature": 0
|
|
149
|
-
},
|
|
150
|
-
"firstMessage": first_message,
|
|
151
|
-
"voicemailMessage": "Please call back when you're available.",
|
|
152
|
-
"endCallFunctionEnabled": True,
|
|
153
|
-
"endCallMessage": "Goodbye.Thank you.",
|
|
154
|
-
"transcriber": {
|
|
155
|
-
"model": "nova-2",
|
|
156
|
-
"language": "en",
|
|
157
|
-
"numerals": False,
|
|
158
|
-
"provider": "deepgram",
|
|
159
|
-
"endpointing": 300,
|
|
160
|
-
"confidenceThreshold": 0.4
|
|
161
|
-
},
|
|
162
|
-
"clientMessages": [
|
|
163
|
-
"transcript",
|
|
164
|
-
"hang",
|
|
165
|
-
"function-call",
|
|
166
|
-
"speech-update",
|
|
167
|
-
"metadata",
|
|
168
|
-
"transfer-update",
|
|
169
|
-
"conversation-update",
|
|
170
|
-
"workflow.node.started"
|
|
171
|
-
],
|
|
172
|
-
"serverMessages": [
|
|
173
|
-
"end-of-call-report",
|
|
174
|
-
"status-update",
|
|
175
|
-
"hang",
|
|
176
|
-
"function-call"
|
|
177
|
-
],
|
|
178
|
-
"hipaaEnabled": False,
|
|
179
|
-
"backgroundSound": "office",
|
|
180
|
-
"backchannelingEnabled": False,
|
|
181
|
-
"backgroundDenoisingEnabled": True,
|
|
182
|
-
"messagePlan": {
|
|
183
|
-
"idleMessages": [
|
|
184
|
-
"Are you still there?"
|
|
185
|
-
],
|
|
186
|
-
"idleMessageMaxSpokenCount": 2,
|
|
187
|
-
"idleTimeoutSeconds": 5
|
|
188
|
-
},
|
|
189
|
-
"startSpeakingPlan": {
|
|
190
|
-
"waitSeconds": 0.4,
|
|
191
|
-
"smartEndpointingEnabled": "livekit",
|
|
192
|
-
"smartEndpointingPlan": {
|
|
193
|
-
"provider": "vapi"
|
|
194
|
-
}
|
|
195
|
-
},
|
|
196
|
-
"stopSpeakingPlan": {
|
|
197
|
-
"numWords": 2,
|
|
198
|
-
"voiceSeconds": 0.3,
|
|
199
|
-
"backoffSeconds": 1
|
|
200
|
-
}
|
|
140
|
+
'api_key': self.api_key,
|
|
141
|
+
'agent_id': agent_id,
|
|
142
|
+
'version': version,
|
|
143
|
+
'assistant_data': assistant_data
|
|
201
144
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
145
|
+
|
|
146
|
+
if knowledge_base is not None:
|
|
147
|
+
payload['knowledgeBase'] = knowledge_base
|
|
148
|
+
if tools is not None:
|
|
149
|
+
payload['tools'] = tools
|
|
150
|
+
if call_forwarding is not None:
|
|
151
|
+
payload['call_forwarding'] = call_forwarding
|
|
152
|
+
|
|
153
|
+
response = requests.post(f'{server_url}/agent/version/create', json=payload)
|
|
154
|
+
if response.status_code != 200:
|
|
155
|
+
raise Exception(f"Failed to create agent version: {response.status_code}, {response.text}")
|
|
156
|
+
return response.json()
|
|
157
|
+
|
|
158
|
+
def update_version(self, agent_id, version_id, version=None, assistant_data=None, composio_app=None):
|
|
159
|
+
if not version_id or not agent_id:
|
|
160
|
+
raise ValueError("version_id and agent_id are required")
|
|
161
|
+
|
|
162
|
+
if version is None and assistant_data is None and composio_app is None:
|
|
163
|
+
raise ValueError("At least one of version, assistant_data, or composio_app must be provided")
|
|
164
|
+
|
|
165
|
+
payload = {
|
|
166
|
+
'api_key': self.api_key,
|
|
167
|
+
'agent_id': agent_id,
|
|
168
|
+
'version_id': version_id
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if version is not None:
|
|
172
|
+
payload['version'] = version
|
|
173
|
+
if assistant_data is not None:
|
|
174
|
+
payload['assistant_data'] = assistant_data
|
|
175
|
+
if composio_app is not None:
|
|
176
|
+
payload['composio-app'] = composio_app
|
|
177
|
+
|
|
178
|
+
response = requests.post(f'{server_url}/agent/version/update', json=payload)
|
|
179
|
+
if response.status_code != 200:
|
|
180
|
+
raise Exception(f"Failed to update agent version: {response.status_code}, {response.text}")
|
|
181
|
+
return response.json()
|
|
182
|
+
|
|
183
|
+
def list(self, page=1, limit=20, inbound_or_outbound=None, search_query=None):
|
|
184
|
+
params = {
|
|
185
|
+
'page': page,
|
|
186
|
+
'limit': limit
|
|
187
|
+
}
|
|
188
|
+
if inbound_or_outbound:
|
|
189
|
+
params['inbound_or_outbound'] = inbound_or_outbound
|
|
190
|
+
|
|
191
|
+
payload = {'api_key': self.api_key}
|
|
192
|
+
if search_query:
|
|
193
|
+
payload['search_query'] = search_query
|
|
194
|
+
|
|
195
|
+
response = requests.post(f'{server_url}/agent/list', json=payload, params=params)
|
|
196
|
+
if response.status_code != 200:
|
|
197
|
+
raise Exception(f"Failed to list agents: {response.status_code}, {response.text}")
|
|
198
|
+
return response.json()
|
|
199
|
+
|
|
200
|
+
def get_version(self, agent_id, version):
|
|
201
|
+
if not agent_id or not version:
|
|
202
|
+
raise ValueError("agent_id and version are required")
|
|
203
|
+
|
|
204
|
+
payload = {
|
|
205
|
+
'api_key': self.api_key,
|
|
206
|
+
'agent_id': agent_id,
|
|
207
|
+
'version': version
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
response = requests.post(f'{server_url}/agent/version/get', json=payload)
|
|
211
|
+
if response.status_code != 200:
|
|
212
|
+
raise Exception(f"Failed to get agent version: {response.status_code}, {response.text}")
|
|
213
|
+
return response.json()
|
|
214
|
+
|
|
215
|
+
def list_versions(self, agent_id):
|
|
216
|
+
if not agent_id:
|
|
217
|
+
raise ValueError("agent_id is required")
|
|
218
|
+
|
|
219
|
+
payload = {
|
|
220
|
+
'api_key': self.api_key,
|
|
221
|
+
'agent_id': agent_id
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
response = requests.post(f'{server_url}/agent/version/list', json=payload)
|
|
225
|
+
if response.status_code != 200:
|
|
226
|
+
raise Exception(f"Failed to list agent versions: {response.status_code}, {response.text}")
|
|
227
|
+
return response.json()
|
|
228
|
+
|
|
229
|
+
def deploy_version(self, agent_id, version_id):
|
|
230
|
+
if not agent_id or not version_id:
|
|
231
|
+
raise ValueError("agent_id and version_id are required")
|
|
232
|
+
|
|
233
|
+
payload = {
|
|
234
|
+
'api_key': self.api_key,
|
|
235
|
+
'agent_id': agent_id,
|
|
236
|
+
'version_id': version_id
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
response = requests.post(f'{server_url}/agent/version/deploy', json=payload)
|
|
240
|
+
if response.status_code != 200:
|
|
241
|
+
raise Exception(f"Failed to deploy agent version: {response.status_code}, {response.text}")
|
|
242
|
+
return response.json()
|
|
243
|
+
|
|
244
|
+
def create_agent(self, type=None, name=None, company_name=None, assistant_name=None, first_message=None,
|
|
245
|
+
voice_id=None, voice_provider='11labs', speed='1.0', bg_noice=False, system_prompt=None,
|
|
246
|
+
industry='Blank Template', useCase='Blank Template', form_model=None, assistant_data=None,
|
|
247
|
+
knowledge_base : [str] = None, tools : [str] = None, call_forwarding : [str] = None):
|
|
248
|
+
payload = {
|
|
249
|
+
'api_key': self.api_key
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if type is not None:
|
|
253
|
+
payload['type'] = type
|
|
254
|
+
|
|
255
|
+
if name:
|
|
256
|
+
payload['name'] = name
|
|
257
|
+
if company_name:
|
|
258
|
+
payload['company_name'] = company_name
|
|
259
|
+
if assistant_name:
|
|
260
|
+
payload['assistant_name'] = assistant_name
|
|
261
|
+
if first_message:
|
|
262
|
+
payload['first_message'] = first_message
|
|
263
|
+
if voice_id:
|
|
264
|
+
payload['voice'] = voice_id
|
|
265
|
+
payload['voice_id'] = voice_id
|
|
266
|
+
if voice_provider:
|
|
267
|
+
payload['voice_provider'] = voice_provider
|
|
268
|
+
if speed:
|
|
269
|
+
payload['speed'] = speed
|
|
270
|
+
if bg_noice is not None:
|
|
271
|
+
payload['bg_noice'] = str(bg_noice).lower()
|
|
272
|
+
payload['backgroundNoise'] = bg_noice
|
|
273
|
+
if system_prompt:
|
|
274
|
+
payload['script'] = system_prompt
|
|
275
|
+
payload['industry'] = industry or 'Blank Template'
|
|
276
|
+
if useCase:
|
|
277
|
+
payload['useCase'] = useCase
|
|
278
|
+
if form_model:
|
|
279
|
+
payload['form_model'] = form_model
|
|
280
|
+
if assistant_data:
|
|
281
|
+
payload['assistant_data'] = assistant_data
|
|
282
|
+
if knowledge_base:
|
|
283
|
+
payload['knowledgeBase'] = knowledge_base
|
|
284
|
+
if tools:
|
|
285
|
+
payload['tools'] = tools
|
|
286
|
+
if call_forwarding:
|
|
287
|
+
payload['call_forwarding'] = call_forwarding
|
|
288
|
+
|
|
289
|
+
response = requests.post(f'{server_url}/agent/create', json=payload)
|
|
207
290
|
if response.status_code != 200:
|
|
208
|
-
raise Exception(f"Failed to
|
|
291
|
+
raise Exception(f"Failed to create agent: {response.status_code}, {response.text}")
|
|
209
292
|
return response.json()
|
|
210
293
|
|
|
211
|
-
def
|
|
212
|
-
|
|
294
|
+
def update_name(self, agent_id, name):
|
|
295
|
+
if not agent_id or not name:
|
|
296
|
+
raise ValueError("agent_id and name are required")
|
|
297
|
+
|
|
298
|
+
payload = {
|
|
299
|
+
'api_key': self.api_key,
|
|
300
|
+
'agent_id': agent_id,
|
|
301
|
+
'name': name
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
response = requests.post(f'{server_url}/agent/update/name', json=payload)
|
|
305
|
+
if response.status_code != 200:
|
|
306
|
+
raise Exception(f"Failed to update agent name: {response.status_code}, {response.text}")
|
|
307
|
+
return response.json()
|
|
308
|
+
|
|
309
|
+
def import_agents(self):
|
|
310
|
+
response = requests.get(f'{server_url}/agent/import/{self.api_key}')
|
|
311
|
+
if response.status_code != 200:
|
|
312
|
+
raise Exception(f"Failed to import agents: {response.status_code}, {response.text}")
|
|
313
|
+
return response.json()
|
|
314
|
+
|
|
315
|
+
def delete(self, agent_id):
|
|
316
|
+
if not agent_id:
|
|
317
|
+
raise ValueError("agent_id is required")
|
|
318
|
+
|
|
319
|
+
payload = {
|
|
320
|
+
'api_key': self.api_key,
|
|
321
|
+
'agent_object_id': agent_id
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
response = requests.post(f'{server_url}/agent/delete', json=payload)
|
|
213
325
|
if response.status_code != 200:
|
|
214
|
-
raise Exception(f"Failed to
|
|
326
|
+
raise Exception(f"Failed to delete agent: {response.status_code}, {response.text}")
|
|
215
327
|
return response.json()
|
|
216
328
|
|
|
217
|
-
class
|
|
329
|
+
class PhoneNumberWrapper:
|
|
218
330
|
def __init__(self, api_key):
|
|
219
331
|
self.api_key = api_key
|
|
220
|
-
|
|
221
|
-
def
|
|
222
|
-
request_start=None, request_complete=None,
|
|
223
|
-
request_failed=None, request_response_delayed=None,
|
|
224
|
-
async_=False, timeout_seconds=10, secret=None, headers=None):
|
|
225
|
-
|
|
226
|
-
tool_url = f"https://toolcaller.superu.ai/{tool_url}"
|
|
227
|
-
|
|
228
|
-
# add a param in url
|
|
229
|
-
tool_url = urllib.parse.urlparse(tool_url)
|
|
230
|
-
tool_url = tool_url.scheme + '://' + tool_url.netloc + tool_url.path + '?' + tool_url.query + '&base_url=' + tool_url_domain
|
|
231
|
-
|
|
232
|
-
messages = []
|
|
233
|
-
if request_start:
|
|
234
|
-
messages.append({"type": "request-start", "content": request_start})
|
|
235
|
-
if request_complete:
|
|
236
|
-
messages.append({"type": "request-complete", "content": request_complete})
|
|
237
|
-
if request_failed:
|
|
238
|
-
messages.append({"type": "request-failed", "content": request_failed})
|
|
239
|
-
if request_response_delayed:
|
|
240
|
-
messages.append({
|
|
241
|
-
"type": "request-response-delayed",
|
|
242
|
-
"content": request_response_delayed,
|
|
243
|
-
"timingMilliseconds": 2000
|
|
244
|
-
})
|
|
245
|
-
|
|
332
|
+
|
|
333
|
+
def get_owned(self):
|
|
246
334
|
payload = {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
},
|
|
255
|
-
"messages": messages,
|
|
256
|
-
"server": {
|
|
257
|
-
"url": tool_url,
|
|
258
|
-
"timeoutSeconds": 120
|
|
259
|
-
},
|
|
260
|
-
"async_": False
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
if secret:
|
|
264
|
-
payload["server"]["secret"] = secret
|
|
265
|
-
if headers:
|
|
266
|
-
payload["server"]["headers"] = headers
|
|
335
|
+
'api_key': self.api_key
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
response = requests.post(f'{server_url}/phoneNumber/owned', json=payload)
|
|
339
|
+
if response.status_code != 200:
|
|
340
|
+
raise Exception(f"Failed to get owned phone numbers: {response.status_code}, {response.text}")
|
|
341
|
+
return response.json()
|
|
267
342
|
|
|
268
|
-
|
|
343
|
+
class CallLogsWrapper:
|
|
344
|
+
def __init__(self, api_key):
|
|
345
|
+
self.api_key = api_key
|
|
346
|
+
|
|
347
|
+
def get_logs(self, assistant_id='all', limit=20, page=1, before=None, after=None,
|
|
348
|
+
status=None, campaign_id=None, search_query=None):
|
|
349
|
+
if not assistant_id:
|
|
350
|
+
raise ValueError("assistant_id is required")
|
|
351
|
+
|
|
352
|
+
params = {
|
|
353
|
+
'limit': limit,
|
|
354
|
+
'page': page
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if before:
|
|
358
|
+
params['before'] = before
|
|
359
|
+
if after:
|
|
360
|
+
params['after'] = after
|
|
361
|
+
if status:
|
|
362
|
+
params['status'] = status
|
|
363
|
+
if campaign_id:
|
|
364
|
+
params['campaign_id'] = campaign_id
|
|
365
|
+
if search_query:
|
|
366
|
+
params['search_query'] = search_query
|
|
367
|
+
|
|
368
|
+
payload = {
|
|
369
|
+
'api_key': self.api_key
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
response = requests.post(f'{server_url}/call-logs/{assistant_id}', json=payload, params=params)
|
|
269
373
|
if response.status_code != 200:
|
|
270
|
-
raise Exception(f"Failed to
|
|
374
|
+
raise Exception(f"Failed to get call logs: {response.status_code}, {response.text}")
|
|
375
|
+
return response.json()
|
|
376
|
+
|
|
377
|
+
class ToolsWrapper:
|
|
378
|
+
def __init__(self, api_key):
|
|
379
|
+
self.api_key = api_key
|
|
380
|
+
|
|
381
|
+
def create(self, tool_data):
|
|
382
|
+
if not tool_data or not isinstance(tool_data, dict):
|
|
383
|
+
raise ValueError("tool_data must be a non-empty dictionary")
|
|
384
|
+
|
|
385
|
+
payload = {
|
|
386
|
+
'api_key': self.api_key,
|
|
387
|
+
**tool_data
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
response = requests.post(f'{server_url}/tool', json=payload)
|
|
391
|
+
if response.status_code != 201:
|
|
392
|
+
raise Exception(f"Failed to create tool: {response.status_code}, {response.text}")
|
|
271
393
|
return response.json()
|
|
272
394
|
|
|
273
|
-
def list(self):
|
|
274
|
-
|
|
395
|
+
def list(self, page=1, per_page=20, tool_type=None):
|
|
396
|
+
payload = {
|
|
397
|
+
'api_key': self.api_key,
|
|
398
|
+
'page': page,
|
|
399
|
+
'per_page': per_page
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if tool_type:
|
|
403
|
+
payload['type'] = tool_type
|
|
404
|
+
|
|
405
|
+
response = requests.post(f'{server_url}/tool/list', json=payload)
|
|
275
406
|
if response.status_code != 200:
|
|
276
407
|
raise Exception(f"Failed to list tools: {response.status_code}, {response.text}")
|
|
277
408
|
return response.json()
|
|
278
409
|
|
|
279
410
|
def get(self, tool_id):
|
|
280
|
-
|
|
411
|
+
if not tool_id:
|
|
412
|
+
raise ValueError("tool_id is required")
|
|
413
|
+
|
|
414
|
+
params = {
|
|
415
|
+
'user_id': self.api_key
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
response = requests.get(f'{server_url}/tool/{tool_id}', params=params)
|
|
281
419
|
if response.status_code != 200:
|
|
282
420
|
raise Exception(f"Failed to get tool: {response.status_code}, {response.text}")
|
|
283
421
|
return response.json()
|
|
422
|
+
|
|
423
|
+
def update(self, tool_id, update_data):
|
|
424
|
+
if not tool_id:
|
|
425
|
+
raise ValueError("tool_id is required")
|
|
426
|
+
if not update_data or not isinstance(update_data, dict):
|
|
427
|
+
raise ValueError("update_data must be a non-empty dictionary")
|
|
428
|
+
|
|
429
|
+
payload = {
|
|
430
|
+
'api_key': self.api_key,
|
|
431
|
+
**update_data
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
response = requests.patch(f'{server_url}/tool/{tool_id}', json=payload)
|
|
435
|
+
if response.status_code != 200:
|
|
436
|
+
raise Exception(f"Failed to update tool: {response.status_code}, {response.text}")
|
|
437
|
+
return response.json()
|
|
438
|
+
|
|
439
|
+
def delete(self, tool_id):
|
|
440
|
+
if not tool_id:
|
|
441
|
+
raise ValueError("tool_id is required")
|
|
442
|
+
|
|
443
|
+
params = {
|
|
444
|
+
'user_id': self.api_key
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
response = requests.delete(f'{server_url}/tool/{tool_id}', params=params)
|
|
448
|
+
if response.status_code != 200:
|
|
449
|
+
raise Exception(f"Failed to delete tool: {response.status_code}, {response.text}")
|
|
450
|
+
return response.json()
|
|
284
451
|
|
|
285
|
-
class
|
|
452
|
+
class FolderWrapper:
|
|
286
453
|
def __init__(self, api_key):
|
|
287
454
|
self.api_key = api_key
|
|
288
|
-
|
|
289
|
-
def
|
|
455
|
+
|
|
456
|
+
def create(self, folder_name, description=None):
|
|
457
|
+
if not folder_name:
|
|
458
|
+
raise ValueError("folder_name is required")
|
|
459
|
+
|
|
290
460
|
payload = {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
"contacts": contacts
|
|
461
|
+
'api_key': self.api_key,
|
|
462
|
+
'folder_name': folder_name
|
|
294
463
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
if field not in contact:
|
|
301
|
-
raise ValueError(f"Missing compulsory field '{field}' in contact: {contact}")
|
|
302
|
-
|
|
303
|
-
if not isinstance(contact.get('phone_number'), str):
|
|
304
|
-
raise ValueError(f"'phone_number' must be a string in contact: {contact}")
|
|
305
|
-
phone_pattern = re.compile(r'^\d{10,15}$')
|
|
306
|
-
if not phone_pattern.match(contact['phone_number']):
|
|
307
|
-
raise ValueError(f"Invalid 'phone_number' format in contact: {contact}. It should contain only digits and be 10 to 15 digits long.")
|
|
308
|
-
country_code_pattern = re.compile(r'^\+\d{1,4}$')
|
|
309
|
-
if not country_code_pattern.match(contact['country_code']):
|
|
310
|
-
raise ValueError(f"Invalid 'country_code' format in contact: {contact}. It should start with '+' followed by 1 to 4 digits.")
|
|
311
|
-
|
|
312
|
-
response = requests.post(f'{server_url}/pypi_support/audience/create', json={**payload , 'api_key': self.api_key})
|
|
464
|
+
|
|
465
|
+
if description is not None:
|
|
466
|
+
payload['description'] = description
|
|
467
|
+
|
|
468
|
+
response = requests.post(f'{server_url}/folder/create', json=payload)
|
|
313
469
|
if response.status_code != 200:
|
|
314
|
-
raise Exception(f"Failed to create
|
|
470
|
+
raise Exception(f"Failed to create folder: {response.status_code}, {response.text}")
|
|
315
471
|
return response.json()
|
|
316
|
-
|
|
317
|
-
def
|
|
472
|
+
|
|
473
|
+
def list(self, page=1, per_page=20):
|
|
318
474
|
payload = {
|
|
319
|
-
|
|
475
|
+
'api_key': self.api_key,
|
|
476
|
+
'page': page,
|
|
477
|
+
'per_page': per_page
|
|
320
478
|
}
|
|
321
|
-
|
|
322
|
-
compulsory_fields = ['first_name', 'last_name', 'email', 'country_code', 'phone_number']
|
|
323
|
-
|
|
324
479
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
response = requests.post(f'{server_url}/pypi_support/contact/create', json={**payload , 'api_key': self.api_key})
|
|
342
|
-
if response.status_code != 200 and response.status_code != 201:
|
|
343
|
-
raise Exception(f"Failed to create contact: {response.status_code}, {response.text}")
|
|
480
|
+
response = requests.post(f'{server_url}/folder/list', json=payload)
|
|
481
|
+
if response.status_code != 200:
|
|
482
|
+
raise Exception(f"Failed to list folders: {response.status_code}, {response.text}")
|
|
483
|
+
return response.json()
|
|
484
|
+
|
|
485
|
+
def get(self, folder_id):
|
|
486
|
+
if not folder_id:
|
|
487
|
+
raise ValueError("folder_id is required")
|
|
488
|
+
|
|
489
|
+
payload = {
|
|
490
|
+
'api_key': self.api_key
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
response = requests.post(f'{server_url}/folder/{folder_id}', json=payload)
|
|
494
|
+
if response.status_code != 200:
|
|
495
|
+
raise Exception(f"Failed to get folder details: {response.status_code}, {response.text}")
|
|
344
496
|
return response.json()
|
|
345
497
|
|
|
346
|
-
def
|
|
347
|
-
|
|
498
|
+
def update(self, folder_id, folder_name=None, description=None):
|
|
499
|
+
if not folder_id:
|
|
500
|
+
raise ValueError("folder_id is required")
|
|
501
|
+
|
|
502
|
+
if folder_name is None and description is None:
|
|
503
|
+
raise ValueError("At least one of folder_name or description must be provided")
|
|
504
|
+
|
|
505
|
+
payload = {
|
|
506
|
+
'api_key': self.api_key,
|
|
507
|
+
'folder_id': folder_id
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if folder_name is not None:
|
|
511
|
+
payload['folder_name'] = folder_name
|
|
512
|
+
if description is not None:
|
|
513
|
+
payload['description'] = description
|
|
514
|
+
|
|
515
|
+
response = requests.post(f'{server_url}/folder/update', json=payload)
|
|
348
516
|
if response.status_code != 200:
|
|
349
|
-
raise Exception(f"Failed to
|
|
517
|
+
raise Exception(f"Failed to update folder: {response.status_code}, {response.text}")
|
|
518
|
+
return response.json()
|
|
519
|
+
|
|
520
|
+
def delete(self, folder_id):
|
|
521
|
+
if not folder_id:
|
|
522
|
+
raise ValueError("folder_id is required")
|
|
523
|
+
|
|
524
|
+
payload = {
|
|
525
|
+
'api_key': self.api_key,
|
|
526
|
+
'folder_id': folder_id
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
response = requests.post(f'{server_url}/folder/delete', json=payload)
|
|
530
|
+
if response.status_code != 200:
|
|
531
|
+
raise Exception(f"Failed to delete folder: {response.status_code}, {response.text}")
|
|
532
|
+
return response.json()
|
|
533
|
+
|
|
534
|
+
def assign_agent(self, agent_id, folder_id=None):
|
|
535
|
+
if not agent_id:
|
|
536
|
+
raise ValueError("agent_id is required")
|
|
537
|
+
|
|
538
|
+
payload = {
|
|
539
|
+
'api_key': self.api_key,
|
|
540
|
+
'agent_id': agent_id
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if folder_id is not None:
|
|
544
|
+
payload['folder_id'] = folder_id
|
|
545
|
+
|
|
546
|
+
response = requests.post(f'{server_url}/folder/assign-agent', json=payload)
|
|
547
|
+
if response.status_code != 200:
|
|
548
|
+
raise Exception(f"Failed to assign agent to folder: {response.status_code}, {response.text}")
|
|
350
549
|
return response.json()
|
|
351
550
|
|
|
551
|
+
def get_agents(self, folder_id, page=1, per_page=20):
|
|
552
|
+
if not folder_id:
|
|
553
|
+
raise ValueError("folder_id is required")
|
|
554
|
+
|
|
555
|
+
payload = {
|
|
556
|
+
'api_key': self.api_key,
|
|
557
|
+
'page': page,
|
|
558
|
+
'per_page': per_page
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
response = requests.post(f'{server_url}/folder/{folder_id}/agents', json=payload)
|
|
562
|
+
if response.status_code != 200:
|
|
563
|
+
raise Exception(f"Failed to get agents by folder: {response.status_code}, {response.text}")
|
|
564
|
+
return response.json()
|
|
565
|
+
|
|
352
566
|
class PlutoWrapper:
|
|
353
567
|
def __init__(self, api_key , user_id , assistants):
|
|
354
568
|
self.api_key = api_key
|
|
@@ -491,6 +705,651 @@ class PlutoWrapper:
|
|
|
491
705
|
if response.status_code != 200:
|
|
492
706
|
raise Exception(f"Failed to get call: {response.status_code}, {response.text}")
|
|
493
707
|
return response.json()
|
|
708
|
+
|
|
709
|
+
class ContactWrapper:
|
|
710
|
+
"""Wrapper for Contact management API endpoints.
|
|
711
|
+
|
|
712
|
+
Provides methods for creating and listing individual contacts.
|
|
713
|
+
"""
|
|
714
|
+
|
|
715
|
+
def __init__(self, api_key: str):
|
|
716
|
+
self.api_key = api_key
|
|
717
|
+
self._phone_pattern = re.compile(r'^[+]?[0-9\s\-\(\)]+$')
|
|
718
|
+
self._country_code_pattern = re.compile(r'^[+]?[0-9]{1,4}$')
|
|
719
|
+
self._email_pattern = re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
|
|
720
|
+
|
|
721
|
+
def _validate_contact_data(self, contact: dict) -> tuple[bool, list[str]]:
|
|
722
|
+
"""Validate contact data fields.
|
|
723
|
+
|
|
724
|
+
Args:
|
|
725
|
+
contact: Contact dictionary with required fields
|
|
726
|
+
|
|
727
|
+
Returns:
|
|
728
|
+
Tuple of (is_valid, list_of_errors)
|
|
729
|
+
"""
|
|
730
|
+
errors = []
|
|
731
|
+
required_fields = ['first_name', 'last_name', 'email', 'country_code', 'phone_number']
|
|
732
|
+
|
|
733
|
+
# Check required fields
|
|
734
|
+
for field in required_fields:
|
|
735
|
+
if not contact.get(field) or str(contact.get(field)).strip() == '':
|
|
736
|
+
errors.append(f"Missing or empty '{field}'")
|
|
737
|
+
|
|
738
|
+
if errors:
|
|
739
|
+
return False, errors
|
|
740
|
+
|
|
741
|
+
# Validate first and last name
|
|
742
|
+
first_name = contact['first_name'].strip()
|
|
743
|
+
last_name = contact['last_name'].strip()
|
|
744
|
+
if not first_name or not last_name:
|
|
745
|
+
errors.append("First name and last name cannot be empty")
|
|
746
|
+
|
|
747
|
+
# Validate email
|
|
748
|
+
email = contact['email'].strip().lower()
|
|
749
|
+
if not self._email_pattern.match(email):
|
|
750
|
+
errors.append("Invalid email format")
|
|
751
|
+
|
|
752
|
+
# Validate phone number
|
|
753
|
+
phone_number = contact['phone_number'].strip()
|
|
754
|
+
if not self._phone_pattern.match(phone_number):
|
|
755
|
+
errors.append("Invalid phone number format")
|
|
756
|
+
|
|
757
|
+
# Validate country code
|
|
758
|
+
country_code = contact['country_code'].strip()
|
|
759
|
+
if not self._country_code_pattern.match(country_code):
|
|
760
|
+
errors.append("Invalid country code format")
|
|
761
|
+
|
|
762
|
+
return len(errors) == 0, errors
|
|
763
|
+
|
|
764
|
+
def create(self, first_name: str, last_name: str, email: str,
|
|
765
|
+
country_code: str, phone_number: str, audience_id: str = None) -> dict:
|
|
766
|
+
"""Create a new contact.
|
|
767
|
+
|
|
768
|
+
Args:
|
|
769
|
+
first_name: Contact's first name
|
|
770
|
+
last_name: Contact's last name
|
|
771
|
+
email: Contact's email address
|
|
772
|
+
country_code: Phone country code (e.g., "+1")
|
|
773
|
+
phone_number: Contact's phone number
|
|
774
|
+
audience_id: Optional audience ID to associate contact with
|
|
775
|
+
|
|
776
|
+
Returns:
|
|
777
|
+
Dict containing success status, message, and contact_id
|
|
778
|
+
|
|
779
|
+
Raises:
|
|
780
|
+
ValueError: If validation fails
|
|
781
|
+
Exception: If API request fails
|
|
782
|
+
"""
|
|
783
|
+
contact_data = {
|
|
784
|
+
'first_name': first_name,
|
|
785
|
+
'last_name': last_name,
|
|
786
|
+
'email': email,
|
|
787
|
+
'country_code': country_code,
|
|
788
|
+
'phone_number': phone_number
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if audience_id:
|
|
792
|
+
contact_data['audience_id'] = audience_id
|
|
793
|
+
|
|
794
|
+
# Validate contact data
|
|
795
|
+
is_valid, errors = self._validate_contact_data(contact_data)
|
|
796
|
+
if not is_valid:
|
|
797
|
+
raise ValueError(f"Contact validation failed: {'; '.join(errors)}")
|
|
798
|
+
|
|
799
|
+
# Prepare payload with normalized data
|
|
800
|
+
payload = {
|
|
801
|
+
'first_name': first_name.strip(),
|
|
802
|
+
'last_name': last_name.strip(),
|
|
803
|
+
'email': email.strip().lower(),
|
|
804
|
+
'country_code': country_code.strip(),
|
|
805
|
+
'phone_number': phone_number.strip()
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
if audience_id:
|
|
809
|
+
payload['audience_id'] = audience_id
|
|
810
|
+
|
|
811
|
+
response = requests.post(
|
|
812
|
+
f'{server_url}/contact/create',
|
|
813
|
+
json=payload,
|
|
814
|
+
headers={'X-API-Key': self.api_key}
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
if response.status_code not in [200, 201]:
|
|
818
|
+
raise Exception(f"Failed to create contact: {response.status_code}, {response.text}")
|
|
819
|
+
|
|
820
|
+
return response.json()
|
|
821
|
+
|
|
822
|
+
def list(self, page: int = 1, limit: int = 10, search_query: str = None) -> dict:
|
|
823
|
+
"""List all contacts for the current user with pagination and search.
|
|
824
|
+
|
|
825
|
+
Args:
|
|
826
|
+
page: Page number (starting from 1)
|
|
827
|
+
limit: Number of contacts per page (1-100)
|
|
828
|
+
search_query: Optional search query to filter contacts by name, email, or phone
|
|
829
|
+
|
|
830
|
+
Returns:
|
|
831
|
+
Dict containing contact_list and pagination metadata
|
|
832
|
+
|
|
833
|
+
Raises:
|
|
834
|
+
ValueError: If parameters are invalid
|
|
835
|
+
Exception: If API request fails
|
|
836
|
+
"""
|
|
837
|
+
# Validate parameters
|
|
838
|
+
if page < 1:
|
|
839
|
+
raise ValueError("Page must be greater than 0")
|
|
840
|
+
if limit < 1 or limit > 100:
|
|
841
|
+
raise ValueError("Limit must be between 1 and 100")
|
|
842
|
+
|
|
843
|
+
params = {
|
|
844
|
+
'page': page,
|
|
845
|
+
'limit': limit
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
if search_query:
|
|
849
|
+
params['search_query'] = search_query
|
|
850
|
+
|
|
851
|
+
response = requests.get(
|
|
852
|
+
f'{server_url}/contact/list',
|
|
853
|
+
params=params,
|
|
854
|
+
headers={'X-API-Key': self.api_key}
|
|
855
|
+
)
|
|
856
|
+
|
|
857
|
+
if response.status_code != 200:
|
|
858
|
+
raise Exception(f"Failed to list contacts: {response.status_code}, {response.text}")
|
|
859
|
+
|
|
860
|
+
return response.json()
|
|
861
|
+
|
|
862
|
+
class AudienceWrapper:
|
|
863
|
+
"""Wrapper for Audience management API endpoints.
|
|
864
|
+
|
|
865
|
+
Provides methods for creating, listing, updating, and deleting audiences,
|
|
866
|
+
as well as managing contacts within audiences.
|
|
867
|
+
"""
|
|
868
|
+
|
|
869
|
+
def __init__(self, api_key: str):
|
|
870
|
+
self.api_key = api_key
|
|
871
|
+
self._phone_pattern = re.compile(r'^\d{7,15}$')
|
|
872
|
+
self._country_code_pattern = re.compile(r'^\+\d{1,4}$')
|
|
873
|
+
self._email_pattern = re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
|
|
874
|
+
|
|
875
|
+
def _validate_phone_number(self, phone_number: str) -> tuple[bool, str | None]:
|
|
876
|
+
"""Validate phone number format (7-15 digits).
|
|
877
|
+
|
|
878
|
+
Args:
|
|
879
|
+
phone_number: Phone number string (digits only or with formatting)
|
|
880
|
+
|
|
881
|
+
Returns:
|
|
882
|
+
Tuple of (is_valid, error_message)
|
|
883
|
+
"""
|
|
884
|
+
cleaned = re.sub(r'[\s\-\(\)\+]+', '', str(phone_number).strip())
|
|
885
|
+
|
|
886
|
+
# Handle float-like strings (e.g., "9970000000.0")
|
|
887
|
+
if '.' in cleaned:
|
|
888
|
+
parts = cleaned.split('.')
|
|
889
|
+
if len(parts) == 2 and (parts[1] == '0' or parts[1] == ''):
|
|
890
|
+
cleaned = parts[0]
|
|
891
|
+
else:
|
|
892
|
+
return False, "Phone number contains invalid decimal characters"
|
|
893
|
+
|
|
894
|
+
if not cleaned.isdigit():
|
|
895
|
+
return False, "Phone number must contain only digits"
|
|
896
|
+
|
|
897
|
+
if len(cleaned) < 7 or len(cleaned) > 15:
|
|
898
|
+
return False, f"Phone number must have 7-15 digits (got {len(cleaned)})"
|
|
899
|
+
|
|
900
|
+
return True, None
|
|
901
|
+
|
|
902
|
+
def _validate_contact(self, contact: dict, index: int = 0) -> tuple[bool, list[str]]:
|
|
903
|
+
"""Validate a single contact dictionary.
|
|
904
|
+
|
|
905
|
+
Args:
|
|
906
|
+
contact: Contact dictionary with first_name, country_code, phone_number, etc.
|
|
907
|
+
index: Contact index for error messaging
|
|
908
|
+
|
|
909
|
+
Returns:
|
|
910
|
+
Tuple of (is_valid, list_of_errors)
|
|
911
|
+
"""
|
|
912
|
+
errors = []
|
|
913
|
+
required_fields = ['first_name', 'country_code', 'phone_number']
|
|
914
|
+
|
|
915
|
+
# Check required fields
|
|
916
|
+
for field in required_fields:
|
|
917
|
+
if not contact.get(field) or str(contact.get(field)).strip() == '':
|
|
918
|
+
errors.append(f"Missing or empty '{field}'")
|
|
919
|
+
|
|
920
|
+
if errors:
|
|
921
|
+
return False, errors
|
|
922
|
+
|
|
923
|
+
# Validate phone number
|
|
924
|
+
phone_number = str(contact['phone_number']).strip()
|
|
925
|
+
is_valid, error_msg = self._validate_phone_number(phone_number)
|
|
926
|
+
if not is_valid:
|
|
927
|
+
errors.append(error_msg)
|
|
928
|
+
|
|
929
|
+
# Validate country code
|
|
930
|
+
country_code = str(contact['country_code']).strip()
|
|
931
|
+
if not self._country_code_pattern.match(country_code):
|
|
932
|
+
errors.append("Invalid country code format (expected: +1 to +9999)")
|
|
933
|
+
|
|
934
|
+
# Validate email if provided
|
|
935
|
+
email = contact.get('email', '').strip()
|
|
936
|
+
if email and not self._email_pattern.match(email):
|
|
937
|
+
errors.append("Invalid email format")
|
|
938
|
+
|
|
939
|
+
return len(errors) == 0, errors
|
|
940
|
+
|
|
941
|
+
def _validate_contacts(self, contacts: list[dict]) -> tuple[bool, list[str]]:
|
|
942
|
+
"""Validate a list of contacts.
|
|
943
|
+
|
|
944
|
+
Args:
|
|
945
|
+
contacts: List of contact dictionaries
|
|
946
|
+
|
|
947
|
+
Returns:
|
|
948
|
+
Tuple of (all_valid, list_of_errors)
|
|
949
|
+
"""
|
|
950
|
+
all_errors = []
|
|
951
|
+
for i, contact in enumerate(contacts):
|
|
952
|
+
is_valid, errors = self._validate_contact(contact, i)
|
|
953
|
+
if not is_valid:
|
|
954
|
+
all_errors.append(f"Contact {i+1}: {', '.join(errors)}")
|
|
955
|
+
|
|
956
|
+
return len(all_errors) == 0, all_errors
|
|
957
|
+
|
|
958
|
+
def create(self, audience_name: str, contacts: list[dict], audience_description: str = "") -> dict:
|
|
959
|
+
"""Create a new audience with contacts.
|
|
960
|
+
|
|
961
|
+
Args:
|
|
962
|
+
audience_name: Name for the audience
|
|
963
|
+
contacts: List of contact dictionaries, each containing:
|
|
964
|
+
- first_name (required): Contact's first name
|
|
965
|
+
- country_code (required): Phone country code (e.g., "+1")
|
|
966
|
+
- phone_number (required): Phone number (7-15 digits)
|
|
967
|
+
- last_name (optional): Contact's last name
|
|
968
|
+
- email (optional): Contact's email address
|
|
969
|
+
- variable_values (optional): Dict of custom variables
|
|
970
|
+
audience_description: Optional description for the audience
|
|
971
|
+
|
|
972
|
+
Returns:
|
|
973
|
+
Dict containing audience_id, contacts_added, and contact details
|
|
974
|
+
|
|
975
|
+
Raises:
|
|
976
|
+
ValueError: If validation fails for any contact
|
|
977
|
+
Exception: If API request fails
|
|
978
|
+
"""
|
|
979
|
+
if not audience_name or not audience_name.strip():
|
|
980
|
+
raise ValueError("audience_name is required and cannot be empty")
|
|
981
|
+
|
|
982
|
+
if not contacts or len(contacts) == 0:
|
|
983
|
+
raise ValueError("At least one contact is required")
|
|
984
|
+
|
|
985
|
+
# Validate all contacts
|
|
986
|
+
is_valid, errors = self._validate_contacts(contacts)
|
|
987
|
+
if not is_valid:
|
|
988
|
+
raise ValueError(f"Contact validation failed: {'; '.join(errors)}")
|
|
989
|
+
|
|
990
|
+
payload = {
|
|
991
|
+
"api_key": self.api_key,
|
|
992
|
+
"audience_name": audience_name.strip(),
|
|
993
|
+
"audience_description": audience_description.strip() if audience_description else "",
|
|
994
|
+
"contacts": contacts
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
response = requests.post(f'{server_url}/audience/create', json=payload)
|
|
998
|
+
|
|
999
|
+
if response.status_code != 200:
|
|
1000
|
+
raise Exception(f"Failed to create audience: {response.status_code}, {response.text}")
|
|
1001
|
+
|
|
1002
|
+
return response.json()
|
|
1003
|
+
|
|
1004
|
+
def list(self) -> dict:
|
|
1005
|
+
"""List all audiences for the current user.
|
|
1006
|
+
|
|
1007
|
+
Returns:
|
|
1008
|
+
Dict containing audience_list with id, name, description, contactCount, createdAt
|
|
1009
|
+
|
|
1010
|
+
Raises:
|
|
1011
|
+
Exception: If API request fails
|
|
1012
|
+
"""
|
|
1013
|
+
params = {}
|
|
1014
|
+
|
|
1015
|
+
response = requests.get(
|
|
1016
|
+
f'{server_url}/audience/list',
|
|
1017
|
+
params=params,
|
|
1018
|
+
headers={"X-API-Key": self.api_key}
|
|
1019
|
+
)
|
|
1020
|
+
|
|
1021
|
+
if response.status_code != 200:
|
|
1022
|
+
raise Exception(f"Failed to list audiences: {response.status_code}, {response.text}")
|
|
1023
|
+
|
|
1024
|
+
return response.json()
|
|
1025
|
+
|
|
1026
|
+
def get(self, audience_id: str) -> dict:
|
|
1027
|
+
"""Get detailed information about a specific audience.
|
|
1028
|
+
|
|
1029
|
+
Args:
|
|
1030
|
+
audience_id: UUID of the audience to retrieve
|
|
1031
|
+
|
|
1032
|
+
Returns:
|
|
1033
|
+
Dict containing audience details and all contacts
|
|
1034
|
+
|
|
1035
|
+
Raises:
|
|
1036
|
+
ValueError: If audience_id is not provided
|
|
1037
|
+
Exception: If API request fails
|
|
1038
|
+
"""
|
|
1039
|
+
if not audience_id:
|
|
1040
|
+
raise ValueError("audience_id is required")
|
|
1041
|
+
|
|
1042
|
+
response = requests.get(
|
|
1043
|
+
f'{server_url}/audience/{audience_id}',
|
|
1044
|
+
headers={"X-API-Key": self.api_key}
|
|
1045
|
+
)
|
|
1046
|
+
|
|
1047
|
+
if response.status_code == 404:
|
|
1048
|
+
raise Exception(f"Audience not found: {audience_id}")
|
|
1049
|
+
|
|
1050
|
+
if response.status_code != 200:
|
|
1051
|
+
raise Exception(f"Failed to get audience: {response.status_code}, {response.text}")
|
|
1052
|
+
|
|
1053
|
+
return response.json()
|
|
1054
|
+
|
|
1055
|
+
def get_contacts(self, audience_id: str, page: int = 1, limit: int = 10) -> dict:
|
|
1056
|
+
"""Get paginated contacts for a specific audience.
|
|
1057
|
+
|
|
1058
|
+
Args:
|
|
1059
|
+
audience_id: UUID of the audience
|
|
1060
|
+
page: Page number (starting from 1)
|
|
1061
|
+
limit: Number of contacts per page
|
|
1062
|
+
|
|
1063
|
+
Returns:
|
|
1064
|
+
Dict containing contact_list and pagination metadata
|
|
1065
|
+
|
|
1066
|
+
Raises:
|
|
1067
|
+
ValueError: If audience_id is not provided
|
|
1068
|
+
Exception: If API request fails
|
|
1069
|
+
"""
|
|
1070
|
+
if not audience_id:
|
|
1071
|
+
raise ValueError("audience_id is required")
|
|
1072
|
+
|
|
1073
|
+
params = {
|
|
1074
|
+
"audience_id": audience_id,
|
|
1075
|
+
"page": max(1, page),
|
|
1076
|
+
"limit": max(1, limit)
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
response = requests.get(
|
|
1080
|
+
f'{server_url}/audience/contacts/list',
|
|
1081
|
+
params=params,
|
|
1082
|
+
headers={"X-API-Key": self.api_key}
|
|
1083
|
+
)
|
|
1084
|
+
|
|
1085
|
+
if response.status_code != 200:
|
|
1086
|
+
raise Exception(f"Failed to get contacts: {response.status_code}, {response.text}")
|
|
1087
|
+
|
|
1088
|
+
return response.json()
|
|
1089
|
+
|
|
1090
|
+
def update(self, audience_id: str, audience_name: str = None, audience_description: str = None) -> dict:
|
|
1091
|
+
"""Update audience name and/or description.
|
|
1092
|
+
|
|
1093
|
+
Args:
|
|
1094
|
+
audience_id: UUID of the audience to update
|
|
1095
|
+
audience_name: New name for the audience (optional)
|
|
1096
|
+
audience_description: New description for the audience (optional)
|
|
1097
|
+
|
|
1098
|
+
Returns:
|
|
1099
|
+
Dict containing updated audience details
|
|
1100
|
+
|
|
1101
|
+
Raises:
|
|
1102
|
+
ValueError: If audience_id is not provided or no fields to update
|
|
1103
|
+
Exception: If API request fails
|
|
1104
|
+
"""
|
|
1105
|
+
if not audience_id:
|
|
1106
|
+
raise ValueError("audience_id is required")
|
|
1107
|
+
|
|
1108
|
+
if audience_name is None and audience_description is None:
|
|
1109
|
+
raise ValueError("At least one of audience_name or audience_description must be provided")
|
|
1110
|
+
|
|
1111
|
+
payload = {
|
|
1112
|
+
"api_key": self.api_key,
|
|
1113
|
+
"audience_id": audience_id
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
if audience_name is not None:
|
|
1117
|
+
if not audience_name.strip():
|
|
1118
|
+
raise ValueError("audience_name cannot be empty")
|
|
1119
|
+
payload["audience_name"] = audience_name.strip()
|
|
1120
|
+
|
|
1121
|
+
if audience_description is not None:
|
|
1122
|
+
payload["audience_description"] = audience_description.strip()
|
|
1123
|
+
|
|
1124
|
+
response = requests.put(f'{server_url}/audience/update', json=payload)
|
|
1125
|
+
|
|
1126
|
+
if response.status_code == 404:
|
|
1127
|
+
raise Exception(f"Audience not found: {audience_id}")
|
|
1128
|
+
|
|
1129
|
+
if response.status_code != 200:
|
|
1130
|
+
raise Exception(f"Failed to update audience: {response.status_code}, {response.text}")
|
|
1131
|
+
|
|
1132
|
+
return response.json()
|
|
1133
|
+
|
|
1134
|
+
def add_contacts(self, audience_id: str, contacts: List[Dict]) -> dict:
|
|
1135
|
+
"""Add one or more contacts to an existing audience.
|
|
1136
|
+
|
|
1137
|
+
Args:
|
|
1138
|
+
audience_id: UUID of the audience
|
|
1139
|
+
contacts: List of contact dictionaries (same format as create())
|
|
1140
|
+
|
|
1141
|
+
Returns:
|
|
1142
|
+
Dict containing contacts_added count and inserted contact details
|
|
1143
|
+
|
|
1144
|
+
Raises:
|
|
1145
|
+
ValueError: If validation fails
|
|
1146
|
+
Exception: If API request fails
|
|
1147
|
+
"""
|
|
1148
|
+
if not audience_id:
|
|
1149
|
+
raise ValueError("audience_id is required")
|
|
1150
|
+
|
|
1151
|
+
if not contacts or len(contacts) == 0:
|
|
1152
|
+
raise ValueError("At least one contact is required")
|
|
1153
|
+
|
|
1154
|
+
# Validate all contacts
|
|
1155
|
+
is_valid, errors = self._validate_contacts(contacts)
|
|
1156
|
+
if not is_valid:
|
|
1157
|
+
raise ValueError(f"Contact validation failed: {'; '.join(errors)}")
|
|
1158
|
+
|
|
1159
|
+
payload = {
|
|
1160
|
+
"api_key": self.api_key,
|
|
1161
|
+
"audience_id": audience_id,
|
|
1162
|
+
"contacts": contacts
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
response = requests.post(f'{server_url}/audience/add-contacts', json=payload)
|
|
1166
|
+
|
|
1167
|
+
if response.status_code == 404:
|
|
1168
|
+
raise Exception(f"Audience not found: {audience_id}")
|
|
1169
|
+
|
|
1170
|
+
if response.status_code != 200:
|
|
1171
|
+
raise Exception(f"Failed to add contacts: {response.status_code}, {response.text}")
|
|
1172
|
+
|
|
1173
|
+
return response.json()
|
|
1174
|
+
|
|
1175
|
+
def delete(self, audience_id: str) -> dict:
|
|
1176
|
+
"""Delete an audience and all its contacts.
|
|
1177
|
+
|
|
1178
|
+
Args:
|
|
1179
|
+
audience_id: UUID of the audience to delete
|
|
1180
|
+
|
|
1181
|
+
Returns:
|
|
1182
|
+
Dict containing deletion confirmation and contacts_deleted count
|
|
1183
|
+
|
|
1184
|
+
Raises:
|
|
1185
|
+
ValueError: If audience_id is not provided
|
|
1186
|
+
Exception: If API request fails
|
|
1187
|
+
"""
|
|
1188
|
+
if not audience_id:
|
|
1189
|
+
raise ValueError("audience_id is required")
|
|
1190
|
+
|
|
1191
|
+
payload = {
|
|
1192
|
+
"api_key": self.api_key,
|
|
1193
|
+
"audience_id": audience_id
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
response = requests.delete(f'{server_url}/audience/delete', json=payload)
|
|
1197
|
+
|
|
1198
|
+
if response.status_code == 404:
|
|
1199
|
+
raise Exception(f"Audience not found: {audience_id}")
|
|
1200
|
+
|
|
1201
|
+
if response.status_code != 200:
|
|
1202
|
+
raise Exception(f"Failed to delete audience: {response.status_code}, {response.text}")
|
|
1203
|
+
|
|
1204
|
+
return response.json()
|
|
1205
|
+
|
|
1206
|
+
class KnowledgeBaseWrapper:
|
|
1207
|
+
"""Wrapper for Knowledge Base management API endpoints.
|
|
1208
|
+
|
|
1209
|
+
Provides methods for creating, listing, and retrieving knowledge bases with file uploads.
|
|
1210
|
+
"""
|
|
1211
|
+
|
|
1212
|
+
def __init__(self, api_key: str):
|
|
1213
|
+
self.api_key = api_key
|
|
1214
|
+
|
|
1215
|
+
def create(self, name: str, description: str, files: list) -> dict:
|
|
1216
|
+
"""Create a new knowledge base with files.
|
|
1217
|
+
|
|
1218
|
+
Args:
|
|
1219
|
+
name: Knowledge base name
|
|
1220
|
+
description: Knowledge base description
|
|
1221
|
+
files: List of file objects or tuples (filename, file_content, content_type)
|
|
1222
|
+
Examples:
|
|
1223
|
+
- [open('file.pdf', 'rb'), open('doc.txt', 'rb')]
|
|
1224
|
+
- [('file.pdf', pdf_bytes, 'application/pdf')]
|
|
1225
|
+
|
|
1226
|
+
Returns:
|
|
1227
|
+
Dict containing knowledge_base_id, kb_uuid, embedding_status, and file details
|
|
1228
|
+
|
|
1229
|
+
Raises:
|
|
1230
|
+
ValueError: If validation fails
|
|
1231
|
+
Exception: If API request fails
|
|
1232
|
+
"""
|
|
1233
|
+
if not name or not name.strip():
|
|
1234
|
+
raise ValueError("name is required and cannot be empty")
|
|
1235
|
+
|
|
1236
|
+
if not description or not description.strip():
|
|
1237
|
+
raise ValueError("description is required and cannot be empty")
|
|
1238
|
+
|
|
1239
|
+
if not files or len(files) == 0:
|
|
1240
|
+
raise ValueError("At least one file is required")
|
|
1241
|
+
|
|
1242
|
+
# Prepare multipart form data
|
|
1243
|
+
form_data = {
|
|
1244
|
+
'name': name.strip(),
|
|
1245
|
+
'description': description.strip()
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
# Prepare files for upload
|
|
1249
|
+
# The 'files' parameter expects a list of tuples: (field_name, (filename, file_object, content_type))
|
|
1250
|
+
files_data = []
|
|
1251
|
+
for file_item in files:
|
|
1252
|
+
if hasattr(file_item, 'read'):
|
|
1253
|
+
# File-like object
|
|
1254
|
+
filename = getattr(file_item, 'name', 'uploaded_file')
|
|
1255
|
+
files_data.append(('files', (filename, file_item, 'application/octet-stream')))
|
|
1256
|
+
elif isinstance(file_item, tuple) and len(file_item) >= 2:
|
|
1257
|
+
# Tuple format: (filename, content) or (filename, content, content_type)
|
|
1258
|
+
filename = file_item[0]
|
|
1259
|
+
content = file_item[1]
|
|
1260
|
+
content_type = file_item[2] if len(file_item) > 2 else 'application/octet-stream'
|
|
1261
|
+
files_data.append(('files', (filename, content, content_type)))
|
|
1262
|
+
else:
|
|
1263
|
+
raise ValueError(f"Invalid file format. Expected file object or tuple, got {type(file_item)}")
|
|
1264
|
+
|
|
1265
|
+
response = requests.post(
|
|
1266
|
+
f'{server_url}/knowledge-base/create',
|
|
1267
|
+
data=form_data,
|
|
1268
|
+
files=files_data,
|
|
1269
|
+
headers={'X-API-Key': self.api_key}
|
|
1270
|
+
)
|
|
1271
|
+
|
|
1272
|
+
if response.status_code != 200:
|
|
1273
|
+
raise Exception(f"Failed to create knowledge base: {response.status_code}, {response.text}")
|
|
1274
|
+
|
|
1275
|
+
result = response.json()
|
|
1276
|
+
if result.get('status') != 'success':
|
|
1277
|
+
raise Exception(f"Failed to create knowledge base: {result.get('message', 'Unknown error')}")
|
|
1278
|
+
|
|
1279
|
+
return result.get('data', result)
|
|
1280
|
+
|
|
1281
|
+
def list(self, page: int = 1, limit: int = 10) -> dict:
|
|
1282
|
+
"""List all knowledge bases for the current user with pagination.
|
|
1283
|
+
|
|
1284
|
+
Args:
|
|
1285
|
+
page: Page number (starting from 1)
|
|
1286
|
+
limit: Number of knowledge bases per page (1-50)
|
|
1287
|
+
|
|
1288
|
+
Returns:
|
|
1289
|
+
Dict containing knowledge_bases list and pagination metadata
|
|
1290
|
+
|
|
1291
|
+
Raises:
|
|
1292
|
+
ValueError: If parameters are invalid
|
|
1293
|
+
Exception: If API request fails
|
|
1294
|
+
"""
|
|
1295
|
+
# Validate parameters
|
|
1296
|
+
if page < 1:
|
|
1297
|
+
raise ValueError("Page must be greater than 0")
|
|
1298
|
+
if limit < 1 or limit > 50:
|
|
1299
|
+
raise ValueError("Limit must be between 1 and 50")
|
|
1300
|
+
|
|
1301
|
+
payload = {
|
|
1302
|
+
'page': page,
|
|
1303
|
+
'limit': limit
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
response = requests.post(
|
|
1307
|
+
f'{server_url}/knowledge-base/list',
|
|
1308
|
+
json=payload,
|
|
1309
|
+
headers={'X-API-Key': self.api_key}
|
|
1310
|
+
)
|
|
1311
|
+
|
|
1312
|
+
if response.status_code != 200:
|
|
1313
|
+
raise Exception(f"Failed to list knowledge bases: {response.status_code}, {response.text}")
|
|
1314
|
+
|
|
1315
|
+
result = response.json()
|
|
1316
|
+
if result.get('status') != 'success':
|
|
1317
|
+
raise Exception(f"Failed to list knowledge bases: {result.get('message', 'Unknown error')}")
|
|
1318
|
+
|
|
1319
|
+
return result.get('data', result)
|
|
1320
|
+
|
|
1321
|
+
def get(self, knowledge_base_id: str) -> dict:
|
|
1322
|
+
"""Get detailed information about a specific knowledge base.
|
|
1323
|
+
|
|
1324
|
+
Args:
|
|
1325
|
+
knowledge_base_id: Knowledge base ID (UUID or MongoDB ObjectId)
|
|
1326
|
+
|
|
1327
|
+
Returns:
|
|
1328
|
+
Dict containing complete knowledge base details including files
|
|
1329
|
+
|
|
1330
|
+
Raises:
|
|
1331
|
+
ValueError: If knowledge_base_id is not provided
|
|
1332
|
+
Exception: If API request fails or knowledge base not found
|
|
1333
|
+
"""
|
|
1334
|
+
if not knowledge_base_id:
|
|
1335
|
+
raise ValueError("knowledge_base_id is required")
|
|
1336
|
+
|
|
1337
|
+
response = requests.get(
|
|
1338
|
+
f'{server_url}/knowledge-base/{knowledge_base_id}',
|
|
1339
|
+
headers={'X-API-Key': self.api_key}
|
|
1340
|
+
)
|
|
1341
|
+
|
|
1342
|
+
if response.status_code == 404:
|
|
1343
|
+
raise Exception(f"Knowledge base not found: {knowledge_base_id}")
|
|
1344
|
+
|
|
1345
|
+
if response.status_code != 200:
|
|
1346
|
+
raise Exception(f"Failed to get knowledge base: {response.status_code}, {response.text}")
|
|
1347
|
+
|
|
1348
|
+
result = response.json()
|
|
1349
|
+
if result.get('status') != 'success':
|
|
1350
|
+
raise Exception(f"Failed to get knowledge base: {result.get('message', 'Unknown error')}")
|
|
1351
|
+
|
|
1352
|
+
return result.get('data', result)
|
|
494
1353
|
|
|
495
1354
|
class SuperU:
|
|
496
1355
|
def __init__(self, api_key):
|
|
@@ -498,10 +1357,16 @@ class SuperU:
|
|
|
498
1357
|
self.api_key = api_key
|
|
499
1358
|
self.user_id = API_key_validation['user_id']
|
|
500
1359
|
self.calls = CallWrapper(self.api_key)
|
|
501
|
-
self.
|
|
502
|
-
self.
|
|
503
|
-
self.
|
|
1360
|
+
self.agents = AssistantWrapper(self.api_key)
|
|
1361
|
+
self.pluto = PlutoWrapper(self.api_key , self.user_id , assistants=self.agents)
|
|
1362
|
+
self.folders = FolderWrapper(self.api_key)
|
|
1363
|
+
self.call_logs = CallLogsWrapper(self.api_key)
|
|
1364
|
+
self.tools = ToolsWrapper(self.api_key)
|
|
1365
|
+
self.phone_numbers = PhoneNumberWrapper(self.api_key)
|
|
504
1366
|
self.contacts = ContactWrapper(self.api_key)
|
|
1367
|
+
self.audience = AudienceWrapper(self.api_key)
|
|
1368
|
+
self.knowledge_base = KnowledgeBaseWrapper(self.api_key)
|
|
1369
|
+
|
|
505
1370
|
|
|
506
1371
|
def validate_api_key(self, api_key):
|
|
507
1372
|
response = requests.post(
|