dhisana 0.0.1.dev85__py3-none-any.whl → 0.0.1.dev236__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. dhisana/schemas/common.py +33 -0
  2. dhisana/schemas/sales.py +224 -23
  3. dhisana/utils/add_mapping.py +72 -63
  4. dhisana/utils/apollo_tools.py +739 -109
  5. dhisana/utils/built_with_api_tools.py +4 -2
  6. dhisana/utils/cache_output_tools.py +23 -23
  7. dhisana/utils/check_email_validity_tools.py +456 -458
  8. dhisana/utils/check_for_intent_signal.py +1 -2
  9. dhisana/utils/check_linkedin_url_validity.py +34 -8
  10. dhisana/utils/clay_tools.py +3 -2
  11. dhisana/utils/clean_properties.py +3 -1
  12. dhisana/utils/compose_salesnav_query.py +0 -1
  13. dhisana/utils/compose_search_query.py +7 -3
  14. dhisana/utils/composite_tools.py +0 -1
  15. dhisana/utils/dataframe_tools.py +2 -2
  16. dhisana/utils/email_body_utils.py +72 -0
  17. dhisana/utils/email_provider.py +375 -0
  18. dhisana/utils/enrich_lead_information.py +585 -85
  19. dhisana/utils/fetch_openai_config.py +129 -0
  20. dhisana/utils/field_validators.py +1 -1
  21. dhisana/utils/g2_tools.py +0 -1
  22. dhisana/utils/generate_content.py +0 -1
  23. dhisana/utils/generate_email.py +69 -16
  24. dhisana/utils/generate_email_response.py +298 -41
  25. dhisana/utils/generate_flow.py +0 -1
  26. dhisana/utils/generate_linkedin_connect_message.py +19 -6
  27. dhisana/utils/generate_linkedin_response_message.py +156 -65
  28. dhisana/utils/generate_structured_output_internal.py +351 -131
  29. dhisana/utils/google_custom_search.py +150 -44
  30. dhisana/utils/google_oauth_tools.py +721 -0
  31. dhisana/utils/google_workspace_tools.py +391 -25
  32. dhisana/utils/hubspot_clearbit.py +3 -1
  33. dhisana/utils/hubspot_crm_tools.py +771 -167
  34. dhisana/utils/instantly_tools.py +3 -1
  35. dhisana/utils/lusha_tools.py +10 -7
  36. dhisana/utils/mailgun_tools.py +150 -0
  37. dhisana/utils/microsoft365_tools.py +447 -0
  38. dhisana/utils/openai_assistant_and_file_utils.py +121 -177
  39. dhisana/utils/openai_helpers.py +19 -16
  40. dhisana/utils/parse_linkedin_messages_txt.py +2 -3
  41. dhisana/utils/profile.py +37 -0
  42. dhisana/utils/proxy_curl_tools.py +507 -206
  43. dhisana/utils/proxycurl_search_leads.py +426 -0
  44. dhisana/utils/research_lead.py +121 -68
  45. dhisana/utils/sales_navigator_crawler.py +1 -6
  46. dhisana/utils/salesforce_crm_tools.py +323 -50
  47. dhisana/utils/search_router.py +131 -0
  48. dhisana/utils/search_router_jobs.py +51 -0
  49. dhisana/utils/sendgrid_tools.py +126 -91
  50. dhisana/utils/serarch_router_local_business.py +75 -0
  51. dhisana/utils/serpapi_additional_tools.py +290 -0
  52. dhisana/utils/serpapi_google_jobs.py +117 -0
  53. dhisana/utils/serpapi_google_search.py +188 -0
  54. dhisana/utils/serpapi_local_business_search.py +129 -0
  55. dhisana/utils/serpapi_search_tools.py +363 -432
  56. dhisana/utils/serperdev_google_jobs.py +125 -0
  57. dhisana/utils/serperdev_local_business.py +154 -0
  58. dhisana/utils/serperdev_search.py +233 -0
  59. dhisana/utils/smtp_email_tools.py +576 -0
  60. dhisana/utils/test_connect.py +1765 -92
  61. dhisana/utils/trasform_json.py +95 -16
  62. dhisana/utils/web_download_parse_tools.py +0 -1
  63. dhisana/utils/zoominfo_tools.py +2 -3
  64. dhisana/workflow/test.py +1 -1
  65. {dhisana-0.0.1.dev85.dist-info → dhisana-0.0.1.dev236.dist-info}/METADATA +5 -2
  66. dhisana-0.0.1.dev236.dist-info/RECORD +100 -0
  67. {dhisana-0.0.1.dev85.dist-info → dhisana-0.0.1.dev236.dist-info}/WHEEL +1 -1
  68. dhisana-0.0.1.dev85.dist-info/RECORD +0 -81
  69. {dhisana-0.0.1.dev85.dist-info → dhisana-0.0.1.dev236.dist-info}/entry_points.txt +0 -0
  70. {dhisana-0.0.1.dev85.dist-info → dhisana-0.0.1.dev236.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,129 @@
1
+ """
2
+ Unified OpenAI / Azure OpenAI helper (no env-fallback for secrets)
3
+ =================================================================
4
+
5
+ Resolution order
6
+ ----------------
7
+ 1. If `tool_config` has a **"openai"** block → public OpenAI
8
+ 2. Else if it has an **"azure_openai"** block → Azure OpenAI
9
+ 3. Otherwise → raise ValueError
10
+
11
+ `api_key` **and** `endpoint` (for Azure) must therefore be supplied in
12
+ `tool_config`. They will never be read from the host environment.
13
+
14
+ Optional:
15
+ • `AZURE_OPENAI_API_VERSION` – defaults to 2025-03-01-preview
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import os
21
+ from typing import Dict, List, Optional, Tuple, Union
22
+
23
+ from openai import AsyncOpenAI, OpenAI, AzureOpenAI, AsyncAzureOpenAI
24
+
25
+
26
+ # ─────────────────────────────────────────────────────────────────────────────
27
+ # 1. Helpers: config parsing
28
+ # ─────────────────────────────────────────────────────────────────────────────
29
+
30
+ def _extract_config(
31
+ tool_config: Optional[List[Dict]], provider_name: str
32
+ ) -> Dict[str, str]:
33
+ """Return the config map for the requested provider name, else {}."""
34
+ if not tool_config:
35
+ return {}
36
+ block = next((b for b in tool_config if b.get("name") == provider_name), {})
37
+ return {entry["name"]: entry["value"] for entry in block.get("configuration", []) if entry}
38
+
39
+
40
+ def _discover_credentials(
41
+ tool_config: Optional[List[Dict]] = None,
42
+ ) -> Tuple[str, str, Optional[str]]:
43
+ """
44
+ Return (provider, api_key, endpoint_or_None).
45
+
46
+ provider ∈ {"public", "azure"}
47
+ """
48
+ # 1️⃣ Public OpenAI
49
+ openai_cfg = _extract_config(tool_config, "openai")
50
+ if openai_cfg:
51
+ key = openai_cfg.get("apiKey")
52
+ if not key:
53
+ raise ValueError(
54
+ "OpenAI integration is not configured. Please configure the connection to OpenAI in Integrations."
55
+ )
56
+ return "public", key, None
57
+
58
+ # 2️⃣ Azure OpenAI
59
+ azure_cfg = _extract_config(tool_config, "azure_openai")
60
+ if azure_cfg:
61
+ key = azure_cfg.get("apiKey")
62
+ endpoint = azure_cfg.get("endpoint")
63
+ if not key or not endpoint:
64
+ raise ValueError(
65
+ "Azure OpenAI integration is not configured. Please configure the connection to Azure OpenAI in Integrations."
66
+ )
67
+ return "azure", key, endpoint
68
+
69
+ # 3️⃣ Neither block present → error
70
+ raise ValueError(
71
+ "OpenAI integration is not configured. Please configure the connection to OpenAI in Integrations."
72
+ )
73
+
74
+
75
+ # ─────────────────────────────────────────────────────────────────────────────
76
+ # 2. Client factories
77
+ # ─────────────────────────────────────────────────────────────────────────────
78
+
79
+ def _api_version() -> str:
80
+ """Return the Azure API version (env-controlled, no secret)."""
81
+ return os.getenv("AZURE_OPENAI_API_VERSION", "2025-03-01-preview")
82
+
83
+
84
+ def create_openai_client(
85
+ tool_config: Optional[List[Dict]] = None,
86
+ ) -> Union[OpenAI, AzureOpenAI]:
87
+ """
88
+ Return a *synchronous* client:
89
+ • openai.OpenAI – public service
90
+ • openai.AzureOpenAI – Azure
91
+ """
92
+ provider, key, endpoint = _discover_credentials(tool_config)
93
+
94
+ if provider == "public":
95
+ return OpenAI(api_key=key)
96
+
97
+ # Azure
98
+ return AzureOpenAI(api_key=key, azure_endpoint=endpoint, api_version=_api_version())
99
+
100
+
101
+ def create_async_openai_client(
102
+ tool_config: Optional[List[Dict]] = None,
103
+ ) -> AsyncOpenAI:
104
+ """
105
+ Return an *async* client (AsyncOpenAI).
106
+
107
+ For Azure we pass both `azure_endpoint` and `api_version`.
108
+ """
109
+ provider, key, endpoint = _discover_credentials(tool_config)
110
+
111
+ if provider == "public":
112
+ return AsyncOpenAI(api_key=key)
113
+
114
+ return AsyncAzureOpenAI(
115
+ api_key=key,
116
+ azure_endpoint=endpoint,
117
+ api_version=_api_version(),
118
+ )
119
+
120
+
121
+
122
+ # ─────────────────────────────────────────────────────────────────────────────
123
+ # 3. Convenience helper (legacy)
124
+ # ─────────────────────────────────────────────────────────────────────────────
125
+
126
+ def get_openai_access_token(tool_config: Optional[List[Dict]] = None) -> str:
127
+ """Return just the API key (legacy helper)."""
128
+ _, key, _ = _discover_credentials(tool_config)
129
+ return key
@@ -1,5 +1,5 @@
1
1
  import logging
2
- from urllib.parse import urlparse, urlunparse
2
+ from urllib.parse import urlparse
3
3
  import urllib.parse
4
4
  import re
5
5
 
dhisana/utils/g2_tools.py CHANGED
@@ -1,5 +1,4 @@
1
1
  import asyncio
2
- import json
3
2
  import logging
4
3
  import os
5
4
  from typing import Optional
@@ -1,4 +1,3 @@
1
- from enum import Enum
2
1
  from typing import Dict, List, Optional
3
2
  from dhisana.schemas.sales import ChannelType, ContentGenerationContext
4
3
  from dhisana.utils.generate_email_response import generate_inbound_email_response_variations
@@ -1,12 +1,10 @@
1
1
  # Import necessary modules
2
- import os
3
- from typing import Any, Dict, List, Optional
4
- import aiohttp
2
+ import html as html_lib
3
+ import re
4
+ from typing import Dict, List, Optional
5
5
  from pydantic import BaseModel
6
- from enum import Enum
7
6
 
8
- from dhisana.schemas.sales import CampaignContext, ContentGenerationContext, ConversationContext, Lead, MessageGenerationInstructions, MessageItem, PromptEngineeringGuidance, SenderInfo
9
- from dhisana.utils.assistant_tool_tag import assistant_tool
7
+ from dhisana.schemas.sales import CampaignContext, ContentGenerationContext, ConversationContext, Lead, MessageGenerationInstructions, MessageItem, SenderInfo
10
8
  from dhisana.utils.generate_structured_output_internal import (
11
9
  get_structured_output_internal,
12
10
  get_structured_output_with_assistant_and_vector_store
@@ -20,9 +18,22 @@ from pydantic import BaseModel, ConfigDict
20
18
  class EmailCopy(BaseModel):
21
19
  subject: str
22
20
  body: str
21
+ body_html: Optional[str] = None
23
22
 
24
23
  model_config = ConfigDict(extra="forbid")
25
24
 
25
+
26
+ def _html_to_plain_text(html_content: str) -> str:
27
+ """Simple HTML to text conversion to backfill plain body."""
28
+ if not html_content:
29
+ return ""
30
+ # Remove tags and normalize whitespace
31
+ text = re.sub(r"<[^>]+>", " ", html_content)
32
+ text = html_lib.unescape(text)
33
+ # Collapse repeated whitespace/newlines
34
+ lines = [line.strip() for line in text.splitlines()]
35
+ return "\n".join([line for line in lines if line])
36
+
26
37
  # -----------------------------------------------------------------------------
27
38
  # Utility to Clean Up Context (if needed)
28
39
  # -----------------------------------------------------------------------------
@@ -93,6 +104,42 @@ async def generate_personalized_email_copy(
93
104
  campaign_data = cleaned_context.campaign_context or CampaignContext()
94
105
  conversation_data = cleaned_context.current_conversation_context or ConversationContext()
95
106
 
107
+ html_note = (
108
+ f"\n Provide the HTML body using this guidance/template when possible:\n {message_instructions.html_template}"
109
+ if getattr(message_instructions, "html_template", None)
110
+ else ""
111
+ )
112
+ important_requirements = """
113
+ IMPORTANT REQUIREMENTS:
114
+ - Output must be JSON with "subject", "body", and "body_html" fields.
115
+ - "body_html" should be clean HTML suitable for email (no external assets), inline styles welcome.
116
+ - "body" must be the plain-text equivalent of "body_html".
117
+ - Keep it concise and relevant. No placeholders or extra instructions.
118
+ - Do not include PII or internal references, guids or content identifiers in the email.
119
+ - Use conversational names for company/person placeholders when provided.
120
+ - Email has salutation Hi <First Name>, unless otherwise specified.
121
+ - Make sure the signature in body has the sender_first_name correct and in the format the user specified.
122
+ - Do Not Make up information. use the information provided in the context and instructions only.
123
+ - Do Not use em dash in the generated output.
124
+ """
125
+ if not getattr(message_instructions, "allow_html", False):
126
+ important_requirements = """
127
+ IMPORTANT REQUIREMENTS:
128
+ - Output must be JSON with "subject" and "body" fields only.
129
+ - In the subject or body DO NOT include any HTML tags like <a>, <b>, <i>, etc.
130
+ - The body and subject should be in plain text.
131
+ - If there is a link provided in email use it as is. dont wrap it in any HTML tags.
132
+ - Keep it concise and relevant. No placeholders or extra instructions.
133
+ - Do not include PII or internal references, guids or content identifiers in the email.
134
+ - User conversational name for company name if used.
135
+ - Email has saluation Hi <First Name>, unless otherwise specified.
136
+ - <First Name> is the first name of the lead. Its conversational name. It does not have any special characters or spaces.
137
+ - Make sure the signature in body has the sender_first_name is correct and in the format user has specified.
138
+ - Do Not Make up information. use the information provided in the context and instructions only.
139
+ - Make sure the body text is well-formatted and that newline and carriage-return characters are correctly present and preserved in the message body.
140
+ - Do Not use em dash in the generated output.
141
+ """
142
+
96
143
  # Construct the consolidated prompt
97
144
  initial_prompt = f"""
98
145
  Hi AI Assistant,
@@ -118,7 +165,7 @@ async def generate_personalized_email_copy(
118
165
  Triage Guidelines (LinkedIn): {campaign_data.linkedin_triage_guidelines or ''}
119
166
 
120
167
  4) Messaging Instructions (template/framework):
121
- {selected_instructions}
168
+ {selected_instructions}{html_note}
122
169
 
123
170
  5) External Data / Vector Store:
