ibm-watsonx-orchestrate 1.9.0b1__py3-none-any.whl → 1.10.0b0__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 (34) 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/knowledge_bases/types.py +2 -2
  4. ibm_watsonx_orchestrate/agent_builder/models/types.py +5 -0
  5. ibm_watsonx_orchestrate/agent_builder/tools/python_tool.py +19 -7
  6. ibm_watsonx_orchestrate/agent_builder/tools/types.py +5 -3
  7. ibm_watsonx_orchestrate/agent_builder/voice_configurations/__init__.py +1 -0
  8. ibm_watsonx_orchestrate/agent_builder/voice_configurations/types.py +98 -0
  9. ibm_watsonx_orchestrate/cli/commands/agents/agents_command.py +20 -0
  10. ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +170 -1
  11. ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +5 -2
  12. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_controller.py +103 -20
  13. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +19 -12
  14. ibm_watsonx_orchestrate/cli/commands/models/model_provider_mapper.py +17 -13
  15. ibm_watsonx_orchestrate/cli/commands/server/server_command.py +17 -4
  16. ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +6 -1
  17. ibm_watsonx_orchestrate/cli/commands/voice_configurations/voice_configurations_command.py +58 -0
  18. ibm_watsonx_orchestrate/cli/commands/voice_configurations/voice_configurations_controller.py +173 -0
  19. ibm_watsonx_orchestrate/cli/main.py +2 -0
  20. ibm_watsonx_orchestrate/client/agents/agent_client.py +64 -1
  21. ibm_watsonx_orchestrate/client/connections/connections_client.py +14 -2
  22. ibm_watsonx_orchestrate/client/copilot/cpe/copilot_cpe_client.py +5 -3
  23. ibm_watsonx_orchestrate/client/voice_configurations/voice_configurations_client.py +75 -0
  24. ibm_watsonx_orchestrate/docker/compose-lite.yml +23 -2
  25. ibm_watsonx_orchestrate/docker/default.env +16 -12
  26. ibm_watsonx_orchestrate/flow_builder/flows/__init__.py +2 -2
  27. ibm_watsonx_orchestrate/flow_builder/flows/flow.py +29 -24
  28. ibm_watsonx_orchestrate/flow_builder/types.py +109 -17
  29. ibm_watsonx_orchestrate/flow_builder/utils.py +7 -3
  30. {ibm_watsonx_orchestrate-1.9.0b1.dist-info → ibm_watsonx_orchestrate-1.10.0b0.dist-info}/METADATA +1 -1
  31. {ibm_watsonx_orchestrate-1.9.0b1.dist-info → ibm_watsonx_orchestrate-1.10.0b0.dist-info}/RECORD +34 -29
  32. {ibm_watsonx_orchestrate-1.9.0b1.dist-info → ibm_watsonx_orchestrate-1.10.0b0.dist-info}/WHEEL +0 -0
  33. {ibm_watsonx_orchestrate-1.9.0b1.dist-info → ibm_watsonx_orchestrate-1.10.0b0.dist-info}/entry_points.txt +0 -0
  34. {ibm_watsonx_orchestrate-1.9.0b1.dist-info → ibm_watsonx_orchestrate-1.10.0b0.dist-info}/licenses/LICENSE +0 -0
@@ -10,10 +10,12 @@ from rich.progress import Progress, SpinnerColumn, TextColumn
10
10
  from requests import ConnectionError
11
11
  from typing import List
12
12
  from ibm_watsonx_orchestrate.client.base_api_client import ClientAPIException
13
+ from ibm_watsonx_orchestrate.agent_builder.knowledge_bases.types import KnowledgeBaseSpec
13
14
  from ibm_watsonx_orchestrate.agent_builder.tools import ToolSpec, ToolPermission, ToolRequestBody, ToolResponseBody
14
15
  from ibm_watsonx_orchestrate.cli.commands.agents.agents_controller import AgentsController, AgentKind, SpecVersion
15
16
  from ibm_watsonx_orchestrate.agent_builder.agents.types import DEFAULT_LLM, BaseAgentSpec
16
17
  from ibm_watsonx_orchestrate.client.agents.agent_client import AgentClient
