alita-sdk 0.3.486__py3-none-any.whl → 0.3.515__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 (124) hide show
  1. alita_sdk/cli/agent_loader.py +27 -6
  2. alita_sdk/cli/agents.py +10 -1
  3. alita_sdk/cli/inventory.py +12 -195
  4. alita_sdk/cli/tools/filesystem.py +95 -9
  5. alita_sdk/community/inventory/__init__.py +12 -0
  6. alita_sdk/community/inventory/toolkit.py +9 -5
  7. alita_sdk/community/inventory/toolkit_utils.py +176 -0
  8. alita_sdk/configurations/ado.py +144 -0
  9. alita_sdk/configurations/confluence.py +76 -42
  10. alita_sdk/configurations/figma.py +76 -0
  11. alita_sdk/configurations/gitlab.py +2 -0
  12. alita_sdk/configurations/qtest.py +72 -1
  13. alita_sdk/configurations/report_portal.py +96 -0
  14. alita_sdk/configurations/sharepoint.py +148 -0
  15. alita_sdk/configurations/testio.py +83 -0
  16. alita_sdk/runtime/clients/artifact.py +2 -2
  17. alita_sdk/runtime/clients/client.py +64 -40
  18. alita_sdk/runtime/clients/sandbox_client.py +14 -0
  19. alita_sdk/runtime/langchain/assistant.py +48 -2
  20. alita_sdk/runtime/langchain/constants.py +3 -1
  21. alita_sdk/runtime/langchain/document_loaders/AlitaExcelLoader.py +103 -60
  22. alita_sdk/runtime/langchain/document_loaders/AlitaJSONLinesLoader.py +77 -0
  23. alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +2 -1
  24. alita_sdk/runtime/langchain/document_loaders/constants.py +12 -7
  25. alita_sdk/runtime/langchain/langraph_agent.py +10 -10
  26. alita_sdk/runtime/langchain/utils.py +6 -1
  27. alita_sdk/runtime/toolkits/artifact.py +14 -5
  28. alita_sdk/runtime/toolkits/datasource.py +13 -6
  29. alita_sdk/runtime/toolkits/mcp.py +94 -219
  30. alita_sdk/runtime/toolkits/planning.py +13 -6
  31. alita_sdk/runtime/toolkits/tools.py +60 -25
  32. alita_sdk/runtime/toolkits/vectorstore.py +11 -5
  33. alita_sdk/runtime/tools/artifact.py +185 -23
  34. alita_sdk/runtime/tools/function.py +2 -1
  35. alita_sdk/runtime/tools/llm.py +155 -34
  36. alita_sdk/runtime/tools/mcp_remote_tool.py +25 -10
  37. alita_sdk/runtime/tools/mcp_server_tool.py +2 -4
  38. alita_sdk/runtime/tools/vectorstore_base.py +3 -3
  39. alita_sdk/runtime/utils/AlitaCallback.py +136 -21
  40. alita_sdk/runtime/utils/mcp_client.py +492 -0
  41. alita_sdk/runtime/utils/mcp_oauth.py +125 -8
  42. alita_sdk/runtime/utils/mcp_sse_client.py +35 -6
  43. alita_sdk/runtime/utils/mcp_tools_discovery.py +124 -0
  44. alita_sdk/runtime/utils/toolkit_utils.py +7 -13
  45. alita_sdk/runtime/utils/utils.py +2 -0
  46. alita_sdk/tools/__init__.py +15 -0
  47. alita_sdk/tools/ado/repos/__init__.py +10 -12
  48. alita_sdk/tools/ado/test_plan/__init__.py +23 -8
  49. alita_sdk/tools/ado/wiki/__init__.py +24 -8
  50. alita_sdk/tools/ado/wiki/ado_wrapper.py +21 -7
  51. alita_sdk/tools/ado/work_item/__init__.py +24 -8
  52. alita_sdk/tools/advanced_jira_mining/__init__.py +10 -8
  53. alita_sdk/tools/aws/delta_lake/__init__.py +12 -9
  54. alita_sdk/tools/aws/delta_lake/tool.py +5 -1
  55. alita_sdk/tools/azure_ai/search/__init__.py +9 -7
  56. alita_sdk/tools/base/tool.py +5 -1
  57. alita_sdk/tools/base_indexer_toolkit.py +26 -1
  58. alita_sdk/tools/bitbucket/__init__.py +14 -10
  59. alita_sdk/tools/bitbucket/api_wrapper.py +50 -2
  60. alita_sdk/tools/browser/__init__.py +5 -4
  61. alita_sdk/tools/carrier/__init__.py +5 -6
  62. alita_sdk/tools/chunkers/sematic/json_chunker.py +1 -0
  63. alita_sdk/tools/chunkers/sematic/markdown_chunker.py +2 -0
  64. alita_sdk/tools/chunkers/universal_chunker.py +1 -0
  65. alita_sdk/tools/cloud/aws/__init__.py +9 -7
  66. alita_sdk/tools/cloud/azure/__init__.py +9 -7
  67. alita_sdk/tools/cloud/gcp/__init__.py +9 -7
  68. alita_sdk/tools/cloud/k8s/__init__.py +9 -7
  69. alita_sdk/tools/code/linter/__init__.py +9 -8
  70. alita_sdk/tools/code/loaders/codesearcher.py +3 -2
  71. alita_sdk/tools/code/sonar/__init__.py +9 -7
  72. alita_sdk/tools/confluence/__init__.py +15 -10
  73. alita_sdk/tools/confluence/api_wrapper.py +63 -14
  74. alita_sdk/tools/custom_open_api/__init__.py +11 -5
  75. alita_sdk/tools/elastic/__init__.py +10 -8
  76. alita_sdk/tools/elitea_base.py +387 -9
  77. alita_sdk/tools/figma/__init__.py +8 -7
  78. alita_sdk/tools/github/__init__.py +12 -14
  79. alita_sdk/tools/github/github_client.py +68 -2
  80. alita_sdk/tools/github/tool.py +5 -1
  81. alita_sdk/tools/gitlab/__init__.py +14 -11
  82. alita_sdk/tools/gitlab/api_wrapper.py +81 -1
  83. alita_sdk/tools/gitlab_org/__init__.py +9 -8
  84. alita_sdk/tools/google/bigquery/__init__.py +12 -12
  85. alita_sdk/tools/google/bigquery/tool.py +5 -1
  86. alita_sdk/tools/google_places/__init__.py +9 -8
  87. alita_sdk/tools/jira/__init__.py +15 -10
  88. alita_sdk/tools/keycloak/__init__.py +10 -8
  89. alita_sdk/tools/localgit/__init__.py +8 -3
  90. alita_sdk/tools/localgit/local_git.py +62 -54
  91. alita_sdk/tools/localgit/tool.py +5 -1
  92. alita_sdk/tools/memory/__init__.py +11 -3
  93. alita_sdk/tools/ocr/__init__.py +10 -8
  94. alita_sdk/tools/openapi/__init__.py +6 -2
  95. alita_sdk/tools/pandas/__init__.py +9 -7
  96. alita_sdk/tools/postman/__init__.py +10 -11
  97. alita_sdk/tools/pptx/__init__.py +9 -9
  98. alita_sdk/tools/qtest/__init__.py +9 -8
  99. alita_sdk/tools/rally/__init__.py +9 -8
  100. alita_sdk/tools/report_portal/__init__.py +11 -9
  101. alita_sdk/tools/salesforce/__init__.py +9 -9
  102. alita_sdk/tools/servicenow/__init__.py +10 -8
  103. alita_sdk/tools/sharepoint/__init__.py +9 -8
  104. alita_sdk/tools/sharepoint/api_wrapper.py +2 -2
  105. alita_sdk/tools/slack/__init__.py +8 -7
  106. alita_sdk/tools/sql/__init__.py +9 -8
  107. alita_sdk/tools/testio/__init__.py +9 -8
  108. alita_sdk/tools/testrail/__init__.py +10 -8
  109. alita_sdk/tools/utils/__init__.py +9 -4
  110. alita_sdk/tools/utils/text_operations.py +254 -0
  111. alita_sdk/tools/vector_adapters/VectorStoreAdapter.py +16 -18
  112. alita_sdk/tools/xray/__init__.py +10 -8
  113. alita_sdk/tools/yagmail/__init__.py +8 -3
  114. alita_sdk/tools/zephyr/__init__.py +8 -7
  115. alita_sdk/tools/zephyr_enterprise/__init__.py +10 -8
  116. alita_sdk/tools/zephyr_essential/__init__.py +9 -8
  117. alita_sdk/tools/zephyr_scale/__init__.py +9 -8
  118. alita_sdk/tools/zephyr_squad/__init__.py +9 -8
  119. {alita_sdk-0.3.486.dist-info → alita_sdk-0.3.515.dist-info}/METADATA +1 -1
  120. {alita_sdk-0.3.486.dist-info → alita_sdk-0.3.515.dist-info}/RECORD +124 -119
  121. {alita_sdk-0.3.486.dist-info → alita_sdk-0.3.515.dist-info}/WHEEL +0 -0
  122. {alita_sdk-0.3.486.dist-info → alita_sdk-0.3.515.dist-info}/entry_points.txt +0 -0
  123. {alita_sdk-0.3.486.dist-info → alita_sdk-0.3.515.dist-info}/licenses/LICENSE +0 -0
  124. {alita_sdk-0.3.486.dist-info → alita_sdk-0.3.515.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,6 @@
1
+ from urllib.parse import quote, urlparse, urlunparse
2
+
3
+ import requests
1
4
  from pydantic import BaseModel, ConfigDict, Field, SecretStr
2
5
 
3
6
 
@@ -17,3 +20,96 @@ class ReportPortalConfiguration(BaseModel):
17
20
  project: str = Field(description="Report Portal Project Name")
18
21
  endpoint: str = Field(description="Report Portal Endpoint URL")
19
22
  api_key: SecretStr = Field(description="Report Portal API Key")
23
+
24
+ @staticmethod
25
+ def check_connection(settings: dict) -> str | None:
26
+ """Check the connection to ReportPortal.
27
+
28
+ Validates:
29
+ - endpoint URL format and reachability
30
+ - API key (token) via an auth-required endpoint
31
+ - project access (because most ReportPortal APIs are scoped to a project)
32
+
33
+ Returns:
34
+ None if connection successful, error message string otherwise
35
+ """
36
+ endpoint_in = settings.get("endpoint")
37
+ endpoint = endpoint_in.strip() if isinstance(endpoint_in, str) else ""
38
+ if not endpoint:
39
+ return "Endpoint is required"
40
+
41
+ if not endpoint.startswith(("http://", "https://")):
42
+ return "Endpoint must start with http:// or https://"
43
+
44
+ # Normalize: remove query/fragment and trailing slash.
45
+ parsed = urlparse(endpoint)
46
+ endpoint = urlunparse(parsed._replace(query="", fragment="")).rstrip("/")
47
+
48
+ # If user pasted an API URL, normalize back to base endpoint.
49
+ # Common pastes: .../api/v1 or .../api/v1/<project>
50
+ # lowered = endpoint.lower()
51
+ # for suffix in ("/api/v1", "/api"):
52
+ # if lowered.endswith(suffix):
53
+ # endpoint = endpoint[: -len(suffix)].rstrip("/")
54
+ # lowered = endpoint.lower()
55
+ # break
56
+
57
+ project_in = settings.get("project")
58
+ project = project_in.strip() if isinstance(project_in, str) else ""
59
+ if not project:
60
+ return "Project is required"
61
+
62
+ api_key = settings.get("api_key")
63
+ if api_key is None:
64
+ return "API key is required"
65
+ api_key_value = api_key.get_secret_value() if hasattr(api_key, "get_secret_value") else str(api_key)
66
+ if not api_key_value or not api_key_value.strip():
67
+ return "API key cannot be empty"
68
+
69
+ # Auth-required endpoint for verification.
70
+ # /user endpoint validates the token and project context.
71
+ project_encoded = quote(project, safe="")
72
+ test_url = f"{endpoint}/api/v1/project/{project_encoded}"
73
+
74
+ try:
75
+ resp = requests.get(
76
+ test_url,
77
+ headers={"Authorization": f"Bearer {api_key_value}"},
78
+ timeout=10,
79
+ )
80
+
81
+ if resp.status_code == 200:
82
+ return None
83
+ if resp.status_code == 401:
84
+ return "Invalid API key"
85
+ if resp.status_code == 403:
86
+ return "Access forbidden - API key has no access to this project"
87
+ if resp.status_code == 404:
88
+ return "API endpoint not found (404) - verify endpoint URL and project name"
89
+ if resp.status_code == 429:
90
+ return "Rate limited (429) - please try again later"
91
+ if 500 <= resp.status_code <= 599:
92
+ return f"ReportPortal service error (HTTP {resp.status_code})"
93
+ return f"Connection failed (HTTP {resp.status_code})"
94
+
95
+ except requests.exceptions.Timeout:
96
+ return "Connection timeout - ReportPortal did not respond within 10 seconds"
97
+ except requests.exceptions.SSLError as e:
98
+ if "Hostname mismatch" in str(e):
99
+ return "API endpoint not found - verify endpoint URL and project name"
100
+ return "SSL error - certificate verification failed"
101
+ except requests.exceptions.ConnectionError:
102
+ return "Connection error - unable to reach ReportPortal. Check endpoint URL and network."
103
+ except requests.exceptions.RequestException as e:
104
+ return f"Request failed: {str(e)}"
105
+ except Exception:
106
+ return "Unexpected error during ReportPortal connection check"
107
+
108
+ if __name__ == '__main__':
109
+ settings = {
110
+ "endpoint": "https://reportportal.epam.com",
111
+ "project": "epm-alta",
112
+ "api_key": "my-api-key_U1kF0zFvToqcweG3x552cI8pnYkMqszgtht_LHGZhpxwrxDl5nXlmrSf_JLEE8jy",
113
+ }
114
+
115
+ print(ReportPortalConfiguration.check_connection(settings))
@@ -1,3 +1,5 @@
1
+ import requests
2
+ from office365.onedrive.sharepoint_settings import SharepointSettings
1
3
  from pydantic import BaseModel, ConfigDict, Field, SecretStr
2
4
 
3
5
 
@@ -17,3 +19,149 @@ class SharepointConfiguration(BaseModel):
17
19
  client_id: str = Field(description="SharePoint Client ID")
18
20
  client_secret: SecretStr = Field(description="SharePoint Client Secret")
19
21
  site_url: str = Field(description="SharePoint Site URL")
22
+
23
+ @staticmethod
24
+ def check_connection(settings: dict) -> str | None:
25
+ """
26
+ Test the connection to SharePoint API using OAuth2 client credentials.
27
+
28
+ Args:
29
+ settings: Dictionary containing 'client_id', 'client_secret', and 'site_url' (all required)
30
+
31
+ Returns:
32
+ None if connection is successful, error message string otherwise
33
+ """
34
+ # Validate client_id
35
+ client_id = settings.get("client_id")
36
+ if client_id is None or client_id == "":
37
+ if client_id == "":
38
+ return "Client ID cannot be empty"
39
+ return "Client ID is required"
40
+
41
+ if not isinstance(client_id, str):
42
+ return "Client ID must be a string"
43
+
44
+ client_id = client_id.strip()
45
+ if not client_id:
46
+ return "Client ID cannot be empty"
47
+
48
+ # Validate client_secret
49
+ client_secret = settings.get("client_secret")
50
+ if client_secret is None:
51
+ return "Client secret is required"
52
+
53
+ # Extract secret value if it's a SecretStr
54
+ if hasattr(client_secret, "get_secret_value"):
55
+ client_secret = client_secret.get_secret_value()
56
+
57
+ if not client_secret or not client_secret.strip():
58
+ return "Client secret cannot be empty"
59
+
60
+ # Validate site_url
61
+ site_url = settings.get("site_url")
62
+ if site_url is None or site_url == "":
63
+ if site_url == "":
64
+ return "Site URL cannot be empty"
65
+ return "Site URL is required"
66
+
67
+ if not isinstance(site_url, str):
68
+ return "Site URL must be a string"
69
+
70
+ site_url = site_url.strip()
71
+ if not site_url:
72
+ return "Site URL cannot be empty"
73
+
74
+ if not site_url.startswith(("http://", "https://")):
75
+ return "Site URL must start with http:// or https://"
76
+
77
+ # Remove trailing slash for consistency
78
+ site_url = site_url.rstrip("/")
79
+
80
+ # Extract tenant and resource from site URL
81
+ # Expected format: https://<tenant>.sharepoint.com/sites/<site>
82
+ try:
83
+ if ".sharepoint.com" not in site_url:
84
+ return "Site URL must be a valid SharePoint URL (*.sharepoint.com)"
85
+
86
+ # Extract tenant (e.g., "contoso" from "contoso.sharepoint.com")
87
+ parts = site_url.split("//")[1].split(".")
88
+ if len(parts) < 3:
89
+ return "Invalid SharePoint URL format"
90
+ tenant = parts[0]
91
+
92
+ # Build token endpoint
93
+ token_url = f"https://accounts.accesscontrol.windows.net/{tenant}.onmicrosoft.com/tokens/OAuth/2"
94
+
95
+ # Build resource (the site URL with /_api appended)
96
+ resource = f"{site_url.split('/sites/')[0]}@{site_url.split('//')[1].split('/')[0].split('.')[0]}"
97
+
98
+ except Exception:
99
+ return "Failed to parse SharePoint URL - ensure it's in format: https://<tenant>.sharepoint.com/sites/<site>"
100
+
101
+ try:
102
+ # Step 1: Get OAuth2 access token using client credentials
103
+ token_response = requests.post(
104
+ token_url,
105
+ data={
106
+ "grant_type": "client_credentials",
107
+ "client_id": f"{client_id}@{tenant}.onmicrosoft.com",
108
+ "client_secret": client_secret,
109
+ "resource": f"00000003-0000-0ff1-ce00-000000000000/{site_url.split('//')[1].split('/')[0]}@{tenant}.onmicrosoft.com"
110
+ },
111
+ timeout=10,
112
+ )
113
+
114
+ if token_response.status_code == 400:
115
+ try:
116
+ error_data = token_response.json()
117
+ error_desc = error_data.get("error_description", "")
118
+ if "not found in the directory" in error_desc.lower():
119
+ return "Invalid client ID. Please check if you provide a correct client ID and try again."
120
+ elif "client_secret" in error_desc.lower():
121
+ return "Invalid client secret"
122
+ else:
123
+ return f"OAuth2 authentication failed: {error_desc}"
124
+ except Exception:
125
+ return "Invalid client credentials"
126
+
127
+ elif token_response.status_code == 401:
128
+ return "Invalid client secret provided. Please check if you provide a correct client secret and try again."
129
+ elif token_response.status_code != 200:
130
+ return f"Failed to obtain access token (status {token_response.status_code})"
131
+
132
+ # Extract access token
133
+ try:
134
+ token_data = token_response.json()
135
+ access_token = token_data.get("access_token")
136
+ if not access_token:
137
+ return "No access token received from SharePoint"
138
+ except Exception:
139
+ return "Failed to parse token response"
140
+
141
+ # Step 2: Test the access token by calling SharePoint API
142
+ api_url = f"{site_url}/_api/web"
143
+ api_response = requests.get(
144
+ api_url,
145
+ headers={"Authorization": f"Bearer {access_token}"},
146
+ timeout=10,
147
+ )
148
+
149
+ if api_response.status_code == 200:
150
+ return None # Connection successful
151
+ elif api_response.status_code == 401:
152
+ return "Access token is invalid or expired"
153
+ elif api_response.status_code == 403:
154
+ return "Access forbidden - client may lack required permissions for this site"
155
+ elif api_response.status_code == 404:
156
+ return f"Site not found or not accessible: {site_url}"
157
+ else:
158
+ return f"SharePoint API request failed with status {api_response.status_code}"
159
+
160
+ except requests.exceptions.Timeout:
161
+ return "Connection timeout - SharePoint is not responding"
162
+ except requests.exceptions.ConnectionError:
163
+ return "Connection error - unable to reach SharePoint"
164
+ except requests.exceptions.RequestException as e:
165
+ return f"Request failed: {str(e)}"
166
+ except Exception as e:
167
+ return f"Unexpected error: {str(e)}"
@@ -1,3 +1,4 @@
1
+ import requests
1
2
  from pydantic import BaseModel, ConfigDict, Field, SecretStr
2
3
 
3
4
 
@@ -16,3 +17,85 @@ class TestIOConfiguration(BaseModel):
16
17
  )
