ibm-watsonx-orchestrate 1.11.0b0__py3-none-any.whl → 1.12.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.
- ibm_watsonx_orchestrate/__init__.py +1 -1
- ibm_watsonx_orchestrate/agent_builder/agents/types.py +30 -0
- ibm_watsonx_orchestrate/agent_builder/connections/connections.py +8 -5
- ibm_watsonx_orchestrate/agent_builder/connections/types.py +14 -0
- ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +25 -10
- ibm_watsonx_orchestrate/agent_builder/tools/__init__.py +1 -0
- ibm_watsonx_orchestrate/agent_builder/tools/langflow_tool.py +124 -0
- ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +3 -3
- ibm_watsonx_orchestrate/agent_builder/tools/types.py +20 -2
- ibm_watsonx_orchestrate/cli/commands/agents/agents_command.py +10 -2
- ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +421 -177
- ibm_watsonx_orchestrate/cli/commands/connections/connections_command.py +18 -0
- ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +114 -0
- ibm_watsonx_orchestrate/cli/commands/copilot/copilot_server_controller.py +24 -91
- ibm_watsonx_orchestrate/cli/commands/environment/environment_command.py +1 -1
- ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_command.py +223 -2
- ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_controller.py +93 -9
- ibm_watsonx_orchestrate/cli/commands/models/models_controller.py +3 -3
- ibm_watsonx_orchestrate/cli/commands/partners/offering/partners_offering_command.py +56 -0
- ibm_watsonx_orchestrate/cli/commands/partners/offering/partners_offering_controller.py +458 -0
- ibm_watsonx_orchestrate/cli/commands/partners/offering/types.py +107 -0
- ibm_watsonx_orchestrate/cli/commands/partners/partners_command.py +12 -0
- ibm_watsonx_orchestrate/cli/commands/partners/partners_controller.py +0 -0
- ibm_watsonx_orchestrate/cli/commands/server/server_command.py +114 -635
- ibm_watsonx_orchestrate/cli/commands/server/types.py +1 -1
- ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_controller.py +2 -2
- ibm_watsonx_orchestrate/cli/commands/tools/tools_command.py +2 -3
- ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +206 -43
- ibm_watsonx_orchestrate/cli/main.py +2 -0
- ibm_watsonx_orchestrate/client/base_api_client.py +31 -10
- ibm_watsonx_orchestrate/client/connections/connections_client.py +18 -1
- ibm_watsonx_orchestrate/client/service_instance.py +19 -34
- ibm_watsonx_orchestrate/client/tools/tempus_client.py +3 -0
- ibm_watsonx_orchestrate/client/tools/tool_client.py +5 -2
- ibm_watsonx_orchestrate/client/utils.py +34 -2
- ibm_watsonx_orchestrate/docker/compose-lite.yml +14 -12
- ibm_watsonx_orchestrate/docker/default.env +17 -17
- ibm_watsonx_orchestrate/flow_builder/flows/flow.py +3 -1
- ibm_watsonx_orchestrate/flow_builder/types.py +252 -1
- ibm_watsonx_orchestrate/utils/docker_utils.py +280 -0
- ibm_watsonx_orchestrate/utils/environment.py +369 -0
- ibm_watsonx_orchestrate/utils/utils.py +1 -1
- {ibm_watsonx_orchestrate-1.11.0b0.dist-info → ibm_watsonx_orchestrate-1.12.0b0.dist-info}/METADATA +2 -2
- {ibm_watsonx_orchestrate-1.11.0b0.dist-info → ibm_watsonx_orchestrate-1.12.0b0.dist-info}/RECORD +47 -39
- {ibm_watsonx_orchestrate-1.11.0b0.dist-info → ibm_watsonx_orchestrate-1.12.0b0.dist-info}/WHEEL +0 -0
- {ibm_watsonx_orchestrate-1.11.0b0.dist-info → ibm_watsonx_orchestrate-1.12.0b0.dist-info}/entry_points.txt +0 -0
- {ibm_watsonx_orchestrate-1.11.0b0.dist-info → ibm_watsonx_orchestrate-1.12.0b0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,280 @@
|
|
1
|
+
import logging
|
2
|
+
import os
|
3
|
+
import subprocess
|
4
|
+
import sys
|
5
|
+
from enum import Enum
|
6
|
+
from pathlib import Path
|
7
|
+
from typing import MutableMapping
|
8
|
+
from urllib.parse import urlparse
|
9
|
+
|
10
|
+
import requests
|
11
|
+
import typer
|
12
|
+
|
13
|
+
from ibm_watsonx_orchestrate.cli.config import Config
|
14
|
+
from ibm_watsonx_orchestrate.utils.environment import EnvService
|
15
|
+
|
16
|
+
logger = logging.getLogger(__name__)
|
17
|
+
|
18
|
+
|
19
|
+
class DockerUtils:
|
20
|
+
|
21
|
+
@staticmethod
|
22
|
+
def ensure_docker_installed () -> None:
|
23
|
+
try:
|
24
|
+
subprocess.run(["docker", "--version"], check=True, capture_output=True)
|
25
|
+
except (FileNotFoundError, subprocess.CalledProcessError):
|
26
|
+
logger.error("Unable to find an installed docker")
|
27
|
+
sys.exit(1)
|
28
|
+
|
29
|
+
@staticmethod
|
30
|
+
def check_exclusive_observability(langfuse_enabled: bool, ibm_tele_enabled: bool):
|
31
|
+
if langfuse_enabled and ibm_tele_enabled:
|
32
|
+
return False
|
33
|
+
if langfuse_enabled and DockerUtils.__is_docker_container_running("docker-frontend-server-1"):
|
34
|
+
return False
|
35
|
+
if ibm_tele_enabled and DockerUtils.__is_docker_container_running("docker-langfuse-web-1"):
|
36
|
+
return False
|
37
|
+
return True
|
38
|
+
|
39
|
+
@staticmethod
|
40
|
+
def __is_docker_container_running(container_name):
|
41
|
+
DockerUtils.ensure_docker_installed()
|
42
|
+
command = ["docker",
|
43
|
+
"ps",
|
44
|
+
"-f",
|
45
|
+
f"name={container_name}"
|
46
|
+
]
|
47
|
+
result = subprocess.run(command, env=os.environ, capture_output=True)
|
48
|
+
if container_name in str(result.stdout):
|
49
|
+
return True
|
50
|
+
return False
|
51
|
+
|
52
|
+
|
53
|
+
class DockerLoginService:
|
54
|
+
|
55
|
+
def __init__(self, env_service: EnvService):
|
56
|
+
self.__env_service = env_service
|
57
|
+
|
58
|
+
def login_by_dev_edition_source(self, env_dict: dict) -> None:
|
59
|
+
source = self.__env_service.get_dev_edition_source_core(env_dict=env_dict)
|
60
|
+
|
61
|
+
if env_dict.get('WO_DEVELOPER_EDITION_SKIP_LOGIN', None) == 'true':
|
62
|
+
logger.info('WO_DEVELOPER_EDITION_SKIP_LOGIN is set to true, skipping login.')
|
63
|
+
logger.warning('If the developer edition images are not already pulled this call will fail without first setting WO_DEVELOPER_EDITION_SKIP_LOGIN=false')
|
64
|
+
else:
|
65
|
+
if not env_dict.get("REGISTRY_URL"):
|
66
|
+
raise ValueError("REGISTRY_URL is not set.")
|
67
|
+
registry_url = env_dict["REGISTRY_URL"].split("/")[0]
|
68
|
+
if source == "internal":
|
69
|
+
iam_api_key = env_dict.get("DOCKER_IAM_KEY")
|
70
|
+
if not iam_api_key:
|
71
|
+
raise ValueError(
|
72
|
+
"DOCKER_IAM_KEY is required in the environment file if WO_DEVELOPER_EDITION_SOURCE is set to 'internal'.")
|
73
|
+
self.__docker_login(iam_api_key, registry_url, "iamapikey")
|
74
|
+
elif source == "myibm":
|
75
|
+
wo_entitlement_key = env_dict.get("WO_ENTITLEMENT_KEY")
|
76
|
+
if not wo_entitlement_key:
|
77
|
+
raise ValueError("WO_ENTITLEMENT_KEY is required in the environment file.")
|
78
|
+
self.__docker_login(wo_entitlement_key, registry_url, "cp")
|
79
|
+
elif source == "orchestrate":
|
80
|
+
wo_auth_type = env_dict.get("WO_AUTH_TYPE")
|
81
|
+
api_key, username = self.__get_docker_cred_by_wo_auth_type(auth_type=wo_auth_type, env_dict=env_dict)
|
82
|
+
self.__docker_login(api_key, registry_url, username)
|
83
|
+
|
84
|
+
@staticmethod
|
85
|
+
def __docker_login(api_key: str, registry_url: str, username: str = "iamapikey") -> None:
|
86
|
+
logger.info(f"Logging into Docker registry: {registry_url} ...")
|
87
|
+
result = subprocess.run(
|
88
|
+
["docker", "login", "-u", username, "--password-stdin", registry_url],
|
89
|
+
input=api_key.encode("utf-8"),
|
90
|
+
capture_output=True,
|
91
|
+
)
|
92
|
+
if result.returncode != 0:
|
93
|
+
logger.error(f"Error logging into Docker:\n{result.stderr.decode('utf-8')}")
|
94
|
+
sys.exit(1)
|
95
|
+
logger.info("Successfully logged in to Docker.")
|
96
|
+
|
97
|
+
@staticmethod
|
98
|
+
def __get_docker_cred_by_wo_auth_type(auth_type: str | None, env_dict: dict) -> tuple[str, str]:
|
99
|
+
# Try infer the auth type if not provided
|
100
|
+
if not auth_type:
|
101
|
+
instance_url = env_dict.get("WO_INSTANCE")
|
102
|
+
if instance_url:
|
103
|
+
if ".cloud.ibm.com" in instance_url:
|
104
|
+
auth_type = "ibm_iam"
|
105
|
+
elif ".ibm.com" in instance_url:
|
106
|
+
auth_type = "mcsp"
|
107
|
+
elif "https://cpd" in instance_url:
|
108
|
+
auth_type = "cpd"
|
109
|
+
|
110
|
+
if auth_type in {"mcsp", "ibm_iam"}:
|
111
|
+
wo_api_key = env_dict.get("WO_API_KEY")
|
112
|
+
if not wo_api_key:
|
113
|
+
raise ValueError(
|
114
|
+
"WO_API_KEY is required in the environment file if the WO_AUTH_TYPE is set to 'mcsp' or 'ibm_iam'.")
|
115
|
+
instance_url = env_dict.get("WO_INSTANCE")
|
116
|
+
if not instance_url:
|
117
|
+
raise ValueError(
|
118
|
+
"WO_INSTANCE is required in the environment file if the WO_AUTH_TYPE is set to 'mcsp' or 'ibm_iam'.")
|
119
|
+
path = urlparse(instance_url).path
|
120
|
+
if not path or '/' not in path:
|
121
|
+
raise ValueError(
|
122
|
+
f"Invalid WO_INSTANCE URL: '{instance_url}'. It should contain the instance (tenant) id.")
|
123
|
+
tenant_id = path.split('/')[-1]
|
124
|
+
return wo_api_key, f"wxouser-{tenant_id}"
|
125
|
+
elif auth_type == "cpd":
|
126
|
+
wo_api_key = env_dict.get("WO_API_KEY")
|
127
|
+
wo_password = env_dict.get("WO_PASSWORD")
|
128
|
+
if not wo_api_key and not wo_password:
|
129
|
+
raise ValueError(
|
130
|
+
"WO_API_KEY or WO_PASSWORD is required in the environment file if the WO_AUTH_TYPE is set to 'cpd'.")
|
131
|
+
wo_username = env_dict.get("WO_USERNAME")
|
132
|
+
if not wo_username:
|
133
|
+
raise ValueError("WO_USERNAME is required in the environment file if the WO_AUTH_TYPE is set to 'cpd'.")
|
134
|
+
return wo_api_key or wo_password, wo_username # type: ignore[return-value]
|
135
|
+
else:
|
136
|
+
raise ValueError(
|
137
|
+
f"Unknown value for WO_AUTH_TYPE: '{auth_type}'. Must be one of ['mcsp', 'ibm_iam', 'cpd'].")
|
138
|
+
|
139
|
+
|
140
|
+
class DockerComposeCore:
|
141
|
+
|
142
|
+
def __init__(self, env_service: EnvService) -> None:
|
143
|
+
self.__env_service = env_service
|
144
|
+
|
145
|
+
def service_up (self, service_name: str, friendly_name: str, final_env_file: Path, compose_env: MutableMapping = None) -> subprocess.CompletedProcess[bytes]:
|
146
|
+
base_command = self.__ensure_docker_compose_installed()
|
147
|
+
compose_path = self.__env_service.get_compose_file()
|
148
|
+
|
149
|
+
command = base_command + [
|
150
|
+
"-f", str(compose_path),
|
151
|
+
"--env-file", str(final_env_file),
|
152
|
+
"up",
|
153
|
+
service_name,
|
154
|
+
"-d",
|
155
|
+
"--remove-orphans"
|
156
|
+
]
|
157
|
+
|
158
|
+
kwargs = {}
|
159
|
+
if compose_env is not None:
|
160
|
+
kwargs["env"] = compose_env
|
161
|
+
|
162
|
+
logger.info(f"Starting docker-compose {friendly_name} service...")
|
163
|
+
|
164
|
+
return subprocess.run(command, capture_output=False, **kwargs)
|
165
|
+
|
166
|
+
def services_up(self, profiles: list[str], final_env_file: Path, supplementary_compose_args: list[str]) -> subprocess.CompletedProcess[bytes]:
|
167
|
+
compose_path = self.__env_service.get_compose_file()
|
168
|
+
command = self.__ensure_docker_compose_installed()[:]
|
169
|
+
|
170
|
+
for profile in profiles:
|
171
|
+
command += ["--profile", profile]
|
172
|
+
|
173
|
+
compose_args = [
|
174
|
+
"-f", str(compose_path),
|
175
|
+
"--env-file", str(final_env_file),
|
176
|
+
"up"
|
177
|
+
]
|
178
|
+
|
179
|
+
for arg in supplementary_compose_args:
|
180
|
+
compose_args.append(arg)
|
181
|
+
|
182
|
+
compose_args.append("-d")
|
183
|
+
compose_args.append("--remove-orphans")
|
184
|
+
|
185
|
+
command += compose_args
|
186
|
+
|
187
|
+
logger.info("Starting docker-compose services...")
|
188
|
+
return subprocess.run(command, capture_output=False)
|
189
|
+
|
190
|
+
def service_down (self, service_name: str, friendly_name: str, final_env_file: Path, is_reset: bool = False) -> subprocess.CompletedProcess[bytes]:
|
191
|
+
base_command = self.__ensure_docker_compose_installed()
|
192
|
+
compose_path = self.__env_service.get_compose_file()
|
193
|
+
|
194
|
+
command = base_command + [
|
195
|
+
"-f", str(compose_path),
|
196
|
+
"--env-file", str(final_env_file),
|
197
|
+
"down",
|
198
|
+
service_name
|
199
|
+
]
|
200
|
+
|
201
|
+
if is_reset:
|
202
|
+
command.append("--volumes")
|
203
|
+
logger.info(f"Stopping docker-compose {friendly_name} service and resetting volumes...")
|
204
|
+
|
205
|
+
else:
|
206
|
+
logger.info(f"Stopping docker-compose {friendly_name} service...")
|
207
|
+
|
208
|
+
return subprocess.run(command, capture_output=False)
|
209
|
+
|
210
|
+
def services_down (self, final_env_file: Path, is_reset: bool = False) -> subprocess.CompletedProcess[bytes]:
|
211
|
+
base_command = self.__ensure_docker_compose_installed()
|
212
|
+
compose_path = self.__env_service.get_compose_file()
|
213
|
+
|
214
|
+
command = base_command + [
|
215
|
+
"--profile", "*",
|
216
|
+
"-f", str(compose_path),
|
217
|
+
"--env-file", str(final_env_file),
|
218
|
+
"down"
|
219
|
+
]
|
220
|
+
|
221
|
+
if is_reset:
|
222
|
+
command.append("--volumes")
|
223
|
+
logger.info("Stopping docker-compose service and resetting volumes...")
|
224
|
+
|
225
|
+
else:
|
226
|
+
logger.info("Stopping docker-compose services...")
|
227
|
+
|
228
|
+
return subprocess.run(command, capture_output=False)
|
229
|
+
|
230
|
+
def services_logs (self, final_env_file: Path, should_follow: bool = True) -> subprocess.CompletedProcess[bytes]:
|
231
|
+
compose_path = self.__env_service.get_compose_file()
|
232
|
+
|
233
|
+
command = [
|
234
|
+
"-f", str(compose_path),
|
235
|
+
"--env-file", str(final_env_file),
|
236
|
+
"--profile", "*",
|
237
|
+
"logs"
|
238
|
+
]
|
239
|
+
|
240
|
+
if should_follow is True:
|
241
|
+
command.append("--follow")
|
242
|
+
|
243
|
+
command = self.__ensure_docker_compose_installed() + command
|
244
|
+
|
245
|
+
logger.info("Docker Logs...")
|
246
|
+
return subprocess.run(command, capture_output=False)
|
247
|
+
|
248
|
+
def service_container_bash_exec (self, service_name: str, log_message: str, final_env_file: Path, bash_command: str) -> subprocess.CompletedProcess[bytes]:
|
249
|
+
base_command = self.__ensure_docker_compose_installed()
|
250
|
+
compose_path = self.__env_service.get_compose_file()
|
251
|
+
|
252
|
+
command = base_command + [
|
253
|
+
"-f", str(compose_path),
|
254
|
+
"--env-file", str(final_env_file),
|
255
|
+
"exec",
|
256
|
+
service_name,
|
257
|
+
"bash",
|
258
|
+
"-c",
|
259
|
+
bash_command
|
260
|
+
]
|
261
|
+
|
262
|
+
logger.info(log_message)
|
263
|
+
return subprocess.run(command, capture_output=False)
|
264
|
+
|
265
|
+
@staticmethod
|
266
|
+
def __ensure_docker_compose_installed() -> list:
|
267
|
+
try:
|
268
|
+
subprocess.run(["docker", "compose", "version"], check=True, capture_output=True)
|
269
|
+
return ["docker", "compose"]
|
270
|
+
except (FileNotFoundError, subprocess.CalledProcessError):
|
271
|
+
pass
|
272
|
+
|
273
|
+
try:
|
274
|
+
subprocess.run(["docker-compose", "version"], check=True, capture_output=True)
|
275
|
+
return ["docker-compose"]
|
276
|
+
except (FileNotFoundError, subprocess.CalledProcessError):
|
277
|
+
# NOTE: ideally, typer should be a type that's injected into the constructor but is referenced directly for
|
278
|
+
# the purposes of reporting some info to the user.
|
279
|
+
typer.echo("Unable to find an installed docker-compose or docker compose")
|
280
|
+
sys.exit(1)
|
@@ -0,0 +1,369 @@
|
|
1
|
+
import importlib.resources as resources
|
2
|
+
import logging
|
3
|
+
import os
|
4
|
+
import platform
|
5
|
+
import subprocess
|
6
|
+
import sys
|
7
|
+
import tempfile
|
8
|
+
from pathlib import Path
|
9
|
+
from urllib.parse import urlparse
|
10
|
+
|
11
|
+
from dotenv import dotenv_values
|
12
|
+
|
13
|
+
from ibm_watsonx_orchestrate.cli.commands.server.types import WatsonXAIEnvConfig, ModelGatewayEnvConfig
|
14
|
+
from ibm_watsonx_orchestrate.cli.config import USER_ENV_CACHE_HEADER, Config
|
15
|
+
from ibm_watsonx_orchestrate.client.utils import is_arm_architecture
|
16
|
+
|
17
|
+
logger = logging.getLogger(__name__)
|
18
|
+
|
19
|
+
|
20
|
+
class EnvService:
|
21
|
+
|
22
|
+
__ALWAYS_UNSET: set[str] = {
|
23
|
+
"WO_API_KEY",
|
24
|
+
"WO_INSTANCE",
|
25
|
+
"DOCKER_IAM_KEY",
|
26
|
+
"WO_DEVELOPER_EDITION_SOURCE",
|
27
|
+
"WATSONX_SPACE_ID",
|
28
|
+
"WATSONX_APIKEY",
|
29
|
+
"WO_USERNAME",
|
30
|
+
"WO_PASSWORD",
|
31
|
+
}
|
32
|
+
|
33
|
+
__NON_SECRET_ENV_ITEMS: set[str] = {
|
34
|
+
"WO_DEVELOPER_EDITION_SOURCE",
|
35
|
+
"WO_INSTANCE",
|
36
|
+
"USE_SAAS_ML_TOOLS_RUNTIME",
|
37
|
+
"AUTHORIZATION_URL",
|
38
|
+
"OPENSOURCE_REGISTRY_PROXY",
|
39
|
+
"SAAS_WDU_RUNTIME",
|
40
|
+
"LATEST_ENV_FILE",
|
41
|
+
}
|
42
|
+
|
43
|
+
def __init__ (self, config: Config):
|
44
|
+
self.__config = config
|
45
|
+
|
46
|
+
def get_compose_file (self) -> Path:
|
47
|
+
custom_compose_path = self.__get_compose_file_path()
|
48
|
+
return Path(custom_compose_path) if custom_compose_path else self.__get_default_compose_file()
|
49
|
+
|
50
|
+
def __get_compose_file_path (self) -> str:
|
51
|
+
return self.__config.read(USER_ENV_CACHE_HEADER, "DOCKER_COMPOSE_FILE_PATH")
|
52
|
+
|
53
|
+
@staticmethod
|
54
|
+
def __get_default_compose_file () -> Path:
|
55
|
+
with resources.as_file(
|
56
|
+
resources.files("ibm_watsonx_orchestrate.docker").joinpath("compose-lite.yml")
|
57
|
+
) as compose_file:
|
58
|
+
return compose_file
|
59
|
+
|
60
|
+
@staticmethod
|
61
|
+
def get_default_env_file () -> Path:
|
62
|
+
with resources.as_file(
|
63
|
+
resources.files("ibm_watsonx_orchestrate.docker").joinpath("default.env")
|
64
|
+
) as env_file:
|
65
|
+
return env_file
|
66
|
+
|
67
|
+
@staticmethod
|
68
|
+
def read_env_file (env_path: Path | str) -> dict:
|
69
|
+
return dotenv_values(str(env_path))
|
70
|
+
|
71
|
+
def get_user_env (self, user_env_file: Path | str, fallback_to_persisted_env: bool = True) -> dict:
|
72
|
+
if user_env_file is not None and isinstance(user_env_file, str):
|
73
|
+
user_env_file = Path(user_env_file)
|
74
|
+
|
75
|
+
user_env = self.read_env_file(user_env_file) if user_env_file is not None else {}
|
76
|
+
|
77
|
+
if fallback_to_persisted_env is True and not user_env:
|
78
|
+
user_env = self.__get_persisted_user_env() or {}
|
79
|
+
|
80
|
+
return user_env
|
81
|
+
|
82
|
+
@staticmethod
|
83
|
+
def get_dev_edition_source_core(env_dict: dict | None) -> str:
|
84
|
+
if not env_dict:
|
85
|
+
return "myibm"
|
86
|
+
|
87
|
+
source = env_dict.get("WO_DEVELOPER_EDITION_SOURCE")
|
88
|
+
|
89
|
+
if source:
|
90
|
+
return source
|
91
|
+
if env_dict.get("WO_INSTANCE"):
|
92
|
+
return "orchestrate"
|
93
|
+
return "myibm"
|
94
|
+
|
95
|
+
def get_dev_edition_source(self, user_env_file: str):
|
96
|
+
return self.get_dev_edition_source_core(self.get_user_env(user_env_file))
|
97
|
+
|
98
|
+
@staticmethod
|
99
|
+
def merge_env (default_env_path: Path, user_env_path: Path | None) -> dict:
|
100
|
+
merged = dotenv_values(str(default_env_path))
|
101
|
+
|
102
|
+
if user_env_path is not None:
|
103
|
+
user_env = dotenv_values(str(user_env_path))
|
104
|
+
merged.update(user_env)
|
105
|
+
|
106
|
+
return merged
|
107
|
+
|
108
|
+
@staticmethod
|
109
|
+
def __get_default_registry_env_vars_by_dev_edition_source (default_env: dict, user_env: dict, source: str) -> dict[str, str]:
|
110
|
+
component_registry_var_names = {key for key in default_env if key.endswith("_REGISTRY")} | {'REGISTRY_URL'}
|
111
|
+
|
112
|
+
registry_url = user_env.get("REGISTRY_URL", None)
|
113
|
+
if not registry_url:
|
114
|
+
if source == "internal":
|
115
|
+
registry_url = "us.icr.io/watson-orchestrate-private"
|
116
|
+
elif source == "myibm":
|
117
|
+
registry_url = "cp.icr.io/cp/wxo-lite"
|
118
|
+
elif source == "orchestrate":
|
119
|
+
# extract the hostname from the WO_INSTANCE URL, and replace the "api." prefix with "registry." to construct the registry URL per region
|
120
|
+
wo_url = user_env.get("WO_INSTANCE")
|
121
|
+
|
122
|
+
if not wo_url:
|
123
|
+
raise ValueError(
|
124
|
+
"WO_INSTANCE is required in the environment file if the developer edition source is set to 'orchestrate'.")
|
125
|
+
|
126
|
+
parsed = urlparse(wo_url)
|
127
|
+
hostname = parsed.hostname
|
128
|
+
|
129
|
+
registry_url = f"registry.{hostname[4:]}/cp/wxo-lite"
|
130
|
+
else:
|
131
|
+
raise ValueError(
|
132
|
+
f"Unknown value for developer edition source: {source}. Must be one of ['internal', 'myibm', 'orchestrate']."
|
133
|
+
)
|
134
|
+
|
135
|
+
result = {name: registry_url for name in component_registry_var_names}
|
136
|
+
return result
|
137
|
+
|
138
|
+
@staticmethod
|
139
|
+
def prepare_clean_env (env_file: Path) -> None:
|
140
|
+
"""Remove env vars so terminal definitions don't override"""
|
141
|
+
keys_from_file = set(dotenv_values(str(env_file)).keys())
|
142
|
+
keys_to_unset = keys_from_file | EnvService.__ALWAYS_UNSET
|
143
|
+
for key in keys_to_unset:
|
144
|
+
os.environ.pop(key, None)
|
145
|
+
|
146
|
+
@staticmethod
|
147
|
+
def write_merged_env_file (merged_env: dict, target_path: str = None) -> Path:
|
148
|
+
if target_path:
|
149
|
+
file = open(target_path, "w")
|
150
|
+
else:
|
151
|
+
file = tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".env")
|
152
|
+
|
153
|
+
with file:
|
154
|
+
for key, val in merged_env.items():
|
155
|
+
file.write(f"{key}={val}\n")
|
156
|
+
return Path(file.name)
|
157
|
+
|
158
|
+
def persist_user_env (self, env: dict, include_secrets: bool = False) -> None:
|
159
|
+
if include_secrets:
|
160
|
+
persistable_env = env
|
161
|
+
else:
|
162
|
+
persistable_env = {k: env[k] for k in EnvService.__NON_SECRET_ENV_ITEMS if k in env}
|
163
|
+
|
164
|
+
self.__config.save(
|
165
|
+
{
|
166
|
+
USER_ENV_CACHE_HEADER: persistable_env
|
167
|
+
}
|
168
|
+
)
|
169
|
+
|
170
|
+
def __get_persisted_user_env (self) -> dict | None:
|
171
|
+
user_env = self.__config.get(USER_ENV_CACHE_HEADER) if self.__config.get(USER_ENV_CACHE_HEADER) else None
|
172
|
+
return user_env
|
173
|
+
|
174
|
+
def set_compose_file_path_in_env (self, path: str = None) -> None:
|
175
|
+
self.__config.save(
|
176
|
+
{
|
177
|
+
USER_ENV_CACHE_HEADER: {
|
178
|
+
"DOCKER_COMPOSE_FILE_PATH": path
|
179
|
+
}
|
180
|
+
}
|
181
|
+
)
|
182
|
+
|
183
|
+
@staticmethod
|
184
|
+
def __get_dbtag_from_architecture (merged_env_dict: dict) -> str:
|
185
|
+
"""Detects system architecture and returns the corresponding DBTAG."""
|
186
|
+
arm64_tag = merged_env_dict.get("ARM64DBTAG")
|
187
|
+
amd_tag = merged_env_dict.get("AMDDBTAG")
|
188
|
+
|
189
|
+
if is_arm_architecture():
|
190
|
+
return arm64_tag
|
191
|
+
else:
|
192
|
+
return amd_tag
|
193
|
+
|
194
|
+
@staticmethod
|
195
|
+
def __apply_server_env_dict_defaults (provided_env_dict: dict) -> dict:
|
196
|
+
|
197
|
+
env_dict = provided_env_dict.copy()
|
198
|
+
|
199
|
+
env_dict['DBTAG'] = EnvService.__get_dbtag_from_architecture(merged_env_dict=env_dict)
|
200
|
+
|
201
|
+
model_config = None
|
202
|
+
try:
|
203
|
+
use_model_proxy = env_dict.get("USE_SAAS_ML_TOOLS_RUNTIME")
|
204
|
+
if not use_model_proxy or use_model_proxy.lower() != 'true':
|
205
|
+
model_config = WatsonXAIEnvConfig.model_validate(env_dict)
|
206
|
+
except ValueError:
|
207
|
+
pass
|
208
|
+
|
209
|
+
# If no watsonx ai detials are found, try build model gateway config
|
210
|
+
if not model_config:
|
211
|
+
try:
|
212
|
+
model_config = ModelGatewayEnvConfig.model_validate(env_dict)
|
213
|
+
except ValueError as e:
|
214
|
+
pass
|
215
|
+
|
216
|
+
if not model_config:
|
217
|
+
logger.error(
|
218
|
+
"Missing required model access environment variables. Please set Watson Orchestrate credentials 'WO_INSTANCE' and 'WO_API_KEY'. For CPD, set 'WO_INSTANCE', 'WO_USERNAME' and either 'WO_API_KEY' or 'WO_PASSWORD'. Alternatively, you can set WatsonX AI credentials directly using 'WATSONX_SPACE_ID' and 'WATSONX_APIKEY'")
|
219
|
+
sys.exit(1)
|
220
|
+
|
221
|
+
env_dict.update(model_config.model_dump(exclude_none=True))
|
222
|
+
|
223
|
+
return env_dict
|
224
|
+
|
225
|
+
@staticmethod
|
226
|
+
def auto_configure_callback_ip (merged_env_dict: dict) -> dict:
|
227
|
+
"""
|
228
|
+
Automatically detect and configure CALLBACK_HOST_URL if it's empty.
|
229
|
+
|
230
|
+
Args:
|
231
|
+
merged_env_dict: The merged environment dictionary
|
232
|
+
|
233
|
+
Returns:
|
234
|
+
Updated environment dictionary with CALLBACK_HOST_URL set
|
235
|
+
"""
|
236
|
+
callback_url = merged_env_dict.get('CALLBACK_HOST_URL', '').strip()
|
237
|
+
|
238
|
+
# Only auto-configure if CALLBACK_HOST_URL is empty
|
239
|
+
if not callback_url:
|
240
|
+
logger.info("Auto-detecting local IP address for async tool callbacks...")
|
241
|
+
|
242
|
+
system = platform.system()
|
243
|
+
ip = None
|
244
|
+
|
245
|
+
try:
|
246
|
+
if system in ("Linux", "Darwin"):
|
247
|
+
result = subprocess.run(["ifconfig"], capture_output=True, text=True, check=True)
|
248
|
+
lines = result.stdout.splitlines()
|
249
|
+
|
250
|
+
for line in lines:
|
251
|
+
line = line.strip()
|
252
|
+
# Unix ifconfig output format: "inet 192.168.1.100 netmask 0xffffff00 broadcast 192.168.1.255"
|
253
|
+
if line.startswith("inet ") and "127.0.0.1" not in line:
|
254
|
+
candidate_ip = line.split()[1]
|
255
|
+
# Validate IP is not loopback or link-local
|
256
|
+
if (candidate_ip and
|
257
|
+
not candidate_ip.startswith("127.") and
|
258
|
+
not candidate_ip.startswith("169.254")):
|
259
|
+
ip = candidate_ip
|
260
|
+
break
|
261
|
+
|
262
|
+
elif system == "Windows":
|
263
|
+
result = subprocess.run(["ipconfig"], capture_output=True, text=True, check=True)
|
264
|
+
lines = result.stdout.splitlines()
|
265
|
+
|
266
|
+
for line in lines:
|
267
|
+
line = line.strip()
|
268
|
+
# Windows ipconfig output format: " IPv4 Address. . . . . . . . . . . : 192.168.1.100"
|
269
|
+
if "IPv4 Address" in line and ":" in line:
|
270
|
+
candidate_ip = line.split(":")[-1].strip()
|
271
|
+
# Validate IP is not loopback or link-local
|
272
|
+
if (candidate_ip and
|
273
|
+
not candidate_ip.startswith("127.") and
|
274
|
+
not candidate_ip.startswith("169.254")):
|
275
|
+
ip = candidate_ip
|
276
|
+
break
|
277
|
+
|
278
|
+
else:
|
279
|
+
logger.warning(f"Unsupported platform: {system}")
|
280
|
+
ip = None
|
281
|
+
|
282
|
+
except Exception as e:
|
283
|
+
logger.debug(f"IP detection failed on {system}: {e}")
|
284
|
+
ip = None
|
285
|
+
|
286
|
+
if ip:
|
287
|
+
callback_url = f"http://{ip}:4321"
|
288
|
+
merged_env_dict['CALLBACK_HOST_URL'] = callback_url
|
289
|
+
logger.info(f"Auto-configured CALLBACK_HOST_URL to: {callback_url}")
|
290
|
+
else:
|
291
|
+
# Fallback for localhost
|
292
|
+
callback_url = "http://host.docker.internal:4321"
|
293
|
+
merged_env_dict['CALLBACK_HOST_URL'] = callback_url
|
294
|
+
logger.info(f"Using Docker internal URL: {callback_url}")
|
295
|
+
logger.info("For external tools, consider using ngrok or similar tunneling service.")
|
296
|
+
else:
|
297
|
+
logger.info(f"Using existing CALLBACK_HOST_URL: {callback_url}")
|
298
|
+
|
299
|
+
return merged_env_dict
|
300
|
+
|
301
|
+
@staticmethod
|
302
|
+
def apply_llm_api_key_defaults (env_dict: dict) -> None:
|
303
|
+
llm_value = env_dict.get("WATSONX_APIKEY")
|
304
|
+
if llm_value:
|
305
|
+
env_dict.setdefault("ASSISTANT_LLM_API_KEY", llm_value)
|
306
|
+
env_dict.setdefault("ASSISTANT_EMBEDDINGS_API_KEY", llm_value)
|
307
|
+
env_dict.setdefault("ROUTING_LLM_API_KEY", llm_value)
|
308
|
+
env_dict.setdefault("BAM_API_KEY", llm_value)
|
309
|
+
env_dict.setdefault("WXAI_API_KEY", llm_value)
|
310
|
+
space_value = env_dict.get("WATSONX_SPACE_ID")
|
311
|
+
if space_value:
|
312
|
+
env_dict.setdefault("ASSISTANT_LLM_SPACE_ID", space_value)
|
313
|
+
env_dict.setdefault("ASSISTANT_EMBEDDINGS_SPACE_ID", space_value)
|
314
|
+
env_dict.setdefault("ROUTING_LLM_SPACE_ID", space_value)
|
315
|
+
|
316
|
+
@staticmethod
|
317
|
+
def __drop_auth_routes (env_dict: dict) -> dict:
|
318
|
+
auth_url_key = "AUTHORIZATION_URL"
|
319
|
+
env_dict_copy = env_dict.copy()
|
320
|
+
|
321
|
+
auth_url = env_dict_copy.get(auth_url_key)
|
322
|
+
if not auth_url:
|
323
|
+
return env_dict_copy
|
324
|
+
|
325
|
+
parsed_url = urlparse(auth_url)
|
326
|
+
new_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
|
327
|
+
env_dict_copy[auth_url_key] = new_url
|
328
|
+
|
329
|
+
return env_dict_copy
|
330
|
+
|
331
|
+
@staticmethod
|
332
|
+
def prepare_server_env_vars_minimal (user_env: dict = {}) -> dict:
|
333
|
+
default_env = EnvService.read_env_file(EnvService.get_default_env_file())
|
334
|
+
dev_edition_source = EnvService.get_dev_edition_source_core(user_env)
|
335
|
+
default_registry_vars = EnvService.__get_default_registry_env_vars_by_dev_edition_source(default_env, user_env,
|
336
|
+
source=dev_edition_source)
|
337
|
+
|
338
|
+
# Update the default environment with the default registry variables only if they are not already set
|
339
|
+
for key in default_registry_vars:
|
340
|
+
if key not in default_env or not default_env[key]:
|
341
|
+
default_env[key] = default_registry_vars[key]
|
342
|
+
|
343
|
+
# Merge the default environment with the user environment
|
344
|
+
merged_env_dict = {
|
345
|
+
**default_env,
|
346
|
+
**user_env,
|
347
|
+
}
|
348
|
+
|
349
|
+
return merged_env_dict
|
350
|
+
|
351
|
+
@staticmethod
|
352
|
+
def prepare_server_env_vars (user_env: dict = {}, should_drop_auth_routes: bool = False) -> dict:
|
353
|
+
merged_env_dict = EnvService.prepare_server_env_vars_minimal(user_env)
|
354
|
+
|
355
|
+
merged_env_dict = EnvService.__apply_server_env_dict_defaults(merged_env_dict)
|
356
|
+
|
357
|
+
if should_drop_auth_routes:
|
358
|
+
# NOTE: this is only needed in the case of co-pilot as of now.
|
359
|
+
merged_env_dict = EnvService.__drop_auth_routes(merged_env_dict)
|
360
|
+
|
361
|
+
# Auto-configure callback IP for async tools
|
362
|
+
merged_env_dict = EnvService.auto_configure_callback_ip(merged_env_dict)
|
363
|
+
|
364
|
+
EnvService.apply_llm_api_key_defaults(merged_env_dict)
|
365
|
+
|
366
|
+
return merged_env_dict
|
367
|
+
|
368
|
+
def define_saas_wdu_runtime (self, value: str = "none") -> None:
|
369
|
+
self.__config.write(USER_ENV_CACHE_HEADER, "SAAS_WDU_RUNTIME", value)
|
@@ -10,7 +10,7 @@ yaml.constructor.SafeConstructor.yaml_constructors[u'tag:yaml.org,2002:timestamp
|
|
10
10
|
def yaml_safe_load(file : BinaryIO) -> dict:
|
11
11
|
return yaml.safe_load(file)
|
12
12
|
|
13
|
-
def
|
13
|
+
def sanitize_app_id(app_id: str) -> str:
|
14
14
|
sanatize_pattern = re.compile(r"[^a-zA-Z0-9]+")
|
15
15
|
return re.sub(sanatize_pattern,'_', app_id)
|
16
16
|
|
{ibm_watsonx_orchestrate-1.11.0b0.dist-info → ibm_watsonx_orchestrate-1.12.0b0.dist-info}/METADATA
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: ibm-watsonx-orchestrate
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.12.0b0
|
4
4
|
Summary: IBM watsonx.orchestrate SDK
|
5
5
|
Author-email: IBM <support@ibm.com>
|
6
6
|
License: MIT License
|
@@ -11,7 +11,7 @@ Requires-Dist: click<8.2.0,>=8.0.0
|
|
11
11
|
Requires-Dist: docstring-parser<1.0,>=0.16
|
12
12
|
Requires-Dist: httpx<1.0.0,>=0.28.1
|
13
13
|
Requires-Dist: ibm-cloud-sdk-core>=3.24.2
|
14
|
-
Requires-Dist: ibm-watsonx-orchestrate-evaluation-framework==1.
|
14
|
+
Requires-Dist: ibm-watsonx-orchestrate-evaluation-framework==1.1.2
|
15
15
|
Requires-Dist: jsonref==1.1.0
|
16
16
|
Requires-Dist: langchain-core<=0.3.63
|
17
17
|
Requires-Dist: langsmith<=0.3.45
|