ibm-watsonx-orchestrate 1.1.0__py3-none-any.whl → 1.3.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 (24) hide show
  1. ibm_watsonx_orchestrate/__init__.py +1 -1
  2. ibm_watsonx_orchestrate/agent_builder/agents/types.py +4 -1
  3. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/knowledge_base.py +16 -3
  4. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/knowledge_base_requests.py +4 -20
  5. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +4 -13
  6. ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +4 -12
  7. ibm_watsonx_orchestrate/agent_builder/tools/types.py +18 -5
  8. ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +2 -0
  9. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_command.py +2 -2
  10. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +63 -38
  11. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_command.py +71 -0
  12. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_controller.py +212 -0
  13. ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +49 -21
  14. ibm_watsonx_orchestrate/cli/init_helper.py +43 -0
  15. ibm_watsonx_orchestrate/cli/main.py +7 -3
  16. ibm_watsonx_orchestrate/client/connections/connections_client.py +13 -13
  17. ibm_watsonx_orchestrate/client/toolkit/toolkit_client.py +81 -0
  18. ibm_watsonx_orchestrate/docker/compose-lite.yml +1 -0
  19. ibm_watsonx_orchestrate/docker/default.env +7 -7
  20. {ibm_watsonx_orchestrate-1.1.0.dist-info → ibm_watsonx_orchestrate-1.3.0.dist-info}/METADATA +3 -2
  21. {ibm_watsonx_orchestrate-1.1.0.dist-info → ibm_watsonx_orchestrate-1.3.0.dist-info}/RECORD +24 -20
  22. {ibm_watsonx_orchestrate-1.1.0.dist-info → ibm_watsonx_orchestrate-1.3.0.dist-info}/WHEEL +0 -0
  23. {ibm_watsonx_orchestrate-1.1.0.dist-info → ibm_watsonx_orchestrate-1.3.0.dist-info}/entry_points.txt +0 -0
  24. {ibm_watsonx_orchestrate-1.1.0.dist-info → ibm_watsonx_orchestrate-1.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,212 @@
1
+ import os
2
+ import zipfile
3
+ import tempfile
4
+ from typing import List, Optional
5
+ from enum import Enum
6
+ import logging
7
+ import sys
8
+ import re
9
+ import requests
10
+ from ibm_watsonx_orchestrate.client.toolkit.toolkit_client import ToolKitClient
11
+ from ibm_watsonx_orchestrate.client.utils import instantiate_client
12
+ from ibm_watsonx_orchestrate.utils.utils import sanatize_app_id
13
+ from ibm_watsonx_orchestrate.client.connections import get_connections_client
14
+ import typer
15
+ import json
16
+ from rich.console import Console
17
+ from rich.progress import Progress, SpinnerColumn, TextColumn
18
+ from ibm_watsonx_orchestrate.client.utils import is_local_dev
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ class ToolkitKind(str, Enum):
23
+ MCP = "mcp"
24
+
25
+ def get_connection_id(app_id: str) -> str:
26
+ connections_client = get_connections_client()
27
+ existing_draft_configuration = connections_client.get_config(app_id=app_id, env='draft')
28
+ existing_live_configuration = connections_client.get_config(app_id=app_id, env='live')
29
+
30
+ for config in [existing_draft_configuration, existing_live_configuration]:
31
+ if config and config.security_scheme != 'key_value_creds':
32
+ logger.error("Only key_value credentials are currently supported")
33
+ exit(1)
34
+ connection_id = None
35
+ if app_id is not None:
36
+ connection = connections_client.get(app_id=app_id)
37
+ if not connection:
38
+ logger.error(f"No connection exists with the app-id '{app_id}'")
39
+ exit(1)
40
+ connection_id = connection.connection_id
41
+ return connection_id
42
+
43
+ def validate_params(kind: str):
44
+ if kind != ToolkitKind.MCP:
45
+ raise ValueError(f"Unsupported toolkit kind: {kind}")
46
+
47
+
48
+ class ToolkitController:
49
+ def __init__(
50
+ self,
51
+ kind: ToolkitKind = None,
52
+ name: str = None,
53
+ description: str = None,
54
+ package_root: str = None,
55
+ command: str = None,
56
+ ):
57
+ self.kind = kind
58
+ self.name = name
59
+ self.description = description
60
+ self.package_root = package_root
61
+ self.command = command
62
+ self.client = None
63
+
64
+ def get_client(self) -> ToolKitClient:
65
+ if not self.client:
66
+ self.client = instantiate_client(ToolKitClient)
67
+ return self.client
68
+
69
+ def import_toolkit(self, tools: Optional[List[str]] = None, app_id: Optional[List[str]] = None):
70
+ if not is_local_dev():
71
+ logger.error("This functionality is only available for Local Environments")
72
+ sys.exit(1)
73
+
74
+ if app_id and isinstance(app_id, str):
75
+ app_id = [app_id]
76
+ elif not app_id:
77
+ app_id = []
78
+
79
+ validate_params(kind=self.kind)
80
+
81
+ remapped_connections = self._remap_connections(app_id)
82
+
83
+ client = self.get_client()
84
+ draft_toolkits = client.get_draft_by_name(toolkit_name=self.name)
85
+ if len(draft_toolkits) > 0:
86
+ logger.error(f"Existing toolkit found with name '{self.name}'. Failed to create toolkit.")
87
+ sys.exit(1)
88
+
89
+ with tempfile.TemporaryDirectory() as tmpdir:
90
+ # Handle zip file or directory
91
+ if self.package_root.endswith(".zip") and os.path.isfile(self.package_root):
92
+ zip_file_path = self.package_root
93
+ else:
94
+ zip_file_path = os.path.join(tmpdir, os.path.basename(f"{self.package_root.rstrip(os.sep)}.zip"))
95
+ with zipfile.ZipFile(zip_file_path, "w", zipfile.ZIP_DEFLATED) as mcp_zip_tool_artifacts:
96
+ self._populate_zip(self.package_root, mcp_zip_tool_artifacts)
97
+
98
+ try:
99
+ command_parts = json.loads(self.command)
100
+ if not isinstance(command_parts, list):
101
+ raise ValueError("JSON command must be a list of strings")
102
+ except (json.JSONDecodeError, ValueError):
103
+ command_parts = self.command.split()
104
+
105
+ command = command_parts[0]
106
+ args = command_parts[1:]
107
+
108
+ console = Console()
109
+ # List tools if not provided
110
+ if tools is None:
111
+ with Progress(
112
+ SpinnerColumn(spinner_name="dots"),
113
+ TextColumn("[progress.description]{task.description}"),
114
+ transient=True,
115
+ console=console,
116
+ ) as progress:
117
+ progress.add_task(description="No tools specified, retrieving all tools from provided MCP server", total=None)
118
+ tools = self.get_client().list_tools(
119
+ zip_file_path=zip_file_path,
120
+ command=command,
121
+ args=args,
122
+ )
123
+ # Normalize tools to a list of tool names
124
+ tools = [
125
+ tool["name"] if isinstance(tool, dict) and "name" in tool else tool
126
+ for tool in tools
127
+ ]
128
+
129
+
130
+ logger.info("✅ The following tools will be imported:")
131
+ for tool in tools:
132
+ console.print(f" • {tool}")
133
+
134
+
135
+ # Create toolkit metadata
136
+ payload = {
137
+ "name": self.name,
138
+ "description": self.description,
139
+ "mcp": {
140
+ "source": "files",
141
+ "command": command,
142
+ "args": args,
143
+ "tools": tools,
144
+ "connections": remapped_connections,
145
+ }
146
+ }
147
+ toolkit = self.get_client().create_toolkit(payload)
148
+ toolkit_id = toolkit["id"]
149
+
150
+ console = Console()
151
+ # Upload zip file
152
+ with Progress(
153
+ SpinnerColumn(spinner_name="dots"),
154
+ TextColumn("[progress.description]{task.description}"),
155
+ transient=True,
156
+ console=console,
157
+ ) as progress:
158
+ progress.add_task(description="Uploading toolkit zip file...", total=None)
159
+ self.get_client().upload(toolkit_id=toolkit_id, zip_file_path=zip_file_path)
160
+ logger.info(f"Successfully imported tool kit {self.name}")
161
+
162
+ def _populate_zip(self, package_root: str, zipfile: zipfile.ZipFile) -> str:
163
+ for root, _, files in os.walk(package_root):
164
+ for file in files:
165
+ full_path = os.path.join(root, file)
166
+ relative_path = os.path.relpath(full_path, start=package_root)
167
+ zipfile.write(full_path, arcname=relative_path)
168
+ return zipfile
169
+
170
+ def _remap_connections(self, app_ids: List[str]):
171
+ app_id_dict = {}
172
+ for app_id in app_ids:
173
+ split_pattern = re.compile(r"(?<!\\)=")
174
+ split_id = re.split(split_pattern, app_id)
175
+ split_id = [x.replace("\\=", "=") for x in split_id]
176
+ if len(split_id) == 2:
177
+ runtime_id, local_id = split_id
178
+ elif len(split_id) == 1:
179
+ runtime_id = split_id[0]
180
+ local_id = split_id[0]
181
+ else:
182
+ 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")
183
+
184
+ if not len(runtime_id.strip()) or not len(local_id.strip()):
185
+ raise typer.BadParameter(f"The provided --app-id '{app_id}' is not valid. --app-id cannot be empty or whitespace")
186
+
187
+ runtime_id = sanatize_app_id(runtime_id)
188
+ app_id_dict[runtime_id] = get_connection_id(local_id)
189
+
190
+ return app_id_dict
191
+
192
+
193
+ def remove_toolkit(self, name: str):
194
+ if not is_local_dev():
195
+ logger.error("This functionality is only available for Local Environments")
196
+ sys.exit(1)
197
+ try:
198
+ client = self.get_client()
199
+ draft_toolkits = client.get_draft_by_name(toolkit_name=name)
200
+ if len(draft_toolkits) > 1:
201
+ logger.error(f"Multiple existing toolkits found with name '{name}'. Failed to remove toolkit")
202
+ sys.exit(1)
203
+ if len(draft_toolkits) > 0:
204
+ draft_toolkit = draft_toolkits[0]
205
+ toolkit_id = draft_toolkit.get("id")
206
+ self.get_client().delete(toolkit_id=toolkit_id)
207
+ logger.info(f"Successfully removed tool {name}")
208
+ else:
209
+ logger.warning(f"No toolkit named '{name}' found")
210
+ except requests.HTTPError as e:
211
+ logger.error(e.response.text)
212
+ exit(1)
@@ -27,8 +27,9 @@ from ibm_watsonx_orchestrate.cli.config import Config, CONTEXT_SECTION_HEADER, C
27
27
  DEFAULT_CONFIG_FILE_CONTENT
28
28
  from ibm_watsonx_orchestrate.agent_builder.connections import ConnectionSecurityScheme, ExpectedCredentials
29
29
  from ibm_watsonx_orchestrate.client.tools.tool_client import ToolClient
30
+ from ibm_watsonx_orchestrate.client.toolkit.toolkit_client import ToolKitClient
30
31
  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.client.utils import instantiate_client, is_local_dev
32
33
  from ibm_watsonx_orchestrate.utils.utils import sanatize_app_id
33
34
 
34
35
  from ibm_watsonx_orchestrate import __version__
@@ -41,19 +42,20 @@ __supported_characters_pattern = re.compile("^(\\w|_)+$")
41
42
  class ToolKind(str, Enum):
42
43
  openapi = "openapi"
43
44
  python = "python"
45
+ mcp = "mcp"
44
46
  # skill = "skill"
45
47
 
46
48
  def validate_app_ids(kind: ToolKind, **args) -> None:
47
49
  app_ids = args.get("app_id")
48
50
  if not app_ids:
49
51
  return
50
-
52
+
51
53
  if kind == ToolKind.openapi:
52
54
  if app_ids and len(app_ids) > 1:
53
55
  raise typer.BadParameter(
54
56
  "Kind 'openapi' can only take one app-id"
55
57
  )
56
-
58
+
57
59
  connections_client = get_connections_client()
58
60
 
59
61
  imported_connections_list = connections_client.list()
@@ -113,7 +115,7 @@ def get_connection_id(app_id: str) -> str:
113
115
 
114
116
  def parse_python_app_ids(app_ids: List[str]) -> dict[str,str]:
115
117
  app_id_dict = {}
116
- for app_id in app_ids:
118
+ for app_id in app_ids:
117
119
  # Split on = but not on \=
118
120
  split_pattern = re.compile(r"(?<!\\)=")
119
121
  split_id = re.split(split_pattern, app_id)
@@ -137,7 +139,7 @@ def parse_python_app_ids(app_ids: List[str]) -> dict[str,str]:
137
139
  def validate_python_connections(tool: BaseTool):
138
140
  if not tool.expected_credentials:
139
141
  return
140
-
142
+
141
143
  connections_client = get_connections_client()
142
144
  connections = tool.__tool_spec__.binding.python.connections
143
145
 
@@ -156,13 +158,13 @@ def validate_python_connections(tool: BaseTool):
156
158
  expected_tool_conn_types = expected_cred.type
157
159
  else:
158
160
  expected_tool_conn_types = [expected_cred.type]
159
-
161
+
160
162
  sanatized_expected_tool_app_id = sanatize_app_id(expected_tool_app_id)
161
163
  if sanatized_expected_tool_app_id in existing_sanatized_expected_tool_app_ids:
162
164
  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.")
163
165
  sys.exit(1)
164
166
  existing_sanatized_expected_tool_app_ids.add(sanatized_expected_tool_app_id)
165
-
167
+
166
168
  if sanatized_expected_tool_app_id not in provided_connections:
167
169
  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")
168
170
  validation_failed = True
@@ -171,7 +173,7 @@ def validate_python_connections(tool: BaseTool):
171
173
  continue
172
174
 
173
175
  connection_id = connections.get(sanatized_expected_tool_app_id)
174
-
176
+
175
177
  imported_connection = imported_connections.get(connection_id)
176
178
  imported_connection_auth_type = get_connection_type(security_scheme=imported_connection.security_scheme, auth_type=imported_connection.auth_type)
177
179
 
@@ -182,7 +184,7 @@ def validate_python_connections(tool: BaseTool):
182
184
  if imported_connection and len(expected_tool_conn_types) and imported_connection_auth_type not in expected_tool_conn_types:
183
185
  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")
184
186
  validation_failed = True
185
-
187
+
186
188
  if validation_failed:
187
189
  exit(1)
188
190
 
@@ -309,7 +311,7 @@ def import_python_tool(file: str, requirements_file: str = None, app_id: List[st
309
311
 
310
312
  validate_python_connections(obj)
311
313
  tools.append(obj)
312
-
314
+
313
315
  return tools
314
316
 
315
317
  async def import_openapi_tool(file: str, connection_id: str) -> List[BaseTool]:
@@ -333,7 +335,7 @@ class ToolsController:
333
335
  # Ensure app_id is a list
334
336
  if args.get("app_id") and isinstance(args.get("app_id"), str):
335
337
  args["app_id"] = [args.get("app_id")]
336
-
338
+
337
339
  validate_params(kind=kind, **args)
338
340
 
339
341
  match kind:
@@ -368,24 +370,27 @@ class ToolsController:
368
370
  response = self.get_client().get()
369
371
  tool_specs = [ToolSpec.model_validate(tool) for tool in response]
370
372
  tools = [BaseTool(spec=spec) for spec in tool_specs]
371
-
372
373
 
373
374
  if verbose:
374
375
  tools_list = []
375
376
  for tool in tools:
376
-
377
377
  tools_list.append(json.loads(tool.dumps_spec()))
378
378
 
379
379
  rich.print(JSON(json.dumps(tools_list, indent=4)))
380
380
  else:
381
381
  table = rich.table.Table(show_header=True, header_style="bold white", show_lines=True)
382
- columns = ["Name", "Description", "Permission", "Type", "App ID"]
382
+ columns = ["Name", "Description", "Permission", "Type", "Toolkit", "App ID"]
383
383
  for column in columns:
384
384
  table.add_column(column)
385
-
385
+
386
+ connections_client = get_connections_client()
387
+ connections = connections_client.list()
388
+
389
+ connections_dict = {conn.connection_id: conn for conn in connections}
390
+
386
391
  for tool in tools:
387
392
  tool_binding = tool.__tool_spec__.binding
388
-
393
+
389
394
  connection_ids = []
390
395
 
391
396
  if tool_binding is not None:
@@ -394,25 +399,48 @@ class ToolsController:
394
399
  elif tool_binding.python is not None and hasattr(tool_binding.python, "connections") and tool_binding.python.connections is not None:
395
400
  for conn in tool_binding.python.connections:
396
401
  connection_ids.append(tool_binding.python.connections[conn])
397
-
398
- connections_client = get_connections_client()
402
+ elif tool_binding.mcp is not None and hasattr(tool_binding.mcp, "connections"):
403
+ for conn in tool_binding.mcp.connections:
404
+ connection_ids.append(tool_binding.mcp.connections[conn])
405
+
399
406
  app_ids = []
400
407
  for connection_id in connection_ids:
401
- app_id = str(connections_client.get_draft_by_id(connection_id))
408
+ connection = connections_dict.get(connection_id)
409
+ if connection:
410
+ app_id = str(connection.app_id or connection.connection_id)
411
+ elif connection_id:
412
+ app_id = str(connection_id)
413
+ else:
414
+ app_id = ""
402
415
  app_ids.append(app_id)
403
416
 
404
417
  if tool_binding.python is not None:
405
418
  tool_type=ToolKind.python
406
419
  elif tool_binding.openapi is not None:
407
420
  tool_type=ToolKind.openapi
421
+ elif tool_binding.mcp is not None:
422
+ tool_type=ToolKind.mcp
408
423
  else:
409
424
  tool_type="Unknown"
410
425
 
426
+ toolkit_name = ""
427
+
428
+ if is_local_dev():
429
+ toolkit_client = instantiate_client(ToolKitClient)
430
+ if tool.__tool_spec__.toolkit_id:
431
+ toolkit = toolkit_client.get_draft_by_id(tool.__tool_spec__.toolkit_id)
432
+ if isinstance(toolkit, dict) and "name" in toolkit:
433
+ toolkit_name = toolkit["name"]
434
+ elif toolkit:
435
+ toolkit_name = str(toolkit)
436
+
437
+
411
438
  table.add_row(
412
439
  tool.__tool_spec__.name,
413
440
  tool.__tool_spec__.description,
414
441
  tool.__tool_spec__.permission,
415
442
  tool_type,
443
+ toolkit_name,
416
444
  ", ".join(app_ids),
417
445
  )
418
446
 
@@ -434,7 +462,7 @@ class ToolsController:
434
462
  if len(existing_tools) > 1:
435
463
  logger.error(f"Multiple existing tools found with name '{tool.__tool_spec__.name}'. Failed to update tool")
436
464
  sys.exit(1)
437
-
465
+
438
466
  if len(existing_tools) > 0:
439
467
  existing_tool = existing_tools[0]
440
468
  exist = True
@@ -540,7 +568,7 @@ class ToolsController:
540
568
  self.get_client().upload_tools_artifact(tool_id=tool_id, file_path=tool_artifact)
541
569
 
542
570
  logger.info(f"Tool '{tool.__tool_spec__.name}' updated successfully")
543
-
571
+
544
572
  def remove_tool(self, name: str):
545
573
  try:
546
574
  client = self.get_client()
@@ -0,0 +1,43 @@
1
+ import importlib.metadata
2
+ from importlib import resources
3
+ from typing import Optional
4
+ from rich import print as pprint
5
+ from dotenv import dotenv_values
6
+ import typer
7
+
8
+ from ibm_watsonx_orchestrate.cli.config import Config, PYTHON_REGISTRY_HEADER, \
9
+ PYTHON_REGISTRY_TEST_PACKAGE_VERSION_OVERRIDE_OPT
10
+
11
+
12
+ def version_callback(checkVersion: bool=True):
13
+ if checkVersion:
14
+ __version__ = importlib.metadata.version('ibm-watsonx-orchestrate')
15
+ default_env = dotenv_values(resources.files("ibm_watsonx_orchestrate.docker").joinpath("default.env"))
16
+ cfg = Config()
17
+ pypi_override = cfg.read(PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_TEST_PACKAGE_VERSION_OVERRIDE_OPT)
18
+
19
+ adk_version_str = f"[bold]ADK Version[/bold]: {__version__}"
20
+ if pypi_override is not None:
21
+ adk_version_str += f" [red bold](override: {pypi_override})[/red bold]"
22
+ pprint(adk_version_str)
23
+
24
+
25
+ pprint("[bold]Developer Edition Image Tags[/bold] [italic](if not overridden in env file)[/italic]")
26
+ for key, value in default_env.items():
27
+ if key.endswith('_TAG') or key == 'DBTAG':
28
+ pprint(f" [bold]{key}[/bold]: {value}")
29
+
30
+ raise typer.Exit()
31
+
32
+
33
+
34
+ def init_callback(
35
+ ctx: typer.Context,
36
+ version: Optional[bool] = typer.Option(
37
+ None,
38
+ "--version",
39
+ help="Show the installed version of the ADK and Developer Edition Tags",
40
+ callback=version_callback
41
+ )
42
+ ):
43
+ pass
@@ -11,19 +11,23 @@ from ibm_watsonx_orchestrate.cli.commands.models.models_command import models_ap
11
11
  from ibm_watsonx_orchestrate.cli.commands.environment.environment_command import environment_app
12
12
  from ibm_watsonx_orchestrate.cli.commands.channels.channels_command import channel_app
13
13
  from ibm_watsonx_orchestrate.cli.commands.knowledge_bases.knowledge_bases_command import knowledge_bases_app
14
+ from ibm_watsonx_orchestrate.cli.commands.toolkit.toolkit_command import toolkits_app
15
+ from ibm_watsonx_orchestrate.cli.init_helper import init_callback
14
16
 
15
17
  app = typer.Typer(
16
18
  no_args_is_help=True,
17
- pretty_exceptions_enable=False
19
+ pretty_exceptions_enable=False,
20
+ callback=init_callback
18
21
  )
19
22
  app.add_typer(login_app)
20
23
  app.add_typer(environment_app, name="env", help='Add, remove, or select the activate env other commands will interact with (either your local server or a production instance)')
21
24
  app.add_typer(agents_app, name="agents", help='Interact with the agents in your active env')
22
25
  app.add_typer(tools_app, name="tools", help='Interact with the tools in your active env')
26
+ app.add_typer(toolkits_app, name="toolkits", help="Interact with the toolkits in your active env")
23
27
  app.add_typer(knowledge_bases_app, name="knowledge-bases", help="Upload knowledge your agents can search through to your active env")
24
28
  app.add_typer(connections_app, name="connections", help='Interact with the agents in your active env')
25
- app.add_typer(server_app, name="server", help='Manipulate your local Orchestrate Developer Edition server [requires an Entitlement]')
26
- app.add_typer(chat_app, name="chat", help='Launch the chat ui for your local Developer Edition server [requires docker pull credentials]')
29
+ app.add_typer(server_app, name="server", help='Manipulate your local Orchestrate Developer Edition server [requires entitlement]')
30
+ app.add_typer(chat_app, name="chat", help='Launch the chat ui for your local Developer Edition server [requires entitlement]')
27
31
  app.add_typer(models_app, name="models", help='List the available large language models (llms) that can be used in your agent definitions')
28
32
  app.add_typer(channel_app, name="channels", help="Configure channels where your agent can exist on (such as embedded webchat)")
29
33
  app.add_typer(settings_app, name="settings", help='Configure the settings for your active env')
@@ -51,7 +51,7 @@ class ConnectionsClient(BaseAPIClient):
51
51
  # DELETE api/v1/connections/applications/{app_id}
52
52
  def delete(self, app_id: str) -> dict:
53
53
  return self._delete(f"/connections/applications/{app_id}")
54
-
54
+
55
55
  # GET /api/v1/connections/applications/{app_id}
56
56
  def get(self, app_id: str) -> GetConnectionResponse:
57
57
  try:
@@ -60,7 +60,7 @@ class ConnectionsClient(BaseAPIClient):
60
60
  if e.response.status_code == 404:
61
61
  return None
62
62
  raise e
63
-
63
+
64
64
 
65
65
  # GET api/v1/connections/applications
66
66
  def list(self) -> List[ListConfigsResponse]:
@@ -75,15 +75,15 @@ class ConnectionsClient(BaseAPIClient):
75
75
  return []
76
76
  raise e
77
77
 
78
-
78
+
79
79
  # POST /api/v1/connections/applications/{app_id}/configurations
80
80
  def create_config(self, app_id: str, payload: dict) -> None:
81
81
  self._post(f"/connections/applications/{app_id}/configurations", data=payload)
82
-
82
+
83
83
  # PATCH /api/v1/connections/applications/{app_id}/configurations/{env}
84
84
  def update_config(self, app_id: str, env: ConnectionEnvironment, payload: dict) -> None:
85
85
  self._patch(f"/connections/applications/{app_id}/configurations/{env}", data=payload)
86
-
86
+
87
87
  # `GET /api/v1/connections/applications/{app_id}/configurations/{env}'
88
88
  def get_config(self, app_id: str, env: ConnectionEnvironment) -> GetConfigResponse:
89
89
  try:
@@ -93,7 +93,7 @@ class ConnectionsClient(BaseAPIClient):
93
93
  if e.response.status_code == 404:
94
94
  return None
95
95
  raise e
96
-
96
+
97
97
  # POST /api/v1/connections/applications/{app_id}/configs/{env}/credentials
98
98
  # POST /api/v1/connections/applications/{app_id}/configs/{env}/runtime_credentials
99
99
  def create_credentials(self, app_id: str, env: ConnectionEnvironment, payload: dict, use_sso: bool) -> None:
@@ -101,7 +101,7 @@ class ConnectionsClient(BaseAPIClient):
101
101
  self._post(f"/connections/applications/{app_id}/configs/{env}/credentials", data=payload)
102
102
  else:
103
103
  self._post(f"/connections/applications/{app_id}/configs/{env}/runtime_credentials", data=payload)
104
-
104
+
105
105
  # PATCH /api/v1/connections/applications/{app_id}/configs/{env}/credentials
106
106
  # PATCH /api/v1/connections/applications/{app_id}/configs/{env}/runtime_credentials
107
107
  def update_credentials(self, app_id: str, env: ConnectionEnvironment, payload: dict, use_sso: bool) -> None:
@@ -109,7 +109,7 @@ class ConnectionsClient(BaseAPIClient):
109
109
  self._patch(f"/connections/applications/{app_id}/configs/{env}/credentials", data=payload)
110
110
  else:
111
111
  self._patch(f"/connections/applications/{app_id}/configs/{env}/runtime_credentials", data=payload)
112
-
112
+
113
113
  # GET /api/v1/connections/applications/{app_id}/configs/credentials?env={env}
114
114
  # GET /api/v1/connections/applications/{app_id}/configs/runtime_credentials?env={env}
115
115
  def get_credentials(self, app_id: str, env: ConnectionEnvironment, use_sso: bool) -> dict:
@@ -122,7 +122,7 @@ class ConnectionsClient(BaseAPIClient):
122
122
  if e.response.status_code == 404:
123
123
  return None
124
124
  raise e
125
-
125
+
126
126
  # DELETE /api/v1/connections/applications/{app_id}/configs/{env}/credentials
127
127
  # DELETE /api/v1/connections/applications/{app_id}/configs/{env}/runtime_credentials
128
128
  def delete_credentials(self, app_id: str, env: ConnectionEnvironment, use_sso: bool) -> None:
@@ -130,7 +130,7 @@ class ConnectionsClient(BaseAPIClient):
130
130
  self._delete(f"/connections/applications/{app_id}/configs/{env}/credentials")
131
131
  else:
132
132
  self._delete(f"/connections/applications/{app_id}/configs/{env}/runtime_credentials")
133
-
133
+
134
134
  def get_draft_by_app_id(self, app_id: str) -> GetConnectionResponse:
135
135
  return self.get(app_id=app_id)
136
136
 
@@ -141,7 +141,7 @@ class ConnectionsClient(BaseAPIClient):
141
141
  if connection:
142
142
  connections += connection
143
143
  return connections
144
-
144
+
145
145
  def get_draft_by_id(self, conn_id) -> str:
146
146
  """Retrieve the app ID for a given connection ID."""
147
147
  if conn_id is None:
@@ -151,12 +151,12 @@ class ConnectionsClient(BaseAPIClient):
151
151
  except ClientAPIException as e:
152
152
  if e.response.status_code == 404:
153
153
  logger.warning(f"Connections not found. Returning connection ID: {conn_id}")
154
- return conn_id
154
+ return conn_id
155
155
  raise
156
156
 
157
157
  app_id = next((conn.app_id for conn in connections if conn.connection_id == conn_id), None)
158
158
 
159
159
  if app_id is None:
160
160
  logger.warning(f"Connection with ID {conn_id} not found. Returning connection ID.")
161
- return conn_id
161
+ return conn_id
162
162
  return app_id
@@ -0,0 +1,81 @@
1
+ from ibm_watsonx_orchestrate.client.base_api_client import BaseAPIClient, ClientAPIException
2
+ from typing_extensions import List
3
+ import os
4
+ import json
5
+
6
+ class ToolKitClient(BaseAPIClient):
7
+ # POST /toolkits/prepare/list-tools
8
+ def list_tools(self, zip_file_path: str, command: str, args: List[str]) -> List[str]:
9
+ """
10
+ List the available tools inside the MCP server
11
+ """
12
+
13
+ filename = os.path.basename(zip_file_path)
14
+
15
+ list_toolkit_obj = {
16
+ "source": "files",
17
+ "command": command,
18
+ "args": args,
19
+ }
20
+
21
+ with open(zip_file_path, "rb") as f:
22
+ files = {
23
+ "list_toolkit_obj": (None, json.dumps(list_toolkit_obj), "application/json"),
24
+ "file": (filename, f, "application/zip"),
25
+ }
26
+
27
+ response = self._post("/orchestrate/toolkits/prepare/list-tools", files=files)
28
+
29
+ return response.get("tools", [])
30
+
31
+
32
+ # POST /api/v1/orchestrate/toolkits
33
+ def create_toolkit(self, payload) -> dict:
34
+ """
35
+ Creates new toolkit metadata
36
+ """
37
+ try:
38
+ return self._post("/orchestrate/toolkits", data=payload)
39
+ except ClientAPIException as e:
40
+ if e.response.status_code == 400 and "already exists" in e.response.text:
41
+ raise ClientAPIException(
42
+ status_code=400,
43
+ message=f"There is already a Toolkit with the same name that exists for this tenant."
44
+ )
45
+ raise(e)
46
+
47
+ # POST /toolkits/{toolkit-id}/upload
48
+ def upload(self, toolkit_id: str, zip_file_path: str) -> dict:
49
+ """
50
+ Upload zip file to the toolkit.
51
+ """
52
+ filename = os.path.basename(zip_file_path)
53
+ with open(zip_file_path, "rb") as f:
54
+ files = {
55
+ "file": (filename, f, "application/zip", {"Expires": "0"})
56
+ }
57
+ return self._post(f"/orchestrate/toolkits/{toolkit_id}/upload", files=files)
58
+
59
+ # DELETE /toolkits/{toolkit-id}
60
+ def delete(self, toolkit_id: str) -> dict:
61
+ return self._delete(f"/orchestrate/toolkits/{toolkit_id}")
62
+
63
+ def get_draft_by_name(self, toolkit_name: str) -> List[dict]:
64
+ return self.get_drafts_by_names([toolkit_name])
65
+
66
+ def get_drafts_by_names(self, toolkit_names: List[str]) -> List[dict]:
67
+ formatted_toolkit_names = [f"names={x}" for x in toolkit_names]
68
+ return self._get(f"/orchestrate/toolkits?{'&'.join(formatted_toolkit_names)}")
69
+
70
+ def get_draft_by_id(self, toolkit_id: str) -> dict:
71
+ if toolkit_id is None:
72
+ return ""
73
+ else:
74
+ try:
75
+ toolkit = self._get(f"/orchestrate/toolkits/{toolkit_id}")
76
+ return toolkit
77
+ except ClientAPIException as e:
78
+ if e.response.status_code == 404 and "not found with the given name" in e.response.text:
79
+ return ""
80
+ raise(e)
81
+
@@ -595,3 +595,4 @@ volumes:
595
595
  networks:
596
596
  default:
597
597
  name: wxo-server
598
+