17
18
  endpoint: str = Field(description="TestIO endpoint")
18
19
  api_key: SecretStr = Field(description="TestIO API Key")
20
+
21
+ @staticmethod
22
+ def check_connection(settings: dict) -> str | None:
23
+ """
24
+ Test the connection to TestIO API.
25
+
26
+ Args:
27
+ settings: Dictionary containing 'endpoint' and 'api_key' (both required)
28
+
29
+ Returns:
30
+ None if connection is successful, error message string otherwise
31
+ """
32
+ endpoint = settings.get("endpoint")
33
+ if endpoint is None or endpoint == "":
34
+ if endpoint == "":
35
+ return "Endpoint cannot be empty"
36
+ return "Endpoint is required"
37
+
38
+ # Validate endpoint format
39
+ if not isinstance(endpoint, str):
40
+ return "Endpoint must be a string"
41
+
42
+ endpoint = endpoint.strip()
43
+ if not endpoint:
44
+ return "Endpoint cannot be empty"
45
+ if not endpoint.startswith(("http://", "https://")):
46
+ return "Endpoint must start with http:// or https://"
47
+
48
+ # Remove trailing slash for consistency
49
+ endpoint = endpoint.rstrip("/")
50
+
51
+ api_key = settings.get("api_key")
52
+ if api_key is None:
53
+ return "API key is required"
54
+
55
+ # Extract secret value if it's a SecretStr
56
+ if hasattr(api_key, "get_secret_value"):
57
+ api_key = api_key.get_secret_value()
58
+
59
+ # Validate API key is not empty
60
+ if not api_key or not api_key.strip():
61
+ return "API key cannot be empty"
62
+
63
+ # Verification strategy:
64
+ # Use an auth-required endpoint and a single, explicit auth scheme:
65
+ # Authorization: Token <token>
66
+ url = f"{endpoint}/customer/v2/products"
67
+
68
+ try:
69
+ resp = TestIOConfiguration._get_with_token(url, api_key)
70
+
71
+ if resp.status_code == 200:
72
+ return None # Connection successful
73
+ if resp.status_code == 401:
74
+ return "Invalid token"
75
+ if resp.status_code == 403:
76
+ return "Access forbidden - token has no access to /customer/v2/products"
77
+ if resp.status_code == 404:
78
+ return "Invalid endpoint. Verify TestIO base endpoint."
79
+ if resp.status_code == 429:
80
+ return "Rate limited - please try again later"
81
+ if 500 <= resp.status_code <= 599:
82
+ return f"TestIO service error (HTTP {resp.status_code})"
83
+ return f"Connection failed (HTTP {resp.status_code})"
84
+
85
+ except requests.exceptions.Timeout:
86
+ return "Connection timeout - TestIO did not respond within 10 seconds"
87
+ except requests.exceptions.ConnectionError:
88
+ return "Connection error - unable to reach TestIO. Check endpoint URL and network."
89
+ except requests.exceptions.RequestException as e:
90
+ return f"Request failed: {str(e)}"
91
+ except Exception:
92
+ return "Unexpected error during TestIO connection check"
93
+
94
+ @staticmethod
95
+ def _get_with_token(url: str, token: str) -> requests.Response:
96
+ """Perform an authenticated GET using `Authorization: Token <token>`."""
97
+ return requests.get(
98
+ url,
99
+ headers={"Authorization": f"Token {token}"},
100
+ timeout=10,
101
+ )
@@ -38,8 +38,8 @@ class Artifact:
38
38
  if len(data) == 0:
