dhisana 0.0.1.dev273__py3-none-any.whl → 0.0.1.dev274__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.
@@ -1,19 +1,86 @@
1
1
  import base64
2
2
  import email.utils
3
+ import quopri
4
+ from email.message import Message
3
5
  from email.utils import parseaddr
4
6
  from typing import Any, Dict, List, Optional, Tuple
7
+
5
8
  from bs4 import BeautifulSoup
6
9
 
7
- def decode_base64_url(data: str) -> str:
10
+ def decode_base64_url(data: str) -> bytes:
8
11
  """
9
- Decodes a Base64-url-encoded string (Gmail API uses URL-safe Base64).
12
+ Decode a Base64-url-encoded string (Gmail API uses URL-safe Base64).
10
13
  """
11
14
  data = data.replace('-', '+').replace('_', '/')
12
15
  # Fix padding
13
16
  missing_padding = len(data) % 4
14
17
  if missing_padding:
15
18
  data += '=' * (4 - missing_padding)
16
- return base64.b64decode(data).decode('utf-8', errors='ignore')
19
+ return base64.b64decode(data)
20
+
21
+
22
+ def _get_charset(headers_list: List[Dict[str, str]], mime_type: str) -> Optional[str]:
23
+ content_type = find_header(headers_list, "Content-Type") or mime_type or ""
24
+ if not content_type:
25
+ return None
26
+ msg = Message()
27
+ msg["Content-Type"] = content_type
28
+ return msg.get_content_charset()
29
+
30
+
31
+ def _decode_transfer_encoding(payload: bytes, transfer_encoding: str) -> bytes:
32
+ encoding = (transfer_encoding or "").lower()
33
+ if "quoted-printable" in encoding:
34
+ return quopri.decodestring(payload)
35
+ if "base64" in encoding:
36
+ try:
37
+ return base64.b64decode(payload, validate=False)
38
+ except Exception:
39
+ return payload
40
+ return payload
41
+
42
+
43
+ _MOJIBAKE_MARKERS = (
44
+ "\u00e2\u0080\u0099",
45
+ "\u00e2\u0080\u0093",
46
+ "\u00e2\u0080\u0094",
47
+ "\u00e2\u0080\u009c",
48
+ "\u00e2\u0080\u009d",
49
+ "\u00e2\u0080\u00a6",
50
+ "\u00c3\u00a9",
51
+ "\u00c3\u00a0",
52
+ "\u00c3\u00b6",
53
+ )
54
+
55
+
56
+ def _repair_mojibake(text: str) -> str:
57
+ if not text or not any(marker in text for marker in _MOJIBAKE_MARKERS):
58
+ return text
59
+ try:
60
+ repaired = text.encode("latin-1").decode("utf-8")
61
+ except UnicodeError:
62
+ return text
63
+ if any(marker in repaired for marker in _MOJIBAKE_MARKERS):
64
+ return text
65
+ return repaired
66
+
67
+
68
+ def _decode_part_text(
69
+ data: str, headers_list: List[Dict[str, str]], mime_type: str
70
+ ) -> str:
71
+ raw_bytes = decode_base64_url(data)
72
+ transfer_encoding = find_header(headers_list, "Content-Transfer-Encoding") or ""
73
+ decoded_bytes = _decode_transfer_encoding(raw_bytes, transfer_encoding)
74
+
75
+ charset = _get_charset(headers_list, mime_type)
76
+ tried = [charset, "utf-8", "windows-1252", "latin-1"]
77
+ for enc in [e for e in tried if e]:
78
+ try:
79
+ text = decoded_bytes.decode(enc)
80
+ return _repair_mojibake(text)
81
+ except (LookupError, UnicodeDecodeError):
82
+ continue
83
+ return _repair_mojibake(decoded_bytes.decode("utf-8", errors="replace"))
17
84
 
18
85
 
19
86
  def parse_plain_text_from_parts(parts: List[Dict[str, Any]]) -> str:
@@ -30,7 +97,9 @@ def parse_plain_text_from_parts(parts: List[Dict[str, Any]]) -> str:
30
97
  mime_type = part.get('mimeType', '')
31
98
  data = part.get('body', {}).get('data', '')
32
99
  if data:
33
- decoded_data = decode_base64_url(data)
100
+ decoded_data = _decode_part_text(
101
+ data, part.get("headers", []), mime_type
102
+ )
34
103
  if 'text/plain' in mime_type:
35
104
  text_chunks.append(decoded_data)
36
105
  elif 'text/html' in mime_type:
