alita-sdk 0.3.379__py3-none-any.whl → 0.3.627__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.
- alita_sdk/cli/__init__.py +10 -0
- alita_sdk/cli/__main__.py +17 -0
- alita_sdk/cli/agent/__init__.py +5 -0
- alita_sdk/cli/agent/default.py +258 -0
- alita_sdk/cli/agent_executor.py +156 -0
- alita_sdk/cli/agent_loader.py +245 -0
- alita_sdk/cli/agent_ui.py +228 -0
- alita_sdk/cli/agents.py +3113 -0
- alita_sdk/cli/callbacks.py +647 -0
- alita_sdk/cli/cli.py +168 -0
- alita_sdk/cli/config.py +306 -0
- alita_sdk/cli/context/__init__.py +30 -0
- alita_sdk/cli/context/cleanup.py +198 -0
- alita_sdk/cli/context/manager.py +731 -0
- alita_sdk/cli/context/message.py +285 -0
- alita_sdk/cli/context/strategies.py +289 -0
- alita_sdk/cli/context/token_estimation.py +127 -0
- alita_sdk/cli/formatting.py +182 -0
- alita_sdk/cli/input_handler.py +419 -0
- alita_sdk/cli/inventory.py +1073 -0
- alita_sdk/cli/mcp_loader.py +315 -0
- alita_sdk/cli/testcases/__init__.py +94 -0
- alita_sdk/cli/testcases/data_generation.py +119 -0
- alita_sdk/cli/testcases/discovery.py +96 -0
- alita_sdk/cli/testcases/executor.py +84 -0
- alita_sdk/cli/testcases/logger.py +85 -0
- alita_sdk/cli/testcases/parser.py +172 -0
- alita_sdk/cli/testcases/prompts.py +91 -0
- alita_sdk/cli/testcases/reporting.py +125 -0
- alita_sdk/cli/testcases/setup.py +108 -0
- alita_sdk/cli/testcases/test_runner.py +282 -0
- alita_sdk/cli/testcases/utils.py +39 -0
- alita_sdk/cli/testcases/validation.py +90 -0
- alita_sdk/cli/testcases/workflow.py +196 -0
- alita_sdk/cli/toolkit.py +327 -0
- alita_sdk/cli/toolkit_loader.py +85 -0
- alita_sdk/cli/tools/__init__.py +43 -0
- alita_sdk/cli/tools/approval.py +224 -0
- alita_sdk/cli/tools/filesystem.py +1751 -0
- alita_sdk/cli/tools/planning.py +389 -0
- alita_sdk/cli/tools/terminal.py +414 -0
- alita_sdk/community/__init__.py +72 -12
- alita_sdk/community/inventory/__init__.py +236 -0
- alita_sdk/community/inventory/config.py +257 -0
- alita_sdk/community/inventory/enrichment.py +2137 -0
- alita_sdk/community/inventory/extractors.py +1469 -0
- alita_sdk/community/inventory/ingestion.py +3172 -0
- alita_sdk/community/inventory/knowledge_graph.py +1457 -0
- alita_sdk/community/inventory/parsers/__init__.py +218 -0
- alita_sdk/community/inventory/parsers/base.py +295 -0
- alita_sdk/community/inventory/parsers/csharp_parser.py +907 -0
- alita_sdk/community/inventory/parsers/go_parser.py +851 -0
- alita_sdk/community/inventory/parsers/html_parser.py +389 -0
- alita_sdk/community/inventory/parsers/java_parser.py +593 -0
- alita_sdk/community/inventory/parsers/javascript_parser.py +629 -0
- alita_sdk/community/inventory/parsers/kotlin_parser.py +768 -0
- alita_sdk/community/inventory/parsers/markdown_parser.py +362 -0
- alita_sdk/community/inventory/parsers/python_parser.py +604 -0
- alita_sdk/community/inventory/parsers/rust_parser.py +858 -0
- alita_sdk/community/inventory/parsers/swift_parser.py +832 -0
- alita_sdk/community/inventory/parsers/text_parser.py +322 -0
- alita_sdk/community/inventory/parsers/yaml_parser.py +370 -0
- alita_sdk/community/inventory/patterns/__init__.py +61 -0
- alita_sdk/community/inventory/patterns/ast_adapter.py +380 -0
- alita_sdk/community/inventory/patterns/loader.py +348 -0
- alita_sdk/community/inventory/patterns/registry.py +198 -0
- alita_sdk/community/inventory/presets.py +535 -0
- alita_sdk/community/inventory/retrieval.py +1403 -0
- alita_sdk/community/inventory/toolkit.py +173 -0
- alita_sdk/community/inventory/toolkit_utils.py +176 -0
- alita_sdk/community/inventory/visualize.py +1370 -0
- alita_sdk/configurations/__init__.py +1 -1
- alita_sdk/configurations/ado.py +141 -20
- alita_sdk/configurations/bitbucket.py +94 -2
- alita_sdk/configurations/confluence.py +130 -1
- alita_sdk/configurations/figma.py +76 -0
- alita_sdk/configurations/gitlab.py +91 -0
- alita_sdk/configurations/jira.py +103 -0
- alita_sdk/configurations/openapi.py +329 -0
- alita_sdk/configurations/qtest.py +72 -1
- alita_sdk/configurations/report_portal.py +96 -0
- alita_sdk/configurations/sharepoint.py +148 -0
- alita_sdk/configurations/testio.py +83 -0
- alita_sdk/configurations/testrail.py +88 -0
- alita_sdk/configurations/xray.py +93 -0
- alita_sdk/configurations/zephyr_enterprise.py +93 -0
- alita_sdk/configurations/zephyr_essential.py +75 -0
- alita_sdk/runtime/clients/artifact.py +3 -3
- alita_sdk/runtime/clients/client.py +388 -46
- alita_sdk/runtime/clients/mcp_discovery.py +342 -0
- alita_sdk/runtime/clients/mcp_manager.py +262 -0
- alita_sdk/runtime/clients/sandbox_client.py +8 -21
- alita_sdk/runtime/langchain/_constants_bkup.py +1318 -0
- alita_sdk/runtime/langchain/assistant.py +157 -39
- alita_sdk/runtime/langchain/constants.py +647 -1
- alita_sdk/runtime/langchain/document_loaders/AlitaDocxMammothLoader.py +315 -3
- alita_sdk/runtime/langchain/document_loaders/AlitaExcelLoader.py +103 -60
- alita_sdk/runtime/langchain/document_loaders/AlitaJSONLinesLoader.py +77 -0
- alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +10 -4
- alita_sdk/runtime/langchain/document_loaders/AlitaPowerPointLoader.py +226 -7
- alita_sdk/runtime/langchain/document_loaders/AlitaTextLoader.py +5 -2
- alita_sdk/runtime/langchain/document_loaders/constants.py +40 -19
- alita_sdk/runtime/langchain/langraph_agent.py +405 -84
- alita_sdk/runtime/langchain/utils.py +106 -7
- alita_sdk/runtime/llms/preloaded.py +2 -6
- alita_sdk/runtime/models/mcp_models.py +61 -0
- alita_sdk/runtime/skills/__init__.py +91 -0
- alita_sdk/runtime/skills/callbacks.py +498 -0
- alita_sdk/runtime/skills/discovery.py +540 -0
- alita_sdk/runtime/skills/executor.py +610 -0
- alita_sdk/runtime/skills/input_builder.py +371 -0
- alita_sdk/runtime/skills/models.py +330 -0
- alita_sdk/runtime/skills/registry.py +355 -0
- alita_sdk/runtime/skills/skill_runner.py +330 -0
- alita_sdk/runtime/toolkits/__init__.py +31 -0
- alita_sdk/runtime/toolkits/application.py +29 -10
- alita_sdk/runtime/toolkits/artifact.py +20 -11
- alita_sdk/runtime/toolkits/datasource.py +13 -6
- alita_sdk/runtime/toolkits/mcp.py +783 -0
- alita_sdk/runtime/toolkits/mcp_config.py +1048 -0
- alita_sdk/runtime/toolkits/planning.py +178 -0
- alita_sdk/runtime/toolkits/skill_router.py +238 -0
- alita_sdk/runtime/toolkits/subgraph.py +251 -6
- alita_sdk/runtime/toolkits/tools.py +356 -69
- alita_sdk/runtime/toolkits/vectorstore.py +11 -5
- alita_sdk/runtime/tools/__init__.py +10 -3
- alita_sdk/runtime/tools/application.py +27 -6
- alita_sdk/runtime/tools/artifact.py +511 -28
- alita_sdk/runtime/tools/data_analysis.py +183 -0
- alita_sdk/runtime/tools/function.py +67 -35
- alita_sdk/runtime/tools/graph.py +10 -4
- alita_sdk/runtime/tools/image_generation.py +148 -46
- alita_sdk/runtime/tools/llm.py +1003 -128
- alita_sdk/runtime/tools/loop.py +3 -1
- alita_sdk/runtime/tools/loop_output.py +3 -1
- alita_sdk/runtime/tools/mcp_inspect_tool.py +284 -0
- alita_sdk/runtime/tools/mcp_remote_tool.py +181 -0
- alita_sdk/runtime/tools/mcp_server_tool.py +8 -5
- alita_sdk/runtime/tools/planning/__init__.py +36 -0
- alita_sdk/runtime/tools/planning/models.py +246 -0
- alita_sdk/runtime/tools/planning/wrapper.py +607 -0
- alita_sdk/runtime/tools/router.py +2 -4
- alita_sdk/runtime/tools/sandbox.py +65 -48
- alita_sdk/runtime/tools/skill_router.py +776 -0
- alita_sdk/runtime/tools/tool.py +3 -1
- alita_sdk/runtime/tools/vectorstore.py +9 -3
- alita_sdk/runtime/tools/vectorstore_base.py +70 -14
- alita_sdk/runtime/utils/AlitaCallback.py +137 -21
- alita_sdk/runtime/utils/constants.py +5 -1
- alita_sdk/runtime/utils/mcp_client.py +492 -0
- alita_sdk/runtime/utils/mcp_oauth.py +361 -0
- alita_sdk/runtime/utils/mcp_sse_client.py +434 -0
- alita_sdk/runtime/utils/mcp_tools_discovery.py +124 -0
- alita_sdk/runtime/utils/serialization.py +155 -0
- alita_sdk/runtime/utils/streamlit.py +40 -13
- alita_sdk/runtime/utils/toolkit_utils.py +30 -9
- alita_sdk/runtime/utils/utils.py +36 -0
- alita_sdk/tools/__init__.py +134 -35
- alita_sdk/tools/ado/repos/__init__.py +51 -32
- alita_sdk/tools/ado/repos/repos_wrapper.py +148 -89
- alita_sdk/tools/ado/test_plan/__init__.py +25 -9
- alita_sdk/tools/ado/test_plan/test_plan_wrapper.py +23 -1
- alita_sdk/tools/ado/utils.py +1 -18
- alita_sdk/tools/ado/wiki/__init__.py +25 -12
- alita_sdk/tools/ado/wiki/ado_wrapper.py +291 -22
- alita_sdk/tools/ado/work_item/__init__.py +26 -13
- alita_sdk/tools/ado/work_item/ado_wrapper.py +73 -11
- alita_sdk/tools/advanced_jira_mining/__init__.py +11 -8
- alita_sdk/tools/aws/delta_lake/__init__.py +13 -9
- alita_sdk/tools/aws/delta_lake/tool.py +5 -1
- alita_sdk/tools/azure_ai/search/__init__.py +11 -8
- alita_sdk/tools/azure_ai/search/api_wrapper.py +1 -1
- alita_sdk/tools/base/tool.py +5 -1
- alita_sdk/tools/base_indexer_toolkit.py +271 -84
- alita_sdk/tools/bitbucket/__init__.py +17 -11
- alita_sdk/tools/bitbucket/api_wrapper.py +59 -11
- alita_sdk/tools/bitbucket/cloud_api_wrapper.py +49 -35
- alita_sdk/tools/browser/__init__.py +5 -4
- alita_sdk/tools/carrier/__init__.py +5 -6
- alita_sdk/tools/carrier/backend_reports_tool.py +6 -6
- alita_sdk/tools/carrier/run_ui_test_tool.py +6 -6
- alita_sdk/tools/carrier/ui_reports_tool.py +5 -5
- alita_sdk/tools/chunkers/__init__.py +3 -1
- alita_sdk/tools/chunkers/code/treesitter/treesitter.py +37 -13
- alita_sdk/tools/chunkers/sematic/json_chunker.py +1 -0
- alita_sdk/tools/chunkers/sematic/markdown_chunker.py +97 -6
- alita_sdk/tools/chunkers/sematic/proposal_chunker.py +1 -1
- alita_sdk/tools/chunkers/universal_chunker.py +270 -0
- alita_sdk/tools/cloud/aws/__init__.py +10 -7
- alita_sdk/tools/cloud/azure/__init__.py +10 -7
- alita_sdk/tools/cloud/gcp/__init__.py +10 -7
- alita_sdk/tools/cloud/k8s/__init__.py +10 -7
- alita_sdk/tools/code/linter/__init__.py +10 -8
- alita_sdk/tools/code/loaders/codesearcher.py +3 -2
- alita_sdk/tools/code/sonar/__init__.py +11 -8
- alita_sdk/tools/code_indexer_toolkit.py +82 -22
- alita_sdk/tools/confluence/__init__.py +22 -16
- alita_sdk/tools/confluence/api_wrapper.py +107 -30
- alita_sdk/tools/confluence/loader.py +14 -2
- alita_sdk/tools/custom_open_api/__init__.py +12 -5
- alita_sdk/tools/elastic/__init__.py +11 -8
- alita_sdk/tools/elitea_base.py +493 -30
- alita_sdk/tools/figma/__init__.py +58 -11
- alita_sdk/tools/figma/api_wrapper.py +1235 -143
- alita_sdk/tools/figma/figma_client.py +73 -0
- alita_sdk/tools/figma/toon_tools.py +2748 -0
- alita_sdk/tools/github/__init__.py +14 -15
- alita_sdk/tools/github/github_client.py +224 -100
- alita_sdk/tools/github/graphql_client_wrapper.py +119 -33
- alita_sdk/tools/github/schemas.py +14 -5
- alita_sdk/tools/github/tool.py +5 -1
- alita_sdk/tools/github/tool_prompts.py +9 -22
- alita_sdk/tools/gitlab/__init__.py +16 -11
- alita_sdk/tools/gitlab/api_wrapper.py +218 -48
- alita_sdk/tools/gitlab_org/__init__.py +10 -9
- alita_sdk/tools/gitlab_org/api_wrapper.py +63 -64
- alita_sdk/tools/google/bigquery/__init__.py +13 -12
- alita_sdk/tools/google/bigquery/tool.py +5 -1
- alita_sdk/tools/google_places/__init__.py +11 -8
- alita_sdk/tools/google_places/api_wrapper.py +1 -1
- alita_sdk/tools/jira/__init__.py +17 -10
- alita_sdk/tools/jira/api_wrapper.py +92 -41
- alita_sdk/tools/keycloak/__init__.py +11 -8
- alita_sdk/tools/localgit/__init__.py +9 -3
- alita_sdk/tools/localgit/local_git.py +62 -54
- alita_sdk/tools/localgit/tool.py +5 -1
- alita_sdk/tools/memory/__init__.py +12 -4
- alita_sdk/tools/non_code_indexer_toolkit.py +1 -0
- alita_sdk/tools/ocr/__init__.py +11 -8
- alita_sdk/tools/openapi/__init__.py +491 -106
- alita_sdk/tools/openapi/api_wrapper.py +1368 -0
- alita_sdk/tools/openapi/tool.py +20 -0
- alita_sdk/tools/pandas/__init__.py +20 -12
- alita_sdk/tools/pandas/api_wrapper.py +38 -25
- alita_sdk/tools/pandas/dataframe/generator/base.py +3 -1
- alita_sdk/tools/postman/__init__.py +10 -9
- alita_sdk/tools/pptx/__init__.py +11 -10
- alita_sdk/tools/pptx/pptx_wrapper.py +1 -1
- alita_sdk/tools/qtest/__init__.py +31 -11
- alita_sdk/tools/qtest/api_wrapper.py +2135 -86
- alita_sdk/tools/rally/__init__.py +10 -9
- alita_sdk/tools/rally/api_wrapper.py +1 -1
- alita_sdk/tools/report_portal/__init__.py +12 -8
- alita_sdk/tools/salesforce/__init__.py +10 -8
- alita_sdk/tools/servicenow/__init__.py +17 -15
- alita_sdk/tools/servicenow/api_wrapper.py +1 -1
- alita_sdk/tools/sharepoint/__init__.py +10 -7
- alita_sdk/tools/sharepoint/api_wrapper.py +129 -38
- alita_sdk/tools/sharepoint/authorization_helper.py +191 -1
- alita_sdk/tools/sharepoint/utils.py +8 -2
- alita_sdk/tools/slack/__init__.py +10 -7
- alita_sdk/tools/slack/api_wrapper.py +2 -2
- alita_sdk/tools/sql/__init__.py +12 -9
- alita_sdk/tools/testio/__init__.py +10 -7
- alita_sdk/tools/testrail/__init__.py +11 -10
- alita_sdk/tools/testrail/api_wrapper.py +1 -1
- alita_sdk/tools/utils/__init__.py +9 -4
- alita_sdk/tools/utils/content_parser.py +103 -18
- alita_sdk/tools/utils/text_operations.py +410 -0
- alita_sdk/tools/utils/tool_prompts.py +79 -0
- alita_sdk/tools/vector_adapters/VectorStoreAdapter.py +30 -13
- alita_sdk/tools/xray/__init__.py +13 -9
- alita_sdk/tools/yagmail/__init__.py +9 -3
- alita_sdk/tools/zephyr/__init__.py +10 -7
- alita_sdk/tools/zephyr_enterprise/__init__.py +11 -7
- alita_sdk/tools/zephyr_essential/__init__.py +10 -7
- alita_sdk/tools/zephyr_essential/api_wrapper.py +30 -13
- alita_sdk/tools/zephyr_essential/client.py +2 -2
- alita_sdk/tools/zephyr_scale/__init__.py +11 -8
- alita_sdk/tools/zephyr_scale/api_wrapper.py +2 -2
- alita_sdk/tools/zephyr_squad/__init__.py +10 -7
- {alita_sdk-0.3.379.dist-info → alita_sdk-0.3.627.dist-info}/METADATA +154 -8
- alita_sdk-0.3.627.dist-info/RECORD +468 -0
- alita_sdk-0.3.627.dist-info/entry_points.txt +2 -0
- alita_sdk-0.3.379.dist-info/RECORD +0 -360
- {alita_sdk-0.3.379.dist-info → alita_sdk-0.3.627.dist-info}/WHEEL +0 -0
- {alita_sdk-0.3.379.dist-info → alita_sdk-0.3.627.dist-info}/licenses/LICENSE +0 -0
- {alita_sdk-0.3.379.dist-info → alita_sdk-0.3.627.dist-info}/top_level.txt +0 -0
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
import fnmatch
|
|
3
3
|
from typing import Any, Dict, List, Optional
|
|
4
4
|
|
|
5
|
+
from gitlab import GitlabGetError
|
|
5
6
|
from langchain_core.tools import ToolException
|
|
6
7
|
from pydantic import create_model, Field, model_validator, SecretStr, PrivateAttr
|
|
7
8
|
|
|
8
9
|
from ..code_indexer_toolkit import CodeIndexerToolkit
|
|
9
10
|
from ..utils.available_tools_decorator import extend_with_parent_available_tools
|
|
11
|
+
from ..elitea_base import extend_with_file_operations, BaseCodeToolApiWrapper
|
|
10
12
|
from ..utils.content_parser import parse_file_content
|
|
13
|
+
from .utils import get_position
|
|
14
|
+
from ..utils.tool_prompts import EDIT_FILE_DESCRIPTION, UPDATE_FILE_PROMPT_WITH_PATH
|
|
11
15
|
|
|
12
16
|
AppendFileModel = create_model(
|
|
13
17
|
"AppendFileModel",
|
|
@@ -19,7 +23,7 @@ DeleteFileModel = create_model(
|
|
|
19
23
|
"DeleteFileModel",
|
|
20
24
|
file_path=(str, Field(description="The path of the file")),
|
|
21
25
|
branch=(str, Field(description="The branch to delete the file from")),
|
|
22
|
-
commit_message=(str, Field(default=None, description="Commit message for deleting the file. Optional.")),
|
|
26
|
+
commit_message=(Optional[str], Field(default=None, description="Commit message for deleting the file. Optional.")),
|
|
23
27
|
)
|
|
24
28
|
CreateFileModel = create_model(
|
|
25
29
|
"CreateFileModel",
|
|
@@ -34,7 +38,7 @@ ReadFileModel = create_model(
|
|
|
34
38
|
)
|
|
35
39
|
UpdateFileModel = create_model(
|
|
36
40
|
"UpdateFileModel",
|
|
37
|
-
file_query=(str, Field(description=
|
|
41
|
+
file_query=(str, Field(description=UPDATE_FILE_PROMPT_WITH_PATH)),
|
|
38
42
|
branch=(str, Field(description="The branch to update the file in")),
|
|
39
43
|
)
|
|
40
44
|
CommentOnIssueModel = create_model(
|
|
@@ -51,6 +55,11 @@ CreatePullRequestModel = create_model(
|
|
|
51
55
|
pr_body=(str, Field(description="The body of the pull request")),
|
|
52
56
|
branch=(str, Field(description="The branch to create the pull request from")),
|
|
53
57
|
)
|
|
58
|
+
CommentOnPRModel = create_model(
|
|
59
|
+
"CommentOnPRModel",
|
|
60
|
+
pr_number=(int, Field(description="The number of the pull request/merge request")),
|
|
61
|
+
comment=(str, Field(description="The comment text to add")),
|
|
62
|
+
)
|
|
54
63
|
|
|
55
64
|
CreateBranchModel = create_model(
|
|
56
65
|
"CreateBranchModel",
|
|
@@ -58,7 +67,7 @@ CreateBranchModel = create_model(
|
|
|
58
67
|
)
|
|
59
68
|
ListBranchesInRepoModel = create_model(
|
|
60
69
|
"ListBranchesInRepoModel",
|
|
61
|
-
limit=(Optional[int], Field(default=20, description="Maximum number of branches to return. If not provided, all branches will be returned.")),
|
|
70
|
+
limit=(Optional[int], Field(default=20, description="Maximum number of branches to return. If not provided, all branches will be returned.", gt=0)),
|
|
62
71
|
branch_wildcard=(Optional[str], Field(default=None, description="Wildcard pattern to filter branches by name. If not provided, all branches will be returned."))
|
|
63
72
|
|
|
64
73
|
)
|
|
@@ -89,9 +98,9 @@ GetPRChangesModel = create_model(
|
|
|
89
98
|
CreatePRChangeCommentModel = create_model(
|
|
90
99
|
"CreatePRChangeCommentModel",
|
|
91
100
|
pr_number=(int, Field(description="GitLab Merge Request (Pull Request) number")),
|
|
92
|
-
file_path=(str, Field(description="File path of the changed file")),
|
|
93
|
-
line_number=(int, Field(description="Line
|
|
94
|
-
comment=(str, Field(description="Comment content")),
|
|
101
|
+
file_path=(str, Field(description="File path of the changed file as shown in the diff")),
|
|
102
|
+
line_number=(int, Field(description="Line index (0-based) from the diff output. Use get_pr_changes first to see the diff and identify the correct line index to comment on.")),
|
|
103
|
+
comment=(str, Field(description="Comment content to add to the specific line")),
|
|
95
104
|
)
|
|
96
105
|
GetCommitsModel = create_model(
|
|
97
106
|
"GetCommitsModel",
|
|
@@ -109,6 +118,12 @@ class GitLabAPIWrapper(CodeIndexerToolkit):
|
|
|
109
118
|
branch: Optional[str] = 'main'
|
|
110
119
|
_git: Any = PrivateAttr()
|
|
111
120
|
_active_branch: Any = PrivateAttr()
|
|
121
|
+
|
|
122
|
+
# Import file operation methods from BaseCodeToolApiWrapper
|
|
123
|
+
read_file_chunk = BaseCodeToolApiWrapper.read_file_chunk
|
|
124
|
+
read_multiple_files = BaseCodeToolApiWrapper.read_multiple_files
|
|
125
|
+
search_file = BaseCodeToolApiWrapper.search_file
|
|
126
|
+
edit_file = BaseCodeToolApiWrapper.edit_file
|
|
112
127
|
|
|
113
128
|
@staticmethod
|
|
114
129
|
def _sanitize_url(url: str) -> str:
|
|
@@ -117,7 +132,11 @@ class GitLabAPIWrapper(CodeIndexerToolkit):
|
|
|
117
132
|
|
|
118
133
|
@model_validator(mode='before')
|
|
119
134
|
@classmethod
|
|
120
|
-
def
|
|
135
|
+
def validate_toolkit_before(cls, values: Dict) -> Dict:
|
|
136
|
+
return super().validate_toolkit(values)
|
|
137
|
+
|
|
138
|
+
@model_validator(mode='after')
|
|
139
|
+
def validate_toolkit(self):
|
|
121
140
|
try:
|
|
122
141
|
import gitlab
|
|
123
142
|
except ImportError:
|
|
@@ -125,17 +144,17 @@ class GitLabAPIWrapper(CodeIndexerToolkit):
|
|
|
125
144
|
"python-gitlab is not installed. "
|
|
126
145
|
"Please install it with `pip install python-gitlab`"
|
|
127
146
|
)
|
|
128
|
-
|
|
147
|
+
self.repository = self._sanitize_url(self.repository)
|
|
129
148
|
g = gitlab.Gitlab(
|
|
130
|
-
url=
|
|
131
|
-
private_token=
|
|
149
|
+
url=self._sanitize_url(self.url),
|
|
150
|
+
private_token=self.private_token.get_secret_value(),
|
|
132
151
|
keep_base_url=True,
|
|
133
152
|
)
|
|
134
153
|
|
|
135
154
|
g.auth()
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
return
|
|
155
|
+
self._git = g
|
|
156
|
+
self._active_branch = self.branch
|
|
157
|
+
return self
|
|
139
158
|
|
|
140
159
|
@property
|
|
141
160
|
def repo_instance(self):
|
|
@@ -211,8 +230,22 @@ class GitLabAPIWrapper(CodeIndexerToolkit):
|
|
|
211
230
|
except Exception as e:
|
|
212
231
|
return f"Unable to get commit hash for {file_path} due to error:\n{e}"
|
|
213
232
|
|
|
214
|
-
def _read_file(self, file_path: str, branch: str):
|
|
215
|
-
|
|
233
|
+
def _read_file(self, file_path: str, branch: str, **kwargs):
|
|
234
|
+
"""
|
|
235
|
+
Read a file from specified branch with optional partial read support.
|
|
236
|
+
|
|
237
|
+
Parameters:
|
|
238
|
+
file_path: the file path
|
|
239
|
+
branch: the branch to read the file from
|
|
240
|
+
**kwargs: Additional parameters (offset, limit, head, tail) - currently ignored,
|
|
241
|
+
partial read handled client-side by base class methods
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
File content as string
|
|
245
|
+
"""
|
|
246
|
+
# Default to active branch if branch is None, consistent with other methods
|
|
247
|
+
branch = branch if branch else self._active_branch
|
|
248
|
+
return str(self.read_file(file_path, branch))
|
|
216
249
|
|
|
217
250
|
def create_branch(self, branch_name: str) -> str:
|
|
218
251
|
try:
|
|
@@ -299,7 +332,30 @@ class GitLabAPIWrapper(CodeIndexerToolkit):
|
|
|
299
332
|
except Exception as e:
|
|
300
333
|
return "Unable to make comment due to error:\n" + str(e)
|
|
301
334
|
|
|
335
|
+
def comment_on_pr(self, pr_number: int, comment: str) -> str:
|
|
336
|
+
"""
|
|
337
|
+
Add a comment to a pull request (merge request) in GitLab.
|
|
338
|
+
|
|
339
|
+
This method adds a general comment to the entire merge request,
|
|
340
|
+
not tied to specific code lines or file changes.
|
|
341
|
+
|
|
342
|
+
Parameters:
|
|
343
|
+
pr_number: GitLab Merge Request (Pull Request) number
|
|
344
|
+
comment: Comment text to add
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
Success message or error description
|
|
348
|
+
"""
|
|
349
|
+
try:
|
|
350
|
+
mr = self.repo_instance.mergerequests.get(pr_number)
|
|
351
|
+
mr.notes.create({"body": comment})
|
|
352
|
+
return "Commented on merge request " + str(pr_number)
|
|
353
|
+
except Exception as e:
|
|
354
|
+
return "Unable to make comment due to error:\n" + str(e)
|
|
355
|
+
|
|
302
356
|
def create_file(self, file_path: str, file_contents: str, branch: str) -> str:
|
|
357
|
+
# Default to active branch if branch is None
|
|
358
|
+
branch = branch if branch else self._active_branch
|
|
303
359
|
try:
|
|
304
360
|
self.set_active_branch(branch)
|
|
305
361
|
self.repo_instance.files.get(file_path, branch)
|
|
@@ -316,50 +372,126 @@ class GitLabAPIWrapper(CodeIndexerToolkit):
|
|
|
316
372
|
return "Created file " + file_path
|
|
317
373
|
|
|
318
374
|
def read_file(self, file_path: str, branch: str) -> str:
|
|
375
|
+
# Default to active branch if branch is None
|
|
376
|
+
branch = branch if branch else self._active_branch
|
|
319
377
|
self.set_active_branch(branch)
|
|
320
378
|
file = self.repo_instance.files.get(file_path, branch)
|
|
321
379
|
return parse_file_content(file_name=file_path,
|
|
322
380
|
file_content=file.decode(),
|
|
323
381
|
llm=self.llm)
|
|
382
|
+
|
|
383
|
+
def _write_file(
|
|
384
|
+
self,
|
|
385
|
+
file_path: str,
|
|
386
|
+
content: str,
|
|
387
|
+
branch: str = None,
|
|
388
|
+
commit_message: str = None
|
|
389
|
+
) -> str:
|
|
390
|
+
"""
|
|
391
|
+
Write content to a file (create or update).
|
|
392
|
+
|
|
393
|
+
Parameters:
|
|
394
|
+
file_path: Path to the file
|
|
395
|
+
content: New file content
|
|
396
|
+
branch: Branch name (uses active branch if None)
|
|
397
|
+
commit_message: Commit message
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
Success message
|
|
401
|
+
"""
|
|
402
|
+
try:
|
|
403
|
+
branch = branch or self._active_branch
|
|
404
|
+
|
|
405
|
+
if branch == self.branch:
|
|
406
|
+
raise ToolException(
|
|
407
|
+
f"Cannot commit directly to the {self.branch} branch. "
|
|
408
|
+
"Please create a new branch and try again."
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
self.set_active_branch(branch)
|
|
412
|
+
|
|
413
|
+
# Check if file exists
|
|
414
|
+
try:
|
|
415
|
+
self.repo_instance.files.get(file_path, branch)
|
|
416
|
+
# File exists, update it
|
|
417
|
+
commit = {
|
|
418
|
+
"branch": branch,
|
|
419
|
+
"commit_message": commit_message or f"Update {file_path}",
|
|
420
|
+
"actions": [
|
|
421
|
+
{
|
|
422
|
+
"action": "update",
|
|
423
|
+
"file_path": file_path,
|
|
424
|
+
"content": content,
|
|
425
|
+
}
|
|
426
|
+
],
|
|
427
|
+
}
|
|
428
|
+
self.repo_instance.commits.create(commit)
|
|
429
|
+
return f"Updated file {file_path}"
|
|
430
|
+
except:
|
|
431
|
+
# File doesn't exist, create it
|
|
432
|
+
data = {
|
|
433
|
+
"branch": branch,
|
|
434
|
+
"commit_message": commit_message or f"Create {file_path}",
|
|
435
|
+
"file_path": file_path,
|
|
436
|
+
"content": content,
|
|
437
|
+
}
|
|
438
|
+
self.repo_instance.files.create(data)
|
|
439
|
+
return f"Created file {file_path}"
|
|
440
|
+
except Exception as e:
|
|
441
|
+
raise ToolException(f"Unable to write file {file_path}: {str(e)}")
|
|
324
442
|
|
|
325
443
|
def update_file(self, file_query: str, branch: str) -> str:
|
|
444
|
+
"""
|
|
445
|
+
Update file using edit_file functionality.
|
|
446
|
+
|
|
447
|
+
This method now delegates to edit_file which uses OLD/NEW markers.
|
|
448
|
+
For backwards compatibility, it extracts the file_path from the query.
|
|
449
|
+
|
|
450
|
+
Expected format:
|
|
451
|
+
file_path
|
|
452
|
+
OLD <<<<
|
|
453
|
+
old content
|
|
454
|
+
>>>> OLD
|
|
455
|
+
NEW <<<<
|
|
456
|
+
new content
|
|
457
|
+
>>>> NEW
|
|
458
|
+
|
|
459
|
+
Args:
|
|
460
|
+
file_query: File path on first line, followed by OLD/NEW markers
|
|
461
|
+
branch: Branch to update the file in
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
Success or error message
|
|
465
|
+
"""
|
|
326
466
|
if branch == self.branch:
|
|
327
467
|
return (
|
|
328
|
-
"You're attempting to commit
|
|
468
|
+
"You're attempting to commit directly "
|
|
329
469
|
f"to the {self.branch} branch, which is protected. "
|
|
330
470
|
"Please create a new branch and try again."
|
|
331
471
|
)
|
|
332
472
|
try:
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if file_content == updated_file_content:
|
|
473
|
+
# Split into lines and find first non-empty line for file_path
|
|
474
|
+
lines = file_query.split("\n")
|
|
475
|
+
first_non_empty_idx = None
|
|
476
|
+
for i, line in enumerate(lines):
|
|
477
|
+
if line.strip():
|
|
478
|
+
first_non_empty_idx = i
|
|
479
|
+
break
|
|
480
|
+
|
|
481
|
+
if first_non_empty_idx is None:
|
|
343
482
|
return (
|
|
344
|
-
"
|
|
345
|
-
"
|
|
346
|
-
"the current file contents."
|
|
483
|
+
"Invalid file_query format. Expected first non-empty line to be the file path "
|
|
484
|
+
"followed by OLD/NEW blocks."
|
|
347
485
|
)
|
|
348
486
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
"content": updated_file_content,
|
|
357
|
-
}
|
|
358
|
-
],
|
|
359
|
-
}
|
|
487
|
+
file_path = lines[first_non_empty_idx].strip()
|
|
488
|
+
# Keep all lines after file_path line (preserving empty lines)
|
|
489
|
+
edit_content = "\n".join(lines[first_non_empty_idx + 1:])
|
|
490
|
+
|
|
491
|
+
# Delegate to edit_file method with appropriate commit message
|
|
492
|
+
commit_message = f"Update {file_path}"
|
|
493
|
+
return self.edit_file(file_path, edit_content, branch, commit_message)
|
|
360
494
|
|
|
361
|
-
self.repo_instance.commits.create(commit)
|
|
362
|
-
return "Updated file " + file_path
|
|
363
495
|
except Exception as e:
|
|
364
496
|
return "Unable to update file due to error:\n" + str(e)
|
|
365
497
|
|
|
@@ -411,10 +543,41 @@ class GitLabAPIWrapper(CodeIndexerToolkit):
|
|
|
411
543
|
return res
|
|
412
544
|
|
|
413
545
|
def create_pr_change_comment(self, pr_number: int, file_path: str, line_number: int, comment: str) -> str:
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
546
|
+
"""
|
|
547
|
+
Create a comment on a specific line in a pull request (merge request) change in GitLab.
|
|
548
|
+
|
|
549
|
+
This method adds an inline comment to a specific line in the diff of a merge request.
|
|
550
|
+
The line_number parameter refers to the line index in the diff output (0-based),
|
|
551
|
+
not the line number in the original file.
|
|
552
|
+
|
|
553
|
+
**Important**: Use get_pr_changes first to see the diff and identify the correct
|
|
554
|
+
line index for commenting.
|
|
555
|
+
|
|
556
|
+
Parameters:
|
|
557
|
+
pr_number: GitLab Merge Request number
|
|
558
|
+
file_path: Path to the file being commented on (as shown in the diff)
|
|
559
|
+
line_number: Line index from the diff (0-based index)
|
|
560
|
+
comment: Comment text to add
|
|
561
|
+
|
|
562
|
+
Returns:
|
|
563
|
+
Success message or error description
|
|
564
|
+
"""
|
|
565
|
+
try:
|
|
566
|
+
mr = self.repo_instance.mergerequests.get(pr_number)
|
|
567
|
+
except GitlabGetError as e:
|
|
568
|
+
if e.response_code == 404:
|
|
569
|
+
raise ToolException(f"Merge request number {pr_number} wasn't found: {e}")
|
|
570
|
+
raise ToolException(f"Error retrieving merge request {pr_number}: {e}")
|
|
571
|
+
|
|
572
|
+
try:
|
|
573
|
+
# Calculate proper position with SHA references and line mappings
|
|
574
|
+
position = get_position(file_path=file_path, line_number=line_number, mr=mr)
|
|
575
|
+
|
|
576
|
+
# Create discussion with the comment
|
|
577
|
+
mr.discussions.create({"body": comment, "position": position})
|
|
578
|
+
return f"Comment added successfully to line {line_number} in {file_path} on MR #{pr_number}"
|
|
579
|
+
except Exception as e:
|
|
580
|
+
raise ToolException(f"Failed to create comment on MR #{pr_number}: {e}")
|
|
418
581
|
|
|
419
582
|
def get_commits(self, sha: Optional[str] = None, path: Optional[str] = None, since: Optional[str] = None, until: Optional[str] = None, author: Optional[str] = None):
|
|
420
583
|
params = {}
|
|
@@ -441,6 +604,7 @@ class GitLabAPIWrapper(CodeIndexerToolkit):
|
|
|
441
604
|
]
|
|
442
605
|
|
|
443
606
|
@extend_with_parent_available_tools
|
|
607
|
+
@extend_with_file_operations
|
|
444
608
|
def get_available_tools(self):
|
|
445
609
|
return [
|
|
446
610
|
{
|
|
@@ -491,6 +655,12 @@ class GitLabAPIWrapper(CodeIndexerToolkit):
|
|
|
491
655
|
"description": self.comment_on_issue.__doc__ or "Comment on an issue in the repository.",
|
|
492
656
|
"args_schema": CommentOnIssueModel,
|
|
493
657
|
},
|
|
658
|
+
{
|
|
659
|
+
"name": "comment_on_pr",
|
|
660
|
+
"ref": self.comment_on_pr,
|
|
661
|
+
"description": self.comment_on_pr.__doc__ or "Comment on a pull request (merge request) in the repository.",
|
|
662
|
+
"args_schema": CommentOnPRModel,
|
|
663
|
+
},
|
|
494
664
|
{
|
|
495
665
|
"name": "create_file",
|
|
496
666
|
"ref": self.create_file,
|
|
@@ -506,7 +676,7 @@ class GitLabAPIWrapper(CodeIndexerToolkit):
|
|
|
506
676
|
{
|
|
507
677
|
"name": "update_file",
|
|
508
678
|
"ref": self.update_file,
|
|
509
|
-
"description":
|
|
679
|
+
"description": EDIT_FILE_DESCRIPTION,
|
|
510
680
|
"args_schema": UpdateFileModel,
|
|
511
681
|
},
|
|
512
682
|
{
|
|
@@ -536,7 +706,7 @@ class GitLabAPIWrapper(CodeIndexerToolkit):
|
|
|
536
706
|
{
|
|
537
707
|
"name": "create_pr_change_comment",
|
|
538
708
|
"ref": self.create_pr_change_comment,
|
|
539
|
-
"description": "Create
|
|
709
|
+
"description": self.create_pr_change_comment.__doc__ or "Create an inline comment on a specific line in a pull request change. Use get_pr_changes first to see the diff and identify the line index for commenting. The line_number is a 0-based index from the diff output, not the file line number.",
|
|
540
710
|
"args_schema": CreatePRChangeCommentModel,
|
|
541
711
|
},
|
|
542
712
|
{
|
|
@@ -6,8 +6,9 @@ from ..base.tool import BaseAction
|
|
|
6
6
|
from pydantic import create_model, BaseModel, ConfigDict, Field, SecretStr
|
|
7
7
|
|
|
8
8
|
from ..elitea_base import filter_missconfigured_index_tools
|
|
9
|
-
from ..utils import clean_string,
|
|
9
|
+
from ..utils import clean_string, get_max_toolkit_length
|
|
10
10
|
from ...configurations.gitlab import GitlabConfiguration
|
|
11
|
+
from ...runtime.utils.constants import TOOLKIT_NAME_META, TOOL_NAME_META, TOOLKIT_TYPE_META
|
|
11
12
|
|
|
12
13
|
name = "gitlab_org"
|
|
13
14
|
|
|
@@ -22,16 +23,12 @@ def get_tools(tool):
|
|
|
22
23
|
|
|
23
24
|
class AlitaGitlabSpaceToolkit(BaseToolkit):
|
|
24
25
|
tools: List[BaseTool] = []
|
|
25
|
-
toolkit_max_length: int = 0
|
|
26
26
|
|
|
27
27
|
@staticmethod
|
|
28
28
|
def toolkit_config_schema() -> BaseModel:
|
|
29
29
|
selected_tools = {x['name']: x['args_schema'].schema() for x in GitLabWorkspaceAPIWrapper.model_construct().get_available_tools()}
|
|
30
|
-
AlitaGitlabSpaceToolkit.toolkit_max_length = get_max_toolkit_length(selected_tools)
|
|
31
30
|
return create_model(
|
|
32
31
|
name,
|
|
33
|
-
name=(str, Field(description="Toolkit name", json_schema_extra={'toolkit_name': True,
|
|
34
|
-
'max_toolkit_length': AlitaGitlabSpaceToolkit.toolkit_max_length})),
|
|
35
32
|
gitlab_configuration=(GitlabConfiguration, Field(description="GitLab configuration",
|
|
36
33
|
json_schema_extra={
|
|
37
34
|
'configuration_types': ['gitlab']})),
|
|
@@ -63,18 +60,22 @@ class AlitaGitlabSpaceToolkit(BaseToolkit):
|
|
|
63
60
|
**kwargs['gitlab_configuration'],
|
|
64
61
|
}
|
|
65
62
|
gitlab_wrapper = GitLabWorkspaceAPIWrapper(**wrapper_payload)
|
|
66
|
-
prefix = clean_string(toolkit_name, AlitaGitlabSpaceToolkit.toolkit_max_length) + TOOLKIT_SPLITTER if toolkit_name else ''
|
|
67
63
|
available_tools = gitlab_wrapper.get_available_tools()
|
|
68
64
|
tools = []
|
|
69
65
|
for tool in available_tools:
|
|
70
66
|
if selected_tools:
|
|
71
67
|
if tool["name"] not in selected_tools:
|
|
72
68
|
continue
|
|
69
|
+
description = tool["description"]
|
|
70
|
+
if toolkit_name:
|
|
71
|
+
description = f"Toolkit: {toolkit_name}\n{description}"
|
|
72
|
+
description = description[:1000]
|
|
73
73
|
tools.append(BaseAction(
|
|
74
74
|
api_wrapper=gitlab_wrapper,
|
|
75
|
-
name=
|
|
76
|
-
description=
|
|
77
|
-
args_schema=tool["args_schema"]
|
|
75
|
+
name=tool['name'],
|
|
76
|
+
description=description,
|
|
77
|
+
args_schema=tool["args_schema"],
|
|
78
|
+
metadata={TOOLKIT_NAME_META: toolkit_name, TOOLKIT_TYPE_META: name, TOOL_NAME_META: tool["name"]} if toolkit_name else {TOOL_NAME_META: tool["name"]}
|
|
78
79
|
))
|
|
79
80
|
return cls(tools=tools)
|
|
80
81
|
|
|
@@ -8,8 +8,9 @@ from langchain_core.tools import ToolException
|
|
|
8
8
|
from pydantic import model_validator, PrivateAttr, create_model, SecretStr
|
|
9
9
|
from pydantic.fields import Field
|
|
10
10
|
|
|
11
|
-
from ..elitea_base import BaseToolApiWrapper
|
|
11
|
+
from ..elitea_base import BaseToolApiWrapper, BaseCodeToolApiWrapper
|
|
12
12
|
from ..gitlab.utils import get_diff_w_position, get_position
|
|
13
|
+
from ..utils.tool_prompts import EDIT_FILE_DESCRIPTION, UPDATE_FILE_PROMPT_NO_PATH
|
|
13
14
|
|
|
14
15
|
logger = logging.getLogger(__name__)
|
|
15
16
|
|
|
@@ -24,7 +25,7 @@ GitLabCreateBranch = create_model(
|
|
|
24
25
|
GitLabListBranches = create_model(
|
|
25
26
|
"GitLabListBranchesModel",
|
|
26
27
|
repository=(Optional[str], Field(description="Name of the repository", default=None)),
|
|
27
|
-
limit=(Optional[int], Field(description="Maximum number of branches to return. If not provided, all branches will be returned.", default=20)),
|
|
28
|
+
limit=(Optional[int], Field(description="Maximum number of branches to return. If not provided, all branches will be returned.", default=20, gt=0)),
|
|
28
29
|
branch_wildcard=(Optional[str], Field(description="Wildcard pattern to filter branches by name. If not provided, all branches will be returned.", default=None))
|
|
29
30
|
)
|
|
30
31
|
|
|
@@ -75,7 +76,7 @@ GitLabReadFile = create_model(
|
|
|
75
76
|
GitLabUpdateFile = create_model(
|
|
76
77
|
"GitLabUpdateFile",
|
|
77
78
|
file_path=(str, Field(description="Path of the file to update")),
|
|
78
|
-
update_query=(str, Field(description=
|
|
79
|
+
update_query=(str, Field(description=UPDATE_FILE_PROMPT_NO_PATH)),
|
|
79
80
|
repository=(Optional[str], Field(description="Name of the repository", default=None)),
|
|
80
81
|
branch=(str, Field(description=branch_description))
|
|
81
82
|
)
|
|
@@ -159,6 +160,9 @@ class GitLabWorkspaceAPIWrapper(BaseToolApiWrapper):
|
|
|
159
160
|
repo_instances: Dict[str, Any] = {}
|
|
160
161
|
_active_branch: Optional[str] = PrivateAttr(default='main')
|
|
161
162
|
|
|
163
|
+
# Reuse common file helpers from BaseCodeToolApiWrapper where applicable
|
|
164
|
+
edit_file = BaseCodeToolApiWrapper.edit_file
|
|
165
|
+
|
|
162
166
|
class Config:
|
|
163
167
|
arbitrary_types_allowed = True
|
|
164
168
|
|
|
@@ -371,51 +375,76 @@ class GitLabWorkspaceAPIWrapper(BaseToolApiWrapper):
|
|
|
371
375
|
except Exception as e:
|
|
372
376
|
return ToolException(e)
|
|
373
377
|
|
|
374
|
-
def
|
|
375
|
-
"""
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
378
|
+
def _read_file(self, file_path: str, branch: str, **kwargs) -> str:
|
|
379
|
+
"""
|
|
380
|
+
Internal read_file used by BaseCodeToolApiWrapper.edit_file.
|
|
381
|
+
Delegates to the public `read_file` implementation which supports an optional repository argument.
|
|
382
|
+
The repository may be passed via kwargs or provided earlier through `update_file` which sets
|
|
383
|
+
a temporary attribute `_tmp_repository_for_edit`.
|
|
384
|
+
"""
|
|
385
|
+
# Repository from temporary context, then None
|
|
386
|
+
repository = getattr(self, "_tmp_repository_for_edit", None)
|
|
387
|
+
try:
|
|
388
|
+
# Public read_file signature: read_file(file_path, branch, repository=None)
|
|
389
|
+
return self.read_file(file_path, branch, repository)
|
|
390
|
+
except Exception as e:
|
|
391
|
+
raise ToolException(f"Can't extract file content (`{file_path}`) due to error:\n{str(e)}")
|
|
392
|
+
|
|
393
|
+
def _write_file(self, file_path: str, content: str, branch: str = None, commit_message: str = None) -> str:
|
|
394
|
+
"""
|
|
395
|
+
Write content to a file (update only) in the specified GitLab repository.
|
|
396
|
+
|
|
397
|
+
This implementation follows the same commit flow as the previous `update_file`:
|
|
398
|
+
it does not attempt to create the file when it is missing — it will always
|
|
399
|
+
create a commit with a single `update` action. If the file does not exist on
|
|
400
|
+
the target branch, the underlying GitLab API will typically return an error.
|
|
389
401
|
"""
|
|
390
402
|
try:
|
|
403
|
+
branch = branch if branch else (self._active_branch if self._active_branch else self.branch)
|
|
404
|
+
# pick repository from temporary edit context
|
|
405
|
+
repository = getattr(self, "_tmp_repository_for_edit", None)
|
|
391
406
|
repo_instance = self._get_repo(repository)
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
for old, new in self.extract_old_new_pairs(update_query):
|
|
395
|
-
if not old.strip():
|
|
396
|
-
continue
|
|
397
|
-
updated_file_content = updated_file_content.replace(old, new)
|
|
398
|
-
if file_content == updated_file_content:
|
|
399
|
-
return (
|
|
400
|
-
"File content was not updated because old content was not found or empty."
|
|
401
|
-
"It may be helpful to use the read_file action to get "
|
|
402
|
-
"the current file contents."
|
|
403
|
-
)
|
|
407
|
+
|
|
408
|
+
# Always perform an 'update' action commit (do not create file when missing)
|
|
404
409
|
commit = {
|
|
405
410
|
"branch": branch,
|
|
406
|
-
"commit_message": "Update "
|
|
411
|
+
"commit_message": commit_message or f"Update {file_path}",
|
|
407
412
|
"actions": [
|
|
408
413
|
{
|
|
409
414
|
"action": "update",
|
|
410
415
|
"file_path": file_path,
|
|
411
|
-
"content":
|
|
416
|
+
"content": content,
|
|
412
417
|
}
|
|
413
418
|
],
|
|
414
419
|
}
|
|
415
420
|
repo_instance.commits.create(commit)
|
|
416
|
-
return "Updated file "
|
|
421
|
+
return f"Updated file {file_path}"
|
|
422
|
+
except ToolException:
|
|
423
|
+
raise
|
|
424
|
+
except Exception as e:
|
|
425
|
+
return ToolException(f"Unable to write file due to error: {str(e)}")
|
|
426
|
+
|
|
427
|
+
def update_file(self, file_path: str, update_query: str, branch: str, repository: Optional[str] = None) -> str:
|
|
428
|
+
"""Updates a file with new content using OLD/NEW markers by delegating to `edit_file`.
|
|
429
|
+
|
|
430
|
+
The method sets a temporary repository context so that `edit_file`'s internal
|
|
431
|
+
calls to `_read_file` and `_write_file` operate on the requested repository.
|
|
432
|
+
"""
|
|
433
|
+
# Set temporary repository context used by _read_file/_write_file
|
|
434
|
+
self._tmp_repository_for_edit = repository
|
|
435
|
+
try:
|
|
436
|
+
commit_message = f"Update {file_path}"
|
|
437
|
+
return self.edit_file(file_path=file_path, file_query=update_query, branch=branch, commit_message=commit_message)
|
|
438
|
+
except ToolException as e:
|
|
439
|
+
return str(e)
|
|
417
440
|
except Exception as e:
|
|
418
441
|
return ToolException(f"Unable to update file due to error: {str(e)}")
|
|
442
|
+
finally:
|
|
443
|
+
# Clear temporary context
|
|
444
|
+
try:
|
|
445
|
+
delattr(self, "_tmp_repository_for_edit")
|
|
446
|
+
except Exception:
|
|
447
|
+
self._tmp_repository_for_edit = None
|
|
419
448
|
|
|
420
449
|
def delete_file(self, file_path: str, branch: str, repository: Optional[str] = None) -> str:
|
|
421
450
|
"""Deletes a file from the repo."""
|
|
@@ -428,36 +457,6 @@ class GitLabWorkspaceAPIWrapper(BaseToolApiWrapper):
|
|
|
428
457
|
except Exception as e:
|
|
429
458
|
return ToolException(f"Unable to delete file due to error: {str(e)}")
|
|
430
459
|
|
|
431
|
-
def extract_old_new_pairs(self, file_query):
|
|
432
|
-
"""Extract old and new content pairs from the file query."""
|
|
433
|
-
code_lines = file_query.split("\n")
|
|
434
|
-
old_contents = []
|
|
435
|
-
new_contents = []
|
|
436
|
-
in_old_section = False
|
|
437
|
-
in_new_section = False
|
|
438
|
-
current_section_content = []
|
|
439
|
-
for line in code_lines:
|
|
440
|
-
if "OLD <<<" in line:
|
|
441
|
-
in_old_section = True
|
|
442
|
-
current_section_content = []
|
|
443
|
-
continue
|
|
444
|
-
if ">>>> OLD" in line:
|
|
445
|
-
in_old_section = False
|
|
446
|
-
old_contents.append("\n".join(current_section_content).strip())
|
|
447
|
-
current_section_content = []
|
|
448
|
-
continue
|
|
449
|
-
if "NEW <<<" in line:
|
|
450
|
-
in_new_section = True
|
|
451
|
-
current_section_content = []
|
|
452
|
-
continue
|
|
453
|
-
if ">>>> NEW" in line:
|
|
454
|
-
in_new_section = False
|
|
455
|
-
new_contents.append("\n".join(current_section_content).strip())
|
|
456
|
-
current_section_content = []
|
|
457
|
-
continue
|
|
458
|
-
if in_old_section or in_new_section:
|
|
459
|
-
current_section_content.append(line)
|
|
460
|
-
return list(zip(old_contents, new_contents))
|
|
461
460
|
|
|
462
461
|
def append_file(self, file_path: str, content: str, branch: str, repository: Optional[str] = None) -> str:
|
|
463
462
|
"""
|
|
@@ -641,7 +640,7 @@ class GitLabWorkspaceAPIWrapper(BaseToolApiWrapper):
|
|
|
641
640
|
},
|
|
642
641
|
{
|
|
643
642
|
"name": "update_file",
|
|
644
|
-
"description":
|
|
643
|
+
"description": EDIT_FILE_DESCRIPTION,
|
|
645
644
|
"args_schema": GitLabUpdateFile,
|
|
646
645
|
"ref": self.update_file,
|
|
647
646
|
},
|