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.
- ibm_watsonx_orchestrate/__init__.py +1 -1
- ibm_watsonx_orchestrate/agent_builder/agents/types.py +4 -1
- ibm_watsonx_orchestrate/agent_builder/knowledge_bases/knowledge_base.py +16 -3
- ibm_watsonx_orchestrate/agent_builder/knowledge_bases/knowledge_base_requests.py +4 -20
- ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +4 -13
- ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +4 -12
- ibm_watsonx_orchestrate/agent_builder/tools/types.py +18 -5
- ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +2 -0
- ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_command.py +2 -2
- ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +63 -38
- ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_command.py +71 -0
- ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_controller.py +212 -0
- ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +49 -21
- ibm_watsonx_orchestrate/cli/init_helper.py +43 -0
- ibm_watsonx_orchestrate/cli/main.py +7 -3
- ibm_watsonx_orchestrate/client/connections/connections_client.py +13 -13
- ibm_watsonx_orchestrate/client/toolkit/toolkit_client.py +81 -0
- ibm_watsonx_orchestrate/docker/compose-lite.yml +1 -0
- ibm_watsonx_orchestrate/docker/default.env +7 -7
- {ibm_watsonx_orchestrate-1.1.0.dist-info → ibm_watsonx_orchestrate-1.3.0.dist-info}/METADATA +3 -2
- {ibm_watsonx_orchestrate-1.1.0.dist-info → ibm_watsonx_orchestrate-1.3.0.dist-info}/RECORD +24 -20
- {ibm_watsonx_orchestrate-1.1.0.dist-info → ibm_watsonx_orchestrate-1.3.0.dist-info}/WHEEL +0 -0
- {ibm_watsonx_orchestrate-1.1.0.dist-info → ibm_watsonx_orchestrate-1.3.0.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
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
|
-
|
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
|
26
|
-
app.add_typer(chat_app, name="chat", help='Launch the chat ui for your local Developer Edition server [requires
|
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
|
+
|