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
@@ -3,18 +3,21 @@ import json
3
3
  import logging
4
4
  import re
5
5
  from traceback import format_exc
6
- from typing import Any, Optional
6
+ from typing import Any, Optional, Generator, Literal
7
7
 
8
8
  import requests
9
9
  import swagger_client
10
+ from langchain_core.documents import Document
10
11
  from langchain_core.tools import ToolException
11
12
  from pydantic import Field, PrivateAttr, model_validator, create_model, SecretStr
12
13
  from sklearn.feature_extraction.text import strip_tags
13
14
  from swagger_client import TestCaseApi, SearchApi, PropertyResource, ModuleApi, ProjectApi, FieldApi
14
15
  from swagger_client.rest import ApiException
15
16
 
16
- from ..elitea_base import BaseToolApiWrapper
17
- from ..utils.content_parser import parse_file_content
17
+ from ..non_code_indexer_toolkit import NonCodeIndexerToolkit
18
+ from ..utils.available_tools_decorator import extend_with_parent_available_tools
19
+ from ..utils.content_parser import parse_file_content, file_extension_by_chunker
20
+ from ...runtime.utils.utils import IndexerKeywords
18
21
 
19
22
  QTEST_ID = "QTest Id"
20
23
 
@@ -253,7 +256,7 @@ NoInput = create_model(
253
256
  "NoInput"
254
257
  )
255
258
 
256
- class QtestApiWrapper(BaseToolApiWrapper):
259
+ class QtestApiWrapper(NonCodeIndexerToolkit):
257
260
  base_url: str
258
261
  qtest_project_id: int
259
262
  qtest_api_token: SecretStr
@@ -263,17 +266,18 @@ class QtestApiWrapper(BaseToolApiWrapper):
263
266
  _client: Any = PrivateAttr()
264
267
  _field_definitions_cache: Optional[dict] = PrivateAttr(default=None)
265
268
  _modules_cache: Optional[list] = PrivateAttr(default=None)
266
- llm: Any
269
+ _chunking_tool: Optional[str] = PrivateAttr(default=None)
270
+ _extract_images: bool = PrivateAttr(default=False)
271
+ _image_prompt: Optional[str] = PrivateAttr(default=None)
267
272
 
268
273
  @model_validator(mode='before')
269
274
  @classmethod
270
- def project_id_alias(cls, values):
271
- if 'project_id' in values:
272
- values['qtest_project_id'] = values.pop('project_id')
273
- return values
274
-
275
- @model_validator(mode='after')
276
- def validate_toolkit(self):
275
+ def validate_toolkit(cls, values):
276
+ # Handle project_id alias
277
+ # There is no such alias and this alias is breaking the scheduled indexing setting to qtest project id the value of the elitea project id.
278
+ # if 'project_id' in values:
279
+ # values['qtest_project_id'] = values.pop('project_id')
280
+
277
281
  try:
278
282
  import swagger_client # noqa: F401
279
283
  except ImportError:
@@ -282,6 +286,15 @@ class QtestApiWrapper(BaseToolApiWrapper):
282
286
  "`pip install git+https://github.com/Roman-Mitusov/qtest-api.git`"
283
287
  )
284
288
 
289
+ cls.llm = values.get('llm')
290
+ # Call parent validator to set up embeddings and vectorstore params
291
+ return super().validate_toolkit(values)
292
+
293
+ @model_validator(mode='after')
294
+ def setup_qtest_client(self):
295
+ """Initialize QTest swagger client after model validation."""
296
+ import swagger_client
297
+
285
298
  if self.qtest_api_token:
286
299
  configuration = swagger_client.Configuration()
287
300
  configuration.host = self.base_url
@@ -938,6 +951,11 @@ class QtestApiWrapper(BaseToolApiWrapper):
938
951
  parsed_data.append(parsed_data_row)
939
952
 
940
953
  def _process_image(self, content: str, extract: bool=False, prompt: str=None):
954
+ """Extract and process base64 images from HTML img tags.
955
+
956
+ IMPORTANT: This method must be called BEFORE strip_tags() because it needs
957
+ the HTML <img> tags to extract base64-encoded images.
958
+ """
941
959
  #extract image by regex
942
960
  img_regex = r'<img\s+src="data:image\/[^;]+;base64,([^"]+)"\s+[^>]*data-filename="([^"]+)"[^>]*>'
