ibm-watsonx-orchestrate 1.11.0b1__py3-none-any.whl → 1.12.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. ibm_watsonx_orchestrate/__init__.py +1 -1
  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 +184 -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/environment/environment_command.py +5 -1
  19. ibm_watsonx_orchestrate/cli/commands/environment/environment_controller.py +6 -3
  20. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_command.py +52 -2
  21. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_controller.py +1 -1
  22. ibm_watsonx_orchestrate/cli/commands/models/model_provider_mapper.py +23 -4
  23. ibm_watsonx_orchestrate/cli/commands/models/models_controller.py +3 -3
  24. ibm_watsonx_orchestrate/cli/commands/partners/offering/partners_offering_command.py +56 -0
  25. ibm_watsonx_orchestrate/cli/commands/partners/offering/partners_offering_controller.py +475 -0
  26. ibm_watsonx_orchestrate/cli/commands/partners/offering/types.py +99 -0
  27. ibm_watsonx_orchestrate/cli/commands/partners/partners_command.py +12 -0
  28. ibm_watsonx_orchestrate/cli/commands/partners/partners_controller.py +0 -0
  29. ibm_watsonx_orchestrate/cli/commands/server/server_command.py +124 -637
  30. ibm_watsonx_orchestrate/cli/commands/server/types.py +1 -1
  31. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_command.py +2 -2
  32. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_controller.py +2 -2
  33. ibm_watsonx_orchestrate/cli/commands/tools/tools_command.py +2 -3
  34. ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +241 -49
  35. ibm_watsonx_orchestrate/cli/config.py +3 -1
  36. ibm_watsonx_orchestrate/cli/main.py +2 -0
  37. ibm_watsonx_orchestrate/client/connections/connections_client.py +4 -1
  38. ibm_watsonx_orchestrate/client/tools/tempus_client.py +3 -0
  39. ibm_watsonx_orchestrate/client/tools/tool_client.py +5 -2
  40. ibm_watsonx_orchestrate/client/utils.py +31 -1
  41. ibm_watsonx_orchestrate/docker/compose-lite.yml +68 -17
  42. ibm_watsonx_orchestrate/docker/default.env +21 -18
  43. ibm_watsonx_orchestrate/flow_builder/flows/decorators.py +10 -2
  44. ibm_watsonx_orchestrate/flow_builder/flows/flow.py +71 -9
  45. ibm_watsonx_orchestrate/flow_builder/node.py +14 -2
  46. ibm_watsonx_orchestrate/flow_builder/types.py +36 -3
  47. ibm_watsonx_orchestrate/langflow/__init__.py +0 -0
  48. ibm_watsonx_orchestrate/langflow/langflow_utils.py +195 -0
  49. ibm_watsonx_orchestrate/langflow/lfx_deps.py +84 -0
  50. ibm_watsonx_orchestrate/utils/docker_utils.py +280 -0
  51. ibm_watsonx_orchestrate/utils/environment.py +369 -0
  52. ibm_watsonx_orchestrate/utils/utils.py +7 -3
  53. {ibm_watsonx_orchestrate-1.11.0b1.dist-info → ibm_watsonx_orchestrate-1.12.0.dist-info}/METADATA +2 -2
  54. {ibm_watsonx_orchestrate-1.11.0b1.dist-info → ibm_watsonx_orchestrate-1.12.0.dist-info}/RECORD +57 -46
  55. {ibm_watsonx_orchestrate-1.11.0b1.dist-info → ibm_watsonx_orchestrate-1.12.0.dist-info}/WHEEL +0 -0
  56. {ibm_watsonx_orchestrate-1.11.0b1.dist-info → ibm_watsonx_orchestrate-1.12.0.dist-info}/entry_points.txt +0 -0
  57. {ibm_watsonx_orchestrate-1.11.0b1.dist-info → ibm_watsonx_orchestrate-1.12.0.dist-info}/licenses/LICENSE +0 -0
@@ -92,7 +92,7 @@ class ModelGatewayEnvConfig(BaseModel):
92
92
  inferred_auth_url = config.get("WO_INSTANCE") + '/icp4d-api/v1/authorize'
93
93
  else:
