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.
- agent_config.yaml +34 -0
- siada/__init__.py +13 -0
- siada/agent_hub/__init__.py +10 -0
- siada/agent_hub/coder/bug_fix_agent.py +398 -0
- siada/agent_hub/coder/code_gen_agent.py +235 -0
- siada/agent_hub/coder/fe_gen_agent.py +37 -0
- siada/agent_hub/coder/issue_review_agent.py +68 -0
- siada/agent_hub/coder/prompt/__init__.py +0 -0
- siada/agent_hub/coder/prompt/base/__init__.py +0 -0
- siada/agent_hub/coder/prompt/base/capabilities.py +18 -0
- siada/agent_hub/coder/prompt/base/non_interactive.py +14 -0
- siada/agent_hub/coder/prompt/base/prompt_builder.py +33 -0
- siada/agent_hub/coder/prompt/base/rules.py +40 -0
- siada/agent_hub/coder/prompt/base/tool_use.py +14 -0
- siada/agent_hub/coder/prompt/bug_prompt/bug_fix_prompt.py +51 -0
- siada/agent_hub/coder/prompt/bug_prompt/rules.py +53 -0
- siada/agent_hub/coder/prompt/code_gen_prompt.py +45 -0
- siada/agent_hub/coder/prompt/fe_gen_prompt.py +45 -0
- siada/agent_hub/coder/prompt/issue_review_prompt.py +121 -0
- siada/agent_hub/coder/tracing/__init__.py +22 -0
- siada/agent_hub/coder/tracing/logger_tracing_processor.py +458 -0
- siada/agent_hub/hooks/__init__.py +0 -0
- siada/agent_hub/hooks/io_hooks.py +30 -0
- siada/agent_hub/siada_agent.py +214 -0
- siada/config/__init__.py +0 -0
- siada/config/config_loader.py +67 -0
- siada/entrypoint/__init__.py +3 -0
- siada/entrypoint/args_parser/__init__.py +0 -0
- siada/entrypoint/args_parser/args.py +195 -0
- siada/entrypoint/interaction/__init__.py +0 -0
- siada/entrypoint/interaction/base_turn.py +107 -0
- siada/entrypoint/interaction/config.py +30 -0
- siada/entrypoint/interaction/controller.py +122 -0
- siada/entrypoint/interaction/nointeractive_controller.py +27 -0
- siada/entrypoint/interaction/turn/interface.py +116 -0
- siada/entrypoint/interaction/turn/models.py +34 -0
- siada/entrypoint/interaction/turn/run_turn.py +641 -0
- siada/entrypoint/interaction/turn_factory.py +35 -0
- siada/entrypoint/interaction/turn_models.py +28 -0
- siada/entrypoint/siadahub.py +413 -0
- siada/foundation/__init__.py +0 -0
- siada/foundation/code_agent_context.py +48 -0
- siada/foundation/config.py +59 -0
- siada/foundation/constants.py +2 -0
- siada/foundation/context.py +76 -0
- siada/foundation/logging.py +341 -0
- siada/foundation/tools/__init__.py +0 -0
- siada/foundation/tools/get_git_diff.py +105 -0
- siada/io/__init__.py +0 -0
- siada/io/banner.py +148 -0
- siada/io/color_settings.py +207 -0
- siada/io/color_utils.py +16 -0
- siada/io/components/__init__.py +0 -0
- siada/io/components/mdstream.py +204 -0
- siada/io/console_printer.py +68 -0
- siada/io/io.py +661 -0
- siada/io/key_bindings.py +129 -0
- siada/io/notification_command.py +40 -0
- siada/io/test_banner.py +64 -0
- siada/models/__init__.py +0 -0
- siada/models/agent.py +47 -0
- siada/models/coder/__init__.py +0 -0
- siada/models/converter.py +27 -0
- siada/models/model_base_config.py +121 -0
- siada/models/model_run_config.py +153 -0
- siada/provider/__init__.py +0 -0
- siada/provider/client_factory.py +112 -0
- siada/provider/llm_client.py +11 -0
- siada/provider/openrouter/__init__.py +0 -0
- siada/provider/openrouter/coverter.py +17 -0
- siada/provider/openrouter/openroute_provider.py +33 -0
- siada/provider/provider_factory.py +53 -0
- siada/queries/tree-sitter-language-pack/README.md +7 -0
- siada/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
- siada/queries/tree-sitter-language-pack/c-tags.scm +9 -0
- siada/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
- siada/queries/tree-sitter-language-pack/commonlisp-tags.scm +122 -0
- siada/queries/tree-sitter-language-pack/cpp-tags.scm +15 -0
- siada/queries/tree-sitter-language-pack/csharp-tags.scm +26 -0
- siada/queries/tree-sitter-language-pack/d-tags.scm +26 -0
- siada/queries/tree-sitter-language-pack/dart-tags.scm +92 -0
- siada/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
- siada/queries/tree-sitter-language-pack/elixir-tags.scm +54 -0
- siada/queries/tree-sitter-language-pack/elm-tags.scm +19 -0
- siada/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
- siada/queries/tree-sitter-language-pack/go-tags.scm +42 -0
- siada/queries/tree-sitter-language-pack/java-tags.scm +20 -0
- siada/queries/tree-sitter-language-pack/javascript-tags.scm +88 -0
- siada/queries/tree-sitter-language-pack/lua-tags.scm +34 -0
- siada/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
- siada/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +98 -0
- siada/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
- siada/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
- siada/queries/tree-sitter-language-pack/python-tags.scm +14 -0
- siada/queries/tree-sitter-language-pack/r-tags.scm +21 -0
- siada/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
- siada/queries/tree-sitter-language-pack/ruby-tags.scm +64 -0
- siada/queries/tree-sitter-language-pack/rust-tags.scm +60 -0
- siada/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
- siada/queries/tree-sitter-language-pack/swift-tags.scm +51 -0
- siada/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
- siada/queries/tree-sitter-languages/README.md +23 -0
- siada/queries/tree-sitter-languages/c-tags.scm +9 -0
- siada/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
- siada/queries/tree-sitter-languages/cpp-tags.scm +15 -0
- siada/queries/tree-sitter-languages/dart-tags.scm +91 -0
- siada/queries/tree-sitter-languages/elisp-tags.scm +8 -0
- siada/queries/tree-sitter-languages/elixir-tags.scm +54 -0
- siada/queries/tree-sitter-languages/elm-tags.scm +19 -0
- siada/queries/tree-sitter-languages/go-tags.scm +30 -0
- siada/queries/tree-sitter-languages/hcl-tags.scm +77 -0
- siada/queries/tree-sitter-languages/java-tags.scm +20 -0
- siada/queries/tree-sitter-languages/javascript-tags.scm +88 -0
- siada/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
- siada/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
- siada/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
- siada/queries/tree-sitter-languages/php-tags.scm +26 -0
- siada/queries/tree-sitter-languages/python-tags.scm +12 -0
- siada/queries/tree-sitter-languages/ql-tags.scm +26 -0
- siada/queries/tree-sitter-languages/ruby-tags.scm +64 -0
- siada/queries/tree-sitter-languages/rust-tags.scm +60 -0
- siada/queries/tree-sitter-languages/scala-tags.scm +65 -0
- siada/queries/tree-sitter-languages/typescript-tags.scm +41 -0
- siada/services/__init__.py +30 -0
- siada/services/at_file_recommendation_design_document.md +1213 -0
- siada/services/code_context_manager.py +136 -0
- siada/services/enhanced_fix_result_check.py +546 -0
- siada/services/execution_trace_collector.py +207 -0
- siada/services/file_recommendation/__init__.py +136 -0
- siada/services/file_recommendation/core/__init__.py +17 -0
- siada/services/file_recommendation/core/completion_engine.py +351 -0
- siada/services/file_recommendation/core/config.py +62 -0
- siada/services/file_recommendation/core/file_discovery.py +353 -0
- siada/services/file_recommendation/core/suggestion.py +110 -0
- siada/services/file_recommendation/utils/__init__.py +22 -0
- siada/services/file_recommendation/utils/path_utils.py +138 -0
- siada/services/file_recommendation/utils/text_utils.py +253 -0
- siada/services/fix_result_check.py +377 -0
- siada/services/handle_at_command/README.md +247 -0
- siada/services/handle_at_command/__init__.py +115 -0
- siada/services/handle_at_command/exceptions.py +46 -0
- siada/services/handle_at_command/models.py +92 -0
- siada/services/handle_at_command/parser.py +189 -0
- siada/services/handle_at_command/processor.py +321 -0
- siada/services/handle_at_command/resolver.py +318 -0
- siada/services/handle_at_command/utils.py +345 -0
- siada/services/model_info_service.py +103 -0
- siada/services/siada_runner.py +170 -0
- siada/session/__init__.py +29 -0
- siada/session/session_manager.py +66 -0
- siada/session/session_models.py +43 -0
- siada/support/__init__.py +0 -0
- siada/support/completer.py +204 -0
- siada/support/editor.py +146 -0
- siada/support/envprocessor.py +54 -0
- siada/support/repo.py +51 -0
- siada/support/slash_commands.py +410 -0
- siada/support/spinner.py +320 -0
- siada/tools/__init__.py +0 -0
- siada/tools/ast/README.md +345 -0
- siada/tools/ast/ast_tool.py +341 -0
- siada/tools/ast/models.py +11 -0
- siada/tools/coder/__init__.py +0 -0
- siada/tools/coder/ask_followup_question.py +70 -0
- siada/tools/coder/cmd_runner.py +132 -0
- siada/tools/coder/file_operator.py +284 -0
- siada/tools/coder/file_search/README.md +223 -0
- siada/tools/coder/file_search/__init__.py +10 -0
- siada/tools/coder/file_search/bin/rg-linux-arm64 +0 -0
- siada/tools/coder/file_search/bin/rg-linux-x64 +0 -0
- siada/tools/coder/file_search/bin/rg-macos-arm64 +0 -0
- siada/tools/coder/file_search/bin/rg-macos-x64 +0 -0
- siada/tools/coder/file_search/bin/rg.exe +0 -0
- siada/tools/coder/file_search/search.py +437 -0
- siada/tools/coder/file_search/setup_package.py +150 -0
- siada/tools/coder/file_search/test_search.py +142 -0
- siada/tools/coder/files.py +147 -0
- siada/tools/coder/fix_attempt_completion.py +58 -0
- siada/tools/coder/issue_review_completion.py +29 -0
- siada/tools/coder/observation/__init__.py +0 -0
- siada/tools/coder/observation/error.py +22 -0
- siada/tools/coder/observation/file_observation.py +216 -0
- siada/tools/coder/observation/observation.py +71 -0
- siada/tools/coder/repo_map/__init__.py +157 -0
- siada/tools/coder/repo_map/dump.py +29 -0
- siada/tools/coder/repo_map/io.py +226 -0
- siada/tools/coder/repo_map/repo_map.py +881 -0
- siada/tools/coder/repo_map/special.py +203 -0
- siada/tools/coder/repo_map/token_counter.py +154 -0
- siada/tools/coder/repo_map/waiting.py +221 -0
- siada/tools/coder/reproduce_completion.py +39 -0
- siada/tools/coder/run_cmd.py +60 -0
- siada/tools/coder/test_completion.py +25 -0
- siada/tools/coder/tool_docs.py +44 -0
- siada/tools/compression_tool.py +200 -0
- siada/tools/read_many_files/README.md +220 -0
- siada/tools/read_many_files/__init__.py +25 -0
- siada/tools/read_many_files/file_processor.py +486 -0
- siada/tools/read_many_files/filters.py +326 -0
- siada/tools/read_many_files/formatters.py +204 -0
- siada/tools/read_many_files/models.py +189 -0
- siada/tools/read_many_files_tool.py +233 -0
- siada/tools/tool_call_format/__init__.py +55 -0
- siada/tools/tool_call_format/file_to_language.py +231 -0
- siada/tools/tool_call_format/formatter_factory.py +80 -0
- siada/tools/tool_call_format/formatters.py +282 -0
- siada/tools/tool_call_format/tool_call_formatter.py +54 -0
- siada/tools/web/__init__.py +0 -0
- siada/tools/web/web_crawl.py +208 -0
- siada/utils.py +194 -0
- siada_cli-0.0.1.dist-info/LICENSE +201 -0
- siada_cli-0.0.1.dist-info/METADATA +457 -0
- siada_cli-0.0.1.dist-info/RECORD +215 -0
- siada_cli-0.0.1.dist-info/WHEEL +4 -0
- 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()
|