alita-sdk 0.3.449__py3-none-any.whl → 0.3.457__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.
- alita_sdk/runtime/langchain/langraph_agent.py +57 -15
- alita_sdk/runtime/langchain/utils.py +19 -3
- alita_sdk/runtime/toolkits/artifact.py +5 -6
- alita_sdk/runtime/toolkits/mcp.py +5 -2
- alita_sdk/runtime/toolkits/tools.py +1 -0
- alita_sdk/runtime/tools/function.py +2 -1
- alita_sdk/runtime/tools/vectorstore_base.py +17 -2
- alita_sdk/runtime/utils/mcp_sse_client.py +64 -6
- alita_sdk/tools/ado/repos/__init__.py +1 -0
- alita_sdk/tools/ado/test_plan/__init__.py +1 -1
- alita_sdk/tools/ado/wiki/__init__.py +1 -5
- alita_sdk/tools/ado/work_item/__init__.py +1 -5
- alita_sdk/tools/base_indexer_toolkit.py +10 -6
- alita_sdk/tools/bitbucket/__init__.py +1 -0
- alita_sdk/tools/code/sonar/__init__.py +1 -1
- alita_sdk/tools/confluence/__init__.py +2 -2
- alita_sdk/tools/github/__init__.py +2 -2
- alita_sdk/tools/gitlab/__init__.py +2 -1
- alita_sdk/tools/gitlab_org/__init__.py +1 -2
- alita_sdk/tools/google_places/__init__.py +2 -1
- alita_sdk/tools/jira/__init__.py +1 -0
- alita_sdk/tools/memory/__init__.py +1 -1
- alita_sdk/tools/pandas/__init__.py +1 -1
- alita_sdk/tools/postman/__init__.py +2 -1
- alita_sdk/tools/pptx/__init__.py +2 -2
- alita_sdk/tools/qtest/__init__.py +3 -3
- alita_sdk/tools/qtest/api_wrapper.py +374 -29
- alita_sdk/tools/rally/__init__.py +1 -2
- alita_sdk/tools/report_portal/__init__.py +1 -0
- alita_sdk/tools/salesforce/__init__.py +1 -0
- alita_sdk/tools/servicenow/__init__.py +2 -3
- alita_sdk/tools/sharepoint/__init__.py +1 -0
- alita_sdk/tools/slack/__init__.py +1 -0
- alita_sdk/tools/sql/__init__.py +2 -1
- alita_sdk/tools/testio/__init__.py +1 -0
- alita_sdk/tools/testrail/__init__.py +1 -3
- alita_sdk/tools/xray/__init__.py +2 -1
- alita_sdk/tools/zephyr/__init__.py +2 -1
- alita_sdk/tools/zephyr_enterprise/__init__.py +1 -0
- alita_sdk/tools/zephyr_essential/__init__.py +1 -0
- alita_sdk/tools/zephyr_scale/__init__.py +1 -0
- alita_sdk/tools/zephyr_squad/__init__.py +1 -0
- {alita_sdk-0.3.449.dist-info → alita_sdk-0.3.457.dist-info}/METADATA +1 -1
- {alita_sdk-0.3.449.dist-info → alita_sdk-0.3.457.dist-info}/RECORD +47 -47
- {alita_sdk-0.3.449.dist-info → alita_sdk-0.3.457.dist-info}/WHEEL +0 -0
- {alita_sdk-0.3.449.dist-info → alita_sdk-0.3.457.dist-info}/licenses/LICENSE +0 -0
- {alita_sdk-0.3.449.dist-info → alita_sdk-0.3.457.dist-info}/top_level.txt +0 -0
|
@@ -62,7 +62,8 @@ class PostmanToolkit(BaseToolkit):
|
|
|
62
62
|
selected_tools=(List[Literal[tuple(selected_tools)]], Field(
|
|
63
63
|
default=[], json_schema_extra={'args_schemas': selected_tools})),
|
|
64
64
|
__config__=ConfigDict(json_schema_extra={'metadata': {
|
|
65
|
-
"label": "Postman", "icon_url": "postman.svg"
|
|
65
|
+
"label": "Postman", "icon_url": "postman.svg",
|
|
66
|
+
"max_length": PostmanToolkit.toolkit_max_length,}})
|
|
66
67
|
)
|
|
67
68
|
|
|
68
69
|
@check_connection_response
|
alita_sdk/tools/pptx/__init__.py
CHANGED
|
@@ -45,13 +45,13 @@ class PPTXToolkit(BaseToolkit):
|
|
|
45
45
|
|
|
46
46
|
return create_model(
|
|
47
47
|
name,
|
|
48
|
-
bucket_name=(str, Field(description="Bucket name where PPTX files are stored",
|
|
49
|
-
json_schema_extra={'toolkit_name': True, 'max_toolkit_length': TOOLKIT_MAX_LENGTH})),
|
|
48
|
+
bucket_name=(str, Field(description="Bucket name where PPTX files are stored")),
|
|
50
49
|
selected_tools=(List[Literal[tuple(selected_tools)]], Field(default=[], json_schema_extra={'args_schemas': selected_tools})),
|
|
51
50
|
__config__=ConfigDict(json_schema_extra={
|
|
52
51
|
'metadata': {
|
|
53
52
|
"label": "PPTX",
|
|
54
53
|
"icon_url": "pptx.svg",
|
|
54
|
+
"max_length": TOOLKIT_MAX_LENGTH,
|
|
55
55
|
"categories": ["office"],
|
|
56
56
|
"extra_categories": ["presentation", "office automation", "document"]
|
|
57
57
|
}
|
|
@@ -37,14 +37,14 @@ class QtestToolkit(BaseToolkit):
|
|
|
37
37
|
name,
|
|
38
38
|
qtest_configuration=(QtestConfiguration, Field(description="QTest API token", json_schema_extra={
|
|
39
39
|
'configuration_types': ['qtest']})),
|
|
40
|
-
qtest_project_id=(int, Field(default=None, description="QTest project id",
|
|
41
|
-
'max_toolkit_length': QtestToolkit.toolkit_max_length})),
|
|
40
|
+
qtest_project_id=(int, Field(default=None, description="QTest project id")),
|
|
42
41
|
no_of_tests_shown_in_dql_search=(Optional[int], Field(description="Max number of items returned by dql search",
|
|
43
42
|
default=10)),
|
|
44
43
|
|
|
45
44
|
selected_tools=(List[Literal[tuple(selected_tools)]],
|
|
46
45
|
Field(default=[], json_schema_extra={'args_schemas': selected_tools})),
|
|
47
46
|
__config__=ConfigDict(json_schema_extra={'metadata': {"label": "QTest", "icon_url": "qtest.svg",
|
|
47
|
+
"max_length": QtestToolkit.toolkit_max_length,
|
|
48
48
|
"categories": ["test management"],
|
|
49
49
|
"extra_categories": ["quality assurance",
|
|
50
50
|
"test case management",
|
|
@@ -93,4 +93,4 @@ class QtestToolkit(BaseToolkit):
|
|
|
93
93
|
return cls(tools=tools)
|
|
94
94
|
|
|
95
95
|
def get_tools(self):
|
|
96
|
-
return self.tools
|
|
96
|
+
return self.tools
|
|
@@ -5,6 +5,7 @@ import re
|
|
|
5
5
|
from traceback import format_exc
|
|
6
6
|
from typing import Any, Optional
|
|
7
7
|
|
|
8
|
+
import requests
|
|
8
9
|
import swagger_client
|
|
9
10
|
from langchain_core.tools import ToolException
|
|
10
11
|
from pydantic import Field, PrivateAttr, model_validator, create_model, SecretStr
|
|
@@ -48,6 +49,11 @@ Steps: Array of test steps with Description and Expected Result.
|
|
|
48
49
|
- Single value: "Team": "Epam"
|
|
49
50
|
- Multiple values: "Team": ["Epam", "EJ"]
|
|
50
51
|
|
|
52
|
+
**Clearing/Unsetting fields**: To clear a field value (unassign, set to empty/blank):
|
|
53
|
+
- Use `null` in JSON: "Priority": null
|
|
54
|
+
- Works for multi-select fields, user assignments, etc. (Note: single-select dropdowns have API limitations)
|
|
55
|
+
- Example: {{"QTest Id": "4626964", "Assigned To": null, "Review status": null}}
|
|
56
|
+
|
|
51
57
|
**For Updates**: Include only the fields you want to modify. The system will validate property values against project configuration.
|
|
52
58
|
|
|
53
59
|
### EXAMPLE
|
|
@@ -142,6 +148,12 @@ GetAllTestCasesFieldsForProject = create_model(
|
|
|
142
148
|
default=False)),
|
|
143
149
|
)
|
|
144
150
|
|
|
151
|
+
FindTestCasesByRequirementId = create_model(
|
|
152
|
+
"FindTestCasesByRequirementId",
|
|
153
|
+
requirement_id=(str, Field(description="QTest requirement ID in format RQ-123. This will find all test cases linked to this requirement.")),
|
|
154
|
+
include_details=(Optional[bool], Field(description="If true, returns full test case details. If false (default), returns Id, QTest Id, Name, and Description fields.", default=False)),
|
|
155
|
+
)
|
|
156
|
+
|
|
145
157
|
NoInput = create_model(
|
|
146
158
|
"NoInput"
|
|
147
159
|
)
|
|
@@ -155,6 +167,7 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
155
167
|
no_of_tests_shown_in_dql_search: int = 10
|
|
156
168
|
_client: Any = PrivateAttr()
|
|
157
169
|
_field_definitions_cache: Optional[dict] = PrivateAttr(default=None)
|
|
170
|
+
_modules_cache: Optional[list] = PrivateAttr(default=None)
|
|
158
171
|
llm: Any
|
|
159
172
|
|
|
160
173
|
@model_validator(mode='before')
|
|
@@ -256,9 +269,41 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
256
269
|
# Skip non-property fields (these are handled separately)
|
|
257
270
|
if field_name in ['Name', 'Description', 'Precondition', 'Steps', 'Id', QTEST_ID]:
|
|
258
271
|
continue
|
|
259
|
-
|
|
260
|
-
# Skip
|
|
261
|
-
if field_value
|
|
272
|
+
|
|
273
|
+
# Skip empty string values (don't update these fields)
|
|
274
|
+
if field_value == '':
|
|
275
|
+
continue
|
|
276
|
+
|
|
277
|
+
# Handle None value - this means "clear/unset this field"
|
|
278
|
+
if field_value is None:
|
|
279
|
+
# Validate field exists before attempting to clear
|
|
280
|
+
if field_name not in field_definitions:
|
|
281
|
+
validation_errors.append(
|
|
282
|
+
f"❌ Unknown field '{field_name}' - not defined in project configuration"
|
|
283
|
+
)
|
|
284
|
+
continue
|
|
285
|
+
|
|
286
|
+
field_def = field_definitions[field_name]
|
|
287
|
+
field_id = field_def['field_id']
|
|
288
|
+
is_multiple = field_def.get('multiple', False)
|
|
289
|
+
|
|
290
|
+
if is_multiple:
|
|
291
|
+
# Multi-select/user fields: can clear using empty array "[]"
|
|
292
|
+
props_dict[field_name] = {
|
|
293
|
+
'field_id': field_id,
|
|
294
|
+
'field_name': field_name,
|
|
295
|
+
'field_value': "[]",
|
|
296
|
+
'field_value_name': None
|
|
297
|
+
}
|
|
298
|
+
else:
|
|
299
|
+
# Single-select fields: QTest API limitation - cannot clear to empty
|
|
300
|
+
# Note: Users CAN clear these fields from UI, but API doesn't expose this capability
|
|
301
|
+
validation_errors.append(
|
|
302
|
+
f"⚠️ Cannot clear single-select field '{field_name}' - this is a QTest API limitation "
|
|
303
|
+
f"(clearing is possible from UI but not exposed via API). "
|
|
304
|
+
f"Please select an alternative value instead. "
|
|
305
|
+
f"Available values: {', '.join(field_def.get('values', {}).keys()) or 'none'}"
|
|
306
|
+
)
|
|
262
307
|
continue
|
|
263
308
|
|
|
264
309
|
# Validate field exists in project - STRICT validation
|
|
@@ -414,6 +459,143 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
414
459
|
Exception: \n {stacktrace}""")
|
|
415
460
|
return modules
|
|
416
461
|
|
|
462
|
+
def __get_field_definitions_from_properties_api(self) -> dict:
|
|
463
|
+
"""
|
|
464
|
+
Fallback method: Get field definitions using /properties and /properties-info APIs.
|
|
465
|
+
|
|
466
|
+
These APIs don't require Field Management permission and are available to all users.
|
|
467
|
+
Requires 2 API calls + 1 search to get a test case ID.
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
dict: Same structure as __get_project_field_definitions()
|
|
471
|
+
"""
|
|
472
|
+
logger.info(
|
|
473
|
+
"Using properties API fallback (no Field Management permission). "
|
|
474
|
+
"This requires getting a template test case first."
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
# Step 1: Get any test case ID to query properties
|
|
478
|
+
search_instance = swagger_client.SearchApi(self._client)
|
|
479
|
+
body = swagger_client.ArtifactSearchParams(
|
|
480
|
+
object_type='test-cases',
|
|
481
|
+
fields=['*'],
|
|
482
|
+
query='' # Empty query returns all test cases
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
try:
|
|
486
|
+
# Search for any test case - just need one
|
|
487
|
+
response = search_instance.search_artifact(
|
|
488
|
+
self.qtest_project_id,
|
|
489
|
+
body,
|
|
490
|
+
page_size=1,
|
|
491
|
+
page=1
|
|
492
|
+
)
|
|
493
|
+
except ApiException as e:
|
|
494
|
+
stacktrace = format_exc()
|
|
495
|
+
logger.error(f"Failed to find test case for properties API: {stacktrace}")
|
|
496
|
+
raise ValueError(
|
|
497
|
+
f"Cannot find any test case to query field definitions. "
|
|
498
|
+
f"Please create at least one test case in project {self.qtest_project_id}"
|
|
499
|
+
) from e
|
|
500
|
+
|
|
501
|
+
if not response or not response.get('items') or len(response['items']) == 0:
|
|
502
|
+
raise ValueError(
|
|
503
|
+
f"No test cases found in project {self.qtest_project_id}. "
|
|
504
|
+
f"Please create at least one test case to retrieve field definitions."
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
test_case_id = response['items'][0]['id']
|
|
508
|
+
logger.info(f"Using test case ID {test_case_id} to retrieve field definitions")
|
|
509
|
+
|
|
510
|
+
# Step 2: Call /properties API
|
|
511
|
+
headers = {
|
|
512
|
+
"Authorization": f"Bearer {self.qtest_api_token.get_secret_value()}"
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
properties_url = f"{self.base_url}/api/v3/projects/{self.qtest_project_id}/test-cases/{test_case_id}/properties"
|
|
516
|
+
properties_info_url = f"{self.base_url}/api/v3/projects/{self.qtest_project_id}/test-cases/{test_case_id}/properties-info"
|
|
517
|
+
|
|
518
|
+
try:
|
|
519
|
+
# Get properties with current values and field metadata
|
|
520
|
+
props_response = requests.get(
|
|
521
|
+
properties_url,
|
|
522
|
+
headers=headers,
|
|
523
|
+
params={'calledBy': 'testcase_properties'}
|
|
524
|
+
)
|
|
525
|
+
props_response.raise_for_status()
|
|
526
|
+
properties_data = props_response.json()
|
|
527
|
+
|
|
528
|
+
# Get properties-info with data types and allowed values
|
|
529
|
+
info_response = requests.get(properties_info_url, headers=headers)
|
|
530
|
+
info_response.raise_for_status()
|
|
531
|
+
info_data = info_response.json()
|
|
532
|
+
|
|
533
|
+
except requests.exceptions.RequestException as e:
|
|
534
|
+
stacktrace = format_exc()
|
|
535
|
+
logger.error(f"Failed to call properties API: {stacktrace}")
|
|
536
|
+
raise ValueError(
|
|
537
|
+
f"Unable to retrieve field definitions using properties API. "
|
|
538
|
+
f"Error: {stacktrace}"
|
|
539
|
+
) from e
|
|
540
|
+
|
|
541
|
+
# Step 3: Build field mapping by merging both responses
|
|
542
|
+
field_mapping = {}
|
|
543
|
+
|
|
544
|
+
# Create lookup by field ID from properties-info
|
|
545
|
+
metadata_by_id = {item['id']: item for item in info_data['metadata']}
|
|
546
|
+
|
|
547
|
+
# Data type mapping to determine 'multiple' flag
|
|
548
|
+
MULTI_SELECT_TYPES = {
|
|
549
|
+
'UserListDataType',
|
|
550
|
+
'MultiSelectionDataType',
|
|
551
|
+
'CheckListDataType'
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
USER_FIELD_TYPES = {'UserListDataType'}
|
|
555
|
+
|
|
556
|
+
# System fields to exclude (same as in property mapping)
|
|
557
|
+
excluded_fields = {'Shared', 'Projects Shared to'}
|
|
558
|
+
|
|
559
|
+
for prop in properties_data:
|
|
560
|
+
field_name = prop.get('name')
|
|
561
|
+
field_id = prop.get('id')
|
|
562
|
+
|
|
563
|
+
if not field_name or field_name in excluded_fields:
|
|
564
|
+
continue
|
|
565
|
+
|
|
566
|
+
# Get metadata for this field
|
|
567
|
+
metadata = metadata_by_id.get(field_id, {})
|
|
568
|
+
data_type_str = metadata.get('data_type')
|
|
569
|
+
|
|
570
|
+
# Determine data_type number (5 for user fields, None for others)
|
|
571
|
+
data_type = 5 if data_type_str in USER_FIELD_TYPES else None
|
|
572
|
+
|
|
573
|
+
# Determine if multi-select
|
|
574
|
+
is_multiple = data_type_str in MULTI_SELECT_TYPES
|
|
575
|
+
|
|
576
|
+
field_mapping[field_name] = {
|
|
577
|
+
'field_id': field_id,
|
|
578
|
+
'required': prop.get('required', False),
|
|
579
|
+
'data_type': data_type,
|
|
580
|
+
'multiple': is_multiple,
|
|
581
|
+
'values': {}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
# Map allowed values from metadata
|
|
585
|
+
allowed_values = metadata.get('allowed_values', [])
|
|
586
|
+
for allowed_val in allowed_values:
|
|
587
|
+
value_text = allowed_val.get('value_text')
|
|
588
|
+
value_id = allowed_val.get('id')
|
|
589
|
+
if value_text and value_id:
|
|
590
|
+
field_mapping[field_name]['values'][value_text] = value_id
|
|
591
|
+
|
|
592
|
+
logger.info(
|
|
593
|
+
f"Retrieved {len(field_mapping)} field definitions using properties API. "
|
|
594
|
+
f"This method works for all users without Field Management permission."
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
return field_mapping
|
|
598
|
+
|
|
417
599
|
def __get_project_field_definitions(self) -> dict:
|
|
418
600
|
"""
|
|
419
601
|
Get structured field definitions for test cases in the project.
|
|
@@ -439,6 +621,15 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
439
621
|
try:
|
|
440
622
|
fields = fields_api.get_fields(self.qtest_project_id, qtest_object)
|
|
441
623
|
except ApiException as e:
|
|
624
|
+
# Check if permission denied (403) - use fallback
|
|
625
|
+
if e.status == 403:
|
|
626
|
+
logger.warning(
|
|
627
|
+
"get_fields permission denied (Field Management permission required). "
|
|
628
|
+
"Using properties API fallback..."
|
|
629
|
+
)
|
|
630
|
+
return self.__get_field_definitions_from_properties_api()
|
|
631
|
+
|
|
632
|
+
# Other API errors
|
|
442
633
|
stacktrace = format_exc()
|
|
443
634
|
logger.error(f"Exception when calling FieldAPI->get_fields:\n {stacktrace}")
|
|
444
635
|
raise ValueError(
|
|
@@ -517,6 +708,10 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
517
708
|
return self.__format_field_info_for_display(field_defs)
|
|
518
709
|
|
|
519
710
|
def _parse_modules(self) -> list[dict]:
|
|
711
|
+
"""Get parsed modules list with caching for the session."""
|
|
712
|
+
if self._modules_cache is not None:
|
|
713
|
+
return self._modules_cache
|
|
714
|
+
|
|
520
715
|
modules = self.__get_all_modules_for_project()
|
|
521
716
|
result = []
|
|
522
717
|
|
|
@@ -537,6 +732,7 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
537
732
|
for module in modules:
|
|
538
733
|
parse_module(module)
|
|
539
734
|
|
|
735
|
+
self._modules_cache = result
|
|
540
736
|
return result
|
|
541
737
|
|
|
542
738
|
def __execute_single_create_test_case_request(self, test_case_api_instance: TestCaseApi, body,
|
|
@@ -556,8 +752,8 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
556
752
|
def __parse_data(self, response_to_parse: dict, parsed_data: list, extract_images: bool=False, prompt: str=None):
|
|
557
753
|
import html
|
|
558
754
|
|
|
559
|
-
#
|
|
560
|
-
|
|
755
|
+
# PERMISSION-FREE: Parse properties directly from API response
|
|
756
|
+
# No get_fields call needed - works for all users
|
|
561
757
|
|
|
562
758
|
for item in response_to_parse['items']:
|
|
563
759
|
# Start with core fields (always present)
|
|
@@ -574,29 +770,15 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
574
770
|
}, enumerate(item['test_steps']))),
|
|
575
771
|
}
|
|
576
772
|
|
|
577
|
-
#
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
#
|
|
584
|
-
field_value =
|
|
585
|
-
|
|
586
|
-
if prop['field_name'] == field_name:
|
|
587
|
-
# Use field_value_name if available (for dropdowns), otherwise field_value
|
|
588
|
-
field_value = prop.get('field_value_name') or prop.get('field_value') or ''
|
|
589
|
-
break
|
|
590
|
-
|
|
591
|
-
# Format based on field type
|
|
592
|
-
if is_multiple and (field_value is None or field_value == ''):
|
|
593
|
-
# Multi-select field with no value: show empty array with hint
|
|
594
|
-
parsed_data_row[field_name] = '[] (multi-select)'
|
|
595
|
-
elif field_value is not None:
|
|
596
|
-
parsed_data_row[field_name] = field_value
|
|
597
|
-
else:
|
|
598
|
-
# Regular field with no value
|
|
599
|
-
parsed_data_row[field_name] = ''
|
|
773
|
+
# Add custom fields directly from API response properties
|
|
774
|
+
for prop in item['properties']:
|
|
775
|
+
field_name = prop.get('field_name')
|
|
776
|
+
if not field_name:
|
|
777
|
+
continue
|
|
778
|
+
|
|
779
|
+
# Use field_value_name if available (for dropdowns/users), otherwise field_value
|
|
780
|
+
field_value = prop.get('field_value_name') or prop.get('field_value') or ''
|
|
781
|
+
parsed_data_row[field_name] = field_value
|
|
600
782
|
|
|
601
783
|
parsed_data.append(parsed_data_row)
|
|
602
784
|
|
|
@@ -810,6 +992,120 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
810
992
|
f"in project {self.qtest_project_id}. Exception: \n{stacktrace}"
|
|
811
993
|
) from e
|
|
812
994
|
|
|
995
|
+
def find_test_cases_by_requirement_id(self, requirement_id: str, include_details: bool = False) -> dict:
|
|
996
|
+
"""Find all test cases linked to a QTest requirement.
|
|
997
|
+
|
|
998
|
+
This method uses the ObjectLinkApi.find() to discover test cases that are
|
|
999
|
+
linked to a specific requirement. This is the correct way to find linked
|
|
1000
|
+
test cases - DQL queries cannot search test cases by linked requirement.
|
|
1001
|
+
|
|
1002
|
+
Args:
|
|
1003
|
+
requirement_id: QTest requirement ID in format RQ-123
|
|
1004
|
+
include_details: If True, fetches full test case details. If False, returns summary with Id, Name, Description.
|
|
1005
|
+
|
|
1006
|
+
Returns:
|
|
1007
|
+
dict with requirement_id, total count, and test_cases list
|
|
1008
|
+
|
|
1009
|
+
Raises:
|
|
1010
|
+
ValueError: If requirement is not found
|
|
1011
|
+
ToolException: If API call fails
|
|
1012
|
+
"""
|
|
1013
|
+
# Get internal QTest ID for the requirement
|
|
1014
|
+
qtest_requirement_id = self.__find_qtest_requirement_id_by_id(requirement_id)
|
|
1015
|
+
|
|
1016
|
+
link_object_api_instance = swagger_client.ObjectLinkApi(self._client)
|
|
1017
|
+
|
|
1018
|
+
try:
|
|
1019
|
+
# Use ObjectLinkApi.find() to get linked artifacts
|
|
1020
|
+
# type='requirements' means we're searching from requirements
|
|
1021
|
+
# ids=[qtest_requirement_id] specifies which requirement(s) to check
|
|
1022
|
+
response = link_object_api_instance.find(
|
|
1023
|
+
self.qtest_project_id,
|
|
1024
|
+
type='requirements',
|
|
1025
|
+
ids=[qtest_requirement_id]
|
|
1026
|
+
)
|
|
1027
|
+
|
|
1028
|
+
# Parse the response to extract linked test cases
|
|
1029
|
+
# Response structure: [{id: req_internal_id, pid: 'RQ-15', objects: [{id: tc_internal_id, pid: 'TC-123'}, ...]}]
|
|
1030
|
+
linked_test_cases = []
|
|
1031
|
+
if response and len(response) > 0:
|
|
1032
|
+
for container in response:
|
|
1033
|
+
# Convert to dict if it's an object
|
|
1034
|
+
container_data = container.to_dict() if hasattr(container, 'to_dict') else container
|
|
1035
|
+
objects = container_data.get('objects', []) if isinstance(container_data, dict) else []
|
|
1036
|
+
|
|
1037
|
+
for obj in objects:
|
|
1038
|
+
obj_data = obj.to_dict() if hasattr(obj, 'to_dict') else obj
|
|
1039
|
+
if isinstance(obj_data, dict):
|
|
1040
|
+
pid = obj_data.get('pid', '')
|
|
1041
|
+
internal_id = obj_data.get('id')
|
|
1042
|
+
if pid and pid.startswith('TC-'):
|
|
1043
|
+
linked_test_cases.append({
|
|
1044
|
+
'Id': pid,
|
|
1045
|
+
QTEST_ID: internal_id
|
|
1046
|
+
})
|
|
1047
|
+
|
|
1048
|
+
if not linked_test_cases:
|
|
1049
|
+
return {
|
|
1050
|
+
'requirement_id': requirement_id,
|
|
1051
|
+
'total': 0,
|
|
1052
|
+
'test_cases': [],
|
|
1053
|
+
'message': f"No test cases are linked to requirement '{requirement_id}'"
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
# Build result based on detail level
|
|
1057
|
+
test_cases_result = []
|
|
1058
|
+
|
|
1059
|
+
if not include_details:
|
|
1060
|
+
# Short view: fetch Name, Description via DQL for each test case
|
|
1061
|
+
for tc in linked_test_cases:
|
|
1062
|
+
try:
|
|
1063
|
+
parsed_data = self.__perform_search_by_dql(f"Id = '{tc['Id']}'")
|
|
1064
|
+
if parsed_data:
|
|
1065
|
+
tc_data = parsed_data[0]
|
|
1066
|
+
test_cases_result.append({
|
|
1067
|
+
'Id': tc['Id'],
|
|
1068
|
+
QTEST_ID: tc[QTEST_ID],
|
|
1069
|
+
'Name': tc_data.get('Name'),
|
|
1070
|
+
'Description': tc_data.get('Description', '')
|
|
1071
|
+
})
|
|
1072
|
+
except Exception as e:
|
|
1073
|
+
logger.warning(f"Could not fetch details for {tc['Id']}: {e}")
|
|
1074
|
+
test_cases_result.append({
|
|
1075
|
+
'Id': tc['Id'],
|
|
1076
|
+
QTEST_ID: tc[QTEST_ID],
|
|
1077
|
+
'Name': 'Unable to fetch',
|
|
1078
|
+
'Description': ''
|
|
1079
|
+
})
|
|
1080
|
+
else:
|
|
1081
|
+
# Full details: fetch complete test case data
|
|
1082
|
+
for tc in linked_test_cases:
|
|
1083
|
+
try:
|
|
1084
|
+
parsed_data = self.__perform_search_by_dql(f"Id = '{tc['Id']}'")
|
|
1085
|
+
if parsed_data:
|
|
1086
|
+
test_cases_result.append(parsed_data[0])
|
|
1087
|
+
except Exception as e:
|
|
1088
|
+
logger.warning(f"Could not fetch details for {tc['Id']}: {e}")
|
|
1089
|
+
test_cases_result.append({
|
|
1090
|
+
'Id': tc['Id'],
|
|
1091
|
+
QTEST_ID: tc[QTEST_ID],
|
|
1092
|
+
'error': f'Unable to fetch details: {str(e)}'
|
|
1093
|
+
})
|
|
1094
|
+
|
|
1095
|
+
return {
|
|
1096
|
+
'requirement_id': requirement_id,
|
|
1097
|
+
'total': len(test_cases_result),
|
|
1098
|
+
'test_cases': test_cases_result
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
except ApiException as e:
|
|
1102
|
+
stacktrace = format_exc()
|
|
1103
|
+
logger.error(f"Error finding test cases by requirement: {stacktrace}")
|
|
1104
|
+
raise ToolException(
|
|
1105
|
+
f"Unable to find test cases linked to requirement '{requirement_id}' "
|
|
1106
|
+
f"in project {self.qtest_project_id}. Exception: \n{stacktrace}"
|
|
1107
|
+
) from e
|
|
1108
|
+
|
|
813
1109
|
def search_by_dql(self, dql: str, extract_images:bool=False, prompt: str=None):
|
|
814
1110
|
"""Search for the test cases in qTest using Data Query Language """
|
|
815
1111
|
parsed_data = self.__perform_search_by_dql(dql, extract_images, prompt)
|
|
@@ -897,7 +1193,37 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
897
1193
|
{
|
|
898
1194
|
"name": "search_by_dql",
|
|
899
1195
|
"mode": "search_by_dql",
|
|
900
|
-
"description":
|
|
1196
|
+
"description": """Search test cases in qTest using Data Query Language (DQL).
|
|
1197
|
+
|
|
1198
|
+
CRITICAL: USE SINGLE QUOTES ONLY - DQL does not support double quotes!
|
|
1199
|
+
- ✓ CORRECT: Description ~ 'Forgot Password'
|
|
1200
|
+
- ✗ WRONG: Description ~ "Forgot Password"
|
|
1201
|
+
|
|
1202
|
+
LIMITATION - CANNOT SEARCH BY LINKED OBJECTS:
|
|
1203
|
+
- ✗ 'Requirement Id' = 'RQ-15' will fail - use 'find_test_cases_by_requirement_id' tool instead
|
|
1204
|
+
- ✗ Linked defects or other relationship queries are not supported
|
|
1205
|
+
|
|
1206
|
+
SEARCHABLE FIELDS:
|
|
1207
|
+
- Direct fields: Id, Name, Description, Status, Type, Priority, Automation, etc.
|
|
1208
|
+
- Module: Use 'Module in' syntax
|
|
1209
|
+
- Custom fields: Use exact field name from project configuration
|
|
1210
|
+
- Date fields: Use ISO DateTime format (e.g., 'Created Date' > '2021-05-07T03:15:37.652Z')
|
|
1211
|
+
|
|
1212
|
+
SYNTAX RULES:
|
|
1213
|
+
1. ALL string values MUST use single quotes (never double quotes)
|
|
1214
|
+
2. Field names with spaces MUST be in single quotes: 'Created Date' > '2024-01-01'
|
|
1215
|
+
3. Use ~ for 'contains', !~ for 'not contains': Description ~ 'login'
|
|
1216
|
+
4. Use 'is not empty' for non-empty check: Name is 'not empty'
|
|
1217
|
+
5. Operators: =, !=, <, >, <=, >=, in, ~, !~
|
|
1218
|
+
|
|
1219
|
+
EXAMPLES:
|
|
1220
|
+
- Id = 'TC-123'
|
|
1221
|
+
- Description ~ 'Forgot Password'
|
|
1222
|
+
- Status = 'New' and Priority = 'High'
|
|
1223
|
+
- Module in 'MD-78 Master Test Suite'
|
|
1224
|
+
- Name ~ 'login'
|
|
1225
|
+
- 'Created Date' > '2024-01-01T00:00:00.000Z'
|
|
1226
|
+
""",
|
|
901
1227
|
"args_schema": QtestDataQuerySearch,
|
|
902
1228
|
"ref": self.search_by_dql,
|
|
903
1229
|
},
|
|
@@ -956,5 +1282,24 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
956
1282
|
"description": "Get information about available test case fields and their valid values for the project. Shows which property values are allowed (e.g., Status: 'New', 'In Progress', 'Completed') based on the project configuration. Use force_refresh=true if project configuration has changed.",
|
|
957
1283
|
"args_schema": GetAllTestCasesFieldsForProject,
|
|
958
1284
|
"ref": self.get_all_test_cases_fields_for_project,
|
|
1285
|
+
},
|
|
1286
|
+
{
|
|
1287
|
+
"name": "find_test_cases_by_requirement_id",
|
|
1288
|
+
"mode": "find_test_cases_by_requirement_id",
|
|
1289
|
+
"description": """Find all test cases linked to a QTest requirement.
|
|
1290
|
+
|
|
1291
|
+
Use this tool to find test cases associated with a specific requirement.
|
|
1292
|
+
DQL search cannot query by linked requirement - use this tool instead.
|
|
1293
|
+
|
|
1294
|
+
Parameters:
|
|
1295
|
+
- requirement_id: QTest requirement ID in format RQ-123
|
|
1296
|
+
- include_details: If true, returns full test case data. If false (default), returns Id, QTest Id, Name, and Description.
|
|
1297
|
+
|
|
1298
|
+
Examples:
|
|
1299
|
+
- Find test cases for RQ-15: requirement_id='RQ-15'
|
|
1300
|
+
- Get full details: requirement_id='RQ-15', include_details=true
|
|
1301
|
+
""",
|
|
1302
|
+
"args_schema": FindTestCasesByRequirementId,
|
|
1303
|
+
"ref": self.find_test_cases_by_requirement_id,
|
|
959
1304
|
}
|
|
960
1305
|
]
|
|
@@ -29,8 +29,6 @@ class RallyToolkit(BaseToolkit):
|
|
|
29
29
|
RallyToolkit.toolkit_max_length = get_max_toolkit_length(selected_tools)
|
|
30
30
|
return create_model(
|
|
31
31
|
name,
|
|
32
|
-
name=(str, Field(description="Toolkit name", json_schema_extra={'toolkit_name': True,
|
|
33
|
-
'max_toolkit_length': RallyToolkit.toolkit_max_length})),
|
|
34
32
|
rally_configuration=(RallyConfiguration, Field(description="Rally configuration", json_schema_extra={'configuration_types': ['rally']})),
|
|
35
33
|
workspace=(Optional[str], Field(default=None, description="Rally workspace")),
|
|
36
34
|
project=(Optional[str], Field(default=None, description="Rally project")),
|
|
@@ -39,6 +37,7 @@ class RallyToolkit(BaseToolkit):
|
|
|
39
37
|
'metadata': {
|
|
40
38
|
"label": "Rally",
|
|
41
39
|
"icon_url": "rally.svg",
|
|
40
|
+
"max_length": RallyToolkit.toolkit_max_length,
|
|
42
41
|
"categories": ["project management"],
|
|
43
42
|
"extra_categories": ["agile management", "test management", "scrum", "kanban"]
|
|
44
43
|
}
|
|
@@ -33,6 +33,7 @@ class ReportPortalToolkit(BaseToolkit):
|
|
|
33
33
|
report_portal_configuration=(ReportPortalConfiguration, Field(description="Report Portal Configuration", json_schema_extra={'configuration_types': ['report_portal']})),
|
|
34
34
|
selected_tools=(List[Literal[tuple(selected_tools)]], Field(default=[], json_schema_extra={'args_schemas': selected_tools})),
|
|
35
35
|
__config__=ConfigDict(json_schema_extra={'metadata': {"label": "Report Portal", "icon_url": "reportportal-icon.svg",
|
|
36
|
+
"max_length": ReportPortalToolkit.toolkit_max_length,
|
|
36
37
|
"categories": ["testing"],
|
|
37
38
|
"extra_categories": ["test reporting", "test automation"]}})
|
|
38
39
|
)
|
|
@@ -31,6 +31,7 @@ class SalesforceToolkit(BaseToolkit):
|
|
|
31
31
|
selected_tools=(List[Literal[tuple(available_tools)]], Field(default=[], json_schema_extra={'args_schemas': available_tools})),
|
|
32
32
|
__config__=ConfigDict(json_schema_extra={'metadata': {
|
|
33
33
|
"label": "Salesforce", "icon_url": "salesforce-icon.svg",
|
|
34
|
+
"max_length": SalesforceToolkit.toolkit_max_length,
|
|
34
35
|
"categories": ["other"],
|
|
35
36
|
"extra_categories": ["customer relationship management", "cloud computing", "marketing automation", "salesforce"]
|
|
36
37
|
}})
|
|
@@ -35,9 +35,7 @@ class ServiceNowToolkit(BaseToolkit):
|
|
|
35
35
|
ServiceNowToolkit.toolkit_max_length = get_max_toolkit_length(selected_tools)
|
|
36
36
|
return create_model(
|
|
37
37
|
name,
|
|
38
|
-
name=(str, Field(description="Toolkit name",
|
|
39
|
-
json_schema_extra={
|
|
40
|
-
'toolkit_name': True, 'max_toolkit_length': ServiceNowToolkit.toolkit_max_length})),
|
|
38
|
+
name=(str, Field(description="Toolkit name")),
|
|
41
39
|
response_fields=(Optional[str], Field(description="Response fields", default=None)),
|
|
42
40
|
servicenow_configuration=(ServiceNowConfiguration, Field(description="ServiceNow Configuration",
|
|
43
41
|
json_schema_extra={
|
|
@@ -49,6 +47,7 @@ class ServiceNowToolkit(BaseToolkit):
|
|
|
49
47
|
'metadata': {
|
|
50
48
|
"label": "ServiceNow",
|
|
51
49
|
"icon_url": "service-now.svg",
|
|
50
|
+
"max_length": ServiceNowToolkit.toolkit_max_length,
|
|
52
51
|
"hidden": False,
|
|
53
52
|
"sections": {
|
|
54
53
|
"auth": {
|
|
@@ -48,6 +48,7 @@ class SharepointToolkit(BaseToolkit):
|
|
|
48
48
|
__config__=ConfigDict(json_schema_extra={
|
|
49
49
|
'metadata': {
|
|
50
50
|
"label": "Sharepoint", "icon_url": "sharepoint.svg",
|
|
51
|
+
"max_length": SharepointToolkit.toolkit_max_length,
|
|
51
52
|
"categories": ["office"],
|
|
52
53
|
"extra_categories": ["microsoft", "cloud storage", "team collaboration", "content management"]
|
|
53
54
|
}})
|
|
@@ -59,6 +59,7 @@ class SlackToolkit(BaseToolkit):
|
|
|
59
59
|
'metadata': {
|
|
60
60
|
"label": "Slack",
|
|
61
61
|
"icon_url": "slack-icon.svg",
|
|
62
|
+
"max_length": SlackToolkit.toolkit_max_length,
|
|
62
63
|
"categories": ["communication"],
|
|
63
64
|
"extra_categories": ["slack", "chat", "messaging", "collaboration"],
|
|
64
65
|
}
|
alita_sdk/tools/sql/__init__.py
CHANGED
|
@@ -34,7 +34,7 @@ class SQLToolkit(BaseToolkit):
|
|
|
34
34
|
return create_model(
|
|
35
35
|
name,
|
|
36
36
|
dialect=(Literal[tuple(supported_dialects)], Field(description="Database dialect (mysql or postgres)")),
|
|
37
|
-
database_name=(str, Field(description="Database name"
|
|
37
|
+
database_name=(str, Field(description="Database name")),
|
|
38
38
|
sql_configuration=(SqlConfiguration, Field(description="SQL Configuration", json_schema_extra={'configuration_types': ['sql']})),
|
|
39
39
|
selected_tools=(List[Literal[tuple(selected_tools)]], Field(default=[], json_schema_extra={'args_schemas': selected_tools})),
|
|
40
40
|
__config__=ConfigDict(json_schema_extra=
|
|
@@ -42,6 +42,7 @@ class SQLToolkit(BaseToolkit):
|
|
|
42
42
|
'metadata':
|
|
43
43
|
{
|
|
44
44
|
"label": "SQL", "icon_url": "sql-icon.svg",
|
|
45
|
+
"max_length": SQLToolkit.toolkit_max_length,
|
|
45
46
|
"categories": ["development"],
|
|
46
47
|
"extra_categories": ["sql", "data management", "data analysis"]}})
|
|
47
48
|
)
|
|
@@ -33,6 +33,7 @@ class TestIOToolkit(BaseToolkit):
|
|
|
33
33
|
testio_configuration=(TestIOConfiguration, Field(description="TestIO Configuration", json_schema_extra={'configuration_types': ['testio']})),
|
|
34
34
|
selected_tools=(List[Literal[tuple(selected_tools)]], Field(default=[], json_schema_extra={'args_schemas': selected_tools})),
|
|
35
35
|
__config__=ConfigDict(json_schema_extra={'metadata': {"label": "TestIO", "icon_url": "testio-icon.svg",
|
|
36
|
+
"max_length": TOOLKIT_MAX_LENGTH,
|
|
36
37
|
"categories": ["testing"],
|
|
37
38
|
"extra_categories": ["test automation", "test case management", "test planning"]}})
|
|
38
39
|
)
|