ibm-watsonx-orchestrate 1.5.1__py3-none-any.whl → 1.7.0a0__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 (70) hide show
  1. ibm_watsonx_orchestrate/__init__.py +1 -1
  2. ibm_watsonx_orchestrate/agent_builder/agents/__init__.py +1 -1
  3. ibm_watsonx_orchestrate/agent_builder/agents/agent.py +1 -0
  4. ibm_watsonx_orchestrate/agent_builder/agents/types.py +58 -4
  5. ibm_watsonx_orchestrate/agent_builder/agents/webchat_customizations/__init__.py +2 -0
  6. ibm_watsonx_orchestrate/agent_builder/agents/webchat_customizations/prompts.py +33 -0
  7. ibm_watsonx_orchestrate/agent_builder/agents/webchat_customizations/welcome_content.py +20 -0
  8. ibm_watsonx_orchestrate/agent_builder/connections/__init__.py +2 -2
  9. ibm_watsonx_orchestrate/agent_builder/connections/types.py +38 -31
  10. ibm_watsonx_orchestrate/agent_builder/model_policies/types.py +1 -1
  11. ibm_watsonx_orchestrate/agent_builder/models/types.py +0 -1
  12. ibm_watsonx_orchestrate/agent_builder/tools/flow_tool.py +83 -0
  13. ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +41 -3
  14. ibm_watsonx_orchestrate/agent_builder/tools/python_tool.py +2 -1
  15. ibm_watsonx_orchestrate/agent_builder/tools/types.py +14 -1
  16. ibm_watsonx_orchestrate/cli/commands/agents/agents_command.py +18 -1
  17. ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +122 -17
  18. ibm_watsonx_orchestrate/cli/commands/channels/webchat/channels_webchat_controller.py +104 -22
  19. ibm_watsonx_orchestrate/cli/commands/connections/connections_command.py +26 -18
  20. ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +56 -58
  21. ibm_watsonx_orchestrate/cli/commands/environment/environment_command.py +29 -4
  22. ibm_watsonx_orchestrate/cli/commands/environment/environment_controller.py +74 -8
  23. ibm_watsonx_orchestrate/cli/commands/environment/types.py +1 -0
  24. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_command.py +224 -0
  25. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_controller.py +158 -0
  26. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_command.py +2 -2
  27. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +2 -2
  28. ibm_watsonx_orchestrate/cli/commands/models/model_provider_mapper.py +31 -25
  29. ibm_watsonx_orchestrate/cli/commands/models/models_command.py +6 -6
  30. ibm_watsonx_orchestrate/cli/commands/models/models_controller.py +17 -8
  31. ibm_watsonx_orchestrate/cli/commands/server/server_command.py +162 -27
  32. ibm_watsonx_orchestrate/cli/commands/server/types.py +2 -1
  33. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_controller.py +9 -6
  34. ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +37 -22
  35. ibm_watsonx_orchestrate/cli/config.py +2 -0
  36. ibm_watsonx_orchestrate/cli/main.py +6 -0
  37. ibm_watsonx_orchestrate/client/agents/agent_client.py +83 -9
  38. ibm_watsonx_orchestrate/client/agents/assistant_agent_client.py +3 -3
  39. ibm_watsonx_orchestrate/client/agents/external_agent_client.py +2 -2
  40. ibm_watsonx_orchestrate/client/base_api_client.py +11 -10
  41. ibm_watsonx_orchestrate/client/connections/connections_client.py +49 -14
  42. ibm_watsonx_orchestrate/client/connections/utils.py +4 -2
  43. ibm_watsonx_orchestrate/client/credentials.py +4 -0
  44. ibm_watsonx_orchestrate/client/local_service_instance.py +1 -1
  45. ibm_watsonx_orchestrate/client/model_policies/model_policies_client.py +2 -2
  46. ibm_watsonx_orchestrate/client/service_instance.py +42 -1
  47. ibm_watsonx_orchestrate/client/tools/tempus_client.py +8 -3
  48. ibm_watsonx_orchestrate/client/utils.py +37 -2
  49. ibm_watsonx_orchestrate/docker/compose-lite.yml +425 -81
  50. ibm_watsonx_orchestrate/docker/default.env +53 -15
  51. ibm_watsonx_orchestrate/docker/proxy-config-single.yaml +12 -0
  52. ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/flows/__init__.py +3 -2
  53. ibm_watsonx_orchestrate/flow_builder/flows/decorators.py +77 -0
  54. ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/flows/events.py +6 -1
  55. ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/flows/flow.py +85 -92
  56. ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/types.py +15 -6
  57. ibm_watsonx_orchestrate/flow_builder/utils.py +215 -0
  58. ibm_watsonx_orchestrate/run/connections.py +4 -4
  59. {ibm_watsonx_orchestrate-1.5.1.dist-info → ibm_watsonx_orchestrate-1.7.0a0.dist-info}/METADATA +2 -1
  60. {ibm_watsonx_orchestrate-1.5.1.dist-info → ibm_watsonx_orchestrate-1.7.0a0.dist-info}/RECORD +68 -61
  61. ibm_watsonx_orchestrate/experimental/flow_builder/flows/decorators.py +0 -144
  62. ibm_watsonx_orchestrate/experimental/flow_builder/utils.py +0 -115
  63. /ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/__init__.py +0 -0
  64. /ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/data_map.py +0 -0
  65. /ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/flows/constants.py +0 -0
  66. /ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/node.py +0 -0
  67. /ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/resources/flow_status.openapi.yml +0 -0
  68. {ibm_watsonx_orchestrate-1.5.1.dist-info → ibm_watsonx_orchestrate-1.7.0a0.dist-info}/WHEEL +0 -0
  69. {ibm_watsonx_orchestrate-1.5.1.dist-info → ibm_watsonx_orchestrate-1.7.0a0.dist-info}/entry_points.txt +0 -0
  70. {ibm_watsonx_orchestrate-1.5.1.dist-info → ibm_watsonx_orchestrate-1.7.0a0.dist-info}/licenses/LICENSE +0 -0
