universal-mcp-applications 0.1.33__py3-none-any.whl → 0.1.39rc16__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.

Potentially problematic release.


This version of universal-mcp-applications might be problematic. Click here for more details.

Files changed (119) hide show
  1. universal_mcp/applications/BEST_PRACTICES.md +1 -1
  2. universal_mcp/applications/ahrefs/app.py +92 -238
  3. universal_mcp/applications/airtable/app.py +36 -135
  4. universal_mcp/applications/apollo/app.py +124 -477
  5. universal_mcp/applications/asana/app.py +605 -1755
  6. universal_mcp/applications/aws_s3/app.py +63 -119
  7. universal_mcp/applications/bill/app.py +644 -2055
  8. universal_mcp/applications/box/app.py +1246 -4159
  9. universal_mcp/applications/braze/app.py +410 -1476
  10. universal_mcp/applications/browser_use/README.md +15 -1
  11. universal_mcp/applications/browser_use/__init__.py +1 -0
  12. universal_mcp/applications/browser_use/app.py +91 -26
  13. universal_mcp/applications/cal_com_v2/app.py +207 -625
  14. universal_mcp/applications/calendly/app.py +103 -242
  15. universal_mcp/applications/canva/app.py +75 -140
  16. universal_mcp/applications/clickup/app.py +331 -798
  17. universal_mcp/applications/coda/app.py +240 -520
  18. universal_mcp/applications/confluence/app.py +497 -1285
  19. universal_mcp/applications/contentful/app.py +40 -155
  20. universal_mcp/applications/crustdata/app.py +44 -123
  21. universal_mcp/applications/dialpad/app.py +451 -924
  22. universal_mcp/applications/digitalocean/app.py +2071 -6082
  23. universal_mcp/applications/domain_checker/app.py +3 -54
  24. universal_mcp/applications/e2b/app.py +17 -68
  25. universal_mcp/applications/elevenlabs/README.md +27 -3
  26. universal_mcp/applications/elevenlabs/app.py +741 -74
  27. universal_mcp/applications/exa/README.md +8 -4
  28. universal_mcp/applications/exa/app.py +415 -186
  29. universal_mcp/applications/falai/README.md +5 -7
  30. universal_mcp/applications/falai/app.py +156 -232
  31. universal_mcp/applications/figma/app.py +91 -175
  32. universal_mcp/applications/file_system/app.py +2 -13
  33. universal_mcp/applications/firecrawl/app.py +198 -176
  34. universal_mcp/applications/fireflies/app.py +59 -281
  35. universal_mcp/applications/fpl/app.py +92 -529
  36. universal_mcp/applications/fpl/utils/fixtures.py +15 -49
  37. universal_mcp/applications/fpl/utils/helper.py +25 -89
  38. universal_mcp/applications/fpl/utils/league_utils.py +20 -64
  39. universal_mcp/applications/ghost_content/app.py +70 -179
  40. universal_mcp/applications/github/app.py +30 -67
  41. universal_mcp/applications/gong/app.py +142 -302
  42. universal_mcp/applications/google_calendar/app.py +26 -78
  43. universal_mcp/applications/google_docs/README.md +15 -14
  44. universal_mcp/applications/google_docs/app.py +103 -206
  45. universal_mcp/applications/google_drive/app.py +194 -793
  46. universal_mcp/applications/google_gemini/app.py +68 -59
  47. universal_mcp/applications/google_mail/README.md +1 -0
  48. universal_mcp/applications/google_mail/app.py +93 -214
  49. universal_mcp/applications/google_searchconsole/app.py +25 -58
  50. universal_mcp/applications/google_sheet/README.md +2 -1
  51. universal_mcp/applications/google_sheet/app.py +226 -624
  52. universal_mcp/applications/google_sheet/helper.py +26 -53
  53. universal_mcp/applications/hashnode/app.py +57 -269
  54. universal_mcp/applications/heygen/README.md +10 -32
  55. universal_mcp/applications/heygen/app.py +339 -811
  56. universal_mcp/applications/http_tools/app.py +10 -32
  57. universal_mcp/applications/hubspot/README.md +1 -1
  58. universal_mcp/applications/hubspot/app.py +7508 -99
  59. universal_mcp/applications/jira/app.py +2419 -8334
  60. universal_mcp/applications/klaviyo/app.py +739 -1621
  61. universal_mcp/applications/linkedin/README.md +18 -1
  62. universal_mcp/applications/linkedin/app.py +729 -251
  63. universal_mcp/applications/mailchimp/app.py +696 -1851
  64. universal_mcp/applications/markitdown/app.py +8 -20
  65. universal_mcp/applications/miro/app.py +333 -815
  66. universal_mcp/applications/ms_teams/app.py +420 -1407
  67. universal_mcp/applications/neon/app.py +144 -250
  68. universal_mcp/applications/notion/app.py +38 -53
  69. universal_mcp/applications/onedrive/app.py +26 -48
  70. universal_mcp/applications/openai/app.py +43 -166
  71. universal_mcp/applications/outlook/README.md +22 -9
  72. universal_mcp/applications/outlook/app.py +403 -141
  73. universal_mcp/applications/perplexity/README.md +2 -1
  74. universal_mcp/applications/perplexity/app.py +161 -20
  75. universal_mcp/applications/pipedrive/app.py +1021 -3331
  76. universal_mcp/applications/posthog/app.py +272 -541
  77. universal_mcp/applications/reddit/app.py +65 -164
  78. universal_mcp/applications/resend/app.py +72 -139
  79. universal_mcp/applications/retell/app.py +23 -50
  80. universal_mcp/applications/rocketlane/app.py +252 -965
  81. universal_mcp/applications/scraper/app.py +114 -142
  82. universal_mcp/applications/semanticscholar/app.py +36 -78
  83. universal_mcp/applications/semrush/app.py +44 -78
  84. universal_mcp/applications/sendgrid/app.py +826 -1576
  85. universal_mcp/applications/sentry/app.py +444 -1079
  86. universal_mcp/applications/serpapi/app.py +44 -146
  87. universal_mcp/applications/sharepoint/app.py +27 -49
  88. universal_mcp/applications/shopify/app.py +1748 -4486
  89. universal_mcp/applications/shortcut/app.py +275 -536
  90. universal_mcp/applications/slack/app.py +43 -125
  91. universal_mcp/applications/spotify/app.py +206 -405
  92. universal_mcp/applications/supabase/app.py +174 -283
  93. universal_mcp/applications/tavily/app.py +2 -2
  94. universal_mcp/applications/trello/app.py +853 -2816
  95. universal_mcp/applications/twilio/app.py +27 -62
  96. universal_mcp/applications/twitter/api_segments/compliance_api.py +4 -14
  97. universal_mcp/applications/twitter/api_segments/dm_conversations_api.py +6 -18
  98. universal_mcp/applications/twitter/api_segments/likes_api.py +1 -3
  99. universal_mcp/applications/twitter/api_segments/lists_api.py +5 -15
  100. universal_mcp/applications/twitter/api_segments/trends_api.py +1 -3
  101. universal_mcp/applications/twitter/api_segments/tweets_api.py +9 -31
  102. universal_mcp/applications/twitter/api_segments/usage_api.py +1 -5
  103. universal_mcp/applications/twitter/api_segments/users_api.py +14 -42
  104. universal_mcp/applications/whatsapp/app.py +35 -186
  105. universal_mcp/applications/whatsapp/audio.py +2 -6
  106. universal_mcp/applications/whatsapp/whatsapp.py +17 -51
  107. universal_mcp/applications/whatsapp_business/app.py +86 -299
  108. universal_mcp/applications/wrike/app.py +80 -153
  109. universal_mcp/applications/yahoo_finance/app.py +19 -65
  110. universal_mcp/applications/youtube/app.py +120 -306
  111. universal_mcp/applications/zenquotes/app.py +3 -3
  112. {universal_mcp_applications-0.1.33.dist-info → universal_mcp_applications-0.1.39rc16.dist-info}/METADATA +4 -2
  113. {universal_mcp_applications-0.1.33.dist-info → universal_mcp_applications-0.1.39rc16.dist-info}/RECORD +115 -119
  114. {universal_mcp_applications-0.1.33.dist-info → universal_mcp_applications-0.1.39rc16.dist-info}/WHEEL +1 -1
  115. universal_mcp/applications/hubspot/api_segments/__init__.py +0 -0
  116. universal_mcp/applications/hubspot/api_segments/api_segment_base.py +0 -54
  117. universal_mcp/applications/hubspot/api_segments/crm_api.py +0 -7337
  118. universal_mcp/applications/hubspot/api_segments/marketing_api.py +0 -1467
  119. {universal_mcp_applications-0.1.33.dist-info → universal_mcp_applications-0.1.39rc16.dist-info}/licenses/LICENSE +0 -0