94
94
  logger.error(f"No 'AUTHORIZATION_URL' found. Auth type '{auth_type}' does not support defaulting. Please set the 'AUTHORIZATION_URL' explictly")
95
- 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[
@@ -11,7 +11,7 @@ import zipfile
11
11
  from enum import Enum
12
12
  from os import path
13
13
  from pathlib import Path
14
- from typing import Iterable, List
14
+ from typing import Iterable, List, cast
15
15
  import rich
16
16
  import json
17
17
  from rich.json import JSON
@@ -25,6 +25,7 @@ from rich.panel import Panel
25
25
 
26
26
  from ibm_watsonx_orchestrate.agent_builder.tools import BaseTool, ToolSpec
27
27
  from ibm_watsonx_orchestrate.agent_builder.tools.flow_tool import create_flow_json_tool
28
+ from ibm_watsonx_orchestrate.agent_builder.tools.langflow_tool import LangflowTool, create_langflow_tool
28
29
  from ibm_watsonx_orchestrate.agent_builder.tools.openapi_tool import create_openapi_json_tools_from_uri,create_openapi_json_tools_from_content
29
30
  from ibm_watsonx_orchestrate.cli.commands.models.models_controller import ModelHighlighter
30
31
  from ibm_watsonx_orchestrate.cli.commands.tools.types import RegistryType
@@ -32,7 +33,7 @@ from ibm_watsonx_orchestrate.cli.commands.connections.connections_controller imp
32
33
  from ibm_watsonx_orchestrate.agent_builder.connections.types import ConnectionType, ConnectionEnvironment, ConnectionPreference
33
34
  from ibm_watsonx_orchestrate.cli.config import Config, CONTEXT_SECTION_HEADER, CONTEXT_ACTIVE_ENV_OPT, \
34
35
  PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_TYPE_OPT, PYTHON_REGISTRY_TEST_PACKAGE_VERSION_OVERRIDE_OPT, \
35
- DEFAULT_CONFIG_FILE_CONTENT
36
+ DEFAULT_CONFIG_FILE_CONTENT, PYTHON_REGISTRY_SKIP_VERSION_CHECK_OPT
36
37
  from ibm_watsonx_orchestrate.agent_builder.connections import ConnectionSecurityScheme, ExpectedCredentials
37
38
  from ibm_watsonx_orchestrate.flow_builder.flows.decorators import FlowWrapper
38
39
  from ibm_watsonx_orchestrate.client.tools.tool_client import ToolClient
@@ -40,8 +41,9 @@ from ibm_watsonx_orchestrate.client.toolkit.toolkit_client import ToolKitClient
40
41
  from ibm_watsonx_orchestrate.client.connections import get_connections_client, get_connection_type
41
42
  from ibm_watsonx_orchestrate.client.utils import instantiate_client, is_local_dev
42
43
  from ibm_watsonx_orchestrate.flow_builder.utils import import_flow_support_tools
43
- from ibm_watsonx_orchestrate.utils.utils import 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,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
- 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
-
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
- else:
105
- # Validate that the connection is not key_value when the tool in openapi
106
- if kind != ToolKind.openapi:
108
+ if kind != ToolKind.python:
109
+ continue
110
+
111
+ permitted_connections_types = []
112
+
113
+ match(kind):
114
+
115
+ case ToolKind.python:
116
+ # Split on = but not on \=
117
+ split_pattern = re.compile(r"(?<!\\)=")
118
+ split_id = re.split(split_pattern, app_id)
119
+ split_id = [x.replace("\\=", "=") for x in split_id]
120
+ if len(split_id) == 2:
121
+ _, app_id = split_id
122
+ elif len(split_id) == 1:
123
+ app_id = split_id[0]
124
+ else:
125
+ raise typer.BadParameter(f"The provided --app-id '{app_id}' is not valid. This is likely caused by having mutliple equal signs, please use '\\=' to represent a literal '=' character")
107
126
  continue
108
127
 
109
- environments = _get_connection_environments()
128
+ # Validate that the connection is not key_value when the tool in openapi
129
+ case ToolKind.openapi:
130
+ permitted_connections_types.extend([
131
+ ConnectionSecurityScheme.API_KEY_AUTH,
132
+ ConnectionSecurityScheme.BASIC_AUTH,
133
+ ConnectionSecurityScheme.BEARER_TOKEN,
134
+ ConnectionSecurityScheme.OAUTH2
135
+ ])
110
136
 
111
- imported_connection = imported_connections.get(app_id)
137
+ # Validate that the connection is key_value when the tool in langflow
138
+ case ToolKind.langflow:
139
+ permitted_connections_types.append(ConnectionSecurityScheme.KEY_VALUE)
112
140
 
113
- for conn_environment in environments:
114
- conn = imported_connection.get(conn_environment)
141
+ imported_connection = imported_connections.get(app_id)
115
142
 
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
143
+ for conn_environment in environments:
144
+ conn = imported_connection.get(conn_environment)
124
145
 
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)
146
+ if conn is None or conn.security_scheme is None:
147
+ message = f"Connection '{app_id}' is not configured in the '{conn_environment}' environment."
148
+ if conn_environment == ConnectionEnvironment.DRAFT:
149
+ logger.error(message)
150
+ sys.exit(1)
151
+ else:
152
+ logger.warning(message + " If you deploy this tool without setting the live configuration the tool will error during execution.")
153
+ continue
154
+
155
+ if conn.security_scheme not in permitted_connections_types:
156
+ logger.error(f"{conn.security_scheme} application connections can not be bound to {kind.value} tools")
157
+ exit(1)
158
+
159
+
160
+
128
161
 
