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.

Files changed (51) hide show
  1. alita_sdk/configurations/bitbucket.py +95 -0
  2. alita_sdk/configurations/confluence.py +96 -1
  3. alita_sdk/configurations/gitlab.py +79 -0
  4. alita_sdk/configurations/jira.py +103 -0
  5. alita_sdk/configurations/testrail.py +88 -0
  6. alita_sdk/configurations/xray.py +93 -0
  7. alita_sdk/configurations/zephyr_enterprise.py +93 -0
  8. alita_sdk/configurations/zephyr_essential.py +75 -0
  9. alita_sdk/runtime/clients/client.py +3 -2
  10. alita_sdk/runtime/clients/sandbox_client.py +8 -0
  11. alita_sdk/runtime/langchain/assistant.py +56 -40
  12. alita_sdk/runtime/langchain/constants.py +4 -0
  13. alita_sdk/runtime/langchain/document_loaders/AlitaDocxMammothLoader.py +315 -3
  14. alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +4 -1
  15. alita_sdk/runtime/langchain/document_loaders/constants.py +28 -12
  16. alita_sdk/runtime/langchain/langraph_agent.py +92 -28
  17. alita_sdk/runtime/langchain/utils.py +24 -4
  18. alita_sdk/runtime/toolkits/application.py +8 -1
  19. alita_sdk/runtime/toolkits/tools.py +80 -49
  20. alita_sdk/runtime/tools/__init__.py +7 -2
  21. alita_sdk/runtime/tools/application.py +7 -0
  22. alita_sdk/runtime/tools/function.py +28 -23
  23. alita_sdk/runtime/tools/graph.py +10 -4
  24. alita_sdk/runtime/tools/image_generation.py +104 -8
  25. alita_sdk/runtime/tools/llm.py +146 -114
  26. alita_sdk/runtime/tools/sandbox.py +166 -63
  27. alita_sdk/runtime/tools/vectorstore.py +22 -21
  28. alita_sdk/runtime/tools/vectorstore_base.py +16 -15
  29. alita_sdk/runtime/utils/utils.py +1 -0
  30. alita_sdk/tools/__init__.py +43 -31
  31. alita_sdk/tools/ado/work_item/ado_wrapper.py +17 -8
  32. alita_sdk/tools/base_indexer_toolkit.py +102 -93
  33. alita_sdk/tools/code_indexer_toolkit.py +15 -5
  34. alita_sdk/tools/confluence/api_wrapper.py +30 -8
  35. alita_sdk/tools/confluence/loader.py +10 -0
  36. alita_sdk/tools/elitea_base.py +22 -22
  37. alita_sdk/tools/gitlab/api_wrapper.py +8 -9
  38. alita_sdk/tools/jira/api_wrapper.py +1 -1
  39. alita_sdk/tools/non_code_indexer_toolkit.py +2 -2
  40. alita_sdk/tools/openapi/__init__.py +10 -1
  41. alita_sdk/tools/qtest/api_wrapper.py +298 -51
  42. alita_sdk/tools/sharepoint/api_wrapper.py +104 -33
  43. alita_sdk/tools/sharepoint/authorization_helper.py +175 -1
  44. alita_sdk/tools/sharepoint/utils.py +8 -2
  45. alita_sdk/tools/utils/content_parser.py +27 -16
  46. alita_sdk/tools/vector_adapters/VectorStoreAdapter.py +38 -25
  47. {alita_sdk-0.3.374.dist-info → alita_sdk-0.3.423.dist-info}/METADATA +1 -1
  48. {alita_sdk-0.3.374.dist-info → alita_sdk-0.3.423.dist-info}/RECORD +51 -51
  49. {alita_sdk-0.3.374.dist-info → alita_sdk-0.3.423.dist-info}/WHEEL +0 -0
  50. {alita_sdk-0.3.374.dist-info → alita_sdk-0.3.423.dist-info}/licenses/LICENSE +0 -0
  51. {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 "Steps" is a list of test step objects with fields "Test Step Number", "Test Step Description", "Test Step Expected Result".
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' or 'Automation - UTAF'. Leave blank if unknown.
40
- Status: Default 'New'.
41
- Priority: Leave blank.
42
- Test Type: Default 'Functional'.
43
- Precondition: List prerequisites in one cell, formatted as: <Step1> <Step2> Leave blank if none..
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='before')
139
- @classmethod
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
- url = values['base_url']
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 = url
154
- configuration.api_key['Authorization'] = api_token
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
- cls._client = swagger_client.ApiClient(configuration)
157
- return values
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
- initial_project_properties = self.__get_properties_form_project()
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
- props = []
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
- body.name = test_case.get('Name')
183
- body.precondition = test_case.get('Precondition')
184
- body.description = test_case.get('Description')
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
- test_steps_resources = []
188
- for step in test_case.get('Steps'):
189
- test_steps_resources.append(
190
- swagger_client.TestStepResource(description=step.get('Test Step Description'),
191
- expected=step.get('Test Step Expected Result')))
192
- body.test_steps = test_steps_resources
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 __get_properties_form_project(self) -> list[dict] | None:
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.get().top(limit).execute_query()
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 e:
101
- logging.error(f"Failed to load items from sharepoint: {e}")
102
- return ToolException("Can not list items. Please, double check List name and read permissions.")
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
- target_folder_url = f"Shared Documents/{folder_name}" if folder_name else "Shared Documents"
112
- files = (self._client.web.get_folder_by_server_relative_path(target_folder_url)
113
- .get_files(True)
114
- .execute_query())
115
-
116
- for file in files:
117
- if len(result) >= limit_files:
118
- break
119
- temp_props = {
120
- 'Name': file.properties['Name'],
121
- 'Path': file.properties['ServerRelativeUrl'],
122
- 'Created': file.properties['TimeCreated'],
123
- 'Modified': file.properties['TimeLastModified'],
124
- 'Link': file.properties['LinkingUrl'],
125
- 'id': file.properties['UniqueId']
126
- }
127
- result.append(temp_props)
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
- logging.error(f"Failed to load files from sharepoint: {e}")
131
- return ToolException("Can not get files. Please, double check folder name and read permissions.")
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
- logging.error(f"Failed to load file from SharePoint: {e}. Path: {path}. Please, double check file name and path.")
147
- return ToolException("File not found. Please, check file name and path.")
148
- return parse_file_content(file_name=file.name,
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
- file = self._client.web.get_file_by_server_relative_path(path)
223
- self._client.load(file).execute_query()
224
- file_content = file.read()
225
- self._client.execute_query()
226
- #
227
- return file_content
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() + [