superu 2025.10.6.2__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 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,182 +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 create(self, name, transcriber, model, voice , **kwargs):
111
- for messages in model['messages']:
112
- messages['content'] = re.sub(r'[\u0000-\u001F\u007F]+', ' ', messages['content'])
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
- "name": name,
115
- "transcriber": transcriber,
116
- "model": model,
117
- "voice": voice,
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
- return self.create(**exmaple_json)
204
-
205
- def list(self):
206
- response = requests.post(f'{server_url}/pypi_support/assistant_list', json={'api_key': self.api_key})
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)
207
179
  if response.status_code != 200:
208
- raise Exception(f"Failed to list assistants: {response.status_code}, {response.text}")
180
+ raise Exception(f"Failed to update agent version: {response.status_code}, {response.text}")
209
181
  return response.json()
210
182
 
211
- def get(self, assistant_id):
212
- response = requests.post(f"{server_url}/pypi_support/assistant_get", json={'api_key': self.api_key, "assistant_id": assistant_id})
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)
213
196
  if response.status_code != 200:
214
- raise Exception(f"Failed to get assistant: {response.status_code}, {response.text}")
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)
290
+ if response.status_code != 200:
291
+ raise Exception(f"Failed to create agent: {response.status_code}, {response.text}")
292
+ return response.json()
293
+
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)
325
+ if response.status_code != 200:
326
+ raise Exception(f"Failed to delete agent: {response.status_code}, {response.text}")
215
327
  return response.json()
216
328
 
217
- class ToolWrapper:
329
+ class PhoneNumberWrapper:
218
330
  def __init__(self, api_key):
219
331
  self.api_key = api_key
220
-
221
- def create(self, name, description, parameters, tool_url, tool_url_domain,
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
- "api_key": self.api_key,
248
- "type": "function",
249
- "function": {
250
- "name": name,
251
- "async": False,
252
- "description": description,
253
- "parameters": parameters
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
- response = requests.post(f'{server_url}/pypi_support/tool_create', headers=headers, json=payload)
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 create assistant: {response.status_code}, {response.text}")
374
+ raise Exception(f"Failed to get call logs: {response.status_code}, {response.text}")
271
375
  return response.json()
376
+
377
+ class ToolsWrapper:
378
+ def __init__(self, api_key):
379
+ self.api_key = api_key
272
380
 
273
- def list(self):
274
- response = requests.get(f'{server_url}/pypi_support/tool_list', headers=self.headers)
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}")
393
+ return response.json()
394
+
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
- response = requests.get(f'{server_url}/pypi_support/tool_get', headers=self.headers)
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()
284
-
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()
451
+
452
+ class FolderWrapper:
453
+ def __init__(self, api_key):
454
+ self.api_key = api_key
455
+
456
+ def create(self, folder_name, description=None):
457
+ if not folder_name:
458
+ raise ValueError("folder_name is required")
459
+
460
+ payload = {
461
+ 'api_key': self.api_key,
462
+ 'folder_name': folder_name
463
+ }
464
+
465
+ if description is not None:
466
+ payload['description'] = description
467
+
468
+ response = requests.post(f'{server_url}/folder/create', json=payload)
469
+ if response.status_code != 200:
470
+ raise Exception(f"Failed to create folder: {response.status_code}, {response.text}")
471
+ return response.json()
472
+
473
+ def list(self, page=1, per_page=20):
474
+ payload = {
475
+ 'api_key': self.api_key,
476
+ 'page': page,
477
+ 'per_page': per_page
478
+ }
479
+
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}")
496
+ return response.json()
497
+
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)
516
+ if response.status_code != 200:
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}")
549
+ return response.json()
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
+
285
566
  class PlutoWrapper:
286
567
  def __init__(self, api_key , user_id , assistants):
287
568
  self.api_key = api_key
@@ -424,6 +705,651 @@ class PlutoWrapper:
424
705
  if response.status_code != 200:
425
706
  raise Exception(f"Failed to get call: {response.status_code}, {response.text}")
426
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)
427
1353
 
428
1354
  class SuperU:
429
1355
  def __init__(self, api_key):
@@ -431,9 +1357,16 @@ class SuperU:
431
1357
  self.api_key = api_key
432
1358
  self.user_id = API_key_validation['user_id']
433
1359
  self.calls = CallWrapper(self.api_key)
434
- self.assistants = AssistantWrapper(self.api_key)
435
- self.tools = ToolWrapper(self.api_key)
436
- self.pluto = PlutoWrapper(self.api_key , self.user_id , assistants=self.assistants)
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)
1366
+ self.contacts = ContactWrapper(self.api_key)
1367
+ self.audience = AudienceWrapper(self.api_key)
1368
+ self.knowledge_base = KnowledgeBaseWrapper(self.api_key)
1369
+
437
1370
 
438
1371
  def validate_api_key(self, api_key):
439
1372
  response = requests.post(