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,247 @@
|
|
|
1
|
+
# HandleAtCommand Service
|
|
2
|
+
|
|
3
|
+
A comprehensive service for processing @ commands in user queries, enabling intelligent file content injection into AI context.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The HandleAtCommand service allows users to reference files using simple `@` syntax in their queries. The service automatically:
|
|
8
|
+
|
|
9
|
+
1. **Parses** user input to extract @ commands
|
|
10
|
+
2. **Resolves** file paths using intelligent matching
|
|
11
|
+
3. **Reads** file contents using the existing ReadManyFiles tool
|
|
12
|
+
4. **Injects** content into the AI context seamlessly
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
### 🎯 Core Functionality
|
|
17
|
+
- **Smart Path Resolution**: Direct matching, fuzzy search, directory expansion
|
|
18
|
+
- **Multiple File Support**: Handle multiple @ commands in a single query
|
|
19
|
+
- **Content Injection**: Automatic file content integration into AI context
|
|
20
|
+
- **Error Handling**: Graceful handling of missing or inaccessible files
|
|
21
|
+
|
|
22
|
+
### 🔍 Supported @ Command Formats
|
|
23
|
+
- `@filename.ext` - Single file reference
|
|
24
|
+
- `@path/to/file.ext` - Relative path file reference
|
|
25
|
+
- `@directory/` - Directory reference (converted to glob pattern)
|
|
26
|
+
- `@my\ file.txt` - Files with spaces (escaped)
|
|
27
|
+
- `请解释 @file1.js 和 @file2.py` - Mixed text and file references
|
|
28
|
+
|
|
29
|
+
### 🛡️ Security Features
|
|
30
|
+
- **Workspace Boundary Checking**: Ensures files are within allowed directories
|
|
31
|
+
- **Path Traversal Protection**: Prevents `../` attacks
|
|
32
|
+
- **Git Ignore Support**: Respects .gitignore rules
|
|
33
|
+
- **File Size Limits**: Prevents reading of excessively large files
|
|
34
|
+
|
|
35
|
+
## Architecture
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
HandleAtCommand/
|
|
39
|
+
├── processor.py # Main coordinator (AtCommandProcessor)
|
|
40
|
+
├── parser.py # @ command parsing (AtCommandParser)
|
|
41
|
+
├── resolver.py # Path resolution (PathResolver)
|
|
42
|
+
├── models.py # Data structures
|
|
43
|
+
├── exceptions.py # Custom exceptions
|
|
44
|
+
├── utils.py # Utility functions
|
|
45
|
+
└── __init__.py # Package interface
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Component Responsibilities
|
|
49
|
+
|
|
50
|
+
#### AtCommandProcessor
|
|
51
|
+
- **Main Entry Point**: Coordinates all components
|
|
52
|
+
- **File Reading**: Integrates with ReadManyFilesTool
|
|
53
|
+
- **Query Reconstruction**: Builds final processed query
|
|
54
|
+
- **Statistics Tracking**: Monitors processing metrics
|
|
55
|
+
|
|
56
|
+
#### AtCommandParser
|
|
57
|
+
- **Input Parsing**: Extracts @ commands from user queries
|
|
58
|
+
- **Escape Handling**: Processes escaped characters in paths
|
|
59
|
+
- **Content Extraction**: Parses file content from tool output
|
|
60
|
+
|
|
61
|
+
#### PathResolver
|
|
62
|
+
- **Path Resolution**: Converts @ paths to actual file paths
|
|
63
|
+
- **Security Validation**: Ensures paths are safe and within workspace
|
|
64
|
+
- **Fuzzy Matching**: Provides glob-based search for partial matches
|
|
65
|
+
- **Directory Handling**: Converts directories to appropriate glob patterns
|
|
66
|
+
|
|
67
|
+
## Usage
|
|
68
|
+
|
|
69
|
+
### Basic Usage
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from siada.services.handle_at_command import AtCommandProcessor, HandleAtCommandParams
|
|
73
|
+
|
|
74
|
+
# Initialize processor
|
|
75
|
+
processor = AtCommandProcessor(config)
|
|
76
|
+
|
|
77
|
+
# Create parameters
|
|
78
|
+
params = HandleAtCommandParams(
|
|
79
|
+
query="Please explain @main.py and @config.json",
|
|
80
|
+
config=config,
|
|
81
|
+
add_item=add_item_function,
|
|
82
|
+
on_debug_message=debug_function,
|
|
83
|
+
message_id=123
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Process @ commands
|
|
87
|
+
result = await processor.handle_at_command(params)
|
|
88
|
+
|
|
89
|
+
if result.should_proceed:
|
|
90
|
+
# Use result.processed_query for AI context
|
|
91
|
+
print("Processed query:", result.processed_query)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Convenience Function
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from siada.services.handle_at_command import handle_at_command
|
|
98
|
+
|
|
99
|
+
result = await handle_at_command(
|
|
100
|
+
query="Show me @file.py",
|
|
101
|
+
config=config,
|
|
102
|
+
add_item=add_item_function,
|
|
103
|
+
on_debug_message=debug_function,
|
|
104
|
+
message_id=123
|
|
105
|
+
)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Configuration
|
|
109
|
+
|
|
110
|
+
### Required Configuration Properties
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
class Config:
|
|
114
|
+
root_dir: str # Root directory for file operations
|
|
115
|
+
|
|
116
|
+
# Optional: Additional workspace directories
|
|
117
|
+
# Optional: File filtering options
|
|
118
|
+
# Optional: Enable recursive search
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### File Filtering Options
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
file_filtering_options = {
|
|
125
|
+
'respect_git_ignore': True, # Respect .gitignore rules
|
|
126
|
+
# Add more filtering options as needed
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Examples
|
|
131
|
+
|
|
132
|
+
### Single File Reference
|
|
133
|
+
```
|
|
134
|
+
Input: "Explain @main.py"
|
|
135
|
+
Output: Query with main.py content injected
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Multiple Files
|
|
139
|
+
```
|
|
140
|
+
Input: "Compare @file1.py and @file2.py"
|
|
141
|
+
Output: Query with both files' content injected
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Directory Reference
|
|
145
|
+
```
|
|
146
|
+
Input: "Show me @src/"
|
|
147
|
+
Output: Query with all files in src/ directory
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Mixed Content
|
|
151
|
+
```
|
|
152
|
+
Input: "The function in @utils.py needs to handle @config.json"
|
|
153
|
+
Output: Query with both files' content injected
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Fuzzy Matching
|
|
157
|
+
```
|
|
158
|
+
Input: "Check @main"
|
|
159
|
+
Output: Finds and includes main.py (if it exists)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Error Handling
|
|
163
|
+
|
|
164
|
+
The service handles various error conditions gracefully:
|
|
165
|
+
|
|
166
|
+
- **File Not Found**: Attempts fuzzy search, reports if not found
|
|
167
|
+
- **Permission Denied**: Skips file, continues with others
|
|
168
|
+
- **Path Outside Workspace**: Security violation, file skipped
|
|
169
|
+
- **File Too Large**: Respects size limits, skips oversized files
|
|
170
|
+
|
|
171
|
+
## Testing
|
|
172
|
+
|
|
173
|
+
### Running Tests
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
# Run parser tests
|
|
177
|
+
cd tests/services/handle_at_command
|
|
178
|
+
python test_parser.py
|
|
179
|
+
|
|
180
|
+
# Run demo
|
|
181
|
+
python demo_handle_at_command.py
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Test Coverage
|
|
185
|
+
|
|
186
|
+
- ✅ @ Command parsing (various formats)
|
|
187
|
+
- ✅ Path resolution (direct, fuzzy, directory)
|
|
188
|
+
- ✅ File content injection
|
|
189
|
+
- ✅ Error handling
|
|
190
|
+
- ✅ Security validation
|
|
191
|
+
- ✅ Integration with ReadManyFilesTool
|
|
192
|
+
|
|
193
|
+
## Performance Considerations
|
|
194
|
+
|
|
195
|
+
### Optimization Features
|
|
196
|
+
- **Concurrent Processing**: Multiple files processed in parallel
|
|
197
|
+
- **Intelligent Caching**: Avoids re-reading unchanged files
|
|
198
|
+
- **Content Limits**: Respects file size and line count limits
|
|
199
|
+
- **Early Exit**: Skips processing if no @ commands found
|
|
200
|
+
|
|
201
|
+
### Performance Metrics
|
|
202
|
+
- Processing time tracking
|
|
203
|
+
- File read statistics
|
|
204
|
+
- Path resolution success rates
|
|
205
|
+
- Error categorization
|
|
206
|
+
|
|
207
|
+
## Integration Points
|
|
208
|
+
|
|
209
|
+
### Dependencies
|
|
210
|
+
- **ReadManyFilesTool**: For actual file reading
|
|
211
|
+
- **FileFilter**: For .gitignore and filtering support
|
|
212
|
+
- **PathLib**: For cross-platform path handling
|
|
213
|
+
|
|
214
|
+
### Extension Points
|
|
215
|
+
- Custom path resolvers
|
|
216
|
+
- Additional file type support
|
|
217
|
+
- Enhanced security policies
|
|
218
|
+
- Custom content formatters
|
|
219
|
+
|
|
220
|
+
## Future Enhancements
|
|
221
|
+
|
|
222
|
+
### Planned Features
|
|
223
|
+
- **Caching Layer**: Persistent file content caching
|
|
224
|
+
- **Advanced Filtering**: More sophisticated ignore rules
|
|
225
|
+
- **Content Preprocessing**: Syntax highlighting, formatting
|
|
226
|
+
- **Batch Operations**: Optimized multi-file processing
|
|
227
|
+
- **Plugin System**: Extensible resolver and formatter plugins
|
|
228
|
+
|
|
229
|
+
### Potential Improvements
|
|
230
|
+
- **AI-Powered Matching**: Semantic file matching
|
|
231
|
+
- **Context-Aware Resolution**: Smart path suggestions
|
|
232
|
+
- **Real-time Updates**: File change monitoring
|
|
233
|
+
- **Performance Analytics**: Detailed performance insights
|
|
234
|
+
|
|
235
|
+
## Contributing
|
|
236
|
+
|
|
237
|
+
When contributing to this service:
|
|
238
|
+
|
|
239
|
+
1. **Follow Architecture**: Maintain separation of concerns
|
|
240
|
+
2. **Add Tests**: Include unit tests for new functionality
|
|
241
|
+
3. **Update Documentation**: Keep README and docstrings current
|
|
242
|
+
4. **Security First**: Consider security implications of changes
|
|
243
|
+
5. **Performance Aware**: Monitor impact on processing speed
|
|
244
|
+
|
|
245
|
+
## License
|
|
246
|
+
|
|
247
|
+
This service is part of the Siada project and follows the project's licensing terms.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HandleAtCommand - A service for processing @ commands in user queries.
|
|
3
|
+
|
|
4
|
+
This package provides functionality to parse user input containing @ commands,
|
|
5
|
+
resolve file paths, read file contents, and inject them into AI context.
|
|
6
|
+
|
|
7
|
+
Main Components:
|
|
8
|
+
- AtCommandProcessor: Main processor that coordinates all components
|
|
9
|
+
- AtCommandParser: Parses @ commands from user input
|
|
10
|
+
- PathResolver: Resolves @ paths to actual file paths
|
|
11
|
+
- Models: Data structures and type definitions
|
|
12
|
+
- Exceptions: Custom exception classes
|
|
13
|
+
- Utils: Utility functions
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
from siada.services.handle_at_command import AtCommandProcessor, HandleAtCommandParams
|
|
17
|
+
|
|
18
|
+
processor = AtCommandProcessor(config)
|
|
19
|
+
params = HandleAtCommandParams(
|
|
20
|
+
query="Please explain @file.py",
|
|
21
|
+
config=config,
|
|
22
|
+
add_item=add_item_func,
|
|
23
|
+
on_debug_message=debug_func,
|
|
24
|
+
message_id=123
|
|
25
|
+
)
|
|
26
|
+
result = await processor.handle_at_command(params)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from .processor import AtCommandProcessor
|
|
30
|
+
from .parser import AtCommandParser
|
|
31
|
+
from .resolver import PathResolver
|
|
32
|
+
from .models import (
|
|
33
|
+
AtCommandPart,
|
|
34
|
+
HandleAtCommandParams,
|
|
35
|
+
HandleAtCommandResult,
|
|
36
|
+
ResolverContext,
|
|
37
|
+
PathResolutionResult,
|
|
38
|
+
ProcessingStats,
|
|
39
|
+
IgnoredFileStats
|
|
40
|
+
)
|
|
41
|
+
from .exceptions import (
|
|
42
|
+
AtCommandError,
|
|
43
|
+
PathNotFoundError,
|
|
44
|
+
PathIgnoredError,
|
|
45
|
+
FileTooLargeError,
|
|
46
|
+
PermissionDeniedError,
|
|
47
|
+
SecurityViolationError,
|
|
48
|
+
InvalidPathError,
|
|
49
|
+
WorkspaceSecurityError
|
|
50
|
+
)
|
|
51
|
+
from . import utils
|
|
52
|
+
|
|
53
|
+
__all__ = [
|
|
54
|
+
# Main classes
|
|
55
|
+
'AtCommandProcessor',
|
|
56
|
+
'AtCommandParser',
|
|
57
|
+
'PathResolver',
|
|
58
|
+
|
|
59
|
+
# Data models
|
|
60
|
+
'AtCommandPart',
|
|
61
|
+
'HandleAtCommandParams',
|
|
62
|
+
'HandleAtCommandResult',
|
|
63
|
+
'ResolverContext',
|
|
64
|
+
'PathResolutionResult',
|
|
65
|
+
'ProcessingStats',
|
|
66
|
+
'IgnoredFileStats',
|
|
67
|
+
|
|
68
|
+
# Exceptions
|
|
69
|
+
'AtCommandError',
|
|
70
|
+
'PathNotFoundError',
|
|
71
|
+
'PathIgnoredError',
|
|
72
|
+
'FileTooLargeError',
|
|
73
|
+
'PermissionDeniedError',
|
|
74
|
+
'SecurityViolationError',
|
|
75
|
+
'InvalidPathError',
|
|
76
|
+
'WorkspaceSecurityError',
|
|
77
|
+
|
|
78
|
+
# Utilities
|
|
79
|
+
'utils'
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
# Version information
|
|
83
|
+
__version__ = '1.0.0'
|
|
84
|
+
__author__ = 'Siada Team'
|
|
85
|
+
__description__ = 'A service for processing @ commands in user queries'
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# Convenience function for easy usage
|
|
89
|
+
async def handle_at_command(query: str, config, add_item: callable,
|
|
90
|
+
on_debug_message: callable, message_id: int,
|
|
91
|
+
signal=None) -> HandleAtCommandResult:
|
|
92
|
+
"""
|
|
93
|
+
Convenience function to handle @ commands
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
query: User input query
|
|
97
|
+
config: Configuration object
|
|
98
|
+
add_item: Function to add items to history
|
|
99
|
+
on_debug_message: Debug message callback
|
|
100
|
+
message_id: Message ID
|
|
101
|
+
signal: Cancellation signal (optional)
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
HandleAtCommandResult with processed query and status
|
|
105
|
+
"""
|
|
106
|
+
processor = AtCommandProcessor(config)
|
|
107
|
+
params = HandleAtCommandParams(
|
|
108
|
+
query=query,
|
|
109
|
+
config=config,
|
|
110
|
+
add_item=add_item,
|
|
111
|
+
on_debug_message=on_debug_message,
|
|
112
|
+
message_id=message_id,
|
|
113
|
+
signal=signal
|
|
114
|
+
)
|
|
115
|
+
return await processor.handle_at_command(params)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom exceptions for HandleAtCommand functionality.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AtCommandError(Exception):
|
|
7
|
+
"""Base class for @ command processing errors"""
|
|
8
|
+
|
|
9
|
+
def __init__(self, message: str, file_path: str = None):
|
|
10
|
+
super().__init__(message)
|
|
11
|
+
self.file_path = file_path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PathNotFoundError(AtCommandError):
|
|
15
|
+
"""Raised when a path cannot be found"""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PathIgnoredError(AtCommandError):
|
|
20
|
+
"""Raised when a path is ignored by filtering rules"""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class FileTooLargeError(AtCommandError):
|
|
25
|
+
"""Raised when a file exceeds size limits"""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PermissionDeniedError(AtCommandError):
|
|
30
|
+
"""Raised when access to a file is denied"""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SecurityViolationError(AtCommandError):
|
|
35
|
+
"""Raised when a security violation is detected (e.g., path traversal)"""
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class InvalidPathError(AtCommandError):
|
|
40
|
+
"""Raised when a path is invalid or malformed"""
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class WorkspaceSecurityError(AtCommandError):
|
|
45
|
+
"""Raised when a path is outside the workspace boundaries"""
|
|
46
|
+
pass
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HandleAtCommand data models and structures.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import List, Dict, Optional, Union, Any, Literal
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class AtCommandPart:
|
|
11
|
+
"""Represents a part of the parsed @ command query"""
|
|
12
|
+
type: Literal['text', 'atPath'] # Part type
|
|
13
|
+
content: str # Content
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class HandleAtCommandParams:
|
|
18
|
+
"""Input parameters for handleAtCommand function"""
|
|
19
|
+
query: str # User's original query
|
|
20
|
+
config: Any # Configuration object
|
|
21
|
+
add_item: callable # History addition function
|
|
22
|
+
on_debug_message: callable # Debug message callback
|
|
23
|
+
message_id: int # Message ID
|
|
24
|
+
signal: Optional[Any] = None # Cancellation signal
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class HandleAtCommandResult:
|
|
29
|
+
"""Output result from handleAtCommand function"""
|
|
30
|
+
processed_query: Optional[List[Dict]] # Processed query parts
|
|
31
|
+
should_proceed: bool # Whether to continue processing
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class ResolverContext:
|
|
36
|
+
"""Context for path resolution"""
|
|
37
|
+
workspace_directories: List[str]
|
|
38
|
+
target_directory: str
|
|
39
|
+
enable_recursive_search: bool = True
|
|
40
|
+
file_filtering_options: Optional[Dict[str, bool]] = None
|
|
41
|
+
|
|
42
|
+
def __post_init__(self):
|
|
43
|
+
if self.file_filtering_options is None:
|
|
44
|
+
self.file_filtering_options = {
|
|
45
|
+
'respect_git_ignore': True
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class PathResolutionResult:
|
|
51
|
+
"""Result of path resolution"""
|
|
52
|
+
resolved_path: Optional[str]
|
|
53
|
+
original_path: str
|
|
54
|
+
resolution_type: Literal['direct', 'glob', 'directory', 'not_found']
|
|
55
|
+
reason: Optional[str] = None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class ProcessingStats:
|
|
60
|
+
"""Statistics for @ command processing"""
|
|
61
|
+
total_at_commands: int = 0
|
|
62
|
+
resolved_paths: int = 0
|
|
63
|
+
failed_paths: int = 0
|
|
64
|
+
ignored_paths: int = 0
|
|
65
|
+
files_read: int = 0
|
|
66
|
+
processing_time: float = 0.0
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class IgnoredFileStats:
|
|
71
|
+
"""Statistics for ignored files"""
|
|
72
|
+
git_ignored: List[str]
|
|
73
|
+
gemini_ignored: List[str]
|
|
74
|
+
both_ignored: List[str]
|
|
75
|
+
|
|
76
|
+
def __init__(self):
|
|
77
|
+
self.git_ignored = []
|
|
78
|
+
self.gemini_ignored = []
|
|
79
|
+
self.both_ignored = []
|
|
80
|
+
|
|
81
|
+
def add_ignored_file(self, file_path: str, reason: Literal['git', 'gemini', 'both']):
|
|
82
|
+
"""Add a file to the ignored list"""
|
|
83
|
+
if reason == 'git':
|
|
84
|
+
self.git_ignored.append(file_path)
|
|
85
|
+
elif reason == 'gemini':
|
|
86
|
+
self.gemini_ignored.append(file_path)
|
|
87
|
+
elif reason == 'both':
|
|
88
|
+
self.both_ignored.append(file_path)
|
|
89
|
+
|
|
90
|
+
def get_total_ignored(self) -> int:
|
|
91
|
+
"""Get total number of ignored files"""
|
|
92
|
+
return len(self.git_ignored) + len(self.gemini_ignored) + len(self.both_ignored)
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@ Command Parser - Parses user input to extract @ commands.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import List
|
|
7
|
+
|
|
8
|
+
from .models import AtCommandPart
|
|
9
|
+
from .exceptions import InvalidPathError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AtCommandParser:
|
|
13
|
+
"""Parser for @ commands in user queries"""
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
# Regex pattern to match file content format from read_many_files
|
|
17
|
+
self.file_content_regex = re.compile(r'^--- (.*?) ---\n\n([\s\S]*?)\n\n$')
|
|
18
|
+
|
|
19
|
+
def parse_all_at_commands(self, query: str) -> List[AtCommandPart]:
|
|
20
|
+
"""
|
|
21
|
+
Parse all @ commands from the query string
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
query: User input query string
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
List of AtCommandPart objects representing parsed parts
|
|
28
|
+
"""
|
|
29
|
+
if not query:
|
|
30
|
+
return []
|
|
31
|
+
|
|
32
|
+
parts = []
|
|
33
|
+
current_index = 0
|
|
34
|
+
|
|
35
|
+
while current_index < len(query):
|
|
36
|
+
# Find next unescaped '@'
|
|
37
|
+
at_index = self._find_next_unescaped_at(query, current_index)
|
|
38
|
+
|
|
39
|
+
if at_index == -1:
|
|
40
|
+
# No more '@' symbols, add remaining text
|
|
41
|
+
if current_index < len(query):
|
|
42
|
+
remaining_text = query[current_index:]
|
|
43
|
+
if remaining_text.strip(): # Only add non-empty text
|
|
44
|
+
parts.append(AtCommandPart('text', remaining_text))
|
|
45
|
+
break
|
|
46
|
+
|
|
47
|
+
# Add text before '@' if any
|
|
48
|
+
if at_index > current_index:
|
|
49
|
+
text_before = query[current_index:at_index]
|
|
50
|
+
if text_before.strip(): # Only add non-empty text
|
|
51
|
+
parts.append(AtCommandPart('text', text_before))
|
|
52
|
+
|
|
53
|
+
# Parse '@path'
|
|
54
|
+
path_end_index = self._find_path_end(query, at_index + 1)
|
|
55
|
+
raw_at_path = query[at_index:path_end_index]
|
|
56
|
+
|
|
57
|
+
# Handle lone '@' symbol
|
|
58
|
+
if raw_at_path == '@':
|
|
59
|
+
parts.append(AtCommandPart('text', '@'))
|
|
60
|
+
else:
|
|
61
|
+
at_path = self._unescape_path(raw_at_path)
|
|
62
|
+
parts.append(AtCommandPart('atPath', at_path))
|
|
63
|
+
|
|
64
|
+
current_index = path_end_index
|
|
65
|
+
|
|
66
|
+
# Filter out empty text parts
|
|
67
|
+
return [p for p in parts if not (p.type == 'text' and not p.content.strip())]
|
|
68
|
+
|
|
69
|
+
def _find_next_unescaped_at(self, query: str, start_index: int) -> int:
|
|
70
|
+
"""
|
|
71
|
+
Find the next unescaped '@' symbol
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
query: Query string
|
|
75
|
+
start_index: Starting index for search
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Index of next unescaped '@', or -1 if not found
|
|
79
|
+
"""
|
|
80
|
+
index = start_index
|
|
81
|
+
while index < len(query):
|
|
82
|
+
if query[index] == '@':
|
|
83
|
+
# Check if it's escaped (preceded by backslash)
|
|
84
|
+
if index == 0 or query[index - 1] != '\\':
|
|
85
|
+
return index
|
|
86
|
+
index += 1
|
|
87
|
+
return -1
|
|
88
|
+
|
|
89
|
+
def _find_path_end(self, query: str, start_index: int) -> int:
|
|
90
|
+
"""
|
|
91
|
+
Find the end of the path, handling escape characters
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
query: Query string
|
|
95
|
+
start_index: Starting index (after '@')
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Index where the path ends
|
|
99
|
+
"""
|
|
100
|
+
index = start_index
|
|
101
|
+
in_escape = False
|
|
102
|
+
|
|
103
|
+
while index < len(query):
|
|
104
|
+
char = query[index]
|
|
105
|
+
|
|
106
|
+
if in_escape:
|
|
107
|
+
# Previous character was escape, skip this character
|
|
108
|
+
in_escape = False
|
|
109
|
+
elif char == '\\':
|
|
110
|
+
# This is an escape character
|
|
111
|
+
in_escape = True
|
|
112
|
+
elif char.isspace():
|
|
113
|
+
# Unescaped whitespace marks end of path
|
|
114
|
+
break
|
|
115
|
+
|
|
116
|
+
index += 1
|
|
117
|
+
|
|
118
|
+
return index
|
|
119
|
+
|
|
120
|
+
def _unescape_path(self, path: str) -> str:
|
|
121
|
+
"""
|
|
122
|
+
Process escape characters in the path
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
path: Raw path string with potential escape characters
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Unescaped path string
|
|
129
|
+
"""
|
|
130
|
+
if not path:
|
|
131
|
+
return path
|
|
132
|
+
|
|
133
|
+
# Handle escaped spaces and other characters
|
|
134
|
+
result = []
|
|
135
|
+
i = 0
|
|
136
|
+
while i < len(path):
|
|
137
|
+
if path[i] == '\\' and i + 1 < len(path):
|
|
138
|
+
# Escape sequence - add the next character literally
|
|
139
|
+
result.append(path[i + 1])
|
|
140
|
+
i += 2
|
|
141
|
+
else:
|
|
142
|
+
result.append(path[i])
|
|
143
|
+
i += 1
|
|
144
|
+
|
|
145
|
+
return ''.join(result)
|
|
146
|
+
|
|
147
|
+
def validate_at_path(self, at_path: str) -> bool:
|
|
148
|
+
"""
|
|
149
|
+
Validate that an @ path is well-formed
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
at_path: @ path string (including '@' prefix)
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
True if valid, False otherwise
|
|
156
|
+
"""
|
|
157
|
+
if not at_path or not at_path.startswith('@'):
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
path_part = at_path[1:] # Remove '@' prefix
|
|
161
|
+
|
|
162
|
+
# Empty path after '@' is invalid (except lone '@')
|
|
163
|
+
if not path_part and at_path != '@':
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
# Check for invalid characters
|
|
167
|
+
invalid_chars = ['<', '>', '|', '"', '*', '?']
|
|
168
|
+
for char in invalid_chars:
|
|
169
|
+
if char in path_part:
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
return True
|
|
173
|
+
|
|
174
|
+
def extract_file_content_info(self, content_part: str) -> tuple[str, str]:
|
|
175
|
+
"""
|
|
176
|
+
Extract file path and content from formatted file content
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
content_part: Formatted content from read_many_files tool
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Tuple of (file_path, content) or (None, content) if not matched
|
|
183
|
+
"""
|
|
184
|
+
match = self.file_content_regex.match(content_part)
|
|
185
|
+
if match:
|
|
186
|
+
file_path = match.group(1)
|
|
187
|
+
content = match.group(2).strip()
|
|
188
|
+
return file_path, content
|
|
189
|
+
return None, content_part
|