alita-sdk 0.3.562__py3-none-any.whl → 0.3.585__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 (74) hide show
  1. alita_sdk/cli/agents.py +358 -165
  2. alita_sdk/configurations/openapi.py +227 -15
  3. alita_sdk/runtime/langchain/langraph_agent.py +93 -20
  4. alita_sdk/runtime/langchain/utils.py +30 -14
  5. alita_sdk/runtime/toolkits/artifact.py +2 -1
  6. alita_sdk/runtime/toolkits/mcp.py +4 -2
  7. alita_sdk/runtime/toolkits/skill_router.py +1 -1
  8. alita_sdk/runtime/toolkits/vectorstore.py +1 -1
  9. alita_sdk/runtime/tools/data_analysis.py +1 -1
  10. alita_sdk/runtime/tools/llm.py +30 -11
  11. alita_sdk/runtime/utils/constants.py +5 -1
  12. alita_sdk/tools/ado/repos/__init__.py +2 -1
  13. alita_sdk/tools/ado/test_plan/__init__.py +2 -1
  14. alita_sdk/tools/ado/wiki/__init__.py +2 -1
  15. alita_sdk/tools/ado/work_item/__init__.py +2 -1
  16. alita_sdk/tools/advanced_jira_mining/__init__.py +2 -1
  17. alita_sdk/tools/aws/delta_lake/__init__.py +2 -1
  18. alita_sdk/tools/azure_ai/search/__init__.py +2 -1
  19. alita_sdk/tools/bitbucket/__init__.py +2 -1
  20. alita_sdk/tools/browser/__init__.py +1 -1
  21. alita_sdk/tools/carrier/__init__.py +1 -1
  22. alita_sdk/tools/cloud/aws/__init__.py +2 -1
  23. alita_sdk/tools/cloud/azure/__init__.py +2 -1
  24. alita_sdk/tools/cloud/gcp/__init__.py +2 -1
  25. alita_sdk/tools/cloud/k8s/__init__.py +2 -1
  26. alita_sdk/tools/code/linter/__init__.py +2 -1
  27. alita_sdk/tools/code/sonar/__init__.py +2 -1
  28. alita_sdk/tools/confluence/__init__.py +2 -1
  29. alita_sdk/tools/custom_open_api/__init__.py +2 -1
  30. alita_sdk/tools/elastic/__init__.py +2 -1
  31. alita_sdk/tools/figma/__init__.py +51 -5
  32. alita_sdk/tools/figma/api_wrapper.py +1157 -123
  33. alita_sdk/tools/figma/figma_client.py +73 -0
  34. alita_sdk/tools/figma/toon_tools.py +2748 -0
  35. alita_sdk/tools/github/__init__.py +2 -1
  36. alita_sdk/tools/gitlab/__init__.py +2 -1
  37. alita_sdk/tools/gitlab/api_wrapper.py +32 -0
  38. alita_sdk/tools/gitlab_org/__init__.py +2 -1
  39. alita_sdk/tools/google/bigquery/__init__.py +2 -1
  40. alita_sdk/tools/google_places/__init__.py +2 -1
  41. alita_sdk/tools/jira/__init__.py +2 -1
  42. alita_sdk/tools/keycloak/__init__.py +2 -1
  43. alita_sdk/tools/localgit/__init__.py +2 -1
  44. alita_sdk/tools/memory/__init__.py +1 -1
  45. alita_sdk/tools/ocr/__init__.py +2 -1
  46. alita_sdk/tools/openapi/__init__.py +227 -15
  47. alita_sdk/tools/openapi/api_wrapper.py +1287 -802
  48. alita_sdk/tools/pandas/__init__.py +3 -2
  49. alita_sdk/tools/postman/__init__.py +2 -1
  50. alita_sdk/tools/pptx/__init__.py +2 -1
  51. alita_sdk/tools/qtest/__init__.py +2 -1
  52. alita_sdk/tools/rally/__init__.py +2 -1
  53. alita_sdk/tools/report_portal/__init__.py +2 -1
  54. alita_sdk/tools/salesforce/__init__.py +2 -1
  55. alita_sdk/tools/servicenow/__init__.py +2 -1
  56. alita_sdk/tools/sharepoint/__init__.py +2 -1
  57. alita_sdk/tools/slack/__init__.py +3 -2
  58. alita_sdk/tools/sql/__init__.py +2 -1
  59. alita_sdk/tools/testio/__init__.py +2 -1
  60. alita_sdk/tools/testrail/__init__.py +2 -1
  61. alita_sdk/tools/utils/content_parser.py +68 -2
  62. alita_sdk/tools/xray/__init__.py +2 -1
  63. alita_sdk/tools/yagmail/__init__.py +2 -1
  64. alita_sdk/tools/zephyr/__init__.py +2 -1
  65. alita_sdk/tools/zephyr_enterprise/__init__.py +2 -1
  66. alita_sdk/tools/zephyr_essential/__init__.py +2 -1
  67. alita_sdk/tools/zephyr_scale/__init__.py +2 -1
  68. alita_sdk/tools/zephyr_squad/__init__.py +2 -1
  69. {alita_sdk-0.3.562.dist-info → alita_sdk-0.3.585.dist-info}/METADATA +1 -1
  70. {alita_sdk-0.3.562.dist-info → alita_sdk-0.3.585.dist-info}/RECORD +74 -72
  71. {alita_sdk-0.3.562.dist-info → alita_sdk-0.3.585.dist-info}/WHEEL +0 -0
  72. {alita_sdk-0.3.562.dist-info → alita_sdk-0.3.585.dist-info}/entry_points.txt +0 -0
  73. {alita_sdk-0.3.562.dist-info → alita_sdk-0.3.585.dist-info}/licenses/LICENSE +0 -0
  74. {alita_sdk-0.3.562.dist-info → alita_sdk-0.3.585.dist-info}/top_level.txt +0 -0
