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
@@ -23,7 +23,8 @@ from langchain_core.tools import ToolException
23
23
  from msrest.authentication import BasicAuthentication
24
24
  from pydantic import Field, PrivateAttr, create_model, model_validator, SecretStr
25
25
 
26
- from ..utils import extract_old_new_pairs, generate_diff, get_content_from_generator
26
+ from ...elitea_base import BaseCodeToolApiWrapper
27
+ from ..utils import generate_diff, get_content_from_generator
27
28
  from ...code_indexer_toolkit import CodeIndexerToolkit
28
29
  from ...utils.available_tools_decorator import extend_with_parent_available_tools
29
30
 
@@ -129,9 +130,30 @@ class ArgsSchema(Enum):
129
130
  )
130
131
  UpdateFile = create_model(
131
132
  "UpdateFile",
132
- branch_name=(str, Field(description="The name of the branch, e.g. `my_branch`.")),
133
- file_path=(str, Field(description="Path of a file to be updated.")),
134
- update_query=(str, Field(description="Update query used to adjust target file.")),
133
+ branch_name=(
134
+ str,
135
+ Field(description="The name of the branch, e.g. `my_branch`.")
136
+ ),
137
+ file_path=(
138
+ str,
139
+ Field(description="Path of a file to be updated."),
140
+ ),
141
+ update_query=(
142
+ str,
143
+ Field(
144
+ description=(
145
+ "The exact OLD text to be replaced and the NEW "
146
+ "text to insert, using one or more block pairs like:"
147
+ "OLD <<<<Hello Earth!>>>> OLD NEW <<<<Hello Mars!>>>> NEW"
148
+ "Each OLD block must contain the exact text that will be replaced "
149
+ "via string replacement (exact full match including non-written characters). Each corresponding NEW "
150
+ "block contains the replacement text. For multi-line changes, it is "
151
+ "preferred to provide several smaller OLD/NEW pairs rather than one "
152
+ "large block, so that each OLD block closely matches a contiguous "
153
+ "snippet from the file."
154
+ )
155
+ ),
156
+ ),
135
157
  )
136
158
  DeleteFile = create_model(
137
159
  "DeleteFile",
@@ -252,6 +274,9 @@ class ReposApiWrapper(CodeIndexerToolkit):
252
274
  token: Optional[SecretStr]
253
275
  _client: Optional[GitClient] = PrivateAttr()
254
276
 
277
+ # Reuse common file helpers from BaseCodeToolApiWrapper
278
+ edit_file = BaseCodeToolApiWrapper.edit_file
279
+
255
280
  class Config:
256
281
  arbitrary_types_allowed = True
257
282
 
@@ -835,22 +860,50 @@ class ReposApiWrapper(CodeIndexerToolkit):
835
860
  logger.error(msg)
836
861
  return ToolException(msg)
837
862
 
838
- def update_file(self, branch_name: str, file_path: str, update_query: str) -> str:
863
+ def _write_file(self, file_path: str, content: str, branch: str = None, commit_message: str = None) -> str:
864
+ """Write content to a file in Azure DevOps by creating an edit commit.
865
+
866
+ This implementation follows the previous `update_file` behavior: it always
867
+ performs an 'edit' change (does not create the file), gets the latest
868
+ commit id for the branch and pushes a new commit containing the change.
839
869
  """
840
- Updates a file with new content in Azure DevOps.
870
+ try:
871
+ # Get the latest commit ID of the target branch
872
+ branch_obj = self._client.get_branch(
873
+ repository_id=self.repository_id,
874
+ project=self.project,
875
+ name=branch,
876
+ )
877
+ if branch_obj is None or not hasattr(branch_obj, 'commit') or not hasattr(branch_obj.commit, 'commit_id'):
878
+ raise ToolException(f"Branch `{branch}` does not exist or has no commits.")
879
+
880
+ latest_commit_id = branch_obj.commit.commit_id
881
+
882
+ # Build edit change and push
883
+ change = GitChange("edit", file_path, content).to_dict()
884
+
885
+ ref_update = GitRefUpdate(name=f"refs/heads/{branch}", old_object_id=latest_commit_id)
886
+ new_commit = GitCommit(comment=commit_message or ("Update " + file_path), changes=[change])
887
+ push = GitPush(commits=[new_commit], ref_updates=[ref_update])
888
+
889
+ self._client.create_push(push=push, repository_id=self.repository_id, project=self.project)
890
+ return f"Updated file {file_path}"
891
+ except ToolException:
892
+ # Re-raise known tool exceptions
893
+ raise
894
+ except Exception as e:
895
+ logger.error(f"Unable to write file {file_path}: {e}")
896
+ raise ToolException(f"Unable to write file {file_path}: {str(e)}")
897
+
898
+ def update_file(self, branch_name: str, file_path: str, update_query: str) -> str:
899
+ """Updates a file with new content in Azure DevOps using OLD/NEW markers.
900
+
841
901
  Parameters:
842
902
  branch_name (str): The name of the branch where update the file.
843
903
  file_path (str): Path to the file for update.
844
- update_query(str): Contains the file contents requried to be updated.
845
- The old file contents is wrapped in OLD <<<< and >>>> OLD
846
- The new file contents is wrapped in NEW <<<< and >>>> NEW
847
- For example:
848
- OLD <<<<
849
- Hello Earth!
850
- >>>> OLD
851
- NEW <<<<
852
- Hello Mars!
853
- >>>> NEW
904
+ update_query(str): Contains the file contents required to be updated,
905
+ wrapped in Git-style OLD/NEW blocks.
906
+
854
907
  Returns:
855
908
  A success or failure message
856
909
  """
@@ -863,43 +916,16 @@ class ReposApiWrapper(CodeIndexerToolkit):
863
916
  "Please create a new branch and try again."
864
917
  )
