msgflux 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- msgflux/__init__.py +104 -0
- msgflux/_private/__init__.py +0 -0
- msgflux/_private/client.py +105 -0
- msgflux/_private/core.py +14 -0
- msgflux/_private/executor.py +134 -0
- msgflux/_private/response.py +230 -0
- msgflux/auto/__init__.py +5 -0
- msgflux/auto/params.py +167 -0
- msgflux/cache.py +54 -0
- msgflux/core/__init__.py +5 -0
- msgflux/core/dotdict.py +264 -0
- msgflux/core/examples.py +169 -0
- msgflux/core/message.py +45 -0
- msgflux/data/__init__.py +0 -0
- msgflux/data/audio.py +1 -0
- msgflux/data/dbs/__init__.py +16 -0
- msgflux/data/dbs/base.py +124 -0
- msgflux/data/dbs/db.py +65 -0
- msgflux/data/dbs/providers/__init__.py +0 -0
- msgflux/data/dbs/providers/cachetools.py +57 -0
- msgflux/data/dbs/providers/diskcache.py +48 -0
- msgflux/data/dbs/providers/faiss.py +220 -0
- msgflux/data/dbs/registry.py +19 -0
- msgflux/data/dbs/response.py +10 -0
- msgflux/data/dbs/types.py +26 -0
- msgflux/data/parsers/__init__.py +16 -0
- msgflux/data/parsers/base.py +325 -0
- msgflux/data/parsers/parser.py +101 -0
- msgflux/data/parsers/providers/__init__.py +32 -0
- msgflux/data/parsers/providers/csv_parser.py +278 -0
- msgflux/data/parsers/providers/email_parser.py +264 -0
- msgflux/data/parsers/providers/html_parser.py +265 -0
- msgflux/data/parsers/providers/openpyxl.py +333 -0
- msgflux/data/parsers/providers/pypdf.py +186 -0
- msgflux/data/parsers/providers/python_docx.py +334 -0
- msgflux/data/parsers/providers/python_pptx.py +197 -0
- msgflux/data/parsers/registry.py +19 -0
- msgflux/data/parsers/response.py +32 -0
- msgflux/data/parsers/types.py +110 -0
- msgflux/data/retrievers/__init__.py +16 -0
- msgflux/data/retrievers/base.py +185 -0
- msgflux/data/retrievers/providers/__init__.py +0 -0
- msgflux/data/retrievers/providers/bm25.py +230 -0
- msgflux/data/retrievers/providers/bm25s.py +221 -0
- msgflux/data/retrievers/providers/rank_bm25.py +148 -0
- msgflux/data/retrievers/providers/rapidfuzz.py +125 -0
- msgflux/data/retrievers/providers/wikipedia.py +281 -0
- msgflux/data/retrievers/registry.py +19 -0
- msgflux/data/retrievers/retriever.py +83 -0
- msgflux/data/retrievers/types.py +24 -0
- msgflux/data/types/__init__.py +29 -0
- msgflux/data/types/audio.py +112 -0
- msgflux/data/types/base.py +70 -0
- msgflux/data/types/file.py +114 -0
- msgflux/data/types/image.py +134 -0
- msgflux/data/types/video.py +121 -0
- msgflux/dsl/__init__.py +0 -0
- msgflux/dsl/inline.py +484 -0
- msgflux/dsl/signature.py +393 -0
- msgflux/dsl/typed_parsers/__init__.py +19 -0
- msgflux/dsl/typed_parsers/base.py +24 -0
- msgflux/dsl/typed_parsers/providers/__init__.py +0 -0
- msgflux/dsl/typed_parsers/providers/xml.py +259 -0
- msgflux/dsl/typed_parsers/registry.py +14 -0
- msgflux/envs.py +100 -0
- msgflux/exceptions.py +84 -0
- msgflux/generation/__init__.py +0 -0
- msgflux/generation/control_flow.py +163 -0
- msgflux/generation/reasoning/__init__.py +5 -0
- msgflux/generation/reasoning/cot.py +7 -0
- msgflux/generation/reasoning/react.py +289 -0
- msgflux/generation/reasoning/self_consistency.py +16 -0
- msgflux/generation/templates.py +53 -0
- msgflux/logger.py +159 -0
- msgflux/models/__init__.py +16 -0
- msgflux/models/base.py +39 -0
- msgflux/models/cache.py +125 -0
- msgflux/models/gateway.py +466 -0
- msgflux/models/httpx.py +81 -0
- msgflux/models/model.py +122 -0
- msgflux/models/profiles/__init__.py +48 -0
- msgflux/models/profiles/base.py +122 -0
- msgflux/models/profiles/loader.py +264 -0
- msgflux/models/profiles/registry.py +207 -0
- msgflux/models/providers/__init__.py +0 -0
- msgflux/models/providers/cerebras.py +30 -0
- msgflux/models/providers/groq.py +52 -0
- msgflux/models/providers/imagerouter.py +33 -0
- msgflux/models/providers/jinaai.py +331 -0
- msgflux/models/providers/ollama.py +51 -0
- msgflux/models/providers/openai.py +1884 -0
- msgflux/models/providers/openrouter.py +64 -0
- msgflux/models/providers/replicate.py +218 -0
- msgflux/models/providers/sambanova.py +38 -0
- msgflux/models/providers/together.py +57 -0
- msgflux/models/providers/vllm.py +131 -0
- msgflux/models/registry.py +19 -0
- msgflux/models/response.py +26 -0
- msgflux/models/tool_call_agg.py +85 -0
- msgflux/models/types.py +196 -0
- msgflux/nn/__init__.py +57 -0
- msgflux/nn/functional.py +645 -0
- msgflux/nn/hooks/__init__.py +24 -0
- msgflux/nn/hooks/guard.py +109 -0
- msgflux/nn/hooks/hook.py +123 -0
- msgflux/nn/modules/__init__.py +68 -0
- msgflux/nn/modules/agent.py +1927 -0
- msgflux/nn/modules/container.py +672 -0
- msgflux/nn/modules/embedder.py +261 -0
- msgflux/nn/modules/generator.py +21 -0
- msgflux/nn/modules/mediamaker.py +285 -0
- msgflux/nn/modules/module.py +2106 -0
- msgflux/nn/modules/predictor.py +238 -0
- msgflux/nn/modules/searcher.py +383 -0
- msgflux/nn/modules/speaker.py +222 -0
- msgflux/nn/modules/tool.py +839 -0
- msgflux/nn/modules/transcriber.py +273 -0
- msgflux/nn/parameter.py +77 -0
- msgflux/protocols/__init__.py +1 -0
- msgflux/protocols/mcp/__init__.py +61 -0
- msgflux/protocols/mcp/auth/__init__.py +17 -0
- msgflux/protocols/mcp/auth/apikey.py +70 -0
- msgflux/protocols/mcp/auth/base.py +83 -0
- msgflux/protocols/mcp/auth/basic.py +57 -0
- msgflux/protocols/mcp/auth/bearer.py +97 -0
- msgflux/protocols/mcp/auth/custom.py +76 -0
- msgflux/protocols/mcp/auth/oauth2.py +133 -0
- msgflux/protocols/mcp/client.py +454 -0
- msgflux/protocols/mcp/exceptions.py +25 -0
- msgflux/protocols/mcp/integration.py +95 -0
- msgflux/protocols/mcp/loglevels.py +16 -0
- msgflux/protocols/mcp/transports.py +435 -0
- msgflux/protocols/mcp/types.py +51 -0
- msgflux/py.typed +0 -0
- msgflux/telemetry/__init__.py +22 -0
- msgflux/telemetry/config.py +104 -0
- msgflux/telemetry/span.py +245 -0
- msgflux/tools/__init__.py +10 -0
- msgflux/tools/builtin/__init__.py +5 -0
- msgflux/tools/builtin/web_fetch.py +121 -0
- msgflux/tools/config.py +196 -0
- msgflux/tools/definitions.py +11 -0
- msgflux/utils/__init__.py +0 -0
- msgflux/utils/chat.py +682 -0
- msgflux/utils/common.py +36 -0
- msgflux/utils/console.py +55 -0
- msgflux/utils/convert.py +20 -0
- msgflux/utils/encode.py +192 -0
- msgflux/utils/html.py +171 -0
- msgflux/utils/imports.py +115 -0
- msgflux/utils/inspect.py +81 -0
- msgflux/utils/logging.py +15 -0
- msgflux/utils/mermaid.py +20 -0
- msgflux/utils/msgspec.py +1116 -0
- msgflux/utils/pooling.py +51 -0
- msgflux/utils/templates.py +51 -0
- msgflux/utils/tenacity.py +38 -0
- msgflux/utils/torch.py +7 -0
- msgflux/utils/validation.py +31 -0
- msgflux/utils/xml.py +7 -0
- msgflux/version.py +1 -0
- msgflux-0.1.0.dist-info/METADATA +170 -0
- msgflux-0.1.0.dist-info/RECORD +165 -0
- msgflux-0.1.0.dist-info/WHEEL +4 -0
- msgflux-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
from typing import Dict, Union
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from pypdf import PdfReader
|
|
5
|
+
except ImportError:
|
|
6
|
+
PdfReader = None
|
|
7
|
+
|
|
8
|
+
from msgflux.data.parsers.base import BaseParser
|
|
9
|
+
from msgflux.data.parsers.registry import register_parser
|
|
10
|
+
from msgflux.data.parsers.response import ParserResponse
|
|
11
|
+
from msgflux.data.parsers.types import PdfParser
|
|
12
|
+
from msgflux.core.dotdict import dotdict
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@register_parser
|
|
16
|
+
class PyPDFPdfParser(BaseParser, PdfParser):
|
|
17
|
+
"""PyPDF-based PDF Parser.
|
|
18
|
+
|
|
19
|
+
Converts PDF documents to Markdown format and extracts embedded images.
|
|
20
|
+
Uses pypdf library for PDF processing.
|
|
21
|
+
|
|
22
|
+
Features:
|
|
23
|
+
- Text extraction with layout preservation
|
|
24
|
+
- Image extraction from PDF pages
|
|
25
|
+
- Page-level organization
|
|
26
|
+
- Markdown output format
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
>>> parser = Parser.pdf("pypdf")
|
|
30
|
+
>>> response = parser("document.pdf")
|
|
31
|
+
>>> print(response.data["text"])
|
|
32
|
+
>>> print(response.data["images"])
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
provider = "pypdf"
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
*,
|
|
40
|
+
extraction_mode: str = "layout",
|
|
41
|
+
encode_images_base64: bool = True,
|
|
42
|
+
):
|
|
43
|
+
"""Initialize PyPDF parser.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
extraction_mode:
|
|
47
|
+
Text extraction mode. Options: "layout" or "plain".
|
|
48
|
+
"layout" preserves text positioning.
|
|
49
|
+
encode_images_base64:
|
|
50
|
+
If True, encode images as base64 strings.
|
|
51
|
+
If False, keep as raw bytes.
|
|
52
|
+
"""
|
|
53
|
+
if PdfReader is None:
|
|
54
|
+
raise ImportError(
|
|
55
|
+
"`pypdf` is not available. Install with `pip install pypdf`"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
self.extraction_mode = extraction_mode
|
|
59
|
+
self.encode_images_base64 = encode_images_base64
|
|
60
|
+
self._initialize()
|
|
61
|
+
|
|
62
|
+
def _initialize(self):
|
|
63
|
+
"""Initialize parser state."""
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
def __call__(self, data: Union[str, bytes], **kwargs) -> ParserResponse:
|
|
67
|
+
"""Parse a PDF document.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
data:
|
|
71
|
+
PDF file path, URL, or bytes.
|
|
72
|
+
**kwargs:
|
|
73
|
+
Additional parsing options (currently unused).
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
ParserResponse containing:
|
|
77
|
+
- text: Markdown-formatted content
|
|
78
|
+
- images: Dictionary of extracted images
|
|
79
|
+
- metadata: Document metadata (num_pages, etc.)
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
FileNotFoundError:
|
|
83
|
+
If file path doesn't exist.
|
|
84
|
+
ValueError:
|
|
85
|
+
If data type is not supported.
|
|
86
|
+
"""
|
|
87
|
+
# Validate file type if it's a path
|
|
88
|
+
if isinstance(data, str) and not data.startswith(("http://", "https://")):
|
|
89
|
+
self._validate_file_type(data, ".pdf")
|
|
90
|
+
|
|
91
|
+
# Parse the document
|
|
92
|
+
result = self._parse(data)
|
|
93
|
+
|
|
94
|
+
# Create response
|
|
95
|
+
response = ParserResponse()
|
|
96
|
+
response.set_response_type("pdf_parse")
|
|
97
|
+
response.add(result)
|
|
98
|
+
|
|
99
|
+
return response
|
|
100
|
+
|
|
101
|
+
def _parse(self, data: Union[str, bytes]) -> Dict[str, any]:
|
|
102
|
+
"""Parse PDF and extract content.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
data:
|
|
106
|
+
PDF file path or bytes.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Dictionary with:
|
|
110
|
+
- text: Markdown content
|
|
111
|
+
- images: Dictionary of images
|
|
112
|
+
- metadata: Document metadata
|
|
113
|
+
"""
|
|
114
|
+
md_content = ""
|
|
115
|
+
images_dict = {}
|
|
116
|
+
|
|
117
|
+
# Load PDF
|
|
118
|
+
if isinstance(data, bytes):
|
|
119
|
+
from io import BytesIO
|
|
120
|
+
|
|
121
|
+
reader = PdfReader(BytesIO(data))
|
|
122
|
+
else:
|
|
123
|
+
reader = PdfReader(data)
|
|
124
|
+
|
|
125
|
+
num_pages = len(reader.pages)
|
|
126
|
+
|
|
127
|
+
# Extract content from each page
|
|
128
|
+
for idx in range(num_pages):
|
|
129
|
+
page = reader.pages[idx]
|
|
130
|
+
|
|
131
|
+
# Add page marker
|
|
132
|
+
md_content += f"\n\n<!-- Page number: {idx + 1} -->\n"
|
|
133
|
+
|
|
134
|
+
# Extract text (blank pages have no /Contents stream)
|
|
135
|
+
try:
|
|
136
|
+
text = page.extract_text(
|
|
137
|
+
extraction_mode=self.extraction_mode,
|
|
138
|
+
layout_mode_space_vertically=False,
|
|
139
|
+
)
|
|
140
|
+
except KeyError:
|
|
141
|
+
text = page.extract_text()
|
|
142
|
+
md_content += text.strip() + "\n"
|
|
143
|
+
|
|
144
|
+
# Extract images from page
|
|
145
|
+
for count, image_file_object in enumerate(page.images):
|
|
146
|
+
img_extension = image_file_object.name.split(".")[-1]
|
|
147
|
+
img_name = f"image_page{idx + 1}_{count}.{img_extension}"
|
|
148
|
+
img_data = image_file_object.data
|
|
149
|
+
|
|
150
|
+
images_dict[img_name] = img_data
|
|
151
|
+
|
|
152
|
+
# Add image reference in markdown
|
|
153
|
+
md_content += f"\n\n"
|
|
154
|
+
|
|
155
|
+
# Prepare images (optionally encode to base64)
|
|
156
|
+
images_dict = self._prepare_images_dict(
|
|
157
|
+
images_dict, encode_base64=self.encode_images_base64
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Prepare metadata
|
|
161
|
+
metadata = dotdict(
|
|
162
|
+
{
|
|
163
|
+
"num_pages": num_pages,
|
|
164
|
+
"extraction_mode": self.extraction_mode,
|
|
165
|
+
}
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Try to extract PDF metadata if available
|
|
169
|
+
if reader.metadata:
|
|
170
|
+
pdf_metadata = {}
|
|
171
|
+
if reader.metadata.title:
|
|
172
|
+
pdf_metadata["title"] = reader.metadata.title
|
|
173
|
+
if reader.metadata.author:
|
|
174
|
+
pdf_metadata["author"] = reader.metadata.author
|
|
175
|
+
if reader.metadata.creator:
|
|
176
|
+
pdf_metadata["creator"] = reader.metadata.creator
|
|
177
|
+
if reader.metadata.subject:
|
|
178
|
+
pdf_metadata["subject"] = reader.metadata.subject
|
|
179
|
+
if pdf_metadata:
|
|
180
|
+
metadata.pdf_info = pdf_metadata
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
"text": md_content.strip(),
|
|
184
|
+
"images": images_dict,
|
|
185
|
+
"metadata": metadata,
|
|
186
|
+
}
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
from typing import Dict, List, Union
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from docx import Document
|
|
5
|
+
from docx.table import Table
|
|
6
|
+
from docx.text.paragraph import Paragraph
|
|
7
|
+
except ImportError:
|
|
8
|
+
Document = None
|
|
9
|
+
Paragraph = None
|
|
10
|
+
Table = None
|
|
11
|
+
|
|
12
|
+
from msgflux.data.parsers.base import BaseParser
|
|
13
|
+
from msgflux.data.parsers.registry import register_parser
|
|
14
|
+
from msgflux.data.parsers.response import ParserResponse
|
|
15
|
+
from msgflux.data.parsers.types import DocxParser
|
|
16
|
+
from msgflux.core.dotdict import dotdict
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@register_parser
|
|
20
|
+
class PythonDocxDocxParser(BaseParser, DocxParser):
|
|
21
|
+
"""Python-DOCX-based DOCX Parser.
|
|
22
|
+
|
|
23
|
+
Converts Word documents to Markdown format and extracts embedded images.
|
|
24
|
+
Uses python-docx library for DOCX processing.
|
|
25
|
+
|
|
26
|
+
Features:
|
|
27
|
+
- Paragraph and heading extraction
|
|
28
|
+
- Text formatting preservation
|
|
29
|
+
- Table extraction (converted to Markdown)
|
|
30
|
+
- Image extraction from document
|
|
31
|
+
- Markdown output format
|
|
32
|
+
|
|
33
|
+
Example:
|
|
34
|
+
>>> parser = Parser.docx("python_docx")
|
|
35
|
+
>>> response = parser("document.docx")
|
|
36
|
+
>>> print(response.data["text"])
|
|
37
|
+
>>> print(response.data["images"])
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
provider = "python_docx"
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
*,
|
|
45
|
+
table_format: str = "markdown",
|
|
46
|
+
encode_images_base64: bool = True,
|
|
47
|
+
):
|
|
48
|
+
"""Initialize Python-DOCX parser.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
table_format:
|
|
52
|
+
Format for table output. Options: "markdown" or "html".
|
|
53
|
+
encode_images_base64:
|
|
54
|
+
If True, encode images as base64 strings.
|
|
55
|
+
If False, keep as raw bytes.
|
|
56
|
+
"""
|
|
57
|
+
if Document is None:
|
|
58
|
+
raise ImportError(
|
|
59
|
+
"`python-docx` is not available. Install with `pip install python-docx`"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
self.table_format = table_format
|
|
63
|
+
self.encode_images_base64 = encode_images_base64
|
|
64
|
+
self._initialize()
|
|
65
|
+
|
|
66
|
+
def _initialize(self):
|
|
67
|
+
"""Initialize parser state."""
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
def __call__(self, data: Union[str, bytes], **kwargs) -> ParserResponse:
|
|
71
|
+
"""Parse a DOCX document.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
data:
|
|
75
|
+
DOCX file path, URL, or bytes.
|
|
76
|
+
**kwargs:
|
|
77
|
+
Additional parsing options (currently unused).
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
ParserResponse containing:
|
|
81
|
+
- text: Markdown-formatted content
|
|
82
|
+
- images: Dictionary of extracted images
|
|
83
|
+
- metadata: Document metadata (num_paragraphs, num_tables, etc.)
|
|
84
|
+
|
|
85
|
+
Raises:
|
|
86
|
+
FileNotFoundError:
|
|
87
|
+
If file path doesn't exist.
|
|
88
|
+
ValueError:
|
|
89
|
+
If data type is not supported.
|
|
90
|
+
"""
|
|
91
|
+
# Validate file type if it's a path
|
|
92
|
+
if isinstance(data, str) and not data.startswith(("http://", "https://")):
|
|
93
|
+
self._validate_file_type(data, ".docx")
|
|
94
|
+
|
|
95
|
+
# Parse the document
|
|
96
|
+
result = self._parse(data)
|
|
97
|
+
|
|
98
|
+
# Create response
|
|
99
|
+
response = ParserResponse()
|
|
100
|
+
response.set_response_type("docx_parse")
|
|
101
|
+
response.add(result)
|
|
102
|
+
|
|
103
|
+
return response
|
|
104
|
+
|
|
105
|
+
def _parse(self, data: Union[str, bytes]) -> Dict[str, any]:
|
|
106
|
+
"""Parse DOCX and extract content.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
data:
|
|
110
|
+
DOCX file path or bytes.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Dictionary with:
|
|
114
|
+
- text: Markdown content
|
|
115
|
+
- images: Dictionary of images
|
|
116
|
+
- metadata: Document metadata
|
|
117
|
+
"""
|
|
118
|
+
md_content = ""
|
|
119
|
+
images_dict = {}
|
|
120
|
+
|
|
121
|
+
# Load document
|
|
122
|
+
if isinstance(data, bytes):
|
|
123
|
+
from io import BytesIO
|
|
124
|
+
|
|
125
|
+
document = Document(BytesIO(data))
|
|
126
|
+
else:
|
|
127
|
+
document = Document(data)
|
|
128
|
+
|
|
129
|
+
# Extract images from document
|
|
130
|
+
image_counter = 0
|
|
131
|
+
for rel in document.part.rels.values():
|
|
132
|
+
if "image" in rel.target_ref:
|
|
133
|
+
try:
|
|
134
|
+
img_data = rel.target_part.blob
|
|
135
|
+
# Get image extension from content type
|
|
136
|
+
content_type = rel.target_part.content_type
|
|
137
|
+
ext = content_type.split("/")[-1]
|
|
138
|
+
if ext == "jpeg":
|
|
139
|
+
ext = "jpg"
|
|
140
|
+
|
|
141
|
+
img_name = f"image_{image_counter}.{ext}"
|
|
142
|
+
images_dict[img_name] = img_data
|
|
143
|
+
image_counter += 1
|
|
144
|
+
except Exception:
|
|
145
|
+
# Skip images that can't be extracted
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
# Process document elements (paragraphs and tables)
|
|
149
|
+
for element in document.element.body:
|
|
150
|
+
# Check if it's a paragraph
|
|
151
|
+
if element.tag.endswith("p"):
|
|
152
|
+
# Find the paragraph object
|
|
153
|
+
for para in document.paragraphs:
|
|
154
|
+
if para._element == element:
|
|
155
|
+
para_text = self._process_paragraph(para)
|
|
156
|
+
if para_text:
|
|
157
|
+
md_content += para_text + "\n\n"
|
|
158
|
+
break
|
|
159
|
+
|
|
160
|
+
# Check if it's a table
|
|
161
|
+
elif element.tag.endswith("tbl"):
|
|
162
|
+
# Find the table object
|
|
163
|
+
for table in document.tables:
|
|
164
|
+
if table._element == element:
|
|
165
|
+
table_md = self._process_table(table)
|
|
166
|
+
if table_md:
|
|
167
|
+
md_content += table_md + "\n\n"
|
|
168
|
+
break
|
|
169
|
+
|
|
170
|
+
# Prepare images (optionally encode to base64)
|
|
171
|
+
images_dict = self._prepare_images_dict(
|
|
172
|
+
images_dict, encode_base64=self.encode_images_base64
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Prepare metadata
|
|
176
|
+
metadata = dotdict(
|
|
177
|
+
{
|
|
178
|
+
"num_paragraphs": len(document.paragraphs),
|
|
179
|
+
"num_tables": len(document.tables),
|
|
180
|
+
"num_images": len(images_dict),
|
|
181
|
+
"table_format": self.table_format,
|
|
182
|
+
}
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Try to extract document metadata if available
|
|
186
|
+
if hasattr(document, "core_properties"):
|
|
187
|
+
props = document.core_properties
|
|
188
|
+
docx_metadata = {}
|
|
189
|
+
if props.title:
|
|
190
|
+
docx_metadata["title"] = props.title
|
|
191
|
+
if props.author:
|
|
192
|
+
docx_metadata["author"] = props.author
|
|
193
|
+
if props.subject:
|
|
194
|
+
docx_metadata["subject"] = props.subject
|
|
195
|
+
if props.created:
|
|
196
|
+
docx_metadata["created"] = str(props.created)
|
|
197
|
+
if props.modified:
|
|
198
|
+
docx_metadata["modified"] = str(props.modified)
|
|
199
|
+
if docx_metadata:
|
|
200
|
+
metadata.docx_info = docx_metadata
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
"text": md_content.strip(),
|
|
204
|
+
"images": images_dict,
|
|
205
|
+
"metadata": metadata,
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
def _process_paragraph(self, paragraph) -> str:
|
|
209
|
+
"""Process a paragraph and convert to Markdown.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
paragraph:
|
|
213
|
+
Python-docx paragraph object.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Markdown-formatted paragraph text.
|
|
217
|
+
"""
|
|
218
|
+
text = paragraph.text.strip()
|
|
219
|
+
if not text:
|
|
220
|
+
return ""
|
|
221
|
+
|
|
222
|
+
# Check paragraph style for headings
|
|
223
|
+
style_name = paragraph.style.name.lower()
|
|
224
|
+
|
|
225
|
+
if "heading 1" in style_name or style_name == "title":
|
|
226
|
+
return f"# {text}"
|
|
227
|
+
elif "heading 2" in style_name:
|
|
228
|
+
return f"## {text}"
|
|
229
|
+
elif "heading 3" in style_name:
|
|
230
|
+
return f"### {text}"
|
|
231
|
+
elif "heading 4" in style_name:
|
|
232
|
+
return f"#### {text}"
|
|
233
|
+
elif "heading 5" in style_name:
|
|
234
|
+
return f"##### {text}"
|
|
235
|
+
elif "heading 6" in style_name:
|
|
236
|
+
return f"###### {text}"
|
|
237
|
+
else:
|
|
238
|
+
return text
|
|
239
|
+
|
|
240
|
+
def _process_table(self, table) -> str:
|
|
241
|
+
"""Process a table and convert to Markdown or HTML.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
table:
|
|
245
|
+
Python-docx table object.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
Markdown or HTML formatted table.
|
|
249
|
+
"""
|
|
250
|
+
# Extract table data
|
|
251
|
+
table_data = []
|
|
252
|
+
for row in table.rows:
|
|
253
|
+
row_data = [cell.text.strip() for cell in row.cells]
|
|
254
|
+
table_data.append(row_data)
|
|
255
|
+
|
|
256
|
+
if not table_data:
|
|
257
|
+
return ""
|
|
258
|
+
|
|
259
|
+
# Convert to appropriate format
|
|
260
|
+
if self.table_format == "html":
|
|
261
|
+
return self._table_to_html(table_data)
|
|
262
|
+
else: # markdown
|
|
263
|
+
return self._table_to_markdown(table_data)
|
|
264
|
+
|
|
265
|
+
def _table_to_markdown(self, table_data: List[List[str]]) -> str:
|
|
266
|
+
"""Convert table data to Markdown format.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
table_data:
|
|
270
|
+
2D list of table data.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Markdown table string.
|
|
274
|
+
"""
|
|
275
|
+
if not table_data:
|
|
276
|
+
return ""
|
|
277
|
+
|
|
278
|
+
# First row is header
|
|
279
|
+
header = table_data[0]
|
|
280
|
+
rows = table_data[1:]
|
|
281
|
+
|
|
282
|
+
# Header line
|
|
283
|
+
header_line = (
|
|
284
|
+
"| " + " | ".join(str(cell) if cell else "" for cell in header) + " |"
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
# Separator line
|
|
288
|
+
separator_line = "| " + " | ".join(["---"] * len(header)) + " |"
|
|
289
|
+
|
|
290
|
+
# Data lines
|
|
291
|
+
data_lines = [
|
|
292
|
+
"| " + " | ".join(str(cell) if cell else "" for cell in row) + " |"
|
|
293
|
+
for row in rows
|
|
294
|
+
]
|
|
295
|
+
|
|
296
|
+
return "\n".join([header_line, separator_line, *data_lines])
|
|
297
|
+
|
|
298
|
+
def _table_to_html(self, table_data: List[List[str]]) -> str:
|
|
299
|
+
"""Convert table data to HTML format.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
table_data:
|
|
303
|
+
2D list of table data.
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
HTML table string.
|
|
307
|
+
"""
|
|
308
|
+
if not table_data:
|
|
309
|
+
return ""
|
|
310
|
+
|
|
311
|
+
# First row is header
|
|
312
|
+
header = table_data[0]
|
|
313
|
+
rows = table_data[1:]
|
|
314
|
+
|
|
315
|
+
# Start table
|
|
316
|
+
html = "<table>\n"
|
|
317
|
+
|
|
318
|
+
# Add header row
|
|
319
|
+
html += " <tr>\n"
|
|
320
|
+
for cell in header:
|
|
321
|
+
html += f" <th>{str(cell) if cell else ''}</th>\n"
|
|
322
|
+
html += " </tr>\n"
|
|
323
|
+
|
|
324
|
+
# Add data rows
|
|
325
|
+
for row in rows:
|
|
326
|
+
html += " <tr>\n"
|
|
327
|
+
for cell in row:
|
|
328
|
+
html += f" <td>{str(cell) if cell else ''}</td>\n"
|
|
329
|
+
html += " </tr>\n"
|
|
330
|
+
|
|
331
|
+
# Close table
|
|
332
|
+
html += "</table>"
|
|
333
|
+
|
|
334
|
+
return html
|