@@ -67,23 +67,28 @@ def docker_login(api_key: str, registry_url: str, username:str = "iamapikey") ->
67
67
  logger.info("Successfully logged in to Docker.")
68
68
 
69
69
  def docker_login_by_dev_edition_source(env_dict: dict, source: str) -> None:
70
- if not env_dict.get("REGISTRY_URL"):
71
- raise ValueError("REGISTRY_URL is not set.")
72
- registry_url = env_dict["REGISTRY_URL"].split("/")[0]
73
- if source == "internal":
74
- iam_api_key = env_dict.get("DOCKER_IAM_KEY")
75
- if not iam_api_key:
76
- raise ValueError("DOCKER_IAM_KEY is required in the environment file if WO_DEVELOPER_EDITION_SOURCE is set to 'internal'.")
77
- docker_login(iam_api_key, registry_url, "iamapikey")
78
- elif source == "myibm":
79
- wo_entitlement_key = env_dict.get("WO_ENTITLEMENT_KEY")
80
- if not wo_entitlement_key:
81
- raise ValueError("WO_ENTITLEMENT_KEY is required in the environment file.")
82
- docker_login(wo_entitlement_key, registry_url, "cp")
83
- elif source == "orchestrate":
84
- wo_auth_type = env_dict.get("WO_AUTH_TYPE")
85
- api_key, username = get_docker_cred_by_wo_auth_type(env_dict, wo_auth_type)
86
- docker_login(api_key, registry_url, username)
70
+ if env_dict.get('WO_DEVELOPER_EDITION_SKIP_LOGIN', None) == 'true':
71
+ logger.info('WO_DEVELOPER_EDITION_SKIP_LOGIN is set to true, skipping login.')
72
+ logger.warning('If the developer edition images are not already pulled this call will fail without first setting WO_DEVELOPER_EDITION_SKIP_LOGIN=false')
73
+ else:
74
+ if not env_dict.get("REGISTRY_URL"):
75
+ raise ValueError("REGISTRY_URL is not set.")
76
+ registry_url = env_dict["REGISTRY_URL"].split("/")[0]
77
+ if source == "internal":
78
+ iam_api_key = env_dict.get("DOCKER_IAM_KEY")
79
+ if not iam_api_key:
80
+ raise ValueError("DOCKER_IAM_KEY is required in the environment file if WO_DEVELOPER_EDITION_SOURCE is set to 'internal'.")
81
+ docker_login(iam_api_key, registry_url, "iamapikey")
82
+ elif source == "myibm":
83
+ wo_entitlement_key = env_dict.get("WO_ENTITLEMENT_KEY")
84
+ if not wo_entitlement_key:
85
+ raise ValueError("WO_ENTITLEMENT_KEY is required in the environment file.")
86
+ docker_login(wo_entitlement_key, registry_url, "cp")
87
+ elif source == "orchestrate":
88
+ wo_auth_type = env_dict.get("WO_AUTH_TYPE")
89
+ api_key, username = get_docker_cred_by_wo_auth_type(env_dict, wo_auth_type)
90
+ docker_login(api_key, registry_url, username)
91
+
87
92
 
88
93
  def get_compose_file() -> Path:
89
94
  with resources.as_file(
@@ -165,6 +170,8 @@ def get_docker_cred_by_wo_auth_type(env_dict: dict, auth_type: str | None) -> tu
165
170
  auth_type = "ibm_iam"
166
171
  elif ".ibm.com" in instance_url:
167
172
  auth_type = "mcsp"
173
+ elif "https://cpd" in instance_url:
174
+ auth_type = "cpd"
168
175
 
169
176
  if auth_type in {"mcsp", "ibm_iam"}:
170
177
  wo_api_key = env_dict.get("WO_API_KEY")
@@ -233,6 +240,27 @@ def apply_llm_api_key_defaults(env_dict: dict) -> None:
233
240
  env_dict.setdefault("ASSISTANT_EMBEDDINGS_SPACE_ID", space_value)
234
241
  env_dict.setdefault("ROUTING_LLM_SPACE_ID", space_value)
235
242
 
243
+ def _is_docker_container_running(container_name):
244
+ ensure_docker_installed()
245
+ command = [ "docker",
246
+ "ps",
247
+ "-f",
248
+ f"name={container_name}"
249
+ ]
250
+ result = subprocess.run(command, env=os.environ, capture_output=True)
251
+ if container_name in str(result.stdout):
252
+ return True
253
+ return False
254
+
255
+ def _check_exclusive_observibility(langfuse_enabled: bool, ibm_tele_enabled: bool):
256
+ if langfuse_enabled and ibm_tele_enabled:
257
+ return False
258
+ if langfuse_enabled and _is_docker_container_running("docker-frontend-server-1"):
259
+ return False
260
+ if ibm_tele_enabled and _is_docker_container_running("docker-langfuse-web-1"):
261
+ return False
262
+ return True
263
+
236
264
  def write_merged_env_file(merged_env: dict) -> Path:
237
265
  tmp = tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".env")
