ibm-watsonx-orchestrate 1.3.0__py3-none-any.whl → 1.4.2__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 +2 -0
- ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +9 -2
- ibm_watsonx_orchestrate/agent_builder/toolkits/base_toolkit.py +32 -0
- ibm_watsonx_orchestrate/agent_builder/toolkits/types.py +42 -0
- ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +10 -1
- ibm_watsonx_orchestrate/agent_builder/tools/python_tool.py +4 -2
- ibm_watsonx_orchestrate/agent_builder/tools/types.py +2 -1
- ibm_watsonx_orchestrate/cli/commands/agents/agents_command.py +29 -0
- ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +271 -12
- ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +17 -2
- ibm_watsonx_orchestrate/cli/commands/models/env_file_model_provider_mapper.py +180 -0
- ibm_watsonx_orchestrate/cli/commands/models/models_command.py +194 -8
- ibm_watsonx_orchestrate/cli/commands/server/server_command.py +117 -48
- ibm_watsonx_orchestrate/cli/commands/server/types.py +105 -0
- ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_command.py +55 -7
- ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_controller.py +123 -42
- ibm_watsonx_orchestrate/cli/commands/tools/tools_command.py +22 -1
- ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +197 -12
- ibm_watsonx_orchestrate/client/agents/agent_client.py +4 -1
- ibm_watsonx_orchestrate/client/agents/assistant_agent_client.py +5 -1
- ibm_watsonx_orchestrate/client/agents/external_agent_client.py +5 -1
- ibm_watsonx_orchestrate/client/analytics/llm/analytics_llm_client.py +2 -6
- ibm_watsonx_orchestrate/client/base_api_client.py +5 -2
- ibm_watsonx_orchestrate/client/connections/connections_client.py +3 -9
- ibm_watsonx_orchestrate/client/model_policies/__init__.py +0 -0
- ibm_watsonx_orchestrate/client/model_policies/model_policies_client.py +47 -0
- ibm_watsonx_orchestrate/client/model_policies/types.py +36 -0
- ibm_watsonx_orchestrate/client/models/__init__.py +0 -0
- ibm_watsonx_orchestrate/client/models/models_client.py +46 -0
- ibm_watsonx_orchestrate/client/models/types.py +177 -0
- ibm_watsonx_orchestrate/client/toolkit/toolkit_client.py +15 -6
- ibm_watsonx_orchestrate/client/tools/tempus_client.py +40 -0
- ibm_watsonx_orchestrate/client/tools/tool_client.py +8 -0
- ibm_watsonx_orchestrate/docker/compose-lite.yml +68 -13
- ibm_watsonx_orchestrate/docker/default.env +22 -12
- ibm_watsonx_orchestrate/docker/tempus/common-config.yaml +1 -1
- ibm_watsonx_orchestrate/experimental/flow_builder/__init__.py +0 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/flows/__init__.py +41 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/flows/constants.py +17 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/flows/data_map.py +91 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/flows/decorators.py +143 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/flows/events.py +72 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/flows/flow.py +1288 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/node.py +97 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/resources/flow_status.openapi.yml +98 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/types.py +492 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/utils.py +113 -0
- ibm_watsonx_orchestrate/utils/utils.py +5 -2
- {ibm_watsonx_orchestrate-1.3.0.dist-info → ibm_watsonx_orchestrate-1.4.2.dist-info}/METADATA +4 -1
- {ibm_watsonx_orchestrate-1.3.0.dist-info → ibm_watsonx_orchestrate-1.4.2.dist-info}/RECORD +54 -32
- {ibm_watsonx_orchestrate-1.3.0.dist-info → ibm_watsonx_orchestrate-1.4.2.dist-info}/WHEEL +0 -0
- {ibm_watsonx_orchestrate-1.3.0.dist-info → ibm_watsonx_orchestrate-1.4.2.dist-info}/entry_points.txt +0 -0
- {ibm_watsonx_orchestrate-1.3.0.dist-info → ibm_watsonx_orchestrate-1.4.2.dist-info}/licenses/LICENSE +0 -0
@@ -8,6 +8,9 @@ import sys
|
|
8
8
|
import re
|
9
9
|
import requests
|
10
10
|
from ibm_watsonx_orchestrate.client.toolkit.toolkit_client import ToolKitClient
|
11
|
+
from ibm_watsonx_orchestrate.client.tools.tool_client import ToolClient
|
12
|
+
from ibm_watsonx_orchestrate.agent_builder.toolkits.base_toolkit import BaseToolkit, ToolkitSpec
|
13
|
+
from ibm_watsonx_orchestrate.agent_builder.toolkits.types import ToolkitKind, Language, ToolkitSource
|
11
14
|
from ibm_watsonx_orchestrate.client.utils import instantiate_client
|
12
15
|
from ibm_watsonx_orchestrate.utils.utils import sanatize_app_id
|
13
16
|
from ibm_watsonx_orchestrate.client.connections import get_connections_client
|
@@ -16,12 +19,13 @@ import json
|
|
16
19
|
from rich.console import Console
|
17
20
|
from rich.progress import Progress, SpinnerColumn, TextColumn
|
18
21
|
from ibm_watsonx_orchestrate.client.utils import is_local_dev
|
22
|
+
from rich.json import JSON
|
23
|
+
import rich
|
24
|
+
import rich.table
|
25
|
+
import json
|
19
26
|
|
20
27
|
logger = logging.getLogger(__name__)
|
21
28
|
|
22
|
-
class ToolkitKind(str, Enum):
|
23
|
-
MCP = "mcp"
|
24
|
-
|
25
29
|
def get_connection_id(app_id: str) -> str:
|
26
30
|
connections_client = get_connections_client()
|
27
31
|
existing_draft_configuration = connections_client.get_config(app_id=app_id, env='draft')
|
@@ -47,29 +51,34 @@ def validate_params(kind: str):
|
|
47
51
|
|
48
52
|
class ToolkitController:
|
49
53
|
def __init__(
|
50
|
-
self,
|
54
|
+
self,
|
51
55
|
kind: ToolkitKind = None,
|
52
56
|
name: str = None,
|
53
57
|
description: str = None,
|
54
|
-
|
55
|
-
|
58
|
+
package: str = None,
|
59
|
+
package_root: str = None,
|
60
|
+
language: Language = None,
|
61
|
+
command: str = None,
|
56
62
|
):
|
57
63
|
self.kind = kind
|
58
64
|
self.name = name
|
59
65
|
self.description = description
|
66
|
+
self.package = package
|
60
67
|
self.package_root = package_root
|
68
|
+
self.language = language
|
61
69
|
self.command = command
|
62
70
|
self.client = None
|
63
71
|
|
72
|
+
self.source: ToolkitSource = (
|
73
|
+
ToolkitSource.PUBLIC_REGISTRY if package else ToolkitSource.FILES
|
74
|
+
)
|
75
|
+
|
64
76
|
def get_client(self) -> ToolKitClient:
|
65
77
|
if not self.client:
|
66
78
|
self.client = instantiate_client(ToolKitClient)
|
67
79
|
return self.client
|
68
80
|
|
69
81
|
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
82
|
|
74
83
|
if app_id and isinstance(app_id, str):
|
75
84
|
app_id = [app_id]
|
@@ -86,26 +95,28 @@ class ToolkitController:
|
|
86
95
|
logger.error(f"Existing toolkit found with name '{self.name}'. Failed to create toolkit.")
|
87
96
|
sys.exit(1)
|
88
97
|
|
89
|
-
|
90
|
-
|
91
|
-
if
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
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:]
|
97
107
|
|
98
|
-
|
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()
|
108
|
+
console = Console()
|
104
109
|
|
105
|
-
|
106
|
-
|
110
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
111
|
+
# Handle zip file or directory
|
112
|
+
if self.package_root:
|
113
|
+
if self.package_root.endswith(".zip") and os.path.isfile(self.package_root):
|
114
|
+
zip_file_path = self.package_root
|
115
|
+
else:
|
116
|
+
zip_file_path = os.path.join(tmpdir, os.path.basename(f"{self.package_root.rstrip(os.sep)}.zip"))
|
117
|
+
with zipfile.ZipFile(zip_file_path, "w", zipfile.ZIP_DEFLATED) as mcp_zip_tool_artifacts:
|
118
|
+
self._populate_zip(self.package_root, mcp_zip_tool_artifacts)
|
107
119
|
|
108
|
-
console = Console()
|
109
120
|
# List tools if not provided
|
110
121
|
if tools is None:
|
111
122
|
with Progress(
|
@@ -116,48 +127,59 @@ class ToolkitController:
|
|
116
127
|
) as progress:
|
117
128
|
progress.add_task(description="No tools specified, retrieving all tools from provided MCP server", total=None)
|
118
129
|
tools = self.get_client().list_tools(
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
130
|
+
zip_file_path=zip_file_path,
|
131
|
+
command=command,
|
132
|
+
args=args,
|
133
|
+
)
|
134
|
+
|
124
135
|
tools = [
|
125
136
|
tool["name"] if isinstance(tool, dict) and "name" in tool else tool
|
126
137
|
for tool in tools
|
127
138
|
]
|
128
139
|
|
129
|
-
|
130
140
|
logger.info("✅ The following tools will be imported:")
|
131
141
|
for tool in tools:
|
132
142
|
console.print(f" • {tool}")
|
133
143
|
|
134
|
-
|
135
144
|
# Create toolkit metadata
|
136
145
|
payload = {
|
137
146
|
"name": self.name,
|
138
147
|
"description": self.description,
|
139
148
|
"mcp": {
|
140
|
-
"source":
|
149
|
+
"source": self.source.value,
|
141
150
|
"command": command,
|
142
151
|
"args": args,
|
143
152
|
"tools": tools,
|
144
153
|
"connections": remapped_connections,
|
145
154
|
}
|
146
155
|
}
|
147
|
-
toolkit = self.get_client().create_toolkit(payload)
|
148
|
-
toolkit_id = toolkit["id"]
|
149
156
|
|
150
|
-
|
151
|
-
# Upload zip file
|
157
|
+
|
152
158
|
with Progress(
|
153
159
|
SpinnerColumn(spinner_name="dots"),
|
154
160
|
TextColumn("[progress.description]{task.description}"),
|
155
161
|
transient=True,
|
156
162
|
console=console,
|
157
163
|
) as progress:
|
158
|
-
progress.add_task(description="
|
159
|
-
self.get_client().
|
160
|
-
|
164
|
+
progress.add_task(description="Creating toolkit...", total=None)
|
165
|
+
toolkit = self.get_client().create_toolkit(payload)
|
166
|
+
|
167
|
+
toolkit_id = toolkit["id"]
|
168
|
+
|
169
|
+
|
170
|
+
|
171
|
+
# Upload zip file
|
172
|
+
if self.package_root:
|
173
|
+
with Progress(
|
174
|
+
SpinnerColumn(spinner_name="dots"),
|
175
|
+
TextColumn("[progress.description]{task.description}"),
|
176
|
+
transient=True,
|
177
|
+
console=console,
|
178
|
+
) as progress:
|
179
|
+
progress.add_task(description="Uploading toolkit zip file...", total=None)
|
180
|
+
self.get_client().upload(toolkit_id=toolkit_id, zip_file_path=zip_file_path)
|
181
|
+
|
182
|
+
logger.info(f"Successfully imported tool kit {self.name}")
|
161
183
|
|
162
184
|
def _populate_zip(self, package_root: str, zipfile: zipfile.ZipFile) -> str:
|
163
185
|
for root, _, files in os.walk(package_root):
|
@@ -209,4 +231,63 @@ class ToolkitController:
|
|
209
231
|
logger.warning(f"No toolkit named '{name}' found")
|
210
232
|
except requests.HTTPError as e:
|
211
233
|
logger.error(e.response.text)
|
212
|
-
exit(1)
|
234
|
+
exit(1)
|
235
|
+
|
236
|
+
def list_toolkits(self, verbose=False):
|
237
|
+
client = self.get_client()
|
238
|
+
response = client.get()
|
239
|
+
toolkit_spec = [ToolkitSpec.model_validate(toolkit) for toolkit in response]
|
240
|
+
toolkits = [BaseToolkit(spec=spec) for spec in toolkit_spec]
|
241
|
+
|
242
|
+
if verbose:
|
243
|
+
tools_list = []
|
244
|
+
for toolkit in toolkits:
|
245
|
+
tools_list.append(json.loads(toolkit.dumps_spec()))
|
246
|
+
rich.print(JSON(json.dumps(tools_list, indent=4)))
|
247
|
+
else:
|
248
|
+
table = rich.table.Table(show_header=True, header_style="bold white", show_lines=True)
|
249
|
+
columns = ["Name", "Kind", "Description", "Tools", "App ID"]
|
250
|
+
for column in columns:
|
251
|
+
table.add_column(column)
|
252
|
+
|
253
|
+
tools_client = instantiate_client(ToolClient)
|
254
|
+
|
255
|
+
connections_client = get_connections_client()
|
256
|
+
connections = connections_client.list()
|
257
|
+
|
258
|
+
connections_dict = {conn.connection_id: conn for conn in connections}
|
259
|
+
|
260
|
+
for toolkit in toolkits:
|
261
|
+
tool_ids = toolkit.__toolkit_spec__.tools or []
|
262
|
+
tool_names = []
|
263
|
+
if len(tool_ids) == 0:
|
264
|
+
logger.warning("This toolkit contains no tools.")
|
265
|
+
|
266
|
+
for tool_id in tool_ids:
|
267
|
+
tool = tools_client.get_draft_by_id(tool_id)
|
268
|
+
tool_names.append(tool["name"])
|
269
|
+
|
270
|
+
app_ids = []
|
271
|
+
connection_ids = toolkit.__toolkit_spec__.mcp.connections.values()
|
272
|
+
|
273
|
+
for connection_id in connection_ids:
|
274
|
+
connection = connections_dict.get(connection_id)
|
275
|
+
if connection:
|
276
|
+
app_id = str(connection.app_id or connection.connection_id)
|
277
|
+
elif connection_id:
|
278
|
+
app_id = str(connection_id)
|
279
|
+
else:
|
280
|
+
app_id = ""
|
281
|
+
app_ids.append(app_id)
|
282
|
+
|
283
|
+
|
284
|
+
|
285
|
+
table.add_row(
|
286
|
+
toolkit.__toolkit_spec__.name,
|
287
|
+
"MCP",
|
288
|
+
toolkit.__toolkit_spec__.description,
|
289
|
+
", ".join(tool_names),
|
290
|
+
", ".join(app_ids),
|
291
|
+
)
|
292
|
+
|
293
|
+
rich.print(table)
|
@@ -15,7 +15,7 @@ def tool_import(
|
|
15
15
|
typer.Option(
|
16
16
|
"--file",
|
17
17
|
"-f",
|
18
|
-
help="Path to Python
|
18
|
+
help="Path to Python, OpenAPI spec YAML file or flow JSON or python file. Required for kind openapi, python and flow",
|
19
19
|
),
|
20
20
|
] = None,
|
21
21
|
# skillset_id: Annotated[
|
@@ -83,3 +83,24 @@ def remove_tool(
|
|
83
83
|
):
|
84
84
|
tools_controller = ToolsController()
|
85
85
|
tools_controller.remove_tool(name=name)
|
86
|
+
|
87
|
+
@tools_app.command(name="export", help='Export a tool to a zip file')
|
88
|
+
def tool_export(
|
89
|
+
name: Annotated[
|
90
|
+
str,
|
91
|
+
typer.Option("--name", "-n", help="The name of the tool you want to export"),
|
92
|
+
],
|
93
|
+
output_file: Annotated[
|
94
|
+
str,
|
95
|
+
typer.Option(
|
96
|
+
"--output",
|
97
|
+
"-o",
|
98
|
+
help="Path to a where the zip file containing the exported data should be saved",
|
99
|
+
),
|
100
|
+
],
|
101
|
+
):
|
102
|
+
tools_controller = ToolsController()
|
103
|
+
tools_controller.export_tool(
|
104
|
+
name=name,
|
105
|
+
output_path=output_file
|
106
|
+
)
|
@@ -3,6 +3,7 @@ import asyncio
|
|
3
3
|
import importlib
|
4
4
|
import inspect
|
5
5
|
import sys
|
6
|
+
import io
|
6
7
|
import re
|
7
8
|
import tempfile
|
8
9
|
import requests
|
@@ -19,18 +20,28 @@ import glob
|
|
19
20
|
import rich.table
|
20
21
|
import typer
|
21
22
|
|
23
|
+
from rich.console import Console
|
24
|
+
from rich.panel import Panel
|
25
|
+
|
22
26
|
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
|
27
|
+
from ibm_watsonx_orchestrate.agent_builder.tools.openapi_tool import create_openapi_json_tools_from_uri,create_openapi_json_tools_from_content
|
28
|
+
from ibm_watsonx_orchestrate.cli.commands.models.models_command import ModelHighlighter
|
24
29
|
from ibm_watsonx_orchestrate.cli.commands.tools.types import RegistryType
|
30
|
+
from ibm_watsonx_orchestrate.cli.commands.connections.connections_controller import configure_connection, remove_connection, add_connection
|
31
|
+
from ibm_watsonx_orchestrate.agent_builder.connections.types import ConnectionType, ConnectionEnvironment, ConnectionPreference
|
25
32
|
from ibm_watsonx_orchestrate.cli.config import Config, CONTEXT_SECTION_HEADER, CONTEXT_ACTIVE_ENV_OPT, \
|
26
33
|
PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_TYPE_OPT, PYTHON_REGISTRY_TEST_PACKAGE_VERSION_OVERRIDE_OPT, \
|
27
34
|
DEFAULT_CONFIG_FILE_CONTENT
|
28
35
|
from ibm_watsonx_orchestrate.agent_builder.connections import ConnectionSecurityScheme, ExpectedCredentials
|
36
|
+
from ibm_watsonx_orchestrate.experimental.flow_builder.flows.decorators import FlowWrapper
|
29
37
|
from ibm_watsonx_orchestrate.client.tools.tool_client import ToolClient
|
30
38
|
from ibm_watsonx_orchestrate.client.toolkit.toolkit_client import ToolKitClient
|
31
39
|
from ibm_watsonx_orchestrate.client.connections import get_connections_client, get_connection_type
|
32
40
|
from ibm_watsonx_orchestrate.client.utils import instantiate_client, is_local_dev
|
33
41
|
from ibm_watsonx_orchestrate.utils.utils import sanatize_app_id
|
42
|
+
from ibm_watsonx_orchestrate.client.utils import is_local_dev
|
43
|
+
from ibm_watsonx_orchestrate.client.tools.tempus_client import TempusClient
|
44
|
+
from ibm_watsonx_orchestrate.experimental.flow_builder.utils import import_flow_model
|
34
45
|
|
35
46
|
from ibm_watsonx_orchestrate import __version__
|
36
47
|
|
@@ -43,6 +54,7 @@ class ToolKind(str, Enum):
|
|
43
54
|
openapi = "openapi"
|
44
55
|
python = "python"
|
45
56
|
mcp = "mcp"
|
57
|
+
flow = "flow"
|
46
58
|
# skill = "skill"
|
47
59
|
|
48
60
|
def validate_app_ids(kind: ToolKind, **args) -> None:
|
@@ -167,18 +179,17 @@ def validate_python_connections(tool: BaseTool):
|
|
167
179
|
|
168
180
|
if sanatized_expected_tool_app_id not in provided_connections:
|
169
181
|
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")
|
170
|
-
|
182
|
+
sys.exit(1)
|
171
183
|
|
172
184
|
if not connections:
|
173
185
|
continue
|
174
|
-
|
186
|
+
|
175
187
|
connection_id = connections.get(sanatized_expected_tool_app_id)
|
176
|
-
|
177
188
|
imported_connection = imported_connections.get(connection_id)
|
178
189
|
imported_connection_auth_type = get_connection_type(security_scheme=imported_connection.security_scheme, auth_type=imported_connection.auth_type)
|
179
190
|
|
180
191
|
if connection_id and not imported_connection:
|
181
|
-
logger.error(f"The expected connection id '{connection_id}' does not match any known connection. This is likely caused by the connection being
|
192
|
+
logger.error(f"The expected connection id '{connection_id}' does not match any known connection. This is likely caused by the connection being deleted. Please rec-reate the connection and re-import the tool")
|
182
193
|
validation_failed = True
|
183
194
|
|
184
195
|
if imported_connection and len(expected_tool_conn_types) and imported_connection_auth_type not in expected_tool_conn_types:
|
@@ -302,9 +313,9 @@ def import_python_tool(file: str, requirements_file: str = None, app_id: List[st
|
|
302
313
|
obj.__tool_spec__.binding.python.function = f"{file_name.replace('.py', '')}:{fn}"
|
303
314
|
|
304
315
|
else:
|
305
|
-
|
316
|
+
pkg = package[1:]
|
306
317
|
fn = obj.__tool_spec__.binding.python.function[obj.__tool_spec__.binding.python.function.index(':')+1:]
|
307
|
-
obj.__tool_spec__.binding.python.function = f"{
|
318
|
+
obj.__tool_spec__.binding.python.function = f"{pkg}:{fn}"
|
308
319
|
|
309
320
|
if app_id and len(app_id):
|
310
321
|
obj.__tool_spec__.binding.python.connections = parse_python_app_ids(app_id)
|
@@ -314,10 +325,129 @@ def import_python_tool(file: str, requirements_file: str = None, app_id: List[st
|
|
314
325
|
|
315
326
|
return tools
|
316
327
|
|
328
|
+
async def import_flow_tool(file: str) -> None:
|
329
|
+
|
330
|
+
'''
|
331
|
+
Import a flow tool from a file. The file can be either a python file or a json file.
|
332
|
+
If the file is a python file, it should contain a flow model builder function decorated with the @flow decorator.
|
333
|
+
If the file is a json file, it should contain a flow model in json format.
|
334
|
+
Also, a connection will be created for the flow if one does not exists and the environment token will be used. This is a
|
335
|
+
workaround until flow bindings are supported in the server.
|
336
|
+
The function will return a list of tools created from the flow model.
|
337
|
+
'''
|
338
|
+
|
339
|
+
theme = rich.theme.Theme({"model.name": "bold cyan"})
|
340
|
+
console = rich.console.Console(highlighter=ModelHighlighter(), theme=theme)
|
341
|
+
|
342
|
+
message = f"""[bold cyan]Flow Tools: Experimental Feature[/bold cyan]
|
343
|
+
|
344
|
+
The [bold]flow tool[/bold] is being imported from [green]`{file}`[/green].
|
345
|
+
|
346
|
+
[bold cyan]Additional information:[/bold cyan]
|
347
|
+
|
348
|
+
- Ensure the flow engine is running by issuing the [bold cyan]orchestrate server start[/bold cyan] command with the [bold cyan]--with-flow-runtime[/bold cyan] option
|
349
|
+
- The [bold green]get_flow_status[/bold green] tool is being imported to support flow tools. Ensure [bold]both this tools and the one you are importing are added to your agent[/bold] to retrieve the flow output.
|
350
|
+
- Include additional instructions in your agent to call the [bold green]get_flow_status[/bold green] tool to retrieve the flow output. For example: [green]"If you get an instance_id, use the tool get_flow_status to retrieve the current status of a flow."[/green]
|
351
|
+
|
352
|
+
"""
|
353
|
+
|
354
|
+
console.print(Panel(message, title="[bold blue]Flow tool support information[/bold blue]", border_style="bright_blue"))
|
355
|
+
|
356
|
+
|
357
|
+
if not is_local_dev():
|
358
|
+
raise typer.BadParameter(f"Flow tools are only supported in local environment.")
|
359
|
+
|
360
|
+
model = None
|
361
|
+
|
362
|
+
# Load the Flow JSON model from the file
|
363
|
+
try:
|
364
|
+
file_path = Path(file).absolute()
|
365
|
+
file_path_str = str(file_path)
|
366
|
+
|
367
|
+
if file_path.is_dir():
|
368
|
+
raise typer.BadParameter(f"Provided flow file path is not a file.")
|
369
|
+
|
370
|
+
elif file_path.is_symlink():
|
371
|
+
raise typer.BadParameter(f"Symbolic links are not supported for flow file path.")
|
372
|
+
|
373
|
+
if file_path.suffix.lower() == ".py":
|
374
|
+
|
375
|
+
# borrow code from python tool import to be able to load the script that holds the flow model
|
376
|
+
|
377
|
+
resolved_package_root = get_package_root(str(file_path.parent))
|
378
|
+
if resolved_package_root:
|
379
|
+
resolved_package_root = str(Path(resolved_package_root).absolute())
|
380
|
+
package_path = str(Path(resolved_package_root).parent.absolute())
|
381
|
+
package_folder = str(Path(resolved_package_root).stem)
|
382
|
+
sys.path.append(package_path) # allows you to resolve non relative imports relative to the root of the module
|
383
|
+
sys.path.append(resolved_package_root) # allows you to resolve relative imports in combination with import_module(..., package=...)
|
384
|
+
package = file_path_str.replace(resolved_package_root, '').replace('.py', '').replace('/', '.').replace('\\', '.')
|
385
|
+
if not path.isdir(resolved_package_root):
|
386
|
+
raise typer.BadParameter(f"The provided package root is not a directory.")
|
387
|
+
|
388
|
+
elif not file_path_str.startswith(str(Path(resolved_package_root))):
|
389
|
+
raise typer.BadParameter(f"The provided tool file path does not belong to the provided package root.")
|
390
|
+
|
391
|
+
temp_path = Path(file_path_str[len(str(Path(resolved_package_root))) + 1:])
|
392
|
+
if any([__supported_characters_pattern.match(x) is None for x in temp_path.parts[:-1]]):
|
393
|
+
raise typer.BadParameter(f"Path to tool file contains unsupported characters. Only alphanumeric characters and underscores are allowed. Path: \"{temp_path}\"")
|
394
|
+
else:
|
395
|
+
package_folder = file_path.parent
|
396
|
+
package = file_path.stem
|
397
|
+
sys.path.append(str(package_folder))
|
398
|
+
|
399
|
+
module = importlib.import_module(package, package=package_folder)
|
400
|
+
|
401
|
+
if resolved_package_root:
|
402
|
+
del sys.path[-1]
|
403
|
+
del sys.path[-1]
|
404
|
+
|
405
|
+
for _, obj in inspect.getmembers(module):
|
406
|
+
|
407
|
+
if not isinstance(obj, FlowWrapper):
|
408
|
+
continue
|
409
|
+
|
410
|
+
model = obj().to_json()
|
411
|
+
break
|
412
|
+
|
413
|
+
elif file_path.suffix.lower() == ".json":
|
414
|
+
with open(file) as f:
|
415
|
+
model = json.load(f)
|
416
|
+
else:
|
417
|
+
raise typer.BadParameter(f"Unknown file type. Only python or json are supported.")
|
418
|
+
|
419
|
+
|
420
|
+
except typer.BadParameter as ex:
|
421
|
+
raise ex
|
422
|
+
|
423
|
+
except Exception as e:
|
424
|
+
raise typer.BadParameter(f"Failed to load model from file {file}: {e}")
|
425
|
+
|
426
|
+
return await import_flow_model(model)
|
427
|
+
|
428
|
+
|
317
429
|
async def import_openapi_tool(file: str, connection_id: str) -> List[BaseTool]:
|
318
430
|
tools = await create_openapi_json_tools_from_uri(file, connection_id)
|
319
431
|
return tools
|
320
432
|
|
433
|
+
def _get_kind_from_spec(spec: dict) -> ToolKind:
|
434
|
+
name = spec.get("name")
|
435
|
+
tool_binding = spec.get("binding")
|
436
|
+
|
437
|
+
if ToolKind.python in tool_binding:
|
438
|
+
return ToolKind.python
|
439
|
+
elif ToolKind.openapi in tool_binding:
|
440
|
+
return ToolKind.openapi
|
441
|
+
else:
|
442
|
+
logger.error(f"Could not determine 'kind' of tool '{name}'")
|
443
|
+
sys.exit(1)
|
444
|
+
|
445
|
+
def get_whl_in_registry(registry_url: str, version: str) -> str| None:
|
446
|
+
orchestrate_links = requests.get(registry_url).text
|
447
|
+
wheel_files = [x.group(1) for x in re.finditer( r'href="(.*\.whl).*"', orchestrate_links)]
|
448
|
+
wheel_file = next(filter(lambda x: f"{version}-py3-none-any.whl" in x, wheel_files), None)
|
449
|
+
return wheel_file
|
450
|
+
|
321
451
|
class ToolsController:
|
322
452
|
def __init__(self, tool_kind: ToolKind = None, file: str = None, requirements_file: str = None):
|
323
453
|
self.client = None
|
@@ -356,6 +486,8 @@ class ToolsController:
|
|
356
486
|
connection = connections_client.get_draft_by_app_id(app_id=app_id)
|
357
487
|
connection_id = connection.connection_id
|
358
488
|
tools = asyncio.run(import_openapi_tool(file=args["file"], connection_id=connection_id))
|
489
|
+
case "flow":
|
490
|
+
tools = asyncio.run(import_flow_tool(file=args["file"]))
|
359
491
|
case "skill":
|
360
492
|
tools = []
|
361
493
|
logger.warning("Skill Import not implemented yet")
|
@@ -400,8 +532,11 @@ class ToolsController:
|
|
400
532
|
for conn in tool_binding.python.connections:
|
401
533
|
connection_ids.append(tool_binding.python.connections[conn])
|
402
534
|
elif tool_binding.mcp is not None and hasattr(tool_binding.mcp, "connections"):
|
403
|
-
|
404
|
-
connection_ids.append(
|
535
|
+
if tool_binding.mcp.connections is None:
|
536
|
+
connection_ids.append(None)
|
537
|
+
else:
|
538
|
+
for conn in tool_binding.mcp.connections:
|
539
|
+
connection_ids.append(tool_binding.mcp.connections[conn])
|
405
540
|
|
406
541
|
app_ids = []
|
407
542
|
for connection_id in connection_ids:
|
@@ -517,12 +652,14 @@ class ToolsController:
|
|
517
652
|
if registry_type == RegistryType.LOCAL:
|
518
653
|
requirements.append(f"/packages/ibm_watsonx_orchestrate-0.6.0-py3-none-any.whl\n")
|
519
654
|
elif registry_type == RegistryType.PYPI:
|
655
|
+
wheel_file = get_whl_in_registry(registry_url='https://pypi.org/simple/ibm-watsonx-orchestrate', version=version)
|
656
|
+
if not wheel_file:
|
657
|
+
logger.error(f"Could not find ibm-watsonx-orchestrate@{version} on https://pypi.org/project/ibm-watsonx-orchestrate")
|
658
|
+
exit(1)
|
520
659
|
requirements.append(f"ibm-watsonx-orchestrate=={version}\n")
|
521
660
|
elif registry_type == RegistryType.TESTPYPI:
|
522
661
|
override_version = cfg.get(PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_TEST_PACKAGE_VERSION_OVERRIDE_OPT) or version
|
523
|
-
|
524
|
-
wheel_files = [x.group(1) for x in re.finditer( r'href="(.*\.whl).*"', orchestrate_links)]
|
525
|
-
wheel_file = next(filter(lambda x: f"{override_version}-py3-none-any.whl" in x, wheel_files), None)
|
662
|
+
wheel_file = get_whl_in_registry(registry_url='https://test.pypi.org/simple/ibm-watsonx-orchestrate', version=override_version)
|
526
663
|
if not wheel_file:
|
527
664
|
logger.error(f"Could not find ibm-watsonx-orchestrate@{override_version} on https://test.pypi.org/project/ibm-watsonx-orchestrate")
|
528
665
|
exit(1)
|
@@ -586,3 +723,51 @@ class ToolsController:
|
|
586
723
|
except requests.HTTPError as e:
|
587
724
|
logger.error(e.response.text)
|
588
725
|
exit(1)
|
726
|
+
|
727
|
+
def download_tool(self, name: str) -> bytes | None:
|
728
|
+
tool_client = self.get_client()
|
729
|
+
draft_tools = tool_client.get_draft_by_name(tool_name=name)
|
730
|
+
if len(draft_tools) > 1:
|
731
|
+
logger.error(f"Multiple existing tools found with name '{name}'. Failed to get tool")
|
732
|
+
sys.exit(1)
|
733
|
+
if len(draft_tools) == 0:
|
734
|
+
logger.error(f"No tool named '{name}' found")
|
735
|
+
sys.exit(1)
|
736
|
+
|
737
|
+
draft_tool = draft_tools[0]
|
738
|
+
draft_tool_kind = _get_kind_from_spec(draft_tool)
|
739
|
+
|
740
|
+
# TODO: Add openapi tool support
|
741
|
+
if draft_tool_kind != ToolKind.python:
|
742
|
+
logger.warning(f"Skipping '{name}', {draft_tool_kind.value} tools are currently unsupported by export")
|
743
|
+
return
|
744
|
+
|
745
|
+
tool_id = draft_tool.get("id")
|
746
|
+
|
747
|
+
tool_artifacts_bytes = tool_client.download_tools_artifact(tool_id=tool_id)
|
748
|
+
return tool_artifacts_bytes
|
749
|
+
|
750
|
+
def export_tool(self, name: str, output_path: str) -> None:
|
751
|
+
|
752
|
+
output_file = Path(output_path)
|
753
|
+
output_file_extension = output_file.suffix
|
754
|
+
if output_file_extension != ".zip":
|
755
|
+
logger.error(f"Output file must end with the extension '.zip'. Provided file '{output_path}' ends with '{output_file_extension}'")
|
756
|
+
sys.exit(1)
|
757
|
+
|
758
|
+
logger.info(f"Exporting tool definition for '{name}' to '{output_path}'")
|
759
|
+
|
760
|
+
tool_artifact_bytes = self.download_tool(name)
|
761
|
+
|
762
|
+
if not tool_artifact_bytes:
|
763
|
+
return
|
764
|
+
|
765
|
+
with zipfile.ZipFile(io.BytesIO(tool_artifact_bytes), "r") as zip_file_in, \
|
766
|
+
zipfile.ZipFile(output_path, 'w') as zip_file_out:
|
767
|
+
|
768
|
+
for item in zip_file_in.infolist():
|
769
|
+
buffer = zip_file_in.read(item.filename)
|
770
|
+
if (item.filename != 'bundle-format'):
|
771
|
+
zip_file_out.writestr(item, buffer)
|
772
|
+
|
773
|
+
logger.info(f"Successfully exported tool definition for '{name}' to '{output_path}'")
|
@@ -42,5 +42,8 @@ class AgentClient(BaseAPIClient):
|
|
42
42
|
if e.response.status_code == 404 and "not found with the given name" in e.response.text:
|
43
43
|
return ""
|
44
44
|
raise(e)
|
45
|
-
|
45
|
+
|
46
|
+
def get_drafts_by_ids(self, agent_ids: List[str]) -> List[dict]:
|
47
|
+
formatted_agent_ids = [f"ids={x}" for x in agent_ids]
|
48
|
+
return self._get(f"{self.base_endpoint}?{'&'.join(formatted_agent_ids)}")
|
46
49
|
|
@@ -25,7 +25,7 @@ class AssistantAgentClient(BaseAPIClient):
|
|
25
25
|
formatted_agent_names = [f"names={x}" for x in agent_names]
|
26
26
|
return self._get(f"/assistants/watsonx?{'&'.join(formatted_agent_names)}")
|
27
27
|
|
28
|
-
def get_draft_by_id(self, agent_id: str) ->
|
28
|
+
def get_draft_by_id(self, agent_id: str) -> dict | str:
|
29
29
|
if agent_id is None:
|
30
30
|
return ""
|
31
31
|
else:
|
@@ -36,3 +36,7 @@ class AssistantAgentClient(BaseAPIClient):
|
|
36
36
|
if e.response.status_code == 404 and "Assistant not found" in e.response.text:
|
37
37
|
return ""
|
38
38
|
raise(e)
|
39
|
+
|
40
|
+
def get_drafts_by_ids(self, agent_ids: List[str]) -> List[dict]:
|
41
|
+
formatted_agent_ids = [f"ids={x}" for x in agent_ids]
|
42
|
+
return self._get(f"/assistants/watsonx?{'&'.join(formatted_agent_ids)}")
|
@@ -33,6 +33,10 @@ class ExternalAgentClient(BaseAPIClient):
|
|
33
33
|
agent = self._get(f"/agents/external-chat/{agent_id}")
|
34
34
|
return agent
|
35
35
|
except ClientAPIException as e:
|
36
|
-
if e.response.status_code == 404 and "not found with the given name" in e.response.text:
|
36
|
+
if e.response.status_code == 404 and ("not found with the given name" in e.response.text or "Assistant not found" in e.response.text):
|
37
37
|
return ""
|
38
38
|
raise(e)
|
39
|
+
|
40
|
+
def get_drafts_by_ids(self, agent_ids: List[str]) -> List[dict]:
|
41
|
+
formatted_agent_ids = [f"ids={x}" for x in agent_ids]
|
42
|
+
return self._get(f"/agents/external-chat?{'&'.join(formatted_agent_ids)}")
|
@@ -32,12 +32,8 @@ class AnalyticsLLMClient(BaseAPIClient):
|
|
32
32
|
def create(self):
|
33
33
|
raise RuntimeError('unimplemented')
|
34
34
|
|
35
|
-
def get(self
|
36
|
-
|
37
|
-
if project_id:
|
38
|
-
params['project_id'] = project_id
|
39
|
-
|
40
|
-
response = self._get(f"/analytics/llm", params=params)
|
35
|
+
def get(self) -> AnalyticsLLMConfig:
|
36
|
+
response = self._get(f"/analytics/llm")
|
41
37
|
|
42
38
|
return AnalyticsLLMConfig.model_validate(response)
|
43
39
|
|
@@ -45,12 +45,15 @@ class BaseAPIClient:
|
|
45
45
|
headers["Authorization"] = f"Bearer {self.authenticator.token_manager.get_token()}"
|
46
46
|
return headers
|
47
47
|
|
48
|
-
def _get(self, path: str, params: dict = None, data=None) -> dict:
|
48
|
+
def _get(self, path: str, params: dict = None, data=None, return_raw=False) -> dict:
|
49
49
|
|
50
50
|
url = f"{self.base_url}{path}"
|
51
51
|
response = requests.get(url, headers=self._get_headers(), params=params, data=data)
|
52
52
|
self._check_response(response)
|
53
|
-
|
53
|
+
if not return_raw:
|
54
|
+
return response.json()
|
55
|
+
else:
|
56
|
+
return response
|
54
57
|
|
55
58
|
def _post(self, path: str, data: dict = None, files: dict = None) -> dict:
|
56
59
|
url = f"{self.base_url}{path}"
|