universal-mcp-applications 0.1.22__py3-none-any.whl → 0.1.39rc8__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 (120) hide show
  1. universal_mcp/applications/ahrefs/app.py +92 -238
  2. universal_mcp/applications/airtable/app.py +23 -122
  3. universal_mcp/applications/apollo/app.py +122 -475
  4. universal_mcp/applications/asana/app.py +605 -1755
  5. universal_mcp/applications/aws_s3/app.py +36 -103
  6. universal_mcp/applications/bill/app.py +644 -2055
  7. universal_mcp/applications/box/app.py +1246 -4159
  8. universal_mcp/applications/braze/app.py +410 -1476
  9. universal_mcp/applications/browser_use/README.md +15 -1
  10. universal_mcp/applications/browser_use/__init__.py +1 -0
  11. universal_mcp/applications/browser_use/app.py +94 -37
  12. universal_mcp/applications/cal_com_v2/app.py +207 -625
  13. universal_mcp/applications/calendly/app.py +103 -242
  14. universal_mcp/applications/canva/app.py +75 -140
  15. universal_mcp/applications/clickup/app.py +331 -798
  16. universal_mcp/applications/coda/app.py +240 -520
  17. universal_mcp/applications/confluence/app.py +497 -1285
  18. universal_mcp/applications/contentful/app.py +36 -151
  19. universal_mcp/applications/crustdata/app.py +42 -121
  20. universal_mcp/applications/dialpad/app.py +451 -924
  21. universal_mcp/applications/digitalocean/app.py +2071 -6082
  22. universal_mcp/applications/domain_checker/app.py +3 -54
  23. universal_mcp/applications/e2b/app.py +14 -64
  24. universal_mcp/applications/elevenlabs/app.py +9 -47
  25. universal_mcp/applications/exa/README.md +8 -4
  26. universal_mcp/applications/exa/app.py +408 -186
  27. universal_mcp/applications/falai/app.py +24 -101
  28. universal_mcp/applications/figma/app.py +91 -175
  29. universal_mcp/applications/file_system/app.py +2 -13
  30. universal_mcp/applications/firecrawl/app.py +186 -163
  31. universal_mcp/applications/fireflies/app.py +59 -281
  32. universal_mcp/applications/fpl/app.py +92 -529
  33. universal_mcp/applications/fpl/utils/fixtures.py +15 -49
  34. universal_mcp/applications/fpl/utils/helper.py +25 -89
  35. universal_mcp/applications/fpl/utils/league_utils.py +20 -64
  36. universal_mcp/applications/ghost_content/app.py +66 -175
  37. universal_mcp/applications/github/app.py +28 -65
  38. universal_mcp/applications/gong/app.py +140 -300
  39. universal_mcp/applications/google_calendar/app.py +26 -78
  40. universal_mcp/applications/google_docs/app.py +324 -354
  41. universal_mcp/applications/google_drive/app.py +194 -793
  42. universal_mcp/applications/google_gemini/app.py +29 -64
  43. universal_mcp/applications/google_mail/README.md +1 -0
  44. universal_mcp/applications/google_mail/app.py +93 -214
  45. universal_mcp/applications/google_searchconsole/app.py +25 -58
  46. universal_mcp/applications/google_sheet/app.py +174 -623
  47. universal_mcp/applications/google_sheet/helper.py +26 -53
  48. universal_mcp/applications/hashnode/app.py +57 -269
  49. universal_mcp/applications/heygen/app.py +77 -155
  50. universal_mcp/applications/http_tools/app.py +10 -32
  51. universal_mcp/applications/hubspot/README.md +1 -1
  52. universal_mcp/applications/hubspot/app.py +7508 -99
  53. universal_mcp/applications/jira/app.py +2419 -8334
  54. universal_mcp/applications/klaviyo/app.py +737 -1619
  55. universal_mcp/applications/linkedin/README.md +23 -4
  56. universal_mcp/applications/linkedin/app.py +861 -155
  57. universal_mcp/applications/mailchimp/app.py +696 -1851
  58. universal_mcp/applications/markitdown/app.py +8 -20
  59. universal_mcp/applications/miro/app.py +333 -815
  60. universal_mcp/applications/ms_teams/app.py +85 -207
  61. universal_mcp/applications/neon/app.py +144 -250
  62. universal_mcp/applications/notion/app.py +36 -51
  63. universal_mcp/applications/onedrive/README.md +24 -0
  64. universal_mcp/applications/onedrive/__init__.py +1 -0
  65. universal_mcp/applications/onedrive/app.py +316 -0
  66. universal_mcp/applications/openai/app.py +42 -165
  67. universal_mcp/applications/outlook/README.md +22 -9
  68. universal_mcp/applications/outlook/app.py +606 -262
  69. universal_mcp/applications/perplexity/README.md +2 -1
  70. universal_mcp/applications/perplexity/app.py +162 -20
  71. universal_mcp/applications/pipedrive/app.py +1021 -3331
  72. universal_mcp/applications/posthog/app.py +272 -541
  73. universal_mcp/applications/reddit/app.py +88 -204
  74. universal_mcp/applications/resend/app.py +41 -107
  75. universal_mcp/applications/retell/app.py +23 -50
  76. universal_mcp/applications/rocketlane/app.py +250 -963
  77. universal_mcp/applications/scraper/README.md +7 -4
  78. universal_mcp/applications/scraper/app.py +245 -283
  79. universal_mcp/applications/semanticscholar/app.py +36 -78
  80. universal_mcp/applications/semrush/app.py +43 -77
  81. universal_mcp/applications/sendgrid/app.py +826 -1576
  82. universal_mcp/applications/sentry/app.py +444 -1079
  83. universal_mcp/applications/serpapi/app.py +40 -143
  84. universal_mcp/applications/sharepoint/README.md +16 -14
  85. universal_mcp/applications/sharepoint/app.py +245 -154
  86. universal_mcp/applications/shopify/app.py +1743 -4479
  87. universal_mcp/applications/shortcut/app.py +272 -534
  88. universal_mcp/applications/slack/app.py +58 -109
  89. universal_mcp/applications/spotify/app.py +206 -405
  90. universal_mcp/applications/supabase/app.py +174 -283
  91. universal_mcp/applications/tavily/app.py +2 -2
  92. universal_mcp/applications/trello/app.py +853 -2816
  93. universal_mcp/applications/twilio/app.py +14 -50
  94. universal_mcp/applications/twitter/api_segments/compliance_api.py +4 -14
  95. universal_mcp/applications/twitter/api_segments/dm_conversations_api.py +6 -18
  96. universal_mcp/applications/twitter/api_segments/likes_api.py +1 -3
  97. universal_mcp/applications/twitter/api_segments/lists_api.py +5 -15
  98. universal_mcp/applications/twitter/api_segments/trends_api.py +1 -3
  99. universal_mcp/applications/twitter/api_segments/tweets_api.py +9 -31
  100. universal_mcp/applications/twitter/api_segments/usage_api.py +1 -5
  101. universal_mcp/applications/twitter/api_segments/users_api.py +14 -42
  102. universal_mcp/applications/whatsapp/app.py +35 -186
  103. universal_mcp/applications/whatsapp/audio.py +2 -6
  104. universal_mcp/applications/whatsapp/whatsapp.py +17 -51
  105. universal_mcp/applications/whatsapp_business/app.py +86 -299
  106. universal_mcp/applications/wrike/app.py +80 -153
  107. universal_mcp/applications/yahoo_finance/app.py +19 -65
  108. universal_mcp/applications/youtube/app.py +120 -306
  109. universal_mcp/applications/zenquotes/app.py +4 -4
  110. {universal_mcp_applications-0.1.22.dist-info → universal_mcp_applications-0.1.39rc8.dist-info}/METADATA +4 -2
  111. {universal_mcp_applications-0.1.22.dist-info → universal_mcp_applications-0.1.39rc8.dist-info}/RECORD +113 -117
  112. {universal_mcp_applications-0.1.22.dist-info → universal_mcp_applications-0.1.39rc8.dist-info}/WHEEL +1 -1
  113. universal_mcp/applications/hubspot/api_segments/__init__.py +0 -0
  114. universal_mcp/applications/hubspot/api_segments/api_segment_base.py +0 -54
  115. universal_mcp/applications/hubspot/api_segments/crm_api.py +0 -7337
  116. universal_mcp/applications/hubspot/api_segments/marketing_api.py +0 -1467
  117. universal_mcp/applications/unipile/README.md +0 -28
  118. universal_mcp/applications/unipile/__init__.py +0 -1
  119. universal_mcp/applications/unipile/app.py +0 -1077
  120. {universal_mcp_applications-0.1.22.dist-info → universal_mcp_applications-0.1.39rc8.dist-info}/licenses/LICENSE +0 -0
