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,162 @@
|
|
|
1
|
+
"""Mail delivery helpers for SendGrid and compatibility exports for Mailgun.
|
|
2
|
+
|
|
3
|
+
This module now contains:
|
|
4
|
+
- SendGrid: helpers to send e-mail via SendGrid's REST API.
|
|
5
|
+
- Mailgun: re-exports for helpers that were moved to `mailgun_tools.py`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
from typing import Optional, List, Dict
|
|
11
|
+
from email.utils import parseaddr
|
|
12
|
+
|
|
13
|
+
import aiohttp
|
|
14
|
+
|
|
15
|
+
from dhisana.utils.assistant_tool_tag import assistant_tool
|
|
16
|
+
from dhisana.schemas.common import SendEmailContext
|
|
17
|
+
from dhisana.utils.email_body_utils import body_variants
|
|
18
|
+
|
|
19
|
+
# --------------------------------------------------------------------------- #
|
|
20
|
+
# Mailgun (re-exported from dedicated module for backward compatibility)
|
|
21
|
+
# --------------------------------------------------------------------------- #
|
|
22
|
+
from .mailgun_tools import (
|
|
23
|
+
get_mailgun_notify_key,
|
|
24
|
+
get_mailgun_notify_domain,
|
|
25
|
+
send_email_with_mailgun,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# --------------------------------------------------------------------------- #
|
|
30
|
+
# SendGrid helpers
|
|
31
|
+
# --------------------------------------------------------------------------- #
|
|
32
|
+
|
|
33
|
+
def get_sendgrid_api_key(tool_config: Optional[List[Dict]] = None) -> str:
|
|
34
|
+
"""
|
|
35
|
+
Retrieve the SendGrid API key from tool_config or environment.
|
|
36
|
+
|
|
37
|
+
Looks for an integration named "sendgrid" and reads configuration item
|
|
38
|
+
with name "apiKey". Falls back to env var SENDGRID_API_KEY.
|
|
39
|
+
"""
|
|
40
|
+
key: Optional[str] = None
|
|
41
|
+
if tool_config:
|
|
42
|
+
cfg = next((c for c in tool_config if c.get("name") == "sendgrid"), None)
|
|
43
|
+
if cfg:
|
|
44
|
+
cfg_map = {i.get("name"): i.get("value") for i in cfg.get("configuration", []) if i}
|
|
45
|
+
key = cfg_map.get("apiKey")
|
|
46
|
+
key = key or os.getenv("SENDGRID_API_KEY")
|
|
47
|
+
if not key:
|
|
48
|
+
raise ValueError(
|
|
49
|
+
"SendGrid integration is not configured. Please configure the connection to SendGrid in Integrations."
|
|
50
|
+
)
|
|
51
|
+
return key
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@assistant_tool
|
|
55
|
+
async def send_email_with_sendgrid(
|
|
56
|
+
sender: str,
|
|
57
|
+
recipients: List[str],
|
|
58
|
+
subject: str,
|
|
59
|
+
message: str,
|
|
60
|
+
tool_config: Optional[List[Dict]] = None,
|
|
61
|
+
body_format: Optional[str] = None,
|
|
62
|
+
custom_headers: Optional[Dict[str, str]] = None,
|
|
63
|
+
):
|
|
64
|
+
"""
|
|
65
|
+
Send an email using SendGrid's v3 Mail Send API.
|
|
66
|
+
|
|
67
|
+
Parameters:
|
|
68
|
+
- sender: Either "Name <email@example.com>" or a plain e-mail address.
|
|
69
|
+
- recipients: List of recipient e-mail addresses.
|
|
70
|
+
- subject: Subject string.
|
|
71
|
+
- message: HTML body content.
|
|
72
|
+
- tool_config: Optional integration configuration list.
|
|
73
|
+
- custom_headers: Optional mapping of header names to values.
|
|
74
|
+
"""
|
|
75
|
+
api_key = get_sendgrid_api_key(tool_config)
|
|
76
|
+
|
|
77
|
+
name, email_addr = parseaddr(sender)
|
|
78
|
+
from_obj: Dict[str, str] = {"email": email_addr or sender}
|
|
79
|
+
if name:
|
|
80
|
+
from_obj["name"] = name
|
|
81
|
+
|
|
82
|
+
to_list = [{"email": r} for r in recipients if r]
|
|
83
|
+
if not to_list:
|
|
84
|
+
return {"error": "No recipients provided"}
|
|
85
|
+
|
|
86
|
+
plain_body, html_body, _ = body_variants(message, body_format)
|
|
87
|
+
content = [
|
|
88
|
+
{"type": "text/plain", "value": plain_body},
|
|
89
|
+
{"type": "text/html", "value": html_body},
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
payload = {
|
|
93
|
+
"personalizations": [
|
|
94
|
+
{
|
|
95
|
+
"to": to_list,
|
|
96
|
+
"subject": subject,
|
|
97
|
+
}
|
|
98
|
+
],
|
|
99
|
+
"from": from_obj,
|
|
100
|
+
"content": content,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if custom_headers:
|
|
104
|
+
payload["headers"] = {
|
|
105
|
+
header: str(value)
|
|
106
|
+
for header, value in custom_headers.items()
|
|
107
|
+
if header and value is not None
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
headers = {
|
|
111
|
+
"Authorization": f"Bearer {api_key}",
|
|
112
|
+
"Content-Type": "application/json",
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
async with aiohttp.ClientSession() as session:
|
|
117
|
+
async with session.post(
|
|
118
|
+
"https://api.sendgrid.com/v3/mail/send",
|
|
119
|
+
headers=headers,
|
|
120
|
+
json=payload,
|
|
121
|
+
) as response:
|
|
122
|
+
# SendGrid returns 202 Accepted on success with empty body
|
|
123
|
+
if response.status == 202:
|
|
124
|
+
return {"status": 202, "message": "accepted"}
|
|
125
|
+
# On error, try to parse JSON for helpful message
|
|
126
|
+
try:
|
|
127
|
+
err = await response.json()
|
|
128
|
+
except Exception:
|
|
129
|
+
err = {"text": await response.text()}
|
|
130
|
+
return {"error": err, "status": response.status}
|
|
131
|
+
except Exception as ex:
|
|
132
|
+
logging.warning(f"Error sending email via SendGrid: {ex}")
|
|
133
|
+
return {"error": str(ex)}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
async def send_email_using_sendgrid_async(
|
|
137
|
+
ctx: SendEmailContext,
|
|
138
|
+
tool_config: Optional[List[Dict]] = None,
|
|
139
|
+
) -> str:
|
|
140
|
+
"""
|
|
141
|
+
Provider-style wrapper for SendGrid using SendEmailContext.
|
|
142
|
+
Returns an opaque token since SendGrid does not return a message id.
|
|
143
|
+
"""
|
|
144
|
+
plain_body, html_body, _ = body_variants(
|
|
145
|
+
ctx.body,
|
|
146
|
+
getattr(ctx, "body_format", None),
|
|
147
|
+
)
|
|
148
|
+
result = await send_email_with_sendgrid(
|
|
149
|
+
sender=f"{ctx.sender_name} <{ctx.sender_email}>",
|
|
150
|
+
recipients=[ctx.recipient],
|
|
151
|
+
subject=ctx.subject,
|
|
152
|
+
message=ctx.body or "",
|
|
153
|
+
body_format=getattr(ctx, "body_format", None),
|
|
154
|
+
tool_config=tool_config,
|
|
155
|
+
custom_headers=getattr(ctx, "headers", None),
|
|
156
|
+
)
|
|
157
|
+
# Normalise output to a string id-like value
|
|
158
|
+
if isinstance(result, dict) and result.get("status") == 202:
|
|
159
|
+
return f"sent:{ctx.sender_email}:{ctx.recipient}:{ctx.subject}"
|
|
160
|
+
if isinstance(result, dict) and "error" in result:
|
|
161
|
+
raise RuntimeError(f"SendGrid send failed: {result['error']}")
|
|
162
|
+
return str(result)
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
|
|
7
|
+
# Your provider-specific helpers (paths may differ in your repo)
|
|
8
|
+
from dhisana.utils.serperdev_local_business import search_local_business_serper
|
|
9
|
+
from dhisana.utils.serpapi_local_business_search import search_local_business_serpai
|
|
10
|
+
|
|
11
|
+
# Re-use your existing provider detector
|
|
12
|
+
from dhisana.utils.search_router import detect_search_provider # or copy the function shown earlier
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
logging.basicConfig(level=logging.INFO)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@assistant_tool
|
|
19
|
+
async def search_local_business_with_tools(
|
|
20
|
+
query: str,
|
|
21
|
+
number_of_results: int = 20,
|
|
22
|
+
offset: int = 0,
|
|
23
|
+
tool_config: Optional[List[Dict]] = None,
|
|
24
|
+
location: Optional[str] = None,
|
|
25
|
+
) -> List[str]:
|
|
26
|
+
"""
|
|
27
|
+
Router that returns local-business (Google Maps) results using whichever
|
|
28
|
+
provider is configured in `tool_config`.
|
|
29
|
+
|
|
30
|
+
Priority order:
|
|
31
|
+
1. Serper.dev – uses `search_local_business_serper`
|
|
32
|
+
2. SerpApi – uses `search_local_business_serpai`
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
query: Search string (e.g. "plumbers near Almaden Valley").
|
|
36
|
+
number_of_results: Desired row count (will paginate if > provider page size).
|
|
37
|
+
offset: Page offset (0-based; converted to provider-specific page or start).
|
|
38
|
+
tool_config: Dhisana tool-configuration blob listing available providers.
|
|
39
|
+
location: Optional city/region hint (Serper + SerpApi both accept it).
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
List[str]: Each element is a JSON-encoded dict with keys:
|
|
43
|
+
full_name, organization_name, phone, organization_website,
|
|
44
|
+
rating, reviews, address, google_maps_url.
|
|
45
|
+
If no provider is configured, returns one item with an "error" key.
|
|
46
|
+
"""
|
|
47
|
+
if not query:
|
|
48
|
+
logger.warning("Empty query received by local-business router.")
|
|
49
|
+
return []
|
|
50
|
+
|
|
51
|
+
provider = detect_search_provider(tool_config)
|
|
52
|
+
logger.debug("Local-business router chose provider: %s", provider)
|
|
53
|
+
|
|
54
|
+
if provider == "serperdev":
|
|
55
|
+
logger.info("Routing to Serper.dev local-business helper.")
|
|
56
|
+
return await search_local_business_serper(
|
|
57
|
+
query=query,
|
|
58
|
+
number_of_results=number_of_results,
|
|
59
|
+
offset=offset,
|
|
60
|
+
tool_config=tool_config,
|
|
61
|
+
location=location,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if provider == "serpapi":
|
|
65
|
+
logger.info("Routing to SerpApi local-business helper.")
|
|
66
|
+
return await search_local_business_serpai(
|
|
67
|
+
query=query,
|
|
68
|
+
number_of_results=number_of_results,
|
|
69
|
+
offset=offset,
|
|
70
|
+
tool_config=tool_config,
|
|
71
|
+
location=location,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
logger.error("No supported local-business provider found in tool_config.")
|
|
75
|
+
return [json.dumps({"error": "No supported local-business provider configured."})]
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
from urllib.parse import urlparse
|
|
6
|
+
import aiohttp
|
|
7
|
+
from bs4 import BeautifulSoup
|
|
8
|
+
import urllib
|
|
9
|
+
|
|
10
|
+
from dhisana.utils.assistant_tool_tag import assistant_tool
|
|
11
|
+
from dhisana.utils.cache_output_tools import cache_output, retrieve_output
|
|
12
|
+
from dhisana.utils.web_download_parse_tools import fetch_html_content
|
|
13
|
+
from dhisana.utils.search_router import search_google_with_tools
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
logging.basicConfig(level=logging.INFO)
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_serp_api_access_token(tool_config: Optional[List[Dict]] = None) -> str:
|
|
21
|
+
"""
|
|
22
|
+
Retrieves the SERPAPI_KEY access token from the provided tool configuration.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
tool_config (list): A list of dictionaries containing the tool configuration.
|
|
26
|
+
Each dictionary should have a "name" key and a "configuration" key,
|
|
27
|
+
where "configuration" is a list of dictionaries containing "name" and "value" keys.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
str: The SERPAPI_KEY access token.
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
ValueError: If the SerpAPI integration has not been configured.
|
|
34
|
+
"""
|
|
35
|
+
logger.info("Entering get_serp_api_access_token")
|
|
36
|
+
SERPAPI_KEY = None
|
|
37
|
+
|
|
38
|
+
if tool_config:
|
|
39
|
+
logger.debug(f"Tool config provided: {tool_config}")
|
|
40
|
+
serpapi_config = next(
|
|
41
|
+
(item for item in tool_config if item.get("name") == "serpapi"), None
|
|
42
|
+
)
|
|
43
|
+
if serpapi_config:
|
|
44
|
+
config_map = {
|
|
45
|
+
item["name"]: item["value"]
|
|
46
|
+
for item in serpapi_config.get("configuration", [])
|
|
47
|
+
if item
|
|
48
|
+
}
|
|
49
|
+
SERPAPI_KEY = config_map.get("apiKey")
|
|
50
|
+
else:
|
|
51
|
+
logger.warning("No 'serpapi' config item found in tool_config.")
|
|
52
|
+
else:
|
|
53
|
+
logger.debug("No tool_config provided or it's None.")
|
|
54
|
+
|
|
55
|
+
SERPAPI_KEY = SERPAPI_KEY or os.getenv("SERPAPI_KEY")
|
|
56
|
+
if not SERPAPI_KEY:
|
|
57
|
+
logger.error("SerpAPI integration is not configured.")
|
|
58
|
+
raise ValueError(
|
|
59
|
+
"SerpAPI integration is not configured. Please configure the connection to SerpAPI in Integrations."
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
logger.info("Retrieved SERPAPI_KEY successfully.")
|
|
63
|
+
return SERPAPI_KEY
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@assistant_tool
|
|
69
|
+
async def search_google_maps(
|
|
70
|
+
query: str,
|
|
71
|
+
number_of_results: int = 3,
|
|
72
|
+
tool_config: Optional[List[Dict]] = None
|
|
73
|
+
) -> List[str]:
|
|
74
|
+
"""
|
|
75
|
+
Search Google Maps using SERP API and return the results as an array of serialized JSON strings.
|
|
76
|
+
|
|
77
|
+
Parameters:
|
|
78
|
+
- query (str): The search query.
|
|
79
|
+
- number_of_results (int): The number of results to return.
|
|
80
|
+
"""
|
|
81
|
+
logger.info("Entering search_google_maps")
|
|
82
|
+
if not query:
|
|
83
|
+
logger.warning("Empty query string provided for search_google_maps.")
|
|
84
|
+
return []
|
|
85
|
+
|
|
86
|
+
SERPAPI_KEY = get_serp_api_access_token(tool_config)
|
|
87
|
+
params = {
|
|
88
|
+
"q": query,
|
|
89
|
+
"num": number_of_results,
|
|
90
|
+
"api_key": SERPAPI_KEY,
|
|
91
|
+
"engine": "google_maps"
|
|
92
|
+
}
|
|
93
|
+
url = "https://serpapi.com/search"
|
|
94
|
+
|
|
95
|
+
logger.debug(f"Searching Google Maps with params: {params}")
|
|
96
|
+
try:
|
|
97
|
+
async with aiohttp.ClientSession() as session:
|
|
98
|
+
async with session.get(url, params=params) as response:
|
|
99
|
+
logger.debug(f"Received status: {response.status}")
|
|
100
|
+
result = await response.json()
|
|
101
|
+
if response.status != 200:
|
|
102
|
+
logger.warning(f"Non-200 response from SERP API: {result}")
|
|
103
|
+
return [json.dumps({"error": result})]
|
|
104
|
+
|
|
105
|
+
serialized_results = [json.dumps(item) for item in result.get('local_results', [])]
|
|
106
|
+
logger.info(f"Returning {len(serialized_results)} map results.")
|
|
107
|
+
return serialized_results
|
|
108
|
+
except Exception as e:
|
|
109
|
+
logger.exception("Exception during search_google_maps request.")
|
|
110
|
+
return [json.dumps({"error": str(e)})]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@assistant_tool
|
|
114
|
+
async def search_google_news(
|
|
115
|
+
query: str,
|
|
116
|
+
number_of_results: int = 3,
|
|
117
|
+
tool_config: Optional[List[Dict]] = None
|
|
118
|
+
) -> List[str]:
|
|
119
|
+
"""
|
|
120
|
+
Search Google News using SERP API and return the results as an array of serialized JSON strings.
|
|
121
|
+
|
|
122
|
+
Parameters:
|
|
123
|
+
- query (str): The search query.
|
|
124
|
+
- number_of_results (int): The number of results to return.
|
|
125
|
+
"""
|
|
126
|
+
logger.info("Entering search_google_news")
|
|
127
|
+
if not query:
|
|
128
|
+
logger.warning("Empty query string provided for search_google_news.")
|
|
129
|
+
return []
|
|
130
|
+
|
|
131
|
+
SERPAPI_KEY = get_serp_api_access_token(tool_config)
|
|
132
|
+
params = {
|
|
133
|
+
"q": query,
|
|
134
|
+
"num": number_of_results,
|
|
135
|
+
"api_key": SERPAPI_KEY,
|
|
136
|
+
"engine": "google_news"
|
|
137
|
+
}
|
|
138
|
+
url = "https://serpapi.com/search"
|
|
139
|
+
|
|
140
|
+
logger.debug(f"Searching Google News with params: {params}")
|
|
141
|
+
try:
|
|
142
|
+
async with aiohttp.ClientSession() as session:
|
|
143
|
+
async with session.get(url, params=params) as response:
|
|
144
|
+
logger.debug(f"Received status: {response.status}")
|
|
145
|
+
result = await response.json()
|
|
146
|
+
if response.status != 200:
|
|
147
|
+
logger.warning(f"Non-200 response from SERP API: {result}")
|
|
148
|
+
return [json.dumps({"error": result})]
|
|
149
|
+
|
|
150
|
+
serialized_results = [json.dumps(item) for item in result.get('news_results', [])]
|
|
151
|
+
logger.info(f"Returning {len(serialized_results)} news results.")
|
|
152
|
+
return serialized_results
|
|
153
|
+
except Exception as e:
|
|
154
|
+
logger.exception("Exception during search_google_news request.")
|
|
155
|
+
return [json.dumps({"error": str(e)})]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@assistant_tool
|
|
159
|
+
async def search_job_postings(
|
|
160
|
+
query: str,
|
|
161
|
+
number_of_results: int,
|
|
162
|
+
tool_config: Optional[List[Dict]] = None
|
|
163
|
+
) -> List[str]:
|
|
164
|
+
"""
|
|
165
|
+
Search for job postings using SERP API and return the results as an array of serialized JSON strings.
|
|
166
|
+
|
|
167
|
+
Parameters:
|
|
168
|
+
- query (str): The search query.
|
|
169
|
+
- number_of_results (int): The number of results to return.
|
|
170
|
+
"""
|
|
171
|
+
logger.info("Entering search_job_postings")
|
|
172
|
+
if not query:
|
|
173
|
+
logger.warning("Empty query string provided for search_job_postings.")
|
|
174
|
+
return []
|
|
175
|
+
|
|
176
|
+
SERPAPI_KEY = get_serp_api_access_token(tool_config)
|
|
177
|
+
params = {
|
|
178
|
+
"q": query,
|
|
179
|
+
"num": number_of_results,
|
|
180
|
+
"api_key": SERPAPI_KEY,
|
|
181
|
+
"engine": "google_jobs"
|
|
182
|
+
}
|
|
183
|
+
url = "https://serpapi.com/search"
|
|
184
|
+
|
|
185
|
+
logger.debug(f"Searching Google Jobs with params: {params}")
|
|
186
|
+
try:
|
|
187
|
+
async with aiohttp.ClientSession() as session:
|
|
188
|
+
async with session.get(url, params=params) as response:
|
|
189
|
+
logger.debug(f"Received status: {response.status}")
|
|
190
|
+
result = await response.json()
|
|
191
|
+
if response.status != 200:
|
|
192
|
+
logger.warning(f"Non-200 response from SERP API: {result}")
|
|
193
|
+
return [json.dumps({"error": result})]
|
|
194
|
+
|
|
195
|
+
serialized_results = [json.dumps(item) for item in result.get('jobs_results', [])]
|
|
196
|
+
logger.info(f"Returning {len(serialized_results)} job posting results.")
|
|
197
|
+
return serialized_results
|
|
198
|
+
except Exception as e:
|
|
199
|
+
logger.exception("Exception during search_job_postings request.")
|
|
200
|
+
return [json.dumps({"error": str(e)})]
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@assistant_tool
|
|
204
|
+
async def search_google_images(
|
|
205
|
+
query: str,
|
|
206
|
+
number_of_results: int,
|
|
207
|
+
tool_config: Optional[List[Dict]] = None
|
|
208
|
+
) -> List[str]:
|
|
209
|
+
"""
|
|
210
|
+
Search Google Images using SERP API and return the results as an array of serialized JSON strings.
|
|
211
|
+
|
|
212
|
+
Parameters:
|
|
213
|
+
- query (str): The search query.
|
|
214
|
+
- number_of_results (int): The number of results to return.
|
|
215
|
+
"""
|
|
216
|
+
logger.info("Entering search_google_images")
|
|
217
|
+
if not query:
|
|
218
|
+
logger.warning("Empty query string provided for search_google_images.")
|
|
219
|
+
return []
|
|
220
|
+
|
|
221
|
+
SERPAPI_KEY = get_serp_api_access_token(tool_config)
|
|
222
|
+
params = {
|
|
223
|
+
"q": query,
|
|
224
|
+
"num": number_of_results,
|
|
225
|
+
"api_key": SERPAPI_KEY,
|
|
226
|
+
"engine": "google_images"
|
|
227
|
+
}
|
|
228
|
+
url = "https://serpapi.com/search"
|
|
229
|
+
|
|
230
|
+
logger.debug(f"Searching Google Images with params: {params}")
|
|
231
|
+
try:
|
|
232
|
+
async with aiohttp.ClientSession() as session:
|
|
233
|
+
async with session.get(url, params=params) as response:
|
|
234
|
+
logger.debug(f"Received status: {response.status}")
|
|
235
|
+
result = await response.json()
|
|
236
|
+
if response.status != 200:
|
|
237
|
+
logger.warning(f"Non-200 response from SERP API: {result}")
|
|
238
|
+
return [json.dumps({"error": result})]
|
|
239
|
+
|
|
240
|
+
serialized_results = [json.dumps(item) for item in result.get('images_results', [])]
|
|
241
|
+
logger.info(f"Returning {len(serialized_results)} image results.")
|
|
242
|
+
return serialized_results
|
|
243
|
+
except Exception as e:
|
|
244
|
+
logger.exception("Exception during search_google_images request.")
|
|
245
|
+
return [json.dumps({"error": str(e)})]
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@assistant_tool
|
|
249
|
+
async def search_google_videos(
|
|
250
|
+
query: str,
|
|
251
|
+
number_of_results: int,
|
|
252
|
+
tool_config: Optional[List[Dict]] = None
|
|
253
|
+
) -> List[str]:
|
|
254
|
+
"""
|
|
255
|
+
Search Google Videos using SERP API and return the results as an array of serialized JSON strings.
|
|
256
|
+
|
|
257
|
+
Parameters:
|
|
258
|
+
- query (str): The search query.
|
|
259
|
+
- number_of_results (int): The number of results to return.
|
|
260
|
+
"""
|
|
261
|
+
logger.info("Entering search_google_videos")
|
|
262
|
+
if not query:
|
|
263
|
+
logger.warning("Empty query string provided for search_google_videos.")
|
|
264
|
+
return []
|
|
265
|
+
|
|
266
|
+
SERPAPI_KEY = get_serp_api_access_token(tool_config)
|
|
267
|
+
params = {
|
|
268
|
+
"q": query,
|
|
269
|
+
"num": number_of_results,
|
|
270
|
+
"api_key": SERPAPI_KEY,
|
|
271
|
+
"engine": "google_videos"
|
|
272
|
+
}
|
|
273
|
+
url = "https://serpapi.com/search"
|
|
274
|
+
|
|
275
|
+
logger.debug(f"Searching Google Videos with params: {params}")
|
|
276
|
+
try:
|
|
277
|
+
async with aiohttp.ClientSession() as session:
|
|
278
|
+
async with session.get(url, params=params) as response:
|
|
279
|
+
logger.debug(f"Received status: {response.status}")
|
|
280
|
+
result = await response.json()
|
|
281
|
+
if response.status != 200:
|
|
282
|
+
logger.warning(f"Non-200 response from SERP API: {result}")
|
|
283
|
+
return [json.dumps({"error": result})]
|
|
284
|
+
|
|
285
|
+
serialized_results = [json.dumps(item) for item in result.get('video_results', [])]
|
|
286
|
+
logger.info(f"Returning {len(serialized_results)} video results.")
|
|
287
|
+
return serialized_results
|
|
288
|
+
except Exception as e:
|
|
289
|
+
logger.exception("Exception during search_google_videos request.")
|
|
290
|
+
return [json.dumps({"error": str(e)})]
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
import aiohttp
|
|
6
|
+
|
|
7
|
+
from dhisana.utils.assistant_tool_tag import assistant_tool
|
|
8
|
+
from dhisana.utils.cache_output_tools import cache_output, retrieve_output
|
|
9
|
+
from dhisana.utils.serpapi_google_search import get_serp_api_access_token
|
|
10
|
+
|
|
11
|
+
logging.basicConfig(level=logging.INFO)
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _normalise_job_result(raw: Dict[str, Any]) -> Dict[str, Any]:
|
|
16
|
+
"""Convert a SerpApi jobs result to a simplified schema."""
|
|
17
|
+
# ...existing code...
|
|
18
|
+
apply_link = ""
|
|
19
|
+
apply_options = raw.get("apply_options") or raw.get("apply_links") or []
|
|
20
|
+
if isinstance(apply_options, list) and apply_options:
|
|
21
|
+
first = apply_options[0]
|
|
22
|
+
if isinstance(first, dict):
|
|
23
|
+
apply_link = first.get("link") or first.get("apply_link") or ""
|
|
24
|
+
if isinstance(apply_options, dict):
|
|
25
|
+
apply_link = apply_options.get("link") or apply_options.get("apply_link") or ""
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
"job_title": raw.get("title", ""),
|
|
29
|
+
"organization_name": raw.get("company_name") or raw.get("company", ""),
|
|
30
|
+
"location": raw.get("location", ""),
|
|
31
|
+
"via": raw.get("via", ""),
|
|
32
|
+
"description": raw.get("description", ""),
|
|
33
|
+
"job_posting_url": raw.get("job_highlight_url")
|
|
34
|
+
or raw.get("apply_link")
|
|
35
|
+
or apply_link
|
|
36
|
+
or raw.get("link", ""),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@assistant_tool
|
|
41
|
+
async def search_google_jobs_serpapi(
|
|
42
|
+
query: str,
|
|
43
|
+
number_of_results: int = 10,
|
|
44
|
+
offset: int = 0,
|
|
45
|
+
tool_config: Optional[List[Dict]] = None,
|
|
46
|
+
location: Optional[str] = None,
|
|
47
|
+
) -> List[str]:
|
|
48
|
+
"""Search Google Jobs via SerpApi and return normalised JSON strings."""
|
|
49
|
+
if not query:
|
|
50
|
+
logger.warning("Empty query provided to search_google_jobs_serpapi")
|
|
51
|
+
return []
|
|
52
|
+
|
|
53
|
+
cache_key = f"jobs_serpapi_{query}_{number_of_results}_{offset}_{location or ''}"
|
|
54
|
+
cached = retrieve_output("search_google_jobs_serpapi", cache_key)
|
|
55
|
+
if cached is not None:
|
|
56
|
+
return cached
|
|
57
|
+
|
|
58
|
+
api_key = get_serp_api_access_token(tool_config)
|
|
59
|
+
page_size = 10
|
|
60
|
+
collected: List[Dict[str, Any]] = []
|
|
61
|
+
next_page_token = None
|
|
62
|
+
|
|
63
|
+
async with aiohttp.ClientSession() as session:
|
|
64
|
+
while len(collected) < number_of_results:
|
|
65
|
+
to_fetch = min(page_size, number_of_results - len(collected))
|
|
66
|
+
params = {
|
|
67
|
+
"engine": "google_jobs",
|
|
68
|
+
"q": query,
|
|
69
|
+
"api_key": api_key,
|
|
70
|
+
"num": to_fetch,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if offset > 0 and next_page_token is None:
|
|
74
|
+
# If offset is provided but we don't have a token yet,
|
|
75
|
+
# we can't properly paginate. Log a warning.
|
|
76
|
+
logger.warning("Offset provided but next_page_token not available. Results may be incomplete.")
|
|
77
|
+
|
|
78
|
+
if next_page_token:
|
|
79
|
+
params["next_page_token"] = next_page_token
|
|
80
|
+
|
|
81
|
+
if location:
|
|
82
|
+
params["location"] = location
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
async with session.get("https://serpapi.com/search", params=params) as resp:
|
|
86
|
+
if resp.status != 200:
|
|
87
|
+
try:
|
|
88
|
+
err = await resp.json()
|
|
89
|
+
except Exception:
|
|
90
|
+
err = await resp.text()
|
|
91
|
+
logger.warning("SerpApi jobs error: %s", err)
|
|
92
|
+
return [json.dumps({"error": err})]
|
|
93
|
+
payload = await resp.json()
|
|
94
|
+
except Exception as exc:
|
|
95
|
+
logger.exception("SerpApi jobs request failed")
|
|
96
|
+
return [json.dumps({"error": str(exc)})]
|
|
97
|
+
|
|
98
|
+
jobs = payload.get("jobs_results", [])
|
|
99
|
+
if not jobs:
|
|
100
|
+
break
|
|
101
|
+
|
|
102
|
+
collected.extend(jobs)
|
|
103
|
+
|
|
104
|
+
# Get the next_page_token for pagination
|
|
105
|
+
next_page_token = payload.get("serpapi_pagination", {}).get("next_page_token")
|
|
106
|
+
if not next_page_token:
|
|
107
|
+
break
|
|
108
|
+
|
|
109
|
+
# If we've collected enough results, stop pagination
|
|
110
|
+
if len(collected) >= number_of_results:
|
|
111
|
+
break
|
|
112
|
+
|
|
113
|
+
normalised = [_normalise_job_result(j) for j in collected[:number_of_results]]
|
|
114
|
+
serialised = [json.dumps(item) for item in normalised]
|
|
115
|
+
cache_output("search_google_jobs_serpapi", cache_key, serialised)
|
|
116
|
+
logger.info("Returned %d job results for '%s'", len(serialised), query)
|
|
117
|
+
return serialised
|