alita-sdk 0.3.462__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.
Files changed (261) hide show
  1. alita_sdk/cli/agent/__init__.py +5 -0
  2. alita_sdk/cli/agent/default.py +258 -0
  3. alita_sdk/cli/agent_executor.py +15 -3
  4. alita_sdk/cli/agent_loader.py +56 -8
  5. alita_sdk/cli/agent_ui.py +93 -31
  6. alita_sdk/cli/agents.py +2274 -230
  7. alita_sdk/cli/callbacks.py +96 -25
  8. alita_sdk/cli/cli.py +10 -1
  9. alita_sdk/cli/config.py +162 -9
  10. alita_sdk/cli/context/__init__.py +30 -0
  11. alita_sdk/cli/context/cleanup.py +198 -0
  12. alita_sdk/cli/context/manager.py +731 -0
  13. alita_sdk/cli/context/message.py +285 -0
  14. alita_sdk/cli/context/strategies.py +289 -0
  15. alita_sdk/cli/context/token_estimation.py +127 -0
  16. alita_sdk/cli/input_handler.py +419 -0
  17. alita_sdk/cli/inventory.py +1073 -0
  18. alita_sdk/cli/testcases/__init__.py +94 -0
  19. alita_sdk/cli/testcases/data_generation.py +119 -0
  20. alita_sdk/cli/testcases/discovery.py +96 -0
  21. alita_sdk/cli/testcases/executor.py +84 -0
  22. alita_sdk/cli/testcases/logger.py +85 -0
  23. alita_sdk/cli/testcases/parser.py +172 -0
  24. alita_sdk/cli/testcases/prompts.py +91 -0
  25. alita_sdk/cli/testcases/reporting.py +125 -0
  26. alita_sdk/cli/testcases/setup.py +108 -0
  27. alita_sdk/cli/testcases/test_runner.py +282 -0
  28. alita_sdk/cli/testcases/utils.py +39 -0
  29. alita_sdk/cli/testcases/validation.py +90 -0
  30. alita_sdk/cli/testcases/workflow.py +196 -0
  31. alita_sdk/cli/toolkit.py +14 -17
  32. alita_sdk/cli/toolkit_loader.py +35 -5
  33. alita_sdk/cli/tools/__init__.py +36 -2
  34. alita_sdk/cli/tools/approval.py +224 -0
  35. alita_sdk/cli/tools/filesystem.py +910 -64
  36. alita_sdk/cli/tools/planning.py +389 -0
  37. alita_sdk/cli/tools/terminal.py +414 -0
  38. alita_sdk/community/__init__.py +72 -12
  39. alita_sdk/community/inventory/__init__.py +236 -0
  40. alita_sdk/community/inventory/config.py +257 -0
  41. alita_sdk/community/inventory/enrichment.py +2137 -0
  42. alita_sdk/community/inventory/extractors.py +1469 -0
  43. alita_sdk/community/inventory/ingestion.py +3172 -0
  44. alita_sdk/community/inventory/knowledge_graph.py +1457 -0
  45. alita_sdk/community/inventory/parsers/__init__.py +218 -0
  46. alita_sdk/community/inventory/parsers/base.py +295 -0
  47. alita_sdk/community/inventory/parsers/csharp_parser.py +907 -0
  48. alita_sdk/community/inventory/parsers/go_parser.py +851 -0
  49. alita_sdk/community/inventory/parsers/html_parser.py +389 -0
  50. alita_sdk/community/inventory/parsers/java_parser.py +593 -0
  51. alita_sdk/community/inventory/parsers/javascript_parser.py +629 -0
  52. alita_sdk/community/inventory/parsers/kotlin_parser.py +768 -0
  53. alita_sdk/community/inventory/parsers/markdown_parser.py +362 -0
  54. alita_sdk/community/inventory/parsers/python_parser.py +604 -0
  55. alita_sdk/community/inventory/parsers/rust_parser.py +858 -0
  56. alita_sdk/community/inventory/parsers/swift_parser.py +832 -0
  57. alita_sdk/community/inventory/parsers/text_parser.py +322 -0
  58. alita_sdk/community/inventory/parsers/yaml_parser.py +370 -0
  59. alita_sdk/community/inventory/patterns/__init__.py +61 -0
  60. alita_sdk/community/inventory/patterns/ast_adapter.py +380 -0
  61. alita_sdk/community/inventory/patterns/loader.py +348 -0
  62. alita_sdk/community/inventory/patterns/registry.py +198 -0
  63. alita_sdk/community/inventory/presets.py +535 -0
  64. alita_sdk/community/inventory/retrieval.py +1403 -0
  65. alita_sdk/community/inventory/toolkit.py +173 -0
  66. alita_sdk/community/inventory/toolkit_utils.py +176 -0
  67. alita_sdk/community/inventory/visualize.py +1370 -0
  68. alita_sdk/configurations/__init__.py +1 -1
  69. alita_sdk/configurations/ado.py +141 -20
  70. alita_sdk/configurations/bitbucket.py +0 -3
  71. alita_sdk/configurations/confluence.py +76 -42
  72. alita_sdk/configurations/figma.py +76 -0
  73. alita_sdk/configurations/gitlab.py +17 -5
  74. alita_sdk/configurations/openapi.py +329 -0
  75. alita_sdk/configurations/qtest.py +72 -1
  76. alita_sdk/configurations/report_portal.py +96 -0
  77. alita_sdk/configurations/sharepoint.py +148 -0
  78. alita_sdk/configurations/testio.py +83 -0
  79. alita_sdk/runtime/clients/artifact.py +3 -3
  80. alita_sdk/runtime/clients/client.py +353 -48
  81. alita_sdk/runtime/clients/sandbox_client.py +0 -21
  82. alita_sdk/runtime/langchain/_constants_bkup.py +1318 -0
  83. alita_sdk/runtime/langchain/assistant.py +123 -26
  84. alita_sdk/runtime/langchain/constants.py +642 -1
  85. alita_sdk/runtime/langchain/document_loaders/AlitaExcelLoader.py +103 -60
  86. alita_sdk/runtime/langchain/document_loaders/AlitaJSONLinesLoader.py +77 -0
  87. alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +6 -3
  88. alita_sdk/runtime/langchain/document_loaders/AlitaPowerPointLoader.py +226 -7
  89. alita_sdk/runtime/langchain/document_loaders/AlitaTextLoader.py +5 -2
  90. alita_sdk/runtime/langchain/document_loaders/constants.py +12 -7
  91. alita_sdk/runtime/langchain/langraph_agent.py +279 -73
  92. alita_sdk/runtime/langchain/utils.py +82 -15
  93. alita_sdk/runtime/llms/preloaded.py +2 -6
  94. alita_sdk/runtime/skills/__init__.py +91 -0
  95. alita_sdk/runtime/skills/callbacks.py +498 -0
  96. alita_sdk/runtime/skills/discovery.py +540 -0
  97. alita_sdk/runtime/skills/executor.py +610 -0
  98. alita_sdk/runtime/skills/input_builder.py +371 -0
  99. alita_sdk/runtime/skills/models.py +330 -0
  100. alita_sdk/runtime/skills/registry.py +355 -0
  101. alita_sdk/runtime/skills/skill_runner.py +330 -0
  102. alita_sdk/runtime/toolkits/__init__.py +7 -0
  103. alita_sdk/runtime/toolkits/application.py +21 -9
  104. alita_sdk/runtime/toolkits/artifact.py +15 -5
  105. alita_sdk/runtime/toolkits/datasource.py +13 -6
  106. alita_sdk/runtime/toolkits/mcp.py +139 -251
  107. alita_sdk/runtime/toolkits/mcp_config.py +1048 -0
  108. alita_sdk/runtime/toolkits/planning.py +178 -0
  109. alita_sdk/runtime/toolkits/skill_router.py +238 -0
  110. alita_sdk/runtime/toolkits/subgraph.py +251 -6
  111. alita_sdk/runtime/toolkits/tools.py +238 -32
  112. alita_sdk/runtime/toolkits/vectorstore.py +11 -5
  113. alita_sdk/runtime/tools/__init__.py +3 -1
  114. alita_sdk/runtime/tools/application.py +20 -6
  115. alita_sdk/runtime/tools/artifact.py +511 -28
  116. alita_sdk/runtime/tools/data_analysis.py +183 -0
  117. alita_sdk/runtime/tools/function.py +43 -15
  118. alita_sdk/runtime/tools/image_generation.py +50 -44
  119. alita_sdk/runtime/tools/llm.py +852 -67
  120. alita_sdk/runtime/tools/loop.py +3 -1
  121. alita_sdk/runtime/tools/loop_output.py +3 -1
  122. alita_sdk/runtime/tools/mcp_remote_tool.py +25 -10
  123. alita_sdk/runtime/tools/mcp_server_tool.py +7 -6
  124. alita_sdk/runtime/tools/planning/__init__.py +36 -0
  125. alita_sdk/runtime/tools/planning/models.py +246 -0
  126. alita_sdk/runtime/tools/planning/wrapper.py +607 -0
  127. alita_sdk/runtime/tools/router.py +2 -4
  128. alita_sdk/runtime/tools/sandbox.py +9 -6
  129. alita_sdk/runtime/tools/skill_router.py +776 -0
  130. alita_sdk/runtime/tools/tool.py +3 -1
  131. alita_sdk/runtime/tools/vectorstore.py +7 -2
  132. alita_sdk/runtime/tools/vectorstore_base.py +51 -11
  133. alita_sdk/runtime/utils/AlitaCallback.py +137 -21
  134. alita_sdk/runtime/utils/constants.py +5 -1
  135. alita_sdk/runtime/utils/mcp_client.py +492 -0
  136. alita_sdk/runtime/utils/mcp_oauth.py +202 -5
  137. alita_sdk/runtime/utils/mcp_sse_client.py +36 -7
  138. alita_sdk/runtime/utils/mcp_tools_discovery.py +124 -0
  139. alita_sdk/runtime/utils/serialization.py +155 -0
  140. alita_sdk/runtime/utils/streamlit.py +6 -10
  141. alita_sdk/runtime/utils/toolkit_utils.py +16 -5
  142. alita_sdk/runtime/utils/utils.py +36 -0
  143. alita_sdk/tools/__init__.py +113 -29
  144. alita_sdk/tools/ado/repos/__init__.py +51 -33
  145. alita_sdk/tools/ado/repos/repos_wrapper.py +148 -89
  146. alita_sdk/tools/ado/test_plan/__init__.py +25 -9
  147. alita_sdk/tools/ado/test_plan/test_plan_wrapper.py +23 -1
  148. alita_sdk/tools/ado/utils.py +1 -18
  149. alita_sdk/tools/ado/wiki/__init__.py +25 -8
  150. alita_sdk/tools/ado/wiki/ado_wrapper.py +291 -22
  151. alita_sdk/tools/ado/work_item/__init__.py +26 -9
  152. alita_sdk/tools/ado/work_item/ado_wrapper.py +56 -3
  153. alita_sdk/tools/advanced_jira_mining/__init__.py +11 -8
  154. alita_sdk/tools/aws/delta_lake/__init__.py +13 -9
  155. alita_sdk/tools/aws/delta_lake/tool.py +5 -1
  156. alita_sdk/tools/azure_ai/search/__init__.py +11 -8
  157. alita_sdk/tools/azure_ai/search/api_wrapper.py +1 -1
  158. alita_sdk/tools/base/tool.py +5 -1
  159. alita_sdk/tools/base_indexer_toolkit.py +170 -45
  160. alita_sdk/tools/bitbucket/__init__.py +17 -12
  161. alita_sdk/tools/bitbucket/api_wrapper.py +59 -11
  162. alita_sdk/tools/bitbucket/cloud_api_wrapper.py +49 -35
  163. alita_sdk/tools/browser/__init__.py +5 -4
  164. alita_sdk/tools/carrier/__init__.py +5 -6
  165. alita_sdk/tools/carrier/backend_reports_tool.py +6 -6
  166. alita_sdk/tools/carrier/run_ui_test_tool.py +6 -6
  167. alita_sdk/tools/carrier/ui_reports_tool.py +5 -5
  168. alita_sdk/tools/chunkers/__init__.py +3 -1
  169. alita_sdk/tools/chunkers/code/treesitter/treesitter.py +37 -13
  170. alita_sdk/tools/chunkers/sematic/json_chunker.py +1 -0
  171. alita_sdk/tools/chunkers/sematic/markdown_chunker.py +97 -6
  172. alita_sdk/tools/chunkers/universal_chunker.py +270 -0
  173. alita_sdk/tools/cloud/aws/__init__.py +10 -7
  174. alita_sdk/tools/cloud/azure/__init__.py +10 -7
  175. alita_sdk/tools/cloud/gcp/__init__.py +10 -7
  176. alita_sdk/tools/cloud/k8s/__init__.py +10 -7
  177. alita_sdk/tools/code/linter/__init__.py +10 -8
  178. alita_sdk/tools/code/loaders/codesearcher.py +3 -2
  179. alita_sdk/tools/code/sonar/__init__.py +10 -7
  180. alita_sdk/tools/code_indexer_toolkit.py +73 -23
  181. alita_sdk/tools/confluence/__init__.py +21 -15
  182. alita_sdk/tools/confluence/api_wrapper.py +78 -23
  183. alita_sdk/tools/confluence/loader.py +4 -2
  184. alita_sdk/tools/custom_open_api/__init__.py +12 -5
  185. alita_sdk/tools/elastic/__init__.py +11 -8
  186. alita_sdk/tools/elitea_base.py +493 -30
  187. alita_sdk/tools/figma/__init__.py +58 -11
  188. alita_sdk/tools/figma/api_wrapper.py +1235 -143
  189. alita_sdk/tools/figma/figma_client.py +73 -0
  190. alita_sdk/tools/figma/toon_tools.py +2748 -0
  191. alita_sdk/tools/github/__init__.py +13 -14
  192. alita_sdk/tools/github/github_client.py +224 -100
  193. alita_sdk/tools/github/graphql_client_wrapper.py +119 -33
  194. alita_sdk/tools/github/schemas.py +14 -5
  195. alita_sdk/tools/github/tool.py +5 -1
  196. alita_sdk/tools/github/tool_prompts.py +9 -22
  197. alita_sdk/tools/gitlab/__init__.py +15 -11
  198. alita_sdk/tools/gitlab/api_wrapper.py +207 -41
  199. alita_sdk/tools/gitlab_org/__init__.py +10 -8
  200. alita_sdk/tools/gitlab_org/api_wrapper.py +63 -64
  201. alita_sdk/tools/google/bigquery/__init__.py +13 -12
  202. alita_sdk/tools/google/bigquery/tool.py +5 -1
  203. alita_sdk/tools/google_places/__init__.py +10 -8
  204. alita_sdk/tools/google_places/api_wrapper.py +1 -1
  205. alita_sdk/tools/jira/__init__.py +17 -11
  206. alita_sdk/tools/jira/api_wrapper.py +91 -40
  207. alita_sdk/tools/keycloak/__init__.py +11 -8
  208. alita_sdk/tools/localgit/__init__.py +9 -3
  209. alita_sdk/tools/localgit/local_git.py +62 -54
  210. alita_sdk/tools/localgit/tool.py +5 -1
  211. alita_sdk/tools/memory/__init__.py +11 -3
  212. alita_sdk/tools/non_code_indexer_toolkit.py +1 -0
  213. alita_sdk/tools/ocr/__init__.py +11 -8
  214. alita_sdk/tools/openapi/__init__.py +490 -114
  215. alita_sdk/tools/openapi/api_wrapper.py +1368 -0
  216. alita_sdk/tools/openapi/tool.py +20 -0
  217. alita_sdk/tools/pandas/__init__.py +20 -12
  218. alita_sdk/tools/pandas/api_wrapper.py +38 -25
  219. alita_sdk/tools/pandas/dataframe/generator/base.py +3 -1
  220. alita_sdk/tools/postman/__init__.py +11 -11
  221. alita_sdk/tools/pptx/__init__.py +10 -9
  222. alita_sdk/tools/pptx/pptx_wrapper.py +1 -1
  223. alita_sdk/tools/qtest/__init__.py +30 -10
  224. alita_sdk/tools/qtest/api_wrapper.py +430 -13
  225. alita_sdk/tools/rally/__init__.py +10 -8
  226. alita_sdk/tools/rally/api_wrapper.py +1 -1
  227. alita_sdk/tools/report_portal/__init__.py +12 -9
  228. alita_sdk/tools/salesforce/__init__.py +10 -9
  229. alita_sdk/tools/servicenow/__init__.py +17 -14
  230. alita_sdk/tools/servicenow/api_wrapper.py +1 -1
  231. alita_sdk/tools/sharepoint/__init__.py +10 -8
  232. alita_sdk/tools/sharepoint/api_wrapper.py +4 -4
  233. alita_sdk/tools/slack/__init__.py +10 -8
  234. alita_sdk/tools/slack/api_wrapper.py +2 -2
  235. alita_sdk/tools/sql/__init__.py +11 -9
  236. alita_sdk/tools/testio/__init__.py +10 -8
  237. alita_sdk/tools/testrail/__init__.py +11 -8
  238. alita_sdk/tools/testrail/api_wrapper.py +1 -1
  239. alita_sdk/tools/utils/__init__.py +9 -4
  240. alita_sdk/tools/utils/content_parser.py +77 -3
  241. alita_sdk/tools/utils/text_operations.py +410 -0
  242. alita_sdk/tools/utils/tool_prompts.py +79 -0
  243. alita_sdk/tools/vector_adapters/VectorStoreAdapter.py +17 -13
  244. alita_sdk/tools/xray/__init__.py +12 -9
  245. alita_sdk/tools/yagmail/__init__.py +9 -3
  246. alita_sdk/tools/zephyr/__init__.py +9 -7
  247. alita_sdk/tools/zephyr_enterprise/__init__.py +11 -8
  248. alita_sdk/tools/zephyr_essential/__init__.py +10 -8
  249. alita_sdk/tools/zephyr_essential/api_wrapper.py +30 -13
  250. alita_sdk/tools/zephyr_essential/client.py +2 -2
  251. alita_sdk/tools/zephyr_scale/__init__.py +11 -9
  252. alita_sdk/tools/zephyr_scale/api_wrapper.py +2 -2
  253. alita_sdk/tools/zephyr_squad/__init__.py +10 -8
  254. {alita_sdk-0.3.462.dist-info → alita_sdk-0.3.627.dist-info}/METADATA +147 -7
  255. alita_sdk-0.3.627.dist-info/RECORD +468 -0
  256. alita_sdk-0.3.627.dist-info/entry_points.txt +2 -0
  257. alita_sdk-0.3.462.dist-info/RECORD +0 -384
  258. alita_sdk-0.3.462.dist-info/entry_points.txt +0 -2
  259. {alita_sdk-0.3.462.dist-info → alita_sdk-0.3.627.dist-info}/WHEEL +0 -0
  260. {alita_sdk-0.3.462.dist-info → alita_sdk-0.3.627.dist-info}/licenses/LICENSE +0 -0
  261. {alita_sdk-0.3.462.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="The file query string")),
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 number from the diff for a changed file")),
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:
@@ -215,8 +230,22 @@ class GitLabAPIWrapper(CodeIndexerToolkit):
215
230
  except Exception as e:
