dhisana 0.0.1.dev305__tar.gz → 0.0.1.dev307__tar.gz
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-0.0.1.dev305 → dhisana-0.0.1.dev307}/PKG-INFO +1 -1
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/setup.py +1 -1
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/schemas/sales.py +2 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/generate_content.py +3 -0
- dhisana-0.0.1.dev307/src/dhisana/utils/generate_sms_whatsapp.py +260 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana.egg-info/PKG-INFO +1 -1
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana.egg-info/SOURCES.txt +2 -0
- dhisana-0.0.1.dev307/tests/test_generate_sms_whatsapp.py +260 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/README.md +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/pyproject.toml +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/setup.cfg +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/__init__.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/cli/__init__.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/cli/cli.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/cli/datasets.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/cli/models.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/cli/predictions.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/schemas/__init__.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/schemas/common.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/ui/__init__.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/ui/components.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/__init__.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/add_mapping.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/agent_tools.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/apollo_tools.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/assistant_tool_tag.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/built_with_api_tools.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/cache_output_tools.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/cache_output_tools_local.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/check_email_validity_tools.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/check_for_intent_signal.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/check_linkedin_url_validity.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/clay_tools.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/clean_properties.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/company_utils.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/compose_salesnav_query.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/compose_search_query.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/compose_three_step_workflow.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/composite_tools.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/dataframe_tools.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/domain_parser.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/email_body_utils.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/email_parse_helpers.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/email_provider.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/enrich_lead_information.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/extract_email_content_for_llm.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/fetch_openai_config.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/field_validators.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/g2_tools.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/generate_custom_message.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/generate_email.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/generate_email_response.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/generate_flow.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/generate_leads_salesnav.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/generate_linkedin_connect_message.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/generate_linkedin_response_message.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/generate_structured_output_internal.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/google_custom_search.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/google_oauth_tools.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/google_workspace_tools.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/hubspot_clearbit.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/hubspot_crm_tools.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/instantly_tools.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/linkedin_crawler.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/lusha_tools.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/mailgun_tools.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/mailreach_tools.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/microsoft365_tools.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/openai_assistant_and_file_utils.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/openai_helpers.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/openapi_spec_to_tools.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/openapi_tool/__init__.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/openapi_tool/api_models.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/openapi_tool/openapi_tool.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/parse_linkedin_messages_txt.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/profile.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/proxy_curl_tools.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/proxycurl_search_leads.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/python_function_to_tools.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/research_lead.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/sales_navigator_crawler.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/salesforce_crm_tools.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/search_router.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/search_router_jobs.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/sendgrid_tools.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/serarch_router_local_business.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/serpapi_additional_tools.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/serpapi_google_jobs.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/serpapi_google_search.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/serpapi_local_business_search.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/serpapi_search_tools.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/serperdev_google_jobs.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/serperdev_local_business.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/serperdev_search.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/smtp_email_tools.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/test_connect.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/trasform_json.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/web_download_parse_tools.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/workflow_code_model.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/zoominfo_tools.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/workflow/__init__.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/workflow/agent.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/workflow/flow.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/workflow/task.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/workflow/test.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana.egg-info/dependency_links.txt +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana.egg-info/entry_points.txt +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana.egg-info/requires.txt +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana.egg-info/top_level.txt +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/tests/test_agent_tools.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/tests/test_apollo_company_search.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/tests/test_apollo_lead_search.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/tests/test_connectivity.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/tests/test_email_body_utils.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/tests/test_generate_email.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/tests/test_google_document.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/tests/test_hubspot_call_logs.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/tests/test_linkedin_serper.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/tests/test_mailreach.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/tests/test_mcp_connectivity.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/tests/test_normalize_graph_datetime.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/tests/test_proxycurl_get_company_search_id.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/tests/test_proxycurl_job_count.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/tests/test_reply_thread_fallback.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/tests/test_send_email_recipients.py +0 -0
- {dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/tests/test_structured_output_with_mcp.py +0 -0
|
@@ -6,6 +6,7 @@ from dhisana.utils.assistant_tool_tag import assistant_tool
|
|
|
6
6
|
from dhisana.utils.generate_email import generate_personalized_email
|
|
7
7
|
from dhisana.utils.generate_linkedin_response_message import get_linkedin_response_message_variations
|
|
8
8
|
from dhisana.utils.generate_custom_message import generate_custom_message
|
|
9
|
+
from dhisana.utils.generate_sms_whatsapp import generate_sms_whatsapp_message
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
@assistant_tool
|
|
@@ -36,6 +37,8 @@ async def generate_content(
|
|
|
36
37
|
return await generate_inbound_email_response_variations(generation_context, number_of_variations, tool_config)
|
|
37
38
|
elif generation_context.target_channel_type == ChannelType.LINKEDIN_USER_MESSAGE.value:
|
|
38
39
|
return await get_linkedin_response_message_variations(generation_context, number_of_variations, tool_config)
|
|
40
|
+
elif generation_context.target_channel_type in (ChannelType.SMS.value, ChannelType.WHATSAPP.value):
|
|
41
|
+
return await generate_sms_whatsapp_message(generation_context, number_of_variations, tool_config)
|
|
39
42
|
else:
|
|
40
43
|
# Default to CUSTOM_MESSAGE for any unrecognized channel type
|
|
41
44
|
return await generate_custom_message(generation_context, number_of_variations, tool_config)
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from typing import Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict
|
|
6
|
+
|
|
7
|
+
from dhisana.schemas.sales import (
|
|
8
|
+
CampaignContext,
|
|
9
|
+
ChannelType,
|
|
10
|
+
ContentGenerationContext,
|
|
11
|
+
ConversationContext,
|
|
12
|
+
Lead,
|
|
13
|
+
MessageGenerationInstructions,
|
|
14
|
+
MessageItem,
|
|
15
|
+
SenderInfo,
|
|
16
|
+
)
|
|
17
|
+
from dhisana.utils.generate_email import _sanitize_lead_for_prompt
|
|
18
|
+
from dhisana.utils.generate_structured_output_internal import (
|
|
19
|
+
get_structured_output_internal,
|
|
20
|
+
get_structured_output_with_assistant_and_vector_store,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
DEFAULT_SMS_GEN_MODEL = "gpt-5.4"
|
|
24
|
+
|
|
25
|
+
SMS_CHAR_LIMIT = 160
|
|
26
|
+
WHATSAPP_CHAR_LIMIT = 1000
|
|
27
|
+
|
|
28
|
+
# Standard opt-out suffix appended to every generated message.
|
|
29
|
+
OPT_OUT_SUFFIX_SMS = "\nReply STOP to opt out."
|
|
30
|
+
OPT_OUT_SUFFIX_WHATSAPP = "\nReply STOP to opt out."
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SMSWhatsAppCopy(BaseModel):
|
|
34
|
+
body: str
|
|
35
|
+
|
|
36
|
+
model_config = ConfigDict(extra="forbid")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _effective_char_limit(channel_type: ChannelType) -> int:
|
|
40
|
+
if channel_type == ChannelType.WHATSAPP:
|
|
41
|
+
return WHATSAPP_CHAR_LIMIT
|
|
42
|
+
return SMS_CHAR_LIMIT
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _opt_out_suffix(channel_type: ChannelType) -> str:
|
|
46
|
+
if channel_type == ChannelType.WHATSAPP:
|
|
47
|
+
return OPT_OUT_SUFFIX_WHATSAPP
|
|
48
|
+
return OPT_OUT_SUFFIX_SMS
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# Matches explicit opt-out instructions like "Reply STOP", "Text STOP to …",
|
|
52
|
+
# "opt out", or "unsubscribe" — requires an action verb before "stop" so
|
|
53
|
+
# normal prose like "stop wasting time" does not suppress the footer.
|
|
54
|
+
_OPT_OUT_RE = re.compile(
|
|
55
|
+
r"(?:reply|text|send|message|say)\s+stop\b"
|
|
56
|
+
r"|\bopt[\s\-]?out\b"
|
|
57
|
+
r"|\bunsubscribe\b",
|
|
58
|
+
re.IGNORECASE,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _ensure_opt_out(body: str, channel_type: ChannelType) -> str:
|
|
63
|
+
suffix = _opt_out_suffix(channel_type)
|
|
64
|
+
if _OPT_OUT_RE.search(body):
|
|
65
|
+
return body
|
|
66
|
+
return body.rstrip() + suffix
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _cleanup_context(
|
|
70
|
+
ctx: ContentGenerationContext,
|
|
71
|
+
) -> ContentGenerationContext:
|
|
72
|
+
clone = ctx.model_copy(deep=True)
|
|
73
|
+
if clone.external_known_data:
|
|
74
|
+
clone.external_known_data.external_openai_vector_store_id = None
|
|
75
|
+
return clone
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async def generate_sms_whatsapp_copy(
|
|
79
|
+
message_context: ContentGenerationContext,
|
|
80
|
+
message_instructions: MessageGenerationInstructions,
|
|
81
|
+
variation_text: str,
|
|
82
|
+
channel_type: ChannelType,
|
|
83
|
+
tool_config: Optional[List[Dict]] = None,
|
|
84
|
+
) -> dict:
|
|
85
|
+
cleaned = _cleanup_context(message_context)
|
|
86
|
+
|
|
87
|
+
user_instructions = (
|
|
88
|
+
message_instructions.instructions_to_generate_message or ""
|
|
89
|
+
).strip()
|
|
90
|
+
selected_instructions = user_instructions if user_instructions else variation_text
|
|
91
|
+
|
|
92
|
+
lead_data = cleaned.lead_info or Lead()
|
|
93
|
+
sender_data = cleaned.sender_info or SenderInfo()
|
|
94
|
+
campaign_data = cleaned.campaign_context or CampaignContext()
|
|
95
|
+
conversation_data = (
|
|
96
|
+
cleaned.current_conversation_context or ConversationContext()
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
char_limit = _effective_char_limit(channel_type)
|
|
100
|
+
opt_out_suffix = _opt_out_suffix(channel_type)
|
|
101
|
+
usable_chars = char_limit - len(opt_out_suffix)
|
|
102
|
+
channel_label = "WhatsApp" if channel_type == ChannelType.WHATSAPP else "SMS"
|
|
103
|
+
|
|
104
|
+
prompt = f"""You are an AI sales copywriter. Your task is to write exactly one personalized outbound business {channel_label} text message on behalf of the sender to the lead described below.
|
|
105
|
+
|
|
106
|
+
This is a legitimate B2B sales outreach message. The sender has opted in to sending these messages through our platform.
|
|
107
|
+
|
|
108
|
+
## Channel Rules — {channel_label}
|
|
109
|
+
|
|
110
|
+
1. The message body (excluding the compliance footer) MUST be {usable_chars} characters or fewer.
|
|
111
|
+
2. Plain text only — no HTML, no markdown, no emojis unless the instructions ask for them.
|
|
112
|
+
3. No subject line — output only a "body" field.
|
|
113
|
+
4. Be concise and conversational. Lead with value, not a greeting. Avoid filler.
|
|
114
|
+
5. Do NOT include any opt-out / STOP language — the system appends it automatically.
|
|
115
|
+
6. Do NOT include phone numbers, email addresses, or links unless the writing instructions explicitly request a specific provided URL.
|
|
116
|
+
7. Ground every claim in the provided context. When information is missing, omit it — never guess or fabricate.
|
|
117
|
+
8. Do not use em dashes.
|
|
118
|
+
9. The "body" field must contain ONLY the message text. Never append JSON, metadata, or structured data to the body.
|
|
119
|
+
|
|
120
|
+
## Lead
|
|
121
|
+
|
|
122
|
+
Name: {lead_data.first_name or ''} {lead_data.last_name or ''}
|
|
123
|
+
Organization: {lead_data.organization_name or ''}
|
|
124
|
+
Title: {lead_data.job_title or ''}
|
|
125
|
+
|
|
126
|
+
## Sender (closed set — use ONLY these facts)
|
|
127
|
+
|
|
128
|
+
- Full Name: {sender_data.sender_full_name or ''}
|
|
129
|
+
- First Name: {sender_data.sender_first_name or ''}
|
|
130
|
+
- Last Name: {sender_data.sender_last_name or ''}
|
|
131
|
+
- Bio: {sender_data.sender_bio or ''}
|
|
132
|
+
|
|
133
|
+
## Campaign
|
|
134
|
+
|
|
135
|
+
- Product: {campaign_data.product_name or ''}
|
|
136
|
+
- Value Proposition: {campaign_data.value_prop or ''}
|
|
137
|
+
- Call to Action: {campaign_data.call_to_action or ''}
|
|
138
|
+
- Pain Points: {campaign_data.pain_points or []}
|
|
139
|
+
- Proof Points: {campaign_data.proof_points or []}
|
|
140
|
+
|
|
141
|
+
## Writing Instructions
|
|
142
|
+
|
|
143
|
+
{selected_instructions}
|
|
144
|
+
|
|
145
|
+
## Conversation History
|
|
146
|
+
|
|
147
|
+
{conversation_data.current_email_thread or ''}
|
|
148
|
+
{conversation_data.current_linkedin_thread or ''}
|
|
149
|
+
|
|
150
|
+
## Output Format
|
|
151
|
+
|
|
152
|
+
Return JSON with a single field: "body" (string, plain text, max {usable_chars} chars).
|
|
153
|
+
|
|
154
|
+
## Additional Lead Context (reference only)
|
|
155
|
+
|
|
156
|
+
{_sanitize_lead_for_prompt(lead_data)}
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
vector_store_id = (
|
|
160
|
+
message_context.external_known_data.external_openai_vector_store_id
|
|
161
|
+
if message_context.external_known_data
|
|
162
|
+
else None
|
|
163
|
+
)
|
|
164
|
+
use_cache = (
|
|
165
|
+
message_context.message_instructions.use_cache
|
|
166
|
+
if message_context.message_instructions
|
|
167
|
+
else True
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if vector_store_id:
|
|
171
|
+
response, status = (
|
|
172
|
+
await get_structured_output_with_assistant_and_vector_store(
|
|
173
|
+
prompt=prompt,
|
|
174
|
+
response_format=SMSWhatsAppCopy,
|
|
175
|
+
vector_store_id=vector_store_id,
|
|
176
|
+
model=DEFAULT_SMS_GEN_MODEL,
|
|
177
|
+
effort="medium",
|
|
178
|
+
tool_config=tool_config,
|
|
179
|
+
use_cache=use_cache,
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
else:
|
|
183
|
+
response, status = await get_structured_output_internal(
|
|
184
|
+
prompt=prompt,
|
|
185
|
+
response_format=SMSWhatsAppCopy,
|
|
186
|
+
model=DEFAULT_SMS_GEN_MODEL,
|
|
187
|
+
effort="medium",
|
|
188
|
+
tool_config=tool_config,
|
|
189
|
+
use_cache=use_cache,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if status != "SUCCESS":
|
|
193
|
+
# Include the refusal/error detail if the model returned text
|
|
194
|
+
detail = ""
|
|
195
|
+
if isinstance(response, str) and response:
|
|
196
|
+
detail = f" Model response: {response[:200]}"
|
|
197
|
+
raise Exception(
|
|
198
|
+
f"Error: Could not generate {channel_label} message.{detail}"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
body = (response.body or "").strip()
|
|
202
|
+
# Truncate if model exceeded the limit
|
|
203
|
+
if len(body) > usable_chars:
|
|
204
|
+
body = body[:usable_chars].rsplit(" ", 1)[0]
|
|
205
|
+
body = _ensure_opt_out(body, channel_type)
|
|
206
|
+
|
|
207
|
+
response_item = MessageItem(
|
|
208
|
+
message_id="",
|
|
209
|
+
thread_id="",
|
|
210
|
+
sender_name=sender_data.sender_full_name or "",
|
|
211
|
+
sender_email=sender_data.sender_email or "",
|
|
212
|
+
receiver_name=(message_context.lead_info.full_name or "") if message_context.lead_info else "",
|
|
213
|
+
receiver_email=(message_context.lead_info.email or "") if message_context.lead_info else "",
|
|
214
|
+
iso_datetime=datetime.now(timezone.utc).isoformat(),
|
|
215
|
+
subject="",
|
|
216
|
+
body=body,
|
|
217
|
+
html_body=None,
|
|
218
|
+
)
|
|
219
|
+
return response_item.model_dump()
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
FRAMEWORK_VARIATIONS_SMS = [
|
|
223
|
+
"Write a brief, personalized outreach message. Lead with a relevant insight about the prospect's company.",
|
|
224
|
+
"Write a short value-first message — mention one pain point and how the product helps.",
|
|
225
|
+
"Write a concise, curiosity-driven message with a clear call to action.",
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
async def generate_sms_whatsapp_message(
|
|
230
|
+
generation_context: ContentGenerationContext,
|
|
231
|
+
number_of_variations: int = 3,
|
|
232
|
+
tool_config: Optional[List[Dict]] = None,
|
|
233
|
+
) -> List[dict]:
|
|
234
|
+
channel_type = generation_context.target_channel_type or ChannelType.SMS
|
|
235
|
+
message_instructions = generation_context.message_instructions
|
|
236
|
+
user_instructions_exist = bool(
|
|
237
|
+
(message_instructions.instructions_to_generate_message or "").strip()
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
variations: List[dict] = []
|
|
241
|
+
for i in range(number_of_variations):
|
|
242
|
+
if user_instructions_exist:
|
|
243
|
+
variation_text = (
|
|
244
|
+
message_instructions.instructions_to_generate_message or ""
|
|
245
|
+
)
|
|
246
|
+
else:
|
|
247
|
+
variation_text = FRAMEWORK_VARIATIONS_SMS[
|
|
248
|
+
i % len(FRAMEWORK_VARIATIONS_SMS)
|
|
249
|
+
]
|
|
250
|
+
|
|
251
|
+
copy = await generate_sms_whatsapp_copy(
|
|
252
|
+
message_context=generation_context,
|
|
253
|
+
message_instructions=message_instructions,
|
|
254
|
+
variation_text=variation_text,
|
|
255
|
+
channel_type=channel_type,
|
|
256
|
+
tool_config=tool_config,
|
|
257
|
+
)
|
|
258
|
+
variations.append(copy)
|
|
259
|
+
|
|
260
|
+
return variations
|
|
@@ -54,6 +54,7 @@ src/dhisana/utils/generate_flow.py
|
|
|
54
54
|
src/dhisana/utils/generate_leads_salesnav.py
|
|
55
55
|
src/dhisana/utils/generate_linkedin_connect_message.py
|
|
56
56
|
src/dhisana/utils/generate_linkedin_response_message.py
|
|
57
|
+
src/dhisana/utils/generate_sms_whatsapp.py
|
|
57
58
|
src/dhisana/utils/generate_structured_output_internal.py
|
|
58
59
|
src/dhisana/utils/google_custom_search.py
|
|
59
60
|
src/dhisana/utils/google_oauth_tools.py
|
|
@@ -110,6 +111,7 @@ tests/test_apollo_lead_search.py
|
|
|
110
111
|
tests/test_connectivity.py
|
|
111
112
|
tests/test_email_body_utils.py
|
|
112
113
|
tests/test_generate_email.py
|
|
114
|
+
tests/test_generate_sms_whatsapp.py
|
|
113
115
|
tests/test_google_document.py
|
|
114
116
|
tests/test_hubspot_call_logs.py
|
|
115
117
|
tests/test_linkedin_serper.py
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import unittest
|
|
3
|
+
from unittest.mock import AsyncMock, patch
|
|
4
|
+
|
|
5
|
+
from dhisana.schemas.sales import (
|
|
6
|
+
CampaignContext,
|
|
7
|
+
ChannelType,
|
|
8
|
+
ContentGenerationContext,
|
|
9
|
+
ConversationContext,
|
|
10
|
+
Lead,
|
|
11
|
+
MessageGenerationInstructions,
|
|
12
|
+
SenderInfo,
|
|
13
|
+
)
|
|
14
|
+
from dhisana.utils.generate_sms_whatsapp import (
|
|
15
|
+
OPT_OUT_SUFFIX_SMS,
|
|
16
|
+
OPT_OUT_SUFFIX_WHATSAPP,
|
|
17
|
+
SMS_CHAR_LIMIT,
|
|
18
|
+
SMSWhatsAppCopy,
|
|
19
|
+
WHATSAPP_CHAR_LIMIT,
|
|
20
|
+
_effective_char_limit,
|
|
21
|
+
_ensure_opt_out,
|
|
22
|
+
generate_sms_whatsapp_copy,
|
|
23
|
+
generate_sms_whatsapp_message,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _build_context(channel: ChannelType = ChannelType.SMS) -> ContentGenerationContext:
|
|
28
|
+
lead = Lead(
|
|
29
|
+
full_name="Jane Smith",
|
|
30
|
+
first_name="Jane",
|
|
31
|
+
last_name="Smith",
|
|
32
|
+
email="jane@acmecorp.com",
|
|
33
|
+
job_title="VP of Engineering",
|
|
34
|
+
organization_name="Acme Corp",
|
|
35
|
+
phone="404-555-1234",
|
|
36
|
+
)
|
|
37
|
+
sender = SenderInfo(
|
|
38
|
+
sender_full_name="John Doe",
|
|
39
|
+
sender_first_name="John",
|
|
40
|
+
sender_last_name="Doe",
|
|
41
|
+
sender_email="john@dhisana.ai",
|
|
42
|
+
sender_bio="Co-founder at Dhisana",
|
|
43
|
+
)
|
|
44
|
+
campaign = CampaignContext(
|
|
45
|
+
product_name="Dhisana AI",
|
|
46
|
+
value_prop="AI-powered sales outreach",
|
|
47
|
+
call_to_action="Book a demo",
|
|
48
|
+
)
|
|
49
|
+
instructions = MessageGenerationInstructions(
|
|
50
|
+
instructions_to_generate_message="Write a brief intro message.",
|
|
51
|
+
use_cache=False,
|
|
52
|
+
allow_html=False,
|
|
53
|
+
)
|
|
54
|
+
return ContentGenerationContext(
|
|
55
|
+
lead_info=lead,
|
|
56
|
+
sender_info=sender,
|
|
57
|
+
campaign_context=campaign,
|
|
58
|
+
current_conversation_context=ConversationContext(),
|
|
59
|
+
target_channel_type=channel,
|
|
60
|
+
message_instructions=instructions,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class TestCharLimits(unittest.TestCase):
|
|
65
|
+
def test_sms_limit(self):
|
|
66
|
+
self.assertEqual(_effective_char_limit(ChannelType.SMS), SMS_CHAR_LIMIT)
|
|
67
|
+
|
|
68
|
+
def test_whatsapp_limit(self):
|
|
69
|
+
self.assertEqual(_effective_char_limit(ChannelType.WHATSAPP), WHATSAPP_CHAR_LIMIT)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class TestOptOut(unittest.TestCase):
|
|
73
|
+
def test_appends_opt_out_to_sms(self):
|
|
74
|
+
result = _ensure_opt_out("Hi there!", ChannelType.SMS)
|
|
75
|
+
self.assertTrue(result.endswith(OPT_OUT_SUFFIX_SMS))
|
|
76
|
+
|
|
77
|
+
def test_appends_opt_out_to_whatsapp(self):
|
|
78
|
+
result = _ensure_opt_out("Hi there!", ChannelType.WHATSAPP)
|
|
79
|
+
self.assertTrue(result.endswith(OPT_OUT_SUFFIX_WHATSAPP))
|
|
80
|
+
|
|
81
|
+
def test_skips_when_reply_stop_present(self):
|
|
82
|
+
msg = "Reply STOP to unsubscribe."
|
|
83
|
+
result = _ensure_opt_out(msg, ChannelType.SMS)
|
|
84
|
+
self.assertEqual(result, msg)
|
|
85
|
+
|
|
86
|
+
def test_skips_when_text_stop_present(self):
|
|
87
|
+
msg = "Text STOP to cancel."
|
|
88
|
+
result = _ensure_opt_out(msg, ChannelType.SMS)
|
|
89
|
+
self.assertEqual(result, msg)
|
|
90
|
+
|
|
91
|
+
def test_skips_when_opt_out_present(self):
|
|
92
|
+
msg = "You may opt out at any time."
|
|
93
|
+
result = _ensure_opt_out(msg, ChannelType.SMS)
|
|
94
|
+
self.assertEqual(result, msg)
|
|
95
|
+
|
|
96
|
+
def test_skips_when_opt_hyphen_out_present(self):
|
|
97
|
+
msg = "Text opt-out to stop receiving messages."
|
|
98
|
+
result = _ensure_opt_out(msg, ChannelType.SMS)
|
|
99
|
+
self.assertEqual(result, msg)
|
|
100
|
+
|
|
101
|
+
def test_skips_when_unsubscribe_present(self):
|
|
102
|
+
msg = "Reply unsubscribe to stop."
|
|
103
|
+
result = _ensure_opt_out(msg, ChannelType.WHATSAPP)
|
|
104
|
+
self.assertEqual(result, msg)
|
|
105
|
+
|
|
106
|
+
def test_does_not_skip_for_stop_in_normal_prose(self):
|
|
107
|
+
msg = "Stop wasting hours on manual outreach."
|
|
108
|
+
result = _ensure_opt_out(msg, ChannelType.SMS)
|
|
109
|
+
self.assertTrue(result.endswith(OPT_OUT_SUFFIX_SMS))
|
|
110
|
+
|
|
111
|
+
def test_does_not_skip_for_stop_mid_sentence(self):
|
|
112
|
+
msg = "Let us help you stop losing deals."
|
|
113
|
+
result = _ensure_opt_out(msg, ChannelType.WHATSAPP)
|
|
114
|
+
self.assertTrue(result.endswith(OPT_OUT_SUFFIX_WHATSAPP))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class TestGenerateSMSWhatsAppCopy(unittest.TestCase):
|
|
118
|
+
def test_sms_generation_single_copy(self):
|
|
119
|
+
ctx = _build_context(ChannelType.SMS)
|
|
120
|
+
mock_response = SMSWhatsAppCopy(body="Hey Jane, quick question about Acme's outreach.")
|
|
121
|
+
|
|
122
|
+
with patch(
|
|
123
|
+
"dhisana.utils.generate_sms_whatsapp.get_structured_output_internal",
|
|
124
|
+
new_callable=AsyncMock,
|
|
125
|
+
return_value=(mock_response, "SUCCESS"),
|
|
126
|
+
):
|
|
127
|
+
result = asyncio.get_event_loop().run_until_complete(
|
|
128
|
+
generate_sms_whatsapp_copy(
|
|
129
|
+
message_context=ctx,
|
|
130
|
+
message_instructions=ctx.message_instructions,
|
|
131
|
+
variation_text="Write a brief message.",
|
|
132
|
+
channel_type=ChannelType.SMS,
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
self.assertIn("body", result)
|
|
136
|
+
self.assertEqual(result["subject"], "")
|
|
137
|
+
self.assertIn("STOP", result["body"])
|
|
138
|
+
|
|
139
|
+
def test_whatsapp_generation_single_copy(self):
|
|
140
|
+
ctx = _build_context(ChannelType.WHATSAPP)
|
|
141
|
+
mock_response = SMSWhatsAppCopy(body="Hi Jane, Dhisana AI can help Acme automate outreach.")
|
|
142
|
+
|
|
143
|
+
with patch(
|
|
144
|
+
"dhisana.utils.generate_sms_whatsapp.get_structured_output_internal",
|
|
145
|
+
new_callable=AsyncMock,
|
|
146
|
+
return_value=(mock_response, "SUCCESS"),
|
|
147
|
+
):
|
|
148
|
+
result = asyncio.get_event_loop().run_until_complete(
|
|
149
|
+
generate_sms_whatsapp_copy(
|
|
150
|
+
message_context=ctx,
|
|
151
|
+
message_instructions=ctx.message_instructions,
|
|
152
|
+
variation_text="Write a brief message.",
|
|
153
|
+
channel_type=ChannelType.WHATSAPP,
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
self.assertIn("body", result)
|
|
157
|
+
self.assertIn("STOP", result["body"])
|
|
158
|
+
|
|
159
|
+
def test_truncation_when_body_exceeds_limit(self):
|
|
160
|
+
ctx = _build_context(ChannelType.SMS)
|
|
161
|
+
long_body = "A" * 200
|
|
162
|
+
mock_response = SMSWhatsAppCopy(body=long_body)
|
|
163
|
+
|
|
164
|
+
with patch(
|
|
165
|
+
"dhisana.utils.generate_sms_whatsapp.get_structured_output_internal",
|
|
166
|
+
new_callable=AsyncMock,
|
|
167
|
+
return_value=(mock_response, "SUCCESS"),
|
|
168
|
+
):
|
|
169
|
+
result = asyncio.get_event_loop().run_until_complete(
|
|
170
|
+
generate_sms_whatsapp_copy(
|
|
171
|
+
message_context=ctx,
|
|
172
|
+
message_instructions=ctx.message_instructions,
|
|
173
|
+
variation_text="Write a brief message.",
|
|
174
|
+
channel_type=ChannelType.SMS,
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
self.assertLessEqual(len(result["body"]), SMS_CHAR_LIMIT)
|
|
178
|
+
|
|
179
|
+
def test_raises_on_llm_failure(self):
|
|
180
|
+
ctx = _build_context(ChannelType.SMS)
|
|
181
|
+
|
|
182
|
+
with patch(
|
|
183
|
+
"dhisana.utils.generate_sms_whatsapp.get_structured_output_internal",
|
|
184
|
+
new_callable=AsyncMock,
|
|
185
|
+
return_value=(None, "ERROR"),
|
|
186
|
+
):
|
|
187
|
+
with self.assertRaises(Exception) as exc_ctx:
|
|
188
|
+
asyncio.get_event_loop().run_until_complete(
|
|
189
|
+
generate_sms_whatsapp_copy(
|
|
190
|
+
message_context=ctx,
|
|
191
|
+
message_instructions=ctx.message_instructions,
|
|
192
|
+
variation_text="Write a brief message.",
|
|
193
|
+
channel_type=ChannelType.SMS,
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
self.assertIn("SMS", str(exc_ctx.exception))
|
|
197
|
+
|
|
198
|
+
def test_no_html_in_body(self):
|
|
199
|
+
ctx = _build_context(ChannelType.SMS)
|
|
200
|
+
mock_response = SMSWhatsAppCopy(body="Hi Jane")
|
|
201
|
+
|
|
202
|
+
with patch(
|
|
203
|
+
"dhisana.utils.generate_sms_whatsapp.get_structured_output_internal",
|
|
204
|
+
new_callable=AsyncMock,
|
|
205
|
+
return_value=(mock_response, "SUCCESS"),
|
|
206
|
+
):
|
|
207
|
+
result = asyncio.get_event_loop().run_until_complete(
|
|
208
|
+
generate_sms_whatsapp_copy(
|
|
209
|
+
message_context=ctx,
|
|
210
|
+
message_instructions=ctx.message_instructions,
|
|
211
|
+
variation_text="Test",
|
|
212
|
+
channel_type=ChannelType.SMS,
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
self.assertIsNone(result.get("html_body"))
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class TestGenerateSMSWhatsAppMessage(unittest.TestCase):
|
|
219
|
+
def test_generates_requested_number_of_variations(self):
|
|
220
|
+
ctx = _build_context(ChannelType.SMS)
|
|
221
|
+
mock_response = SMSWhatsAppCopy(body="Hey Jane, quick q about Acme.")
|
|
222
|
+
|
|
223
|
+
with patch(
|
|
224
|
+
"dhisana.utils.generate_sms_whatsapp.get_structured_output_internal",
|
|
225
|
+
new_callable=AsyncMock,
|
|
226
|
+
return_value=(mock_response, "SUCCESS"),
|
|
227
|
+
):
|
|
228
|
+
results = asyncio.get_event_loop().run_until_complete(
|
|
229
|
+
generate_sms_whatsapp_message(ctx, number_of_variations=2)
|
|
230
|
+
)
|
|
231
|
+
self.assertEqual(len(results), 2)
|
|
232
|
+
for r in results:
|
|
233
|
+
self.assertIn("body", r)
|
|
234
|
+
|
|
235
|
+
def test_uses_framework_variations_when_no_user_instructions(self):
|
|
236
|
+
ctx = _build_context(ChannelType.WHATSAPP)
|
|
237
|
+
ctx.message_instructions.instructions_to_generate_message = None
|
|
238
|
+
mock_response = SMSWhatsAppCopy(body="Hi Jane, value-first msg.")
|
|
239
|
+
|
|
240
|
+
with patch(
|
|
241
|
+
"dhisana.utils.generate_sms_whatsapp.get_structured_output_internal",
|
|
242
|
+
new_callable=AsyncMock,
|
|
243
|
+
return_value=(mock_response, "SUCCESS"),
|
|
244
|
+
) as mock_llm:
|
|
245
|
+
asyncio.get_event_loop().run_until_complete(
|
|
246
|
+
generate_sms_whatsapp_message(ctx, number_of_variations=3)
|
|
247
|
+
)
|
|
248
|
+
self.assertEqual(mock_llm.call_count, 3)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class TestChannelTypeEnum(unittest.TestCase):
|
|
252
|
+
def test_sms_in_channel_type(self):
|
|
253
|
+
self.assertEqual(ChannelType.SMS.value, "sms")
|
|
254
|
+
|
|
255
|
+
def test_whatsapp_in_channel_type(self):
|
|
256
|
+
self.assertEqual(ChannelType.WHATSAPP.value, "whatsapp")
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
if __name__ == "__main__":
|
|
260
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/check_email_validity_tools.py
RENAMED
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/check_linkedin_url_validity.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/compose_three_step_workflow.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/extract_email_content_for_llm.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/generate_linkedin_connect_message.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/openai_assistant_and_file_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/openapi_tool/openapi_tool.py
RENAMED
|
File without changes
|
{dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/parse_linkedin_messages_txt.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/serarch_router_local_business.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev305 → dhisana-0.0.1.dev307}/src/dhisana/utils/serpapi_local_business_search.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|