238
266
  with tmp:
@@ -285,7 +313,7 @@ def get_persisted_user_env() -> dict | None:
285
313
  user_env = cfg.get(USER_ENV_CACHE_HEADER) if cfg.get(USER_ENV_CACHE_HEADER) else None
286
314
  return user_env
287
315
 
288
- def run_compose_lite(final_env_file: Path, experimental_with_langfuse=False) -> None:
316
+ def run_compose_lite(final_env_file: Path, experimental_with_langfuse=False, experimental_with_ibm_telemetry=False, with_docproc=False) -> None:
289
317
  compose_path = get_compose_file()
290
318
  compose_command = ensure_docker_compose_installed()
291
319
  db_tag = read_env_file(final_env_file).get('DBTAG', None)
@@ -312,13 +340,17 @@ def run_compose_lite(final_env_file: Path, experimental_with_langfuse=False) ->
312
340
 
313
341
 
314
342
  # Step 2: Start all remaining services (except DB)
343
+ profiles = []
315
344
  if experimental_with_langfuse:
316
- command = compose_command + [
317
- '--profile',
318
- 'langfuse'
319
- ]
320
- else:
321
- command = compose_command
345
+ profiles.append("langfuse")
346
+ if experimental_with_ibm_telemetry:
347
+ profiles.append("ibm-telemetry")
348
+ if with_docproc:
349
+ profiles.append("docproc")
350
+
351
+ command = compose_command[:]
352
+ for profile in profiles:
353
+ command += ["--profile", profile]
322
354
 
