ibm-watsonx-orchestrate 1.5.1__py3-none-any.whl → 1.6.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 (71) 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 +34 -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 +153 -21
  18. ibm_watsonx_orchestrate/cli/commands/channels/webchat/channels_webchat_controller.py +104 -22
  19. ibm_watsonx_orchestrate/cli/commands/chat/chat_command.py +2 -0
  20. ibm_watsonx_orchestrate/cli/commands/connections/connections_command.py +26 -18
  21. ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +61 -61
  22. ibm_watsonx_orchestrate/cli/commands/environment/environment_command.py +29 -4
  23. ibm_watsonx_orchestrate/cli/commands/environment/environment_controller.py +74 -8
  24. ibm_watsonx_orchestrate/cli/commands/environment/types.py +1 -0
  25. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_command.py +312 -0
  26. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_controller.py +171 -0
  27. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_command.py +2 -2
  28. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +2 -2
  29. ibm_watsonx_orchestrate/cli/commands/models/model_provider_mapper.py +31 -25
  30. ibm_watsonx_orchestrate/cli/commands/models/models_command.py +6 -6
  31. ibm_watsonx_orchestrate/cli/commands/models/models_controller.py +17 -8
  32. ibm_watsonx_orchestrate/cli/commands/server/server_command.py +147 -21
  33. ibm_watsonx_orchestrate/cli/commands/server/types.py +2 -1
  34. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_controller.py +9 -6
  35. ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +111 -32
  36. ibm_watsonx_orchestrate/cli/config.py +2 -0
  37. ibm_watsonx_orchestrate/cli/main.py +6 -0
  38. ibm_watsonx_orchestrate/client/agents/agent_client.py +83 -9
  39. ibm_watsonx_orchestrate/client/agents/assistant_agent_client.py +3 -3
  40. ibm_watsonx_orchestrate/client/agents/external_agent_client.py +2 -2
  41. ibm_watsonx_orchestrate/client/base_api_client.py +11 -10
  42. ibm_watsonx_orchestrate/client/connections/connections_client.py +49 -14
  43. ibm_watsonx_orchestrate/client/connections/utils.py +4 -2
  44. ibm_watsonx_orchestrate/client/credentials.py +4 -0
  45. ibm_watsonx_orchestrate/client/local_service_instance.py +1 -1
  46. ibm_watsonx_orchestrate/client/model_policies/model_policies_client.py +2 -2
  47. ibm_watsonx_orchestrate/client/service_instance.py +42 -1
  48. ibm_watsonx_orchestrate/client/tools/tempus_client.py +8 -3
  49. ibm_watsonx_orchestrate/client/utils.py +37 -2
  50. ibm_watsonx_orchestrate/docker/compose-lite.yml +252 -81
  51. ibm_watsonx_orchestrate/docker/default.env +40 -15
  52. ibm_watsonx_orchestrate/docker/proxy-config-single.yaml +12 -0
  53. ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/flows/__init__.py +3 -2
  54. ibm_watsonx_orchestrate/flow_builder/flows/decorators.py +77 -0
  55. ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/flows/events.py +6 -1
  56. ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/flows/flow.py +85 -92
  57. ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/types.py +15 -6
  58. ibm_watsonx_orchestrate/flow_builder/utils.py +215 -0
  59. ibm_watsonx_orchestrate/run/connections.py +4 -4
  60. {ibm_watsonx_orchestrate-1.5.1.dist-info → ibm_watsonx_orchestrate-1.6.0.dist-info}/METADATA +2 -1
  61. {ibm_watsonx_orchestrate-1.5.1.dist-info → ibm_watsonx_orchestrate-1.6.0.dist-info}/RECORD +69 -62
  62. ibm_watsonx_orchestrate/experimental/flow_builder/flows/decorators.py +0 -144
  63. ibm_watsonx_orchestrate/experimental/flow_builder/utils.py +0 -115
  64. /ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/__init__.py +0 -0
  65. /ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/data_map.py +0 -0
  66. /ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/flows/constants.py +0 -0
  67. /ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/node.py +0 -0
  68. /ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/resources/flow_status.openapi.yml +0 -0
  69. {ibm_watsonx_orchestrate-1.5.1.dist-info → ibm_watsonx_orchestrate-1.6.0.dist-info}/WHEEL +0 -0
  70. {ibm_watsonx_orchestrate-1.5.1.dist-info → ibm_watsonx_orchestrate-1.6.0.dist-info}/entry_points.txt +0 -0
  71. {ibm_watsonx_orchestrate-1.5.1.dist-info → ibm_watsonx_orchestrate-1.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -18,7 +18,7 @@ from ibm_watsonx_orchestrate.agent_builder.model_policies.types import ModelPoli
18
18
  ModelPolicyRetry, ModelPolicyStrategy, ModelPolicyStrategyMode, ModelPolicyTarget
19
19
  from ibm_watsonx_orchestrate.client.models.models_client import ModelsClient
20
20
  from ibm_watsonx_orchestrate.agent_builder.models.types import VirtualModel, ProviderConfig, ModelType, ANTHROPIC_DEFAULT_MAX_TOKENS
21
- from ibm_watsonx_orchestrate.client.utils import instantiate_client
21
+ from ibm_watsonx_orchestrate.client.utils import instantiate_client, is_cpd_env
22
22
  from ibm_watsonx_orchestrate.client.connections import get_connection_id, ConnectionType
23
23
 
24
24
  logger = logging.getLogger(__name__)
@@ -85,6 +85,11 @@ def import_python_policy(file: str) -> List[ModelPolicy]:
85
85
  models.append(obj)
86
86
  return models
87
87
 
88
+ def validate_spec_content(content: dict) -> None:
89
+ if not content.get("spec_version"):
90
+ logger.error(f"Field 'spec_version' not provided. Please ensure provided spec conforms to a valid spec format")
91
+ sys.exit(1)
92
+
88
93
  def parse_model_file(file: str) -> List[VirtualModel]:
89
94
  if file.endswith('.yaml') or file.endswith('.yml') or file.endswith(".json"):
90
95
  with open(file, 'r') as f:
@@ -92,6 +97,7 @@ def parse_model_file(file: str) -> List[VirtualModel]:
92
97
  content = json.load(f)
93
98
  else:
94
99
  content = yaml.load(f, Loader=yaml.SafeLoader)
100
+ validate_spec_content(content)
95
101
  model = create_model_from_spec(spec=content)
96
102
  return [model]
97
103
  elif file.endswith('.py'):
@@ -107,6 +113,7 @@ def parse_policy_file(file: str) -> List[ModelPolicy]:
107
113
  content = json.load(f)
108
114
  else:
109
115
  content = yaml.load(f, Loader=yaml.SafeLoader)
116
+ validate_spec_content(content)
110
117
  policy = create_policy_from_spec(spec=content)
111
118
  return [policy]
112
119
  elif file.endswith('.py'):
@@ -160,13 +167,15 @@ class ModelsController:
160
167
  logger.error("Error: WATSONX_URL is required in the environment.")
161
168
  sys.exit(1)
162
169
 
163
- logger.info("Retrieving virtual-model models list...")
164
- virtual_models = models_client.list()
165
-
166
-
170
+ if is_cpd_env(models_client.base_url):
171
+ virtual_models = []
172
+ virtual_model_policies = []
173
+ else:
174
+ logger.info("Retrieving virtual-model models list...")
175
+ virtual_models = models_client.list()
167
176
 
168
- logger.info("Retrieving virtual-policies models list...")
169
- virtual_model_policies = model_policies_client.list()
177
+ logger.info("Retrieving virtual-policies models list...")
178
+ virtual_model_policies = model_policies_client.list()
170
179
 
171
180
  logger.info("Retrieving watsonx.ai models list...")
172
181
  found_models = _get_wxai_foundational_models()
@@ -382,7 +391,7 @@ class ModelsController:
382
391
  mode=strategy,
383
392
  on_status_codes=strategy_on_code
384
393
  )
