alita-sdk 0.3.554__py3-none-any.whl → 0.3.602__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.

Potentially problematic release.


This version of alita-sdk might be problematic. Click here for more details.

Files changed (116) hide show
  1. alita_sdk/cli/agent_executor.py +2 -1
  2. alita_sdk/cli/agent_loader.py +34 -4
  3. alita_sdk/cli/agents.py +433 -203
  4. alita_sdk/configurations/openapi.py +227 -15
  5. alita_sdk/runtime/clients/client.py +4 -2
  6. alita_sdk/runtime/langchain/_constants_bkup.py +1318 -0
  7. alita_sdk/runtime/langchain/assistant.py +61 -11
  8. alita_sdk/runtime/langchain/constants.py +419 -171
  9. alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +4 -2
  10. alita_sdk/runtime/langchain/document_loaders/AlitaTextLoader.py +5 -2
  11. alita_sdk/runtime/langchain/langraph_agent.py +106 -21
  12. alita_sdk/runtime/langchain/utils.py +30 -14
  13. alita_sdk/runtime/toolkits/__init__.py +3 -0
  14. alita_sdk/runtime/toolkits/artifact.py +2 -1
  15. alita_sdk/runtime/toolkits/mcp.py +6 -3
  16. alita_sdk/runtime/toolkits/mcp_config.py +1048 -0
  17. alita_sdk/runtime/toolkits/skill_router.py +2 -2
  18. alita_sdk/runtime/toolkits/tools.py +64 -2
  19. alita_sdk/runtime/toolkits/vectorstore.py +1 -1
  20. alita_sdk/runtime/tools/artifact.py +15 -0
  21. alita_sdk/runtime/tools/data_analysis.py +183 -0
  22. alita_sdk/runtime/tools/llm.py +30 -11
  23. alita_sdk/runtime/tools/mcp_server_tool.py +6 -3
  24. alita_sdk/runtime/tools/router.py +2 -4
  25. alita_sdk/runtime/tools/sandbox.py +9 -6
  26. alita_sdk/runtime/utils/constants.py +5 -1
  27. alita_sdk/runtime/utils/mcp_client.py +1 -1
  28. alita_sdk/runtime/utils/mcp_sse_client.py +1 -1
  29. alita_sdk/runtime/utils/toolkit_utils.py +2 -0
  30. alita_sdk/tools/__init__.py +3 -1
  31. alita_sdk/tools/ado/repos/__init__.py +26 -8
  32. alita_sdk/tools/ado/repos/repos_wrapper.py +78 -52
  33. alita_sdk/tools/ado/test_plan/__init__.py +3 -2
  34. alita_sdk/tools/ado/test_plan/test_plan_wrapper.py +23 -1
  35. alita_sdk/tools/ado/utils.py +1 -18
  36. alita_sdk/tools/ado/wiki/__init__.py +2 -1
  37. alita_sdk/tools/ado/wiki/ado_wrapper.py +23 -1
  38. alita_sdk/tools/ado/work_item/__init__.py +3 -2
  39. alita_sdk/tools/ado/work_item/ado_wrapper.py +23 -1
  40. alita_sdk/tools/advanced_jira_mining/__init__.py +2 -1
  41. alita_sdk/tools/aws/delta_lake/__init__.py +2 -1
  42. alita_sdk/tools/azure_ai/search/__init__.py +2 -1
  43. alita_sdk/tools/azure_ai/search/api_wrapper.py +1 -1
  44. alita_sdk/tools/base_indexer_toolkit.py +15 -6
  45. alita_sdk/tools/bitbucket/__init__.py +2 -1
  46. alita_sdk/tools/bitbucket/api_wrapper.py +1 -1
  47. alita_sdk/tools/bitbucket/cloud_api_wrapper.py +3 -3
  48. alita_sdk/tools/browser/__init__.py +1 -1
  49. alita_sdk/tools/carrier/__init__.py +1 -1
  50. alita_sdk/tools/chunkers/code/treesitter/treesitter.py +37 -13
  51. alita_sdk/tools/cloud/aws/__init__.py +2 -1
  52. alita_sdk/tools/cloud/azure/__init__.py +2 -1
  53. alita_sdk/tools/cloud/gcp/__init__.py +2 -1
  54. alita_sdk/tools/cloud/k8s/__init__.py +2 -1
  55. alita_sdk/tools/code/linter/__init__.py +2 -1
  56. alita_sdk/tools/code/sonar/__init__.py +2 -1
  57. alita_sdk/tools/code_indexer_toolkit.py +19 -2
  58. alita_sdk/tools/confluence/__init__.py +7 -6
  59. alita_sdk/tools/confluence/api_wrapper.py +2 -2
  60. alita_sdk/tools/custom_open_api/__init__.py +2 -1
  61. alita_sdk/tools/elastic/__init__.py +2 -1
  62. alita_sdk/tools/elitea_base.py +28 -9
  63. alita_sdk/tools/figma/__init__.py +52 -6
  64. alita_sdk/tools/figma/api_wrapper.py +1158 -123
  65. alita_sdk/tools/figma/figma_client.py +73 -0
  66. alita_sdk/tools/figma/toon_tools.py +2748 -0
  67. alita_sdk/tools/github/__init__.py +2 -1
  68. alita_sdk/tools/github/github_client.py +56 -92
  69. alita_sdk/tools/github/schemas.py +4 -4
  70. alita_sdk/tools/gitlab/__init__.py +2 -1
  71. alita_sdk/tools/gitlab/api_wrapper.py +118 -38
  72. alita_sdk/tools/gitlab_org/__init__.py +2 -1
  73. alita_sdk/tools/gitlab_org/api_wrapper.py +60 -62
  74. alita_sdk/tools/google/bigquery/__init__.py +2 -1
  75. alita_sdk/tools/google_places/__init__.py +2 -1
  76. alita_sdk/tools/jira/__init__.py +2 -1
  77. alita_sdk/tools/keycloak/__init__.py +2 -1
  78. alita_sdk/tools/localgit/__init__.py +2 -1
  79. alita_sdk/tools/memory/__init__.py +1 -1
  80. alita_sdk/tools/ocr/__init__.py +2 -1
  81. alita_sdk/tools/openapi/__init__.py +227 -15
  82. alita_sdk/tools/openapi/api_wrapper.py +1287 -802
  83. alita_sdk/tools/pandas/__init__.py +11 -5
  84. alita_sdk/tools/pandas/api_wrapper.py +38 -25
  85. alita_sdk/tools/postman/__init__.py +2 -1
  86. alita_sdk/tools/pptx/__init__.py +2 -1
  87. alita_sdk/tools/qtest/__init__.py +21 -2
  88. alita_sdk/tools/qtest/api_wrapper.py +430 -13
  89. alita_sdk/tools/rally/__init__.py +2 -1
  90. alita_sdk/tools/rally/api_wrapper.py +1 -1
  91. alita_sdk/tools/report_portal/__init__.py +2 -1
  92. alita_sdk/tools/salesforce/__init__.py +2 -1
  93. alita_sdk/tools/servicenow/__init__.py +2 -1
  94. alita_sdk/tools/sharepoint/__init__.py +2 -1
  95. alita_sdk/tools/sharepoint/api_wrapper.py +2 -2
  96. alita_sdk/tools/slack/__init__.py +3 -2
  97. alita_sdk/tools/slack/api_wrapper.py +2 -2
  98. alita_sdk/tools/sql/__init__.py +3 -2
  99. alita_sdk/tools/testio/__init__.py +2 -1
  100. alita_sdk/tools/testrail/__init__.py +2 -1
  101. alita_sdk/tools/utils/content_parser.py +77 -3
  102. alita_sdk/tools/utils/text_operations.py +163 -71
  103. alita_sdk/tools/xray/__init__.py +3 -2
  104. alita_sdk/tools/yagmail/__init__.py +2 -1
  105. alita_sdk/tools/zephyr/__init__.py +2 -1
  106. alita_sdk/tools/zephyr_enterprise/__init__.py +2 -1
  107. alita_sdk/tools/zephyr_essential/__init__.py +2 -1
  108. alita_sdk/tools/zephyr_scale/__init__.py +3 -2
  109. alita_sdk/tools/zephyr_scale/api_wrapper.py +2 -2
  110. alita_sdk/tools/zephyr_squad/__init__.py +2 -1
  111. {alita_sdk-0.3.554.dist-info → alita_sdk-0.3.602.dist-info}/METADATA +7 -6
  112. {alita_sdk-0.3.554.dist-info → alita_sdk-0.3.602.dist-info}/RECORD +116 -111
  113. {alita_sdk-0.3.554.dist-info → alita_sdk-0.3.602.dist-info}/WHEEL +0 -0
  114. {alita_sdk-0.3.554.dist-info → alita_sdk-0.3.602.dist-info}/entry_points.txt +0 -0
  115. {alita_sdk-0.3.554.dist-info → alita_sdk-0.3.602.dist-info}/licenses/LICENSE +0 -0
  116. {alita_sdk-0.3.554.dist-info → alita_sdk-0.3.602.dist-info}/top_level.txt +0 -0
