ibm-watsonx-orchestrate 1.11.0b1__py3-none-any.whl → 1.12.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 +22 -5
- ibm_watsonx_orchestrate/agent_builder/connections/connections.py +3 -3
- ibm_watsonx_orchestrate/agent_builder/connections/types.py +14 -0
- ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +1 -1
- ibm_watsonx_orchestrate/agent_builder/models/types.py +1 -0
- ibm_watsonx_orchestrate/agent_builder/toolkits/base_toolkit.py +1 -1
- ibm_watsonx_orchestrate/agent_builder/tools/__init__.py +1 -0
- ibm_watsonx_orchestrate/agent_builder/tools/base_tool.py +1 -1
- ibm_watsonx_orchestrate/agent_builder/tools/langflow_tool.py +184 -0
- ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +9 -3
- ibm_watsonx_orchestrate/agent_builder/tools/types.py +20 -2
- ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +19 -6
- ibm_watsonx_orchestrate/cli/commands/connections/connections_command.py +18 -0
- ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +114 -0
- ibm_watsonx_orchestrate/cli/commands/copilot/copilot_controller.py +2 -6
- ibm_watsonx_orchestrate/cli/commands/copilot/copilot_server_controller.py +24 -91
- ibm_watsonx_orchestrate/cli/commands/environment/environment_command.py +5 -1
- ibm_watsonx_orchestrate/cli/commands/environment/environment_controller.py +6 -3
- ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_command.py +52 -2
- ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_controller.py +1 -1
- ibm_watsonx_orchestrate/cli/commands/models/model_provider_mapper.py +23 -4
- ibm_watsonx_orchestrate/cli/commands/models/models_controller.py +3 -3
- ibm_watsonx_orchestrate/cli/commands/partners/offering/partners_offering_command.py +56 -0
- ibm_watsonx_orchestrate/cli/commands/partners/offering/partners_offering_controller.py +475 -0
- ibm_watsonx_orchestrate/cli/commands/partners/offering/types.py +99 -0
- ibm_watsonx_orchestrate/cli/commands/partners/partners_command.py +12 -0
- ibm_watsonx_orchestrate/cli/commands/partners/partners_controller.py +0 -0
- ibm_watsonx_orchestrate/cli/commands/server/server_command.py +124 -637
- ibm_watsonx_orchestrate/cli/commands/server/types.py +1 -1
- ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_command.py +2 -2
- ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_controller.py +2 -2
- ibm_watsonx_orchestrate/cli/commands/tools/tools_command.py +2 -3
- ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +241 -49
- ibm_watsonx_orchestrate/cli/config.py +3 -1
- ibm_watsonx_orchestrate/cli/main.py +2 -0
- ibm_watsonx_orchestrate/client/connections/connections_client.py +4 -1
- ibm_watsonx_orchestrate/client/tools/tempus_client.py +3 -0
- ibm_watsonx_orchestrate/client/tools/tool_client.py +5 -2
- ibm_watsonx_orchestrate/client/utils.py +31 -1
- ibm_watsonx_orchestrate/docker/compose-lite.yml +68 -17
- ibm_watsonx_orchestrate/docker/default.env +21 -18
- ibm_watsonx_orchestrate/flow_builder/flows/decorators.py +10 -2
- ibm_watsonx_orchestrate/flow_builder/flows/flow.py +71 -9
- ibm_watsonx_orchestrate/flow_builder/node.py +14 -2
- ibm_watsonx_orchestrate/flow_builder/types.py +36 -3
- ibm_watsonx_orchestrate/langflow/__init__.py +0 -0
- ibm_watsonx_orchestrate/langflow/langflow_utils.py +195 -0
- ibm_watsonx_orchestrate/langflow/lfx_deps.py +84 -0
- ibm_watsonx_orchestrate/utils/docker_utils.py +280 -0
- ibm_watsonx_orchestrate/utils/environment.py +369 -0
- ibm_watsonx_orchestrate/utils/utils.py +7 -3
- {ibm_watsonx_orchestrate-1.11.0b1.dist-info → ibm_watsonx_orchestrate-1.12.0.dist-info}/METADATA +2 -2
- {ibm_watsonx_orchestrate-1.11.0b1.dist-info → ibm_watsonx_orchestrate-1.12.0.dist-info}/RECORD +57 -46
- {ibm_watsonx_orchestrate-1.11.0b1.dist-info → ibm_watsonx_orchestrate-1.12.0.dist-info}/WHEEL +0 -0
- {ibm_watsonx_orchestrate-1.11.0b1.dist-info → ibm_watsonx_orchestrate-1.12.0.dist-info}/entry_points.txt +0 -0
- {ibm_watsonx_orchestrate-1.11.0b1.dist-info → ibm_watsonx_orchestrate-1.12.0.dist-info}/licenses/LICENSE +0 -0
@@ -92,7 +92,7 @@ class ModelGatewayEnvConfig(BaseModel):
|
|
92
92
|
inferred_auth_url = config.get("WO_INSTANCE") + '/icp4d-api/v1/authorize'
|
93
93
|
else:
|
94
94
|
logger.error(f"No 'AUTHORIZATION_URL' found. Auth type '{auth_type}' does not support defaulting. Please set the 'AUTHORIZATION_URL' explictly")
|
95
|
-
|
95
|
+
sys.exit(1)
|
96
96
|
config["AUTHORIZATION_URL"] = inferred_auth_url
|
97
97
|
|
98
98
|
if auth_type != WoAuthType.CPD:
|
@@ -47,11 +47,11 @@ def import_toolkit(
|
|
47
47
|
] = None,
|
48
48
|
url: Annotated[
|
49
49
|
Optional[str],
|
50
|
-
typer.Option("--url", "-u", help="The URL of the remote MCP server"
|
50
|
+
typer.Option("--url", "-u", help="The URL of the remote MCP server"),
|
51
51
|
] = None,
|
52
52
|
transport: Annotated[
|
53
53
|
ToolkitTransportKind,
|
54
|
-
typer.Option("--transport", help="The communication protocol to use for the remote MCP server. Only \"sse\" or \"streamable_http\" supported"
|
54
|
+
typer.Option("--transport", help="The communication protocol to use for the remote MCP server. Only \"sse\" or \"streamable_http\" supported"),
|
55
55
|
] = None,
|
56
56
|
tools: Annotated[
|
57
57
|
Optional[str],
|
@@ -12,7 +12,7 @@ from ibm_watsonx_orchestrate.client.tools.tool_client import ToolClient
|
|
12
12
|
from ibm_watsonx_orchestrate.agent_builder.toolkits.base_toolkit import BaseToolkit, ToolkitSpec
|
13
13
|
from ibm_watsonx_orchestrate.agent_builder.toolkits.types import ToolkitKind, Language, ToolkitSource, ToolkitTransportKind
|
14
14
|
from ibm_watsonx_orchestrate.client.utils import instantiate_client
|
15
|
-
from ibm_watsonx_orchestrate.utils.utils import
|
15
|
+
from ibm_watsonx_orchestrate.utils.utils import sanitize_app_id
|
16
16
|
from ibm_watsonx_orchestrate.client.connections import get_connections_client
|
17
17
|
import typer
|
18
18
|
import json
|
@@ -240,7 +240,7 @@ class ToolkitController:
|
|
240
240
|
if not len(runtime_id.strip()) or not len(local_id.strip()):
|
241
241
|
raise typer.BadParameter(f"The provided --app-id '{app_id}' is not valid. --app-id cannot be empty or whitespace")
|
242
242
|
|
243
|
-
runtime_id =
|
243
|
+
runtime_id = sanitize_app_id(runtime_id)
|
244
244
|
is_local_mcp = self.package is not None or self.package_root is not None
|
245
245
|
app_id_dict[runtime_id] = get_connection_id(local_id, is_local_mcp)
|
246
246
|
|
@@ -61,9 +61,8 @@ relative to this package root folder or imported using relative imports from the
|
|
61
61
|
requirements_file=requirements_file,
|
62
62
|
package_root=package_root
|
63
63
|
)
|
64
|
-
|
65
|
-
|
66
|
-
|
64
|
+
tools_controller.publish_or_update_tools(tools=tools, package_root=package_root)
|
65
|
+
|
67
66
|
@tools_app.command(name="list", help='List the imported tools in the active environment')
|
68
67
|
def list_tools(
|
69
68
|
verbose: Annotated[
|
@@ -11,7 +11,7 @@ import zipfile
|
|
11
11
|
from enum import Enum
|
12
12
|
from os import path
|
13
13
|
from pathlib import Path
|
14
|
-
from typing import Iterable, List
|
14
|
+
from typing import Iterable, List, cast
|
15
15
|
import rich
|
16
16
|
import json
|
17
17
|
from rich.json import JSON
|
@@ -25,6 +25,7 @@ from rich.panel import Panel
|
|
25
25
|
|
26
26
|
from ibm_watsonx_orchestrate.agent_builder.tools import BaseTool, ToolSpec
|
27
27
|
from ibm_watsonx_orchestrate.agent_builder.tools.flow_tool import create_flow_json_tool
|
28
|
+
from ibm_watsonx_orchestrate.agent_builder.tools.langflow_tool import LangflowTool, create_langflow_tool
|
28
29
|
from ibm_watsonx_orchestrate.agent_builder.tools.openapi_tool import create_openapi_json_tools_from_uri,create_openapi_json_tools_from_content
|
29
30
|
from ibm_watsonx_orchestrate.cli.commands.models.models_controller import ModelHighlighter
|
30
31
|
from ibm_watsonx_orchestrate.cli.commands.tools.types import RegistryType
|
@@ -32,7 +33,7 @@ from ibm_watsonx_orchestrate.cli.commands.connections.connections_controller imp
|
|
32
33
|
from ibm_watsonx_orchestrate.agent_builder.connections.types import ConnectionType, ConnectionEnvironment, ConnectionPreference
|
33
34
|
from ibm_watsonx_orchestrate.cli.config import Config, CONTEXT_SECTION_HEADER, CONTEXT_ACTIVE_ENV_OPT, \
|
34
35
|
PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_TYPE_OPT, PYTHON_REGISTRY_TEST_PACKAGE_VERSION_OVERRIDE_OPT, \
|
35
|
-
DEFAULT_CONFIG_FILE_CONTENT
|
36
|
+
DEFAULT_CONFIG_FILE_CONTENT, PYTHON_REGISTRY_SKIP_VERSION_CHECK_OPT
|
36
37
|
from ibm_watsonx_orchestrate.agent_builder.connections import ConnectionSecurityScheme, ExpectedCredentials
|
37
38
|
from ibm_watsonx_orchestrate.flow_builder.flows.decorators import FlowWrapper
|
38
39
|
from ibm_watsonx_orchestrate.client.tools.tool_client import ToolClient
|
@@ -40,8 +41,9 @@ from ibm_watsonx_orchestrate.client.toolkit.toolkit_client import ToolKitClient
|
|
40
41
|
from ibm_watsonx_orchestrate.client.connections import get_connections_client, get_connection_type
|
41
42
|
from ibm_watsonx_orchestrate.client.utils import instantiate_client, is_local_dev
|
42
43
|
from ibm_watsonx_orchestrate.flow_builder.utils import import_flow_support_tools
|
43
|
-
from ibm_watsonx_orchestrate.utils.utils import
|
44
|
+
from ibm_watsonx_orchestrate.utils.utils import sanitize_app_id
|
44
45
|
from ibm_watsonx_orchestrate.utils.exceptions import BadRequest
|
46
|
+
from ibm_watsonx_orchestrate.client.tools.tempus_client import TempusClient
|
45
47
|
|
46
48
|
from ibm_watsonx_orchestrate import __version__
|
47
49
|
|
@@ -50,11 +52,21 @@ logger = logging.getLogger(__name__)
|
|
50
52
|
__supported_characters_pattern = re.compile("^(\\w|_)+$")
|
51
53
|
|
52
54
|
|
55
|
+
DEFAULT_LANGFLOW_TOOL_REQUIREMENTS = [
|
56
|
+
"lfx==0.1.8"
|
57
|
+
]
|
58
|
+
|
59
|
+
DEFAULT_LANGFLOW_RUNNER_MODULES = [
|
60
|
+
"lfx",
|
61
|
+
"lfx-nightly"
|
62
|
+
]
|
63
|
+
|
53
64
|
class ToolKind(str, Enum):
|
54
65
|
openapi = "openapi"
|
55
66
|
python = "python"
|
56
67
|
mcp = "mcp"
|
57
68
|
flow = "flow"
|
69
|
+
langflow = "langflow"
|
58
70
|
# skill = "skill"
|
59
71
|
|
60
72
|
def _get_connection_environments() -> List[ConnectionEnvironment]:
|
@@ -64,6 +76,9 @@ def _get_connection_environments() -> List[ConnectionEnvironment]:
|
|
64
76
|
return [env.value for env in ConnectionEnvironment]
|
65
77
|
|
66
78
|
def validate_app_ids(kind: ToolKind, **args) -> None:
|
79
|
+
|
80
|
+
environments = _get_connection_environments()
|
81
|
+
|
67
82
|
app_ids = args.get("app_id")
|
68
83
|
if not app_ids:
|
69
84
|
return
|
@@ -87,44 +102,62 @@ def validate_app_ids(kind: ToolKind, **args) -> None:
|
|
87
102
|
imported_connections[app_id] = {conn_env: conn}
|
88
103
|
|
89
104
|
for app_id in app_ids:
|
90
|
-
|
91
|
-
# Split on = but not on \=
|
92
|
-
split_pattern = re.compile(r"(?<!\\)=")
|
93
|
-
split_id = re.split(split_pattern, app_id)
|
94
|
-
split_id = [x.replace("\\=", "=") for x in split_id]
|
95
|
-
if len(split_id) == 2:
|
96
|
-
_, app_id = split_id
|
97
|
-
elif len(split_id) == 1:
|
98
|
-
app_id = split_id[0]
|
99
|
-
else:
|
100
|
-
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")
|
101
|
-
|
105
|
+
|
102
106
|
if app_id not in imported_connections:
|
103
107
|
logger.warning(f"No connection found for provided app-id '{app_id}'. Please create the connection using `orchestrate connections add`")
|
104
|
-
|
105
|
-
|
106
|
-
|
108
|
+
if kind != ToolKind.python:
|
109
|
+
continue
|
110
|
+
|
111
|
+
permitted_connections_types = []
|
112
|
+
|
113
|
+
match(kind):
|
114
|
+
|
115
|
+
case ToolKind.python:
|
116
|
+
# Split on = but not on \=
|
117
|
+
split_pattern = re.compile(r"(?<!\\)=")
|
118
|
+
split_id = re.split(split_pattern, app_id)
|
119
|
+
split_id = [x.replace("\\=", "=") for x in split_id]
|
120
|
+
if len(split_id) == 2:
|
121
|
+
_, app_id = split_id
|
122
|
+
elif len(split_id) == 1:
|
123
|
+
app_id = split_id[0]
|
124
|
+
else:
|
125
|
+
raise typer.BadParameter(f"The provided --app-id '{app_id}' is not valid. This is likely caused by having mutliple equal signs, please use '\\=' to represent a literal '=' character")
|
107
126
|
continue
|
108
127
|
|
109
|
-
|
128
|
+
# Validate that the connection is not key_value when the tool in openapi
|
129
|
+
case ToolKind.openapi:
|
130
|
+
permitted_connections_types.extend([
|
131
|
+
ConnectionSecurityScheme.API_KEY_AUTH,
|
132
|
+
ConnectionSecurityScheme.BASIC_AUTH,
|
133
|
+
ConnectionSecurityScheme.BEARER_TOKEN,
|
134
|
+
ConnectionSecurityScheme.OAUTH2
|
135
|
+
])
|
110
136
|
|
111
|
-
|
137
|
+
# Validate that the connection is key_value when the tool in langflow
|
138
|
+
case ToolKind.langflow:
|
139
|
+
permitted_connections_types.append(ConnectionSecurityScheme.KEY_VALUE)
|
112
140
|
|
113
|
-
|
114
|
-
conn = imported_connection.get(conn_environment)
|
141
|
+
imported_connection = imported_connections.get(app_id)
|
115
142
|
|
116
|
-
|
117
|
-
|
118
|
-
if conn_environment == ConnectionEnvironment.DRAFT:
|
119
|
-
logger.error(message)
|
120
|
-
sys.exit(1)
|
121
|
-
else:
|
122
|
-
logger.warning(message + " If you deploy this tool without setting the live configuration the tool will error during execution.")
|
123
|
-
continue
|
143
|
+
for conn_environment in environments:
|
144
|
+
conn = imported_connection.get(conn_environment)
|
124
145
|
|
125
|
-
|
126
|
-
|
127
|
-
|
146
|
+
if conn is None or conn.security_scheme is None:
|
147
|
+
message = f"Connection '{app_id}' is not configured in the '{conn_environment}' environment."
|
148
|
+
if conn_environment == ConnectionEnvironment.DRAFT:
|
149
|
+
logger.error(message)
|
150
|
+
sys.exit(1)
|
151
|
+
else:
|
152
|
+
logger.warning(message + " If you deploy this tool without setting the live configuration the tool will error during execution.")
|
153
|
+
continue
|
154
|
+
|
155
|
+
if conn.security_scheme not in permitted_connections_types:
|
156
|
+
logger.error(f"{conn.security_scheme} application connections can not be bound to {kind.value} tools")
|
157
|
+
exit(1)
|
158
|
+
|
159
|
+
|
160
|
+
|
128
161
|
|
129
162
|
def validate_params(kind: ToolKind, **args) -> None:
|
130
163
|
if kind in {"openapi", "python"} and args["file"] is None:
|
@@ -157,8 +190,37 @@ def get_connection_id(app_id: str) -> str:
|
|
157
190
|
connection_id = connection.connection_id
|
158
191
|
return connection_id
|
159
192
|
|
193
|
+
def get_connections(app_ids: list[str] | str = None, environment: str = None, allow_missing: bool = True) -> dict:
|
194
|
+
if not app_ids:
|
195
|
+
return {}
|
196
|
+
if app_ids is str:
|
197
|
+
app_ids = [app_ids]
|
198
|
+
|
199
|
+
connections_client = get_connections_client()
|
200
|
+
if environment:
|
201
|
+
connections = {
|
202
|
+
x.app_id:x for x in connections_client.list() \
|
203
|
+
if x.app_id in app_ids and x.environment == ConnectionEnvironment(environment)
|
204
|
+
}
|
205
|
+
else:
|
206
|
+
connections = { x.app_id:x for x in connections_client.list() if x.app_id in app_ids }
|
207
|
+
|
208
|
+
|
209
|
+
missing = 0
|
210
|
+
for id in app_ids:
|
211
|
+
if not connections.get(id,None):
|
212
|
+
missing += 1
|
213
|
+
|
214
|
+
if missing > 0 and not allow_missing:
|
215
|
+
raise ValueError(f"Could not find {missing} of {len(app_ids)} required connections")
|
216
|
+
|
217
|
+
return connections
|
218
|
+
|
219
|
+
def get_connection_ids(app_ids: list[str] | str = None, environment: str = None, allow_missing: bool = True):
|
220
|
+
connections = get_connections(app_ids=app_ids, environment=environment, allow_missing=allow_missing)
|
221
|
+
return { k:v.connection_id for k,v in connections.items() }
|
160
222
|
|
161
|
-
def
|
223
|
+
def parse_app_ids(app_ids: List[str]) -> dict[str,str]:
|
162
224
|
app_id_dict = {}
|
163
225
|
for app_id in app_ids:
|
164
226
|
# Split on = but not on \=
|
@@ -176,7 +238,7 @@ def parse_python_app_ids(app_ids: List[str]) -> dict[str,str]:
|
|
176
238
|
if not len(runtime_id.strip()) or not len(local_id.strip()):
|
177
239
|
raise typer.BadParameter(f"The provided --app-id '{app_id}' is not valid. --app-id cannot be empty or whitespace")
|
178
240
|
|
179
|
-
runtime_id =
|
241
|
+
runtime_id = sanitize_app_id(runtime_id)
|
180
242
|
app_id_dict[runtime_id] = get_connection_id(local_id)
|
181
243
|
|
182
244
|
return app_id_dict
|
@@ -211,7 +273,7 @@ def validate_python_connections(tool: BaseTool):
|
|
211
273
|
else:
|
212
274
|
expected_tool_conn_types = [expected_cred.type]
|
213
275
|
|
214
|
-
sanatized_expected_tool_app_id =
|
276
|
+
sanatized_expected_tool_app_id = sanitize_app_id(expected_tool_app_id)
|
215
277
|
if sanatized_expected_tool_app_id in existing_sanatized_expected_tool_app_ids:
|
216
278
|
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.")
|
217
279
|
sys.exit(1)
|
@@ -289,6 +351,8 @@ def get_requirement_lines (requirements_file, remove_trailing_newlines=True):
|
|
289
351
|
|
290
352
|
return requirements
|
291
353
|
|
354
|
+
|
355
|
+
|
292
356
|
def import_python_tool(file: str, requirements_file: str = None, app_id: List[str] = None, package_root: str = None) -> List[BaseTool]:
|
293
357
|
try:
|
294
358
|
file_path = Path(file).absolute()
|
@@ -373,7 +437,7 @@ def import_python_tool(file: str, requirements_file: str = None, app_id: List[st
|
|
373
437
|
obj.__tool_spec__.binding.python.function = f"{pkg}:{fn}"
|
374
438
|
|
375
439
|
if app_id and len(app_id):
|
376
|
-
obj.__tool_spec__.binding.python.connections =
|
440
|
+
obj.__tool_spec__.binding.python.connections = parse_app_ids(app_id)
|
377
441
|
|
378
442
|
validate_python_connections(obj)
|
379
443
|
tools.append(obj)
|
@@ -495,11 +559,42 @@ The [bold]flow tool[/bold] is being imported from [green]`{file}`[/green].
|
|
495
559
|
|
496
560
|
return tools
|
497
561
|
|
498
|
-
|
499
562
|
async def import_openapi_tool(file: str, connection_id: str) -> List[BaseTool]:
|
500
563
|
tools = await create_openapi_json_tools_from_uri(file, connection_id)
|
501
564
|
return tools
|
502
565
|
|
566
|
+
async def import_langflow_tool(file: str, app_id: List[str] = None):
|
567
|
+
try:
|
568
|
+
file_path = Path(file).absolute()
|
569
|
+
|
570
|
+
if file_path.is_dir():
|
571
|
+
raise typer.BadParameter(f"Provided langflow file path is not a file.")
|
572
|
+
|
573
|
+
if file_path.is_symlink():
|
574
|
+
raise typer.BadParameter(f"Symbolic links are not supported for langflow file path.")
|
575
|
+
|
576
|
+
if file_path.suffix.lower() != ".json":
|
577
|
+
raise typer.BadParameter(f"Unsupported langflow file type. Only json files are supported.")
|
578
|
+
|
579
|
+
with open(file) as f:
|
580
|
+
imported_tool = json.load(f)
|
581
|
+
|
582
|
+
except typer.BadParameter as ex:
|
583
|
+
raise BadRequest(ex)
|
584
|
+
|
585
|
+
|
586
|
+
except Exception:
|
587
|
+
raise BadRequest(f"Failed to load langflow tool from file {file}")
|
588
|
+
|
589
|
+
validate_app_ids(kind=ToolKind.langflow, app_ids=app_id)
|
590
|
+
connections = get_connection_ids(app_ids=app_id, environment='draft')
|
591
|
+
|
592
|
+
tool = create_langflow_tool(tool_definition=imported_tool, connections=connections)
|
593
|
+
|
594
|
+
|
595
|
+
return tool
|
596
|
+
|
597
|
+
|
503
598
|
def _get_kind_from_spec(spec: dict) -> ToolKind:
|
504
599
|
name = spec.get("name")
|
505
600
|
tool_binding = spec.get("binding")
|
@@ -508,9 +603,11 @@ def _get_kind_from_spec(spec: dict) -> ToolKind:
|
|
508
603
|
return ToolKind.python
|
509
604
|
elif ToolKind.openapi in tool_binding:
|
510
605
|
return ToolKind.openapi
|
606
|
+
elif ToolKind.langflow in tool_binding:
|
607
|
+
return ToolKind.langflow
|
511
608
|
elif ToolKind.mcp in tool_binding:
|
512
609
|
return ToolKind.mcp
|
513
|
-
elif '
|
610
|
+
elif 'flow' in tool_binding:
|
514
611
|
return ToolKind.flow
|
515
612
|
else:
|
516
613
|
logger.error(f"Could not determine 'kind' of tool '{name}'")
|
@@ -565,9 +662,14 @@ class ToolsController:
|
|
565
662
|
case "skill":
|
566
663
|
tools = []
|
567
664
|
logger.warning("Skill Import not implemented yet")
|
665
|
+
case "langflow":
|
666
|
+
tools = asyncio.run(import_langflow_tool(file=args["file"],app_id=args.get('app_id',None)))
|
568
667
|
case _:
|
569
668
|
raise BadRequest("Invalid kind selected")
|
570
669
|
|
670
|
+
if not isinstance(tools,list):
|
671
|
+
tools = [tools]
|
672
|
+
|
571
673
|
for tool in tools:
|
572
674
|
yield tool
|
573
675
|
|
@@ -631,6 +733,12 @@ class ToolsController:
|
|
631
733
|
else:
|
632
734
|
for conn in tool_binding.mcp.connections:
|
633
735
|
connection_ids.append(tool_binding.mcp.connections[conn])
|
736
|
+
elif tool_binding.langflow is not None and hasattr(tool_binding.langflow, "connections"):
|
737
|
+
if tool_binding.langflow.connections is None:
|
738
|
+
connection_ids.append(None)
|
739
|
+
else:
|
740
|
+
for conn in tool_binding.langflow.connections:
|
741
|
+
connection_ids.append(tool_binding.langflow.connections[conn])
|
634
742
|
|
635
743
|
app_ids = []
|
636
744
|
for connection_id in connection_ids:
|
@@ -650,7 +758,9 @@ class ToolsController:
|
|
650
758
|
elif tool_binding.mcp is not None:
|
651
759
|
tool_type=ToolKind.mcp
|
652
760
|
elif tool_binding.flow is not None:
|
653
|
-
tool_type=ToolKind.flow
|
761
|
+
tool_type=ToolKind.flow
|
762
|
+
elif tool_binding.langflow is not None:
|
763
|
+
tool_type=ToolKind.langflow
|
654
764
|
else:
|
655
765
|
tool_type="Unknown"
|
656
766
|
|
@@ -746,15 +856,18 @@ class ToolsController:
|
|
746
856
|
|
747
857
|
cfg = Config()
|
748
858
|
registry_type = cfg.read(PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_TYPE_OPT) or DEFAULT_CONFIG_FILE_CONTENT[PYTHON_REGISTRY_HEADER][PYTHON_REGISTRY_TYPE_OPT]
|
859
|
+
skip_version_check = cfg.read(PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_SKIP_VERSION_CHECK_OPT) or DEFAULT_CONFIG_FILE_CONTENT[PYTHON_REGISTRY_HEADER][PYTHON_REGISTRY_SKIP_VERSION_CHECK_OPT]
|
749
860
|
|
750
861
|
version = __version__
|
751
862
|
if registry_type == RegistryType.LOCAL:
|
863
|
+
logger.warning(f"Using a local registry which is for development purposes only")
|
752
864
|
requirements.append(f"/packages/ibm_watsonx_orchestrate-0.6.0-py3-none-any.whl\n")
|
753
865
|
elif registry_type == RegistryType.PYPI:
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
866
|
+
if not skip_version_check:
|
867
|
+
wheel_file = get_whl_in_registry(registry_url='https://pypi.org/simple/ibm-watsonx-orchestrate', version=version)
|
868
|
+
if not wheel_file:
|
869
|
+
logger.error(f"Could not find ibm-watsonx-orchestrate@{version} on https://pypi.org/project/ibm-watsonx-orchestrate")
|
870
|
+
exit(1)
|
758
871
|
requirements.append(f"ibm-watsonx-orchestrate=={version}\n")
|
759
872
|
elif registry_type == RegistryType.TESTPYPI:
|
760
873
|
override_version = cfg.get(PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_TEST_PACKAGE_VERSION_OVERRIDE_OPT) or version
|
@@ -776,11 +889,51 @@ class ToolsController:
|
|
776
889
|
zip_tool_artifacts.write(requirements_file_path, arcname='requirements.txt')
|
777
890
|
|
778
891
|
zip_tool_artifacts.writestr("bundle-format", "2.0.0\n")
|
892
|
+
|
893
|
+
elif self.tool_kind == ToolKind.langflow:
|
894
|
+
|
895
|
+
tool_artifact = path.join(tmpdir, "artifacts.zip")
|
896
|
+
|
897
|
+
with zipfile.ZipFile(tool_artifact, "w", zipfile.ZIP_DEFLATED) as zip_tool_artifacts:
|
898
|
+
tool_path = Path(self.file)
|
899
|
+
zip_tool_artifacts.write(tool_path, arcname=f"{tool_path.stem}.json")
|
900
|
+
|
901
|
+
requirements = []
|
902
|
+
|
903
|
+
if self.requirements_file:
|
904
|
+
requirements_file_path = Path(self.requirements_file)
|
905
|
+
requirements.extend(
|
906
|
+
get_requirement_lines(requirements_file=requirements_file_path, remove_trailing_newlines=False)
|
907
|
+
)
|
908
|
+
|
909
|
+
langflowTool = cast(LangflowTool, tool)
|
910
|
+
# if there are additional requriements from the langflow model, we should add it to the requirement set
|
911
|
+
if langflowTool.requirements and len(langflowTool.requirements) > 0:
|
912
|
+
requirements.extend(langflowTool.requirements)
|
913
|
+
|
914
|
+
# now check if the requirements contain modules listed in DEFAULT_LANGFLOW_RUNNER_MODULES
|
915
|
+
# if it is needed, we are assuming the user wants to override the default langflow module
|
916
|
+
# with a specific version
|
917
|
+
runner_overridden = False
|
918
|
+
for r in requirements:
|
919
|
+
# get the module name from the requirements
|
920
|
+
module_name = r.strip().split('==')[0].split('=')[0].split('>=')[0].split('<=')[0].split('~=')[0].lower()
|
921
|
+
if not module_name.startswith('#'):
|
922
|
+
if module_name in DEFAULT_LANGFLOW_RUNNER_MODULES:
|
923
|
+
runner_overridden = True
|
924
|
+
|
925
|
+
if not runner_overridden:
|
926
|
+
# add the default runner to the top of requirement list
|
927
|
+
requirements = DEFAULT_LANGFLOW_TOOL_REQUIREMENTS + list(requirements)
|
928
|
+
|
929
|
+
requirements_content = '\n'.join(requirements) + '\n'
|
930
|
+
zip_tool_artifacts.writestr("requirements.txt",requirements_content)
|
931
|
+
zip_tool_artifacts.writestr("bundle-format", "2.0.0\n")
|
779
932
|
|
780
933
|
if exist:
|
781
934
|
self.update_tool(tool_id=tool_id, tool=tool, tool_artifact=tool_artifact)
|
782
935
|
else:
|
783
|
-
self.publish_tool(tool, tool_artifact=tool_artifact)
|
936
|
+
self.publish_tool(tool=tool, tool_artifact=tool_artifact)
|
784
937
|
|
785
938
|
def publish_tool(self, tool: BaseTool, tool_artifact: str) -> None:
|
786
939
|
tool_spec = tool.__tool_spec__.model_dump(mode='json', exclude_unset=True, exclude_none=True, by_alias=True)
|
@@ -789,7 +942,11 @@ class ToolsController:
|
|
789
942
|
tool_id = response.get("id")
|
790
943
|
|
791
944
|
if tool_artifact is not None:
|
792
|
-
self.
|
945
|
+
match self.tool_kind:
|
946
|
+
case ToolKind.langflow | ToolKind.python:
|
947
|
+
self.get_client().upload_tools_artifact(tool_id=tool_id, file_path=tool_artifact)
|
948
|
+
case _:
|
949
|
+
raise ValueError(f"Unexpected artifact for {self.tool_kind} tool")
|
793
950
|
|
794
951
|
logger.info(f"Tool '{tool.__tool_spec__.name}' imported successfully")
|
795
952
|
|
@@ -801,7 +958,11 @@ class ToolsController:
|
|
801
958
|
self.get_client().update(tool_id, tool_spec)
|
802
959
|
|
803
960
|
if tool_artifact is not None:
|
804
|
-
self.
|
961
|
+
match self.tool_kind:
|
962
|
+
case ToolKind.langflow | ToolKind.python:
|
963
|
+
self.get_client().upload_tools_artifact(tool_id=tool_id, file_path=tool_artifact)
|
964
|
+
case _:
|
965
|
+
raise ValueError(f"Unexpected artifact for {self.tool_kind} tool")
|
805
966
|
|
806
967
|
logger.info(f"Tool '{tool.__tool_spec__.name}' updated successfully")
|
807
968
|
|
@@ -823,6 +984,23 @@ class ToolsController:
|
|
823
984
|
logger.error(e.response.text)
|
824
985
|
exit(1)
|
825
986
|
|
987
|
+
def serialize_to_json_in_zip(self, obj: any, filename: str) -> bytes:
|
988
|
+
# Serialize the Python object to a JSON string
|
989
|
+
json_str = json.dumps(obj, indent=2)
|
990
|
+
|
991
|
+
# Create a BytesIO object to hold the in-memory zip file
|
992
|
+
zip_in_memory = io.BytesIO()
|
993
|
+
|
994
|
+
# Create a ZipFile object in append mode
|
995
|
+
with zipfile.ZipFile(zip_in_memory, 'a') as zip_file:
|
996
|
+
# Write the JSON string as a file named 'data.json' inside the zip
|
997
|
+
zip_file.writestr(filename, json_str)
|
998
|
+
|
999
|
+
# Seek to the beginning of the BytesIO object to return the in-memory zip file as bytes
|
1000
|
+
zip_in_memory.seek(0)
|
1001
|
+
|
1002
|
+
return zip_in_memory.getvalue()
|
1003
|
+
|
826
1004
|
def download_tool(self, name: str) -> bytes | None:
|
827
1005
|
tool_client = self.get_client()
|
828
1006
|
draft_tools = tool_client.get_draft_by_name(tool_name=name)
|
@@ -837,13 +1015,27 @@ class ToolsController:
|
|
837
1015
|
draft_tool_kind = _get_kind_from_spec(draft_tool)
|
838
1016
|
|
839
1017
|
# TODO: Add openapi tool support
|
840
|
-
|
1018
|
+
supported_toolkinds = [ToolKind.python,ToolKind.langflow,ToolKind.flow]
|
1019
|
+
if draft_tool_kind not in supported_toolkinds:
|
841
1020
|
logger.warning(f"Skipping '{name}', {draft_tool_kind.value} tools are currently unsupported by export")
|
842
1021
|
return
|
843
1022
|
|
844
1023
|
tool_id = draft_tool.get("id")
|
845
1024
|
|
846
|
-
|
1025
|
+
if draft_tool_kind == ToolKind.python or draft_tool_kind == ToolKind.langflow:
|
1026
|
+
tool_artifacts_bytes = tool_client.download_tools_artifact(tool_id=tool_id)
|
1027
|
+
elif draft_tool_kind == ToolKind.flow:
|
1028
|
+
if not is_local_dev():
|
1029
|
+
logger.warning("Skipping '{name}', Flow tool export is only supported in local dev mode")
|
1030
|
+
return
|
1031
|
+
|
1032
|
+
client = instantiate_client(TempusClient)
|
1033
|
+
flow_model = client.get_flow_model(tool_id)
|
1034
|
+
# we need to fix the name as sometimes it is left as 'untitled' by the builder
|
1035
|
+
if "data" in flow_model:
|
1036
|
+
flow_model["data"]["spec"]["name"] = name
|
1037
|
+
tool_artifacts_bytes = self.serialize_to_json_in_zip(flow_model["data"], f"{name}.json")
|
1038
|
+
|
847
1039
|
return tool_artifacts_bytes
|
848
1040
|
|
849
1041
|
def export_tool(self, name: str, output_path: str) -> None:
|
@@ -22,6 +22,7 @@ AUTH_MCSP_TOKEN_OPT = "wxo_mcsp_token"
|
|
22
22
|
AUTH_MCSP_TOKEN_EXPIRY_OPT = "wxo_mcsp_token_expiry"
|
23
23
|
CONTEXT_ACTIVE_ENV_OPT = "active_environment"
|
24
24
|
PYTHON_REGISTRY_TYPE_OPT = "type"
|
25
|
+
PYTHON_REGISTRY_SKIP_VERSION_CHECK_OPT = "skip_version_check"
|
25
26
|
PYTHON_REGISTRY_TEST_PACKAGE_VERSION_OVERRIDE_OPT = "test_package_version_override"
|
26
27
|
ENV_WXO_URL_OPT = "wxo_url"
|
27
28
|
ENV_IAM_URL_OPT = "iam_url"
|
@@ -40,7 +41,8 @@ DEFAULT_CONFIG_FILE_CONTENT = {
|
|
40
41
|
CONTEXT_SECTION_HEADER: {CONTEXT_ACTIVE_ENV_OPT: None},
|
41
42
|
PYTHON_REGISTRY_HEADER: {
|
42
43
|
PYTHON_REGISTRY_TYPE_OPT: str(RegistryType.PYPI),
|
43
|
-
PYTHON_REGISTRY_TEST_PACKAGE_VERSION_OVERRIDE_OPT: None
|
44
|
+
PYTHON_REGISTRY_TEST_PACKAGE_VERSION_OVERRIDE_OPT: None,
|
45
|
+
PYTHON_REGISTRY_SKIP_VERSION_CHECK_OPT: False
|
44
46
|
},
|
45
47
|
ENVIRONMENTS_SECTION_HEADER: {
|
46
48
|
PROTECTED_ENV_NAME: {
|
@@ -11,6 +11,7 @@ from ibm_watsonx_orchestrate.cli.commands.server.server_command import server_ap
|
|
11
11
|
from ibm_watsonx_orchestrate.cli.commands.chat.chat_command import chat_app
|
12
12
|
from ibm_watsonx_orchestrate.cli.commands.models.models_command import models_app
|
13
13
|
from ibm_watsonx_orchestrate.cli.commands.environment.environment_command import environment_app
|
14
|
+
from ibm_watsonx_orchestrate.cli.commands.partners.partners_command import partners_app
|
14
15
|
from ibm_watsonx_orchestrate.cli.commands.channels.channels_command import channel_app
|
15
16
|
from ibm_watsonx_orchestrate.cli.commands.knowledge_bases.knowledge_bases_command import knowledge_bases_app
|
16
17
|
from ibm_watsonx_orchestrate.cli.commands.toolkit.toolkit_command import toolkits_app
|
@@ -44,6 +45,7 @@ app.add_typer(channel_app, name="channels", help="Configure channels where your
|
|
44
45
|
app.add_typer(evaluation_app, name="evaluations", help='Evaluate the performance of your agents in your active env')
|
45
46
|
app.add_typer(copilot_app, name="copilot", help='Access AI powered assistance to help refine your agents')
|
46
47
|
app.add_typer(settings_app, name="settings", help='Configure the settings for your active env')
|
48
|
+
app.add_typer(partners_app, name="partners", help='Generate a well-structured, submission-ready agent artifact package for partner-built agents')
|
47
49
|
|
48
50
|
if __name__ == "__main__":
|
49
51
|
app()
|
@@ -6,7 +6,7 @@ from typing import Optional
|
|
6
6
|
from enum import Enum
|
7
7
|
|
8
8
|
from ibm_watsonx_orchestrate.client.base_api_client import BaseAPIClient, ClientAPIException
|
9
|
-
from ibm_watsonx_orchestrate.agent_builder.connections.types import ConnectionEnvironment, ConnectionPreference, ConnectionAuthType, ConnectionSecurityScheme, IdpConfigData, AppConfigData, ConnectionType
|
9
|
+
from ibm_watsonx_orchestrate.agent_builder.connections.types import ConnectionEnvironment, ConnectionPreference, ConnectionConfiguration, ConnectionAuthType, ConnectionSecurityScheme, IdpConfigData, AppConfigData, ConnectionType
|
10
10
|
from ibm_watsonx_orchestrate.client.utils import is_cpd_env, is_local_dev
|
11
11
|
|
12
12
|
import logging
|
@@ -47,6 +47,9 @@ class GetConfigResponse(BaseModel):
|
|
47
47
|
idp_config_data: Optional[IdpConfigData] = None
|
48
48
|
app_config_data: Optional[AppConfigData] = None
|
49
49
|
|
50
|
+
def as_config(self):
|
51
|
+
return ConnectionConfiguration(**dict(self))
|
52
|
+
|
50
53
|
class GetConnectionResponse(BaseModel):
|
51
54
|
connection_id: str = None
|
52
55
|
app_id: str = None
|
@@ -42,4 +42,7 @@ class TempusClient(BaseAPIClient):
|
|
42
42
|
|
43
43
|
def arun_flow(self, flow_id: str, input: dict) -> dict:
|
44
44
|
return self._post(f"/flows/{flow_id}/versions/TIP/run/async", data=input)
|
45
|
+
|
46
|
+
def get_flow_model(self, flow_id: str, version: str = "TIP") -> dict:
|
47
|
+
return self._get(f"/flow-models/{flow_id}/versions/{version}")
|
45
48
|
|
@@ -10,7 +10,7 @@ class ToolClient(BaseAPIClient):
|
|
10
10
|
def create(self, payload: dict) -> dict:
|
11
11
|
return self._post("/tools", data=payload)
|
12
12
|
|
13
|
-
def get(self) -> dict:
|
13
|
+
def get(self) -> dict:
|
14
14
|
return self._get("/tools")
|
15
15
|
|
16
16
|
def update(self, agent_id: str, data: dict) -> dict:
|
@@ -21,10 +21,13 @@ class ToolClient(BaseAPIClient):
|
|
21
21
|
|
22
22
|
def upload_tools_artifact(self, tool_id: str, file_path: str) -> dict:
|
23
23
|
return self._post(f"/tools/{tool_id}/upload", files={"file": (f"{tool_id}.zip", open(file_path, "rb"), "application/zip", {"Expires": "0"})})
|
24
|
-
|
24
|
+
|
25
25
|
def download_tools_artifact(self, tool_id: str) -> bytes:
|
26
26
|
response = self._get(f"/tools/{tool_id}/download", return_raw=True)
|
27
27
|
return response.content
|
28
|
+
|
29
|
+
def download_tools_json(self, tool_id: str) -> dict:
|
30
|
+
return self.download_tools_artifact(tool_id)
|
28
31
|
|
29
32
|
def get_draft_by_name(self, tool_name: str) -> List[dict]:
|
30
33
|
return self.get_drafts_by_names([tool_name])
|