alita-sdk 0.3.528__py3-none-any.whl → 0.3.554__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 (46) hide show
  1. alita_sdk/community/__init__.py +8 -4
  2. alita_sdk/configurations/__init__.py +1 -0
  3. alita_sdk/configurations/openapi.py +111 -0
  4. alita_sdk/runtime/clients/client.py +185 -10
  5. alita_sdk/runtime/langchain/langraph_agent.py +2 -2
  6. alita_sdk/runtime/langchain/utils.py +46 -0
  7. alita_sdk/runtime/skills/__init__.py +91 -0
  8. alita_sdk/runtime/skills/callbacks.py +498 -0
  9. alita_sdk/runtime/skills/discovery.py +540 -0
  10. alita_sdk/runtime/skills/executor.py +610 -0
  11. alita_sdk/runtime/skills/input_builder.py +371 -0
  12. alita_sdk/runtime/skills/models.py +330 -0
  13. alita_sdk/runtime/skills/registry.py +355 -0
  14. alita_sdk/runtime/skills/skill_runner.py +330 -0
  15. alita_sdk/runtime/toolkits/__init__.py +2 -0
  16. alita_sdk/runtime/toolkits/skill_router.py +238 -0
  17. alita_sdk/runtime/toolkits/tools.py +76 -9
  18. alita_sdk/runtime/tools/__init__.py +3 -1
  19. alita_sdk/runtime/tools/artifact.py +70 -21
  20. alita_sdk/runtime/tools/image_generation.py +50 -44
  21. alita_sdk/runtime/tools/llm.py +363 -44
  22. alita_sdk/runtime/tools/loop.py +3 -1
  23. alita_sdk/runtime/tools/loop_output.py +3 -1
  24. alita_sdk/runtime/tools/skill_router.py +776 -0
  25. alita_sdk/runtime/tools/tool.py +3 -1
  26. alita_sdk/runtime/tools/vectorstore.py +7 -2
  27. alita_sdk/runtime/tools/vectorstore_base.py +7 -2
  28. alita_sdk/runtime/utils/AlitaCallback.py +2 -1
  29. alita_sdk/runtime/utils/utils.py +34 -0
  30. alita_sdk/tools/__init__.py +41 -1
  31. alita_sdk/tools/ado/work_item/ado_wrapper.py +33 -2
  32. alita_sdk/tools/base_indexer_toolkit.py +36 -24
  33. alita_sdk/tools/confluence/api_wrapper.py +5 -6
  34. alita_sdk/tools/confluence/loader.py +4 -2
  35. alita_sdk/tools/openapi/__init__.py +280 -120
  36. alita_sdk/tools/openapi/api_wrapper.py +883 -0
  37. alita_sdk/tools/openapi/tool.py +20 -0
  38. alita_sdk/tools/pandas/dataframe/generator/base.py +3 -1
  39. alita_sdk/tools/servicenow/__init__.py +9 -9
  40. alita_sdk/tools/servicenow/api_wrapper.py +1 -1
  41. {alita_sdk-0.3.528.dist-info → alita_sdk-0.3.554.dist-info}/METADATA +2 -2
  42. {alita_sdk-0.3.528.dist-info → alita_sdk-0.3.554.dist-info}/RECORD +46 -33
  43. {alita_sdk-0.3.528.dist-info → alita_sdk-0.3.554.dist-info}/WHEEL +0 -0
  44. {alita_sdk-0.3.528.dist-info → alita_sdk-0.3.554.dist-info}/entry_points.txt +0 -0
  45. {alita_sdk-0.3.528.dist-info → alita_sdk-0.3.554.dist-info}/licenses/LICENSE +0 -0
  46. {alita_sdk-0.3.528.dist-info → alita_sdk-0.3.554.dist-info}/top_level.txt +0 -0
@@ -1,140 +1,300 @@
1
+ from __future__ import annotations
2
+
1
3
  import json
2
- import re
3
- import logging
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ from langchain_core.tools import BaseTool, BaseToolkit
7
+ from pydantic import BaseModel, ConfigDict, Field, create_model
4
8
  import yaml
5
- from typing import List, Any, Optional, Dict
6
- from langchain_core.tools import BaseTool, BaseToolkit, ToolException
7
- from requests_openapi import Operation, Client, Server
8
9
 
