dhisana 0.0.1.dev265__py3-none-any.whl → 0.0.1.dev267__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.
@@ -124,9 +124,12 @@ async def send_email_async(
124
124
 
125
125
  Returns whatever the underlying provider helper returns:
126
126
 
127
+ * Mailgun → str (message-id from Mailgun)
128
+ * SendGrid → str (X-Message-Id from SendGrid)
127
129
  * SMTP → str (Message-ID)
128
130
  * Microsoft 365 → str (message-id)
129
131
  * Google Workspace → str (message-id)
132
+ * Google OAuth → str (message-id)
130
133
  """
131
134
  # ------------------------------------------------------------------ #
132
135
  # 1) Try the preferred providers in order
@@ -19,6 +19,126 @@ from dhisana.utils.fetch_openai_config import (
19
19
  create_async_openai_client,
20
20
  )
21
21
 
22
+ # Import search and scrape utilities for web search tools
23
+ try:
24
+ from dhisana.utils.search_router import search_google_with_tools
25
+ except Exception:
26
+ async def search_google_with_tools(*a, **k):
27
+ return []
28
+
29
+ try:
30
+ from dhisana.utils.web_download_parse_tools import get_text_content_from_url
31
+ except Exception:
32
+ async def get_text_content_from_url(url: str) -> str:
33
+ return ""
34
+
35
+
36
+ # ──────────────────────────────────────────────────────────────────────────────
37
+ # Web search tool definitions for the Responses API
38
+ # ──────────────────────────────────────────────────────────────────────────────
39
+
40
+ SEARCH_GOOGLE_TOOL = {
41
+ "type": "function",
42
+ "name": "search_google",
43
+ "description": "Search Google for information. Returns a list of search results with titles, links, and snippets.",
44
+ "parameters": {
45
+ "type": "object",
46
+ "properties": {
47
+ "query": {
48
+ "type": "string",
49
+ "description": "The search query to look up on Google"
50
+ },
51
+ "num_results": {
52
+ "type": "integer",
53
+ "description": "Number of results to return (default: 5, max: 10)"
54
+ }
55
+ },
56
+ "required": ["query"],
57
+ "additionalProperties": False
58
+ }
59
+ }
60
+
61
+ FETCH_URL_CONTENT_TOOL = {
62
+ "type": "function",
63
+ "name": "fetch_url_content",
64
+ "description": "Fetch and extract text content from a URL. Use this to read the full content of a webpage.",
65
+ "parameters": {
66
+ "type": "object",
67
+ "properties": {
68
+ "url": {
69
+ "type": "string",
70
+ "description": "The URL to fetch content from"
71
+ }
72
+ },
73
+ "required": ["url"],
74
+ "additionalProperties": False
75
+ }
76
+ }
77
+
78
+
79
+ async def _execute_search_google(
80
+ query: str, num_results: int, tool_config: Optional[List[Dict]]
81
+ ) -> str:
82
+ """Execute Google search and return results as JSON string."""
83
+ try:
84
+ num_results = min(max(num_results, 1), 10)
85
+ raw = await search_google_with_tools(
86
+ query, number_of_results=num_results, offset=0, tool_config=tool_config
87
+ )
88
+ results = []
89
+ if isinstance(raw, list):
90
+ for item in raw:
91
+ try:
92
+ data = json.loads(item) if isinstance(item, str) else item
93
+ results.append({
94
+ "title": data.get("title", ""),
95
+ "link": data.get("link", ""),
96
+ "snippet": data.get("snippet", "")
97
+ })
98
+ except Exception:
99
+ continue
100
+ return json.dumps(results, default=str)
101
+ except Exception as e:
102
+ logging.warning("search_google tool failed: %s", e)
103
+ return json.dumps({"error": str(e)})
104
+
105
+
106
+ async def _execute_fetch_url_content(url: str) -> str:
107
+ """Fetch URL content and return as string."""
108
+ try:
109
+ content = await get_text_content_from_url(url)
110
+ if content:
111
+ max_len = 15000
112
+ if len(content) > max_len:
113
+ content = content[:max_len] + "\n... [content truncated]"
114
+ return content
115
+ return "Failed to fetch content from URL"
116
+ except Exception as e:
117
+ logging.warning("fetch_url_content tool failed for %s: %s", url, e)
118
+ return f"Error fetching URL: {str(e)}"
119
+
120
+
121
+ async def _execute_web_search_tool(
122
+ tool_name: str, args: dict, tool_config: Optional[List[Dict]]
123
+ ) -> str:
124
+ """Execute a web search tool and return the result as a string."""
125
+ if tool_name == "search_google":
126
+ query = args.get("query", "")
127
+ num_results = args.get("num_results", 5)
128
+ if not query:
129
+ return json.dumps({"error": "Missing required parameter: query"})
130
+ return await _execute_search_google(query, num_results, tool_config)
131
+
132
+ elif tool_name == "fetch_url_content":
133
+ url = args.get("url", "")
134
+ if not url:
135
+ return json.dumps({"error": "Missing required parameter: url"})
136
+ return await _execute_fetch_url_content(url)
137
+
138
+ else:
139
+ logging.warning(f"Unknown tool requested: {tool_name}")
140
+ return json.dumps({"error": f"Unknown tool: {tool_name}"})
141
+
22
142
 
23
143
  # ──────────────────────────────────────────────────────────────────────────────
24
144
  # 1. Helper functions
@@ -71,6 +191,9 @@ async def get_structured_output_internal(
71
191
 
72
192
  On a 429 (rate-limit) error the call is retried once after
73
193
  20 s + random exponential back-off.
194
+
195
+ If use_web_search=True, uses Google search and URL scraping tools
196
+ to enable web research (works with both OpenAI and Azure OpenAI).
74
197
  """
75
198
  try:
76
199
  # ─── caching bookkeeping ────────────────────────────────────────────
@@ -95,30 +218,24 @@ async def get_structured_output_internal(
95
218
  "schema": schema["json_schema"]["schema"],
96
219
  }
97
220
 
98
- # ─── client initialisation (NEW) ────────────────────────────────────
221
+ # ─── client initialisation ──────────────────────────────────────────
99
222
  client_async = create_async_openai_client(tool_config)
100
223
 
101
- openai_cfg = _extract_config(tool_config, "openai")
102
- # TODO: Azure OpenAI does not support web_search yet
103
- if not openai_cfg:
104
- use_web_search = False
224
+ # ─── Web search path (uses Google search + URL scraping tools) ──────
225
+ if use_web_search:
226
+ return await _get_structured_output_with_web_search(
227
+ client_async=client_async,
228
+ prompt=prompt,
229
+ response_format=response_format,
230
+ json_schema_format=json_schema_format,
231
+ model=model,
232
+ effort=effort,
233
+ tool_config=tool_config,
234
+ cache_key=cache_key,
235
+ )
105
236
 
106
- # -------------------------------------------------------------------
107
- # Internal helper to perform ONE attempt
108
- # -------------------------------------------------------------------
237
+ # ─── Standard path (no web search) ──────────────────────────────────
109
238
  async def _make_request():
110
- if use_web_search and model.startswith("gpt-"):
111
- return await client_async.responses.create(
112
- input=[
113
- {"role": "system", "content": "You are a helpful AI. Output JSON only."},
114
- {"role": "user", "content": prompt},
115
- ],
116
- model=model,
117
- text={"format": json_schema_format},
118
- tool_choice="required",
119
- tools=[{"type": "web_search_preview"}],
120
- store=False,
121
- )
122
239
  if model.startswith("o"): # reasoning param only for "o" family
123
240
  return await client_async.responses.create(
124
241
  input=[
@@ -175,45 +292,8 @@ async def get_structured_output_internal(
175
292
  logging.error(f"OpenAI API error: {e}")
176
293
  return f"OpenAI API error: {str(e)}", "API_ERROR"
177
294
 
178
- # ─── handle model output (unchanged) ────────────────────────────────
179
- if completion and completion.output and len(completion.output) > 0:
180
- raw_text = None
181
- for out in completion.output:
182
- if out.type == "message" and out.content:
183
- for content_item in out.content:
184
- if hasattr(content_item, "text"):
185
- raw_text = content_item.text
186
- break
187
- else:
188
- logging.warning("request refused: %s", str(content_item))
189
- return "Request refused.", "FAIL"
190
- if raw_text:
191
- break
192
-
193
- if not raw_text or not raw_text.strip():
194
- return "No text returned (possibly refusal or empty response)", "FAIL"
195
-
196
- try:
197
- parsed_obj = response_format.parse_raw(raw_text)
198
- cache_output_tools.cache_output(
199
- "get_structured_output_internal", cache_key, parsed_obj.json()
200
- )
201
- return parsed_obj, "SUCCESS"
202
-
203
- except Exception:
204
- logging.warning("ERROR: Could not parse JSON from model output.")
205
- try:
206
- fixed_json = repair_json(raw_text)
207
- parsed_obj = response_format.parse_raw(fixed_json)
208
- cache_output_tools.cache_output(
209
- "get_structured_output_internal", cache_key, parsed_obj.json()
210
- )
211
- return parsed_obj, "SUCCESS"
212
- except Exception as e2:
213
- logging.warning("JSON repair failed: %s", str(e2))
214
- return raw_text, "FAIL"
215
- else:
216
- return "No output returned", "FAIL"
295
+ # ─── handle model output ────────────────────────────────────────────
296
+ return _parse_completion_response(completion, response_format, cache_key)
217
297
 
218
298
  # Safety fallback: catch any OpenAI errors not caught by inner retry loop
219
299
  except OpenAIError as e:
@@ -226,6 +306,173 @@ async def get_structured_output_internal(
226
306
  return f"Unexpected error: {str(e)}", "ERROR"
227
307
 
228
308
 
309
+ async def _get_structured_output_with_web_search(
310
+ client_async,
311
+ prompt: str,
312
+ response_format: BaseModel,
313
+ json_schema_format: Dict,
314
+ model: str,
315
+ effort: str,
316
+ tool_config: Optional[List[Dict]],
317
+ cache_key: str,
318
+ ):
319
+ """
320
+ Handles structured output with web search using Google search and URL scraping tools.
321
+ Works with both OpenAI and Azure OpenAI.
322
+ """
323
+ tools = [SEARCH_GOOGLE_TOOL, FETCH_URL_CONTENT_TOOL]
324
+
325
+ system_content = (
326
+ "You are a helpful AI. Output JSON only.\n\n"
327
+ "Web Search Instructions:\n"
328
+ "- Use search_google to find relevant information on the web.\n"
329
+ "- Use fetch_url_content to read the full content of relevant URLs.\n"
330
+ "- After gathering information, provide your response in the required JSON format."
331
+ )
332
+
333
+ input_messages = [
334
+ {"role": "system", "content": system_content},
335
+ {"role": "user", "content": prompt},
336
+ ]
337
+
338
+ base_request = {
339
+ "input": input_messages,
340
+ "model": model,
341
+ "text": {"format": json_schema_format},
342
+ "tools": tools,
343
+ "store": False,
344
+ }
345
+
346
+ if model.startswith("o"):
347
+ base_request["reasoning"] = {"effort": effort}
348
+
349
+ max_tool_iterations = 10
350
+ tool_iteration = 0
351
+ completion = None
352
+
353
+ while tool_iteration < max_tool_iterations:
354
+ tool_iteration += 1
355
+
356
+ # Retry logic for rate limits
357
+ for attempt in range(2):
358
+ try:
359
+ completion = await client_async.responses.create(**base_request)
360
+ break
361
+ except (RateLimitError, OpenAIError) as e:
362
+ is_rl = (
363
+ isinstance(e, RateLimitError)
364
+ or getattr(e, "status_code", None) == 429
365
+ or "rate_limit" in str(e).lower()
366
+ )
367
+ if attempt == 0 and is_rl:
368
+ wait_time = 20 + random.uniform(0, 2.0)
369
+ logging.warning(f"Rate-limit hit (429). Waiting {wait_time:.2f}s then retrying.")
370
+ await asyncio.sleep(wait_time)
371
+ continue
372
+ logging.error(f"OpenAI API error: {e}")
373
+ raise HTTPException(status_code=502, detail="Error communicating with the OpenAI API.")
374
+
375
+ if not completion:
376
+ raise HTTPException(status_code=502, detail="OpenAI request failed.")
377
+
378
+ # Check for function tool calls in the response
379
+ tool_calls = []
380
+ for item in (completion.output or []):
381
+ item_type = getattr(item, "type", None)
382
+ if item_type == "function_call":
383
+ tool_calls.append(item)
384
+
385
+ if not tool_calls:
386
+ # No tool calls, we have the final response
387
+ break
388
+
389
+ # Execute tool calls and prepare for next iteration
390
+ logging.info(f"Processing {len(tool_calls)} web search tool call(s) in iteration {tool_iteration}")
391
+
392
+ # Use previous_response_id for conversation threading
393
+ if hasattr(completion, "id") and completion.id:
394
+ base_request = {
395
+ "model": model,
396
+ "text": {"format": json_schema_format},
397
+ "tools": tools,
398
+ "store": False,
399
+ "previous_response_id": completion.id,
400
+ "input": [],
401
+ }
402
+ if model.startswith("o"):
403
+ base_request["reasoning"] = {"effort": effort}
404
+
405
+ for tc in tool_calls:
406
+ func_name = getattr(tc, "name", "")
407
+ call_id = getattr(tc, "call_id", "")
408
+ args_str = getattr(tc, "arguments", "{}")
409
+
410
+ try:
411
+ args = json.loads(args_str) if args_str else {}
412
+ except json.JSONDecodeError:
413
+ args = {}
414
+
415
+ # Execute the tool
416
+ tool_result = await _execute_web_search_tool(func_name, args, tool_config)
417
+
418
+ # Add tool result to input
419
+ base_request["input"].append({
420
+ "type": "function_call_output",
421
+ "call_id": call_id,
422
+ "output": tool_result
423
+ })
424
+
425
+ logging.info(f"Executed web search tool {func_name}, result length: {len(tool_result)}")
426
+ else:
427
+ logging.warning("No response ID available, breaking tool call loop")
428
+ break
429
+
430
+ # Parse and return the final response
431
+ return _parse_completion_response(completion, response_format, cache_key)
432
+
433
+
434
+ def _parse_completion_response(completion, response_format: BaseModel, cache_key: str):
435
+ """Parse completion response and return structured output."""
436
+ if completion and completion.output and len(completion.output) > 0:
437
+ raw_text = None
438
+ for out in completion.output:
439
+ if out.type == "message" and out.content:
440
+ for content_item in out.content:
441
+ if hasattr(content_item, "text"):
442
+ raw_text = content_item.text
443
+ break
444
+ else:
445
+ logging.warning("request refused: %s", str(content_item))
446
+ return "Request refused.", "FAIL"
447
+ if raw_text:
448
+ break
449
+
450
+ if not raw_text or not raw_text.strip():
451
+ return "No text returned (possibly refusal or empty response)", "FAIL"
452
+
453
+ try:
454
+ parsed_obj = response_format.parse_raw(raw_text)
455
+ cache_output_tools.cache_output(
456
+ "get_structured_output_internal", cache_key, parsed_obj.json()
457
+ )
458
+ return parsed_obj, "SUCCESS"
459
+
460
+ except Exception:
461
+ logging.warning("ERROR: Could not parse JSON from model output.")
462
+ try:
463
+ fixed_json = repair_json(raw_text)
464
+ parsed_obj = response_format.parse_raw(fixed_json)
465
+ cache_output_tools.cache_output(
466
+ "get_structured_output_internal", cache_key, parsed_obj.json()
467
+ )
468
+ return parsed_obj, "SUCCESS"
469
+ except Exception as e2:
470
+ logging.warning("JSON repair failed: %s", str(e2))
471
+ return raw_text, "FAIL"
472
+ else:
473
+ return "No output returned", "FAIL"
474
+
475
+
229
476
 
230
477
  async def get_structured_output_with_mcp(
231
478
  prompt: str,
@@ -1,4 +1,3 @@
1
- import asyncio
2
1
  import logging
3
2
  import os
4
3
  from typing import Optional, List, Dict
@@ -21,7 +20,7 @@ def get_mailgun_notify_key(tool_config: Optional[List[Dict]] = None) -> str:
21
20
  if tool_config:
22
21
  cfg = next((item for item in tool_config if item.get("name") == "mailgun"), None)
23
22
  if cfg:
24
- cfg_map = {i["name"]: i["value"] for i in cfg.get("configuration", []) if i}
23
+ cfg_map = {i.get("name"): i.get("value") for i in cfg.get("configuration", []) if i}
25
24
  key = cfg_map.get("apiKey")
26
25
  key = key or os.getenv("MAILGUN_NOTIFY_KEY")
27
26
  if not key:
@@ -43,7 +42,7 @@ def get_mailgun_notify_domain(tool_config: Optional[List[Dict]] = None) -> str:
43
42
  if tool_config:
44
43
  cfg = next((item for item in tool_config if item.get("name") == "mailgun"), None)
45
44
  if cfg:
46
- cfg_map = {i["name"]: i["value"] for i in cfg.get("configuration", []) if i}
45
+ cfg_map = {i.get("name"): i.get("value") for i in cfg.get("configuration", []) if i}
47
46
  domain = cfg_map.get("domain") or cfg_map.get("notifyDomain")
48
47
  domain = domain or os.getenv("MAILGUN_DOMAIN") or os.getenv("MAILGUN_NOTIFY_DOMAIN")
49
48
  if not domain:
@@ -96,11 +95,15 @@ async def send_email_with_mailgun(
96
95
  # Try to return JSON payload if available
97
96
  try:
98
97
  return await response.json()
99
- except Exception:
98
+ except Exception as parse_ex:
99
+ logging.debug(f"Could not parse Mailgun response as JSON: {parse_ex}")
100
100
  return await response.text()
101
- except Exception as ex:
101
+ except (aiohttp.ClientError, ValueError) as ex:
102
102
  logging.warning(f"Error sending email via Mailgun: {ex}")
103
103
  return {"error": str(ex)}
104
+ except Exception as ex:
105
+ logging.exception(f"Unexpected error sending email via Mailgun: {ex}")
106
+ raise
104
107
 
105
108
 
106
109
  async def send_email_using_mailgun_async(
@@ -147,10 +150,10 @@ async def send_email_using_mailgun_async(
147
150
  raise RuntimeError(f"Mailgun send failed: {detail}")
148
151
  try:
149
152
  payload = await response.json()
150
- except Exception:
153
+ except Exception as parse_ex:
154
+ logging.debug(f"Could not parse Mailgun response as JSON: {parse_ex}")
151
155
  payload = {"message": await response.text()}
152
156
 
153
157
  # Normalise return value akin to other providers
154
158
  msg_id = payload.get("id") if isinstance(payload, dict) else None
155
- await asyncio.sleep(20)
156
159
  return msg_id or str(payload)
@@ -119,13 +119,17 @@ async def send_email_with_sendgrid(
119
119
  headers=headers,
120
120
  json=payload,
121
121
  ) as response:
122
- # SendGrid returns 202 Accepted on success with empty body
122
+ # SendGrid returns 202 Accepted on success with empty body but includes X-Message-Id header
123
123
  if response.status == 202:
124
- return {"status": 202, "message": "accepted"}
124
+ message_id = response.headers.get("X-Message-Id")
125
+ if not message_id:
126
+ logging.warning("SendGrid did not return X-Message-Id header")
127
+ return {"status": 202, "message": "accepted", "message_id": message_id}
125
128
  # On error, try to parse JSON for helpful message
126
129
  try:
127
130
  err = await response.json()
128
- except Exception:
131
+ except Exception as parse_ex:
132
+ logging.debug(f"Could not parse SendGrid error as JSON: {parse_ex}")
129
133
  err = {"text": await response.text()}
130
134
  return {"error": err, "status": response.status}
131
135
  except Exception as ex:
@@ -139,12 +143,8 @@ async def send_email_using_sendgrid_async(
139
143
  ) -> str:
140
144
  """
141
145
  Provider-style wrapper for SendGrid using SendEmailContext.
142
- Returns an opaque token since SendGrid does not return a message id.
146
+ Returns the message ID from SendGrid's X-Message-Id response header.
143
147
  """
144
- plain_body, html_body, _ = body_variants(
145
- ctx.body,
146
- getattr(ctx, "body_format", None),
147
- )
148
148
  result = await send_email_with_sendgrid(
149
149
  sender=f"{ctx.sender_name} <{ctx.sender_email}>",
150
150
  recipients=[ctx.recipient],
@@ -156,6 +156,10 @@ async def send_email_using_sendgrid_async(
156
156
  )
157
157
  # Normalise output to a string id-like value
158
158
  if isinstance(result, dict) and result.get("status") == 202:
159
+ message_id = result.get("message_id")
160
+ if message_id:
161
+ return message_id
162
+ # Fallback if header wasn't present (shouldn't happen)
159
163
  return f"sent:{ctx.sender_email}:{ctx.recipient}:{ctx.subject}"
160
164
  if isinstance(result, dict) and "error" in result:
161
165
  raise RuntimeError(f"SendGrid send failed: {result['error']}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dhisana
3
- Version: 0.0.1.dev265
3
+ Version: 0.0.1.dev267
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
@@ -31,7 +31,7 @@ dhisana/utils/dataframe_tools.py,sha256=R6eUXjwR5SG6_K87rWjj4T5PT2w6xvVF2EKBajIv
31
31
  dhisana/utils/domain_parser.py,sha256=Kw5MPP06wK2azWQzuSiOE-DffOezLqDyF-L9JEBsMSU,1206
32
32
  dhisana/utils/email_body_utils.py,sha256=rlCVjdBlqNnEiUberJGXGcrYY1GQOkW0-aB6AEpS3L4,2302
33
33
  dhisana/utils/email_parse_helpers.py,sha256=LIdm1B1IyGSW50y8EkxOk6YRjvxO2SJTgTKPLxYls_o,4613
34
- dhisana/utils/email_provider.py,sha256=spjbNdnaVfCZEUw62EEHKijuXjI7vTVNqsftxJ15Erw,14352
34
+ dhisana/utils/email_provider.py,sha256=ukW_0nHcjTQmpnE9pdJci78LrZcsK1_0v6kcgc2ChPY,14573
35
35
  dhisana/utils/enrich_lead_information.py,sha256=O0fV-8MlXFT_z5aXvmvXVT76AISN94GpvAOlq3q_Phw,39411
36
36
  dhisana/utils/extract_email_content_for_llm.py,sha256=SQmMZ3YJtm3ZI44XiWEVAItcAwrsSSy1QzDne7LTu_Q,3713
37
37
  dhisana/utils/fetch_openai_config.py,sha256=LjWdFuUeTNeAW106pb7DLXZNElos2PlmXRe6bHZJ2hw,5159
@@ -45,7 +45,7 @@ dhisana/utils/generate_flow.py,sha256=QMn6bWo0nH0fBvy2Ebub1XfH5udnVAqsPsbIqCtQPX
45
45
  dhisana/utils/generate_leads_salesnav.py,sha256=FG7q6GSm9IywZ9TgQnn5_N3QNfiI-Qk2gaO_3GS99nY,12236
46
46
  dhisana/utils/generate_linkedin_connect_message.py,sha256=QxsxDiT-3eQOqAAbW13d0HGJXV36WYPvC-7Zsw_2VTI,10208
47
47
  dhisana/utils/generate_linkedin_response_message.py,sha256=mWoSs5p2JSTIoFZFGm86x1kgs67J7dHPvGKZPzcdGdU,14569
48
- dhisana/utils/generate_structured_output_internal.py,sha256=k6w5zaaMigp7cUgFALr-TRBsoEQTlzyGfH4R8HIIfGU,22116
48
+ dhisana/utils/generate_structured_output_internal.py,sha256=N1C-LhczMiJhNaDECeFJIY1vc9ta6cwkLWKxvdK4v7M,31266
49
49
  dhisana/utils/google_custom_search.py,sha256=5rQ4uAF-hjFpd9ooJkd6CjRvSmhZHhqM0jfHItsbpzk,10071
50
50
  dhisana/utils/google_oauth_tools.py,sha256=ReG5lCpXL3_e_s0yn6ai4U7B4-feOWHJVtbv_c0g0rE,28525
51
51
  dhisana/utils/google_workspace_tools.py,sha256=fuV0UcvAqF9drLzj7-p6D5zh7d5jMXl1jNJTICk4XOo,50224
@@ -54,7 +54,7 @@ dhisana/utils/hubspot_crm_tools.py,sha256=lbXFCeq690_TDLjDG8Gm5E-2f1P5EuDqNf5j8P
54
54
  dhisana/utils/instantly_tools.py,sha256=hhqjDPyLE6o0dzzuvryszbK3ipnoGU2eBm6NlsUGJjY,4771
55
55
  dhisana/utils/linkedin_crawler.py,sha256=6fMQTY5lTw2kc65SFHgOAM6YfezAS0Yhg-jkiX8LGHo,6533
56
56
  dhisana/utils/lusha_tools.py,sha256=MdiWlxBBjSNpSKz8rhNOyLPtbeh-YWHgGiUq54vN_gM,12734
57
- dhisana/utils/mailgun_tools.py,sha256=OTesN8spYrvoH4Q5BheC8TPUvUlfbZhliYOhzCrD7Mg,5506
57
+ dhisana/utils/mailgun_tools.py,sha256=brOgfEx-ciqEdDkXEfzMBfXkG0kRWVscg76tQDXb_lk,5826
58
58
  dhisana/utils/mailreach_tools.py,sha256=uJ_gIcg8qrj5-k3jnYYhpwLVnQncoA1swzr5Jfkc1JU,3864
59
59
  dhisana/utils/microsoft365_tools.py,sha256=ClqBzTrJ2SZM5K9nsOFyyHRfV-d-6jlxXNpNONtgLlY,18596
60
60
  dhisana/utils/openai_assistant_and_file_utils.py,sha256=-eyPcxFvtS-DDtYQGle1SU6C6CuxjulVIojFy27HeWc,8957
@@ -70,7 +70,7 @@ dhisana/utils/sales_navigator_crawler.py,sha256=z8yurwUTLXdM71xWPDSAFNuDyA_SlanT
70
70
  dhisana/utils/salesforce_crm_tools.py,sha256=r6tROej4PtfcRN2AViPD7tV24oxBNm6QCE7uwhDH5Hc,17169
71
71
  dhisana/utils/search_router.py,sha256=p_1MPHbjalBM8gZuU4LADbmqSLNtZ4zll6CbPOc0POU,4610
72
72
  dhisana/utils/search_router_jobs.py,sha256=LgCHNGLMSv-ovgzF32muprfaDTdTpIKgrP5F7swAqhk,1721
73
- dhisana/utils/sendgrid_tools.py,sha256=VyiLbQfa0xYY-onWqhHRcNKL7z-Wev-t4lim1d-vDVw,5526
73
+ dhisana/utils/sendgrid_tools.py,sha256=0aafzCxcmCtKVt7kWYNTH_Np9KF0-RPqAZ9LsslMlqs,5931
74
74
  dhisana/utils/serarch_router_local_business.py,sha256=n9yZjeXKOSgBnr0lCSQomP1nN3ucbC9ZTTSmSHQLeVo,2920
75
75
  dhisana/utils/serpapi_additional_tools.py,sha256=Xb1tc_oK-IjI9ZrEruYhFg8UJMLHQDaO9B51YiNbeBs,10569
76
76
  dhisana/utils/serpapi_google_jobs.py,sha256=HUJFZEW8UvYqsW0sWlEDXgI_IUomh5fTkzRJzEgsDGc,4509
@@ -95,8 +95,8 @@ dhisana/workflow/agent.py,sha256=esv7_i_XuMkV2j1nz_UlsHov_m6X5WZZiZm_tG4OBHU,565
95
95
  dhisana/workflow/flow.py,sha256=xWE3qQbM7j2B3FH8XnY3zOL_QXX4LbTW4ArndnEYJE0,1638
96
96
  dhisana/workflow/task.py,sha256=HlWz9mtrwLYByoSnePOemBUBrMEcj7KbgNjEE1oF5wo,1830
97
97
  dhisana/workflow/test.py,sha256=E7lRnXK0PguTNzyasHytLzTJdkqIPxG5_4qk4hMEeKc,3399
98
- dhisana-0.0.1.dev265.dist-info/METADATA,sha256=S6o7HBV1UGxYP3ltCxcwGEYDhpQ5VsZ_sEUXvf_cIcY,1190
99
- dhisana-0.0.1.dev265.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
100
- dhisana-0.0.1.dev265.dist-info/entry_points.txt,sha256=jujxteZmNI9EkEaK-pOCoWuBujU8TCevdkfl9ZcKHek,49
101
- dhisana-0.0.1.dev265.dist-info/top_level.txt,sha256=NETTHt6YifG_P7XtRHbQiXZlgSFk9Qh9aR-ng1XTf4s,8
102
- dhisana-0.0.1.dev265.dist-info/RECORD,,
98
+ dhisana-0.0.1.dev267.dist-info/METADATA,sha256=zAT6piCTBLFYagzP8-Vuogwo1s1meUG3ch_nk8yLiwg,1190
99
+ dhisana-0.0.1.dev267.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
100
+ dhisana-0.0.1.dev267.dist-info/entry_points.txt,sha256=jujxteZmNI9EkEaK-pOCoWuBujU8TCevdkfl9ZcKHek,49
101
+ dhisana-0.0.1.dev267.dist-info/top_level.txt,sha256=NETTHt6YifG_P7XtRHbQiXZlgSFk9Qh9aR-ng1XTf4s,8
102
+ dhisana-0.0.1.dev267.dist-info/RECORD,,