943
961
 
@@ -957,6 +975,33 @@ class QtestApiWrapper(BaseToolApiWrapper):
957
975
  content = re.sub(img_regex, replace_image, content)
958
976
  return content
959
977
 
978
+ def _clean_html_content(self, content: str, extract_images: bool = False, image_prompt: str = None) -> str:
979
+ """Clean HTML content with proper order of operations.
980
+
981
+ The correct order is:
982
+ 1. Process images first (extracts from <img> tags - needs HTML intact)
983
+ 2. Strip remaining HTML tags
984
+ 3. Unescape HTML entities
985
+
986
+ Args:
987
+ content: Raw HTML content from QTest
988
+ extract_images: Whether to extract and describe images using LLM
989
+ image_prompt: Custom prompt for image analysis
990
+
991
+ Returns:
992
+ Cleaned text content with optional image descriptions
993
+ """
994
+ import html
995
+ if not content:
996
+ return ''
997
+ # Step 1: Process images FIRST (needs HTML <img> tags intact)
998
+ content = self._process_image(content, extract_images, image_prompt)
999
+ # Step 2: Strip remaining HTML tags
1000
+ content = strip_tags(content)
1001
+ # Step 3: Unescape HTML entities
1002
+ content = html.unescape(content)
1003
+ return content
1004
+
960
1005
  def __perform_search_by_dql(self, dql: str, extract_images:bool=False, prompt: str=None) -> list:
961
1006
  search_instance: SearchApi = swagger_client.SearchApi(self._client)