9
- from pydantic import create_model, Field
10
- from functools import partial
10
+ from .api_wrapper import _get_base_url_from_spec, build_wrapper
11
+ from .tool import OpenApiAction
12
+ from ..elitea_base import filter_missconfigured_index_tools
13
+ from ...configurations.openapi import OpenApiConfiguration
11
14
 
12
- logger = logging.getLogger(__name__)
15
+ name = 'openapi'
13
16
 
14
- name = "openapi"
15
17
 
16
- def get_tools(tool):
17
- headers = {}
18
- if tool['settings'].get('authentication'):
19
- if tool['settings']['authentication']['type'] == 'api_key':
20
- auth_type = tool['settings']['authentication']['settings']['auth_type']
21
- auth_key = tool["settings"]["authentication"]["settings"]["api_key"]
22
- if auth_type.lower() == 'bearer':
23
- headers['Authorization'] = f'Bearer {auth_key}'
24
- if auth_type.lower() == 'basic':
25
- headers['Authorization'] = f'Basic {auth_key}'
26
- if auth_type.lower() == 'custom':
27
- headers[
28
- tool["settings"]["authentication"]["settings"]["custom_header_name"]] = f'{auth_key}'
18
+ def get_toolkit(tool) -> BaseToolkit:
19
+ settings = tool.get('settings', {}) or {}
20
+ # Extract selected_tools separately to avoid duplicate keyword argument when unpacking **settings
21
+ selected_tools = settings.get('selected_tools', [])
22
+ # Filter out selected_tools from settings to prevent "got multiple values for keyword argument"
23
+ filtered_settings = {k: v for k, v in settings.items() if k != 'selected_tools'}
29
24
  return AlitaOpenAPIToolkit.get_toolkit(
30
- openapi_spec=tool['settings']['schema_settings'],
31
- selected_tools=tool['settings'].get('selected_tools', []),
32
- headers=headers).get_tools()
33
-
34
-
35
- def create_api_tool(name: str, op: Operation):
36
- fields = {}
37
- headers = {}
38
- headers_descriptions = []
39
-
40
- for parameter in op.spec.parameters:
41
- if "header" in parameter.param_in:
42
- headers[parameter.name] = parameter.param_schema.default
43
- headers_descriptions.append(f"Header: {parameter.name}. Description: {parameter.description}.")
44
- continue
45
- fields[parameter.name] = (str, Field(default=parameter.param_schema.default,
46
- description=parameter.description))
47
-
48
- # add headers
49
- if headers:
50
- fields['headers'] = (Optional[dict], Field(default = headers, description="The dict that represents headers for request:\n" + '\n'.join(headers_descriptions)))
51
-
52
- if op.spec.requestBody:
53
- fields['json'] = (Optional[str], Field(default = None, description="JSON request body provided as a string"))
54
-
55
- op.server = Server.from_openapi_server(op.server) # patch this
56
- op.server.get_url = partial(Server.get_url, op.server)
57
- op.server.set_url = partial(Server.set_url, op.server)
58
- return ApiTool(
59
- name=name,
60
- description=op.spec.description if op.spec.description else op.spec.summary,
61
- args_schema=create_model(
62
- 'request_params',
63
- regexp = (Optional[str], Field(description="Regular expression used to remove from final output if any", default=None)),
64
- **fields),
65
- callable=op
25
+ selected_tools=selected_tools,
26
+ toolkit_name=tool.get('toolkit_name'),
27
+ **filtered_settings,
66
28
  )
67
29
 
