attachments 0.2.4__tar.gz → 0.3.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.
- {attachments-0.2.4/src/attachments.egg-info → attachments-0.3.0}/PKG-INFO +13 -44
- {attachments-0.2.4 → attachments-0.3.0}/README.md +11 -43
- {attachments-0.2.4 → attachments-0.3.0}/pyproject.toml +2 -1
- {attachments-0.2.4 → attachments-0.3.0}/src/attachments/core.py +89 -7
- {attachments-0.2.4 → attachments-0.3.0}/src/attachments/parsers.py +21 -0
- {attachments-0.2.4 → attachments-0.3.0/src/attachments.egg-info}/PKG-INFO +13 -44
- {attachments-0.2.4 → attachments-0.3.0}/src/attachments.egg-info/requires.txt +1 -0
- {attachments-0.2.4 → attachments-0.3.0}/tests/test_core_functionality.py +50 -0
- {attachments-0.2.4 → attachments-0.3.0}/LICENSE +0 -0
- {attachments-0.2.4 → attachments-0.3.0}/setup.cfg +0 -0
- {attachments-0.2.4 → attachments-0.3.0}/src/attachments/__init__.py +0 -0
- {attachments-0.2.4 → attachments-0.3.0}/src/attachments/audio_processing.py +0 -0
- {attachments-0.2.4 → attachments-0.3.0}/src/attachments/config.py +0 -0
- {attachments-0.2.4 → attachments-0.3.0}/src/attachments/detectors.py +0 -0
- {attachments-0.2.4 → attachments-0.3.0}/src/attachments/exceptions.py +0 -0
- {attachments-0.2.4 → attachments-0.3.0}/src/attachments/image_processing.py +0 -0
- {attachments-0.2.4 → attachments-0.3.0}/src/attachments/office_contact_sheet.py +0 -0
- {attachments-0.2.4 → attachments-0.3.0}/src/attachments/renderers.py +0 -0
- {attachments-0.2.4 → attachments-0.3.0}/src/attachments/utils.py +0 -0
- {attachments-0.2.4 → attachments-0.3.0}/src/attachments.egg-info/SOURCES.txt +0 -0
- {attachments-0.2.4 → attachments-0.3.0}/src/attachments.egg-info/dependency_links.txt +0 -0
- {attachments-0.2.4 → attachments-0.3.0}/src/attachments.egg-info/top_level.txt +0 -0
- {attachments-0.2.4 → attachments-0.3.0}/tests/test_audio_processing.py +0 -0
- {attachments-0.2.4 → attachments-0.3.0}/tests/test_base.py +0 -0
- {attachments-0.2.4 → attachments-0.3.0}/tests/test_document_parsing.py +0 -0
- {attachments-0.2.4 → attachments-0.3.0}/tests/test_html_parsing.py +0 -0
- {attachments-0.2.4 → attachments-0.3.0}/tests/test_image_processing.py +0 -0
- {attachments-0.2.4 → attachments-0.3.0}/tests/test_indexing_slicing.py +0 -0
- {attachments-0.2.4 → attachments-0.3.0}/tests/test_markdown_rendering.py +0 -0
- {attachments-0.2.4 → attachments-0.3.0}/tests/test_office_contact_sheet.py +0 -0
- {attachments-0.2.4 → attachments-0.3.0}/tests/test_pdf_parsing.py +0 -0
- {attachments-0.2.4 → attachments-0.3.0}/tests/test_pptx_parsing.py +0 -0
- {attachments-0.2.4 → attachments-0.3.0}/tests/test_simple.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: attachments
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A Python library to handle various file types for LLMs.
|
|
5
5
|
Author-email: Maxim Rivest <mrive052@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -35,13 +35,16 @@ Requires-Dist: flake8; extra == "dev"
|
|
|
35
35
|
Requires-Dist: mypy; extra == "dev"
|
|
36
36
|
Requires-Dist: black; extra == "dev"
|
|
37
37
|
Requires-Dist: filelock; extra == "dev"
|
|
38
|
+
Requires-Dist: mystmd; extra == "dev"
|
|
38
39
|
Dynamic: license-file
|
|
39
40
|
|
|
40
|
-
# Attachments
|
|
41
|
+
# Attachments: The Python Library for Effortless LLM Context
|
|
41
42
|
## Easiest way to give context to LLMs
|
|
42
43
|
|
|
43
44
|
Attachments has the ambition to be the general funnel for any files to be transformed into images+text for large language models context. For this to be true we, has a community, have to stop implementing this funnel (file -> images+text) each on our own. If you have a detector + parser + renderer contibute it! This will help you and us. Don't let your code alone in a dark corner of you computer.
|
|
44
45
|
|
|
46
|
+
From a `path` to a `string` and a `base64` image encoded string. That is the goal.
|
|
47
|
+
|
|
45
48
|
## Very quickstart
|
|
46
49
|
|
|
47
50
|
```bash
|
|
@@ -76,15 +79,11 @@ from attachments import Attachments
|
|
|
76
79
|
|
|
77
80
|
pdf_attachment = Attachments("https://github.com/microsoft/markitdown/raw/refs/heads/main/packages/markitdown/tests/test_files/test.pdf")
|
|
78
81
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
{
|
|
82
|
-
"""
|
|
83
|
-
|
|
84
|
-
content = [{"type": "input_image","image_url": image} for image in pdf_attachment.images] + \
|
|
85
|
-
[{"type": "input_text", "text": prompt}]
|
|
86
|
-
|
|
87
|
-
response = OpenAI().responses.create(model="gpt-4.1-nano", input=[{"role": "user", "content": content}])
|
|
82
|
+
response = OpenAI().responses.create(
|
|
83
|
+
model="gpt-4.1-nano",
|
|
84
|
+
input=[{
|
|
85
|
+
"role": "user",
|
|
86
|
+
"content": pdf_attachment.to_openai_content("Analyze the following documents:")}])
|
|
88
87
|
response.output_text
|
|
89
88
|
```
|
|
90
89
|
|
|
@@ -97,27 +96,10 @@ from attachments import Attachments
|
|
|
97
96
|
pptx_file = Attachments(
|
|
98
97
|
"https://github.com/microsoft/markitdown/raw/refs/heads/main/packages/markitdown/tests/test_files/test.pptx")
|
|
99
98
|
|
|
100
|
-
prompt = f"""
|
|
101
|
-
Analyze the following documents:
|
|
102
|
-
{pptx_file}
|
|
103
|
-
"""
|
|
104
|
-
|
|
105
|
-
content = [
|
|
106
|
-
{
|
|
107
|
-
"type": "image",
|
|
108
|
-
"source": {
|
|
109
|
-
"type": "base64",
|
|
110
|
-
"media_type": image.split(";")[0].split(":")[1], # this is ugly but it works
|
|
111
|
-
"data": image.split(",")[1] # this is ugly but it works
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
for image in pptx_file.images
|
|
115
|
-
] + [{"type": "text", "text": prompt}]
|
|
116
|
-
|
|
117
99
|
message = anthropic.Anthropic().messages.create(
|
|
118
100
|
max_tokens=8192,
|
|
119
101
|
model="claude-3-5-haiku-20241022",
|
|
120
|
-
messages=[{"role": "user", "content":
|
|
102
|
+
messages=[{"role": "user", "content": pptx_file.to_claude_content("Analyze the following documents:")}])
|
|
121
103
|
print(message.content)
|
|
122
104
|
```
|
|
123
105
|
|
|
@@ -126,6 +108,7 @@ print(message.content)
|
|
|
126
108
|
I am not a fan of the object that langchain and anthropic make you create, but hey!, I don't make the rules...
|
|
127
109
|
|
|
128
110
|
```python
|
|
111
|
+
#pip install -U langchain-anthropic
|
|
129
112
|
from langchain.chat_models import init_chat_model
|
|
130
113
|
from attachments import Attachments
|
|
131
114
|
|
|
@@ -134,21 +117,7 @@ docx_file = Attachments("https://github.com/microsoft/markitdown/raw/refs/heads/
|
|
|
134
117
|
llm = init_chat_model("anthropic:claude-3-5-sonnet-latest")
|
|
135
118
|
|
|
136
119
|
# Creation of a list of object as expected by langchain/anthropic
|
|
137
|
-
content =
|
|
138
|
-
|
|
139
|
-
for image in docx_file.images
|
|
140
|
-
media_type_i = image.split(";")[0].split(":")[1]
|
|
141
|
-
media_data_i = image.split(",")[1]
|
|
142
|
-
content.append({
|
|
143
|
-
"type": "image",
|
|
144
|
-
"source": {
|
|
145
|
-
"type": "base64",
|
|
146
|
-
"media_type": media_type_i,
|
|
147
|
-
"data": media_data_i
|
|
148
|
-
}
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
|
|
120
|
+
content = docx_file.to_claude_content("Analyze the following documents:")
|
|
152
121
|
message = {
|
|
153
122
|
"role": "user",
|
|
154
123
|
"content": content,
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
# Attachments
|
|
1
|
+
# Attachments: The Python Library for Effortless LLM Context
|
|
2
2
|
## Easiest way to give context to LLMs
|
|
3
3
|
|
|
4
4
|
Attachments has the ambition to be the general funnel for any files to be transformed into images+text for large language models context. For this to be true we, has a community, have to stop implementing this funnel (file -> images+text) each on our own. If you have a detector + parser + renderer contibute it! This will help you and us. Don't let your code alone in a dark corner of you computer.
|
|
5
5
|
|
|
6
|
+
From a `path` to a `string` and a `base64` image encoded string. That is the goal.
|
|
7
|
+
|
|
6
8
|
## Very quickstart
|
|
7
9
|
|
|
8
10
|
```bash
|
|
@@ -37,15 +39,11 @@ from attachments import Attachments
|
|
|
37
39
|
|
|
38
40
|
pdf_attachment = Attachments("https://github.com/microsoft/markitdown/raw/refs/heads/main/packages/markitdown/tests/test_files/test.pdf")
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
{
|
|
43
|
-
"""
|
|
44
|
-
|
|
45
|
-
content = [{"type": "input_image","image_url": image} for image in pdf_attachment.images] + \
|
|
46
|
-
[{"type": "input_text", "text": prompt}]
|
|
47
|
-
|
|
48
|
-
response = OpenAI().responses.create(model="gpt-4.1-nano", input=[{"role": "user", "content": content}])
|
|
42
|
+
response = OpenAI().responses.create(
|
|
43
|
+
model="gpt-4.1-nano",
|
|
44
|
+
input=[{
|
|
45
|
+
"role": "user",
|
|
46
|
+
"content": pdf_attachment.to_openai_content("Analyze the following documents:")}])
|
|
49
47
|
response.output_text
|
|
50
48
|
```
|
|
51
49
|
|
|
@@ -58,27 +56,10 @@ from attachments import Attachments
|
|
|
58
56
|
pptx_file = Attachments(
|
|
59
57
|
"https://github.com/microsoft/markitdown/raw/refs/heads/main/packages/markitdown/tests/test_files/test.pptx")
|
|
60
58
|
|
|
61
|
-
prompt = f"""
|
|
62
|
-
Analyze the following documents:
|
|
63
|
-
{pptx_file}
|
|
64
|
-
"""
|
|
65
|
-
|
|
66
|
-
content = [
|
|
67
|
-
{
|
|
68
|
-
"type": "image",
|
|
69
|
-
"source": {
|
|
70
|
-
"type": "base64",
|
|
71
|
-
"media_type": image.split(";")[0].split(":")[1], # this is ugly but it works
|
|
72
|
-
"data": image.split(",")[1] # this is ugly but it works
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
for image in pptx_file.images
|
|
76
|
-
] + [{"type": "text", "text": prompt}]
|
|
77
|
-
|
|
78
59
|
message = anthropic.Anthropic().messages.create(
|
|
79
60
|
max_tokens=8192,
|
|
80
61
|
model="claude-3-5-haiku-20241022",
|
|
81
|
-
messages=[{"role": "user", "content":
|
|
62
|
+
messages=[{"role": "user", "content": pptx_file.to_claude_content("Analyze the following documents:")}])
|
|
82
63
|
print(message.content)
|
|
83
64
|
```
|
|
84
65
|
|
|
@@ -87,6 +68,7 @@ print(message.content)
|
|
|
87
68
|
I am not a fan of the object that langchain and anthropic make you create, but hey!, I don't make the rules...
|
|
88
69
|
|
|
89
70
|
```python
|
|
71
|
+
#pip install -U langchain-anthropic
|
|
90
72
|
from langchain.chat_models import init_chat_model
|
|
91
73
|
from attachments import Attachments
|
|
92
74
|
|
|
@@ -95,21 +77,7 @@ docx_file = Attachments("https://github.com/microsoft/markitdown/raw/refs/heads/
|
|
|
95
77
|
llm = init_chat_model("anthropic:claude-3-5-sonnet-latest")
|
|
96
78
|
|
|
97
79
|
# Creation of a list of object as expected by langchain/anthropic
|
|
98
|
-
content =
|
|
99
|
-
|
|
100
|
-
for image in docx_file.images
|
|
101
|
-
media_type_i = image.split(";")[0].split(":")[1]
|
|
102
|
-
media_data_i = image.split(",")[1]
|
|
103
|
-
content.append({
|
|
104
|
-
"type": "image",
|
|
105
|
-
"source": {
|
|
106
|
-
"type": "base64",
|
|
107
|
-
"media_type": media_type_i,
|
|
108
|
-
"data": media_data_i
|
|
109
|
-
}
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
|
|
80
|
+
content = docx_file.to_claude_content("Analyze the following documents:")
|
|
113
81
|
message = {
|
|
114
82
|
"role": "user",
|
|
115
83
|
"content": content,
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "attachments"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name="Maxim Rivest", email="mrive052@gmail.com" },
|
|
10
10
|
]
|
|
@@ -53,6 +53,7 @@ dev = [
|
|
|
53
53
|
"mypy",
|
|
54
54
|
"black",
|
|
55
55
|
"filelock",
|
|
56
|
+
"mystmd",
|
|
56
57
|
]
|
|
57
58
|
|
|
58
59
|
[tool.setuptools]
|
|
@@ -11,7 +11,7 @@ from typing import List, Dict, Tuple, Optional, Any, Union, cast
|
|
|
11
11
|
from collections import defaultdict
|
|
12
12
|
|
|
13
13
|
from .detectors import Detector
|
|
14
|
-
from .parsers import ParserRegistry, PDFParser, PPTXParser, HTMLParser, ImageParser, AudioParser, DOCXParser, ODTParser
|
|
14
|
+
from .parsers import ParserRegistry, PDFParser, PPTXParser, HTMLParser, ImageParser, AudioParser, DOCXParser, ODTParser, TextParser
|
|
15
15
|
from .renderers import RendererRegistry, DefaultXMLRenderer, PlainTextRenderer
|
|
16
16
|
from .exceptions import DetectionError, ParsingError
|
|
17
17
|
from .image_processing import DEFAULT_IMAGE_OUTPUT_FORMAT, DEFAULT_IMAGE_QUALITY
|
|
@@ -73,6 +73,7 @@ class Attachments:
|
|
|
73
73
|
|
|
74
74
|
self.parser_registry.register('docx', DOCXParser)
|
|
75
75
|
self.parser_registry.register('odt', ODTParser)
|
|
76
|
+
self.parser_registry.register('txt', TextParser) # Register TextParser
|
|
76
77
|
|
|
77
78
|
# Register ImageParser class for various image types
|
|
78
79
|
# The registry will create an instance for each, configured with self._config
|
|
@@ -188,14 +189,42 @@ class Attachments:
|
|
|
188
189
|
|
|
189
190
|
file_type = self.detector.detect(path_for_detector_and_parser, content_type=detected_file_type_arg)
|
|
190
191
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
192
|
+
parser = None
|
|
193
|
+
original_detected_type = file_type # Keep track of initially detected type
|
|
194
|
+
|
|
195
|
+
if file_type:
|
|
196
|
+
try:
|
|
197
|
+
parser = self.parser_registry.get_parser(file_type)
|
|
198
|
+
except KeyError:
|
|
199
|
+
if self.verbose:
|
|
200
|
+
print(f"Warning: No parser registered for detected type '{file_type}' for '{path_for_detector_and_parser}' (from input '{path_str}'). Attempting to parse as plain text.")
|
|
201
|
+
file_type = 'txt' # Fallback to txt
|
|
202
|
+
else:
|
|
203
|
+
if self.verbose:
|
|
204
|
+
print(f"Warning: Could not detect file type for '{path_for_detector_and_parser}' (from input '{path_str}'). Attempting to parse as plain text.")
|
|
205
|
+
file_type = 'txt' # Fallback to txt
|
|
194
206
|
|
|
195
|
-
parser
|
|
207
|
+
if not parser: # If parser is still None, it means we are in a fallback scenario or initial detection failed
|
|
208
|
+
try:
|
|
209
|
+
parser = self.parser_registry.get_parser(file_type) # This should get TextParser if file_type is 'txt'
|
|
210
|
+
except KeyError:
|
|
211
|
+
# This should ideally not happen if TextParser is registered, but as a safeguard:
|
|
212
|
+
print(f"Critical Warning: Fallback TextParser not found or not registered for type '{file_type}'. Cannot process '{path_for_detector_and_parser}'. Skipping.")
|
|
213
|
+
# Clean up temp file if it was created for a URL
|
|
214
|
+
if is_url and temp_file_path_for_parsing and os.path.exists(temp_file_path_for_parsing):
|
|
215
|
+
try:
|
|
216
|
+
os.remove(temp_file_path_for_parsing)
|
|
217
|
+
except Exception as e_clean_critical:
|
|
218
|
+
print(f"Warning: Could not clean up temp file {temp_file_path_for_parsing} during critical skip: {e_clean_critical}")
|
|
219
|
+
continue # Skip this file
|
|
220
|
+
|
|
221
|
+
# If file_type was overridden for fallback, ensure the parsed_content reflects that.
|
|
222
|
+
# However, we might want to store the original_detected_type if it existed.
|
|
196
223
|
parsed_content = parser.parse(path_for_detector_and_parser, indices=indices)
|
|
197
224
|
|
|
198
|
-
parsed_content['type'] = file_type
|
|
225
|
+
parsed_content['type'] = file_type # Use the final file_type (could be 'txt')
|
|
226
|
+
if original_detected_type and original_detected_type != file_type:
|
|
227
|
+
parsed_content['original_detected_type'] = original_detected_type
|
|
199
228
|
parsed_content['id'] = f"{file_type}{i+1}"
|
|
200
229
|
parsed_content['original_path_str'] = path_str
|
|
201
230
|
parsed_content['file_path'] = original_file_path_or_url
|
|
@@ -278,7 +307,7 @@ class Attachments:
|
|
|
278
307
|
'm4a': 'audio/m4a', # mimetypes might say audio/mp4, which is also fine for .m4a
|
|
279
308
|
'mp4_audio': 'audio/mp4',
|
|
280
309
|
'wav': 'audio/wav',
|
|
281
|
-
'flac': 'audio/flac',
|
|
310
|
+
'flac': 'audio/flac',
|
|
282
311
|
'ogg_audio': 'audio/ogg',
|
|
283
312
|
'oga': 'audio/ogg',
|
|
284
313
|
'webm_audio': 'audio/webm',
|
|
@@ -634,6 +663,59 @@ class Attachments:
|
|
|
634
663
|
def another_method_or_end_of_class_placeholder(self):
|
|
635
664
|
pass
|
|
636
665
|
|
|
666
|
+
def to_openai_content(self, prompt: str) -> List[Dict[str, Any]]:
|
|
667
|
+
"""
|
|
668
|
+
Formats attachments and a prompt for the OpenAI API.
|
|
669
|
+
Currently supports images.
|
|
670
|
+
|
|
671
|
+
Args:
|
|
672
|
+
prompt: The text prompt to include.
|
|
673
|
+
|
|
674
|
+
Returns:
|
|
675
|
+
A list of dictionaries formatted for the OpenAI API,
|
|
676
|
+
combining image data and the text prompt.
|
|
677
|
+
"""
|
|
678
|
+
content = [{"type": "input_image", "image_url": image_data_uri} for image_data_uri in self.images]
|
|
679
|
+
content.append({"type": "input_text", "text": prompt + "\n\n" + self.__str__()})
|
|
680
|
+
return content
|
|
681
|
+
|
|
682
|
+
def to_claude_content(self, prompt: str) -> List[Dict[str, Any]]:
|
|
683
|
+
"""
|
|
684
|
+
Formats attachments and a prompt for the Anthropic Claude API.
|
|
685
|
+
Currently supports images.
|
|
686
|
+
|
|
687
|
+
Args:
|
|
688
|
+
prompt: The text prompt to include.
|
|
689
|
+
|
|
690
|
+
Returns:
|
|
691
|
+
A list of dictionaries formatted for the Anthropic Claude API,
|
|
692
|
+
combining image data and the text prompt.
|
|
693
|
+
"""
|
|
694
|
+
content = []
|
|
695
|
+
for image_data_uri in self.images:
|
|
696
|
+
try:
|
|
697
|
+
# Example data URI: "data:image/jpeg;base64,BASE64_STRING"
|
|
698
|
+
header, base64_data = image_data_uri.split(',', 1)
|
|
699
|
+
media_type = header.split(';')[0].split(':')[1]
|
|
700
|
+
|
|
701
|
+
content.append({
|
|
702
|
+
"type": "image",
|
|
703
|
+
"source": {
|
|
704
|
+
"type": "base64",
|
|
705
|
+
"media_type": media_type,
|
|
706
|
+
"data": base64_data
|
|
707
|
+
}
|
|
708
|
+
})
|
|
709
|
+
except ValueError as e:
|
|
710
|
+
# Handle cases where the data URI might not be as expected, though self.images should provide valid ones.
|
|
711
|
+
if self.verbose:
|
|
712
|
+
print(f"Warning: Could not parse image data URI for Claude content: {image_data_uri}. Error: {e}")
|
|
713
|
+
# Optionally, append an error or skip this image
|
|
714
|
+
continue # Skip malformed URIs for now
|
|
715
|
+
|
|
716
|
+
content.append({"type": "text", "text": prompt})
|
|
717
|
+
return content
|
|
718
|
+
|
|
637
719
|
# Potentially other methods follow, or end of class / file
|
|
638
720
|
|
|
639
721
|
def _format_operations_for_markdown(self, operations: Dict[str, Any]) -> str:
|
|
@@ -150,6 +150,27 @@ class PPTXParser(BaseParser):
|
|
|
150
150
|
except Exception as e:
|
|
151
151
|
raise ParsingError(message=f"Failed to parse PPTX '{file_path}': {e}", file_path=file_path, parser_name=self.__class__.__name__) from e
|
|
152
152
|
|
|
153
|
+
class TextParser(BaseParser):
|
|
154
|
+
"""Parses plain text files."""
|
|
155
|
+
def parse(self, file_path: str, indices: Optional[str] = None) -> Dict[str, Any]:
|
|
156
|
+
"""Reads the file content as UTF-8 text.
|
|
157
|
+
`indices` argument is ignored for text files.
|
|
158
|
+
"""
|
|
159
|
+
try:
|
|
160
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
161
|
+
text_content = f.read()
|
|
162
|
+
return {
|
|
163
|
+
"text": text_content,
|
|
164
|
+
"original_basename": os.path.basename(file_path),
|
|
165
|
+
"file_path": file_path,
|
|
166
|
+
}
|
|
167
|
+
except FileNotFoundError:
|
|
168
|
+
raise ParsingError(message=f"Text file not found: {file_path}", file_path=file_path, parser_name=self.__class__.__name__) from None
|
|
169
|
+
except UnicodeDecodeError as e:
|
|
170
|
+
raise ParsingError(message=f"Error decoding text file '{file_path}' as UTF-8: {e}. The file might not be plain text or uses a different encoding.", file_path=file_path, parser_name=self.__class__.__name__) from e
|
|
171
|
+
except Exception as e:
|
|
172
|
+
raise ParsingError(message=f"Failed to parse text file '{file_path}': {e}", file_path=file_path, parser_name=self.__class__.__name__) from e
|
|
173
|
+
|
|
153
174
|
# ImageParser and AudioParser __init__ are fine from previous step.
|
|
154
175
|
# Their full parse methods will be restored/verified after document parsers.
|
|
155
176
|
class ImageParser(BaseParser):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: attachments
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A Python library to handle various file types for LLMs.
|
|
5
5
|
Author-email: Maxim Rivest <mrive052@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -35,13 +35,16 @@ Requires-Dist: flake8; extra == "dev"
|
|
|
35
35
|
Requires-Dist: mypy; extra == "dev"
|
|
36
36
|
Requires-Dist: black; extra == "dev"
|
|
37
37
|
Requires-Dist: filelock; extra == "dev"
|
|
38
|
+
Requires-Dist: mystmd; extra == "dev"
|
|
38
39
|
Dynamic: license-file
|
|
39
40
|
|
|
40
|
-
# Attachments
|
|
41
|
+
# Attachments: The Python Library for Effortless LLM Context
|
|
41
42
|
## Easiest way to give context to LLMs
|
|
42
43
|
|
|
43
44
|
Attachments has the ambition to be the general funnel for any files to be transformed into images+text for large language models context. For this to be true we, has a community, have to stop implementing this funnel (file -> images+text) each on our own. If you have a detector + parser + renderer contibute it! This will help you and us. Don't let your code alone in a dark corner of you computer.
|
|
44
45
|
|
|
46
|
+
From a `path` to a `string` and a `base64` image encoded string. That is the goal.
|
|
47
|
+
|
|
45
48
|
## Very quickstart
|
|
46
49
|
|
|
47
50
|
```bash
|
|
@@ -76,15 +79,11 @@ from attachments import Attachments
|
|
|
76
79
|
|
|
77
80
|
pdf_attachment = Attachments("https://github.com/microsoft/markitdown/raw/refs/heads/main/packages/markitdown/tests/test_files/test.pdf")
|
|
78
81
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
{
|
|
82
|
-
"""
|
|
83
|
-
|
|
84
|
-
content = [{"type": "input_image","image_url": image} for image in pdf_attachment.images] + \
|
|
85
|
-
[{"type": "input_text", "text": prompt}]
|
|
86
|
-
|
|
87
|
-
response = OpenAI().responses.create(model="gpt-4.1-nano", input=[{"role": "user", "content": content}])
|
|
82
|
+
response = OpenAI().responses.create(
|
|
83
|
+
model="gpt-4.1-nano",
|
|
84
|
+
input=[{
|
|
85
|
+
"role": "user",
|
|
86
|
+
"content": pdf_attachment.to_openai_content("Analyze the following documents:")}])
|
|
88
87
|
response.output_text
|
|
89
88
|
```
|
|
90
89
|
|
|
@@ -97,27 +96,10 @@ from attachments import Attachments
|
|
|
97
96
|
pptx_file = Attachments(
|
|
98
97
|
"https://github.com/microsoft/markitdown/raw/refs/heads/main/packages/markitdown/tests/test_files/test.pptx")
|
|
99
98
|
|
|
100
|
-
prompt = f"""
|
|
101
|
-
Analyze the following documents:
|
|
102
|
-
{pptx_file}
|
|
103
|
-
"""
|
|
104
|
-
|
|
105
|
-
content = [
|
|
106
|
-
{
|
|
107
|
-
"type": "image",
|
|
108
|
-
"source": {
|
|
109
|
-
"type": "base64",
|
|
110
|
-
"media_type": image.split(";")[0].split(":")[1], # this is ugly but it works
|
|
111
|
-
"data": image.split(",")[1] # this is ugly but it works
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
for image in pptx_file.images
|
|
115
|
-
] + [{"type": "text", "text": prompt}]
|
|
116
|
-
|
|
117
99
|
message = anthropic.Anthropic().messages.create(
|
|
118
100
|
max_tokens=8192,
|
|
119
101
|
model="claude-3-5-haiku-20241022",
|
|
120
|
-
messages=[{"role": "user", "content":
|
|
102
|
+
messages=[{"role": "user", "content": pptx_file.to_claude_content("Analyze the following documents:")}])
|
|
121
103
|
print(message.content)
|
|
122
104
|
```
|
|
123
105
|
|
|
@@ -126,6 +108,7 @@ print(message.content)
|
|
|
126
108
|
I am not a fan of the object that langchain and anthropic make you create, but hey!, I don't make the rules...
|
|
127
109
|
|
|
128
110
|
```python
|
|
111
|
+
#pip install -U langchain-anthropic
|
|
129
112
|
from langchain.chat_models import init_chat_model
|
|
130
113
|
from attachments import Attachments
|
|
131
114
|
|
|
@@ -134,21 +117,7 @@ docx_file = Attachments("https://github.com/microsoft/markitdown/raw/refs/heads/
|
|
|
134
117
|
llm = init_chat_model("anthropic:claude-3-5-sonnet-latest")
|
|
135
118
|
|
|
136
119
|
# Creation of a list of object as expected by langchain/anthropic
|
|
137
|
-
content =
|
|
138
|
-
|
|
139
|
-
for image in docx_file.images
|
|
140
|
-
media_type_i = image.split(";")[0].split(":")[1]
|
|
141
|
-
media_data_i = image.split(",")[1]
|
|
142
|
-
content.append({
|
|
143
|
-
"type": "image",
|
|
144
|
-
"source": {
|
|
145
|
-
"type": "base64",
|
|
146
|
-
"media_type": media_type_i,
|
|
147
|
-
"data": media_data_i
|
|
148
|
-
}
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
|
|
120
|
+
content = docx_file.to_claude_content("Analyze the following documents:")
|
|
152
121
|
message = {
|
|
153
122
|
"role": "user",
|
|
154
123
|
"content": content,
|
|
@@ -282,5 +282,55 @@ class TestCoreFunctionality(unittest.TestCase):
|
|
|
282
282
|
# It's unclear what this test is intended to check, so it's left unchanged
|
|
283
283
|
pass
|
|
284
284
|
|
|
285
|
+
def test_fallback_to_text_parser(self):
|
|
286
|
+
"""Test that files with unknown extensions or no extension fall back to TextParser."""
|
|
287
|
+
unknown_ext_content = "Unknown extension fallback test."
|
|
288
|
+
no_ext_content = "No extension fallback test."
|
|
289
|
+
|
|
290
|
+
temp_dir = TEST_DATA_DIR # Use the existing test data directory for temp files
|
|
291
|
+
unknown_ext_filepath = os.path.join(temp_dir, "test_unknown.unknownext")
|
|
292
|
+
no_ext_filepath = os.path.join(temp_dir, "test_no_ext_file")
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
with open(unknown_ext_filepath, "w", encoding="utf-8") as f:
|
|
296
|
+
f.write(unknown_ext_content)
|
|
297
|
+
with open(no_ext_filepath, "w", encoding="utf-8") as f:
|
|
298
|
+
f.write(no_ext_content)
|
|
299
|
+
|
|
300
|
+
atts = Attachments(unknown_ext_filepath, no_ext_filepath, verbose=True) # Verbose for debugging if needed
|
|
301
|
+
self.assertEqual(len(atts.attachments_data), 2, "Should process two files with fallback")
|
|
302
|
+
|
|
303
|
+
# Sort by original path to ensure consistent order for assertions
|
|
304
|
+
sorted_attachments = sorted(atts.attachments_data, key=lambda x: x['original_path_str'])
|
|
305
|
+
|
|
306
|
+
# Test the file with no extension
|
|
307
|
+
# The order depends on how Attachments processes and potentially sorts them, or how listdir returns them.
|
|
308
|
+
# Assuming Attachments processes in order of input for now, or we find by path.
|
|
309
|
+
no_ext_data = next((item for item in sorted_attachments if item['original_path_str'] == no_ext_filepath), None)
|
|
310
|
+
self.assertIsNotNone(no_ext_data, "Data for no-extension file not found")
|
|
311
|
+
if no_ext_data: # Check to satisfy mypy
|
|
312
|
+
self.assertEqual(no_ext_data['type'], 'txt', "File with no extension should be parsed as txt")
|
|
313
|
+
self.assertEqual(no_ext_data['text'], no_ext_content, "Text content mismatch for no-extension file")
|
|
314
|
+
self.assertIsNone(no_ext_data.get('original_detected_type'), "original_detected_type should be None for no-extension file")
|
|
315
|
+
|
|
316
|
+
# Test the file with an unknown extension
|
|
317
|
+
unknown_ext_data = next((item for item in sorted_attachments if item['original_path_str'] == unknown_ext_filepath), None)
|
|
318
|
+
self.assertIsNotNone(unknown_ext_data, "Data for unknown-extension file not found")
|
|
319
|
+
if unknown_ext_data: # Check to satisfy mypy
|
|
320
|
+
self.assertEqual(unknown_ext_data['type'], 'txt', "File with unknown extension should be parsed as txt")
|
|
321
|
+
self.assertEqual(unknown_ext_data['text'], unknown_ext_content, "Text content mismatch for unknown-extension file")
|
|
322
|
+
self.assertIsNone(unknown_ext_data.get('original_detected_type'), "original_detected_type should be None for unknown-extension file")
|
|
323
|
+
|
|
324
|
+
finally:
|
|
325
|
+
# Clean up temporary files
|
|
326
|
+
if os.path.exists(unknown_ext_filepath):
|
|
327
|
+
os.remove(unknown_ext_filepath)
|
|
328
|
+
if os.path.exists(no_ext_filepath):
|
|
329
|
+
os.remove(no_ext_filepath)
|
|
330
|
+
|
|
331
|
+
def test_initialize_attachments_with_list_of_paths(self):
|
|
332
|
+
"""Test initializing Attachments with a list of paths."""
|
|
333
|
+
# ... existing code ...
|
|
334
|
+
|
|
285
335
|
if __name__ == '__main__':
|
|
286
336
|
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|