lfx-enconvert 0.0.1__tar.gz

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.
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+
3
+ All notable changes to `lfx-enconvert` are documented here.
4
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [0.0.1] - 2026-08-27
8
+
9
+ ### Added
10
+
11
+ - First release: the **EnConvert** bundle for Langflow with six components.
12
+ - **Perceive URL** — `POST /v2/perceive`, with a `render_quality` score on every read.
13
+ - **Web Search** — `POST /v2/search`.
14
+ - **Discover URLs** — `POST /v2/discover`.
15
+ - **Extract Structured** — `POST /v2/distill`.
16
+ - **Convert File to Markdown** — `POST /v1/convert/anything-to-markdown` (fetches the file URL, then converts).
17
+ - **Convert File to PDF** — `POST /v1/convert/anything-to-pdf` (fetches the file URL, then converts).
18
+ - Pip-installable extension bundle, auto-discovered by Langflow at server start via the
19
+ `langflow.extensions` entry point.
20
+ - Private-key (`sk_`) auth over the `X-API-Key` header; public `pk_` keys are rejected with a clear message.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 EnConvert
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,57 @@
1
+ Metadata-Version: 2.5
2
+ Name: lfx-enconvert
3
+ Version: 0.0.1
4
+ Summary: EnConvert bundle for Langflow: perceive URLs, search the web, discover URLs, extract structured data, and convert files — every read scored with render_quality.
5
+ Project-URL: Homepage, https://www.enconvert.com
6
+ Project-URL: Documentation, https://www.enconvert.com/docs
7
+ Project-URL: Repository, https://github.com/enconvert/lfx-enconvert
8
+ Author-email: EnConvert <support@enconvert.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: bundle,components,enconvert,langflow,lfx,markdown,rag,web
12
+ Requires-Python: >=3.10
13
+ Requires-Dist: lfx<2.0.0
14
+ Requires-Dist: requests>=2.28
15
+ Description-Content-Type: text/markdown
16
+
17
+ # EnConvert bundle for Langflow
18
+
19
+ `lfx-enconvert` adds six [EnConvert](https://www.enconvert.com) components to Langflow.
20
+ EnConvert renders web pages and files into agent-ready data, and puts a `render_quality`
21
+ honesty score (0.0–1.0) on every read — so a blocked or empty page comes back flagged, not trusted.
22
+
23
+ ## Components
24
+
25
+ | Component | Endpoint | What it does |
26
+ |-----------|----------|--------------|
27
+ | **Perceive URL** | `POST /v2/perceive` | A page → markdown, structured data, screenshot, PDF, links… with a `render_quality` score |
28
+ | **Web Search** | `POST /v2/search` | Web / news / images / scholar / patents / maps results |
29
+ | **Discover URLs** | `POST /v2/discover` | Map a site (sitemap, crawl, hybrid) into a list of URLs |
30
+ | **Extract Structured** | `POST /v2/distill` | Typed data from pages against a JSON schema |
31
+ | **Convert File to Markdown** | `POST /v1/convert/anything-to-markdown` | Any file URL → clean markdown |
32
+ | **Convert File to PDF** | `POST /v1/convert/anything-to-pdf` | Any file URL → PDF |
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ pip install lfx-enconvert
38
+ ```
39
+
40
+ Langflow auto-discovers the bundle at server start. The six components appear under the
41
+ **EnConvert** bundle in the component sidebar — no config, no manual registration.
42
+
43
+ ## API key
44
+
45
+ Each component has an **EnConvert API Key** field. Use a **private** key (`sk_…`) from your
46
+ [dashboard](https://www.enconvert.com/dashboard/api-keys); public `pk_` keys are rejected.
47
+ Store it once as a Langflow **global variable** and select it in the key field, so it never
48
+ lives in the flow JSON.
49
+
50
+ ## Publishing / distribution
51
+
52
+ Building, publishing to PyPI, the single-file drop-in route, and the optional core PR are all in
53
+ the **[deploy guide](../langflow-enconvert-deploy/README.md)**.
54
+
55
+ ## Licence
56
+
57
+ [MIT](LICENSE)
@@ -0,0 +1,41 @@
1
+ # EnConvert bundle for Langflow
2
+
3
+ `lfx-enconvert` adds six [EnConvert](https://www.enconvert.com) components to Langflow.
4
+ EnConvert renders web pages and files into agent-ready data, and puts a `render_quality`
5
+ honesty score (0.0–1.0) on every read — so a blocked or empty page comes back flagged, not trusted.
6
+
7
+ ## Components
8
+
9
+ | Component | Endpoint | What it does |
10
+ |-----------|----------|--------------|
11
+ | **Perceive URL** | `POST /v2/perceive` | A page → markdown, structured data, screenshot, PDF, links… with a `render_quality` score |
12
+ | **Web Search** | `POST /v2/search` | Web / news / images / scholar / patents / maps results |
13
+ | **Discover URLs** | `POST /v2/discover` | Map a site (sitemap, crawl, hybrid) into a list of URLs |
14
+ | **Extract Structured** | `POST /v2/distill` | Typed data from pages against a JSON schema |
15
+ | **Convert File to Markdown** | `POST /v1/convert/anything-to-markdown` | Any file URL → clean markdown |
16
+ | **Convert File to PDF** | `POST /v1/convert/anything-to-pdf` | Any file URL → PDF |
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ pip install lfx-enconvert
22
+ ```
23
+
24
+ Langflow auto-discovers the bundle at server start. The six components appear under the
25
+ **EnConvert** bundle in the component sidebar — no config, no manual registration.
26
+
27
+ ## API key
28
+
29
+ Each component has an **EnConvert API Key** field. Use a **private** key (`sk_…`) from your
30
+ [dashboard](https://www.enconvert.com/dashboard/api-keys); public `pk_` keys are rejected.
31
+ Store it once as a Langflow **global variable** and select it in the key field, so it never
32
+ lives in the flow JSON.
33
+
34
+ ## Publishing / distribution
35
+
36
+ Building, publishing to PyPI, the single-file drop-in route, and the optional core PR are all in
37
+ the **[deploy guide](../langflow-enconvert-deploy/README.md)**.
38
+
39
+ ## Licence
40
+
41
+ [MIT](LICENSE)
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "lfx-enconvert"
7
+ version = "0.0.1"
8
+ description = "EnConvert bundle for Langflow: perceive URLs, search the web, discover URLs, extract structured data, and convert files — every read scored with render_quality."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "EnConvert", email = "support@enconvert.com" }]
13
+ keywords = ["langflow", "lfx", "enconvert", "bundle", "components", "rag", "web", "markdown"]
14
+ dependencies = [
15
+ "lfx<2.0.0",
16
+ "requests>=2.28",
17
+ ]
18
+
19
+ [project.urls]
20
+ Homepage = "https://www.enconvert.com"
21
+ Documentation = "https://www.enconvert.com/docs"
22
+ Repository = "https://github.com/enconvert/lfx-enconvert"
23
+
24
+ # Langflow discovers installed extension bundles at server start through this entry point.
25
+ # Editable installs whose dist.files only surface dist-info entries fall back to this
26
+ # entry point to locate the extension.json manifest.
27
+ [project.entry-points."langflow.extensions"]
28
+ lfx-enconvert = "lfx_enconvert"
29
+
30
+ [tool.hatch.build.targets.wheel]
31
+ # extension.json + the component files must live inside the lfx_enconvert package so
32
+ # importlib.metadata.files(dist) finds the manifest and the loader resolves
33
+ # bundles[].path relative to it. The explicit include guarantees the JSON ships.
34
+ packages = ["src/lfx_enconvert"]
35
+ include = ["src/lfx_enconvert/extension.json", "src/lfx_enconvert/components/**/*.py"]
@@ -0,0 +1,7 @@
1
+ """lfx-enconvert — the EnConvert extension bundle for Langflow.
2
+
3
+ Langflow discovers this package at server start via the ``langflow.extensions``
4
+ entry point (see pyproject.toml) and reads the co-located ``extension.json`` manifest.
5
+ """
6
+
7
+ __version__ = "0.0.1"
@@ -0,0 +1 @@
1
+ """Component bundles for the lfx-enconvert extension."""
@@ -0,0 +1,17 @@
1
+ """EnConvert bundle — six components that turn web pages and files into agent-ready data."""
2
+
3
+ from .convert_markdown import EnConvertConvertToMarkdown
4
+ from .convert_pdf import EnConvertConvertToPdf
5
+ from .discover_urls import EnConvertDiscoverUrls
6
+ from .extract_structured import EnConvertExtractStructured
7
+ from .perceive import EnConvertPerceive
8
+ from .web_search import EnConvertWebSearch
9
+
10
+ __all__ = [
11
+ "EnConvertPerceive",
12
+ "EnConvertWebSearch",
13
+ "EnConvertDiscoverUrls",
14
+ "EnConvertExtractStructured",
15
+ "EnConvertConvertToMarkdown",
16
+ "EnConvertConvertToPdf",
17
+ ]
@@ -0,0 +1,78 @@
1
+ """Convert File to Markdown — any file URL into clean markdown."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import io
6
+
7
+ import requests
8
+ from lfx.custom.custom_component.component import Component
9
+ from lfx.io import MessageTextInput, Output, SecretStrInput
10
+ from lfx.schema.data import Data
11
+
12
+ BASE_URL = "https://api.enconvert.com"
13
+ TIMEOUT = 120
14
+
15
+
16
+ def _convert(endpoint: str, api_key: str, file_url: str) -> dict:
17
+ """Fetch file_url bytes (NO api key on that GET), then POST as multipart ``file``.
18
+
19
+ Agent platforms hand tools a URL, not a local path, so the source fetch — and any
20
+ presigned result URL — are retrieved without the api-key header; only the EnConvert
21
+ POST carries X-API-Key. Inlined per component (bundle files are imported standalone).
22
+ """
23
+ if not api_key:
24
+ raise ValueError(
25
+ "EnConvert API key is missing. Add your private key (starts with sk_) "
26
+ "from https://www.enconvert.com/dashboard/api-keys."
27
+ )
28
+ if not file_url:
29
+ raise ValueError("Provide a file URL to convert.")
30
+ src = requests.get(file_url, timeout=TIMEOUT) # no api-key header on the source fetch
31
+ src.raise_for_status()
32
+ filename = file_url.split("?", 1)[0].rsplit("/", 1)[-1] or "file"
33
+ resp = requests.post(
34
+ BASE_URL + endpoint,
35
+ files={"file": (filename, io.BytesIO(src.content))},
36
+ headers={"X-API-Key": api_key},
37
+ timeout=TIMEOUT,
38
+ )
39
+ if resp.status_code in (401, 403):
40
+ raise ValueError(
41
+ f"EnConvert rejected the API key (HTTP {resp.status_code}). Use a private "
42
+ "key (sk_...); public pk_ keys are not accepted."
43
+ )
44
+ resp.raise_for_status()
45
+ return resp.json()
46
+
47
+
48
+ class EnConvertConvertToMarkdown(Component):
49
+ display_name = "Convert File to Markdown"
50
+ description = (
51
+ "Convert any file (from a URL) into clean markdown. The file bytes are fetched "
52
+ "from the URL, converted, and returned as a presigned download URL."
53
+ )
54
+ documentation = "https://www.enconvert.com/docs"
55
+ icon = "FileText"
56
+ name = "enconvert_convert_to_markdown" # stable internal id — do not rename
57
+
58
+ inputs = [
59
+ MessageTextInput(
60
+ name="file_url",
61
+ display_name="File URL",
62
+ required=True,
63
+ info="URL of the file to convert. Fetched without your api key.",
64
+ ),
65
+ SecretStrInput(
66
+ name="api_key",
67
+ display_name="EnConvert API Key",
68
+ required=True,
69
+ info="Private key (sk_...) from https://www.enconvert.com/dashboard/api-keys.",
70
+ ),
71
+ ]
72
+
73
+ outputs = [Output(display_name="Data", name="data", method="build")]
74
+
75
+ def build(self) -> Data:
76
+ result = _convert("/v1/convert/anything-to-markdown", self.api_key, self.file_url)
77
+ self.status = result.get("presigned_url", "done")
78
+ return Data(data=result)
@@ -0,0 +1,78 @@
1
+ """Convert File to PDF — any file URL into a PDF."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import io
6
+
7
+ import requests
8
+ from lfx.custom.custom_component.component import Component
9
+ from lfx.io import MessageTextInput, Output, SecretStrInput
10
+ from lfx.schema.data import Data
11
+
12
+ BASE_URL = "https://api.enconvert.com"
13
+ TIMEOUT = 120
14
+
15
+
16
+ def _convert(endpoint: str, api_key: str, file_url: str) -> dict:
17
+ """Fetch file_url bytes (NO api key on that GET), then POST as multipart ``file``.
18
+
19
+ Agent platforms hand tools a URL, not a local path, so the source fetch — and any
20
+ presigned result URL — are retrieved without the api-key header; only the EnConvert
21
+ POST carries X-API-Key. Inlined per component (bundle files are imported standalone).
22
+ """
23
+ if not api_key:
24
+ raise ValueError(
25
+ "EnConvert API key is missing. Add your private key (starts with sk_) "
26
+ "from https://www.enconvert.com/dashboard/api-keys."
27
+ )
28
+ if not file_url:
29
+ raise ValueError("Provide a file URL to convert.")
30
+ src = requests.get(file_url, timeout=TIMEOUT) # no api-key header on the source fetch
31
+ src.raise_for_status()
32
+ filename = file_url.split("?", 1)[0].rsplit("/", 1)[-1] or "file"
33
+ resp = requests.post(
34
+ BASE_URL + endpoint,
35
+ files={"file": (filename, io.BytesIO(src.content))},
36
+ headers={"X-API-Key": api_key},
37
+ timeout=TIMEOUT,
38
+ )
39
+ if resp.status_code in (401, 403):
40
+ raise ValueError(
41
+ f"EnConvert rejected the API key (HTTP {resp.status_code}). Use a private "
42
+ "key (sk_...); public pk_ keys are not accepted."
43
+ )
44
+ resp.raise_for_status()
45
+ return resp.json()
46
+
47
+
48
+ class EnConvertConvertToPdf(Component):
49
+ display_name = "Convert File to PDF"
50
+ description = (
51
+ "Convert any file (from a URL) into a PDF. The file bytes are fetched from the "
52
+ "URL, converted, and returned as a presigned download URL."
53
+ )
54
+ documentation = "https://www.enconvert.com/docs"
55
+ icon = "File"
56
+ name = "enconvert_convert_to_pdf" # stable internal id — do not rename
57
+
58
+ inputs = [
59
+ MessageTextInput(
60
+ name="file_url",
61
+ display_name="File URL",
62
+ required=True,
63
+ info="URL of the file to convert. Fetched without your api key.",
64
+ ),
65
+ SecretStrInput(
66
+ name="api_key",
67
+ display_name="EnConvert API Key",
68
+ required=True,
69
+ info="Private key (sk_...) from https://www.enconvert.com/dashboard/api-keys.",
70
+ ),
71
+ ]
72
+
73
+ outputs = [Output(display_name="Data", name="data", method="build")]
74
+
75
+ def build(self) -> Data:
76
+ result = _convert("/v1/convert/anything-to-pdf", self.api_key, self.file_url)
77
+ self.status = result.get("presigned_url", "done")
78
+ return Data(data=result)
@@ -0,0 +1,107 @@
1
+ """Discover URLs — map or crawl a site into a list of URLs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import requests
6
+ from lfx.custom.custom_component.component import Component
7
+ from lfx.io import BoolInput, DropdownInput, IntInput, MessageTextInput, Output, SecretStrInput
8
+ from lfx.schema.data import Data
9
+
10
+ BASE_URL = "https://api.enconvert.com"
11
+ TIMEOUT = 120
12
+
13
+
14
+ def _post(endpoint: str, api_key: str, payload: dict) -> dict:
15
+ """POST JSON to an EnConvert endpoint with the private-key header, return parsed JSON.
16
+
17
+ Inlined per component: the Langflow loader imports bundle files standalone, so
18
+ relative imports between them are unsupported.
19
+ """
20
+ if not api_key:
21
+ raise ValueError(
22
+ "EnConvert API key is missing. Add your private key (starts with sk_) "
23
+ "from https://www.enconvert.com/dashboard/api-keys."
24
+ )
25
+ resp = requests.post(
26
+ BASE_URL + endpoint, json=payload, headers={"X-API-Key": api_key}, timeout=TIMEOUT
27
+ )
28
+ if resp.status_code in (401, 403):
29
+ raise ValueError(
30
+ f"EnConvert rejected the API key (HTTP {resp.status_code}). Use a private "
31
+ "key (sk_...); public pk_ keys are not accepted."
32
+ )
33
+ resp.raise_for_status()
34
+ return resp.json()
35
+
36
+
37
+ def _csv(raw: str) -> list[str]:
38
+ return [s.strip() for s in raw.split(",") if s.strip()] if raw else []
39
+
40
+
41
+ class EnConvertDiscoverUrls(Component):
42
+ display_name = "Discover URLs"
43
+ description = "Map a site via sitemap, crawl, or hybrid, and return the discovered URLs."
44
+ documentation = "https://www.enconvert.com/docs"
45
+ icon = "Compass"
46
+ name = "enconvert_discover_urls" # stable internal id — do not rename
47
+
48
+ inputs = [
49
+ MessageTextInput(
50
+ name="url",
51
+ display_name="URL",
52
+ required=True,
53
+ info="The site to discover URLs from.",
54
+ ),
55
+ DropdownInput(
56
+ name="mode",
57
+ display_name="Mode",
58
+ options=["hybrid", "sitemap", "crawl"],
59
+ value="hybrid",
60
+ ),
61
+ IntInput(name="max_urls", display_name="Max URLs", value=100),
62
+ IntInput(name="max_depth", display_name="Max depth", value=2),
63
+ MessageTextInput(
64
+ name="include_patterns",
65
+ display_name="Include patterns",
66
+ info="Optional comma-separated glob patterns to keep.",
67
+ advanced=True,
68
+ ),
69
+ MessageTextInput(
70
+ name="exclude_patterns",
71
+ display_name="Exclude patterns",
72
+ info="Optional comma-separated glob patterns to drop.",
73
+ advanced=True,
74
+ ),
75
+ BoolInput(
76
+ name="same_domain_only",
77
+ display_name="Same domain only",
78
+ value=True,
79
+ advanced=True,
80
+ ),
81
+ SecretStrInput(
82
+ name="api_key",
83
+ display_name="EnConvert API Key",
84
+ required=True,
85
+ info="Private key (sk_...) from https://www.enconvert.com/dashboard/api-keys.",
86
+ ),
87
+ ]
88
+
89
+ outputs = [Output(display_name="Data", name="data", method="build")]
90
+
91
+ def build(self) -> Data:
92
+ payload: dict = {
93
+ "url": self.url,
94
+ "mode": self.mode,
95
+ "max_urls": self.max_urls,
96
+ "max_depth": self.max_depth,
97
+ "same_domain_only": self.same_domain_only,
98
+ }
99
+ include = _csv(self.include_patterns)
100
+ exclude = _csv(self.exclude_patterns)
101
+ if include:
102
+ payload["include_patterns"] = include
103
+ if exclude:
104
+ payload["exclude_patterns"] = exclude
105
+ result = _post("/v2/discover", self.api_key, payload)
106
+ self.status = f"{result.get('total', len(result.get('urls', [])))} URLs"
107
+ return Data(data=result)
@@ -0,0 +1,110 @@
1
+ """Extract Structured (distill) — pull typed data from pages against a schema."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ import requests
8
+ from lfx.custom.custom_component.component import Component
9
+ from lfx.io import IntInput, MessageTextInput, MultilineInput, Output, SecretStrInput
10
+ from lfx.schema.data import Data
11
+
12
+ BASE_URL = "https://api.enconvert.com"
13
+ TIMEOUT = 120
14
+
15
+
16
+ def _post(endpoint: str, api_key: str, payload: dict) -> dict:
17
+ """POST JSON to an EnConvert endpoint with the private-key header, return parsed JSON.
18
+
19
+ Inlined per component: the Langflow loader imports bundle files standalone, so
20
+ relative imports between them are unsupported.
21
+ """
22
+ if not api_key:
23
+ raise ValueError(
24
+ "EnConvert API key is missing. Add your private key (starts with sk_) "
25
+ "from https://www.enconvert.com/dashboard/api-keys."
26
+ )
27
+ resp = requests.post(
28
+ BASE_URL + endpoint, json=payload, headers={"X-API-Key": api_key}, timeout=TIMEOUT
29
+ )
30
+ if resp.status_code in (401, 403):
31
+ raise ValueError(
32
+ f"EnConvert rejected the API key (HTTP {resp.status_code}). Use a private "
33
+ "key (sk_...); public pk_ keys are not accepted."
34
+ )
35
+ resp.raise_for_status()
36
+ return resp.json()
37
+
38
+
39
+ def _csv(raw: str) -> list[str]:
40
+ return [s.strip() for s in raw.split(",") if s.strip()] if raw else []
41
+
42
+
43
+ class EnConvertExtractStructured(Component):
44
+ display_name = "Extract Structured"
45
+ description = (
46
+ "Extract structured data from one or more pages against a JSON schema "
47
+ "(or a flat field:description map). Give either URLs or a site to discover from."
48
+ )
49
+ documentation = "https://www.enconvert.com/docs"
50
+ icon = "Braces"
51
+ name = "enconvert_extract_structured" # stable internal id — do not rename
52
+
53
+ inputs = [
54
+ MultilineInput(
55
+ name="extraction_schema",
56
+ display_name="Schema",
57
+ required=True,
58
+ info=(
59
+ 'A JSON schema {"type":"object","properties":{...}} OR a flat '
60
+ '{"field":"description"} map.'
61
+ ),
62
+ ),
63
+ MessageTextInput(
64
+ name="urls",
65
+ display_name="URLs",
66
+ info="Comma-separated page URLs (max 50). Provide this OR 'Discover from URL'.",
67
+ ),
68
+ MessageTextInput(
69
+ name="discover_from_url",
70
+ display_name="Discover from URL",
71
+ info="A site to discover pages from, instead of listing URLs.",
72
+ ),
73
+ IntInput(
74
+ name="discover_max_pages",
75
+ display_name="Discover max pages",
76
+ value=25,
77
+ advanced=True,
78
+ info="Only used with 'Discover from URL'.",
79
+ ),
80
+ SecretStrInput(
81
+ name="api_key",
82
+ display_name="EnConvert API Key",
83
+ required=True,
84
+ info="Private key (sk_...) from https://www.enconvert.com/dashboard/api-keys.",
85
+ ),
86
+ ]
87
+
88
+ outputs = [Output(display_name="Data", name="data", method="build")]
89
+
90
+ def build(self) -> Data:
91
+ urls = _csv(self.urls)
92
+ discover = (self.discover_from_url or "").strip()
93
+ if bool(urls) == bool(discover):
94
+ raise ValueError(
95
+ "Provide exactly one of 'URLs' or 'Discover from URL' (not both, not neither)."
96
+ )
97
+ raw_schema = (self.extraction_schema or "").strip()
98
+ if not raw_schema:
99
+ raise ValueError("Provide a JSON schema or a flat {field: description} map.")
100
+ payload: dict = {"schema": json.loads(raw_schema)}
101
+ if urls:
102
+ payload["urls"] = urls
103
+ else:
104
+ discover_from: dict = {"url": discover}
105
+ if self.discover_max_pages:
106
+ discover_from["max_pages"] = self.discover_max_pages
107
+ payload["discover_from"] = discover_from
108
+ result = _post("/v2/distill", self.api_key, payload)
109
+ self.status = f"tier={result.get('extraction_tier')}"
110
+ return Data(data=result)
@@ -0,0 +1,93 @@
1
+ """Perceive URL — render a web page into agent-ready outputs with a quality score."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import requests
6
+ from lfx.custom.custom_component.component import Component
7
+ from lfx.io import BoolInput, MessageTextInput, Output, SecretStrInput
8
+ from lfx.schema.data import Data
9
+
10
+ BASE_URL = "https://api.enconvert.com"
11
+ TIMEOUT = 120
12
+
13
+
14
+ def _post(endpoint: str, api_key: str, payload: dict) -> dict:
15
+ """POST JSON to an EnConvert endpoint with the private-key header, return parsed JSON.
16
+
17
+ Bundle modules are imported standalone by the Langflow loader, so this helper is
18
+ inlined per component (relative imports between bundle files are unsupported).
19
+ """
20
+ if not api_key:
21
+ raise ValueError(
22
+ "EnConvert API key is missing. Add your private key (starts with sk_) "
23
+ "from https://www.enconvert.com/dashboard/api-keys."
24
+ )
25
+ resp = requests.post(
26
+ BASE_URL + endpoint, json=payload, headers={"X-API-Key": api_key}, timeout=TIMEOUT
27
+ )
28
+ if resp.status_code in (401, 403):
29
+ raise ValueError(
30
+ f"EnConvert rejected the API key (HTTP {resp.status_code}). Use a private "
31
+ "key (sk_...); public pk_ keys are not accepted."
32
+ )
33
+ resp.raise_for_status()
34
+ return resp.json()
35
+
36
+
37
+ def _csv(raw: str) -> list[str]:
38
+ return [s.strip() for s in raw.split(",") if s.strip()] if raw else []
39
+
40
+
41
+ class EnConvertPerceive(Component):
42
+ display_name = "Perceive URL"
43
+ description = (
44
+ "Render a web page into agent-ready outputs (markdown, structured data, "
45
+ "screenshot, PDF, links...) with a render_quality honesty score on every read."
46
+ )
47
+ documentation = "https://www.enconvert.com/docs"
48
+ icon = "Eye"
49
+ name = "enconvert_perceive" # stable internal id — do not rename
50
+
51
+ inputs = [
52
+ MessageTextInput(
53
+ name="url",
54
+ display_name="URL",
55
+ required=True,
56
+ info="The page to perceive.",
57
+ ),
58
+ MessageTextInput(
59
+ name="formats",
60
+ display_name="Outputs",
61
+ value="markdown,structured",
62
+ info=(
63
+ "Comma-separated. Allowed: markdown, html_cleaned, html_raw, screenshot, "
64
+ "screenshot_full_page, pdf, links, images, structured. markdown and "
65
+ "structured come back inline; screenshot/pdf/html return 15-minute signed URLs."
66
+ ),
67
+ ),
68
+ BoolInput(
69
+ name="only_main_content",
70
+ display_name="Only main content",
71
+ value=False,
72
+ info="Strip navigation and boilerplate, keep the main content.",
73
+ ),
74
+ SecretStrInput(
75
+ name="api_key",
76
+ display_name="EnConvert API Key",
77
+ required=True,
78
+ info="Private key (sk_...) from https://www.enconvert.com/dashboard/api-keys.",
79
+ ),
80
+ ]
81
+
82
+ outputs = [Output(display_name="Data", name="data", method="build")]
83
+
84
+ def build(self) -> Data:
85
+ payload: dict = {"url": self.url}
86
+ formats = _csv(self.formats)
87
+ if formats:
88
+ payload["outputs"] = formats
89
+ if self.only_main_content:
90
+ payload["only_main_content"] = True
91
+ result = _post("/v2/perceive", self.api_key, payload)
92
+ self.status = f"render_quality={result.get('render_quality')}"
93
+ return Data(data=result)
@@ -0,0 +1,104 @@
1
+ """Web Search — search the web (or news/images/scholar/patents/maps) via EnConvert."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import requests
6
+ from lfx.custom.custom_component.component import Component
7
+ from lfx.io import DropdownInput, IntInput, MessageTextInput, Output, SecretStrInput
8
+ from lfx.schema.data import Data
9
+
10
+ BASE_URL = "https://api.enconvert.com"
11
+ TIMEOUT = 120
12
+
13
+
14
+ def _post(endpoint: str, api_key: str, payload: dict) -> dict:
15
+ """POST JSON to an EnConvert endpoint with the private-key header, return parsed JSON.
16
+
17
+ Inlined per component: the Langflow loader imports bundle files standalone, so
18
+ relative imports between them are unsupported.
19
+ """
20
+ if not api_key:
21
+ raise ValueError(
22
+ "EnConvert API key is missing. Add your private key (starts with sk_) "
23
+ "from https://www.enconvert.com/dashboard/api-keys."
24
+ )
25
+ resp = requests.post(
26
+ BASE_URL + endpoint, json=payload, headers={"X-API-Key": api_key}, timeout=TIMEOUT
27
+ )
28
+ if resp.status_code in (401, 403):
29
+ raise ValueError(
30
+ f"EnConvert rejected the API key (HTTP {resp.status_code}). Use a private "
31
+ "key (sk_...); public pk_ keys are not accepted."
32
+ )
33
+ resp.raise_for_status()
34
+ return resp.json()
35
+
36
+
37
+ class EnConvertWebSearch(Component):
38
+ display_name = "Web Search"
39
+ description = "Search the web and get back structured results (title, url, snippet, position)."
40
+ documentation = "https://www.enconvert.com/docs"
41
+ icon = "Search"
42
+ name = "enconvert_web_search" # stable internal id — do not rename
43
+
44
+ inputs = [
45
+ MessageTextInput(
46
+ name="query",
47
+ display_name="Query",
48
+ required=True,
49
+ info="The search query.",
50
+ ),
51
+ DropdownInput(
52
+ name="category",
53
+ display_name="Category",
54
+ options=["web", "news", "images", "scholar", "patents", "maps"],
55
+ value="web",
56
+ ),
57
+ IntInput(
58
+ name="num_results",
59
+ display_name="Results",
60
+ value=10,
61
+ info="Number of results to return.",
62
+ ),
63
+ MessageTextInput(
64
+ name="country",
65
+ display_name="Country",
66
+ info="Optional ISO country code (e.g. us).",
67
+ advanced=True,
68
+ ),
69
+ MessageTextInput(
70
+ name="locale",
71
+ display_name="Locale",
72
+ info="Optional locale (e.g. en).",
73
+ advanced=True,
74
+ ),
75
+ DropdownInput(
76
+ name="time_filter",
77
+ display_name="Time filter",
78
+ options=["", "hour", "day", "week", "month", "year"],
79
+ value="",
80
+ advanced=True,
81
+ ),
82
+ SecretStrInput(
83
+ name="api_key",
84
+ display_name="EnConvert API Key",
85
+ required=True,
86
+ info="Private key (sk_...) from https://www.enconvert.com/dashboard/api-keys.",
87
+ ),
88
+ ]
89
+
90
+ outputs = [Output(display_name="Data", name="data", method="build")]
91
+
92
+ def build(self) -> Data:
93
+ payload: dict = {"query": self.query, "category": self.category}
94
+ if self.num_results:
95
+ payload["num_results"] = self.num_results
96
+ if self.country:
97
+ payload["country"] = self.country
98
+ if self.locale:
99
+ payload["locale"] = self.locale
100
+ if self.time_filter:
101
+ payload["time_filter"] = self.time_filter
102
+ result = _post("/v2/search", self.api_key, payload)
103
+ self.status = f"{len(result.get('results', []))} results"
104
+ return Data(data=result)
@@ -0,0 +1,16 @@
1
+ {
2
+ "$schema": "https://schemas.langflow.org/extension/v1.json",
3
+ "id": "lfx-enconvert",
4
+ "version": "0.0.1",
5
+ "name": "EnConvert",
6
+ "description": "EnConvert bundle for Langflow: perceive URLs, search the web, discover URLs, extract structured data, and convert files — every read scored with render_quality.",
7
+ "lfx": {
8
+ "compat": ["1"]
9
+ },
10
+ "bundles": [
11
+ {
12
+ "name": "enconvert",
13
+ "path": "components/enconvert"
14
+ }
15
+ ]
16
+ }