39
39
  # empty file might be created
40
40
  return ""
41
- if isinstance(data, dict) and data['error']:
42
- return f"{data['error']}. {data['content'] if data['content'] else ''}"
41
+ if isinstance(data, dict) and data.get('error'):
42
+ return f"{data['error']}. {data.get('content', '')}"
43
43
  detected = chardet.detect(data)
44
44
  if detected['encoding'] is not None:
45
45
  try:
@@ -20,8 +20,8 @@ from .prompt import AlitaPrompt
20
20
  from .datasource import AlitaDataSource
21
21
  from .artifact import Artifact
22
22
  from ..langchain.chat_message_template import Jinja2TemplatedChatMessagesTemplate
23
- from ..utils.utils import TOOLKIT_SPLITTER
24
- from ...tools import get_available_toolkit_models
23
+ from ..utils.mcp_oauth import McpAuthorizationRequired
24
+ from ...tools import get_available_toolkit_models, instantiate_toolkit
25
25
  from ...tools.base_indexer_toolkit import IndexTools
26
26
 
27
27
  logger = logging.getLogger(__name__)
@@ -145,6 +145,19 @@ class AlitaClient:
145
145
  data = requests.get(url, headers=self.headers, verify=False).json()
146
146
  return data
147
147
 
148
+ def toolkit(self, toolkit_id: int):
149
+ url = f"{self.base_url}{self.api_path}/tool/prompt_lib/{self.project_id}/{toolkit_id}"
150
+ response = requests.get(url, headers=self.headers, verify=False)
151
+ if not response.ok:
152
+ raise ValueError(f"Failed to fetch toolkit {toolkit_id}: {response.text}")
153
+
154
+ tool_data = response.json()
155
+ if 'settings' not in tool_data:
156
+ tool_data['settings'] = {}
157
+ tool_data['settings']['alita'] = self
158
+
159
+ return instantiate_toolkit(tool_data)
160
+
148
161
  def get_list_of_apps(self):