@@ -49,7 +118,9 @@ def extract_email_body_in_plain_text(message_data: Dict[str, Any]) -> str:
49
118
  # If top-level body has data (i.e. single-part message)
50
119
  if payload.get('body', {}).get('data'):
51
120
  raw_data = payload['body']['data']
52
- decoded_data = decode_base64_url(raw_data)
121
+ decoded_data = _decode_part_text(
122
+ raw_data, payload.get("headers", []), payload.get("mimeType", "")
123
+ )
53
124
  # Check if it might be HTML
54
125
  mime_type = payload.get('mimeType', '')
55
126
  if 'text/html' in mime_type:
@@ -85,7 +156,7 @@ def find_header(headers_list: List[Dict[str, str]], header_name: str) -> Optiona
85
156
  return None
86
157
 
87
158
 
88
- def parse_single_address(display_str: str) -> (str, str):
159
+ def parse_single_address(display_str: str) -> Tuple[str, str]:
89
160
  """
90
161
  Parses a single display string like "Alice <alice@example.com>"
91
162
  returning (name, email).
@@ -112,7 +183,7 @@ def parse_address_list(display_str: str) -> List[Tuple[str, str]]:
112
183
  return addresses
113
184
 
114
185
 
115
- def find_all_recipients_in_headers(headers_list: List[Dict[str, str]]) -> (str, str):
186
+ def find_all_recipients_in_headers(headers_list: List[Dict[str, str]]) -> Tuple[str, str]:
116
187
  """
117
188
  Collect 'To', 'Cc', 'Bcc' headers, parse each address, and return:
118
189
  (comma-separated receiver names, comma-separated receiver emails)
