ibm-watsonx-orchestrate 1.11.0b1__py3-none-any.whl → 1.12.0b1__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 -2
- 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 +124 -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 +49 -0
- 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 +458 -0
- ibm_watsonx_orchestrate/cli/commands/partners/offering/types.py +107 -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 +206 -43
- 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 +8 -2
- ibm_watsonx_orchestrate/flow_builder/flows/flow.py +31 -7
- ibm_watsonx_orchestrate/flow_builder/node.py +1 -1
- ibm_watsonx_orchestrate/flow_builder/types.py +18 -3
- ibm_watsonx_orchestrate/utils/docker_utils.py +280 -0
- ibm_watsonx_orchestrate/utils/environment.py +369 -0
- ibm_watsonx_orchestrate/utils/utils.py +1 -1
- {ibm_watsonx_orchestrate-1.11.0b1.dist-info → ibm_watsonx_orchestrate-1.12.0b1.dist-info}/METADATA +2 -2
- {ibm_watsonx_orchestrate-1.11.0b1.dist-info → ibm_watsonx_orchestrate-1.12.0b1.dist-info}/RECORD +50 -42
- {ibm_watsonx_orchestrate-1.11.0b1.dist-info → ibm_watsonx_orchestrate-1.12.0b1.dist-info}/WHEEL +0 -0
- {ibm_watsonx_orchestrate-1.11.0b1.dist-info → ibm_watsonx_orchestrate-1.12.0b1.dist-info}/entry_points.txt +0 -0
- {ibm_watsonx_orchestrate-1.11.0b1.dist-info → ibm_watsonx_orchestrate-1.12.0b1.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[
|
@@ -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 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,16 @@ 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
|
+
|
53
59
|
class ToolKind(str, Enum):
|
54
60
|
openapi = "openapi"
|
55
61
|
python = "python"
|
56
62
|
mcp = "mcp"
|
57
63
|
flow = "flow"
|
64
|
+
langflow = "langflow"
|
58
65
|
# skill = "skill"
|
59
66
|
|
60
67
|
def _get_connection_environments() -> List[ConnectionEnvironment]:
|
@@ -64,6 +71,9 @@ def _get_connection_environments() -> List[ConnectionEnvironment]:
|
|
64
71
|
return [env.value for env in ConnectionEnvironment]
|
65
72
|
|
66
73
|
def validate_app_ids(kind: ToolKind, **args) -> None:
|
74
|
+
|
75
|
+
environments = _get_connection_environments()
|
76
|
+
|
67
77
|
app_ids = args.get("app_id")
|
68
78
|
if not app_ids:
|
69
79
|
return
|
@@ -87,44 +97,62 @@ def validate_app_ids(kind: ToolKind, **args) -> None:
|
|
87
97
|
imported_connections[app_id] = {conn_env: conn}
|
88
98
|
|
89
99
|
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
|
-
|
100
|
+
|
102
101
|
if app_id not in imported_connections:
|
103
102
|
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:
|
103
|
+
if kind != ToolKind.python:
|
107
104
|
continue
|
108
105
|
|
109
|
-
|
106
|
+
permitted_connections_types = []
|
110
107
|
|
111
|
-
|
108
|
+
match(kind):
|
112
109
|
|
113
|
-
|
114
|
-
|
110
|
+
case ToolKind.python:
|
111
|
+
# Split on = but not on \=
|
112
|
+
split_pattern = re.compile(r"(?<!\\)=")
|
113
|
+
split_id = re.split(split_pattern, app_id)
|
114
|
+
split_id = [x.replace("\\=", "=") for x in split_id]
|
115
|
+
if len(split_id) == 2:
|
116
|
+
_, app_id = split_id
|
117
|
+
elif len(split_id) == 1:
|
118
|
+
app_id = split_id[0]
|
119
|
+
else:
|
120
|
+
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")
|
121
|
+
continue
|
115
122
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
123
|
+
# Validate that the connection is not key_value when the tool in openapi
|
124
|
+
case ToolKind.openapi:
|
125
|
+
permitted_connections_types.extend([
|
126
|
+
ConnectionSecurityScheme.API_KEY_AUTH,
|
127
|
+
ConnectionSecurityScheme.BASIC_AUTH,
|
128
|
+
ConnectionSecurityScheme.BEARER_TOKEN,
|
129
|
+
ConnectionSecurityScheme.OAUTH2
|
130
|
+
])
|
131
|
+
|
132
|
+
# Validate that the connection is key_value when the tool in langflow
|
133
|
+
case ToolKind.langflow:
|
134
|
+
permitted_connections_types.append(ConnectionSecurityScheme.KEY_VALUE)
|
124
135
|
|
125
|
-
|
126
|
-
|
127
|
-
|
136
|
+
imported_connection = imported_connections.get(app_id)
|
137
|
+
|
138
|
+
for conn_environment in environments:
|
139
|
+
conn = imported_connection.get(conn_environment)
|
140
|
+
|
141
|
+
if conn is None or conn.security_scheme is None:
|
142
|
+
message = f"Connection '{app_id}' is not configured in the '{conn_environment}' environment."
|
143
|
+
if conn_environment == ConnectionEnvironment.DRAFT:
|
144
|
+
logger.error(message)
|
145
|
+
sys.exit(1)
|
146
|
+
else:
|
147
|
+
logger.warning(message + " If you deploy this tool without setting the live configuration the tool will error during execution.")
|
148
|
+
continue
|
149
|
+
|
150
|
+
if conn.security_scheme not in permitted_connections_types:
|
151
|
+
logger.error(f"{conn.security_scheme} application connections can not be bound to {kind.value} tools")
|
152
|
+
exit(1)
|
153
|
+
|
154
|
+
|
155
|
+
|
128
156
|
|
129
157
|
def validate_params(kind: ToolKind, **args) -> None:
|
130
158
|
if kind in {"openapi", "python"} and args["file"] is None:
|
@@ -157,8 +185,37 @@ def get_connection_id(app_id: str) -> str:
|
|
157
185
|
connection_id = connection.connection_id
|
158
186
|
return connection_id
|
159
187
|
|
188
|
+
def get_connections(app_ids: list[str] | str = None, environment: str = None, allow_missing: bool = True) -> dict:
|
189
|
+
if not app_ids:
|
190
|
+
return {}
|
191
|
+
if app_ids is str:
|
192
|
+
app_ids = [app_ids]
|
193
|
+
|
194
|
+
connections_client = get_connections_client()
|
195
|
+
if environment:
|
196
|
+
connections = {
|
197
|
+
x.app_id:x for x in connections_client.list() \
|
198
|
+
if x.app_id in app_ids and x.environment == ConnectionEnvironment(environment)
|
199
|
+
}
|
200
|
+
else:
|
201
|
+
connections = { x.app_id:x for x in connections_client.list() if x.app_id in app_ids }
|
202
|
+
|
203
|
+
|
204
|
+
missing = 0
|
205
|
+
for id in app_ids:
|
206
|
+
if not connections.get(id,None):
|
207
|
+
missing += 1
|
208
|
+
|
209
|
+
if missing > 0 and not allow_missing:
|
210
|
+
raise ValueError(f"Could not find {missing} of {len(app_ids)} required connections")
|
160
211
|
|
161
|
-
|
212
|
+
return connections
|
213
|
+
|
214
|
+
def get_connection_ids(app_ids: list[str] | str = None, environment: str = None, allow_missing: bool = True):
|
215
|
+
connections = get_connections(app_ids=app_ids, environment=environment, allow_missing=allow_missing)
|
216
|
+
return { k:v.connection_id for k,v in connections.items() }
|
217
|
+
|
218
|
+
def parse_app_ids(app_ids: List[str]) -> dict[str,str]:
|
162
219
|
app_id_dict = {}
|
163
220
|
for app_id in app_ids:
|
164
221
|
# Split on = but not on \=
|
@@ -176,7 +233,7 @@ def parse_python_app_ids(app_ids: List[str]) -> dict[str,str]:
|
|
176
233
|
if not len(runtime_id.strip()) or not len(local_id.strip()):
|
177
234
|
raise typer.BadParameter(f"The provided --app-id '{app_id}' is not valid. --app-id cannot be empty or whitespace")
|
178
235
|
|
179
|
-
runtime_id =
|
236
|
+
runtime_id = sanitize_app_id(runtime_id)
|
180
237
|
app_id_dict[runtime_id] = get_connection_id(local_id)
|
181
238
|
|
182
239
|
return app_id_dict
|
@@ -211,7 +268,7 @@ def validate_python_connections(tool: BaseTool):
|
|
211
268
|
else:
|
212
269
|
expected_tool_conn_types = [expected_cred.type]
|
213
270
|
|
214
|
-
sanatized_expected_tool_app_id =
|
271
|
+
sanatized_expected_tool_app_id = sanitize_app_id(expected_tool_app_id)
|
215
272
|
if sanatized_expected_tool_app_id in existing_sanatized_expected_tool_app_ids:
|
216
273
|
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
274
|
sys.exit(1)
|
@@ -289,6 +346,8 @@ def get_requirement_lines (requirements_file, remove_trailing_newlines=True):
|
|
289
346
|
|
290
347
|
return requirements
|
291
348
|
|
349
|
+
|
350
|
+
|
292
351
|
def import_python_tool(file: str, requirements_file: str = None, app_id: List[str] = None, package_root: str = None) -> List[BaseTool]:
|
293
352
|
try:
|
294
353
|
file_path = Path(file).absolute()
|
@@ -373,7 +432,7 @@ def import_python_tool(file: str, requirements_file: str = None, app_id: List[st
|
|
373
432
|
obj.__tool_spec__.binding.python.function = f"{pkg}:{fn}"
|
374
433
|
|
375
434
|
if app_id and len(app_id):
|
376
|
-
obj.__tool_spec__.binding.python.connections =
|
435
|
+
obj.__tool_spec__.binding.python.connections = parse_app_ids(app_id)
|
377
436
|
|
378
437
|
validate_python_connections(obj)
|
379
438
|
tools.append(obj)
|
@@ -495,11 +554,42 @@ The [bold]flow tool[/bold] is being imported from [green]`{file}`[/green].
|
|
495
554
|
|
496
555
|
return tools
|
497
556
|
|
498
|
-
|
499
557
|
async def import_openapi_tool(file: str, connection_id: str) -> List[BaseTool]:
|
500
558
|
tools = await create_openapi_json_tools_from_uri(file, connection_id)
|
501
559
|
return tools
|
502
560
|
|
561
|
+
async def import_langflow_tool(file: str, app_id: List[str] = None):
|
562
|
+
try:
|
563
|
+
file_path = Path(file).absolute()
|
564
|
+
|
565
|
+
if file_path.is_dir():
|
566
|
+
raise typer.BadParameter(f"Provided langflow file path is not a file.")
|
567
|
+
|
568
|
+
if file_path.is_symlink():
|
569
|
+
raise typer.BadParameter(f"Symbolic links are not supported for langflow file path.")
|
570
|
+
|
571
|
+
if file_path.suffix.lower() != ".json":
|
572
|
+
raise typer.BadParameter(f"Unsupported langflow file type. Only json files are supported.")
|
573
|
+
|
574
|
+
with open(file) as f:
|
575
|
+
imported_tool = json.load(f)
|
576
|
+
|
577
|
+
except typer.BadParameter as ex:
|
578
|
+
raise BadRequest(ex)
|
579
|
+
|
580
|
+
|
581
|
+
except Exception:
|
582
|
+
raise BadRequest(f"Failed to load langflow tool from file {file}")
|
583
|
+
|
584
|
+
validate_app_ids(kind=ToolKind.langflow, app_ids=app_id)
|
585
|
+
connections = get_connection_ids(app_ids=app_id, environment='draft')
|
586
|
+
|
587
|
+
tool = create_langflow_tool(tool_definition=imported_tool, connections=connections)
|
588
|
+
|
589
|
+
|
590
|
+
return tool
|
591
|
+
|
592
|
+
|
503
593
|
def _get_kind_from_spec(spec: dict) -> ToolKind:
|
504
594
|
name = spec.get("name")
|
505
595
|
tool_binding = spec.get("binding")
|
@@ -508,9 +598,11 @@ def _get_kind_from_spec(spec: dict) -> ToolKind:
|
|
508
598
|
return ToolKind.python
|
509
599
|
elif ToolKind.openapi in tool_binding:
|
510
600
|
return ToolKind.openapi
|
601
|
+
elif ToolKind.langflow in tool_binding:
|
602
|
+
return ToolKind.langflow
|
511
603
|
elif ToolKind.mcp in tool_binding:
|
512
604
|
return ToolKind.mcp
|
513
|
-
elif '
|
605
|
+
elif 'flow' in tool_binding:
|
514
606
|
return ToolKind.flow
|
515
607
|
else:
|
516
608
|
logger.error(f"Could not determine 'kind' of tool '{name}'")
|
@@ -565,9 +657,14 @@ class ToolsController:
|
|
565
657
|
case "skill":
|
566
658
|
tools = []
|
567
659
|
logger.warning("Skill Import not implemented yet")
|
660
|
+
case "langflow":
|
661
|
+
tools = asyncio.run(import_langflow_tool(file=args["file"],app_id=args.get('app_id',None)))
|
568
662
|
case _:
|
569
663
|
raise BadRequest("Invalid kind selected")
|
570
664
|
|
665
|
+
if not isinstance(tools,list):
|
666
|
+
tools = [tools]
|
667
|
+
|
571
668
|
for tool in tools:
|
572
669
|
yield tool
|
573
670
|
|
@@ -631,6 +728,12 @@ class ToolsController:
|
|
631
728
|
else:
|
632
729
|
for conn in tool_binding.mcp.connections:
|
633
730
|
connection_ids.append(tool_binding.mcp.connections[conn])
|
731
|
+
elif tool_binding.langflow is not None and hasattr(tool_binding.langflow, "connections"):
|
732
|
+
if tool_binding.langflow.connections is None:
|
733
|
+
connection_ids.append(None)
|
734
|
+
else:
|
735
|
+
for conn in tool_binding.langflow.connections:
|
736
|
+
connection_ids.append(tool_binding.langflow.connections[conn])
|
634
737
|
|
635
738
|
app_ids = []
|
636
739
|
for connection_id in connection_ids:
|
@@ -650,7 +753,9 @@ class ToolsController:
|
|
650
753
|
elif tool_binding.mcp is not None:
|
651
754
|
tool_type=ToolKind.mcp
|
652
755
|
elif tool_binding.flow is not None:
|
653
|
-
tool_type=ToolKind.flow
|
756
|
+
tool_type=ToolKind.flow
|
757
|
+
elif tool_binding.langflow is not None:
|
758
|
+
tool_type=ToolKind.langflow
|
654
759
|
else:
|
655
760
|
tool_type="Unknown"
|
656
761
|
|
@@ -776,11 +881,30 @@ class ToolsController:
|
|
776
881
|
zip_tool_artifacts.write(requirements_file_path, arcname='requirements.txt')
|
777
882
|
|
778
883
|
zip_tool_artifacts.writestr("bundle-format", "2.0.0\n")
|
884
|
+
|
885
|
+
elif self.tool_kind == ToolKind.langflow:
|
886
|
+
|
887
|
+
tool_artifact = path.join(tmpdir, "artifacts.zip")
|
888
|
+
|
889
|
+
with zipfile.ZipFile(tool_artifact, "w", zipfile.ZIP_DEFLATED) as zip_tool_artifacts:
|
890
|
+
tool_path = Path(self.file)
|
891
|
+
zip_tool_artifacts.write(tool_path, arcname=f"{tool_path.stem}.json")
|
892
|
+
|
893
|
+
requirements = DEFAULT_LANGFLOW_TOOL_REQUIREMENTS
|
894
|
+
|
895
|
+
if self.requirements_file:
|
896
|
+
requirements_file_path = Path(self.requirements_file)
|
897
|
+
requirements.extend(
|
898
|
+
get_requirement_lines(requirements_file=requirements_file_path, remove_trailing_newlines=False)
|
899
|
+
)
|
900
|
+
requirements_content = '\n'.join(requirements) + '\n'
|
901
|
+
zip_tool_artifacts.writestr("requirements.txt",requirements_content)
|
902
|
+
zip_tool_artifacts.writestr("bundle-format", "2.0.0\n")
|
779
903
|
|
780
904
|
if exist:
|
781
905
|
self.update_tool(tool_id=tool_id, tool=tool, tool_artifact=tool_artifact)
|
782
906
|
else:
|
783
|
-
self.publish_tool(tool, tool_artifact=tool_artifact)
|
907
|
+
self.publish_tool(tool=tool, tool_artifact=tool_artifact)
|
784
908
|
|
785
909
|
def publish_tool(self, tool: BaseTool, tool_artifact: str) -> None:
|
786
910
|
tool_spec = tool.__tool_spec__.model_dump(mode='json', exclude_unset=True, exclude_none=True, by_alias=True)
|
@@ -789,7 +913,11 @@ class ToolsController:
|
|
789
913
|
tool_id = response.get("id")
|
790
914
|
|
791
915
|
if tool_artifact is not None:
|
792
|
-
self.
|
916
|
+
match self.tool_kind:
|
917
|
+
case ToolKind.langflow | ToolKind.python:
|
918
|
+
self.get_client().upload_tools_artifact(tool_id=tool_id, file_path=tool_artifact)
|
919
|
+
case _:
|
920
|
+
raise ValueError(f"Unexpected artifact for {self.tool_kind} tool")
|
793
921
|
|
794
922
|
logger.info(f"Tool '{tool.__tool_spec__.name}' imported successfully")
|
795
923
|
|
@@ -801,7 +929,11 @@ class ToolsController:
|
|
801
929
|
self.get_client().update(tool_id, tool_spec)
|
802
930
|
|
803
931
|
if tool_artifact is not None:
|
804
|
-
self.
|
932
|
+
match self.tool_kind:
|
933
|
+
case ToolKind.langflow | ToolKind.python:
|
934
|
+
self.get_client().upload_tools_artifact(tool_id=tool_id, file_path=tool_artifact)
|
935
|
+
case _:
|
936
|
+
raise ValueError(f"Unexpected artifact for {self.tool_kind} tool")
|
805
937
|
|
806
938
|
logger.info(f"Tool '{tool.__tool_spec__.name}' updated successfully")
|
807
939
|
|
@@ -823,6 +955,23 @@ class ToolsController:
|
|
823
955
|
logger.error(e.response.text)
|
824
956
|
exit(1)
|
825
957
|
|
958
|
+
def serialize_to_json_in_zip(self, obj: any, filename: str) -> bytes:
|
959
|
+
# Serialize the Python object to a JSON string
|
960
|
+
json_str = json.dumps(obj, indent=2)
|
961
|
+
|
962
|
+
# Create a BytesIO object to hold the in-memory zip file
|
963
|
+
zip_in_memory = io.BytesIO()
|
964
|
+
|
965
|
+
# Create a ZipFile object in append mode
|
966
|
+
with zipfile.ZipFile(zip_in_memory, 'a') as zip_file:
|
967
|
+
# Write the JSON string as a file named 'data.json' inside the zip
|
968
|
+
zip_file.writestr(filename, json_str)
|
969
|
+
|
970
|
+
# Seek to the beginning of the BytesIO object to return the in-memory zip file as bytes
|
971
|
+
zip_in_memory.seek(0)
|
972
|
+
|
973
|
+
return zip_in_memory.getvalue()
|
974
|
+
|
826
975
|
def download_tool(self, name: str) -> bytes | None:
|
827
976
|
tool_client = self.get_client()
|
828
977
|
draft_tools = tool_client.get_draft_by_name(tool_name=name)
|
@@ -837,13 +986,27 @@ class ToolsController:
|
|
837
986
|
draft_tool_kind = _get_kind_from_spec(draft_tool)
|
838
987
|
|
839
988
|
# TODO: Add openapi tool support
|
840
|
-
|
989
|
+
supported_toolkinds = [ToolKind.python,ToolKind.langflow,ToolKind.flow]
|
990
|
+
if draft_tool_kind not in supported_toolkinds:
|
841
991
|
logger.warning(f"Skipping '{name}', {draft_tool_kind.value} tools are currently unsupported by export")
|
842
992
|
return
|
843
993
|
|
844
994
|
tool_id = draft_tool.get("id")
|
845
995
|
|
846
|
-
|
996
|
+
if draft_tool_kind == ToolKind.python or draft_tool_kind == ToolKind.langflow:
|
997
|
+
tool_artifacts_bytes = tool_client.download_tools_artifact(tool_id=tool_id)
|
998
|
+
elif draft_tool_kind == ToolKind.flow:
|
999
|
+
if not is_local_dev():
|
1000
|
+
logger.warning("Skipping '{name}', Flow tool export is only supported in local dev mode")
|
1001
|
+
return
|
1002
|
+
|
1003
|
+
client = instantiate_client(TempusClient)
|
1004
|
+
flow_model = client.get_flow_model(tool_id)
|
1005
|
+
# we need to fix the name as sometimes it is left as 'untitled' by the builder
|
1006
|
+
if "data" in flow_model:
|
1007
|
+
flow_model["data"]["spec"]["name"] = name
|
1008
|
+
tool_artifacts_bytes = self.serialize_to_json_in_zip(flow_model["data"], f"{name}.json")
|
1009
|
+
|
847
1010
|
return tool_artifacts_bytes
|
848
1011
|
|
849
1012
|
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)
|