@@ -10,6 +10,7 @@ from ..elitea_base import filter_missconfigured_index_tools
10
10
  from ..utils import clean_string, get_max_toolkit_length
11
11
  from ...configurations.github import GithubConfiguration
12
12
  from ...configurations.pgvector import PgVectorConfiguration
13
+ from ...runtime.utils.constants import TOOLKIT_NAME_META, TOOL_NAME_META, TOOLKIT_TYPE_META
13
14
 
14
15
  name = "github"
15
16
 
@@ -96,7 +97,7 @@ class AlitaGitHubToolkit(BaseToolkit):
96
97
  mode=tool["mode"],
97
98
  description=description,
98
99
  args_schema=tool["args_schema"],
99
- metadata={"toolkit_name": toolkit_name} if toolkit_name else {}
100
+ metadata={TOOLKIT_NAME_META: toolkit_name, TOOLKIT_TYPE_META: name, TOOL_NAME_META: tool["name"]} if toolkit_name else {TOOL_NAME_META: tool["name"]}
100
101
  ))
101
102
  return cls(tools=tools)
102
103
 
@@ -11,6 +11,7 @@ from ..elitea_base import filter_missconfigured_index_tools
11
11
  from ..utils import clean_string, get_max_toolkit_length
12
12
  from ...configurations.gitlab import GitlabConfiguration
13
13
  from ...configurations.pgvector import PgVectorConfiguration
14
+ from ...runtime.utils.constants import TOOLKIT_NAME_META, TOOL_NAME_META, TOOLKIT_TYPE_META
14
15
 
15
16
  name = "gitlab"
16
17
 
@@ -92,7 +93,7 @@ class AlitaGitlabToolkit(BaseToolkit):
92
93
  name=tool["name"],
93
94
  description=description,
94
95
  args_schema=tool["args_schema"],
95
- metadata={"toolkit_name": toolkit_name} if toolkit_name else {}
96
+ metadata={TOOLKIT_NAME_META: toolkit_name, TOOLKIT_TYPE_META: name, TOOL_NAME_META: tool["name"]} if toolkit_name else {TOOL_NAME_META: tool["name"]}
96
97
  ))
97
98
  return cls(tools=tools)
98
99
 
@@ -54,6 +54,11 @@ CreatePullRequestModel = create_model(
54
54
  pr_body=(str, Field(description="The body of the pull request")),
55
55
  branch=(str, Field(description="The branch to create the pull request from")),
56
56
  )
57
+ CommentOnPRModel = create_model(
58
+ "CommentOnPRModel",
59
+ pr_number=(int, Field(description="The number of the pull request/merge request")),
60
+ comment=(str, Field(description="The comment text to add")),
61
+ )
57
62
 
