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.
- ibm_watsonx_orchestrate/__init__.py +2 -1
- ibm_watsonx_orchestrate/agent_builder/agents/types.py +2 -0
- ibm_watsonx_orchestrate/agent_builder/connections/__init__.py +1 -1
- ibm_watsonx_orchestrate/agent_builder/connections/connections.py +1 -1
- ibm_watsonx_orchestrate/agent_builder/connections/types.py +16 -12
- ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +47 -3
- ibm_watsonx_orchestrate/agent_builder/toolkits/types.py +18 -15
- ibm_watsonx_orchestrate/agent_builder/tools/python_tool.py +19 -7
- ibm_watsonx_orchestrate/agent_builder/tools/types.py +1 -1
- ibm_watsonx_orchestrate/agent_builder/voice_configurations/__init__.py +1 -0
- ibm_watsonx_orchestrate/agent_builder/voice_configurations/types.py +98 -0
- ibm_watsonx_orchestrate/cli/commands/agents/agents_command.py +20 -0
- ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +170 -1
- ibm_watsonx_orchestrate/cli/commands/connections/connections_command.py +7 -7
- ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +36 -26
- ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +51 -22
- ibm_watsonx_orchestrate/cli/commands/server/server_command.py +110 -16
- ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_command.py +43 -10
- ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_controller.py +52 -25
- ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +5 -0
- ibm_watsonx_orchestrate/cli/commands/voice_configurations/voice_configurations_command.py +58 -0
- ibm_watsonx_orchestrate/cli/commands/voice_configurations/voice_configurations_controller.py +173 -0
- ibm_watsonx_orchestrate/cli/main.py +2 -0
- ibm_watsonx_orchestrate/client/agents/agent_client.py +64 -1
- ibm_watsonx_orchestrate/client/connections/connections_client.py +4 -3
- ibm_watsonx_orchestrate/client/knowledge_bases/knowledge_base_client.py +4 -4
- ibm_watsonx_orchestrate/client/voice_configurations/voice_configurations_client.py +75 -0
- ibm_watsonx_orchestrate/docker/compose-lite.yml +54 -5
- ibm_watsonx_orchestrate/docker/default.env +21 -13
- ibm_watsonx_orchestrate/flow_builder/flows/__init__.py +2 -0
- ibm_watsonx_orchestrate/flow_builder/flows/flow.py +115 -31
- ibm_watsonx_orchestrate/flow_builder/node.py +39 -15
- ibm_watsonx_orchestrate/flow_builder/types.py +114 -25
- ibm_watsonx_orchestrate/run/connections.py +2 -2
- {ibm_watsonx_orchestrate-1.9.0b2.dist-info → ibm_watsonx_orchestrate-1.10.0b1.dist-info}/METADATA +1 -1
- {ibm_watsonx_orchestrate-1.9.0b2.dist-info → ibm_watsonx_orchestrate-1.10.0b1.dist-info}/RECORD +39 -34
- {ibm_watsonx_orchestrate-1.9.0b2.dist-info → ibm_watsonx_orchestrate-1.10.0b1.dist-info}/WHEEL +0 -0
- {ibm_watsonx_orchestrate-1.9.0b2.dist-info → ibm_watsonx_orchestrate-1.10.0b1.dist-info}/entry_points.txt +0 -0
- {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(
|
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:
|
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
|
-
|
1023
|
-
|
1024
|
-
|
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
|
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
|
68
|
-
|
69
|
-
|
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
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
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
|
36
|
-
|
37
|
-
|
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
|
-
|
99
|
-
command_parts =
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
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
|
-
|
155
|
-
|
156
|
-
|
157
|
-
"
|
158
|
-
"
|
159
|
-
"
|
160
|
-
|
161
|
-
|
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
|
-
|
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
|
+
|