18
+ from ibm_watsonx_orchestrate.client.knowledge_bases.knowledge_base_client import KnowledgeBaseClient
17
19
  from ibm_watsonx_orchestrate.client.tools.tool_client import ToolClient
18
20
  from ibm_watsonx_orchestrate.client.copilot.cpe.copilot_cpe_client import CPEClient
19
21
  from ibm_watsonx_orchestrate.client.utils import instantiate_client
@@ -56,10 +58,16 @@ def _get_incomplete_tool_from_name(tool_name: str) -> dict:
56
58
  "input_schema": input_schema, "output_schema": output_schema})
57
59
  return spec.model_dump()
58
60
 
61
+
59
62
  def _get_incomplete_agent_from_name(agent_name: str) -> dict:
60
63
  spec = BaseAgentSpec(**{"name": agent_name, "description": agent_name, "kind": AgentKind.NATIVE})
61
64
  return spec.model_dump()
62
65
 
66
+ def _get_incomplete_knowledge_base_from_name(kb_name: str) -> dict:
67
+ spec = KnowledgeBaseSpec(**{"name": kb_name, "description": kb_name})
68
+ return spec.model_dump()
69
+
70
+
63
71
  def _get_tools_from_names(tool_names: List[str]) -> List[dict]:
64
72
  if not len(tool_names):
65
73
  return []
@@ -115,6 +123,34 @@ def _get_agents_from_names(collaborators_names: List[str]) -> List[dict]:
115
123
 
116
124
  return agents
117
125
 
126
+ def _get_knowledge_bases_from_names(kb_names: List[str]) -> List[dict]:
127
+ if not len(kb_names):
128
+ return []
129
+
130
+ kb_client = get_knowledge_bases_client()
131
+
132
+ try:
133
+ with _get_progress_spinner() as progress:
134
+ task = progress.add_task(description="Fetching Knowledge Bases", total=None)
135
+ knowledge_bases = kb_client.get_by_names(kb_names)
136
+ found_kbs = {kb.get("name") for kb in knowledge_bases}
137
+ progress.remove_task(task)
138
+ progress.refresh()
139
+ for kb_name in kb_names:
140
+ if kb_name not in found_kbs:
141
+ logger.warning(
142
+ f"Failed to find knowledge base named '{kb_name}'. Falling back to incomplete knowledge base definition. Copilot performance maybe effected.")
143
+ knowledge_bases.append(_get_incomplete_knowledge_base_from_name(kb_name))
144
+ except ConnectionError:
145
+ logger.warning(
146
+ f"Failed to fetch knowledge bases from server. For optimal results please start the server and import the relevant knowledge bases {', '.join(kb_names)}.")
147
+ knowledge_bases = []
148
+ for kb_name in kb_names:
149
+ knowledge_bases.append(_get_incomplete_knowledge_base_from_name(kb_name))
150
+
151
+ return knowledge_bases
152
+
153
+
118
154
  def get_cpe_client() -> CPEClient:
119
155
  url = os.getenv('CPE_URL', "http://localhost:8081")
120
156
  return instantiate_client(client=CPEClient, url=url)
@@ -124,6 +160,10 @@ def get_tool_client(*args, **kwargs):
124
160
  return instantiate_client(ToolClient)
125
161
 
126
162
 
163
+ def get_knowledge_bases_client(*args, **kwargs):
164
+ return instantiate_client(KnowledgeBaseClient)
165
+
166
+
127
167
  def get_native_client(*args, **kwargs):
128
168
  return instantiate_client(AgentClient)
129
169
 
@@ -144,18 +184,34 @@ def gather_utterances(max: int) -> list[str]:
144
184
  return utterances
145
185
 
146
186
 
147
- def get_deployed_tools_agents():
187
+ def get_knowledge_bases(client):
188
+ with _get_progress_spinner() as progress:
189
+ task = progress.add_task(description="Fetching Knowledge Bases", total=None)
190
+ try:
191
+ knowledge_bases = client.get()
192
+ progress.remove_task(task)
193
+ except ConnectionError:
194
+ knowledge_bases = []
195
+ progress.remove_task(task)
196
+ progress.refresh()
197
+ logger.warning("Failed to contact wxo server to fetch knowledge_bases. Proceeding with empty agent list")
198
+ return knowledge_bases
199
+
200
+
201
+ def get_deployed_tools_agents_and_knowledge_bases():
148
202
  all_tools = find_tools_by_description(tool_client=get_tool_client(), description=None)
