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.
Files changed (102) hide show
  1. dhisana/__init__.py +1 -0
  2. dhisana/cli/__init__.py +1 -0
  3. dhisana/cli/cli.py +20 -0
  4. dhisana/cli/datasets.py +27 -0
  5. dhisana/cli/models.py +26 -0
  6. dhisana/cli/predictions.py +20 -0
  7. dhisana/schemas/__init__.py +1 -0
  8. dhisana/schemas/common.py +399 -0
  9. dhisana/schemas/sales.py +965 -0
  10. dhisana/ui/__init__.py +1 -0
  11. dhisana/ui/components.py +472 -0
  12. dhisana/utils/__init__.py +1 -0
  13. dhisana/utils/add_mapping.py +352 -0
  14. dhisana/utils/agent_tools.py +51 -0
  15. dhisana/utils/apollo_tools.py +1597 -0
  16. dhisana/utils/assistant_tool_tag.py +4 -0
  17. dhisana/utils/built_with_api_tools.py +282 -0
  18. dhisana/utils/cache_output_tools.py +98 -0
  19. dhisana/utils/cache_output_tools_local.py +78 -0
  20. dhisana/utils/check_email_validity_tools.py +717 -0
  21. dhisana/utils/check_for_intent_signal.py +107 -0
  22. dhisana/utils/check_linkedin_url_validity.py +209 -0
  23. dhisana/utils/clay_tools.py +43 -0
  24. dhisana/utils/clean_properties.py +135 -0
  25. dhisana/utils/company_utils.py +60 -0
  26. dhisana/utils/compose_salesnav_query.py +259 -0
  27. dhisana/utils/compose_search_query.py +759 -0
  28. dhisana/utils/compose_three_step_workflow.py +234 -0
  29. dhisana/utils/composite_tools.py +137 -0
  30. dhisana/utils/dataframe_tools.py +237 -0
  31. dhisana/utils/domain_parser.py +45 -0
  32. dhisana/utils/email_body_utils.py +72 -0
  33. dhisana/utils/email_parse_helpers.py +132 -0
  34. dhisana/utils/email_provider.py +375 -0
  35. dhisana/utils/enrich_lead_information.py +933 -0
  36. dhisana/utils/extract_email_content_for_llm.py +101 -0
  37. dhisana/utils/fetch_openai_config.py +129 -0
  38. dhisana/utils/field_validators.py +426 -0
  39. dhisana/utils/g2_tools.py +104 -0
  40. dhisana/utils/generate_content.py +41 -0
  41. dhisana/utils/generate_custom_message.py +271 -0
  42. dhisana/utils/generate_email.py +278 -0
  43. dhisana/utils/generate_email_response.py +465 -0
  44. dhisana/utils/generate_flow.py +102 -0
  45. dhisana/utils/generate_leads_salesnav.py +303 -0
  46. dhisana/utils/generate_linkedin_connect_message.py +224 -0
  47. dhisana/utils/generate_linkedin_response_message.py +317 -0
  48. dhisana/utils/generate_structured_output_internal.py +462 -0
  49. dhisana/utils/google_custom_search.py +267 -0
  50. dhisana/utils/google_oauth_tools.py +727 -0
  51. dhisana/utils/google_workspace_tools.py +1294 -0
  52. dhisana/utils/hubspot_clearbit.py +96 -0
  53. dhisana/utils/hubspot_crm_tools.py +2440 -0
  54. dhisana/utils/instantly_tools.py +149 -0
  55. dhisana/utils/linkedin_crawler.py +168 -0
  56. dhisana/utils/lusha_tools.py +333 -0
  57. dhisana/utils/mailgun_tools.py +156 -0
  58. dhisana/utils/mailreach_tools.py +123 -0
  59. dhisana/utils/microsoft365_tools.py +455 -0
  60. dhisana/utils/openai_assistant_and_file_utils.py +267 -0
  61. dhisana/utils/openai_helpers.py +977 -0
  62. dhisana/utils/openapi_spec_to_tools.py +45 -0
  63. dhisana/utils/openapi_tool/__init__.py +1 -0
  64. dhisana/utils/openapi_tool/api_models.py +633 -0
  65. dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +271 -0
  66. dhisana/utils/openapi_tool/openapi_tool.py +319 -0
  67. dhisana/utils/parse_linkedin_messages_txt.py +100 -0
  68. dhisana/utils/profile.py +37 -0
  69. dhisana/utils/proxy_curl_tools.py +1226 -0
  70. dhisana/utils/proxycurl_search_leads.py +426 -0
  71. dhisana/utils/python_function_to_tools.py +83 -0
  72. dhisana/utils/research_lead.py +176 -0
  73. dhisana/utils/sales_navigator_crawler.py +1103 -0
  74. dhisana/utils/salesforce_crm_tools.py +477 -0
  75. dhisana/utils/search_router.py +131 -0
  76. dhisana/utils/search_router_jobs.py +51 -0
  77. dhisana/utils/sendgrid_tools.py +162 -0
  78. dhisana/utils/serarch_router_local_business.py +75 -0
  79. dhisana/utils/serpapi_additional_tools.py +290 -0
  80. dhisana/utils/serpapi_google_jobs.py +117 -0
  81. dhisana/utils/serpapi_google_search.py +188 -0
  82. dhisana/utils/serpapi_local_business_search.py +129 -0
  83. dhisana/utils/serpapi_search_tools.py +852 -0
  84. dhisana/utils/serperdev_google_jobs.py +125 -0
  85. dhisana/utils/serperdev_local_business.py +154 -0
  86. dhisana/utils/serperdev_search.py +233 -0
  87. dhisana/utils/smtp_email_tools.py +582 -0
  88. dhisana/utils/test_connect.py +2087 -0
  89. dhisana/utils/trasform_json.py +173 -0
  90. dhisana/utils/web_download_parse_tools.py +189 -0
  91. dhisana/utils/workflow_code_model.py +5 -0
  92. dhisana/utils/zoominfo_tools.py +357 -0
  93. dhisana/workflow/__init__.py +1 -0
  94. dhisana/workflow/agent.py +18 -0
  95. dhisana/workflow/flow.py +44 -0
  96. dhisana/workflow/task.py +43 -0
  97. dhisana/workflow/test.py +90 -0
  98. dhisana-0.0.1.dev243.dist-info/METADATA +43 -0
  99. dhisana-0.0.1.dev243.dist-info/RECORD +102 -0
  100. dhisana-0.0.1.dev243.dist-info/WHEEL +5 -0
  101. dhisana-0.0.1.dev243.dist-info/entry_points.txt +2 -0
  102. dhisana-0.0.1.dev243.dist-info/top_level.txt +1 -0
