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.
Files changed (33) hide show
  1. {attachments-0.2.4/src/attachments.egg-info → attachments-0.3.0}/PKG-INFO +13 -44
  2. {attachments-0.2.4 → attachments-0.3.0}/README.md +11 -43
  3. {attachments-0.2.4 → attachments-0.3.0}/pyproject.toml +2 -1
  4. {attachments-0.2.4 → attachments-0.3.0}/src/attachments/core.py +89 -7
  5. {attachments-0.2.4 → attachments-0.3.0}/src/attachments/parsers.py +21 -0
  6. {attachments-0.2.4 → attachments-0.3.0/src/attachments.egg-info}/PKG-INFO +13 -44
  7. {attachments-0.2.4 → attachments-0.3.0}/src/attachments.egg-info/requires.txt +1 -0
  8. {attachments-0.2.4 → attachments-0.3.0}/tests/test_core_functionality.py +50 -0
  9. {attachments-0.2.4 → attachments-0.3.0}/LICENSE +0 -0
  10. {attachments-0.2.4 → attachments-0.3.0}/setup.cfg +0 -0
  11. {attachments-0.2.4 → attachments-0.3.0}/src/attachments/__init__.py +0 -0
  12. {attachments-0.2.4 → attachments-0.3.0}/src/attachments/audio_processing.py +0 -0
  13. {attachments-0.2.4 → attachments-0.3.0}/src/attachments/config.py +0 -0
  14. {attachments-0.2.4 → attachments-0.3.0}/src/attachments/detectors.py +0 -0
  15. {attachments-0.2.4 → attachments-0.3.0}/src/attachments/exceptions.py +0 -0
  16. {attachments-0.2.4 → attachments-0.3.0}/src/attachments/image_processing.py +0 -0
  17. {attachments-0.2.4 → attachments-0.3.0}/src/attachments/office_contact_sheet.py +0 -0
  18. {attachments-0.2.4 → attachments-0.3.0}/src/attachments/renderers.py +0 -0
  19. {attachments-0.2.4 → attachments-0.3.0}/src/attachments/utils.py +0 -0
  20. {attachments-0.2.4 → attachments-0.3.0}/src/attachments.egg-info/SOURCES.txt +0 -0
  21. {attachments-0.2.4 → attachments-0.3.0}/src/attachments.egg-info/dependency_links.txt +0 -0
  22. {attachments-0.2.4 → attachments-0.3.0}/src/attachments.egg-info/top_level.txt +0 -0
  23. {attachments-0.2.4 → attachments-0.3.0}/tests/test_audio_processing.py +0 -0
  24. {attachments-0.2.4 → attachments-0.3.0}/tests/test_base.py +0 -0
  25. {attachments-0.2.4 → attachments-0.3.0}/tests/test_document_parsing.py +0 -0
  26. {attachments-0.2.4 → attachments-0.3.0}/tests/test_html_parsing.py +0 -0
  27. {attachments-0.2.4 → attachments-0.3.0}/tests/test_image_processing.py +0 -0
  28. {attachments-0.2.4 → attachments-0.3.0}/tests/test_indexing_slicing.py +0 -0
  29. {attachments-0.2.4 → attachments-0.3.0}/tests/test_markdown_rendering.py +0 -0
  30. {attachments-0.2.4 → attachments-0.3.0}/tests/test_office_contact_sheet.py +0 -0
  31. {attachments-0.2.4 → attachments-0.3.0}/tests/test_pdf_parsing.py +0 -0
  32. {attachments-0.2.4 → attachments-0.3.0}/tests/test_pptx_parsing.py +0 -0
  33. {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.2.4
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
- prompt = f"""
80
- Analyze the following documents:
81
- {pdf_attachment}
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": 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 = [{"type": "text", "text": f"Analyze the following documents: {docx_file}"}]
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
- prompt = f"""
41
- Analyze the following documents:
42
- {pdf_attachment}
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": 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 = [{"type": "text", "text": f"Analyze the following documents: {docx_file}"}]
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.2.4"
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
- if not file_type:
192
- print(f"Warning: Could not detect file type for '{path_for_detector_and_parser}' (from input '{path_str}'). Skipping.")
193
- continue
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 = self.parser_registry.get_parser(file_type)
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.2.4
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
- prompt = f"""
80
- Analyze the following documents:
81
- {pdf_attachment}
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": 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 = [{"type": "text", "text": f"Analyze the following documents: {docx_file}"}]
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,
@@ -23,3 +23,4 @@ flake8
23
23
  mypy
24
24
  black
25
25
  filelock
26
+ mystmd
@@ -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