149
203
  # TODO: this brings only the "native" agents. Can external and assistant agents also be collaborators?
150
204
  all_agents = find_agents(agent_client=get_native_client())
151
- return {"tools": all_tools, "agents": all_agents}
205
+ all_knowledge_bases = get_knowledge_bases(get_knowledge_bases_client())
206
+
207
+ return {"tools": all_tools, "collaborators": all_agents, "knowledge_bases": all_knowledge_bases}
152
208
 
153
209
 
154
210
  def pre_cpe_step(cpe_client):
155
- tools_agents = get_deployed_tools_agents()
211
+ tools_agents_and_knowledge_bases = get_deployed_tools_agents_and_knowledge_bases()
156
212
  user_message = ""
157
213
  with _get_progress_spinner() as progress:
158
- task = progress.add_task(description="Initilizing Prompt Engine", total=None)
214
+ task = progress.add_task(description="Initializing Prompt Engine", total=None)
159
215
  response = cpe_client.submit_pre_cpe_chat(user_message=user_message)
160
216
  progress.remove_task(task)
161
217
 
@@ -165,15 +221,26 @@ def pre_cpe_step(cpe_client):
165
221
  rich.print('\n🤖 Copilot: ' + response["message"])
166
222
  user_message = Prompt.ask("\n👤 You").strip()
167
223
  message_content = {"user_message": user_message}
168
- elif "description" in response and response["description"]:
224
+ elif "description" in response and response["description"]: # after we have a description, we pass the all tools
169
225
  res["description"] = response["description"]
170
- message_content = tools_agents
171
- elif "metadata" in response:
172
- res["agent_name"] = response["metadata"]["agent_name"]
173
- res["agent_style"] = response["metadata"]["style"]
174
- res["tools"] = [t for t in tools_agents["tools"] if t["name"] in response["metadata"]["tools"]]
175
- res["collaborators"] = [a for a in tools_agents["agents"] if
176
- a["name"] in response["metadata"]["collaborators"]]
226
+ message_content = {"tools": tools_agents_and_knowledge_bases['tools']}
227
+ elif "tools" in response and response[
228
+ 'tools'] is not None: # after tools were selected, we pass all collaborators
229
+ res["tools"] = [t for t in tools_agents_and_knowledge_bases["tools"] if
230
+ t["name"] in response["tools"]]
231
+ message_content = {"collaborators": tools_agents_and_knowledge_bases['collaborators']}
232
+ elif "collaborators" in response and response[
233
+ 'collaborators'] is not None: # after we have collaborators, we pass all knowledge bases
234
+ res["collaborators"] = [a for a in tools_agents_and_knowledge_bases["collaborators"] if
235
+ a["name"] in response["collaborators"]]
236
+ message_content = {"knowledge_bases": tools_agents_and_knowledge_bases['knowledge_bases']}
237
+ elif "knowledge_bases" in response and response['knowledge_bases'] is not None: # after we have knowledge bases, we pass selected=True to mark that all selection were done
238
+ res["knowledge_bases"] = [a for a in tools_agents_and_knowledge_bases["knowledge_bases"] if
239
+ a["name"] in response["knowledge_bases"]]
240
+ message_content = {"selected": True}
241
+ elif "agent_name" in response and response['agent_name'] is not None: # once we have a name and style, this phase has ended
242
+ res["agent_name"] = response["agent_name"]
243
+ res["agent_style"] = response["agent_style"]
177
244
  return res
178
245
  with _get_progress_spinner() as progress:
179
246
  task = progress.add_task(description="Thinking...", total=None)
@@ -194,6 +261,7 @@ def find_tools_by_description(description, tool_client):
194
261
  logger.warning("Failed to contact wxo server to fetch tools. Proceeding with empty tool list")
195
262
  return tools
196
263
 
