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,142 @@
1
+ """
2
+ Simple test script to verify the file_search functionality.
3
+ """
4
+
5
+ import os
6
+ import tempfile
7
+ from pathlib import Path
8
+ try:
9
+ from .search import regex_search_files, RipgrepSearcher
10
+ except ImportError:
11
+ from search import regex_search_files, RipgrepSearcher
12
+
13
+
14
+ def create_test_files():
15
+ """Create temporary test files for testing."""
16
+ test_dir = Path(tempfile.mkdtemp())
17
+
18
+ # Create test Python file
19
+ python_file = test_dir / "test.py"
20
+ python_file.write_text("""
21
+ def hello_world():
22
+ # TODO: Add proper greeting
23
+ print("Hello, World!")
24
+ return "success"
25
+
26
+ class TestClass:
27
+ def __init__(self):
28
+ # FIXME: Initialize properly
29
+ self.value = 42
30
+
31
+ def process_data(self, data):
32
+ # TODO: Implement data processing
33
+ return data * 2
34
+ """)
35
+
36
+ # Create test JavaScript file
37
+ js_file = test_dir / "test.js"
38
+ js_file.write_text("""
39
+ function greet(name) {
40
+ // TODO: Add validation
41
+ console.log(`Hello, ${name}!`);
42
+ }
43
+
44
+ const utils = {
45
+ // FIXME: Add error handling
46
+ multiply: (a, b) => a * b
47
+ };
48
+ """)
49
+
50
+ return test_dir
51
+
52
+
53
+ def test_basic_functionality():
54
+ """Test basic search functionality."""
55
+ print("Creating test files...")
56
+ test_dir = create_test_files()
57
+ searcher = RipgrepSearcher()
58
+ try:
59
+ print(f"Test directory: {test_dir}")
60
+
61
+ # Test 1: Search for TODO comments in Python files
62
+ print("\n=== Test 1: Search for TODO comments in Python files ===")
63
+ try:
64
+ results = searcher.search_in_files(
65
+ cwd=str(test_dir),
66
+ directory_path=str(test_dir),
67
+ regex=r"TODO:",
68
+ file_pattern="*.py"
69
+ )
70
+ print(results)
71
+ except RuntimeError as e:
72
+ print(f"Error: {e}")
73
+ print("Note: This error is expected if ripgrep binary is not installed.")
74
+
75
+ # Test 2: Search for function definitions
76
+ print("\n=== Test 2: Search for function definitions ===")
77
+ try:
78
+
79
+ results = searcher.search_in_files(
80
+ directory_path=str(test_dir),
81
+ regex=r"def\s+\w+\(|function\s+\w+\(",
82
+ file_pattern="*",
83
+ cwd=str(test_dir)
84
+ )
85
+ print(results)
86
+ except RuntimeError as e:
87
+ print(f"Error: {e}")
88
+ print("Note: This error is expected if ripgrep binary is not installed.")
89
+
90
+ # Test 3: Search for all TODO/FIXME comments
91
+ print("\n=== Test 3: Search for all TODO/FIXME comments ===")
92
+ try:
93
+ searcher = RipgrepSearcher()
94
+ results = searcher.search_in_files(
95
+ directory_path=str(test_dir),
96
+ regex=r"TODO:|FIXME:",
97
+ file_pattern="*",
98
+ cwd=str(test_dir)
99
+ )
100
+ print(results)
101
+ except RuntimeError as e:
102
+ print(f"Error: {e}")
103
+ print("Note: This error is expected if ripgrep binary is not installed.")
104
+
105
+ finally:
106
+ # Clean up test files
107
+ import shutil
108
+ shutil.rmtree(test_dir)
109
+ print(f"\nCleaned up test directory: {test_dir}")
110
+
111
+
112
+ def test_binary_detection():
113
+ """Test ripgrep binary detection."""
114
+ print("\n=== Binary Detection Test ===")
115
+ try:
116
+ searcher = RipgrepSearcher()
117
+ print(f"✓ Ripgrep binary found at: {searcher.rg_path}")
118
+ return True
119
+ except RuntimeError as e:
120
+ print(f"✗ {e}")
121
+ print("\nTo fix this:")
122
+ print("1. Download ripgrep from: https://github.com/BurntSushi/ripgrep/releases")
123
+ print("2. Place the binary in file_search/bin/ directory")
124
+ print("3. Rename according to platform (see README.md)")
125
+ print("4. Set execute permissions on Unix systems: chmod +x file_search/bin/rg-*")
126
+ return False
127
+
128
+
129
+ if __name__ == "__main__":
130
+ """
131
+ 运行测试用例需要先注释掉 search.py 中的 @function_tool注解
132
+ """
133
+ print("File Search - Test Script")
134
+ print("=" * 40)
135
+
136
+ # Test binary detection first
137
+ if test_binary_detection():
138
+ # If binary is found, run functionality tests
139
+ test_basic_functionality()
140
+ else:
141
+ print("\nSkipping functionality tests due to missing ripgrep binary.")
142
+ print("Please install ripgrep binary as described above and try again.")
@@ -0,0 +1,147 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ from siada.tools.coder.observation.error import ErrorObservation
5
+ from siada.tools.coder.observation.file_observation import FileReadObservation, FileWriteObservation
6
+ from siada.tools.coder.observation.observation import FunctionCallResult
7
+
8
+
9
+ def resolve_path(
10
+ file_path: str,
11
+ working_directory: str,
12
+ workspace_base: str,
13
+ workspace_mount_path_in_sandbox: str,
14
+ ) -> Path:
15
+ """Resolve a file path to a path on the host filesystem.
16
+
17
+ Args:
18
+ file_path: The path to resolve.
19
+ working_directory: The working directory of the agent.
20
+ workspace_mount_path_in_sandbox: The path to the workspace inside the sandbox.
21
+ workspace_base: The base path of the workspace on the host filesystem.
22
+
23
+ Returns:
24
+ The resolved path on the host filesystem.
25
+ """
26
+ path_in_sandbox = Path(file_path)
27
+
28
+ # Apply working directory
29
+ if not path_in_sandbox.is_absolute():
30
+ path_in_sandbox = Path(working_directory) / path_in_sandbox
31
+
32
+ # Sanitize the path with respect to the root of the full sandbox
33
+ # (deny any .. path traversal to parent directories of the sandbox)
34
+ abs_path_in_sandbox = path_in_sandbox.resolve()
35
+
36
+ # If the path is outside the workspace, deny it
37
+ if not abs_path_in_sandbox.is_relative_to(workspace_mount_path_in_sandbox):
38
+ raise PermissionError(f'File access not permitted: {file_path}')
39
+
40
+ # Get path relative to the root of the workspace inside the sandbox
41
+ path_in_workspace = abs_path_in_sandbox.relative_to(
42
+ Path(workspace_mount_path_in_sandbox)
43
+ )
44
+
45
+ # Get path relative to host
46
+ path_in_host_workspace = Path(workspace_base) / path_in_workspace
47
+
48
+ return path_in_host_workspace
49
+
50
+
51
+ def read_lines(all_lines: list[str], start: int = 0, end: int = -1) -> list[str]:
52
+ start = max(start, 0)
53
+ start = min(start, len(all_lines))
54
+ end = -1 if end == -1 else max(end, 0)
55
+ end = min(end, len(all_lines))
56
+ if end == -1:
57
+ if start == 0:
58
+ return all_lines
59
+ else:
60
+ return all_lines[start:]
61
+ else:
62
+ num_lines = len(all_lines)
63
+ begin = max(0, min(start, num_lines - 2))
64
+ end = -1 if end > num_lines else max(begin + 1, end)
65
+ return all_lines[begin:end]
66
+
67
+
68
+ async def read_file(
69
+ path: str,
70
+ workdir: str,
71
+ workspace_base: str,
72
+ workspace_mount_path_in_sandbox: str,
73
+ start: int = 0,
74
+ end: int = -1,
75
+ ) -> FunctionCallResult:
76
+ try:
77
+ whole_path = resolve_path(
78
+ path, workdir, workspace_base, workspace_mount_path_in_sandbox
79
+ )
80
+ except PermissionError:
81
+ return ErrorObservation(
82
+ f"You're not allowed to access this path: {path}. You can only access paths inside the workspace."
83
+ )
84
+
85
+ try:
86
+ with open(whole_path, 'r', encoding='utf-8') as file: # noqa: ASYNC101
87
+ lines = read_lines(file.readlines(), start, end)
88
+ except FileNotFoundError:
89
+ return ErrorObservation(f'File not found: {path}')
90
+ except UnicodeDecodeError:
91
+ return ErrorObservation(f'File could not be decoded as utf-8: {path}')
92
+ except IsADirectoryError:
93
+ return ErrorObservation(f'Path is a directory: {path}. You can only read files')
94
+ code_view = ''.join(lines)
95
+ return FileReadObservation(path=path, content=code_view)
96
+
97
+
98
+ def insert_lines(
99
+ to_insert: list[str], original: list[str], start: int = 0, end: int = -1
100
+ ) -> list[str]:
101
+ """Insert the new content to the original content based on start and end"""
102
+ new_lines = [''] if start == 0 else original[:start]
103
+ new_lines += [i + '\n' for i in to_insert]
104
+ new_lines += [''] if end == -1 else original[end:]
105
+ return new_lines
106
+
107
+
108
+ async def write_file(
109
+ path: str,
110
+ workdir: str,
111
+ workspace_base: str,
112
+ workspace_mount_path_in_sandbox: str,
113
+ content: str,
114
+ start: int = 0,
115
+ end: int = -1,
116
+ ) -> FunctionCallResult:
117
+ insert = content.split('\n')
118
+
119
+ try:
120
+ whole_path = resolve_path(
121
+ path, workdir, workspace_base, workspace_mount_path_in_sandbox
122
+ )
123
+ if not os.path.exists(os.path.dirname(whole_path)):
124
+ os.makedirs(os.path.dirname(whole_path))
125
+ mode = 'w' if not os.path.exists(whole_path) else 'r+'
126
+ try:
127
+ with open(whole_path, mode, encoding='utf-8') as file: # noqa: ASYNC101
128
+ if mode != 'w':
129
+ all_lines = file.readlines()
130
+ new_file = insert_lines(insert, all_lines, start, end)
131
+ else:
132
+ new_file = [i + '\n' for i in insert]
133
+
134
+ file.seek(0)
135
+ file.writelines(new_file)
136
+ file.truncate()
137
+ except FileNotFoundError:
138
+ return ErrorObservation(f'File not found: {path}')
139
+ except IsADirectoryError:
140
+ return ErrorObservation(
141
+ f'Path is a directory: {path}. You can only write to files'
142
+ )
143
+ except UnicodeDecodeError:
144
+ return ErrorObservation(f'File could not be decoded as utf-8: {path}')
145
+ except PermissionError as e:
146
+ return ErrorObservation(f'Permission error on {path}: {e}')
147
+ return FileWriteObservation(content='', path=path)
@@ -0,0 +1,58 @@
1
+ from agents import function_tool, RunContextWrapper
2
+ from siada.foundation.code_agent_context import CodeAgentContext
3
+ from siada.tools.coder.observation.observation import FunctionCallResult
4
+
5
+
6
+ class FixAttemptCompletionResult(FunctionCallResult):
7
+
8
+ def __init__(self, content: str):
9
+ super().__init__(content=content)
10
+
11
+ def __str__(self):
12
+ return self.content
13
+
14
+ def format_for_display(self):
15
+ return "Bug Fix Task Status: COMPLETED"
16
+
17
+
18
+ @function_tool(
19
+ name_override="fix_attempt_completion",
20
+ description_override="Complete the bug fix task and mark it as finished. This tool MUST be called to properly complete any bug fix task. Failure to call this tool means the bug fix task is incomplete and unacceptable."
21
+ )
22
+ async def fix_attempt_completion(
23
+ context: RunContextWrapper[CodeAgentContext],
24
+ result: str,
25
+ ) -> FunctionCallResult:
26
+ """
27
+ Complete the bug fix task and mark it as finished.
28
+
29
+ IMPORTANT NOTE: This tool MUST be called to properly complete any bug fix task. Failure to call this tool means the bug fix task is incomplete and unacceptable. Before completing any bug fix work, you must ask yourself if you have successfully fixed all the bugs mentioned in the task. If not, then DO NOT use this tool until all bugs are fixed.
30
+
31
+ Args:
32
+ context: The run context wrapper containing agent context
33
+ result: (required) A detailed summary of the bug fix work completed, including:
34
+ - Summary of the bug fix logic
35
+ - What changes were made
36
+ - What files were modified
37
+ - Any testing or verification performed
38
+ Returns:
39
+ Observation: A completion observation with the fix summary
40
+
41
+ Example:
42
+ fix_attempt_completion(
43
+ result="Successfully fixed the login authentication bug. Modified auth.py to properly validate user credentials and updated the password hashing algorithm. All tests are now passing.",
44
+ )
45
+ """
46
+
47
+ # Format the completion message
48
+ completion_message = f"""
49
+ === Bug Fix Completed ===
50
+
51
+ {result}
52
+
53
+ """
54
+
55
+
56
+ return FixAttemptCompletionResult(
57
+ content=completion_message,
58
+ )
@@ -0,0 +1,29 @@
1
+ from agents import RunContextWrapper, function_tool
2
+
3
+ from siada.foundation.code_agent_context import CodeAgentContext
4
+
5
+
6
+ ISSUE_REVIEW_COMPLETION_DOCS = f"""Issue Review Completion Tool
7
+
8
+ This tool is called when completing an issue review process. It captures the final assessment of whether an issue has been successfully resolved.
9
+
10
+ Parameters:
11
+ - is_fixed (bool): Indicates whether the issue has been fixed. True if the issue is fixed, False if the issue is not fixed.
12
+ - check_summary (str): The review conclusion summary. When is_fixed is False, this summary must be very detailed, explaining what aspects of the issue remain unresolved, what problems were found during the review, and what additional work is needed. Must include:
13
+ - Specific basis for fix status judgment
14
+ - If not fixed, clearly explain reasons for non-fix
15
+ - If fixed, assess implementation quality and potential risks
16
+ - Specific improvement suggestions or alternative solutions
17
+
18
+ """
19
+
20
+
21
+ @function_tool(
22
+ name_override="issue_review_completion", description_override=ISSUE_REVIEW_COMPLETION_DOCS, failure_error_function=None
23
+ )
24
+ async def issue_review_completion(context: RunContextWrapper[CodeAgentContext], is_fixed: bool,
25
+ check_summary: str) -> dict:
26
+ return {
27
+ "is_fixed": is_fixed,
28
+ "check_summary": check_summary
29
+ }
File without changes
@@ -0,0 +1,22 @@
1
+ from dataclasses import dataclass
2
+
3
+ from siada.tools.coder.observation.observation import FunctionCallResult
4
+
5
+
6
+ @dataclass
7
+ class ErrorObservation(FunctionCallResult):
8
+ """This data class represents an error encountered by the agent.
9
+
10
+ This is the type of error that LLM can recover from.
11
+ E.g., Linter error after editing a file.
12
+ """
13
+
14
+ observation: str = 'error'
15
+ error_id: str = ''
16
+
17
+ @property
18
+ def message(self) -> str:
19
+ return self.content
20
+
21
+ def __str__(self) -> str:
22
+ return f'**ErrorObservation**\n{self.content}'
@@ -0,0 +1,216 @@
1
+ from dataclasses import dataclass
2
+ from difflib import SequenceMatcher
3
+
4
+ import re
5
+ from siada.tools.coder.observation.observation import FunctionCallResult, FileReadSource, ObservationType, FileEditSource
6
+
7
+
8
+ @dataclass
9
+ class FileReadObservation(FunctionCallResult):
10
+ """This data class represents the content of a file."""
11
+
12
+ path: str
13
+ observation: str = ObservationType.READ
14
+ impl_source: FileReadSource = FileReadSource.DEFAULT
15
+
16
+ @property
17
+ def message(self) -> str:
18
+ """Get a human-readable message describing the file read operation."""
19
+ return f'I read the file {self.path}.'
20
+
21
+ def __str__(self) -> str:
22
+ """Get a string representation of the file read observation."""
23
+ return f'[Read from {self.path} is successful.]\n{self.content}'
24
+
25
+ @dataclass
26
+ class FileWriteObservation(FunctionCallResult):
27
+ """This data class represents a file write operation."""
28
+
29
+ path: str
30
+ observation: str = ObservationType.WRITE
31
+
32
+ @property
33
+ def message(self) -> str:
34
+ """Get a human-readable message describing the file write operation."""
35
+ return f'I wrote to the file {self.path}.'
36
+
37
+ def format_for_display(self) -> str:
38
+ return self.message
39
+
40
+ def __str__(self) -> str:
41
+ """Get a string representation of the file write observation."""
42
+ return f'[Write to {self.path} is successful.]\n{self.content}'
43
+
44
+
45
+ @dataclass
46
+ class FileEditObservation(FunctionCallResult):
47
+ """This data class represents a file edit operation.
48
+
49
+ The observation includes both the old and new content of the file, and can
50
+ generate a diff visualization showing the changes. The diff is computed lazily
51
+ and cached to improve performance.
52
+
53
+ The .content property can either be:
54
+ - Git diff in LLM-based editing mode
55
+ - the rendered message sent to the LLM in OH_ACI mode (e.g., "The file /path/to/file.txt is created with the provided content.")
56
+ """
57
+
58
+ path: str = ''
59
+ prev_exist: bool = False
60
+ old_content: str | None = None
61
+ new_content: str | None = None
62
+ observation: str = ObservationType.EDIT
63
+ impl_source: FileEditSource = FileEditSource.LLM_BASED_EDIT
64
+ diff: str | None = (
65
+ None # The raw diff between old and new content, used in OH_ACI mode
66
+ )
67
+ _diff_cache: str | None = (
68
+ None # Cache for the diff visualization, used in LLM-based editing mode
69
+ )
70
+ command: str = "unknown"
71
+
72
+ @property
73
+ def message(self) -> str:
74
+ # Check if path is likely a file or directory
75
+ import os.path
76
+ try:
77
+ is_file = bool(os.path.splitext(str(self.path))[1]) if self.path else True
78
+ except Exception:
79
+ # if path is invalid, default to directory
80
+ is_file = False
81
+
82
+ if self.command == "view":
83
+ return "✓ File content loaded" if is_file else "✓ Directory listing completed"
84
+ elif self.command == "create":
85
+ return "✓ File created successfully" if is_file else "✓ Directory created successfully"
86
+ elif self.command == "str_replace":
87
+ return "✓ Text replacement completed"
88
+ elif self.command == "insert":
89
+ return "✓ Content insertion completed"
90
+ elif self.command == "undo_edit":
91
+ return "✓ Edit operation reverted"
92
+ else:
93
+ return "✓ Operation completed successfully"
94
+
95
+ def format_for_display(self) -> str:
96
+ return self.message
97
+
98
+ def get_edit_groups(self, n_context_lines: int = 2) -> list[dict[str, list[str]]]:
99
+ """Get the edit groups showing changes between old and new content.
100
+
101
+ Args:
102
+ n_context_lines: Number of context lines to show around each change.
103
+
104
+ Returns:
105
+ A list of edit groups, where each group contains before/after edits.
106
+ """
107
+ if self.old_content is None or self.new_content is None:
108
+ return []
109
+ old_lines = self.old_content.split('\n')
110
+ new_lines = self.new_content.split('\n')
111
+ # Borrowed from difflib.unified_diff to directly parse into structured format
112
+ edit_groups: list[dict] = []
113
+ for group in SequenceMatcher(None, old_lines, new_lines).get_grouped_opcodes(
114
+ n_context_lines
115
+ ):
116
+ # Take the max line number in the group
117
+ _indent_pad_size = len(str(group[-1][3])) + 1 # +1 for "*" prefix
118
+ cur_group: dict[str, list[str]] = {
119
+ 'before_edits': [],
120
+ 'after_edits': [],
121
+ }
122
+ for tag, i1, i2, j1, j2 in group:
123
+ if tag == 'equal':
124
+ for idx, line in enumerate(old_lines[i1:i2]):
125
+ line_num = i1 + idx + 1
126
+ cur_group['before_edits'].append(
127
+ f'{line_num:>{_indent_pad_size}}|{line}'
128
+ )
129
+ for idx, line in enumerate(new_lines[j1:j2]):
130
+ line_num = j1 + idx + 1
131
+ cur_group['after_edits'].append(
132
+ f'{line_num:>{_indent_pad_size}}|{line}'
133
+ )
134
+ continue
135
+ if tag in {'replace', 'delete'}:
136
+ for idx, line in enumerate(old_lines[i1:i2]):
137
+ line_num = i1 + idx + 1
138
+ cur_group['before_edits'].append(
139
+ f'-{line_num:>{_indent_pad_size - 1}}|{line}'
140
+ )
141
+ if tag in {'replace', 'insert'}:
142
+ for idx, line in enumerate(new_lines[j1:j2]):
143
+ line_num = j1 + idx + 1
144
+ cur_group['after_edits'].append(
145
+ f'+{line_num:>{_indent_pad_size - 1}}|{line}'
146
+ )
147
+ edit_groups.append(cur_group)
148
+ return edit_groups
149
+
150
+ def visualize_diff(
151
+ self,
152
+ n_context_lines: int = 2,
153
+ change_applied: bool = True,
154
+ ) -> str:
155
+ """Visualize the diff of the file edit. Used in the LLM-based editing mode.
156
+
157
+ Instead of showing the diff line by line, this function shows each hunk
158
+ of changes as a separate entity.
159
+
160
+ Args:
161
+ n_context_lines: Number of context lines to show before/after changes.
162
+ change_applied: Whether changes are applied. If false, shows as
163
+ attempted edit.
164
+
165
+ Returns:
166
+ A string containing the formatted diff visualization.
167
+ """
168
+ # Use cached diff if available
169
+ if self._diff_cache is not None:
170
+ return self._diff_cache
171
+
172
+ # Check if there are any changes
173
+ if change_applied and self.old_content == self.new_content:
174
+ msg = '(no changes detected. Please make sure your edits change '
175
+ msg += 'the content of the existing file.)\n'
176
+ self._diff_cache = msg
177
+ return self._diff_cache
178
+
179
+ edit_groups = self.get_edit_groups(n_context_lines=n_context_lines)
180
+
181
+ if change_applied:
182
+ header = f'[Existing file {self.path} is edited with '
183
+ header += f'{len(edit_groups)} changes.]'
184
+ else:
185
+ header = f"[Changes are NOT applied to {self.path} - Here's how "
186
+ header += 'the file looks like if changes are applied.]'
187
+ result = [header]
188
+
189
+ op_type = 'edit' if change_applied else 'ATTEMPTED edit'
190
+ for i, cur_edit_group in enumerate(edit_groups):
191
+ if i != 0:
192
+ result.append('-------------------------')
193
+ result.append(f'[begin of {op_type} {i + 1} / {len(edit_groups)}]')
194
+ result.append(f'(content before {op_type})')
195
+ result.extend(cur_edit_group['before_edits'])
196
+ result.append(f'(content after {op_type})')
197
+ result.extend(cur_edit_group['after_edits'])
198
+ result.append(f'[end of {op_type} {i + 1} / {len(edit_groups)}]')
199
+
200
+ # Cache the result
201
+ self._diff_cache = '\n'.join(result)
202
+ return self._diff_cache
203
+
204
+ def __str__(self) -> str:
205
+ """Get a string representation of the file edit observation."""
206
+ if self.impl_source == FileEditSource.OH_ACI:
207
+ return self.content
208
+
209
+ if not self.prev_exist:
210
+ assert self.old_content == '', (
211
+ 'old_content should be empty if the file is new (prev_exist=False).'
212
+ )
213
+ return f'[New file {self.path} is created with the provided content.]\n'
214
+
215
+ # Use cached diff if available, otherwise compute it
216
+ return self.visualize_diff().rstrip() + '\n'
@@ -0,0 +1,71 @@
1
+ from dataclasses import dataclass
2
+ from enum import Enum
3
+
4
+ class ObservationType(str, Enum):
5
+ READ = 'read'
6
+ """The content of a file
7
+ """
8
+
9
+ WRITE = 'write'
10
+
11
+ EDIT = 'edit'
12
+
13
+ BROWSE = 'browse'
14
+ """The HTML content of a URL
15
+ """
16
+
17
+ RUN = 'run'
18
+ """The output of a command
19
+ """
20
+
21
+ RUN_IPYTHON = 'run_ipython'
22
+ """Runs a IPython cell.
23
+ """
24
+
25
+ CHAT = 'chat'
26
+ """A message from the user
27
+ """
28
+
29
+ DELEGATE = 'delegate'
30
+ """The result of a task delegated to another agent
31
+ """
32
+
33
+ MESSAGE = 'message'
34
+
35
+ ERROR = 'error'
36
+
37
+ SUCCESS = 'success'
38
+
39
+ NULL = 'null'
40
+
41
+ THINK = 'think'
42
+
43
+ AGENT_STATE_CHANGED = 'agent_state_changed'
44
+
45
+ USER_REJECTED = 'user_rejected'
46
+
47
+ CONDENSE = 'condense'
48
+ """Result of a condensation operation."""
49
+
50
+ RECALL = 'recall'
51
+ """Result of a recall operation. This can be the workspace context, a microagent, or other types of information."""
52
+
53
+ MCP = 'mcp'
54
+ """Result of a MCP Server operation"""
55
+
56
+
57
+ class FileReadSource(str, Enum):
58
+ OH_ACI = 'oh_aci' # openhands-aci
59
+ DEFAULT = 'default'
60
+
61
+ class FileEditSource(str, Enum):
62
+ LLM_BASED_EDIT = 'llm_based_edit'
63
+ OH_ACI = 'oh_aci' # openhands-aci
64
+
65
+
66
+ @dataclass
67
+ class FunctionCallResult:
68
+ content: str
69
+
70
+ def format_for_display(self) -> str:
71
+ return self.__str__()