dhisana 0.0.1.dev116__py3-none-any.whl → 0.0.1.dev236__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. dhisana/schemas/common.py +10 -1
  2. dhisana/schemas/sales.py +203 -22
  3. dhisana/utils/add_mapping.py +0 -2
  4. dhisana/utils/apollo_tools.py +739 -119
  5. dhisana/utils/built_with_api_tools.py +4 -2
  6. dhisana/utils/check_email_validity_tools.py +35 -18
  7. dhisana/utils/check_for_intent_signal.py +1 -2
  8. dhisana/utils/check_linkedin_url_validity.py +34 -8
  9. dhisana/utils/clay_tools.py +3 -2
  10. dhisana/utils/clean_properties.py +1 -4
  11. dhisana/utils/compose_salesnav_query.py +0 -1
  12. dhisana/utils/compose_search_query.py +7 -3
  13. dhisana/utils/composite_tools.py +0 -1
  14. dhisana/utils/dataframe_tools.py +2 -2
  15. dhisana/utils/email_body_utils.py +72 -0
  16. dhisana/utils/email_provider.py +174 -35
  17. dhisana/utils/enrich_lead_information.py +183 -53
  18. dhisana/utils/fetch_openai_config.py +129 -0
  19. dhisana/utils/field_validators.py +1 -1
  20. dhisana/utils/g2_tools.py +0 -1
  21. dhisana/utils/generate_content.py +0 -1
  22. dhisana/utils/generate_email.py +68 -23
  23. dhisana/utils/generate_email_response.py +294 -46
  24. dhisana/utils/generate_flow.py +0 -1
  25. dhisana/utils/generate_linkedin_connect_message.py +9 -2
  26. dhisana/utils/generate_linkedin_response_message.py +137 -66
  27. dhisana/utils/generate_structured_output_internal.py +317 -164
  28. dhisana/utils/google_custom_search.py +150 -44
  29. dhisana/utils/google_oauth_tools.py +721 -0
  30. dhisana/utils/google_workspace_tools.py +278 -54
  31. dhisana/utils/hubspot_clearbit.py +3 -1
  32. dhisana/utils/hubspot_crm_tools.py +718 -272
  33. dhisana/utils/instantly_tools.py +3 -1
  34. dhisana/utils/lusha_tools.py +10 -7
  35. dhisana/utils/mailgun_tools.py +150 -0
  36. dhisana/utils/microsoft365_tools.py +447 -0
  37. dhisana/utils/openai_assistant_and_file_utils.py +121 -177
  38. dhisana/utils/openai_helpers.py +8 -6
  39. dhisana/utils/parse_linkedin_messages_txt.py +1 -3
  40. dhisana/utils/profile.py +37 -0
  41. dhisana/utils/proxy_curl_tools.py +377 -76
  42. dhisana/utils/proxycurl_search_leads.py +426 -0
  43. dhisana/utils/research_lead.py +3 -3
  44. dhisana/utils/sales_navigator_crawler.py +1 -6
  45. dhisana/utils/salesforce_crm_tools.py +323 -50
  46. dhisana/utils/search_router.py +131 -0
  47. dhisana/utils/search_router_jobs.py +51 -0
  48. dhisana/utils/sendgrid_tools.py +126 -91
  49. dhisana/utils/serarch_router_local_business.py +75 -0
  50. dhisana/utils/serpapi_additional_tools.py +290 -0
  51. dhisana/utils/serpapi_google_jobs.py +117 -0
  52. dhisana/utils/serpapi_google_search.py +188 -0
  53. dhisana/utils/serpapi_local_business_search.py +129 -0
  54. dhisana/utils/serpapi_search_tools.py +360 -432
  55. dhisana/utils/serperdev_google_jobs.py +125 -0
  56. dhisana/utils/serperdev_local_business.py +154 -0
  57. dhisana/utils/serperdev_search.py +233 -0
  58. dhisana/utils/smtp_email_tools.py +178 -18
  59. dhisana/utils/test_connect.py +1603 -130
  60. dhisana/utils/trasform_json.py +3 -3
  61. dhisana/utils/web_download_parse_tools.py +0 -1
  62. dhisana/utils/zoominfo_tools.py +2 -3
  63. dhisana/workflow/test.py +1 -1
  64. {dhisana-0.0.1.dev116.dist-info → dhisana-0.0.1.dev236.dist-info}/METADATA +1 -1
  65. dhisana-0.0.1.dev236.dist-info/RECORD +100 -0
  66. {dhisana-0.0.1.dev116.dist-info → dhisana-0.0.1.dev236.dist-info}/WHEEL +1 -1
  67. dhisana-0.0.1.dev116.dist-info/RECORD +0 -83
  68. {dhisana-0.0.1.dev116.dist-info → dhisana-0.0.1.dev236.dist-info}/entry_points.txt +0 -0
  69. {dhisana-0.0.1.dev116.dist-info → dhisana-0.0.1.dev236.dist-info}/top_level.txt +0 -0