962
1007
  body = swagger_client.ArtifactSearchParams(object_type='test-cases', fields=['*'],
@@ -1891,6 +1936,7 @@ class QtestApiWrapper(BaseToolApiWrapper):
1891
1936
  kwargs["search"] = search
1892
1937
  return module_api.get_sub_modules_of(project_id=self.qtest_project_id, **kwargs)
1893
1938
 
1939
+ @extend_with_parent_available_tools
1894
1940
  def get_available_tools(self):
1895
1941
  return [
1896
1942
  {
@@ -2141,4 +2187,375 @@ Examples:
2141
2187
  "args_schema": FindEntityById,
2142
2188
  "ref": self.find_entity_by_id,
2143
2189
  }
2144
- ]
2190
+ ]
2191
+
2192
+ # ==================== INDEXER METHODS ====================
2193
+
2194
+ def _index_tool_params(self, **kwargs) -> dict[str, tuple[type, Field]]:
2195
+ """
2196
+ Returns a list of fields for index_data args schema.
2197
+ Defines three indexing modes: DQL query, module-based, and full project traversal.
2198
+ """
2199
+ return {
2200
+ "chunking_tool": (Literal['markdown', ''], Field(
2201
+ description="Name of chunking tool for test case content",
2202
+ default='markdown')),
2203
+ "indexing_mode": (Literal['dql', 'module', 'full'], Field(
2204
+ description="Indexing mode: 'dql' - use DQL query (may have API limitations), "
2205
+ "'module' - index specific module/folder (most deterministic), "
2206
+ "'full' - traverse entire project with pagination",
2207
+ default='full')),
2208
+ "dql": (Optional[str], Field(
2209
+ description="DQL query for 'dql' mode. Example: \"Status = 'New' and Priority = 'High'\". "
2210
+ "Can also filter by module: \"Module in 'MD-7 Master Test Suite'\". "
2211
+ "Note: DQL via API may return incomplete results for complex queries.",
2212
+ default=None,
2213
+ json_schema_extra={'visible_when': {'field': 'indexing_mode', 'value': 'dql'}})),
2214
+ "module_name": (Optional[str], Field(
2215
+ description="Module/folder name for 'module' mode. Use the visible name from UI "
2216
+ "e.g., 'MD-7 Master Test Suite'. Most deterministic way to index a specific folder.",
2217
+ default=None,
2218
+ json_schema_extra={'visible_when': {'field': 'indexing_mode', 'value': 'module'}})),
2219
+ "extract_images": (Optional[bool], Field(
2220
+ description="Whether to extract and process images from test steps using LLM",
2221
+ default=False)),
2222
+ "image_prompt": (Optional[str], Field(
2223
+ description="Custom prompt for image analysis (only used if extract_images=True)",
2224
+ default="Analyze this image from a test case step. Describe what the image shows, including any UI elements, text, buttons, or visual indicators. Focus on elements relevant to testing.",
2225
+ json_schema_extra={'visible_when': {'field': 'extract_images', 'value': True}})),
2226
+ }
2227
+
2228
+ def _base_loader(self, **kwargs) -> Generator[Document, None, None]:
2229
+ """
2230
+ Base loader for QTest test cases. Supports three indexing modes:
2231
+ - dql: Use DQL query (may have API limitations for complex queries)
2232
+ - module: Index specific module/folder by name (most deterministic)
2233
+ - full: Full project traversal with pagination
2234
+ """
2235
+ self._chunking_tool = kwargs.get('chunking_tool', 'markdown')
2236
+ self._extract_images = kwargs.get('extract_images', False)
2237
+ self._image_prompt = kwargs.get('image_prompt', None)
2238
+
2239
+ indexing_mode = kwargs.get('indexing_mode', 'full')
2240
+ dql = kwargs.get('dql')
2241
+ module_name = kwargs.get('module_name')
2242
+
2243
+ logger.info(f"Starting QTest indexing in '{indexing_mode}' mode for project {self.qtest_project_id}")
2244
+
2245
+ if indexing_mode == 'dql':
2246
+ if not dql:
2247
+ raise ToolException("DQL query is required for 'dql' indexing mode")
2248
+ yield from self._load_test_cases_by_dql(dql)
2249
+ elif indexing_mode == 'module':
2250
+ if not module_name:
2251
+ raise ToolException("module_name is required for 'module' indexing mode")
2252
+ # Resolve module name to internal ID
2253
+ module_id = self._resolve_module_name_to_id(module_name)
2254
+ if not module_id:
2255
+ raise ToolException(
2256
+ f"Module '{module_name}' not found in project {self.qtest_project_id}. "
2257
+ f"Use get_modules tool to see available modules."
2258
+ )
2259
+ yield from self._load_test_cases_by_module(module_id)
2260
+ else: # full mode
2261
+ yield from self._load_test_cases_full_project()
2262
+
2263
+ def _resolve_module_name_to_id(self, module_name: str) -> Optional[int]:
2264
+ """
2265
+ Resolve a module name (e.g., 'MD-7 Master Test Suite') to its internal ID.
2266
+ Uses the same approach as __build_body_for_create_test_case.
2267
+ """
2268
+ modules = self._parse_modules()
2269
+ for module in modules:
2270
+ if module.get('full_module_name') == module_name:
2271
+ return module.get('module_id')
2272
+ return None
2273
+
2274
+ def _load_test_cases_by_dql(self, dql: str) -> Generator[Document, None, None]:
2275
+ """Load test cases using DQL query."""
2276
+ logger.info(f"Loading test cases by DQL: {dql}")
2277
+ search_instance: SearchApi = swagger_client.SearchApi(self._client)
2278
+ body = swagger_client.ArtifactSearchParams(
2279
+ object_type='test-cases',
2280
+ fields=['*'],
2281
+ query=dql
2282
+ )
2283
+
2284
+ page = 1
2285
+ while True:
2286
+ try:
2287
+ response = search_instance.search_artifact(
2288
+ self.qtest_project_id,
2289
+ body,
2290
+ append_test_steps='true',
2291
+ include_external_properties='true',
2292
+ page_size=self.no_of_items_per_page,
2293
+ page=page
2294
+ )
2295
+
2296
+ items = response.get('items', [])
2297
+ if not items:
2298
+ break
2299
+
2300
+ for item in items:
2301
+ yield self._create_test_case_document(item)
2302
+
2303
+ # Check for next page
2304
+ links = response.get('links', [])
2305
+ has_next = any(link.get('rel') == 'next' for link in links)
2306
+ if not has_next:
2307
+ break
2308
+ page += 1
2309
+
2310
+ except ApiException as e:
2311
+ stacktrace = format_exc()
2312
+ logger.error(f"Error loading test cases by DQL: {stacktrace}")
2313
+ raise ToolException(f"Failed to load test cases by DQL: {stacktrace}") from e
2314
+
2315
+ def _load_test_cases_by_module(self, module_id: int) -> Generator[Document, None, None]:
2316
+ """Load test cases from a specific module/folder."""
2317
+ logger.info(f"Loading test cases from module {module_id}")
2318
+ test_case_api: TestCaseApi = self.__instantiate_test_api_instance()
2319
+
2320
+ page = 1
2321
+ while True:
2322
+ try:
2323
+ response = test_case_api.get_test_cases(
2324
+ self.qtest_project_id,
2325
+ parent_id=module_id,
2326
+ page=page,
2327
+ size=self.no_of_items_per_page,
2328
+ expand_steps='true'
2329
+ )
2330
+
2331
+ if not response:
2332
+ break
2333
+
2334
+ # Convert response objects to dicts if needed
2335
+ items = [item.to_dict() if hasattr(item, 'to_dict') else item for item in response]
2336
+
2337
+ if not items:
2338
+ break
2339
+
2340
+ for item in items:
2341
+ yield self._create_test_case_document(item)
2342
+
2343
+ if len(items) < self.no_of_items_per_page:
2344
+ break
2345
+ page += 1
2346
+
2347
+ except ApiException as e:
2348
+ stacktrace = format_exc()
2349
+ logger.error(f"Error loading test cases from module: {stacktrace}")
2350
+ raise ToolException(f"Failed to load test cases from module {module_id}: {stacktrace}") from e
2351
+
2352
+ def _load_test_cases_full_project(self) -> Generator[Document, None, None]:
2353
+ """Load all test cases from the project using pagination."""
2354
+ logger.info(f"Loading all test cases from project {self.qtest_project_id}")
2355
+ test_case_api: TestCaseApi = self.__instantiate_test_api_instance()
2356
+
2357
+ page = 1
2358
+ while True:
2359
+ try:
2360
+ response = test_case_api.get_test_cases(
2361
+ self.qtest_project_id,
2362
+ page=page,
2363
+ size=self.no_of_items_per_page,
2364
+ expand_steps='true'
2365
+ )
2366
+
2367
+ if not response:
2368
+ break
2369
+
2370
+ # Convert response objects to dicts if needed
2371
+ items = [item.to_dict() if hasattr(item, 'to_dict') else item for item in response]
2372
+
2373
+ if not items:
2374
+ break
2375
+
2376
+ for item in items:
2377
+ yield self._create_test_case_document(item)
2378
+
2379
+ if len(items) < self.no_of_items_per_page:
2380
+ break
2381
+ page += 1
2382
+
2383
+ except ApiException as e:
2384
+ stacktrace = format_exc()
2385
+ logger.error(f"Error loading test cases: {stacktrace}")
2386
+ raise ToolException(f"Failed to load test cases from project: {stacktrace}") from e
2387
+
2388
+ def _create_test_case_document(self, item: dict) -> Document:
2389
+ """Create a Document from a test case item with basic metadata for duplicate detection."""
2390
+
2391
+ # Extract basic identifiers
2392
+ test_case_id = item.get('pid', '')
2393
+ qtest_id = item.get('id', '')
2394
+
2395
+ # Get updated timestamp for duplicate detection
2396
+ # Try different timestamp fields
2397
+ updated_on = (
2398
+ item.get('last_modified_date') or
2399
+ item.get('updated_date') or
2400
+ item.get('created_date') or
2401
+ ''
2402
+ )
2403
+
2404
+ # Get module/folder info
2405
+ parent_id = item.get('parent_id')
2406
+ module_name = self._get_module_name(parent_id) if parent_id else ''
2407
+
2408
+ # Build basic metadata for the document
2409
+ metadata = {
2410
+ 'id': test_case_id,
2411
+ 'qtest_id': qtest_id,
2412
+ 'updated_on': updated_on,
2413
+ 'name': item.get('name', ''),
2414
+ 'parent_id': parent_id,
2415
+ 'module_name': module_name,
2416
+ 'project_id': self.qtest_project_id,
2417
+ 'type': 'test_case',
2418
+ # Store full item for later processing in _extend_data
2419
+ '_raw_item': item,
2420
+ }
2421
+
2422
+ return Document(page_content="", metadata=metadata)
2423
+
2424
+ def _get_module_name(self, module_id: int) -> str:
2425
+ """Get module name by ID from cached modules."""
2426
+ if self._modules_cache is None:
2427
+ self._parse_modules()
2428
+
2429
+ for module in self._modules_cache or []:
2430
+ if module.get('module_id') == module_id:
2431
+ return module.get('full_module_name', module.get('module_name', ''))
2432
+ return ''
2433
+
2434
+ def _extend_data(self, documents: Generator[Document, None, None]) -> Generator[Document, None, None]:
2435
+ """
2436
+ Extend base documents with full content formatted as markdown.
2437
+ This is called after duplicate detection, so we only process documents that need indexing.
2438
+ """
2439
+
2440
+ for document in documents:
2441
+ try:
2442
+ raw_item = document.metadata.pop('_raw_item', None)
2443
+ if not raw_item:
2444
+ yield document
2445
+ continue
2446
+
2447
+ # Build markdown content for the test case
2448
+ content = self._format_test_case_as_markdown(raw_item)
2449
+
2450
+ # Store content for chunking
2451
+ document.metadata[IndexerKeywords.CONTENT_IN_BYTES.value] = content.encode('utf-8')
2452
+ document.metadata[IndexerKeywords.CONTENT_FILE_NAME.value] = f"test_case{file_extension_by_chunker(self._chunking_tool)}"
2453
+
2454
+ # Add additional metadata from properties
2455
+ for prop in raw_item.get('properties', []):
2456
+ field_name = prop.get('field_name')
2457
+ if field_name and field_name not in document.metadata:
2458
+ document.metadata[field_name.lower().replace(' ', '_')] = self.__format_property_value(prop)
2459
+
2460
+ except Exception as e:
2461
+ logger.error(f"Failed to extend document {document.metadata.get('id')}: {e}")
2462
+
2463
+ yield document
2464
+
2465
+ def _format_test_case_as_markdown(self, item: dict) -> str:
2466
+ """Format a test case as markdown for better semantic search."""
2467
+
2468
+ lines = []
2469
+
2470
+ # Header
2471
+ test_id = item.get('pid', 'Unknown')
2472
+ name = item.get('name', 'Untitled')
2473
+ lines.append(f"# Test Case: {test_id} - {name}")
2474
+ lines.append("")
2475
+
2476
+ # Module/Folder
2477
+ parent_id = item.get('parent_id')
2478
+ if parent_id:
2479
+ module_name = self._get_module_name(parent_id)
2480
+ if module_name:
2481
+ lines.append(f"## Module")
2482
+ lines.append(module_name)
2483
+ lines.append("")
2484
+
2485
+ # Description
2486
+ description = item.get('description', '')
2487
+ if description:
2488
+ description = self._clean_html_content(
2489
+ description,
2490
+ self._extract_images,
2491
+ self._image_prompt
2492
+ )
2493
+ lines.append("## Description")
2494
+ lines.append(description)
2495
+ lines.append("")
2496
+
2497
+ # Precondition
2498
+ precondition = item.get('precondition', '')
2499
+ if precondition:
2500
+ precondition = self._clean_html_content(
2501
+ precondition,
2502
+ self._extract_images,
2503
+ self._image_prompt
2504
+ )
2505
+ lines.append("## Precondition")
2506
+ lines.append(precondition)
2507
+ lines.append("")
2508
+
2509
+ # Properties (Status, Type, Priority, etc.)
2510
+ properties = item.get('properties', [])
2511
+ if properties:
2512
+ lines.append("## Properties")
2513
+ for prop in properties:
2514
+ field_name = prop.get('field_name', '')
2515
+ field_value = self.__format_property_value(prop)
2516
+ if field_name and field_value:
2517
+ if isinstance(field_value, list):
2518
+ field_value = ', '.join(str(v) for v in field_value)
2519
+ lines.append(f"- **{field_name}**: {field_value}")
2520
+ lines.append("")
2521
+
2522
+ # Test Steps
2523
+ test_steps = item.get('test_steps', [])
2524
+ if test_steps:
2525
+ lines.append("## Test Steps")
2526
+ lines.append("")
2527
+
2528
+ for idx, step in enumerate(test_steps, 1):
2529
+ step_desc = step.get('description', '')
2530
+ step_expected = step.get('expected', '')
2531
+
2532
+ # Clean HTML content (processes images first, then strips tags)
2533
+ step_desc = self._clean_html_content(
2534
+ step_desc,
2535
+ self._extract_images,
2536
+ self._image_prompt
2537
+ )
2538
+ step_expected = self._clean_html_content(
2539
+ step_expected,
2540
+ self._extract_images,
2541
+ self._image_prompt
2542
+ )
2543
+
2544
+ lines.append(f"### Step {idx}")
2545
+ if step_desc:
2546
+ lines.append(f"**Action:** {step_desc}")
2547
+ if step_expected:
2548
+ lines.append(f"**Expected Result:** {step_expected}")
2549
+ lines.append("")
2550
+
2551
+ return '\n'.join(lines)
2552
+
2553
+ def _process_document(self, base_document: Document) -> Generator[Document, None, None]:
2554
+ """
2555
+ Process a base document to extract dependent documents (images).
2556
+ Currently yields nothing as image content is inline in the markdown.
2557
+ Can be extended to yield separate image documents if needed.
2558
+ """
2559
+ # For now, images are processed inline in the markdown content.
2560
+ # If separate image documents are needed in the future, they can be yielded here.
2561
+ yield from ()
@@ -5,8 +5,9 @@ from .api_wrapper import RallyApiWrapper
5
5
  from langchain_core.tools import BaseTool