@@ -1,226 +1,455 @@
1
- from typing import Any
1
+ from typing import Any, Literal
2
+
3
+ from loguru import logger
4
+
5
+ try:
6
+ from exa_py import AsyncExa
7
+
8
+ ExaClient: type[AsyncExa] | None = AsyncExa
9
+ except ImportError:
10
+ ExaClient = None
11
+ logger.error("Failed to import Exa. Please ensure 'exa-py' is installed.")
2
12
 
3
13
  from universal_mcp.applications.application import APIApplication
14
+ from universal_mcp.exceptions import NotAuthorizedError, ToolError
4
15
  from universal_mcp.integrations import Integration
5
16
 
6
17
 
7
18
  class ExaApp(APIApplication):
8
- def __init__(self, integration: Integration = None, **kwargs) -> None:
19
+ """
20
+ Application for interacting with the Exa API (exa.ai) using the official SDK.
21
+ Provides advanced search, find similar links, page contents retrieval,
22
+ knowledge synthesis (answer), and multi-step research tasks.
23
+ """
24
+
25
+ def __init__(self, integration: Integration | None = None, **kwargs: Any) -> None:
9
26
  super().__init__(name="exa", integration=integration, **kwargs)
10
- self.base_url = "https://api.exa.ai"
27
+ self._exa_client: AsyncExa | None = None
28
+
29
+ async def get_exa_client(self) -> AsyncExa:
30
+ """
31
+ Lazily initializes and returns the Exa client.
32
+ """
33
+ if self._exa_client is None:
34
+ if ExaClient is None:
35
+ raise ToolError("Exa SDK (exa-py) is not installed.")
36
+
37
+ if not self.integration:
38
+ raise NotAuthorizedError("Exa App: Integration not configured.")
39
+
40
+ credentials = await self.integration.get_credentials_async()
41
+ api_key = credentials.get("api_key") or credentials.get("API_KEY") or credentials.get("apiKey")
42
+
43
+ if not api_key:
44
+ raise NotAuthorizedError("Exa API key not found in credentials.")
45
+
46
+ self._exa_client = ExaClient(api_key=api_key)
47
+ logger.info("Exa client successfully initialized.")
48
+
49
+ return self._exa_client
50
+
51
+ def _to_serializable(self, obj: Any) -> Any:
52
+ """
53
+ Recursively converts objects to dictionaries for JSON serialization.
54
+ """
55
+ if isinstance(obj, list):
56
+ return [self._to_serializable(item) for item in obj]
57
+ if hasattr(obj, "to_dict"):
58
+ return obj.to_dict()
59
+ if hasattr(obj, "dict"):
60
+ return obj.dict()
61
+ if hasattr(obj, "__dict__"):
62
+ return {k: self._to_serializable(v) for k, v in obj.__dict__.items() if not k.startswith("_")}
63
+ return obj
64
+
65
+ async def search( # noqa: PLR0913
66
+ self,
67
+ query: str,
68
+ num_results: int | None = 10,
69
+ include_domains: list[str] | None = None,
70
+ exclude_domains: list[str] | None = None,
71
+ start_crawl_date: str | None = None,
72
+ end_crawl_date: str | None = None,
73
+ start_published_date: str | None = None,
74
+ end_published_date: str | None = None,
75
+ type: Literal["auto", "neural", "fast", "deep", "hybrid"] | None = "auto",
76
+ category: str | None = None,
77
+ include_text: list[str] | None = None,
78
+ exclude_text: list[str] | None = None,
79
+ additional_queries: list[str] | None = None,
80
+ text: bool = True,
81
+ highlights: bool = False,
82
+ summary: dict[str, Any] | None = None,
83
+ context: dict[str, Any] | bool | None = None,
84
+ flags: list[str] | None = None,
85
+ moderation: bool | None = None,
86
+ user_location: str | None = None,
87
+ ) -> Any:
88
+ """
89
+ Performs a semantic or keyword search across the web and returns ranked results.
90
+ Ideal for finding high-quality links, research papers, news, or general information.
91
+
92
+ Args:
93
+ query: The search query. For best results with 'neural' or 'deep' search, use a
94
+ descriptive natural language statement (e.g., "Check out this amazing new
95
+ AI tool for developers:").
96
+ num_results: Total results to return (default: 10). For 'deep' search, leave blank
97
+ to let the model determine the optimal number of results dynamically.
98
+ include_domains: Restrict search to these domains (e.g., ['github.com', 'arxiv.org']).
99
+ exclude_domains: Block these domains from appearing in results.
100
+ start_crawl_date: ISO 8601 date. Only results crawled after this (e.g., '2024-01-01').
101
+ end_crawl_date: ISO 8601 date. Only results crawled before this.
102
+ start_published_date: ISO 8601 date. Only results published after this.
103
+ end_published_date: ISO 8601 date. Only results published before this.
104
+ type: The search methodology:
105
+ - 'auto' (default): Automatically selects the best type based on query.
106
+ - 'neural': Semantic search using embeddings. Best for concept-based queries.
107
+ - 'fast': Keyword-based search. Best for specific names or terms.
108
+ - 'deep': Multi-query expansion and reasoning. Best for complex, multi-faceted research.
109
+ - 'hybrid': Combines neural and fast for balanced results.
110
+ category: Filter by content type (e.g., 'company', 'research paper', 'news', 'pdf', 'tweet').
111
+ include_text: Webpage MUST contain these exact strings (max 5 words per string).
112
+ exclude_text: Webpage MUST NOT contain these exact strings.
113
+ additional_queries: (Deep only) Up to 5 manually specified queries to skip automatic expansion.
114
+ text: Include the full webpage text in the response (default: True).
115
+ highlights: Include high-relevance snippets (highlights) from each result.
116
+ summary: Generate a concise summary of each page. Can be a boolean or a dict:
117
+ {"query": "Refining summary...", "schema": {"type": "object", "properties": {...}}}.
118
+ context: Optimized for RAG. Returns a combined context object instead of raw results.
119
+ flags: Experimental feature flags for specialized Exa behavior.
120
+ moderation: Enable safety filtering for sensitive content.
121
+ user_location: ISO country code (e.g., 'US') to personalize results.
122
+
123
+ Returns:
124
+ A serialized SearchResponse containing 'results' (list of Result objects with url, title, text, etc.).
125
+
126
+ Tags:
127
+ search, semantic, keyword, neural, important
128
+ """
129
+ logger.info(f"Exa search for: {query}")
130
+
131
+ # Build contents options
132
+ contents = {}
133
+ if text:
134
+ contents["text"] = True
135
+ if highlights:
136
+ contents["highlights"] = True
137
+ if summary:
138
+ contents["summary"] = summary
139
+ if context:
140
+ contents["context"] = context
141
+
142
+ client = await self.get_exa_client()
143
+ response = await client.search(
144
+ query=query,
145
+ num_results=num_results,
146
+ include_domains=include_domains,
147
+ exclude_domains=exclude_domains,
148
+ start_crawl_date=start_crawl_date,
149
+ end_crawl_date=end_crawl_date,
150
+ start_published_date=start_published_date,
151
+ end_published_date=end_published_date,
152
+ type=type,
153
+ category=category,
154
+ include_text=include_text,
155
+ exclude_text=exclude_text,
156
+ additional_queries=additional_queries,
157
+ contents=contents if contents else None,
158
+ flags=flags,
159
+ moderation=moderation,
160
+ user_location=user_location,
161
+ )
162
+ return self._to_serializable(response)
163
+
164
+ async def find_similar( # noqa: PLR0913
165
+ self,
166
+ url: str,
167
+ num_results: int | None = 10,
168
+ include_domains: list[str] | None = None,
169
+ exclude_domains: list[str] | None = None,
170
+ start_crawl_date: str | None = None,
171
+ end_crawl_date: str | None = None,
172
+ start_published_date: str | None = None,
173
+ end_published_date: str | None = None,
174
+ exclude_source_domain: bool | None = None,
175
+ category: str | None = None,
176
+ include_text: list[str] | None = None,
177
+ exclude_text: list[str] | None = None,
178
+ text: bool = True,
179
+ highlights: bool = False,
180
+ summary: dict[str, Any] | None = None,
181
+ flags: list[str] | None = None,
182
+ ) -> Any:
183
+ """
184
+ Retrieves webpages that are semantically similar to a provided URL.
185
+ Useful for finding "more like this", competitors, or related research.
186
+
187
+ Args:
188
+ url: The source URL to find similarity for.
189
+ num_results: Number of similar results to return (default: 10).
190
+ include_domains: List of domains to include in results.
191
+ exclude_domains: List of domains to block.
192
+ start_crawl_date: ISO 8601 date. Only results crawled after this.
193
+ end_crawl_date: ISO 8601 date. Only results crawled before this.
194
+ start_published_date: ISO 8601 date. Only results published after this.
195
+ end_published_date: ISO 8601 date. Only results published before this.
196
+ exclude_source_domain: If True, do not return results from the same domain as the input URL.
197
+ category: Filter similar results by content type (e.g., 'personal site', 'github').
198
+ include_text: Webpage MUST contain these exact strings.
199
+ exclude_text: Webpage MUST NOT contain these exact strings.
200
+ text: Include full text content in the response (default: True).
201
+ highlights: Include relevance snippets (highlights).
202
+ summary: Generate a summary for each similar page.
203
+ flags: Experimental feature flags.
204
+
205
+ Returns:
206
+ A serialized SearchResponse with results semantically ranked by similarity to the input URL.
207
+
208
+ Tags:
209
+ similar, related, mapping, semantic
210
+ """
211
+ logger.info(f"Exa find_similar for URL: {url}")
212
+
213
+ contents = {}
214
+ if text:
215
+ contents["text"] = True
216
+ if highlights:
217
+ contents["highlights"] = True
218
+ if summary:
219
+ contents["summary"] = summary
11
220
 