@@ -3,9 +3,128 @@
3
3
 
4
4
  import json
5
5
  import os
6
+ import requests
6
7
  from dhisana.utils.assistant_tool_tag import assistant_tool
7
8
  from simple_salesforce import Salesforce
8
9
  from urllib.parse import urljoin
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ # Mapping between our internal property names and Salesforce Contact fields
13
+ CONTACT_FIELD_MAPPING: Dict[str, str] = {
14
+ "first_name": "FirstName",
15
+ "last_name": "LastName",
16
+ "email": "Email",
17
+ "phone": "Phone",
18
+ "job_title": "Title",
19
+ "user_linkedin_url": "LinkedIn_URL__c",
20
+ }
21
+
22
+ # Mapping between our internal property names and Salesforce Account fields
23
+ COMPANY_FIELD_MAPPING: Dict[str, str] = {
24
+ "organization_name": "Name",
25
+ "primary_domain_of_organization": "Website",
26
+ "organization_website": "Website",
27
+ "phone": "Phone",
28
+ }
29
+
30
+
31
+ def _map_contact_to_internal(record: Dict[str, Any]) -> Dict[str, Any]:
32
+ """Convert a Salesforce contact to our internal schema."""
33
+ account = record.get("Account", {}) or {}
34
+ return {
35
+ "first_name": record.get("FirstName"),
36
+ "last_name": record.get("LastName"),
37
+ "email": record.get("Email"),
38
+ "phone": record.get("Phone"),
39
+ "job_title": record.get("Title"),
40
+ "user_linkedin_url": record.get("LinkedIn_URL__c"),
41
+ "organization_name": account.get("Name"),
42
+ "organization_website": account.get("Website"),
43
+ }
44
+
45
+
46
+ def _map_account_to_internal(record: Dict[str, Any]) -> Dict[str, Any]:
47
+ """Convert a Salesforce account to our internal schema."""
48
+ return {
49
+ "organization_name": record.get("Name"),
50
+ "organization_website": record.get("Website"),
51
+ "phone": record.get("Phone"),
52
+ "primary_domain_of_organization": record.get("Website"),
53
+ }
54
+
55
+
56
+ def _create_salesforce_client(tool_config: Optional[List[Dict]] = None) -> Salesforce:
57
+ """Create a Salesforce client from tool_config or environment variables.
58
+
59
+ Supports two authentication flows:
60
+ 1. Username/password/security token (simple_salesforce default).
61
+ 2. OAuth2 password grant using client_id/client_secret.
62
+ The second flow is preferred for production access.
63
+
64
+ Raises:
65
+ ValueError: If the Salesforce integration has not been configured.
66
+ """
67
+
68
+ username = password = security_token = domain = None
69
+ client_id = client_secret = None
70
+
71
+ if tool_config:
72
+ sf_conf = next((c for c in tool_config if c.get("name") == "salesforce"), None)
73
+ if sf_conf:
74
+ cfg_map = {
75
+ item.get("name"): item.get("value")
76
+ for item in sf_conf.get("configuration", [])
77
+ if item
78
+ }
79
+ username = cfg_map.get("username")
80
+ password = cfg_map.get("password")
81
+ security_token = cfg_map.get("security_token")
82
+ domain = cfg_map.get("domain")
83
+ client_id = cfg_map.get("client_id")
84
+ client_secret = cfg_map.get("client_secret")
85
+
86
+ username = username or os.getenv("SALESFORCE_USERNAME")
87
+ password = password or os.getenv("SALESFORCE_PASSWORD")
88
+ security_token = security_token or os.getenv("SALESFORCE_SECURITY_TOKEN")
89
+ domain = domain or os.getenv("SALESFORCE_DOMAIN", "login")
90
+ client_id = client_id or os.getenv("SALESFORCE_CLIENT_ID")
91
+ client_secret = client_secret or os.getenv("SALESFORCE_CLIENT_SECRET")
92
+
93
+ if not all([username, password, security_token]):
94
+ raise ValueError(
95
+ "Salesforce integration is not configured. Please configure the connection to Salesforce in Integrations."
96
+ )
97
+
98
+ # If client credentials are provided, perform OAuth2 password grant
99
+ if client_id and client_secret:
100
+ token_url = f"https://{domain}.salesforce.com/services/oauth2/token"
101
+ try:
102
+ resp = requests.post(
103
+ token_url,
104
+ data={
105
+ "grant_type": "password",
106
+ "client_id": client_id,
107
+ "client_secret": client_secret,
108
+ "username": username,
109
+ "password": f"{password}{security_token}",
110
+ },
111
+ timeout=10,
112
+ )
113
+ resp.raise_for_status()
114
+ data = resp.json()
115
+ access_token = data["access_token"]
116
+ instance_url = data["instance_url"]
117
+ return Salesforce(instance_url=instance_url, session_id=access_token)
118
+ except Exception as e:
119
+ raise ValueError(f"Failed to authenticate with Salesforce: {e}")
120
+
121
+ # Fallback to simple_salesforce username/password login
122
+ return Salesforce(
123
+ username=username,
124
+ password=password,
125
+ security_token=security_token,
126
+ domain=domain,
127
+ )
9
128
 