6
6
  from ..base.tool import BaseAction
7
7
  from ..elitea_base import filter_missconfigured_index_tools
8
- from ..utils import clean_string, TOOLKIT_SPLITTER, get_max_toolkit_length
8
+ from ..utils import clean_string, get_max_toolkit_length
9
9
  from ...configurations.rally import RallyConfiguration
10
+ from ...runtime.utils.constants import TOOLKIT_NAME_META, TOOL_NAME_META, TOOLKIT_TYPE_META
10
11
 
11
12
  name = "rally"
12
13
 
@@ -21,12 +22,10 @@ def get_tools(tool):
21
22
 
22
23
  class RallyToolkit(BaseToolkit):
23
24
  tools: List[BaseTool] = []
24
- toolkit_max_length: int = 0
25
25
 
26
26
  @staticmethod
27
27
  def toolkit_config_schema() -> BaseModel:
28
28
  selected_tools = {x['name']: x['args_schema'].schema() for x in RallyApiWrapper.model_construct().get_available_tools()}
29
- RallyToolkit.toolkit_max_length = get_max_toolkit_length(selected_tools)
30
29
  return create_model(
31
30
  name,
32
31
  rally_configuration=(RallyConfiguration, Field(description="Rally configuration", json_schema_extra={'configuration_types': ['rally']})),
@@ -37,7 +36,6 @@ class RallyToolkit(BaseToolkit):
37
36
  'metadata': {
38
37
  "label": "Rally",
39
38
  "icon_url": "rally.svg",
40
- "max_length": RallyToolkit.toolkit_max_length,
41
39
  "categories": ["project management"],
42
40
  "extra_categories": ["agile management", "test management", "scrum", "kanban"]
43
41
  }
@@ -54,18 +52,22 @@ class RallyToolkit(BaseToolkit):
54
52
  **kwargs.get('rally_configuration'),
55
53
  }