@@ -0,0 +1,125 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ import aiohttp
7
+
8
+ from dhisana.utils.assistant_tool_tag import assistant_tool
9
+ from dhisana.utils.cache_output_tools import cache_output, retrieve_output
10
+
11
+ logging.basicConfig(level=logging.INFO)
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def get_serper_dev_api_access_token(tool_config: Optional[List[Dict]] = None) -> str:
16
+ """Retrieve the SERPER_API_KEY from tool_config or environment.
17
+
18
+ Raises:
19
+ ValueError: If the Serper.dev integration has not been configured.
20
+ """
21
+ key = None
22
+ if tool_config:
23
+ cfg = next((c for c in tool_config if c.get("name") == "serperdev"), None)
24
+ if cfg:
25
+ kv = {i["name"]: i["value"] for i in cfg.get("configuration", [])}
26
+ key = kv.get("apiKey")
27
+ key = key or os.getenv("SERPER_API_KEY")
28
+ if not key:
29
+ raise ValueError(
30
+ "Serper.dev integration is not configured. Please configure the connection to Serper.dev in Integrations."
31
+ )
32
+ return key
33
+
34
+
35
+ def _normalise_job_result(raw: Dict[str, Any]) -> Dict[str, Any]:
36
+ """Map a Serper job result onto a simplified schema."""
37
+ apply_link = ""
38
+ if isinstance(raw.get("apply_link"), str):
39
+ apply_link = raw.get("apply_link")
40
+ apply_options = raw.get("apply_options") or raw.get("apply_links") or []
41
+ if not apply_link and isinstance(apply_options, list) and apply_options:
42
+ first = apply_options[0]
43
+ if isinstance(first, dict):
44
+ apply_link = first.get("link") or first.get("apply_link") or ""
45
+
46
+ return {
47
+ "job_title": raw.get("title", ""),
48
+ "company_name": raw.get("company_name") or raw.get("company", ""),
49
+ "location": raw.get("location", ""),
50
+ "via": raw.get("via", ""),
51
+ "description": raw.get("description", ""),
52
+ "job_posting_url": raw.get("link") or apply_link,
53
+ }
54
+
55
+
56
+ @assistant_tool
57
+ async def search_google_jobs_serper(
58
+ query: str,
59
+ number_of_results: int = 10,
60
+ offset: int = 0,
61
+ tool_config: Optional[List[Dict]] = None,
62
+ location: Optional[str] = None,
63
+ ) -> List[str]:
64
+ """Search Google Jobs via Serper.dev and return normalised JSON strings."""
65
+ if not query:
66
+ logger.warning("Empty query provided to search_google_jobs_serper")
67
+ return []
68
+
69
+ cache_key = f"jobs_serper_{query}_{number_of_results}_{offset}_{location or ''}"
70
+ cached = retrieve_output("search_google_jobs_serper", cache_key)
71
+ if cached is not None:
72
+ return cached
73
+
74
+ api_key = get_serper_dev_api_access_token(tool_config)
75
+ headers = {"X-API-KEY": api_key, "Content-Type": "application/json"}
76
+ url = "https://google.serper.dev/search" # ← fixed endpoint
77
+
78
+ page = offset + 1
79
+ collected: List[Dict[str, Any]] = []
80
+
81
+ async with aiohttp.ClientSession() as session:
82
+ while len(collected) < number_of_results:
83
+ payload = {
84
+ "q": query,
85
+ "page": page,
86
+ "type": "jobs", # keeps us in the Jobs vertical
87
+ "autocorrect": True,
88
+ "gl": "us",
89
+ "hl": "en",
90
+ }
91
+ if location:
92
+ payload["location"] = location
93
+ try:
94
+ async with session.post(url, headers=headers, json=payload) as resp:
95
+ if resp.status != 200:
96
+ try:
97
+ err = await resp.json()
98
+ except Exception:
99
+ err = await resp.text()
100
+ logger.warning("Serper jobs error: %s", err)
101
+ return [json.dumps({"error": err})]
102
+ data = await resp.json()
103
+ except Exception as exc:
104
+ logger.exception("Serper jobs request failed")
105
+ return [json.dumps({"error": str(exc)})]
106
+
107
+ jobs = (
108
+ data.get("jobs")
109
+ or data.get("job_results")
110
+ or data.get("jobs_results")
111
+ or []
112
+ )
113
+ if not jobs:
114
+ break
115
+ collected.extend(jobs)
116
+ if len(collected) >= number_of_results:
117
+ break
118
+ page += 1
119
+
120
+ serialised = [
121
+ json.dumps(_normalise_job_result(j)) for j in collected[:number_of_results]
122
+ ]
123
+ cache_output("search_google_jobs_serper", cache_key, serialised)
124
+ logger.info("Returned %d job results for '%s'", len(serialised), query)
125
+ return serialised
@@ -0,0 +1,154 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ import aiohttp
7
+
8
+ # Dhisana utils (adjust imports to your project structure)
9
+ from dhisana.utils.assistant_tool_tag import assistant_tool
10
+ from dhisana.utils.cache_output_tools import cache_output, retrieve_output
11
+
12
+ logger = logging.getLogger(__name__)
13
+ logging.basicConfig(level=logging.INFO)
14
+
15
+
16
+ # ────────────────────────────────────────────────────────────────
17
+ # 1. API-key retrieval helper (mirrors your existing pattern)
18
+ # ────────────────────────────────────────────────────────────────
19
+ def get_serper_dev_api_access_token(tool_config: Optional[List[Dict]] = None) -> str:
20
+ """
21
+ Grab SERPER_API_KEY from tool_config or environment.
22
+
23
+ Raises:
24
+ ValueError: If the Serper.dev integration has not been configured.
25
+ """
26
+ SERPER_API_KEY = None
27
+ if tool_config:
28
+ serper_cfg = next(
29
+ (c for c in tool_config if c.get("name") == "serperdev"), None
30
+ )
31
+ if serper_cfg:
32
+ kv = {i["name"]: i["value"] for i in serper_cfg.get("configuration", [])}
33
+ SERPER_API_KEY = kv.get("apiKey")
34
+ SERPER_API_KEY = SERPER_API_KEY or os.getenv("SERPER_API_KEY")
35
+ if not SERPER_API_KEY:
36
+ raise ValueError(
37
+ "Serper.dev integration is not configured. Please configure the connection to Serper.dev in Integrations."
38
+ )
39
+ return SERPER_API_KEY
40
+
41
+
42
+ # ────────────────────────────────────────────────────────────────
43
+ # 2. Result normaliser
44
+ # ────────────────────────────────────────────────────────────────
45
+ def _normalise_local_result_serper(raw: Dict[str, Any]) -> Dict[str, Any]:
46
+ """
47
+ Map a Serper 'places' / 'placeResults' item onto Dhisana's schema.
48
+ """
49
+ cid = raw.get("cid") or raw.get("placeId") or raw.get("place_id")
50
+ maps_url = f"https://maps.google.com/?cid={cid}" if cid else ""
51
+
52
+ return {
53
+ "full_name": raw.get("title", ""),
54
+ "organization_name": raw.get("title", ""),
55
+ "phone": raw.get("phoneNumber") or raw.get("phone", ""),
56
+ "organization_website": raw.get("website", ""),
57
+ "rating": raw.get("rating"),
58
+ "reviews": raw.get("reviews"),
59
+ "address": raw.get("address", ""),
60
+ "google_maps_url": maps_url,
61
+ }
62
+
63
+
64
+ # ────────────────────────────────────────────────────────────────
65
+ # 3. Search helper (decorated for Dhisana agents)
66
+ # ────────────────────────────────────────────────────────────────
67
+ @assistant_tool
68
+ async def search_local_business_serper(
69
+ query: str,
70
+ number_of_results: int = 20,
71
+ offset: int = 0,
72
+ tool_config: Optional[List[Dict]] = None,
73
+ location: Optional[str] = None,
74
+ ) -> List[str]:
75
+ """
76
+ Fetch Google-Maps local business results via Serper.dev and return a
77
+ List[str] of JSON-encoded business objects in Dhisana's schema.
78
+
79
+ Args:
80
+ query: Main search string (e.g. "coffee shops").
81
+ number_of_results: Total rows desired.
82
+ offset: Page offset (page index starts at 0).
83
+ tool_config: Optional Dhisana tool-config containing the API key.
84
+ location: Optional "San Jose, CA" style hint to refine results.
85
+ """
86
+ if not query:
87
+ logger.warning("Empty query.")
88
+ return []
89
+
90
+ # ── caching
91
+ cache_key = f"local_serper_{query}_{number_of_results}_{offset}_{location or ''}"
92
+ cached = retrieve_output("search_local_serper", cache_key)
93
+ if cached is not None:
94
+ return cached
95
+
96
+ api_key = get_serper_dev_api_access_token(tool_config)
97
+ headers = {"X-API-KEY": api_key, "Content-Type": "application/json"}
98
+ url = "https://google.serper.dev/places"
99
+
100
+ page_size = 20 # Serper returns ≤20 rows per page
101
+ page = offset + 1
102
+ collected: List[Dict[str, Any]] = []
103
+
104
+ async with aiohttp.ClientSession() as session:
105
+ while len(collected) < number_of_results:
106
+ payload = {
107
+ "q": query,
108
+ "page": page,
109
+ "type": "places", # explicit although /places path implies it
110
+ "autocorrect": True,
111
+ "gl": "us",
112
+ "hl": "en",
113
+ }
114
+ if location:
115
+ payload["location"] = location
116
+
117
+ try:
118
+ async with session.post(url, headers=headers, json=payload) as resp:
119
+ if resp.status != 200:
120
+ # Bubble up API errors
121
+ try:
122
+ err = await resp.json()
123
+ except Exception:
124
+ err = await resp.text()
125
+ logger.warning("Serper Places error: %s", err)
126
+ return [json.dumps({"error": err})]
127
+ data = await resp.json()
128
+ except Exception as exc:
129
+ logger.exception("Serper Places request failed.")
130
+ return [json.dumps({"error": str(exc)})]
131
+
132
+ # Handle both field names
133
+ places = (
134
+ data.get("places")
135
+ or data.get("placeResults")
136
+ or data.get("local_results")
137
+ or []
138
+ )
139
+ if not places:
140
+ break
141
+
142
+ collected.extend(places)
143
+ if len(collected) >= number_of_results:
144
+ break
145
+ page += 1
146
+
147
+ # normalise → serialise
148
+ serialised = [
149
+ json.dumps(_normalise_local_result_serper(p))
150
+ for p in collected[:number_of_results]
151
+ ]
152
+ cache_output("search_local_serper", cache_key, serialised)
153
+ logger.info("Returned %d local businesses for '%s'", len(serialised), query)
154
+ return serialised
@@ -0,0 +1,233 @@
1
+ import asyncio
2
+ import json
3
+ import os
4
+ from typing import Any, Dict, List, Optional
5
+ import aiohttp
6
+
7
+ import logging
8
+ logging.basicConfig(level=logging.INFO)
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ # If you have these utils in your project, import them; otherwise, remove them or replace them.
13
+ from dhisana.utils.cache_output_tools import cache_output, retrieve_output
14
+
15
+
16
+ def get_serper_dev_api_access_token(tool_config: Optional[List[Dict]] = None) -> str:
17
+ """
18
+ Retrieves the Serper.dev API access token from the provided tool configuration
19
+ or from the SERPER_API_KEY environment variable.
20
+
21
+ Args:
22
+ tool_config (list): A list of dictionaries containing the tool configuration.
23
+ Each dictionary should have a "name" key and a "configuration" key,
24
+ where "configuration" is a list of dictionaries containing "name" and "value" keys.
25
+
26
+ Returns:
27
+ str: The Serper.dev API access token.
28
+
29
+ Raises:
30
+ ValueError: If the Serper.dev integration has not been configured.
31
+ """
32
+ logger.info("Entering get_serper_dev_api_access_token")
33
+ SERPER_API_KEY = None
34
+
35
+ if tool_config:
36
+ logger.debug(f"Tool config provided: {tool_config}")
37
+ serper_config = next(
38
+ (item for item in tool_config if item.get("name") == "serperdev"), None
39
+ )
40
+ if serper_config:
41
+ config_map = {
42
+ item["name"]: item["value"]
43
+ for item in serper_config.get("configuration", [])
44
+ if item
45
+ }
46
+ SERPER_API_KEY = config_map.get("apiKey")
47
+ else:
48
+ logger.warning("No 'serperdev' config item found in tool_config.")
49
+ else:
50
+ logger.debug("No tool_config provided or it's None.")
51
+
52
+ SERPER_API_KEY = SERPER_API_KEY or os.getenv("SERPER_API_KEY")
53
+ if not SERPER_API_KEY:
54
+ logger.error("Serper.dev integration is not configured.")
55
+ raise ValueError(
56
+ "Serper.dev integration is not configured. Please configure the connection to Serper.dev in Integrations."
57
+ )
58
+
59
+ logger.info("Retrieved SERPER_API_KEY successfully.")
60
+ return SERPER_API_KEY
61
+
62
+
63
+
64
+ async def search_google_serper(
65
+ query: str,
66
+ number_of_results: int = 10,
67
+ offset: int = 0,
68
+ tool_config: Optional[List[Dict]] = None,
69
+ as_oq: Optional[str] = None
70
+ ) -> List[str]:
71
+ """
72
+ Search Google using Serper.dev. Mimics the signature and usage of the old SerpAPI function,
73
+ and normalizes the response JSON objects so that they contain:
74
+ - "title"
75
+ - "link"
76
+ - "snippet"
77
+ - "position"
78
+
79
+ This ensures consistency with SerpAPI-based code.
80
+
81
+ Parameters:
82
+ - query (str): The search query.
83
+ - number_of_results (int): The total number of results to return. Default is 10.
84
+ - offset (int): The "page offset" to start from (used to compute the page).
85
+ - tool_config (Optional[List[Dict]]): Configuration containing the Serper.dev API token, etc.
86
+ - as_oq (Optional[str]): Optional additional query terms, appended to 'query'.
87
+
88
+ Returns:
89
+ - List[str]: A list of organic search results, each serialized as a JSON string
90
+ with "title", "link", "snippet", and "position".
91
+ """
92
+ logger.info("Entering search_google_serper")
93
+
94
+ if not query:
95
+ logger.warning("Empty query string provided.")
96
+ return []
97
+
98
+ # Combine main query with optional terms
99
+ full_query = query
100
+ if as_oq:
101
+ full_query += f" {as_oq}"
102
+
103
+ # Check cache
104
+ cache_key = f"{full_query}_{number_of_results}_{offset}"
105
+ cached_response = retrieve_output("search_google_serper", cache_key)
106
+ if cached_response is not None:
107
+ logger.info("Cache hit for search_google_serper.")
108
+ return cached_response
109
+
110
+ # Retrieve your Serper.dev API key (replace with your own function if needed)
111
+ SERPER_API_KEY = get_serper_dev_api_access_token(tool_config)
112
+
113
+ url = "https://google.serper.dev/search"
114
+ headers = {
115
+ "X-API-KEY": SERPER_API_KEY,
116
+ "Content-Type": "application/json"
117
+ }
118
+
119
+ # Serper.dev uses 'page' to paginate. We need to calculate the page number based on offset.
120
+ # Serper.dev uses 1-based indexing for pages, so we need to adjust accordingly
121
+ # e.g., if offset is 0, we want page 1; if offset is 10, we want page 2, etc.
122
+ # Assuming number_of_results is the number of results per page.
123
+ # If offset is 0, we want the first page,
124
+ # if offset is 10 and number_of_results is 10, we want the second page, etc.
125
+ # This means we can calculate the page as follows:
126
+ # page = (offset // number_of_results) + 1
127
+ # If offset is 0, page will be 1; if offset is 10 and number_of_results is 10, page will be 2.
128
+ # This is consistent with how Serper.dev handles pagination.
129
+ page = 1 if offset == 0 else (offset // number_of_results) + 1
130
+ all_results: List[Dict[str, Any]] = []
131
+
132
+ # We'll collect results from "organic", converting each to a SerpAPI-like format.
133
+ timeout = aiohttp.ClientTimeout(total=30, connect=10)
134
+ async with aiohttp.ClientSession(timeout=timeout) as session:
135
+ while len(all_results) < number_of_results:
136
+ payload = {
137
+ "q": full_query,
138
+ "gl": "us", # geolocation
139
+ "hl": "en", # language
140
+ "autocorrect": True,
141
+ "page": page,
142
+ "num": number_of_results,
143
+ "type": "search" # or 'news', 'images', etc., if needed
144
+ }
145
+
146
+ logger.debug(f"Requesting Serper.dev page {page} for query '{full_query}'.")
147
+ for attempt in range(3):
148
+ try:
149
+ async with session.post(url, headers=headers, json=payload) as response:
150
+ if response.status != 200:
151
+ try:
152
+ error_content = await response.json()
153
+ except Exception:
154
+ error_content = await response.text()
155
+ logger.warning(
156
+ "Non-200 response from Serper.dev: %s (status=%s)",
157
+ error_content,
158
+ response.status,
159
+ )
160
+ return [json.dumps({"error": error_content})]
161
+
162
+ result_json = await response.json()
163
+ break
164
+ except asyncio.TimeoutError:
165
+ logger.warning(
166
+ "Timeout contacting Serper.dev (attempt %s/3) for query '%s'",
167
+ attempt + 1,
168
+ full_query,
169
+ )
170
+ except aiohttp.ClientError as exc:
171
+ logger.warning(
172
+ "Client error contacting Serper.dev (attempt %s/3): %s",
173
+ attempt + 1,
174
+ exc,
175
+ )
176
+ if attempt == 2:
177
+ logger.exception("Exception during Serper.dev request.")
178
+ return [json.dumps({"error": str(exc)})]
179
+ except Exception as e:
180
+ logger.exception("Unexpected exception during Serper.dev request.")
181
+ return [json.dumps({"error": str(e)})]
182
+ else:
183
+ # Successful request, exit retry loop
184
+ break
185
+ await asyncio.sleep(2 ** attempt)
186
+ else:
187
+ logger.error(
188
+ "Failed to retrieve data from Serper.dev after multiple attempts for query '%s'",
189
+ full_query,
190
+ )
191
+ return [
192
+ json.dumps(
193
+ {
194
+ "error": "Serper.dev request timed out after multiple attempts.",
195
+ }
196
+ )
197
+ ]
198
+
199
+ organic_results = result_json.get("organic", [])
200
+ if not organic_results:
201
+ logger.debug("No more organic results returned; stopping.")
202
+ break
203
+
204
+ all_results.extend(organic_results)
205
+ page += 1
206
+
207
+ if len(all_results) >= number_of_results:
208
+ break
209
+
210
+ # Limit to the requested number_of_results
211
+ all_results = all_results[:number_of_results]
212
+
213
+ # Convert each Serper.dev result to a SerpAPI-like format
214
+ # SerpAPI typically returns objects with keys: "position", "title", "link", "snippet", etc.
215
+ normalized_results = []
216
+ for idx, item in enumerate(all_results):
217
+ # item from Serper.dev might have: { "title": "...", "link": "...", "snippet": "..." }
218
+ # If the field name for snippet is different, change accordingly.
219
+ # But as of serper.dev docs, "snippet" is used.
220
+ normalized_item = {
221
+ "position": idx + 1,
222
+ "title": item.get("title", ""),
223
+ "link": item.get("link", ""),
224
+ "snippet": item.get("snippet", ""),
225
+ # Copy any other fields if you want them
226
+ }
227
+ normalized_results.append(json.dumps(normalized_item))
228
+
229
+ logger.info(f"Found {len(normalized_results)} normalized results for query '{full_query}'.")
230
+ cache_output("search_google_serper", cache_key, normalized_results)
231
+
232
+ return normalized_results
233
+