10
129
  @assistant_tool
11
130
  async def run_salesforce_crm_query(query: str):
@@ -27,23 +146,8 @@ async def run_salesforce_crm_query(query: str):
27
146
  if not query.strip():
28
147
  return json.dumps({"error": "The query string cannot be empty"})
29
148
 
30
- # Salesforce credentials from environment variables
31
- SF_USERNAME = os.environ.get('SALESFORCE_USERNAME')
32
- SF_PASSWORD = os.environ.get('SALESFORCE_PASSWORD')
33
- SF_SECURITY_TOKEN = os.environ.get('SALESFORCE_SECURITY_TOKEN')
34
- SF_DOMAIN = os.environ.get('SALESFORCE_DOMAIN', 'login') # Use 'test' for sandbox
35
-
36
- if not all([SF_USERNAME, SF_PASSWORD, SF_SECURITY_TOKEN]):
37
- return json.dumps({"error": "Salesforce credentials not found in environment variables"})
38
-
39
- # Initialize Salesforce connection
40
149
  try:
41
- sf = Salesforce(
42
- username=SF_USERNAME,
43
- password=SF_PASSWORD,
44
- security_token=SF_SECURITY_TOKEN,
45
- domain=SF_DOMAIN
46
- )
150
+ sf = _create_salesforce_client()
47
151
 
48
152
  # Execute the query
49
153
  result = sf.query_all(query)
@@ -56,7 +160,7 @@ async def run_salesforce_crm_query(query: str):
56
160
  return json.dumps(result)
57
161
 
58
162
  @assistant_tool
