dhisana 0.0.1.dev227__tar.gz → 0.0.1.dev229__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.dev227 → dhisana-0.0.1.dev229}/PKG-INFO +1 -1
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/setup.py +1 -1
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/schemas/common.py +9 -1
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/compose_search_query.py +2 -2
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/dataframe_tools.py +2 -2
- dhisana-0.0.1.dev229/src/dhisana/utils/email_body_utils.py +72 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/enrich_lead_information.py +3 -3
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/generate_email.py +2 -2
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/generate_email_response.py +4 -4
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/generate_linkedin_connect_message.py +2 -2
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/generate_linkedin_response_message.py +2 -2
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/generate_structured_output_internal.py +3 -3
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/google_oauth_tools.py +25 -2
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/google_workspace_tools.py +33 -9
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/mailgun_tools.py +14 -2
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/microsoft365_tools.py +10 -2
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/openai_assistant_and_file_utils.py +3 -3
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/openai_helpers.py +2 -2
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/research_lead.py +2 -2
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/sendgrid_tools.py +14 -3
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/serpapi_search_tools.py +2 -2
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/smtp_email_tools.py +10 -6
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/test_connect.py +1 -1
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/trasform_json.py +1 -1
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/workflow/test.py +1 -1
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana.egg-info/PKG-INFO +1 -1
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana.egg-info/SOURCES.txt +2 -0
- dhisana-0.0.1.dev229/tests/test_email_body_utils.py +23 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/README.md +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/pyproject.toml +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/setup.cfg +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/__init__.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/cli/__init__.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/cli/cli.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/cli/datasets.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/cli/models.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/cli/predictions.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/schemas/__init__.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/schemas/sales.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/ui/__init__.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/ui/components.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/__init__.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/add_mapping.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/agent_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/apollo_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/assistant_tool_tag.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/built_with_api_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/cache_output_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/cache_output_tools_local.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/check_email_validity_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/check_for_intent_signal.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/check_linkedin_url_validity.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/clay_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/clean_properties.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/company_utils.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/compose_salesnav_query.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/compose_three_step_workflow.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/composite_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/domain_parser.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/email_parse_helpers.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/email_provider.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/extract_email_content_for_llm.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/fetch_openai_config.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/field_validators.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/g2_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/generate_content.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/generate_flow.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/generate_leads_salesnav.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/google_custom_search.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/hubspot_clearbit.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/hubspot_crm_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/instantly_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/linkedin_crawler.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/lusha_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/openapi_spec_to_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/openapi_tool/__init__.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/openapi_tool/api_models.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/openapi_tool/openapi_tool.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/parse_linkedin_messages_txt.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/profile.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/proxy_curl_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/proxycurl_search_leads.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/python_function_to_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/sales_navigator_crawler.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/salesforce_crm_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/search_router.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/search_router_jobs.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/serarch_router_local_business.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/serpapi_additional_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/serpapi_google_jobs.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/serpapi_google_search.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/serpapi_local_business_search.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/serperdev_google_jobs.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/serperdev_local_business.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/serperdev_search.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/web_download_parse_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/workflow_code_model.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/zoominfo_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/workflow/__init__.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/workflow/agent.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/workflow/flow.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/workflow/task.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana.egg-info/dependency_links.txt +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana.egg-info/entry_points.txt +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana.egg-info/requires.txt +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana.egg-info/top_level.txt +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/tests/test_agent_tools.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/tests/test_apollo_company_search.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/tests/test_apollo_lead_search.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/tests/test_connectivity.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/tests/test_google_document.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/tests/test_hubspot_call_logs.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/tests/test_linkedin_serper.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/tests/test_mcp_connectivity.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/tests/test_proxycurl_get_company_search_id.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/tests/test_proxycurl_job_count.py +0 -0
- {dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/tests/test_structured_output_with_mcp.py +0 -0
|
@@ -364,6 +364,12 @@ class Integration(IntegrationBase):
|
|
|
364
364
|
Integration.model_rebuild()
|
|
365
365
|
IntegrationUpdate.model_rebuild()
|
|
366
366
|
|
|
367
|
+
class BodyFormat(str, Enum):
|
|
368
|
+
AUTO = "auto"
|
|
369
|
+
HTML = "html"
|
|
370
|
+
TEXT = "text"
|
|
371
|
+
|
|
372
|
+
|
|
367
373
|
class SendEmailContext(BaseModel):
|
|
368
374
|
recipient: str
|
|
369
375
|
subject: str
|
|
@@ -371,6 +377,7 @@ class SendEmailContext(BaseModel):
|
|
|
371
377
|
sender_name: str
|
|
372
378
|
sender_email: str
|
|
373
379
|
labels: Optional[List[str]]
|
|
380
|
+
body_format: BodyFormat = BodyFormat.AUTO
|
|
374
381
|
|
|
375
382
|
class QueryEmailContext(BaseModel):
|
|
376
383
|
start_time: str
|
|
@@ -386,4 +393,5 @@ class ReplyEmailContext(BaseModel):
|
|
|
386
393
|
sender_email: str
|
|
387
394
|
sender_name: str
|
|
388
395
|
mark_as_read: str = "True"
|
|
389
|
-
add_labels: Optional[List[str]] = None
|
|
396
|
+
add_labels: Optional[List[str]] = None
|
|
397
|
+
reply_body_format: BodyFormat = BodyFormat.AUTO
|
|
@@ -472,7 +472,7 @@ Output must be valid JSON, e.g.:
|
|
|
472
472
|
prompt=prompt,
|
|
473
473
|
response_format=TechnologyUsedCheck,
|
|
474
474
|
effort="high",
|
|
475
|
-
model="gpt-
|
|
475
|
+
model="gpt-5.1-chat",
|
|
476
476
|
tool_config=tool_config
|
|
477
477
|
)
|
|
478
478
|
|
|
@@ -534,7 +534,7 @@ Output must be valid JSON, e.g.:
|
|
|
534
534
|
prompt=prompt,
|
|
535
535
|
response_format=TechnologyAndRoleCheck,
|
|
536
536
|
effort="high",
|
|
537
|
-
model="gpt-
|
|
537
|
+
model="gpt-5.1-chat",
|
|
538
538
|
tool_config=tool_config
|
|
539
539
|
)
|
|
540
540
|
|
|
@@ -33,13 +33,13 @@ class PandasQuery(BaseModel):
|
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
@assistant_tool
|
|
36
|
-
async def get_structured_output(message: str, response_type, model: str = "gpt-
|
|
36
|
+
async def get_structured_output(message: str, response_type, model: str = "gpt-5.1-chat"):
|
|
37
37
|
"""
|
|
38
38
|
Asynchronously retrieves structured output from the OpenAI API based on the input message.
|
|
39
39
|
|
|
40
40
|
:param message: The input message to be processed by the OpenAI API.
|
|
41
41
|
:param response_type: The expected format of the response (e.g., JSON).
|
|
42
|
-
:param model: The model to be used for processing the input message. Defaults to "gpt-
|
|
42
|
+
:param model: The model to be used for processing the input message. Defaults to "gpt-5.1-chat".
|
|
43
43
|
:return: A tuple containing the parsed response and a status string ('SUCCESS' or 'FAIL').
|
|
44
44
|
"""
|
|
45
45
|
try:
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Small helpers for handling e-mail bodies across providers."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Tuple
|
|
4
|
+
import html as html_lib
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def looks_like_html(text: str) -> bool:
|
|
9
|
+
"""Heuristically determine whether the body contains HTML markup."""
|
|
10
|
+
return bool(text and re.search(r"<[a-zA-Z][^>]*>", text))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _normalize_format_hint(format_hint: Optional[str]) -> str:
|
|
14
|
+
"""
|
|
15
|
+
Normalize a user-supplied format hint into html/text/auto.
|
|
16
|
+
|
|
17
|
+
Accepts variations like "plain" or "plaintext" as text.
|
|
18
|
+
"""
|
|
19
|
+
if not format_hint:
|
|
20
|
+
return "auto"
|
|
21
|
+
fmt_raw = getattr(format_hint, "value", format_hint)
|
|
22
|
+
fmt = str(fmt_raw).strip().lower()
|
|
23
|
+
if fmt in ("html",):
|
|
24
|
+
return "html"
|
|
25
|
+
if fmt in ("text", "plain", "plain_text", "plaintext"):
|
|
26
|
+
return "text"
|
|
27
|
+
return "auto"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def html_to_plain_text(html: str) -> str:
|
|
31
|
+
"""
|
|
32
|
+
Produce a very lightweight plain-text version of an HTML fragment.
|
|
33
|
+
This keeps newlines on block boundaries and strips tags.
|
|
34
|
+
"""
|
|
35
|
+
if not html:
|
|
36
|
+
return ""
|
|
37
|
+
text = re.sub(r"(?is)<(script|style).*?>.*?</\1>", " ", html)
|
|
38
|
+
text = re.sub(r"(?i)<br\s*/?>", "\n", text)
|
|
39
|
+
text = re.sub(r"(?i)</(p|div|li|h[1-6])\s*>", "\n", text)
|
|
40
|
+
text = re.sub(r"(?is)<.*?>", "", text)
|
|
41
|
+
text = html_lib.unescape(text)
|
|
42
|
+
text = re.sub(r"\s+\n", "\n", text)
|
|
43
|
+
text = re.sub(r"\n{3,}", "\n\n", text)
|
|
44
|
+
return text.strip()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def plain_text_to_html(text: str) -> str:
|
|
48
|
+
"""Wrap plain text in a minimal HTML container that preserves newlines."""
|
|
49
|
+
if text is None:
|
|
50
|
+
return ""
|
|
51
|
+
escaped = html_lib.escape(text)
|
|
52
|
+
return f'<div style="white-space: pre-wrap">{escaped}</div>'
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def body_variants(body: Optional[str], format_hint: Optional[str]) -> Tuple[str, str, str]:
|
|
56
|
+
"""
|
|
57
|
+
Return (plain, html, resolved_format) honoring an optional format hint.
|
|
58
|
+
|
|
59
|
+
resolved_format is "html" or "text" after applying auto-detection.
|
|
60
|
+
"""
|
|
61
|
+
content = body or ""
|
|
62
|
+
fmt = _normalize_format_hint(format_hint)
|
|
63
|
+
|
|
64
|
+
if fmt == "html":
|
|
65
|
+
return html_to_plain_text(content), content, "html"
|
|
66
|
+
if fmt == "text":
|
|
67
|
+
return content, plain_text_to_html(content), "text"
|
|
68
|
+
|
|
69
|
+
if looks_like_html(content):
|
|
70
|
+
return html_to_plain_text(content), content, "html"
|
|
71
|
+
|
|
72
|
+
return content, plain_text_to_html(content), "text"
|
|
@@ -162,7 +162,7 @@ async def get_clean_lead_info_with_llm(lead_info_str: str, tool_config: Optional
|
|
|
162
162
|
lead_info, status = await get_structured_output_internal(
|
|
163
163
|
prompt,
|
|
164
164
|
BasicLeadInformation,
|
|
165
|
-
model="gpt-
|
|
165
|
+
model="gpt-5.1-chat",
|
|
166
166
|
tool_config=tool_config
|
|
167
167
|
)
|
|
168
168
|
if status == "ERROR":
|
|
@@ -493,7 +493,7 @@ async def get_user_linkedin_url_from_github_profile(
|
|
|
493
493
|
response, status = await get_structured_output_internal(
|
|
494
494
|
instructions,
|
|
495
495
|
UserInfoFromGithubProfileId,
|
|
496
|
-
model="gpt-
|
|
496
|
+
model="gpt-5.1-chat",
|
|
497
497
|
use_web_search=True,
|
|
498
498
|
tool_config=tool_config
|
|
499
499
|
)
|
|
@@ -903,7 +903,7 @@ async def get_company_domain_from_llm_web_search(
|
|
|
903
903
|
response, status = await get_structured_output_internal(
|
|
904
904
|
instructions,
|
|
905
905
|
CompanyInfoFromName,
|
|
906
|
-
model="gpt-
|
|
906
|
+
model="gpt-5.1-chat",
|
|
907
907
|
use_web_search=True,
|
|
908
908
|
tool_config=tool_config
|
|
909
909
|
)
|
|
@@ -152,7 +152,7 @@ async def generate_personalized_email_copy(
|
|
|
152
152
|
prompt=initial_prompt,
|
|
153
153
|
response_format=EmailCopy,
|
|
154
154
|
vector_store_id=vector_store_id,
|
|
155
|
-
model="gpt-
|
|
155
|
+
model="gpt-5.1-chat",
|
|
156
156
|
tool_config=tool_config,
|
|
157
157
|
use_cache=email_context.message_instructions.use_cache if email_context.message_instructions else True
|
|
158
158
|
)
|
|
@@ -161,7 +161,7 @@ async def generate_personalized_email_copy(
|
|
|
161
161
|
initial_response, initial_status = await get_structured_output_internal(
|
|
162
162
|
prompt=initial_prompt,
|
|
163
163
|
response_format=EmailCopy,
|
|
164
|
-
model="gpt-
|
|
164
|
+
model="gpt-5.1-chat",
|
|
165
165
|
tool_config=tool_config,
|
|
166
166
|
use_cache=email_context.message_instructions.use_cache if email_context.message_instructions else True
|
|
167
167
|
)
|
|
@@ -194,7 +194,7 @@ async def get_inbound_email_triage_action(
|
|
|
194
194
|
triage_only, status = await get_structured_output_with_assistant_and_vector_store(
|
|
195
195
|
prompt=triage_prompt,
|
|
196
196
|
response_format=InboundEmailTriageResponse,
|
|
197
|
-
model="gpt-
|
|
197
|
+
model="gpt-5.1-chat",
|
|
198
198
|
vector_store_id=cleaned_context.external_known_data.external_openai_vector_store_id,
|
|
199
199
|
tool_config=tool_config,
|
|
200
200
|
use_cache=cleaned_context.message_instructions.use_cache if cleaned_context.message_instructions else True
|
|
@@ -203,7 +203,7 @@ async def get_inbound_email_triage_action(
|
|
|
203
203
|
triage_only, status = await get_structured_output_internal(
|
|
204
204
|
prompt=triage_prompt,
|
|
205
205
|
response_format=InboundEmailTriageResponse,
|
|
206
|
-
model="gpt-
|
|
206
|
+
model="gpt-5.1-chat",
|
|
207
207
|
tool_config=tool_config,
|
|
208
208
|
use_cache=cleaned_context.message_instructions.use_cache if cleaned_context.message_instructions else True
|
|
209
209
|
)
|
|
@@ -376,7 +376,7 @@ async def generate_inbound_email_response_copy(
|
|
|
376
376
|
initial_response, status = await get_structured_output_with_assistant_and_vector_store(
|
|
377
377
|
prompt=prompt,
|
|
378
378
|
response_format=InboundEmailTriageResponse,
|
|
379
|
-
model="gpt-
|
|
379
|
+
model="gpt-5.1-chat",
|
|
380
380
|
vector_store_id=cleaned_context.external_known_data.external_openai_vector_store_id,
|
|
381
381
|
tool_config=tool_config
|
|
382
382
|
)
|
|
@@ -384,7 +384,7 @@ async def generate_inbound_email_response_copy(
|
|
|
384
384
|
initial_response, status = await get_structured_output_internal(
|
|
385
385
|
prompt=prompt,
|
|
386
386
|
response_format=InboundEmailTriageResponse,
|
|
387
|
-
model="gpt-
|
|
387
|
+
model="gpt-5.1-chat",
|
|
388
388
|
tool_config=tool_config
|
|
389
389
|
)
|
|
390
390
|
|
{dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/generate_linkedin_connect_message.py
RENAMED
|
@@ -149,7 +149,7 @@ async def generate_personalized_linkedin_copy(
|
|
|
149
149
|
prompt=prompt,
|
|
150
150
|
response_format=LinkedInConnectMessage,
|
|
151
151
|
vector_store_id=vector_store_id,
|
|
152
|
-
model="gpt-
|
|
152
|
+
model="gpt-5.1-chat",
|
|
153
153
|
tool_config=tool_config,
|
|
154
154
|
use_cache=linkedin_context.message_instructions.use_cache if linkedin_context.message_instructions else True
|
|
155
155
|
)
|
|
@@ -158,7 +158,7 @@ async def generate_personalized_linkedin_copy(
|
|
|
158
158
|
response_data, status = await get_structured_output_internal(
|
|
159
159
|
prompt=prompt,
|
|
160
160
|
response_format=LinkedInConnectMessage,
|
|
161
|
-
model="gpt-
|
|
161
|
+
model="gpt-5.1-chat",
|
|
162
162
|
tool_config=tool_config,
|
|
163
163
|
use_cache=linkedin_context.message_instructions.use_cache if linkedin_context.message_instructions else True
|
|
164
164
|
)
|
|
@@ -224,7 +224,7 @@ async def generate_linkedin_response_message_copy(
|
|
|
224
224
|
initial_response, status = await get_structured_output_with_assistant_and_vector_store(
|
|
225
225
|
prompt=prompt,
|
|
226
226
|
response_format=LinkedInTriageResponse,
|
|
227
|
-
model="gpt-
|
|
227
|
+
model="gpt-5.1-chat",
|
|
228
228
|
vector_store_id=cleaned_context.external_known_data.external_openai_vector_store_id,
|
|
229
229
|
tool_config=tool_config,
|
|
230
230
|
use_cache=linkedin_context.message_instructions.use_cache if linkedin_context.message_instructions else True
|
|
@@ -233,7 +233,7 @@ async def generate_linkedin_response_message_copy(
|
|
|
233
233
|
initial_response, status = await get_structured_output_internal(
|
|
234
234
|
prompt,
|
|
235
235
|
LinkedInTriageResponse,
|
|
236
|
-
model="gpt-
|
|
236
|
+
model="gpt-5.1-chat",
|
|
237
237
|
tool_config=tool_config,
|
|
238
238
|
use_cache=linkedin_context.message_instructions.use_cache if linkedin_context.message_instructions else True
|
|
239
239
|
)
|
|
@@ -56,7 +56,7 @@ async def get_structured_output_internal(
|
|
|
56
56
|
response_format: BaseModel,
|
|
57
57
|
effort: str = "low",
|
|
58
58
|
use_web_search: bool = False,
|
|
59
|
-
model: str = "gpt-
|
|
59
|
+
model: str = "gpt-5.1-chat",
|
|
60
60
|
tool_config: Optional[List[Dict]] = None,
|
|
61
61
|
use_cache: bool = True
|
|
62
62
|
):
|
|
@@ -217,7 +217,7 @@ async def get_structured_output_with_mcp(
|
|
|
217
217
|
response_format: BaseModel,
|
|
218
218
|
effort: str = "low",
|
|
219
219
|
use_web_search: bool = False,
|
|
220
|
-
model: str = "gpt-
|
|
220
|
+
model: str = "gpt-5.1-chat",
|
|
221
221
|
tool_config: Optional[List[Dict[str, Any]]] = None,
|
|
222
222
|
) -> Tuple[Union[BaseModel, str], str]:
|
|
223
223
|
"""
|
|
@@ -376,7 +376,7 @@ async def get_structured_output_with_assistant_and_vector_store(
|
|
|
376
376
|
response_format: BaseModel,
|
|
377
377
|
vector_store_id: str,
|
|
378
378
|
effort: str = "low",
|
|
379
|
-
model="gpt-
|
|
379
|
+
model="gpt-5.1-chat",
|
|
380
380
|
tool_config: Optional[List[Dict]] = None,
|
|
381
381
|
use_cache: bool = True
|
|
382
382
|
):
|
|
@@ -2,6 +2,7 @@ import base64
|
|
|
2
2
|
import json
|
|
3
3
|
import logging
|
|
4
4
|
import re
|
|
5
|
+
from email.mime.multipart import MIMEMultipart
|
|
5
6
|
from email.mime.text import MIMEText
|
|
6
7
|
from typing import Any, Dict, List, Optional
|
|
7
8
|
|
|
@@ -22,6 +23,7 @@ from dhisana.utils.email_parse_helpers import (
|
|
|
22
23
|
)
|
|
23
24
|
from dhisana.utils.assistant_tool_tag import assistant_tool
|
|
24
25
|
from dhisana.utils.cache_output_tools import retrieve_output, cache_output
|
|
26
|
+
from dhisana.utils.email_body_utils import body_variants
|
|
25
27
|
from typing import Optional as _Optional # avoid name clash in wrappers
|
|
26
28
|
|
|
27
29
|
def _status_phrase(code: int) -> str:
|
|
@@ -127,7 +129,18 @@ async def send_email_using_google_oauth_async(
|
|
|
127
129
|
"""
|
|
128
130
|
token = get_google_access_token(tool_config)
|
|
129
131
|
|
|
130
|
-
|
|
132
|
+
plain_body, html_body, resolved_fmt = body_variants(
|
|
133
|
+
send_email_context.body,
|
|
134
|
+
getattr(send_email_context, "body_format", None),
|
|
135
|
+
)
|
|
136
|
+
# Use multipart/alternative when we have both; fall back to single part for pure text.
|
|
137
|
+
if resolved_fmt == "text":
|
|
138
|
+
message = MIMEText(plain_body, "plain", _charset="utf-8")
|
|
139
|
+
else:
|
|
140
|
+
message = MIMEMultipart("alternative")
|
|
141
|
+
message.attach(MIMEText(plain_body, "plain", _charset="utf-8"))
|
|
142
|
+
message.attach(MIMEText(html_body, "html", _charset="utf-8"))
|
|
143
|
+
|
|
131
144
|
message["to"] = send_email_context.recipient
|
|
132
145
|
message["from"] = f"{send_email_context.sender_name} <{send_email_context.sender_email}>"
|
|
133
146
|
message["subject"] = send_email_context.subject
|
|
@@ -265,7 +278,17 @@ async def reply_to_email_google_oauth_async(
|
|
|
265
278
|
message_id_header = headers_map.get("Message-ID", "") or ""
|
|
266
279
|
|
|
267
280
|
# 2) Build reply MIME
|
|
268
|
-
|
|
281
|
+
plain_reply, html_reply, resolved_reply_fmt = body_variants(
|
|
282
|
+
reply_email_context.reply_body,
|
|
283
|
+
getattr(reply_email_context, "reply_body_format", None),
|
|
284
|
+
)
|
|
285
|
+
if resolved_reply_fmt == "text":
|
|
286
|
+
msg = MIMEText(plain_reply, "plain", _charset="utf-8")
|
|
287
|
+
else:
|
|
288
|
+
msg = MIMEMultipart("alternative")
|
|
289
|
+
msg.attach(MIMEText(plain_reply, "plain", _charset="utf-8"))
|
|
290
|
+
msg.attach(MIMEText(html_reply, "html", _charset="utf-8"))
|
|
291
|
+
|
|
269
292
|
msg["To"] = to_addresses
|
|
270
293
|
if cc_addresses:
|
|
271
294
|
msg["Cc"] = cc_addresses
|
|
@@ -24,8 +24,9 @@ from googleapiclient.http import MediaFileUpload, MediaIoBaseDownload
|
|
|
24
24
|
from dhisana.schemas.sales import MessageItem
|
|
25
25
|
from dhisana.utils.assistant_tool_tag import assistant_tool
|
|
26
26
|
from dhisana.utils.email_parse_helpers import *
|
|
27
|
+
from dhisana.utils.email_body_utils import body_variants
|
|
27
28
|
import asyncio
|
|
28
|
-
from dhisana.schemas.common import (SendEmailContext, QueryEmailContext, ReplyEmailContext)
|
|
29
|
+
from dhisana.schemas.common import (SendEmailContext, QueryEmailContext, ReplyEmailContext, BodyFormat)
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
################################################################################
|
|
@@ -161,15 +162,18 @@ async def send_email_using_service_account_async(
|
|
|
161
162
|
|
|
162
163
|
gmail_api_url = 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send'
|
|
163
164
|
|
|
164
|
-
|
|
165
|
+
plain_body, html_body, resolved_fmt = body_variants(
|
|
166
|
+
send_email_context.body,
|
|
167
|
+
getattr(send_email_context, "body_format", None),
|
|
168
|
+
)
|
|
165
169
|
|
|
166
|
-
if
|
|
170
|
+
if resolved_fmt == "text":
|
|
171
|
+
message = MIMEText(plain_body, _subtype="plain", _charset="utf-8")
|
|
172
|
+
else:
|
|
167
173
|
# Gmail prefers multipart/alternative when HTML is present.
|
|
168
174
|
message = MIMEMultipart("alternative")
|
|
169
|
-
message.attach(MIMEText(
|
|
170
|
-
message.attach(MIMEText(
|
|
171
|
-
else:
|
|
172
|
-
message = MIMEText(body, _subtype="plain", _charset="utf-8")
|
|
175
|
+
message.attach(MIMEText(plain_body, "plain", _charset="utf-8"))
|
|
176
|
+
message.attach(MIMEText(html_body, "html", _charset="utf-8"))
|
|
173
177
|
|
|
174
178
|
message['to'] = send_email_context.recipient
|
|
175
179
|
message['from'] = f"{send_email_context.sender_name} <{send_email_context.sender_email}>"
|
|
@@ -509,6 +513,7 @@ class SendEmailContext(BaseModel):
|
|
|
509
513
|
sender_name: str
|
|
510
514
|
sender_email: str
|
|
511
515
|
labels: Optional[List[str]]
|
|
516
|
+
body_format: BodyFormat = BodyFormat.AUTO
|
|
512
517
|
|
|
513
518
|
@assistant_tool
|
|
514
519
|
async def send_email_using_service_account_async(
|
|
@@ -537,8 +542,18 @@ async def send_email_using_service_account_async(
|
|
|
537
542
|
|
|
538
543
|
gmail_api_url = 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send'
|
|
539
544
|
|
|
545
|
+
plain_body, html_body, resolved_fmt = body_variants(
|
|
546
|
+
send_email_context.body,
|
|
547
|
+
getattr(send_email_context, "body_format", None),
|
|
548
|
+
)
|
|
549
|
+
|
|
540
550
|
# Construct the MIME text message
|
|
541
|
-
|
|
551
|
+
if resolved_fmt == "text":
|
|
552
|
+
message = MIMEText(plain_body, _subtype="plain", _charset="utf-8")
|
|
553
|
+
else:
|
|
554
|
+
message = MIMEMultipart("alternative")
|
|
555
|
+
message.attach(MIMEText(plain_body, "plain", _charset="utf-8"))
|
|
556
|
+
message.attach(MIMEText(html_body, "html", _charset="utf-8"))
|
|
542
557
|
message['to'] = send_email_context.recipient
|
|
543
558
|
message['from'] = f"{send_email_context.sender_name} <{send_email_context.sender_email}>"
|
|
544
559
|
message['subject'] = send_email_context.subject
|
|
@@ -893,7 +908,16 @@ async def reply_to_email_async(
|
|
|
893
908
|
message_id_header = headers_dict.get('Message-ID', '')
|
|
894
909
|
|
|
895
910
|
# 3. Create the reply email message
|
|
896
|
-
|
|
911
|
+
plain_reply, html_reply, resolved_reply_fmt = body_variants(
|
|
912
|
+
reply_email_context.reply_body,
|
|
913
|
+
getattr(reply_email_context, "reply_body_format", None),
|
|
914
|
+
)
|
|
915
|
+
if resolved_reply_fmt == "text":
|
|
916
|
+
msg = MIMEText(plain_reply, _subtype="plain", _charset="utf-8")
|
|
917
|
+
else:
|
|
918
|
+
msg = MIMEMultipart("alternative")
|
|
919
|
+
msg.attach(MIMEText(plain_reply, "plain", _charset="utf-8"))
|
|
920
|
+
msg.attach(MIMEText(html_reply, "html", _charset="utf-8"))
|
|
897
921
|
msg['To'] = to_addresses
|
|
898
922
|
if cc_addresses:
|
|
899
923
|
msg['Cc'] = cc_addresses
|
|
@@ -7,6 +7,7 @@ import aiohttp
|
|
|
7
7
|
|
|
8
8
|
from dhisana.utils.assistant_tool_tag import assistant_tool
|
|
9
9
|
from dhisana.schemas.common import SendEmailContext
|
|
10
|
+
from dhisana.utils.email_body_utils import body_variants
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
def get_mailgun_notify_key(tool_config: Optional[List[Dict]] = None) -> str:
|
|
@@ -59,6 +60,7 @@ async def send_email_with_mailgun(
|
|
|
59
60
|
subject: str,
|
|
60
61
|
message: str,
|
|
61
62
|
tool_config: Optional[List[Dict]] = None,
|
|
63
|
+
body_format: Optional[str] = None,
|
|
62
64
|
):
|
|
63
65
|
"""
|
|
64
66
|
Send an email using the Mailgun API.
|
|
@@ -74,13 +76,17 @@ async def send_email_with_mailgun(
|
|
|
74
76
|
api_key = get_mailgun_notify_key(tool_config)
|
|
75
77
|
domain = get_mailgun_notify_domain(tool_config)
|
|
76
78
|
|
|
79
|
+
body = message or ""
|
|
77
80
|
data = {
|
|
78
81
|
"from": sender,
|
|
79
82
|
"to": recipients,
|
|
80
83
|
"subject": subject,
|
|
81
|
-
"html": message,
|
|
82
84
|
}
|
|
83
85
|
|
|
86
|
+
plain_body, html_body, _ = body_variants(body, body_format)
|
|
87
|
+
data["text"] = plain_body
|
|
88
|
+
data["html"] = html_body
|
|
89
|
+
|
|
84
90
|
async with aiohttp.ClientSession() as session:
|
|
85
91
|
async with session.post(
|
|
86
92
|
f"https://api.mailgun.net/v3/{domain}/messages",
|
|
@@ -107,11 +113,17 @@ async def send_email_using_mailgun_async(
|
|
|
107
113
|
api_key = get_mailgun_notify_key(tool_config)
|
|
108
114
|
domain = get_mailgun_notify_domain(tool_config)
|
|
109
115
|
|
|
116
|
+
plain_body, html_body, _ = body_variants(
|
|
117
|
+
send_email_context.body,
|
|
118
|
+
getattr(send_email_context, "body_format", None),
|
|
119
|
+
)
|
|
120
|
+
|
|
110
121
|
data = {
|
|
111
122
|
"from": f"{send_email_context.sender_name} <{send_email_context.sender_email}>",
|
|
112
123
|
"to": [send_email_context.recipient],
|
|
113
124
|
"subject": send_email_context.subject,
|
|
114
|
-
"
|
|
125
|
+
"text": plain_body,
|
|
126
|
+
"html": html_body,
|
|
115
127
|
}
|
|
116
128
|
|
|
117
129
|
async with aiohttp.ClientSession() as session:
|
|
@@ -12,6 +12,7 @@ from dhisana.schemas.common import (
|
|
|
12
12
|
ReplyEmailContext,
|
|
13
13
|
)
|
|
14
14
|
from dhisana.schemas.sales import MessageItem
|
|
15
|
+
from dhisana.utils.email_body_utils import body_variants
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
def get_microsoft365_access_token(tool_config: Optional[List[Dict]] = None) -> str:
|
|
@@ -149,11 +150,18 @@ async def send_email_using_microsoft_graph_async(
|
|
|
149
150
|
base_url = "https://graph.microsoft.com/v1.0"
|
|
150
151
|
base_res = _base_resource(sender_email, tool_config, auth_mode)
|
|
151
152
|
|
|
153
|
+
plain_body, html_body, resolved_fmt = body_variants(
|
|
154
|
+
send_email_context.body,
|
|
155
|
+
getattr(send_email_context, "body_format", None),
|
|
156
|
+
)
|
|
157
|
+
content_type = "Text" if resolved_fmt == "text" else "HTML"
|
|
158
|
+
content_body = plain_body if resolved_fmt == "text" else html_body
|
|
159
|
+
|
|
152
160
|
message_payload: Dict[str, Any] = {
|
|
153
161
|
"subject": send_email_context.subject,
|
|
154
162
|
"body": {
|
|
155
|
-
"contentType":
|
|
156
|
-
"content":
|
|
163
|
+
"contentType": content_type,
|
|
164
|
+
"content": content_body,
|
|
157
165
|
},
|
|
158
166
|
"toRecipients": [
|
|
159
167
|
{"emailAddress": {"address": send_email_context.recipient}}
|
{dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/openai_assistant_and_file_utils.py
RENAMED
|
@@ -169,7 +169,7 @@ async def delete_files(
|
|
|
169
169
|
async def run_file_search(
|
|
170
170
|
query: str,
|
|
171
171
|
vector_store_id: str,
|
|
172
|
-
model: str = "gpt-
|
|
172
|
+
model: str = "gpt-5.1-chat",
|
|
173
173
|
max_num_results: int = 5,
|
|
174
174
|
store: bool = True,
|
|
175
175
|
tool_config: Optional[List[Dict]] = None,
|
|
@@ -213,7 +213,7 @@ async def run_file_search(
|
|
|
213
213
|
|
|
214
214
|
async def run_response_text(
|
|
215
215
|
prompt: str,
|
|
216
|
-
model: str = "gpt-
|
|
216
|
+
model: str = "gpt-5.1-chat",
|
|
217
217
|
max_tokens: int = 2048,
|
|
218
218
|
store: bool = True,
|
|
219
219
|
tool_config: Optional[List[Dict]] = None,
|
|
@@ -237,7 +237,7 @@ async def run_response_text(
|
|
|
237
237
|
async def run_response_structured(
|
|
238
238
|
prompt: str,
|
|
239
239
|
response_format: dict,
|
|
240
|
-
model: str = "gpt-
|
|
240
|
+
model: str = "gpt-5.1-chat",
|
|
241
241
|
max_tokens: int = 1024,
|
|
242
242
|
store: bool = True,
|
|
243
243
|
tool_config: Optional[List[Dict]] = None,
|
|
@@ -330,7 +330,7 @@ async def process_agent_request(row_batch: List[Dict], workflow: Dict, custom_in
|
|
|
330
330
|
name="AI Assistant",
|
|
331
331
|
instructions=instructions,
|
|
332
332
|
tools=[],
|
|
333
|
-
model="gpt-
|
|
333
|
+
model="gpt-5.1-chat"
|
|
334
334
|
)
|
|
335
335
|
thread = await client.beta.threads.create()
|
|
336
336
|
parsed_outputs = []
|
|
@@ -955,7 +955,7 @@ async def get_function_call_arguments(input_text: str, function_name: str) -> Tu
|
|
|
955
955
|
|
|
956
956
|
# Make the API call
|
|
957
957
|
response = await client.beta.chat.completions.parse(
|
|
958
|
-
model="gpt-
|
|
958
|
+
model="gpt-5.1-chat",
|
|
959
959
|
messages=[
|
|
960
960
|
{"role": "system", "content": "Extract function arguments in JSON format."},
|
|
961
961
|
{"role": "user", "content": prompt},
|
|
@@ -91,7 +91,7 @@ async def research_lead_with_full_info_ai(
|
|
|
91
91
|
response, status = await get_structured_output_internal(
|
|
92
92
|
instructions,
|
|
93
93
|
LeadResearchInformation,
|
|
94
|
-
model="gpt-
|
|
94
|
+
model="gpt-5.1-chat",
|
|
95
95
|
tool_config=tool_config
|
|
96
96
|
)
|
|
97
97
|
if status == "SUCCESS":
|
|
@@ -165,7 +165,7 @@ async def research_company_with_full_info_ai(
|
|
|
165
165
|
response, status = await get_structured_output_internal(
|
|
166
166
|
instructions,
|
|
167
167
|
CompanyResearchInformation,
|
|
168
|
-
model="gpt-
|
|
168
|
+
model="gpt-5.1-chat",
|
|
169
169
|
use_web_search=False,
|
|
170
170
|
tool_config=tool_config
|
|
171
171
|
)
|
|
@@ -14,6 +14,7 @@ import aiohttp
|
|
|
14
14
|
|
|
15
15
|
from dhisana.utils.assistant_tool_tag import assistant_tool
|
|
16
16
|
from dhisana.schemas.common import SendEmailContext
|
|
17
|
+
from dhisana.utils.email_body_utils import body_variants
|
|
17
18
|
|
|
18
19
|
# --------------------------------------------------------------------------- #
|
|
19
20
|
# Mailgun (re-exported from dedicated module for backward compatibility)
|
|
@@ -57,6 +58,7 @@ async def send_email_with_sendgrid(
|
|
|
57
58
|
subject: str,
|
|
58
59
|
message: str,
|
|
59
60
|
tool_config: Optional[List[Dict]] = None,
|
|
61
|
+
body_format: Optional[str] = None,
|
|
60
62
|
):
|
|
61
63
|
"""
|
|
62
64
|
Send an email using SendGrid's v3 Mail Send API.
|
|
@@ -79,6 +81,12 @@ async def send_email_with_sendgrid(
|
|
|
79
81
|
if not to_list:
|
|
80
82
|
return {"error": "No recipients provided"}
|
|
81
83
|
|
|
84
|
+
plain_body, html_body, _ = body_variants(message, body_format)
|
|
85
|
+
content = [
|
|
86
|
+
{"type": "text/plain", "value": plain_body},
|
|
87
|
+
{"type": "text/html", "value": html_body},
|
|
88
|
+
]
|
|
89
|
+
|
|
82
90
|
payload = {
|
|
83
91
|
"personalizations": [
|
|
84
92
|
{
|
|
@@ -87,9 +95,7 @@ async def send_email_with_sendgrid(
|
|
|
87
95
|
}
|
|
88
96
|
],
|
|
89
97
|
"from": from_obj,
|
|
90
|
-
"content":
|
|
91
|
-
{"type": "text/html", "value": message or ""}
|
|
92
|
-
],
|
|
98
|
+
"content": content,
|
|
93
99
|
}
|
|
94
100
|
|
|
95
101
|
headers = {
|
|
@@ -126,11 +132,16 @@ async def send_email_using_sendgrid_async(
|
|
|
126
132
|
Provider-style wrapper for SendGrid using SendEmailContext.
|
|
127
133
|
Returns an opaque token since SendGrid does not return a message id.
|
|
128
134
|
"""
|
|
135
|
+
plain_body, html_body, _ = body_variants(
|
|
136
|
+
ctx.body,
|
|
137
|
+
getattr(ctx, "body_format", None),
|
|
138
|
+
)
|
|
129
139
|
result = await send_email_with_sendgrid(
|
|
130
140
|
sender=f"{ctx.sender_name} <{ctx.sender_email}>",
|
|
131
141
|
recipients=[ctx.recipient],
|
|
132
142
|
subject=ctx.subject,
|
|
133
143
|
message=ctx.body or "",
|
|
144
|
+
body_format=getattr(ctx, "body_format", None),
|
|
134
145
|
tool_config=tool_config,
|
|
135
146
|
)
|
|
136
147
|
# Normalise output to a string id-like value
|
|
@@ -51,7 +51,7 @@ async def get_structured_output(text: str, tool_config: Optional[List[Dict]] = N
|
|
|
51
51
|
f"Text:\n{text}"
|
|
52
52
|
)
|
|
53
53
|
result, status = await get_structured_output_internal(
|
|
54
|
-
prompt, LeadSearchResult, model = "gpt-
|
|
54
|
+
prompt, LeadSearchResult, model = "gpt-5.1-chat", tool_config=tool_config
|
|
55
55
|
)
|
|
56
56
|
if status != "SUCCESS" or result is None:
|
|
57
57
|
return LeadSearchResult()
|
|
@@ -137,7 +137,7 @@ async def pick_best_linkedin_candidate_with_llm(
|
|
|
137
137
|
result, status = await get_structured_output_internal(
|
|
138
138
|
prompt,
|
|
139
139
|
LinkedinCandidateChoice,
|
|
140
|
-
model="gpt-
|
|
140
|
+
model="gpt-5.1-chat",
|
|
141
141
|
tool_config=tool_config,
|
|
142
142
|
)
|
|
143
143
|
|
|
@@ -33,6 +33,7 @@ from dhisana.utils.google_workspace_tools import (
|
|
|
33
33
|
QueryEmailContext,
|
|
34
34
|
SendEmailContext,
|
|
35
35
|
)
|
|
36
|
+
from dhisana.utils.email_body_utils import body_variants
|
|
36
37
|
|
|
37
38
|
|
|
38
39
|
# --------------------------------------------------------------------------- #
|
|
@@ -151,15 +152,18 @@ async def send_email_via_smtp_async(
|
|
|
151
152
|
str
|
|
152
153
|
The Message-ID of the sent message (e.g., "<uuid@yourdomain.com>").
|
|
153
154
|
"""
|
|
154
|
-
|
|
155
|
+
plain_body, html_body, resolved_fmt = body_variants(
|
|
156
|
+
ctx.body,
|
|
157
|
+
getattr(ctx, "body_format", None),
|
|
158
|
+
)
|
|
155
159
|
|
|
156
|
-
if
|
|
160
|
+
if resolved_fmt == "text":
|
|
161
|
+
msg = MIMEText(plain_body, _subtype="plain", _charset="utf-8")
|
|
162
|
+
else:
|
|
157
163
|
# Build multipart/alternative so HTML-capable clients see rich content.
|
|
158
164
|
msg = MIMEMultipart("alternative")
|
|
159
|
-
msg.attach(MIMEText(
|
|
160
|
-
msg.attach(MIMEText(
|
|
161
|
-
else:
|
|
162
|
-
msg = MIMEText(body, _subtype="plain", _charset="utf-8")
|
|
165
|
+
msg.attach(MIMEText(plain_body, "plain", _charset="utf-8"))
|
|
166
|
+
msg.attach(MIMEText(html_body, "html", _charset="utf-8"))
|
|
163
167
|
|
|
164
168
|
msg["From"] = f"{ctx.sender_name} <{ctx.sender_email}>"
|
|
165
169
|
msg["To"] = ctx.recipient
|
|
@@ -1619,7 +1619,7 @@ async def test_connectivity(tool_config: List[Dict[str, Any]]) -> Dict[str, Dict
|
|
|
1619
1619
|
|
|
1620
1620
|
# OpenAI needs extra args
|
|
1621
1621
|
if tool_name == "openai":
|
|
1622
|
-
model_name = next((c["value"] for c in config_entries if c["name"] == "modelName"), "gpt-
|
|
1622
|
+
model_name = next((c["value"] for c in config_entries if c["name"] == "modelName"), "gpt-5.1-chat")
|
|
1623
1623
|
reasoning_effort = next((c["value"] for c in config_entries if c["name"] == "reasoningEffort"), "medium")
|
|
1624
1624
|
results[tool_name] = await test_openai(api_key, model_name, reasoning_effort)
|
|
1625
1625
|
|
|
@@ -159,7 +159,7 @@ async def create_property_mapping(
|
|
|
159
159
|
prompt=user_prompt,
|
|
160
160
|
response_format=PropertyMappingList,
|
|
161
161
|
effort="high",
|
|
162
|
-
model="gpt-
|
|
162
|
+
model="gpt-5.1-chat",
|
|
163
163
|
tool_config=tool_config
|
|
164
164
|
)
|
|
165
165
|
if status == "SUCCESS" and response and response.properties:
|
|
@@ -21,7 +21,7 @@ async def call_openai_api(system_content: str, user_content: str, max_tokens: in
|
|
|
21
21
|
try:
|
|
22
22
|
# Call the OpenAI API using the new client method
|
|
23
23
|
response = client.chat.completions.create(
|
|
24
|
-
model="gpt-
|
|
24
|
+
model="gpt-5.1-chat",
|
|
25
25
|
messages=[
|
|
26
26
|
{"role": "system", "content": system_content},
|
|
27
27
|
{"role": "user", "content": user_content}
|
|
@@ -38,6 +38,7 @@ src/dhisana/utils/compose_three_step_workflow.py
|
|
|
38
38
|
src/dhisana/utils/composite_tools.py
|
|
39
39
|
src/dhisana/utils/dataframe_tools.py
|
|
40
40
|
src/dhisana/utils/domain_parser.py
|
|
41
|
+
src/dhisana/utils/email_body_utils.py
|
|
41
42
|
src/dhisana/utils/email_parse_helpers.py
|
|
42
43
|
src/dhisana/utils/email_provider.py
|
|
43
44
|
src/dhisana/utils/enrich_lead_information.py
|
|
@@ -105,6 +106,7 @@ tests/test_agent_tools.py
|
|
|
105
106
|
tests/test_apollo_company_search.py
|
|
106
107
|
tests/test_apollo_lead_search.py
|
|
107
108
|
tests/test_connectivity.py
|
|
109
|
+
tests/test_email_body_utils.py
|
|
108
110
|
tests/test_google_document.py
|
|
109
111
|
tests/test_hubspot_call_logs.py
|
|
110
112
|
tests/test_linkedin_serper.py
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from dhisana.schemas.common import BodyFormat
|
|
4
|
+
from dhisana.utils.email_body_utils import body_variants
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.mark.parametrize(
|
|
8
|
+
"body,format_hint,expected_resolved",
|
|
9
|
+
[
|
|
10
|
+
("<p>Hello</p>", BodyFormat.HTML, "html"),
|
|
11
|
+
("Hello", BodyFormat.TEXT, "text"),
|
|
12
|
+
],
|
|
13
|
+
)
|
|
14
|
+
def test_body_variants_honors_body_format_enum(body, format_hint, expected_resolved):
|
|
15
|
+
plain, html, resolved = body_variants(body, format_hint)
|
|
16
|
+
|
|
17
|
+
if expected_resolved == "html":
|
|
18
|
+
assert html == body
|
|
19
|
+
assert plain == "Hello"
|
|
20
|
+
else:
|
|
21
|
+
assert plain == body
|
|
22
|
+
assert html.startswith("<div")
|
|
23
|
+
assert resolved == expected_resolved
|
|
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.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/check_email_validity_tools.py
RENAMED
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/check_linkedin_url_validity.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/compose_three_step_workflow.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/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
|
|
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.dev227 → dhisana-0.0.1.dev229}/src/dhisana/utils/openapi_tool/openapi_tool.py
RENAMED
|
File without changes
|
{dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/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
|
{dhisana-0.0.1.dev227 → dhisana-0.0.1.dev229}/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.dev227 → dhisana-0.0.1.dev229}/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
|