58
63
  CreateBranchModel = create_model(
59
64
  "CreateBranchModel",
@@ -326,6 +331,27 @@ class GitLabAPIWrapper(CodeIndexerToolkit):
326
331
  except Exception as e:
327
332
  return "Unable to make comment due to error:\n" + str(e)
328
333
 
334
+ def comment_on_pr(self, pr_number: int, comment: str) -> str:
335
+ """
336
+ Add a comment to a pull request (merge request) in GitLab.
337
+
338
+ This method adds a general comment to the entire merge request,
339
+ not tied to specific code lines or file changes.
340
+
341
+ Parameters:
342
+ pr_number: GitLab Merge Request (Pull Request) number
343
+ comment: Comment text to add
344
+
345
+ Returns:
346
+ Success message or error description
347
+ """
348
+ try:
349
+ mr = self.repo_instance.mergerequests.get(pr_number)
350
+ mr.notes.create({"body": comment})
351
+ return "Commented on merge request " + str(pr_number)
352
+ except Exception as e:
353
+ return "Unable to make comment due to error:\n" + str(e)
354
+
329
355
  def create_file(self, file_path: str, file_contents: str, branch: str) -> str:
330
356
  # Default to active branch if branch is None
331
357
  branch = branch if branch else self._active_branch
@@ -623,6 +649,12 @@ class GitLabAPIWrapper(CodeIndexerToolkit):
623
649
  "description": self.comment_on_issue.__doc__ or "Comment on an issue in the repository.",
624
650
  "args_schema": CommentOnIssueModel,
625
651
  },
652
+ {
653
+ "name": "comment_on_pr",
654
+ "ref": self.comment_on_pr,
655
+ "description": self.comment_on_pr.__doc__ or "Comment on a pull request (merge request) in the repository.",
656
+ "args_schema": CommentOnPRModel,
657
+ },
626
658
  {
627
659
  "name": "create_file",
628
660
  "ref": self.create_file,
@@ -8,6 +8,7 @@ from pydantic import create_model, BaseModel, ConfigDict, Field, SecretStr
8
8
  from ..elitea_base import filter_missconfigured_index_tools
9
9
  from ..utils import clean_string, get_max_toolkit_length
10
10
  from ...configurations.gitlab import GitlabConfiguration
11
+ from ...runtime.utils.constants import TOOLKIT_NAME_META, TOOL_NAME_META, TOOLKIT_TYPE_META
11
12
 
12
13
  name = "gitlab_org"
13
14
 
@@ -74,7 +75,7 @@ class AlitaGitlabSpaceToolkit(BaseToolkit):
74
75
  name=tool['name'],
75
76
  description=description,
76
77
  args_schema=tool["args_schema"],
77
- metadata={"toolkit_name": toolkit_name} if toolkit_name else {}
78
+ metadata={TOOLKIT_NAME_META: toolkit_name, TOOLKIT_TYPE_META: name, TOOL_NAME_META: tool["name"]} if toolkit_name else {TOOL_NAME_META: tool["name"]}
78
79
  ))
79
80
  return cls(tools=tools)
80
81
 
@@ -8,6 +8,7 @@ from ....configurations.bigquery import BigQueryConfiguration
8
8
  from ...utils import clean_string, get_max_toolkit_length
9
9
  from .api_wrapper import BigQueryApiWrapper
10
10
  from .tool import BigQueryAction
11
+ from ....runtime.utils.constants import TOOLKIT_NAME_META, TOOL_NAME_META, TOOLKIT_TYPE_META
11
12
 
12
13
  name = "bigquery"
13
14
 
@@ -129,7 +130,7 @@ class BigQueryToolkit(BaseToolkit):
129
130
  name=t["name"],
130
131
  description=description,
131
132
  args_schema=t["args_schema"],
132
- metadata={"toolkit_name": toolkit_name} if toolkit_name else {}
133
+ metadata={TOOLKIT_NAME_META: toolkit_name, TOOLKIT_TYPE_META: name, TOOL_NAME_META: t["name"]} if toolkit_name else {TOOL_NAME_META: t["name"]}
133
134
  )
134
135
  )
135
136
  return instance
@@ -8,6 +8,7 @@ from ..base.tool import BaseAction
8
8
  from ..elitea_base import filter_missconfigured_index_tools
