alita-sdk 0.3.462__py3-none-any.whl → 0.3.627__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.
- alita_sdk/cli/agent/__init__.py +5 -0
- alita_sdk/cli/agent/default.py +258 -0
- alita_sdk/cli/agent_executor.py +15 -3
- alita_sdk/cli/agent_loader.py +56 -8
- alita_sdk/cli/agent_ui.py +93 -31
- alita_sdk/cli/agents.py +2274 -230
- alita_sdk/cli/callbacks.py +96 -25
- alita_sdk/cli/cli.py +10 -1
- alita_sdk/cli/config.py +162 -9
- alita_sdk/cli/context/__init__.py +30 -0
- alita_sdk/cli/context/cleanup.py +198 -0
- alita_sdk/cli/context/manager.py +731 -0
- alita_sdk/cli/context/message.py +285 -0
- alita_sdk/cli/context/strategies.py +289 -0
- alita_sdk/cli/context/token_estimation.py +127 -0
- alita_sdk/cli/input_handler.py +419 -0
- alita_sdk/cli/inventory.py +1073 -0
- alita_sdk/cli/testcases/__init__.py +94 -0
- alita_sdk/cli/testcases/data_generation.py +119 -0
- alita_sdk/cli/testcases/discovery.py +96 -0
- alita_sdk/cli/testcases/executor.py +84 -0
- alita_sdk/cli/testcases/logger.py +85 -0
- alita_sdk/cli/testcases/parser.py +172 -0
- alita_sdk/cli/testcases/prompts.py +91 -0
- alita_sdk/cli/testcases/reporting.py +125 -0
- alita_sdk/cli/testcases/setup.py +108 -0
- alita_sdk/cli/testcases/test_runner.py +282 -0
- alita_sdk/cli/testcases/utils.py +39 -0
- alita_sdk/cli/testcases/validation.py +90 -0
- alita_sdk/cli/testcases/workflow.py +196 -0
- alita_sdk/cli/toolkit.py +14 -17
- alita_sdk/cli/toolkit_loader.py +35 -5
- alita_sdk/cli/tools/__init__.py +36 -2
- alita_sdk/cli/tools/approval.py +224 -0
- alita_sdk/cli/tools/filesystem.py +910 -64
- alita_sdk/cli/tools/planning.py +389 -0
- alita_sdk/cli/tools/terminal.py +414 -0
- alita_sdk/community/__init__.py +72 -12
- alita_sdk/community/inventory/__init__.py +236 -0
- alita_sdk/community/inventory/config.py +257 -0
- alita_sdk/community/inventory/enrichment.py +2137 -0
- alita_sdk/community/inventory/extractors.py +1469 -0
- alita_sdk/community/inventory/ingestion.py +3172 -0
- alita_sdk/community/inventory/knowledge_graph.py +1457 -0
- alita_sdk/community/inventory/parsers/__init__.py +218 -0
- alita_sdk/community/inventory/parsers/base.py +295 -0
- alita_sdk/community/inventory/parsers/csharp_parser.py +907 -0
- alita_sdk/community/inventory/parsers/go_parser.py +851 -0
- alita_sdk/community/inventory/parsers/html_parser.py +389 -0
- alita_sdk/community/inventory/parsers/java_parser.py +593 -0
- alita_sdk/community/inventory/parsers/javascript_parser.py +629 -0
- alita_sdk/community/inventory/parsers/kotlin_parser.py +768 -0
- alita_sdk/community/inventory/parsers/markdown_parser.py +362 -0
- alita_sdk/community/inventory/parsers/python_parser.py +604 -0
- alita_sdk/community/inventory/parsers/rust_parser.py +858 -0
- alita_sdk/community/inventory/parsers/swift_parser.py +832 -0
- alita_sdk/community/inventory/parsers/text_parser.py +322 -0
- alita_sdk/community/inventory/parsers/yaml_parser.py +370 -0
- alita_sdk/community/inventory/patterns/__init__.py +61 -0
- alita_sdk/community/inventory/patterns/ast_adapter.py +380 -0
- alita_sdk/community/inventory/patterns/loader.py +348 -0
- alita_sdk/community/inventory/patterns/registry.py +198 -0
- alita_sdk/community/inventory/presets.py +535 -0
- alita_sdk/community/inventory/retrieval.py +1403 -0
- alita_sdk/community/inventory/toolkit.py +173 -0
- alita_sdk/community/inventory/toolkit_utils.py +176 -0
- alita_sdk/community/inventory/visualize.py +1370 -0
- alita_sdk/configurations/__init__.py +1 -1
- alita_sdk/configurations/ado.py +141 -20
- alita_sdk/configurations/bitbucket.py +0 -3
- alita_sdk/configurations/confluence.py +76 -42
- alita_sdk/configurations/figma.py +76 -0
- alita_sdk/configurations/gitlab.py +17 -5
- alita_sdk/configurations/openapi.py +329 -0
- alita_sdk/configurations/qtest.py +72 -1
- alita_sdk/configurations/report_portal.py +96 -0
- alita_sdk/configurations/sharepoint.py +148 -0
- alita_sdk/configurations/testio.py +83 -0
- alita_sdk/runtime/clients/artifact.py +3 -3
- alita_sdk/runtime/clients/client.py +353 -48
- alita_sdk/runtime/clients/sandbox_client.py +0 -21
- alita_sdk/runtime/langchain/_constants_bkup.py +1318 -0
- alita_sdk/runtime/langchain/assistant.py +123 -26
- alita_sdk/runtime/langchain/constants.py +642 -1
- alita_sdk/runtime/langchain/document_loaders/AlitaExcelLoader.py +103 -60
- alita_sdk/runtime/langchain/document_loaders/AlitaJSONLinesLoader.py +77 -0
- alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +6 -3
- alita_sdk/runtime/langchain/document_loaders/AlitaPowerPointLoader.py +226 -7
- alita_sdk/runtime/langchain/document_loaders/AlitaTextLoader.py +5 -2
- alita_sdk/runtime/langchain/document_loaders/constants.py +12 -7
- alita_sdk/runtime/langchain/langraph_agent.py +279 -73
- alita_sdk/runtime/langchain/utils.py +82 -15
- alita_sdk/runtime/llms/preloaded.py +2 -6
- alita_sdk/runtime/skills/__init__.py +91 -0
- alita_sdk/runtime/skills/callbacks.py +498 -0
- alita_sdk/runtime/skills/discovery.py +540 -0
- alita_sdk/runtime/skills/executor.py +610 -0
- alita_sdk/runtime/skills/input_builder.py +371 -0
- alita_sdk/runtime/skills/models.py +330 -0
- alita_sdk/runtime/skills/registry.py +355 -0
- alita_sdk/runtime/skills/skill_runner.py +330 -0
- alita_sdk/runtime/toolkits/__init__.py +7 -0
- alita_sdk/runtime/toolkits/application.py +21 -9
- alita_sdk/runtime/toolkits/artifact.py +15 -5
- alita_sdk/runtime/toolkits/datasource.py +13 -6
- alita_sdk/runtime/toolkits/mcp.py +139 -251
- alita_sdk/runtime/toolkits/mcp_config.py +1048 -0
- alita_sdk/runtime/toolkits/planning.py +178 -0
- alita_sdk/runtime/toolkits/skill_router.py +238 -0
- alita_sdk/runtime/toolkits/subgraph.py +251 -6
- alita_sdk/runtime/toolkits/tools.py +238 -32
- alita_sdk/runtime/toolkits/vectorstore.py +11 -5
- alita_sdk/runtime/tools/__init__.py +3 -1
- alita_sdk/runtime/tools/application.py +20 -6
- alita_sdk/runtime/tools/artifact.py +511 -28
- alita_sdk/runtime/tools/data_analysis.py +183 -0
- alita_sdk/runtime/tools/function.py +43 -15
- alita_sdk/runtime/tools/image_generation.py +50 -44
- alita_sdk/runtime/tools/llm.py +852 -67
- alita_sdk/runtime/tools/loop.py +3 -1
- alita_sdk/runtime/tools/loop_output.py +3 -1
- alita_sdk/runtime/tools/mcp_remote_tool.py +25 -10
- alita_sdk/runtime/tools/mcp_server_tool.py +7 -6
- alita_sdk/runtime/tools/planning/__init__.py +36 -0
- alita_sdk/runtime/tools/planning/models.py +246 -0
- alita_sdk/runtime/tools/planning/wrapper.py +607 -0
- alita_sdk/runtime/tools/router.py +2 -4
- alita_sdk/runtime/tools/sandbox.py +9 -6
- alita_sdk/runtime/tools/skill_router.py +776 -0
- alita_sdk/runtime/tools/tool.py +3 -1
- alita_sdk/runtime/tools/vectorstore.py +7 -2
- alita_sdk/runtime/tools/vectorstore_base.py +51 -11
- alita_sdk/runtime/utils/AlitaCallback.py +137 -21
- alita_sdk/runtime/utils/constants.py +5 -1
- alita_sdk/runtime/utils/mcp_client.py +492 -0
- alita_sdk/runtime/utils/mcp_oauth.py +202 -5
- alita_sdk/runtime/utils/mcp_sse_client.py +36 -7
- alita_sdk/runtime/utils/mcp_tools_discovery.py +124 -0
- alita_sdk/runtime/utils/serialization.py +155 -0
- alita_sdk/runtime/utils/streamlit.py +6 -10
- alita_sdk/runtime/utils/toolkit_utils.py +16 -5
- alita_sdk/runtime/utils/utils.py +36 -0
- alita_sdk/tools/__init__.py +113 -29
- alita_sdk/tools/ado/repos/__init__.py +51 -33
- alita_sdk/tools/ado/repos/repos_wrapper.py +148 -89
- alita_sdk/tools/ado/test_plan/__init__.py +25 -9
- alita_sdk/tools/ado/test_plan/test_plan_wrapper.py +23 -1
- alita_sdk/tools/ado/utils.py +1 -18
- alita_sdk/tools/ado/wiki/__init__.py +25 -8
- alita_sdk/tools/ado/wiki/ado_wrapper.py +291 -22
- alita_sdk/tools/ado/work_item/__init__.py +26 -9
- alita_sdk/tools/ado/work_item/ado_wrapper.py +56 -3
- alita_sdk/tools/advanced_jira_mining/__init__.py +11 -8
- alita_sdk/tools/aws/delta_lake/__init__.py +13 -9
- alita_sdk/tools/aws/delta_lake/tool.py +5 -1
- alita_sdk/tools/azure_ai/search/__init__.py +11 -8
- alita_sdk/tools/azure_ai/search/api_wrapper.py +1 -1
- alita_sdk/tools/base/tool.py +5 -1
- alita_sdk/tools/base_indexer_toolkit.py +170 -45
- alita_sdk/tools/bitbucket/__init__.py +17 -12
- alita_sdk/tools/bitbucket/api_wrapper.py +59 -11
- alita_sdk/tools/bitbucket/cloud_api_wrapper.py +49 -35
- alita_sdk/tools/browser/__init__.py +5 -4
- alita_sdk/tools/carrier/__init__.py +5 -6
- alita_sdk/tools/carrier/backend_reports_tool.py +6 -6
- alita_sdk/tools/carrier/run_ui_test_tool.py +6 -6
- alita_sdk/tools/carrier/ui_reports_tool.py +5 -5
- alita_sdk/tools/chunkers/__init__.py +3 -1
- alita_sdk/tools/chunkers/code/treesitter/treesitter.py +37 -13
- alita_sdk/tools/chunkers/sematic/json_chunker.py +1 -0
- alita_sdk/tools/chunkers/sematic/markdown_chunker.py +97 -6
- alita_sdk/tools/chunkers/universal_chunker.py +270 -0
- alita_sdk/tools/cloud/aws/__init__.py +10 -7
- alita_sdk/tools/cloud/azure/__init__.py +10 -7
- alita_sdk/tools/cloud/gcp/__init__.py +10 -7
- alita_sdk/tools/cloud/k8s/__init__.py +10 -7
- alita_sdk/tools/code/linter/__init__.py +10 -8
- alita_sdk/tools/code/loaders/codesearcher.py +3 -2
- alita_sdk/tools/code/sonar/__init__.py +10 -7
- alita_sdk/tools/code_indexer_toolkit.py +73 -23
- alita_sdk/tools/confluence/__init__.py +21 -15
- alita_sdk/tools/confluence/api_wrapper.py +78 -23
- alita_sdk/tools/confluence/loader.py +4 -2
- alita_sdk/tools/custom_open_api/__init__.py +12 -5
- alita_sdk/tools/elastic/__init__.py +11 -8
- alita_sdk/tools/elitea_base.py +493 -30
- alita_sdk/tools/figma/__init__.py +58 -11
- alita_sdk/tools/figma/api_wrapper.py +1235 -143
- alita_sdk/tools/figma/figma_client.py +73 -0
- alita_sdk/tools/figma/toon_tools.py +2748 -0
- alita_sdk/tools/github/__init__.py +13 -14
- alita_sdk/tools/github/github_client.py +224 -100
- alita_sdk/tools/github/graphql_client_wrapper.py +119 -33
- alita_sdk/tools/github/schemas.py +14 -5
- alita_sdk/tools/github/tool.py +5 -1
- alita_sdk/tools/github/tool_prompts.py +9 -22
- alita_sdk/tools/gitlab/__init__.py +15 -11
- alita_sdk/tools/gitlab/api_wrapper.py +207 -41
- alita_sdk/tools/gitlab_org/__init__.py +10 -8
- alita_sdk/tools/gitlab_org/api_wrapper.py +63 -64
- alita_sdk/tools/google/bigquery/__init__.py +13 -12
- alita_sdk/tools/google/bigquery/tool.py +5 -1
- alita_sdk/tools/google_places/__init__.py +10 -8
- alita_sdk/tools/google_places/api_wrapper.py +1 -1
- alita_sdk/tools/jira/__init__.py +17 -11
- alita_sdk/tools/jira/api_wrapper.py +91 -40
- alita_sdk/tools/keycloak/__init__.py +11 -8
- alita_sdk/tools/localgit/__init__.py +9 -3
- alita_sdk/tools/localgit/local_git.py +62 -54
- alita_sdk/tools/localgit/tool.py +5 -1
- alita_sdk/tools/memory/__init__.py +11 -3
- alita_sdk/tools/non_code_indexer_toolkit.py +1 -0
- alita_sdk/tools/ocr/__init__.py +11 -8
- alita_sdk/tools/openapi/__init__.py +490 -114
- alita_sdk/tools/openapi/api_wrapper.py +1368 -0
- alita_sdk/tools/openapi/tool.py +20 -0
- alita_sdk/tools/pandas/__init__.py +20 -12
- alita_sdk/tools/pandas/api_wrapper.py +38 -25
- alita_sdk/tools/pandas/dataframe/generator/base.py +3 -1
- alita_sdk/tools/postman/__init__.py +11 -11
- alita_sdk/tools/pptx/__init__.py +10 -9
- alita_sdk/tools/pptx/pptx_wrapper.py +1 -1
- alita_sdk/tools/qtest/__init__.py +30 -10
- alita_sdk/tools/qtest/api_wrapper.py +430 -13
- alita_sdk/tools/rally/__init__.py +10 -8
- alita_sdk/tools/rally/api_wrapper.py +1 -1
- alita_sdk/tools/report_portal/__init__.py +12 -9
- alita_sdk/tools/salesforce/__init__.py +10 -9
- alita_sdk/tools/servicenow/__init__.py +17 -14
- alita_sdk/tools/servicenow/api_wrapper.py +1 -1
- alita_sdk/tools/sharepoint/__init__.py +10 -8
- alita_sdk/tools/sharepoint/api_wrapper.py +4 -4
- alita_sdk/tools/slack/__init__.py +10 -8
- alita_sdk/tools/slack/api_wrapper.py +2 -2
- alita_sdk/tools/sql/__init__.py +11 -9
- alita_sdk/tools/testio/__init__.py +10 -8
- alita_sdk/tools/testrail/__init__.py +11 -8
- alita_sdk/tools/testrail/api_wrapper.py +1 -1
- alita_sdk/tools/utils/__init__.py +9 -4
- alita_sdk/tools/utils/content_parser.py +77 -3
- alita_sdk/tools/utils/text_operations.py +410 -0
- alita_sdk/tools/utils/tool_prompts.py +79 -0
- alita_sdk/tools/vector_adapters/VectorStoreAdapter.py +17 -13
- alita_sdk/tools/xray/__init__.py +12 -9
- alita_sdk/tools/yagmail/__init__.py +9 -3
- alita_sdk/tools/zephyr/__init__.py +9 -7
- alita_sdk/tools/zephyr_enterprise/__init__.py +11 -8
- alita_sdk/tools/zephyr_essential/__init__.py +10 -8
- alita_sdk/tools/zephyr_essential/api_wrapper.py +30 -13
- alita_sdk/tools/zephyr_essential/client.py +2 -2
- alita_sdk/tools/zephyr_scale/__init__.py +11 -9
- alita_sdk/tools/zephyr_scale/api_wrapper.py +2 -2
- alita_sdk/tools/zephyr_squad/__init__.py +10 -8
- {alita_sdk-0.3.462.dist-info → alita_sdk-0.3.627.dist-info}/METADATA +147 -7
- alita_sdk-0.3.627.dist-info/RECORD +468 -0
- alita_sdk-0.3.627.dist-info/entry_points.txt +2 -0
- alita_sdk-0.3.462.dist-info/RECORD +0 -384
- alita_sdk-0.3.462.dist-info/entry_points.txt +0 -2
- {alita_sdk-0.3.462.dist-info → alita_sdk-0.3.627.dist-info}/WHEEL +0 -0
- {alita_sdk-0.3.462.dist-info → alita_sdk-0.3.627.dist-info}/licenses/LICENSE +0 -0
- {alita_sdk-0.3.462.dist-info → alita_sdk-0.3.627.dist-info}/top_level.txt +0 -0
|
@@ -3,18 +3,21 @@ import json
|
|
|
3
3
|
import logging
|
|
4
4
|
import re
|
|
5
5
|
from traceback import format_exc
|
|
6
|
-
from typing import Any, Optional
|
|
6
|
+
from typing import Any, Optional, Generator, Literal
|
|
7
7
|
|
|
8
8
|
import requests
|
|
9
9
|
import swagger_client
|
|
10
|
+
from langchain_core.documents import Document
|
|
10
11
|
from langchain_core.tools import ToolException
|
|
11
12
|
from pydantic import Field, PrivateAttr, model_validator, create_model, SecretStr
|
|
12
13
|
from sklearn.feature_extraction.text import strip_tags
|
|
13
14
|
from swagger_client import TestCaseApi, SearchApi, PropertyResource, ModuleApi, ProjectApi, FieldApi
|
|
14
15
|
from swagger_client.rest import ApiException
|
|
15
16
|
|
|
16
|
-
from ..
|
|
17
|
-
from ..utils.
|
|
17
|
+
from ..non_code_indexer_toolkit import NonCodeIndexerToolkit
|
|
18
|
+
from ..utils.available_tools_decorator import extend_with_parent_available_tools
|
|
19
|
+
from ..utils.content_parser import parse_file_content, file_extension_by_chunker
|
|
20
|
+
from ...runtime.utils.utils import IndexerKeywords
|
|
18
21
|
|
|
19
22
|
QTEST_ID = "QTest Id"
|
|
20
23
|
|
|
@@ -253,7 +256,7 @@ NoInput = create_model(
|
|
|
253
256
|
"NoInput"
|
|
254
257
|
)
|
|
255
258
|
|
|
256
|
-
class QtestApiWrapper(
|
|
259
|
+
class QtestApiWrapper(NonCodeIndexerToolkit):
|
|
257
260
|
base_url: str
|
|
258
261
|
qtest_project_id: int
|
|
259
262
|
qtest_api_token: SecretStr
|
|
@@ -263,17 +266,18 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
263
266
|
_client: Any = PrivateAttr()
|
|
264
267
|
_field_definitions_cache: Optional[dict] = PrivateAttr(default=None)
|
|
265
268
|
_modules_cache: Optional[list] = PrivateAttr(default=None)
|
|
266
|
-
|
|
269
|
+
_chunking_tool: Optional[str] = PrivateAttr(default=None)
|
|
270
|
+
_extract_images: bool = PrivateAttr(default=False)
|
|
271
|
+
_image_prompt: Optional[str] = PrivateAttr(default=None)
|
|
267
272
|
|
|
268
273
|
@model_validator(mode='before')
|
|
269
274
|
@classmethod
|
|
270
|
-
def
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
def validate_toolkit(self):
|
|
275
|
+
def validate_toolkit(cls, values):
|
|
276
|
+
# Handle project_id alias
|
|
277
|
+
# There is no such alias and this alias is breaking the scheduled indexing setting to qtest project id the value of the elitea project id.
|
|
278
|
+
# if 'project_id' in values:
|
|
279
|
+
# values['qtest_project_id'] = values.pop('project_id')
|
|
280
|
+
|
|
277
281
|
try:
|
|
278
282
|
import swagger_client # noqa: F401
|
|
279
283
|
except ImportError:
|
|
@@ -282,6 +286,15 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
282
286
|
"`pip install git+https://github.com/Roman-Mitusov/qtest-api.git`"
|
|
283
287
|
)
|
|
284
288
|
|
|
289
|
+
cls.llm = values.get('llm')
|
|
290
|
+
# Call parent validator to set up embeddings and vectorstore params
|
|
291
|
+
return super().validate_toolkit(values)
|
|
292
|
+
|
|
293
|
+
@model_validator(mode='after')
|
|
294
|
+
def setup_qtest_client(self):
|
|
295
|
+
"""Initialize QTest swagger client after model validation."""
|
|
296
|
+
import swagger_client
|
|
297
|
+
|
|
285
298
|
if self.qtest_api_token:
|
|
286
299
|
configuration = swagger_client.Configuration()
|
|
287
300
|
configuration.host = self.base_url
|
|
@@ -938,6 +951,11 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
938
951
|
parsed_data.append(parsed_data_row)
|
|
939
952
|
|
|
940
953
|
def _process_image(self, content: str, extract: bool=False, prompt: str=None):
|
|
954
|
+
"""Extract and process base64 images from HTML img tags.
|
|
955
|
+
|
|
956
|
+
IMPORTANT: This method must be called BEFORE strip_tags() because it needs
|
|
957
|
+
the HTML <img> tags to extract base64-encoded images.
|
|
958
|
+
"""
|
|
941
959
|
#extract image by regex
|
|
942
960
|
img_regex = r'<img\s+src="data:image\/[^;]+;base64,([^"]+)"\s+[^>]*data-filename="([^"]+)"[^>]*>'
|
|
943
961
|
|
|
@@ -957,6 +975,33 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
957
975
|
content = re.sub(img_regex, replace_image, content)
|
|
958
976
|
return content
|
|
959
977
|
|
|
978
|
+
def _clean_html_content(self, content: str, extract_images: bool = False, image_prompt: str = None) -> str:
|
|
979
|
+
"""Clean HTML content with proper order of operations.
|
|
980
|
+
|
|
981
|
+
The correct order is:
|
|
982
|
+
1. Process images first (extracts from <img> tags - needs HTML intact)
|
|
983
|
+
2. Strip remaining HTML tags
|
|
984
|
+
3. Unescape HTML entities
|
|
985
|
+
|
|
986
|
+
Args:
|
|
987
|
+
content: Raw HTML content from QTest
|
|
988
|
+
extract_images: Whether to extract and describe images using LLM
|
|
989
|
+
image_prompt: Custom prompt for image analysis
|
|
990
|
+
|
|
991
|
+
Returns:
|
|
992
|
+
Cleaned text content with optional image descriptions
|
|
993
|
+
"""
|
|
994
|
+
import html
|
|
995
|
+
if not content:
|
|
996
|
+
return ''
|
|
997
|
+
# Step 1: Process images FIRST (needs HTML <img> tags intact)
|
|
998
|
+
content = self._process_image(content, extract_images, image_prompt)
|
|
999
|
+
# Step 2: Strip remaining HTML tags
|
|
1000
|
+
content = strip_tags(content)
|
|
1001
|
+
# Step 3: Unescape HTML entities
|
|
1002
|
+
content = html.unescape(content)
|
|
1003
|
+
return content
|
|
1004
|
+
|
|
960
1005
|
def __perform_search_by_dql(self, dql: str, extract_images:bool=False, prompt: str=None) -> list:
|
|
961
1006
|
search_instance: SearchApi = swagger_client.SearchApi(self._client)
|
|
962
1007
|
body = swagger_client.ArtifactSearchParams(object_type='test-cases', fields=['*'],
|
|
@@ -1891,6 +1936,7 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
1891
1936
|
kwargs["search"] = search
|
|
1892
1937
|
return module_api.get_sub_modules_of(project_id=self.qtest_project_id, **kwargs)
|
|
1893
1938
|
|
|
1939
|
+
@extend_with_parent_available_tools
|
|
1894
1940
|
def get_available_tools(self):
|
|
1895
1941
|
return [
|
|
1896
1942
|
{
|
|
@@ -2141,4 +2187,375 @@ Examples:
|
|
|
2141
2187
|
"args_schema": FindEntityById,
|
|
2142
2188
|
"ref": self.find_entity_by_id,
|
|
2143
2189
|
}
|
|
2144
|
-
]
|
|
2190
|
+
]
|
|
2191
|
+
|
|
2192
|
+
# ==================== INDEXER METHODS ====================
|
|
2193
|
+
|
|
2194
|
+
def _index_tool_params(self, **kwargs) -> dict[str, tuple[type, Field]]:
|
|
2195
|
+
"""
|
|
2196
|
+
Returns a list of fields for index_data args schema.
|
|
2197
|
+
Defines three indexing modes: DQL query, module-based, and full project traversal.
|
|
2198
|
+
"""
|
|
2199
|
+
return {
|
|
2200
|
+
"chunking_tool": (Literal['markdown', ''], Field(
|
|
2201
|
+
description="Name of chunking tool for test case content",
|
|
2202
|
+
default='markdown')),
|
|
2203
|
+
"indexing_mode": (Literal['dql', 'module', 'full'], Field(
|
|
2204
|
+
description="Indexing mode: 'dql' - use DQL query (may have API limitations), "
|
|
2205
|
+
"'module' - index specific module/folder (most deterministic), "
|
|
2206
|
+
"'full' - traverse entire project with pagination",
|
|
2207
|
+
default='full')),
|
|
2208
|
+
"dql": (Optional[str], Field(
|
|
2209
|
+
description="DQL query for 'dql' mode. Example: \"Status = 'New' and Priority = 'High'\". "
|
|
2210
|
+
"Can also filter by module: \"Module in 'MD-7 Master Test Suite'\". "
|
|
2211
|
+
"Note: DQL via API may return incomplete results for complex queries.",
|
|
2212
|
+
default=None,
|
|
2213
|
+
json_schema_extra={'visible_when': {'field': 'indexing_mode', 'value': 'dql'}})),
|
|
2214
|
+
"module_name": (Optional[str], Field(
|
|
2215
|
+
description="Module/folder name for 'module' mode. Use the visible name from UI "
|
|
2216
|
+
"e.g., 'MD-7 Master Test Suite'. Most deterministic way to index a specific folder.",
|
|
2217
|
+
default=None,
|
|
2218
|
+
json_schema_extra={'visible_when': {'field': 'indexing_mode', 'value': 'module'}})),
|
|
2219
|
+
"extract_images": (Optional[bool], Field(
|
|
2220
|
+
description="Whether to extract and process images from test steps using LLM",
|
|
2221
|
+
default=False)),
|
|
2222
|
+
"image_prompt": (Optional[str], Field(
|
|
2223
|
+
description="Custom prompt for image analysis (only used if extract_images=True)",
|
|
2224
|
+
default="Analyze this image from a test case step. Describe what the image shows, including any UI elements, text, buttons, or visual indicators. Focus on elements relevant to testing.",
|
|
2225
|
+
json_schema_extra={'visible_when': {'field': 'extract_images', 'value': True}})),
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
def _base_loader(self, **kwargs) -> Generator[Document, None, None]:
|
|
2229
|
+
"""
|
|
2230
|
+
Base loader for QTest test cases. Supports three indexing modes:
|
|
2231
|
+
- dql: Use DQL query (may have API limitations for complex queries)
|
|
2232
|
+
- module: Index specific module/folder by name (most deterministic)
|
|
2233
|
+
- full: Full project traversal with pagination
|
|
2234
|
+
"""
|
|
2235
|
+
self._chunking_tool = kwargs.get('chunking_tool', 'markdown')
|
|
2236
|
+
self._extract_images = kwargs.get('extract_images', False)
|
|
2237
|
+
self._image_prompt = kwargs.get('image_prompt', None)
|
|
2238
|
+
|
|
2239
|
+
indexing_mode = kwargs.get('indexing_mode', 'full')
|
|
2240
|
+
dql = kwargs.get('dql')
|
|
2241
|
+
module_name = kwargs.get('module_name')
|
|
2242
|
+
|
|
2243
|
+
logger.info(f"Starting QTest indexing in '{indexing_mode}' mode for project {self.qtest_project_id}")
|
|
2244
|
+
|
|
2245
|
+
if indexing_mode == 'dql':
|
|
2246
|
+
if not dql:
|
|
2247
|
+
raise ToolException("DQL query is required for 'dql' indexing mode")
|
|
2248
|
+
yield from self._load_test_cases_by_dql(dql)
|
|
2249
|
+
elif indexing_mode == 'module':
|
|
2250
|
+
if not module_name:
|
|
2251
|
+
raise ToolException("module_name is required for 'module' indexing mode")
|
|
2252
|
+
# Resolve module name to internal ID
|
|
2253
|
+
module_id = self._resolve_module_name_to_id(module_name)
|
|
2254
|
+
if not module_id:
|
|
2255
|
+
raise ToolException(
|
|
2256
|
+
f"Module '{module_name}' not found in project {self.qtest_project_id}. "
|
|
2257
|
+
f"Use get_modules tool to see available modules."
|
|
2258
|
+
)
|
|
2259
|
+
yield from self._load_test_cases_by_module(module_id)
|
|
2260
|
+
else: # full mode
|
|
2261
|
+
yield from self._load_test_cases_full_project()
|
|
2262
|
+
|
|
2263
|
+
def _resolve_module_name_to_id(self, module_name: str) -> Optional[int]:
|
|
2264
|
+
"""
|
|
2265
|
+
Resolve a module name (e.g., 'MD-7 Master Test Suite') to its internal ID.
|
|
2266
|
+
Uses the same approach as __build_body_for_create_test_case.
|
|
2267
|
+
"""
|
|
2268
|
+
modules = self._parse_modules()
|
|
2269
|
+
for module in modules:
|
|
2270
|
+
if module.get('full_module_name') == module_name:
|
|
2271
|
+
return module.get('module_id')
|
|
2272
|
+
return None
|
|
2273
|
+
|
|
2274
|
+
def _load_test_cases_by_dql(self, dql: str) -> Generator[Document, None, None]:
|
|
2275
|
+
"""Load test cases using DQL query."""
|
|
2276
|
+
logger.info(f"Loading test cases by DQL: {dql}")
|
|
2277
|
+
search_instance: SearchApi = swagger_client.SearchApi(self._client)
|
|
2278
|
+
body = swagger_client.ArtifactSearchParams(
|
|
2279
|
+
object_type='test-cases',
|
|
2280
|
+
fields=['*'],
|
|
2281
|
+
query=dql
|
|
2282
|
+
)
|
|
2283
|
+
|
|
2284
|
+
page = 1
|
|
2285
|
+
while True:
|
|
2286
|
+
try:
|
|
2287
|
+
response = search_instance.search_artifact(
|
|
2288
|
+
self.qtest_project_id,
|
|
2289
|
+
body,
|
|
2290
|
+
append_test_steps='true',
|
|
2291
|
+
include_external_properties='true',
|
|
2292
|
+
page_size=self.no_of_items_per_page,
|
|
2293
|
+
page=page
|
|
2294
|
+
)
|
|
2295
|
+
|
|
2296
|
+
items = response.get('items', [])
|
|
2297
|
+
if not items:
|
|
2298
|
+
break
|
|
2299
|
+
|
|
2300
|
+
for item in items:
|
|
2301
|
+
yield self._create_test_case_document(item)
|
|
2302
|
+
|
|
2303
|
+
# Check for next page
|
|
2304
|
+
links = response.get('links', [])
|
|
2305
|
+
has_next = any(link.get('rel') == 'next' for link in links)
|
|
2306
|
+
if not has_next:
|
|
2307
|
+
break
|
|
2308
|
+
page += 1
|
|
2309
|
+
|
|
2310
|
+
except ApiException as e:
|
|
2311
|
+
stacktrace = format_exc()
|
|
2312
|
+
logger.error(f"Error loading test cases by DQL: {stacktrace}")
|
|
2313
|
+
raise ToolException(f"Failed to load test cases by DQL: {stacktrace}") from e
|
|
2314
|
+
|
|
2315
|
+
def _load_test_cases_by_module(self, module_id: int) -> Generator[Document, None, None]:
|
|
2316
|
+
"""Load test cases from a specific module/folder."""
|
|
2317
|
+
logger.info(f"Loading test cases from module {module_id}")
|
|
2318
|
+
test_case_api: TestCaseApi = self.__instantiate_test_api_instance()
|
|
2319
|
+
|
|
2320
|
+
page = 1
|
|
2321
|
+
while True:
|
|
2322
|
+
try:
|
|
2323
|
+
response = test_case_api.get_test_cases(
|
|
2324
|
+
self.qtest_project_id,
|
|
2325
|
+
parent_id=module_id,
|
|
2326
|
+
page=page,
|
|
2327
|
+
size=self.no_of_items_per_page,
|
|
2328
|
+
expand_steps='true'
|
|
2329
|
+
)
|
|
2330
|
+
|
|
2331
|
+
if not response:
|
|
2332
|
+
break
|
|
2333
|
+
|
|
2334
|
+
# Convert response objects to dicts if needed
|
|
2335
|
+
items = [item.to_dict() if hasattr(item, 'to_dict') else item for item in response]
|
|
2336
|
+
|
|
2337
|
+
if not items:
|
|
2338
|
+
break
|
|
2339
|
+
|
|
2340
|
+
for item in items:
|
|
2341
|
+
yield self._create_test_case_document(item)
|
|
2342
|
+
|
|
2343
|
+
if len(items) < self.no_of_items_per_page:
|
|
2344
|
+
break
|
|
2345
|
+
page += 1
|
|
2346
|
+
|
|
2347
|
+
except ApiException as e:
|
|
2348
|
+
stacktrace = format_exc()
|
|
2349
|
+
logger.error(f"Error loading test cases from module: {stacktrace}")
|
|
2350
|
+
raise ToolException(f"Failed to load test cases from module {module_id}: {stacktrace}") from e
|
|
2351
|
+
|
|
2352
|
+
def _load_test_cases_full_project(self) -> Generator[Document, None, None]:
|
|
2353
|
+
"""Load all test cases from the project using pagination."""
|
|
2354
|
+
logger.info(f"Loading all test cases from project {self.qtest_project_id}")
|
|
2355
|
+
test_case_api: TestCaseApi = self.__instantiate_test_api_instance()
|
|
2356
|
+
|
|
2357
|
+
page = 1
|
|
2358
|
+
while True:
|
|
2359
|
+
try:
|
|
2360
|
+
response = test_case_api.get_test_cases(
|
|
2361
|
+
self.qtest_project_id,
|
|
2362
|
+
page=page,
|
|
2363
|
+
size=self.no_of_items_per_page,
|
|
2364
|
+
expand_steps='true'
|
|
2365
|
+
)
|
|
2366
|
+
|
|
2367
|
+
if not response:
|
|
2368
|
+
break
|
|
2369
|
+
|
|
2370
|
+
# Convert response objects to dicts if needed
|
|
2371
|
+
items = [item.to_dict() if hasattr(item, 'to_dict') else item for item in response]
|
|
2372
|
+
|
|
2373
|
+
if not items:
|
|
2374
|
+
break
|
|
2375
|
+
|
|
2376
|
+
for item in items:
|
|
2377
|
+
yield self._create_test_case_document(item)
|
|
2378
|
+
|
|
2379
|
+
if len(items) < self.no_of_items_per_page:
|
|
2380
|
+
break
|
|
2381
|
+
page += 1
|
|
2382
|
+
|
|
2383
|
+
except ApiException as e:
|
|
2384
|
+
stacktrace = format_exc()
|
|
2385
|
+
logger.error(f"Error loading test cases: {stacktrace}")
|
|
2386
|
+
raise ToolException(f"Failed to load test cases from project: {stacktrace}") from e
|
|
2387
|
+
|
|
2388
|
+
def _create_test_case_document(self, item: dict) -> Document:
|
|
2389
|
+
"""Create a Document from a test case item with basic metadata for duplicate detection."""
|
|
2390
|
+
|
|
2391
|
+
# Extract basic identifiers
|
|
2392
|
+
test_case_id = item.get('pid', '')
|
|
2393
|
+
qtest_id = item.get('id', '')
|
|
2394
|
+
|
|
2395
|
+
# Get updated timestamp for duplicate detection
|
|
2396
|
+
# Try different timestamp fields
|
|
2397
|
+
updated_on = (
|
|
2398
|
+
item.get('last_modified_date') or
|
|
2399
|
+
item.get('updated_date') or
|
|
2400
|
+
item.get('created_date') or
|
|
2401
|
+
''
|
|
2402
|
+
)
|
|
2403
|
+
|
|
2404
|
+
# Get module/folder info
|
|
2405
|
+
parent_id = item.get('parent_id')
|
|
2406
|
+
module_name = self._get_module_name(parent_id) if parent_id else ''
|
|
2407
|
+
|
|
2408
|
+
# Build basic metadata for the document
|
|
2409
|
+
metadata = {
|
|
2410
|
+
'id': test_case_id,
|
|
2411
|
+
'qtest_id': qtest_id,
|
|
2412
|
+
'updated_on': updated_on,
|
|
2413
|
+
'name': item.get('name', ''),
|
|
2414
|
+
'parent_id': parent_id,
|
|
2415
|
+
'module_name': module_name,
|
|
2416
|
+
'project_id': self.qtest_project_id,
|
|
2417
|
+
'type': 'test_case',
|
|
2418
|
+
# Store full item for later processing in _extend_data
|
|
2419
|
+
'_raw_item': item,
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
return Document(page_content="", metadata=metadata)
|
|
2423
|
+
|
|
2424
|
+
def _get_module_name(self, module_id: int) -> str:
|
|
2425
|
+
"""Get module name by ID from cached modules."""
|
|
2426
|
+
if self._modules_cache is None:
|
|
2427
|
+
self._parse_modules()
|
|
2428
|
+
|
|
2429
|
+
for module in self._modules_cache or []:
|
|
2430
|
+
if module.get('module_id') == module_id:
|
|
2431
|
+
return module.get('full_module_name', module.get('module_name', ''))
|
|
2432
|
+
return ''
|
|
2433
|
+
|
|
2434
|
+
def _extend_data(self, documents: Generator[Document, None, None]) -> Generator[Document, None, None]:
|
|
2435
|
+
"""
|
|
2436
|
+
Extend base documents with full content formatted as markdown.
|
|
2437
|
+
This is called after duplicate detection, so we only process documents that need indexing.
|
|
2438
|
+
"""
|
|
2439
|
+
|
|
2440
|
+
for document in documents:
|
|
2441
|
+
try:
|
|
2442
|
+
raw_item = document.metadata.pop('_raw_item', None)
|
|
2443
|
+
if not raw_item:
|
|
2444
|
+
yield document
|
|
2445
|
+
continue
|
|
2446
|
+
|
|
2447
|
+
# Build markdown content for the test case
|
|
2448
|
+
content = self._format_test_case_as_markdown(raw_item)
|
|
2449
|
+
|
|
2450
|
+
# Store content for chunking
|
|
2451
|
+
document.metadata[IndexerKeywords.CONTENT_IN_BYTES.value] = content.encode('utf-8')
|
|
2452
|
+
document.metadata[IndexerKeywords.CONTENT_FILE_NAME.value] = f"test_case{file_extension_by_chunker(self._chunking_tool)}"
|
|
2453
|
+
|
|
2454
|
+
# Add additional metadata from properties
|
|
2455
|
+
for prop in raw_item.get('properties', []):
|
|
2456
|
+
field_name = prop.get('field_name')
|
|
2457
|
+
if field_name and field_name not in document.metadata:
|
|
2458
|
+
document.metadata[field_name.lower().replace(' ', '_')] = self.__format_property_value(prop)
|
|
2459
|
+
|
|
2460
|
+
except Exception as e:
|
|
2461
|
+
logger.error(f"Failed to extend document {document.metadata.get('id')}: {e}")
|
|
2462
|
+
|
|
2463
|
+
yield document
|
|
2464
|
+
|
|
2465
|
+
def _format_test_case_as_markdown(self, item: dict) -> str:
|
|
2466
|
+
"""Format a test case as markdown for better semantic search."""
|
|
2467
|
+
|
|
2468
|
+
lines = []
|
|
2469
|
+
|
|
2470
|
+
# Header
|
|
2471
|
+
test_id = item.get('pid', 'Unknown')
|
|
2472
|
+
name = item.get('name', 'Untitled')
|
|
2473
|
+
lines.append(f"# Test Case: {test_id} - {name}")
|
|
2474
|
+
lines.append("")
|
|
2475
|
+
|
|
2476
|
+
# Module/Folder
|
|
2477
|
+
parent_id = item.get('parent_id')
|
|
2478
|
+
if parent_id:
|
|
2479
|
+
module_name = self._get_module_name(parent_id)
|
|
2480
|
+
if module_name:
|
|
2481
|
+
lines.append(f"## Module")
|
|
2482
|
+
lines.append(module_name)
|
|
2483
|
+
lines.append("")
|
|
2484
|
+
|
|
2485
|
+
# Description
|
|
2486
|
+
description = item.get('description', '')
|
|
2487
|
+
if description:
|
|
2488
|
+
description = self._clean_html_content(
|
|
2489
|
+
description,
|
|
2490
|
+
self._extract_images,
|
|
2491
|
+
self._image_prompt
|
|
2492
|
+
)
|
|
2493
|
+
lines.append("## Description")
|
|
2494
|
+
lines.append(description)
|
|
2495
|
+
lines.append("")
|
|
2496
|
+
|
|
2497
|
+
# Precondition
|
|
2498
|
+
precondition = item.get('precondition', '')
|
|
2499
|
+
if precondition:
|
|
2500
|
+
precondition = self._clean_html_content(
|
|
2501
|
+
precondition,
|
|
2502
|
+
self._extract_images,
|
|
2503
|
+
self._image_prompt
|
|
2504
|
+
)
|
|
2505
|
+
lines.append("## Precondition")
|
|
2506
|
+
lines.append(precondition)
|
|
2507
|
+
lines.append("")
|
|
2508
|
+
|
|
2509
|
+
# Properties (Status, Type, Priority, etc.)
|
|
2510
|
+
properties = item.get('properties', [])
|
|
2511
|
+
if properties:
|
|
2512
|
+
lines.append("## Properties")
|
|
2513
|
+
for prop in properties:
|
|
2514
|
+
field_name = prop.get('field_name', '')
|
|
2515
|
+
field_value = self.__format_property_value(prop)
|
|
2516
|
+
if field_name and field_value:
|
|
2517
|
+
if isinstance(field_value, list):
|
|
2518
|
+
field_value = ', '.join(str(v) for v in field_value)
|
|
2519
|
+
lines.append(f"- **{field_name}**: {field_value}")
|
|
2520
|
+
lines.append("")
|
|
2521
|
+
|
|
2522
|
+
# Test Steps
|
|
2523
|
+
test_steps = item.get('test_steps', [])
|
|
2524
|
+
if test_steps:
|
|
2525
|
+
lines.append("## Test Steps")
|
|
2526
|
+
lines.append("")
|
|
2527
|
+
|
|
2528
|
+
for idx, step in enumerate(test_steps, 1):
|
|
2529
|
+
step_desc = step.get('description', '')
|
|
2530
|
+
step_expected = step.get('expected', '')
|
|
2531
|
+
|
|
2532
|
+
# Clean HTML content (processes images first, then strips tags)
|
|
2533
|
+
step_desc = self._clean_html_content(
|
|
2534
|
+
step_desc,
|
|
2535
|
+
self._extract_images,
|
|
2536
|
+
self._image_prompt
|
|
2537
|
+
)
|
|
2538
|
+
step_expected = self._clean_html_content(
|
|
2539
|
+
step_expected,
|
|
2540
|
+
self._extract_images,
|
|
2541
|
+
self._image_prompt
|
|
2542
|
+
)
|
|
2543
|
+
|
|
2544
|
+
lines.append(f"### Step {idx}")
|
|
2545
|
+
if step_desc:
|
|
2546
|
+
lines.append(f"**Action:** {step_desc}")
|
|
2547
|
+
if step_expected:
|
|
2548
|
+
lines.append(f"**Expected Result:** {step_expected}")
|
|
2549
|
+
lines.append("")
|
|
2550
|
+
|
|
2551
|
+
return '\n'.join(lines)
|
|
2552
|
+
|
|
2553
|
+
def _process_document(self, base_document: Document) -> Generator[Document, None, None]:
|
|
2554
|
+
"""
|
|
2555
|
+
Process a base document to extract dependent documents (images).
|
|
2556
|
+
Currently yields nothing as image content is inline in the markdown.
|
|
2557
|
+
Can be extended to yield separate image documents if needed.
|
|
2558
|
+
"""
|
|
2559
|
+
# For now, images are processed inline in the markdown content.
|
|
2560
|
+
# If separate image documents are needed in the future, they can be yielded here.
|
|
2561
|
+
yield from ()
|
|
@@ -5,8 +5,9 @@ from .api_wrapper import RallyApiWrapper
|
|
|
5
5
|
from langchain_core.tools import BaseTool
|
|
6
6
|
from ..base.tool import BaseAction
|
|
7
7
|
from ..elitea_base import filter_missconfigured_index_tools
|
|
8
|
-
from ..utils import clean_string,
|
|
8
|
+
from ..utils import clean_string, get_max_toolkit_length
|
|
9
9
|
from ...configurations.rally import RallyConfiguration
|
|
10
|
+
from ...runtime.utils.constants import TOOLKIT_NAME_META, TOOL_NAME_META, TOOLKIT_TYPE_META
|
|
10
11
|
|
|
11
12
|
name = "rally"
|
|
12
13
|
|
|
@@ -21,12 +22,10 @@ def get_tools(tool):
|
|
|
21
22
|
|
|
22
23
|
class RallyToolkit(BaseToolkit):
|
|
23
24
|
tools: List[BaseTool] = []
|
|
24
|
-
toolkit_max_length: int = 0
|
|
25
25
|
|
|
26
26
|
@staticmethod
|
|
27
27
|
def toolkit_config_schema() -> BaseModel:
|
|
28
28
|
selected_tools = {x['name']: x['args_schema'].schema() for x in RallyApiWrapper.model_construct().get_available_tools()}
|
|
29
|
-
RallyToolkit.toolkit_max_length = get_max_toolkit_length(selected_tools)
|
|
30
29
|
return create_model(
|
|
31
30
|
name,
|
|
32
31
|
rally_configuration=(RallyConfiguration, Field(description="Rally configuration", json_schema_extra={'configuration_types': ['rally']})),
|
|
@@ -37,7 +36,6 @@ class RallyToolkit(BaseToolkit):
|
|
|
37
36
|
'metadata': {
|
|
38
37
|
"label": "Rally",
|
|
39
38
|
"icon_url": "rally.svg",
|
|
40
|
-
"max_length": RallyToolkit.toolkit_max_length,
|
|
41
39
|
"categories": ["project management"],
|
|
42
40
|
"extra_categories": ["agile management", "test management", "scrum", "kanban"]
|
|
43
41
|
}
|
|
@@ -54,18 +52,22 @@ class RallyToolkit(BaseToolkit):
|
|
|
54
52
|
**kwargs.get('rally_configuration'),
|
|
55
53
|
}
|
|
56
54
|
rally_api_wrapper = RallyApiWrapper(**wrapper_payload)
|
|
57
|
-
prefix = clean_string(toolkit_name, cls.toolkit_max_length) + TOOLKIT_SPLITTER if toolkit_name else ''
|
|
58
55
|
available_tools = rally_api_wrapper.get_available_tools()
|
|
59
56
|
tools = []
|
|
60
57
|
for tool in available_tools:
|
|
61
58
|
if selected_tools:
|
|
62
59
|
if tool["name"] not in selected_tools:
|
|
63
60
|
continue
|
|
61
|
+
description = f"{tool['description']}\nWorkspace: {rally_api_wrapper.workspace}. Project: {rally_api_wrapper.project}"
|
|
62
|
+
if toolkit_name:
|
|
63
|
+
description = f"{description}\nToolkit: {toolkit_name}"
|
|
64
|
+
description = description[:1000]
|
|
64
65
|
tools.append(BaseAction(
|
|
65
66
|
api_wrapper=rally_api_wrapper,
|
|
66
|
-
name=
|
|
67
|
-
description=
|
|
68
|
-
args_schema=tool["args_schema"]
|
|
67
|
+
name=tool["name"],
|
|
68
|
+
description=description,
|
|
69
|
+
args_schema=tool["args_schema"],
|
|
70
|
+
metadata={TOOLKIT_NAME_META: toolkit_name, TOOLKIT_TYPE_META: name, TOOL_NAME_META: tool["name"]} if toolkit_name else {TOOL_NAME_META: tool["name"]}
|
|
69
71
|
))
|
|
70
72
|
return cls(tools=tools)
|
|
71
73
|
|
|
@@ -40,7 +40,7 @@ RallyGetEntities = create_model(
|
|
|
40
40
|
entity_type=(Optional[str], Field(description="Artifact type, e.g. 'HierarchicalRequirement', 'Defect', 'UserStory'", default="UserStory")),
|
|
41
41
|
query=(Optional[str], Field(description="Query for searching Rally stories", default=None)),
|
|
42
42
|
fetch=(Optional[bool], Field(description="Whether to fetch the full details of the stories", default=True)),
|
|
43
|
-
limit=(Optional[int], Field(description="Limit the number of results", default=10))
|
|
43
|
+
limit=(Optional[int], Field(description="Limit the number of results", default=10, gt=0))
|
|
44
44
|
)
|
|
45
45
|
|
|
46
46
|
RallyGetProject = create_model(
|
|
@@ -7,8 +7,9 @@ from pydantic import create_model, BaseModel, ConfigDict, Field
|
|
|
7
7
|
from .api_wrapper import ReportPortalApiWrapper
|
|
8
8
|
from ..base.tool import BaseAction
|
|
9
9
|
from ..elitea_base import filter_missconfigured_index_tools
|
|
10
|
-
from ..utils import clean_string,
|
|
10
|
+
from ..utils import clean_string, get_max_toolkit_length
|
|
11
11
|
from ...configurations.report_portal import ReportPortalConfiguration
|
|
12
|
+
from ...runtime.utils.constants import TOOLKIT_NAME_META, TOOL_NAME_META, TOOLKIT_TYPE_META
|
|
12
13
|
|
|
13
14
|
name = "report_portal"
|
|
14
15
|
|
|
@@ -21,19 +22,16 @@ def get_tools(tool):
|
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
class ReportPortalToolkit(BaseToolkit):
|
|
24
|
-
tools:
|
|
25
|
-
toolkit_max_length: int = 0
|
|
25
|
+
tools: List[BaseTool] = []
|
|
26
26
|
|
|
27
27
|
@staticmethod
|
|
28
28
|
def toolkit_config_schema() -> BaseModel:
|
|
29
29
|
selected_tools = {x['name']: x['args_schema'].schema() for x in ReportPortalApiWrapper.model_construct().get_available_tools()}
|
|
30
|
-
ReportPortalToolkit.toolkit_max_length = get_max_toolkit_length(selected_tools)
|
|
31
30
|
return create_model(
|
|
32
31
|
name,
|
|
33
32
|
report_portal_configuration=(ReportPortalConfiguration, Field(description="Report Portal Configuration", json_schema_extra={'configuration_types': ['report_portal']})),
|
|
34
33
|
selected_tools=(List[Literal[tuple(selected_tools)]], Field(default=[], json_schema_extra={'args_schemas': selected_tools})),
|
|
35
34
|
__config__=ConfigDict(json_schema_extra={'metadata': {"label": "Report Portal", "icon_url": "reportportal-icon.svg",
|
|
36
|
-
"max_length": ReportPortalToolkit.toolkit_max_length,
|
|
37
35
|
"categories": ["testing"],
|
|
38
36
|
"extra_categories": ["test reporting", "test automation"]}})
|
|
39
37
|
)
|
|
@@ -48,17 +46,22 @@ class ReportPortalToolkit(BaseToolkit):
|
|
|
48
46
|
**kwargs.get('report_portal_configuration', {}),
|
|
49
47
|
}
|
|
50
48
|
report_portal_api_wrapper = ReportPortalApiWrapper(**wrapper_payload)
|
|
51
|
-
prefix = clean_string(toolkit_name, cls.toolkit_max_length) + TOOLKIT_SPLITTER if toolkit_name else ''
|
|
52
49
|
available_tools = report_portal_api_wrapper.get_available_tools()
|
|
53
50
|
tools = []
|
|
54
51
|
for tool in available_tools:
|
|
55
52
|
if selected_tools and tool["name"] not in selected_tools:
|
|
56
53
|
continue
|
|
54
|
+
description = tool['description']
|
|
55
|
+
if toolkit_name:
|
|
56
|
+
description = f"Toolkit: {toolkit_name}\n{description}"
|
|
57
|
+
description = f"{description}\nReport portal configuration: 'url - {report_portal_api_wrapper.endpoint}, project - {report_portal_api_wrapper.project}'"
|
|
58
|
+
description = description[:1000]
|
|
57
59
|
tools.append(BaseAction(
|
|
58
60
|
api_wrapper=report_portal_api_wrapper,
|
|
59
|
-
name=
|
|
60
|
-
description=
|
|
61
|
-
args_schema=tool["args_schema"]
|
|
61
|
+
name=tool["name"],
|
|
62
|
+
description=description,
|
|
63
|
+
args_schema=tool["args_schema"],
|
|
64
|
+
metadata={TOOLKIT_NAME_META: toolkit_name, TOOLKIT_TYPE_META: name, TOOL_NAME_META: tool["name"]} if toolkit_name else {TOOL_NAME_META: tool["name"]}
|
|
62
65
|
))
|
|
63
66
|
return cls(tools=tools)
|
|
64
67
|
|