264
+
197
265
  def find_agents(agent_client):
198
266
  with _get_progress_spinner() as progress:
199
267
  task = progress.add_task(description="Fetching Agents", total=None)
@@ -279,16 +347,25 @@ def prompt_tune(agent_spec: str, output_file: str | None, samples_file: str | No
279
347
  tools = _get_tools_from_names(agent.tools)
280
348
 
281
349
  collaborators = _get_agents_from_names(agent.collaborators)
350
+
351
+ knowledge_bases = _get_knowledge_bases_from_names(agent.knowledge_base)
282
352
  try:
283
- new_prompt = talk_to_cpe(cpe_client=client, samples_file=samples_file,
284
- context_data={"initial_instruction": instr, 'tools': tools, 'description': agent.description,
285
- "collaborators": collaborators})
353
+ new_prompt = talk_to_cpe(cpe_client=client,
354
+ samples_file=samples_file,
355
+ context_data={
356
+ "initial_instruction": instr,
357
+ 'tools': tools,
358
+ 'description': agent.description,
359
+ "collaborators": collaborators,
360
+ "knowledge_bases": knowledge_bases
361
+ })
286
362
  except ConnectionError:
287
363
  logger.error(
288
364
  "Failed to connect to Copilot server. Please ensure Copilot is running via `orchestrate copilot start`")
289
365
  sys.exit(1)
290
366
  except ClientAPIException:
291
- logger.error("An unexpected server error has occur with in the Copilot server. Please check the logs via `orchestrate server logs`")
367
+ logger.error(
368
+ "An unexpected server error has occur with in the Copilot server. Please check the logs via `orchestrate server logs`")
292
369
  sys.exit(1)
293
370
 
294
371
  if new_prompt:
@@ -316,17 +393,21 @@ def create_agent(output_file: str, llm: str, samples_file: str | None, dry_run_f
316
393
  "Failed to connect to Copilot server. Please ensure Copilot is running via `orchestrate copilot start`")
317
394
  sys.exit(1)
318
395
  except ClientAPIException:
319
- logger.error("An unexpected server error has occur with in the Copilot server. Please check the logs via `orchestrate server logs`")
396
+ logger.error(
397
+ "An unexpected server error has occur with in the Copilot server. Please check the logs via `orchestrate server logs`")
320
398
  sys.exit(1)
321
-
399
+
322
400
  tools = res["tools"]
323
401
  collaborators = res["collaborators"]
402
+ knowledge_bases = res["knowledge_bases"]
324
403
  description = res["description"]
325
404
  agent_name = res["agent_name"]
326
405
  agent_style = res["agent_style"]
327
406
 
328
407
  # 4. discuss the instructions
329
- instructions = talk_to_cpe(cpe_client, samples_file, {'description': description, 'tools': tools, 'collaborators': collaborators})
408
+ instructions = talk_to_cpe(cpe_client, samples_file,
409
+ {'description': description, 'tools': tools, 'collaborators': collaborators,
410
+ 'knowledge_bases': knowledge_bases})
330
411
 
331
412
  # 6. create and save the agent
332
413
  llm = llm if llm else DEFAULT_LLM
@@ -334,7 +415,9 @@ def create_agent(output_file: str, llm: str, samples_file: str | None, dry_run_f
334
415
  'style': agent_style,
335
416
  'tools': [t['name'] for t in tools],
336
417
  'llm': llm,
337
- 'collaborators': [c['name'] for c in collaborators]
418
+ 'collaborators': [c['name'] for c in collaborators],
419
+ 'knowledge_base': [k['name'] for k in knowledge_bases]
420
+ # generate_agent_spec expects knowledge_base and not knowledge_bases
338
421
  }
339
422
  agent = AgentsController.generate_agent_spec(agent_name, AgentKind.NATIVE, description, **params)
340
423
  agent.instructions = instructions
@@ -72,6 +72,21 @@ class KnowledgeBaseController:
72
72
 
73
73
  knowledge_bases = parse_file(file=file)
74
74
 
75
+ if app_id:
76
+ connections_client = get_connections_client()
77
+ connection_id = None
78
+
79
+ connections = connections_client.get_draft_by_app_id(app_id=app_id)
80
+ if not connections:
81
+ logger.error(f"No connection exists with the app-id '{app_id}'")
82
+ exit(1)
83
+
84
+ connection_id = connections.connection_id
85
+
86
+ for kb in knowledge_bases:
87
+ if kb.conversational_search_tool and kb.conversational_search_tool.index_config and len(kb.conversational_search_tool.index_config) > 0:
88
+ kb.conversational_search_tool.index_config[0].connection_id = connection_id
89
+
75
90
  existing_knowledge_bases = client.get_by_names([kb.name for kb in knowledge_bases])
76
91
 
77
92
  for kb in knowledge_bases:
@@ -97,18 +112,10 @@ class KnowledgeBaseController:
97
112
  if len(kb.conversational_search_tool.index_config) != 1:
98
113
  raise ValueError(f"Must provide exactly one conversational_search_tool.index_config. Provided {len(kb.conversational_search_tool.index_config)}.")
99
114
 
100
-
101
- if app_id:
102
- connections_client = get_connections_client()
103
- connection_id = None
104
- if app_id is not None:
105
- connections = connections_client.get_draft_by_app_id(app_id=app_id)
106
- if not connections:
107
- logger.error(f"No connection exists with the app-id '{app_id}'")
108
- exit(1)
109
-
110
- connection_id = connections.connection_id
111
- kb.conversational_search_tool.index_config[0].connection_id = connection_id
115
+ if (kb.conversational_search_tool.index_config[0].milvus or \
116
+ kb.conversational_search_tool.index_config[0].elastic_search) and \
117
+ not kb.conversational_search_tool.index_config[0].connection_id:
118
+ raise ValueError(f"Must provide credentials (via --app-id) when using milvus or elastic_search.")
112
119
 
113
120
  kb.prioritize_built_in_index = False
114
121
  client.create(payload=kb.model_dump(exclude_none=True))
@@ -10,19 +10,23 @@ _BASIC_PROVIDER_CONFIG_KEYS = {'provider', 'api_key', 'custom_host', 'url_to_fet
10
10
 
11
11
  PROVIDER_EXTRA_PROPERTIES_LUT = {
12
12
  ModelProvider.ANTHROPIC: {'anthropic_beta', 'anthropic_version'},
13
- # ModelProvider.AZURE_AI: {
14
- # 'azure_resource_name',
15
- # 'azure_deployment_id',
16
- # 'azure_api_version',
17
- # 'ad_auth',
18
- # 'azure_auth_mode',
19
- # 'azure_managed_client_id',
20
- # 'azure_entra_client_id',
21
- # 'azure_entra_client_secret',
22
- # 'azure_entra_tenant_id',
23
- # 'azure_ad_token',
24
- # 'azure_model_name'
25
- # },
13
+ ModelProvider.AZURE_AI: {
14
+ 'azure_resource_name',
15
+ 'azure_deployment_id',
16
+ 'azure_api_version',
17
+ 'ad_auth',
18
+ 'azure_auth_mode',
19
+ 'azure_managed_client_id',
20
+ 'azure_entra_client_id',
21
+ 'azure_entra_client_secret',
22
+ 'azure_entra_tenant_id',
23
+ 'azure_ad_token',
24
+ 'azure_model_name',
25
+ 'azure_inference_deployment_name',
26
+ 'azure_inference_api_version',
27
+ 'azure_inference_extra_params',
28
+ 'azure_inference_foundry_url'
29
+ },
26
30
  ModelProvider.AZURE_OPENAI: {
27
31
  'azure_resource_name',
28
32
  'azure_deployment_id',
@@ -370,9 +370,13 @@ def get_persisted_user_env() -> dict | None:
370
370
  user_env = cfg.get(USER_ENV_CACHE_HEADER) if cfg.get(USER_ENV_CACHE_HEADER) else None
371
371
  return user_env
372
372
 
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
-
373
+ def run_compose_lite(
374
+ final_env_file: Path,
375
+ experimental_with_langfuse=False,
376
+ experimental_with_ibm_telemetry=False,
377
+ with_doc_processing=False,
378
+ with_voice=False
379
+ ) -> None:
376
380
  compose_path = get_compose_file()
377
381
 
378
382
  compose_command = ensure_docker_compose_installed()
@@ -408,6 +412,8 @@ def run_compose_lite(final_env_file: Path, experimental_with_langfuse=False, exp
408
412
  profiles.append("ibm-telemetry")
409
413
  if with_doc_processing:
410
414
  profiles.append("docproc")
415
+ if with_voice:
416
+ profiles.append("voice")
411
417
 
412
418
  command = compose_command[:]
413
419
  for profile in profiles:
@@ -855,6 +861,11 @@ def server_start(
855
861
  '--compose-file', '-f',
856
862
  help='Provide the path to a custom docker-compose file to use instead of the default compose file'
857
863
  ),
864
+ with_voice: bool = typer.Option(
865
+ False,
866
+ '--with-voice', '-v',
867
+ help='Enable voice controller to interact with the chat via voice channels'
868
+ ),
858
869
  ):
859
870
  confirm_accepts_license_agreement(accept_terms_and_conditions)
860
871
 
@@ -896,6 +907,7 @@ def server_start(
896
907
  if experimental_with_ibm_telemetry:
897
908
  merged_env_dict['USE_IBM_TELEMETRY'] = 'true'
898
909
 
910
+
899
911
  try:
900
912
  dev_edition_source = get_dev_edition_source(merged_env_dict)
901
913
  docker_login_by_dev_edition_source(merged_env_dict, dev_edition_source)
@@ -908,7 +920,8 @@ def server_start(
908
920
  run_compose_lite(final_env_file=final_env_file,
909
921
  experimental_with_langfuse=experimental_with_langfuse,
910
922
  experimental_with_ibm_telemetry=experimental_with_ibm_telemetry,
911
- with_doc_processing=with_doc_processing)
923
+ with_doc_processing=with_doc_processing,
924
+ with_voice=with_voice)
912
925
 
913
926
  run_db_migration()
914
927
 
@@ -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":
@@ -590,7 +595,7 @@ class ToolsController:
590
595
  for tool in tools:
591
596
  tools_list.append(json.loads(tool.dumps_spec()))
592
597
 
593
- rich.print(JSON(json.dumps(tools_list, indent=4)))
598
+ rich.print_json(json.dumps(tools_list, indent=4))
594
599
  else:
595
600
  table = rich.table.Table(show_header=True, header_style="bold white", show_lines=True)
596
601
  column_args = {
@@ -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
+
@@ -16,6 +16,7 @@ from ibm_watsonx_orchestrate.cli.commands.knowledge_bases.knowledge_bases_comman
16
16
  from ibm_watsonx_orchestrate.cli.commands.toolkit.toolkit_command import toolkits_app
17
17
  from ibm_watsonx_orchestrate.cli.commands.evaluations.evaluations_command import evaluation_app
18
18
  from ibm_watsonx_orchestrate.cli.commands.copilot.copilot_command import copilot_app
19
+ from ibm_watsonx_orchestrate.cli.commands.voice_configurations.voice_configurations_command import voice_configurations_app
19
20
  from ibm_watsonx_orchestrate.cli.init_helper import init_callback
20
21
 
21
22
  import urllib3
@@ -35,6 +36,7 @@ app.add_typer(tools_app, name="tools", help='Interact with the tools in your act
35
36
  app.add_typer(toolkits_app, name="toolkits", help="Interact with the toolkits in your active env")
36
37
  app.add_typer(knowledge_bases_app, name="knowledge-bases", help="Upload knowledge your agents can search through to your active env")
37
38
  app.add_typer(connections_app, name="connections", help='Interact with the agents in your active env')
39
+ app.add_typer(voice_configurations_app, name="voice-configs", help="Configure voice providers to enable voice interaction with your agents")
38
40
  app.add_typer(server_app, name="server", help='Manipulate your local Orchestrate Developer Edition server [requires entitlement]')
39
41
  app.add_typer(chat_app, name="chat", help='Launch the chat ui for your local Developer Edition server [requires entitlement]')
40
42
  app.add_typer(models_app, name="models", help='List the available large language models (llms) that can be used in your agent definitions')