56
54
  rally_api_wrapper = RallyApiWrapper(**wrapper_payload)
57
- prefix = clean_string(toolkit_name, cls.toolkit_max_length) + TOOLKIT_SPLITTER if toolkit_name else ''
58
55
  available_tools = rally_api_wrapper.get_available_tools()
59
56
  tools = []
60
57
  for tool in available_tools:
61
58
  if selected_tools:
62
59
  if tool["name"] not in selected_tools:
63
60
  continue
61
+ description = f"{tool['description']}\nWorkspace: {rally_api_wrapper.workspace}. Project: {rally_api_wrapper.project}"
62
+ if toolkit_name:
63
+ description = f"{description}\nToolkit: {toolkit_name}"
64
+ description = description[:1000]
64
65
  tools.append(BaseAction(
65
66
  api_wrapper=rally_api_wrapper,
66
- name=prefix + tool["name"],
67
- description=f"{tool['description']}\nWorkspace: {rally_api_wrapper.workspace}. Project: {rally_api_wrapper.project}",
68
- args_schema=tool["args_schema"]
67
+ name=tool["name"],
68
+ description=description,
69
+ args_schema=tool["args_schema"],
70
+ metadata={TOOLKIT_NAME_META: toolkit_name, TOOLKIT_TYPE_META: name, TOOL_NAME_META: tool["name"]} if toolkit_name else {TOOL_NAME_META: tool["name"]}
69
71
  ))
