ibm-watsonx-orchestrate 1.9.0b2__py3-none-any.whl → 1.10.0b1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. ibm_watsonx_orchestrate/__init__.py +2 -1
  2. ibm_watsonx_orchestrate/agent_builder/agents/types.py +2 -0
  3. ibm_watsonx_orchestrate/agent_builder/connections/__init__.py +1 -1
  4. ibm_watsonx_orchestrate/agent_builder/connections/connections.py +1 -1
  5. ibm_watsonx_orchestrate/agent_builder/connections/types.py +16 -12
  6. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +47 -3
  7. ibm_watsonx_orchestrate/agent_builder/toolkits/types.py +18 -15
  8. ibm_watsonx_orchestrate/agent_builder/tools/python_tool.py +19 -7
  9. ibm_watsonx_orchestrate/agent_builder/tools/types.py +1 -1
  10. ibm_watsonx_orchestrate/agent_builder/voice_configurations/__init__.py +1 -0
  11. ibm_watsonx_orchestrate/agent_builder/voice_configurations/types.py +98 -0
  12. ibm_watsonx_orchestrate/cli/commands/agents/agents_command.py +20 -0
  13. ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +170 -1
  14. ibm_watsonx_orchestrate/cli/commands/connections/connections_command.py +7 -7
  15. ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +36 -26
  16. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +51 -22
  17. ibm_watsonx_orchestrate/cli/commands/server/server_command.py +110 -16
  18. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_command.py +43 -10
  19. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_controller.py +52 -25
  20. ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +5 -0
  21. ibm_watsonx_orchestrate/cli/commands/voice_configurations/voice_configurations_command.py +58 -0
  22. ibm_watsonx_orchestrate/cli/commands/voice_configurations/voice_configurations_controller.py +173 -0
  23. ibm_watsonx_orchestrate/cli/main.py +2 -0
  24. ibm_watsonx_orchestrate/client/agents/agent_client.py +64 -1
  25. ibm_watsonx_orchestrate/client/connections/connections_client.py +4 -3
  26. ibm_watsonx_orchestrate/client/knowledge_bases/knowledge_base_client.py +4 -4
  27. ibm_watsonx_orchestrate/client/voice_configurations/voice_configurations_client.py +75 -0
  28. ibm_watsonx_orchestrate/docker/compose-lite.yml +54 -5
  29. ibm_watsonx_orchestrate/docker/default.env +21 -13
  30. ibm_watsonx_orchestrate/flow_builder/flows/__init__.py +2 -0
  31. ibm_watsonx_orchestrate/flow_builder/flows/flow.py +115 -31
  32. ibm_watsonx_orchestrate/flow_builder/node.py +39 -15
  33. ibm_watsonx_orchestrate/flow_builder/types.py +114 -25
  34. ibm_watsonx_orchestrate/run/connections.py +2 -2
  35. {ibm_watsonx_orchestrate-1.9.0b2.dist-info → ibm_watsonx_orchestrate-1.10.0b1.dist-info}/METADATA +1 -1
  36. {ibm_watsonx_orchestrate-1.9.0b2.dist-info → ibm_watsonx_orchestrate-1.10.0b1.dist-info}/RECORD +39 -34
  37. {ibm_watsonx_orchestrate-1.9.0b2.dist-info → ibm_watsonx_orchestrate-1.10.0b1.dist-info}/WHEEL +0 -0
  38. {ibm_watsonx_orchestrate-1.9.0b2.dist-info → ibm_watsonx_orchestrate-1.10.0b1.dist-info}/entry_points.txt +0 -0
  39. {ibm_watsonx_orchestrate-1.9.0b2.dist-info → ibm_watsonx_orchestrate-1.10.0b1.dist-info}/licenses/LICENSE +0 -0
@@ -332,7 +332,6 @@ def write_merged_env_file(merged_env: dict, target_path: str = None) -> Path:
332
332
  file.write(f"{key}={val}\n")
333
333
  return Path(file.name)
334
334
 
335
-
336
335
  def get_dbtag_from_architecture(merged_env_dict: dict) -> str:
337
336
  """Detects system architecture and returns the corresponding DBTAG."""