129
162
  def validate_params(kind: ToolKind, **args) -> None:
130
163
  if kind in {"openapi", "python"} and args["file"] is None:
@@ -157,8 +190,37 @@ def get_connection_id(app_id: str) -> str:
157
190
  connection_id = connection.connection_id
158
191
  return connection_id
159
192
 
193
+ def get_connections(app_ids: list[str] | str = None, environment: str = None, allow_missing: bool = True) -> dict:
194
+ if not app_ids:
195
+ return {}
196
+ if app_ids is str:
197
+ app_ids = [app_ids]
198
+
199
+ connections_client = get_connections_client()
200
+ if environment:
201
+ connections = {
202
+ x.app_id:x for x in connections_client.list() \
203
+ if x.app_id in app_ids and x.environment == ConnectionEnvironment(environment)
204
+ }
205
+ else:
206
+ connections = { x.app_id:x for x in connections_client.list() if x.app_id in app_ids }
207
+
208
+
209
+ missing = 0
210
+ for id in app_ids:
211
+ if not connections.get(id,None):
212
+ missing += 1
213
+
214
+ if missing > 0 and not allow_missing:
215
+ raise ValueError(f"Could not find {missing} of {len(app_ids)} required connections")
216
+
217
+ return connections
218
+
219
+ def get_connection_ids(app_ids: list[str] | str = None, environment: str = None, allow_missing: bool = True):
220
+ connections = get_connections(app_ids=app_ids, environment=environment, allow_missing=allow_missing)
221
+ return { k:v.connection_id for k,v in connections.items() }
160
222
 