865
918
  try:
866
- file_content = self._read_file(file_path, branch_name)
867
-
868
- if isinstance(file_content, ToolException):
869
- return file_content
870
-
871
- updated_file_content = file_content
872
- for old, new in extract_old_new_pairs(update_query):
873
- if not old.strip():
874
- continue
875
- updated_file_content = updated_file_content.replace(old, new)
876
-
877
- if file_content == updated_file_content:
878
- return (
879
- "File content was not updated because old content was not found or empty. "
880
- "It may be helpful to use the read_file action to get "
881
- "the current file contents."
882
- )
883
-
884
- # Get the latest commit ID of the active branch to use as oldObjectId
885
- branch = self._client.get_branch(
886
- repository_id=self.repository_id,
887
- project=self.project,
888
- name=self.active_branch,
889
- )
890
- latest_commit_id = branch.commit.commit_id
891
-
892
- change = GitChange("edit", file_path, updated_file_content).to_dict()
893
-
894
- ref_update = GitRefUpdate(
895
- name=f"refs/heads/{self.active_branch}", old_object_id=latest_commit_id
896
- )
897
- new_commit = GitCommit(comment=f"Update {file_path}", changes=[change])
898
- push = GitPush(commits=[new_commit], ref_updates=[ref_update])
899
- self._client.create_push(
900
- push=push, repository_id=self.repository_id, project=self.project
919
+ # Let edit_file handle parsing and content updates; this will call _read_file and _write_file.
920
+ # For ADO, branch_name is used as branch; commit message is derived from file_path.
921
+ return self.edit_file(
922
+ file_path=file_path,
923
+ file_query=update_query,
924
+ branch=self.active_branch,
925
+ commit_message=f"Update {file_path}",
901
926
  )
902
- return "Updated file " + file_path
927
+ except ToolException as e:
928
+ return str(e)
903
929
  except Exception as e:
904
930
  msg = f"Unable to update file due to error:\n{str(e)}"
905
931
  logger.error(msg)
@@ -11,6 +11,7 @@ from ....configurations.pgvector import PgVectorConfiguration
11
11
  from .test_plan_wrapper import TestPlanApiWrapper
12
12
  from ...base.tool import BaseAction
13
13
  from ...utils import clean_string, get_max_toolkit_length, check_connection_response
14
+ from ....runtime.utils.constants import TOOLKIT_NAME_META, TOOL_NAME_META, TOOLKIT_TYPE_META
14
15
 
15
16
 
16
17
  name = "azure_devops_plans"
@@ -40,7 +41,7 @@ class AzureDevOpsPlansToolkit(BaseToolkit):
40
41
  m = create_model(
41
42
  name_alias,
42
43
  ado_configuration=(AdoConfiguration, Field(description="Ado configuration", json_schema_extra={'configuration_types': ['ado']})),
43
- limit=(Optional[int], Field(description="ADO plans limit used for limitation of the list with results", default=5)),
44
+ limit=(Optional[int], Field(description="ADO plans limit used for limitation of the list with results", default=5, gt=0)),
44
45
  # indexer settings
45
46
  pgvector_configuration=(Optional[PgVectorConfiguration], Field(default=None,
46
47
  description="PgVector Configuration", json_schema_extra={'configuration_types': ['pgvector']})),
@@ -122,7 +123,7 @@ class AzureDevOpsPlansToolkit(BaseToolkit):
122
123
  name=tool["name"],
123
124
  description=description,
124
125
  args_schema=tool["args_schema"],
125
- metadata={"toolkit_name": toolkit_name} if toolkit_name else {}
126
+ metadata={TOOLKIT_NAME_META: toolkit_name, TOOLKIT_TYPE_META: name, TOOL_NAME_META: tool["name"]} if toolkit_name else {TOOL_NAME_META: tool["name"]}
126
127
  ))