68
- class ApiTool(BaseTool):
69
- name: str
70
- description: str
71
- callable: Operation
72
-
73
- def _run(self, regexp: str = None, **kwargs):
74
- # set in query parameter from header (actual for authentication)
75
- rq_args = self.args.keys()
76
- headers = self.callable.requestor.headers
77
- for arg in rq_args:
78
- arg_value = headers.get(arg)
79
- if arg_value:
80
- kwargs.update({arg : arg_value})
81
-
82
- if kwargs.get("json"):
83
- # add json to payload
84
- kwargs.update({"json": json.loads(kwargs.get("json"))})
85
- output = self.callable(**kwargs).content
30
+
31
+ def get_tools(tool):
32
+ return get_toolkit(tool).get_tools()
33
+
34
+
35
+ def get_toolkit_available_tools(settings: dict) -> dict:
36
+ """Return instance-dependent tool list + per-tool args JSON schemas.
37
+
38
+ This is used by backend services when the UI needs spec-derived tool names
39
+ and input schemas (one tool per operationId). It must be JSON-serializable.
40
+ """
41
+ if not isinstance(settings, dict):
42
+ settings = {}
43
+
44
+ # Extract and merge openapi_configuration if present (same pattern as get_toolkit)
45
+ openapi_configuration = settings.get('openapi_configuration') or {}
46
+ if hasattr(openapi_configuration, 'model_dump'):
47
+ openapi_configuration = openapi_configuration.model_dump(mode='json')
48
+ if not isinstance(openapi_configuration, dict):
49
+ openapi_configuration = {}
50
+
51
+ # Merge settings with openapi_configuration so api_key, auth_type etc. are at root level
52
+ merged_settings: Dict[str, Any] = {
53
+ **settings,
54
+ **openapi_configuration,
55
+ }
56
+
57
+ spec = merged_settings.get('spec') or merged_settings.get('schema_settings') or merged_settings.get('openapi_spec')
58
+ base_url_override = merged_settings.get('base_url') or merged_settings.get('base_url_override')
59
+
60
+ if not spec or not isinstance(spec, (str, dict)):
61
+ return {"tools": [], "args_schemas": {}, "error": "OpenAPI spec is missing"}
62
+
63
+ try:
64
+ headers = _build_headers_from_settings(merged_settings)
65
+ api_wrapper = build_wrapper(
66
+ openapi_spec=spec,
67
+ base_headers=headers,
68
+ base_url_override=base_url_override,
69
+ )
70
+
71
+ tool_defs = api_wrapper.get_available_tools(selected_tools=None)
72
+
73
+ tools = []
74
+ args_schemas = {}
75
+
76
+ for tool_def in tool_defs:
77
+ name_val = tool_def.get('name')
78
+ if not isinstance(name_val, str) or not name_val:
79
+ continue
80
+
81
+ desc_val = tool_def.get('description')
82
+ if not isinstance(desc_val, str):
83
+ desc_val = ''
84
+
85
+ tools.append({"name": name_val, "description": desc_val})
86
+
87
+ args_schema = tool_def.get('args_schema')
88
+ if args_schema is None:
89
+ args_schemas[name_val] = {"type": "object", "properties": {}, "required": []}
90
+ continue
91
+
92
+ try:
93
+ if hasattr(args_schema, 'model_json_schema'):
94
+ args_schemas[name_val] = args_schema.model_json_schema()
95
+ elif hasattr(args_schema, 'schema'):
96
+ args_schemas[name_val] = args_schema.schema()
97
+ else:
98
+ args_schemas[name_val] = {"type": "object", "properties": {}, "required": []}
99
+ except Exception:
100
+ args_schemas[name_val] = {"type": "object", "properties": {}, "required": []}
101
+
102
+ # Ensure stable JSON-serializability.
86
103
  try:
87
- if regexp is not None:
88
- output = re.sub(rf'{regexp}', "", str(output))
89
- finally:
90
- return output
104
+ json.dumps({"tools": tools, "args_schemas": args_schemas})
105
+ except Exception:
106
+ return {"tools": tools, "args_schemas": {}}
107
+
108
+ return {"tools": tools, "args_schemas": args_schemas}
109
+
110
+ except Exception as e: # pylint: disable=W0718
111
+ return {"tools": [], "args_schemas": {}, "error": str(e)}
91
112
 
92
113
  class AlitaOpenAPIToolkit(BaseToolkit):
93
114
  request_session: Any #: :meta private:
94
115
  tools: List[BaseTool] = []
95
116
 