338
337
  arch = platform.machine()
@@ -370,9 +369,14 @@ def get_persisted_user_env() -> dict | None:
370
369
  user_env = cfg.get(USER_ENV_CACHE_HEADER) if cfg.get(USER_ENV_CACHE_HEADER) else None
371
370
  return user_env
372
371
 
373
- def run_compose_lite(final_env_file: Path, experimental_with_langfuse=False, experimental_with_ibm_telemetry=False, with_doc_processing=False) -> None:
374
-
375
-
372
+ def run_compose_lite(
373
+ final_env_file: Path,
374
+ experimental_with_langfuse=False,
375
+ experimental_with_ibm_telemetry=False,
376
+ with_doc_processing=False,
377
+ with_voice=False,
378
+ experimental_with_langflow=False,
379
+ ) -> None:
376
380
  compose_path = get_compose_file()
377
381
 
378
382
  compose_command = ensure_docker_compose_installed()
@@ -400,7 +404,11 @@ def run_compose_lite(final_env_file: Path, experimental_with_langfuse=False, exp
400
404
  logger.info("Database container started successfully. Now starting other services...")
401
405
 
402
406
 
403
- # Step 2: Start all remaining services (except DB)
407
+ # Step 2: Create Langflow DB (if enabled)
408
+ if experimental_with_langflow:
409
+ create_langflow_db()
410
+
411
+ # Step 3: Start all remaining services (except DB)
404
412
  profiles = []
405
413
  if experimental_with_langfuse:
406
414
  profiles.append("langfuse")
@@ -408,6 +416,10 @@ def run_compose_lite(final_env_file: Path, experimental_with_langfuse=False, exp
408
416
  profiles.append("ibm-telemetry")
409
417
  if with_doc_processing:
410
418
  profiles.append("docproc")
419
+ if with_voice:
420
+ profiles.append("voice")
421
+ if experimental_with_langflow:
422
+ profiles.append("langflow")
411
423
 
412
424
  command = compose_command[:]
413
425
  for profile in profiles:
@@ -659,7 +671,6 @@ def run_compose_lite_down(final_env_file: Path, is_reset: bool = False) -> None:
659
671
  )
660
672
  sys.exit(1)
661
673
 
662
-
663
674
  def run_compose_lite_logs(final_env_file: Path, is_reset: bool = False) -> None:
664
675
  compose_path = get_compose_file()
665
676
  compose_command = ensure_docker_compose_installed()
@@ -855,6 +866,17 @@ def server_start(
855
866
  '--compose-file', '-f',
856
867
  help='Provide the path to a custom docker-compose file to use instead of the default compose file'
857
868
  ),
869
+ with_voice: bool = typer.Option(
870
+ False,
871
+ '--with-voice', '-v',
872
+ help='Enable voice controller to interact with the chat via voice channels'
873
+ ),
874
+ experimental_with_langflow: bool = typer.Option(
875
+ False,
876
+ '--experimental-with-langflow',
877
+ help='(Experimental) Enable Langflow UI, available at http://localhost:7861',
878
+ hidden=True
879
+ ),
858
880
  ):
859
881
  confirm_accepts_license_agreement(accept_terms_and_conditions)
860
882
 
