alita-sdk 0.3.374__py3-none-any.whl → 0.3.423__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/configurations/bitbucket.py +95 -0
- alita_sdk/configurations/confluence.py +96 -1
- alita_sdk/configurations/gitlab.py +79 -0
- alita_sdk/configurations/jira.py +103 -0
- alita_sdk/configurations/testrail.py +88 -0
- alita_sdk/configurations/xray.py +93 -0
- alita_sdk/configurations/zephyr_enterprise.py +93 -0
- alita_sdk/configurations/zephyr_essential.py +75 -0
- alita_sdk/runtime/clients/client.py +3 -2
- alita_sdk/runtime/clients/sandbox_client.py +8 -0
- alita_sdk/runtime/langchain/assistant.py +56 -40
- alita_sdk/runtime/langchain/constants.py +4 -0
- alita_sdk/runtime/langchain/document_loaders/AlitaDocxMammothLoader.py +315 -3
- alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +4 -1
- alita_sdk/runtime/langchain/document_loaders/constants.py +28 -12
- alita_sdk/runtime/langchain/langraph_agent.py +92 -28
- alita_sdk/runtime/langchain/utils.py +24 -4
- alita_sdk/runtime/toolkits/application.py +8 -1
- alita_sdk/runtime/toolkits/tools.py +80 -49
- alita_sdk/runtime/tools/__init__.py +7 -2
- alita_sdk/runtime/tools/application.py +7 -0
- alita_sdk/runtime/tools/function.py +28 -23
- alita_sdk/runtime/tools/graph.py +10 -4
- alita_sdk/runtime/tools/image_generation.py +104 -8
- alita_sdk/runtime/tools/llm.py +146 -114
- alita_sdk/runtime/tools/sandbox.py +166 -63
- alita_sdk/runtime/tools/vectorstore.py +22 -21
- alita_sdk/runtime/tools/vectorstore_base.py +16 -15
- alita_sdk/runtime/utils/utils.py +1 -0
- alita_sdk/tools/__init__.py +43 -31
- alita_sdk/tools/ado/work_item/ado_wrapper.py +17 -8
- alita_sdk/tools/base_indexer_toolkit.py +102 -93
- alita_sdk/tools/code_indexer_toolkit.py +15 -5
- alita_sdk/tools/confluence/api_wrapper.py +30 -8
- alita_sdk/tools/confluence/loader.py +10 -0
- alita_sdk/tools/elitea_base.py +22 -22
- alita_sdk/tools/gitlab/api_wrapper.py +8 -9
- alita_sdk/tools/jira/api_wrapper.py +1 -1
- alita_sdk/tools/non_code_indexer_toolkit.py +2 -2
- alita_sdk/tools/openapi/__init__.py +10 -1
- alita_sdk/tools/qtest/api_wrapper.py +298 -51
- alita_sdk/tools/sharepoint/api_wrapper.py +104 -33
- alita_sdk/tools/sharepoint/authorization_helper.py +175 -1
- alita_sdk/tools/sharepoint/utils.py +8 -2
- alita_sdk/tools/utils/content_parser.py +27 -16
- alita_sdk/tools/vector_adapters/VectorStoreAdapter.py +38 -25
- {alita_sdk-0.3.374.dist-info → alita_sdk-0.3.423.dist-info}/METADATA +1 -1
- {alita_sdk-0.3.374.dist-info → alita_sdk-0.3.423.dist-info}/RECORD +51 -51
- {alita_sdk-0.3.374.dist-info → alita_sdk-0.3.423.dist-info}/WHEEL +0 -0
- {alita_sdk-0.3.374.dist-info → alita_sdk-0.3.423.dist-info}/licenses/LICENSE +0 -0
- {alita_sdk-0.3.374.dist-info → alita_sdk-0.3.423.dist-info}/top_level.txt +0 -0
|
@@ -9,7 +9,7 @@ import swagger_client
|
|
|
9
9
|
from langchain_core.tools import ToolException
|
|
10
10
|
from pydantic import Field, PrivateAttr, model_validator, create_model, SecretStr
|
|
11
11
|
from sklearn.feature_extraction.text import strip_tags
|
|
12
|
-
from swagger_client import TestCaseApi, SearchApi, PropertyResource, ModuleApi
|
|
12
|
+
from swagger_client import TestCaseApi, SearchApi, PropertyResource, ModuleApi, ProjectApi, FieldApi
|
|
13
13
|
from swagger_client.rest import ApiException
|
|
14
14
|
|
|
15
15
|
from ..elitea_base import BaseToolApiWrapper
|
|
@@ -26,21 +26,25 @@ If generated data was used, put appropriate note to the test case description fi
|
|
|
26
26
|
### CRITERIA
|
|
27
27
|
1. The structure should be as in EXAMPLE.
|
|
28
28
|
2. Case and spaces for field names should be exactly the same as in NOTES.
|
|
29
|
-
3. Extra fields are allowed.
|
|
29
|
+
3. Extra fields are allowed and will be mapped to project's custom fields if they exist.
|
|
30
30
|
4. "{QTEST_ID}" is required to update, change or replace values in test case.
|
|
31
31
|
5. Do not provide "Id" and "{QTEST_ID}" to create test case.
|
|
32
|
-
6
|
|
32
|
+
6. "Steps" is a list of test step objects with fields "Test Step Number", "Test Step Description", "Test Step Expected Result".
|
|
33
|
+
7. For updates, provide ONLY the fields you want to change. Omitted fields will remain unchanged.
|
|
33
34
|
|
|
34
35
|
### NOTES
|
|
35
|
-
Id: Unique identifier (e.g., TC-123).
|
|
36
|
-
QTest id: Unique identifier (e.g., 4626964).
|
|
37
|
-
Name: Brief title.
|
|
38
|
-
Description: Short purpose.
|
|
39
|
-
Type: 'Manual'
|
|
40
|
-
Status:
|
|
41
|
-
Priority:
|
|
42
|
-
Test Type:
|
|
43
|
-
Precondition:
|
|
36
|
+
Id: Unique identifier (e.g., TC-123). Read-only.
|
|
37
|
+
QTest id: Unique identifier (e.g., 4626964). Required for updates.
|
|
38
|
+
Name: Brief title of the test case.
|
|
39
|
+
Description: Short description of the test purpose.
|
|
40
|
+
Type: Type of test (e.g., 'Manual', 'Automation - UTAF').
|
|
41
|
+
Status: Current status (e.g., 'New', 'In Progress', 'Completed').
|
|
42
|
+
Priority: Priority level (e.g., 'High', 'Medium', 'Low').
|
|
43
|
+
Test Type: Category of test (e.g., 'Functional', 'Regression', 'Smoke').
|
|
44
|
+
Precondition: Prerequisites for the test, formatted as: <Step1> <Step2> Leave blank if none.
|
|
45
|
+
Steps: Array of test steps with Description and Expected Result.
|
|
46
|
+
|
|
47
|
+
**For Updates**: Include only the fields you want to modify. The system will validate property values against project configuration.
|
|
44
48
|
|
|
45
49
|
### EXAMPLE
|
|
46
50
|
{{
|
|
@@ -118,6 +122,17 @@ GetModules = create_model(
|
|
|
118
122
|
|
|
119
123
|
)
|
|
120
124
|
|
|
125
|
+
GetAllTestCasesFieldsForProject = create_model(
|
|
126
|
+
"GetAllTestCasesFieldsForProject",
|
|
127
|
+
force_refresh=(Optional[bool],
|
|
128
|
+
Field(description="Set to true to reload field definitions from API if project configuration has changed (new fields added, dropdown values modified). Default: false (uses cached data).",
|
|
129
|
+
default=False)),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
NoInput = create_model(
|
|
133
|
+
"NoInput"
|
|
134
|
+
)
|
|
135
|
+
|
|
121
136
|
class QtestApiWrapper(BaseToolApiWrapper):
|
|
122
137
|
base_url: str
|
|
123
138
|
qtest_project_id: int
|
|
@@ -126,6 +141,7 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
126
141
|
page: int = 1
|
|
127
142
|
no_of_tests_shown_in_dql_search: int = 10
|
|
128
143
|
_client: Any = PrivateAttr()
|
|
144
|
+
_field_definitions_cache: Optional[dict] = PrivateAttr(default=None)
|
|
129
145
|
llm: Any
|
|
130
146
|
|
|
131
147
|
@model_validator(mode='before')
|
|
@@ -135,9 +151,8 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
135
151
|
values['qtest_project_id'] = values.pop('project_id')
|
|
136
152
|
return values
|
|
137
153
|
|
|
138
|
-
@model_validator(mode='
|
|
139
|
-
|
|
140
|
-
def validate_toolkit(cls, values):
|
|
154
|
+
@model_validator(mode='after')
|
|
155
|
+
def validate_toolkit(self):
|
|
141
156
|
try:
|
|
142
157
|
import swagger_client # noqa: F401
|
|
143
158
|
except ImportError:
|
|
@@ -146,15 +161,13 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
146
161
|
"`pip install git+https://github.com/Roman-Mitusov/qtest-api.git`"
|
|
147
162
|
)
|
|
148
163
|
|
|
149
|
-
|
|
150
|
-
api_token = values.get('qtest_api_token')
|
|
151
|
-
if api_token:
|
|
164
|
+
if self.qtest_api_token:
|
|
152
165
|
configuration = swagger_client.Configuration()
|
|
153
|
-
configuration.host =
|
|
154
|
-
configuration.api_key['Authorization'] =
|
|
166
|
+
configuration.host = self.base_url
|
|
167
|
+
configuration.api_key['Authorization'] = self.qtest_api_token.get_secret_value()
|
|
155
168
|
configuration.api_key_prefix['Authorization'] = 'Bearer'
|
|
156
|
-
|
|
157
|
-
return
|
|
169
|
+
self._client = swagger_client.ApiClient(configuration)
|
|
170
|
+
return self
|
|
158
171
|
|
|
159
172
|
def __instantiate_test_api_instance(self) -> TestCaseApi:
|
|
160
173
|
# Instantiate the TestCaseApi instance according to the qtest api documentation and swagger client
|
|
@@ -163,33 +176,177 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
163
176
|
def __instantiate_module_api_instance(self) -> ModuleApi:
|
|
164
177
|
return swagger_client.ModuleApi(self._client)
|
|
165
178
|
|
|
179
|
+
def __instantiate_fields_api_instance(self) -> FieldApi:
|
|
180
|
+
return swagger_client.FieldApi(self._client)
|
|
181
|
+
|
|
182
|
+
def __get_field_definitions_cached(self) -> dict:
|
|
183
|
+
"""Get field definitions with session-level caching.
|
|
184
|
+
|
|
185
|
+
Field definitions are cached for the lifetime of this wrapper instance.
|
|
186
|
+
If project field configuration changes, call refresh_field_definitions_cache()
|
|
187
|
+
to reload the definitions.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
dict: Field definitions mapping
|
|
191
|
+
"""
|
|
192
|
+
if self._field_definitions_cache is None:
|
|
193
|
+
self._field_definitions_cache = self.__get_project_field_definitions()
|
|
194
|
+
return self._field_definitions_cache
|
|
195
|
+
|
|
196
|
+
def refresh_field_definitions_cache(self) -> dict:
|
|
197
|
+
"""Manually refresh the field definitions cache.
|
|
198
|
+
|
|
199
|
+
Call this method if project field configuration has been updated
|
|
200
|
+
(new fields added, dropdown values changed, etc.) and you need to
|
|
201
|
+
reload the definitions within the same agent session.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
dict: Freshly loaded field definitions
|
|
205
|
+
"""
|
|
206
|
+
self._field_definitions_cache = None
|
|
207
|
+
return self.__get_field_definitions_cached()
|
|
208
|
+
|
|
209
|
+
def __map_properties_to_api_format(self, test_case_data: dict, field_definitions: dict,
|
|
210
|
+
base_properties: list = None) -> list:
|
|
211
|
+
"""
|
|
212
|
+
Convert user-friendly property names/values to QTest API PropertyResource format.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
test_case_data: Dict with property names as keys (e.g., {"Status": "New", "Priority": "High"})
|
|
216
|
+
field_definitions: Output from __get_project_field_definitions()
|
|
217
|
+
base_properties: Existing properties from a test case (for updates, optional)
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
list[PropertyResource]: Properties ready for API submission
|
|
221
|
+
|
|
222
|
+
Raises:
|
|
223
|
+
ValueError: If any field names are unknown or values are invalid (shows ALL errors)
|
|
224
|
+
"""
|
|
225
|
+
# Start with base properties or empty dict
|
|
226
|
+
props_dict = {}
|
|
227
|
+
if base_properties:
|
|
228
|
+
for prop in base_properties:
|
|
229
|
+
field_name = prop.get('field_name')
|
|
230
|
+
if field_name:
|
|
231
|
+
props_dict[field_name] = {
|
|
232
|
+
'field_id': prop['field_id'],
|
|
233
|
+
'field_name': field_name,
|
|
234
|
+
'field_value': prop['field_value'],
|
|
235
|
+
'field_value_name': prop.get('field_value_name')
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
# Collect ALL validation errors before raising
|
|
239
|
+
validation_errors = []
|
|
240
|
+
|
|
241
|
+
# Map incoming properties from test_case_data
|
|
242
|
+
for field_name, field_value in test_case_data.items():
|
|
243
|
+
# Skip non-property fields (these are handled separately)
|
|
244
|
+
if field_name in ['Name', 'Description', 'Precondition', 'Steps', 'Id', QTEST_ID]:
|
|
245
|
+
continue
|
|
246
|
+
|
|
247
|
+
# Skip None or empty string values (don't update these fields)
|
|
248
|
+
if field_value is None or field_value == '':
|
|
249
|
+
continue
|
|
250
|
+
|
|
251
|
+
# Validate field exists in project - STRICT validation
|
|
252
|
+
if field_name not in field_definitions:
|
|
253
|
+
validation_errors.append(
|
|
254
|
+
f"❌ Unknown field '{field_name}' - not defined in project configuration"
|
|
255
|
+
)
|
|
256
|
+
continue # Skip to next field, keep collecting errors
|
|
257
|
+
|
|
258
|
+
field_def = field_definitions[field_name]
|
|
259
|
+
field_id = field_def['field_id']
|
|
260
|
+
|
|
261
|
+
# Validate value for dropdown fields (only if field has allowed values)
|
|
262
|
+
if field_def['values']:
|
|
263
|
+
# Field has allowed values (dropdown/combobox) - validate strictly
|
|
264
|
+
if field_value not in field_def['values']:
|
|
265
|
+
available = ", ".join(sorted(field_def['values'].keys()))
|
|
266
|
+
validation_errors.append(
|
|
267
|
+
f"❌ Invalid value '{field_value}' for field '{field_name}'. "
|
|
268
|
+
f"Allowed values: {available}"
|
|
269
|
+
)
|
|
270
|
+
continue # Skip to next field, keep collecting errors
|
|
271
|
+
field_value_id = field_def['values'][field_value]
|
|
272
|
+
field_value_name = field_value
|
|
273
|
+
else:
|
|
274
|
+
# Text field or field without restricted values - use value directly
|
|
275
|
+
# No validation needed - users can write anything (by design)
|
|
276
|
+
field_value_id = field_value
|
|
277
|
+
field_value_name = field_value if isinstance(field_value, str) else None
|
|
278
|
+
|
|
279
|
+
# Update or add property (only if no errors for this field)
|
|
280
|
+
props_dict[field_name] = {
|
|
281
|
+
'field_id': field_id,
|
|
282
|
+
'field_name': field_name,
|
|
283
|
+
'field_value': field_value_id,
|
|
284
|
+
'field_value_name': field_value_name
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
# If ANY validation errors found, raise comprehensive error with all issues
|
|
288
|
+
if validation_errors:
|
|
289
|
+
available_fields = ", ".join(sorted(field_definitions.keys()))
|
|
290
|
+
error_msg = (
|
|
291
|
+
f"Found {len(validation_errors)} validation error(s) in test case properties:\n\n" +
|
|
292
|
+
"\n".join(validation_errors) +
|
|
293
|
+
f"\n\n📋 Available fields for this project: {available_fields}\n\n"
|
|
294
|
+
f"💡 Tip: Use 'get_all_test_cases_fields_for_project' tool to see all fields with their allowed values."
|
|
295
|
+
)
|
|
296
|
+
raise ValueError(error_msg)
|
|
297
|
+
|
|
298
|
+
# Convert to PropertyResource list, filtering out special fields
|
|
299
|
+
result = []
|
|
300
|
+
for field_name, prop_data in props_dict.items():
|
|
301
|
+
if field_name in ['Shared', 'Projects Shared to']:
|
|
302
|
+
continue
|
|
303
|
+
result.append(PropertyResource(
|
|
304
|
+
field_id=prop_data['field_id'],
|
|
305
|
+
field_name=prop_data['field_name'],
|
|
306
|
+
field_value=prop_data['field_value'],
|
|
307
|
+
field_value_name=prop_data.get('field_value_name')
|
|
308
|
+
))
|
|
309
|
+
|
|
310
|
+
return result
|
|
311
|
+
|
|
166
312
|
def __build_body_for_create_test_case(self, test_cases_data: list[dict],
|
|
167
313
|
folder_to_place_test_cases_to: str = '') -> list:
|
|
168
|
-
|
|
314
|
+
# Get field definitions for property mapping (cached for session)
|
|
315
|
+
field_definitions = self.__get_field_definitions_cached()
|
|
316
|
+
|
|
169
317
|
modules = self._parse_modules()
|
|
170
318
|
parent_id = ''.join(str(module['module_id']) for module in modules if
|
|
171
319
|
folder_to_place_test_cases_to and module['full_module_name'] == folder_to_place_test_cases_to)
|
|
172
|
-
|
|
173
|
-
for prop in initial_project_properties:
|
|
174
|
-
if prop.get('field_name', '') == 'Shared' or prop.get('field_name', '') == 'Projects Shared to':
|
|
175
|
-
continue
|
|
176
|
-
props.append(PropertyResource(field_id=prop['field_id'], field_name=prop['field_name'],
|
|
177
|
-
field_value_name=prop.get('field_value_name', None),
|
|
178
|
-
field_value=prop['field_value']))
|
|
320
|
+
|
|
179
321
|
bodies = []
|
|
180
322
|
for test_case in test_cases_data:
|
|
323
|
+
# Map properties from user format to API format
|
|
324
|
+
props = self.__map_properties_to_api_format(test_case, field_definitions)
|
|
325
|
+
|
|
181
326
|
body = swagger_client.TestCaseWithCustomFieldResource(properties=props)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
327
|
+
|
|
328
|
+
# Only set fields if they are explicitly provided in the input
|
|
329
|
+
# This prevents overwriting existing values with None during partial updates
|
|
330
|
+
if 'Name' in test_case:
|
|
331
|
+
body.name = test_case['Name']
|
|
332
|
+
if 'Precondition' in test_case:
|
|
333
|
+
body.precondition = test_case['Precondition']
|
|
334
|
+
if 'Description' in test_case:
|
|
335
|
+
body.description = test_case['Description']
|
|
336
|
+
|
|
185
337
|
if parent_id:
|
|
186
338
|
body.parent_id = parent_id
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
339
|
+
|
|
340
|
+
# Only set test_steps if Steps are provided in the input
|
|
341
|
+
# This prevents overwriting existing steps during partial updates
|
|
342
|
+
if 'Steps' in test_case and test_case['Steps'] is not None:
|
|
343
|
+
test_steps_resources = []
|
|
344
|
+
for step in test_case['Steps']:
|
|
345
|
+
test_steps_resources.append(
|
|
346
|
+
swagger_client.TestStepResource(description=step.get('Test Step Description'),
|
|
347
|
+
expected=step.get('Test Step Expected Result')))
|
|
348
|
+
body.test_steps = test_steps_resources
|
|
349
|
+
|
|
193
350
|
bodies.append(body)
|
|
194
351
|
return bodies
|
|
195
352
|
|
|
@@ -206,6 +363,100 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
206
363
|
Exception: \n {stacktrace}""")
|
|
207
364
|
return modules
|
|
208
365
|
|
|
366
|
+
def __get_project_field_definitions(self) -> dict:
|
|
367
|
+
"""
|
|
368
|
+
Get structured field definitions for test cases in the project.
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
dict: Mapping of field names to their IDs and allowed values.
|
|
372
|
+
Example: {
|
|
373
|
+
'Status': {
|
|
374
|
+
'field_id': 12345,
|
|
375
|
+
'required': True,
|
|
376
|
+
'values': {'New': 1, 'In Progress': 2, 'Completed': 3}
|
|
377
|
+
},
|
|
378
|
+
'Priority': {
|
|
379
|
+
'field_id': 12346,
|
|
380
|
+
'required': False,
|
|
381
|
+
'values': {'High': 1, 'Medium': 2, 'Low': 3}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
"""
|
|
385
|
+
fields_api = self.__instantiate_fields_api_instance()
|
|
386
|
+
qtest_object = 'test-cases'
|
|
387
|
+
|
|
388
|
+
try:
|
|
389
|
+
fields = fields_api.get_fields(self.qtest_project_id, qtest_object)
|
|
390
|
+
except ApiException as e:
|
|
391
|
+
stacktrace = format_exc()
|
|
392
|
+
logger.error(f"Exception when calling FieldAPI->get_fields:\n {stacktrace}")
|
|
393
|
+
raise ValueError(
|
|
394
|
+
f"Unable to get test case fields for project {self.qtest_project_id}. Exception: \n {stacktrace}")
|
|
395
|
+
|
|
396
|
+
# Build structured mapping
|
|
397
|
+
field_mapping = {}
|
|
398
|
+
for field in fields:
|
|
399
|
+
field_name = field.label
|
|
400
|
+
field_mapping[field_name] = {
|
|
401
|
+
'field_id': field.id,
|
|
402
|
+
'required': getattr(field, 'required', False),
|
|
403
|
+
'values': {}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
# Map allowed values if field has them (dropdown/combobox fields)
|
|
407
|
+
if hasattr(field, 'allowed_values') and field.allowed_values:
|
|
408
|
+
for allowed_value in field.allowed_values:
|
|
409
|
+
# AllowedValueResource has 'label' for the display name and 'value' for the ID
|
|
410
|
+
# Note: 'value' is the field_value, not 'id'
|
|
411
|
+
value_label = allowed_value.label
|
|
412
|
+
value_id = allowed_value.value
|
|
413
|
+
field_mapping[field_name]['values'][value_label] = value_id
|
|
414
|
+
|
|
415
|
+
return field_mapping
|
|
416
|
+
|
|
417
|
+
def __format_field_info_for_display(self, field_definitions: dict) -> str:
|
|
418
|
+
"""
|
|
419
|
+
Format field definitions in human-readable format for LLM.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
field_definitions: Output from __get_project_field_definitions()
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
Formatted string with field information
|
|
426
|
+
"""
|
|
427
|
+
output = [f"Available Test Case Fields for Project {self.qtest_project_id}:\n"]
|
|
428
|
+
|
|
429
|
+
for field_name, field_info in sorted(field_definitions.items()):
|
|
430
|
+
required_marker = " (Required)" if field_info.get('required') else ""
|
|
431
|
+
output.append(f"\n{field_name}{required_marker}:")
|
|
432
|
+
|
|
433
|
+
if field_info.get('values'):
|
|
434
|
+
for value_name, value_id in sorted(field_info['values'].items()):
|
|
435
|
+
output.append(f" - {value_name} (id: {value_id})")
|
|
436
|
+
else:
|
|
437
|
+
output.append(" Type: text")
|
|
438
|
+
|
|
439
|
+
output.append("\n\nUse these exact value names when creating or updating test cases.")
|
|
440
|
+
return ''.join(output)
|
|
441
|
+
|
|
442
|
+
def get_all_test_cases_fields_for_project(self, force_refresh: bool = False) -> str:
|
|
443
|
+
"""
|
|
444
|
+
Get formatted information about available test case fields and their values.
|
|
445
|
+
This method is exposed as a tool for LLM to query field information.
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
force_refresh: If True, reload field definitions from API instead of using cache.
|
|
449
|
+
Use this if project configuration has changed (new fields added,
|
|
450
|
+
dropdown values modified, etc.).
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
Formatted string with field names and allowed values
|
|
454
|
+
"""
|
|
455
|
+
if force_refresh:
|
|
456
|
+
self.refresh_field_definitions_cache()
|
|
457
|
+
field_defs = self.__get_field_definitions_cached()
|
|
458
|
+
return self.__format_field_info_for_display(field_defs)
|
|
459
|
+
|
|
209
460
|
def _parse_modules(self) -> list[dict]:
|
|
210
461
|
modules = self.__get_all_modules_for_project()
|
|
211
462
|
result = []
|
|
@@ -323,18 +574,7 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
323
574
|
parsed_data = self.__perform_search_by_dql(dql)
|
|
324
575
|
return parsed_data[0]['QTest Id']
|
|
325
576
|
|
|
326
|
-
def
|
|
327
|
-
test_api_instance = self.__instantiate_test_api_instance()
|
|
328
|
-
expand_props = 'true'
|
|
329
|
-
try:
|
|
330
|
-
response = test_api_instance.get_test_cases(self.qtest_project_id, 1, 1, expand_props=expand_props)
|
|
331
|
-
return response[0]['properties']
|
|
332
|
-
except ApiException as e:
|
|
333
|
-
stacktrace = format_exc()
|
|
334
|
-
logger.error(f"Exception when calling TestCaseApi->get_test_cases: \n {stacktrace}")
|
|
335
|
-
raise e
|
|
336
|
-
|
|
337
|
-
def __is_jira_requirement_present(self, jira_issue_id: str) -> (bool, dict):
|
|
577
|
+
def __is_jira_requirement_present(self, jira_issue_id: str) -> tuple[bool, dict]:
|
|
338
578
|
""" Define if particular Jira requirement is present in qtest or not """
|
|
339
579
|
dql = f"'External Id' = '{jira_issue_id}'"
|
|
340
580
|
search_instance: SearchApi = swagger_client.SearchApi(self._client)
|
|
@@ -508,5 +748,12 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
508
748
|
"description": self.get_modules.__doc__,
|
|
509
749
|
"args_schema": GetModules,
|
|
510
750
|
"ref": self.get_modules,
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
"name": "get_all_test_cases_fields_for_project",
|
|
754
|
+
"mode": "get_all_test_cases_fields_for_project",
|
|
755
|
+
"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.",
|
|
756
|
+
"args_schema": GetAllTestCasesFieldsForProject,
|
|
757
|
+
"ref": self.get_all_test_cases_fields_for_project,
|
|
511
758
|
}
|
|
512
759
|
]
|
|
@@ -8,6 +8,7 @@ from office365.runtime.auth.client_credential import ClientCredential
|
|
|
8
8
|
from office365.sharepoint.client_context import ClientContext
|
|
9
9
|
from pydantic import Field, PrivateAttr, create_model, model_validator, SecretStr
|
|
10
10
|
|
|
11
|
+
from .utils import decode_sharepoint_string
|
|
11
12
|
from ..non_code_indexer_toolkit import NonCodeIndexerToolkit
|
|
12
13
|
from ..utils.content_parser import parse_file_content
|
|
13
14
|
from ...runtime.utils.utils import IndexerKeywords
|
|
@@ -91,44 +92,85 @@ class SharepointApiWrapper(NonCodeIndexerToolkit):
|
|
|
91
92
|
target_list = self._client.web.lists.get_by_title(list_title)
|
|
92
93
|
self._client.load(target_list)
|
|
93
94
|
self._client.execute_query()
|
|
94
|
-
items = target_list.items.
|
|
95
|
-
logging.info("{0} items from sharepoint loaded successfully.".format(len(items)))
|
|
95
|
+
items = target_list.items.top(limit).get().execute_query()
|
|
96
|
+
logging.info("{0} items from sharepoint loaded successfully via SharePoint REST API.".format(len(items)))
|
|
96
97
|
result = []
|
|
97
98
|
for item in items:
|
|
98
99
|
result.append(item.properties)
|
|
99
100
|
return result
|
|
100
|
-
except Exception as
|
|
101
|
-
logging.
|
|
102
|
-
|
|
101
|
+
except Exception as base_e:
|
|
102
|
+
logging.warning(f"Primary SharePoint REST list read failed: {base_e}. Attempting Graph API fallback.")
|
|
103
|
+
# Attempt Graph API fallback
|
|
104
|
+
try:
|
|
105
|
+
from .authorization_helper import SharepointAuthorizationHelper
|
|
106
|
+
auth_helper = SharepointAuthorizationHelper(
|
|
107
|
+
client_id=self.client_id,
|
|
108
|
+
client_secret=self.client_secret.get_secret_value() if self.client_secret else None,
|
|
109
|
+
tenant="", # optional for graph api (derived inside helper)
|
|
110
|
+
scope="", # optional for graph api
|
|
111
|
+
token_json="", # not needed for client credentials flow here
|
|
112
|
+
)
|
|
113
|
+
graph_items = auth_helper.get_list_items(self.site_url, list_title, limit)
|
|
114
|
+
if graph_items:
|
|
115
|
+
logging.info(f"{len(graph_items)} items from sharepoint loaded successfully via Graph API fallback.")
|
|
116
|
+
return graph_items
|
|
117
|
+
else:
|
|
118
|
+
return ToolException("List appears empty or inaccessible via both REST and Graph APIs.")
|
|
119
|
+
except Exception as graph_e:
|
|
120
|
+
logging.error(f"Graph API fallback failed: {graph_e}")
|
|
121
|
+
return ToolException(f"Cannot read list '{list_title}'. Check list name and permissions: {base_e} | {graph_e}")
|
|
103
122
|
|
|
104
123
|
|
|
105
124
|
def get_files_list(self, folder_name: str = None, limit_files: int = 100):
|
|
106
125
|
""" If folder name is specified, lists all files in this folder under Shared Documents path. If folder name is empty, lists all files under root catalog (Shared Documents). Number of files is limited by limit_files (default is 100)."""
|
|
107
126
|
try:
|
|
127
|
+
# exclude default system libraries like 'Form Templates', 'Site Assets', 'Style Library'
|
|
128
|
+
all_libraries = self._client.web.lists.filter("BaseTemplate eq 101 and Title ne 'Form Templates' and Title ne 'Site Assets' and Title ne 'Style Library'").get().execute_query()
|
|
108
129
|
result = []
|
|
109
130
|
if not limit_files:
|
|
110
131
|
limit_files = 100
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
132
|
+
#
|
|
133
|
+
for lib in all_libraries:
|
|
134
|
+
library_type = decode_sharepoint_string(lib.properties["EntityTypeName"])
|
|
135
|
+
target_folder_url = f"{library_type}/{folder_name}" if folder_name else library_type
|
|
136
|
+
files = (self._client.web.get_folder_by_server_relative_path(target_folder_url)
|
|
137
|
+
.get_files(True)
|
|
138
|
+
.execute_query())
|
|
139
|
+
#
|
|
140
|
+
for file in files:
|
|
141
|
+
if f"{library_type}/Forms" in file.properties['ServerRelativeUrl']:
|
|
142
|
+
# skip files from system folder "Forms"
|
|
143
|
+
continue
|
|
144
|
+
if len(result) >= limit_files:
|
|
145
|
+
break
|
|
146
|
+
temp_props = {
|
|
147
|
+
'Name': file.properties['Name'],
|
|
148
|
+
'Path': file.properties['ServerRelativeUrl'],
|
|
149
|
+
'Created': file.properties['TimeCreated'],
|
|
150
|
+
'Modified': file.properties['TimeLastModified'],
|
|
151
|
+
'Link': file.properties['LinkingUrl'],
|
|
152
|
+
'id': file.properties['UniqueId']
|
|
153
|
+
}
|
|
154
|
+
result.append(temp_props)
|
|
128
155
|
return result if result else ToolException("Can not get files or folder is empty. Please, double check folder name and read permissions.")
|
|
129
156
|
except Exception as e:
|
|
130
|
-
|
|
131
|
-
|
|
157
|
+
# attempt to get via graph api
|
|
158
|
+
try:
|
|
159
|
+
# attempt to get files via graph api
|
|
160
|
+
from .authorization_helper import SharepointAuthorizationHelper
|
|
161
|
+
auth_helper = SharepointAuthorizationHelper(
|
|
162
|
+
client_id=self.client_id,
|
|
163
|
+
client_secret=self.client_secret.get_secret_value(),
|
|
164
|
+
tenant="", # optional for graph api
|
|
165
|
+
scope="", # optional for graph api
|
|
166
|
+
token_json="", # optional for graph api
|
|
167
|
+
)
|
|
168
|
+
files = auth_helper.get_files_list(self.site_url, folder_name, limit_files)
|
|
169
|
+
return files
|
|
170
|
+
except Exception as graph_e:
|
|
171
|
+
logging.error(f"Failed to load files from sharepoint via base api: {e}")
|
|
172
|
+
logging.error(f"Failed to load files from sharepoint via graph api: {graph_e}")
|
|
173
|
+
return ToolException(f"Can not get files. Please, double check folder name and read permissions: {e} and {graph_e}")
|
|
132
174
|
|
|
133
175
|
def read_file(self, path,
|
|
134
176
|
is_capture_image: bool = False,
|
|
@@ -141,11 +183,28 @@ class SharepointApiWrapper(NonCodeIndexerToolkit):
|
|
|
141
183
|
self._client.load(file).execute_query()
|
|
142
184
|
|
|
143
185
|
file_content = file.read()
|
|
186
|
+
file_name = file.name
|
|
144
187
|
self._client.execute_query()
|
|
145
188
|
except Exception as e:
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
189
|
+
# attempt to get via graph api
|
|
190
|
+
try:
|
|
191
|
+
# attempt to get files via graph api
|
|
192
|
+
from .authorization_helper import SharepointAuthorizationHelper
|
|
193
|
+
auth_helper = SharepointAuthorizationHelper(
|
|
194
|
+
client_id=self.client_id,
|
|
195
|
+
client_secret=self.client_secret.get_secret_value(),
|
|
196
|
+
tenant="", # optional for graph api
|
|
197
|
+
scope="", # optional for graph api
|
|
198
|
+
token_json="", # optional for graph api
|
|
199
|
+
)
|
|
200
|
+
file_content = auth_helper.get_file_content(self.site_url, path)
|
|
201
|
+
file_name = path.split('/')[-1]
|
|
202
|
+
except Exception as graph_e:
|
|
203
|
+
logging.error(f"Failed to load file from SharePoint via base api: {e}. Path: {path}. Please, double check file name and path.")
|
|
204
|
+
logging.error(f"Failed to load file from SharePoint via graph api: {graph_e}. Path: {path}. Please, double check file name and path.")
|
|
205
|
+
return ToolException(f"File not found. Please, check file name and path: {e} and {graph_e}")
|
|
206
|
+
#
|
|
207
|
+
return parse_file_content(file_name=file_name,
|
|
149
208
|
file_content=file_content,
|
|
150
209
|
is_capture_image=is_capture_image,
|
|
151
210
|
page_number=page_number,
|
|
@@ -219,12 +278,24 @@ class SharepointApiWrapper(NonCodeIndexerToolkit):
|
|
|
219
278
|
yield document
|
|
220
279
|
|
|
221
280
|
def _load_file_content_in_bytes(self, path):
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
281
|
+
try:
|
|
282
|
+
file = self._client.web.get_file_by_server_relative_path(path)
|
|
283
|
+
self._client.load(file).execute_query()
|
|
284
|
+
file_content = file.read()
|
|
285
|
+
self._client.execute_query()
|
|
286
|
+
#
|
|
287
|
+
return file_content
|
|
288
|
+
except Exception as e:
|
|
289
|
+
# attempt to get via graph api
|
|
290
|
+
from .authorization_helper import SharepointAuthorizationHelper
|
|
291
|
+
auth_helper = SharepointAuthorizationHelper(
|
|
292
|
+
client_id=self.client_id,
|
|
293
|
+
client_secret=self.client_secret.get_secret_value(),
|
|
294
|
+
tenant="", # optional for graph api
|
|
295
|
+
scope="", # optional for graph api
|
|
296
|
+
token_json="", # optional for graph api
|
|
297
|
+
)
|
|
298
|
+
return auth_helper.get_file_content(self.site_url, path)
|
|
228
299
|
|
|
229
300
|
def get_available_tools(self):
|
|
230
301
|
return super().get_available_tools() + [
|