alita-sdk 0.3.257__py3-none-any.whl → 0.3.584__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 (281) hide show
  1. alita_sdk/cli/__init__.py +10 -0
  2. alita_sdk/cli/__main__.py +17 -0
  3. alita_sdk/cli/agent/__init__.py +5 -0
  4. alita_sdk/cli/agent/default.py +258 -0
  5. alita_sdk/cli/agent_executor.py +155 -0
  6. alita_sdk/cli/agent_loader.py +215 -0
  7. alita_sdk/cli/agent_ui.py +228 -0
  8. alita_sdk/cli/agents.py +3794 -0
  9. alita_sdk/cli/callbacks.py +647 -0
  10. alita_sdk/cli/cli.py +168 -0
  11. alita_sdk/cli/config.py +306 -0
  12. alita_sdk/cli/context/__init__.py +30 -0
  13. alita_sdk/cli/context/cleanup.py +198 -0
  14. alita_sdk/cli/context/manager.py +731 -0
  15. alita_sdk/cli/context/message.py +285 -0
  16. alita_sdk/cli/context/strategies.py +289 -0
  17. alita_sdk/cli/context/token_estimation.py +127 -0
  18. alita_sdk/cli/formatting.py +182 -0
  19. alita_sdk/cli/input_handler.py +419 -0
  20. alita_sdk/cli/inventory.py +1073 -0
  21. alita_sdk/cli/mcp_loader.py +315 -0
  22. alita_sdk/cli/toolkit.py +327 -0
  23. alita_sdk/cli/toolkit_loader.py +85 -0
  24. alita_sdk/cli/tools/__init__.py +43 -0
  25. alita_sdk/cli/tools/approval.py +224 -0
  26. alita_sdk/cli/tools/filesystem.py +1751 -0
  27. alita_sdk/cli/tools/planning.py +389 -0
  28. alita_sdk/cli/tools/terminal.py +414 -0
  29. alita_sdk/community/__init__.py +72 -12
  30. alita_sdk/community/inventory/__init__.py +236 -0
  31. alita_sdk/community/inventory/config.py +257 -0
  32. alita_sdk/community/inventory/enrichment.py +2137 -0
  33. alita_sdk/community/inventory/extractors.py +1469 -0
  34. alita_sdk/community/inventory/ingestion.py +3172 -0
  35. alita_sdk/community/inventory/knowledge_graph.py +1457 -0
  36. alita_sdk/community/inventory/parsers/__init__.py +218 -0
  37. alita_sdk/community/inventory/parsers/base.py +295 -0
  38. alita_sdk/community/inventory/parsers/csharp_parser.py +907 -0
  39. alita_sdk/community/inventory/parsers/go_parser.py +851 -0
  40. alita_sdk/community/inventory/parsers/html_parser.py +389 -0
  41. alita_sdk/community/inventory/parsers/java_parser.py +593 -0
  42. alita_sdk/community/inventory/parsers/javascript_parser.py +629 -0
  43. alita_sdk/community/inventory/parsers/kotlin_parser.py +768 -0
  44. alita_sdk/community/inventory/parsers/markdown_parser.py +362 -0
  45. alita_sdk/community/inventory/parsers/python_parser.py +604 -0
  46. alita_sdk/community/inventory/parsers/rust_parser.py +858 -0
  47. alita_sdk/community/inventory/parsers/swift_parser.py +832 -0
  48. alita_sdk/community/inventory/parsers/text_parser.py +322 -0
  49. alita_sdk/community/inventory/parsers/yaml_parser.py +370 -0
  50. alita_sdk/community/inventory/patterns/__init__.py +61 -0
  51. alita_sdk/community/inventory/patterns/ast_adapter.py +380 -0
  52. alita_sdk/community/inventory/patterns/loader.py +348 -0
  53. alita_sdk/community/inventory/patterns/registry.py +198 -0
  54. alita_sdk/community/inventory/presets.py +535 -0
  55. alita_sdk/community/inventory/retrieval.py +1403 -0
  56. alita_sdk/community/inventory/toolkit.py +173 -0
  57. alita_sdk/community/inventory/toolkit_utils.py +176 -0
  58. alita_sdk/community/inventory/visualize.py +1370 -0
  59. alita_sdk/configurations/__init__.py +11 -0
  60. alita_sdk/configurations/ado.py +148 -2
  61. alita_sdk/configurations/azure_search.py +1 -1
  62. alita_sdk/configurations/bigquery.py +1 -1
  63. alita_sdk/configurations/bitbucket.py +94 -2
  64. alita_sdk/configurations/browser.py +18 -0
  65. alita_sdk/configurations/carrier.py +19 -0
  66. alita_sdk/configurations/confluence.py +130 -1
  67. alita_sdk/configurations/delta_lake.py +1 -1
  68. alita_sdk/configurations/figma.py +76 -5
  69. alita_sdk/configurations/github.py +65 -1
  70. alita_sdk/configurations/gitlab.py +81 -0
  71. alita_sdk/configurations/google_places.py +17 -0
  72. alita_sdk/configurations/jira.py +103 -0
  73. alita_sdk/configurations/openapi.py +323 -0
  74. alita_sdk/configurations/postman.py +1 -1
  75. alita_sdk/configurations/qtest.py +72 -3
  76. alita_sdk/configurations/report_portal.py +115 -0
  77. alita_sdk/configurations/salesforce.py +19 -0
  78. alita_sdk/configurations/service_now.py +1 -12
  79. alita_sdk/configurations/sharepoint.py +167 -0
  80. alita_sdk/configurations/sonar.py +18 -0
  81. alita_sdk/configurations/sql.py +20 -0
  82. alita_sdk/configurations/testio.py +101 -0
  83. alita_sdk/configurations/testrail.py +88 -0
  84. alita_sdk/configurations/xray.py +94 -1
  85. alita_sdk/configurations/zephyr_enterprise.py +94 -1
  86. alita_sdk/configurations/zephyr_essential.py +95 -0
  87. alita_sdk/runtime/clients/artifact.py +21 -4
  88. alita_sdk/runtime/clients/client.py +458 -67
  89. alita_sdk/runtime/clients/mcp_discovery.py +342 -0
  90. alita_sdk/runtime/clients/mcp_manager.py +262 -0
  91. alita_sdk/runtime/clients/sandbox_client.py +352 -0
  92. alita_sdk/runtime/langchain/_constants_bkup.py +1318 -0
  93. alita_sdk/runtime/langchain/assistant.py +183 -43
  94. alita_sdk/runtime/langchain/constants.py +647 -1
  95. alita_sdk/runtime/langchain/document_loaders/AlitaDocxMammothLoader.py +315 -3
  96. alita_sdk/runtime/langchain/document_loaders/AlitaExcelLoader.py +209 -31
  97. alita_sdk/runtime/langchain/document_loaders/AlitaImageLoader.py +1 -1
  98. alita_sdk/runtime/langchain/document_loaders/AlitaJSONLinesLoader.py +77 -0
  99. alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +10 -3
  100. alita_sdk/runtime/langchain/document_loaders/AlitaMarkdownLoader.py +66 -0
  101. alita_sdk/runtime/langchain/document_loaders/AlitaPDFLoader.py +79 -10
  102. alita_sdk/runtime/langchain/document_loaders/AlitaPowerPointLoader.py +52 -15
  103. alita_sdk/runtime/langchain/document_loaders/AlitaPythonLoader.py +9 -0
  104. alita_sdk/runtime/langchain/document_loaders/AlitaTableLoader.py +1 -4
  105. alita_sdk/runtime/langchain/document_loaders/AlitaTextLoader.py +15 -2
  106. alita_sdk/runtime/langchain/document_loaders/ImageParser.py +30 -0
  107. alita_sdk/runtime/langchain/document_loaders/constants.py +189 -41
  108. alita_sdk/runtime/langchain/interfaces/llm_processor.py +4 -2
  109. alita_sdk/runtime/langchain/langraph_agent.py +493 -105
  110. alita_sdk/runtime/langchain/utils.py +118 -8
  111. alita_sdk/runtime/llms/preloaded.py +2 -6
  112. alita_sdk/runtime/models/mcp_models.py +61 -0
  113. alita_sdk/runtime/skills/__init__.py +91 -0
  114. alita_sdk/runtime/skills/callbacks.py +498 -0
  115. alita_sdk/runtime/skills/discovery.py +540 -0
  116. alita_sdk/runtime/skills/executor.py +610 -0
  117. alita_sdk/runtime/skills/input_builder.py +371 -0
  118. alita_sdk/runtime/skills/models.py +330 -0
  119. alita_sdk/runtime/skills/registry.py +355 -0
  120. alita_sdk/runtime/skills/skill_runner.py +330 -0
  121. alita_sdk/runtime/toolkits/__init__.py +28 -0
  122. alita_sdk/runtime/toolkits/application.py +14 -4
  123. alita_sdk/runtime/toolkits/artifact.py +25 -9
  124. alita_sdk/runtime/toolkits/datasource.py +13 -6
  125. alita_sdk/runtime/toolkits/mcp.py +782 -0
  126. alita_sdk/runtime/toolkits/planning.py +178 -0
  127. alita_sdk/runtime/toolkits/skill_router.py +238 -0
  128. alita_sdk/runtime/toolkits/subgraph.py +11 -6
  129. alita_sdk/runtime/toolkits/tools.py +314 -70
  130. alita_sdk/runtime/toolkits/vectorstore.py +11 -5
  131. alita_sdk/runtime/tools/__init__.py +24 -0
  132. alita_sdk/runtime/tools/application.py +16 -4
  133. alita_sdk/runtime/tools/artifact.py +367 -33
  134. alita_sdk/runtime/tools/data_analysis.py +183 -0
  135. alita_sdk/runtime/tools/function.py +100 -4
  136. alita_sdk/runtime/tools/graph.py +81 -0
  137. alita_sdk/runtime/tools/image_generation.py +218 -0
  138. alita_sdk/runtime/tools/llm.py +1032 -177
  139. alita_sdk/runtime/tools/loop.py +3 -1
  140. alita_sdk/runtime/tools/loop_output.py +3 -1
  141. alita_sdk/runtime/tools/mcp_inspect_tool.py +284 -0
  142. alita_sdk/runtime/tools/mcp_remote_tool.py +181 -0
  143. alita_sdk/runtime/tools/mcp_server_tool.py +3 -1
  144. alita_sdk/runtime/tools/planning/__init__.py +36 -0
  145. alita_sdk/runtime/tools/planning/models.py +246 -0
  146. alita_sdk/runtime/tools/planning/wrapper.py +607 -0
  147. alita_sdk/runtime/tools/router.py +2 -1
  148. alita_sdk/runtime/tools/sandbox.py +375 -0
  149. alita_sdk/runtime/tools/skill_router.py +776 -0
  150. alita_sdk/runtime/tools/tool.py +3 -1
  151. alita_sdk/runtime/tools/vectorstore.py +69 -65
  152. alita_sdk/runtime/tools/vectorstore_base.py +163 -90
  153. alita_sdk/runtime/utils/AlitaCallback.py +137 -21
  154. alita_sdk/runtime/utils/constants.py +5 -1
  155. alita_sdk/runtime/utils/mcp_client.py +492 -0
  156. alita_sdk/runtime/utils/mcp_oauth.py +361 -0
  157. alita_sdk/runtime/utils/mcp_sse_client.py +434 -0
  158. alita_sdk/runtime/utils/mcp_tools_discovery.py +124 -0
  159. alita_sdk/runtime/utils/streamlit.py +41 -14
  160. alita_sdk/runtime/utils/toolkit_utils.py +28 -9
  161. alita_sdk/runtime/utils/utils.py +48 -0
  162. alita_sdk/tools/__init__.py +135 -37
  163. alita_sdk/tools/ado/__init__.py +2 -2
  164. alita_sdk/tools/ado/repos/__init__.py +16 -19
  165. alita_sdk/tools/ado/repos/repos_wrapper.py +12 -20
  166. alita_sdk/tools/ado/test_plan/__init__.py +27 -8
  167. alita_sdk/tools/ado/test_plan/test_plan_wrapper.py +56 -28
  168. alita_sdk/tools/ado/wiki/__init__.py +28 -12
  169. alita_sdk/tools/ado/wiki/ado_wrapper.py +114 -40
  170. alita_sdk/tools/ado/work_item/__init__.py +28 -12
  171. alita_sdk/tools/ado/work_item/ado_wrapper.py +95 -11
  172. alita_sdk/tools/advanced_jira_mining/__init__.py +13 -8
  173. alita_sdk/tools/aws/delta_lake/__init__.py +15 -11
  174. alita_sdk/tools/aws/delta_lake/tool.py +5 -1
  175. alita_sdk/tools/azure_ai/search/__init__.py +14 -8
  176. alita_sdk/tools/base/tool.py +5 -1
  177. alita_sdk/tools/base_indexer_toolkit.py +454 -110
  178. alita_sdk/tools/bitbucket/__init__.py +28 -19
  179. alita_sdk/tools/bitbucket/api_wrapper.py +285 -27
  180. alita_sdk/tools/bitbucket/cloud_api_wrapper.py +5 -5
  181. alita_sdk/tools/browser/__init__.py +41 -16
  182. alita_sdk/tools/browser/crawler.py +3 -1
  183. alita_sdk/tools/browser/utils.py +15 -6
  184. alita_sdk/tools/carrier/__init__.py +18 -17
  185. alita_sdk/tools/carrier/backend_reports_tool.py +8 -4
  186. alita_sdk/tools/carrier/excel_reporter.py +8 -4
  187. alita_sdk/tools/chunkers/__init__.py +3 -1
  188. alita_sdk/tools/chunkers/code/codeparser.py +1 -1
  189. alita_sdk/tools/chunkers/sematic/json_chunker.py +2 -1
  190. alita_sdk/tools/chunkers/sematic/markdown_chunker.py +97 -6
  191. alita_sdk/tools/chunkers/sematic/proposal_chunker.py +1 -1
  192. alita_sdk/tools/chunkers/universal_chunker.py +270 -0
  193. alita_sdk/tools/cloud/aws/__init__.py +12 -7
  194. alita_sdk/tools/cloud/azure/__init__.py +12 -7
  195. alita_sdk/tools/cloud/gcp/__init__.py +12 -7
  196. alita_sdk/tools/cloud/k8s/__init__.py +12 -7
  197. alita_sdk/tools/code/linter/__init__.py +10 -8
  198. alita_sdk/tools/code/loaders/codesearcher.py +3 -2
  199. alita_sdk/tools/code/sonar/__init__.py +21 -13
  200. alita_sdk/tools/code_indexer_toolkit.py +199 -0
  201. alita_sdk/tools/confluence/__init__.py +22 -14
  202. alita_sdk/tools/confluence/api_wrapper.py +197 -58
  203. alita_sdk/tools/confluence/loader.py +14 -2
  204. alita_sdk/tools/custom_open_api/__init__.py +12 -5
  205. alita_sdk/tools/elastic/__init__.py +11 -8
  206. alita_sdk/tools/elitea_base.py +546 -64
  207. alita_sdk/tools/figma/__init__.py +60 -11
  208. alita_sdk/tools/figma/api_wrapper.py +1400 -167
  209. alita_sdk/tools/figma/figma_client.py +73 -0
  210. alita_sdk/tools/figma/toon_tools.py +2748 -0
  211. alita_sdk/tools/github/__init__.py +18 -17
  212. alita_sdk/tools/github/api_wrapper.py +9 -26
  213. alita_sdk/tools/github/github_client.py +81 -12
  214. alita_sdk/tools/github/schemas.py +2 -1
  215. alita_sdk/tools/github/tool.py +5 -1
  216. alita_sdk/tools/gitlab/__init__.py +19 -13
  217. alita_sdk/tools/gitlab/api_wrapper.py +256 -80
  218. alita_sdk/tools/gitlab_org/__init__.py +14 -10
  219. alita_sdk/tools/google/bigquery/__init__.py +14 -13
  220. alita_sdk/tools/google/bigquery/tool.py +5 -1
  221. alita_sdk/tools/google_places/__init__.py +21 -11
  222. alita_sdk/tools/jira/__init__.py +22 -11
  223. alita_sdk/tools/jira/api_wrapper.py +315 -168
  224. alita_sdk/tools/keycloak/__init__.py +11 -8
  225. alita_sdk/tools/localgit/__init__.py +9 -3
  226. alita_sdk/tools/localgit/local_git.py +62 -54
  227. alita_sdk/tools/localgit/tool.py +5 -1
  228. alita_sdk/tools/memory/__init__.py +38 -14
  229. alita_sdk/tools/non_code_indexer_toolkit.py +7 -2
  230. alita_sdk/tools/ocr/__init__.py +11 -8
  231. alita_sdk/tools/openapi/__init__.py +491 -106
  232. alita_sdk/tools/openapi/api_wrapper.py +1357 -0
  233. alita_sdk/tools/openapi/tool.py +20 -0
  234. alita_sdk/tools/pandas/__init__.py +20 -12
  235. alita_sdk/tools/pandas/api_wrapper.py +40 -45
  236. alita_sdk/tools/pandas/dataframe/generator/base.py +3 -1
  237. alita_sdk/tools/postman/__init__.py +11 -11
  238. alita_sdk/tools/postman/api_wrapper.py +19 -8
  239. alita_sdk/tools/postman/postman_analysis.py +8 -1
  240. alita_sdk/tools/pptx/__init__.py +11 -10
  241. alita_sdk/tools/qtest/__init__.py +22 -14
  242. alita_sdk/tools/qtest/api_wrapper.py +1784 -88
  243. alita_sdk/tools/rally/__init__.py +13 -10
  244. alita_sdk/tools/report_portal/__init__.py +23 -16
  245. alita_sdk/tools/salesforce/__init__.py +22 -16
  246. alita_sdk/tools/servicenow/__init__.py +21 -16
  247. alita_sdk/tools/servicenow/api_wrapper.py +1 -1
  248. alita_sdk/tools/sharepoint/__init__.py +17 -14
  249. alita_sdk/tools/sharepoint/api_wrapper.py +179 -39
  250. alita_sdk/tools/sharepoint/authorization_helper.py +191 -1
  251. alita_sdk/tools/sharepoint/utils.py +8 -2
  252. alita_sdk/tools/slack/__init__.py +13 -8
  253. alita_sdk/tools/sql/__init__.py +22 -19
  254. alita_sdk/tools/sql/api_wrapper.py +71 -23
  255. alita_sdk/tools/testio/__init__.py +21 -13
  256. alita_sdk/tools/testrail/__init__.py +13 -11
  257. alita_sdk/tools/testrail/api_wrapper.py +214 -46
  258. alita_sdk/tools/utils/__init__.py +28 -4
  259. alita_sdk/tools/utils/content_parser.py +241 -55
  260. alita_sdk/tools/utils/text_operations.py +254 -0
  261. alita_sdk/tools/vector_adapters/VectorStoreAdapter.py +83 -27
  262. alita_sdk/tools/xray/__init__.py +18 -14
  263. alita_sdk/tools/xray/api_wrapper.py +58 -113
  264. alita_sdk/tools/yagmail/__init__.py +9 -3
  265. alita_sdk/tools/zephyr/__init__.py +12 -7
  266. alita_sdk/tools/zephyr_enterprise/__init__.py +16 -9
  267. alita_sdk/tools/zephyr_enterprise/api_wrapper.py +30 -15
  268. alita_sdk/tools/zephyr_essential/__init__.py +16 -10
  269. alita_sdk/tools/zephyr_essential/api_wrapper.py +297 -54
  270. alita_sdk/tools/zephyr_essential/client.py +6 -4
  271. alita_sdk/tools/zephyr_scale/__init__.py +13 -8
  272. alita_sdk/tools/zephyr_scale/api_wrapper.py +39 -31
  273. alita_sdk/tools/zephyr_squad/__init__.py +12 -7
  274. {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.584.dist-info}/METADATA +184 -37
  275. alita_sdk-0.3.584.dist-info/RECORD +452 -0
  276. alita_sdk-0.3.584.dist-info/entry_points.txt +2 -0
  277. alita_sdk/tools/bitbucket/tools.py +0 -304
  278. alita_sdk-0.3.257.dist-info/RECORD +0 -343
  279. {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.584.dist-info}/WHEEL +0 -0
  280. {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.584.dist-info}/licenses/LICENSE +0 -0
  281. {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.584.dist-info}/top_level.txt +0 -0