161
- def parse_python_app_ids(app_ids: List[str]) -> dict[str,str]:
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 = sanatize_app_id(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 = sanatize_app_id(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 = parse_python_app_ids(app_id)
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 'wxflows' in tool_binding:
610
+ elif 'flow' in tool_binding:
514
611
  return ToolKind.flow
515
612
  else:
516
613
  logger.error(f"Could not determine 'kind' of tool '{name}'")
@@ -565,9 +662,14 @@ class ToolsController:
565
662
  case "skill":
566
663
  tools = []
567
664
  logger.warning("Skill Import not implemented yet")
665
+ case "langflow":
666
+ tools = asyncio.run(import_langflow_tool(file=args["file"],app_id=args.get('app_id',None)))
568
667
  case _:
569
668
  raise BadRequest("Invalid kind selected")
570
669
 
670
+ if not isinstance(tools,list):
671
+ tools = [tools]
672
+
571
673
  for tool in tools:
572
674
  yield tool
573
675
 
@@ -631,6 +733,12 @@ class ToolsController:
631
733
  else:
632
734
  for conn in tool_binding.mcp.connections:
633
735
  connection_ids.append(tool_binding.mcp.connections[conn])
736
+ elif tool_binding.langflow is not None and hasattr(tool_binding.langflow, "connections"):
737
+ if tool_binding.langflow.connections is None:
738
+ connection_ids.append(None)
739
+ else:
740
+ for conn in tool_binding.langflow.connections:
741
+ connection_ids.append(tool_binding.langflow.connections[conn])
634
742
 
635
743
  app_ids = []
636
744
  for connection_id in connection_ids:
@@ -650,7 +758,9 @@ class ToolsController:
650
758
  elif tool_binding.mcp is not None:
651
759
  tool_type=ToolKind.mcp
652
760
  elif tool_binding.flow is not None:
653
- tool_type=ToolKind.flow
761
+ tool_type=ToolKind.flow
762
+ elif tool_binding.langflow is not None:
763
+ tool_type=ToolKind.langflow
654
764
  else:
655
765
  tool_type="Unknown"
656
766
 
@@ -746,15 +856,18 @@ class ToolsController:
746
856
 
747
857
  cfg = Config()
748
858
  registry_type = cfg.read(PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_TYPE_OPT) or DEFAULT_CONFIG_FILE_CONTENT[PYTHON_REGISTRY_HEADER][PYTHON_REGISTRY_TYPE_OPT]
859
+ skip_version_check = cfg.read(PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_SKIP_VERSION_CHECK_OPT) or DEFAULT_CONFIG_FILE_CONTENT[PYTHON_REGISTRY_HEADER][PYTHON_REGISTRY_SKIP_VERSION_CHECK_OPT]
749
860
 
750
861
  version = __version__
751
862
  if registry_type == RegistryType.LOCAL:
863
+ logger.warning(f"Using a local registry which is for development purposes only")
752
864
  requirements.append(f"/packages/ibm_watsonx_orchestrate-0.6.0-py3-none-any.whl\n")
753
865
  elif registry_type == RegistryType.PYPI:
754
- wheel_file = get_whl_in_registry(registry_url='https://pypi.org/simple/ibm-watsonx-orchestrate', version=version)
755
- if not wheel_file:
756
- logger.error(f"Could not find ibm-watsonx-orchestrate@{version} on https://pypi.org/project/ibm-watsonx-orchestrate")
757
- exit(1)
866
+ if not skip_version_check:
867
+ wheel_file = get_whl_in_registry(registry_url='https://pypi.org/simple/ibm-watsonx-orchestrate', version=version)
868
+ if not wheel_file:
869
+ logger.error(f"Could not find ibm-watsonx-orchestrate@{version} on https://pypi.org/project/ibm-watsonx-orchestrate")
870
+ exit(1)
758
871
  requirements.append(f"ibm-watsonx-orchestrate=={version}\n")
759
872
  elif registry_type == RegistryType.TESTPYPI:
760
873
  override_version = cfg.get(PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_TEST_PACKAGE_VERSION_OVERRIDE_OPT) or version
@@ -776,11 +889,51 @@ class ToolsController:
776
889
  zip_tool_artifacts.write(requirements_file_path, arcname='requirements.txt')
777
890
 
778
891
  zip_tool_artifacts.writestr("bundle-format", "2.0.0\n")
892
+
893
+ elif self.tool_kind == ToolKind.langflow:
894
+
895
+ tool_artifact = path.join(tmpdir, "artifacts.zip")
896
+
897
+ with zipfile.ZipFile(tool_artifact, "w", zipfile.ZIP_DEFLATED) as zip_tool_artifacts:
898
+ tool_path = Path(self.file)
899
+ zip_tool_artifacts.write(tool_path, arcname=f"{tool_path.stem}.json")
900
+
901
+ requirements = []
902
+
903
+ if self.requirements_file:
904
+ requirements_file_path = Path(self.requirements_file)
905
+ requirements.extend(
906
+ get_requirement_lines(requirements_file=requirements_file_path, remove_trailing_newlines=False)
907
+ )
908
+
909
+ langflowTool = cast(LangflowTool, tool)
910
+ # if there are additional requriements from the langflow model, we should add it to the requirement set
911
+ if langflowTool.requirements and len(langflowTool.requirements) > 0:
912
+ requirements.extend(langflowTool.requirements)
913
+
914
+ # now check if the requirements contain modules listed in DEFAULT_LANGFLOW_RUNNER_MODULES
915
+ # if it is needed, we are assuming the user wants to override the default langflow module
916
+ # with a specific version
917
+ runner_overridden = False
918
+ for r in requirements:
919
+ # get the module name from the requirements
920
+ module_name = r.strip().split('==')[0].split('=')[0].split('>=')[0].split('<=')[0].split('~=')[0].lower()
921
+ if not module_name.startswith('#'):
922
+ if module_name in DEFAULT_LANGFLOW_RUNNER_MODULES:
923
+ runner_overridden = True
924
+
925
+ if not runner_overridden:
926
+ # add the default runner to the top of requirement list
927
+ requirements = DEFAULT_LANGFLOW_TOOL_REQUIREMENTS + list(requirements)
928
+
929
+ requirements_content = '\n'.join(requirements) + '\n'
930
+ zip_tool_artifacts.writestr("requirements.txt",requirements_content)
931
+ zip_tool_artifacts.writestr("bundle-format", "2.0.0\n")
779
932
 
780
933
  if exist:
781
934
  self.update_tool(tool_id=tool_id, tool=tool, tool_artifact=tool_artifact)
782
935
  else:
783
- self.publish_tool(tool, tool_artifact=tool_artifact)
936
+ self.publish_tool(tool=tool, tool_artifact=tool_artifact)
784
937
 
785
938
  def publish_tool(self, tool: BaseTool, tool_artifact: str) -> None:
786
939
  tool_spec = tool.__tool_spec__.model_dump(mode='json', exclude_unset=True, exclude_none=True, by_alias=True)
@@ -789,7 +942,11 @@ class ToolsController:
789
942
  tool_id = response.get("id")
790
943
 
791
944
  if tool_artifact is not None:
792
- self.get_client().upload_tools_artifact(tool_id=tool_id, file_path=tool_artifact)
945
+ match self.tool_kind:
946
+ case ToolKind.langflow | ToolKind.python:
947
+ self.get_client().upload_tools_artifact(tool_id=tool_id, file_path=tool_artifact)
948
+ case _:
949
+ raise ValueError(f"Unexpected artifact for {self.tool_kind} tool")
793
950
 
794
951
  logger.info(f"Tool '{tool.__tool_spec__.name}' imported successfully")
795
952
 
@@ -801,7 +958,11 @@ class ToolsController:
801
958
  self.get_client().update(tool_id, tool_spec)
802
959
 
803
960
  if tool_artifact is not None:
804
- self.get_client().upload_tools_artifact(tool_id=tool_id, file_path=tool_artifact)
961
+ match self.tool_kind:
962
+ case ToolKind.langflow | ToolKind.python:
963
+ self.get_client().upload_tools_artifact(tool_id=tool_id, file_path=tool_artifact)
964
+ case _:
965
+ raise ValueError(f"Unexpected artifact for {self.tool_kind} tool")
805
966
 
806
967
  logger.info(f"Tool '{tool.__tool_spec__.name}' updated successfully")
807
968
 
@@ -823,6 +984,23 @@ class ToolsController:
823
984
  logger.error(e.response.text)
824
985
  exit(1)
825
986
 
987
+ def serialize_to_json_in_zip(self, obj: any, filename: str) -> bytes:
988
+ # Serialize the Python object to a JSON string
989
+ json_str = json.dumps(obj, indent=2)
990
+
991
+ # Create a BytesIO object to hold the in-memory zip file
992
+ zip_in_memory = io.BytesIO()
993
+
994
+ # Create a ZipFile object in append mode
995
+ with zipfile.ZipFile(zip_in_memory, 'a') as zip_file:
996
+ # Write the JSON string as a file named 'data.json' inside the zip
997
+ zip_file.writestr(filename, json_str)
998
+
999
+ # Seek to the beginning of the BytesIO object to return the in-memory zip file as bytes
1000
+ zip_in_memory.seek(0)
1001
+
1002
+ return zip_in_memory.getvalue()
1003
+
826
1004
  def download_tool(self, name: str) -> bytes | None:
827
1005
  tool_client = self.get_client()
828
1006
  draft_tools = tool_client.get_draft_by_name(tool_name=name)
@@ -837,13 +1015,27 @@ class ToolsController:
837
1015
  draft_tool_kind = _get_kind_from_spec(draft_tool)
838
1016
 
839
1017
  # TODO: Add openapi tool support
840
- if draft_tool_kind != ToolKind.python:
1018
+ supported_toolkinds = [ToolKind.python,ToolKind.langflow,ToolKind.flow]
1019
+ if draft_tool_kind not in supported_toolkinds:
841
1020
  logger.warning(f"Skipping '{name}', {draft_tool_kind.value} tools are currently unsupported by export")
842
1021
  return
843
1022
 
844
1023
  tool_id = draft_tool.get("id")
845
1024
 
846
- tool_artifacts_bytes = tool_client.download_tools_artifact(tool_id=tool_id)
1025
+ if draft_tool_kind == ToolKind.python or draft_tool_kind == ToolKind.langflow:
1026
+ tool_artifacts_bytes = tool_client.download_tools_artifact(tool_id=tool_id)
1027
+ elif draft_tool_kind == ToolKind.flow:
1028
+ if not is_local_dev():
1029
+ logger.warning("Skipping '{name}', Flow tool export is only supported in local dev mode")
1030
+ return
1031
+
1032
+ client = instantiate_client(TempusClient)
1033
+ flow_model = client.get_flow_model(tool_id)
1034
+ # we need to fix the name as sometimes it is left as 'untitled' by the builder
1035
+ if "data" in flow_model:
1036
+ flow_model["data"]["spec"]["name"] = name
1037
+ tool_artifacts_bytes = self.serialize_to_json_in_zip(flow_model["data"], f"{name}.json")
1038
+
847
1039
  return tool_artifacts_bytes
848
1040
 
849
1041
  def export_tool(self, name: str, output_path: str) -> None:
@@ -22,6 +22,7 @@ AUTH_MCSP_TOKEN_OPT = "wxo_mcsp_token"
22
22
  AUTH_MCSP_TOKEN_EXPIRY_OPT = "wxo_mcsp_token_expiry"
23
23
  CONTEXT_ACTIVE_ENV_OPT = "active_environment"
24
24
  PYTHON_REGISTRY_TYPE_OPT = "type"
25
+ PYTHON_REGISTRY_SKIP_VERSION_CHECK_OPT = "skip_version_check"
25
26
  PYTHON_REGISTRY_TEST_PACKAGE_VERSION_OVERRIDE_OPT = "test_package_version_override"
26
27
  ENV_WXO_URL_OPT = "wxo_url"
27
28
  ENV_IAM_URL_OPT = "iam_url"
@@ -40,7 +41,8 @@ DEFAULT_CONFIG_FILE_CONTENT = {
40
41
  CONTEXT_SECTION_HEADER: {CONTEXT_ACTIVE_ENV_OPT: None},
41
42
  PYTHON_REGISTRY_HEADER: {
42
43
  PYTHON_REGISTRY_TYPE_OPT: str(RegistryType.PYPI),
43
- PYTHON_REGISTRY_TEST_PACKAGE_VERSION_OVERRIDE_OPT: None
44
+ PYTHON_REGISTRY_TEST_PACKAGE_VERSION_OVERRIDE_OPT: None,
45
+ PYTHON_REGISTRY_SKIP_VERSION_CHECK_OPT: False
44
46
  },
45
47
  ENVIRONMENTS_SECTION_HEADER: {
46
48
  PROTECTED_ENV_NAME: {
@@ -11,6 +11,7 @@ from ibm_watsonx_orchestrate.cli.commands.server.server_command import server_ap
11
11
  from ibm_watsonx_orchestrate.cli.commands.chat.chat_command import chat_app
12
12
  from ibm_watsonx_orchestrate.cli.commands.models.models_command import models_app
13
13
  from ibm_watsonx_orchestrate.cli.commands.environment.environment_command import environment_app
14
+ from ibm_watsonx_orchestrate.cli.commands.partners.partners_command import partners_app
14
15
  from ibm_watsonx_orchestrate.cli.commands.channels.channels_command import channel_app
15
16
  from ibm_watsonx_orchestrate.cli.commands.knowledge_bases.knowledge_bases_command import knowledge_bases_app
16
17
  from ibm_watsonx_orchestrate.cli.commands.toolkit.toolkit_command import toolkits_app
@@ -44,6 +45,7 @@ app.add_typer(channel_app, name="channels", help="Configure channels where your
44
45
  app.add_typer(evaluation_app, name="evaluations", help='Evaluate the performance of your agents in your active env')
45
46
  app.add_typer(copilot_app, name="copilot", help='Access AI powered assistance to help refine your agents')
46
47
  app.add_typer(settings_app, name="settings", help='Configure the settings for your active env')
48
+ app.add_typer(partners_app, name="partners", help='Generate a well-structured, submission-ready agent artifact package for partner-built agents')
47
49
 
48
50
  if __name__ == "__main__":
49
51
  app()
@@ -6,7 +6,7 @@ from typing import Optional
6
6
  from enum import Enum
7
7
 
8
8
  from ibm_watsonx_orchestrate.client.base_api_client import BaseAPIClient, ClientAPIException
9
- from ibm_watsonx_orchestrate.agent_builder.connections.types import ConnectionEnvironment, ConnectionPreference, ConnectionAuthType, ConnectionSecurityScheme, IdpConfigData, AppConfigData, ConnectionType
9
+ from ibm_watsonx_orchestrate.agent_builder.connections.types import ConnectionEnvironment, ConnectionPreference, ConnectionConfiguration, ConnectionAuthType, ConnectionSecurityScheme, IdpConfigData, AppConfigData, ConnectionType
10
10
  from ibm_watsonx_orchestrate.client.utils import is_cpd_env, is_local_dev
11
11
 
12
12
  import logging
@@ -47,6 +47,9 @@ class GetConfigResponse(BaseModel):
47
47
  idp_config_data: Optional[IdpConfigData] = None
48
48
  app_config_data: Optional[AppConfigData] = None
49
49
 
50
+ def as_config(self):
51
+ return ConnectionConfiguration(**dict(self))
52
+
50
53
  class GetConnectionResponse(BaseModel):
51
54
  connection_id: str = None
52
55
  app_id: str = None
@@ -42,4 +42,7 @@ class TempusClient(BaseAPIClient):
42
42
 
43
43
  def arun_flow(self, flow_id: str, input: dict) -> dict:
44
44
  return self._post(f"/flows/{flow_id}/versions/TIP/run/async", data=input)
45
+
46
+ def get_flow_model(self, flow_id: str, version: str = "TIP") -> dict:
47
+ return self._get(f"/flow-models/{flow_id}/versions/{version}")
45
48
 
@@ -10,7 +10,7 @@ class ToolClient(BaseAPIClient):
10
10
  def create(self, payload: dict) -> dict:
11
11
  return self._post("/tools", data=payload)
12
12
 
13
- def get(self) -> dict:
13
+ def get(self) -> dict:
14
14
  return self._get("/tools")
15
15
 
16
16
  def update(self, agent_id: str, data: dict) -> dict:
@@ -21,10 +21,13 @@ class ToolClient(BaseAPIClient):
21
21
 
22
22
  def upload_tools_artifact(self, tool_id: str, file_path: str) -> dict:
23
23
  return self._post(f"/tools/{tool_id}/upload", files={"file": (f"{tool_id}.zip", open(file_path, "rb"), "application/zip", {"Expires": "0"})})
24
-
24
+
25
25
  def download_tools_artifact(self, tool_id: str) -> bytes:
26
26
  response = self._get(f"/tools/{tool_id}/download", return_raw=True)
27
27
  return response.content
28
+
29
+ def download_tools_json(self, tool_id: str) -> dict:
30
+ return self.download_tools_artifact(tool_id)
28
31
 
29
32
  def get_draft_by_name(self, tool_name: str) -> List[dict]:
30
33
  return self.get_drafts_by_names([tool_name])