dhisana 0.0.1.dev243__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/__init__.py +1 -0
- dhisana/cli/__init__.py +1 -0
- dhisana/cli/cli.py +20 -0
- dhisana/cli/datasets.py +27 -0
- dhisana/cli/models.py +26 -0
- dhisana/cli/predictions.py +20 -0
- dhisana/schemas/__init__.py +1 -0
- dhisana/schemas/common.py +399 -0
- dhisana/schemas/sales.py +965 -0
- dhisana/ui/__init__.py +1 -0
- dhisana/ui/components.py +472 -0
- dhisana/utils/__init__.py +1 -0
- dhisana/utils/add_mapping.py +352 -0
- dhisana/utils/agent_tools.py +51 -0
- dhisana/utils/apollo_tools.py +1597 -0
- dhisana/utils/assistant_tool_tag.py +4 -0
- dhisana/utils/built_with_api_tools.py +282 -0
- dhisana/utils/cache_output_tools.py +98 -0
- dhisana/utils/cache_output_tools_local.py +78 -0
- dhisana/utils/check_email_validity_tools.py +717 -0
- dhisana/utils/check_for_intent_signal.py +107 -0
- dhisana/utils/check_linkedin_url_validity.py +209 -0
- dhisana/utils/clay_tools.py +43 -0
- dhisana/utils/clean_properties.py +135 -0
- dhisana/utils/company_utils.py +60 -0
- dhisana/utils/compose_salesnav_query.py +259 -0
- dhisana/utils/compose_search_query.py +759 -0
- dhisana/utils/compose_three_step_workflow.py +234 -0
- dhisana/utils/composite_tools.py +137 -0
- dhisana/utils/dataframe_tools.py +237 -0
- dhisana/utils/domain_parser.py +45 -0
- dhisana/utils/email_body_utils.py +72 -0
- dhisana/utils/email_parse_helpers.py +132 -0
- dhisana/utils/email_provider.py +375 -0
- dhisana/utils/enrich_lead_information.py +933 -0
- dhisana/utils/extract_email_content_for_llm.py +101 -0
- dhisana/utils/fetch_openai_config.py +129 -0
- dhisana/utils/field_validators.py +426 -0
- dhisana/utils/g2_tools.py +104 -0
- dhisana/utils/generate_content.py +41 -0
- dhisana/utils/generate_custom_message.py +271 -0
- dhisana/utils/generate_email.py +278 -0
- dhisana/utils/generate_email_response.py +465 -0
- dhisana/utils/generate_flow.py +102 -0
- dhisana/utils/generate_leads_salesnav.py +303 -0
- dhisana/utils/generate_linkedin_connect_message.py +224 -0
- dhisana/utils/generate_linkedin_response_message.py +317 -0
- dhisana/utils/generate_structured_output_internal.py +462 -0
- dhisana/utils/google_custom_search.py +267 -0
- dhisana/utils/google_oauth_tools.py +727 -0
- dhisana/utils/google_workspace_tools.py +1294 -0
- dhisana/utils/hubspot_clearbit.py +96 -0
- dhisana/utils/hubspot_crm_tools.py +2440 -0
- dhisana/utils/instantly_tools.py +149 -0
- dhisana/utils/linkedin_crawler.py +168 -0
- dhisana/utils/lusha_tools.py +333 -0
- dhisana/utils/mailgun_tools.py +156 -0
- dhisana/utils/mailreach_tools.py +123 -0
- dhisana/utils/microsoft365_tools.py +455 -0
- dhisana/utils/openai_assistant_and_file_utils.py +267 -0
- dhisana/utils/openai_helpers.py +977 -0
- dhisana/utils/openapi_spec_to_tools.py +45 -0
- dhisana/utils/openapi_tool/__init__.py +1 -0
- dhisana/utils/openapi_tool/api_models.py +633 -0
- dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +271 -0
- dhisana/utils/openapi_tool/openapi_tool.py +319 -0
- dhisana/utils/parse_linkedin_messages_txt.py +100 -0
- dhisana/utils/profile.py +37 -0
- dhisana/utils/proxy_curl_tools.py +1226 -0
- dhisana/utils/proxycurl_search_leads.py +426 -0
- dhisana/utils/python_function_to_tools.py +83 -0
- dhisana/utils/research_lead.py +176 -0
- dhisana/utils/sales_navigator_crawler.py +1103 -0
- dhisana/utils/salesforce_crm_tools.py +477 -0
- dhisana/utils/search_router.py +131 -0
- dhisana/utils/search_router_jobs.py +51 -0
- dhisana/utils/sendgrid_tools.py +162 -0
- 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 +852 -0
- 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 +582 -0
- dhisana/utils/test_connect.py +2087 -0
- dhisana/utils/trasform_json.py +173 -0
- dhisana/utils/web_download_parse_tools.py +189 -0
- dhisana/utils/workflow_code_model.py +5 -0
- dhisana/utils/zoominfo_tools.py +357 -0
- dhisana/workflow/__init__.py +1 -0
- dhisana/workflow/agent.py +18 -0
- dhisana/workflow/flow.py +44 -0
- dhisana/workflow/task.py +43 -0
- dhisana/workflow/test.py +90 -0
- dhisana-0.0.1.dev243.dist-info/METADATA +43 -0
- dhisana-0.0.1.dev243.dist-info/RECORD +102 -0
- dhisana-0.0.1.dev243.dist-info/WHEEL +5 -0
- dhisana-0.0.1.dev243.dist-info/entry_points.txt +2 -0
- dhisana-0.0.1.dev243.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
# Sales force CRM Tools
|
|
2
|
+
# TODO: This needs to be tested and validated like the HubSpot CRM tools.
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import requests
|
|
7
|
+
from dhisana.utils.assistant_tool_tag import assistant_tool
|
|
8
|
+
from simple_salesforce import Salesforce
|
|
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
|
+
)
|
|
128
|
+
|
|
129
|
+
@assistant_tool
|
|
130
|
+
async def run_salesforce_crm_query(query: str):
|
|
131
|
+
"""
|
|
132
|
+
Executes a Salesforce SOQL query and returns the results as JSON.
|
|
133
|
+
Use this to query Salesforce CRM data like Contacts, Leads, Company etc.
|
|
134
|
+
|
|
135
|
+
Parameters:
|
|
136
|
+
query (str): The SOQL query string to execute.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
str: JSON string containing the query results or error message.
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
ValueError: If Salesforce credentials are not found.
|
|
143
|
+
ValueError: If the query is empty.
|
|
144
|
+
Exception: If the query fails or returns no results.
|
|
145
|
+
"""
|
|
146
|
+
if not query.strip():
|
|
147
|
+
return json.dumps({"error": "The query string cannot be empty"})
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
sf = _create_salesforce_client()
|
|
151
|
+
|
|
152
|
+
# Execute the query
|
|
153
|
+
result = sf.query_all(query)
|
|
154
|
+
if not result['records']:
|
|
155
|
+
return json.dumps({"error": "No records found for the provided query"})
|
|
156
|
+
except Exception as e:
|
|
157
|
+
return json.dumps({"error": f"Query failed: {e}"})
|
|
158
|
+
|
|
159
|
+
# Return the results as a JSON string
|
|
160
|
+
return json.dumps(result)
|
|
161
|
+
|
|
162
|
+
@assistant_tool
|
|
163
|
+
async def fetch_salesforce_contact_info(contact_id: str = None, email: str = None, tool_config: Optional[List[Dict]] = None):
|
|
164
|
+
"""
|
|
165
|
+
Fetch contact information from Salesforce using the contact's Salesforce ID or email.
|
|
166
|
+
|
|
167
|
+
Parameters:
|
|
168
|
+
contact_id (str): Unique Salesforce contact ID.
|
|
169
|
+
email (str): Contact's email address.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
dict: JSON response containing contact information.
|
|
173
|
+
|
|
174
|
+
Raises:
|
|
175
|
+
ValueError: If Salesforce credentials are not provided or if neither contact_id nor email is provided.
|
|
176
|
+
ValueError: If no contact is found.
|
|
177
|
+
"""
|
|
178
|
+
try:
|
|
179
|
+
sf = _create_salesforce_client(tool_config)
|
|
180
|
+
except Exception as e:
|
|
181
|
+
return json.dumps({"error": str(e)})
|
|
182
|
+
|
|
183
|
+
if not contact_id and not email:
|
|
184
|
+
return json.dumps({"error": "Either Salesforce contact ID or email must be provided"})
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
|
|
188
|
+
if contact_id:
|
|
189
|
+
# Fetch contact by ID
|
|
190
|
+
contact = sf.Contact.get(contact_id)
|
|
191
|
+
else:
|
|
192
|
+
# Sanitize email input
|
|
193
|
+
sanitized_email = email.replace("'", "\\'")
|
|
194
|
+
query = f"""
|
|
195
|
+
SELECT Id, Name, Email, Phone, MobilePhone, Title, Department, MailingAddress, LastActivityDate, LeadSource,
|
|
196
|
+
Account.Id, Account.Name, Account.Industry, Account.Website, Account.Phone, Account.BillingAddress
|
|
197
|
+
FROM Contact
|
|
198
|
+
WHERE Email = '{sanitized_email}'
|
|
199
|
+
"""
|
|
200
|
+
result = sf.query(query)
|
|
201
|
+
if result['totalSize'] == 0:
|
|
202
|
+
return json.dumps({"error": "No contact found with the provided email"})
|
|
203
|
+
contact = result['records'][0]
|
|
204
|
+
|
|
205
|
+
mapped = _map_contact_to_internal(contact)
|
|
206
|
+
return json.dumps(mapped)
|
|
207
|
+
except Exception as e:
|
|
208
|
+
return json.dumps({"error": f"Failed to fetch contact information: {e}"})
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@assistant_tool
|
|
212
|
+
async def read_salesforce_list_entries(object_type: str, listview_name: str, entries_count: int, tool_config: Optional[List[Dict]] = None):
|
|
213
|
+
"""
|
|
214
|
+
Reads entries from a Salesforce list view and returns the results as JSON.
|
|
215
|
+
Retrieves up to the specified number of entries.
|
|
216
|
+
|
|
217
|
+
Parameters:
|
|
218
|
+
object_type (str): The Salesforce object type (e.g., 'Contact').
|
|
219
|
+
listview_name (str): The name of the list view to read from.
|
|
220
|
+
entries_count (int): The number of entries to read.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
str: JSON string containing the list entries or error message.
|
|
224
|
+
"""
|
|
225
|
+
if not listview_name.strip() or not object_type.strip():
|
|
226
|
+
return json.dumps({"error": "The object type and list view name cannot be empty"})
|
|
227
|
+
|
|
228
|
+
if entries_count <= 0:
|
|
229
|
+
return json.dumps({"error": "Entries count must be a positive integer"})
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
sf = _create_salesforce_client(tool_config)
|
|
233
|
+
|
|
234
|
+
# Step 1: Get List View ID
|
|
235
|
+
list_views_url = urljoin(sf.base_url, f"sobjects/{object_type}/listviews")
|
|
236
|
+
list_views_response = sf._call_salesforce('GET', list_views_url)
|
|
237
|
+
|
|
238
|
+
if list_views_response.status_code != 200:
|
|
239
|
+
return json.dumps({"error": f"Failed to retrieve list views: {list_views_response.text}"})
|
|
240
|
+
|
|
241
|
+
list_views_data = list_views_response.json()
|
|
242
|
+
list_view = next(
|
|
243
|
+
(lv for lv in list_views_data['listviews'] if lv['label'] == listview_name),
|
|
244
|
+
None
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
if not list_view:
|
|
248
|
+
return json.dumps({"error": "List view not found"})
|
|
249
|
+
|
|
250
|
+
# Step 2: Fetch entries with pagination
|
|
251
|
+
entries_url = urljoin(sf.base_url, list_view['resultsUrl'])
|
|
252
|
+
records = []
|
|
253
|
+
|
|
254
|
+
while len(records) < entries_count and entries_url:
|
|
255
|
+
entries_response = sf._call_salesforce('GET', entries_url)
|
|
256
|
+
if entries_response.status_code != 200:
|
|
257
|
+
return json.dumps({"error": f"Failed to retrieve entries: {entries_response.text}"})
|
|
258
|
+
|
|
259
|
+
entries_data = entries_response.json()
|
|
260
|
+
entries = entries_data.get('records', [])
|
|
261
|
+
records.extend(entries)
|
|
262
|
+
|
|
263
|
+
if len(records) >= entries_count:
|
|
264
|
+
break
|
|
265
|
+
|
|
266
|
+
# Check for next page
|
|
267
|
+
next_page_url = entries_data.get('nextPageUrl')
|
|
268
|
+
if next_page_url:
|
|
269
|
+
entries_url = urljoin(sf.base_url, next_page_url)
|
|
270
|
+
else:
|
|
271
|
+
entries_url = None # No more pages
|
|
272
|
+
|
|
273
|
+
# Trim the records to the desired count
|
|
274
|
+
records = records[:entries_count]
|
|
275
|
+
|
|
276
|
+
if not records:
|
|
277
|
+
return json.dumps({"error": "No entries found for the specified list view"})
|
|
278
|
+
|
|
279
|
+
except Exception as e:
|
|
280
|
+
return json.dumps({"error": f"Query failed: {e}"})
|
|
281
|
+
|
|
282
|
+
# Return the results as a JSON string
|
|
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."})]
|