dhisana 0.0.1.dev243__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 (102) hide show
  1. dhisana/__init__.py +1 -0
  2. dhisana/cli/__init__.py +1 -0
  3. dhisana/cli/cli.py +20 -0
  4. dhisana/cli/datasets.py +27 -0
  5. dhisana/cli/models.py +26 -0
  6. dhisana/cli/predictions.py +20 -0
  7. dhisana/schemas/__init__.py +1 -0
  8. dhisana/schemas/common.py +399 -0
  9. dhisana/schemas/sales.py +965 -0
  10. dhisana/ui/__init__.py +1 -0
  11. dhisana/ui/components.py +472 -0
  12. dhisana/utils/__init__.py +1 -0
  13. dhisana/utils/add_mapping.py +352 -0
  14. dhisana/utils/agent_tools.py +51 -0
  15. dhisana/utils/apollo_tools.py +1597 -0
  16. dhisana/utils/assistant_tool_tag.py +4 -0
  17. dhisana/utils/built_with_api_tools.py +282 -0
  18. dhisana/utils/cache_output_tools.py +98 -0
  19. dhisana/utils/cache_output_tools_local.py +78 -0
  20. dhisana/utils/check_email_validity_tools.py +717 -0
  21. dhisana/utils/check_for_intent_signal.py +107 -0
  22. dhisana/utils/check_linkedin_url_validity.py +209 -0
  23. dhisana/utils/clay_tools.py +43 -0
  24. dhisana/utils/clean_properties.py +135 -0
  25. dhisana/utils/company_utils.py +60 -0
  26. dhisana/utils/compose_salesnav_query.py +259 -0
  27. dhisana/utils/compose_search_query.py +759 -0
  28. dhisana/utils/compose_three_step_workflow.py +234 -0
  29. dhisana/utils/composite_tools.py +137 -0
  30. dhisana/utils/dataframe_tools.py +237 -0
  31. dhisana/utils/domain_parser.py +45 -0
  32. dhisana/utils/email_body_utils.py +72 -0
  33. dhisana/utils/email_parse_helpers.py +132 -0
  34. dhisana/utils/email_provider.py +375 -0
  35. dhisana/utils/enrich_lead_information.py +933 -0
  36. dhisana/utils/extract_email_content_for_llm.py +101 -0
  37. dhisana/utils/fetch_openai_config.py +129 -0
  38. dhisana/utils/field_validators.py +426 -0
  39. dhisana/utils/g2_tools.py +104 -0
  40. dhisana/utils/generate_content.py +41 -0
  41. dhisana/utils/generate_custom_message.py +271 -0
  42. dhisana/utils/generate_email.py +278 -0
  43. dhisana/utils/generate_email_response.py +465 -0
  44. dhisana/utils/generate_flow.py +102 -0
  45. dhisana/utils/generate_leads_salesnav.py +303 -0
  46. dhisana/utils/generate_linkedin_connect_message.py +224 -0
  47. dhisana/utils/generate_linkedin_response_message.py +317 -0
  48. dhisana/utils/generate_structured_output_internal.py +462 -0
  49. dhisana/utils/google_custom_search.py +267 -0
  50. dhisana/utils/google_oauth_tools.py +727 -0
  51. dhisana/utils/google_workspace_tools.py +1294 -0
  52. dhisana/utils/hubspot_clearbit.py +96 -0
  53. dhisana/utils/hubspot_crm_tools.py +2440 -0
  54. dhisana/utils/instantly_tools.py +149 -0
  55. dhisana/utils/linkedin_crawler.py +168 -0
  56. dhisana/utils/lusha_tools.py +333 -0
  57. dhisana/utils/mailgun_tools.py +156 -0
  58. dhisana/utils/mailreach_tools.py +123 -0
  59. dhisana/utils/microsoft365_tools.py +455 -0
  60. dhisana/utils/openai_assistant_and_file_utils.py +267 -0
  61. dhisana/utils/openai_helpers.py +977 -0
  62. dhisana/utils/openapi_spec_to_tools.py +45 -0
  63. dhisana/utils/openapi_tool/__init__.py +1 -0
  64. dhisana/utils/openapi_tool/api_models.py +633 -0
  65. dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +271 -0
  66. dhisana/utils/openapi_tool/openapi_tool.py +319 -0
  67. dhisana/utils/parse_linkedin_messages_txt.py +100 -0
  68. dhisana/utils/profile.py +37 -0
  69. dhisana/utils/proxy_curl_tools.py +1226 -0
  70. dhisana/utils/proxycurl_search_leads.py +426 -0
  71. dhisana/utils/python_function_to_tools.py +83 -0
  72. dhisana/utils/research_lead.py +176 -0
  73. dhisana/utils/sales_navigator_crawler.py +1103 -0
  74. dhisana/utils/salesforce_crm_tools.py +477 -0
  75. dhisana/utils/search_router.py +131 -0
  76. dhisana/utils/search_router_jobs.py +51 -0
  77. dhisana/utils/sendgrid_tools.py +162 -0
  78. dhisana/utils/serarch_router_local_business.py +75 -0
  79. dhisana/utils/serpapi_additional_tools.py +290 -0
  80. dhisana/utils/serpapi_google_jobs.py +117 -0
  81. dhisana/utils/serpapi_google_search.py +188 -0
  82. dhisana/utils/serpapi_local_business_search.py +129 -0
  83. dhisana/utils/serpapi_search_tools.py +852 -0
  84. dhisana/utils/serperdev_google_jobs.py +125 -0
  85. dhisana/utils/serperdev_local_business.py +154 -0
  86. dhisana/utils/serperdev_search.py +233 -0
  87. dhisana/utils/smtp_email_tools.py +582 -0
  88. dhisana/utils/test_connect.py +2087 -0
  89. dhisana/utils/trasform_json.py +173 -0
  90. dhisana/utils/web_download_parse_tools.py +189 -0
  91. dhisana/utils/workflow_code_model.py +5 -0
  92. dhisana/utils/zoominfo_tools.py +357 -0
  93. dhisana/workflow/__init__.py +1 -0
  94. dhisana/workflow/agent.py +18 -0
  95. dhisana/workflow/flow.py +44 -0
  96. dhisana/workflow/task.py +43 -0
  97. dhisana/workflow/test.py +90 -0
  98. dhisana-0.0.1.dev243.dist-info/METADATA +43 -0
  99. dhisana-0.0.1.dev243.dist-info/RECORD +102 -0
  100. dhisana-0.0.1.dev243.dist-info/WHEEL +5 -0
  101. dhisana-0.0.1.dev243.dist-info/entry_points.txt +2 -0
  102. dhisana-0.0.1.dev243.dist-info/top_level.txt +1 -0