59
- async def fetch_salesforce_contact_info(contact_id=None, email=None):
163
+ async def fetch_salesforce_contact_info(contact_id: str = None, email: str = None, tool_config: Optional[List[Dict]] = None):
60
164
  """
61
165
  Fetch contact information from Salesforce using the contact's Salesforce ID or email.
62
166
 
@@ -71,26 +175,15 @@ async def fetch_salesforce_contact_info(contact_id=None, email=None):
71
175
  ValueError: If Salesforce credentials are not provided or if neither contact_id nor email is provided.
72
176
  ValueError: If no contact is found.
73
177
  """
74
- # Salesforce credentials from environment variables
75
- SF_USERNAME = os.environ.get('SALESFORCE_USERNAME')
76
- SF_PASSWORD = os.environ.get('SALESFORCE_PASSWORD')
77
- SF_SECURITY_TOKEN = os.environ.get('SALESFORCE_SECURITY_TOKEN')
78
- SF_DOMAIN = os.environ.get('SALESFORCE_DOMAIN', 'login') # Use 'test' for sandbox
79
-
80
- if not all([SF_USERNAME, SF_PASSWORD, SF_SECURITY_TOKEN]):
81
- return json.dumps({"error": "Salesforce credentials not found in environment variables"})
178
+ try:
179
+ sf = _create_salesforce_client(tool_config)
180
+ except Exception as e:
181
+ return json.dumps({"error": str(e)})
82
182
 
83
183
  if not contact_id and not email:
84
184
  return json.dumps({"error": "Either Salesforce contact ID or email must be provided"})
85
185
 
86
186
  try:
87
- # Connect to Salesforce
88
- sf = Salesforce(
89
- username=SF_USERNAME,
90
- password=SF_PASSWORD,
91
- security_token=SF_SECURITY_TOKEN,
92
- domain=SF_DOMAIN
93
- )
94
187
 
95
188
  if contact_id:
96
189
  # Fetch contact by ID
@@ -109,13 +202,14 @@ async def fetch_salesforce_contact_info(contact_id=None, email=None):
109
202
  return json.dumps({"error": "No contact found with the provided email"})
110
203
  contact = result['records'][0]
111
204
 
112
- return json.dumps(contact)
205
+ mapped = _map_contact_to_internal(contact)
206
+ return json.dumps(mapped)
113
207
  except Exception as e:
114
208
  return json.dumps({"error": f"Failed to fetch contact information: {e}"})
115
209
 
116
210
 
117
211
  @assistant_tool
