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
@@ -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 ()
@@ -7,6 +7,7 @@ from ..base.tool import BaseAction
7
7
  from ..elitea_base import filter_missconfigured_index_tools
8
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
 
@@ -66,7 +67,7 @@ class RallyToolkit(BaseToolkit):
66
67
  name=tool["name"],
67
68
  description=description,
68
69
  args_schema=tool["args_schema"],
69
- metadata={"toolkit_name": toolkit_name} if toolkit_name else {}
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"]}
70
71
  ))
71
72
  return cls(tools=tools)
72
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(
@@ -9,6 +9,7 @@ from ..base.tool import BaseAction
9
9
  from ..elitea_base import filter_missconfigured_index_tools
10
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
 
@@ -60,7 +61,7 @@ class ReportPortalToolkit(BaseToolkit):
60
61
  name=tool["name"],
61
62
  description=description,
62
63
  args_schema=tool["args_schema"],
63
- metadata={"toolkit_name": toolkit_name} if toolkit_name else {}
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"]}
64
65
  ))
65
66
  return cls(tools=tools)
66
67
 
@@ -7,6 +7,7 @@ from pydantic import create_model, BaseModel, ConfigDict, Field
7
7
  from ..elitea_base import filter_missconfigured_index_tools
8
8
  from ..utils import clean_string, get_max_toolkit_length
9
9
  from ...configurations.salesforce import SalesforceConfiguration
10
+ from ...runtime.utils.constants import TOOLKIT_NAME_META, TOOL_NAME_META, TOOLKIT_TYPE_META
10
11
 
11
12
  name = "salesforce"
12
13
 
@@ -59,7 +60,7 @@ class SalesforceToolkit(BaseToolkit):
59
60
  name=tool["name"],
60
61
  description=description,
61
62
  args_schema=tool["args_schema"],
62
- metadata={"toolkit_name": toolkit_name} if toolkit_name else {}
63
+ metadata={TOOLKIT_NAME_META: toolkit_name, TOOLKIT_TYPE_META: name, TOOL_NAME_META: tool["name"]} if toolkit_name else {TOOL_NAME_META: tool["name"]}
63
64
  ))
64
65
 
65
66
  return cls(tools=tools)
@@ -8,6 +8,7 @@ from pydantic import create_model, BaseModel, ConfigDict, Field
8
8
 
9
9
  from ..elitea_base import filter_missconfigured_index_tools
10
10
  from ...configurations.service_now import ServiceNowConfiguration
11
+ from ...runtime.utils.constants import TOOLKIT_NAME_META, TOOL_NAME_META, TOOLKIT_TYPE_META
11
12
 
12
13
 
13
14
  name = "service_now"
@@ -92,7 +93,7 @@ class ServiceNowToolkit(BaseToolkit):
92
93
  name=tool["name"],
93
94
  description=description,
94
95
  args_schema=tool["args_schema"],
95
- metadata={"toolkit_name": toolkit_name} if toolkit_name else {}
96
+ metadata={TOOLKIT_NAME_META: toolkit_name, TOOLKIT_TYPE_META: name, TOOL_NAME_META: tool["name"]} if toolkit_name else {TOOL_NAME_META: tool["name"]}
96
97
  ))
97
98
  return cls(tools=tools)
98
99
 
@@ -8,6 +8,7 @@ from ..elitea_base import filter_missconfigured_index_tools
8
8
  from ..utils import clean_string, get_max_toolkit_length
9
9
  from ...configurations.pgvector import PgVectorConfiguration
10
10
  from ...configurations.sharepoint import SharepointConfiguration
11
+ from ...runtime.utils.constants import TOOLKIT_NAME_META, TOOL_NAME_META, TOOLKIT_TYPE_META
11
12
 
12
13
  name = "sharepoint"
13
14
 
@@ -77,7 +78,7 @@ class SharepointToolkit(BaseToolkit):
77
78
  name=tool["name"],
78
79
  description=description,
79
80
  args_schema=tool["args_schema"],
80
- metadata={"toolkit_name": toolkit_name} if toolkit_name else {}
81
+ metadata={TOOLKIT_NAME_META: toolkit_name, TOOLKIT_TYPE_META: name, TOOL_NAME_META: tool["name"]} if toolkit_name else {TOOL_NAME_META: tool["name"]}
81
82
  ))
82
83
  return cls(tools=tools)
83
84
 
@@ -20,7 +20,7 @@ NoInput = create_model(
20
20
  ReadList = create_model(
21
21
  "ReadList",
22
22
  list_title=(str, Field(description="Name of a Sharepoint list to be read.")),
23
- limit=(Optional[int], Field(description="Limit (maximum number) of list items to be returned", default=1000))
23
+ limit=(Optional[int], Field(description="Limit (maximum number) of list items to be returned", default=1000, gt=0))
24
24
  )
25
25
 
26
26
  GetFiles = create_model(
@@ -30,7 +30,7 @@ GetFiles = create_model(
30
30
  "Can be called with synonyms, such as First, Top, etc., "
31
31
  "or can be reflected just by a number for example 'Top 10 files'. "
32
32
  "Use default value if not specified in a query WITH NO EXTRA "
33
- "CONFIRMATION FROM A USER", default=100)),
33
+ "CONFIRMATION FROM A USER", default=100, gt=0)),
34
34
  )
35
35
 
36
36
  ReadDocument = create_model(
@@ -15,6 +15,7 @@ from .api_wrapper import SlackApiWrapper
15
15
  from ..utils import clean_string, get_max_toolkit_length, check_connection_response
16
16
  from slack_sdk.errors import SlackApiError
17
17
  from slack_sdk import WebClient
18
+ from ...runtime.utils.constants import TOOLKIT_NAME_META, TOOL_NAME_META, TOOLKIT_TYPE_META
18
19
 
19
20
  name = "slack"
20
21
 
@@ -85,12 +86,12 @@ class SlackToolkit(BaseToolkit):
85
86
  if toolkit_name:
86
87
  description = f"{description}\nToolkit: {toolkit_name}"
87
88
  description = description[:1000]
88
- tools.append(BaseAction(
89
+ tools.append(BaseAction(
89
90
  api_wrapper=slack_api_wrapper,
90
91
  name=tool["name"],
91
92
  description=description,
92
93
  args_schema=tool["args_schema"],
93
- metadata={"toolkit_name": toolkit_name} if toolkit_name else {}
94
+ metadata={TOOLKIT_NAME_META: toolkit_name, TOOLKIT_TYPE_META: name, TOOL_NAME_META: tool["name"]} if toolkit_name else {TOOL_NAME_META: tool["name"]}
94
95
  ))
95
96
  return cls(tools=tools)
96
97
 
@@ -17,8 +17,8 @@ SendMessageModel = create_model(
17
17
 
18
18
  ReadMessagesModel = create_model(
19
19
  "ReadMessagesModel",
20
- channel_id=(Optional[str], Field(default=None,description="Channel ID, user ID, or conversation ID to read messages from. (like C12345678 for public channels, D12345678 for DMs)")),
21
- limit=(int, Field(default=10, description="The number of messages to fetch (default is 10)."))
20
+ channel_id=(Optional[str], Field(default=None,description="Channel ID, user ID, or conversation ID to read messages from. (like C12345678 for public channels, D12345678 for DMs)")),
21
+ limit=(int, Field(default=10, description="The number of messages to fetch (default is 10).", gt=0))
22
22
  )
23
23
 
24
24
  CreateChannelModel = create_model(