127
128
  return cls(tools=tools)
128
129
 
@@ -180,7 +180,29 @@ class TestPlanApiWrapper(NonCodeIndexerToolkit):
180
180
  connection = Connection(base_url=values['organization_url'], creds=credentials)
181
181
  cls._client = connection.clients.get_test_plan_client()
182
182
  except Exception as e:
183
- raise ImportError(f"Failed to connect to Azure DevOps: {e}")
183
+ error_msg = str(e).lower()
184
+ if "expired" in error_msg or "token" in error_msg and ("invalid" in error_msg or "unauthorized" in error_msg):
185
+ raise ValueError(
186
+ "Azure DevOps connection failed: Your access token has expired or is invalid. "
187
+ "Please refresh your token in the toolkit configuration."
188
+ )
189
+ elif "401" in error_msg or "unauthorized" in error_msg:
190
+ raise ValueError(
191
+ "Azure DevOps connection failed: Authentication failed. "
192
+ "Please check your credentials in the toolkit configuration."
193
+ )
194
+ elif "404" in error_msg or "not found" in error_msg:
195
+ raise ValueError(
196
+ "Azure DevOps connection failed: Organization or project not found. "
197
+ "Please verify your organization URL and project name."
198
+ )
199
+ elif "timeout" in error_msg or "timed out" in error_msg:
200
+ raise ValueError(
201
+ "Azure DevOps connection failed: Connection timed out. "
202
+ "Please check your network connection and try again."
203
+ )
204
+ else:
205
+ raise ValueError(f"Azure DevOps connection failed: {e}")
184
206
  return super().validate_toolkit(values)
185
207
 
186
208
  def create_test_plan(self, test_plan_create_params: str):
@@ -1,24 +1,6 @@
1
- import re
2
1
  import difflib
3
2
 
4
3
 
5
- def extract_old_new_pairs(file_query: str):
6
- """
7
- Extracts old and new content pairs from a file query.
8
- Parameters:
9
- file_query (str): The file query containing old and new content.
10
- Returns:
11
- list of tuples: A list where each tuple contains (old_content, new_content).
12
- """
13
- old_pattern = re.compile(r"OLD <<<<\s*(.*?)\s*>>>> OLD", re.DOTALL)
14
- new_pattern = re.compile(r"NEW <<<<\s*(.*?)\s*>>>> NEW", re.DOTALL)
15
-
16
- old_contents = old_pattern.findall(file_query)
17
- new_contents = new_pattern.findall(file_query)
18
-
19
- return list(zip(old_contents, new_contents))
20
-
21
-
22
4
  def generate_diff(base_text, target_text, file_path):