9
9
  from ..utils import clean_string, get_max_toolkit_length
10
10
  from ...configurations.google_places import GooglePlacesConfiguration
11
+ from ...runtime.utils.constants import TOOLKIT_NAME_META, TOOL_NAME_META, TOOLKIT_TYPE_META
11
12
 
12
13
  name = "google_places"
13
14
 
@@ -67,7 +68,7 @@ class GooglePlacesToolkit(BaseToolkit):
67
68
  name=tool["name"],
68
69
  description=description,
69
70
  args_schema=tool["args_schema"],
70
- metadata={"toolkit_name": toolkit_name} if toolkit_name else {}
71
+ metadata={TOOLKIT_NAME_META: toolkit_name, TOOLKIT_TYPE_META: name, TOOL_NAME_META: tool["name"]} if toolkit_name else {TOOL_NAME_META: tool["name"]}
71
72
  ))
72
73
  return cls(tools=tools)
73
74
 
@@ -9,6 +9,7 @@ from ..elitea_base import filter_missconfigured_index_tools
9
9
  from ..utils import clean_string, get_max_toolkit_length, parse_list, check_connection_response
10
10
  from ...configurations.jira import JiraConfiguration
11
11
  from ...configurations.pgvector import PgVectorConfiguration
12
+ from ...runtime.utils.constants import TOOLKIT_NAME_META, TOOLKIT_TYPE_META, TOOL_NAME_META
12
13
 
13
14
  name = "jira"
14
15
 
@@ -126,7 +127,7 @@ class JiraToolkit(BaseToolkit):
126
127
  name=tool["name"],
127
128
  description=description,
128
129
  args_schema=tool["args_schema"],
129
- metadata={"toolkit_name": toolkit_name} if toolkit_name else {}
130
+ metadata={TOOLKIT_NAME_META: toolkit_name, TOOLKIT_TYPE_META: name, TOOL_NAME_META: tool["name"]} if toolkit_name else {TOOL_NAME_META: tool["name"]}
130
131
  ))
131
132
  return cls(tools=tools)
132
133
 
@@ -6,6 +6,7 @@ from pydantic import BaseModel, ConfigDict, create_model, Field, SecretStr
6
6
  from .api_wrapper import KeycloakApiWrapper
7
7
  from ..base.tool import BaseAction
8
8
  from ..utils import clean_string, get_max_toolkit_length
9
+ from ...runtime.utils.constants import TOOLKIT_NAME_META, TOOL_NAME_META, TOOLKIT_TYPE_META
9
10
 
10
11
  name = "keycloak"
11
12
 
@@ -54,7 +55,7 @@ class KeycloakToolkit(BaseToolkit):
54
55
  name=tool["name"],
55
56
  description=description,
56
57
  args_schema=tool["args_schema"],
57
- metadata={"toolkit_name": toolkit_name} if toolkit_name else {}
58
+ metadata={TOOLKIT_NAME_META: toolkit_name, TOOLKIT_TYPE_META: name, TOOL_NAME_META: tool["name"]} if toolkit_name else {TOOL_NAME_META: tool["name"]}
58
59
  ))
59
60
  return cls(tools=tools)
60
61
 
@@ -5,6 +5,7 @@ from pydantic import BaseModel, ConfigDict, create_model, Field
5
5
 
6
6
  from .local_git import LocalGit
7
7
  from .tool import LocalGitAction
8
+ from ...runtime.utils.constants import TOOLKIT_NAME_META, TOOL_NAME_META, TOOLKIT_TYPE_META
8
9
 
9
10
  name = "localgit"
10
11
 
@@ -55,7 +56,7 @@ class AlitaLocalGitToolkit(BaseToolkit):
55
56
  mode=tool["mode"],
56
57
  description=description,
57
58
  args_schema=tool["args_schema"],
58
- metadata={"toolkit_name": toolkit_name} if toolkit_name else {}
59
+ metadata={TOOLKIT_NAME_META: toolkit_name, TOOLKIT_TYPE_META: name, TOOL_NAME_META: tool["name"]} if toolkit_name else {TOOL_NAME_META: tool["name"]}
59
60
  ))
60
61
  return cls(tools=tools)
61
62
 
@@ -118,7 +118,7 @@ class MemoryToolkit(BaseToolkit):
118
118
  # Add metadata to tools if toolkit_name is provided