149
162
  apps = []
150
163
  limit = 10
@@ -347,7 +360,7 @@ class AlitaClient:
347
360
  application_variables: Optional[dict] = None,
348
361
  version_details: Optional[dict] = None, store: Optional[BaseStore] = None,
349
362
  llm: Optional[ChatOpenAI] = None, mcp_tokens: Optional[dict] = None,
350
- conversation_id: Optional[str] = None):
363
+ conversation_id: Optional[str] = None, ignored_mcp_servers: Optional[list] = None):
351
364
  if tools is None:
352
365
  tools = []
353
366
  if chat_history is None:
@@ -396,12 +409,12 @@ class AlitaClient:
396
409
  if runtime == 'nonrunnable':
397
410
  return LangChainAssistant(self, data, llm, chat_history, app_type,
398
411
  tools=tools, memory=memory, store=store, mcp_tokens=mcp_tokens,
399
- conversation_id=conversation_id)
412
+ conversation_id=conversation_id, ignored_mcp_servers=ignored_mcp_servers)
400
413
  if runtime == 'langchain':
401
414
  return LangChainAssistant(self, data, llm,
402
415
  chat_history, app_type,
403
416
  tools=tools, memory=memory, store=store, mcp_tokens=mcp_tokens,
404
- conversation_id=conversation_id).runnable()
417
+ conversation_id=conversation_id, ignored_mcp_servers=ignored_mcp_servers).runnable()
405
418
  elif runtime == 'llama':
