alita-sdk 0.3.532__py3-none-any.whl → 0.3.602__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_executor.py +2 -1
- alita_sdk/cli/agent_loader.py +34 -4
- alita_sdk/cli/agents.py +433 -203
- alita_sdk/community/__init__.py +8 -4
- alita_sdk/configurations/__init__.py +1 -0
- alita_sdk/configurations/openapi.py +323 -0
- alita_sdk/runtime/clients/client.py +165 -7
- alita_sdk/runtime/langchain/_constants_bkup.py +1318 -0
- alita_sdk/runtime/langchain/assistant.py +61 -11
- alita_sdk/runtime/langchain/constants.py +419 -171
- alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +4 -2
- alita_sdk/runtime/langchain/document_loaders/AlitaTextLoader.py +5 -2
- alita_sdk/runtime/langchain/langraph_agent.py +108 -23
- alita_sdk/runtime/langchain/utils.py +76 -14
- 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 +5 -0
- alita_sdk/runtime/toolkits/artifact.py +2 -1
- alita_sdk/runtime/toolkits/mcp.py +6 -3
- alita_sdk/runtime/toolkits/mcp_config.py +1048 -0
- alita_sdk/runtime/toolkits/skill_router.py +238 -0
- alita_sdk/runtime/toolkits/tools.py +139 -10
- alita_sdk/runtime/toolkits/vectorstore.py +1 -1
- alita_sdk/runtime/tools/__init__.py +3 -1
- alita_sdk/runtime/tools/artifact.py +15 -0
- alita_sdk/runtime/tools/data_analysis.py +183 -0
- alita_sdk/runtime/tools/llm.py +260 -73
- alita_sdk/runtime/tools/loop.py +3 -1
- alita_sdk/runtime/tools/loop_output.py +3 -1
- alita_sdk/runtime/tools/mcp_server_tool.py +6 -3
- 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 +7 -2
- alita_sdk/runtime/utils/constants.py +5 -1
- alita_sdk/runtime/utils/mcp_client.py +1 -1
- alita_sdk/runtime/utils/mcp_sse_client.py +1 -1
- alita_sdk/runtime/utils/toolkit_utils.py +2 -0
- alita_sdk/tools/__init__.py +44 -2
- alita_sdk/tools/ado/repos/__init__.py +26 -8
- alita_sdk/tools/ado/repos/repos_wrapper.py +78 -52
- alita_sdk/tools/ado/test_plan/__init__.py +3 -2
- 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 +2 -1
- alita_sdk/tools/ado/wiki/ado_wrapper.py +23 -1
- alita_sdk/tools/ado/work_item/__init__.py +3 -2
- alita_sdk/tools/ado/work_item/ado_wrapper.py +56 -3
- alita_sdk/tools/advanced_jira_mining/__init__.py +2 -1
- alita_sdk/tools/aws/delta_lake/__init__.py +2 -1
- alita_sdk/tools/azure_ai/search/__init__.py +2 -1
- alita_sdk/tools/azure_ai/search/api_wrapper.py +1 -1
- alita_sdk/tools/base_indexer_toolkit.py +51 -30
- alita_sdk/tools/bitbucket/__init__.py +2 -1
- alita_sdk/tools/bitbucket/api_wrapper.py +1 -1
- alita_sdk/tools/bitbucket/cloud_api_wrapper.py +3 -3
- alita_sdk/tools/browser/__init__.py +1 -1
- alita_sdk/tools/carrier/__init__.py +1 -1
- alita_sdk/tools/chunkers/code/treesitter/treesitter.py +37 -13
- alita_sdk/tools/cloud/aws/__init__.py +2 -1
- alita_sdk/tools/cloud/azure/__init__.py +2 -1
- alita_sdk/tools/cloud/gcp/__init__.py +2 -1
- alita_sdk/tools/cloud/k8s/__init__.py +2 -1
- alita_sdk/tools/code/linter/__init__.py +2 -1
- alita_sdk/tools/code/sonar/__init__.py +2 -1
- alita_sdk/tools/code_indexer_toolkit.py +19 -2
- alita_sdk/tools/confluence/__init__.py +7 -6
- alita_sdk/tools/confluence/api_wrapper.py +7 -8
- alita_sdk/tools/confluence/loader.py +4 -2
- alita_sdk/tools/custom_open_api/__init__.py +2 -1
- alita_sdk/tools/elastic/__init__.py +2 -1
- alita_sdk/tools/elitea_base.py +28 -9
- alita_sdk/tools/figma/__init__.py +52 -6
- alita_sdk/tools/figma/api_wrapper.py +1158 -123
- alita_sdk/tools/figma/figma_client.py +73 -0
- alita_sdk/tools/figma/toon_tools.py +2748 -0
- alita_sdk/tools/github/__init__.py +2 -1
- alita_sdk/tools/github/github_client.py +56 -92
- alita_sdk/tools/github/schemas.py +4 -4
- alita_sdk/tools/gitlab/__init__.py +2 -1
- alita_sdk/tools/gitlab/api_wrapper.py +118 -38
- alita_sdk/tools/gitlab_org/__init__.py +2 -1
- alita_sdk/tools/gitlab_org/api_wrapper.py +60 -62
- alita_sdk/tools/google/bigquery/__init__.py +2 -1
- alita_sdk/tools/google_places/__init__.py +2 -1
- alita_sdk/tools/jira/__init__.py +2 -1
- alita_sdk/tools/keycloak/__init__.py +2 -1
- alita_sdk/tools/localgit/__init__.py +2 -1
- alita_sdk/tools/memory/__init__.py +1 -1
- alita_sdk/tools/ocr/__init__.py +2 -1
- alita_sdk/tools/openapi/__init__.py +490 -118
- alita_sdk/tools/openapi/api_wrapper.py +1368 -0
- alita_sdk/tools/openapi/tool.py +20 -0
- alita_sdk/tools/pandas/__init__.py +11 -5
- 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 +2 -1
- alita_sdk/tools/pptx/__init__.py +2 -1
- alita_sdk/tools/qtest/__init__.py +21 -2
- alita_sdk/tools/qtest/api_wrapper.py +430 -13
- alita_sdk/tools/rally/__init__.py +2 -1
- alita_sdk/tools/rally/api_wrapper.py +1 -1
- alita_sdk/tools/report_portal/__init__.py +2 -1
- alita_sdk/tools/salesforce/__init__.py +2 -1
- alita_sdk/tools/servicenow/__init__.py +11 -10
- alita_sdk/tools/servicenow/api_wrapper.py +1 -1
- alita_sdk/tools/sharepoint/__init__.py +2 -1
- alita_sdk/tools/sharepoint/api_wrapper.py +2 -2
- alita_sdk/tools/slack/__init__.py +3 -2
- alita_sdk/tools/slack/api_wrapper.py +2 -2
- alita_sdk/tools/sql/__init__.py +3 -2
- alita_sdk/tools/testio/__init__.py +2 -1
- alita_sdk/tools/testrail/__init__.py +2 -1
- alita_sdk/tools/utils/content_parser.py +77 -3
- alita_sdk/tools/utils/text_operations.py +163 -71
- alita_sdk/tools/xray/__init__.py +3 -2
- alita_sdk/tools/yagmail/__init__.py +2 -1
- alita_sdk/tools/zephyr/__init__.py +2 -1
- alita_sdk/tools/zephyr_enterprise/__init__.py +2 -1
- alita_sdk/tools/zephyr_essential/__init__.py +2 -1
- alita_sdk/tools/zephyr_scale/__init__.py +3 -2
- alita_sdk/tools/zephyr_scale/api_wrapper.py +2 -2
- alita_sdk/tools/zephyr_squad/__init__.py +2 -1
- {alita_sdk-0.3.532.dist-info → alita_sdk-0.3.602.dist-info}/METADATA +7 -6
- {alita_sdk-0.3.532.dist-info → alita_sdk-0.3.602.dist-info}/RECORD +137 -119
- {alita_sdk-0.3.532.dist-info → alita_sdk-0.3.602.dist-info}/WHEEL +0 -0
- {alita_sdk-0.3.532.dist-info → alita_sdk-0.3.602.dist-info}/entry_points.txt +0 -0
- {alita_sdk-0.3.532.dist-info → alita_sdk-0.3.602.dist-info}/licenses/LICENSE +0 -0
- {alita_sdk-0.3.532.dist-info → alita_sdk-0.3.602.dist-info}/top_level.txt +0 -0
alita_sdk/community/__init__.py
CHANGED
|
@@ -96,18 +96,22 @@ def get_toolkits():
|
|
|
96
96
|
def get_tools(tools_list: list, alita_client, llm) -> list:
|
|
97
97
|
"""Get community tools based on the tools list configuration."""
|
|
98
98
|
tools = []
|
|
99
|
-
|
|
99
|
+
|
|
100
100
|
# Tool type to class mapping
|
|
101
101
|
_tool_mapping = {
|
|
102
102
|
'analyse_jira': 'AnalyseJira',
|
|
103
103
|
'analyse_ado': 'AnalyseAdo',
|
|
104
|
-
'analyse_gitlab': 'AnalyseGitLab',
|
|
104
|
+
'analyse_gitlab': 'AnalyseGitLab',
|
|
105
105
|
'analyse_github': 'AnalyseGithub',
|
|
106
106
|
'inventory': 'InventoryToolkit'
|
|
107
107
|
}
|
|
108
|
-
|
|
108
|
+
|
|
109
109
|
for tool in tools_list:
|
|
110
|
-
|
|
110
|
+
if isinstance(tool, dict):
|
|
111
|
+
tool_type = tool.get('type')
|
|
112
|
+
else:
|
|
113
|
+
logger.error(f"Community tools received non-dict tool: {tool} (type: {type(tool)})")
|
|
114
|
+
continue
|
|
111
115
|
class_name = _tool_mapping.get(tool_type)
|
|
112
116
|
|
|
113
117
|
if class_name and class_name in globals():
|
|
@@ -53,6 +53,7 @@ _safe_import_configuration('sharepoint', 'sharepoint', 'SharepointConfiguration'
|
|
|
53
53
|
_safe_import_configuration('carrier', 'carrier', 'CarrierConfiguration')
|
|
54
54
|
_safe_import_configuration('report_portal', 'report_portal', 'ReportPortalConfiguration')
|
|
55
55
|
_safe_import_configuration('testio', 'testio', 'TestIOConfiguration')
|
|
56
|
+
_safe_import_configuration('openapi', 'openapi', 'OpenApiConfiguration')
|
|
56
57
|
|
|
57
58
|
# Log import summary
|
|
58
59
|
available_count = len(AVAILABLE_CONFIGURATIONS)
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
from typing import Any, Literal, Optional
|
|
2
|
+
from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator
|
|
3
|
+
|
|
4
|
+
import base64
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class OpenApiConfiguration(BaseModel):
|
|
9
|
+
"""
|
|
10
|
+
OpenAPI configuration for authentication.
|
|
11
|
+
|
|
12
|
+
Supports three authentication modes:
|
|
13
|
+
- Anonymous: No authentication (all fields empty)
|
|
14
|
+
- API Key: Static key sent via header (Bearer, Basic, or Custom)
|
|
15
|
+
- OAuth2 Client Credentials: Machine-to-machine authentication flow
|
|
16
|
+
|
|
17
|
+
Note: Only OAuth2 Client Credentials flow is supported. Authorization Code flow
|
|
18
|
+
is not supported as it requires user interaction and pre-registered redirect URLs.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
model_config = ConfigDict(
|
|
22
|
+
extra='allow',
|
|
23
|
+
json_schema_extra={
|
|
24
|
+
"metadata": {
|
|
25
|
+
"label": "OpenAPI",
|
|
26
|
+
"icon_url": "openapi.svg",
|
|
27
|
+
"categories": ["integrations"],
|
|
28
|
+
"type": "openapi",
|
|
29
|
+
"extra_categories": ["api", "openapi", "swagger"],
|
|
30
|
+
"sections": {
|
|
31
|
+
"auth": {
|
|
32
|
+
"required": False,
|
|
33
|
+
"subsections": [
|
|
34
|
+
{
|
|
35
|
+
"name": "API Key",
|
|
36
|
+
"fields": ["api_key", "auth_type", "custom_header_name"],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "OAuth",
|
|
40
|
+
"fields": [
|
|
41
|
+
"client_id",
|
|
42
|
+
"client_secret",
|
|
43
|
+
"token_url",
|
|
44
|
+
"scope",
|
|
45
|
+
"method",
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
"section": "credentials",
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# =========================================================================
|
|
57
|
+
# API Key Authentication Fields
|
|
58
|
+
# =========================================================================
|
|
59
|
+
|
|
60
|
+
api_key: Optional[SecretStr] = Field(
|
|
61
|
+
default=None,
|
|
62
|
+
description=(
|
|
63
|
+
"API key value (stored as a secret). Used when selecting 'API Key' authentication subsection."
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
auth_type: Optional[Literal['Basic', 'Bearer', 'Custom']] = Field(
|
|
67
|
+
default='Bearer',
|
|
68
|
+
description=(
|
|
69
|
+
"How to apply the API key. "
|
|
70
|
+
"- 'Bearer': sets 'Authorization: Bearer <api_key>' "
|
|
71
|
+
"- 'Basic': sets 'Authorization: Basic <api_key>' "
|
|
72
|
+
"- 'custom': sets '<custom_header_name>: <api_key>'"
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
custom_header_name: Optional[str] = Field(
|
|
76
|
+
default=None,
|
|
77
|
+
description="Custom header name to use when auth_type='custom' (e.g. 'X-Api-Key').",
|
|
78
|
+
json_schema_extra={'visible_when': {'field': 'auth_type', 'value': 'custom'}},
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# =========================================================================
|
|
82
|
+
# OAuth2 Client Credentials Flow Fields
|
|
83
|
+
# =========================================================================
|
|
84
|
+
|
|
85
|
+
client_id: Optional[str] = Field(
|
|
86
|
+
default=None,
|
|
87
|
+
description='OAuth2 client ID (also known as Application ID or App ID)'
|
|
88
|
+
)
|
|
89
|
+
client_secret: Optional[SecretStr] = Field(
|
|
90
|
+
default=None,
|
|
91
|
+
description='OAuth2 client secret (stored securely)'
|
|
92
|
+
)
|
|
93
|
+
token_url: Optional[str] = Field(
|
|
94
|
+
default=None,
|
|
95
|
+
description=(
|
|
96
|
+
'OAuth2 token endpoint URL for obtaining access tokens. '
|
|
97
|
+
'Examples: '
|
|
98
|
+
'Azure AD: https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token, '
|
|
99
|
+
'Google: https://oauth2.googleapis.com/token, '
|
|
100
|
+
'Auth0: https://{domain}/oauth/token, '
|
|
101
|
+
'Spotify: https://accounts.spotify.com/api/token'
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
scope: Optional[str] = Field(
|
|
105
|
+
default=None,
|
|
106
|
+
description=(
|
|
107
|
+
'OAuth2 scope(s), space-separated if multiple (per OAuth2 RFC 6749). '
|
|
108
|
+
'Examples: "user-read-private user-read-email" (Spotify), '
|
|
109
|
+
'"api://app-id/.default" (Azure), '
|
|
110
|
+
'"https://www.googleapis.com/auth/cloud-platform" (Google)'
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
method: Optional[Literal['default', 'Basic']] = Field(
|
|
114
|
+
default='default',
|
|
115
|
+
description=(
|
|
116
|
+
"Token exchange method for client credentials flow. "
|
|
117
|
+
"'default': Sends client_id and client_secret in POST body (Azure AD, Auth0, most providers). "
|
|
118
|
+
"'Basic': Sends credentials via HTTP Basic auth header - required by Spotify, some AWS services, and certain OAuth providers."
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
@model_validator(mode='before')
|
|
123
|
+
@classmethod
|
|
124
|
+
def _validate_auth_consistency(cls, values):
|
|
125
|
+
if not isinstance(values, dict):
|
|
126
|
+
return values
|
|
127
|
+
|
|
128
|
+
# OAuth: if any OAuth field is provided, require the essential ones
|
|
129
|
+
has_any_oauth = any(
|
|
130
|
+
(values.get('client_id'), values.get('client_secret'), values.get('token_url'))
|
|
131
|
+
)
|
|
132
|
+
if has_any_oauth:
|
|
133
|
+
missing = []
|
|
134
|
+
if not values.get('client_id'):
|
|
135
|
+
missing.append('client_id')
|
|
136
|
+
if not values.get('client_secret'):
|
|
137
|
+
missing.append('client_secret')
|
|
138
|
+
if not values.get('token_url'):
|
|
139
|
+
missing.append('token_url')
|
|
140
|
+
if missing:
|
|
141
|
+
raise ValueError(f"OAuth is misconfigured; missing: {', '.join(missing)}")
|
|
142
|
+
|
|
143
|
+
# API key: if auth_type is custom, custom_header_name must be present
|
|
144
|
+
auth_type = values.get('auth_type')
|
|
145
|
+
if isinstance(auth_type, str) and auth_type.strip().lower() == 'custom' and values.get('api_key'):
|
|
146
|
+
if not values.get('custom_header_name'):
|
|
147
|
+
raise ValueError("custom_header_name is required when auth_type='custom'")
|
|
148
|
+
|
|
149
|
+
return values
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def check_connection(settings: dict) -> str | None:
|
|
153
|
+
"""
|
|
154
|
+
Validate the OpenAPI configuration by testing connectivity where possible.
|
|
155
|
+
|
|
156
|
+
Validation behavior by authentication type:
|
|
157
|
+
|
|
158
|
+
1. ANONYMOUS (no auth fields configured):
|
|
159
|
+
- Cannot validate without making actual API calls
|
|
160
|
+
- Returns None (success) - validation skipped
|
|
161
|
+
|
|
162
|
+
2. API KEY (api_key field configured):
|
|
163
|
+
- Cannot validate without knowing which endpoint to call
|
|
164
|
+
- The OpenAPI spec is not available at configuration time
|
|
165
|
+
- Returns None (success) - validation skipped
|
|
166
|
+
|
|
167
|
+
3. OAUTH2 CLIENT CREDENTIALS (client_id, client_secret, token_url configured):
|
|
168
|
+
- CAN validate by attempting token exchange with the OAuth provider
|
|
169
|
+
- Makes a real HTTP request to token_url
|
|
170
|
+
- Returns None on success, error message on failure
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
settings: Dictionary containing OpenAPI configuration fields
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
None: Configuration is valid (or cannot be validated for this auth type)
|
|
177
|
+
str: Error message describing the validation failure
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
# =====================================================================
|
|
181
|
+
# Determine authentication type from configured fields
|
|
182
|
+
# =====================================================================
|
|
183
|
+
|
|
184
|
+
client_id = settings.get('client_id')
|
|
185
|
+
client_secret = settings.get('client_secret')
|
|
186
|
+
token_url = settings.get('token_url')
|
|
187
|
+
|
|
188
|
+
has_oauth_fields = client_id or client_secret or token_url
|
|
189
|
+
|
|
190
|
+
# =====================================================================
|
|
191
|
+
# ANONYMOUS or API KEY: Cannot validate, return success
|
|
192
|
+
# =====================================================================
|
|
193
|
+
|
|
194
|
+
if not has_oauth_fields:
|
|
195
|
+
# No OAuth fields configured - this is either:
|
|
196
|
+
# - Anonymous authentication (no auth at all)
|
|
197
|
+
# - API Key authentication (api_key field may be set)
|
|
198
|
+
#
|
|
199
|
+
# Neither can be validated without making actual API calls to the
|
|
200
|
+
# target service, and we don't have the OpenAPI spec available here.
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
# =====================================================================
|
|
204
|
+
# OAUTH2: Validate by attempting token exchange
|
|
205
|
+
# =====================================================================
|
|
206
|
+
|
|
207
|
+
# Check for required OAuth fields
|
|
208
|
+
if not client_id:
|
|
209
|
+
return "OAuth client_id is required when using OAuth authentication"
|
|
210
|
+
if not client_secret:
|
|
211
|
+
return "OAuth client_secret is required when using OAuth authentication"
|
|
212
|
+
if not token_url:
|
|
213
|
+
return "OAuth token_url is required when using OAuth authentication"
|
|
214
|
+
|
|
215
|
+
# Extract secret value if it's a SecretStr
|
|
216
|
+
if hasattr(client_secret, 'get_secret_value'):
|
|
217
|
+
client_secret = client_secret.get_secret_value()
|
|
218
|
+
|
|
219
|
+
if not client_secret or not str(client_secret).strip():
|
|
220
|
+
return "OAuth client_secret cannot be empty"
|
|
221
|
+
|
|
222
|
+
# Validate token_url format
|
|
223
|
+
token_url = token_url.strip()
|
|
224
|
+
if not token_url.startswith(('http://', 'https://')):
|
|
225
|
+
return "OAuth token_url must start with http:// or https://"
|
|
226
|
+
|
|
227
|
+
# Get optional OAuth settings
|
|
228
|
+
scope = settings.get('scope')
|
|
229
|
+
method = settings.get('method', 'default') or 'default'
|
|
230
|
+
|
|
231
|
+
# ---------------------------------------------------------------------
|
|
232
|
+
# Attempt OAuth2 Client Credentials token exchange
|
|
233
|
+
# ---------------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
headers = {
|
|
237
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
238
|
+
'Accept': 'application/json',
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
data = {
|
|
242
|
+
'grant_type': 'client_credentials',
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
# Apply credentials based on method
|
|
246
|
+
if method == 'Basic':
|
|
247
|
+
# Basic method: credentials in Authorization header (Spotify, some AWS)
|
|
248
|
+
credentials = f"{client_id}:{client_secret}"
|
|
249
|
+
encoded = base64.b64encode(credentials.encode('utf-8')).decode('utf-8')
|
|
250
|
+
headers['Authorization'] = f'Basic {encoded}'
|
|
251
|
+
else:
|
|
252
|
+
# Default method: credentials in POST body (Azure AD, Auth0, most providers)
|
|
253
|
+
data['client_id'] = client_id
|
|
254
|
+
data['client_secret'] = str(client_secret)
|
|
255
|
+
|
|
256
|
+
if scope:
|
|
257
|
+
data['scope'] = scope
|
|
258
|
+
|
|
259
|
+
response = requests.post(
|
|
260
|
+
token_url,
|
|
261
|
+
headers=headers,
|
|
262
|
+
data=data,
|
|
263
|
+
timeout=30,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# ---------------------------------------------------------------------
|
|
267
|
+
# Handle response
|
|
268
|
+
# ---------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
if response.status_code == 200:
|
|
271
|
+
try:
|
|
272
|
+
token_data = response.json()
|
|
273
|
+
if 'access_token' in token_data:
|
|
274
|
+
return None # Success - token obtained
|
|
275
|
+
return "OAuth response did not contain 'access_token'"
|
|
276
|
+
except Exception:
|
|
277
|
+
return "Failed to parse OAuth token response"
|
|
278
|
+
|
|
279
|
+
# Handle common error status codes with helpful messages
|
|
280
|
+
if response.status_code == 400:
|
|
281
|
+
try:
|
|
282
|
+
error_data = response.json()
|
|
283
|
+
error = error_data.get('error', 'bad_request')
|
|
284
|
+
error_desc = error_data.get('error_description', '')
|
|
285
|
+
if error_desc:
|
|
286
|
+
return f"OAuth error: {error} - {error_desc}"
|
|
287
|
+
return f"OAuth error: {error}"
|
|
288
|
+
except Exception:
|
|
289
|
+
return "OAuth request failed: bad request (400)"
|
|
290
|
+
|
|
291
|
+
if response.status_code == 401:
|
|
292
|
+
return "OAuth authentication failed: invalid client_id or client_secret"
|
|
293
|
+
|
|
294
|
+
if response.status_code == 403:
|
|
295
|
+
return "OAuth access forbidden: client may lack required permissions"
|
|
296
|
+
|
|
297
|
+
if response.status_code == 404:
|
|
298
|
+
return f"OAuth token endpoint not found: {token_url}"
|
|
299
|
+
|
|
300
|
+
return f"OAuth token request failed with status {response.status_code}"
|
|
301
|
+
|
|
302
|
+
except requests.exceptions.SSLError as e:
|
|
303
|
+
error_str = str(e).lower()
|
|
304
|
+
if 'hostname mismatch' in error_str:
|
|
305
|
+
return "OAuth token_url hostname does not match SSL certificate - verify the URL is correct"
|
|
306
|
+
if 'certificate verify failed' in error_str:
|
|
307
|
+
return "SSL certificate verification failed for OAuth endpoint - the server may have an invalid or self-signed certificate"
|
|
308
|
+
if 'certificate has expired' in error_str:
|
|
309
|
+
return "SSL certificate has expired for OAuth endpoint"
|
|
310
|
+
return "SSL error connecting to OAuth endpoint - verify the token_url is correct"
|
|
311
|
+
except requests.exceptions.ConnectionError as e:
|
|
312
|
+
error_str = str(e).lower()
|
|
313
|
+
if 'name or service not known' in error_str or 'nodename nor servname provided' in error_str:
|
|
314
|
+
return "OAuth token_url hostname could not be resolved - verify the URL is correct"
|
|
315
|
+
if 'connection refused' in error_str:
|
|
316
|
+
return "Connection refused by OAuth endpoint - verify the token_url and port are correct"
|
|
317
|
+
return "Cannot connect to OAuth token endpoint - verify the token_url is correct"
|
|
318
|
+
except requests.exceptions.Timeout:
|
|
319
|
+
return "OAuth token request timed out - the endpoint may be unreachable"
|
|
320
|
+
except requests.exceptions.RequestException:
|
|
321
|
+
return "OAuth request failed - verify the token_url is correct and accessible"
|
|
322
|
+
except Exception:
|
|
323
|
+
return "Unexpected error during OAuth configuration validation"
|
|
@@ -259,21 +259,22 @@ class AlitaClient:
|
|
|
259
259
|
use_responses_api = True
|
|
260
260
|
break
|
|
261
261
|
|
|
262
|
-
# handle case when max_tokens are auto-configurable == -1
|
|
262
|
+
# handle case when max_tokens are auto-configurable == -1 or None
|
|
263
263
|
llm_max_tokens = model_config.get("max_tokens", None)
|
|
264
|
-
if llm_max_tokens
|
|
265
|
-
logger.warning(f'User selected `MAX COMPLETION TOKENS` as `auto`')
|
|
266
|
-
# default
|
|
264
|
+
if llm_max_tokens is None or llm_max_tokens == -1:
|
|
265
|
+
logger.warning(f'User selected `MAX COMPLETION TOKENS` as `auto` or value is None/missing')
|
|
266
|
+
# default number for a case when auto is selected for an agent
|
|
267
267
|
llm_max_tokens = 4000
|
|
268
268
|
|
|
269
269
|
if is_anthropic:
|
|
270
270
|
# ChatAnthropic configuration
|
|
271
|
+
# Anthropic requires max_tokens to be an integer, never None
|
|
271
272
|
target_kwargs = {
|
|
272
273
|
"base_url": f"{self.base_url}{self.allm_path}",
|
|
273
274
|
"model": model_name,
|
|
274
275
|
"api_key": self.auth_token,
|
|
275
276
|
"streaming": model_config.get("streaming", True),
|
|
276
|
-
"max_tokens": llm_max_tokens,
|
|
277
|
+
"max_tokens": llm_max_tokens, # Always an integer now
|
|
277
278
|
"temperature": model_config.get("temperature"),
|
|
278
279
|
"max_retries": model_config.get("max_retries", 3),
|
|
279
280
|
"default_headers": {"openai-organization": str(self.project_id),
|
|
@@ -716,7 +717,7 @@ class AlitaClient:
|
|
|
716
717
|
memory=None, runtime='langchain', variables: Optional[list] = None,
|
|
717
718
|
store: Optional[BaseStore] = None, debug_mode: Optional[bool] = False,
|
|
718
719
|
mcp_tokens: Optional[dict] = None, conversation_id: Optional[str] = None,
|
|
719
|
-
ignored_mcp_servers: Optional[list] = None):
|
|
720
|
+
ignored_mcp_servers: Optional[list] = None, persona: Optional[str] = "generic"):
|
|
720
721
|
"""
|
|
721
722
|
Create a predict-type agent with minimal configuration.
|
|
722
723
|
|
|
@@ -733,6 +734,7 @@ class AlitaClient:
|
|
|
733
734
|
store: Optional store for memory
|
|
734
735
|
debug_mode: Enable debug mode for cases when assistant can be initialized without tools
|
|
735
736
|
ignored_mcp_servers: Optional list of MCP server URLs to ignore (user chose to continue without auth)
|
|
737
|
+
persona: Default persona for chat: 'generic' or 'qa' (default: 'generic')
|
|
736
738
|
|
|
737
739
|
Returns:
|
|
738
740
|
Runnable agent ready for execution
|
|
@@ -767,7 +769,8 @@ class AlitaClient:
|
|
|
767
769
|
debug_mode=debug_mode,
|
|
768
770
|
mcp_tokens=mcp_tokens,
|
|
769
771
|
conversation_id=conversation_id,
|
|
770
|
-
ignored_mcp_servers=ignored_mcp_servers
|
|
772
|
+
ignored_mcp_servers=ignored_mcp_servers,
|
|
773
|
+
persona=persona
|
|
771
774
|
).runnable()
|
|
772
775
|
|
|
773
776
|
def test_toolkit_tool(self, toolkit_config: dict, tool_name: str, tool_params: dict = None,
|
|
@@ -1153,3 +1156,158 @@ class AlitaClient:
|
|
|
1153
1156
|
"events_dispatched": [],
|
|
1154
1157
|
"execution_time_seconds": 0.0
|
|
1155
1158
|
}
|
|
1159
|
+
|
|
1160
|
+
def test_mcp_connection(self, toolkit_config: dict, mcp_tokens: dict = None) -> dict:
|
|
1161
|
+
"""
|
|
1162
|
+
Test MCP server connection using protocol-level list_tools.
|
|
1163
|
+
|
|
1164
|
+
This method verifies MCP server connectivity and authentication by calling
|
|
1165
|
+
the protocol-level tools/list JSON-RPC method (NOT executing a tool).
|
|
1166
|
+
This is ideal for auth checks as it validates the connection without
|
|
1167
|
+
requiring any tool execution.
|
|
1168
|
+
|
|
1169
|
+
Args:
|
|
1170
|
+
toolkit_config: Configuration dictionary for the MCP toolkit containing:
|
|
1171
|
+
- toolkit_name: Name of the toolkit
|
|
1172
|
+
- settings: Dictionary with 'url', optional 'headers', 'session_id'
|
|
1173
|
+
mcp_tokens: Optional dictionary of MCP OAuth tokens by server URL
|
|
1174
|
+
Format: {canonical_url: {access_token: str, session_id: str}}
|
|
1175
|
+
|
|
1176
|
+
Returns:
|
|
1177
|
+
Dictionary containing:
|
|
1178
|
+
- success: Boolean indicating if the connection was successful
|
|
1179
|
+
- tools: List of tool names available on the MCP server (if successful)
|
|
1180
|
+
- tools_count: Number of tools discovered
|
|
1181
|
+
- server_session_id: Session ID provided by the server (if any)
|
|
1182
|
+
- error: Error message (if unsuccessful)
|
|
1183
|
+
- toolkit_config: Original toolkit configuration
|
|
1184
|
+
|
|
1185
|
+
Raises:
|
|
1186
|
+
McpAuthorizationRequired: If MCP server requires OAuth authorization
|
|
1187
|
+
|
|
1188
|
+
Example:
|
|
1189
|
+
>>> config = {
|
|
1190
|
+
... 'toolkit_name': 'my-mcp-server',
|
|
1191
|
+
... 'type': 'mcp',
|
|
1192
|
+
... 'settings': {
|
|
1193
|
+
... 'url': 'https://mcp-server.example.com/mcp',
|
|
1194
|
+
... 'headers': {'X-Custom': 'value'}
|
|
1195
|
+
... }
|
|
1196
|
+
... }
|
|
1197
|
+
>>> result = client.test_mcp_connection(config)
|
|
1198
|
+
>>> if result['success']:
|
|
1199
|
+
... print(f"Connected! Found {result['tools_count']} tools")
|
|
1200
|
+
"""
|
|
1201
|
+
import asyncio
|
|
1202
|
+
import time
|
|
1203
|
+
from ..utils.mcp_client import McpClient
|
|
1204
|
+
from ..utils.mcp_oauth import canonical_resource
|
|
1205
|
+
|
|
1206
|
+
toolkit_name = toolkit_config.get('toolkit_name', 'unknown')
|
|
1207
|
+
settings = toolkit_config.get('settings', {})
|
|
1208
|
+
|
|
1209
|
+
# Extract connection parameters
|
|
1210
|
+
url = settings.get('url')
|
|
1211
|
+
if not url:
|
|
1212
|
+
return {
|
|
1213
|
+
"success": False,
|
|
1214
|
+
"error": "MCP toolkit configuration missing 'url' in settings",
|
|
1215
|
+
"toolkit_config": toolkit_config,
|
|
1216
|
+
"tools": [],
|
|
1217
|
+
"tools_count": 0
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
headers = settings.get('headers') or {}
|
|
1221
|
+
session_id = settings.get('session_id')
|
|
1222
|
+
|
|
1223
|
+
# Apply OAuth token if available
|
|
1224
|
+
if mcp_tokens and url:
|
|
1225
|
+
canonical_url = canonical_resource(url)
|
|
1226
|
+
token_data = mcp_tokens.get(canonical_url)
|
|
1227
|
+
if token_data:
|
|
1228
|
+
if isinstance(token_data, dict):
|
|
1229
|
+
access_token = token_data.get('access_token')
|
|
1230
|
+
if not session_id:
|
|
1231
|
+
session_id = token_data.get('session_id')
|
|
1232
|
+
else:
|
|
1233
|
+
# Backward compatibility: plain token string
|
|
1234
|
+
access_token = token_data
|
|
1235
|
+
|
|
1236
|
+
if access_token:
|
|
1237
|
+
headers = dict(headers) # Copy to avoid mutating original
|
|
1238
|
+
headers.setdefault('Authorization', f'Bearer {access_token}')
|
|
1239
|
+
logger.info(f"[MCP Auth Check] Applied OAuth token for {canonical_url}")
|
|
1240
|
+
|
|
1241
|
+
logger.info(f"Testing MCP connection to '{toolkit_name}' at {url}")
|
|
1242
|
+
|
|
1243
|
+
start_time = time.time()
|
|
1244
|
+
|
|
1245
|
+
async def _test_connection():
|
|
1246
|
+
client = McpClient(
|
|
1247
|
+
url=url,
|
|
1248
|
+
session_id=session_id,
|
|
1249
|
+
headers=headers,
|
|
1250
|
+
timeout=60 # Reasonable timeout for connection test
|
|
1251
|
+
)
|
|
1252
|
+
|
|
1253
|
+
async with client:
|
|
1254
|
+
# Initialize MCP protocol session
|
|
1255
|
+
await client.initialize()
|
|
1256
|
+
logger.info(f"[MCP Auth Check] Session initialized (transport={client.detected_transport})")
|
|
1257
|
+
|
|
1258
|
+
# Call protocol-level list_tools (tools/list JSON-RPC method)
|
|
1259
|
+
tools = await client.list_tools()
|
|
1260
|
+
|
|
1261
|
+
return {
|
|
1262
|
+
"tools": tools,
|
|
1263
|
+
"server_session_id": client.server_session_id,
|
|
1264
|
+
"transport": client.detected_transport
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
try:
|
|
1268
|
+
# Run async operation
|
|
1269
|
+
try:
|
|
1270
|
+
loop = asyncio.get_event_loop()
|
|
1271
|
+
if loop.is_running():
|
|
1272
|
+
# If we're already in an async context, create a new task
|
|
1273
|
+
import concurrent.futures
|
|
1274
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
1275
|
+
future = executor.submit(asyncio.run, _test_connection())
|
|
1276
|
+
result = future.result(timeout=120)
|
|
1277
|
+
else:
|
|
1278
|
+
result = loop.run_until_complete(_test_connection())
|
|
1279
|
+
except RuntimeError:
|
|
1280
|
+
# No event loop, create one
|
|
1281
|
+
result = asyncio.run(_test_connection())
|
|
1282
|
+
|
|
1283
|
+
execution_time = time.time() - start_time
|
|
1284
|
+
|
|
1285
|
+
# Extract tool names for the response
|
|
1286
|
+
tool_names = [tool.get('name', 'unknown') for tool in result.get('tools', [])]
|
|
1287
|
+
|
|
1288
|
+
logger.info(f"[MCP Auth Check] Connection successful to '{toolkit_name}': {len(tool_names)} tools in {execution_time:.3f}s")
|
|
1289
|
+
|
|
1290
|
+
return {
|
|
1291
|
+
"success": True,
|
|
1292
|
+
"tools": tool_names,
|
|
1293
|
+
"tools_count": len(tool_names),
|
|
1294
|
+
"server_session_id": result.get('server_session_id'),
|
|
1295
|
+
"transport": result.get('transport'),
|
|
1296
|
+
"toolkit_config": toolkit_config,
|
|
1297
|
+
"execution_time_seconds": execution_time
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
except McpAuthorizationRequired:
|
|
1301
|
+
# Re-raise to allow proper handling upstream
|
|
1302
|
+
raise
|
|
1303
|
+
except Exception as e:
|
|
1304
|
+
execution_time = time.time() - start_time
|
|
1305
|
+
logger.error(f"[MCP Auth Check] Connection failed to '{toolkit_name}': {str(e)}")
|
|
1306
|
+
return {
|
|
1307
|
+
"success": False,
|
|
1308
|
+
"error": f"MCP connection failed: {str(e)}",
|
|
1309
|
+
"toolkit_config": toolkit_config,
|
|
1310
|
+
"tools": [],
|
|
1311
|
+
"tools_count": 0,
|
|
1312
|
+
"execution_time_seconds": execution_time
|
|
1313
|
+
}
|