119
119
  if toolkit_name:
120
120
  for tool in tools:
121
- tool.metadata = {"toolkit_name": toolkit_name}
121
+ tool.metadata = {"toolkit_name": toolkit_name, "toolkit_type": name}
122
122
 
123
123
  return cls(tools=tools)
124
124
 
@@ -6,6 +6,7 @@ from pydantic import create_model, BaseModel, ConfigDict, Field
6
6
  from .api_wrapper import OCRApiWrapper
7
7
  from ..base.tool import BaseAction
8
8
  from ..utils import clean_string, get_max_toolkit_length
9
+ from ...runtime.utils.constants import TOOLKIT_NAME_META, TOOL_NAME_META, TOOLKIT_TYPE_META
9
10
 
10
11
  name = "ocr"
11
12
 
@@ -59,7 +60,7 @@ class OCRToolkit(BaseToolkit):
59
60
  name=tool["name"],
60
61
  description=description,
61
62
  args_schema=tool["args_schema"],
62
- metadata={"toolkit_name": toolkit_name} if toolkit_name else {}
63
+ metadata={TOOLKIT_NAME_META: toolkit_name, TOOLKIT_TYPE_META: name, TOOL_NAME_META: tool["name"]} if toolkit_name else {TOOL_NAME_META: tool["name"]}
63
64
  ))
64
65
  return cls(tools=tools)
65
66
 
@@ -1,19 +1,218 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import base64
3
4
  import json
4
- from typing import Any, Dict, List, Optional
5
+ import logging
6
+ import threading
7
+ import time
8
+ from typing import Any, Dict, List, Optional, Tuple
9
+ from urllib.parse import urlparse
5
10
 
6
11
  from langchain_core.tools import BaseTool, BaseToolkit
7
12
  from pydantic import BaseModel, ConfigDict, Field, create_model
13
+ import requests
8
14
  import yaml
9
15
 
10
16
  from .api_wrapper import _get_base_url_from_spec, build_wrapper
11
17
  from .tool import OpenApiAction
12
18
  from ..elitea_base import filter_missconfigured_index_tools
13
19
  from ...configurations.openapi import OpenApiConfiguration
20
+ from ...runtime.utils.constants import TOOLKIT_NAME_META, TOOL_NAME_META, TOOLKIT_TYPE_META
21
+
22
+ logger = logging.getLogger(__name__)
14
23
 
15
24
  name = 'openapi'
16
25
 