406
419
  raise NotImplementedError("LLama runtime is not supported")
407
420
 
@@ -469,11 +482,44 @@ class AlitaClient:
469
482
  return self._process_requst(data)
470
483
 
471
484
  def create_artifact(self, bucket_name, artifact_name, artifact_data):
485
+ # Sanitize filename to prevent regex errors during indexing
486
+ sanitized_name, was_modified = self._sanitize_artifact_name(artifact_name)
487
+ if was_modified:
488
+ logger.warning(f"Artifact filename sanitized: '{artifact_name}' -> '{sanitized_name}'")
489
+
472
490
  url = f'{self.artifacts_url}/{bucket_name.lower()}'
473
491
  data = requests.post(url, headers=self.headers, files={
474
- 'file': (artifact_name, artifact_data)
492
+ 'file': (sanitized_name, artifact_data)
475
493
  }, verify=False)
476
494
  return self._process_requst(data)
495
+
496
+ @staticmethod
497
+ def _sanitize_artifact_name(filename: str) -> tuple:
498
+ """Sanitize filename for safe storage and regex pattern matching."""
499
+ import re
500
+ from pathlib import Path
501
+
502
+ if not filename or not filename.strip():
503
+ return "unnamed_file", True
504
+
505
+ original = filename
506
+ path_obj = Path(filename)
507
+ name = path_obj.stem
508
+ extension = path_obj.suffix
509
+
510
+ # Whitelist: alphanumeric, underscore, hyphen, space, Unicode letters/digits
511
+ sanitized_name = re.sub(r'[^\w\s-]', '', name, flags=re.UNICODE)
512
+ sanitized_name = re.sub(r'[-\s]+', '-', sanitized_name)
513
+ sanitized_name = sanitized_name.strip('-').strip()
514
+
515
+ if not sanitized_name:
516
+ sanitized_name = "file"
517
+
518
+ if extension:
519
+ extension = re.sub(r'[^\w.-]', '', extension, flags=re.UNICODE)
520
+
521
+ sanitized = sanitized_name + extension
522
+ return sanitized, (sanitized != original)
477
523
 
