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.
- lfx_enconvert-0.0.1/CHANGELOG.md +20 -0
- lfx_enconvert-0.0.1/LICENSE +21 -0
- lfx_enconvert-0.0.1/PKG-INFO +57 -0
- lfx_enconvert-0.0.1/README.md +41 -0
- lfx_enconvert-0.0.1/pyproject.toml +35 -0
- lfx_enconvert-0.0.1/src/lfx_enconvert/__init__.py +7 -0
- lfx_enconvert-0.0.1/src/lfx_enconvert/components/__init__.py +1 -0
- lfx_enconvert-0.0.1/src/lfx_enconvert/components/enconvert/__init__.py +17 -0
- lfx_enconvert-0.0.1/src/lfx_enconvert/components/enconvert/convert_markdown.py +78 -0
- lfx_enconvert-0.0.1/src/lfx_enconvert/components/enconvert/convert_pdf.py +78 -0
- lfx_enconvert-0.0.1/src/lfx_enconvert/components/enconvert/discover_urls.py +107 -0
- lfx_enconvert-0.0.1/src/lfx_enconvert/components/enconvert/extract_structured.py +110 -0
- lfx_enconvert-0.0.1/src/lfx_enconvert/components/enconvert/perceive.py +93 -0
- lfx_enconvert-0.0.1/src/lfx_enconvert/components/enconvert/web_search.py +104 -0
- lfx_enconvert-0.0.1/src/lfx_enconvert/extension.json +16 -0
|
@@ -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
|
+
}
|