117
+ @staticmethod
118
+ def toolkit_config_schema() -> BaseModel:
119
+ # OpenAPI tool names + per-tool args schemas depend on the user-provided spec,
120
+ # so `selected_tools` cannot be an enum here (unlike most toolkits).
121
+
122
+ model = create_model(
123
+ name,
124
+ __config__=ConfigDict(
125
+ extra='ignore',
126
+ json_schema_extra={
127
+ 'metadata': {
128
+ 'label': 'OpenAPI',
129
+ 'icon_url': 'openapi.svg',
130
+ 'categories': ['integrations'],
131
+ 'extra_categories': ['api', 'openapi', 'swagger'],
132
+ }
133
+ }
134
+ ),
135
+ openapi_configuration=(
136
+ OpenApiConfiguration,
137
+ Field(
138
+ description='OpenAPI credentials configuration',
139
+ json_schema_extra={'configuration_types': ['openapi']},
140
+ ),
141
+ ),
142
+ base_url=(
143
+ Optional[str],
144
+ Field(
145
+ default=None,
146
+ description=(
147
+ "Optional base URL override (absolute, starting with http:// or https://). "
148
+ "Use this when your OpenAPI spec has no `servers` entry, or when `servers[0].url` "
149
+ "is not absolute (e.g. '/api/v3'). Example: 'https://petstore3.swagger.io'."
150
+ ),
151
+ ),
152
+ ),
153
+ spec=(
154
+ str,
155
+ Field(
156
+ description=(
157
+ 'OpenAPI specification (URL or raw JSON/YAML text). '
158
+ 'Used to generate per-operation tools (one tool per operationId).'
159
+ ),
160
+ json_schema_extra={'ui_component': 'openapi_spec'},
161
+ ),
162
+ ),
163
+ selected_tools=(
164
+ List[str],
165
+ Field(
166
+ default=[],
167
+ description='Optional list of operationIds to enable. If empty, all operations are enabled.',
168
+ json_schema_extra={'args_schemas': {}},
169
+ ),
170
+ ),
171
+ )
172
+ return model
173
+
96
174
  @classmethod
97
- def get_toolkit(cls, openapi_spec: str | dict,
98
- selected_tools: list[dict] | None = None,
99
- headers: Optional[Dict[str, str]] = None,
100
- toolkit_name: Optional[str] = None):
101
- if selected_tools is not None:
102
- tools_set = set([i if not isinstance(i, dict) else i.get('name') for i in selected_tools])
103
- else:
104
- tools_set = {}
105
- if isinstance(openapi_spec, str):
106
- # Try to detect if it's YAML or JSON by attempting to parse as JSON first
107
- try:
108
- openapi_spec = json.loads(openapi_spec)
109
- except json.JSONDecodeError:
110
- # If JSON parsing fails, try YAML
111
- try:
112
- openapi_spec = yaml.safe_load(openapi_spec)
113
- except yaml.YAMLError as e:
114
- raise ToolException(f"Failed to parse OpenAPI spec as JSON or YAML: {e}")
115
- c = Client()
116
- c.load_spec(openapi_spec)
117
- if headers:
118
- c.requestor.headers.update(headers)
119
- tools = []
120
- for i in tools_set:
175
+ @filter_missconfigured_index_tools
176
+ def get_toolkit(
177
+ cls,
178
+ selected_tools: list[str] | None = None,
179
+ toolkit_name: Optional[str] = None,
180
+ **kwargs,
181
+ ):
182
+ if selected_tools is None:
183
+ selected_tools = []
121
184
 