70
72
  return cls(tools=tools)
71
73
 
@@ -40,7 +40,7 @@ RallyGetEntities = create_model(
40
40
  entity_type=(Optional[str], Field(description="Artifact type, e.g. 'HierarchicalRequirement', 'Defect', 'UserStory'", default="UserStory")),
41
41
  query=(Optional[str], Field(description="Query for searching Rally stories", default=None)),
42
42
  fetch=(Optional[bool], Field(description="Whether to fetch the full details of the stories", default=True)),
43
- limit=(Optional[int], Field(description="Limit the number of results", default=10))
43
+ limit=(Optional[int], Field(description="Limit the number of results", default=10, gt=0))
44
44
  )
45
45
 
46
46
  RallyGetProject = create_model(
@@ -7,8 +7,9 @@ from pydantic import create_model, BaseModel, ConfigDict, Field
7
7
  from .api_wrapper import ReportPortalApiWrapper
8
8
  from ..base.tool import BaseAction
9
9
  from ..elitea_base import filter_missconfigured_index_tools
10
- from ..utils import clean_string, TOOLKIT_SPLITTER, get_max_toolkit_length
10
+ from ..utils import clean_string, get_max_toolkit_length
11
11
  from ...configurations.report_portal import ReportPortalConfiguration
12
+ from ...runtime.utils.constants import TOOLKIT_NAME_META, TOOL_NAME_META, TOOLKIT_TYPE_META
12
13
 
13
14
  name = "report_portal"
14
15
 
@@ -21,19 +22,16 @@ def get_tools(tool):
21
22
 
22
23
 
23
24
  class ReportPortalToolkit(BaseToolkit):
24
- tools: list[BaseTool] = []
25
- toolkit_max_length: int = 0
25
+ tools: List[BaseTool] = []
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 ReportPortalApiWrapper.model_construct().get_available_tools()}
30
- ReportPortalToolkit.toolkit_max_length = get_max_toolkit_length(selected_tools)
31
30
  return create_model(
32
31
  name,
33
32
  report_portal_configuration=(ReportPortalConfiguration, Field(description="Report Portal Configuration", json_schema_extra={'configuration_types': ['report_portal']})),
34
33
  selected_tools=(List[Literal[tuple(selected_tools)]], Field(default=[], json_schema_extra={'args_schemas': selected_tools})),
35
34
  __config__=ConfigDict(json_schema_extra={'metadata': {"label": "Report Portal", "icon_url": "reportportal-icon.svg",
36
- "max_length": ReportPortalToolkit.toolkit_max_length,
37
35
  "categories": ["testing"],
38
36
  "extra_categories": ["test reporting", "test automation"]}})
39
37
  )