@@ -5,15 +5,18 @@ from pydantic import create_model, BaseModel, Field
5
5
 
6
6
  from .api_wrapper import ZephyrEssentialApiWrapper
7
7
  from ..base.tool import BaseAction
8
- from ..utils import clean_string, TOOLKIT_SPLITTER, get_max_toolkit_length
8
+ from ..elitea_base import filter_missconfigured_index_tools
9
+ from ..utils import clean_string, get_max_toolkit_length
9
10
  from ...configurations.pgvector import PgVectorConfiguration
11
+ from ...configurations.zephyr_essential import ZephyrEssentialConfiguration
12
+ from ...runtime.utils.constants import TOOLKIT_NAME_META, TOOL_NAME_META, TOOLKIT_TYPE_META
10
13
 
11
14
  name = "zephyr_essential"
12
15
 
13
16
  def get_tools(tool):
14
17
  return ZephyrEssentialToolkit().get_toolkit(
15
18
  selected_tools=tool['settings'].get('selected_tools', []),
16
- token=tool['settings']["token"],
19
+ zephyr_essential_configuration=tool['settings']['zephyr_essential_configuration'],
17
20
  toolkit_name=tool.get('toolkit_name'),
18
21
  llm = tool['settings'].get('llm', None),
19
22
  alita=tool['settings'].get('alita', None),
@@ -27,16 +30,13 @@ def get_tools(tool):
27
30
 
28
31
  class ZephyrEssentialToolkit(BaseToolkit):
29
32
  tools: List[BaseTool] = []
30
- toolkit_max_length: int = 0
31
33
 
32
34
  @staticmethod
33
35
  def toolkit_config_schema() -> BaseModel:
34
36
  selected_tools = {x['name']: x['args_schema'].schema() for x in ZephyrEssentialApiWrapper.model_construct().get_available_tools()}
35
- ZephyrEssentialToolkit.toolkit_max_length = get_max_toolkit_length(selected_tools)
36
37
  return create_model(
37
38
  name,
38
- token=(str, Field(description="Bearer api token")),
39
- base_url=(Optional[str], Field(description="Zephyr Essential base url", default=None)),
39
+ zephyr_essential_configuration=(ZephyrEssentialConfiguration, Field(description="Zephyr Essential Configuration", json_schema_extra={'configuration_types': ['zephyr_essential']})),
40
40
  selected_tools=(List[Literal[tuple(selected_tools)]], Field(default=[], json_schema_extra={'args_schemas': selected_tools})),
41
41
  pgvector_configuration=(Optional[PgVectorConfiguration], Field(default=None,
42
42
  description="PgVector Configuration",
@@ -51,26 +51,32 @@ class ZephyrEssentialToolkit(BaseToolkit):
51
51
  )
52
52
 
53
53
  @classmethod
54
+ @filter_missconfigured_index_tools
54
55
  def get_toolkit(cls, selected_tools: list[str] | None = None, toolkit_name: Optional[str] = None, **kwargs):
55
56
  if selected_tools is None:
56
57
  selected_tools = []
57
58
  wrapper_payload = {
58
59
  **kwargs,
60
+ **kwargs.get('zephyr_essential_configuration', {}),
59
61
  **(kwargs.get('pgvector_configuration') or {}),
60
62
  }
61
63
  zephyr_api_wrapper = ZephyrEssentialApiWrapper(**wrapper_payload)
62
- prefix = clean_string(toolkit_name, cls.toolkit_max_length) + TOOLKIT_SPLITTER if toolkit_name else ''
63
64
  available_tools = zephyr_api_wrapper.get_available_tools()
64
65
  tools = []
65
66
  for tool in available_tools:
66
67
  if selected_tools:
67
68
  if tool["name"] not in selected_tools:
68
69
  continue
70
+ description = tool["description"]
71
+ if toolkit_name:
72
+ description = f"Toolkit: {toolkit_name}\n{description}"
73
+ description = description[:1000]
69
74
  tools.append(BaseAction(
70
75
  api_wrapper=zephyr_api_wrapper,
71
- name=prefix + tool["name"],
72
- description=tool["description"],
73
- args_schema=tool["args_schema"]
76
+ name=tool["name"],
77
+ description=description,
78
+ args_schema=tool["args_schema"],
79
+ metadata={TOOLKIT_NAME_META: toolkit_name, TOOLKIT_TYPE_META: name, TOOL_NAME_META: tool["name"]} if toolkit_name else {TOOL_NAME_META: tool["name"]}
74
80
  ))
75
81
  return cls(tools=tools)
76
82
 
@@ -1,15 +1,17 @@
1
+ import hashlib
1
2
  import json
2
3
  import logging
3
4
  from typing import Optional, Generator, Literal
4
5
  from pydantic import model_validator, create_model, Field, SecretStr, PrivateAttr
5
6
 
6
7
  from .client import ZephyrEssentialAPI
7
- from ..elitea_base import extend_with_vector_tools, BaseVectorStoreToolApiWrapper
8
8
  from langchain_core.documents import Document
9
9
  from langchain_core.tools import ToolException
10
10
 
11
11
  from ..non_code_indexer_toolkit import NonCodeIndexerToolkit
12
12
  from ..utils.available_tools_decorator import extend_with_parent_available_tools
13
+ from ..utils.content_parser import file_extension_by_chunker
14
+ from ...runtime.utils.utils import IndexerKeywords
13
15
 
14
16
 
15
17
  class ZephyrEssentialApiWrapper(NonCodeIndexerToolkit):
@@ -180,12 +182,43 @@ class ZephyrEssentialApiWrapper(NonCodeIndexerToolkit):
180
182
  def create_folder(self, json: str):
181
183
  """Create a new folder."""
182
184
  folder_data = self._parse_json(json)
185
+ if 'parentId' not in folder_data:
186
+ if 'parentName' in folder_data:
187
+ parent_folder_name = folder_data['parentName']
188
+
189
+ parent_folder = self.find_folder_by_name(parent_folder_name)
190
+
191
+ if isinstance(parent_folder, ToolException):
192
+ return ToolException(f"Folder with name '{parent_folder_name}' not found.")
193
+ else:
194
+ folder_data['parentId'] = parent_folder['id']
195
+
183
196
  return self._client.create_folder(folder_data)
184
197
 
185
198
  def get_folder(self, folder_id: str):
186
199
  """Retrieve details of a specific folder."""
187
200
  return self._client.get_folder(folder_id)
188
201
 
202
+ def find_folder_by_name(self, name: str, project_key: Optional[str] = None, folder_type: Optional[str] = None):
203
+ """
204
+ Find a folder by its name, ignoring case.
205
+
206
+ :param name: The name of the folder to search for.
207
+ :param project_key: Optional filter by project key.
208
+ :param folder_type: Optional filter by folder type.
209
+ :return: The folder details if found, otherwise None.
210
+ """
211
+ # Fetch all folders with optional filters
212
+ folders = self.list_folders(project_key=project_key, folder_type=folder_type)
213
+
214
+ # Iterate through the folders and search for the matching name
215
+ for folder in folders['values']:
216
+ if folder.get('name', '').lower() == name.lower():
217
+ return folder
218
+
219
+ # Return None if no folder is found
220
+ return ToolException(f"Folder with name {name} was not found")
221
+
189
222
  def delete_link(self, link_id: str):
190
223
  """Delete a specific link."""
191
224
  return self._client.delete_link(link_id)
@@ -236,11 +269,11 @@ class ZephyrEssentialApiWrapper(NonCodeIndexerToolkit):
236
269
 
237
270
  def _index_tool_params(self):
238
271
  return {
239
- 'chunking_tool':(Literal[None, 'json'],
240
- Field(description="Name of chunking tool", default='json'))
272
+ 'chunking_tool':(Literal['json', ''], Field(description="Name of chunking tool", default='json'))
241
273
  }
242
274
 
243
275
  def _base_loader(self, **kwargs) -> Generator[Document, None, None]:
276
+ self._chunking_tool = kwargs.get('chunking_tool', None)
244
277
  try:
245
278
  test_cases = self.list_test_cases()
246
279
  except Exception as e:
@@ -252,27 +285,29 @@ class ZephyrEssentialApiWrapper(NonCodeIndexerToolkit):
252
285
  if isinstance(v, (str, int, float, bool, list, dict))
253
286
  }
254
287
  metadata['type'] = "TEST_CASE"
255
-
288
+ #
289
+ try:
290
+ additional_content = self._process_test_case(metadata['key'])
291
+ for steps_type, content in additional_content.items():
292
+ if content:
293
+ page_content = json.dumps(content)
294
+ content_hash = hashlib.sha256(page_content.encode('utf-8')).hexdigest()
295
+ metadata[IndexerKeywords.UPDATED_ON.value] = content_hash
296
+ metadata[IndexerKeywords.CONTENT_IN_BYTES.value] = page_content.encode('utf-8')
297
+ metadata["steps_type"] = steps_type
298
+ except Exception as e:
299
+ logging.error(f"Failed to process document: {e}")
300
+ #
256
301
  yield Document(page_content="", metadata=metadata)
257
302
 
258
- def _extend_data(self, documents: Generator[Document, None, None]) -> Generator[Document, None, None]:
259
- for document in documents:
260
- try:
261
- if document.metadata['type'] and document.metadata['type'] == "TEST_CASE":
262
- additional_content = self._process_test_case(document.metadata['key'])
263
- document.page_content = json.dumps(additional_content)
264
- except json.JSONDecodeError as e:
265
- logging.error(f"Failed to decode JSON from document: {e}")
266
- yield document
267
-
268
- def _process_test_case(self, key):
303
+ def _process_test_case(self, key) -> dict:
269
304
  steps = self.get_test_case_test_steps(key)
305
+ if steps and not isinstance(steps, ToolException):
306
+ return {"steps": steps}
270
307
  script = self.get_test_case_test_script(key)
271
- additional_content = {
272
- "steps": "" if isinstance(steps, ToolException) else steps,
273
- "script": "" if isinstance(script, ToolException) else script,
274
- }
275
- return additional_content
308
+ if script and not isinstance(script, ToolException):
309
+ return {"script": script}
310
+ return {"empty": ""}
276
311
 
277
312
  @extend_with_parent_available_tools
278
313
  def get_available_tools(self):
@@ -481,6 +516,12 @@ class ZephyrEssentialApiWrapper(NonCodeIndexerToolkit):
481
516
  "args_schema": GetFolder,
482
517
  "ref": self.get_folder,
483
518
  },
519
+ {
520
+ "name": "find_folder_by_name",
521
+ "description": self.find_folder_by_name.__doc__,
522
+ "args_schema": FindFolderByName,
523
+ "ref": self.find_folder_by_name,
524
+ },
484
525
  {
485
526
  "name": "delete_link",
486
527
  "description": self.delete_link.__doc__,
@@ -576,8 +617,74 @@ UpdateTestCase = create_model(
576
617
  json=(str, Field(description=("""
577
618
  JSON body to update a test case. Example:
578
619
  {
579
- "name": "Updated Test Case Name",
580
- "description": "Updated Test Case Description"
620
+ "id": 1,
621
+ "key": "SA-T10",
622
+ "name": "Check axial pump",
623
+ "project": {
624
+ "id": 10005,
625
+ "self": "https://<api-base-url>/projects/10005"
626
+ },
627
+ "createdOn": "2018-05-15T13:15:13Z",
628
+ "objective": "To ensure the axial pump can be enabled",
629
+ "precondition": "Latest version of the axial pump available",
630
+ "estimatedTime": 138000,
631
+ "labels": [
632
+ "Regression",
633
+ "Performance",
634
+ "Automated"
635
+ ],
636
+ "component": {
637
+ "id": 10001,
638
+ "self": "https://<jira-instance>.atlassian.net/rest/api/2/component/10001"
639
+ },
640
+ "priority": {
641
+ "id": 10002,
642
+ "self": "https://<api-base-url>/priorities/10002"
643
+ },
644
+ "status": {
645
+ "id": 10000,
646
+ "self": "https://<api-base-url>/statuses/10000"
647
+ },
648
+ "folder": {
649
+ "id": 100006,
650
+ "self": "https://<api-base-url>/folders/10006"
651
+ },
652
+ "owner": {
653
+ "self": "https://<jira-instance>.atlassian.net/rest/api/2/user?accountId=5b10a2844c20165700ede21g",
654
+ "accountId": "5b10a2844c20165700ede21g"
655
+ },
656
+ "testScript": {
657
+ "self": "https://<api-base-url>/testCases/PROJ-T1/testscript"
658
+ },
659
+ "customFields": {
660
+ "Build Number": 20,
661
+ "Release Date": "2020-01-01",
662
+ "Pre-Condition(s)": "User should have logged in. <br> User should have navigated to the administration panel.",
663
+ "Implemented": false,
664
+ "Category": [
665
+ "Performance",
666
+ "Regression"
667
+ ],
668
+ "Tester": "fa2e582e-5e15-521e-92e3-47e6ca2e7256"
669
+ },
670
+ "links": {
671
+ "self": "string",
672
+ "issues": [
673
+ {
674
+ "self": "string",
675
+ "issueId": 10100,
676
+ "id": 1,
677
+ "target": "https://<jira-instance>.atlassian.net/rest/api/2/issue/10000",
678
+ "type": "COVERAGE"
679
+ }
680
+ ],
681
+ "webLinks": [
682
+ {
683
+ "self": "string",
684
+ "description": "A link to atlassian.com",
685
+ "url": "https://atlassian.com",
686
+ "id": 1,
687
+ "type": "COVERAGE"
581
688
  }
582
689
  """
583
690
  )))
@@ -594,9 +701,9 @@ CreateTestCaseIssueLink = create_model(
594
701
  json=(str, Field(description=("""
595
702
  JSON body to create an issue link. Example:
596
703
  {
597
- "issueKey": "ISSUE_KEY",
598
- "description": "Link Description"
704
+ "issueId": 10100
599
705
  }
706
+ where issueId - Jira issue id
600
707
  """
601
708
  )))
602
709
  )
@@ -638,8 +745,10 @@ CreateTestCaseTestScript = create_model(
638
745
  json=(str, Field(description=("""
639
746
  JSON body to create a test script. Example:
640
747
  {
641
- "script": "Test Script Content"
748
+ "type": "bdd",
749
+ "text": "Attempt to login to the application"
642
750
  }
751
+ Where type - Test script type. Allowed: plain, bdd
643
752
  """
644
753
  )))
645
754
  )
@@ -656,13 +765,43 @@ CreateTestCaseTestSteps = create_model(
656
765
  test_case_key=(str, Field(description="Key of the test case to create test steps for.")),
657
766
  json=(str, Field(description=("""
658
767
  JSON body to create test steps. Example:
659
- [
660
- {
661
- "step": "Step 1",
662
- "data": "Test Data",
663
- "result": "Expected Result"
664
- }
665
- ]
768
+ {
769
+ "mode": "APPEND",
770
+ "items": [
771
+ {
772
+ "inline": {
773
+ "description": "Attempt to login to the application",
774
+ "testData": "Username = SmartBear Password = weLoveAtlassian",
775
+ "expectedResult": "Login succeeds, web-app redirects to the dashboard view",
776
+ "customFields": {
777
+ "Build Number": 20,
778
+ "Release Date": "2020-01-01",
779
+ "Pre-Condition(s)": "User should have logged in. <br> User should have navigated to the administration panel.",
780
+ "Implemented": false,
781
+ "Category": [
782
+ "Performance",
783
+ "Regression"
784
+ ],
785
+ "Tester": "fa2e582e-5e15-521e-92e3-47e6ca2e7256"
786
+ }
787
+ },
788
+ "testCase": {
789
+ "self": "string",
790
+ "testCaseKey": "PROJ-T123",
791
+ "parameters": [
792
+ {
793
+ "name": "username",
794
+ "type": "DEFAULT_VALUE",
795
+ "value": "admin"
796
+ }
797
+ ]
798
+ }
799
+ }
800
+ ]
801
+ }
802
+ Where:
803
+ mode: str - required - Valid values: "APPEND", "OVERWRITE",
804
+ items - The list of test steps. Each step should be an object containing inline or testCase. They should only include one of these fields at a time.
666
805
  """
667
806
  )))
668
807
  )
@@ -700,9 +839,71 @@ UpdateTestCycle = create_model(
700
839
  json=(str, Field(description=("""
701
840
  JSON body to update a test cycle. Example:
702
841
  {
703
- "name": "Updated Test Cycle Name",
704
- "description": "Updated Test Cycle Description"
705
- }
842
+ "id": 1,
843
+ "key": "SA-R40",
844
+ "name": "Sprint 1 Regression Test Cycle",
845
+ "project": {
846
+ "id": 10005,
847
+ "self": "https://<api-base-url>/projects/10005"
848
+ },
849
+ "jiraProjectVersion": {
850
+ "id": 10000,
851
+ "self": "https://<jira-instance>.atlassian.net/rest/api/2/version/10000"
852
+ },
853
+ "status": {
854
+ "id": 10000,
855
+ "self": "https://<api-base-url>/statuses/10000"
856
+ },
857
+ "folder": {
858
+ "id": 100006,
859
+ "self": "https://<api-base-url>/folders/10006"
860
+ },
861
+ "description": "Regression test cycle 1 to ensure no breaking changes",
862
+ "plannedStartDate": "2018-05-19T13:15:13Z",
863
+ "plannedEndDate": "2018-05-20T13:15:13Z",
864
+ "owner": {
865
+ "self": "https://<jira-instance>.atlassian.net/rest/api/2/user?accountId=5b10a2844c20165700ede21g",
866
+ "accountId": "5b10a2844c20165700ede21g"
867
+ },
868
+ "customFields": {
869
+ "Build Number": 20,
870
+ "Release Date": "2020-01-01",
871
+ "Pre-Condition(s)": "User should have logged in. <br> User should have navigated to the administration panel.",
872
+ "Implemented": false,
873
+ "Category": [
874
+ "Performance",
875
+ "Regression"
876
+ ],
877
+ "Tester": "fa2e582e-5e15-521e-92e3-47e6ca2e7256"
878
+ },
879
+ "links": {
880
+ "self": "string",
881
+ "issues": [
882
+ {
883
+ "self": "string",
884
+ "issueId": 10100,
885
+ "id": 1,
886
+ "target": "https://<jira-instance>.atlassian.net/rest/api/2/issue/10000",
887
+ "type": "COVERAGE"
888
+ }
889
+ ],
890
+ "webLinks": [
891
+ {
892
+ "self": "string",
893
+ "description": "A link to atlassian.com",
894
+ "url": "https://atlassian.com",
895
+ "id": 1,
896
+ "type": "COVERAGE"
897
+ }
898
+ ],
899
+ "testPlans": [
900
+ {
901
+ "id": 1,
902
+ "self": "https://<api-base-url>/links/1",
903
+ "type": "RELATED",
904
+ "testPlanId": 2,
905
+ "target": "https://<jira-instance>.atlassian.net/rest/api/2/testplan/123"
906
+ }
706
907
  """
707
908
  )))
708
909
  )
@@ -718,9 +919,9 @@ CreateTestCycleIssueLink = create_model(
718
919
  json=(str, Field(description=("""
719
920
  JSON body to create an issue link. Example:
720
921
  {
721
- "issueKey": "ISSUE_KEY",
722
- "description": "Link Description"
922
+ "issueId": 10100
723
923
  }
924
+ where issueId - Jira issue id
724
925
  """
725
926
  )))
726
927
  )
@@ -752,10 +953,34 @@ CreateTestExecution = create_model(
752
953
  json=(str, Field(description=("""
753
954
  JSON body to create a test execution. Example:
754
955
  {
755
- "testCaseKey": "TEST_CASE_KEY",
756
- "testCycleKey": "TEST_CYCLE_KEY",
757
- "status": "PASS"
758
- }
956
+ "projectKey": "TIS",
957
+ "testCaseKey": "SA-T10",
958
+ "testCycleKey": "SA-R10",
959
+ "statusName": "In Progress",
960
+ "testScriptResults": [
961
+ {
962
+ "statusName": "In Progress",
963
+ "actualEndDate": "2018-05-20T13:15:13Z",
964
+ "actualResult": "User logged in successfully"
965
+ }
966
+ ],
967
+ "environmentName": "Chrome Latest Version",
968
+ "actualEndDate": "2018-05-20T13:15:13Z",
969
+ "executionTime": 120000,
970
+ "executedById": "5b10a2844c20165700ede21g",
971
+ "assignedToId": "5b10a2844c20165700ede21g",
972
+ "comment": "Test failed user could not login",
973
+ "customFields": {
974
+ "Build Number": 20,
975
+ "Release Date": "2020-01-01",
976
+ "Pre-Condition(s)": "User should have logged in. <br> User should have navigated to the administration panel.",
977
+ "Implemented": false,
978
+ "Category": [
979
+ "Performance",
980
+ "Regression"
981
+ ],
982
+ "Tester": "fa2e582e-5e15-521e-92e3-47e6ca2e7256"
983
+ }
759
984
  """
760
985
  )))
761
986
  )
@@ -771,8 +996,13 @@ UpdateTestExecution = create_model(
771
996
  json=(str, Field(description=("""
772
997
  JSON body to update a test execution. Example:
773
998
  {
774
- "status": "FAIL",
775
- "comment": "Updated comment"
999
+ "statusName": "In Progress",
1000
+ "environmentName": "Chrome Latest Version",
1001
+ "actualEndDate": "2018-05-20T13:15:13Z",
1002
+ "executionTime": 120000,
1003
+ "executedById": "5b10a2844c20165700ede21g",
1004
+ "assignedToId": "5b10a2844c20165700ede21g",
1005
+ "comment": "Test failed user could not login"
776
1006
  }
777
1007
  """
778
1008
  )))
@@ -790,12 +1020,14 @@ UpdateTestExecutionTestSteps = create_model(
790
1020
  test_execution_id_or_key=(str, Field(description="ID or key of the test execution to update test steps for.")),
791
1021
  json=(str, Field(description=("""
792
1022
  "JSON body to update test steps. Example:
793
- ["
794
- {"
795
- "step": "Step 1",
796
- "status": "PASS"
797
- }
798
- ]
1023
+ {
1024
+ "steps": [
1025
+ {
1026
+ "actualResult": "User logged in successfully",
1027
+ "statusName": "In Progress"
1028
+ }
1029
+ ]
1030
+ }
799
1031
  """
800
1032
  )))
801
1033
  )
@@ -816,9 +1048,9 @@ CreateTestExecutionIssueLink = create_model(
816
1048
  json=(str, Field(description=("""
817
1049
  JSON body to create an issue link. Example:
818
1050
  {
819
- "issueKey": "ISSUE_KEY",
820
- "description": "Link Description"
1051
+ "issueId": 10100
821
1052
  }
1053
+ where issueId - Jira issue id
822
1054
  """
823
1055
  )))
824
1056
  )
@@ -847,10 +1079,13 @@ CreateFolder = create_model(
847
1079
  json=(str, Field(description=("""
848
1080
  JSON body to create a folder. Example:
849
1081
  {
850
- "name": "Folder Name",
851
- "description": "Folder Description",
852
- "projectKey": "PROJECT_KEY"
1082
+ "parentId": 123456,
1083
+ "parentName": "parentFolder",
1084
+ "name": "ZephyrEssential_test",
1085
+ "projectKey": "PRJ",
1086
+ "folderType": "TEST_CASE"
853
1087
  }
1088
+ Possible folder types: "TEST_CASE", "TEST_PLAN", "TEST_CYCLE",
854
1089
  """
855
1090
  )))
856
1091
  )
@@ -860,6 +1095,14 @@ GetFolder = create_model(
860
1095
  folder_id=(str, Field(description="ID of the folder to retrieve."))
861
1096
  )
862
1097
 
1098
+ FindFolderByName = create_model(
1099
+ "GetFolder",
1100
+ name=(str, Field(description="Name of the folder to retrieve.")),
1101
+ project_key=(Optional[str], Field(description="Project key", default=None)),
1102
+ folder_type=(Optional[str], Field(description="""Folder type. Possible values: "TEST_CASE", "TEST_PLAN", "TEST_CYCLE" """,
1103
+ default=None)),
1104
+ )
1105
+
863
1106
  DeleteLink = create_model(
864
1107
  "DeleteLink",
865
1108
  link_id=(str, Field(description="ID of the link to delete."))
@@ -1,4 +1,5 @@
1
1
  import requests
2
+ from langchain_core.tools import ToolException
2
3
 
3
4
  class ZephyrEssentialAPI:
4
5
  def __init__(self, base_url: str, token: str):
@@ -14,10 +15,11 @@ class ZephyrEssentialAPI:
14
15
  })
15
16
  try:
16
17
  resp = requests.request(method=method, url=url, headers=headers, json=json, params=params, files=files)
17
- resp.raise_for_status()
18
- if resp.headers.get("Content-Type", "").startswith("application/json"):
19
- return resp.json()
20
- return resp.text
18
+ if resp.status_code < 300:
19
+ if resp.headers.get("Content-Type", "").startswith("application/json"):
20
+ return resp.json()
21
+ return resp.text
22
+ return ToolException(f"Error performing request {method} {api_path}: {resp.content}")
21
23
  except requests.RequestException as e:
22
24
  raise Exception(f"Error performing request {method} {api_path}: {str(e)}")
23
25
 
@@ -6,9 +6,11 @@ from pydantic import create_model, BaseModel, Field
6
6
 
7
7
  from .api_wrapper import ZephyrScaleApiWrapper
8
8
  from ..base.tool import BaseAction
9
- from ..utils import clean_string, get_max_toolkit_length, TOOLKIT_SPLITTER
9
+ from ..elitea_base import filter_missconfigured_index_tools
10
+ from ..utils import clean_string, get_max_toolkit_length
10
11
  from ...configurations.pgvector import PgVectorConfiguration
11
12
  from ...configurations.zephyr import ZephyrConfiguration
13
+ from ...runtime.utils.constants import TOOLKIT_NAME_META, TOOL_NAME_META, TOOLKIT_TYPE_META
12
14
 
13
15
  name = "zephyr_scale"
14
16
 
@@ -31,16 +33,14 @@ def get_tools(tool):
31
33
 
32
34
  class ZephyrScaleToolkit(BaseToolkit):
33
35
  tools: List[BaseTool] = []
34
- toolkit_max_length: int = 0
35
36
 
36
37
  @staticmethod
37
38
  def toolkit_config_schema() -> BaseModel:
38
39
  selected_tools = {x['name']: x['args_schema'].schema() for x in ZephyrScaleApiWrapper.model_construct().get_available_tools()}
39
- ZephyrScaleToolkit.toolkit_max_length = get_max_toolkit_length(selected_tools)
40
40
  return create_model(
41
41
  name,
42
42
  max_results=(int, Field(default=100, description="Results count to show")),
43
- zephyr_configuration=(Optional[ZephyrConfiguration], Field(description="Zephyr Configuration",
43
+ zephyr_configuration=(ZephyrConfiguration, Field(description="Zephyr Configuration",
44
44
  json_schema_extra={'configuration_types': ['zephyr']})),
45
45
  pgvector_configuration=(Optional[PgVectorConfiguration], Field(default=None, description="PgVector Configuration",
46
46
  json_schema_extra={
@@ -63,6 +63,7 @@ class ZephyrScaleToolkit(BaseToolkit):
63
63
  )
64
64
 
65
65
  @classmethod
66
+ @filter_missconfigured_index_tools
66
67
  def get_toolkit(cls, selected_tools: list[str] | None = None, toolkit_name: Optional[str] = None, **kwargs):
67
68
  if selected_tools is None:
68
69
  selected_tools = []
@@ -73,18 +74,22 @@ class ZephyrScaleToolkit(BaseToolkit):
73
74
  **(kwargs.get('pgvector_configuration') or {}),
74
75
  }
75
76
  zephyr_wrapper = ZephyrScaleApiWrapper(**wrapper_payload)
76
- prefix = clean_string(toolkit_name, cls.toolkit_max_length) + TOOLKIT_SPLITTER if toolkit_name else ''
77
77
  available_tools = zephyr_wrapper.get_available_tools()
78
78
  tools = []
79
79
  for tool in available_tools:
80
80
  if selected_tools:
81
81
  if tool["name"] not in selected_tools:
82
82
  continue
83
+ description = tool["description"]
84
+ if toolkit_name:
85
+ description = f"Toolkit: {toolkit_name}\n{description}"
86
+ description = description[:1000]
83
87
  tools.append(BaseAction(
84
88
  api_wrapper=zephyr_wrapper,
85
- name=prefix + tool["name"],
86
- description=tool["description"],
87
- args_schema=tool["args_schema"]
89
+ name=tool["name"],
90
+ description=description,
91
+ args_schema=tool["args_schema"],
92
+ metadata={TOOLKIT_NAME_META: toolkit_name, TOOLKIT_TYPE_META: name, TOOL_NAME_META: tool["name"]} if toolkit_name else {TOOL_NAME_META: tool["name"]}
88
93
  ))
89
94
  return cls(tools=tools)
90
95