@@ -10,6 +10,7 @@ from ..elitea_base import filter_missconfigured_index_tools
10
10
  from ..utils import clean_string, get_max_toolkit_length
11
11
  from ...configurations.github import GithubConfiguration
12
12
  from ...configurations.pgvector import PgVectorConfiguration
13
+ from ...runtime.utils.constants import TOOLKIT_NAME_META, TOOL_NAME_META, TOOLKIT_TYPE_META
13
14
 
14
15
  name = "github"
15
16
 
@@ -96,7 +97,7 @@ class AlitaGitHubToolkit(BaseToolkit):
96
97
  mode=tool["mode"],
97
98
  description=description,
98
99
  args_schema=tool["args_schema"],
99
- metadata={"toolkit_name": toolkit_name} if toolkit_name else {}
100
+ metadata={TOOLKIT_NAME_META: toolkit_name, TOOLKIT_TYPE_META: name, TOOL_NAME_META: tool["name"]} if toolkit_name else {TOOL_NAME_META: tool["name"]}
100
101
  ))
101
102
  return cls(tools=tools)
102
103
 
@@ -12,10 +12,6 @@ from github.Consts import DEFAULT_BASE_URL
12
12
  from langchain_core.tools import ToolException
13
13
 
14
14
  from ..elitea_base import extend_with_file_operations, BaseCodeToolApiWrapper
