siada-cli 0.0.1__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.
Files changed (215) hide show
  1. agent_config.yaml +34 -0
  2. siada/__init__.py +13 -0
  3. siada/agent_hub/__init__.py +10 -0
  4. siada/agent_hub/coder/bug_fix_agent.py +398 -0
  5. siada/agent_hub/coder/code_gen_agent.py +235 -0
  6. siada/agent_hub/coder/fe_gen_agent.py +37 -0
  7. siada/agent_hub/coder/issue_review_agent.py +68 -0
  8. siada/agent_hub/coder/prompt/__init__.py +0 -0
  9. siada/agent_hub/coder/prompt/base/__init__.py +0 -0
  10. siada/agent_hub/coder/prompt/base/capabilities.py +18 -0
  11. siada/agent_hub/coder/prompt/base/non_interactive.py +14 -0
  12. siada/agent_hub/coder/prompt/base/prompt_builder.py +33 -0
  13. siada/agent_hub/coder/prompt/base/rules.py +40 -0
  14. siada/agent_hub/coder/prompt/base/tool_use.py +14 -0
  15. siada/agent_hub/coder/prompt/bug_prompt/bug_fix_prompt.py +51 -0
  16. siada/agent_hub/coder/prompt/bug_prompt/rules.py +53 -0
  17. siada/agent_hub/coder/prompt/code_gen_prompt.py +45 -0
  18. siada/agent_hub/coder/prompt/fe_gen_prompt.py +45 -0
  19. siada/agent_hub/coder/prompt/issue_review_prompt.py +121 -0
  20. siada/agent_hub/coder/tracing/__init__.py +22 -0
  21. siada/agent_hub/coder/tracing/logger_tracing_processor.py +458 -0
  22. siada/agent_hub/hooks/__init__.py +0 -0
  23. siada/agent_hub/hooks/io_hooks.py +30 -0
  24. siada/agent_hub/siada_agent.py +214 -0
  25. siada/config/__init__.py +0 -0
  26. siada/config/config_loader.py +67 -0
  27. siada/entrypoint/__init__.py +3 -0
  28. siada/entrypoint/args_parser/__init__.py +0 -0
  29. siada/entrypoint/args_parser/args.py +195 -0
  30. siada/entrypoint/interaction/__init__.py +0 -0
  31. siada/entrypoint/interaction/base_turn.py +107 -0
  32. siada/entrypoint/interaction/config.py +30 -0
  33. siada/entrypoint/interaction/controller.py +122 -0
  34. siada/entrypoint/interaction/nointeractive_controller.py +27 -0
  35. siada/entrypoint/interaction/turn/interface.py +116 -0
  36. siada/entrypoint/interaction/turn/models.py +34 -0
  37. siada/entrypoint/interaction/turn/run_turn.py +641 -0
  38. siada/entrypoint/interaction/turn_factory.py +35 -0
  39. siada/entrypoint/interaction/turn_models.py +28 -0
  40. siada/entrypoint/siadahub.py +413 -0
  41. siada/foundation/__init__.py +0 -0
  42. siada/foundation/code_agent_context.py +48 -0
  43. siada/foundation/config.py +59 -0
  44. siada/foundation/constants.py +2 -0
  45. siada/foundation/context.py +76 -0
  46. siada/foundation/logging.py +341 -0
  47. siada/foundation/tools/__init__.py +0 -0
  48. siada/foundation/tools/get_git_diff.py +105 -0
  49. siada/io/__init__.py +0 -0
  50. siada/io/banner.py +148 -0
  51. siada/io/color_settings.py +207 -0
  52. siada/io/color_utils.py +16 -0
  53. siada/io/components/__init__.py +0 -0
  54. siada/io/components/mdstream.py +204 -0
  55. siada/io/console_printer.py +68 -0
  56. siada/io/io.py +661 -0
  57. siada/io/key_bindings.py +129 -0
  58. siada/io/notification_command.py +40 -0
  59. siada/io/test_banner.py +64 -0
  60. siada/models/__init__.py +0 -0
  61. siada/models/agent.py +47 -0
  62. siada/models/coder/__init__.py +0 -0
  63. siada/models/converter.py +27 -0
  64. siada/models/model_base_config.py +121 -0
  65. siada/models/model_run_config.py +153 -0
  66. siada/provider/__init__.py +0 -0
  67. siada/provider/client_factory.py +112 -0
  68. siada/provider/llm_client.py +11 -0
  69. siada/provider/openrouter/__init__.py +0 -0
  70. siada/provider/openrouter/coverter.py +17 -0
  71. siada/provider/openrouter/openroute_provider.py +33 -0
  72. siada/provider/provider_factory.py +53 -0
  73. siada/queries/tree-sitter-language-pack/README.md +7 -0
  74. siada/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
  75. siada/queries/tree-sitter-language-pack/c-tags.scm +9 -0
  76. siada/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
  77. siada/queries/tree-sitter-language-pack/commonlisp-tags.scm +122 -0
  78. siada/queries/tree-sitter-language-pack/cpp-tags.scm +15 -0
  79. siada/queries/tree-sitter-language-pack/csharp-tags.scm +26 -0
  80. siada/queries/tree-sitter-language-pack/d-tags.scm +26 -0
  81. siada/queries/tree-sitter-language-pack/dart-tags.scm +92 -0
  82. siada/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
  83. siada/queries/tree-sitter-language-pack/elixir-tags.scm +54 -0
  84. siada/queries/tree-sitter-language-pack/elm-tags.scm +19 -0
  85. siada/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
  86. siada/queries/tree-sitter-language-pack/go-tags.scm +42 -0
  87. siada/queries/tree-sitter-language-pack/java-tags.scm +20 -0
  88. siada/queries/tree-sitter-language-pack/javascript-tags.scm +88 -0
  89. siada/queries/tree-sitter-language-pack/lua-tags.scm +34 -0
  90. siada/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
  91. siada/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +98 -0
  92. siada/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
  93. siada/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
  94. siada/queries/tree-sitter-language-pack/python-tags.scm +14 -0
  95. siada/queries/tree-sitter-language-pack/r-tags.scm +21 -0
  96. siada/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
  97. siada/queries/tree-sitter-language-pack/ruby-tags.scm +64 -0
  98. siada/queries/tree-sitter-language-pack/rust-tags.scm +60 -0
  99. siada/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
  100. siada/queries/tree-sitter-language-pack/swift-tags.scm +51 -0
  101. siada/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
  102. siada/queries/tree-sitter-languages/README.md +23 -0
  103. siada/queries/tree-sitter-languages/c-tags.scm +9 -0
  104. siada/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
  105. siada/queries/tree-sitter-languages/cpp-tags.scm +15 -0
  106. siada/queries/tree-sitter-languages/dart-tags.scm +91 -0
  107. siada/queries/tree-sitter-languages/elisp-tags.scm +8 -0
  108. siada/queries/tree-sitter-languages/elixir-tags.scm +54 -0
  109. siada/queries/tree-sitter-languages/elm-tags.scm +19 -0
  110. siada/queries/tree-sitter-languages/go-tags.scm +30 -0
  111. siada/queries/tree-sitter-languages/hcl-tags.scm +77 -0
  112. siada/queries/tree-sitter-languages/java-tags.scm +20 -0
  113. siada/queries/tree-sitter-languages/javascript-tags.scm +88 -0
  114. siada/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
  115. siada/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
  116. siada/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
  117. siada/queries/tree-sitter-languages/php-tags.scm +26 -0
  118. siada/queries/tree-sitter-languages/python-tags.scm +12 -0
  119. siada/queries/tree-sitter-languages/ql-tags.scm +26 -0
  120. siada/queries/tree-sitter-languages/ruby-tags.scm +64 -0
  121. siada/queries/tree-sitter-languages/rust-tags.scm +60 -0
  122. siada/queries/tree-sitter-languages/scala-tags.scm +65 -0
  123. siada/queries/tree-sitter-languages/typescript-tags.scm +41 -0
  124. siada/services/__init__.py +30 -0
  125. siada/services/at_file_recommendation_design_document.md +1213 -0
  126. siada/services/code_context_manager.py +136 -0
  127. siada/services/enhanced_fix_result_check.py +546 -0
  128. siada/services/execution_trace_collector.py +207 -0
  129. siada/services/file_recommendation/__init__.py +136 -0
  130. siada/services/file_recommendation/core/__init__.py +17 -0
  131. siada/services/file_recommendation/core/completion_engine.py +351 -0
  132. siada/services/file_recommendation/core/config.py +62 -0
  133. siada/services/file_recommendation/core/file_discovery.py +353 -0
  134. siada/services/file_recommendation/core/suggestion.py +110 -0
  135. siada/services/file_recommendation/utils/__init__.py +22 -0
  136. siada/services/file_recommendation/utils/path_utils.py +138 -0
  137. siada/services/file_recommendation/utils/text_utils.py +253 -0
  138. siada/services/fix_result_check.py +377 -0
  139. siada/services/handle_at_command/README.md +247 -0
  140. siada/services/handle_at_command/__init__.py +115 -0
  141. siada/services/handle_at_command/exceptions.py +46 -0
  142. siada/services/handle_at_command/models.py +92 -0
  143. siada/services/handle_at_command/parser.py +189 -0
  144. siada/services/handle_at_command/processor.py +321 -0
  145. siada/services/handle_at_command/resolver.py +318 -0
  146. siada/services/handle_at_command/utils.py +345 -0
  147. siada/services/model_info_service.py +103 -0
  148. siada/services/siada_runner.py +170 -0
  149. siada/session/__init__.py +29 -0
  150. siada/session/session_manager.py +66 -0
  151. siada/session/session_models.py +43 -0
  152. siada/support/__init__.py +0 -0
  153. siada/support/completer.py +204 -0
  154. siada/support/editor.py +146 -0
  155. siada/support/envprocessor.py +54 -0
  156. siada/support/repo.py +51 -0
  157. siada/support/slash_commands.py +410 -0
  158. siada/support/spinner.py +320 -0
  159. siada/tools/__init__.py +0 -0
  160. siada/tools/ast/README.md +345 -0
  161. siada/tools/ast/ast_tool.py +341 -0
  162. siada/tools/ast/models.py +11 -0
  163. siada/tools/coder/__init__.py +0 -0
  164. siada/tools/coder/ask_followup_question.py +70 -0
  165. siada/tools/coder/cmd_runner.py +132 -0
  166. siada/tools/coder/file_operator.py +284 -0
  167. siada/tools/coder/file_search/README.md +223 -0
  168. siada/tools/coder/file_search/__init__.py +10 -0
  169. siada/tools/coder/file_search/bin/rg-linux-arm64 +0 -0
  170. siada/tools/coder/file_search/bin/rg-linux-x64 +0 -0
  171. siada/tools/coder/file_search/bin/rg-macos-arm64 +0 -0
  172. siada/tools/coder/file_search/bin/rg-macos-x64 +0 -0
  173. siada/tools/coder/file_search/bin/rg.exe +0 -0
  174. siada/tools/coder/file_search/search.py +437 -0
  175. siada/tools/coder/file_search/setup_package.py +150 -0
  176. siada/tools/coder/file_search/test_search.py +142 -0
  177. siada/tools/coder/files.py +147 -0
  178. siada/tools/coder/fix_attempt_completion.py +58 -0
  179. siada/tools/coder/issue_review_completion.py +29 -0
  180. siada/tools/coder/observation/__init__.py +0 -0
  181. siada/tools/coder/observation/error.py +22 -0
  182. siada/tools/coder/observation/file_observation.py +216 -0
  183. siada/tools/coder/observation/observation.py +71 -0
  184. siada/tools/coder/repo_map/__init__.py +157 -0
  185. siada/tools/coder/repo_map/dump.py +29 -0
  186. siada/tools/coder/repo_map/io.py +226 -0
  187. siada/tools/coder/repo_map/repo_map.py +881 -0
  188. siada/tools/coder/repo_map/special.py +203 -0
  189. siada/tools/coder/repo_map/token_counter.py +154 -0
  190. siada/tools/coder/repo_map/waiting.py +221 -0
  191. siada/tools/coder/reproduce_completion.py +39 -0
  192. siada/tools/coder/run_cmd.py +60 -0
  193. siada/tools/coder/test_completion.py +25 -0
  194. siada/tools/coder/tool_docs.py +44 -0
  195. siada/tools/compression_tool.py +200 -0
  196. siada/tools/read_many_files/README.md +220 -0
  197. siada/tools/read_many_files/__init__.py +25 -0
  198. siada/tools/read_many_files/file_processor.py +486 -0
  199. siada/tools/read_many_files/filters.py +326 -0
  200. siada/tools/read_many_files/formatters.py +204 -0
  201. siada/tools/read_many_files/models.py +189 -0
  202. siada/tools/read_many_files_tool.py +233 -0
  203. siada/tools/tool_call_format/__init__.py +55 -0
  204. siada/tools/tool_call_format/file_to_language.py +231 -0
  205. siada/tools/tool_call_format/formatter_factory.py +80 -0
  206. siada/tools/tool_call_format/formatters.py +282 -0
  207. siada/tools/tool_call_format/tool_call_formatter.py +54 -0
  208. siada/tools/web/__init__.py +0 -0
  209. siada/tools/web/web_crawl.py +208 -0
  210. siada/utils.py +194 -0
  211. siada_cli-0.0.1.dist-info/LICENSE +201 -0
  212. siada_cli-0.0.1.dist-info/METADATA +457 -0
  213. siada_cli-0.0.1.dist-info/RECORD +215 -0
  214. siada_cli-0.0.1.dist-info/WHEEL +4 -0
  215. siada_cli-0.0.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,486 @@