23
5
  base_lines = base_text.splitlines(keepends=True)
24
6
  target_lines = target_text.splitlines(keepends=True)
@@ -28,6 +10,7 @@ def generate_diff(base_text, target_text, file_path):
28
10
 
29
11
  return "".join(diff)
30
12
 
13
+
31
14
  def get_content_from_generator(content_generator):
32
15
  def safe_decode(chunk):
33
16
  try:
@@ -10,6 +10,7 @@ from ....configurations.ado import AdoConfiguration
10
10
  from ....configurations.pgvector import PgVectorConfiguration
11
11
  from ...base.tool import BaseAction
12
12
  from ...utils import clean_string, get_max_toolkit_length, check_connection_response
13
+ from ....runtime.utils.constants import TOOLKIT_NAME_META, TOOL_NAME_META, TOOLKIT_TYPE_META
13
14
 
14
15
  name = "azure_devops_wiki"
15
16
  name_alias = 'ado_wiki'
@@ -116,7 +117,7 @@ class AzureDevOpsWikiToolkit(BaseToolkit):
116
117
  name=tool["name"],
117
118
  description=description,
118
119
  args_schema=tool["args_schema"],
119
- metadata={"toolkit_name": toolkit_name} if toolkit_name else {}
120
+ metadata={TOOLKIT_NAME_META: toolkit_name, TOOLKIT_TYPE_META: name, TOOL_NAME_META: tool["name"]} if toolkit_name else {TOOL_NAME_META: tool["name"]}
120
121
  ))
121
122
  return cls(tools=tools)
122
123
 
@@ -105,7 +105,29 @@ class AzureDevOpsApiWrapper(NonCodeIndexerToolkit):
105
105
  cls._core_client = connection.clients.get_core_client()
106
106
 
107
107
  except Exception as e:
108
- return ImportError(f"Failed to connect to Azure DevOps: {e}")
108
+ error_msg = str(e).lower()
109
+ if "expired" in error_msg or "token" in error_msg and ("invalid" in error_msg or "unauthorized" in error_msg):
110
+ raise ValueError(
111
+ "Azure DevOps connection failed: Your access token has expired or is invalid. "
112
+ "Please refresh your token in the toolkit configuration."
113
+ )
114
+ elif "401" in error_msg or "unauthorized" in error_msg:
115
+ raise ValueError(
116
+ "Azure DevOps connection failed: Authentication failed. "
117
+ "Please check your credentials in the toolkit configuration."
118
+ )
119
+ elif "404" in error_msg or "not found" in error_msg:
120
+ raise ValueError(
121
+ "Azure DevOps connection failed: Organization or project not found. "
122
+ "Please verify your organization URL and project name."
123
+ )
124
+ elif "timeout" in error_msg or "timed out" in error_msg:
125
+ raise ValueError(
126
+ "Azure DevOps connection failed: Connection timed out. "
127
+ "Please check your network connection and try again."
128
+ )
129
+ else:
130
+ raise ValueError(f"Azure DevOps connection failed: {e}")
109
131
 
110
132
  return super().validate_toolkit(values)
111
133
 
@@ -10,6 +10,7 @@ from ....configurations.ado import AdoConfiguration
10
10
  from ....configurations.pgvector import PgVectorConfiguration
11
11
  from ...base.tool import BaseAction
12
12
  from ...utils import clean_string, get_max_toolkit_length, check_connection_response
13
+ from ....runtime.utils.constants import TOOLKIT_NAME_META, TOOL_NAME_META, TOOLKIT_TYPE_META
13
14
 
14
15
  name = "ado_boards"
15
16
 