15
- from .schemas import (
16
- GitHubAuthConfig,
17
- GitHubRepoConfig,
18
- )
19
15
 
20
16
  from .schemas import (
21
17
  GitHubAuthConfig,
@@ -415,26 +411,53 @@ class GitHubClient(BaseModel):
415
411
  Returns:
416
412
  str: A detailed diff comparison between the two commits or an error message.
417
413
  """
414
+ def safe_author_info(commit_obj):
415
+ """Safely extract author info from a commit object, handling None values."""
416
+ author = commit_obj.commit.author
417
+ if author:
418
+ return {
419
+ "name": author.name or "Unknown",
420
+ "date": author.date.isoformat() if author.date else None
421
+ }
422
+ # Fallback to GitHub user info if git author is not available
423
+ elif commit_obj.author:
424
+ return {
425
+ "name": commit_obj.author.login,
426
+ "date": None
427
+ }
428
+ return {"name": "Unknown", "date": None}
429
+
418
430
  try:
419
431
  # Get the repository
420
432
  repo = self.github_api.get_repo(repo_name) if repo_name else self.github_repo_instance
421
-
433
+
422
434
  # Get the comparison between the two commits
423
435
  comparison = repo.compare(base_sha, head_sha)
424
-
436
+
437
+ # Get head commit - the GitHub Compare API doesn't return head_commit,
438
+ # so we get it from the commits list or fetch it directly
439
+ if comparison.commits:
440
+ head_commit_obj = comparison.commits[-1]
441
+ else:
442
+ # For identical commits or edge cases, fetch head commit directly
443
+ head_commit_obj = repo.get_commit(head_sha)
444
+
445
+ base_author = safe_author_info(comparison.base_commit)
446
+ head_author = safe_author_info(head_commit_obj)
447
+
425
448
  # Extract comparison information
426
449
  diff_info = {
427
450
  "base_commit": {
428
451
  "sha": comparison.base_commit.sha,
429
452
  "message": comparison.base_commit.commit.message,
430
- "author": comparison.base_commit.commit.author.name,
431
- "date": comparison.base_commit.commit.author.date.isoformat()
453
+ "author": base_author["name"],
454
+ "date": base_author["date"]
432
455
  },
433
456
  "head_commit": {
434
- "sha": comparison.head_commit.sha,
435
- "message": comparison.head_commit.commit.message,
436
- "author": comparison.head_commit.commit.author.name,
437
- "date": comparison.head_commit.commit.author.date.isoformat()
457
+ "sha": head_commit_obj.sha,
458
+ "message": head_commit_obj.commit.message,
459
+ "author": head_author["name"],
460
+ "date": head_author["date"]
438
461
  },
439
462
  "status": comparison.status, # ahead, behind, identical, or diverged
440
463
  "ahead_by": comparison.ahead_by,
@@ -443,14 +466,15 @@ class GitHubClient(BaseModel):
443
466
  "commits": [],
444
467
  "files": []
445
468
  }
446
-
469
+
447
470
  # Get commits in the comparison
448
471
  for commit in comparison.commits:
472
+ author_info = safe_author_info(commit)
449
473
  commit_info = {
450
474
  "sha": commit.sha,
451
475
  "message": commit.commit.message,
452
- "author": commit.commit.author.name,
453
- "date": commit.commit.author.date.isoformat(),
476
+ "author": author_info["name"],
477
+ "date": author_info["date"],
454
478
  "url": commit.html_url
455
479
  }
456
480
  diff_info["commits"].append(commit_info)
@@ -1094,65 +1118,12 @@ class GitHubClient(BaseModel):
1094
1118
  except Exception as e:
1095
1119
  return f"Unable to create file due to error:\n{str(e)}"
1096
1120
 
1097
- def extract_old_new_pairs(self, file_query):
1098
- # Split the file content by lines
1099
- code_lines = file_query.split("\n")
1100
-
1101
- # Initialize lists to hold the contents of OLD and NEW sections
1102
- old_contents = []
1103
- new_contents = []
1104
-
1105
- # Initialize variables to track whether the current line is within an OLD or NEW section
1106
- in_old_section = False
1107
- in_new_section = False
1108
-
1109
- # Temporary storage for the current section's content
1110
- current_section_content = []
1111
-
1112
- # Iterate through each line in the file content
1113
- for line in code_lines:
1114
- # Check for OLD section start
1115
- if "OLD <<<" in line:
1116
- in_old_section = True
1117
- current_section_content = [] # Reset current section content
1118
- continue # Skip the line with the marker
1119
-
1120
- # Check for OLD section end
1121
- if ">>>> OLD" in line:
1122
- in_old_section = False
1123
- old_contents.append("\n".join(current_section_content).strip()) # Add the captured content
1124
- current_section_content = [] # Reset current section content
1125
- continue # Skip the line with the marker
1126
-
1127
- # Check for NEW section start
1128
- if "NEW <<<" in line:
1129
- in_new_section = True
1130
- current_section_content = [] # Reset current section content
1131
- continue # Skip the line with the marker
1132
-
1133
- # Check for NEW section end
1134
- if ">>>> NEW" in line:
1135
- in_new_section = False
1136
- new_contents.append("\n".join(current_section_content).strip()) # Add the captured content
1137
- current_section_content = [] # Reset current section content
1138
- continue # Skip the line with the marker
1139
-
1140
- # If currently in an OLD or NEW section, add the line to the current section content
1141
- if in_old_section or in_new_section:
1142
- current_section_content.append(line)
1143
-
1144
- # Pair the OLD and NEW contents
1145
- paired_contents = list(zip(old_contents, new_contents))
1146
-
1147
- return paired_contents
1148
-
1149
1121
  def update_file(self, file_query: str, repo_name: Optional[str] = None, commit_message: Optional[str] = None) -> str:
1150
- """
1151
- Updates a file with new content.
1122
+ """Updates a file with new content using OLD/NEW markers and edit_file.
1123
+
1152
1124
  Parameters:
1153
- file_query(str): Contains the file path and the file contents.
1154
- The old file contents is wrapped in OLD <<<< and >>>> OLD
1155
- The new file contents is wrapped in NEW <<<< and >>>> NEW
1125
+ file_query(str): Contains the file path on the first line and the file contents
1126
+ wrapped in OLD <<<< and >>>> OLD / NEW <<<< and >>>> NEW markers.
1156
1127
  For example:
1157
1128
  /test/hello.txt
1158
1129
  OLD <<<<
@@ -1161,13 +1132,13 @@ class GitHubClient(BaseModel):
1161
1132
  NEW <<<<
1162
1133
  Hello Mars!
1163
1134
  >>>> NEW
1164
- repo_name (Optional[str]): Name of the repository in format 'owner/repo'
1135
+ repo_name (Optional[str]): Name of the repository in format 'owner/repo'. Currently
1136
+ not used by edit_file and must refer to the initialized repository.
1165
1137
 
1166
1138
  Returns:
1167
1139
  A success or failure message
1168
1140
  """
1169
1141
  try:
1170
- repo = self.github_api.get_repo(repo_name) if repo_name else self.github_repo_instance
1171
1142
  branch = self.active_branch
1172
1143
 
1173
1144
  if branch == self.github_base_branch:
@@ -1176,29 +1147,22 @@ class GitHubClient(BaseModel):
1176
1147
  "which is protected. Please create a new branch and try again."
1177
1148
  )
