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,203 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
ROOT_IMPORTANT_FILES = [
|
|
4
|
+
# Version Control
|
|
5
|
+
".gitignore",
|
|
6
|
+
".gitattributes",
|
|
7
|
+
# Documentation
|
|
8
|
+
"README",
|
|
9
|
+
"README.md",
|
|
10
|
+
"README.txt",
|
|
11
|
+
"README.rst",
|
|
12
|
+
"CONTRIBUTING",
|
|
13
|
+
"CONTRIBUTING.md",
|
|
14
|
+
"CONTRIBUTING.txt",
|
|
15
|
+
"CONTRIBUTING.rst",
|
|
16
|
+
"LICENSE",
|
|
17
|
+
"LICENSE.md",
|
|
18
|
+
"LICENSE.txt",
|
|
19
|
+
"CHANGELOG",
|
|
20
|
+
"CHANGELOG.md",
|
|
21
|
+
"CHANGELOG.txt",
|
|
22
|
+
"CHANGELOG.rst",
|
|
23
|
+
"SECURITY",
|
|
24
|
+
"SECURITY.md",
|
|
25
|
+
"SECURITY.txt",
|
|
26
|
+
"CODEOWNERS",
|
|
27
|
+
# Package Management and Dependencies
|
|
28
|
+
"requirements.txt",
|
|
29
|
+
"Pipfile",
|
|
30
|
+
"Pipfile.lock",
|
|
31
|
+
"pyproject.toml",
|
|
32
|
+
"setup.py",
|
|
33
|
+
"setup.cfg",
|
|
34
|
+
"package.json",
|
|
35
|
+
"package-lock.json",
|
|
36
|
+
"yarn.lock",
|
|
37
|
+
"npm-shrinkwrap.json",
|
|
38
|
+
"Gemfile",
|
|
39
|
+
"Gemfile.lock",
|
|
40
|
+
"composer.json",
|
|
41
|
+
"composer.lock",
|
|
42
|
+
"pom.xml",
|
|
43
|
+
"build.gradle",
|
|
44
|
+
"build.gradle.kts",
|
|
45
|
+
"build.sbt",
|
|
46
|
+
"go.mod",
|
|
47
|
+
"go.sum",
|
|
48
|
+
"Cargo.toml",
|
|
49
|
+
"Cargo.lock",
|
|
50
|
+
"mix.exs",
|
|
51
|
+
"rebar.config",
|
|
52
|
+
"project.clj",
|
|
53
|
+
"Podfile",
|
|
54
|
+
"Cartfile",
|
|
55
|
+
"dub.json",
|
|
56
|
+
"dub.sdl",
|
|
57
|
+
# Configuration and Settings
|
|
58
|
+
".env",
|
|
59
|
+
".env.example",
|
|
60
|
+
".editorconfig",
|
|
61
|
+
"tsconfig.json",
|
|
62
|
+
"jsconfig.json",
|
|
63
|
+
".babelrc",
|
|
64
|
+
"babel.config.js",
|
|
65
|
+
".eslintrc",
|
|
66
|
+
".eslintignore",
|
|
67
|
+
".prettierrc",
|
|
68
|
+
".stylelintrc",
|
|
69
|
+
"tslint.json",
|
|
70
|
+
".pylintrc",
|
|
71
|
+
".flake8",
|
|
72
|
+
".rubocop.yml",
|
|
73
|
+
".scalafmt.conf",
|
|
74
|
+
".dockerignore",
|
|
75
|
+
".gitpod.yml",
|
|
76
|
+
"sonar-project.properties",
|
|
77
|
+
"renovate.json",
|
|
78
|
+
"dependabot.yml",
|
|
79
|
+
".pre-commit-config.yaml",
|
|
80
|
+
"mypy.ini",
|
|
81
|
+
"tox.ini",
|
|
82
|
+
".yamllint",
|
|
83
|
+
"pyrightconfig.json",
|
|
84
|
+
# Build and Compilation
|
|
85
|
+
"webpack.config.js",
|
|
86
|
+
"rollup.config.js",
|
|
87
|
+
"parcel.config.js",
|
|
88
|
+
"gulpfile.js",
|
|
89
|
+
"Gruntfile.js",
|
|
90
|
+
"build.xml",
|
|
91
|
+
"build.boot",
|
|
92
|
+
"project.json",
|
|
93
|
+
"build.cake",
|
|
94
|
+
"MANIFEST.in",
|
|
95
|
+
# Testing
|
|
96
|
+
"pytest.ini",
|
|
97
|
+
"phpunit.xml",
|
|
98
|
+
"karma.conf.js",
|
|
99
|
+
"jest.config.js",
|
|
100
|
+
"cypress.json",
|
|
101
|
+
".nycrc",
|
|
102
|
+
".nycrc.json",
|
|
103
|
+
# CI/CD
|
|
104
|
+
".travis.yml",
|
|
105
|
+
".gitlab-ci.yml",
|
|
106
|
+
"Jenkinsfile",
|
|
107
|
+
"azure-pipelines.yml",
|
|
108
|
+
"bitbucket-pipelines.yml",
|
|
109
|
+
"appveyor.yml",
|
|
110
|
+
"circle.yml",
|
|
111
|
+
".circleci/config.yml",
|
|
112
|
+
".github/dependabot.yml",
|
|
113
|
+
"codecov.yml",
|
|
114
|
+
".coveragerc",
|
|
115
|
+
# Docker and Containers
|
|
116
|
+
"Dockerfile",
|
|
117
|
+
"docker-compose.yml",
|
|
118
|
+
"docker-compose.override.yml",
|
|
119
|
+
# Cloud and Serverless
|
|
120
|
+
"serverless.yml",
|
|
121
|
+
"firebase.json",
|
|
122
|
+
"now.json",
|
|
123
|
+
"netlify.toml",
|
|
124
|
+
"vercel.json",
|
|
125
|
+
"app.yaml",
|
|
126
|
+
"terraform.tf",
|
|
127
|
+
"main.tf",
|
|
128
|
+
"cloudformation.yaml",
|
|
129
|
+
"cloudformation.json",
|
|
130
|
+
"ansible.cfg",
|
|
131
|
+
"kubernetes.yaml",
|
|
132
|
+
"k8s.yaml",
|
|
133
|
+
# Database
|
|
134
|
+
"schema.sql",
|
|
135
|
+
"liquibase.properties",
|
|
136
|
+
"flyway.conf",
|
|
137
|
+
# Framework-specific
|
|
138
|
+
"next.config.js",
|
|
139
|
+
"nuxt.config.js",
|
|
140
|
+
"vue.config.js",
|
|
141
|
+
"angular.json",
|
|
142
|
+
"gatsby-config.js",
|
|
143
|
+
"gridsome.config.js",
|
|
144
|
+
# API Documentation
|
|
145
|
+
"swagger.yaml",
|
|
146
|
+
"swagger.json",
|
|
147
|
+
"openapi.yaml",
|
|
148
|
+
"openapi.json",
|
|
149
|
+
# Development environment
|
|
150
|
+
".nvmrc",
|
|
151
|
+
".ruby-version",
|
|
152
|
+
".python-version",
|
|
153
|
+
"Vagrantfile",
|
|
154
|
+
# Quality and metrics
|
|
155
|
+
".codeclimate.yml",
|
|
156
|
+
"codecov.yml",
|
|
157
|
+
# Documentation
|
|
158
|
+
"mkdocs.yml",
|
|
159
|
+
"_config.yml",
|
|
160
|
+
"book.toml",
|
|
161
|
+
"readthedocs.yml",
|
|
162
|
+
".readthedocs.yaml",
|
|
163
|
+
# Package registries
|
|
164
|
+
".npmrc",
|
|
165
|
+
".yarnrc",
|
|
166
|
+
# Linting and formatting
|
|
167
|
+
".isort.cfg",
|
|
168
|
+
".markdownlint.json",
|
|
169
|
+
".markdownlint.yaml",
|
|
170
|
+
# Security
|
|
171
|
+
".bandit",
|
|
172
|
+
".secrets.baseline",
|
|
173
|
+
# Misc
|
|
174
|
+
".pypirc",
|
|
175
|
+
".gitkeep",
|
|
176
|
+
".npmignore",
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# Normalize the lists once
|
|
181
|
+
NORMALIZED_ROOT_IMPORTANT_FILES = set(os.path.normpath(path) for path in ROOT_IMPORTANT_FILES)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def is_important(file_path):
|
|
185
|
+
file_name = os.path.basename(file_path)
|
|
186
|
+
dir_name = os.path.normpath(os.path.dirname(file_path))
|
|
187
|
+
normalized_path = os.path.normpath(file_path)
|
|
188
|
+
|
|
189
|
+
# Check for GitHub Actions workflow files
|
|
190
|
+
if dir_name == os.path.normpath(".github/workflows") and file_name.endswith(".yml"):
|
|
191
|
+
return True
|
|
192
|
+
|
|
193
|
+
return normalized_path in NORMALIZED_ROOT_IMPORTANT_FILES
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def filter_important_files(file_paths):
|
|
197
|
+
"""
|
|
198
|
+
Filter a list of file paths to return only those that are commonly important in codebases.
|
|
199
|
+
|
|
200
|
+
:param file_paths: List of file paths to check
|
|
201
|
+
:return: List of file paths that match important file patterns
|
|
202
|
+
"""
|
|
203
|
+
return list(filter(is_important, file_paths))
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Token counting functionality for language models.
|
|
3
|
+
|
|
4
|
+
Provides token calculation with litellm integration and fallback estimation.
|
|
5
|
+
Supports any model with zero configuration and basic caching for performance.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TokenCounterModel:
|
|
13
|
+
"""
|
|
14
|
+
Token counting model with litellm integration and fallback estimation.
|
|
15
|
+
|
|
16
|
+
Uses litellm for accurate counting when available, otherwise falls back
|
|
17
|
+
to simple estimation (4 characters ≈ 1 token). Supports any model name
|
|
18
|
+
with basic caching mechanism.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, model_name: str):
|
|
22
|
+
"""
|
|
23
|
+
Initialize TokenCounterModel instance.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
model_name (str): Language model name (any name supported)
|
|
27
|
+
"""
|
|
28
|
+
self.model_name = model_name
|
|
29
|
+
self._litellm_available = self._check_litellm_availability()
|
|
30
|
+
self._cache = {}
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
34
|
+
logger.debug(f"Initialized TokenCounterModel: {self.model_name}")
|
|
35
|
+
|
|
36
|
+
def token_count(self, text: str) -> int:
|
|
37
|
+
"""
|
|
38
|
+
Calculate token count for given text.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
text (str): Text to count tokens for
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
int: Number of tokens
|
|
45
|
+
"""
|
|
46
|
+
if not text:
|
|
47
|
+
return 0
|
|
48
|
+
|
|
49
|
+
text_hash = hash(text)
|
|
50
|
+
if text_hash in self._cache:
|
|
51
|
+
return self._cache[text_hash]
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
if self._litellm_available:
|
|
55
|
+
token_count = self._count_with_litellm(text)
|
|
56
|
+
else:
|
|
57
|
+
token_count = self._count_with_estimation(text)
|
|
58
|
+
|
|
59
|
+
if len(self._cache) >= 1000:
|
|
60
|
+
self._cache.clear()
|
|
61
|
+
self._cache[text_hash] = token_count
|
|
62
|
+
|
|
63
|
+
return token_count
|
|
64
|
+
|
|
65
|
+
except Exception as e:
|
|
66
|
+
logging.getLogger(__name__).warning(f"Token calculation failed, using estimation: {e}")
|
|
67
|
+
return self._count_with_estimation(text)
|
|
68
|
+
|
|
69
|
+
def _check_litellm_availability(self) -> bool:
|
|
70
|
+
"""Check if litellm is available."""
|
|
71
|
+
try:
|
|
72
|
+
from siada.provider.lazy_lite_llm import litellm
|
|
73
|
+
return True
|
|
74
|
+
except ImportError:
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
def _count_with_litellm(self, text: str) -> int:
|
|
78
|
+
"""Count tokens using litellm."""
|
|
79
|
+
from siada.provider.lazy_lite_llm import litellm
|
|
80
|
+
response = litellm.token_counter(model=self.model_name, text=text)
|
|
81
|
+
return int(response)
|
|
82
|
+
|
|
83
|
+
def _count_with_estimation(self, text: str) -> int:
|
|
84
|
+
"""Count tokens using simple estimation (4 chars ≈ 1 token)."""
|
|
85
|
+
return max(1, len(text) // 4)
|
|
86
|
+
|
|
87
|
+
def __str__(self) -> str:
|
|
88
|
+
"""String representation."""
|
|
89
|
+
return f"TokenCounterModel(model={self.model_name}, litellm={self._litellm_available})"
|
|
90
|
+
|
|
91
|
+
def __repr__(self) -> str:
|
|
92
|
+
"""Detailed string representation."""
|
|
93
|
+
return f"TokenCounterModel(model_name='{self.model_name}', litellm_available={self._litellm_available})"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class OptimizedTokenCounterModel(TokenCounterModel):
|
|
97
|
+
"""
|
|
98
|
+
Optimized token counter for long texts and large codebases.
|
|
99
|
+
|
|
100
|
+
Uses sampling for texts exceeding the threshold to improve performance.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def __init__(self, model_name: str, sampling_threshold: int = 10000):
|
|
104
|
+
"""
|
|
105
|
+
Initialize optimized TokenCounterModel.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
model_name (str): Model name
|
|
109
|
+
sampling_threshold (int): Threshold for sampling, texts longer than this will use sampling
|
|
110
|
+
"""
|
|
111
|
+
super().__init__(model_name)
|
|
112
|
+
self.sampling_threshold = sampling_threshold
|
|
113
|
+
|
|
114
|
+
def token_count(self, text: str) -> int:
|
|
115
|
+
"""
|
|
116
|
+
Optimized token counting with sampling for long texts.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
text (str): Text to count tokens for
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
int: Number of tokens
|
|
123
|
+
"""
|
|
124
|
+
if not text:
|
|
125
|
+
return 0
|
|
126
|
+
|
|
127
|
+
if len(text) <= self.sampling_threshold:
|
|
128
|
+
return super().token_count(text)
|
|
129
|
+
|
|
130
|
+
return self._count_with_sampling(text)
|
|
131
|
+
|
|
132
|
+
def _count_with_sampling(self, text: str) -> int:
|
|
133
|
+
"""Count tokens for long text using sampling method."""
|
|
134
|
+
lines = text.splitlines(keepends=True)
|
|
135
|
+
num_lines = len(lines)
|
|
136
|
+
|
|
137
|
+
if num_lines <= 100:
|
|
138
|
+
return super().token_count(text)
|
|
139
|
+
|
|
140
|
+
step = max(1, num_lines // 100)
|
|
141
|
+
sampled_lines = lines[::step]
|
|
142
|
+
sample_text = "".join(sampled_lines)
|
|
143
|
+
|
|
144
|
+
sample_tokens = super().token_count(sample_text)
|
|
145
|
+
|
|
146
|
+
sample_length = len(sample_text)
|
|
147
|
+
total_length = len(text)
|
|
148
|
+
|
|
149
|
+
if sample_length > 0:
|
|
150
|
+
estimated_tokens = int(sample_tokens * total_length / sample_length)
|
|
151
|
+
else:
|
|
152
|
+
estimated_tokens = self._count_with_estimation(text)
|
|
153
|
+
|
|
154
|
+
return estimated_tokens
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Thread-based, killable spinner utility.
|
|
5
|
+
|
|
6
|
+
Use it like:
|
|
7
|
+
|
|
8
|
+
from .waiting import WaitingSpinner
|
|
9
|
+
|
|
10
|
+
spinner = WaitingSpinner("Waiting for LLM")
|
|
11
|
+
spinner.start()
|
|
12
|
+
... # long task
|
|
13
|
+
spinner.stop()
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import sys
|
|
17
|
+
import threading
|
|
18
|
+
import time
|
|
19
|
+
|
|
20
|
+
from rich.console import Console
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Spinner:
|
|
24
|
+
"""
|
|
25
|
+
Minimal spinner that scans a single marker back and forth across a line.
|
|
26
|
+
|
|
27
|
+
The animation is pre-rendered into a list of frames. If the terminal
|
|
28
|
+
cannot display unicode the frames are converted to plain ASCII.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
last_frame_idx = 0 # Class variable to store the last frame index
|
|
32
|
+
|
|
33
|
+
def __init__(self, text: str, width: int = 7):
|
|
34
|
+
self.text = text
|
|
35
|
+
self.start_time = time.time()
|
|
36
|
+
self.last_update = 0.0
|
|
37
|
+
self.visible = False
|
|
38
|
+
self.is_tty = sys.stdout.isatty()
|
|
39
|
+
self.console = Console()
|
|
40
|
+
|
|
41
|
+
# Pre-render the animation frames using pure ASCII so they will
|
|
42
|
+
# always display, even on very limited terminals.
|
|
43
|
+
ascii_frames = [
|
|
44
|
+
"#= ", # C1 C2 space(8)
|
|
45
|
+
"=# ", # C2 C1 space(8)
|
|
46
|
+
" =# ", # space(1) C2 C1 space(7)
|
|
47
|
+
" =# ", # space(2) C2 C1 space(6)
|
|
48
|
+
" =# ", # space(3) C2 C1 space(5)
|
|
49
|
+
" =# ", # space(4) C2 C1 space(4)
|
|
50
|
+
" =# ", # space(5) C2 C1 space(3)
|
|
51
|
+
" =# ", # space(6) C2 C1 space(2)
|
|
52
|
+
" =# ", # space(7) C2 C1 space(1)
|
|
53
|
+
" =#", # space(8) C2 C1
|
|
54
|
+
" #=", # space(8) C1 C2
|
|
55
|
+
" #= ", # space(7) C1 C2 space(1)
|
|
56
|
+
" #= ", # space(6) C1 C2 space(2)
|
|
57
|
+
" #= ", # space(5) C1 C2 space(3)
|
|
58
|
+
" #= ", # space(4) C1 C2 space(4)
|
|
59
|
+
" #= ", # space(3) C1 C2 space(5)
|
|
60
|
+
" #= ", # space(2) C1 C2 space(6)
|
|
61
|
+
" #= ", # space(1) C1 C2 space(7)
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
self.unicode_palette = "░█"
|
|
65
|
+
xlate_from, xlate_to = ("=#", self.unicode_palette)
|
|
66
|
+
|
|
67
|
+
# If unicode is supported, swap the ASCII chars for nicer glyphs.
|
|
68
|
+
if self._supports_unicode():
|
|
69
|
+
translation_table = str.maketrans(xlate_from, xlate_to)
|
|
70
|
+
frames = [f.translate(translation_table) for f in ascii_frames]
|
|
71
|
+
self.scan_char = xlate_to[xlate_from.find("#")]
|
|
72
|
+
else:
|
|
73
|
+
frames = ascii_frames
|
|
74
|
+
self.scan_char = "#"
|
|
75
|
+
|
|
76
|
+
# Bounce the scanner back and forth.
|
|
77
|
+
self.frames = frames
|
|
78
|
+
self.frame_idx = Spinner.last_frame_idx # Initialize from class variable
|
|
79
|
+
self.width = len(frames[0]) - 2 # number of chars between the brackets
|
|
80
|
+
self.animation_len = len(frames[0])
|
|
81
|
+
self.last_display_len = 0 # Length of the last spinner line (frame + text)
|
|
82
|
+
|
|
83
|
+
def _supports_unicode(self) -> bool:
|
|
84
|
+
if not self.is_tty:
|
|
85
|
+
return False
|
|
86
|
+
try:
|
|
87
|
+
out = self.unicode_palette
|
|
88
|
+
out += "\b" * len(self.unicode_palette)
|
|
89
|
+
out += " " * len(self.unicode_palette)
|
|
90
|
+
out += "\b" * len(self.unicode_palette)
|
|
91
|
+
sys.stdout.write(out)
|
|
92
|
+
sys.stdout.flush()
|
|
93
|
+
return True
|
|
94
|
+
except UnicodeEncodeError:
|
|
95
|
+
return False
|
|
96
|
+
except Exception:
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
def _next_frame(self) -> str:
|
|
100
|
+
frame = self.frames[self.frame_idx]
|
|
101
|
+
self.frame_idx = (self.frame_idx + 1) % len(self.frames)
|
|
102
|
+
Spinner.last_frame_idx = self.frame_idx # Update class variable
|
|
103
|
+
return frame
|
|
104
|
+
|
|
105
|
+
def step(self, text: str = None) -> None:
|
|
106
|
+
if text is not None:
|
|
107
|
+
self.text = text
|
|
108
|
+
|
|
109
|
+
if not self.is_tty:
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
now = time.time()
|
|
113
|
+
if not self.visible and now - self.start_time >= 0.5:
|
|
114
|
+
self.visible = True
|
|
115
|
+
self.last_update = 0.0
|
|
116
|
+
if self.is_tty:
|
|
117
|
+
self.console.show_cursor(False)
|
|
118
|
+
|
|
119
|
+
if not self.visible or now - self.last_update < 0.1:
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
self.last_update = now
|
|
123
|
+
frame_str = self._next_frame()
|
|
124
|
+
|
|
125
|
+
# Determine the maximum width for the spinner line
|
|
126
|
+
# Subtract 2 as requested, to leave a margin or prevent cursor wrapping issues
|
|
127
|
+
max_spinner_width = self.console.width - 2
|
|
128
|
+
if max_spinner_width < 0: # Handle extremely narrow terminals
|
|
129
|
+
max_spinner_width = 0
|
|
130
|
+
|
|
131
|
+
current_text_payload = f" {self.text}"
|
|
132
|
+
line_to_display = f"{frame_str}{current_text_payload}"
|
|
133
|
+
|
|
134
|
+
# Truncate the line if it's too long for the console width
|
|
135
|
+
if len(line_to_display) > max_spinner_width:
|
|
136
|
+
line_to_display = line_to_display[:max_spinner_width]
|
|
137
|
+
|
|
138
|
+
len_line_to_display = len(line_to_display)
|
|
139
|
+
|
|
140
|
+
# Calculate padding to clear any remnants from a longer previous line
|
|
141
|
+
padding_to_clear = " " * max(0, self.last_display_len - len_line_to_display)
|
|
142
|
+
|
|
143
|
+
# Write the spinner frame, text, and any necessary clearing spaces
|
|
144
|
+
sys.stdout.write(f"\r{line_to_display}{padding_to_clear}")
|
|
145
|
+
self.last_display_len = len_line_to_display
|
|
146
|
+
|
|
147
|
+
# Calculate number of backspaces to position cursor at the scanner character
|
|
148
|
+
scan_char_abs_pos = frame_str.find(self.scan_char)
|
|
149
|
+
|
|
150
|
+
# Total characters written to the line (frame + text + padding)
|
|
151
|
+
total_chars_written_on_line = len_line_to_display + len(padding_to_clear)
|
|
152
|
+
|
|
153
|
+
# num_backspaces will be non-positive if scan_char_abs_pos is beyond
|
|
154
|
+
# total_chars_written_on_line (e.g., if the scan char itself was truncated).
|
|
155
|
+
# (e.g., if the scan char itself was truncated).
|
|
156
|
+
# In such cases, (effectively) 0 backspaces are written,
|
|
157
|
+
# and the cursor stays at the end of the line.
|
|
158
|
+
num_backspaces = total_chars_written_on_line - scan_char_abs_pos
|
|
159
|
+
sys.stdout.write("\b" * num_backspaces)
|
|
160
|
+
sys.stdout.flush()
|
|
161
|
+
|
|
162
|
+
def end(self) -> None:
|
|
163
|
+
if self.visible and self.is_tty:
|
|
164
|
+
clear_len = self.last_display_len # Use the length of the last displayed content
|
|
165
|
+
sys.stdout.write("\r" + " " * clear_len + "\r")
|
|
166
|
+
sys.stdout.flush()
|
|
167
|
+
self.console.show_cursor(True)
|
|
168
|
+
self.visible = False
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class WaitingSpinner:
|
|
172
|
+
"""Background spinner that can be started/stopped safely."""
|
|
173
|
+
|
|
174
|
+
def __init__(self, text: str = "Waiting for LLM", delay: float = 0.15):
|
|
175
|
+
self.spinner = Spinner(text)
|
|
176
|
+
self.delay = delay
|
|
177
|
+
self._stop_event = threading.Event()
|
|
178
|
+
self._thread = threading.Thread(target=self._spin, daemon=True)
|
|
179
|
+
|
|
180
|
+
def _spin(self):
|
|
181
|
+
while not self._stop_event.is_set():
|
|
182
|
+
self.spinner.step()
|
|
183
|
+
time.sleep(self.delay)
|
|
184
|
+
self.spinner.end()
|
|
185
|
+
|
|
186
|
+
def start(self):
|
|
187
|
+
"""Start the spinner in a background thread."""
|
|
188
|
+
if not self._thread.is_alive():
|
|
189
|
+
self._thread.start()
|
|
190
|
+
|
|
191
|
+
def stop(self):
|
|
192
|
+
"""Request the spinner to stop and wait briefly for the thread to exit."""
|
|
193
|
+
self._stop_event.set()
|
|
194
|
+
if self._thread.is_alive():
|
|
195
|
+
self._thread.join(timeout=self.delay)
|
|
196
|
+
self.spinner.end()
|
|
197
|
+
|
|
198
|
+
# Allow use as a context-manager
|
|
199
|
+
def __enter__(self):
|
|
200
|
+
self.start()
|
|
201
|
+
return self
|
|
202
|
+
|
|
203
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
204
|
+
self.stop()
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def main():
|
|
208
|
+
spinner = Spinner("Running spinner...")
|
|
209
|
+
try:
|
|
210
|
+
for _ in range(100):
|
|
211
|
+
time.sleep(0.15)
|
|
212
|
+
spinner.step()
|
|
213
|
+
print("Success!")
|
|
214
|
+
except KeyboardInterrupt:
|
|
215
|
+
print("\nInterrupted by user.")
|
|
216
|
+
finally:
|
|
217
|
+
spinner.end()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
if __name__ == "__main__":
|
|
221
|
+
main()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from agents import RunContextWrapper, function_tool
|
|
2
|
+
|
|
3
|
+
from siada.foundation.code_agent_context import CodeAgentContext
|
|
4
|
+
from siada.tools.coder.observation.observation import FunctionCallResult
|
|
5
|
+
|
|
6
|
+
REPRODUCE_COMPLETION_DOCS = f"""ISSUE Reproduction Completion Tool
|
|
7
|
+
Use this tool to submit your work results to the user whenever the ISSUE has been successfully reproduced—that is, when executing the test cases you generated can reliably reproduce the ISSUE. Include a brief description of your reproduction process.
|
|
8
|
+
IMPORTANT NOTE: Do not invoke this tool unless the issue has been confirmed as reproduced.
|
|
9
|
+
Args:
|
|
10
|
+
test_case: The full path of the test case used to reproduce the issue in the task.
|
|
11
|
+
bug_analysis: Analysis of the cause of the bug, including the involved code files, methods, and other relevant components.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
class ReproduceCompletionResult(FunctionCallResult):
|
|
15
|
+
def __init__(self, content: str):
|
|
16
|
+
super().__init__(content=content)
|
|
17
|
+
|
|
18
|
+
def __str__(self):
|
|
19
|
+
return self.content
|
|
20
|
+
|
|
21
|
+
def format_for_display(self):
|
|
22
|
+
return "Reproduction Completed"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@function_tool(
|
|
26
|
+
name_override="reproduce_completion", description_override=REPRODUCE_COMPLETION_DOCS, failure_error_function=None
|
|
27
|
+
)
|
|
28
|
+
async def reproduce_completion(context: RunContextWrapper[CodeAgentContext], test_case: str, bug_analysis: str) -> ReproduceCompletionResult:
|
|
29
|
+
# 获取 session_id
|
|
30
|
+
content = (f"====\n"
|
|
31
|
+
f"This issue can be reproduced using test case : {test_case}.\n Analysis of the issue: {bug_analysis} \n"
|
|
32
|
+
f"When you start fixing the issue, you must thoroughly refer to this information."
|
|
33
|
+
f"\n====")
|
|
34
|
+
|
|
35
|
+
return ReproduceCompletionResult(
|
|
36
|
+
content=content
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from agents import function_tool, RunContextWrapper
|
|
2
|
+
|
|
3
|
+
from siada.foundation.code_agent_context import CodeAgentContext
|
|
4
|
+
from siada.tools.coder.cmd_runner import run_cmd_impl
|
|
5
|
+
from siada.tools.coder.observation.observation import FunctionCallResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
RUN_CMD_DOCS = """Execute a shell command using the most appropriate method for the current environment.
|
|
9
|
+
|
|
10
|
+
This function automatically selects between pexpect (for interactive terminals on Unix-like
|
|
11
|
+
systems) and subprocess (for Windows or non-interactive environments) to execute shell
|
|
12
|
+
commands. It provides real-time output streaming and proper error handling.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
command (str): The shell command to execute as a string.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RunCmdResult(FunctionCallResult):
|
|
20
|
+
"""This data class represents the output of a command."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, command: str, output: str, code: int):
|
|
23
|
+
self.command = command
|
|
24
|
+
self.output = output
|
|
25
|
+
self.code = code if code is not None else 1
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def content(self) -> str:
|
|
29
|
+
"""Return the command output as content."""
|
|
30
|
+
return str((self.code, self.output))
|
|
31
|
+
|
|
32
|
+
def format_for_display(self) -> str:
|
|
33
|
+
if self.code == 0:
|
|
34
|
+
return f"`{self.command}` executed successfully!"
|
|
35
|
+
else:
|
|
36
|
+
return f"`{self.command}` executed with error!"
|
|
37
|
+
|
|
38
|
+
def __str__(self):
|
|
39
|
+
return self.content
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@function_tool
|
|
43
|
+
def run_cmd(context: RunContextWrapper[CodeAgentContext], command) -> FunctionCallResult:
|
|
44
|
+
"""Execute a shell command using the most appropriate method for the current environment.
|
|
45
|
+
|
|
46
|
+
This function automatically selects between pexpect (for interactive terminals on Unix-like
|
|
47
|
+
systems) and subprocess (for Windows or non-interactive environments) to execute shell
|
|
48
|
+
commands. It provides real-time output streaming and proper error handling.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
command (str): The shell command to execute as a string.
|
|
52
|
+
verbose (bool, optional): If True, prints detailed execution information including
|
|
53
|
+
the execution method used, shell information, and command details. Defaults to False.
|
|
54
|
+
error_print (callable, optional): Custom error printing function. If provided, errors
|
|
55
|
+
will be output using this function instead of the default print(). Should accept
|
|
56
|
+
a single string argument. Defaults to None.
|
|
57
|
+
"""
|
|
58
|
+
cwd = context.context.root_dir
|
|
59
|
+
code, output = run_cmd_impl(command=command, cwd=cwd)
|
|
60
|
+
return RunCmdResult(command=command, output=output, code=code)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from agents import RunContextWrapper, function_tool
|
|
2
|
+
|
|
3
|
+
from siada.foundation.code_agent_context import CodeAgentContext
|
|
4
|
+
|
|
5
|
+
TEST_COMPLETION_DOCS = f"""Test Completion Tool
|
|
6
|
+
Use this tool to submit your test work results to the user whenever the test task has been completed. This tool reports whether tests passed or failed and provides detailed information about the test execution.
|
|
7
|
+
IMPORTANT NOTE: Do not invoke this tool unless the test task has been confirmed as completed.
|
|
8
|
+
Args:
|
|
9
|
+
is_passed: Integer type, 0 indicates test failed, 1 indicates test passed.
|
|
10
|
+
test_detail: String type. If tests passed, describe which test cases were executed. If tests failed, describe each failed test case and the failure reason.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@function_tool(
|
|
15
|
+
name_override="test_completion", description_override=TEST_COMPLETION_DOCS, failure_error_function=None
|
|
16
|
+
)
|
|
17
|
+
async def test_completion(context: RunContextWrapper[CodeAgentContext], is_passed: int, test_detail: str) -> str:
|
|
18
|
+
# 获取 session_id
|
|
19
|
+
test_result = "Tests passed" if is_passed == 1 else "Tests failed"
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
"is_passed": is_passed,
|
|
23
|
+
"test_detail": test_result + ":" + test_detail
|
|
24
|
+
}
|
|
25
|
+
|