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.
- dhisana/schemas/common.py +33 -0
- dhisana/schemas/sales.py +224 -23
- dhisana/utils/add_mapping.py +72 -63
- dhisana/utils/apollo_tools.py +739 -109
- dhisana/utils/built_with_api_tools.py +4 -2
- dhisana/utils/cache_output_tools.py +23 -23
- dhisana/utils/check_email_validity_tools.py +456 -458
- dhisana/utils/check_for_intent_signal.py +1 -2
- dhisana/utils/check_linkedin_url_validity.py +34 -8
- dhisana/utils/clay_tools.py +3 -2
- dhisana/utils/clean_properties.py +3 -1
- dhisana/utils/compose_salesnav_query.py +0 -1
- dhisana/utils/compose_search_query.py +7 -3
- dhisana/utils/composite_tools.py +0 -1
- dhisana/utils/dataframe_tools.py +2 -2
- dhisana/utils/email_body_utils.py +72 -0
- dhisana/utils/email_provider.py +375 -0
- dhisana/utils/enrich_lead_information.py +585 -85
- dhisana/utils/fetch_openai_config.py +129 -0
- dhisana/utils/field_validators.py +1 -1
- dhisana/utils/g2_tools.py +0 -1
- dhisana/utils/generate_content.py +0 -1
- dhisana/utils/generate_email.py +69 -16
- dhisana/utils/generate_email_response.py +298 -41
- dhisana/utils/generate_flow.py +0 -1
- dhisana/utils/generate_linkedin_connect_message.py +19 -6
- dhisana/utils/generate_linkedin_response_message.py +156 -65
- dhisana/utils/generate_structured_output_internal.py +351 -131
- dhisana/utils/google_custom_search.py +150 -44
- dhisana/utils/google_oauth_tools.py +721 -0
- dhisana/utils/google_workspace_tools.py +391 -25
- dhisana/utils/hubspot_clearbit.py +3 -1
- dhisana/utils/hubspot_crm_tools.py +771 -167
- dhisana/utils/instantly_tools.py +3 -1
- dhisana/utils/lusha_tools.py +10 -7
- dhisana/utils/mailgun_tools.py +150 -0
- dhisana/utils/microsoft365_tools.py +447 -0
- dhisana/utils/openai_assistant_and_file_utils.py +121 -177
- dhisana/utils/openai_helpers.py +19 -16
- dhisana/utils/parse_linkedin_messages_txt.py +2 -3
- dhisana/utils/profile.py +37 -0
- dhisana/utils/proxy_curl_tools.py +507 -206
- dhisana/utils/proxycurl_search_leads.py +426 -0
- dhisana/utils/research_lead.py +121 -68
- dhisana/utils/sales_navigator_crawler.py +1 -6
- dhisana/utils/salesforce_crm_tools.py +323 -50
- dhisana/utils/search_router.py +131 -0
- dhisana/utils/search_router_jobs.py +51 -0
- dhisana/utils/sendgrid_tools.py +126 -91
- dhisana/utils/serarch_router_local_business.py +75 -0
- dhisana/utils/serpapi_additional_tools.py +290 -0
- dhisana/utils/serpapi_google_jobs.py +117 -0
- dhisana/utils/serpapi_google_search.py +188 -0
- dhisana/utils/serpapi_local_business_search.py +129 -0
- dhisana/utils/serpapi_search_tools.py +363 -432
- dhisana/utils/serperdev_google_jobs.py +125 -0
- dhisana/utils/serperdev_local_business.py +154 -0
- dhisana/utils/serperdev_search.py +233 -0
- dhisana/utils/smtp_email_tools.py +576 -0
- dhisana/utils/test_connect.py +1765 -92
- dhisana/utils/trasform_json.py +95 -16
- dhisana/utils/web_download_parse_tools.py +0 -1
- dhisana/utils/zoominfo_tools.py +2 -3
- dhisana/workflow/test.py +1 -1
- {dhisana-0.0.1.dev85.dist-info → dhisana-0.0.1.dev236.dist-info}/METADATA +5 -2
- dhisana-0.0.1.dev236.dist-info/RECORD +100 -0
- {dhisana-0.0.1.dev85.dist-info → dhisana-0.0.1.dev236.dist-info}/WHEEL +1 -1
- dhisana-0.0.1.dev85.dist-info/RECORD +0 -81
- {dhisana-0.0.1.dev85.dist-info → dhisana-0.0.1.dev236.dist-info}/entry_points.txt +0 -0
- {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
|
dhisana/utils/g2_tools.py
CHANGED
dhisana/utils/generate_email.py
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
# Import necessary modules
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
import
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
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
|
|