@@ -37,7 +38,7 @@ class AzureDevOpsWorkItemsToolkit(BaseToolkit):
37
38
  m = create_model(
38
39
  name,
39
40
  ado_configuration=(AdoConfiguration, Field(description="Ado Work Item configuration", json_schema_extra={'configuration_types': ['ado']})),
40
- limit=(Optional[int], Field(description="ADO plans limit used for limitation of the list with results", default=5)),
41
+ limit=(Optional[int], Field(description="Default ADO boards result limit (can be overridden by agent instructions)", default=5, gt=0)),
41
42
  selected_tools=(List[Literal[tuple(selected_tools)]], Field(default=[], json_schema_extra={'args_schemas': selected_tools})),
42
43
  # indexer settings
43
44
  pgvector_configuration=(Optional[PgVectorConfiguration], Field(default = None,
@@ -117,7 +118,7 @@ class AzureDevOpsWorkItemsToolkit(BaseToolkit):
117
118
  name=tool["name"],
118
119
  description=description,
119
120
  args_schema=tool["args_schema"],
120
- metadata={"toolkit_name": toolkit_name} if toolkit_name else {}
121
+ metadata={TOOLKIT_NAME_META: toolkit_name, TOOLKIT_TYPE_META: name, TOOL_NAME_META: tool["name"]} if toolkit_name else {TOOL_NAME_META: tool["name"]}
121
122
  ))
122
123
  return cls(tools=tools)
123
124
 
@@ -127,7 +127,29 @@ class AzureDevOpsApiWrapper(NonCodeIndexerToolkit):
127
127
  cls._core_client = connection.clients_v7_1.get_core_client()
128
128
 
129
129
  except Exception as e:
130
- return ImportError(f"Failed to connect to Azure DevOps: {e}")
130
+ error_msg = str(e).lower()
131
+ if "expired" in error_msg or "token" in error_msg and ("invalid" in error_msg or "unauthorized" in error_msg):
132
+ raise ValueError(
133
+ "Azure DevOps connection failed: Your access token has expired or is invalid. "
134
+ "Please refresh your token in the toolkit configuration."
135
+ )
136
+ elif "401" in error_msg or "unauthorized" in error_msg:
137
+ raise ValueError(
138
+ "Azure DevOps connection failed: Authentication failed. "
139
+ "Please check your credentials in the toolkit configuration."
140
+ )
141
+ elif "404" in error_msg or "not found" in error_msg:
142
+ raise ValueError(
143
+ "Azure DevOps connection failed: Organization or project not found. "
144
+ "Please verify your organization URL and project name."
145
+ )
146
+ elif "timeout" in error_msg or "timed out" in error_msg:
147
+ raise ValueError(
148
+ "Azure DevOps connection failed: Connection timed out. "
149
+ "Please check your network connection and try again."
150
+ )
151
+ else:
152
+ raise ValueError(f"Azure DevOps connection failed: {e}")
131
153
 
132
154
  return super().validate_toolkit(values)
133
155
 
@@ -7,6 +7,7 @@ from .data_mining_wrapper import AdvancedJiraMiningWrapper
7
7
  from ..base.tool import BaseAction
8
8
  from ..elitea_base import filter_missconfigured_index_tools
9
9
  from ..utils import clean_string, get_max_toolkit_length
10
+ from ...runtime.utils.constants import TOOLKIT_NAME_META, TOOL_NAME_META, TOOLKIT_TYPE_META
10
11
 
11
12
  name = "advanced_jira_mining"
12
13
 
@@ -78,7 +79,7 @@ class AdvancedJiraMiningToolkit(BaseToolkit):
78
79
  name=tool["name"],
79
80
  description=description,
80
81
  args_schema=tool["args_schema"],
81
- metadata={"toolkit_name": toolkit_name} if toolkit_name else {}
82
+ metadata={TOOLKIT_NAME_META: toolkit_name, TOOLKIT_TYPE_META: name, TOOL_NAME_META: tool["name"]} if toolkit_name else {TOOL_NAME_META: tool["name"]}
82
83
  ))
83
84
  return cls(tools=tools)
84
85
 
@@ -9,6 +9,7 @@ from alita_sdk.configurations.delta_lake import DeltaLakeConfiguration
9
9
  from ...utils import clean_string, get_max_toolkit_length