1178
1149
 
1179
- file_path: str = file_query.split("\n")[0]
1180
-
1181
- file_content = self._read_file(file_path, branch, repo_name)
1182
- updated_file_content = file_content
1183
- for old, new in self.extract_old_new_pairs(file_query):
1184
- if not old.strip():
1185
- continue
1186
- updated_file_content = updated_file_content.replace(old, new)
1187
-
1188
- if file_content == updated_file_content:
1150
+ if "\n" not in file_query:
1189
1151
  return (
1190
- "File content was not updated because old content was not found or empty. "
1191
- "It may be helpful to use the read_file action to get the current file contents."
1152
+ "Invalid file_query format. Expected first line to be the file path "
1153
+ "followed by OLD/NEW blocks."
1192
1154
  )
1193
1155
 
1194
- repo.update_file(
1195
- path=file_path,
1196
- message=commit_message if commit_message else f"Update {file_path}",
1197
- content=updated_file_content,
1156
+ file_path, edit_content = file_query.split("\n", 1)
1157
+ file_path = file_path.strip()
1158
+
1159
+ # Delegate to shared edit_file implementation
1160
+ return self.edit_file(
1161
+ file_path=file_path,
1162
+ file_query=edit_content,
1198
1163
  branch=branch,
1199
- sha=repo.get_contents(file_path, ref=branch).sha,
1164
+ commit_message=commit_message or f"Update {file_path}",
1200
1165
  )
1201
- return f"Updated file {file_path}"
1202
1166
  except Exception as e:
1203
1167
  return f"Unable to update file due to error:\n{str(e)}"
1204
1168
 
@@ -92,7 +92,7 @@ SearchIssues = create_model(
92
92
  "SearchIssues",
93
93
  search_query=(str, Field(description="Keywords or query for searching issues and PRs in Github")),
94
94
  repo_name=(Optional[str], Field(description="Name of the repository to search issues in", default=None)),
95
- max_count=(Optional[int], Field(description="Maximum number of issues to return", default=30))
95
+ max_count=(Optional[int], Field(description="Maximum number of issues to return", default=30, gt=0))
96
96
  )
97
97
 
98
98
  CreateIssue = create_model(
@@ -152,7 +152,7 @@ GetCommits = create_model(
152
152
  since=(Optional[str], Field(description="Only commits after this date will be returned (ISO format)", default=None)),
153
153
  until=(Optional[str], Field(description="Only commits before this date will be returned (ISO format)", default=None)),
154
154
  author=(Optional[str], Field(description="The author of the commits", default=None)),
155
- max_count=(Optional[int], Field(description="Maximum number of commits to return (default: 30)", default=30))
155
+ max_count=(Optional[int], Field(description="Maximum number of commits to return (default: 30)", default=30, gt=0))
156
156
  )
157
157
 
158
158
  GetCommitChanges = create_model(
@@ -213,7 +213,7 @@ ListProjectIssues = create_model(
213
213
  "ListProjectIssues",
214
214
  board_repo=(str, Field(description="The organization and repository for the board (project). Example: 'org-name/repo-name'")),
215
215
  project_number=(int, Field(description="The project number as shown in the project URL")),
216
- items_count=(Optional[int], Field(description="Maximum number of items to retrieve", default=100))
216
+ items_count=(Optional[int], Field(description="Maximum number of items to retrieve", default=100, gt=0))
217
217
  )
218
218
 
219
219
  SearchProjectIssues = create_model(
@@ -221,7 +221,7 @@ SearchProjectIssues = create_model(
221
221
  board_repo=(str, Field(description="The organization and repository for the board (project). Example: 'org-name/repo-name'")),
222
222
  project_number=(int, Field(description="The project number as shown in the project URL")),
223
223
  search_query=(str, Field(description="Search query for filtering issues. Examples: 'status:In Progress', 'release:v1.0'")),
224
- items_count=(Optional[int], Field(description="Maximum number of items to retrieve", default=100))
224
+ items_count=(Optional[int], Field(description="Maximum number of items to retrieve", default=100, gt=0))
225
225
  )
226
226
 
227
227
  ListProjectViews = create_model(
@@ -11,6 +11,7 @@ from ..elitea_base import filter_missconfigured_index_tools
11
11
  from ..utils import clean_string, get_max_toolkit_length
12
12
  from ...configurations.gitlab import GitlabConfiguration
13
13
  from ...configurations.pgvector import PgVectorConfiguration
14
+ from ...runtime.utils.constants import TOOLKIT_NAME_META, TOOL_NAME_META, TOOLKIT_TYPE_META
14
15
 
15
16
  name = "gitlab"
16
17
 
@@ -92,7 +93,7 @@ class AlitaGitlabToolkit(BaseToolkit):
92
93
  name=tool["name"],
93
94
  description=description,
94
95
  args_schema=tool["args_schema"],
95
- metadata={"toolkit_name": toolkit_name} if toolkit_name else {}
96
+ metadata={TOOLKIT_NAME_META: toolkit_name, TOOLKIT_TYPE_META: name, TOOL_NAME_META: tool["name"]} if toolkit_name else {TOOL_NAME_META: tool["name"]}
96
97
  ))
97
98
  return cls(tools=tools)
98
99
 
@@ -2,6 +2,7 @@
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
 
@@ -9,6 +10,7 @@ from ..code_indexer_toolkit import CodeIndexerToolkit
9
10
  from ..utils.available_tools_decorator import extend_with_parent_available_tools
10
11
  from ..elitea_base import extend_with_file_operations, BaseCodeToolApiWrapper
11
12
  from ..utils.content_parser import parse_file_content
13
+ from .utils import get_position
12
14
 
13
15
  AppendFileModel = create_model(
14
16
  "AppendFileModel",
@@ -35,7 +37,7 @@ ReadFileModel = create_model(
35
37
  )
36
38
  UpdateFileModel = create_model(
37
39
  "UpdateFileModel",
38
- file_query=(str, Field(description="The file query string")),
40
+ file_query=(str, Field(description="The file query string containing file path on first line, followed by OLD/NEW markers. Format: file_path\\nOLD <<<< old content >>>> OLD\\nNEW <<<< new content >>>> NEW")),
39
41
  branch=(str, Field(description="The branch to update the file in")),
40
42
  )
41
43
  CommentOnIssueModel = create_model(
@@ -52,6 +54,11 @@ CreatePullRequestModel = create_model(
52
54
  pr_body=(str, Field(description="The body of the pull request")),
53
55
  branch=(str, Field(description="The branch to create the pull request from")),
54
56
  )
57
+ CommentOnPRModel = create_model(
58
+ "CommentOnPRModel",
59
+ pr_number=(int, Field(description="The number of the pull request/merge request")),
60
+ comment=(str, Field(description="The comment text to add")),
61
+ )
55
62
 
56
63
  CreateBranchModel = create_model(
57
64
  "CreateBranchModel",
@@ -59,7 +66,7 @@ CreateBranchModel = create_model(
59
66
  )
60
67
  ListBranchesInRepoModel = create_model(
61
68
  "ListBranchesInRepoModel",
62
- limit=(Optional[int], Field(default=20, description="Maximum number of branches to return. If not provided, all branches will be returned.")),
69
+ limit=(Optional[int], Field(default=20, description="Maximum number of branches to return. If not provided, all branches will be returned.", gt=0)),
63
70
  branch_wildcard=(Optional[str], Field(default=None, description="Wildcard pattern to filter branches by name. If not provided, all branches will be returned."))
64
71
 
65
72
  )
@@ -90,9 +97,9 @@ GetPRChangesModel = create_model(
90
97
  CreatePRChangeCommentModel = create_model(
91
98
  "CreatePRChangeCommentModel",
92
99
  pr_number=(int, Field(description="GitLab Merge Request (Pull Request) number")),
93
- file_path=(str, Field(description="File path of the changed file")),
94
- line_number=(int, Field(description="Line number from the diff for a changed file")),
95
- comment=(str, Field(description="Comment content")),
100
+ file_path=(str, Field(description="File path of the changed file as shown in the diff")),
101
+ 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.")),
102
+ comment=(str, Field(description="Comment content to add to the specific line")),
96
103
  )
97
104
  GetCommitsModel = create_model(
98
105
  "GetCommitsModel",
@@ -235,7 +242,9 @@ class GitLabAPIWrapper(CodeIndexerToolkit):
235
242
  Returns:
236
243
  File content as string
237
244
  """
238
- return self.read_file(file_path, branch)
245
+ # Default to active branch if branch is None, consistent with other methods
246
+ branch = branch if branch else self._active_branch
247
+ return str(self.read_file(file_path, branch))
239
248
 
240
249
  def create_branch(self, branch_name: str) -> str:
241
250
  try:
@@ -322,7 +331,30 @@ class GitLabAPIWrapper(CodeIndexerToolkit):
322
331
  except Exception as e:
323
332
  return "Unable to make comment due to error:\n" + str(e)
324
333
 
334
+ def comment_on_pr(self, pr_number: int, comment: str) -> str:
335
+ """
336
+ Add a comment to a pull request (merge request) in GitLab.
337
+
338
+ This method adds a general comment to the entire merge request,
339
+ not tied to specific code lines or file changes.
340
+
341
+ Parameters:
342
+ pr_number: GitLab Merge Request (Pull Request) number
343
+ comment: Comment text to add
344
+
345
+ Returns:
346
+ Success message or error description
347
+ """
348
+ try:
349
+ mr = self.repo_instance.mergerequests.get(pr_number)
350
+ mr.notes.create({"body": comment})
351
+ return "Commented on merge request " + str(pr_number)
352
+ except Exception as e:
353
+ return "Unable to make comment due to error:\n" + str(e)
354
+
325
355
  def create_file(self, file_path: str, file_contents: str, branch: str) -> str:
356
+ # Default to active branch if branch is None
357
+ branch = branch if branch else self._active_branch
326
358
  try:
327
359
  self.set_active_branch(branch)
328
360
  self.repo_instance.files.get(file_path, branch)
@@ -339,6 +371,8 @@ class GitLabAPIWrapper(CodeIndexerToolkit):
339
371
  return "Created file " + file_path
340
372
 
341
373
  def read_file(self, file_path: str, branch: str) -> str:
374
+ # Default to active branch if branch is None
375
+ branch = branch if branch else self._active_branch
342
376
  self.set_active_branch(branch)
343
377
  file = self.repo_instance.files.get(file_path, branch)
344
378
  return parse_file_content(file_name=file_path,
@@ -406,43 +440,52 @@ class GitLabAPIWrapper(CodeIndexerToolkit):
406
440
  raise ToolException(f"Unable to write file {file_path}: {str(e)}")
407
441
 
408
442
  def update_file(self, file_query: str, branch: str) -> str:
443
+ """
444
+ Update file using edit_file functionality.
445
+
446
+ This method now delegates to edit_file which uses OLD/NEW markers.
447
+ For backwards compatibility, it extracts the file_path from the query.
448
+
449
+ Expected format:
450
+ file_path
451
+ OLD <<<<
452
+ old content
453
+ >>>> OLD
454
+ NEW <<<<
455
+ new content
456
+ >>>> NEW
457
+
458
+ Args:
459
+ file_query: File path on first line, followed by OLD/NEW markers
460
+ branch: Branch to update the file in
461
+
462
+ Returns:
463
+ Success or error message
464
+ """
409
465
  if branch == self.branch:
410
466
  return (
411
- "You're attempting to commit to the directly"
467
+ "You're attempting to commit directly "
412
468
  f"to the {self.branch} branch, which is protected. "
413
469
  "Please create a new branch and try again."
414
470
  )
415
471
  try:
416
- file_path: str = file_query.split("\n")[0]
417
- self.set_active_branch(branch)
418
- file_content = self.read_file(file_path, branch)
419
- updated_file_content = file_content
420
- for old, new in self.extract_old_new_pairs(file_query):
421
- if not old.strip():
422
- continue
423
- updated_file_content = updated_file_content.replace(old, new)
424
-
425
- if file_content == updated_file_content:
472
+ # Extract file path from first line
473
+ lines = file_query.split("\n", 1)
474
+ if len(lines) < 2:
426
475
  return (
427
- "File content was not updated because old content was not found or empty."
428
- "It may be helpful to use the read_file action to get "
429
- "the current file contents."
476
+ "Invalid file_query format. Expected:\n"
477
+ "file_path\n"
478
+ "OLD <<<< old content >>>> OLD\n"
479
+ "NEW <<<< new content >>>> NEW"
430
480
  )
431
481
 
432
- commit = {
433
- "branch": branch,
434
- "commit_message": "Create " + file_path,
435
- "actions": [
436
- {
437
- "action": "update",
438
- "file_path": file_path,
439
- "content": updated_file_content,
440
- }
441
- ],
442
- }
482
+ file_path = lines[0].strip()
483
+ edit_content = lines[1] if len(lines) > 1 else ""
484
+
485
+ # Delegate to edit_file method with appropriate commit message
486
+ commit_message = f"Update {file_path}"
487
+ return self.edit_file(file_path, edit_content, branch, commit_message)
443
488
 
444
- self.repo_instance.commits.create(commit)
445
- return "Updated file " + file_path
446
489
  except Exception as e:
447
490
  return "Unable to update file due to error:\n" + str(e)
448
491
 
@@ -494,10 +537,41 @@ class GitLabAPIWrapper(CodeIndexerToolkit):
494
537
  return res
495
538
 
496
539
  def create_pr_change_comment(self, pr_number: int, file_path: str, line_number: int, comment: str) -> str:
497
- mr = self.repo_instance.mergerequests.get(pr_number)
498
- position = {"position_type": "text", "new_path": file_path, "new_line": line_number}
499
- mr.discussions.create({"body": comment, "position": position})
500
- return "Comment added"
540
+ """
541
+ Create a comment on a specific line in a pull request (merge request) change in GitLab.
542
+
543
+ This method adds an inline comment to a specific line in the diff of a merge request.
544
+ The line_number parameter refers to the line index in the diff output (0-based),
545
+ not the line number in the original file.
546
+
547
+ **Important**: Use get_pr_changes first to see the diff and identify the correct
548
+ line index for commenting.
549
+
550
+ Parameters:
551
+ pr_number: GitLab Merge Request number
552
+ file_path: Path to the file being commented on (as shown in the diff)
553
+ line_number: Line index from the diff (0-based index)
554
+ comment: Comment text to add
555
+
556
+ Returns:
557
+ Success message or error description
558
+ """
559
+ try:
560
+ mr = self.repo_instance.mergerequests.get(pr_number)
561
+ except GitlabGetError as e:
562
+ if e.response_code == 404:
563
+ raise ToolException(f"Merge request number {pr_number} wasn't found: {e}")
564
+ raise ToolException(f"Error retrieving merge request {pr_number}: {e}")
565
+
566
+ try:
567
+ # Calculate proper position with SHA references and line mappings
568
+ position = get_position(file_path=file_path, line_number=line_number, mr=mr)
569
+
570
+ # Create discussion with the comment
571
+ mr.discussions.create({"body": comment, "position": position})
572
+ return f"Comment added successfully to line {line_number} in {file_path} on MR #{pr_number}"
573
+ except Exception as e:
574
+ raise ToolException(f"Failed to create comment on MR #{pr_number}: {e}")
501
575
 
502
576
  def get_commits(self, sha: Optional[str] = None, path: Optional[str] = None, since: Optional[str] = None, until: Optional[str] = None, author: Optional[str] = None):
503
577
  params = {}
@@ -575,6 +649,12 @@ class GitLabAPIWrapper(CodeIndexerToolkit):
575
649
  "description": self.comment_on_issue.__doc__ or "Comment on an issue in the repository.",
576
650
  "args_schema": CommentOnIssueModel,
577
651
  },
652
+ {
653
+ "name": "comment_on_pr",
654
+ "ref": self.comment_on_pr,
655
+ "description": self.comment_on_pr.__doc__ or "Comment on a pull request (merge request) in the repository.",
656
+ "args_schema": CommentOnPRModel,
657
+ },
578
658
  {
579
659
  "name": "create_file",
580
660
  "ref": self.create_file,
@@ -620,7 +700,7 @@ class GitLabAPIWrapper(CodeIndexerToolkit):
620
700
  {
621
701
  "name": "create_pr_change_comment",
622
702
  "ref": self.create_pr_change_comment,
623
- "description": "Create a comment on a pull request change.",
703
+ "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.",
624
704
  "args_schema": CreatePRChangeCommentModel,
625
705
  },
626
706
  {
@@ -8,6 +8,7 @@ from pydantic import create_model, BaseModel, ConfigDict, Field, SecretStr
8
8
  from ..elitea_base import filter_missconfigured_index_tools
9
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
 
@@ -74,7 +75,7 @@ class AlitaGitlabSpaceToolkit(BaseToolkit):
74
75
  name=tool['name'],
75
76
  description=description,
76
77
  args_schema=tool["args_schema"],
77
- metadata={"toolkit_name": toolkit_name} if toolkit_name else {}
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