dhisana 0.0.1.dev227__py3-none-any.whl → 0.0.1.dev229__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
dhisana/schemas/common.py CHANGED
@@ -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-4.1-mini",
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-4.1-mini",
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-4.1-mini"):
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-4.1-mini".
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-4.1-mini",
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-4.1-mini",
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-4.1-mini",
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-4.1",
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-4.1",
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-4.1",
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-4.1",
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-4.1",
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-4.1",
387
+ model="gpt-5.1-chat",
388
388
  tool_config=tool_config
389
389
  )
390
390
 
@@ -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-4.1",
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-4.1",
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-4.1",
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-4.1",
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-4.1-mini",
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-4.1-mini",
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-4.1-mini",
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
- message = MIMEText(send_email_context.body, _subtype="html")
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
- msg = MIMEText(reply_email_context.reply_body, _subtype="html")
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
- body = send_email_context.body or ""
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 _looks_like_html(body):
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(_html_to_plain_text(body), "plain", _charset="utf-8"))
170
- message.attach(MIMEText(body, "html", _charset="utf-8"))
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
- message = MIMEText(send_email_context.body)
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
- msg = MIMEText(reply_email_context.reply_body)
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
- "html": send_email_context.body or "",
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": "HTML",
156
- "content": send_email_context.body or "",
163
+ "contentType": content_type,
164
+ "content": content_body,
157
165
  },
158
166
  "toRecipients": [
159
167
  {"emailAddress": {"address": send_email_context.recipient}}
@@ -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-4.1-mini",
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-4.1-mini",
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-4.1-mini",
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-4.1-mini"
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-4.1-mini",
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-4.1-mini",
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-4.1-mini",
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-4.1-nano", tool_config=tool_config
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-4.1-mini",
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
- body = ctx.body or ""
155
+ plain_body, html_body, resolved_fmt = body_variants(
156
+ ctx.body,
157
+ getattr(ctx, "body_format", None),
158
+ )
155
159
 
156
- if _looks_like_html(body):
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(_html_to_plain_text(body), "plain", _charset="utf-8"))
160
- msg.attach(MIMEText(body, "html", _charset="utf-8"))
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-4o-mini")
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-4.1-mini",
162
+ model="gpt-5.1-chat",
163
163
  tool_config=tool_config
164
164
  )
165
165
  if status == "SUCCESS" and response and response.properties:
