dhisana 0.0.1.dev85__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.
- dhisana/schemas/common.py +33 -0
- dhisana/schemas/sales.py +224 -23
- dhisana/utils/add_mapping.py +72 -63
- dhisana/utils/apollo_tools.py +739 -109
- dhisana/utils/built_with_api_tools.py +4 -2
- dhisana/utils/cache_output_tools.py +23 -23
- dhisana/utils/check_email_validity_tools.py +456 -458
- dhisana/utils/check_for_intent_signal.py +1 -2
- dhisana/utils/check_linkedin_url_validity.py +34 -8
- dhisana/utils/clay_tools.py +3 -2
- dhisana/utils/clean_properties.py +3 -1
- dhisana/utils/compose_salesnav_query.py +0 -1
- dhisana/utils/compose_search_query.py +7 -3
- dhisana/utils/composite_tools.py +0 -1
- dhisana/utils/dataframe_tools.py +2 -2
- dhisana/utils/email_body_utils.py +72 -0
- dhisana/utils/email_provider.py +375 -0
- dhisana/utils/enrich_lead_information.py +585 -85
- dhisana/utils/fetch_openai_config.py +129 -0
- dhisana/utils/field_validators.py +1 -1
- dhisana/utils/g2_tools.py +0 -1
- dhisana/utils/generate_content.py +0 -1
- dhisana/utils/generate_email.py +69 -16
- dhisana/utils/generate_email_response.py +298 -41
- dhisana/utils/generate_flow.py +0 -1
- dhisana/utils/generate_linkedin_connect_message.py +19 -6
- dhisana/utils/generate_linkedin_response_message.py +156 -65
- dhisana/utils/generate_structured_output_internal.py +351 -131
- dhisana/utils/google_custom_search.py +150 -44
- dhisana/utils/google_oauth_tools.py +721 -0
- dhisana/utils/google_workspace_tools.py +391 -25
- dhisana/utils/hubspot_clearbit.py +3 -1
- dhisana/utils/hubspot_crm_tools.py +771 -167
- dhisana/utils/instantly_tools.py +3 -1
- dhisana/utils/lusha_tools.py +10 -7
- dhisana/utils/mailgun_tools.py +150 -0
- dhisana/utils/microsoft365_tools.py +447 -0
- dhisana/utils/openai_assistant_and_file_utils.py +121 -177
- dhisana/utils/openai_helpers.py +19 -16
- dhisana/utils/parse_linkedin_messages_txt.py +2 -3
- dhisana/utils/profile.py +37 -0
- dhisana/utils/proxy_curl_tools.py +507 -206
- dhisana/utils/proxycurl_search_leads.py +426 -0
- dhisana/utils/research_lead.py +121 -68
- dhisana/utils/sales_navigator_crawler.py +1 -6
- dhisana/utils/salesforce_crm_tools.py +323 -50
- dhisana/utils/search_router.py +131 -0
- dhisana/utils/search_router_jobs.py +51 -0
- dhisana/utils/sendgrid_tools.py +126 -91
- dhisana/utils/serarch_router_local_business.py +75 -0
- dhisana/utils/serpapi_additional_tools.py +290 -0
- dhisana/utils/serpapi_google_jobs.py +117 -0
- dhisana/utils/serpapi_google_search.py +188 -0
- dhisana/utils/serpapi_local_business_search.py +129 -0
- dhisana/utils/serpapi_search_tools.py +363 -432
- dhisana/utils/serperdev_google_jobs.py +125 -0
- dhisana/utils/serperdev_local_business.py +154 -0
- dhisana/utils/serperdev_search.py +233 -0
- dhisana/utils/smtp_email_tools.py +576 -0
- dhisana/utils/test_connect.py +1765 -92
- dhisana/utils/trasform_json.py +95 -16
- dhisana/utils/web_download_parse_tools.py +0 -1
- dhisana/utils/zoominfo_tools.py +2 -3
- dhisana/workflow/test.py +1 -1
- {dhisana-0.0.1.dev85.dist-info → dhisana-0.0.1.dev236.dist-info}/METADATA +5 -2
- dhisana-0.0.1.dev236.dist-info/RECORD +100 -0
- {dhisana-0.0.1.dev85.dist-info → dhisana-0.0.1.dev236.dist-info}/WHEEL +1 -1
- dhisana-0.0.1.dev85.dist-info/RECORD +0 -81
- {dhisana-0.0.1.dev85.dist-info → dhisana-0.0.1.dev236.dist-info}/entry_points.txt +0 -0
- {dhisana-0.0.1.dev85.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 =
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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 =
|
|
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."})]
|