universal-mcp-applications 0.1.33__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.
- universal_mcp/applications/ahrefs/app.py +92 -238
- universal_mcp/applications/airtable/app.py +23 -122
- universal_mcp/applications/apollo/app.py +122 -475
- universal_mcp/applications/asana/app.py +605 -1755
- universal_mcp/applications/aws_s3/app.py +36 -103
- universal_mcp/applications/bill/app.py +644 -2055
- universal_mcp/applications/box/app.py +1246 -4159
- universal_mcp/applications/braze/app.py +410 -1476
- universal_mcp/applications/browser_use/README.md +15 -1
- universal_mcp/applications/browser_use/__init__.py +1 -0
- universal_mcp/applications/browser_use/app.py +86 -24
- universal_mcp/applications/cal_com_v2/app.py +207 -625
- universal_mcp/applications/calendly/app.py +103 -242
- universal_mcp/applications/canva/app.py +75 -140
- universal_mcp/applications/clickup/app.py +331 -798
- universal_mcp/applications/coda/app.py +240 -520
- universal_mcp/applications/confluence/app.py +497 -1285
- universal_mcp/applications/contentful/app.py +36 -151
- universal_mcp/applications/crustdata/app.py +42 -121
- universal_mcp/applications/dialpad/app.py +451 -924
- universal_mcp/applications/digitalocean/app.py +2071 -6082
- universal_mcp/applications/domain_checker/app.py +3 -54
- universal_mcp/applications/e2b/app.py +14 -64
- universal_mcp/applications/elevenlabs/app.py +9 -47
- universal_mcp/applications/exa/README.md +8 -4
- universal_mcp/applications/exa/app.py +408 -186
- universal_mcp/applications/falai/app.py +24 -101
- universal_mcp/applications/figma/app.py +91 -175
- universal_mcp/applications/file_system/app.py +2 -13
- universal_mcp/applications/firecrawl/app.py +186 -163
- universal_mcp/applications/fireflies/app.py +59 -281
- universal_mcp/applications/fpl/app.py +92 -529
- universal_mcp/applications/fpl/utils/fixtures.py +15 -49
- universal_mcp/applications/fpl/utils/helper.py +25 -89
- universal_mcp/applications/fpl/utils/league_utils.py +20 -64
- universal_mcp/applications/ghost_content/app.py +66 -175
- universal_mcp/applications/github/app.py +28 -65
- universal_mcp/applications/gong/app.py +140 -300
- universal_mcp/applications/google_calendar/app.py +26 -78
- universal_mcp/applications/google_docs/app.py +98 -202
- universal_mcp/applications/google_drive/app.py +194 -793
- universal_mcp/applications/google_gemini/app.py +27 -62
- universal_mcp/applications/google_mail/README.md +1 -0
- universal_mcp/applications/google_mail/app.py +93 -214
- universal_mcp/applications/google_searchconsole/app.py +25 -58
- universal_mcp/applications/google_sheet/app.py +171 -624
- universal_mcp/applications/google_sheet/helper.py +26 -53
- universal_mcp/applications/hashnode/app.py +57 -269
- universal_mcp/applications/heygen/app.py +77 -155
- universal_mcp/applications/http_tools/app.py +10 -32
- universal_mcp/applications/hubspot/README.md +1 -1
- universal_mcp/applications/hubspot/app.py +7508 -99
- universal_mcp/applications/jira/app.py +2419 -8334
- universal_mcp/applications/klaviyo/app.py +737 -1619
- universal_mcp/applications/linkedin/README.md +5 -0
- universal_mcp/applications/linkedin/app.py +332 -227
- universal_mcp/applications/mailchimp/app.py +696 -1851
- universal_mcp/applications/markitdown/app.py +8 -20
- universal_mcp/applications/miro/app.py +333 -815
- universal_mcp/applications/ms_teams/app.py +85 -207
- universal_mcp/applications/neon/app.py +144 -250
- universal_mcp/applications/notion/app.py +36 -51
- universal_mcp/applications/onedrive/app.py +26 -48
- universal_mcp/applications/openai/app.py +42 -165
- universal_mcp/applications/outlook/README.md +22 -9
- universal_mcp/applications/outlook/app.py +403 -141
- universal_mcp/applications/perplexity/README.md +2 -1
- universal_mcp/applications/perplexity/app.py +162 -20
- universal_mcp/applications/pipedrive/app.py +1021 -3331
- universal_mcp/applications/posthog/app.py +272 -541
- universal_mcp/applications/reddit/app.py +61 -160
- universal_mcp/applications/resend/app.py +41 -107
- universal_mcp/applications/retell/app.py +23 -50
- universal_mcp/applications/rocketlane/app.py +250 -963
- universal_mcp/applications/scraper/app.py +67 -125
- universal_mcp/applications/semanticscholar/app.py +36 -78
- universal_mcp/applications/semrush/app.py +43 -77
- universal_mcp/applications/sendgrid/app.py +826 -1576
- universal_mcp/applications/sentry/app.py +444 -1079
- universal_mcp/applications/serpapi/app.py +40 -143
- universal_mcp/applications/sharepoint/app.py +27 -49
- universal_mcp/applications/shopify/app.py +1743 -4479
- universal_mcp/applications/shortcut/app.py +272 -534
- universal_mcp/applications/slack/app.py +41 -123
- universal_mcp/applications/spotify/app.py +206 -405
- universal_mcp/applications/supabase/app.py +174 -283
- universal_mcp/applications/tavily/app.py +2 -2
- universal_mcp/applications/trello/app.py +853 -2816
- universal_mcp/applications/twilio/app.py +14 -50
- universal_mcp/applications/twitter/api_segments/compliance_api.py +4 -14
- universal_mcp/applications/twitter/api_segments/dm_conversations_api.py +6 -18
- universal_mcp/applications/twitter/api_segments/likes_api.py +1 -3
- universal_mcp/applications/twitter/api_segments/lists_api.py +5 -15
- universal_mcp/applications/twitter/api_segments/trends_api.py +1 -3
- universal_mcp/applications/twitter/api_segments/tweets_api.py +9 -31
- universal_mcp/applications/twitter/api_segments/usage_api.py +1 -5
- universal_mcp/applications/twitter/api_segments/users_api.py +14 -42
- universal_mcp/applications/whatsapp/app.py +35 -186
- universal_mcp/applications/whatsapp/audio.py +2 -6
- universal_mcp/applications/whatsapp/whatsapp.py +17 -51
- universal_mcp/applications/whatsapp_business/app.py +86 -299
- universal_mcp/applications/wrike/app.py +80 -153
- universal_mcp/applications/yahoo_finance/app.py +19 -65
- universal_mcp/applications/youtube/app.py +120 -306
- universal_mcp/applications/zenquotes/app.py +3 -3
- {universal_mcp_applications-0.1.33.dist-info → universal_mcp_applications-0.1.39rc8.dist-info}/METADATA +4 -2
- {universal_mcp_applications-0.1.33.dist-info → universal_mcp_applications-0.1.39rc8.dist-info}/RECORD +109 -113
- {universal_mcp_applications-0.1.33.dist-info → universal_mcp_applications-0.1.39rc8.dist-info}/WHEEL +1 -1
- universal_mcp/applications/hubspot/api_segments/__init__.py +0 -0
- universal_mcp/applications/hubspot/api_segments/api_segment_base.py +0 -54
- universal_mcp/applications/hubspot/api_segments/crm_api.py +0 -7337
- universal_mcp/applications/hubspot/api_segments/marketing_api.py +0 -1467
- {universal_mcp_applications-0.1.33.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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
273
|
+
A serialized SearchResponse containing enriched content for each URL.
|
|
50
274
|
|
|
51
275
|
Tags:
|
|
52
|
-
|
|
53
|
-
"""
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
317
|
+
A serialized AnswerResponse with the 'answer' text and 'citations' list.
|
|
109
318
|
|
|
110
319
|
Tags:
|
|
111
|
-
important
|
|
112
|
-
"""
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
353
|
+
A serialized ResearchDto containing the 'research_id' (Task ID) used for polling and status checks.
|
|
168
354
|
|
|
169
355
|
Tags:
|
|
170
|
-
|
|
171
|
-
"""
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
197
|
-
|
|
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
|
-
|
|
375
|
+
A serialized ResearchDto with status ('queued', 'running', 'completed', etc.) and data.
|
|
203
376
|
|
|
204
377
|
Tags:
|
|
205
|
-
|
|
206
|
-
"""
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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.
|
|
223
|
-
self.
|
|
224
|
-
self.
|
|
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
|
]
|