@@ -896,6 +918,10 @@ def server_start(
896
918
  if experimental_with_ibm_telemetry:
897
919
  merged_env_dict['USE_IBM_TELEMETRY'] = 'true'
898
920
 
921
+ if experimental_with_langflow:
922
+ merged_env_dict['LANGFLOW_ENABLED'] = 'true'
923
+
924
+
899
925
  try:
900
926
  dev_edition_source = get_dev_edition_source(merged_env_dict)
901
927
  docker_login_by_dev_edition_source(merged_env_dict, dev_edition_source)
@@ -908,7 +934,9 @@ def server_start(
908
934
  run_compose_lite(final_env_file=final_env_file,
909
935
  experimental_with_langfuse=experimental_with_langfuse,
910
936
  experimental_with_ibm_telemetry=experimental_with_ibm_telemetry,
911
- with_doc_processing=with_doc_processing)
937
+ with_doc_processing=with_doc_processing,
938
+ with_voice=with_voice,
939
+ experimental_with_langflow=experimental_with_langflow)
912
940
 
913
941
  run_db_migration()
914
942
 
@@ -938,6 +966,8 @@ def server_start(
938
966
  logger.info(f"You can access the observability platform Langfuse at http://localhost:3010, username: orchestrate@ibm.com, password: orchestrate")
939
967
  if with_doc_processing:
940
968
  logger.info(f"Document processing in Flows (Public Preview) has been enabled.")
969
+ if experimental_with_langflow:
970
+ logger.info("Langflow has been enabled, the Langflow UI is available at http://localhost:7861")
941
971
 
942
972
  @server_app.command(name="stop")
943
973
  def server_stop(
@@ -1018,15 +1048,11 @@ def run_db_migration() -> None:
1018
1048
  merged_env_dict['ROUTING_LLM_API_KEY'] = ''
1019
1049
  merged_env_dict['ASSISTANT_LLM_API_KEY'] = ''
1020
1050
  final_env_file = write_merged_env_file(merged_env_dict)
1051
+
1021
1052
 
1022
- command = compose_command + [
1023
- "-f", str(compose_path),
1024
- "--env-file", str(final_env_file),
1025
- "exec",
1026
- "wxo-server-db",
1027
- "bash",
1028
- "-c",
1029
- '''
1053
+ pg_user = merged_env_dict.get("POSTGRES_USER","postgres")
1054
+
1055
+ migration_command = f'''
1030
1056
  APPLIED_MIGRATIONS_FILE="/var/lib/postgresql/applied_migrations/applied_migrations.txt"
1031
1057
  touch "$APPLIED_MIGRATIONS_FILE"
1032
1058
 
@@ -1037,7 +1063,7 @@ def run_db_migration() -> None:
1037
1063
  echo "Skipping already applied migration: $filename"
1038
1064
  else
1039
1065
  echo "Applying migration: $filename"
1040
- if psql -U postgres -d postgres -q -f "$file" > /dev/null 2>&1; then
1066
+ if psql -U {pg_user} -d postgres -q -f "$file" > /dev/null 2>&1; then
1041
1067
  echo "$filename" >> "$APPLIED_MIGRATIONS_FILE"
1042
1068
  else
1043
1069
  echo "Error applying $filename. Stopping migrations."
@@ -1046,6 +1072,15 @@ def run_db_migration() -> None:
1046
1072
  fi
1047
1073
  done
1048
1074
  '''
1075
+
1076
+ command = compose_command + [
1077
+ "-f", str(compose_path),
1078
+ "--env-file", str(final_env_file),
1079
+ "exec",
1080
+ "wxo-server-db",
1081
+ "bash",
1082
+ "-c",
1083
+ migration_command
1049
1084
  ]
1050
1085
 
1051
1086
  logger.info("Running Database Migration...")
@@ -1060,6 +1095,65 @@ def run_db_migration() -> None:
1060
1095
  )
1061
1096
  sys.exit(1)
1062
1097
 
1098
+ def create_langflow_db() -> None:
1099
+ compose_path = get_compose_file()
1100
+ compose_command = ensure_docker_compose_installed()
1101
+ default_env_path = get_default_env_file()
1102
+ merged_env_dict = merge_env(default_env_path, user_env_path=None)
1103
+ merged_env_dict['WATSONX_SPACE_ID']='X'
1104
+ merged_env_dict['WATSONX_APIKEY']='X'
1105
+ merged_env_dict['WXAI_API_KEY'] = ''
1106
+ merged_env_dict['ASSISTANT_EMBEDDINGS_API_KEY'] = ''
1107
+ merged_env_dict['ASSISTANT_LLM_SPACE_ID'] = ''
1108
+ merged_env_dict['ROUTING_LLM_SPACE_ID'] = ''
1109
+ merged_env_dict['USE_SAAS_ML_TOOLS_RUNTIME'] = ''
1110
+ merged_env_dict['BAM_API_KEY'] = ''
1111
+ merged_env_dict['ASSISTANT_EMBEDDINGS_SPACE_ID'] = ''
1112
+ merged_env_dict['ROUTING_LLM_API_KEY'] = ''
1113
+ merged_env_dict['ASSISTANT_LLM_API_KEY'] = ''
1114
+ final_env_file = write_merged_env_file(merged_env_dict)
1115
+
1116
+ pg_timeout = merged_env_dict.get('POSTGRES_READY_TIMEOUT','10')
1117
+
1118
+ pg_user = merged_env_dict.get("POSTGRES_USER","postgres")
1119
+
1120
+ creation_command = f"""
1121
+ echo 'Waiting for pg to initialize...'
1122
+
1123
+ timeout={pg_timeout}
1124
+ while [[ -z `pg_isready | grep 'accepting connections'` ]] && (( timeout > 0 )); do
1125
+ ((timeout-=1)) && sleep 1;
1126
+ done
1127
+
1128
+ if psql -U {pg_user} -lqt | cut -d \\| -f 1 | grep -qw langflow; then
1129
+ echo 'Existing Langflow DB found'
1130
+ else
1131
+ echo 'Creating Langflow DB'
1132
+ createdb -U "{pg_user}" -O "{pg_user}" langflow;
1133
+ psql -U {pg_user} -q -d postgres -c "GRANT CONNECT ON DATABASE langflow TO {pg_user}";
1134
+ fi
1135
+ """
1136
+ command = compose_command + [
1137
+ "-f", str(compose_path),
1138
+ "--env-file", str(final_env_file),
1139
+ "exec",
1140
+ "wxo-server-db",
1141
+ "bash",
1142
+ "-c",
1143
+ creation_command
1144
+ ]
1145
+
1146
+ logger.info("Preparing Langflow resources...")
1147
+ result = subprocess.run(command, capture_output=False)
1148
+
1149
+ if result.returncode == 0:
1150
+ logger.info("Langflow resources sucessfully created")
1151
+ else:
1152
+ error_message = result.stderr.decode('utf-8') if result.stderr else "Error occurred."
1153
+ logger.error(
1154
+ f"Failed to create Langflow resources\n{error_message}"
1155
+ )
1156
+ sys.exit(1)
1063
1157
 
1064
1158
  def bump_file_iteration(filename: str) -> str:
1065
1159
  regex = re.compile(f"^(?P<name>[^\\(\\s\\.\\)]+)(\\((?P<num>\\d+)\\))?(?P<type>\\.(?:{'|'.join(_EXPORT_FILE_TYPES)}))?$")
@@ -1,7 +1,7 @@
1
1
  import typer
2
2
  from typing import List
3
3
  from typing_extensions import Annotated, Optional
4
- from ibm_watsonx_orchestrate.agent_builder.toolkits.types import ToolkitKind, Language
4
+ from ibm_watsonx_orchestrate.agent_builder.toolkits.types import ToolkitKind, Language, ToolkitTransportKind
5
5
  from ibm_watsonx_orchestrate.cli.commands.toolkit.toolkit_controller import ToolkitController
6
6
  import logging
7
7
  import sys
@@ -45,6 +45,14 @@ def import_toolkit(
45
45
  "The first argument will be used as the executable, the rest as its arguments."
46
46
  ),
47
47
  ] = None,
48
+ url: Annotated[
49
+ Optional[str],
50
+ typer.Option("--url", "-u", help="The URL of the remote MCP server", hidden=True),
51
+ ] = None,
52
+ transport: Annotated[
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),
55
+ ] = None,
48
56
  tools: Annotated[
49
57
  Optional[str],
50
58
  typer.Option("--tools", "-t", help="Comma-separated list of tools to import. Or you can use \"*\" to use all tools"),
@@ -64,18 +72,41 @@ def import_toolkit(
64
72
  else:
65
73
  tool_list = None
66
74
 
67
- if not package and not package_root and not command:
68
- logger.error("You must provide either '--package', '--package-root' or '--command'.")
69
- sys.exit(1)
75
+ if not url and not transport:
76
+ if not package and not package_root and not command:
77
+ logger.error("You must provide either '--package', '--package-root' or '--command'.")
78
+ sys.exit(1)
70
79
 
71
- if package_root and not command:
72
- logger.error("Error: '--command' flag must be provided when '--package-root' is specified.")
73
- sys.exit(1)
74
-
75
- if package_root and package:
76
- logger.error("Please choose either '--package-root' or '--package' but not both.")
80
+ if package_root and not command:
81
+ logger.error("Error: '--command' flag must be provided when '--package-root' is specified.")
82
+ sys.exit(1)
83
+
84
+ if package_root and package:
85
+ logger.error("Please choose either '--package-root' or '--package' but not both.")
86
+ sys.exit(1)
87
+
88
+ if (url and not transport) or (transport and not url):
89
+ logger.error("Both '--url' and '--transport' must be provided together for remote MCP.")
77
90
  sys.exit(1)
78
91
 
92
+ if url and transport:
93
+ forbidden_local_opts = []
94
+ if package:
95
+ forbidden_local_opts.append("--package")
96
+ if package_root:
97
+ forbidden_local_opts.append("--package-root")
98
+ if language:
99
+ forbidden_local_opts.append("--language")
100
+ if command:
101
+ forbidden_local_opts.append("--command")
102
+
103
+ if forbidden_local_opts:
104
+ logger.error(
105
+ f"When using '--url' and '--transport' for a remote MCP, you cannot specify: "
106
+ f"{', '.join(forbidden_local_opts)}"
107
+ )
108
+ sys.exit(1)
109
+
79
110
  if package and not package_root:
80
111
  if not command:
81
112
  if language == Language.NODE:
@@ -97,6 +128,8 @@ def import_toolkit(
97
128
  package_root=package_root,
98
129
  language=language,
99
130
  command=command,
131
+ url=url,
132
+ transport=transport
100
133
  )
101
134
  toolkit_controller.import_toolkit(tools=tool_list, app_id=app_id)
102
135
 
@@ -10,7 +10,7 @@ import requests
10
10
  from ibm_watsonx_orchestrate.client.toolkit.toolkit_client import ToolKitClient
11
11
  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
- from ibm_watsonx_orchestrate.agent_builder.toolkits.types import ToolkitKind, Language, ToolkitSource
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
15
  from ibm_watsonx_orchestrate.utils.utils import sanatize_app_id
16
16
  from ibm_watsonx_orchestrate.client.connections import get_connections_client
@@ -26,15 +26,16 @@ import json
26
26
 
27
27
  logger = logging.getLogger(__name__)
28
28
 
29
- def get_connection_id(app_id: str) -> str:
29
+ def get_connection_id(app_id: str, is_local_mcp: bool) -> str:
30
30
  connections_client = get_connections_client()
31
31
  existing_draft_configuration = connections_client.get_config(app_id=app_id, env='draft')
32
32
  existing_live_configuration = connections_client.get_config(app_id=app_id, env='live')
33
33
 
34
34
  for config in [existing_draft_configuration, existing_live_configuration]:
35
- if config and config.security_scheme != 'key_value_creds':
36
- logger.error("Only key_value credentials are currently supported")
37
- exit(1)
35
+ if is_local_mcp is True:
36
+ if config and config.security_scheme != 'key_value_creds':
37
+ logger.error("Only key_value credentials are currently supported for local MCP")
38
+ exit(1)
38
39
  connection_id = None
39
40
  if app_id is not None:
40
41
  connection = connections_client.get(app_id=app_id)
@@ -59,6 +60,8 @@ class ToolkitController:
59
60
  package_root: str = None,
60
61
  language: Language = None,
61
62
  command: str = None,
63
+ url: str = None,
64
+ transport: ToolkitTransportKind = None
62
65
  ):
63
66
  self.kind = kind
64
67
  self.name = name
@@ -68,6 +71,8 @@ class ToolkitController:
68
71
  self.language = language
69
72
  self.command = command
70
73
  self.client = None
74
+ self.url = url
75
+ self.transport = transport
71
76
 
72
77
  self.source: ToolkitSource = (
73
78
  ToolkitSource.FILES if package_root else ToolkitSource.PUBLIC_REGISTRY
@@ -95,15 +100,23 @@ class ToolkitController:
95
100
  logger.error(f"Existing toolkit found with name '{self.name}'. Failed to create toolkit.")
96
101
  sys.exit(1)
97
102
 
98
- try:
99
- command_parts = json.loads(self.command)
100
- if not isinstance(command_parts, list):
101
- raise ValueError("JSON command must be a list of strings")
102
- except (json.JSONDecodeError, ValueError):
103
- command_parts = self.command.split()
103
+ if not self.command:
104
+ command_parts = []
105
+ else:
106
+ try:
107
+ command_parts = json.loads(self.command)
108
+ if not isinstance(command_parts, list):
109
+ raise ValueError("JSON command must be a list of strings")
110
+ except (json.JSONDecodeError, ValueError):
111
+ command_parts = self.command.split()
112
+
113
+ if command_parts:
114
+ command = command_parts[0]
115
+ args = command_parts[1:]
116
+ else:
117
+ command = None
118
+ args = []
104
119
 
105
- command = command_parts[0]
106
- args = command_parts[1:]
107
120
 
108
121
  if self.package_root:
109
122
  is_folder = os.path.isdir(self.package_root)
@@ -150,18 +163,31 @@ class ToolkitController:
150
163
  console.print(f" • {tool}")
151
164
 
152
165
  # Create toolkit metadata
153
- payload = {
154
- "name": self.name,
155
- "description": self.description,
156
- "mcp": {
157
- "source": self.source.value,
158
- "command": command,
159
- "args": args,
160
- "tools": tools,
161
- "connections": remapped_connections,
166
+ payload = {}
167
+
168
+ if self.transport is not None and self.url is not None:
169
+ payload = {
170
+ "name": self.name,
171
+ "description": self.description,
172
+ "mcp": {
173
+ "server_url": self.url,
174
+ "transport": self.transport.value,
175
+ "tools": tools,
176
+ "connections": remapped_connections,
177
+ }
178
+ }
179
+ else:
180
+ payload = {
181
+ "name": self.name,
182
+ "description": self.description,
183
+ "mcp": {
184
+ "source": self.source.value,
185
+ "command": command,
186
+ "args": args,
187
+ "tools": tools,
188
+ "connections": remapped_connections,
189
+ }
162
190
  }
163
- }
164
-
165
191
 
166
192
  with Progress(
167
193
  SpinnerColumn(spinner_name="dots"),
@@ -215,7 +241,8 @@ class ToolkitController:
215
241
  raise typer.BadParameter(f"The provided --app-id '{app_id}' is not valid. --app-id cannot be empty or whitespace")
216
242
 
217
243
  runtime_id = sanatize_app_id(runtime_id)
218
- app_id_dict[runtime_id] = get_connection_id(local_id)
244
+ is_local_mcp = self.package is not None or self.package_root is not None
245
+ app_id_dict[runtime_id] = get_connection_id(local_id, is_local_mcp)
219
246
 
220
247
  return app_id_dict
221
248
 
@@ -464,6 +464,11 @@ The [bold]flow tool[/bold] is being imported from [green]`{file}`[/green].
464
464
  continue
465
465
 
466
466
  model = obj().to_json()
467
+ # Ensure metadata exists and is correct
468
+ if "metadata" not in model or not isinstance(model["metadata"], dict):
469
+ model["metadata"] = {}
470
+ if "source_kind" not in model["metadata"]:
471
+ model["metadata"]["source_kind"] = "adk/python"
467
472
  break
468
473
 
469
474
  elif file_path.suffix.lower() == ".json":
@@ -0,0 +1,58 @@
1
+ import sys
2
+ from typing import Annotated
3
+ import typer
4
+ import logging
5
+
6
+ from ibm_watsonx_orchestrate.cli.commands.voice_configurations.voice_configurations_controller import VoiceConfigurationsController
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ voice_configurations_app = typer.Typer(no_args_is_help=True)
11
+
12
+ @voice_configurations_app.command(name="import", help="Import a voice configuration into the active environment from a file")
13
+ def import_voice_config(
14
+ file: Annotated[
15
+ str,
16
+ typer.Option(
17
+ "--file",
18
+ "-f",
19
+ help="YAML file with voice configuraton definition"
20
+ )
21
+ ],
22
+ ):
23
+ voice_config_controller = VoiceConfigurationsController()
24
+ imported_config = voice_config_controller.import_voice_config(file)
25
+ voice_config_controller.publish_or_update_voice_config(imported_config)
26
+
27
+ @voice_configurations_app.command(name="remove", help="Remove a voice configuration from the active environment")
28
+ def remove_voice_config(
29
+ voice_config_name: Annotated[
30
+ str,
31
+ typer.Option(
32
+ "--name",
33
+ "-n",
34
+ help="name of the voice configuration to remove"
35
+ )
36
+ ] = None,
37
+ ):
38
+ voice_config_controller = VoiceConfigurationsController()
39
+ if voice_config_name:
40
+ voice_config_controller.remove_voice_config_by_name(voice_config_name)
41
+ else:
42
+ raise TypeError("You must specify the name of a voice configuration")
43
+
44
+
45
+
46
+ @voice_configurations_app.command(name="list", help="List all voice configurations in the active environment")
47
+ def list_voice_configs(
48
+ verbose: Annotated[
49
+ bool,
50
+ typer.Option(
51
+ "--verbose",
52
+ "-v",
53
+ help="List full details of all voice configurations in json format"
54
+ )
55
+ ] = False,
56
+ ):
57
+ voice_config_controller = VoiceConfigurationsController()
58
+ voice_config_controller.list_voice_configs(verbose)
@@ -0,0 +1,173 @@
1
+ import json
2
+ import sys
3
+ import rich
4
+ import yaml
5
+ import logging
6
+ from ibm_watsonx_orchestrate.agent_builder.voice_configurations import VoiceConfiguration
7
+ from ibm_watsonx_orchestrate.client.utils import instantiate_client
8
+ from ibm_watsonx_orchestrate.client.voice_configurations.voice_configurations_client import VoiceConfigurationsClient
9
+ from ibm_watsonx_orchestrate.utils.exceptions import BadRequest
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class VoiceConfigurationsController:
14
+
15
+ def __init__(self):
16
+ self.voice_configs_client = None
17
+
18
+ def get_voice_configurations_client(self):
19
+ if not self.voice_configs_client:
20
+ self.voice_configs_client = instantiate_client(VoiceConfigurationsClient)
21
+ return self.voice_configs_client
22
+
23
+
24
+ def import_voice_config(self, file: str) -> VoiceConfiguration:
25
+
26
+ if file.endswith('.yaml') or file.endswith('.yml'):
27
+ with open(file, 'r') as f:
28
+ content = yaml.load(f, Loader=yaml.SafeLoader)
29
+
30
+ elif file.endswith(".json"):
31
+ with open(file, 'r') as f:
32
+ content = json.load(f)
33
+
34
+ else:
35
+ raise BadRequest("file must end in .yaml, .yml or .json")
36
+
37
+ return VoiceConfiguration.model_validate(content)
38
+
39
+
40
+ def fetch_voice_configs(self) -> list[VoiceConfiguration]:
41
+ client = self.get_voice_configurations_client()
42
+ res = client.list()
43
+
44
+ voice_configs = []
45
+
46
+ for config in res:
47
+ try:
48
+ voice_configs.append(VoiceConfiguration.model_validate(config))
49
+ except:
50
+ name = config.get('name', None)
51
+ logger.error(f"Config '{name}' could not be parsed")
52
+
53
+ return voice_configs
54
+
55
+ def get_voice_config(self, voice_config_id: str) -> VoiceConfiguration | None:
56
+ client = self.get_voice_configurations_client()
57
+ return client.get(voice_config_id)
58
+
59
+ def get_voice_config_by_name(self, voice_config_name: str) -> VoiceConfiguration | None:
60
+ client = self.get_voice_configurations_client()
61
+ configs = client.get_by_name(voice_config_name)
62
+ if len(configs) == 0:
63
+ logger.error(f"No voice_configs with the name '{voice_config_name}' found. Failed to get config")
64
+ sys.exit(1)
65
+
66
+ if len(configs) > 1:
67
+ logger.error(f"Multiple voice_configs with the name '{voice_config_name}' found. Failed to get config")
68
+ sys.exit(1)
69
+
70
+ return configs[0]
71
+
72
+ def list_voice_configs(self, verbose: bool) -> None:
73
+ voice_configs = self.fetch_voice_configs()
74
+
75
+ if verbose:
76
+ json_configs = [json.loads(x.dumps_spec()) for x in voice_configs]
77
+ rich.print_json(json.dumps(json_configs, indent=4))
78
+ else:
79
+ config_table = rich.table.Table(
80
+ show_header=True,
81
+ header_style="bold white",
82
+ title="Voice Configurations",
83
+ show_lines=True
84
+ )
85
+
86
+ column_args={
87
+ "Name" : {"overflow": "fold"},
88
+ "ID" : {"overflow": "fold"},
89
+ "STT Provider" : {"overflow": "fold"},
90
+ "TTS Provider" : {"overflow": "fold"},
91
+ "Attached Agents" : {}
92
+ }
93
+
94
+ for column in column_args:
95
+ config_table.add_column(column, **column_args[column])
96
+
97
+ for config in voice_configs:
98
+ attached_agents = [x.display_name or x.name or x.id for x in config.attached_agents]
99
+ config_table.add_row(
100
+ config.name,
101
+ config.voice_configuration_id,
102
+ config.speech_to_text.provider,
103
+ config.text_to_speech.provider,
104
+ ",".join(attached_agents)
105
+ )
106
+
107
+ rich.print(config_table)
108
+
109
+
110
+ def create_voice_config(self, voice_config: VoiceConfiguration) -> str | None:
111
+ client = self.get_voice_configurations_client()
112
+ res = client.create(voice_config)
113
+ config_id = res.get("id",None)
114
+ if config_id:
115
+ logger.info(f"Sucessfully created voice config '{voice_config['name']}'. id: '{config_id}'")
116
+
117
+ return config_id
118
+
119
+
120
+ def update_voice_config_by_id(self, voice_config_id: str, voice_config: VoiceConfiguration) -> str | None:
121
+ client = self.get_voice_configurations_client()
122
+ res = client.update(voice_config_id,voice_config)
123
+ config_id = res.get("id",None)
124
+ if config_id:
125
+ logger.info(f"Sucessfully updated voice config '{voice_config['name']}'. id: '{config_id}'")
126
+
127
+ return config_id
128
+
129
+ def update_voice_config_by_name(self, voice_config_name: str, voice_config: VoiceConfiguration) -> str | None:
130
+ client = self.get_voice_configurations_client()
131
+ existing_config = client.get_by_name(voice_config_name)
132
+
133
+ if existing_config and len(existing_config) > 0:
134
+ config_id = existing_config[0].voice_configuration_id
135
+ client.update(config_id,voice_config)
136
+ else:
137
+ logger.warning(f"Voice config '{voice_config_name}' not found, creating new config instead")
138
+ config_id = self.create_voice_config(voice_config)
139
+
140
+ return config_id
141
+
142
+ def publish_or_update_voice_config(self, voice_config: VoiceConfiguration) -> str | None:
143
+ client = self.get_voice_configurations_client()
144
+ voice_config_name = voice_config.name
145
+ existing_config = client.get_by_name(voice_config_name)
146
+
147
+ if existing_config and len(existing_config) > 0:
148
+ config_id = existing_config[0].voice_configuration_id
149
+ client.update(config_id,voice_config)
150
+ else:
151
+ client.create(voice_config)
152
+
153
+ def remove_voice_config_by_id(self, voice_config_id: str) -> None:
154
+ client = self.get_voice_configurations_client()
155
+ client.delete(voice_config_id)
156
+ logger.info(f"Sucessfully deleted voice config '{voice_config_id}'")
157
+
158
+ def remove_voice_config_by_name(self, voice_config_name: str) -> None:
159
+ client = self.get_voice_configurations_client()
160
+ voice_config = self.get_voice_config_by_name(voice_config_name)
161
+ if voice_config:
162
+ client.delete(voice_config.voice_configuration_id)
163
+ logger.info(f"Sucessfully deleted voice config '{voice_config_name}'")
164
+ else:
165
+ logger.info(f"Voice config '{voice_config_name}' not found")
166
+
167
+
168
+
169
+
170
+
171
+
172
+
173
+