385
- inner.targets = [ModelPolicyTarget(weight=1, model_name=m) for m in models]
394
+ inner.targets = [ModelPolicyTarget(model_name=m) for m in models]
386
395
  if retry_on_code:
387
396
  inner.retry = ModelPolicyRetry(
388
397
  on_status_codes=retry_on_code,
@@ -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,8 @@ 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
+
317
+ def run_compose_lite(final_env_file: Path, experimental_with_langfuse=False, experimental_with_ibm_telemetry=False) -> None:
289
318
  compose_path = get_compose_file()
290
319
  compose_command = ensure_docker_compose_installed()
291
320
  db_tag = read_env_file(final_env_file).get('DBTAG', None)
@@ -317,9 +346,15 @@ def run_compose_lite(final_env_file: Path, experimental_with_langfuse=False) ->
317
346
  '--profile',
318
347
  'langfuse'
319
348
  ]
349
+ elif experimental_with_ibm_telemetry:
350
+ command = compose_command + [
351
+ '--profile',
352
+ 'ibm-telemetry'
353
+ ]
320
354
  else:
321
355
  command = compose_command
322
356
 
357
+
323
358
  command += [
324
359
  "-f", str(compose_path),
325
360
  "--env-file", str(final_env_file),
@@ -437,6 +472,9 @@ def run_compose_lite_ui(user_env_file: Path) -> bool:
437
472
  # do nothing, as the docker login here is not mandatory
438
473
  pass
439
474
 
475
+ # Auto-configure callback IP for async tools
476
+ merged_env_dict = auto_configure_callback_ip(merged_env_dict)
477
+
440
478
  #These are to removed warning and not used in UI component
441
479
  if not 'WATSONX_SPACE_ID' in merged_env_dict:
442
480
  merged_env_dict['WATSONX_SPACE_ID']='X'
@@ -614,8 +652,80 @@ def confirm_accepts_license_agreement(accepts_by_argument: bool):
614
652
  logger.error('The terms and conditions were not accepted, exiting.')
615
653
  exit(1)
616
654
 
617
-
618
-
655
+ def auto_configure_callback_ip(merged_env_dict: dict) -> dict:
656
+ """
657
+ Automatically detect and configure CALLBACK_HOST_URL if it's empty.
658
+
659
+ Args:
660
+ merged_env_dict: The merged environment dictionary
661
+
662
+ Returns:
663
+ Updated environment dictionary with CALLBACK_HOST_URL set
664
+ """
665
+ callback_url = merged_env_dict.get('CALLBACK_HOST_URL', '').strip()
666
+
667
+ # Only auto-configure if CALLBACK_HOST_URL is empty
668
+ if not callback_url:
669
+ logger.info("Auto-detecting local IP address for async tool callbacks...")
670
+
671
+ system = platform.system()
672
+ ip = None
673
+
674
+ try:
675
+ if system in ("Linux", "Darwin"):
676
+ result = subprocess.run(["ifconfig"], capture_output=True, text=True, check=True)
677
+ lines = result.stdout.splitlines()
678
+
679
+ for line in lines:
680
+ line = line.strip()
681
+ # Unix ifconfig output format: "inet 192.168.1.100 netmask 0xffffff00 broadcast 192.168.1.255"
682
+ if line.startswith("inet ") and "127.0.0.1" not in line:
683
+ candidate_ip = line.split()[1]
684
+ # Validate IP is not loopback or link-local
685
+ if (candidate_ip and
686
+ not candidate_ip.startswith("127.") and
687
+ not candidate_ip.startswith("169.254")):
688
+ ip = candidate_ip
689
+ break
690
+
691
+ elif system == "Windows":
692
+ result = subprocess.run(["ipconfig"], capture_output=True, text=True, check=True)
693
+ lines = result.stdout.splitlines()
694
+
695
+ for line in lines:
696
+ line = line.strip()
697
+ # Windows ipconfig output format: " IPv4 Address. . . . . . . . . . . : 192.168.1.100"
698
+ if "IPv4 Address" in line and ":" in line:
699
+ candidate_ip = line.split(":")[-1].strip()
700
+ # Validate IP is not loopback or link-local
701
+ if (candidate_ip and
702
+ not candidate_ip.startswith("127.") and
703
+ not candidate_ip.startswith("169.254")):
704
+ ip = candidate_ip
705
+ break
706
+
707
+ else:
708
+ logger.warning(f"Unsupported platform: {system}")
709
+ ip = None
710
+
711
+ except Exception as e:
712
+ logger.debug(f"IP detection failed on {system}: {e}")
713
+ ip = None
714
+
715
+ if ip:
716
+ callback_url = f"http://{ip}:4321"
717
+ merged_env_dict['CALLBACK_HOST_URL'] = callback_url
718
+ logger.info(f"Auto-configured CALLBACK_HOST_URL to: {callback_url}")
719
+ else:
720
+ # Fallback for localhost
721
+ callback_url = "http://host.docker.internal:4321"
722
+ merged_env_dict['CALLBACK_HOST_URL'] = callback_url
723
+ logger.info(f"Using Docker internal URL: {callback_url}")
724
+ logger.info("For external tools, consider using ngrok or similar tunneling service.")
725
+ else:
726
+ logger.info(f"Using existing CALLBACK_HOST_URL: {callback_url}")
727
+
728
+ return merged_env_dict
619
729
 
620
730
  @server_app.command(name="start")
621
731
  def server_start(
@@ -629,6 +739,11 @@ def server_start(
629
739
  '--with-langfuse', '-l',
630
740
  help='Option to enable Langfuse support.'
631
741
  ),
742
+ experimental_with_ibm_telemetry: bool = typer.Option(
743
+ False,
744
+ '--with-ibm-telemetry', '-i',
745
+ help=''
746
+ ),
632
747
  persist_env_secrets: bool = typer.Option(
633
748
  False,
634
749
  '--persist-env-secrets', '-p',
@@ -668,9 +783,18 @@ def server_start(
668
783
 
669
784
  merged_env_dict = apply_server_env_dict_defaults(merged_env_dict)
670
785
 
786
+ # Auto-configure callback IP for async tools
787
+ merged_env_dict = auto_configure_callback_ip(merged_env_dict)
788
+ if not _check_exclusive_observibility(experimental_with_langfuse, experimental_with_ibm_telemetry):
789
+ logger.error("Please select either langfuse or ibm telemetry for observability not both")
790
+ sys.exit(1)
791
+
671
792
  # Add LANGFUSE_ENABLED into the merged_env_dict, for tempus to pick up.
672
793
  if experimental_with_langfuse:
673
794
  merged_env_dict['LANGFUSE_ENABLED'] = 'true'
795
+
796
+ if experimental_with_ibm_telemetry:
797
+ merged_env_dict['USE_IBM_TELEMETRY'] = 'true'
674
798
 
675
799
  try:
676
800
  docker_login_by_dev_edition_source(merged_env_dict, dev_edition_source)
@@ -682,7 +806,9 @@ def server_start(
682
806
 
683
807
 
684
808
  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)
809
+ run_compose_lite(final_env_file=final_env_file,
810
+ experimental_with_langfuse=experimental_with_langfuse,
811
+ experimental_with_ibm_telemetry=experimental_with_ibm_telemetry)
686
812
 
687
813
  run_db_migration()
688
814
 
@@ -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
 
@@ -57,6 +57,12 @@ class ToolKind(str, Enum):
57
57
  flow = "flow"
58
58
  # skill = "skill"
59
59
 
60
+ def _get_connection_environments() -> List[ConnectionEnvironment]:
61
+ if is_local_dev():
62
+ return [ConnectionEnvironment.DRAFT]
63
+ else:
64
+ return [env.value for env in ConnectionEnvironment]
65
+
60
66
  def validate_app_ids(kind: ToolKind, **args) -> None:
61
67
  app_ids = args.get("app_id")
62
68
  if not app_ids:
@@ -71,7 +77,14 @@ def validate_app_ids(kind: ToolKind, **args) -> None:
71
77
  connections_client = get_connections_client()
72
78
 
73
79
  imported_connections_list = connections_client.list()
74
- imported_connections = {conn.app_id:conn for conn in imported_connections_list}
80
+ imported_connections = {}
81
+ for conn in imported_connections_list:
82
+ app_id = conn.app_id
83
+ conn_env = conn.environment
84
+ if app_id in imported_connections:
85
+ imported_connections[app_id][conn_env] = conn
86
+ else:
87
+ imported_connections[app_id] = {conn_env: conn}
75
88
 
76
89
  for app_id in app_ids:
77
90
  if kind == ToolKind.python:
@@ -89,9 +102,23 @@ def validate_app_ids(kind: ToolKind, **args) -> None:
89
102
  if app_id not in imported_connections:
90
103
  logger.warning(f"No connection found for provided app-id '{app_id}'. Please create the connection using `orchestrate connections add`")
91
104
  else:
92
- if kind == ToolKind.openapi and imported_connections.get(app_id).security_scheme == ConnectionSecurityScheme.KEY_VALUE:
93
- logger.error(f"Key value application connections can not be bound to an openapi tool")
94
- exit(1)
105
+ environments = _get_connection_environments()
106
+
107
+ imported_connection = imported_connections.get(app_id)
108
+
109
+ for conn_environment in environments:
110
+ conn = imported_connection.get(conn_environment)
111
+
112
+ if conn is None or conn.security_scheme is None:
113
+ logger.error(f"Connection '{app_id}' is not configured in the '{conn_environment}' environment.")
114
+ if conn_environment == ConnectionEnvironment.DRAFT:
115
+ sys.exit(1)
116
+ logger.error("If you deploy this tool without setting the live configuration the tool will error during execution.")
117
+ continue
118
+
119
+ if kind == ToolKind.openapi and conn.security_scheme == ConnectionSecurityScheme.KEY_VALUE:
120
+ logger.error(f"Key value application connections can not be bound to an openapi tool")
121
+ exit(1)
95
122
 
96
123
  def validate_params(kind: ToolKind, **args) -> None:
97
124
  if kind in {"openapi", "python"} and args["file"] is None:
@@ -157,7 +184,14 @@ def validate_python_connections(tool: BaseTool):
157
184
 
158
185
  provided_connections = list(connections.keys()) if connections else []
159
186
  imported_connections_list = connections_client.list()
160
- imported_connections = {conn.connection_id:conn for conn in imported_connections_list}
187
+ imported_connections = {}
188
+ for conn in imported_connections_list:
189
+ conn_id = conn.connection_id
190
+ conn_env = conn.environment
191
+ if conn_id in imported_connections:
192
+ imported_connections[conn_id][conn_env] = conn
193
+ else:
194
+ imported_connections[conn_id] = {conn_env: conn}
161
195
 
162
196
  validation_failed = False
163
197
 
@@ -186,15 +220,28 @@ def validate_python_connections(tool: BaseTool):
186
220
 
187
221
  connection_id = connections.get(sanatized_expected_tool_app_id)
188
222
  imported_connection = imported_connections.get(connection_id)
189
- imported_connection_auth_type = get_connection_type(security_scheme=imported_connection.security_scheme, auth_type=imported_connection.auth_type)
190
223
 
191
224
  if connection_id and not imported_connection:
192
225
  logger.error(f"The expected connection id '{connection_id}' does not match any known connection. This is likely caused by the connection being deleted. Please rec-reate the connection and re-import the tool")
193
226
  validation_failed = True
227
+
228
+ environments = _get_connection_environments()
229
+
230
+ for conn_environment in environments:
231
+ conn = imported_connection.get(conn_environment)
232
+ conn_identifier = conn.app_id if conn is not None else connection_id
233
+ if conn is None or conn.security_scheme is None:
234
+ logger.error(f"Connection '{conn_identifier}' is not configured in the '{conn_environment}' environment.")
235
+ if conn_environment == ConnectionEnvironment.DRAFT:
236
+ sys.exit(1)
237
+ logger.error("If you deploy this tool without setting the live configuration the tool will error during execution.")
238
+ continue
194
239
 
195
- if imported_connection and len(expected_tool_conn_types) and imported_connection_auth_type not in expected_tool_conn_types:
196
- logger.error(f"The app-id '{imported_connection.app_id}' is of type '{imported_connection_auth_type.value}'. The tool '{tool.__tool_spec__.name}' accepts connections of the following types '{', '.join(expected_tool_conn_types)}'. Use `orchestrate connections list` to view current connections and use `orchestrate connections add` to create the relevent connection")
197
- validation_failed = True
240
+ imported_connection_auth_type = get_connection_type(security_scheme=conn.security_scheme, auth_type=conn.auth_type)
241
+
242
+ if conn and len(expected_tool_conn_types) and imported_connection_auth_type not in expected_tool_conn_types:
243
+ logger.error(f"The app-id '{conn.app_id}' is of type '{imported_connection_auth_type.value}' in the '{conn_environment}' environment. The tool '{tool.__tool_spec__.name}' accepts connections of the following types '{', '.join(expected_tool_conn_types)}'. Use `orchestrate connections list` to view current connections and use `orchestrate connections add` to create the relevent connection")
244
+ validation_failed = True
198
245
 
199
246
  if validation_failed:
200
247
  exit(1)
@@ -339,24 +386,18 @@ async def import_flow_tool(file: str) -> None:
339
386
  theme = rich.theme.Theme({"model.name": "bold cyan"})
340
387
  console = rich.console.Console(highlighter=ModelHighlighter(), theme=theme)
341
388
 
342
- message = f"""[bold cyan]Flow Tools: Experimental Feature[/bold cyan]
389
+ message = f"""[bold cyan]Flow Tools[/bold cyan]
343
390
 
344
391
  The [bold]flow tool[/bold] is being imported from [green]`{file}`[/green].
345
392
 
346
393
  [bold cyan]Additional information:[/bold cyan]
347
394
 
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]
395
+ - 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
396
 
352
397
  """
353
398
 
354
399
  console.print(Panel(message, title="[bold blue]Flow tool support information[/bold blue]", border_style="bright_blue"))
355
400
 
356
-
357
- if not is_local_dev():
358
- raise typer.BadParameter(f"Flow tools are only supported in local environment.")
359
-
360
401
  model = None
361
402
 
362
403
  # Load the Flow JSON model from the file
@@ -423,7 +464,16 @@ The [bold]flow tool[/bold] is being imported from [green]`{file}`[/green].
423
464
  except Exception as e:
424
465
  raise typer.BadParameter(f"Failed to load model from file {file}: {e}")
425
466
 
426
- return await import_flow_model(model)
467
+ tool = create_flow_json_tool(name=model["spec"]["name"],
468
+ description=model["spec"]["description"],
469
+ permission="read_only",
470
+ flow_model=model)
471
+
472
+ tools = import_flow_support_tools()
473
+
474
+ tools.append(tool)
475
+
476
+ return tools
427
477
 
428
478
 
429
479
  async def import_openapi_tool(file: str, connection_id: str) -> List[BaseTool]:
@@ -438,6 +488,10 @@ def _get_kind_from_spec(spec: dict) -> ToolKind:
438
488
  return ToolKind.python
439
489
  elif ToolKind.openapi in tool_binding:
440
490
  return ToolKind.openapi
491
+ elif ToolKind.mcp in tool_binding:
492
+ return ToolKind.mcp
493
+ elif 'wxflows' in tool_binding:
494
+ return ToolKind.flow
441
495
  else:
442
496
  logger.error(f"Could not determine 'kind' of tool '{name}'")
443
497
  sys.exit(1)
@@ -500,7 +554,20 @@ class ToolsController:
500
554
 
501
555
  def list_tools(self, verbose=False):
502
556
  response = self.get_client().get()
503
- tool_specs = [ToolSpec.model_validate(tool) for tool in response]
557
+ tool_specs = []
558
+ parse_errors = []
559
+
560
+ for tool in response:
561
+ try:
562
+ tool_specs.append(ToolSpec.model_validate(tool))
563
+ except Exception as e:
564
+ name = tool.get('name', None)
565
+ parse_errors.append([
566
+ f"Tool '{name}' could not be parsed",
567
+ json.dumps(tool),
568
+ e
569
+ ])
570
+
504
571
  tools = [BaseTool(spec=spec) for spec in tool_specs]
505
572
 
506
573
  if verbose:
@@ -511,9 +578,16 @@ class ToolsController:
511
578
  rich.print(JSON(json.dumps(tools_list, indent=4)))
512
579
  else:
513
580
  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)
581
+ column_args = {
582
+ "Name": {"overflow": "fold"},
583
+ "Description": {},
584
+ "Permission": {},
585
+ "Type": {},
586
+ "Toolkit": {},
587
+ "App ID": {"overflow": "fold"}
588
+ }
589
+ for column in column_args:
590
+ table.add_column(column,**column_args[column])
517
591
 
518
592
  connections_client = get_connections_client()
519
593
  connections = connections_client.list()
@@ -555,19 +629,20 @@ class ToolsController:
555
629
  tool_type=ToolKind.openapi
556
630
  elif tool_binding.mcp is not None:
557
631
  tool_type=ToolKind.mcp
632
+ elif tool_binding.flow is not None:
633
+ tool_type=ToolKind.flow
558
634
  else:
559
635
  tool_type="Unknown"
560
636
 
561
637
  toolkit_name = ""
562
638
 
563
- if is_local_dev():
639
+ if tool.__tool_spec__.toolkit_id:
564
640
  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)
641
+ toolkit = toolkit_client.get_draft_by_id(tool.__tool_spec__.toolkit_id)
642
+ if isinstance(toolkit, dict) and "name" in toolkit:
643
+ toolkit_name = toolkit["name"]
644
+ elif toolkit:
645
+ toolkit_name = str(toolkit)
571
646
 
572
647
 
573
648
  table.add_row(
@@ -581,6 +656,10 @@ class ToolsController:
581
656
 
582
657
  rich.print(table)
583
658
 
659
+ for error in parse_errors:
660
+ for l in error:
661
+ logger.error(l)
662
+
584
663
  def get_all_tools(self) -> dict:
585
664
  return {entry["name"]: entry["id"] for entry in self.get_client().get()}
586
665
 
@@ -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__":