10
10
  from .api_wrapper import DeltaLakeApiWrapper
11
11
  from .tool import DeltaLakeAction
12
+ from ....runtime.utils.constants import TOOLKIT_NAME_META, TOOL_NAME_META, TOOLKIT_TYPE_META
12
13
 
13
14
  name = "delta_lake"
14
15
 
@@ -126,7 +127,7 @@ class DeltaLakeToolkit(BaseToolkit):
126
127
  name=t["name"],
127
128
  description=description,
128
129
  args_schema=t["args_schema"],
129
- metadata={"toolkit_name": toolkit_name} if toolkit_name else {}
130
+ metadata={TOOLKIT_NAME_META: toolkit_name, TOOLKIT_TYPE_META: name, TOOL_NAME_META: t["name"]} if toolkit_name else {TOOL_NAME_META: t["name"]}
130
131
  )
131
132
  )
132
133
  return instance
@@ -9,6 +9,7 @@ from ...elitea_base import filter_missconfigured_index_tools
9
9
  from ...utils import clean_string, get_max_toolkit_length, check_connection_response
10
10
  from ....configurations.azure_search import AzureSearchConfiguration
11
11
  import requests
12
+ from ....runtime.utils.constants import TOOLKIT_NAME_META, TOOL_NAME_META, TOOLKIT_TYPE_META
12
13
 
13
14
  logger = getLogger(__name__)
14
15
 
@@ -91,7 +92,7 @@ class AzureSearchToolkit(BaseToolkit):
91
92
  name=tool["name"],
92
93
  description=description,
93
94
  args_schema=tool["args_schema"],
94
- metadata={"toolkit_name": toolkit_name} if toolkit_name else {}
95
+ metadata={TOOLKIT_NAME_META: toolkit_name, TOOLKIT_TYPE_META: name, TOOL_NAME_META: tool["name"]} if toolkit_name else {TOOL_NAME_META: tool["name"]}
95
96
  ))
96
97
  return cls(tools=tools)
97
98
 
@@ -11,7 +11,7 @@ logger = logging.getLogger(__name__)
11
11
 
12
12
  class AzureSearchInput(BaseModel):
13
13
  search_text: str = Field(..., description="The text to search for in the Azure Search index.")
14
- limit: int = Field(10, description="The number of results to return.")
14
+ limit: int = Field(10, description="The number of results to return.", gt=0)
15
15
  selected_fields: Optional[List[str]] = Field(None, description="The fields to retrieve from the document.")
16
16
 
17
17
  class AzureDocumentInput(BaseModel):
@@ -46,7 +46,7 @@ BaseSearchParams = create_model(
46
46
  examples=["{\"key\": \"value\"}", "{\"status\": \"active\"}"]
47
47
  )),
48
48
  cut_off=(Optional[float], Field(description="Cut-off score for search results", default=DEFAULT_CUT_OFF, ge=0, le=1)),