dhisana/workflow/test.py CHANGED
@@ -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-4.1-mini",
24
+ model="gpt-5.1-chat",
25
25
  messages=[
26
26
  {"role": "system", "content": system_content},
27
27
  {"role": "user", "content": user_content}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dhisana
3
- Version: 0.0.1.dev227
3
+ Version: 0.0.1.dev229
4
4
  Summary: A Python SDK for Dhisana AI Platform
5
5
  Home-page: https://github.com/dhisana-ai/dhisana-python-sdk
6
6
  Author: Admin
@@ -5,7 +5,7 @@ dhisana/cli/datasets.py,sha256=OwzoCrVQqmh0pKpUAKAg_w9uGYncbWU7ZrAL_QukxAk,839
5
5
  dhisana/cli/models.py,sha256=IzUFZW_X32mL3fpM1_j4q8AF7v5nrxJcxBoqvG-TTgA,706
6
6
  dhisana/cli/predictions.py,sha256=VYgoLK1Ksv6MFImoYZqjQJkds7e5Hso65dHwbxTNNzE,646
7
7
  dhisana/schemas/__init__.py,sha256=jv2YF__bseklT3OWEzlqJ5qE24c4aWd5F4r0TTjOrWQ,65
8
- dhisana/schemas/common.py,sha256=WErBbyZAQtkmSOXrMkamJkMptj3uCY1S2CGHJWkB_Ps,9103
8
+ dhisana/schemas/common.py,sha256=SI6nldyfvZhNhfJwy2Qo1VJ7xwuAlg4QHYGLWPoVvZo,9287
9
9
  dhisana/schemas/sales.py,sha256=KUicIFC8uy4WLJegD6xIdOCUb63yGoqlDq-lALIrJKU,33395
10
10
  dhisana/ui/__init__.py,sha256=jv2YF__bseklT3OWEzlqJ5qE24c4aWd5F4r0TTjOrWQ,65
11
11
  dhisana/ui/components.py,sha256=4NXrAyl9tx2wWwoVYyABO-EOGnreGMvql1AkXWajIIo,14316
@@ -24,62 +24,63 @@ dhisana/utils/clay_tools.py,sha256=Pi3UMcxSDz2pd2Xq_tn9G8Zot5DUQM_cYNgWI2478EM,1
24
24
  dhisana/utils/clean_properties.py,sha256=D3GHL9grpe6baV7JyS9ZdHboESk1-ddcfLXHOLoFRws,4372
25
25
  dhisana/utils/company_utils.py,sha256=6icf3MuCzBp7iSoiq6LkQHg8A9hOqKuFyUwv0og1vBg,2038
26
26
  dhisana/utils/compose_salesnav_query.py,sha256=dPf23JEr_9i7E91EU6h4jd2IGHf4Rhma5WgGdlAockY,16486
27
- dhisana/utils/compose_search_query.py,sha256=QOBM1rUmBqKHsZIoyrewpgtl3k97SpkkWvYFcEWVgHs,29435
27
+ dhisana/utils/compose_search_query.py,sha256=bqx7PyIgubRn_qd3pxgquQrofkqIyuEclzuwFNgo5GE,29435
28
28
  dhisana/utils/compose_three_step_workflow.py,sha256=-I-8Mr6k_fjRM5qciziwZ5L_mBYjW81vEsgHeahy-Sk,10512
29
29
  dhisana/utils/composite_tools.py,sha256=ZlwHCp7PXjYFUWUEeR_fTF0Z4Wg-4F6eBi1reE3FfUA,6092
30
- dhisana/utils/dataframe_tools.py,sha256=jxyvyXAMKxccST_W6o6FnBqAsvp7mGNOD6HV5V6xgeA,9242
30
+ dhisana/utils/dataframe_tools.py,sha256=R6eUXjwR5SG6_K87rWjj4T5PT2w6xvVF2EKBajIv-RE,9242
31
31
  dhisana/utils/domain_parser.py,sha256=Kw5MPP06wK2azWQzuSiOE-DffOezLqDyF-L9JEBsMSU,1206
32
+ dhisana/utils/email_body_utils.py,sha256=rlCVjdBlqNnEiUberJGXGcrYY1GQOkW0-aB6AEpS3L4,2302
32
33
  dhisana/utils/email_parse_helpers.py,sha256=LIdm1B1IyGSW50y8EkxOk6YRjvxO2SJTgTKPLxYls_o,4613
33
34
  dhisana/utils/email_provider.py,sha256=spjbNdnaVfCZEUw62EEHKijuXjI7vTVNqsftxJ15Erw,14352
34
- dhisana/utils/enrich_lead_information.py,sha256=DzhaAO5scOcQ95oSgIyxkYRbz96gbXoASjsgrAalOo8,38730
35
+ dhisana/utils/enrich_lead_information.py,sha256=xJXxhLqQcR2wukM9MDBJ80BAoE-SH0cwTpDWOEHx-gs,38730
35
36
  dhisana/utils/extract_email_content_for_llm.py,sha256=SQmMZ3YJtm3ZI44XiWEVAItcAwrsSSy1QzDne7LTu_Q,3713
36
37
  dhisana/utils/fetch_openai_config.py,sha256=LjWdFuUeTNeAW106pb7DLXZNElos2PlmXRe6bHZJ2hw,5159
37
38
  dhisana/utils/field_validators.py,sha256=BZgNCpBG264aRqNUu_J67c6zfr15zlAaIw2XRy8J7DY,11809
38
39
  dhisana/utils/g2_tools.py,sha256=a4vmBYCBvLae5CdpOhMN1oNlvO8v9J1B5Sd8T5PzuU8,3346
39
40
  dhisana/utils/generate_content.py,sha256=1ycqHhxuxXnsX_5CNp5M6zW40VsM6toYMCwJU9jf__4,2111
40
- dhisana/utils/generate_email.py,sha256=N3XlswJV0b8v_eoCKpVW2UY345BsNzao1OKLOPLYp8Q,10732
41
- dhisana/utils/generate_email_response.py,sha256=YXa9gjvYN2UxOWyfsl1Lop5Q8LEHn1SMTNBmxhh6VH8,21142
41
+ dhisana/utils/generate_email.py,sha256=CNRL5wjFHnsi3EUpqwIhMtbS2g0W-loM-1dBd7fI8AM,10742
42
+ dhisana/utils/generate_email_response.py,sha256=4ikjGMc0NIKETWMxS-bHxCrB7H1AdTvDgXda0VpFwww,21162
42
43
  dhisana/utils/generate_flow.py,sha256=QMn6bWo0nH0fBvy2Ebub1XfH5udnVAqsPsbIqCtQPXU,4728
43
44
  dhisana/utils/generate_leads_salesnav.py,sha256=AONP1KXDJdg95JQBmKx5PQXUD2BHctc-MZOMuRfuK9U,12156
44
- dhisana/utils/generate_linkedin_connect_message.py,sha256=eL4RV2B1ByyMnjoGohdTA066rjFRaMKOU2hBfFMLsrE,9821
45
- dhisana/utils/generate_linkedin_response_message.py,sha256=udAt4V_vNuieyyfhrtTFWA1CiBx63jfac9E35wunS5k,14404
46
- dhisana/utils/generate_structured_output_internal.py,sha256=83SaThDAa_fANJEZ5CSCMcPpD_MN5zMI9NU1uEtQO2E,20705
45
+ dhisana/utils/generate_linkedin_connect_message.py,sha256=xOe41CgYWJNyZAehk_IuLudKbCxVtYZAexlmAMd2vmI,9831
46
+ dhisana/utils/generate_linkedin_response_message.py,sha256=W_nwMjmUT8Sk2pilR7HmPhrRtooj12Zw4-FbU8vwirw,14414
47
+ dhisana/utils/generate_structured_output_internal.py,sha256=DmZ5QzW-79Jo3JL5nDCZQ-Fjl8Nz7FHK6S0rZxXbKyg,20705
47
48
  dhisana/utils/google_custom_search.py,sha256=5rQ4uAF-hjFpd9ooJkd6CjRvSmhZHhqM0jfHItsbpzk,10071
48
- dhisana/utils/google_oauth_tools.py,sha256=cYynzqRgv79aPMK8DehrXUyAPReO3hyDJiJ8-1008XM,25232
49
- dhisana/utils/google_workspace_tools.py,sha256=UVxEicpPsm_DfadXygp6ELzXuVfFv9cVSaQrQwFoxqM,45898
49
+ dhisana/utils/google_oauth_tools.py,sha256=pN5YGkM50OieCFpz9RlmEwfrnGzkh342e0h5XschuuE,26211
50
+ dhisana/utils/google_workspace_tools.py,sha256=wyBy5WN3-eUCrKz1HYr_CS0vdsiQgOA-SFb368jSDrY,46957
50
51
  dhisana/utils/hubspot_clearbit.py,sha256=keNX1F_RnDl9AOPxYEOTMdukV_A9g8v9j1fZyT4tuP4,3440
51
52
  dhisana/utils/hubspot_crm_tools.py,sha256=lbXFCeq690_TDLjDG8Gm5E-2f1P5EuDqNf5j8PYpMm8,99298
52
53
  dhisana/utils/instantly_tools.py,sha256=hhqjDPyLE6o0dzzuvryszbK3ipnoGU2eBm6NlsUGJjY,4771
53
54
  dhisana/utils/linkedin_crawler.py,sha256=6fMQTY5lTw2kc65SFHgOAM6YfezAS0Yhg-jkiX8LGHo,6533
54
55
  dhisana/utils/lusha_tools.py,sha256=MdiWlxBBjSNpSKz8rhNOyLPtbeh-YWHgGiUq54vN_gM,12734
55
- dhisana/utils/mailgun_tools.py,sha256=AOnU0B_wFRoCtF-QmkLR97eZEAR3IvmO-cesjsjuQ0g,4900
56
- dhisana/utils/microsoft365_tools.py,sha256=W5kAkLE_94fvU1v8x16XgwckglW05QJvPcBpBsO-QTk,16387
57
- dhisana/utils/openai_assistant_and_file_utils.py,sha256=qg3MEKN6wfpFZmkyIZUpWPPKXBUh8YUW7gahGFr1LoE,8957
58
- dhisana/utils/openai_helpers.py,sha256=NkTqbdql31GmwTcRd90KsSHpEoAFGpiimchjVTG5Rdc,40155
56
+ dhisana/utils/mailgun_tools.py,sha256=qUD-jFMZpmkkkKtyihVSe9tgFzYe-UiiBDHQKtsLq0M,5284
57
+ dhisana/utils/microsoft365_tools.py,sha256=AwWSdE-xeHkCx9T_cVgDbzBmsz7Co4KE45rAmt_lnAc,16723
58
+ dhisana/utils/openai_assistant_and_file_utils.py,sha256=-eyPcxFvtS-DDtYQGle1SU6C6CuxjulVIojFy27HeWc,8957
59
+ dhisana/utils/openai_helpers.py,sha256=ZK9S5-jcLCpiiD6XBLkCqYcNz-AGYmO9xh4e2H-FDLo,40155
59
60
  dhisana/utils/openapi_spec_to_tools.py,sha256=oBLVq3WeDWvW9O02NCvY8bxQURQdKwHJHGcX8bC_b2I,1926
60
61
  dhisana/utils/parse_linkedin_messages_txt.py,sha256=g3N_ac70mAEuDDQ7Ott6mkOaBwI3ZvcsJD3R9RlYwPQ,3320
61
62
  dhisana/utils/profile.py,sha256=12IhefaLp3j74zzBzVRe50_KWqtWZ_cdzUKlYNy9T2Y,1192
62
63
  dhisana/utils/proxy_curl_tools.py,sha256=V54zXOP3K2EWGSYanvw43n45hP_4KG8kw2n_LBiL0ak,49971
63
64
  dhisana/utils/proxycurl_search_leads.py,sha256=6PlraPNYQ4fIDzTYnY-T2g_ip5fPkqHigbGoPD8ZosQ,16131
64
65
  dhisana/utils/python_function_to_tools.py,sha256=jypddM6WTlIQmRWnqAugYJXvaPYaXaMgWAZRYeeGlj4,2682
65
- dhisana/utils/research_lead.py,sha256=i7xk3edNzYKeJ_-JzKXwGL-NeeApZuWpx4vd4Uvguw4,7009
66
+ dhisana/utils/research_lead.py,sha256=L6w2fK5in8z2xmWnXBjbkvTdrwPf8ZfvAXq3gb7-S6s,7009
66
67
  dhisana/utils/sales_navigator_crawler.py,sha256=z8yurwUTLXdM71xWPDSAFNuDyA_SlanTpTncayG8YP8,44670
67
68
  dhisana/utils/salesforce_crm_tools.py,sha256=r6tROej4PtfcRN2AViPD7tV24oxBNm6QCE7uwhDH5Hc,17169
68
69
  dhisana/utils/search_router.py,sha256=p_1MPHbjalBM8gZuU4LADbmqSLNtZ4zll6CbPOc0POU,4610
69
70
  dhisana/utils/search_router_jobs.py,sha256=LgCHNGLMSv-ovgzF32muprfaDTdTpIKgrP5F7swAqhk,1721
70
- dhisana/utils/sendgrid_tools.py,sha256=uBaGuDFX1ef6WBNBBJTs_znw6T0nVYLaVqBOgc0n0I8,4761
71
+ dhisana/utils/sendgrid_tools.py,sha256=Q3Mr7mF0iM_zoF-Vhb1lH5r8AyqkVGgYXaCMR2i7zLQ,5157
71
72
  dhisana/utils/serarch_router_local_business.py,sha256=n9yZjeXKOSgBnr0lCSQomP1nN3ucbC9ZTTSmSHQLeVo,2920
72
73
  dhisana/utils/serpapi_additional_tools.py,sha256=Xb1tc_oK-IjI9ZrEruYhFg8UJMLHQDaO9B51YiNbeBs,10569
73
74
  dhisana/utils/serpapi_google_jobs.py,sha256=HUJFZEW8UvYqsW0sWlEDXgI_IUomh5fTkzRJzEgsDGc,4509
74
75
  dhisana/utils/serpapi_google_search.py,sha256=B3sVq2OXdrYmPbH7cjQN4RFoek96qgKzXIayKXn0HLU,7318
75
76
  dhisana/utils/serpapi_local_business_search.py,sha256=vinmuXLaQ_0BpEdwnONZ2vLTq5xnRh6ICmPbnpckSN4,5775
76
- dhisana/utils/serpapi_search_tools.py,sha256=MglMPN9wHkGAHb7YAQXvEDsWK3RGS-zu3wUIvZAYxfo,31738
77
+ dhisana/utils/serpapi_search_tools.py,sha256=_lPnrGyqO6iYGtB714_FBKw6_2XSDQWz2TZnZBOfSCk,31738
77
78
  dhisana/utils/serperdev_google_jobs.py,sha256=m5_2f_5y79FOFZz1A_go6m0hIUfbbAoZ0YTjUMO2BSI,4508
78
79
  dhisana/utils/serperdev_local_business.py,sha256=JoZfTg58Hojv61cyuwA2lcnPdLT1lawnWaBNrUYWnuQ,6447
79
80
  dhisana/utils/serperdev_search.py,sha256=_iBKIfHMq4gFv5StYz58eArriygoi1zW6VnLlux8vto,9363
80
- dhisana/utils/smtp_email_tools.py,sha256=yWg5BmQgRfnHiID7waHkq2sCNuCFBHe0-uVFgWtlr7c,17035
81
- dhisana/utils/test_connect.py,sha256=wkswcwEyMSVhZACHtqsZDhP6QpDMW4wq5MTcfRokvOM,69021
82
- dhisana/utils/trasform_json.py,sha256=s48DoyzVVCI4dTvSUwF5X-exX6VH6nPWrjbUENkYuNE,6979
81
+ dhisana/utils/smtp_email_tools.py,sha256=tF6GoNqkS9pWP52VTTrYSgL7wPdIp3XTklxrHLdzU5o,17186
82
+ dhisana/utils/test_connect.py,sha256=fTJW2UzC4zbzFKk_CvJJk-hRmMXYb4XJmyGa_ja-pbA,69022
83
+ dhisana/utils/trasform_json.py,sha256=7V72XNDpuxUX0GHN5D83z4anj_gIf5zabaHeQm7b1_E,6979
83
84
  dhisana/utils/web_download_parse_tools.py,sha256=ouXwH7CmjcRjoBfP5BWat86MvcGO-8rLCmWQe_eZKjc,7810
84
85
  dhisana/utils/workflow_code_model.py,sha256=YPWse5vBb3O6Km2PvKh1Q3AB8qBkzLt1CrR5xOL9Mro,99
85
86
  dhisana/utils/zoominfo_tools.py,sha256=8RwOuhnfABWvrJAjDHvC32JsG1vgVfgckpRuoU6BvEU,13409
@@ -91,9 +92,9 @@ dhisana/workflow/__init__.py,sha256=jv2YF__bseklT3OWEzlqJ5qE24c4aWd5F4r0TTjOrWQ,
91
92
  dhisana/workflow/agent.py,sha256=esv7_i_XuMkV2j1nz_UlsHov_m6X5WZZiZm_tG4OBHU,565
92
93
  dhisana/workflow/flow.py,sha256=xWE3qQbM7j2B3FH8XnY3zOL_QXX4LbTW4ArndnEYJE0,1638
93
94
  dhisana/workflow/task.py,sha256=HlWz9mtrwLYByoSnePOemBUBrMEcj7KbgNjEE1oF5wo,1830
94
- dhisana/workflow/test.py,sha256=kwW8jWqSBNcRmoyaxlTuZCMOpGJpTbJQgHI7gSjwdzM,3399
95
- dhisana-0.0.1.dev227.dist-info/METADATA,sha256=xJCNdyGsigROGAFmkncnMU5ogDb2sScbvmOmCfTh1SQ,1190
96
- dhisana-0.0.1.dev227.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
97
- dhisana-0.0.1.dev227.dist-info/entry_points.txt,sha256=jujxteZmNI9EkEaK-pOCoWuBujU8TCevdkfl9ZcKHek,49
98
- dhisana-0.0.1.dev227.dist-info/top_level.txt,sha256=NETTHt6YifG_P7XtRHbQiXZlgSFk9Qh9aR-ng1XTf4s,8
99
- dhisana-0.0.1.dev227.dist-info/RECORD,,
95
+ dhisana/workflow/test.py,sha256=E7lRnXK0PguTNzyasHytLzTJdkqIPxG5_4qk4hMEeKc,3399
96
+ dhisana-0.0.1.dev229.dist-info/METADATA,sha256=Op2HWN1M1FV1xx_flhumbWWujZg84_paPU2ax6Ft2Z0,1190
97
+ dhisana-0.0.1.dev229.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
98
+ dhisana-0.0.1.dev229.dist-info/entry_points.txt,sha256=jujxteZmNI9EkEaK-pOCoWuBujU8TCevdkfl9ZcKHek,49
99
+ dhisana-0.0.1.dev229.dist-info/top_level.txt,sha256=NETTHt6YifG_P7XtRHbQiXZlgSFk9Qh9aR-ng1XTf4s,8
100
+ dhisana-0.0.1.dev229.dist-info/RECORD,,