ibm-watsonx-orchestrate 1.9.0.dev0__py3-none-any.whl → 1.10.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.
- ibm_watsonx_orchestrate/__init__.py +1 -2
- 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 +45 -2
- ibm_watsonx_orchestrate/agent_builder/toolkits/types.py +18 -15
- ibm_watsonx_orchestrate/agent_builder/tools/python_tool.py +1 -1
- 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 +53 -5
- ibm_watsonx_orchestrate/docker/default.env +22 -14
- 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.0.dev0.dist-info → ibm_watsonx_orchestrate-1.10.0.dist-info}/METADATA +1 -1
- {ibm_watsonx_orchestrate-1.9.0.dev0.dist-info → ibm_watsonx_orchestrate-1.10.0.dist-info}/RECORD +39 -34
- {ibm_watsonx_orchestrate-1.9.0.dev0.dist-info → ibm_watsonx_orchestrate-1.10.0.dist-info}/WHEEL +0 -0
- {ibm_watsonx_orchestrate-1.9.0.dev0.dist-info → ibm_watsonx_orchestrate-1.10.0.dist-info}/entry_points.txt +0 -0
- {ibm_watsonx_orchestrate-1.9.0.dev0.dist-info → ibm_watsonx_orchestrate-1.10.0.dist-info}/licenses/LICENSE +0 -0
@@ -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
|
+
|
@@ -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')
|
@@ -1,7 +1,26 @@
|
|
1
1
|
from ibm_watsonx_orchestrate.client.base_api_client import BaseAPIClient, ClientAPIException
|
2
2
|
from typing_extensions import List, Optional
|
3
|
+
from enum import Enum
|
4
|
+
|
3
5
|
from ibm_watsonx_orchestrate.client.utils import is_local_dev
|
4
6
|
from pydantic import BaseModel
|
7
|
+
import time
|
8
|
+
import logging
|
9
|
+
|
10
|
+
logger = logging.getLogger(__name__)
|
11
|
+
|
12
|
+
POLL_INTERVAL = 2
|
13
|
+
MAX_RETRIES = 10
|
14
|
+
|
15
|
+
class ReleaseMode(str, Enum):
|
16
|
+
DEPLOY = "deploy"
|
17
|
+
UNDEPLOY = "undeploy"
|
18
|
+
|
19
|
+
class ReleaseStatus(str, Enum):
|
20
|
+
SUCCESS = "success"
|
21
|
+
NONE = "none"
|
22
|
+
FAILED = "failed"
|
23
|
+
IN_PROGRESS = "in_progress"
|
5
24
|
|
6
25
|
def transform_agents_from_flat_agent_spec(agents: dict | list[dict] ) -> dict | list[dict]:
|
7
26
|
if isinstance(agents,list):
|
@@ -83,7 +102,6 @@ class AgentClient(BaseAPIClient):
|
|
83
102
|
super().__init__(*args, **kwargs)
|
84
103
|
self.base_endpoint = "/orchestrate/agents" if is_local_dev(self.base_url) else "/agents"
|
85
104
|
|
86
|
-
|
87
105
|
def create(self, payload: dict) -> AgentUpsertResponse:
|
88
106
|
response = self._post(self.base_endpoint, data=transform_agents_from_flat_agent_spec(payload))
|
89
107
|
return AgentUpsertResponse.model_validate(response)
|
@@ -120,4 +138,49 @@ class AgentClient(BaseAPIClient):
|
|
120
138
|
def get_drafts_by_ids(self, agent_ids: List[str]) -> List[dict]:
|
121
139
|
formatted_agent_ids = [f"ids={x}" for x in agent_ids]
|
122
140
|
return transform_agents_to_flat_agent_spec(self._get(f"{self.base_endpoint}?{'&'.join(formatted_agent_ids)}&include_hidden=true"))
|
141
|
+
|
142
|
+
def poll_release_status(self, agent_id: str, environment_id: str, mode: str = "deploy") -> bool:
|
143
|
+
expected_status = {
|
144
|
+
ReleaseMode.DEPLOY: ReleaseStatus.SUCCESS,
|
145
|
+
ReleaseMode.UNDEPLOY: ReleaseStatus.NONE
|
146
|
+
}[mode]
|
147
|
+
|
148
|
+
for attempt in range(MAX_RETRIES):
|
149
|
+
try:
|
150
|
+
response = self._get(
|
151
|
+
f"{self.base_endpoint}/{agent_id}/releases/status?environment_id={environment_id}"
|
152
|
+
)
|
153
|
+
except Exception as e:
|
154
|
+
logger.error(f"Polling for Deployment/Undeployment failed on attempt {attempt + 1}: {e}")
|
155
|
+
return False
|
156
|
+
|
157
|
+
if not isinstance(response, dict):
|
158
|
+
logger.warning(f"Invalid response format: {response}")
|
159
|
+
return False
|
160
|
+
|
161
|
+
status = response.get("deployment_status")
|
162
|
+
|
163
|
+
if status == expected_status:
|
164
|
+
return True
|
165
|
+
elif status == "failed":
|
166
|
+
return False
|
167
|
+
elif status == "in_progress":
|
168
|
+
pass
|
169
|
+
|
170
|
+
time.sleep(POLL_INTERVAL)
|
171
|
+
|
172
|
+
logger.warning(f"{mode.capitalize()} status polling timed out")
|
173
|
+
return False
|
174
|
+
|
175
|
+
def deploy(self, agent_id: str, environment_id: str) -> bool:
|
176
|
+
self._post(f"{self.base_endpoint}/{agent_id}/releases", data={"environment_id": environment_id})
|
177
|
+
return self.poll_release_status(agent_id, environment_id, mode=ReleaseMode.DEPLOY)
|
178
|
+
|
179
|
+
def undeploy(self, agent_id: str, version: str, environment_id: str) -> bool:
|
180
|
+
self._post(f"{self.base_endpoint}/{agent_id}/releases/{version}/undeploy")
|
181
|
+
return self.poll_release_status(agent_id, environment_id, mode=ReleaseMode.UNDEPLOY)
|
182
|
+
|
183
|
+
def get_environments_for_agent(self, agent_id: str):
|
184
|
+
return self._get(f"{self.base_endpoint}/{agent_id}/environment")
|
185
|
+
|
123
186
|
|
@@ -19,7 +19,7 @@ class FetchConfigAuthTypes(str, Enum):
|
|
19
19
|
API_KEY_AUTH = ConnectionType.API_KEY_AUTH.value
|
20
20
|
OAUTH2_AUTH_CODE = ConnectionType.OAUTH2_AUTH_CODE.value
|
21
21
|
OAUTH2_IMPLICIT = 'oauth2_implicit'
|
22
|
-
OAUTH2_PASSWORD =
|
22
|
+
OAUTH2_PASSWORD = ConnectionType.OAUTH2_PASSWORD.value
|
23
23
|
OAUTH2_CLIENT_CREDS = ConnectionType.OAUTH2_CLIENT_CREDS.value
|
24
24
|
OAUTH_ON_BEHALF_OF_FLOW = ConnectionType.OAUTH_ON_BEHALF_OF_FLOW.value
|
25
25
|
KEY_VALUE = ConnectionType.KEY_VALUE.value
|
@@ -50,7 +50,7 @@ class GetConfigResponse(BaseModel):
|
|
50
50
|
class GetConnectionResponse(BaseModel):
|
51
51
|
connection_id: str = None
|
52
52
|
app_id: str = None
|
53
|
-
tenant_id: str = None
|
53
|
+
tenant_id: Optional[str] = None
|
54
54
|
|
55
55
|
|
56
56
|
|
@@ -141,7 +141,8 @@ class ConnectionsClient(BaseAPIClient):
|
|
141
141
|
else:
|
142
142
|
return self._get(f"/connections/applications/runtime_credentials?app_id={app_id}&env={env}")
|
143
143
|
except ClientAPIException as e:
|
144
|
-
|
144
|
+
# Returns 400 when app creds exist but runtime cred don't yet exist
|
145
|
+
if e.response.status_code == 404 or e.response.status_code == 400:
|
145
146
|
return None
|
146
147
|
raise e
|
147
148
|
|
@@ -15,10 +15,10 @@ class KnowledgeBaseClient(BaseAPIClient):
|
|
15
15
|
self.base_endpoint = "/orchestrate/knowledge-bases" if is_local_dev(self.base_url) else "/knowledge-bases"
|
16
16
|
|
17
17
|
def create(self, payload: dict) -> dict:
|
18
|
-
return self._post_form_data(f"{self.base_endpoint}/documents", data=
|
18
|
+
return self._post_form_data(f"{self.base_endpoint}/documents", data=payload)
|
19
19
|
|
20
20
|
def create_built_in(self, payload: dict, files: list) -> dict:
|
21
|
-
return self._post_form_data(f"{self.base_endpoint}/documents", data=
|
21
|
+
return self._post_form_data(f"{self.base_endpoint}/documents", data=payload, files=files)
|
22
22
|
|
23
23
|
def get(self) -> dict:
|
24
24
|
return self._get(self.base_endpoint)
|
@@ -38,10 +38,10 @@ class KnowledgeBaseClient(BaseAPIClient):
|
|
38
38
|
return self._get(f"{self.base_endpoint}/{knowledge_base_id}/status")
|
39
39
|
|
40
40
|
def update(self, knowledge_base_id: str, payload: dict) -> dict:
|
41
|
-
return self._patch_form_data(f"{self.base_endpoint}/{knowledge_base_id}/documents", data=
|
41
|
+
return self._patch_form_data(f"{self.base_endpoint}/{knowledge_base_id}/documents", data=payload)
|
42
42
|
|
43
43
|
def update_with_documents(self, knowledge_base_id: str, payload: dict, files: list) -> dict:
|
44
|
-
return self._patch_form_data(f"{self.base_endpoint}/{knowledge_base_id}/documents", data=
|
44
|
+
return self._patch_form_data(f"{self.base_endpoint}/{knowledge_base_id}/documents", data=payload, files=files)
|
45
45
|
|
46
46
|
def delete(self, knowledge_base_id: str,) -> dict:
|
47
47
|
return self._delete(f"{self.base_endpoint}/{knowledge_base_id}")
|
@@ -0,0 +1,75 @@
|
|
1
|
+
from pydantic import ValidationError
|
2
|
+
from ibm_watsonx_orchestrate.agent_builder.voice_configurations import VoiceConfiguration
|
3
|
+
from ibm_watsonx_orchestrate.client.base_api_client import BaseAPIClient, ClientAPIException
|
4
|
+
from ibm_watsonx_orchestrate.client.client_errors import MissingArgument
|
5
|
+
|
6
|
+
import logging
|
7
|
+
logger = logging.getLogger(__name__)
|
8
|
+
|
9
|
+
class VoiceConfigurationsClient(BaseAPIClient):
|
10
|
+
|
11
|
+
def create(self, voice_config: VoiceConfiguration) -> dict:
|
12
|
+
return self._post("/voice_configurations", data=voice_config.model_dump(exclude_none=True))
|
13
|
+
|
14
|
+
|
15
|
+
def update(self, voice_config_id: str, voice_config: VoiceConfiguration) -> dict:
|
16
|
+
if voice_config_id in [None,""]:
|
17
|
+
raise MissingArgument("voice_config_id")
|
18
|
+
return self._patch(f"/voice_configurations/{voice_config_id}", data=voice_config.model_dump(exclude_none=True))
|
19
|
+
|
20
|
+
|
21
|
+
def get_by_id(self, voice_config_id: str) -> dict | None:
|
22
|
+
if voice_config_id in [None,""]:
|
23
|
+
raise MissingArgument("voice_config_id")
|
24
|
+
|
25
|
+
try:
|
26
|
+
response = self._get(f"/voice_configurations/{voice_config_id}")
|
27
|
+
return VoiceConfiguration.model_validate(response)
|
28
|
+
|
29
|
+
except ClientAPIException as e:
|
30
|
+
if e.response.status_code == 404:
|
31
|
+
return None
|
32
|
+
raise e
|
33
|
+
|
34
|
+
except ValidationError as e:
|
35
|
+
logger.error("Recieved unexpected response from server")
|
36
|
+
raise e
|
37
|
+
|
38
|
+
def get_by_name(self, name: str) -> list[VoiceConfiguration]:
|
39
|
+
return self.get_by_names([name])
|
40
|
+
|
41
|
+
def get_by_names(self, names: list[str]) -> list[VoiceConfiguration]:
|
42
|
+
# server will implement query by name, below can be uncommented then
|
43
|
+
# formatted_config_names = [f"names={n}" for n in names]
|
44
|
+
# return self._get(f"/voice_configurations?{"&".join(formatted_config_names)}")
|
45
|
+
config_list = self.list()
|
46
|
+
filtered_list = [cfg for cfg in config_list if cfg.name in names]
|
47
|
+
|
48
|
+
try:
|
49
|
+
return [ VoiceConfiguration.model_validate(cfg) for cfg in filtered_list ]
|
50
|
+
except ValidationError as e:
|
51
|
+
logger.error("Recieved unexpected response from server")
|
52
|
+
raise e
|
53
|
+
|
54
|
+
|
55
|
+
|
56
|
+
|
57
|
+
def delete(self, voice_config_id: str) -> None:
|
58
|
+
if voice_config_id in [None,""]:
|
59
|
+
raise MissingArgument("voice_config_id")
|
60
|
+
self._delete(f"/voice_configurations/{voice_config_id}")
|
61
|
+
|
62
|
+
|
63
|
+
def list(self) -> list[dict]:
|
64
|
+
try:
|
65
|
+
response = self._get("/voice_configurations")
|
66
|
+
return [VoiceConfiguration.model_validate(x) for x in response.get('voice_configurations',[])]
|
67
|
+
|
68
|
+
except ClientAPIException as e:
|
69
|
+
if e.response.status_code == 404:
|
70
|
+
return []
|
71
|
+
raise e
|
72
|
+
|
73
|
+
except ValidationError as e:
|
74
|
+
logger.error("Recieved unexpected response from server")
|
75
|
+
raise e
|