26
+ # Module-level token cache: {cache_key: (access_token, expires_at_timestamp)}
27
+ # Protected by _oauth_token_cache_lock for thread-safe access
28
+ _oauth_token_cache: Dict[str, Tuple[str, float]] = {}
29
+ _oauth_token_cache_lock = threading.Lock()
30
+
31
+ # Token expiry buffer in seconds (refresh 60 seconds before actual expiry)
32
+ _TOKEN_EXPIRY_BUFFER = 60
33
+
34
+
35
+ def _get_oauth_cache_key(client_id: str, token_url: str, scope: Optional[str]) -> str:
36
+ """Generate a cache key for OAuth tokens."""
37
+ return f"{client_id}:{token_url}:{scope or ''}"
38
+
39
+
40
+ def _get_cached_token(cache_key: str) -> Optional[str]:
41
+ """Get a cached token if it exists and is not expired. Thread-safe."""
42
+ with _oauth_token_cache_lock:
43
+ if cache_key not in _oauth_token_cache:
44
+ return None
45
+ token, expires_at = _oauth_token_cache[cache_key]
46
+ if time.time() >= expires_at - _TOKEN_EXPIRY_BUFFER:
47
+ # Token expired or about to expire
48
+ del _oauth_token_cache[cache_key]
49
+ return None
50
+ return token
51
+
52
+
53
+ def _cache_token(cache_key: str, token: str, expires_in: Optional[int]) -> None:
54
+ """Cache a token with its expiry time. Thread-safe."""
55
+ # Default to 1 hour if expires_in not provided
56
+ expires_in = expires_in or 3600
57
+ expires_at = time.time() + expires_in
58
+ with _oauth_token_cache_lock:
59
+ _oauth_token_cache[cache_key] = (token, expires_at)
60
+
61
+
62
+ def _obtain_oauth_token(
63
+ client_id: str,
64
+ client_secret: str,
65
+ token_url: str,
66
+ scope: Optional[str] = None,
67
+ method: str = 'default',
68
+ timeout: int = 30,
69
+ ) -> Tuple[str, Optional[str]]:
70
+ """
71
+ Obtain an OAuth2 access token using client credentials flow.
72
+
73
+ Args:
74
+ client_id: OAuth client ID
75
+ client_secret: OAuth client secret
76
+ token_url: OAuth token endpoint URL
77
+ scope: Optional OAuth scope(s), space-separated if multiple
78
+ method: Token exchange method - 'default' (POST body) or 'Basic' (Basic auth header)
79
+ timeout: Request timeout in seconds
80
+
81
+ Returns:
82
+ Tuple of (access_token, error_message)
83
+ On success: (token, None)
84
+ On failure: (None, error_message)
85
+ """
86
+ try:
87
+ headers = {
88
+ 'Content-Type': 'application/x-www-form-urlencoded',
89
+ 'Accept': 'application/json',
90
+ }
91
+
92
+ # Build form data
93
+ data: Dict[str, str] = {
94
+ 'grant_type': 'client_credentials',
95
+ }
96
+
97
+ if method == 'Basic':
98
+ # Use Basic auth header for client credentials
99
+ credentials = f"{client_id}:{client_secret}"
100
+ encoded_credentials = base64.b64encode(credentials.encode('utf-8')).decode('utf-8')
101
+ headers['Authorization'] = f'Basic {encoded_credentials}'
102
+ else:
103
+ # Default: include credentials in POST body
104
+ data['client_id'] = client_id
105
+ data['client_secret'] = client_secret
106
+
107
+ if scope:
108
+ data['scope'] = scope
109
+
110
+ # Log only the domain to avoid exposing sensitive path parameters (e.g., tenant IDs)
111
+ token_domain = urlparse(token_url).netloc or 'unknown'
112
+ logger.debug(f"OAuth token request to {token_domain} using method '{method}'")
113
+
114
+ response = requests.post(
115
+ token_url,
116
+ headers=headers,
117
+ data=data,
118
+ timeout=timeout,
119
+ )
120
+
121
+ if response.status_code == 200:
122
+ try:
123
+ token_data = response.json()
124
+ access_token = token_data.get('access_token')
125
+ if not access_token:
126
+ return None, "OAuth response did not contain 'access_token'"
127
+
128
+ # Cache the token
129
+ cache_key = _get_oauth_cache_key(client_id, token_url, scope)
130
+ expires_in = token_data.get('expires_in')
131
+ _cache_token(cache_key, access_token, expires_in)
132
+
133
+ logger.debug(f"OAuth token obtained successfully (expires_in: {expires_in})")
134
+ return access_token, None
135
+ except json.JSONDecodeError as e:
136
+ return None, f"Failed to parse OAuth token response as JSON: {e}"
137
+
138
+ # Handle error responses
139
+ error_msg = f"OAuth token request failed with status {response.status_code}"
140
+ try:
141
+ error_data = response.json()
142
+ if 'error' in error_data:
143
+ error_msg = f"{error_msg}: {error_data.get('error')}"
144
+ if 'error_description' in error_data:
145
+ error_msg = f"{error_msg} - {error_data.get('error_description')}"
146
+ except Exception:
147
+ if response.text:
148
+ error_msg = f"{error_msg}: {response.text[:500]}"
149
+
150
+ return None, error_msg
151
+
152
+ except requests.exceptions.Timeout:
153
+ return None, f"OAuth token request to {token_url} timed out"
154
+ except requests.exceptions.ConnectionError as e:
155
+ return None, f"Failed to connect to OAuth token endpoint {token_url}: {e}"
156
+ except requests.exceptions.RequestException as e:
157
+ return None, f"OAuth token request failed: {e}"
158
+ except Exception as e:
159
+ return None, f"Unexpected error during OAuth token exchange: {e}"
160
+
161
+
162
+ def _secret_to_str(value: Any) -> Optional[str]:
163
+ """Convert a secret value to string, handling SecretStr and other types."""
164
+ if value is None:
165
+ return None
166
+ if hasattr(value, 'get_secret_value'):
167
+ try:
168
+ value = value.get_secret_value()
169
+ except Exception:
170
+ pass
171
+ if isinstance(value, str):
172
+ return value
173
+ return str(value)
174
+
175
+
176
+ def _get_oauth_access_token(settings: Dict[str, Any]) -> Tuple[Optional[str], Optional[str]]:
177
+ """
178
+ Get an OAuth access token from settings, using cache if available.
179
+
180
+ Args:
181
+ settings: Dictionary containing OAuth configuration
182
+
183
+ Returns:
184
+ Tuple of (access_token, error_message)
185
+ On success: (token, None)
186
+ On failure: (None, error_message)
187
+ If OAuth not configured: (None, None)
188
+ """
189
+ client_id = settings.get('client_id')
190
+ client_secret = _secret_to_str(settings.get('client_secret'))
191
+ token_url = settings.get('token_url')
192
+
193
+ # Check if OAuth is configured
194
+ if not client_id or not client_secret or not token_url:
195
+ return None, None # OAuth not configured
196
+
197
+ scope = settings.get('scope')
198
+ method = settings.get('method', 'default') or 'default'
199
+
200
+ # Try to get cached token
201
+ cache_key = _get_oauth_cache_key(client_id, token_url, scope)
202
+ cached_token = _get_cached_token(cache_key)
203
+ if cached_token:
204
+ logger.debug("Using cached OAuth token")
205
+ return cached_token, None
206
+
207
+ # Obtain new token
208
+ return _obtain_oauth_token(
209
+ client_id=client_id,
210
+ client_secret=client_secret,
211
+ token_url=token_url,
212
+ scope=scope,
213
+ method=method,
214
+ )
215
+
17
216
 
