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.
Files changed (49) hide show
  1. everalgo_parser-0.1.0/.gitignore +58 -0
  2. everalgo_parser-0.1.0/CHANGELOG.md +76 -0
  3. everalgo_parser-0.1.0/LICENSE +21 -0
  4. everalgo_parser-0.1.0/PKG-INFO +104 -0
  5. everalgo_parser-0.1.0/README.md +79 -0
  6. everalgo_parser-0.1.0/pyproject.toml +45 -0
  7. everalgo_parser-0.1.0/src/everalgo/parser/__init__.py +148 -0
  8. everalgo_parser-0.1.0/src/everalgo/parser/_utils.py +216 -0
  9. everalgo_parser-0.1.0/src/everalgo/parser/audio.py +72 -0
  10. everalgo_parser-0.1.0/src/everalgo/parser/document.py +428 -0
  11. everalgo_parser-0.1.0/src/everalgo/parser/image.py +266 -0
  12. everalgo_parser-0.1.0/src/everalgo/parser/prompts/__init__.py +12 -0
  13. everalgo_parser-0.1.0/src/everalgo/parser/prompts/en/__init__.py +13 -0
  14. everalgo_parser-0.1.0/src/everalgo/parser/prompts/en/audio.py +15 -0
  15. everalgo_parser-0.1.0/src/everalgo/parser/prompts/en/document.py +40 -0
  16. everalgo_parser-0.1.0/src/everalgo/parser/prompts/en/image.py +36 -0
  17. everalgo_parser-0.1.0/src/everalgo/parser/prompts/zh/__init__.py +13 -0
  18. everalgo_parser-0.1.0/src/everalgo/parser/prompts/zh/audio.py +14 -0
  19. everalgo_parser-0.1.0/src/everalgo/parser/prompts/zh/document.py +40 -0
  20. everalgo_parser-0.1.0/src/everalgo/parser/prompts/zh/image.py +37 -0
  21. everalgo_parser-0.1.0/src/everalgo/parser/py.typed +0 -0
  22. everalgo_parser-0.1.0/src/everalgo/parser/url.py +270 -0
  23. everalgo_parser-0.1.0/src/everalgo/parser/video.py +38 -0
  24. everalgo_parser-0.1.0/tests/fixtures/sample.bmp +0 -0
  25. everalgo_parser-0.1.0/tests/fixtures/sample.docx +0 -0
  26. everalgo_parser-0.1.0/tests/fixtures/sample.eml +255 -0
  27. everalgo_parser-0.1.0/tests/fixtures/sample.htm +33 -0
  28. everalgo_parser-0.1.0/tests/fixtures/sample.html +33 -0
  29. everalgo_parser-0.1.0/tests/fixtures/sample.jpg +0 -0
  30. everalgo_parser-0.1.0/tests/fixtures/sample.pdf +49 -0
  31. everalgo_parser-0.1.0/tests/fixtures/sample.png +0 -0
  32. everalgo_parser-0.1.0/tests/fixtures/sample.pptx +0 -0
  33. everalgo_parser-0.1.0/tests/fixtures/sample.svg +9 -0
  34. everalgo_parser-0.1.0/tests/fixtures/sample.tiff +0 -0
  35. everalgo_parser-0.1.0/tests/fixtures/sample.txt +14 -0
  36. everalgo_parser-0.1.0/tests/fixtures/sample.wav +0 -0
  37. everalgo_parser-0.1.0/tests/fixtures/sample.webp +0 -0
  38. everalgo_parser-0.1.0/tests/fixtures/sample.xlsx +0 -0
  39. everalgo_parser-0.1.0/tests/parser/__init__.py +0 -0
  40. everalgo_parser-0.1.0/tests/parser/test_audio.py +48 -0
  41. everalgo_parser-0.1.0/tests/parser/test_dispatch.py +305 -0
  42. everalgo_parser-0.1.0/tests/parser/test_document.py +253 -0
  43. everalgo_parser-0.1.0/tests/parser/test_e2e_all_formats.py +202 -0
  44. everalgo_parser-0.1.0/tests/parser/test_e2e_pdf_openrouter.py +69 -0
  45. everalgo_parser-0.1.0/tests/parser/test_image.py +185 -0
  46. everalgo_parser-0.1.0/tests/parser/test_public_api.py +38 -0
  47. everalgo_parser-0.1.0/tests/parser/test_url.py +296 -0
  48. everalgo_parser-0.1.0/tests/parser/test_utils.py +160 -0
  49. 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})