124
171
  (I will be provided with file_search tool if present.)
@@ -127,48 +174,54 @@ async def generate_personalized_email_copy(
127
174
  Email Thread: {conversation_data.current_email_thread or ''}
128
175
  LinkedIn Thread: {conversation_data.current_linkedin_thread or ''}
129
176
 
130
- IMPORTANT REQUIREMENTS:
131
- - Output must be JSON with "subject" and "body" fields only.
132
- - Keep it concise and relevant. No placeholders or extra instructions.
133
- - Do not include PII or internal references, guids or content identifiers in the email.
177
+ {important_requirements}
134
178
  """
135
179
 
136
180
  # Check if a vector store is available
137
181
  vector_store_id = (email_context.external_known_data.external_openai_vector_store_id
138
182
  if email_context.external_known_data else None)
139
183
 
140
- used_vector_store = False
141
184
  initial_response = None
142
185
  initial_status = ""
143
186
 
144
187
  # Generate initial draft
145
188
  if vector_store_id:
146
- used_vector_store = True
147
189
  initial_response, initial_status = await get_structured_output_with_assistant_and_vector_store(
148
190
  prompt=initial_prompt,
149
191
  response_format=EmailCopy,
150
192
  vector_store_id=vector_store_id,
151
- tool_config=tool_config
193
+ model="gpt-5.1-chat",
194
+ tool_config=tool_config,
195
+ use_cache=email_context.message_instructions.use_cache if email_context.message_instructions else True
152
196
  )
153
197
  else:
154
198
  # Otherwise, generate the initial draft internally
155
199
  initial_response, initial_status = await get_structured_output_internal(
156
200
  prompt=initial_prompt,
157
201
  response_format=EmailCopy,
158
- tool_config=tool_config
202
+ model="gpt-5.1-chat",
203
+ tool_config=tool_config,
204
+ use_cache=email_context.message_instructions.use_cache if email_context.message_instructions else True
159
205
  )
160
206
 
161
207
  if initial_status != "SUCCESS":
162
208
  raise Exception("Error: Could not generate initial draft for the personalized email.")
209
+ plain_body = initial_response.body
210
+ html_body = getattr(initial_response, "body_html", None)
211
+ if not plain_body and html_body:
212
+ plain_body = _html_to_plain_text(html_body)
213
+
163
214
  response_item = MessageItem(
164
215
  message_id="", # or some real ID if you have it
216
+ thread_id="",
165
217
  sender_name=email_context.sender_info.sender_full_name or "",
166
218
  sender_email=email_context.sender_info.sender_email or "",
167
219
  receiver_name=email_context.lead_info.full_name or "",
168
220
  receiver_email=email_context.lead_info.email or "",
169
221
  iso_datetime=datetime.utcnow().isoformat(),
170
222
  subject=initial_response.subject,
171
- body=initial_response.body
223
+ body=plain_body,
224
+ html_body=html_body if getattr(message_instructions, "allow_html", False) else None,
172
225
  )
173
226
  return response_item.model_dump()
174
227