216
231
  return f"Unable to get commit hash for {file_path} due to error:\n{e}"
217
232
 
218
- def _read_file(self, file_path: str, branch: str):
219
- return self.read_file(file_path, branch)
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))
220
249
 
221
250
  def create_branch(self, branch_name: str) -> str:
222
251
  try:
@@ -303,7 +332,30 @@ class GitLabAPIWrapper(CodeIndexerToolkit):
303
332
  except Exception as e:
304
333
  return "Unable to make comment due to error:\n" + str(e)
305
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
+
306
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
307
359
  try:
308
360
  self.set_active_branch(branch)
309
361
  self.repo_instance.files.get(file_path, branch)
@@ -320,50 +372,126 @@ class GitLabAPIWrapper(CodeIndexerToolkit):
320
372
  return "Created file " + file_path
321
373
 
322
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
323
377
  self.set_active_branch(branch)
324
378
  file = self.repo_instance.files.get(file_path, branch)
325
379
  return parse_file_content(file_name=file_path,
326
380
  file_content=file.decode(),
327
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)}")
328
442
 
329
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
+ """
330
466
  if branch == self.branch:
331
467
  return (
332
- "You're attempting to commit to the directly"
468
+ "You're attempting to commit directly "
333
469
  f"to the {self.branch} branch, which is protected. "
334
470
  "Please create a new branch and try again."
335
471
  )
336
472
  try:
337
- file_path: str = file_query.split("\n")[0]
338
- self.set_active_branch(branch)
339
- file_content = self.read_file(file_path, branch)
340
- updated_file_content = file_content
341
- for old, new in self.extract_old_new_pairs(file_query):
342
- if not old.strip():
343
- continue
344
- updated_file_content = updated_file_content.replace(old, new)
345
-
346
- 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:
347
482
  return (
348
- "File content was not updated because old content was not found or empty."
349
- "It may be helpful to use the read_file action to get "
350
- "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."
351
485
  )
352
486
 
353
- commit = {
354
- "branch": branch,
355
- "commit_message": "Create " + file_path,
356
- "actions": [
357
- {
358
- "action": "update",
359
- "file_path": file_path,
360
- "content": updated_file_content,
361
- }
362
- ],
363
- }
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)
364
494
 
365
- self.repo_instance.commits.create(commit)
366
- return "Updated file " + file_path
367
495
  except Exception as e:
368
496
  return "Unable to update file due to error:\n" + str(e)
369
497
 
@@ -415,10 +543,41 @@ class GitLabAPIWrapper(CodeIndexerToolkit):
415
543
  return res
416
544
 
417
545
  def create_pr_change_comment(self, pr_number: int, file_path: str, line_number: int, comment: str) -> str:
418
- mr = self.repo_instance.mergerequests.get(pr_number)
419
- position = {"position_type": "text", "new_path": file_path, "new_line": line_number}
420
- mr.discussions.create({"body": comment, "position": position})
421
- return "Comment added"
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}")
422
581
 
423
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):
424
583
  params = {}
@@ -445,6 +604,7 @@ class GitLabAPIWrapper(CodeIndexerToolkit):
445
604
  ]
446
605
 
447
606
  @extend_with_parent_available_tools
607
+ @extend_with_file_operations
448
608
  def get_available_tools(self):
449
609
  return [
450
610
  {
@@ -495,6 +655,12 @@ class GitLabAPIWrapper(CodeIndexerToolkit):
495
655
  "description": self.comment_on_issue.__doc__ or "Comment on an issue in the repository.",
496
656
  "args_schema": CommentOnIssueModel,
497
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
+ },
498
664
  {
499
665
  "name": "create_file",
500
666
  "ref": self.create_file,
@@ -510,7 +676,7 @@ class GitLabAPIWrapper(CodeIndexerToolkit):
510
676
  {
511
677
  "name": "update_file",
512
678
  "ref": self.update_file,
513
- "description": self.update_file.__doc__ or "Update the contents of a file in the repository.",
679
+ "description": EDIT_FILE_DESCRIPTION,
514
680
  "args_schema": UpdateFileModel,
515
681
  },
516
682
  {
@@ -540,7 +706,7 @@ class GitLabAPIWrapper(CodeIndexerToolkit):
540
706
  {
541
707
  "name": "create_pr_change_comment",
542
708
  "ref": self.create_pr_change_comment,
543
- "description": "Create a comment on a pull request change.",
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.",
544
710
  "args_schema": CreatePRChangeCommentModel,
545
711
  },
546
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, TOOLKIT_SPLITTER, get_max_toolkit_length
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,12 +23,10 @@ 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
32
  gitlab_configuration=(GitlabConfiguration, Field(description="GitLab configuration",
@@ -44,7 +43,6 @@ class AlitaGitlabSpaceToolkit(BaseToolkit):
44
43
  'metadata': {
45
44
  "label": "GitLab Org",
46
45
  "icon_url": None,
47
- "max_length": AlitaGitlabSpaceToolkit.toolkit_max_length,
48
46
  "categories": ["code repositories"],
49
47
  "extra_categories": ["gitlab", "git", "repository", "code", "version control"],
50
48
  }
@@ -62,18 +60,22 @@ class AlitaGitlabSpaceToolkit(BaseToolkit):
62
60
  **kwargs['gitlab_configuration'],
63
61
  }
64
62
  gitlab_wrapper = GitLabWorkspaceAPIWrapper(**wrapper_payload)
65
- prefix = clean_string(toolkit_name, AlitaGitlabSpaceToolkit.toolkit_max_length) + TOOLKIT_SPLITTER if toolkit_name else ''
66
63
  available_tools = gitlab_wrapper.get_available_tools()
67
64
  tools = []
68
65
  for tool in available_tools:
69
66
  if selected_tools:
70
67
  if tool["name"] not in selected_tools:
71
68
  continue
69
+ description = tool["description"]
70
+ if toolkit_name:
71
+ description = f"Toolkit: {toolkit_name}\n{description}"
72
+ description = description[:1000]
72
73
  tools.append(BaseAction(
73
74
  api_wrapper=gitlab_wrapper,
74
- name=prefix + tool['name'],
75
- description=tool["description"],
76
- 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"]}
77
79
  ))
78
80
  return cls(tools=tools)
79
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="File path followed by the old and new contents")),
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 update_file(self, file_path: str, update_query: str, branch: str, repository: Optional[str] = None) -> str:
375
- """Updates a file with new content.
376
- Parameters:
377
- branch (str): The name of the branch where update the file.
378
- update_query(str): Contains file contents.
379
- The old file contents is wrapped in OLD <<<< and >>>> OLD
380
- The new file contents is wrapped in NEW <<<< and >>>> NEW
381
- For example:
382
- /test/hello.txt
383
- OLD <<<<
384
- Hello Earth!
385
- >>>> OLD
386
- NEW <<<<
387
- Hello Mars!
388
- >>>> NEW
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
- file_content = self.read_file(file_path, branch, repository)
393
- updated_file_content = file_content
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 " + file_path,
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": updated_file_content,
416
+ "content": content,
412
417
  }
413
418
  ],
414
419
  }
415
420
  repo_instance.commits.create(commit)
416
- return "Updated file " + file_path
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": self.update_file.__doc__ or "Updates a file in the GitLab repository.",
643
+ "description": EDIT_FILE_DESCRIPTION,
645
644
  "args_schema": GitLabUpdateFile,
646
645
  "ref": self.update_file,
647
646
  },