1
+ """
2
+ File processing utilities for ReadManyFiles tool.
3
+ """
4
+
5
+ import os
6
+ import glob
7
+ import asyncio
8
+ import mimetypes
9
+ import base64
10
+ from pathlib import Path
11
+ from typing import List, Set, Dict, Tuple, Optional, Any, Union
12
+ import time
13
+
14
+ from .models import (
15
+ FileProcessResult,
16
+ ProcessingStats,
17
+ TEXT_FILE_EXTENSIONS,
18
+ IMAGE_FILE_EXTENSIONS,
19
+ PDF_FILE_EXTENSIONS
20
+ )
21
+ from .filters import FileFilter
22
+
23
+
24
+ class FileProcessor:
25
+ """File processing utility class"""
26
+
27
+ # Configuration constants
28
+ MAX_FILE_SIZE_BYTES = 20 * 1024 * 1024 # 20MB
29
+ MAX_TEXT_LINES = 2000
30
+ MAX_CONTENT_CHARS = 100000
31
+ MAX_CONCURRENT_FILES = 10
32
+ DEFAULT_OUTPUT_SEPARATOR_FORMAT = "--- {filePath} ---"
33
+
34
+ def __init__(self, target_dir: str):
35
+ self.target_dir = Path(target_dir).resolve()
36
+ self.file_filter = FileFilter(str(self.target_dir))
37
+ self.stats = ProcessingStats()
38
+
39
+ async def search_files_with_glob(self, search_patterns: List[str],
40
+ exclusion_patterns: List[str],
41
+ signal=None) -> Set[str]:
42
+ """
43
+ Search files using glob patterns
44
+
45
+ Args:
46
+ search_patterns: List of glob search patterns
47
+ exclusion_patterns: List of exclusion patterns
48
+ signal: Cancellation signal (optional)
49
+
50
+ Returns:
51
+ Set of absolute file paths
52
+ """
53
+ all_entries = set()
54
+
55
+ # Use target_dir as the single workspace directory
56
+ workspace_dir = str(self.target_dir)
57
+
58
+ for pattern in search_patterns:
59
+ # Check cancellation signal
60
+ if signal and signal.is_cancelled():
61
+ break
62
+
63
+ try:
64
+ # Normalize path separators (Windows compatibility)
65
+ normalized_pattern = pattern.replace('\\', '/')
66
+
67
+ # Build full path pattern
68
+ if os.path.isabs(normalized_pattern):
69
+ full_pattern = normalized_pattern
70
+ else:
71
+ full_pattern = os.path.join(workspace_dir, normalized_pattern)
72
+
73
+ # Execute glob search in thread pool to avoid blocking
74
+ loop = asyncio.get_event_loop()
75
+ matches = await loop.run_in_executor(
76
+ None,
77
+ lambda: glob.glob(full_pattern, recursive=True)
78
+ )
79
+
80
+ # Filter directories, keep only files
81
+ file_matches = [f for f in matches if os.path.isfile(f)]
82
+
83
+ # Apply exclusion patterns
84
+ for file_path in file_matches:
85
+ if not self.file_filter.should_exclude_file(
86
+ file_path, workspace_dir, exclusion_patterns
87
+ ):
88
+ all_entries.add(file_path)
89
+
90
+ except Exception as error:
91
+ # Log error but continue processing other patterns
92
+ print(f"Glob pattern '{pattern}' failed: {error}")
93
+
94
+ self.stats.total_files_found = len(all_entries)
95
+ return all_entries
96
+
97
+ async def process_files(self, file_paths: List[str],
98
+ input_patterns: List[str]) -> Tuple[List[Any], List[str], List[Dict]]:
99
+ """
100
+ Process multiple files concurrently
101
+
102
+ Args:
103
+ file_paths: List of absolute file paths to process
104
+ input_patterns: Original input patterns for explicit request checking
105
+
106
+ Returns:
107
+ Tuple of (content_parts, processed_files, skipped_files)
108
+ """
109
+ content_parts = []
110
+ processed_files = []
111
+ skipped_files = []
112
+
113
+ # Sort file paths for consistent output order
114
+ sorted_files = sorted(file_paths)
115
+
116
+ # Process files concurrently with semaphore to control concurrency
117
+ semaphore = asyncio.Semaphore(self.MAX_CONCURRENT_FILES)
118
+
119
+ async def process_single_file_wrapper(file_path: str):
120
+ async with semaphore:
121
+ return await self.process_single_file(file_path, input_patterns)
122
+
123
+ # Create tasks for all files
124
+ tasks = [process_single_file_wrapper(fp) for fp in sorted_files]
125
+
126
+ # Execute all tasks concurrently
127
+ results = await asyncio.gather(*tasks, return_exceptions=True)
128
+
129
+ # Process results
130
+ for i, result in enumerate(results):
131
+ file_path = sorted_files[i]
132
+ relative_path = os.path.relpath(file_path, self.target_dir).replace('\\', '/')
133
+
134
+ if isinstance(result, Exception):
135
+ skipped_files.append({
136
+ 'path': relative_path,
137
+ 'reason': f'Processing error: {str(result)}'
138
+ })
139
+ self.stats.error_files += 1
140
+ elif result.success:
141
+ content_parts.append(result.content)
142
+ processed_files.append(relative_path)
143
+ self.stats.processed_files += 1
144
+
145
+ # Update file type statistics
146
+ if result.file_type == 'text':
147
+ self.stats.text_files += 1
148
+ elif result.file_type == 'image':
149
+ self.stats.image_files += 1
150
+ elif result.file_type == 'pdf':
151
+ self.stats.pdf_files += 1
152
+ else:
153
+ self.stats.binary_files += 1
154
+
155
+ if result.size:
156
+ self.stats.total_size += result.size
157
+ else:
158
+ skipped_files.append({
159
+ 'path': relative_path,
160
+ 'reason': result.reason
161
+ })
162
+ self.stats.skipped_files += 1
163
+
164
+ return content_parts, processed_files, skipped_files
165
+
166
+ async def process_single_file(self, file_path: str,
167
+ input_patterns: List[str]) -> FileProcessResult:
168
+ """
169
+ Process a single file
170
+
171
+ Args:
172
+ file_path: Absolute path to the file
173
+ input_patterns: Original input patterns for explicit request checking
174
+
175
+ Returns:
176
+ FileProcessResult object
177
+ """
178
+ try:
179
+ # Detect file type
180
+ file_type = await self.detect_file_type(file_path)
181
+
182
+ # Check if resource files are explicitly requested
183
+ if file_type in ['image', 'pdf']:
184
+ if not self.is_explicitly_requested(file_path, input_patterns):
185
+ return FileProcessResult(
186
+ success=False,
187
+ path=file_path,
188
+ reason=f'{file_type} file was not explicitly requested by name or extension',
189
+ file_type=file_type
190
+ )
191
+
192
+ # Check file size
193
+ file_size = os.path.getsize(file_path)
194
+ if file_size > self.MAX_FILE_SIZE_BYTES:
195
+ return FileProcessResult(
196
+ success=False,
197
+ path=file_path,
198
+ reason=f'File too large ({file_size / 1024 / 1024:.1f}MB > {self.MAX_FILE_SIZE_BYTES / 1024 / 1024}MB)',
199
+ file_type=file_type,
200
+ size=file_size
201
+ )
202
+
203
+ # Read file content
204
+ file_content = await self.read_file_content_by_type(file_path, file_type)
205
+
206
+ if file_content.get('error'):
207
+ return FileProcessResult(
208
+ success=False,
209
+ path=file_path,
210
+ reason=f"Read error: {file_content['error']}",
211
+ file_type=file_type,
212
+ size=file_size
213
+ )
214
+
215
+ # Format content
216
+ formatted_content = self.format_file_content(file_path, file_content['content'])
217
+
218
+ return FileProcessResult(
219
+ success=True,
220
+ path=file_path,
221
+ content=formatted_content,
222
+ file_type=file_type,
223
+ size=file_size
224
+ )
225
+
226
+ except Exception as error:
227
+ return FileProcessResult(
228
+ success=False,
229
+ path=file_path,
230
+ reason=f'Unexpected error: {str(error)}'
231
+ )
232
+
233
+ async def detect_file_type(self, file_path: str) -> str:
234
+ """
235
+ Detect file type based on extension and MIME type
236
+
237
+ Args:
238
+ file_path: Path to the file
239
+
240
+ Returns:
241
+ File type: 'text', 'image', 'pdf', or 'binary'
242
+ """
243
+ file_extension = Path(file_path).suffix.lower()
244
+
245
+ # Check by extension first
246
+ if file_extension in TEXT_FILE_EXTENSIONS:
247
+ return 'text'
248
+ elif file_extension in IMAGE_FILE_EXTENSIONS:
249
+ return 'image'
250
+ elif file_extension in PDF_FILE_EXTENSIONS:
251
+ return 'pdf'
252
+
253
+ # Check MIME type as fallback
254
+ mime_type, _ = mimetypes.guess_type(file_path)
255
+ if mime_type:
256
+ if mime_type.startswith('text/'):
257
+ return 'text'
258
+ elif mime_type.startswith('image/'):
259
+ return 'image'
260
+ elif mime_type == 'application/pdf':
261
+ return 'pdf'
262
+
263
+ return 'binary'
264
+
265
+ def is_explicitly_requested(self, file_path: str, input_patterns: List[str]) -> bool:
266
+ """
267
+ Check if image/PDF file is explicitly requested
268
+
269
+ Args:
270
+ file_path: Path to the file
271
+ input_patterns: Original input patterns
272
+
273
+ Returns:
274
+ True if file is explicitly requested, False otherwise
275
+ """
276
+ file_extension = Path(file_path).suffix.lower()
277
+ file_name = Path(file_path).name.lower()
278
+
279
+ for pattern in input_patterns:
280
+ pattern_lower = pattern.lower()
281
+
282
+ # Check if extension is mentioned in pattern
283
+ if file_extension in pattern_lower:
284
+ return True
285
+
286
+ # Check if filename is mentioned in pattern
287
+ if file_name in pattern_lower:
288
+ return True
289
+
290
+ return False
291
+
292
+ async def read_file_content_by_type(self, file_path: str, file_type: str) -> Dict[str, Any]:
293
+ """
294
+ Read file content based on file type
295
+
296
+ Args:
297
+ file_path: Path to the file
298
+ file_type: Type of the file
299
+
300
+ Returns:
301
+ Dictionary with content and error information
302
+ """
303
+ try:
304
+ if file_type == 'text':
305
+ return await self.read_text_file(file_path)
306
+ elif file_type == 'image':
307
+ return await self.read_image_file(file_path)
308
+ elif file_type == 'pdf':
309
+ return await self.read_pdf_file(file_path)
310
+ else:
311
+ return {'content': None, 'error': f'Unsupported file type: {file_type}'}
312
+
313
+ except Exception as error:
314
+ return {'content': None, 'error': str(error)}
315
+
316
+ async def read_text_file(self, file_path: str) -> Dict[str, Any]:
317
+ """
318
+ Read text file with encoding detection and content truncation
319
+
320
+ Args:
321
+ file_path: Path to the text file
322
+
323
+ Returns:
324
+ Dictionary with content, error, and encoding information
325
+ """
326
+ loop = asyncio.get_event_loop()
327
+
328
+ def read_sync():
329
+ encodings_to_try = ['utf-8', 'utf-16', 'latin-1', 'cp1252']
330
+
331
+ for encoding in encodings_to_try:
332
+ try:
333
+ with open(file_path, 'r', encoding=encoding) as f:
334
+ content = f.read()
335
+
336
+ # Limit line count
337
+ lines = content.split('\n')
338
+ if len(lines) > self.MAX_TEXT_LINES:
339
+ content = '\n'.join(lines[:self.MAX_TEXT_LINES])
340
+ content += f"\n\n[Content truncated: showing first {self.MAX_TEXT_LINES} of {len(lines)} lines]"
341
+
342
+ # Limit total character count
343
+ if len(content) > self.MAX_CONTENT_CHARS:
344
+ content = content[:self.MAX_CONTENT_CHARS]
345
+ content += "\n\n[Content truncated due to size limit]"
346
+
347
+ return {'content': content, 'error': None, 'encoding': encoding}
348
+
349
+ except UnicodeDecodeError:
350
+ continue
351
+ except Exception as error:
352
+ return {'content': None, 'error': str(error)}
353
+
354
+ return {'content': None, 'error': 'Unable to decode file with any supported encoding'}
355
+
356
+ return await loop.run_in_executor(None, read_sync)
357
+
358
+ async def read_image_file(self, file_path: str) -> Dict[str, Any]:
359
+ """
360
+ Read image file and return Part object for LLM processing
361
+
362
+ Args:
363
+ file_path: Path to the image file
364
+
365
+ Returns:
366
+ Dictionary with Part object content or error
367
+ """
368
+ try:
369
+ loop = asyncio.get_event_loop()
370
+
371
+ def read_sync():
372
+ with open(file_path, 'rb') as f:
373
+ return f.read()
374
+
375
+ binary_data = await loop.run_in_executor(None, read_sync)
376
+
377
+ # Detect MIME type
378
+ mime_type = self.get_mime_type(file_path)
379
+
380
+ # Create Part object (format compatible with LLM APIs)
381
+ part_object = {
382
+ 'type': 'image',
383
+ 'mime_type': mime_type,
384
+ 'data': base64.b64encode(binary_data).decode('utf-8'),
385
+ 'source': 'file',
386
+ 'file_path': file_path
387
+ }
388
+
389
+ return {'content': part_object, 'error': None}
390
+
391
+ except Exception as error:
392
+ return {'content': None, 'error': str(error)}
393
+
394
+ async def read_pdf_file(self, file_path: str) -> Dict[str, Any]:
395
+ """
396
+ Read PDF file and return Part object for LLM processing
397
+
398
+ Args:
399
+ file_path: Path to the PDF file
400
+
401
+ Returns:
402
+ Dictionary with Part object content or error
403
+ """
404
+ try:
405
+ loop = asyncio.get_event_loop()
406
+
407
+ def read_sync():
408
+ with open(file_path, 'rb') as f:
409
+ return f.read()
410
+
411
+ binary_data = await loop.run_in_executor(None, read_sync)
412
+
413
+ # Create Part object for PDF
414
+ part_object = {
415
+ 'type': 'pdf',
416
+ 'mime_type': 'application/pdf',
417
+ 'data': base64.b64encode(binary_data).decode('utf-8'),
418
+ 'source': 'file',
419
+ 'file_path': file_path
420
+ }
421
+
422
+ return {'content': part_object, 'error': None}
423
+
424
+ except Exception as error:
425
+ return {'content': None, 'error': str(error)}
426
+
427
+ def get_mime_type(self, file_path: str) -> str:
428
+ """
429
+ Get MIME type for file
430
+
431
+ Args:
432
+ file_path: Path to the file
433
+
434
+ Returns:
435
+ MIME type string
436
+ """
437
+ mime_type, _ = mimetypes.guess_type(file_path)
438
+ if mime_type:
439
+ return mime_type
440
+
441
+ # Fallback based on extension
442
+ extension = Path(file_path).suffix.lower()
443
+ mime_map = {
444
+ '.png': 'image/png',
445
+ '.jpg': 'image/jpeg',
446
+ '.jpeg': 'image/jpeg',
447
+ '.gif': 'image/gif',
448
+ '.bmp': 'image/bmp',
449
+ '.webp': 'image/webp',
450
+ '.svg': 'image/svg+xml',
451
+ '.pdf': 'application/pdf'
452
+ }
453
+
454
+ return mime_map.get(extension, 'application/octet-stream')
455
+
456
+ def format_file_content(self, file_path: str, content: Any) -> Any:
457
+ """
458
+ Format file content for output
459
+
460
+ Args:
461
+ file_path: Path to the file
462
+ content: File content
463
+
464
+ Returns:
465
+ Formatted content
466
+ """
467
+ if isinstance(content, str):
468
+ # Text file: add standard separator
469
+ separator = self.DEFAULT_OUTPUT_SEPARATOR_FORMAT.format(filePath=file_path)
470
+ return f"{separator}\n\n{content}\n\n"
471
+ else:
472
+ # Non-text file (image, PDF): return Part object directly
473
+ return content
474
+
475
+ def get_stats(self) -> ProcessingStats:
476
+ """
477
+ Get processing statistics
478
+
479
+ Returns:
480
+ ProcessingStats object
481
+ """
482
+ return self.stats
483
+
484
+ def reset_stats(self):
485
+ """Reset processing statistics"""
486
+ self.stats = ProcessingStats()