323
355
  command += [
324
356
  "-f", str(compose_path),
@@ -437,6 +469,9 @@ def run_compose_lite_ui(user_env_file: Path) -> bool:
437
469
  # do nothing, as the docker login here is not mandatory
438
470
  pass
439
471
 
472
+ # Auto-configure callback IP for async tools
473
+ merged_env_dict = auto_configure_callback_ip(merged_env_dict)
474
+
440
475
  #These are to removed warning and not used in UI component
441
476
  if not 'WATSONX_SPACE_ID' in merged_env_dict:
442
477
  merged_env_dict['WATSONX_SPACE_ID']='X'
@@ -614,8 +649,80 @@ def confirm_accepts_license_agreement(accepts_by_argument: bool):
614
649
  logger.error('The terms and conditions were not accepted, exiting.')
615
650
  exit(1)
616
651
 
652
+ def auto_configure_callback_ip(merged_env_dict: dict) -> dict:
653
+ """
654
+ Automatically detect and configure CALLBACK_HOST_URL if it's empty.
655
+
656
+ Args:
657
+ merged_env_dict: The merged environment dictionary
658
+
659
+ Returns:
660
+ Updated environment dictionary with CALLBACK_HOST_URL set
661
+ """
662
+ callback_url = merged_env_dict.get('CALLBACK_HOST_URL', '').strip()
663
+
664
+ # Only auto-configure if CALLBACK_HOST_URL is empty
665
+ if not callback_url:
666
+ logger.info("Auto-detecting local IP address for async tool callbacks...")
667
+
668
+ system = platform.system()
669
+ ip = None
670
+
671
+ try:
672
+ if system in ("Linux", "Darwin"):
673
+ result = subprocess.run(["ifconfig"], capture_output=True, text=True, check=True)
674
+ lines = result.stdout.splitlines()
675
+
676
+ for line in lines:
677
+ line = line.strip()
678
+ # Unix ifconfig output format: "inet 192.168.1.100 netmask 0xffffff00 broadcast 192.168.1.255"
679
+ if line.startswith("inet ") and "127.0.0.1" not in line:
680
+ candidate_ip = line.split()[1]
681
+ # Validate IP is not loopback or link-local
682
+ if (candidate_ip and
683
+ not candidate_ip.startswith("127.") and
684
+ not candidate_ip.startswith("169.254")):
685
+ ip = candidate_ip
686
+ break
687
+
688
+ elif system == "Windows":
689
+ result = subprocess.run(["ipconfig"], capture_output=True, text=True, check=True)
690
+ lines = result.stdout.splitlines()
691
+
692
+ for line in lines:
693
+ line = line.strip()
694
+ # Windows ipconfig output format: " IPv4 Address. . . . . . . . . . . : 192.168.1.100"
695
+ if "IPv4 Address" in line and ":" in line:
696
+ candidate_ip = line.split(":")[-1].strip()
697
+ # Validate IP is not loopback or link-local
698
+ if (candidate_ip and
699
+ not candidate_ip.startswith("127.") and
700
+ not candidate_ip.startswith("169.254")):
701
+ ip = candidate_ip
702
+ break
617
703
 
704
+ else:
705
+ logger.warning(f"Unsupported platform: {system}")
706
+ ip = None
707
+
708
+ except Exception as e:
709
+ logger.debug(f"IP detection failed on {system}: {e}")
710
+ ip = None
711
+
712
+ if ip:
713
+ callback_url = f"http://{ip}:4321"
714
+ merged_env_dict['CALLBACK_HOST_URL'] = callback_url
715
+ logger.info(f"Auto-configured CALLBACK_HOST_URL to: {callback_url}")
716
+ else:
717
+ # Fallback for localhost
718
+ callback_url = "http://host.docker.internal:4321"
719
+ merged_env_dict['CALLBACK_HOST_URL'] = callback_url
720
+ logger.info(f"Using Docker internal URL: {callback_url}")
721
+ logger.info("For external tools, consider using ngrok or similar tunneling service.")
722
+ else:
723
+ logger.info(f"Using existing CALLBACK_HOST_URL: {callback_url}")
618
724
 
725
+ return merged_env_dict
619
726
 
620
727
  @server_app.command(name="start")
621
728
  def server_start(
@@ -629,6 +736,11 @@ def server_start(
629
736
  '--with-langfuse', '-l',
630
737
  help='Option to enable Langfuse support.'
631
738
  ),
739
+ experimental_with_ibm_telemetry: bool = typer.Option(
740
+ False,
741
+ '--with-ibm-telemetry', '-i',
742
+ help=''
743
+ ),
632
744
  persist_env_secrets: bool = typer.Option(
633
745
  False,
634
746
  '--persist-env-secrets', '-p',
@@ -640,6 +752,11 @@ def server_start(
640
752
  "--accept-terms-and-conditions",
641
753
  help="By providing this flag you accept the terms and conditions outlined in the logs on server start."
642
754
  ),
755
+ with_docproc: bool = typer.Option(
756
+ False,
757
+ '--with-docproc', '-d',
758
+ help='Enable IBM Document Processing to extract information from your business documents. Enabling this activates the Watson Document Understanding service.'
759
+ ),
643
760
  ):
644
761
  confirm_accepts_license_agreement(accept_terms_and_conditions)
645
762
 
@@ -668,10 +785,22 @@ def server_start(
668
785
 
669
786
  merged_env_dict = apply_server_env_dict_defaults(merged_env_dict)
670
787
 
671
- # Add LANGFUSE_ENABLED into the merged_env_dict, for tempus to pick up.
788
+ # Auto-configure callback IP for async tools
789
+ merged_env_dict = auto_configure_callback_ip(merged_env_dict)
790
+ if not _check_exclusive_observibility(experimental_with_langfuse, experimental_with_ibm_telemetry):
791
+ logger.error("Please select either langfuse or ibm telemetry for observability not both")
792
+ sys.exit(1)
793
+
794
+ # Add LANGFUSE_ENABLED and DOCPROC_ENABLED into the merged_env_dict, for tempus to pick up.
672
795
  if experimental_with_langfuse:
673
796
  merged_env_dict['LANGFUSE_ENABLED'] = 'true'
674
797
 
798
+ if with_docproc:
799
+ merged_env_dict['DOCPROC_ENABLED'] = 'true'
800
+
801
+ if experimental_with_ibm_telemetry:
802
+ merged_env_dict['USE_IBM_TELEMETRY'] = 'true'
803
+
675
804
  try:
676
805
  docker_login_by_dev_edition_source(merged_env_dict, dev_edition_source)
677
806
  except ValueError as e:
@@ -682,7 +811,11 @@ def server_start(
682
811
 
683
812
 
684
813
  final_env_file = write_merged_env_file(merged_env_dict)
685
- run_compose_lite(final_env_file=final_env_file, experimental_with_langfuse=experimental_with_langfuse)
814
+
815
+ run_compose_lite(final_env_file=final_env_file,
816
+ experimental_with_langfuse=experimental_with_langfuse,
817
+ experimental_with_ibm_telemetry=experimental_with_ibm_telemetry,
818
+ with_docproc=with_docproc)
686
819
 
687
820
  run_db_migration()
688
821
 
@@ -710,6 +843,8 @@ def server_start(
710
843
 
711
844
  if experimental_with_langfuse:
712
845
  logger.info(f"You can access the observability platform Langfuse at http://localhost:3010, username: orchestrate@ibm.com, password: orchestrate")
846
+ if with_docproc:
847
+ logger.info(f"Document processing capabilities are now available for use in Flows (both ADK and runtime). Note: This option is currently available only in the Developer edition.")
713
848
 
714
849
  @server_app.command(name="stop")
715
850
  def server_stop(
@@ -817,4 +952,4 @@ def run_db_migration() -> None:
817
952
  sys.exit(1)
818
953
 
819
954
  if __name__ == "__main__":
820
- server_app()
955
+ server_app()
@@ -26,7 +26,8 @@ def _infer_auth_type_from_instance_url(instance_url: str) -> WoAuthType:
26
26
  return WoAuthType.IBM_IAM
27
27
  if ".ibm.com" in instance_url:
28
28
  return WoAuthType.MCSP
29
- return WoAuthType.CPD
29
+ if "https://cpd" in instance_url:
30
+ return WoAuthType.CPD
30
31
 
31
32
 
32
33
  class WatsonXAIEnvConfig(BaseModel):
@@ -213,9 +213,6 @@ class ToolkitController:
213
213
 
214
214
 
215
215
  def remove_toolkit(self, name: str):
216
- if not is_local_dev():
217
- logger.error("This functionality is only available for Local Environments")
218
- sys.exit(1)
219
216
  try:
220
217
  client = self.get_client()
221
218
  draft_toolkits = client.get_draft_by_name(toolkit_name=name)
@@ -246,9 +243,15 @@ class ToolkitController:
246
243
  rich.print(JSON(json.dumps(tools_list, indent=4)))
247
244
  else:
248
245
  table = rich.table.Table(show_header=True, header_style="bold white", show_lines=True)
249
- columns = ["Name", "Kind", "Description", "Tools", "App ID"]
250
- for column in columns:
251
- table.add_column(column)
246
+ column_args = {
247
+ "Name": {"overflow": "fold"},
248
+ "Kind": {},
249
+ "Description": {},
250
+ "Tools": {},
251
+ "App ID": {"overflow": "fold"}
252
+ }
253
+ for column in column_args:
254
+ table.add_column(column,**column_args[column])
252
255
 
253
256
  tools_client = instantiate_client(ToolClient)
254
257
 
@@ -24,6 +24,7 @@ from rich.console import Console
24
24
  from rich.panel import Panel
25
25
 
26
26
  from ibm_watsonx_orchestrate.agent_builder.tools import BaseTool, ToolSpec
27
+ from ibm_watsonx_orchestrate.agent_builder.tools.flow_tool import create_flow_json_tool
27
28
  from ibm_watsonx_orchestrate.agent_builder.tools.openapi_tool import create_openapi_json_tools_from_uri,create_openapi_json_tools_from_content
28
29
  from ibm_watsonx_orchestrate.cli.commands.models.models_controller import ModelHighlighter
29
30
  from ibm_watsonx_orchestrate.cli.commands.tools.types import RegistryType
@@ -33,15 +34,14 @@ from ibm_watsonx_orchestrate.cli.config import Config, CONTEXT_SECTION_HEADER, C
33
34
  PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_TYPE_OPT, PYTHON_REGISTRY_TEST_PACKAGE_VERSION_OVERRIDE_OPT, \
34
35
  DEFAULT_CONFIG_FILE_CONTENT
35
36
  from ibm_watsonx_orchestrate.agent_builder.connections import ConnectionSecurityScheme, ExpectedCredentials
36
- from ibm_watsonx_orchestrate.experimental.flow_builder.flows.decorators import FlowWrapper
37
+ from ibm_watsonx_orchestrate.flow_builder.flows.decorators import FlowWrapper
37
38
  from ibm_watsonx_orchestrate.client.tools.tool_client import ToolClient
38
39
  from ibm_watsonx_orchestrate.client.toolkit.toolkit_client import ToolKitClient
39
40
  from ibm_watsonx_orchestrate.client.connections import get_connections_client, get_connection_type
40
41
  from ibm_watsonx_orchestrate.client.utils import instantiate_client, is_local_dev
42
+ from ibm_watsonx_orchestrate.flow_builder.utils import import_flow_support_tools
41
43
  from ibm_watsonx_orchestrate.utils.utils import sanatize_app_id
42
44
  from ibm_watsonx_orchestrate.client.utils import is_local_dev
43
- from ibm_watsonx_orchestrate.client.tools.tempus_client import TempusClient
44
- from ibm_watsonx_orchestrate.experimental.flow_builder.utils import import_flow_model
45
45
 
46
46
  from ibm_watsonx_orchestrate import __version__
47
47
 
@@ -339,24 +339,18 @@ async def import_flow_tool(file: str) -> None:
339
339
  theme = rich.theme.Theme({"model.name": "bold cyan"})
340
340
  console = rich.console.Console(highlighter=ModelHighlighter(), theme=theme)
341
341
 
342
- message = f"""[bold cyan]Flow Tools: Experimental Feature[/bold cyan]
342
+ message = f"""[bold cyan]Flow Tools[/bold cyan]
343
343
 
344
344
  The [bold]flow tool[/bold] is being imported from [green]`{file}`[/green].
345
345
 
346
346
  [bold cyan]Additional information:[/bold cyan]
347
347
 
348
- - Ensure the flow engine is running by issuing the [bold cyan]orchestrate server start[/bold cyan] command with the [bold cyan]--with-flow-runtime[/bold cyan] option
349
- - The [bold green]get_flow_status[/bold green] tool is being imported to support flow tools. Ensure [bold]both this tools and the one you are importing are added to your agent[/bold] to retrieve the flow output.
350
- - Include additional instructions in your agent to call the [bold green]get_flow_status[/bold green] tool to retrieve the flow output. For example: [green]"If you get an instance_id, use the tool get_flow_status to retrieve the current status of a flow."[/green]
348
+ - The [bold green]Get flow status[/bold green] tool is being imported to support flow tools. This tool can query the status of a flow tool instance. You can add it to your agent using the UI or including the following tool name in your agent definition: [green]i__get_flow_status_intrinsic_tool__[/green].
351
349
 
352
350
  """
353
351
 
354
352
  console.print(Panel(message, title="[bold blue]Flow tool support information[/bold blue]", border_style="bright_blue"))
355
353
 
356
-
357
- if not is_local_dev():
358
- raise typer.BadParameter(f"Flow tools are only supported in local environment.")
359
-
360
354
  model = None
361
355
 
362
356
  # Load the Flow JSON model from the file
@@ -423,7 +417,16 @@ The [bold]flow tool[/bold] is being imported from [green]`{file}`[/green].
423
417
  except Exception as e:
424
418
  raise typer.BadParameter(f"Failed to load model from file {file}: {e}")
425
419
 
426
- return await import_flow_model(model)
420
+ tool = create_flow_json_tool(name=model["spec"]["name"],
421
+ description=model["spec"]["description"],
422
+ permission="read_only",
423
+ flow_model=model)
424
+
425
+ tools = import_flow_support_tools()
426
+
427
+ tools.append(tool)
428
+
429
+ return tools
427
430
 
428
431
 
429
432
  async def import_openapi_tool(file: str, connection_id: str) -> List[BaseTool]:
@@ -438,6 +441,10 @@ def _get_kind_from_spec(spec: dict) -> ToolKind:
438
441
  return ToolKind.python
439
442
  elif ToolKind.openapi in tool_binding:
440
443
  return ToolKind.openapi
444
+ elif ToolKind.mcp in tool_binding:
445
+ return ToolKind.mcp
446
+ elif 'wxflows' in tool_binding:
447
+ return ToolKind.flow
441
448
  else:
442
449
  logger.error(f"Could not determine 'kind' of tool '{name}'")
443
450
  sys.exit(1)
@@ -511,9 +518,16 @@ class ToolsController:
511
518
  rich.print(JSON(json.dumps(tools_list, indent=4)))
512
519
  else:
513
520
  table = rich.table.Table(show_header=True, header_style="bold white", show_lines=True)
514
- columns = ["Name", "Description", "Permission", "Type", "Toolkit", "App ID"]
515
- for column in columns:
516
- table.add_column(column)
521
+ column_args = {
522
+ "Name": {"overflow": "fold"},
523
+ "Description": {},
524
+ "Permission": {},
525
+ "Type": {},
526
+ "Toolkit": {},
527
+ "App ID": {"overflow": "fold"}
528
+ }
529
+ for column in column_args:
530
+ table.add_column(column,**column_args[column])
517
531
 
518
532
  connections_client = get_connections_client()
519
533
  connections = connections_client.list()
@@ -555,19 +569,20 @@ class ToolsController:
555
569
  tool_type=ToolKind.openapi
556
570
  elif tool_binding.mcp is not None:
557
571
  tool_type=ToolKind.mcp
572
+ elif tool_binding.flow is not None:
573
+ tool_type=ToolKind.flow
558
574
  else:
559
575
  tool_type="Unknown"
560
576
 
561
577
  toolkit_name = ""
562
578
 
563
- if is_local_dev():
579
+ if tool.__tool_spec__.toolkit_id:
564
580
  toolkit_client = instantiate_client(ToolKitClient)
565
- if tool.__tool_spec__.toolkit_id:
566
- toolkit = toolkit_client.get_draft_by_id(tool.__tool_spec__.toolkit_id)
567
- if isinstance(toolkit, dict) and "name" in toolkit:
568
- toolkit_name = toolkit["name"]
569
- elif toolkit:
570
- toolkit_name = str(toolkit)
581
+ toolkit = toolkit_client.get_draft_by_id(tool.__tool_spec__.toolkit_id)
582
+ if isinstance(toolkit, dict) and "name" in toolkit:
583
+ toolkit_name = toolkit["name"]
584
+ elif toolkit:
585
+ toolkit_name = str(toolkit)
571
586
 
572
587
 
573
588
  table.add_row(
@@ -26,6 +26,8 @@ ENV_WXO_URL_OPT = "wxo_url"
26
26
  ENV_IAM_URL_OPT = "iam_url"
27
27
  PROTECTED_ENV_NAME = "local"
28
28
  ENV_AUTH_TYPE = "auth_type"
29
+ BYPASS_SSL = "bypass_ssl"
30
+ VERIFY = "verify"
29
31
  ENV_ACCEPT_LICENSE = 'accepts_license_agreements'
30
32
 
31
33
  DEFAULT_LOCAL_SERVICE_URL = "http://localhost:4321"
@@ -12,8 +12,13 @@ from ibm_watsonx_orchestrate.cli.commands.environment.environment_command import
12
12
  from ibm_watsonx_orchestrate.cli.commands.channels.channels_command import channel_app
13
13
  from ibm_watsonx_orchestrate.cli.commands.knowledge_bases.knowledge_bases_command import knowledge_bases_app
14
14
  from ibm_watsonx_orchestrate.cli.commands.toolkit.toolkit_command import toolkits_app
15
+ from ibm_watsonx_orchestrate.cli.commands.evaluations.evaluations_command import evaluation_app
15
16
  from ibm_watsonx_orchestrate.cli.init_helper import init_callback
16
17
 
18
+ import urllib3
19
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
20
+
21
+
17
22
  app = typer.Typer(
18
23
  no_args_is_help=True,
19
24
  pretty_exceptions_enable=False,
@@ -30,6 +35,7 @@ app.add_typer(server_app, name="server", help='Manipulate your local Orchestrate
30
35
  app.add_typer(chat_app, name="chat", help='Launch the chat ui for your local Developer Edition server [requires entitlement]')
31
36
  app.add_typer(models_app, name="models", help='List the available large language models (llms) that can be used in your agent definitions')
32
37
  app.add_typer(channel_app, name="channels", help="Configure channels where your agent can exist on (such as embedded webchat)")
38
+ app.add_typer(evaluation_app, name="evaluations", help='Evaluate the performance of your agents in your active env')
33
39
  app.add_typer(settings_app, name="settings", help='Configure the settings for your active env')
34
40
 
35
41
  if __name__ == "__main__":
@@ -1,7 +1,79 @@
1
1
  from ibm_watsonx_orchestrate.client.base_api_client import BaseAPIClient, ClientAPIException
2
- from typing_extensions import List
2
+ from typing_extensions import List, Optional
3
3
  from ibm_watsonx_orchestrate.client.utils import is_local_dev
4
+ from pydantic import BaseModel
4
5
 
6
+ def transform_agents_from_flat_agent_spec(agents: dict | list[dict] ) -> dict | list[dict]:
7
+ if isinstance(agents,list):
8
+ new_agents = []
9
+ for agent in agents:
10
+ new_agents.append(_transform_agent_from_flat_agent_spec(agent))
11
+ agents = new_agents
12
+ else:
13
+ agents = _transform_agent_from_flat_agent_spec(agents)
14
+
15
+ return agents
16
+
17
+
18
+ def _transform_agent_from_flat_agent_spec(agent_spec: dict ) -> dict:
19
+ transformed = {"additional_properties": {}}
20
+ for key,value in agent_spec.items():
21
+ if key == "starter_prompts":
22
+ if value:
23
+ value.pop("is_default_prompts",None)
24
+ value["customize"] = value.pop("prompts", [])
25
+
26
+ transformed["additional_properties"] |= { key: value }
27
+
28
+ elif key == "welcome_content":
29
+ if value:
30
+ value.pop("is_default_message", None)
31
+
32
+ transformed["additional_properties"] |= { key: value }
33
+
34
+ else:
35
+ transformed |= { key: value }
36
+
37
+ return transformed
38
+
39
+ def transform_agents_to_flat_agent_spec(agents: dict | list[dict] ) -> dict | list[dict]:
40
+ if isinstance(agents,list):
41
+ new_agents = []
42
+ for agent in agents:
43
+ new_agents.append(_transform_agent_to_flat_agent_spec(agent))
44
+ agents = new_agents
45
+ else:
46
+ agents = _transform_agent_to_flat_agent_spec(agents)
47
+
48
+ return agents
49
+
50
+ def _transform_agent_to_flat_agent_spec(agent_spec: dict ) -> dict:
51
+ additional_properties = agent_spec.get("additional_properties", None)
52
+ if not additional_properties:
53
+ return agent_spec
54
+
55
+ transformed = agent_spec
56
+ for key,value in additional_properties.items():
57
+ if key == "starter_prompts":
58
+ if value:
59
+ value["is_default_prompts"] = False
60
+ value["prompts"] = value.pop("customize", [])
61
+
62
+ transformed[key] = value
63
+
64
+ elif key == "welcome_content":
65
+ if value:
66
+ value["is_default_message"] = False
67
+
68
+ transformed[key] = value
69
+
70
+ transformed.pop("additional_properties",None)
71
+
72
+ return transformed
73
+
74
+ class AgentUpsertResponse(BaseModel):
75
+ id: Optional[str] = None
76
+ warning: Optional[str] = None
5
77
 
6
78
  class AgentClient(BaseAPIClient):
7
79
  """
@@ -12,14 +84,16 @@ class AgentClient(BaseAPIClient):
12
84
  self.base_endpoint = "/orchestrate/agents" if is_local_dev(self.base_url) else "/agents"
13
85
 
14
86
 
15
- def create(self, payload: dict) -> dict:
16
- return self._post(self.base_endpoint, data=payload)
87
+ def create(self, payload: dict) -> AgentUpsertResponse:
88
+ response = self._post(self.base_endpoint, data=transform_agents_from_flat_agent_spec(payload))
89
+ return AgentUpsertResponse.model_validate(response)
17
90
 
18
91
  def get(self) -> dict:
19
- return self._get(self.base_endpoint)
92
+ return transform_agents_to_flat_agent_spec(self._get(f"{self.base_endpoint}?include_hidden=true"))
20
93
 
21
- def update(self, agent_id: str, data: dict) -> dict:
22
- return self._patch(f"{self.base_endpoint}/{agent_id}", data=data)
94
+ def update(self, agent_id: str, data: dict) -> AgentUpsertResponse:
95
+ response = self._patch(f"{self.base_endpoint}/{agent_id}", data=transform_agents_from_flat_agent_spec(data))
96
+ return AgentUpsertResponse.model_validate(response)
23
97
 
24
98
  def delete(self, agent_id: str) -> dict:
25
99
  return self._delete(f"{self.base_endpoint}/{agent_id}")
@@ -29,14 +103,14 @@ class AgentClient(BaseAPIClient):
29
103
 
30
104
  def get_drafts_by_names(self, agent_names: List[str]) -> List[dict]:
31
105
  formatted_agent_names = [f"names={x}" for x in agent_names]
32
- return self._get(f"{self.base_endpoint}?{'&'.join(formatted_agent_names)}")
106
+ return transform_agents_to_flat_agent_spec(self._get(f"{self.base_endpoint}?{'&'.join(formatted_agent_names)}&include_hidden=true"))
33
107
 
34
108
  def get_draft_by_id(self, agent_id: str) -> List[dict]:
35
109
  if agent_id is None:
36
110
  return ""
37
111
  else:
38
112
  try:
39
- agent = self._get(f"{self.base_endpoint}/{agent_id}")
113
+ agent = transform_agents_to_flat_agent_spec(self._get(f"{self.base_endpoint}/{agent_id}"))
40
114
  return agent
41
115
  except ClientAPIException as e:
42
116
  if e.response.status_code == 404 and "not found with the given name" in e.response.text:
@@ -45,5 +119,5 @@ class AgentClient(BaseAPIClient):
45
119
 
46
120
  def get_drafts_by_ids(self, agent_ids: List[str]) -> List[dict]:
47
121
  formatted_agent_ids = [f"ids={x}" for x in agent_ids]
48
- return self._get(f"{self.base_endpoint}?{'&'.join(formatted_agent_ids)}")
122
+ return transform_agents_to_flat_agent_spec(self._get(f"{self.base_endpoint}?{'&'.join(formatted_agent_ids)}&include_hidden=true"))
49
123
 
@@ -10,7 +10,7 @@ class AssistantAgentClient(BaseAPIClient):
10
10
  return self._post("/assistants/watsonx", data=payload)
11
11
 
12
12
  def get(self) -> dict:
13
- return self._get("/assistants/watsonx")
13
+ return self._get("/assistants/watsonx?include_hidden=true")
14
14
 
15
15
  def update(self, agent_id: str, data: dict) -> dict:
16
16
  return self._patch(f"/assistants/watsonx/{agent_id}", data=data)
@@ -23,7 +23,7 @@ class AssistantAgentClient(BaseAPIClient):
23
23
 
24
24
  def get_drafts_by_names(self, agent_names: List[str]) -> List[dict]:
25
25
  formatted_agent_names = [f"names={x}" for x in agent_names]
26
- return self._get(f"/assistants/watsonx?{'&'.join(formatted_agent_names)}")
26
+ return self._get(f"/assistants/watsonx?{'&'.join(formatted_agent_names)}&include_hidden=true")
27
27
 
28
28
  def get_draft_by_id(self, agent_id: str) -> dict | str:
29
29
  if agent_id is None:
@@ -39,4 +39,4 @@ class AssistantAgentClient(BaseAPIClient):
39
39
 
40
40
  def get_drafts_by_ids(self, agent_ids: List[str]) -> List[dict]:
41
41
  formatted_agent_ids = [f"ids={x}" for x in agent_ids]
42
- return self._get(f"/assistants/watsonx?{'&'.join(formatted_agent_ids)}")
42
+ return self._get(f"/assistants/watsonx?{'&'.join(formatted_agent_ids)}&include_hidden=true")
@@ -10,7 +10,7 @@ class ExternalAgentClient(BaseAPIClient):
10
10
  return self._post("/agents/external-chat", data=payload)
11
11
 
12
12
  def get(self) -> dict:
13
- return self._get("/agents/external-chat")
13
+ return self._get("/agents/external-chat?include_hidden=true")
14
14
 
15
15
  def update(self, agent_id: str, data: dict) -> dict:
16
16
  return self._patch(f"/agents/external-chat/{agent_id}", data=data)
@@ -39,4 +39,4 @@ class ExternalAgentClient(BaseAPIClient):
39
39
 
40
40
  def get_drafts_by_ids(self, agent_ids: List[str]) -> List[dict]:
41
41
  formatted_agent_ids = [f"ids={x}" for x in agent_ids]
42
- return self._get(f"/agents/external-chat?{'&'.join(formatted_agent_ids)}")
42
+ return self._get(f"/agents/external-chat?{'&'.join(formatted_agent_ids)}&include_hidden=true")