ibm-watsonx-orchestrate 1.0.0__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.
- ibm_watsonx_orchestrate/__init__.py +28 -0
- ibm_watsonx_orchestrate/agent_builder/__init__.py +0 -0
- ibm_watsonx_orchestrate/agent_builder/agents/__init__.py +5 -0
- ibm_watsonx_orchestrate/agent_builder/agents/agent.py +27 -0
- ibm_watsonx_orchestrate/agent_builder/agents/assistant_agent.py +28 -0
- ibm_watsonx_orchestrate/agent_builder/agents/external_agent.py +28 -0
- ibm_watsonx_orchestrate/agent_builder/agents/types.py +204 -0
- ibm_watsonx_orchestrate/agent_builder/connections/__init__.py +27 -0
- ibm_watsonx_orchestrate/agent_builder/connections/connections.py +123 -0
- ibm_watsonx_orchestrate/agent_builder/connections/types.py +260 -0
- ibm_watsonx_orchestrate/agent_builder/knowledge_bases/knowledge_base.py +27 -0
- ibm_watsonx_orchestrate/agent_builder/knowledge_bases/knowledge_base_requests.py +59 -0
- ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +243 -0
- ibm_watsonx_orchestrate/agent_builder/tools/__init__.py +4 -0
- ibm_watsonx_orchestrate/agent_builder/tools/base_tool.py +36 -0
- ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +332 -0
- ibm_watsonx_orchestrate/agent_builder/tools/python_tool.py +195 -0
- ibm_watsonx_orchestrate/agent_builder/tools/types.py +162 -0
- ibm_watsonx_orchestrate/agent_builder/utils/__init__.py +0 -0
- ibm_watsonx_orchestrate/agent_builder/utils/pydantic_utils.py +149 -0
- ibm_watsonx_orchestrate/cli/__init__.py +0 -0
- ibm_watsonx_orchestrate/cli/commands/__init__.py +0 -0
- ibm_watsonx_orchestrate/cli/commands/agents/agents_command.py +192 -0
- ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +660 -0
- ibm_watsonx_orchestrate/cli/commands/channels/channels_command.py +15 -0
- ibm_watsonx_orchestrate/cli/commands/channels/channels_controller.py +16 -0
- ibm_watsonx_orchestrate/cli/commands/channels/types.py +15 -0
- ibm_watsonx_orchestrate/cli/commands/channels/webchat/channels_webchat_command.py +32 -0
- ibm_watsonx_orchestrate/cli/commands/channels/webchat/channels_webchat_controller.py +141 -0
- ibm_watsonx_orchestrate/cli/commands/chat/chat_command.py +43 -0
- ibm_watsonx_orchestrate/cli/commands/connections/connections_command.py +307 -0
- ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +517 -0
- ibm_watsonx_orchestrate/cli/commands/environment/environment_command.py +78 -0
- ibm_watsonx_orchestrate/cli/commands/environment/environment_controller.py +189 -0
- ibm_watsonx_orchestrate/cli/commands/environment/types.py +9 -0
- ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_command.py +79 -0
- ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +201 -0
- ibm_watsonx_orchestrate/cli/commands/login/login_command.py +17 -0
- ibm_watsonx_orchestrate/cli/commands/models/models_command.py +128 -0
- ibm_watsonx_orchestrate/cli/commands/server/server_command.py +623 -0
- ibm_watsonx_orchestrate/cli/commands/settings/__init__.py +0 -0
- ibm_watsonx_orchestrate/cli/commands/settings/observability/__init__.py +0 -0
- ibm_watsonx_orchestrate/cli/commands/settings/observability/langfuse/__init__.py +0 -0
- ibm_watsonx_orchestrate/cli/commands/settings/observability/langfuse/langfuse_command.py +175 -0
- ibm_watsonx_orchestrate/cli/commands/settings/observability/observability_command.py +11 -0
- ibm_watsonx_orchestrate/cli/commands/settings/settings_command.py +10 -0
- ibm_watsonx_orchestrate/cli/commands/tools/tools_command.py +85 -0
- ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +564 -0
- ibm_watsonx_orchestrate/cli/commands/tools/types.py +10 -0
- ibm_watsonx_orchestrate/cli/config.py +226 -0
- ibm_watsonx_orchestrate/cli/main.py +32 -0
- ibm_watsonx_orchestrate/client/__init__.py +0 -0
- ibm_watsonx_orchestrate/client/agents/agent_client.py +46 -0
- ibm_watsonx_orchestrate/client/agents/assistant_agent_client.py +38 -0
- ibm_watsonx_orchestrate/client/agents/external_agent_client.py +38 -0
- ibm_watsonx_orchestrate/client/analytics/__init__.py +0 -0
- ibm_watsonx_orchestrate/client/analytics/llm/__init__.py +0 -0
- ibm_watsonx_orchestrate/client/analytics/llm/analytics_llm_client.py +50 -0
- ibm_watsonx_orchestrate/client/base_api_client.py +113 -0
- ibm_watsonx_orchestrate/client/base_service_instance.py +10 -0
- ibm_watsonx_orchestrate/client/client.py +71 -0
- ibm_watsonx_orchestrate/client/client_errors.py +359 -0
- ibm_watsonx_orchestrate/client/connections/__init__.py +10 -0
- ibm_watsonx_orchestrate/client/connections/connections_client.py +162 -0
- ibm_watsonx_orchestrate/client/connections/utils.py +27 -0
- ibm_watsonx_orchestrate/client/credentials.py +123 -0
- ibm_watsonx_orchestrate/client/knowledge_bases/knowledge_base_client.py +46 -0
- ibm_watsonx_orchestrate/client/local_service_instance.py +91 -0
- ibm_watsonx_orchestrate/client/service_instance.py +73 -0
- ibm_watsonx_orchestrate/client/tools/tool_client.py +41 -0
- ibm_watsonx_orchestrate/client/utils.py +95 -0
- ibm_watsonx_orchestrate/docker/compose-lite.yml +595 -0
- ibm_watsonx_orchestrate/docker/default.env +125 -0
- ibm_watsonx_orchestrate/docker/sdk/ibm_watsonx_orchestrate-0.6.0-py3-none-any.whl +0 -0
- ibm_watsonx_orchestrate/docker/sdk/ibm_watsonx_orchestrate-0.6.0.tar.gz +0 -0
- ibm_watsonx_orchestrate/docker/start-up.sh +61 -0
- ibm_watsonx_orchestrate/docker/tempus/common-config.yaml +1 -0
- ibm_watsonx_orchestrate/run/__init__.py +0 -0
- ibm_watsonx_orchestrate/run/connections.py +40 -0
- ibm_watsonx_orchestrate/utils/__init__.py +0 -0
- ibm_watsonx_orchestrate/utils/logging/__init__.py +0 -0
- ibm_watsonx_orchestrate/utils/logging/logger.py +26 -0
- ibm_watsonx_orchestrate/utils/logging/logging.yaml +18 -0
- ibm_watsonx_orchestrate/utils/utils.py +15 -0
- ibm_watsonx_orchestrate-1.0.0.dist-info/METADATA +34 -0
- ibm_watsonx_orchestrate-1.0.0.dist-info/RECORD +89 -0
- ibm_watsonx_orchestrate-1.0.0.dist-info/WHEEL +4 -0
- ibm_watsonx_orchestrate-1.0.0.dist-info/entry_points.txt +2 -0
- ibm_watsonx_orchestrate-1.0.0.dist-info/licenses/LICENSE +22 -0
@@ -0,0 +1,564 @@
|
|
1
|
+
import logging
|
2
|
+
import asyncio
|
3
|
+
import importlib
|
4
|
+
import inspect
|
5
|
+
import sys
|
6
|
+
import re
|
7
|
+
import tempfile
|
8
|
+
import requests
|
9
|
+
import zipfile
|
10
|
+
from enum import Enum
|
11
|
+
from os import path
|
12
|
+
from pathlib import Path
|
13
|
+
from typing import Iterable, List
|
14
|
+
import rich
|
15
|
+
import json
|
16
|
+
from rich.json import JSON
|
17
|
+
import glob
|
18
|
+
|
19
|
+
import rich.table
|
20
|
+
import typer
|
21
|
+
|
22
|
+
from ibm_watsonx_orchestrate.agent_builder.tools import BaseTool, ToolSpec
|
23
|
+
from ibm_watsonx_orchestrate.agent_builder.tools.openapi_tool import create_openapi_json_tools_from_uri
|
24
|
+
from ibm_watsonx_orchestrate.cli.commands.tools.types import RegistryType
|
25
|
+
from ibm_watsonx_orchestrate.cli.config import Config, CONTEXT_SECTION_HEADER, CONTEXT_ACTIVE_ENV_OPT, \
|
26
|
+
PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_TYPE_OPT, PYTHON_REGISTRY_TEST_PACKAGE_VERSION_OVERRIDE_OPT, \
|
27
|
+
DEFAULT_CONFIG_FILE_CONTENT
|
28
|
+
from ibm_watsonx_orchestrate.agent_builder.connections import ConnectionSecurityScheme, ExpectedCredentials
|
29
|
+
from ibm_watsonx_orchestrate.client.tools.tool_client import ToolClient
|
30
|
+
from ibm_watsonx_orchestrate.client.connections import get_connections_client, get_connection_type
|
31
|
+
from ibm_watsonx_orchestrate.client.utils import instantiate_client
|
32
|
+
from ibm_watsonx_orchestrate.utils.utils import sanatize_app_id
|
33
|
+
|
34
|
+
logger = logging.getLogger(__name__)
|
35
|
+
|
36
|
+
__supported_characters_pattern = re.compile("^(\\w|_)+$")
|
37
|
+
|
38
|
+
|
39
|
+
class ToolKind(str, Enum):
|
40
|
+
openapi = "openapi"
|
41
|
+
python = "python"
|
42
|
+
# skill = "skill"
|
43
|
+
|
44
|
+
def validate_app_ids(kind: ToolKind, **args) -> None:
|
45
|
+
app_ids = args.get("app_id")
|
46
|
+
if not app_ids:
|
47
|
+
return
|
48
|
+
|
49
|
+
if kind == ToolKind.openapi:
|
50
|
+
if app_ids and len(app_ids) > 1:
|
51
|
+
raise typer.BadParameter(
|
52
|
+
"Kind 'openapi' can only take one app-id"
|
53
|
+
)
|
54
|
+
|
55
|
+
connections_client = get_connections_client()
|
56
|
+
|
57
|
+
imported_connections_list = connections_client.list()
|
58
|
+
imported_connections = {conn.app_id:conn for conn in imported_connections_list}
|
59
|
+
|
60
|
+
for app_id in app_ids:
|
61
|
+
if kind == ToolKind.python:
|
62
|
+
# Split on = but not on \=
|
63
|
+
split_pattern = re.compile(r"(?<!\\)=")
|
64
|
+
split_id = re.split(split_pattern, app_id)
|
65
|
+
split_id = [x.replace("\\=", "=") for x in split_id]
|
66
|
+
if len(split_id) == 2:
|
67
|
+
_, app_id = split_id
|
68
|
+
elif len(split_id) == 1:
|
69
|
+
app_id = split_id[0]
|
70
|
+
else:
|
71
|
+
raise typer.BadParameter(f"The provided --app-id '{app_id}' is not valid. This is likely caused by having mutliple equal signs, please use '\\=' to represent a literal '=' character")
|
72
|
+
|
73
|
+
if app_id not in imported_connections:
|
74
|
+
logger.warning(f"No connection found for provided app-id '{app_id}'. Please create the connection using `orchestrate connections add`")
|
75
|
+
else:
|
76
|
+
if kind == ToolKind.openapi and imported_connections.get(app_id).security_scheme == ConnectionSecurityScheme.KEY_VALUE:
|
77
|
+
logger.error(f"Key value application connections can not be bound to an openapi tool")
|
78
|
+
exit(1)
|
79
|
+
|
80
|
+
def validate_params(kind: ToolKind, **args) -> None:
|
81
|
+
if kind in {"openapi", "python"} and args["file"] is None:
|
82
|
+
raise typer.BadParameter(
|
83
|
+
"--file (-f) is required when kind is set to either python or openapi"
|
84
|
+
)
|
85
|
+
elif kind == "skill":
|
86
|
+
missing_params = []
|
87
|
+
if args["skillset_id"] is None:
|
88
|
+
missing_params.append("--skillset_id")
|
89
|
+
if args["skill_id"] is None:
|
90
|
+
missing_params.append("--skill_id")
|
91
|
+
if args["skill_operation_path"] is None:
|
92
|
+
missing_params.append("--skill_operation_path")
|
93
|
+
|
94
|
+
if len(missing_params) > 0:
|
95
|
+
raise typer.BadParameter(
|
96
|
+
f"Missing flags {missing_params} required for kind skill"
|
97
|
+
)
|
98
|
+
validate_app_ids(kind=kind, **args)
|
99
|
+
|
100
|
+
def get_connection_id(app_id: str) -> str:
|
101
|
+
connections_client = get_connections_client()
|
102
|
+
connection_id = None
|
103
|
+
if app_id is not None:
|
104
|
+
connection = connections_client.get(app_id=app_id)
|
105
|
+
if not connection:
|
106
|
+
logger.error(f"No connection exists with the app-id '{app_id}'")
|
107
|
+
exit(1)
|
108
|
+
connection_id = connection.connection_id
|
109
|
+
return connection_id
|
110
|
+
|
111
|
+
|
112
|
+
def parse_python_app_ids(app_ids: List[str]) -> dict[str,str]:
|
113
|
+
app_id_dict = {}
|
114
|
+
for app_id in app_ids:
|
115
|
+
# Split on = but not on \=
|
116
|
+
split_pattern = re.compile(r"(?<!\\)=")
|
117
|
+
split_id = re.split(split_pattern, app_id)
|
118
|
+
split_id = [x.replace("\\=", "=") for x in split_id]
|
119
|
+
if len(split_id) == 2:
|
120
|
+
runtime_id, local_id = split_id
|
121
|
+
elif len(split_id) == 1:
|
122
|
+
runtime_id = split_id[0]
|
123
|
+
local_id = split_id[0]
|
124
|
+
else:
|
125
|
+
raise typer.BadParameter(f"The provided --app-id '{app_id}' is not valid. This is likely caused by having mutliple equal signs, please use '\\=' to represent a literal '=' character")
|
126
|
+
|
127
|
+
if not len(runtime_id.strip()) or not len(local_id.strip()):
|
128
|
+
raise typer.BadParameter(f"The provided --app-id '{app_id}' is not valid. --app-id cannot be empty or whitespace")
|
129
|
+
|
130
|
+
runtime_id = sanatize_app_id(runtime_id)
|
131
|
+
app_id_dict[runtime_id] = get_connection_id(local_id)
|
132
|
+
|
133
|
+
return app_id_dict
|
134
|
+
|
135
|
+
def validate_python_connections(tool: BaseTool):
|
136
|
+
if not tool.expected_credentials:
|
137
|
+
return
|
138
|
+
|
139
|
+
connections_client = get_connections_client()
|
140
|
+
connections = tool.__tool_spec__.binding.python.connections
|
141
|
+
|
142
|
+
provided_connections = list(connections.keys()) if connections else []
|
143
|
+
imported_connections_list = connections_client.list()
|
144
|
+
imported_connections = {conn.connection_id:conn for conn in imported_connections_list}
|
145
|
+
|
146
|
+
validation_failed = False
|
147
|
+
|
148
|
+
existing_sanatized_expected_tool_app_ids = set()
|
149
|
+
|
150
|
+
for expected_cred in tool.expected_credentials:
|
151
|
+
|
152
|
+
expected_tool_app_id = expected_cred.app_id
|
153
|
+
if isinstance(expected_cred.type, List):
|
154
|
+
expected_tool_conn_types = expected_cred.type
|
155
|
+
else:
|
156
|
+
expected_tool_conn_types = [expected_cred.type]
|
157
|
+
|
158
|
+
sanatized_expected_tool_app_id = sanatize_app_id(expected_tool_app_id)
|
159
|
+
if sanatized_expected_tool_app_id in existing_sanatized_expected_tool_app_ids:
|
160
|
+
logger.error(f"Duplicate App ID found '{expected_tool_app_id}'. Multiple expected app ids in the tool '{tool.__tool_spec__.name}' collide after sanaitization to '{sanatized_expected_tool_app_id}'. Please rename the offending app id in your tool.")
|
161
|
+
sys.exit(1)
|
162
|
+
existing_sanatized_expected_tool_app_ids.add(sanatized_expected_tool_app_id)
|
163
|
+
|
164
|
+
if sanatized_expected_tool_app_id not in provided_connections:
|
165
|
+
logger.error(f"The tool '{tool.__tool_spec__.name}' requires an app-id '{expected_tool_app_id}'. Please use the `--app-id` flag to provide the required app-id")
|
166
|
+
validation_failed = True
|
167
|
+
|
168
|
+
if not connections:
|
169
|
+
continue
|
170
|
+
|
171
|
+
connection_id = connections.get(sanatized_expected_tool_app_id)
|
172
|
+
|
173
|
+
imported_connection = imported_connections.get(connection_id)
|
174
|
+
imported_connection_auth_type = get_connection_type(security_scheme=imported_connection.security_scheme, auth_type=imported_connection.auth_type)
|
175
|
+
|
176
|
+
if connection_id and not imported_connection:
|
177
|
+
logger.error(f"The expected connection id '{connection_id}' does not match any known connection. This is likely caused by the connection being delted. Please rec-reate the connection and re-import the tool")
|
178
|
+
validation_failed = True
|
179
|
+
|
180
|
+
if imported_connection and len(expected_tool_conn_types) and imported_connection_auth_type not in expected_tool_conn_types:
|
181
|
+
logger.error(f"The app-id '{imported_connection.app_id}' is of type '{imported_connection_auth_type.value}'. The tool '{tool.__tool_spec__.name}' accepts connections of the following types '{', '.join(expected_tool_conn_types)}'. Use `orchestrate connections list` to view current connections and use `orchestrate connections add` to create the relevent connection")
|
182
|
+
validation_failed = True
|
183
|
+
|
184
|
+
if validation_failed:
|
185
|
+
exit(1)
|
186
|
+
|
187
|
+
|
188
|
+
def get_package_root(package_root):
|
189
|
+
return None if package_root is None or package_root.strip() == "" else package_root.strip()
|
190
|
+
|
191
|
+
def get_resolved_py_tool_reqs_file (tool_file, requirements_file, package_root):
|
192
|
+
resolved_requirements_file = requirements_file if requirements_file is not None else None
|
193
|
+
tool_sibling_reqs_file = Path(tool_file).absolute().parent.joinpath("requirements.txt")
|
194
|
+
package_root_reqs_file = Path(package_root).absolute().joinpath(
|
195
|
+
"requirements.txt") if get_package_root(package_root) is not None else None
|
196
|
+
|
197
|
+
if resolved_requirements_file is None:
|
198
|
+
# first favor requirements which is sibling root. if not, fallback to the one at package root.
|
199
|
+
if tool_sibling_reqs_file.exists():
|
200
|
+
resolved_requirements_file = str(tool_sibling_reqs_file)
|
201
|
+
|
202
|
+
elif package_root_reqs_file is not None and package_root_reqs_file.exists():
|
203
|
+
resolved_requirements_file = str(package_root_reqs_file)
|
204
|
+
|
205
|
+
return resolved_requirements_file
|
206
|
+
|
207
|
+
def get_requirement_lines (requirements_file, remove_trailing_newlines=True):
|
208
|
+
requirements = []
|
209
|
+
|
210
|
+
if requirements_file is not None:
|
211
|
+
with open(requirements_file, 'r') as fp:
|
212
|
+
requirements = fp.readlines()
|
213
|
+
|
214
|
+
if remove_trailing_newlines is True:
|
215
|
+
requirements = [x.strip() for x in requirements]
|
216
|
+
|
217
|
+
requirements = [x for x in requirements if not x.startswith("ibm-watsonx-orchestrate")]
|
218
|
+
requirements = list(dict.fromkeys(requirements))
|
219
|
+
|
220
|
+
return requirements
|
221
|
+
|
222
|
+
def import_python_tool(file: str, requirements_file: str = None, app_id: List[str] = None, package_root: str = None) -> List[BaseTool]:
|
223
|
+
try:
|
224
|
+
file_path = Path(file).absolute()
|
225
|
+
file_path_str = str(file_path)
|
226
|
+
|
227
|
+
if file_path.is_dir():
|
228
|
+
raise typer.BadParameter(f"Provided tool file path is not a file.")
|
229
|
+
|
230
|
+
elif file_path.is_symlink():
|
231
|
+
raise typer.BadParameter(f"Symbolic links are not supported for tool file path.")
|
232
|
+
|
233
|
+
file_name = file_path.stem
|
234
|
+
|
235
|
+
if __supported_characters_pattern.match(file_name) is None:
|
236
|
+
raise typer.BadParameter(f"File name contains unsupported characters. Only alphanumeric characters and underscores are allowed. Filename: \"{file_name}\"")
|
237
|
+
|
238
|
+
resolved_package_root = get_package_root(package_root)
|
239
|
+
if resolved_package_root:
|
240
|
+
resolved_package_root = str(Path(resolved_package_root).absolute())
|
241
|
+
package_path = str(Path(resolved_package_root).parent.absolute())
|
242
|
+
package_folder = str(Path(resolved_package_root).stem)
|
243
|
+
sys.path.append(package_path) # allows you to resolve non relative imports relative to the root of the module
|
244
|
+
sys.path.append(resolved_package_root) # allows you to resolve relative imports in combination with import_module(..., package=...)
|
245
|
+
package = file_path_str.replace(resolved_package_root, '').replace('.py', '').replace('/', '.').replace('\\', '.')
|
246
|
+
if not path.isdir(resolved_package_root):
|
247
|
+
raise typer.BadParameter(f"The provided package root is not a directory.")
|
248
|
+
|
249
|
+
elif not file_path_str.startswith(str(Path(resolved_package_root))):
|
250
|
+
raise typer.BadParameter(f"The provided tool file path does not belong to the provided package root.")
|
251
|
+
|
252
|
+
temp_path = Path(file_path_str[len(str(Path(resolved_package_root))) + 1:])
|
253
|
+
if any([__supported_characters_pattern.match(x) is None for x in temp_path.parts[:-1]]):
|
254
|
+
raise typer.BadParameter(f"Path to tool file contains unsupported characters. Only alphanumeric characters and underscores are allowed. Path: \"{temp_path}\"")
|
255
|
+
else:
|
256
|
+
package_folder = file_path.parent
|
257
|
+
package = file_path.stem
|
258
|
+
sys.path.append(str(package_folder))
|
259
|
+
|
260
|
+
module = importlib.import_module(package, package=package_folder)
|
261
|
+
if resolved_package_root:
|
262
|
+
del sys.path[-1]
|
263
|
+
del sys.path[-1]
|
264
|
+
|
265
|
+
|
266
|
+
except typer.BadParameter as ex:
|
267
|
+
raise ex
|
268
|
+
|
269
|
+
except Exception as e:
|
270
|
+
raise typer.BadParameter(f"Failed to load python module from file {file}: {e}")
|
271
|
+
|
272
|
+
requirements = []
|
273
|
+
resolved_requirements_file = get_resolved_py_tool_reqs_file(tool_file=file, requirements_file=requirements_file,
|
274
|
+
package_root=resolved_package_root)
|
275
|
+
|
276
|
+
if resolved_requirements_file is None:
|
277
|
+
logger.warning(f"No requirements file.")
|
278
|
+
|
279
|
+
if resolved_requirements_file != requirements_file:
|
280
|
+
logger.info(f"Resolved Requirements file: \"{resolved_requirements_file}\"")
|
281
|
+
|
282
|
+
else:
|
283
|
+
logger.info(f"Requirements file: \"{requirements_file}\"")
|
284
|
+
|
285
|
+
if resolved_requirements_file is not None:
|
286
|
+
try:
|
287
|
+
requirements = get_requirement_lines(resolved_requirements_file)
|
288
|
+
|
289
|
+
except Exception as e:
|
290
|
+
raise typer.BadParameter(f"Failed to read file {resolved_requirements_file} {e}")
|
291
|
+
|
292
|
+
tools = []
|
293
|
+
for _, obj in inspect.getmembers(module):
|
294
|
+
if not isinstance(obj, BaseTool):
|
295
|
+
continue
|
296
|
+
|
297
|
+
obj.__tool_spec__.binding.python.requirements = requirements
|
298
|
+
|
299
|
+
if __supported_characters_pattern.match(obj.__tool_spec__.name) is None:
|
300
|
+
raise typer.BadParameter(f"Tool name contains unsupported characters. Only alphanumeric characters and underscores are allowed. Name: \"{obj.__tool_spec__.name}\"")
|
301
|
+
|
302
|
+
elif resolved_package_root is None:
|
303
|
+
fn = obj.__tool_spec__.binding.python.function[obj.__tool_spec__.binding.python.function.index(':')+1:]
|
304
|
+
obj.__tool_spec__.binding.python.function = f"{file_name.replace('.py', '')}:{fn}"
|
305
|
+
|
306
|
+
else:
|
307
|
+
package = package[1:]
|
308
|
+
fn = obj.__tool_spec__.binding.python.function[obj.__tool_spec__.binding.python.function.index(':')+1:]
|
309
|
+
obj.__tool_spec__.binding.python.function = f"{package}:{fn}"
|
310
|
+
|
311
|
+
if app_id and len(app_id):
|
312
|
+
obj.__tool_spec__.binding.python.connections = parse_python_app_ids(app_id)
|
313
|
+
|
314
|
+
validate_python_connections(obj)
|
315
|
+
tools.append(obj)
|
316
|
+
|
317
|
+
return tools
|
318
|
+
|
319
|
+
async def import_openapi_tool(file: str, connection_id: str) -> List[BaseTool]:
|
320
|
+
tools = await create_openapi_json_tools_from_uri(file, connection_id)
|
321
|
+
return tools
|
322
|
+
|
323
|
+
class ToolsController:
|
324
|
+
def __init__(self, tool_kind: ToolKind = None, file: str = None, requirements_file: str = None):
|
325
|
+
self.client = None
|
326
|
+
self.tool_kind = tool_kind
|
327
|
+
self.file = file
|
328
|
+
self.requirements_file = requirements_file
|
329
|
+
|
330
|
+
def get_client(self) -> ToolClient:
|
331
|
+
if not self.client:
|
332
|
+
self.client = instantiate_client(ToolClient)
|
333
|
+
return self.client
|
334
|
+
|
335
|
+
@staticmethod
|
336
|
+
def import_tool(kind: ToolKind, **args) -> Iterable[BaseTool]:
|
337
|
+
# Ensure app_id is a list
|
338
|
+
if args.get("app_id") and isinstance(args.get("app_id"), str):
|
339
|
+
args["app_id"] = [args.get("app_id")]
|
340
|
+
|
341
|
+
validate_params(kind=kind, **args)
|
342
|
+
|
343
|
+
match kind:
|
344
|
+
case "python":
|
345
|
+
tools = import_python_tool(
|
346
|
+
file=args["file"],
|
347
|
+
requirements_file=args.get("requirements_file"),
|
348
|
+
app_id=args.get("app_id"),
|
349
|
+
package_root=args.get("package_root")
|
350
|
+
)
|
351
|
+
|
352
|
+
case "openapi":
|
353
|
+
connections_client = get_connections_client()
|
354
|
+
app_id = args.get('app_id', None)
|
355
|
+
connection_id = None
|
356
|
+
if app_id is not None:
|
357
|
+
app_id = app_id[0]
|
358
|
+
connection = connections_client.get_draft_by_app_id(app_id=app_id)
|
359
|
+
connection_id = connection.connection_id
|
360
|
+
tools = asyncio.run(import_openapi_tool(file=args["file"], connection_id=connection_id))
|
361
|
+
case "skill":
|
362
|
+
tools = []
|
363
|
+
logger.warning("Skill Import not implemented yet")
|
364
|
+
case _:
|
365
|
+
raise ValueError("Invalid kind selected")
|
366
|
+
|
367
|
+
for tool in tools:
|
368
|
+
yield tool
|
369
|
+
|
370
|
+
|
371
|
+
def list_tools(self, verbose=False):
|
372
|
+
response = self.get_client().get()
|
373
|
+
tool_specs = [ToolSpec.model_validate(tool) for tool in response]
|
374
|
+
tools = [BaseTool(spec=spec) for spec in tool_specs]
|
375
|
+
|
376
|
+
|
377
|
+
if verbose:
|
378
|
+
tools_list = []
|
379
|
+
for tool in tools:
|
380
|
+
|
381
|
+
tools_list.append(json.loads(tool.dumps_spec()))
|
382
|
+
|
383
|
+
rich.print(JSON(json.dumps(tools_list, indent=4)))
|
384
|
+
else:
|
385
|
+
table = rich.table.Table(show_header=True, header_style="bold white", show_lines=True)
|
386
|
+
columns = ["Name", "Description", "Permission", "Type", "App ID"]
|
387
|
+
for column in columns:
|
388
|
+
table.add_column(column)
|
389
|
+
|
390
|
+
for tool in tools:
|
391
|
+
tool_binding = tool.__tool_spec__.binding
|
392
|
+
|
393
|
+
connection_ids = []
|
394
|
+
|
395
|
+
if tool_binding is not None:
|
396
|
+
if tool_binding.openapi is not None and hasattr(tool_binding.openapi, "connection_id"):
|
397
|
+
connection_ids = [tool_binding.openapi.connection_id]
|
398
|
+
elif tool_binding.python is not None and hasattr(tool_binding.python, "connections") and tool_binding.python.connections is not None:
|
399
|
+
for conn in tool_binding.python.connections:
|
400
|
+
connection_ids.append(tool_binding.python.connections[conn])
|
401
|
+
|
402
|
+
connections_client = get_connections_client()
|
403
|
+
app_ids = []
|
404
|
+
for connection_id in connection_ids:
|
405
|
+
app_id = str(connections_client.get_draft_by_id(connection_id))
|
406
|
+
app_ids.append(app_id)
|
407
|
+
|
408
|
+
if tool_binding.python is not None:
|
409
|
+
tool_type=ToolKind.python
|
410
|
+
elif tool_binding.openapi is not None:
|
411
|
+
tool_type=ToolKind.openapi
|
412
|
+
else:
|
413
|
+
tool_type="Unknown"
|
414
|
+
|
415
|
+
table.add_row(
|
416
|
+
tool.__tool_spec__.name,
|
417
|
+
tool.__tool_spec__.description,
|
418
|
+
tool.__tool_spec__.permission,
|
419
|
+
tool_type,
|
420
|
+
", ".join(app_ids),
|
421
|
+
)
|
422
|
+
|
423
|
+
rich.print(table)
|
424
|
+
|
425
|
+
def get_all_tools(self) -> dict:
|
426
|
+
return {entry["name"]: entry["id"] for entry in self.get_client().get()}
|
427
|
+
|
428
|
+
def publish_or_update_tools(self, tools: Iterable[BaseTool], package_root: str = None) -> None:
|
429
|
+
resolved_package_root = get_package_root(package_root)
|
430
|
+
|
431
|
+
# Zip the tool's supporting artifacts for python tools
|
432
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
433
|
+
for tool in tools:
|
434
|
+
exist = False
|
435
|
+
tool_id = None
|
436
|
+
|
437
|
+
existing_tools = self.get_client().get_draft_by_name(tool.__tool_spec__.name)
|
438
|
+
if len(existing_tools) > 1:
|
439
|
+
logger.error(f"Multiple existing tools found with name '{tool.__tool_spec__.name}'. Failed to update tool")
|
440
|
+
sys.exit(1)
|
441
|
+
|
442
|
+
if len(existing_tools) > 0:
|
443
|
+
existing_tool = existing_tools[0]
|
444
|
+
exist = True
|
445
|
+
tool_id = existing_tool.get("id")
|
446
|
+
|
447
|
+
tool_artifact = None
|
448
|
+
if self.tool_kind == ToolKind.python:
|
449
|
+
tool_artifact = path.join(tmpdir, "artifacts.zip")
|
450
|
+
with zipfile.ZipFile(tool_artifact, "w", zipfile.ZIP_DEFLATED) as zip_tool_artifacts:
|
451
|
+
resolved_requirements_file = get_resolved_py_tool_reqs_file(tool_file=self.file,
|
452
|
+
requirements_file=self.requirements_file,
|
453
|
+
package_root=resolved_package_root)
|
454
|
+
|
455
|
+
if resolved_package_root is None:
|
456
|
+
# single file.
|
457
|
+
file_path = Path(self.file)
|
458
|
+
zip_tool_artifacts.write(file_path, arcname=f"{file_path.stem}.py")
|
459
|
+
|
460
|
+
else:
|
461
|
+
# multi-file.
|
462
|
+
path_strs = sorted(set([x for x in glob.iglob(path.join(resolved_package_root, '**/**'), include_hidden=True, recursive=True)]))
|
463
|
+
for path_str in path_strs:
|
464
|
+
path_obj = Path(path_str)
|
465
|
+
|
466
|
+
if not path_obj.is_file() or "/__pycache__/" in path_str or path_obj.name.lower() == "requirements.txt":
|
467
|
+
continue
|
468
|
+
|
469
|
+
if path_obj.is_symlink():
|
470
|
+
raise typer.BadParameter(f"Symbolic links in packages are not supported. - {path_str}")
|
471
|
+
|
472
|
+
try:
|
473
|
+
zip_tool_artifacts.write(path_str, arcname=path_str[len(str(Path(resolved_package_root))) + 1:])
|
474
|
+
|
475
|
+
except Exception as ex:
|
476
|
+
logger.error(f"Could not write file {path_str} to artifact. {ex}")
|
477
|
+
raise ex
|
478
|
+
|
479
|
+
zip_tool_artifacts.writestr("tool-spec.json", tool.dumps_spec())
|
480
|
+
|
481
|
+
requirements = []
|
482
|
+
if resolved_requirements_file is not None:
|
483
|
+
requirements = get_requirement_lines(requirements_file=resolved_requirements_file, remove_trailing_newlines=False)
|
484
|
+
|
485
|
+
# Ensure there is a newline at the end of the file
|
486
|
+
if len(requirements) > 0 and not requirements[-1].endswith("\n"):
|
487
|
+
requirements[-1] = requirements[-1]+"\n"
|
488
|
+
|
489
|
+
cfg = Config()
|
490
|
+
registry_type = cfg.read(PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_TYPE_OPT) or DEFAULT_CONFIG_FILE_CONTENT[PYTHON_REGISTRY_HEADER][PYTHON_REGISTRY_TYPE_OPT]
|
491
|
+
|
492
|
+
version = importlib.import_module('ibm_watsonx_orchestrate').__version__
|
493
|
+
if registry_type == RegistryType.LOCAL:
|
494
|
+
requirements.append(f"/packages/ibm_watsonx_orchestrate-0.6.0-py3-none-any.whl\n")
|
495
|
+
elif registry_type == RegistryType.PYPI:
|
496
|
+
requirements.append(f"ibm-watsonx-orchestrate=={version}\n")
|
497
|
+
elif registry_type == RegistryType.TESTPYPI:
|
498
|
+
override_version = cfg.get(PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_TEST_PACKAGE_VERSION_OVERRIDE_OPT) or version
|
499
|
+
orchestrate_links = requests.get('https://test.pypi.org/simple/ibm-watsonx-orchestrate').text
|
500
|
+
wheel_files = [x.group(1) for x in re.finditer( r'href="(.*\.whl).*"', orchestrate_links)]
|
501
|
+
wheel_file = next(filter(lambda x: f"{override_version}-py3-none-any.whl" in x, wheel_files), None)
|
502
|
+
if not wheel_file:
|
503
|
+
logger.error(f"Could not find ibm-watsonx-orchestrate@{override_version} on https://test.pypi.org/project/ibm-watsonx-orchestrate")
|
504
|
+
exit(1)
|
505
|
+
requirements.append(f"ibm-watsonx-orchestrate @ {wheel_file}\n")
|
506
|
+
else:
|
507
|
+
logger.error(f"Unrecognized registry type provided to orchestrate env activate local --registry <registry>")
|
508
|
+
exit(1)
|
509
|
+
requirements_file = path.join(tmpdir, 'requirements.txt')
|
510
|
+
|
511
|
+
requirements = list(dict.fromkeys(requirements))
|
512
|
+
|
513
|
+
with open(requirements_file, 'w') as fp:
|
514
|
+
fp.writelines(requirements)
|
515
|
+
requirements_file_path = Path(requirements_file)
|
516
|
+
zip_tool_artifacts.write(requirements_file_path, arcname='requirements.txt')
|
517
|
+
|
518
|
+
zip_tool_artifacts.writestr("bundle-format", "2.0.0\n")
|
519
|
+
|
520
|
+
if exist:
|
521
|
+
self.update_tool(tool_id=tool_id, tool=tool, tool_artifact=tool_artifact)
|
522
|
+
else:
|
523
|
+
self.publish_tool(tool, tool_artifact=tool_artifact)
|
524
|
+
|
525
|
+
def publish_tool(self, tool: BaseTool, tool_artifact: str) -> None:
|
526
|
+
tool_spec = tool.__tool_spec__.model_dump(mode='json', exclude_unset=True, exclude_none=True, by_alias=True)
|
527
|
+
|
528
|
+
response = self.get_client().create(tool_spec)
|
529
|
+
tool_id = response.get("id")
|
530
|
+
|
531
|
+
if tool_artifact is not None:
|
532
|
+
self.get_client().upload_tools_artifact(tool_id=tool_id, file_path=tool_artifact)
|
533
|
+
|
534
|
+
logger.info(f"Tool '{tool.__tool_spec__.name}' imported successfully")
|
535
|
+
|
536
|
+
def update_tool(self, tool_id: str, tool: BaseTool, tool_artifact: str) -> None:
|
537
|
+
tool_spec = tool.__tool_spec__.model_dump(mode='json', exclude_unset=True, exclude_none=True, by_alias=True)
|
538
|
+
|
539
|
+
logger.info(f"Existing Tool '{tool.__tool_spec__.name}' found. Updating...")
|
540
|
+
|
541
|
+
self.get_client().update(tool_id, tool_spec)
|
542
|
+
|
543
|
+
if tool_artifact is not None:
|
544
|
+
self.get_client().upload_tools_artifact(tool_id=tool_id, file_path=tool_artifact)
|
545
|
+
|
546
|
+
logger.info(f"Tool '{tool.__tool_spec__.name}' updated successfully")
|
547
|
+
|
548
|
+
def remove_tool(self, name: str):
|
549
|
+
try:
|
550
|
+
client = self.get_client()
|
551
|
+
draft_tools = client.get_draft_by_name(tool_name=name)
|
552
|
+
if len(draft_tools) > 1:
|
553
|
+
logger.error(f"Multiple existing tools found with name '{name}'. Failed to remove tool")
|
554
|
+
sys.exit(1)
|
555
|
+
if len(draft_tools) > 0:
|
556
|
+
draft_tool = draft_tools[0]
|
557
|
+
tool_id = draft_tool.get("id")
|
558
|
+
self.get_client().delete(tool_id=tool_id)
|
559
|
+
logger.info(f"Successfully removed tool {name}")
|
560
|
+
else:
|
561
|
+
logger.warning(f"No tool named '{name}' found")
|
562
|
+
except requests.HTTPError as e:
|
563
|
+
logger.error(e.response.text)
|
564
|
+
exit(1)
|