18
217
  def get_toolkit(tool) -> BaseToolkit:
19
218
  settings = tool.get('settings', {}) or {}
@@ -221,7 +420,7 @@ class AlitaOpenAPIToolkit(BaseToolkit):
221
420
  name=tool_def['name'],
222
421
  description=description,
223
422
  args_schema=tool_def.get('args_schema'),
224
- metadata={"toolkit_name": toolkit_name} if toolkit_name else {},
423
+ metadata={TOOLKIT_NAME_META: toolkit_name, TOOLKIT_TYPE_META: name, TOOL_NAME_META: tool_def["name"]} if toolkit_name else {TOOL_NAME_META: tool_def["name"]},
225
424
  )
226
425
  )
227
426
 
@@ -249,22 +448,35 @@ def _coerce_selected_tool_names(selected_tools: Any) -> list[str]:
249
448
  return []
250
449
 
251
450
 
252
- def _secret_to_str(value: Any) -> Optional[str]:
253
- if value is None:
254
- return None
255
- if hasattr(value, 'get_secret_value'):
256
- try:
257
- value = value.get_secret_value()
258
- except Exception:
259
- pass
260
- if isinstance(value, str):
261
- return value
262
- return str(value)
263
-
264
-
265
451
  def _build_headers_from_settings(settings: Dict[str, Any]) -> Dict[str, str]:
452
+ """
453
+ Build HTTP headers from settings, supporting API key and OAuth authentication.
454
+
455
+ Authentication priority:
456
+ 1. OAuth (client credentials flow) - if client_id, client_secret, and token_url are provided
457
+ 2. API Key - if api_key is provided
458
+ 3. Legacy authentication structure (for backward compatibility)
459
+
460
+ Args:
461
+ settings: Dictionary containing authentication settings
462
+
463
+ Returns:
464
+ Dictionary of HTTP headers to include in requests
465
+ """
266
466
  headers: Dict[str, str] = {}
267
467
 
468
+ # First, try OAuth authentication (client credentials flow)
469
+ # This takes priority because it's more secure and commonly used with modern APIs
470
+ oauth_token, oauth_error = _get_oauth_access_token(settings)
471
+ if oauth_token:
472
+ headers['Authorization'] = f'Bearer {oauth_token}'
473
+ logger.debug("Using OAuth Bearer token for authentication")
474
+ return headers
475
+ elif oauth_error:
476
+ # OAuth was configured but failed - log the error
477
+ # We'll still try API key auth as fallback
478
+ logger.warning(f"OAuth token exchange failed: {oauth_error}")
479
+
268
480
  # Legacy structure used by the custom OpenAPI UI
269
481
  auth = settings.get('authentication')
270
482
  if isinstance(auth, dict) and auth.get('type') == 'api_key':