lfx-exa 0.1.0__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.
lfx_exa/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ """lfx-exa: Exa Search bundle.
2
+
3
+ This package is the distribution unit ``lfx-exa``. At runtime
4
+ Langflow's loader discovers ``extension.json`` shipped alongside this
5
+ ``__init__.py`` and registers ``ExaSearchToolkit`` under the namespaced
6
+ ID ``ext:exa:ExaSearchToolkit@official``.
7
+
8
+ Graduated out of the manifest-less ``lfx-bundles`` metapackage when the
9
+ component was modernized onto the ``exa-py`` SDK (the metapackage copy
10
+ still used the deprecated ``metaphor-python`` client). The bundle name
11
+ (``exa``) and class name are unchanged, so the canonical component ID --
12
+ and every migration-table entry pointing at it -- is stable across the
13
+ move.
14
+ """
15
+
16
+ from lfx_exa.components.exa.exa_search import ExaSearchToolkit
17
+
18
+ __all__ = ["ExaSearchToolkit"]
@@ -0,0 +1,3 @@
1
+ from .exa_search import ExaSearchToolkit
2
+
3
+ __all__ = ["ExaSearchToolkit"]
@@ -0,0 +1,213 @@
1
+ from exa_py import Exa
2
+ from langchain_core.tools import tool
3
+ from lfx.custom.custom_component.component import Component
4
+ from lfx.field_typing import Tool
5
+ from lfx.io import (
6
+ BoolInput,
7
+ DropdownInput,
8
+ IntInput,
9
+ MessageTextInput,
10
+ Output,
11
+ SecretStrInput,
12
+ )
13
+
14
+ EXA_INTEGRATION_NAME = "langflow-integration"
15
+
16
+
17
+ class ExaSearchToolkit(Component):
18
+ display_name = "Exa Search"
19
+ description = "Exa search and contents tools for agents and MCP clients."
20
+ documentation = "https://docs.exa.ai/reference/getting-started"
21
+ beta = True
22
+ name = "ExaSearch"
23
+ icon = "ExaSearch"
24
+
25
+ inputs = [
26
+ SecretStrInput(
27
+ name="exa_api_key",
28
+ display_name="Exa API Key",
29
+ info="Get one at https://dashboard.exa.ai/api-keys",
30
+ password=True,
31
+ ),
32
+ # Kept for backward compatibility with flows saved before the field
33
+ # was renamed. Hidden from the UI; users should fill in `exa_api_key`.
34
+ SecretStrInput(
35
+ name="metaphor_api_key",
36
+ display_name="Metaphor API Key (legacy)",
37
+ info="Deprecated. Use Exa API Key instead.",
38
+ password=True,
39
+ advanced=True,
40
+ show=False,
41
+ ),
42
+ DropdownInput(
43
+ name="search_type",
44
+ display_name="Search type",
45
+ options=["auto", "fast", "instant", "deep"],
46
+ value="auto",
47
+ info="Latency vs. depth tradeoff. `auto` is recommended for most use cases.",
48
+ ),
49
+ IntInput(
50
+ name="search_num_results",
51
+ display_name="Number of results",
52
+ value=10,
53
+ ),
54
+ BoolInput(
55
+ name="include_highlights",
56
+ display_name="Include highlights",
57
+ value=True,
58
+ advanced=True,
59
+ info="Token-efficient extracts of the most relevant text per result. Recommended default.",
60
+ ),
61
+ IntInput(
62
+ name="highlights_max_characters",
63
+ display_name="Highlights max characters",
64
+ value=0,
65
+ advanced=True,
66
+ info="Optional cap on the length of each highlight, in characters. 0 leaves it at Exa's default.",
67
+ ),
68
+ BoolInput(
69
+ name="include_text",
70
+ display_name="Include full text",
71
+ value=False,
72
+ advanced=True,
73
+ info="Return full page text. Off by default; prefer highlights for token efficiency.",
74
+ ),
75
+ DropdownInput(
76
+ name="category",
77
+ display_name="Category",
78
+ options=[
79
+ "",
80
+ "company",
81
+ "people",
82
+ "research paper",
83
+ "news",
84
+ "personal site",
85
+ "financial report",
86
+ ],
87
+ value="",
88
+ advanced=True,
89
+ info="Restrict to a specific Exa data category. Leave empty for general web search.",
90
+ ),
91
+ IntInput(
92
+ name="max_age_hours",
93
+ display_name="Max age (hours)",
94
+ value=0,
95
+ advanced=True,
96
+ info=(
97
+ "Max age of cached content (in hours) before refetching. "
98
+ "0 = always refetch. -1 = never refetch (use cache only)."
99
+ ),
100
+ ),
101
+ MessageTextInput(
102
+ name="include_domains",
103
+ display_name="Include domains",
104
+ value="",
105
+ advanced=True,
106
+ info="Comma-separated allowlist of domains.",
107
+ ),
108
+ MessageTextInput(
109
+ name="exclude_domains",
110
+ display_name="Exclude domains",
111
+ value="",
112
+ advanced=True,
113
+ info="Comma-separated denylist of domains.",
114
+ ),
115
+ MessageTextInput(
116
+ name="start_published_date",
117
+ display_name="Start published date",
118
+ value="",
119
+ advanced=True,
120
+ info="ISO date (YYYY-MM-DD). Only return results published on or after this date.",
121
+ ),
122
+ MessageTextInput(
123
+ name="end_published_date",
124
+ display_name="End published date",
125
+ value="",
126
+ advanced=True,
127
+ info="ISO date (YYYY-MM-DD). Only return results published on or before this date.",
128
+ ),
129
+ ]
130
+
131
+ outputs = [
132
+ Output(name="tools", display_name="Tools", method="build_toolkit"),
133
+ ]
134
+
135
+ def _resolved_api_key(self) -> str:
136
+ key = self.exa_api_key or self.metaphor_api_key
137
+ if not key:
138
+ msg = "Exa API key is required. Set the Exa API Key input."
139
+ raise ValueError(msg)
140
+ return key
141
+
142
+ def _build_client(self) -> Exa:
143
+ client = Exa(api_key=self._resolved_api_key())
144
+ client.headers["x-exa-integration"] = EXA_INTEGRATION_NAME
145
+ return client
146
+
147
+ def _highlights_value(self) -> bool | dict | None:
148
+ if not self.include_highlights:
149
+ return None
150
+ if self.highlights_max_characters and self.highlights_max_characters > 0:
151
+ return {"max_characters": self.highlights_max_characters}
152
+ return True
153
+
154
+ def _contents(self) -> dict | None:
155
+ contents: dict = {}
156
+ highlights = self._highlights_value()
157
+ if highlights is not None:
158
+ contents["highlights"] = highlights
159
+ if self.include_text:
160
+ contents["text"] = True
161
+ if self.max_age_hours and self.max_age_hours != 0:
162
+ contents["max_age_hours"] = self.max_age_hours
163
+ return contents or None
164
+
165
+ @staticmethod
166
+ def _split_csv(raw: str) -> list[str] | None:
167
+ items = [s.strip() for s in (raw or "").split(",") if s.strip()]
168
+ return items or None
169
+
170
+ def build_toolkit(self) -> Tool:
171
+ client = self._build_client()
172
+ contents = self._contents()
173
+ category = self.category or None
174
+ include_domains = self._split_csv(self.include_domains)
175
+ exclude_domains = self._split_csv(self.exclude_domains)
176
+ start_published_date = (self.start_published_date or "").strip() or None
177
+ end_published_date = (self.end_published_date or "").strip() or None
178
+
179
+ @tool
180
+ def search(query: str):
181
+ """Search the web with Exa and return results with optional highlights."""
182
+ kwargs: dict = {
183
+ "type": self.search_type,
184
+ "num_results": self.search_num_results,
185
+ }
186
+ if contents is not None:
187
+ kwargs["contents"] = contents
188
+ if category:
189
+ kwargs["category"] = category
190
+ if include_domains:
191
+ kwargs["include_domains"] = include_domains
192
+ if exclude_domains:
193
+ kwargs["exclude_domains"] = exclude_domains
194
+ if start_published_date:
195
+ kwargs["start_published_date"] = start_published_date
196
+ if end_published_date:
197
+ kwargs["end_published_date"] = end_published_date
198
+ return client.search(query, **kwargs)
199
+
200
+ @tool
201
+ def get_contents(ids: list[str]):
202
+ """Fetch contents (highlights and/or text) for result IDs returned by `search`."""
203
+ kwargs: dict = {}
204
+ highlights = self._highlights_value()
205
+ if highlights is not None:
206
+ kwargs["highlights"] = highlights
207
+ if self.include_text:
208
+ kwargs["text"] = True
209
+ if self.max_age_hours and self.max_age_hours != 0:
210
+ kwargs["max_age_hours"] = self.max_age_hours
211
+ return client.get_contents(ids, **kwargs)
212
+
213
+ return [search, get_contents]
lfx_exa/extension.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "$schema": "https://schemas.langflow.org/extension/v1.json",
3
+ "id": "lfx-exa",
4
+ "version": "0.1.0",
5
+ "name": "Exa",
6
+ "description": "Exa web-search and contents tools (exa-py SDK) as a standalone Langflow Extension Bundle.",
7
+ "lfx": {
8
+ "compat": ["1"]
9
+ },
10
+ "bundles": [
11
+ {
12
+ "name": "exa",
13
+ "path": "components/exa"
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,64 @@
1
+ Metadata-Version: 2.4
2
+ Name: lfx-exa
3
+ Version: 0.1.0
4
+ Summary: Exa web-search and contents tools (exa-py SDK) as a standalone Langflow Extension Bundle.
5
+ Project-URL: Homepage, https://github.com/langflow-ai/langflow
6
+ Project-URL: Documentation, https://docs.langflow.org/extensions
7
+ Project-URL: Repository, https://github.com/langflow-ai/langflow
8
+ Author-email: Langflow <contact@langflow.org>
9
+ License: MIT
10
+ Keywords: bundle,exa,extension,langflow,lfx,search
11
+ Requires-Python: <3.15,>=3.10
12
+ Requires-Dist: exa-py<3,>=1.14
13
+ Requires-Dist: langchain-core<2.0.0,>=1.2.28
14
+ Requires-Dist: lfx<2.0.0,>=1.11.0.dev0
15
+ Description-Content-Type: text/markdown
16
+
17
+ # lfx-exa
18
+
19
+ [Exa](https://exa.ai/) web-search and contents tools as a standalone
20
+ Langflow Extension Bundle.
21
+
22
+ ## What it ships
23
+
24
+ One component, registered under the `exa` bundle group:
25
+
26
+ - **Exa Search** (`ExaSearchToolkit`, canonical ID
27
+ `ext:exa:ExaSearchToolkit@official`) — a toolkit exposing two tools for
28
+ a Langflow **Agent** component or MCP client:
29
+ - `search` — search the web with Exa (`auto` / `fast` / `instant` /
30
+ `deep` search types, category, domain allow/deny lists, published-date
31
+ range), returning token-efficient highlights by default.
32
+ - `get_contents` — fetch highlights and/or full text for result IDs
33
+ returned by `search`.
34
+
35
+ The component is built on the [`exa-py`](https://pypi.org/project/exa-py/)
36
+ SDK. It previously shipped inside the manifest-less `lfx-bundles`
37
+ metapackage on the deprecated `metaphor-python` client; the bundle name and
38
+ class name are unchanged, so saved flows load without migration changes.
39
+ The legacy `metaphor_api_key` field is still honored (hidden) — new flows
40
+ should use `exa_api_key`.
41
+
42
+ ## Install
43
+
44
+ ```bash
45
+ pip install lfx-exa
46
+ ```
47
+
48
+ `pip install langflow` already includes it.
49
+
50
+ ## Develop
51
+
52
+ The bundle is a uv workspace member of the Langflow monorepo:
53
+
54
+ ```bash
55
+ uv sync
56
+ uv run pytest src/bundles/exa/tests -q
57
+ uv run lfx extension validate src/bundles/exa/src/lfx_exa
58
+ ```
59
+
60
+ To iterate on the component with a live palette:
61
+
62
+ ```bash
63
+ uv run lfx extension dev src/bundles/exa
64
+ ```
@@ -0,0 +1,8 @@
1
+ lfx_exa/__init__.py,sha256=fCJ7e8qjfR-0NaACIK3LzgEXGmy71cH2wtL8d5UqSKw,745
2
+ lfx_exa/extension.json,sha256=ovBxXhdLSzNLIiMyYL18zVy8REMpazrUSVgLF9REtIg,354
3
+ lfx_exa/components/exa/__init__.py,sha256=ESYlh6mAyXrpDIIfgf7xJy0zeMmesIMy7xV5MR-FdJY,73
4
+ lfx_exa/components/exa/exa_search.py,sha256=VQw9rT8ezSpbiEJenVNGGJa8NJml2Xfl1n70KIgghJM,7605
5
+ lfx_exa-0.1.0.dist-info/METADATA,sha256=Sve_kAvHnYy4qrCmaMde75MM3WKUNyXcMb_LDISsWPo,2083
6
+ lfx_exa-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
7
+ lfx_exa-0.1.0.dist-info/entry_points.txt,sha256=Z_SqIQOZ38m0FL3NNmfJTE9TsZ1o3bl3otIhx8N9URQ,40
8
+ lfx_exa-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [langflow.extensions]
2
+ lfx-exa = lfx_exa