478
524
  def download_artifact(self, bucket_name, artifact_name):
479
525
  url = f'{self.artifact_url}/{bucket_name.lower()}/{artifact_name}'
@@ -622,7 +668,8 @@ class AlitaClient:
622
668
  tools: Optional[list] = None, chat_history: Optional[List[Any]] = None,
623
669
  memory=None, runtime='langchain', variables: Optional[list] = None,
624
670
  store: Optional[BaseStore] = None, debug_mode: Optional[bool] = False,
625
- mcp_tokens: Optional[dict] = None, conversation_id: Optional[str] = None):
671
+ mcp_tokens: Optional[dict] = None, conversation_id: Optional[str] = None,
672
+ ignored_mcp_servers: Optional[list] = None):
626
673
  """
627
674
  Create a predict-type agent with minimal configuration.
628
675
 
@@ -638,6 +685,7 @@ class AlitaClient:
638
685
  variables: Optional list of variables for the agent
639
686
  store: Optional store for memory
640
687
  debug_mode: Enable debug mode for cases when assistant can be initialized without tools
688
+ ignored_mcp_servers: Optional list of MCP server URLs to ignore (user chose to continue without auth)
641
689
 
642
690
  Returns:
643
691
  Runnable agent ready for execution
@@ -671,7 +719,8 @@ class AlitaClient:
671
719
  store=store,
672
720
  debug_mode=debug_mode,
673
721
  mcp_tokens=mcp_tokens,
674
- conversation_id=conversation_id
722
+ conversation_id=conversation_id,
723
+ ignored_mcp_servers=ignored_mcp_servers
675
724
  ).runnable()
676
725
 
677
726
  def test_toolkit_tool(self, toolkit_config: dict, tool_name: str, tool_params: dict = None,
@@ -814,26 +863,12 @@ class AlitaClient:
814
863
 
815
864
  # Instantiate the toolkit with client and LLM support
816
865
  try:
817
- tools = instantiate_toolkit_with_client(toolkit_config, llm, self, mcp_tokens=mcp_tokens)
818
- except Exception as toolkit_error:
866
+ tools = instantiate_toolkit_with_client(toolkit_config, llm, self, mcp_tokens=mcp_tokens, use_prefix=False)
867
+ except McpAuthorizationRequired:
819
868
  # Re-raise McpAuthorizationRequired to allow proper handling upstream
820
- from ..utils.mcp_oauth import McpAuthorizationRequired
821
-
822
- # Check if it's McpAuthorizationRequired directly
823
- if isinstance(toolkit_error, McpAuthorizationRequired):
824
- logger.info(f"McpAuthorizationRequired detected, re-raising")
825
- raise
826
-
827
- # Also check for wrapped exceptions (e.g., from asyncio)
828
- if hasattr(toolkit_error, '__cause__') and isinstance(toolkit_error.__cause__, McpAuthorizationRequired):
829
- logger.info(f"Wrapped McpAuthorizationRequired detected, re-raising cause")
830
- raise toolkit_error.__cause__
831
-
832
- # Check exception class name as fallback (in case of module reload issues)
833
- if toolkit_error.__class__.__name__ == 'McpAuthorizationRequired':
834
- logger.info(f"McpAuthorizationRequired detected by name, re-raising")
835
- raise
836
-
869
+ logger.info(f"McpAuthorizationRequired detected, re-raising")
870
+ raise
871
+ except Exception as toolkit_error:
837
872
  # For other errors, return error response
838
873
  return {
839
874
  "success": False,
@@ -919,7 +954,6 @@ class AlitaClient:
919
954
  if target_tool is None:
920
955
  available_tools = []
921
956
  base_available_tools = []
922
- full_available_tools = []
923
957
 
924
958
  for tool in tools:
925
959
  tool_name_attr = None
@@ -936,10 +970,6 @@ class AlitaClient:
936
970
  if base_name not in base_available_tools:
937
971
  base_available_tools.append(base_name)
938
972
 
939
- # Track full names separately
940
- if TOOLKIT_SPLITTER in tool_name_attr:
941
- full_available_tools.append(tool_name_attr)
942
-
943
973
  # Create comprehensive error message
944
974
  error_msg = f"Tool '{tool_name}' not found in toolkit '{toolkit_config.get('toolkit_name')}'.\n"
945
975
 
@@ -947,9 +977,7 @@ class AlitaClient:
947
977
  if toolkit_name in [tool.value for tool in IndexTools]:
948
978
  error_msg += f" Please make sure proper PGVector configuration and embedding model are set in the platform.\n"
949
979
 
950
- if base_available_tools and full_available_tools:
951
- error_msg += f" Available tools: {base_available_tools} (base names) or {full_available_tools} (full names)"
952
- elif base_available_tools:
980
+ if base_available_tools:
953
981
  error_msg += f" Available tools: {base_available_tools}"
954
982
  elif available_tools:
955
983
  error_msg += f" Available tools: {available_tools}"
@@ -958,10 +986,7 @@ class AlitaClient:
958
986
 
959
987
  # Add helpful hint about naming conventions
960
988
  if '___' in tool_name:
961
- error_msg += f" Note: You provided a full name '{tool_name}'. Try using just the base name '{extract_base_tool_name(tool_name)}'."
962
- elif full_available_tools:
963
- possible_full_name = create_full_tool_name(tool_name, toolkit_name)
964
- error_msg += f" Note: You provided a base name '{tool_name}'. The full name might be '{possible_full_name}'."
989
+ error_msg += f" Note: Tool names no longer use '___' prefixes. Try using just the base name '{extract_base_tool_name(tool_name)}'."
965
990
 
966
991
  return {
967
992
  "success": False,
@@ -1068,7 +1093,6 @@ class AlitaClient:
1068
1093
 
1069
1094
  except Exception as e:
1070
1095
  # Re-raise McpAuthorizationRequired to allow proper handling upstream
1071
- from ..utils.mcp_oauth import McpAuthorizationRequired
1072
1096
  if isinstance(e, McpAuthorizationRequired):
1073
1097
  raise
1074
1098
  logger = logging.getLogger(__name__)
@@ -6,6 +6,7 @@ import requests
6
6
  from typing import Any
7
7
  from json import dumps
8
8
  import chardet
9
+ from ...tools import instantiate_toolkit
9
10
 
10
11
  logger = logging.getLogger(__name__)
11
12
 
@@ -184,6 +185,19 @@ class SandboxClient:
184
185
  data = requests.get(url, headers=self.headers, verify=False).json()
185
186
  return data
186
187
 
188
+ def toolkit(self, toolkit_id: int):
189
+ url = f"{self.base_url}{self.api_path}/tool/prompt_lib/{self.project_id}/{toolkit_id}"
190
+ response = requests.get(url, headers=self.headers, verify=False)
191
+ if not response.ok:
192
+ raise ValueError(f"Failed to fetch toolkit {toolkit_id}: {response.text}")
193
+
194
+ tool_data = response.json()
195
+ if 'settings' not in tool_data:
196
+ tool_data['settings'] = {}
197
+ tool_data['settings']['alita'] = self
198
+
199
+ return instantiate_toolkit(tool_data)
200
+
187
201
  def get_list_of_apps(self):
188
202
  apps = []
189
203
  limit = 10
@@ -33,7 +33,8 @@ class Assistant:
33
33
  store: Optional[BaseStore] = None,
34
34
  debug_mode: Optional[bool] = False,
35
35
  mcp_tokens: Optional[dict] = None,
36
- conversation_id: Optional[str] = None):
36
+ conversation_id: Optional[str] = None,
37
+ ignored_mcp_servers: Optional[list] = None):
37
38
 
38
39
  self.app_type = app_type
39
40
  self.memory = memory
@@ -98,10 +99,55 @@ class Assistant:
98
99
  memory_store=self.store,
99
100
  debug_mode=debug_mode,
100
101
  mcp_tokens=mcp_tokens,
101
- conversation_id=conversation_id
102
+ conversation_id=conversation_id,
103
+ ignored_mcp_servers=ignored_mcp_servers
102
104
  )
103
105
  if tools:
104
106
  self.tools += tools
107
+
108
+ # Create ToolRegistry to track tool metadata and handle name collisions
109
+ self.tool_registry = {}
110
+ tool_name_counts = {} # Track how many times each base name appears
111
+
112
+ for tool in self.tools:
113
+ if hasattr(tool, 'name'):
114
+ original_name = tool.name
115
+ base_name = original_name
116
+
117
+ # Extract toolkit metadata from tool configuration
118
+ toolkit_name = ""
119
+ toolkit_type = ""
120
+
121
+ # Find matching tool config to extract metadata
122
+ for tool_config in version_tools:
123
+ # Try to match by toolkit_name or name field
124
+ config_toolkit_name = tool_config.get('toolkit_name', tool_config.get('name', ''))
125
+ # Simple heuristic: toolkit info should be accessible from tool config
126
+ # For now, use toolkit_name and type from config
127
+ toolkit_name = config_toolkit_name
128
+ toolkit_type = tool_config.get('type', '')
129
+ break # Use first match for now; will refine with better matching
130
+
131
+ # Handle duplicate tool names by appending numeric suffix
132
+ if base_name in tool_name_counts:
133
+ tool_name_counts[base_name] += 1
134
+ # Append suffix to make unique
135
+ new_name = f"{base_name}_{tool_name_counts[base_name]}"
136
+ tool.name = new_name
137
+ logger.info(f"Tool name collision detected: '{base_name}' -> '{new_name}'")
138
+ else:
139
+ tool_name_counts[base_name] = 0
140
+ new_name = base_name
141
+
142
+ # Store in registry
143
+ self.tool_registry[tool.name] = {
144
+ 'toolkit_name': toolkit_name,
145
+ 'toolkit_type': toolkit_type,
146
+ 'original_tool_name': base_name
147
+ }
148
+
149
+ logger.info(f"ToolRegistry initialized with {len(self.tool_registry)} tools")
150
+
105
151
  # Handle prompt setup
106
152
  if app_type in ["pipeline", "predict", "react"]:
107
153
  self.prompt = data['instructions']
@@ -84,4 +84,6 @@ DEFAULT_MULTIMODAL_PROMPT = """
84
84
  ELITEA_RS = "elitea_response"
85
85
  PRINTER = "printer"
86
86
  PRINTER_NODE_RS = "printer_output"
87
- PRINTER_COMPLETED_STATE = "PRINTER_COMPLETED"
87
+ PRINTER_COMPLETED_STATE = "PRINTER_COMPLETED"
88
+
89
+ LOADER_MAX_TOKENS_DEFAULT = 512