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.
Files changed (50) hide show
  1. ibm_watsonx_orchestrate/__init__.py +1 -2
  2. ibm_watsonx_orchestrate/agent_builder/agents/types.py +22 -5
  3. ibm_watsonx_orchestrate/agent_builder/connections/connections.py +3 -3
  4. ibm_watsonx_orchestrate/agent_builder/connections/types.py +14 -0
  5. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +1 -1
  6. ibm_watsonx_orchestrate/agent_builder/models/types.py +1 -0
  7. ibm_watsonx_orchestrate/agent_builder/toolkits/base_toolkit.py +1 -1
  8. ibm_watsonx_orchestrate/agent_builder/tools/__init__.py +1 -0
  9. ibm_watsonx_orchestrate/agent_builder/tools/base_tool.py +1 -1
  10. ibm_watsonx_orchestrate/agent_builder/tools/langflow_tool.py +124 -0
  11. ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +9 -3
  12. ibm_watsonx_orchestrate/agent_builder/tools/types.py +20 -2
  13. ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +19 -6
  14. ibm_watsonx_orchestrate/cli/commands/connections/connections_command.py +18 -0
  15. ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +114 -0
  16. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_controller.py +2 -6
  17. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_server_controller.py +24 -91
  18. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_command.py +49 -0
  19. ibm_watsonx_orchestrate/cli/commands/models/model_provider_mapper.py +23 -4
  20. ibm_watsonx_orchestrate/cli/commands/models/models_controller.py +3 -3
  21. ibm_watsonx_orchestrate/cli/commands/partners/offering/partners_offering_command.py +56 -0
  22. ibm_watsonx_orchestrate/cli/commands/partners/offering/partners_offering_controller.py +458 -0
  23. ibm_watsonx_orchestrate/cli/commands/partners/offering/types.py +107 -0
  24. ibm_watsonx_orchestrate/cli/commands/partners/partners_command.py +12 -0
  25. ibm_watsonx_orchestrate/cli/commands/partners/partners_controller.py +0 -0
  26. ibm_watsonx_orchestrate/cli/commands/server/server_command.py +124 -637
  27. ibm_watsonx_orchestrate/cli/commands/server/types.py +1 -1
  28. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_command.py +2 -2
  29. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_controller.py +2 -2
  30. ibm_watsonx_orchestrate/cli/commands/tools/tools_command.py +2 -3
  31. ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +206 -43
  32. ibm_watsonx_orchestrate/cli/main.py +2 -0
  33. ibm_watsonx_orchestrate/client/connections/connections_client.py +4 -1
  34. ibm_watsonx_orchestrate/client/tools/tempus_client.py +3 -0
  35. ibm_watsonx_orchestrate/client/tools/tool_client.py +5 -2
  36. ibm_watsonx_orchestrate/client/utils.py +31 -1
  37. ibm_watsonx_orchestrate/docker/compose-lite.yml +68 -17
  38. ibm_watsonx_orchestrate/docker/default.env +21 -18
  39. ibm_watsonx_orchestrate/flow_builder/flows/decorators.py +8 -2
  40. ibm_watsonx_orchestrate/flow_builder/flows/flow.py +31 -7
  41. ibm_watsonx_orchestrate/flow_builder/node.py +1 -1
  42. ibm_watsonx_orchestrate/flow_builder/types.py +18 -3
  43. ibm_watsonx_orchestrate/utils/docker_utils.py +280 -0
  44. ibm_watsonx_orchestrate/utils/environment.py +369 -0
  45. ibm_watsonx_orchestrate/utils/utils.py +1 -1
  46. {ibm_watsonx_orchestrate-1.11.0b1.dist-info → ibm_watsonx_orchestrate-1.12.0b1.dist-info}/METADATA +2 -2
  47. {ibm_watsonx_orchestrate-1.11.0b1.dist-info → ibm_watsonx_orchestrate-1.12.0b1.dist-info}/RECORD +50 -42
  48. {ibm_watsonx_orchestrate-1.11.0b1.dist-info → ibm_watsonx_orchestrate-1.12.0b1.dist-info}/WHEEL +0 -0
  49. {ibm_watsonx_orchestrate-1.11.0b1.dist-info → ibm_watsonx_orchestrate-1.12.0b1.dist-info}/entry_points.txt +0 -0
  50. {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
- sys.exit(1)
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", hidden=True),
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", hidden=True),
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 sanatize_app_id
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 = sanatize_app_id(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
- tools_controller.publish_or_update_tools(tools, package_root=package_root)
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 sanatize_app_id
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
- if kind == ToolKind.python:
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
- else:
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
- environments = _get_connection_environments()
106
+ permitted_connections_types = []
110
107
 
111
- imported_connection = imported_connections.get(app_id)
108
+ match(kind):
112
109
 
113
- for conn_environment in environments:
114
- conn = imported_connection.get(conn_environment)
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
- if conn is None or conn.security_scheme is None:
117
- message = f"Connection '{app_id}' is not configured in the '{conn_environment}' environment."
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
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
- if conn.security_scheme == ConnectionSecurityScheme.KEY_VALUE:
126
- logger.error(f"Key value application connections can not be bound to an openapi tool")
127
- exit(1)
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
- def parse_python_app_ids(app_ids: List[str]) -> dict[str,str]:
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 = sanatize_app_id(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 = sanatize_app_id(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 = parse_python_app_ids(app_id)
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 'wxflows' in tool_binding:
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.get_client().upload_tools_artifact(tool_id=tool_id, file_path=tool_artifact)
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.get_client().upload_tools_artifact(tool_id=tool_id, file_path=tool_artifact)
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
- if draft_tool_kind != ToolKind.python:
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
- tool_artifacts_bytes = tool_client.download_tools_artifact(tool_id=tool_id)
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)