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.
- alita_sdk/cli/agent_loader.py +27 -6
- alita_sdk/cli/agents.py +10 -1
- alita_sdk/cli/inventory.py +12 -195
- alita_sdk/cli/tools/filesystem.py +95 -9
- alita_sdk/community/inventory/__init__.py +12 -0
- alita_sdk/community/inventory/toolkit.py +9 -5
- alita_sdk/community/inventory/toolkit_utils.py +176 -0
- alita_sdk/configurations/ado.py +144 -0
- alita_sdk/configurations/confluence.py +76 -42
- alita_sdk/configurations/figma.py +76 -0
- alita_sdk/configurations/gitlab.py +2 -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 +2 -2
- alita_sdk/runtime/clients/client.py +64 -40
- alita_sdk/runtime/clients/sandbox_client.py +14 -0
- alita_sdk/runtime/langchain/assistant.py +48 -2
- alita_sdk/runtime/langchain/constants.py +3 -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 +2 -1
- alita_sdk/runtime/langchain/document_loaders/constants.py +12 -7
- alita_sdk/runtime/langchain/langraph_agent.py +10 -10
- alita_sdk/runtime/langchain/utils.py +6 -1
- alita_sdk/runtime/toolkits/artifact.py +14 -5
- alita_sdk/runtime/toolkits/datasource.py +13 -6
- alita_sdk/runtime/toolkits/mcp.py +94 -219
- alita_sdk/runtime/toolkits/planning.py +13 -6
- alita_sdk/runtime/toolkits/tools.py +60 -25
- alita_sdk/runtime/toolkits/vectorstore.py +11 -5
- alita_sdk/runtime/tools/artifact.py +185 -23
- alita_sdk/runtime/tools/function.py +2 -1
- alita_sdk/runtime/tools/llm.py +155 -34
- alita_sdk/runtime/tools/mcp_remote_tool.py +25 -10
- alita_sdk/runtime/tools/mcp_server_tool.py +2 -4
- alita_sdk/runtime/tools/vectorstore_base.py +3 -3
- alita_sdk/runtime/utils/AlitaCallback.py +136 -21
- alita_sdk/runtime/utils/mcp_client.py +492 -0
- alita_sdk/runtime/utils/mcp_oauth.py +125 -8
- alita_sdk/runtime/utils/mcp_sse_client.py +35 -6
- alita_sdk/runtime/utils/mcp_tools_discovery.py +124 -0
- alita_sdk/runtime/utils/toolkit_utils.py +7 -13
- alita_sdk/runtime/utils/utils.py +2 -0
- alita_sdk/tools/__init__.py +15 -0
- alita_sdk/tools/ado/repos/__init__.py +10 -12
- alita_sdk/tools/ado/test_plan/__init__.py +23 -8
- alita_sdk/tools/ado/wiki/__init__.py +24 -8
- alita_sdk/tools/ado/wiki/ado_wrapper.py +21 -7
- alita_sdk/tools/ado/work_item/__init__.py +24 -8
- alita_sdk/tools/advanced_jira_mining/__init__.py +10 -8
- alita_sdk/tools/aws/delta_lake/__init__.py +12 -9
- alita_sdk/tools/aws/delta_lake/tool.py +5 -1
- alita_sdk/tools/azure_ai/search/__init__.py +9 -7
- alita_sdk/tools/base/tool.py +5 -1
- alita_sdk/tools/base_indexer_toolkit.py +26 -1
- alita_sdk/tools/bitbucket/__init__.py +14 -10
- alita_sdk/tools/bitbucket/api_wrapper.py +50 -2
- alita_sdk/tools/browser/__init__.py +5 -4
- alita_sdk/tools/carrier/__init__.py +5 -6
- alita_sdk/tools/chunkers/sematic/json_chunker.py +1 -0
- alita_sdk/tools/chunkers/sematic/markdown_chunker.py +2 -0
- alita_sdk/tools/chunkers/universal_chunker.py +1 -0
- alita_sdk/tools/cloud/aws/__init__.py +9 -7
- alita_sdk/tools/cloud/azure/__init__.py +9 -7
- alita_sdk/tools/cloud/gcp/__init__.py +9 -7
- alita_sdk/tools/cloud/k8s/__init__.py +9 -7
- alita_sdk/tools/code/linter/__init__.py +9 -8
- alita_sdk/tools/code/loaders/codesearcher.py +3 -2
- alita_sdk/tools/code/sonar/__init__.py +9 -7
- alita_sdk/tools/confluence/__init__.py +15 -10
- alita_sdk/tools/confluence/api_wrapper.py +63 -14
- alita_sdk/tools/custom_open_api/__init__.py +11 -5
- alita_sdk/tools/elastic/__init__.py +10 -8
- alita_sdk/tools/elitea_base.py +387 -9
- alita_sdk/tools/figma/__init__.py +8 -7
- alita_sdk/tools/github/__init__.py +12 -14
- alita_sdk/tools/github/github_client.py +68 -2
- alita_sdk/tools/github/tool.py +5 -1
- alita_sdk/tools/gitlab/__init__.py +14 -11
- alita_sdk/tools/gitlab/api_wrapper.py +81 -1
- alita_sdk/tools/gitlab_org/__init__.py +9 -8
- alita_sdk/tools/google/bigquery/__init__.py +12 -12
- alita_sdk/tools/google/bigquery/tool.py +5 -1
- alita_sdk/tools/google_places/__init__.py +9 -8
- alita_sdk/tools/jira/__init__.py +15 -10
- alita_sdk/tools/keycloak/__init__.py +10 -8
- alita_sdk/tools/localgit/__init__.py +8 -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/ocr/__init__.py +10 -8
- alita_sdk/tools/openapi/__init__.py +6 -2
- alita_sdk/tools/pandas/__init__.py +9 -7
- alita_sdk/tools/postman/__init__.py +10 -11
- alita_sdk/tools/pptx/__init__.py +9 -9
- alita_sdk/tools/qtest/__init__.py +9 -8
- alita_sdk/tools/rally/__init__.py +9 -8
- alita_sdk/tools/report_portal/__init__.py +11 -9
- alita_sdk/tools/salesforce/__init__.py +9 -9
- alita_sdk/tools/servicenow/__init__.py +10 -8
- alita_sdk/tools/sharepoint/__init__.py +9 -8
- alita_sdk/tools/sharepoint/api_wrapper.py +2 -2
- alita_sdk/tools/slack/__init__.py +8 -7
- alita_sdk/tools/sql/__init__.py +9 -8
- alita_sdk/tools/testio/__init__.py +9 -8
- alita_sdk/tools/testrail/__init__.py +10 -8
- alita_sdk/tools/utils/__init__.py +9 -4
- alita_sdk/tools/utils/text_operations.py +254 -0
- alita_sdk/tools/vector_adapters/VectorStoreAdapter.py +16 -18
- alita_sdk/tools/xray/__init__.py +10 -8
- alita_sdk/tools/yagmail/__init__.py +8 -3
- alita_sdk/tools/zephyr/__init__.py +8 -7
- alita_sdk/tools/zephyr_enterprise/__init__.py +10 -8
- alita_sdk/tools/zephyr_essential/__init__.py +9 -8
- alita_sdk/tools/zephyr_scale/__init__.py +9 -8
- alita_sdk/tools/zephyr_squad/__init__.py +9 -8
- {alita_sdk-0.3.486.dist-info → alita_sdk-0.3.515.dist-info}/METADATA +1 -1
- {alita_sdk-0.3.486.dist-info → alita_sdk-0.3.515.dist-info}/RECORD +124 -119
- {alita_sdk-0.3.486.dist-info → alita_sdk-0.3.515.dist-info}/WHEEL +0 -0
- {alita_sdk-0.3.486.dist-info → alita_sdk-0.3.515.dist-info}/entry_points.txt +0 -0
- {alita_sdk-0.3.486.dist-info → alita_sdk-0.3.515.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
42
|
-
return f"{data['error']}. {data
|
|
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.
|
|
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': (
|
|
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
|
|
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
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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
|
|
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:
|
|
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']
|