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,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__()
|