118
- async def read_salesforce_list_entries(object_type: str, listview_name: str, entries_count: int):
212
+ async def read_salesforce_list_entries(object_type: str, listview_name: str, entries_count: int, tool_config: Optional[List[Dict]] = None):
119
213
  """
120
214
  Reads entries from a Salesforce list view and returns the results as JSON.
121
215
  Retrieves up to the specified number of entries.
@@ -134,23 +228,8 @@ async def read_salesforce_list_entries(object_type: str, listview_name: str, ent
134
228
  if entries_count <= 0:
135
229
  return json.dumps({"error": "Entries count must be a positive integer"})
136
230
 
137
- # Salesforce credentials from environment variables
138
- SF_USERNAME = os.environ.get('SALESFORCE_USERNAME')
139
- SF_PASSWORD = os.environ.get('SALESFORCE_PASSWORD')
140
- SF_SECURITY_TOKEN = os.environ.get('SALESFORCE_SECURITY_TOKEN')
141
- SF_DOMAIN = os.environ.get('SALESFORCE_DOMAIN', 'login') # Use 'test' for sandbox
142
-
143
- if not all([SF_USERNAME, SF_PASSWORD, SF_SECURITY_TOKEN]):
144
- return json.dumps({"error": "Salesforce credentials not found in environment variables"})
145
-
146
- # Initialize Salesforce connection
147
231
  try:
148
- sf = Salesforce(
149
- username=SF_USERNAME,
150
- password=SF_PASSWORD,
151
- security_token=SF_SECURITY_TOKEN,
152
- domain=SF_DOMAIN
153
- )
232
+ sf = _create_salesforce_client(tool_config)
154
233
 
155
234
  # Step 1: Get List View ID
156
235
  list_views_url = urljoin(sf.base_url, f"sobjects/{object_type}/listviews")
@@ -202,3 +281,197 @@ async def read_salesforce_list_entries(object_type: str, listview_name: str, ent
202
281
 
203
282
  # Return the results as a JSON string
204
283
  return json.dumps(records)
284
+
285
+
286
+ @assistant_tool
287
+ async def fetch_salesforce_list_views(object_type: str, tool_config: Optional[List[Dict]] = None) -> List[Dict[str, Any]]:
288
+ """Return available list views for a Salesforce object."""
289
+ try:
290
+ sf = _create_salesforce_client(tool_config)
291
+ url = urljoin(sf.base_url, f"sobjects/{object_type}/listviews")
292
+ resp = sf._call_salesforce("GET", url)
293
+ if resp.status_code != 200:
294
+ return {"error": resp.text}
295
+ data = resp.json()
296
+ return data.get("listviews", [])
297
+ except Exception as e:
298
+ return {"error": str(e)}
299
+
300
+
301
+ @assistant_tool
302
+ async def fetch_salesforce_list_records(
303
+ object_type: str,
304
+ listview_name: str,
305
+ offset: int = 0,
306
+ limit: int = 10,
307
+ tool_config: Optional[List[Dict]] = None,
308
+ ) -> List[Dict[str, Any]]:
309
+ """Fetch records from a Salesforce list view with offset and limit."""
310
+ try:
311
+ sf = _create_salesforce_client(tool_config)
312
+ url = urljoin(sf.base_url, f"sobjects/{object_type}/listviews")
313
+ lv_resp = sf._call_salesforce("GET", url)
314
+ if lv_resp.status_code != 200:
315
+ return {"error": lv_resp.text}
316
+ lv_data = lv_resp.json()
317
+ list_view = next((lv for lv in lv_data.get("listviews", []) if lv.get("label") == listview_name), None)
318
+ if not list_view:
319
+ return {"error": "List view not found"}
320
+ next_url = urljoin(sf.base_url, list_view["resultsUrl"])
321
+ records: List[Dict[str, Any]] = []
322
+ skipped = 0
323
+ collected = 0
324
+ while next_url and collected < limit:
325
+ resp = sf._call_salesforce("GET", next_url)
326
+ if resp.status_code != 200:
327
+ return {"error": resp.text}
328
+ data = resp.json()
329
+ for rec in data.get("records", []):
330
+ if skipped < offset:
331
+ skipped += 1
332
+ else:
333
+ records.append(rec)
334
+ collected += 1
335
+ if collected >= limit:
336
+ break
337
+ next_page = data.get("nextPageUrl")
338
+ next_url = urljoin(sf.base_url, next_page) if next_page else None
339
+ return records
340
+ except Exception as e:
341
+ return {"error": str(e)}
342
+
343
+
344
+ @assistant_tool
345
+ async def create_salesforce_contact(properties: Dict[str, Any], tool_config: Optional[List[Dict]] = None) -> Dict[str, Any]:
346
+ """Create a contact in Salesforce."""
347
+ try:
348
+ sf = _create_salesforce_client(tool_config)
349
+ result = sf.Contact.create(properties)
350
+ return result
351
+ except Exception as e:
352
+ return {"error": str(e)}
353
+
354
+
355
+ @assistant_tool
356
+ async def update_salesforce_contact(contact_id: str, properties: Dict[str, Any], tool_config: Optional[List[Dict]] = None) -> Dict[str, Any]:
357
+ """Update an existing Salesforce contact."""
358
+ try:
359
+ sf = _create_salesforce_client(tool_config)
360
+ sf.Contact.update(contact_id, properties)
361
+ return {"success": True}
362
+ except Exception as e:
363
+ return {"error": str(e)}
364
+
365
+
366
+ def _find_contact_id(
367
+ sf: Salesforce,
368
+ email: Optional[str] = None,
369
+ first_name: Optional[str] = None,
370
+ last_name: Optional[str] = None,
371
+ ) -> Optional[str]:
372
+ """Return the first matching contact ID or None."""
373
+ if email:
374
+ sanitized = email.replace("'", "\\'")
375
+ q = f"SELECT Id FROM Contact WHERE Email = '{sanitized}' LIMIT 1"
376
+ elif first_name and last_name:
377
+ q = (
378
+ "SELECT Id FROM Contact WHERE FirstName = '"
379
+ + first_name.replace("'", "\\'")
380
+ + "' AND LastName = '"
381
+ + last_name.replace("'", "\\'")
382
+ + "' LIMIT 1"
383
+ )
384
+ else:
385
+ return None
386
+ res = sf.query(q)
387
+ return res["records"][0]["Id"] if res.get("records") else None
388
+
389
+
390
+ def _find_account_id(sf: Salesforce, website: Optional[str] = None, name: Optional[str] = None) -> Optional[str]:
391
+ """Return the first matching account ID or None."""
392
+ if website:
393
+ sanitized = website.replace("'", "\\'")
394
+ q = f"SELECT Id FROM Account WHERE Website = '{sanitized}' LIMIT 1"
395
+ elif name:
396
+ sanitized = name.replace("'", "\\'")
397
+ q = f"SELECT Id FROM Account WHERE Name = '{sanitized}' LIMIT 1"
398
+ else:
399
+ return None
400
+ res = sf.query(q)
401
+ return res["records"][0]["Id"] if res.get("records") else None
402
+
403
+
404
+ @assistant_tool
405
+ async def update_crm_contact_record_function(
406
+ contact_values: Dict[str, Any],
407
+ is_update: bool,
408
+ salesforce_contact_id: Optional[str] = None,
409
+ email: Optional[str] = None,
410
+ first_name: Optional[str] = None,
411
+ last_name: Optional[str] = None,
412
+ tool_config: Optional[List[Dict]] = None,
413
+ ) -> Dict[str, Any]:
414
+ """Create or update a Salesforce contact."""
415
+ try:
416
+ sf = _create_salesforce_client(tool_config)
417
+ except Exception as e:
418
+ return {"error": str(e)}
419
+
420
+ properties: Dict[str, Any] = {}
421
+ for k, v in contact_values.items():
422
+ field = CONTACT_FIELD_MAPPING.get(k)
423
+ if field and v is not None:
424
+ properties[field] = v
425
+
426
+ if first_name:
427
+ properties.setdefault("FirstName", first_name)
428
+ if last_name:
429
+ properties.setdefault("LastName", last_name)
430
+ if email:
431
+ properties.setdefault("Email", email)
432
+
433
+ if is_update:
434
+ cid = salesforce_contact_id or _find_contact_id(sf, email=email, first_name=first_name, last_name=last_name)
435
+ if cid:
436
+ sf.Contact.update(cid, properties)
437
+ return sf.Contact.get(cid)
438
+
439
+ result = sf.Contact.create(properties)
440
+ return result
441
+
442
+
443
+ @assistant_tool
444
+ async def update_crm_company_record_function(
445
+ company_values: Dict[str, Any],
446
+ is_update: bool,
447
+ salesforce_company_id: Optional[str] = None,
448
+ organization_name: Optional[str] = None,
449
+ organization_website: Optional[str] = None,
450
+ tool_config: Optional[List[Dict]] = None,
451
+ ) -> Dict[str, Any]:
452
+ """Create or update a Salesforce Account."""
453
+ try:
454
+ sf = _create_salesforce_client(tool_config)
455
+ except Exception as e:
456
+ return {"error": str(e)}
457
+
458
+ properties: Dict[str, Any] = {}
459
+ for k, v in company_values.items():
460
+ field = COMPANY_FIELD_MAPPING.get(k)
461
+ if field and v is not None:
462
+ properties[field] = v
463
+
464
+ if organization_name:
465
+ properties.setdefault("Name", organization_name)
466
+ if organization_website:
467
+ properties.setdefault("Website", organization_website)
468
+
469
+ if is_update:
470
+ aid = salesforce_company_id or _find_account_id(sf, website=organization_website, name=organization_name)
471
+ if aid:
472
+ sf.Account.update(aid, properties)
473
+ return sf.Account.get(aid)
474
+
475
+ result = sf.Account.create(properties)
476
+ return result
477
+
@@ -0,0 +1,131 @@
1
+ import json
2
+ import logging
3
+ from typing import Any, Dict, List, Optional
4
+
5
+ logging.basicConfig(level=logging.INFO)
6
+ logger = logging.getLogger(__name__)
7
+
8
+ from dhisana.utils.assistant_tool_tag import assistant_tool
9
+
10
+ # Adjust these imports to your actual module paths:
11
+ from dhisana.utils.serpapi_google_search import search_google_serpai
12
+ from dhisana.utils.serperdev_search import search_google_serper
13
+ from dhisana.utils.google_custom_search import search_google_custom_search
14
+ # Only SERP API Supported as of now
15
+
16
+ def detect_search_provider(tool_config: Optional[List[Dict]] = None) -> Optional[str]:
17
+ """
18
+ Detect which search provider is available in tool_config, in priority order:
19
+ 1. serperdev
20
+ 2. serpapi
21
+ 3. google_custom_search
22
+
23
+ Returns:
24
+ - 'serperdev' if Serper.dev config is found
25
+ - 'serpapi' if SerpAPI config is found
26
+ - 'google_custom_search' if Custom Search config is found
27
+ - None if no known provider config is found
28
+ """
29
+ if not tool_config:
30
+ return None
31
+
32
+ # 1) Check if 'serperdev' provider is available
33
+ serper_config = next((cfg for cfg in tool_config if cfg.get("name") == "serperdev"), None)
34
+ if serper_config:
35
+ config_map = {
36
+ item["name"]: item["value"]
37
+ for item in serper_config.get("configuration", [])
38
+ if item
39
+ }
40
+ if "apiKey" in config_map and config_map["apiKey"]:
41
+ return "serperdev"
42
+
43
+ # # 2) Check if 'serpapi' provider is available
44
+ serpapi_config = next((cfg for cfg in tool_config if cfg.get("name") == "serpapi"), None)
45
+ if serpapi_config:
46
+ config_map = {
47
+ item["name"]: item["value"]
48
+ for item in serpapi_config.get("configuration", [])
49
+ if item
50
+ }
51
+ if "apiKey" in config_map and config_map["apiKey"]:
52
+ return "serpapi"
53
+
54
+ # 3) Check if 'google_custom_search' is available
55
+ custom_search_config = next((cfg for cfg in tool_config if cfg.get("name") == "google_custom_search"), None)
56
+ if custom_search_config:
57
+ config_map = {
58
+ item["name"]: item["value"]
59
+ for item in custom_search_config.get("configuration", [])
60
+ if item
61
+ }
62
+ if "apiKey" in config_map and "cx" in config_map:
63
+ return "google_custom_search"
64
+
65
+ # No recognized provider found
66
+ return None
67
+
68
+
69
+ @assistant_tool
70
+ async def search_google_with_tools(
71
+ query: str,
72
+ number_of_results: int = 10,
73
+ offset: int = 0,
74
+ tool_config: Optional[List[Dict]] = None,
75
+ as_oq: Optional[str] = None
76
+ ) -> List[str]:
77
+ """
78
+ Common router function that searches using whichever provider is available in tool_config:
79
+ 1. Serper.dev (priority)
80
+ 2. SerpAPI
81
+ 3. Google Custom Search (last fallback)
82
+
83
+ Parameters:
84
+ - query (str): The search query
85
+ - number_of_results (int): Number of results to return
86
+ - offset (int): Offset for pagination or 'page' for some providers
87
+ - tool_config (Optional[List[Dict]]): Configuration for possible providers
88
+ - as_oq (Optional[str]): Additional optional search terms appended to 'query'
89
+
90
+ Returns:
91
+ - List[str]: A list of JSON-serialized results, or an error message if no provider is available.
92
+ """
93
+ logger.info("Entering search_google_with_tools")
94
+
95
+ if not query:
96
+ logger.warning("Empty query string provided to search_google_with_tools.")
97
+ return []
98
+
99
+ provider = detect_search_provider(tool_config)
100
+ logger.debug(f"Detected provider: {provider}")
101
+
102
+ if provider == "serperdev":
103
+ logger.info("Using Serper.dev provider")
104
+ return await search_google_serper(
105
+ query=query,
106
+ number_of_results=number_of_results,
107
+ offset=offset,
108
+ tool_config=tool_config,
109
+ as_oq=as_oq
110
+ )
111
+ elif provider == "serpapi":
112
+ logger.info("Using SerpAPI provider")
113
+ return await search_google_serpai(
114
+ query=query,
115
+ number_of_results=number_of_results,
116
+ offset=offset,
117
+ tool_config=tool_config,
118
+ as_oq=as_oq
119
+ )
120
+ elif provider == "google_custom_search":
121
+ logger.info("Using Google Custom Search provider")
122
+ return await search_google_custom_search(
123
+ query=query,
124
+ number_of_results=number_of_results,
125
+ offset=offset,
126
+ tool_config=tool_config,
127
+ as_oq=as_oq
128
+ )
129
+ else:
130
+ logger.error("No supported search provider found in tool_config.")
131
+ return [json.dumps({"error": "No supported search provider found."})]
@@ -0,0 +1,51 @@
1
+ import json
2
+ import logging
3
+ from typing import Any, Dict, List, Optional
4
+
5
+ from dhisana.utils.assistant_tool_tag import assistant_tool
6
+ from dhisana.utils.search_router import detect_search_provider
7
+ from dhisana.utils.serperdev_google_jobs import search_google_jobs_serper
8
+ from dhisana.utils.serpapi_google_jobs import search_google_jobs_serpapi
9
+
10
+ logging.basicConfig(level=logging.INFO)
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ @assistant_tool
15
+ async def search_google_jobs_with_tools(
16
+ query: str,
17
+ number_of_results: int = 10,
18
+ offset: int = 0,
19
+ tool_config: Optional[List[Dict]] = None,
20
+ location: Optional[str] = None,
21
+ ) -> List[str]:
22
+ """Router that searches Google Jobs using the configured provider."""
23
+ if not query:
24
+ logger.warning("Empty query received by jobs router")
25
+ return []
26
+
27
+ provider = detect_search_provider(tool_config)
28
+ logger.debug("Jobs router chose provider: %s", provider)
29
+
30
+ if provider == "serperdev":
31
+ logger.info("Routing to Serper.dev job search helper")
32
+ return await search_google_jobs_serper(
33
+ query=query,
34
+ number_of_results=number_of_results,
35
+ offset=offset,
36
+ tool_config=tool_config,
37
+ location=location,
38
+ )
39
+
40
+ if provider == "serpapi":
41
+ logger.info("Routing to SerpApi job search helper")
42
+ return await search_google_jobs_serpapi(
43
+ query=query,
44
+ number_of_results=number_of_results,
45
+ offset=offset,
46
+ tool_config=tool_config,
47
+ location=location,
48
+ )
49
+
50
+ logger.error("No supported jobs provider found in tool_config")
51
+ return [json.dumps({"error": "No supported jobs provider configured."})]