@@ -1,226 +1,448 @@
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
+ @property
30
+ def exa_client(self) -> AsyncExa:
31
+ """
32
+ Lazily initializes and returns the Exa client.
33
+ """
34
+ if self._exa_client is None:
35
+ if ExaClient is None:
36
+ raise ToolError("Exa SDK (exa-py) is not installed.")
37
+
38
+ if not self.integration:
39
+ raise NotAuthorizedError("Exa App: Integration not configured.")
40
+
41
+ credentials = self.integration.get_credentials()
42
+ api_key = credentials.get("api_key") or credentials.get("API_KEY") or credentials.get("apiKey")
43
+
44
+ if not api_key:
45
+ raise NotAuthorizedError("Exa API key not found in credentials.")
46
+
47
+ self._exa_client = ExaClient(api_key=api_key)
48
+ logger.info("Exa client successfully initialized.")
49
+
50
+ return self._exa_client
51
+
52
+ def _to_serializable(self, obj: Any) -> Any:
53
+ """
54
+ Recursively converts objects to dictionaries for JSON serialization.
55
+ """
56
+ if isinstance(obj, list):
57
+ return [self._to_serializable(item) for item in obj]
58
+ if hasattr(obj, "to_dict"):
59
+ return obj.to_dict()
60
+ if hasattr(obj, "dict"):
61
+ return obj.dict()
62
+ if hasattr(obj, "__dict__"):
63
+ return {k: self._to_serializable(v) for k, v in obj.__dict__.items() if not k.startswith("_")}
64
+ return obj
65
+
66
+ async def search( # noqa: PLR0913
67
+ self,
68
+ query: str,
69
+ num_results: int | None = 10,
70
+ include_domains: list[str] | None = None,
71
+ exclude_domains: list[str] | None = None,
72
+ start_crawl_date: str | None = None,
73
+ end_crawl_date: str | None = None,
74
+ start_published_date: str | None = None,
75
+ end_published_date: str | None = None,
76
+ type: Literal["auto", "neural", "fast", "deep", "hybrid"] | None = "auto",
77
+ category: str | None = None,
78
+ include_text: list[str] | None = None,
79
+ exclude_text: list[str] | None = None,
80
+ additional_queries: list[str] | None = None,
81
+ text: bool = True,
82
+ highlights: bool = False,
83
+ summary: dict[str, Any] | None = None,
84
+ context: dict[str, Any] | bool | None = None,
85
+ flags: list[str] | None = None,
86
+ moderation: bool | None = None,
87
+ user_location: str | None = None,
88
+ ) -> Any:
89
+ """
90
+ Performs a semantic or keyword search across the web and returns ranked results.
91
+ Ideal for finding high-quality links, research papers, news, or general information.
92
+
93
+ Args:
94
+ query: The search query. For best results with 'neural' or 'deep' search, use a
95
+ descriptive natural language statement (e.g., "Check out this amazing new
96
+ AI tool for developers:").
97
+ num_results: Total results to return (default: 10). For 'deep' search, leave blank
98
+ to let the model determine the optimal number of results dynamically.
99
+ include_domains: Restrict search to these domains (e.g., ['github.com', 'arxiv.org']).
100
+ exclude_domains: Block these domains from appearing in results.
101
+ start_crawl_date: ISO 8601 date. Only results crawled after this (e.g., '2024-01-01').
102
+ end_crawl_date: ISO 8601 date. Only results crawled before this.
103
+ start_published_date: ISO 8601 date. Only results published after this.
104
+ end_published_date: ISO 8601 date. Only results published before this.
105
+ type: The search methodology:
106
+ - 'auto' (default): Automatically selects the best type based on query.
107
+ - 'neural': Semantic search using embeddings. Best for concept-based queries.
108
+ - 'fast': Keyword-based search. Best for specific names or terms.
109
+ - 'deep': Multi-query expansion and reasoning. Best for complex, multi-faceted research.
110
+ - 'hybrid': Combines neural and fast for balanced results.
111
+ category: Filter by content type (e.g., 'company', 'research paper', 'news', 'pdf', 'tweet').
112
+ include_text: Webpage MUST contain these exact strings (max 5 words per string).
113
+ exclude_text: Webpage MUST NOT contain these exact strings.
114
+ additional_queries: (Deep only) Up to 5 manually specified queries to skip automatic expansion.
115
+ text: Include the full webpage text in the response (default: True).
116
+ highlights: Include high-relevance snippets (highlights) from each result.
117
+ summary: Generate a concise summary of each page. Can be a boolean or a dict:
118
+ {"query": "Refining summary...", "schema": {"type": "object", "properties": {...}}}.
119
+ context: Optimized for RAG. Returns a combined context object instead of raw results.
120
+ flags: Experimental feature flags for specialized Exa behavior.
121
+ moderation: Enable safety filtering for sensitive content.
122
+ user_location: ISO country code (e.g., 'US') to personalize results.
123
+
124
+ Returns:
125
+ A serialized SearchResponse containing 'results' (list of Result objects with url, title, text, etc.).
126
+
127
+ Tags:
128
+ search, semantic, keyword, neural, important
129
+ """
130
+ logger.info(f"Exa search for: {query}")
131
+
132
+ # Build contents options
133
+ contents = {}
134
+ if text:
135
+ contents["text"] = True
136
+ if highlights:
137
+ contents["highlights"] = True
138
+ if summary:
139
+ contents["summary"] = summary
140
+ if context:
141
+ contents["context"] = context
142
+
143
+ response = await self.exa_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
+ response = await self.exa_client.find_similar(
222
+ url=url,
223
+ num_results=num_results,
224
+ include_domains=include_domains,
225
+ exclude_domains=exclude_domains,
226
+ start_crawl_date=start_crawl_date,
227
+ end_crawl_date=end_crawl_date,
228
+ start_published_date=start_published_date,
229
+ end_published_date=end_published_date,
230
+ exclude_source_domain=exclude_source_domain,
231
+ category=category,
232
+ include_text=include_text,
233
+ exclude_text=exclude_text,
234
+ contents=contents if contents else None,
235
+ flags=flags,
236
+ )
237
+ return self._to_serializable(response)
238
+
239
+ async def get_contents( # noqa: PLR0913
13
240
  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.
241
+ urls: list[str],
242
+ text: bool = True,
243
+ summary: dict[str, Any] | None = None,
244
+ subpages: int | None = None,
245
+ subpage_target: str | list[str] | None = None,
246
+ livecrawl: Literal["always", "never", "fallback", "auto"] | None = None,
247
+ livecrawl_timeout: int | None = None,
248
+ filter_empty_results: bool | None = None,
249
+ extras: dict[str, Any] | None = None,
250
+ flags: list[str] | None = None,
251
+ ) -> Any:
252
+ """
253
+ Deep-fetches the actual content of specific URLs or Result IDs.
254
+ Provides robust data extraction including text, snippets, and structured summaries.
31
255
 
32
256
  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
257
+ urls: List of URLs or Exa Result IDs to retrieve.
258
+ text: Include full page text (default: True).
259
+ summary: Generate structured or unstructured summaries of each URL.
260
+ subpages: Number of additional pages to crawl automatically from the same domain.
261
+ subpage_target: Focus subpage crawling on specific terms (e.g., 'pricing', 'technical doc').
262
+ livecrawl: Controls real-time crawling behavior:
263
+ - 'auto' (default): Uses Exa's cache first, crawls if data is missing or stale.
264
+ - 'always': Forces a fresh crawl, bypassing cache entirely.
265
+ - 'never': Strictly uses cached data.
266
+ - 'fallback': Uses cache, only crawls if cache retrieval fails.
267
+ livecrawl_timeout: Maximum time allowed for fresh crawls (in milliseconds).
268
+ filter_empty_results: Automatically remove results where no meaningful content was found.
269
+ extras: Advanced extraction features (e.g., {'links': 20, 'image_links': 10}).
270
+ flags: Experimental feature flags.
47
271
 
48
272
  Returns:
49
- dict[str, Any]: OK
273
+ A serialized SearchResponse containing enriched content for each URL.
50
274
 
51
275
  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(
276
+ content, fetch, crawl, subpages, extract
277
+ """
278
+ logger.info(f"Exa get_contents for {len(urls)} URLs.")
279
+ response = await self.exa_client.get_contents(
280
+ urls=urls,
281
+ text=text,
282
+ summary=summary,
283
+ subpages=subpages,
284
+ subpage_target=subpage_target,
285
+ livecrawl=livecrawl,
286
+ livecrawl_timeout=livecrawl_timeout,
287
+ filter_empty_results=filter_empty_results,
288
+ extras=extras,
289
+ flags=flags,
290
+ )
291
+ return self._to_serializable(response)
292
+
293
+ async def answer( # noqa: PLR0913
78
294
  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.
295
+ query: str,
296
+ text: bool = False,
297
+ system_prompt: str | None = None,
298
+ model: Literal["exa", "exa-pro"] | None = None,
299
+ output_schema: dict[str, Any] | None = None,
300
+ user_location: str | None = None,
301
+ ) -> Any:
302
+ """
303
+ Synthesizes a direct, objective answer to a research question based on multiple web sources.
304
+ Includes inline citations linked to the original pages.
93
305
 
94
306
  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
307
+ query: The research question (e.g., "What are the latest breakthroughs in fusion power?").
308
+ text: Include the full text of cited pages in the response (default: False).
309
+ system_prompt: Guiding prompt to control the LLM's persona or formatting style.
310
+ model: Answer engine:
311
+ - 'exa-pro' (default): High-performance, multi-query reasoning for deep answers.
312
+ - 'exa': Faster, single-pass answer generation.
313
+ output_schema: Optional JSON Schema to force the result into a specific JSON structure.
314
+ user_location: ISO country code for localized answers.
106
315
 
107
316
  Returns:
108
- dict[str, Any]: OK
317
+ A serialized AnswerResponse with the 'answer' text and 'citations' list.
109
318
 
110
319
  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(
320
+ answer, synthesis, knowledge, citations, research, important
321
+ """
322
+ logger.info(f"Exa answer for query: {query}")
323
+ response = await self.exa_client.answer(
324
+ query=query,
325
+ text=text,
326
+ system_prompt=system_prompt,
327
+ model=model,
328
+ output_schema=output_schema,
329
+ user_location=user_location,
330
+ )
331
+ return self._to_serializable(response)
332
+
333
+ async def create_research_task(
134
334
  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.
335
+ instructions: str,
336
+ output_schema: dict[str, Any] | None = None,
337
+ model: Literal["exa-research", "exa-research-pro", "exa-research-fast"] | None = "exa-research-fast",
338
+ ) -> Any:
339
+ """
340
+ Initiates a long-running, autonomous research task that explores the web to fulfill complex instructions.
341
+ Ideal for tasks that require multiple searches and deep analysis.
148
342
 
149
343
  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.
344
+ instructions: Detailed briefing for the research goal (e.g., "Find all AI unicorns founded
345
+ in 2024 and summarize their lead investors and core technology.").
346
+ output_schema: Optional JSON Schema to structure the final researched output.
347
+ model: Research intelligence level:
348
+ - 'exa-research-fast' (default): Quick, focused investigation.
349
+ - 'exa-research': Standard depth, balanced speed.
350
+ - 'exa-research-pro': Maximum depth, exhaustive exploration.
165
351
 
166
352
  Returns:
167
- dict[str, Any]: OK
353
+ A serialized ResearchDto containing the 'research_id' (Task ID) used for polling and status checks.
168
354
 
169
355
  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.
356
+ research, task, async, create
357
+ """
358
+ logger.info(f"Exa create_research_task: {instructions}")
359
+ response = await self.exa_client.research.create(
360
+ instructions=instructions,
361
+ output_schema=output_schema,
362
+ model=model,
363
+ )
364
+ return self._to_serializable(response)
365
+
366
+ async def get_research_task(self, task_id: str, events: bool = False) -> Any:
367
+ """
368
+ Retrieves the current status, metadata, and (if finished) final results of a research task.
194
369
 
195
370
  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.
371
+ task_id: The unique ID assigned during task creation.
372
+ events: If True, returns a chronological log of all actions the researcher has taken.
200
373
 
201
374
  Returns:
202
- dict[str, Any]: OK
375
+ A serialized ResearchDto with status ('queued', 'running', 'completed', etc.) and data.
203
376
 
204
377
  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()
378
+ research, status, task, check
379
+ """
380
+ logger.info(f"Exa get_research_task: {task_id}")
381
+ response = await self.exa_client.research.get(research_id=task_id, events=events)
382
+ return self._to_serializable(response)
383
+
384
+ async def poll_research_task(
385
+ self,
386
+ task_id: str,
387
+ poll_interval_ms: int = 1000,
388
+ timeout_ms: int = 600000,
389
+ events: bool = False,
390
+ ) -> Any:
391
+ """
392
+ Blocks until a research task completes, fails, or times out.
393
+ Provides a convenient way to wait for results without manual looping.
394
+
395
+ Args:
396
+ task_id: The ID of the task to monitor.
397
+ poll_interval_ms: Frequency of status checks in milliseconds (default: 1000).
398
+ timeout_ms: Maximum duration to block before giving up (default: 10 minutes).
399
+ events: If True, include activity logs in the final response.
400
+
401
+ Returns:
402
+ The terminal ResearchDto state containing the final research findings.
403
+
404
+ Tags:
405
+ research, poll, wait, task, terminal
406
+ """
407
+ logger.info(f"Exa poll_research_task: {task_id}")
408
+ response = await self.exa_client.research.poll_until_finished(
409
+ research_id=task_id,
410
+ poll_interval=poll_interval_ms,
411
+ timeout_ms=timeout_ms,
412
+ events=events,
413
+ )
414
+ return self._to_serializable(response)
415
+
416
+ async def list_research_tasks(
417
+ self,
418
+ cursor: str | None = None,
419
+ limit: int | None = None,
420
+ ) -> Any:
421
+ """
422
+ Provides a paginated list of all past and current research tasks for auditing or recovery.
423
+
424
+ Args:
425
+ cursor: Token for retrieving the next page of task history.
426
+ limit: Maximum number of records to return in this call.
427
+
428
+ Returns:
429
+ A ListResearchResponseDto containing an array of research tasks.
430
+
431
+ Tags:
432
+ research, list, tasks, history
433
+ """
434
+ logger.info(f"Exa list_research_tasks (limit: {limit})")
435
+ response = await self.exa_client.research.list(cursor=cursor, limit=limit)
436
+ return self._to_serializable(response)
219
437
 
220
438
  def list_tools(self):
221
439
  return [
222
- self.search_with_filters,
223
- self.find_similar_by_url,
224
- self.fetch_page_content,
440
+ self.search,
441
+ self.find_similar,
442
+ self.get_contents,
225
443
  self.answer,
444
+ self.create_research_task,
445
+ self.get_research_task,
446
+ self.poll_research_task,
447
+ self.list_research_tasks,
226
448
  ]