122
- try:
123
- if not i:
124
- raise ToolException("Operation id is missing for some of declared operations.")
125
- tool = c.operations[i]
126
- if not isinstance(tool, Operation):
127
- raise ToolException(f"Operation {i} is not an instance of Operation class.")
128
- api_tool = create_api_tool(i, tool)
129
- if toolkit_name:
130
- api_tool.metadata = {"toolkit_name": toolkit_name}
131
- tools.append(api_tool)
132
- except ToolException:
133
- raise
134
- except Exception as e:
135
- logger.warning(f"Tool {i} not found in OpenAPI spec.")
136
- raise ToolException(f"Cannot create API tool ({i}): \n{e}.")
137
- return cls(request_session=c, tools=tools)
185
+ tool_names = _coerce_selected_tool_names(selected_tools)
186
+
187
+ openapi_configuration = kwargs.get('openapi_configuration') or {}
188
+ if hasattr(openapi_configuration, 'model_dump'):
189
+ openapi_configuration = openapi_configuration.model_dump(mode='json')
190
+ if not isinstance(openapi_configuration, dict):
191
+ openapi_configuration = {}
192
+
193
+ merged_settings: Dict[str, Any] = {
194
+ **kwargs,
195
+ **openapi_configuration,
196
+ }
197
+
198
+ openapi_spec = merged_settings.get('spec') or merged_settings.get('schema_settings') or merged_settings.get('openapi_spec')
199
+ base_url_override = merged_settings.get('base_url') or merged_settings.get('base_url_override')
200
+ headers = _build_headers_from_settings(merged_settings)
201
+
202
+ api_wrapper = build_wrapper(
203
+ openapi_spec=openapi_spec,
204
+ base_headers=headers,
205
+ base_url_override=base_url_override,
206
+ )
207
+ base_url = _get_base_url_from_spec(api_wrapper.spec)
208
+
209
+ tools: List[BaseTool] = []
210
+ for tool_def in api_wrapper.get_available_tools(selected_tools=tool_names):
211
+ description = tool_def.get('description') or ''
212
+ if toolkit_name:
213
+ description = f"{description}\nToolkit: {toolkit_name}"
214
+ if base_url:
215
+ description = f"{description}\nBase URL: {base_url}"
216
+ description = description[:1000]
217
+
218
+ tools.append(
219
+ OpenApiAction(
220
+ api_wrapper=api_wrapper,
221
+ name=tool_def['name'],
222
+ description=description,
223
+ args_schema=tool_def.get('args_schema'),
224
+ metadata={"toolkit_name": toolkit_name} if toolkit_name else {},
225
+ )
226
+ )
227
+
228
+ return cls(request_session=api_wrapper, tools=tools)
138
229
 
139
230
  def get_tools(self):
140
231
  return self.tools
232
+
233
+
234
+ def _coerce_selected_tool_names(selected_tools: Any) -> list[str]:
235
+ if not selected_tools:
236
+ return []
237
+
238
+ if isinstance(selected_tools, list):
239
+ tool_names: List[str] = []
240
+ for item in selected_tools:
241
+ if isinstance(item, str):
242
+ tool_names.append(item)
243
+ elif isinstance(item, dict):
244
+ name_val = item.get('name')
245
+ if isinstance(name_val, str) and name_val.strip():
246
+ tool_names.append(name_val)
247
+ return [t for t in tool_names if t]
248
+
249
+ return []
250
+
251
+
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
+ def _build_headers_from_settings(settings: Dict[str, Any]) -> Dict[str, str]:
266
+ headers: Dict[str, str] = {}
267
+
268
+ # Legacy structure used by the custom OpenAPI UI
269
+ auth = settings.get('authentication')
270
+ if isinstance(auth, dict) and auth.get('type') == 'api_key':
271
+ auth_settings = auth.get('settings') or {}
272
+ if isinstance(auth_settings, dict):
273
+ auth_type = str(auth_settings.get('auth_type', '')).strip().lower()
274
+ api_key = _secret_to_str(auth_settings.get('api_key'))
275
+ if api_key:
276
+ if auth_type == 'bearer':
277
+ headers['Authorization'] = f'Bearer {api_key}'
278
+ elif auth_type == 'basic':
279
+ headers['Authorization'] = f'Basic {api_key}'
280
+ elif auth_type == 'custom':
281
+ header_name = auth_settings.get('custom_header_name')
282
+ if header_name:
283
+ headers[str(header_name)] = f'{api_key}'
284
+
285
+ # New regular-schema structure (GitHub-style sections) uses flattened fields
286
+ if not headers:
287
+ api_key = _secret_to_str(settings.get('api_key'))
288
+ if api_key:
289
+ auth_type = str(settings.get('auth_type', 'Bearer'))
290
+ auth_type_norm = auth_type.strip().lower()
291
+ if auth_type_norm == 'bearer':
292
+ headers['Authorization'] = f'Bearer {api_key}'
293
+ elif auth_type_norm == 'basic':
294
+ headers['Authorization'] = f'Basic {api_key}'
295
+ elif auth_type_norm == 'custom':
296
+ header_name = settings.get('custom_header_name')
297
+ if header_name:
298
+ headers[str(header_name)] = f'{api_key}'
299
+
300
+ return headers