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.
Files changed (89) hide show
  1. ibm_watsonx_orchestrate/__init__.py +28 -0
  2. ibm_watsonx_orchestrate/agent_builder/__init__.py +0 -0
  3. ibm_watsonx_orchestrate/agent_builder/agents/__init__.py +5 -0
  4. ibm_watsonx_orchestrate/agent_builder/agents/agent.py +27 -0
  5. ibm_watsonx_orchestrate/agent_builder/agents/assistant_agent.py +28 -0
  6. ibm_watsonx_orchestrate/agent_builder/agents/external_agent.py +28 -0
  7. ibm_watsonx_orchestrate/agent_builder/agents/types.py +204 -0
  8. ibm_watsonx_orchestrate/agent_builder/connections/__init__.py +27 -0
  9. ibm_watsonx_orchestrate/agent_builder/connections/connections.py +123 -0
  10. ibm_watsonx_orchestrate/agent_builder/connections/types.py +260 -0
  11. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/knowledge_base.py +27 -0
  12. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/knowledge_base_requests.py +59 -0
  13. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +243 -0
  14. ibm_watsonx_orchestrate/agent_builder/tools/__init__.py +4 -0
  15. ibm_watsonx_orchestrate/agent_builder/tools/base_tool.py +36 -0
  16. ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +332 -0
  17. ibm_watsonx_orchestrate/agent_builder/tools/python_tool.py +195 -0
  18. ibm_watsonx_orchestrate/agent_builder/tools/types.py +162 -0
  19. ibm_watsonx_orchestrate/agent_builder/utils/__init__.py +0 -0
  20. ibm_watsonx_orchestrate/agent_builder/utils/pydantic_utils.py +149 -0
  21. ibm_watsonx_orchestrate/cli/__init__.py +0 -0
  22. ibm_watsonx_orchestrate/cli/commands/__init__.py +0 -0
  23. ibm_watsonx_orchestrate/cli/commands/agents/agents_command.py +192 -0
  24. ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +660 -0
  25. ibm_watsonx_orchestrate/cli/commands/channels/channels_command.py +15 -0
  26. ibm_watsonx_orchestrate/cli/commands/channels/channels_controller.py +16 -0
  27. ibm_watsonx_orchestrate/cli/commands/channels/types.py +15 -0
  28. ibm_watsonx_orchestrate/cli/commands/channels/webchat/channels_webchat_command.py +32 -0
  29. ibm_watsonx_orchestrate/cli/commands/channels/webchat/channels_webchat_controller.py +141 -0
  30. ibm_watsonx_orchestrate/cli/commands/chat/chat_command.py +43 -0
  31. ibm_watsonx_orchestrate/cli/commands/connections/connections_command.py +307 -0
  32. ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +517 -0
  33. ibm_watsonx_orchestrate/cli/commands/environment/environment_command.py +78 -0
  34. ibm_watsonx_orchestrate/cli/commands/environment/environment_controller.py +189 -0
  35. ibm_watsonx_orchestrate/cli/commands/environment/types.py +9 -0
  36. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_command.py +79 -0
  37. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +201 -0
  38. ibm_watsonx_orchestrate/cli/commands/login/login_command.py +17 -0
  39. ibm_watsonx_orchestrate/cli/commands/models/models_command.py +128 -0
  40. ibm_watsonx_orchestrate/cli/commands/server/server_command.py +623 -0
  41. ibm_watsonx_orchestrate/cli/commands/settings/__init__.py +0 -0
  42. ibm_watsonx_orchestrate/cli/commands/settings/observability/__init__.py +0 -0
  43. ibm_watsonx_orchestrate/cli/commands/settings/observability/langfuse/__init__.py +0 -0
  44. ibm_watsonx_orchestrate/cli/commands/settings/observability/langfuse/langfuse_command.py +175 -0
  45. ibm_watsonx_orchestrate/cli/commands/settings/observability/observability_command.py +11 -0
  46. ibm_watsonx_orchestrate/cli/commands/settings/settings_command.py +10 -0
  47. ibm_watsonx_orchestrate/cli/commands/tools/tools_command.py +85 -0
  48. ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +564 -0
  49. ibm_watsonx_orchestrate/cli/commands/tools/types.py +10 -0
  50. ibm_watsonx_orchestrate/cli/config.py +226 -0
  51. ibm_watsonx_orchestrate/cli/main.py +32 -0
  52. ibm_watsonx_orchestrate/client/__init__.py +0 -0
  53. ibm_watsonx_orchestrate/client/agents/agent_client.py +46 -0
  54. ibm_watsonx_orchestrate/client/agents/assistant_agent_client.py +38 -0
  55. ibm_watsonx_orchestrate/client/agents/external_agent_client.py +38 -0
  56. ibm_watsonx_orchestrate/client/analytics/__init__.py +0 -0
  57. ibm_watsonx_orchestrate/client/analytics/llm/__init__.py +0 -0
  58. ibm_watsonx_orchestrate/client/analytics/llm/analytics_llm_client.py +50 -0
  59. ibm_watsonx_orchestrate/client/base_api_client.py +113 -0
  60. ibm_watsonx_orchestrate/client/base_service_instance.py +10 -0
  61. ibm_watsonx_orchestrate/client/client.py +71 -0
  62. ibm_watsonx_orchestrate/client/client_errors.py +359 -0
  63. ibm_watsonx_orchestrate/client/connections/__init__.py +10 -0
  64. ibm_watsonx_orchestrate/client/connections/connections_client.py +162 -0
  65. ibm_watsonx_orchestrate/client/connections/utils.py +27 -0
  66. ibm_watsonx_orchestrate/client/credentials.py +123 -0
  67. ibm_watsonx_orchestrate/client/knowledge_bases/knowledge_base_client.py +46 -0
  68. ibm_watsonx_orchestrate/client/local_service_instance.py +91 -0
  69. ibm_watsonx_orchestrate/client/service_instance.py +73 -0
  70. ibm_watsonx_orchestrate/client/tools/tool_client.py +41 -0
  71. ibm_watsonx_orchestrate/client/utils.py +95 -0
  72. ibm_watsonx_orchestrate/docker/compose-lite.yml +595 -0
  73. ibm_watsonx_orchestrate/docker/default.env +125 -0
  74. ibm_watsonx_orchestrate/docker/sdk/ibm_watsonx_orchestrate-0.6.0-py3-none-any.whl +0 -0
  75. ibm_watsonx_orchestrate/docker/sdk/ibm_watsonx_orchestrate-0.6.0.tar.gz +0 -0
  76. ibm_watsonx_orchestrate/docker/start-up.sh +61 -0
  77. ibm_watsonx_orchestrate/docker/tempus/common-config.yaml +1 -0
  78. ibm_watsonx_orchestrate/run/__init__.py +0 -0
  79. ibm_watsonx_orchestrate/run/connections.py +40 -0
  80. ibm_watsonx_orchestrate/utils/__init__.py +0 -0
  81. ibm_watsonx_orchestrate/utils/logging/__init__.py +0 -0
  82. ibm_watsonx_orchestrate/utils/logging/logger.py +26 -0
  83. ibm_watsonx_orchestrate/utils/logging/logging.yaml +18 -0
  84. ibm_watsonx_orchestrate/utils/utils.py +15 -0
  85. ibm_watsonx_orchestrate-1.0.0.dist-info/METADATA +34 -0
  86. ibm_watsonx_orchestrate-1.0.0.dist-info/RECORD +89 -0
  87. ibm_watsonx_orchestrate-1.0.0.dist-info/WHEEL +4 -0
  88. ibm_watsonx_orchestrate-1.0.0.dist-info/entry_points.txt +2 -0
  89. 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)
@@ -0,0 +1,10 @@
1
+ from enum import Enum
2
+
3
+
4
+ class RegistryType(str, Enum):
5
+ PYPI = 'pypi'
6
+ TESTPYPI = 'testpypi'
7
+ LOCAL = 'local'
8
+
9
+ def __str__(self):
10
+ return str(self.value)