49
- search_top=(Optional[int], Field(description="Number of top results to return", default=10)),
49
+ search_top=(Optional[int], Field(description="Number of top results to return", default=10, gt=0)),
50
50
  full_text_search=(Optional[Dict[str, Any]], Field(
51
51
  description="Full text search parameters. Can be a dictionary with search options.",
52
52
  default=None
@@ -76,7 +76,7 @@ BaseStepbackSearchParams = create_model(
76
76
  examples=["{\"key\": \"value\"}", "{\"status\": \"active\"}"]
77
77
  )),
78
78
  cut_off=(Optional[float], Field(description="Cut-off score for search results", default=DEFAULT_CUT_OFF, ge=0, le=1)),
79
- search_top=(Optional[int], Field(description="Number of top results to return", default=10)),
79
+ search_top=(Optional[int], Field(description="Number of top results to return", default=10, gt=0)),
80
80
  full_text_search=(Optional[Dict[str, Any]], Field(
81
81
  description="Full text search parameters. Can be a dictionary with search options.",
82
82
  default=None
@@ -186,7 +186,7 @@ class BaseIndexerToolkit(VectorStoreWrapperBase):
186
186
  #
187
187
  results_count = result["count"]
188
188
  # Final update should always be forced
189
- self.index_meta_update(index_name, IndexerKeywords.INDEX_META_COMPLETED.value, results_count, update_force=True)
189
+ self.index_meta_update(index_name, IndexerKeywords.INDEX_META_COMPLETED.value, results_count, update_force=True, error=None)
190
190
  self._emit_index_event(index_name)
191
191
  #
192
192
  return {"status": "ok", "message": f"successfully indexed {results_count} documents" if results_count > 0
@@ -195,8 +195,8 @@ class BaseIndexerToolkit(VectorStoreWrapperBase):
195
195
  # Do maximum effort at least send custom event for supposed changed status
196
196
  msg = str(e)
197
197
  try:
198
- # Error update should also be forced
199
- self.index_meta_update(index_name, IndexerKeywords.INDEX_META_FAILED.value, result["count"], update_force=True)
198
+ # Error update should also be forced and include the error message
199
+ self.index_meta_update(index_name, IndexerKeywords.INDEX_META_FAILED.value, result["count"], update_force=True, error=msg)
200
200
  except Exception as ie:
201
201
  logger.error(f"Failed to update index meta status to FAILED for index '{index_name}': {ie}")
202
202
  msg = f"{msg}; additionally failed to update index meta status to FAILED: {ie}"
@@ -505,12 +505,14 @@ class BaseIndexerToolkit(VectorStoreWrapperBase):
505
505
  "task_id": None,
506
506
  "conversation_id": None,
507
507
  "toolkit_id": self.toolkit_id,
508
+ # Initialize error field to keep track of the latest failure reason if any
509
+ "error": None,
508
510
  }
509
511
  metadata["history"] = json.dumps([metadata])
510
512
  index_meta_doc = Document(page_content=f"{IndexerKeywords.INDEX_META_TYPE.value}_{index_name}", metadata=metadata)
511
513
  add_documents(vectorstore=self.vectorstore, documents=[index_meta_doc])
512
514
 
513
- def index_meta_update(self, index_name: str, state: str, result: int, update_force: bool = True, interval: Optional[float] = None):
515
+ def index_meta_update(self, index_name: str, state: str, result: int, update_force: bool = True, interval: Optional[float] = None, error: Optional[str] = None):
514
516
  """Update `index_meta` document with optional time-based throttling.
515
517
 
516
518
  Args:
@@ -522,6 +524,7 @@ class BaseIndexerToolkit(VectorStoreWrapperBase):
522
524
  interval: Optional custom interval (in seconds) for this call when `update_force` is `False`.
523
525
  If `None`, falls back to the value stored in `self._index_meta_config["update_interval"]`
524
526
  if present, otherwise uses `INDEX_META_UPDATE_INTERVAL`.
527
+ error: Optional error message to record when the state represents a failed index.
525
528
  """
526
529
  self._ensure_vectorstore_initialized()
527
530
  if not hasattr(self, "_index_meta_last_update_time"):
@@ -560,6 +563,12 @@ class BaseIndexerToolkit(VectorStoreWrapperBase):
560
563
  metadata["updated"] = result
561
564
  metadata["state"] = state
562
565
  metadata["updated_on"] = time.time()
566
+ # Attach error if provided, else clear on success
567
+ if error is not None:
568
+ metadata["error"] = error
569
+ elif state == IndexerKeywords.INDEX_META_COMPLETED.value:
570
+ # Clear previous error on successful completion
571
+ metadata["error"] = None
563
572
  #
564
573
  history_raw = metadata.pop("history", "[]")
565
574
  try:
@@ -13,6 +13,7 @@ from ..utils import clean_string, get_max_toolkit_length, check_connection_respo
13
13
  from ...configurations.bitbucket import BitbucketConfiguration
14
14
  from ...configurations.pgvector import PgVectorConfiguration
15
15
  import requests
16
+ from ...runtime.utils.constants import TOOLKIT_NAME_META, TOOL_NAME_META, TOOLKIT_TYPE_META
16
17
 
17
18
 
18
19
  name = "bitbucket"
@@ -114,7 +115,7 @@ class AlitaBitbucketToolkit(BaseToolkit):
114
115
  name=tool["name"],
115
116
  description=description,
116
117
  args_schema=tool["args_schema"],
117
- metadata={"toolkit_name": toolkit_name} if toolkit_name else {}
118
+ metadata={TOOLKIT_NAME_META: toolkit_name, TOOLKIT_TYPE_META: name, TOOL_NAME_META: tool["name"]} if toolkit_name else {TOOL_NAME_META: tool["name"]}
118
119
  ))
119
120
  return cls(tools=tools)
120
121
 
@@ -57,7 +57,7 @@ SetActiveBranchModel = create_model(
57
57
 
58
58
  ListBranchesInRepoModel = create_model(
59
59
  "ListBranchesInRepoModel",
60
- limit=(Optional[int], Field(default=20, description="Maximum number of branches to return. If not provided, all branches will be returned.")),
60
+ limit=(Optional[int], Field(default=20, description="Maximum number of branches to return. If not provided, all branches will be returned.", gt=0)),
61
61
  branch_wildcard=(Optional[str], Field(default=None, description="Wildcard pattern to filter branches by name. If not provided, all branches will be returned."))
62
62
  )
63
63
 
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Any, Dict, List
8
8
  from atlassian.bitbucket import Bitbucket, Cloud
9
9
  from langchain_core.tools import ToolException
10
10
  from requests import Response
11
- from ..ado.utils import extract_old_new_pairs
11
+ from ..utils.text_operations import parse_old_new_markers
12
12
 
13
13
  logger = logging.getLogger(__name__)
14
14
  logging.basicConfig(level=logging.DEBUG)
@@ -145,7 +145,7 @@ class BitbucketServerApi(BitbucketApiAbstract):
145
145
  def update_file(self, file_path: str, update_query: str, branch: str) -> str:
146
146
  file_content = self.get_file(file_path=file_path, branch=branch)
147
147
  updated_file_content = file_content
148
- for old, new in extract_old_new_pairs(update_query):
148
+ for old, new in parse_old_new_markers(update_query):
149
149
  if not old.strip():
150
150
  continue
151
151
  updated_file_content = updated_file_content.replace(old, new)
@@ -319,7 +319,7 @@ class BitbucketCloudApi(BitbucketApiAbstract):
319
319
 
320
320
  file_content = self.get_file(file_path=file_path, branch=branch)
321
321
  updated_file_content = file_content
322
- for old, new in extract_old_new_pairs(file_query=update_query):
322
+ for old, new in parse_old_new_markers(file_query=update_query):
323
323
  if not old.strip():
324
324
  continue
325
325
  updated_file_content = updated_file_content.replace(old, new)
@@ -128,7 +128,7 @@ class BrowserToolkit(BaseToolkit):
128
128
  if toolkit_name:
129
129
  tool_entry.description = f"{tool_entry.description}\nToolkit: {toolkit_name}"
130
130
  tool_entry.description = tool_entry.description[:1000]
131
- tool_entry.metadata = {"toolkit_name": toolkit_name}
131
+ tool_entry.metadata = {"toolkit_name": toolkit_name, "toolkit_type": name}
132
132
  tools.append(tool_entry)
133
133
  return cls(tools=tools)
134
134
 
@@ -77,7 +77,7 @@ class AlitaCarrierToolkit(BaseToolkit):
77
77
  if toolkit_name:
78
78
  tool_instance.description = f"{tool_instance.description}\nToolkit: {toolkit_name}"
79
79
  tool_instance.description = tool_instance.description[:1000]
80
- tool_instance.metadata = {"toolkit_name": toolkit_name}
80
+ tool_instance.metadata = {"toolkit_name": toolkit_name, "toolkit_type": name}
81
81
  tools.append(tool_instance)
82
82
  logger.info(f"[AlitaCarrierToolkit] Successfully initialized tool '{tool_instance.name}'")
83
83
  except Exception as e: