everalgo-parser 0.1.0__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.
- everalgo_parser-0.1.0/.gitignore +58 -0
- everalgo_parser-0.1.0/CHANGELOG.md +76 -0
- everalgo_parser-0.1.0/LICENSE +21 -0
- everalgo_parser-0.1.0/PKG-INFO +104 -0
- everalgo_parser-0.1.0/README.md +79 -0
- everalgo_parser-0.1.0/pyproject.toml +45 -0
- everalgo_parser-0.1.0/src/everalgo/parser/__init__.py +148 -0
- everalgo_parser-0.1.0/src/everalgo/parser/_utils.py +216 -0
- everalgo_parser-0.1.0/src/everalgo/parser/audio.py +72 -0
- everalgo_parser-0.1.0/src/everalgo/parser/document.py +428 -0
- everalgo_parser-0.1.0/src/everalgo/parser/image.py +266 -0
- everalgo_parser-0.1.0/src/everalgo/parser/prompts/__init__.py +12 -0
- everalgo_parser-0.1.0/src/everalgo/parser/prompts/en/__init__.py +13 -0
- everalgo_parser-0.1.0/src/everalgo/parser/prompts/en/audio.py +15 -0
- everalgo_parser-0.1.0/src/everalgo/parser/prompts/en/document.py +40 -0
- everalgo_parser-0.1.0/src/everalgo/parser/prompts/en/image.py +36 -0
- everalgo_parser-0.1.0/src/everalgo/parser/prompts/zh/__init__.py +13 -0
- everalgo_parser-0.1.0/src/everalgo/parser/prompts/zh/audio.py +14 -0
- everalgo_parser-0.1.0/src/everalgo/parser/prompts/zh/document.py +40 -0
- everalgo_parser-0.1.0/src/everalgo/parser/prompts/zh/image.py +37 -0
- everalgo_parser-0.1.0/src/everalgo/parser/py.typed +0 -0
- everalgo_parser-0.1.0/src/everalgo/parser/url.py +270 -0
- everalgo_parser-0.1.0/src/everalgo/parser/video.py +38 -0
- everalgo_parser-0.1.0/tests/fixtures/sample.bmp +0 -0
- everalgo_parser-0.1.0/tests/fixtures/sample.docx +0 -0
- everalgo_parser-0.1.0/tests/fixtures/sample.eml +255 -0
- everalgo_parser-0.1.0/tests/fixtures/sample.htm +33 -0
- everalgo_parser-0.1.0/tests/fixtures/sample.html +33 -0
- everalgo_parser-0.1.0/tests/fixtures/sample.jpg +0 -0
- everalgo_parser-0.1.0/tests/fixtures/sample.pdf +49 -0
- everalgo_parser-0.1.0/tests/fixtures/sample.png +0 -0
- everalgo_parser-0.1.0/tests/fixtures/sample.pptx +0 -0
- everalgo_parser-0.1.0/tests/fixtures/sample.svg +9 -0
- everalgo_parser-0.1.0/tests/fixtures/sample.tiff +0 -0
- everalgo_parser-0.1.0/tests/fixtures/sample.txt +14 -0
- everalgo_parser-0.1.0/tests/fixtures/sample.wav +0 -0
- everalgo_parser-0.1.0/tests/fixtures/sample.webp +0 -0
- everalgo_parser-0.1.0/tests/fixtures/sample.xlsx +0 -0
- everalgo_parser-0.1.0/tests/parser/__init__.py +0 -0
- everalgo_parser-0.1.0/tests/parser/test_audio.py +48 -0
- everalgo_parser-0.1.0/tests/parser/test_dispatch.py +305 -0
- everalgo_parser-0.1.0/tests/parser/test_document.py +253 -0
- everalgo_parser-0.1.0/tests/parser/test_e2e_all_formats.py +202 -0
- everalgo_parser-0.1.0/tests/parser/test_e2e_pdf_openrouter.py +69 -0
- everalgo_parser-0.1.0/tests/parser/test_image.py +185 -0
- everalgo_parser-0.1.0/tests/parser/test_public_api.py +38 -0
- everalgo_parser-0.1.0/tests/parser/test_url.py +296 -0
- everalgo_parser-0.1.0/tests/parser/test_utils.py +160 -0
- everalgo_parser-0.1.0/tests/parser/test_video.py +24 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# Distribution / packaging
|
|
7
|
+
build/
|
|
8
|
+
dist/
|
|
9
|
+
*.egg-info/
|
|
10
|
+
*.egg
|
|
11
|
+
wheels/
|
|
12
|
+
|
|
13
|
+
# Environments
|
|
14
|
+
.env
|
|
15
|
+
.env.local
|
|
16
|
+
.venv
|
|
17
|
+
env/
|
|
18
|
+
venv/
|
|
19
|
+
|
|
20
|
+
# uv
|
|
21
|
+
.uv/
|
|
22
|
+
|
|
23
|
+
# Test / coverage / cache
|
|
24
|
+
.pytest_cache/
|
|
25
|
+
.cache
|
|
26
|
+
.coverage
|
|
27
|
+
.coverage.*
|
|
28
|
+
coverage.xml
|
|
29
|
+
report.xml
|
|
30
|
+
htmlcov/
|
|
31
|
+
.tox/
|
|
32
|
+
.nox/
|
|
33
|
+
.hypothesis/
|
|
34
|
+
|
|
35
|
+
# Type checkers / linters
|
|
36
|
+
.mypy_cache/
|
|
37
|
+
.dmypy.json
|
|
38
|
+
.pyre/
|
|
39
|
+
.pytype/
|
|
40
|
+
.ruff_cache/
|
|
41
|
+
|
|
42
|
+
# Jupyter
|
|
43
|
+
.ipynb_checkpoints/
|
|
44
|
+
|
|
45
|
+
# IDE / OS
|
|
46
|
+
.vscode/
|
|
47
|
+
.idea/
|
|
48
|
+
*.swp
|
|
49
|
+
.DS_Store
|
|
50
|
+
|
|
51
|
+
# Logs
|
|
52
|
+
*.log
|
|
53
|
+
|
|
54
|
+
# Project local artifacts (placed under local/ per project convention)
|
|
55
|
+
local/
|
|
56
|
+
|
|
57
|
+
# Claude Code session-local state (scheduled tasks, agent caches, locks)
|
|
58
|
+
.claude/
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Changelog — everalgo-parser
|
|
2
|
+
|
|
3
|
+
All notable changes to this distribution will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
Entries are generated by [git-cliff](https://git-cliff.org/) from Conventional
|
|
9
|
+
Commit messages on `main`. See the repository root [`README.md` § Cutting a release](../../README.md#cutting-a-release)
|
|
10
|
+
for the full workflow.
|
|
11
|
+
|
|
12
|
+
<!-- git-cliff-unreleased-start -->
|
|
13
|
+
## [Unreleased]
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **PDF parser** — single multimodal LLM call, mirrors
|
|
18
|
+
`evermemos-multimodal/src/parser/pdf/gemini_parser.py`.
|
|
19
|
+
- **Image parser** — single LLM call for normal-ratio images; tall
|
|
20
|
+
screenshots (height/width > 10) are split into overlapping vertical
|
|
21
|
+
slices, OCR'd per slice, and merged with a second LLM call using
|
|
22
|
+
`PROMPT_FOR_MERGE`. BMP / TIFF are transcoded to PNG via Pillow before
|
|
23
|
+
upload (Gemini does not accept those MIMEs natively); SVG is rasterised
|
|
24
|
+
via the optional `cairosvg` extra (`everalgo-parser[svg]`).
|
|
25
|
+
- **Audio parser** — ASR via multimodal LLM; covers
|
|
26
|
+
mp3 / wav / m4a / amr / aiff / aac / ogg / flac.
|
|
27
|
+
- **HTML / `htm` handler** (in `document.aparse`) — bs4 noise cleanup
|
|
28
|
+
(`clean_html_for_llm` drops `<script>` / `<style>` / `<nav>` / `<footer>`
|
|
29
|
+
/ `<iframe>` and bloat attributes) then LLM extraction.
|
|
30
|
+
- **Email (`.eml`) handler** (in `document.aparse`) — stdlib `email`
|
|
31
|
+
parsing for headers + body, with inline-image OCR via `cid:` placeholder
|
|
32
|
+
substitution.
|
|
33
|
+
- **Office document handler** (in `document.aparse`) — covers docx / xlsx
|
|
34
|
+
/ pptx / doc / xls / ppt / pages / key / numbers / odt / ods / odp /
|
|
35
|
+
rtf via a LibreOffice subprocess (`soffice --convert-to pdf` → reuse PDF
|
|
36
|
+
path). `soffice` must be installed on the host (see README).
|
|
37
|
+
- **URL parser** — fetches `http`/`https` URIs via httpx, then **dispatches
|
|
38
|
+
by the fetched `Content-Type`**: `application/pdf` → PDF handler,
|
|
39
|
+
`image/*` → image handler, `audio/*` → audio handler, `text/html` (or
|
|
40
|
+
unknown) → HTML handler with Open Graph / Twitter Card / `<meta>` tag
|
|
41
|
+
extraction into `ParsedContent.metadata`. `file://` and other local-
|
|
42
|
+
filesystem URIs are rejected (AGENTS.md §1). Inner modality recorded in
|
|
43
|
+
`metadata["inner_modality"]`. Metadata-extractor schema lifted from
|
|
44
|
+
`evermemos-opensource/src/common_utils/url_extractor.py`.
|
|
45
|
+
- **MIME-aware top-level dispatch** — `parser.aparse` now falls back to
|
|
46
|
+
`mime` when `extension` is missing or `UNKNOWN`, so the canonical
|
|
47
|
+
`RawFile(uri=..., mime="application/pdf")` example actually routes to
|
|
48
|
+
the PDF handler. Added `Modality` helpers in `everalgo-core/types/
|
|
49
|
+
modality.py`: `MIME_TO_MODALITY` / `MIME_TO_EXTENSION` /
|
|
50
|
+
`get_modality_from_mime(mime)` / `get_extension_from_mime(mime)`.
|
|
51
|
+
- **Chinese prompts** at `prompts/zh/{image,audio,document}.py`
|
|
52
|
+
(`PROMPT_FOR_PICTURE` / `PROMPT_FOR_MERGE` / `PROMPT_FOR_FILE` /
|
|
53
|
+
`PROMPT_FOR_HTML` / `PROMPT_FOR_AUDIO`), mirroring the English set.
|
|
54
|
+
- **Optional `[svg]` extra** pulling in `cairosvg` for SVG rasterisation.
|
|
55
|
+
- **Re-exports** of `ParsedContent` / `Modality` / `RawFile` from
|
|
56
|
+
`everalgo.parser` so callers can `from everalgo.parser import aparse,
|
|
57
|
+
RawFile, ParsedContent`.
|
|
58
|
+
|
|
59
|
+
### Notes
|
|
60
|
+
|
|
61
|
+
- `video` submodule remains a stub — upstream evermemos-multimodal has no
|
|
62
|
+
video implementation to port; backend selection pending an ADR.
|
|
63
|
+
- The parser layer deliberately omits retry / fallback / multi-key
|
|
64
|
+
rotation / Prometheus metrics. Those are deployment concerns
|
|
65
|
+
(ADR-012); algorithm-layer operators surface failures via `LLMError`
|
|
66
|
+
and callers wrap as needed.
|
|
67
|
+
|
|
68
|
+
### Dependencies
|
|
69
|
+
|
|
70
|
+
- `pillow >= 10.0.0` — image transcode + aspect-ratio check.
|
|
71
|
+
- `beautifulsoup4 >= 4.12.0` — HTML cleanup + OG metadata extraction.
|
|
72
|
+
- `asgiref >= 3.8.0` — sync bridge (ADR-010).
|
|
73
|
+
- Optional `cairosvg >= 2.7.0` via `[svg]` extra.
|
|
74
|
+
- System: LibreOffice (`soffice` on PATH) for Office document parsing.
|
|
75
|
+
|
|
76
|
+
<!-- git-cliff-unreleased-end -->
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 EverMind
|
|
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,104 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: everalgo-parser
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: EverAlgo parser: multimodal raw-file parsing (image / audio / document / video / url) into ParsedContent.
|
|
5
|
+
Project-URL: Homepage, https://github.com/EverMind-AI/EverAlgo
|
|
6
|
+
Project-URL: Repository, https://github.com/EverMind-AI/EverAlgo
|
|
7
|
+
Project-URL: Issues, https://github.com/EverMind-AI/EverAlgo/issues
|
|
8
|
+
Project-URL: Documentation, https://github.com/EverMind-AI/EverAlgo/tree/main/packages/everalgo-parser
|
|
9
|
+
Project-URL: Changelog, https://github.com/EverMind-AI/EverAlgo/blob/main/packages/everalgo-parser/CHANGELOG.md
|
|
10
|
+
Author: EverMind
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Requires-Dist: asgiref>=3.8.0
|
|
18
|
+
Requires-Dist: beautifulsoup4>=4.12.0
|
|
19
|
+
Requires-Dist: everalgo-core<2.0.0,>=0.1.0
|
|
20
|
+
Requires-Dist: httpx>=0.27.0
|
|
21
|
+
Requires-Dist: pillow>=10.0.0
|
|
22
|
+
Provides-Extra: svg
|
|
23
|
+
Requires-Dist: cairosvg>=2.7.0; extra == 'svg'
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# everalgo-parser
|
|
27
|
+
|
|
28
|
+
Multimodal parsing — image / audio / document / video / url raw inputs into `ParsedContent`. Used by `everalgo-knowledge` for file ingestion and by evermem step 1 for inline parsing.
|
|
29
|
+
|
|
30
|
+
See the umbrella project: [EverAlgo monorepo](../../README.md) and the architecture document at [`docs/concepts/architecture.md`](../../docs/concepts/architecture.md).
|
|
31
|
+
|
|
32
|
+
## Quick start
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import everalgo
|
|
36
|
+
from everalgo.llm.config import LLMConfig
|
|
37
|
+
from everalgo.llm.providers.openai_compat import OpenAICompatClient
|
|
38
|
+
from everalgo.parser import aparse, RawFile
|
|
39
|
+
|
|
40
|
+
# Configure an LLM once (process-wide). The parser uses OpenAI-compatible
|
|
41
|
+
# clients; OpenRouter is the reference deployment (Gemini multimodal via
|
|
42
|
+
# OpenRouter passthrough).
|
|
43
|
+
everalgo.configure(OpenAICompatClient(LLMConfig(
|
|
44
|
+
model="google/gemini-3-flash-preview",
|
|
45
|
+
api_key="sk-or-v1-...",
|
|
46
|
+
base_url="https://openrouter.ai/api/v1",
|
|
47
|
+
)))
|
|
48
|
+
|
|
49
|
+
# Bytes-in: caller already hydrated the file.
|
|
50
|
+
parsed = await aparse(RawFile(content=pdf_bytes, extension="pdf"))
|
|
51
|
+
print(parsed.text)
|
|
52
|
+
|
|
53
|
+
# URL-in: parser fetches over HTTP, then delegates to the HTML handler.
|
|
54
|
+
parsed = await aparse(RawFile(uri="https://example.com/article"))
|
|
55
|
+
print(parsed.metadata["title"], parsed.text[:500])
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Supported formats
|
|
59
|
+
|
|
60
|
+
| Modality | Extensions | Backend |
|
|
61
|
+
|----------|------------|---------|
|
|
62
|
+
| `PDF` | `pdf` | Multimodal LLM (single call, full doc) |
|
|
63
|
+
| `IMAGE` | `png` / `jpg` / `jpeg` / `webp` / `bmp` / `tiff` / `tif` / `svg` | Multimodal LLM; BMP/TIFF transcoded to PNG via Pillow; SVG rasterised via `cairosvg`; tall screenshots split + merged |
|
|
64
|
+
| `AUDIO` | `mp3` / `wav` / `m4a` / `amr` / `aiff` / `aac` / `ogg` / `flac` | Multimodal LLM ASR |
|
|
65
|
+
| `HTML` | `html` / `htm` | bs4 cleanup → LLM extraction |
|
|
66
|
+
| `EMAIL` | `eml` | stdlib `email` + inline-image OCR via the LLM |
|
|
67
|
+
| `DOCUMENT` | `docx` / `pptx` / `xlsx` / `doc` / `ppt` / `xls` / `pages` / `key` / `numbers` / `odt` / `ods` / `odp` / `rtf` | LibreOffice `soffice --convert-to pdf` → reuse PDF path |
|
|
68
|
+
| `URL` | (any `http`/`https` URI) | httpx fetch → HTML handler |
|
|
69
|
+
| `DIRECT` | `txt` / `md` / `csv` / `tsv` / `vtt` | UTF-8 decode, no LLM |
|
|
70
|
+
| `VIDEO` | — | Deferred (no upstream implementation; ADR pending) |
|
|
71
|
+
|
|
72
|
+
## Installation
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install everalgo-parser # core: pdf / image / audio / html / eml / direct / url
|
|
76
|
+
pip install 'everalgo-parser[svg]' # adds SVG support (cairosvg)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### System dependency for Office documents
|
|
80
|
+
|
|
81
|
+
Office document parsing (docx / xlsx / pptx / …) shells out to **LibreOffice**, which is a system package, not a pip wheel. Install before parsing Office files:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# Ubuntu / Debian
|
|
85
|
+
sudo apt-get install -y libreoffice
|
|
86
|
+
|
|
87
|
+
# macOS
|
|
88
|
+
brew install --cask libreoffice
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The parser detects `soffice` via `shutil.which("soffice")` and the canonical macOS Applications path. Missing → `RuntimeError` with install instructions when an Office file is parsed; non-Office paths are unaffected.
|
|
92
|
+
|
|
93
|
+
## Conventions
|
|
94
|
+
|
|
95
|
+
- `aparse(...)` is async; `parse(...)` is the sync bridge via `asgiref.async_to_sync`.
|
|
96
|
+
- Prompts live as module-level string constants under `prompts/{en,zh}/<operator>.py` ([AGENTS.md §5](../../AGENTS.md#5-code-style)). Swap languages by re-binding the constant at startup.
|
|
97
|
+
- The library is **stateless**: it never reads the filesystem and never owns business state. HTTP I/O (LLM calls, URL fetching) is explicitly allowed.
|
|
98
|
+
- No retry / fallback / metrics inside operators — surface failures via `LLMError`, let the caller wrap.
|
|
99
|
+
|
|
100
|
+
## Reference
|
|
101
|
+
|
|
102
|
+
- Architecture (definitive): [`docs/concepts/architecture.md`](../../docs/concepts/architecture.md)
|
|
103
|
+
- Schema source for PDF / image / audio / document / html / email: `evermemos-multimodal` (tag `prod-20260306-0331-v1`).
|
|
104
|
+
- Schema source for URL metadata extraction: `evermemos-opensource/src/common_utils/url_extractor.py`.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# everalgo-parser
|
|
2
|
+
|
|
3
|
+
Multimodal parsing — image / audio / document / video / url raw inputs into `ParsedContent`. Used by `everalgo-knowledge` for file ingestion and by evermem step 1 for inline parsing.
|
|
4
|
+
|
|
5
|
+
See the umbrella project: [EverAlgo monorepo](../../README.md) and the architecture document at [`docs/concepts/architecture.md`](../../docs/concepts/architecture.md).
|
|
6
|
+
|
|
7
|
+
## Quick start
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
import everalgo
|
|
11
|
+
from everalgo.llm.config import LLMConfig
|
|
12
|
+
from everalgo.llm.providers.openai_compat import OpenAICompatClient
|
|
13
|
+
from everalgo.parser import aparse, RawFile
|
|
14
|
+
|
|
15
|
+
# Configure an LLM once (process-wide). The parser uses OpenAI-compatible
|
|
16
|
+
# clients; OpenRouter is the reference deployment (Gemini multimodal via
|
|
17
|
+
# OpenRouter passthrough).
|
|
18
|
+
everalgo.configure(OpenAICompatClient(LLMConfig(
|
|
19
|
+
model="google/gemini-3-flash-preview",
|
|
20
|
+
api_key="sk-or-v1-...",
|
|
21
|
+
base_url="https://openrouter.ai/api/v1",
|
|
22
|
+
)))
|
|
23
|
+
|
|
24
|
+
# Bytes-in: caller already hydrated the file.
|
|
25
|
+
parsed = await aparse(RawFile(content=pdf_bytes, extension="pdf"))
|
|
26
|
+
print(parsed.text)
|
|
27
|
+
|
|
28
|
+
# URL-in: parser fetches over HTTP, then delegates to the HTML handler.
|
|
29
|
+
parsed = await aparse(RawFile(uri="https://example.com/article"))
|
|
30
|
+
print(parsed.metadata["title"], parsed.text[:500])
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Supported formats
|
|
34
|
+
|
|
35
|
+
| Modality | Extensions | Backend |
|
|
36
|
+
|----------|------------|---------|
|
|
37
|
+
| `PDF` | `pdf` | Multimodal LLM (single call, full doc) |
|
|
38
|
+
| `IMAGE` | `png` / `jpg` / `jpeg` / `webp` / `bmp` / `tiff` / `tif` / `svg` | Multimodal LLM; BMP/TIFF transcoded to PNG via Pillow; SVG rasterised via `cairosvg`; tall screenshots split + merged |
|
|
39
|
+
| `AUDIO` | `mp3` / `wav` / `m4a` / `amr` / `aiff` / `aac` / `ogg` / `flac` | Multimodal LLM ASR |
|
|
40
|
+
| `HTML` | `html` / `htm` | bs4 cleanup → LLM extraction |
|
|
41
|
+
| `EMAIL` | `eml` | stdlib `email` + inline-image OCR via the LLM |
|
|
42
|
+
| `DOCUMENT` | `docx` / `pptx` / `xlsx` / `doc` / `ppt` / `xls` / `pages` / `key` / `numbers` / `odt` / `ods` / `odp` / `rtf` | LibreOffice `soffice --convert-to pdf` → reuse PDF path |
|
|
43
|
+
| `URL` | (any `http`/`https` URI) | httpx fetch → HTML handler |
|
|
44
|
+
| `DIRECT` | `txt` / `md` / `csv` / `tsv` / `vtt` | UTF-8 decode, no LLM |
|
|
45
|
+
| `VIDEO` | — | Deferred (no upstream implementation; ADR pending) |
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install everalgo-parser # core: pdf / image / audio / html / eml / direct / url
|
|
51
|
+
pip install 'everalgo-parser[svg]' # adds SVG support (cairosvg)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### System dependency for Office documents
|
|
55
|
+
|
|
56
|
+
Office document parsing (docx / xlsx / pptx / …) shells out to **LibreOffice**, which is a system package, not a pip wheel. Install before parsing Office files:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# Ubuntu / Debian
|
|
60
|
+
sudo apt-get install -y libreoffice
|
|
61
|
+
|
|
62
|
+
# macOS
|
|
63
|
+
brew install --cask libreoffice
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The parser detects `soffice` via `shutil.which("soffice")` and the canonical macOS Applications path. Missing → `RuntimeError` with install instructions when an Office file is parsed; non-Office paths are unaffected.
|
|
67
|
+
|
|
68
|
+
## Conventions
|
|
69
|
+
|
|
70
|
+
- `aparse(...)` is async; `parse(...)` is the sync bridge via `asgiref.async_to_sync`.
|
|
71
|
+
- Prompts live as module-level string constants under `prompts/{en,zh}/<operator>.py` ([AGENTS.md §5](../../AGENTS.md#5-code-style)). Swap languages by re-binding the constant at startup.
|
|
72
|
+
- The library is **stateless**: it never reads the filesystem and never owns business state. HTTP I/O (LLM calls, URL fetching) is explicitly allowed.
|
|
73
|
+
- No retry / fallback / metrics inside operators — surface failures via `LLMError`, let the caller wrap.
|
|
74
|
+
|
|
75
|
+
## Reference
|
|
76
|
+
|
|
77
|
+
- Architecture (definitive): [`docs/concepts/architecture.md`](../../docs/concepts/architecture.md)
|
|
78
|
+
- Schema source for PDF / image / audio / document / html / email: `evermemos-multimodal` (tag `prod-20260306-0331-v1`).
|
|
79
|
+
- Schema source for URL metadata extraction: `evermemos-opensource/src/common_utils/url_extractor.py`.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "everalgo-parser"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "EverAlgo parser: multimodal raw-file parsing (image / audio / document / video / url) into ParsedContent."
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
authors = [{ name = "EverMind" }]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
]
|
|
18
|
+
dependencies = [
|
|
19
|
+
"everalgo-core>=0.1.0,<2.0.0",
|
|
20
|
+
"asgiref>=3.8.0",
|
|
21
|
+
"pillow>=10.0.0",
|
|
22
|
+
"beautifulsoup4>=4.12.0",
|
|
23
|
+
# The URL parser uses httpx directly for ``http``/``https`` fetches; declare
|
|
24
|
+
# explicitly rather than relying on the transitive pull-in via openai SDK.
|
|
25
|
+
"httpx>=0.27.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/EverMind-AI/EverAlgo"
|
|
30
|
+
Repository = "https://github.com/EverMind-AI/EverAlgo"
|
|
31
|
+
Issues = "https://github.com/EverMind-AI/EverAlgo/issues"
|
|
32
|
+
Documentation = "https://github.com/EverMind-AI/EverAlgo/tree/main/packages/everalgo-parser"
|
|
33
|
+
Changelog = "https://github.com/EverMind-AI/EverAlgo/blob/main/packages/everalgo-parser/CHANGELOG.md"
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
# SVG rasterisation; pulled in only when callers parse .svg files.
|
|
37
|
+
svg = ["cairosvg>=2.7.0"]
|
|
38
|
+
# Office document conversion (docx / xlsx / pptx / ...) requires LibreOffice
|
|
39
|
+
# (``soffice``) as a SYSTEM dependency, not a pip package. Install via:
|
|
40
|
+
# Linux: apt install libreoffice
|
|
41
|
+
# macOS: brew install --cask libreoffice
|
|
42
|
+
# There is no pip extra for it — the parser shells out at runtime.
|
|
43
|
+
|
|
44
|
+
[tool.hatch.build.targets.wheel]
|
|
45
|
+
packages = ["src/everalgo"]
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Multimodal parser — top-level dispatch by modality.
|
|
2
|
+
|
|
3
|
+
Public surface (re-exports for ergonomic ``from everalgo.parser import ...``):
|
|
4
|
+
|
|
5
|
+
- Functions: ``aparse`` / ``parse`` — dispatch entry points.
|
|
6
|
+
- Submodules: ``image`` / ``audio`` / ``document`` / ``url`` / ``video``.
|
|
7
|
+
- Types: ``ParsedContent`` / ``Modality`` / ``RawFile`` (re-exported from
|
|
8
|
+
``everalgo.types`` for one-stop import).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
from urllib.parse import urlparse
|
|
16
|
+
|
|
17
|
+
from asgiref.sync import async_to_sync
|
|
18
|
+
|
|
19
|
+
from everalgo.parser import audio, document, image, url, video
|
|
20
|
+
from everalgo.types import (
|
|
21
|
+
Modality,
|
|
22
|
+
ParsedContent,
|
|
23
|
+
RawFile,
|
|
24
|
+
get_extension_from_mime,
|
|
25
|
+
get_modality,
|
|
26
|
+
get_modality_from_mime,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from everalgo.llm import LLMClient
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"Modality",
|
|
34
|
+
"ParsedContent",
|
|
35
|
+
"RawFile",
|
|
36
|
+
"aparse",
|
|
37
|
+
"audio",
|
|
38
|
+
"document",
|
|
39
|
+
"image",
|
|
40
|
+
"parse",
|
|
41
|
+
"url",
|
|
42
|
+
"video",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
logger = logging.getLogger(__name__)
|
|
46
|
+
logger.addHandler(logging.NullHandler())
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def aparse(raw_file: RawFile, *, llm: LLMClient) -> ParsedContent:
|
|
50
|
+
"""Top-level dispatch: classify the input and route to a submodule.
|
|
51
|
+
|
|
52
|
+
Dispatch precedence:
|
|
53
|
+
|
|
54
|
+
1. ``content`` empty + ``uri`` is ``http``/``https`` → ``url.aparse``
|
|
55
|
+
(the URL parser fetches and re-dispatches by the response Content-Type).
|
|
56
|
+
2. ``content`` non-empty → resolve a ``Modality`` from
|
|
57
|
+
``extension`` first, falling back to ``mime`` when extension is
|
|
58
|
+
missing or ``UNKNOWN``.
|
|
59
|
+
3. Submodule routing by resolved ``Modality``:
|
|
60
|
+
- ``IMAGE`` → ``image.aparse``
|
|
61
|
+
- ``AUDIO`` → ``audio.aparse``
|
|
62
|
+
- ``PDF`` / ``DOCUMENT`` / ``HTML`` / ``EMAIL`` → ``document.aparse``
|
|
63
|
+
- ``DIRECT`` → no LLM; bytes are UTF-8-decoded inline.
|
|
64
|
+
- Anything else → ``ValueError``.
|
|
65
|
+
|
|
66
|
+
When dispatch resolves a modality via ``mime`` but the submodule
|
|
67
|
+
internally requires ``extension`` (image / audio / document all do for
|
|
68
|
+
their ``_MIME_MAP`` lookups), this function fills ``extension`` in
|
|
69
|
+
from the canonical extension table before forwarding.
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
raw_file : RawFile
|
|
74
|
+
Hydrated ``content`` or ``uri`` (at least one is required).
|
|
75
|
+
llm : LLMClient
|
|
76
|
+
Per-call LLM client. Required for all LLM-backed modalities (IMAGE, AUDIO, PDF, etc.).
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
-------
|
|
80
|
+
ParsedContent
|
|
81
|
+
Normalized parser output.
|
|
82
|
+
"""
|
|
83
|
+
# URL path: only when caller did not pre-hydrate content.
|
|
84
|
+
if not raw_file.content and raw_file.uri and _is_http_uri(raw_file.uri):
|
|
85
|
+
return await url.aparse(raw_file, llm=llm)
|
|
86
|
+
|
|
87
|
+
if not raw_file.content:
|
|
88
|
+
raise ValueError(
|
|
89
|
+
"Cannot dispatch: RawFile has no content. "
|
|
90
|
+
"Provide either an http(s) uri (the URL parser fetches) or "
|
|
91
|
+
"hydrated bytes."
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Resolve modality: extension wins; fall back to mime.
|
|
95
|
+
modality = get_modality(raw_file.extension) if raw_file.extension else Modality.UNKNOWN
|
|
96
|
+
if modality is Modality.UNKNOWN and raw_file.mime:
|
|
97
|
+
modality = get_modality_from_mime(raw_file.mime)
|
|
98
|
+
if modality is Modality.UNKNOWN:
|
|
99
|
+
raise ValueError(
|
|
100
|
+
f"Cannot dispatch: extension={raw_file.extension!r} mime={raw_file.mime!r} do not map to a known Modality."
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Ensure ``extension`` is populated for downstream submodules that key
|
|
104
|
+
# their internal MIME tables off it (image / audio / document do).
|
|
105
|
+
routed = _ensure_extension(raw_file)
|
|
106
|
+
|
|
107
|
+
if modality is Modality.IMAGE:
|
|
108
|
+
return await image.aparse(routed, llm=llm)
|
|
109
|
+
if modality is Modality.AUDIO:
|
|
110
|
+
return await audio.aparse(routed, llm=llm)
|
|
111
|
+
if modality in (Modality.PDF, Modality.DOCUMENT, Modality.HTML, Modality.EMAIL):
|
|
112
|
+
return await document.aparse(routed, llm=llm)
|
|
113
|
+
if modality is Modality.DIRECT:
|
|
114
|
+
return ParsedContent(
|
|
115
|
+
text=routed.content.decode("utf-8", errors="replace"),
|
|
116
|
+
modality=Modality.DIRECT,
|
|
117
|
+
mime=routed.mime,
|
|
118
|
+
)
|
|
119
|
+
raise ValueError(
|
|
120
|
+
f"Unsupported modality {modality.value!r} (extension={raw_file.extension!r}, mime={raw_file.mime!r})"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
parse = async_to_sync(aparse)
|
|
125
|
+
"""Sync bridge for non-event-loop callers. See ``document.parse`` for details."""
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# ---- helpers ----
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _is_http_uri(uri: str) -> bool:
|
|
132
|
+
"""Return True for ``http`` / ``https`` schemes only."""
|
|
133
|
+
return urlparse(uri).scheme in ("http", "https")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _ensure_extension(raw_file: RawFile) -> RawFile:
|
|
137
|
+
"""Return a ``RawFile`` whose ``extension`` is populated.
|
|
138
|
+
|
|
139
|
+
If ``extension`` is missing but ``mime`` resolves via ``MIME_TO_EXTENSION``,
|
|
140
|
+
we hand the submodule a copy carrying the canonical extension. Returns
|
|
141
|
+
the original when ``extension`` is already set or no mime fallback exists.
|
|
142
|
+
"""
|
|
143
|
+
if raw_file.extension:
|
|
144
|
+
return raw_file
|
|
145
|
+
derived = get_extension_from_mime(raw_file.mime)
|
|
146
|
+
if not derived:
|
|
147
|
+
return raw_file
|
|
148
|
+
return raw_file.model_copy(update={"extension": derived})
|