ibm-watsonx-orchestrate 1.11.1__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/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/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/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 +233 -44
- 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 +58 -7
- ibm_watsonx_orchestrate/docker/default.env +20 -17
- 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.1.dist-info → ibm_watsonx_orchestrate-1.12.0.dist-info}/METADATA +2 -2
- {ibm_watsonx_orchestrate-1.11.1.dist-info → ibm_watsonx_orchestrate-1.12.0.dist-info}/RECORD +52 -41
- {ibm_watsonx_orchestrate-1.11.1.dist-info → ibm_watsonx_orchestrate-1.12.0.dist-info}/WHEEL +0 -0
- {ibm_watsonx_orchestrate-1.11.1.dist-info → ibm_watsonx_orchestrate-1.12.0.dist-info}/entry_points.txt +0 -0
- {ibm_watsonx_orchestrate-1.11.1.dist-info → ibm_watsonx_orchestrate-1.12.0.dist-info}/licenses/LICENSE +0 -0
@@ -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
|
@@ -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
|
-
# Validate that the connection is not key_value when the tool in openapi
|
106
|
-
if kind != ToolKind.openapi:
|
108
|
+
if kind != ToolKind.python:
|
107
109
|
continue
|
108
110
|
|
109
|
-
|
111
|
+
permitted_connections_types = []
|
112
|
+
|
113
|
+
match(kind):
|
110
114
|
|
111
|
-
|
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")
|
126
|
+
continue
|
112
127
|
|
113
|
-
|
114
|
-
|
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
|
+
])
|
115
136
|
|
116
|
-
|
117
|
-
|
118
|
-
|
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
|
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)
|
124
140
|
|
125
|
-
|
126
|
-
|
127
|
-
|
141
|
+
imported_connection = imported_connections.get(app_id)
|
142
|
+
|
143
|
+
for conn_environment in environments:
|
144
|
+
conn = imported_connection.get(conn_environment)
|
145
|
+
|
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
|
160
218
|
|
161
|
-
def
|
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() }
|
222
|
+
|
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
|
|
@@ -779,11 +889,51 @@ class ToolsController:
|
|
779
889
|
zip_tool_artifacts.write(requirements_file_path, arcname='requirements.txt')
|
780
890
|
|
781
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")
|
782
932
|
|
783
933
|
if exist:
|
784
934
|
self.update_tool(tool_id=tool_id, tool=tool, tool_artifact=tool_artifact)
|
785
935
|
else:
|
786
|
-
self.publish_tool(tool, tool_artifact=tool_artifact)
|
936
|
+
self.publish_tool(tool=tool, tool_artifact=tool_artifact)
|
787
937
|
|
788
938
|
def publish_tool(self, tool: BaseTool, tool_artifact: str) -> None:
|
789
939
|
tool_spec = tool.__tool_spec__.model_dump(mode='json', exclude_unset=True, exclude_none=True, by_alias=True)
|
@@ -792,7 +942,11 @@ class ToolsController:
|
|
792
942
|
tool_id = response.get("id")
|
793
943
|
|
794
944
|
if tool_artifact is not None:
|
795
|
-
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")
|
796
950
|
|
797
951
|
logger.info(f"Tool '{tool.__tool_spec__.name}' imported successfully")
|
798
952
|
|
@@ -804,7 +958,11 @@ class ToolsController:
|
|
804
958
|
self.get_client().update(tool_id, tool_spec)
|
805
959
|
|
806
960
|
if tool_artifact is not None:
|
807
|
-
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")
|
808
966
|
|
809
967
|
logger.info(f"Tool '{tool.__tool_spec__.name}' updated successfully")
|
810
968
|
|
@@ -826,6 +984,23 @@ class ToolsController:
|
|
826
984
|
logger.error(e.response.text)
|
827
985
|
exit(1)
|
828
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
|
+
|
829
1004
|
def download_tool(self, name: str) -> bytes | None:
|
830
1005
|
tool_client = self.get_client()
|
831
1006
|
draft_tools = tool_client.get_draft_by_name(tool_name=name)
|
@@ -840,13 +1015,27 @@ class ToolsController:
|
|
840
1015
|
draft_tool_kind = _get_kind_from_spec(draft_tool)
|
841
1016
|
|
842
1017
|
# TODO: Add openapi tool support
|
843
|
-
|
1018
|
+
supported_toolkinds = [ToolKind.python,ToolKind.langflow,ToolKind.flow]
|
1019
|
+
if draft_tool_kind not in supported_toolkinds:
|
844
1020
|
logger.warning(f"Skipping '{name}', {draft_tool_kind.value} tools are currently unsupported by export")
|
845
1021
|
return
|
846
1022
|
|
847
1023
|
tool_id = draft_tool.get("id")
|
848
1024
|
|
849
|
-
|
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
|
+
|
850
1039
|
return tool_artifacts_bytes
|
851
1040
|
|
852
1041
|
def export_tool(self, name: str, output_path: str) -> None:
|
@@ -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])
|
@@ -1,3 +1,5 @@
|
|
1
|
+
import platform
|
2
|
+
|
1
3
|
from ibm_watsonx_orchestrate.cli.config import (
|
2
4
|
Config,
|
3
5
|
DEFAULT_CONFIG_FILE_FOLDER,
|
@@ -170,4 +172,32 @@ def instantiate_client(client: type[T] , url: str | None=None) -> T:
|
|
170
172
|
except FileNotFoundError as e:
|
171
173
|
message = "No active environment found. Please run `orchestrate env activate` to activate an environment"
|
172
174
|
logger.error(message)
|
173
|
-
raise FileNotFoundError(message)
|
175
|
+
raise FileNotFoundError(message)
|
176
|
+
|
177
|
+
|
178
|
+
def get_architecture () -> str:
|
179
|
+
arch = platform.machine().lower()
|
180
|
+
if arch in ("amd64", "x86_64"):
|
181
|
+
return "amd64"
|
182
|
+
|
183
|
+
elif arch == "i386":
|
184
|
+
return "386"
|
185
|
+
|
186
|
+
elif arch in ("aarch64", "arm64", "arm"):
|
187
|
+
return "arm"
|
188
|
+
|
189
|
+
else:
|
190
|
+
raise Exception("Unsupported architecture %s" % arch)
|
191
|
+
|
192
|
+
|
193
|
+
def is_arm_architecture () -> bool:
|
194
|
+
return platform.machine().lower() in ("aarch64", "arm64", "arm")
|
195
|
+
|
196
|
+
|
197
|
+
def get_os_type () -> str:
|
198
|
+
system = platform.system().lower()
|
199
|
+
if system in ("linux", "darwin", "windows"):
|
200
|
+
return system
|
201
|
+
|
202
|
+
else:
|
203
|
+
raise Exception("Unsupported operating system %s" % system)
|