@@ -48,17 +46,22 @@ class ReportPortalToolkit(BaseToolkit):
48
46
  **kwargs.get('report_portal_configuration', {}),
49
47
  }
50
48
  report_portal_api_wrapper = ReportPortalApiWrapper(**wrapper_payload)
51
- prefix = clean_string(toolkit_name, cls.toolkit_max_length) + TOOLKIT_SPLITTER if toolkit_name else ''
52
49
  available_tools = report_portal_api_wrapper.get_available_tools()
53
50
  tools = []
54
51
  for tool in available_tools:
55
52
  if selected_tools and tool["name"] not in selected_tools:
56
53
  continue
54
+ description = tool['description']
55
+ if toolkit_name:
56
+ description = f"Toolkit: {toolkit_name}\n{description}"
57
+ description = f"{description}\nReport portal configuration: 'url - {report_portal_api_wrapper.endpoint}, project - {report_portal_api_wrapper.project}'"
58
+ description = description[:1000]
57
59
  tools.append(BaseAction(
58
60
  api_wrapper=report_portal_api_wrapper,
59
- name=prefix + tool["name"],
60
- description=f"{tool['description']}\nReport portal configuration: 'url - {report_portal_api_wrapper.endpoint}, project - {report_portal_api_wrapper.project}'",
61
- args_schema=tool["args_schema"]
61
+ name=tool["name"],
62
+ description=description,
63
+ args_schema=tool["args_schema"],
64
+ metadata={TOOLKIT_NAME_META: toolkit_name, TOOLKIT_TYPE_META: name, TOOL_NAME_META: tool["name"]} if toolkit_name else {TOOL_NAME_META: tool["name"]}
62
65
  ))
63
66
  return cls(tools=tools)
64
67