@@ -71,7 +71,6 @@ async def get_inbound_email_triage_action(
71
71
  "SCHEDULE_MEETING",
72
72
  "SEND_REPLY",
73
73
  "OOF_MESSAGE",
74
- "NEED_MORE_INFO",
75
74
  "FORWARD_TO_OTHER_USER",
76
75
  "NO_MORE_IN_ORGANIZATION",
77
76
  "OBJECTION_RAISED",
@@ -93,6 +92,8 @@ async def get_inbound_email_triage_action(
93
92
  allowed_actions =
94
93
  {allowed_actions}
95
94
 
95
+ If you need more info, use SEND_REPLY and ask a short clarifying question.
96
+
96
97
  1. Email thread or conversation:
97
98
  {[thread_item.model_dump() for thread_item in cleaned_context.current_conversation_context.current_email_thread]}
98
99
 
@@ -129,8 +130,7 @@ async def get_inbound_email_triage_action(
129
130
  Handling interest & objections
130
131
  ------------------------------
131
132
  • If the prospect asks for **pricing, docs, case studies, or more info**
132
- → **NEED_MORE_INFO** and craft a short response that promises to send
133
- the material (or includes it if ≤ 150 words fits).
133
+ → **SEND_REPLY** and ask a short clarifying question or promise a follow-up.
134
134
 
135
135
  • If they mention **budget, timing, or competitor concerns**
136
136
  → **OBJECTION_RAISED** and reply with a brief acknowledgement
@@ -183,7 +183,7 @@ async def get_inbound_email_triage_action(
183
183
  "triage_status": "...",
184
184
  "triage_reason": null or "<reason>",
185
185
  "response_action_to_take": "one of {allowed_actions}",
186
- "response_message": "<only if SEND_REPLY/SCHEDULE_MEETING, else empty>"
186
+ "response_message": "<only if SEND_REPLY/SCHEDULE_MEETING/OBJECTION_RAISED, else empty>"
187
187
  }}
188
188
 
189
189
  Current date is: {current_date_iso}.
@@ -251,7 +251,6 @@ async def generate_inbound_email_response_copy(
251
251
  "UNSUBSCRIBE",
252
252
  "OOF_MESSAGE",
253
253
  "NOT_INTERESTED",
254
- "NEED_MORE_INFO",
255
254
  "FORWARD_TO_OTHER_USER",
256
255
  "NO_MORE_IN_ORGANIZATION",
257
256
  "OBJECTION_RAISED",
@@ -282,7 +281,7 @@ async def generate_inbound_email_response_copy(
282
281
  if cleaned_context.current_conversation_context.current_email_thread else []}
283
282
 
284
283
  2) Lead information:
285
- {lead_data.dict()}
284
+ {lead_data.model_dump()}
286
285
 
287
286
  Sender information:
288
287
  - Full name: {sender_data.sender_full_name or ''}
@@ -316,16 +315,17 @@ async def generate_inbound_email_response_copy(
316
315
  Choose exactly ONE action from:
317
316
  {allowed_actions}
318
317
 
318
+ If you need more info, use SEND_REPLY and ask a short clarifying question.
319
+
319
320
  Priority order:
320
321
  1. UNSUBSCRIBE
321
322
  2. NOT_INTERESTED
322
323
  3. OOF_MESSAGE
323
324
  4. SCHEDULE_MEETING
324
325
  5. FORWARD_TO_OTHER_USER
325
- 6. NEED_MORE_INFO
326
- 7. OBJECTION_RAISED
327
- 8. SEND_REPLY
328
- 9. END_CONVERSATION
326
+ 6. OBJECTION_RAISED
327
+ 7. SEND_REPLY
328
+ 8. END_CONVERSATION
329
329
 
330
330
  =====================================================
331
331
  HOW THE RESPONSE SHOULD SOUND
@@ -391,7 +391,7 @@ async def generate_inbound_email_response_copy(
391
391
  "triage_status": "AUTOMATIC" or "END_CONVERSATION",
392
392
  "triage_reason": "<string if END_CONVERSATION, otherwise null>",
393
393
  "response_action_to_take": "one of {allowed_actions}",
394
- "response_message": "<reply body only if SEND_REPLY or SCHEDULE_MEETING, otherwise empty>"
394
+ "response_message": "<reply body only if SEND_REPLY/SCHEDULE_MEETING/OBJECTION_RAISED, otherwise empty>"
395
395
  }}
396
396
 
397
397
  =====================================================
@@ -446,6 +446,12 @@ async def generate_inbound_email_response_copy(
446
446
  f"Campaign ID: {campaign_id}"
447
447
  )
448
448
 
449
+ response_action = initial_response.response_action_to_take
450
+ if response_action == "NEED_MORE_INFO":
451
+ response_action = "SEND_REPLY"
452
+
453
+ response_message = initial_response.response_message or ""
454
+
449
455
  response_item = MessageItem(
450
456
  message_id="", # or generate one if appropriate
451
457
  thread_id="",
@@ -455,7 +461,7 @@ async def generate_inbound_email_response_copy(
455
461
  receiver_email=campaign_context.lead_info.email or "",
456
462
  iso_datetime=datetime.datetime.utcnow().isoformat(),
457
463
  subject="", # or set some triage subject if needed
458
- body=initial_response.response_message
464
+ body=response_message
459
465
  )
460
466
 
461
467
  # Build a MessageResponse that includes triage metadata plus your message item
@@ -463,9 +469,8 @@ async def generate_inbound_email_response_copy(
463
469
  triage_status=initial_response.triage_status,
464
470
  triage_reason=initial_response.triage_reason,
465
471
  message_item=response_item,
466
- response_action_to_take=initial_response.response_action_to_take
472
+ response_action_to_take=response_action
467
473
  )
468
- print(response_message.model_dump())
469
474
  return response_message.model_dump()
470
475
 
471
476
 
@@ -9,7 +9,7 @@ import json
9
9
  import logging
10
10
  import re
11
11
  import traceback
12
- from typing import Any, Dict, List, Optional
12
+ from typing import Any, Dict, List, Optional, Tuple
13
13
 
14
14
  from fastapi import HTTPException
15
15
 
@@ -52,6 +52,14 @@ async def delete_vector_store(
52
52
  client = create_openai_client(tool_config)
53
53
  try:
54
54
  client.vector_stores.delete(vector_store_id=vector_store_id)
55
+ except openai.NotFoundError:
56
+ logging.warning(f"Vector store not found during delete: {vector_store_id}")
57
+ except openai.APIStatusError as e:
58
+ if getattr(e, "status_code", None) == 404:
59
+ logging.warning(f"Vector store not found during delete: {vector_store_id}")
60
+ return
61
+ logging.error(f"Error deleting vector store {vector_store_id}: {e}")
62
+ raise HTTPException(status_code=400, detail=str(e))
55
63
  except Exception as e:
56
64
  logging.error(f"Error deleting vector store {vector_store_id}: {e}")
57
65
  raise HTTPException(status_code=400, detail=str(e))
@@ -74,10 +82,11 @@ async def upload_file_openai_and_vector_store(
74
82
 
75
83
  try:
76
84
  if isinstance(file_path_or_bytes, str):
77
- file_upload = client.files.create(
78
- file=open(file_path_or_bytes, "rb"),
79
- purpose=purpose,
80
- )
85
+ with open(file_path_or_bytes, "rb") as f:
86
+ file_upload = client.files.create(
87
+ file=f,
88
+ purpose=purpose,
89
+ )
81
90
  elif isinstance(file_path_or_bytes, bytes):
82
91
  file_upload = client.files.create(
83
92
  file=(file_name, file_path_or_bytes, mime_type),
@@ -108,10 +117,11 @@ async def upload_file_openai(
108
117
 
109
118
  try:
110
119
  if isinstance(file_path_or_bytes, str):
111
- file_upload = client.files.create(
112
- file=open(file_path_or_bytes, "rb"),
113
- purpose=purpose,
114
- )
120
+ with open(file_path_or_bytes, "rb") as f:
121
+ file_upload = client.files.create(
122
+ file=f,
123
+ purpose=purpose,
124
+ )
115
125
  else:
116
126
  file_upload = client.files.create(
117
127
  file=(file_name, file_path_or_bytes, mime_type),
@@ -217,7 +227,7 @@ async def run_response_text(
217
227
  max_tokens: int = 2048,
218
228
  store: bool = True,
219
229
  tool_config: Optional[List[Dict]] = None,
220
- ) -> (str, str):
230
+ ) -> Tuple[str, str]:
221
231
  """Plain text completion via the Responses API."""
222
232
  client = create_openai_client(tool_config)
223
233
 
@@ -241,7 +251,7 @@ async def run_response_structured(
241
251
  max_tokens: int = 1024,
242
252
  store: bool = True,
243
253
  tool_config: Optional[List[Dict]] = None,
244
- ) -> (Any, str):
254
+ ) -> Tuple[Any, str]:
245
255
  """Structured JSON output via Responses API."""
246
256
  client = create_openai_client(tool_config)
247
257
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dhisana
3
- Version: 0.0.1.dev273
3
+ Version: 0.0.1.dev274
4
4
  Summary: A Python SDK for Dhisana AI Platform
5
5
  Home-page: https://github.com/dhisana-ai/dhisana-python-sdk
6
6
  Author: Admin
@@ -30,7 +30,7 @@ dhisana/utils/composite_tools.py,sha256=ZlwHCp7PXjYFUWUEeR_fTF0Z4Wg-4F6eBi1reE3F
30
30
  dhisana/utils/dataframe_tools.py,sha256=R6eUXjwR5SG6_K87rWjj4T5PT2w6xvVF2EKBajIv-RE,9242
31
31
  dhisana/utils/domain_parser.py,sha256=Kw5MPP06wK2azWQzuSiOE-DffOezLqDyF-L9JEBsMSU,1206
32
32
  dhisana/utils/email_body_utils.py,sha256=rlCVjdBlqNnEiUberJGXGcrYY1GQOkW0-aB6AEpS3L4,2302
33
- dhisana/utils/email_parse_helpers.py,sha256=LIdm1B1IyGSW50y8EkxOk6YRjvxO2SJTgTKPLxYls_o,4613
33
+ dhisana/utils/email_parse_helpers.py,sha256=rl72ggS-yoB-w3ZHW2sevKJulQ-_8iLdpVTH6QnKPcs,6789
34
34
  dhisana/utils/email_provider.py,sha256=ukW_0nHcjTQmpnE9pdJci78LrZcsK1_0v6kcgc2ChPY,14573
35
35
  dhisana/utils/enrich_lead_information.py,sha256=O0fV-8MlXFT_z5aXvmvXVT76AISN94GpvAOlq3q_Phw,39411
36
36
  dhisana/utils/extract_email_content_for_llm.py,sha256=SQmMZ3YJtm3ZI44XiWEVAItcAwrsSSy1QzDne7LTu_Q,3713
@@ -40,7 +40,7 @@ dhisana/utils/g2_tools.py,sha256=a4vmBYCBvLae5CdpOhMN1oNlvO8v9J1B5Sd8T5PzuU8,334
40
40
  dhisana/utils/generate_content.py,sha256=kkf-aPuA32BNgwk_j5N6unYHOZpO7zIfO6zP95XM9fA,2298
41
41
  dhisana/utils/generate_custom_message.py,sha256=tQsryytoYKP5uF3bRENeZks1LvOMFCP6L1487P_r_hk,12072
42
42
  dhisana/utils/generate_email.py,sha256=cEIr9HbGduk-l_0dNY2lCBABFBmmcOHaMyI7oy8ChWQ,16470
43
- dhisana/utils/generate_email_response.py,sha256=Xk6t2hW_QbumvXf5uUdsD-Lkq8hfzRD9QWioYcdWM1k,21194
43
+ dhisana/utils/generate_email_response.py,sha256=LPsi2Orhb0W9SG8HDN2aA0w2pgW_c_HTDFAlw9R1cns,21391
44
44
  dhisana/utils/generate_flow.py,sha256=QMn6bWo0nH0fBvy2Ebub1XfH5udnVAqsPsbIqCtQPXU,4728
45
45
  dhisana/utils/generate_leads_salesnav.py,sha256=FG7q6GSm9IywZ9TgQnn5_N3QNfiI-Qk2gaO_3GS99nY,12236
46
46
  dhisana/utils/generate_linkedin_connect_message.py,sha256=niKnL1YwtROVsPctaIQFHeSIvr4ewqEougUBAvMmoC0,13385
@@ -57,7 +57,7 @@ dhisana/utils/lusha_tools.py,sha256=MdiWlxBBjSNpSKz8rhNOyLPtbeh-YWHgGiUq54vN_gM,
57
57
  dhisana/utils/mailgun_tools.py,sha256=brOgfEx-ciqEdDkXEfzMBfXkG0kRWVscg76tQDXb_lk,5826
58
58
  dhisana/utils/mailreach_tools.py,sha256=uJ_gIcg8qrj5-k3jnYYhpwLVnQncoA1swzr5Jfkc1JU,3864
59
59
  dhisana/utils/microsoft365_tools.py,sha256=ClqBzTrJ2SZM5K9nsOFyyHRfV-d-6jlxXNpNONtgLlY,18596
60
- dhisana/utils/openai_assistant_and_file_utils.py,sha256=-eyPcxFvtS-DDtYQGle1SU6C6CuxjulVIojFy27HeWc,8957
60
+ dhisana/utils/openai_assistant_and_file_utils.py,sha256=snaawY7Y94q-XMzjK7UUylLy9fWlcmpWSOKcu1u04LE,9507
61
61
  dhisana/utils/openai_helpers.py,sha256=ZK9S5-jcLCpiiD6XBLkCqYcNz-AGYmO9xh4e2H-FDLo,40155
62
62
  dhisana/utils/openapi_spec_to_tools.py,sha256=oBLVq3WeDWvW9O02NCvY8bxQURQdKwHJHGcX8bC_b2I,1926
63
63
  dhisana/utils/parse_linkedin_messages_txt.py,sha256=g3N_ac70mAEuDDQ7Ott6mkOaBwI3ZvcsJD3R9RlYwPQ,3320
@@ -95,8 +95,8 @@ dhisana/workflow/agent.py,sha256=esv7_i_XuMkV2j1nz_UlsHov_m6X5WZZiZm_tG4OBHU,565
95
95
  dhisana/workflow/flow.py,sha256=xWE3qQbM7j2B3FH8XnY3zOL_QXX4LbTW4ArndnEYJE0,1638
96
96
  dhisana/workflow/task.py,sha256=HlWz9mtrwLYByoSnePOemBUBrMEcj7KbgNjEE1oF5wo,1830
97
97
  dhisana/workflow/test.py,sha256=E7lRnXK0PguTNzyasHytLzTJdkqIPxG5_4qk4hMEeKc,3399
98
- dhisana-0.0.1.dev273.dist-info/METADATA,sha256=NXgT5Z07SUxtokSLs_puNFVFvN0dRX7BNqR_QxB_em4,1190
99
- dhisana-0.0.1.dev273.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
100
- dhisana-0.0.1.dev273.dist-info/entry_points.txt,sha256=jujxteZmNI9EkEaK-pOCoWuBujU8TCevdkfl9ZcKHek,49
101
- dhisana-0.0.1.dev273.dist-info/top_level.txt,sha256=NETTHt6YifG_P7XtRHbQiXZlgSFk9Qh9aR-ng1XTf4s,8
102
- dhisana-0.0.1.dev273.dist-info/RECORD,,
98
+ dhisana-0.0.1.dev274.dist-info/METADATA,sha256=sPREfHEIvxxGk24-YL0zauwjU1n_pAUvaVmyLL_XKj0,1190
99
+ dhisana-0.0.1.dev274.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
100
+ dhisana-0.0.1.dev274.dist-info/entry_points.txt,sha256=jujxteZmNI9EkEaK-pOCoWuBujU8TCevdkfl9ZcKHek,49
101
+ dhisana-0.0.1.dev274.dist-info/top_level.txt,sha256=NETTHt6YifG_P7XtRHbQiXZlgSFk9Qh9aR-ng1XTf4s,8
102
+ dhisana-0.0.1.dev274.dist-info/RECORD,,