@@ -0,0 +1,317 @@
1
+ from typing import Any, Dict, List, Optional
2
+ from pydantic import BaseModel
3
+ from dhisana.schemas.sales import (
4
+ ContentGenerationContext,
5
+ Lead,
6
+ MessageItem,
7
+ MessageResponse,
8
+ MessageGenerationInstructions,
9
+ SenderInfo
10
+ )
11
+ from dhisana.utils.generate_structured_output_internal import (
12
+ get_structured_output_internal,
13
+ get_structured_output_with_assistant_and_vector_store
14
+ )
15
+ from dhisana.utils.assistant_tool_tag import assistant_tool
16
+ import datetime
17
+
18
+ # ---------------------------------------------------------------------------------------
19
+ # MODEL
20
+ # ---------------------------------------------------------------------------------------
21
+ class LinkedInTriageResponse(BaseModel):
22
+ """
23
+ Model representing the structured response for a LinkedIn conversation triage.
24
+ - triage_status: "AUTOMATIC" or "REQUIRES_APPROVAL"
25
+ - triage_reason: Optional reason text if triage_status == "REQUIRES_APPROVAL"
26
+ - response_action_to_take: The recommended next action (e.g., SEND_REPLY, WAIT_TO_SEND, STOP_SENDING, etc.)
27
+ - response_message: The actual message (body) to be sent or used for approval.
28
+ """
29
+ triage_status: str # "AUTOMATIC" or "REQUIRES_APPROVAL"
30
+ triage_reason: Optional[str]
31
+ response_action_to_take: str
32
+ response_message: str
33
+ meeting_offer_sent: Optional[bool]
34
+
35
+
36
+ # ---------------------------------------------------------------------------------------
37
+ # HELPER FUNCTIONS
38
+ # ---------------------------------------------------------------------------------------
39
+ def cleanup_reply_linkedin_context(linkedin_context: ContentGenerationContext) -> ContentGenerationContext:
40
+ """
41
+ Create a copy of the context and remove unneeded or sensitive fields.
42
+ """
43
+ clone_context = linkedin_context.copy(deep=True)
44
+
45
+ # Example: removing tasks or statuses that are not needed for triage
46
+ clone_context.lead_info.task_ids = None
47
+ clone_context.lead_info.research_status = None
48
+ clone_context.lead_info.email_validation_status = None
49
+ clone_context.lead_info.linkedin_validation_status = None
50
+ clone_context.lead_info.enchrichment_status = None
51
+
52
+ return clone_context
53
+
54
+
55
+ async def generate_linkedin_response_message_copy(
56
+ linkedin_context: ContentGenerationContext,
57
+ variation: str,
58
+ tool_config: Optional[List[Dict]] = None
59
+ ) -> Dict[str, Any]:
60
+ """
61
+ Generates a single variation of a triaged LinkedIn response using the provided context.
62
+ Returns a structured result conforming to LinkedInTriageResponse.
63
+ """
64
+ allowed_actions = [
65
+ "SCHEDULE_MEETING",
66
+ "SEND_REPLY",
67
+ "UNSUBSCRIBE",
68
+ "OOF_MESSAGE",
69
+ "NOT_INTERESTED",
70
+ "NEED_MORE_INFO",
71
+ "FORWARD_TO_OTHER_USER",
72
+ "NO_MORE_IN_ORGANIZATION",
73
+ "OBJECTION_RAISED",
74
+ "END_CONVERSATION"
75
+ ]
76
+
77
+ cleaned_context = cleanup_reply_linkedin_context(linkedin_context)
78
+
79
+ # Safely handle the current_conversation_context if it exists.
80
+ if cleaned_context.current_conversation_context:
81
+ if not cleaned_context.current_conversation_context.current_email_thread:
82
+ cleaned_context.current_conversation_context.current_email_thread = []
83
+
84
+ if not cleaned_context.current_conversation_context.current_linkedin_thread:
85
+ cleaned_context.current_conversation_context.current_linkedin_thread = []
86
+
87
+ # Safely extract the conversation thread for prompt formatting.
88
+ conversation_thread_dump = [
89
+ thread_item.model_dump()
90
+ for thread_item in cleaned_context.current_conversation_context.current_linkedin_thread
91
+ ]
92
+ else:
93
+ # If current_conversation_context is None, use an empty thread.
94
+ conversation_thread_dump = []
95
+
96
+ current_date_iso = datetime.datetime.now().isoformat()
97
+ lead_data = linkedin_context.lead_info or Lead()
98
+ sender_data = linkedin_context.sender_info or SenderInfo()
99
+ campaign_context = linkedin_context.campaign_context or CampaignContext()
100
+
101
+ prompt = f"""
102
+ You are a specialized linkedin message reply assistant.
103
+ Your task is to analyze the user's linkedin message thread, the user/company info,
104
+ and the provided triage guidelines to craft an appropriate response.
105
+
106
+ Follow these instructions to generate the reply:
107
+ {variation}
108
+
109
+ 1. Message thread or conversation to respond to:
110
+ {conversation_thread_dump}
111
+
112
+ 2.
113
+ Lead Information:
114
+ {lead_data.dict()}
115
+
116
+ Sender Information:
117
+ Full Name: {sender_data.sender_full_name or ''}
118
+ First Name: {sender_data.sender_first_name or ''}
119
+ Last Name: {sender_data.sender_last_name or ''}
120
+ Bio: {sender_data.sender_bio or ''}
121
+
122
+ 3. Campaign-specific triage guidelines (user overrides always win):
123
+ {campaign_context.linkedin_triage_guidelines}
124
+
125
+ -----------------------------------------------------------------
126
+ Core decision logic
127
+ -----------------------------------------------------------------
128
+ • If the request is routine, non-sensitive, and clearly actionable
129
+ → **triage_status = "AUTOMATIC"**.
130
+ • If the thread contains PII, finance, legal, or any sensitive/NSFW content
131
+ → **triage_status = "END_CONVERSATION"** and give a concise **triage_reason**.
132
+
133
+ 4. Choose exactly ONE of: {allowed_actions}
134
+
135
+ -----------------------------------------------------------------
136
+ Response best practices
137
+ -----------------------------------------------------------------
138
+ • MAX 150 words, friendly & concise, single clear CTA.
139
+ • Begin with a thank-you, mirror the prospect’s wording briefly, then answer /
140
+ propose next step.
141
+ • Never contradict, trash-talk, or disparage {lead_data.organization_name}.
142
+ • Plain-text only – NO HTML tags (<a>, <b>, <i>, etc.).
143
+ • If a link already exists in the inbound message, include it verbatim—do not re-wrap or shorten.
144
+
145
+ Meeting & follow-up rules
146
+ -------------------------
147
+ 1. Let `meeting_offer_sent` = **true** if any earlier assistant message offered a
148
+ meeting.
149
+ 2. If First “Thanks / Sounds good” & *no* prior meeting offer
150
+ → **SEND_REPLY** asking for a 15-min call (≤150 words).
151
+ 3. If Second non-committal reply *after* meeting_offer_sent, or explicit “not interested”
152
+ → **END_CONVERSATION**.
153
+ 4. If prospect explicitly asks for times / requests your link
154
+ → **SCHEDULE_MEETING** and confirm or propose times.
155
+ 5. If One unsolicited follow-up maximum; stop unless prospect re-engages.
156
+
157
+ If you have not proposed a meeting even once in the thread, and the user response is polite acknowledgment then you MUST request for a meeting.
158
+
159
+
160
+ Objections & info requests
161
+ --------------------------
162
+ • Pricing / docs / case-studies request → **NEED_MORE_INFO**.
163
+ • Budget, timing, or competitor concerns → **OBJECTION_RAISED**
164
+ (acknowledge + one clarifying Q or concise value point).
165
+ • “Loop in {{colleague_name}}” → **FORWARD_TO_OTHER_USER**.
166
+
167
+ Unsubscribe & priority handling
168
+ -------------------------------
169
+ 1. “Unsubscribe / Remove me” → **UNSUBSCRIBE**
170
+ 2. Clear lack of interest → **NOT_INTERESTED**
171
+ 3. Auto OOO reply → **OOF_MESSAGE**
172
+ 4. Explicit meeting request → **SCHEDULE_MEETING**
173
+ 5. Otherwise follow the Meeting & follow-up rules above
174
+ 6. Default → **END_CONVERSATION**
175
+
176
+ Style guard-rails
177
+ -----------------
178
+ • Plain language; no jargon or filler.
179
+ • Do **not** repeat previous messages verbatim.
180
+ • Signature must include sender_first_name exactly as provided.
181
+ • Check UNSUBSCRIBE / NOT_INTERESTED first before other triage.
182
+
183
+ If you have not proposed a meeting even once in the thread, and the user response is polite acknowledgment then you MUST request for a meeting.
184
+
185
+ • Meeting ask template example:
186
+ Hi {{lead_first_name}}, would you be open to a quick 15-min call to
187
+ understand your use-case and share notes?
188
+
189
+ • Competitor-stack mention template example:
190
+ Hi {{lead_first_name}}, thanks for sharing your current stack. Would you be
191
+ open to a 15-min call to explore where we can add value?
192
+
193
+ Use conversational name for company name.
194
+ Use conversational name when using lead first name.
195
+ Do not use special characters or spaces when using lead’s first name.
196
+ In the subject or body DO NOT include any HTML tags like <a>, <b>, <i>, etc.
197
+ The body and subject should be in plain text.
198
+ If there is a link provided in the message, use it as is; do not wrap it in any HTML tags.
199
+ DO NOT make up information. Use only the information provided in the context and instructions.
200
+ Do NOT repeat the same message sent to the user in the past.
201
+ Keep the thread conversational and friendly as a good account executive would respond.
202
+ Do NOT rehash/repeat the same previous message already sent. Keep the reply to the point.
203
+ DO NOT try to spam users with multiple messages.
204
+ Current date is: {current_date_iso}.
205
+ DO NOT share any link to internal or made up document. You can attach or send any document.
206
+ If the user is asking for any additional document END_CONVERSATION and let Account executive handle it.
207
+ Make sure the body text is well-formatted and that newline and carriage-return characters are correctly present and preserved in the message body.
208
+ - Do Not use em dash in the generated output.
209
+
210
+ Required JSON output
211
+ --------------------
212
+ {{
213
+ "triage_status": "AUTOMATIC" or "END_CONVERSATION",
214
+ "triage_reason": "<reason if END_CONVERSATION; otherwise null>",
215
+ "response_action_to_take": "one of {allowed_actions}",
216
+ "response_message": "<the reply body if response_action_to_take is SEND_REPLY or SCHEDULE_MEETING; otherwise empty>"
217
+ }}
218
+ """
219
+
220
+ # Decide if we use a vector store
221
+ if (
222
+ cleaned_context.external_known_data
223
+ and cleaned_context.external_known_data.external_openai_vector_store_id
224
+ ):
225
+ initial_response, status = await get_structured_output_with_assistant_and_vector_store(
226
+ prompt=prompt,
227
+ response_format=LinkedInTriageResponse,
228
+ model="gpt-5.1-chat",
229
+ vector_store_id=cleaned_context.external_known_data.external_openai_vector_store_id,
230
+ tool_config=tool_config,
231
+ use_cache=linkedin_context.message_instructions.use_cache if linkedin_context.message_instructions else True
232
+ )
233
+ else:
234
+ initial_response, status = await get_structured_output_internal(
235
+ prompt,
236
+ LinkedInTriageResponse,
237
+ model="gpt-5.1-chat",
238
+ tool_config=tool_config,
239
+ use_cache=linkedin_context.message_instructions.use_cache if linkedin_context.message_instructions else True
240
+ )
241
+
242
+ if status != 'SUCCESS':
243
+ raise Exception("Error in generating the triaged LinkedIn message.")
244
+
245
+ response_item = MessageItem(
246
+ message_id="", # or generate one if appropriate
247
+ thread_id="",
248
+ sender_name=linkedin_context.sender_info.sender_full_name or "",
249
+ sender_email=linkedin_context.sender_info.sender_email or "",
250
+ receiver_name=linkedin_context.lead_info.full_name or "",
251
+ receiver_email=linkedin_context.lead_info.email or "",
252
+ iso_datetime=datetime.datetime.utcnow().isoformat(),
253
+ subject="", # or set a triage subject if needed
254
+ body=initial_response.response_message
255
+ )
256
+
257
+ # Build a MessageResponse that includes triage metadata plus your message item
258
+ response_message = MessageResponse(
259
+ triage_status=initial_response.triage_status,
260
+ triage_reason=initial_response.triage_reason,
261
+ message_item=response_item,
262
+ response_action_to_take=initial_response.response_action_to_take
263
+ )
264
+ return response_message.model_dump()
265
+
266
+
267
+ # ---------------------------------------------------------------------------------------
268
+ # MAIN ENTRY POINT
269
+ # ---------------------------------------------------------------------------------------
270
+ @assistant_tool
271
+ async def get_linkedin_response_message_variations(
272
+ linkedin_context: ContentGenerationContext,
273
+ number_of_variations: int = 3,
274
+ tool_config: Optional[List[Dict]] = None
275
+ ) -> List[Dict[str, Any]]:
276
+ """
277
+ Generates multiple variations of a triaged LinkedIn message and returns them all.
278
+ Each variation is a dict conforming to LinkedInTriageResponse with keys:
279
+ - triage_status
280
+ - triage_reason
281
+ - response_action_to_take
282
+ - response_message
283
+ """
284
+ variation_specs = [
285
+ "Friendly, short response with empathetic tone.",
286
+ "Direct response referencing user’s last message or question.",
287
+ "Meeting-oriented approach if the user seems interested in a deeper discussion.",
288
+ "Longer, more detailed approach – reference relevant success stories or context.",
289
+ "Minimalistic approach focusing on primary CTA only."
290
+ ]
291
+
292
+ # Check if the user provided custom instructions
293
+ message_instructions = linkedin_context.message_instructions or MessageGenerationInstructions()
294
+ user_instructions = (message_instructions.instructions_to_generate_message or "").strip()
295
+ user_instructions_exist = bool(user_instructions)
296
+
297
+ triaged_responses = []
298
+ for i in range(number_of_variations):
299
+ try:
300
+ # If user has instructions, use those for every variation
301
+ if user_instructions_exist:
302
+ variation_style = user_instructions
303
+ else:
304
+ # Otherwise, fallback to variation_specs
305
+ variation_style = variation_specs[i % len(variation_specs)]
306
+
307
+ triaged_response = await generate_linkedin_response_message_copy(
308
+ linkedin_context=linkedin_context,
309
+ variation=variation_style,
310
+ tool_config=tool_config
311
+ )
312
+ triaged_responses.append(triaged_response)
313
+ except Exception as e:
314
+ # You may want to log or handle the error
315
+ raise e
316
+
317
+ return triaged_responses