12
- def search_with_filters(
221
+ client = await self.get_exa_client()
222
+ response = await client.find_similar(
223
+ url=url,
224
+ num_results=num_results,
225
+ include_domains=include_domains,
226
+ exclude_domains=exclude_domains,
227
+ start_crawl_date=start_crawl_date,
228
+ end_crawl_date=end_crawl_date,
229
+ start_published_date=start_published_date,
230
+ end_published_date=end_published_date,
231
+ exclude_source_domain=exclude_source_domain,
232
+ category=category,
233
+ include_text=include_text,
234
+ exclude_text=exclude_text,
235
+ contents=contents if contents else None,
236
+ flags=flags,
237
+ )
238
+ return self._to_serializable(response)
239
+
240
+ async def get_contents( # noqa: PLR0913
13
241
  self,
14
- query,
15
- useAutoprompt=None,
16
- type=None,
17
- category=None,
18
- numResults=None,
19
- includeDomains=None,
20
- excludeDomains=None,
21
- startCrawlDate=None,
22
- endCrawlDate=None,
23
- startPublishedDate=None,
24
- endPublishedDate=None,
25
- includeText=None,
26
- excludeText=None,
27
- contents=None,
28
- ) -> dict[str, Any]:
29
- """
30
- Executes a query against the Exa API's `/search` endpoint, returning a list of results. This function supports extensive filtering by search type, category, domains, publication dates, and specific text content to refine the search query and tailor the API's response.
242
+ urls: list[str],
243
+ text: bool = True,
244
+ summary: dict[str, Any] | None = None,
245
+ subpages: int | None = None,
246
+ subpage_target: str | list[str] | None = None,
247
+ livecrawl: Literal["always", "never", "fallback", "auto"] | None = None,
248
+ livecrawl_timeout: int | None = None,
249
+ filter_empty_results: bool | None = None,
250
+ extras: dict[str, Any] | None = None,
251
+ flags: list[str] | None = None,
252
+ ) -> Any:
253
+ """
254
+ Deep-fetches the actual content of specific URLs or Result IDs.
255
+ Provides robust data extraction including text, snippets, and structured summaries.
31
256
 
32
257
  Args:
33
- query (string): The query string for the search. Example: 'Latest developments in LLM capabilities'.
34
- useAutoprompt (boolean): Autoprompt converts your query to an Exa-style query. Enabled by default for auto search, optional for neural search, and not available for keyword search. Example: 'True'.
35
- type (string): The type of search. Neural uses an embeddings-based model, keyword is google-like SERP. Default is auto, which automatically decides between keyword and neural. Example: 'auto'.
36
- category (string): A data category to focus on. Example: 'research paper'.
37
- numResults (integer): Number of results to return (up to thousands of results available for custom plans) Example: '10'.
38
- includeDomains (array): List of domains to include in the search. If specified, results will only come from these domains. Example: "['arxiv.org', 'paperswithcode.com']".
39
- excludeDomains (array): List of domains to exclude from search results. If specified, no results will be returned from these domains.
40
- startCrawlDate (string): Crawl date refers to the date that Exa discovered a link. Results will include links that were crawled after this date. Must be specified in ISO 8601 format. Example: '2023-01-01'.
41
- endCrawlDate (string): Crawl date refers to the date that Exa discovered a link. Results will include links that were crawled before this date. Must be specified in ISO 8601 format. Example: '2023-12-31'.
42
- startPublishedDate (string): Only links with a published date after this will be returned. Must be specified in ISO 8601 format. Example: '2023-01-01'.
43
- endPublishedDate (string): Only links with a published date before this will be returned. Must be specified in ISO 8601 format. Example: '2023-12-31'.
44
- includeText (array): List of strings that must be present in webpage text of results. Currently, only 1 string is supported, of up to 5 words. Example: "['large language model']".
45
- excludeText (array): List of strings that must not be present in webpage text of results. Currently, only 1 string is supported, of up to 5 words. Example: "['course']".
46
- contents (object): contents
258
+ urls: List of URLs or Exa Result IDs to retrieve.
259
+ text: Include full page text (default: True).
260
+ summary: Generate structured or unstructured summaries of each URL.
261
+ subpages: Number of additional pages to crawl automatically from the same domain.
262
+ subpage_target: Focus subpage crawling on specific terms (e.g., 'pricing', 'technical doc').
263
+ livecrawl: Controls real-time crawling behavior:
264
+ - 'auto' (default): Uses Exa's cache first, crawls if data is missing or stale.
265
+ - 'always': Forces a fresh crawl, bypassing cache entirely.
266
+ - 'never': Strictly uses cached data.
267
+ - 'fallback': Uses cache, only crawls if cache retrieval fails.
268
+ livecrawl_timeout: Maximum time allowed for fresh crawls (in milliseconds).
269
+ filter_empty_results: Automatically remove results where no meaningful content was found.
270
+ extras: Advanced extraction features (e.g., {'links': 20, 'image_links': 10}).
271
+ flags: Experimental feature flags.
47
272
 
48
273
  Returns:
49
- dict[str, Any]: OK
274
+ A serialized SearchResponse containing enriched content for each URL.
50
275
 
51
276
  Tags:
52
- important
53
- """
54
- request_body = {
55
- "query": query,
56
- "useAutoprompt": useAutoprompt,
57
- "type": type,
58
- "category": category,
59
- "numResults": numResults,
60
- "includeDomains": includeDomains,
61
- "excludeDomains": excludeDomains,
62
- "startCrawlDate": startCrawlDate,
63
- "endCrawlDate": endCrawlDate,
64
- "startPublishedDate": startPublishedDate,
65
- "endPublishedDate": endPublishedDate,
66
- "includeText": includeText,
67
- "excludeText": excludeText,
68
- "contents": contents,
69
- }
70
- request_body = {k: v for k, v in request_body.items() if v is not None}
71
- url = f"{self.base_url}/search"
72
- query_params = {}
73
- response = self._post(url, data=request_body, params=query_params)
74
- response.raise_for_status()
75
- return response.json()
76
-
77
- def find_similar_by_url(
277
+ content, fetch, crawl, subpages, extract
278
+ """
279
+ logger.info(f"Exa get_contents for {len(urls)} URLs.")
280
+ client = await self.get_exa_client()
281
+ response = await client.get_contents(
282
+ urls=urls,
283
+ text=text,
284
+ summary=summary,
285
+ subpages=subpages,
286
+ subpage_target=subpage_target,
287
+ livecrawl=livecrawl,
288
+ livecrawl_timeout=livecrawl_timeout,
289
+ filter_empty_results=filter_empty_results,
290
+ extras=extras,
291
+ flags=flags,
292
+ )
293
+ return self._to_serializable(response)
294
+
295
+ async def answer( # noqa: PLR0913
78
296
  self,
79
- url,
80
- numResults=None,
81
- includeDomains=None,
82
- excludeDomains=None,
83
- startCrawlDate=None,
84
- endCrawlDate=None,
85
- startPublishedDate=None,
86
- endPublishedDate=None,
87
- includeText=None,
88
- excludeText=None,
89
- contents=None,
90
- ) -> dict[str, Any]:
91
- """
92
- Finds web pages semantically similar to a given URL. Unlike the `search` function, which uses a text query, this method takes a specific link and returns a list of related results, with options to filter by domain, publication date, and content.
297
+ query: str,
298
+ text: bool = False,
299
+ system_prompt: str | None = None,
300
+ model: Literal["exa", "exa-pro"] | None = None,
301
+ output_schema: dict[str, Any] | None = None,
302
+ user_location: str | None = None,
303
+ ) -> Any:
304
+ """
305
+ Synthesizes a direct, objective answer to a research question based on multiple web sources.
306
+ Includes inline citations linked to the original pages.
93
307
 
94
308
  Args:
95
- url (string): The url for which you would like to find similar links. Example: 'https://arxiv.org/abs/2307.06435'.
96
- numResults (integer): Number of results to return (up to thousands of results available for custom plans) Example: '10'.
97
- includeDomains (array): List of domains to include in the search. If specified, results will only come from these domains. Example: "['arxiv.org', 'paperswithcode.com']".
98
- excludeDomains (array): List of domains to exclude from search results. If specified, no results will be returned from these domains.
99
- startCrawlDate (string): Crawl date refers to the date that Exa discovered a link. Results will include links that were crawled after this date. Must be specified in ISO 8601 format. Example: '2023-01-01'.
100
- endCrawlDate (string): Crawl date refers to the date that Exa discovered a link. Results will include links that were crawled before this date. Must be specified in ISO 8601 format. Example: '2023-12-31'.
101
- startPublishedDate (string): Only links with a published date after this will be returned. Must be specified in ISO 8601 format. Example: '2023-01-01'.
102
- endPublishedDate (string): Only links with a published date before this will be returned. Must be specified in ISO 8601 format. Example: '2023-12-31'.
103
- includeText (array): List of strings that must be present in webpage text of results. Currently, only 1 string is supported, of up to 5 words. Example: "['large language model']".
104
- excludeText (array): List of strings that must not be present in webpage text of results. Currently, only 1 string is supported, of up to 5 words. Example: "['course']".
105
- contents (object): contents
309
+ query: The research question (e.g., "What are the latest breakthroughs in fusion power?").
310
+ text: Include the full text of cited pages in the response (default: False).
311
+ system_prompt: Guiding prompt to control the LLM's persona or formatting style.
312
+ model: Answer engine:
313
+ - 'exa-pro' (default): High-performance, multi-query reasoning for deep answers.
314
+ - 'exa': Faster, single-pass answer generation.
315
+ output_schema: Optional JSON Schema to force the result into a specific JSON structure.
316
+ user_location: ISO country code for localized answers.
106
317
 
107
318
  Returns:
108
- dict[str, Any]: OK
319
+ A serialized AnswerResponse with the 'answer' text and 'citations' list.
109
320
 
110
321
  Tags:
111
- important
112
- """
113
- request_body = {
114
- "url": url,
115
- "numResults": numResults,
116
- "includeDomains": includeDomains,
117
- "excludeDomains": excludeDomains,
118
- "startCrawlDate": startCrawlDate,
119
- "endCrawlDate": endCrawlDate,
120
- "startPublishedDate": startPublishedDate,
121
- "endPublishedDate": endPublishedDate,
122
- "includeText": includeText,
123
- "excludeText": excludeText,
124
- "contents": contents,
125
- }
126
- request_body = {k: v for k, v in request_body.items() if v is not None}
127
- url = f"{self.base_url}/findSimilar"
128
- query_params = {}
129
- response = self._post(url, data=request_body, params=query_params)
130
- response.raise_for_status()
131
- return response.json()
132
-
133
- def fetch_page_content(
322
+ answer, synthesis, knowledge, citations, research, important
323
+ """
324
+ logger.info(f"Exa answer for query: {query}")
325
+ client = await self.get_exa_client()
326
+ response = await client.answer(
327
+ query=query,
328
+ text=text,
329
+ system_prompt=system_prompt,
330
+ model=model,
331
+ output_schema=output_schema,
332
+ user_location=user_location,
333
+ )
334
+ return self._to_serializable(response)
335
+
336
+ async def create_research_task(
134
337
  self,
135
- urls,
136
- ids=None,
137
- text=None,
138
- highlights=None,
139
- summary=None,
140
- livecrawl=None,
141
- livecrawlTimeout=None,
142
- subpages=None,
143
- subpageTarget=None,
144
- extras=None,
145
- ) -> dict[str, Any]:
146
- """
147
- Retrieves and processes content from a list of URLs, returning full text, summaries, or highlights. Unlike the search function which finds links, this function fetches the actual page content, with optional support for live crawling to get the most up-to-date information.
338
+ instructions: str,
339
+ output_schema: dict[str, Any] | None = None,
340
+ model: Literal["exa-research", "exa-research-pro", "exa-research-fast"] | None = "exa-research-fast",
341
+ ) -> Any:
342
+ """
343
+ Initiates a long-running, autonomous research task that explores the web to fulfill complex instructions.
344
+ Ideal for tasks that require multiple searches and deep analysis.
148
345
 
149
346
  Args:
150
- urls (array): Array of URLs to crawl (backwards compatible with 'ids' parameter). Example: "['https://arxiv.org/pdf/2307.06435']".
151
- ids (array): Deprecated - use 'urls' instead. Array of document IDs obtained from searches. Example: "['https://arxiv.org/pdf/2307.06435']".
152
- text (string): text
153
- highlights (object): Text snippets the LLM identifies as most relevant from each page.
154
- summary (object): Summary of the webpage
155
- livecrawl (string): Options for livecrawling pages.
156
- 'never': Disable livecrawling (default for neural search).
157
- 'fallback': Livecrawl when cache is empty (default for keyword search).
158
- 'always': Always livecrawl.
159
- 'auto': Use an LLM to detect if query needs real-time content.
160
- Example: 'always'.
161
- livecrawlTimeout (integer): The timeout for livecrawling in milliseconds. Example: '1000'.
162
- subpages (integer): The number of subpages to crawl. The actual number crawled may be limited by system constraints. Example: '1'.
163
- subpageTarget (string): Keyword to find specific subpages of search results. Can be a single string or an array of strings, comma delimited. Example: 'sources'.
164
- extras (object): Extra parameters to pass.
347
+ instructions: Detailed briefing for the research goal (e.g., "Find all AI unicorns founded
348
+ in 2024 and summarize their lead investors and core technology.").
349
+ output_schema: Optional JSON Schema to structure the final researched output.
350
+ model: Research intelligence level:
351
+ - 'exa-research-fast' (default): Quick, focused investigation.
352
+ - 'exa-research': Standard depth, balanced speed.
353
+ - 'exa-research-pro': Maximum depth, exhaustive exploration.
165
354
 
166
355
  Returns:
167
- dict[str, Any]: OK
356
+ A serialized ResearchDto containing the 'research_id' (Task ID) used for polling and status checks.
168
357
 
169
358
  Tags:
170
- important
171
- """
172
- request_body = {
173
- "urls": urls,
174
- "ids": ids,
175
- "text": text,
176
- "highlights": highlights,
177
- "summary": summary,
178
- "livecrawl": livecrawl,
179
- "livecrawlTimeout": livecrawlTimeout,
180
- "subpages": subpages,
181
- "subpageTarget": subpageTarget,
182
- "extras": extras,
183
- }
184
- request_body = {k: v for k, v in request_body.items() if v is not None}
185
- url = f"{self.base_url}/contents"
186
- query_params = {}
187
- response = self._post(url, data=request_body, params=query_params)
188
- response.raise_for_status()
189
- return response.json()
190
-
191
- def answer(self, query, stream=None, text=None, model=None) -> dict[str, Any]:
192
- """
193
- Retrieves a direct, synthesized answer for a given query by calling the Exa `/answer` API endpoint. Unlike `search`, which returns web results, this function provides a conclusive response. It supports streaming, including source text, and selecting a search model.
359
+ research, task, async, create
360
+ """
361
+ logger.info(f"Exa create_research_task: {instructions}")
362
+ client = await self.get_exa_client()
363
+ response = await client.research.create(
364
+ instructions=instructions,
365
+ output_schema=output_schema,
366
+ model=model,
367
+ )
368
+ return self._to_serializable(response)
369
+
370
+ async def get_research_task(self, task_id: str, events: bool = False) -> Any:
371
+ """
372
+ Retrieves the current status, metadata, and (if finished) final results of a research task.
194
373
 
195
374
  Args:
196
- query (string): The question or query to answer. Example: 'What is the latest valuation of SpaceX?'.
197
- stream (boolean): If true, the response is returned as a server-sent events (SSS) stream.
198
- text (boolean): If true, the response includes full text content in the search results
199
- model (string): The search model to use for the answer. Exa passes only one query to exa, while exa-pro also passes 2 expanded queries to our search model.
375
+ task_id: The unique ID assigned during task creation.
376
+ events: If True, returns a chronological log of all actions the researcher has taken.
200
377
 
201
378
  Returns:
202
- dict[str, Any]: OK
379
+ A serialized ResearchDto with status ('queued', 'running', 'completed', etc.) and data.
203
380
 
204
381
  Tags:
205
- important
206
- """
207
- request_body = {
208
- "query": query,
209
- "stream": stream,
210
- "text": text,
211
- "model": model,
212
- }
213
- request_body = {k: v for k, v in request_body.items() if v is not None}
214
- url = f"{self.base_url}/answer"
215
- query_params = {}
216
- response = self._post(url, data=request_body, params=query_params)
217
- response.raise_for_status()
218
- return response.json()
382
+ research, status, task, check
383
+ """
384
+ logger.info(f"Exa get_research_task: {task_id}")
385
+ client = await self.get_exa_client()
386
+ response = await client.research.get(research_id=task_id, events=events)
387
+ return self._to_serializable(response)
388
+
389
+ async def poll_research_task(
390
+ self,
391
+ task_id: str,
392
+ poll_interval_ms: int = 1000,
393
+ timeout_ms: int = 600000,
394
+ events: bool = False,
395
+ ) -> Any:
396
+ """
397
+ Blocks until a research task completes, fails, or times out.
398
+ Provides a convenient way to wait for results without manual looping.
399
+
400
+ Args:
401
+ task_id: The ID of the task to monitor.
402
+ poll_interval_ms: Frequency of status checks in milliseconds (default: 1000).
403
+ timeout_ms: Maximum duration to block before giving up (default: 10 minutes).
404
+ events: If True, include activity logs in the final response.
405
+
406
+ Returns:
407
+ The terminal ResearchDto state containing the final research findings.
408
+
409
+ Tags:
410
+ research, poll, wait, task, terminal
411
+ """
412
+ logger.info(f"Exa poll_research_task: {task_id}")
413
+ client = await self.get_exa_client()
414
+ response = await client.research.poll_until_finished(
415
+ research_id=task_id,
416
+ poll_interval=poll_interval_ms,
417
+ timeout_ms=timeout_ms,
418
+ events=events,
419
+ )
420
+ return self._to_serializable(response)
421
+
422
+ async def list_research_tasks(
423
+ self,
424
+ cursor: str | None = None,
425
+ limit: int | None = None,
426
+ ) -> Any:
427
+ """
428
+ Provides a paginated list of all past and current research tasks for auditing or recovery.
429
+
430
+ Args:
431
+ cursor: Token for retrieving the next page of task history.
432
+ limit: Maximum number of records to return in this call.
433
+
434
+ Returns:
435
+ A ListResearchResponseDto containing an array of research tasks.
436
+
437
+ Tags:
438
+ research, list, tasks, history
439
+ """
440
+ logger.info(f"Exa list_research_tasks (limit: {limit})")
441
+ client = await self.get_exa_client()
442
+ response = await client.research.list(cursor=cursor, limit=limit)
443
+ return self._to_serializable(response)
219
444
 
220
445
  def list_tools(self):
221
446
  return [
222
- self.search_with_filters,
223
- self.find_similar_by_url,
224
- self.fetch_page_content,
447
+ self.search,
448
+ self.find_similar,
449
+ self.get_contents,
225
450
  self.answer,
451
+ self.create_research_task,
452
+ self.get_research_task,
453
+ self.poll_research_task,
454
+ self.list_research_tasks,
226
455
  ]