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.
Files changed (47) hide show
  1. ibm_watsonx_orchestrate/__init__.py +1 -1
  2. ibm_watsonx_orchestrate/agent_builder/agents/types.py +30 -0
  3. ibm_watsonx_orchestrate/agent_builder/connections/connections.py +8 -5
  4. ibm_watsonx_orchestrate/agent_builder/connections/types.py +14 -0
  5. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +25 -10
  6. ibm_watsonx_orchestrate/agent_builder/tools/__init__.py +1 -0
  7. ibm_watsonx_orchestrate/agent_builder/tools/langflow_tool.py +124 -0
  8. ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +3 -3
  9. ibm_watsonx_orchestrate/agent_builder/tools/types.py +20 -2
  10. ibm_watsonx_orchestrate/cli/commands/agents/agents_command.py +10 -2
  11. ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +421 -177
  12. ibm_watsonx_orchestrate/cli/commands/connections/connections_command.py +18 -0
  13. ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +114 -0
  14. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_server_controller.py +24 -91
  15. ibm_watsonx_orchestrate/cli/commands/environment/environment_command.py +1 -1
  16. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_command.py +223 -2
  17. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_controller.py +93 -9
  18. ibm_watsonx_orchestrate/cli/commands/models/models_controller.py +3 -3
  19. ibm_watsonx_orchestrate/cli/commands/partners/offering/partners_offering_command.py +56 -0
  20. ibm_watsonx_orchestrate/cli/commands/partners/offering/partners_offering_controller.py +458 -0
  21. ibm_watsonx_orchestrate/cli/commands/partners/offering/types.py +107 -0
  22. ibm_watsonx_orchestrate/cli/commands/partners/partners_command.py +12 -0
  23. ibm_watsonx_orchestrate/cli/commands/partners/partners_controller.py +0 -0
  24. ibm_watsonx_orchestrate/cli/commands/server/server_command.py +114 -635
  25. ibm_watsonx_orchestrate/cli/commands/server/types.py +1 -1
  26. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_controller.py +2 -2
  27. ibm_watsonx_orchestrate/cli/commands/tools/tools_command.py +2 -3
  28. ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +206 -43
  29. ibm_watsonx_orchestrate/cli/main.py +2 -0
  30. ibm_watsonx_orchestrate/client/base_api_client.py +31 -10
  31. ibm_watsonx_orchestrate/client/connections/connections_client.py +18 -1
  32. ibm_watsonx_orchestrate/client/service_instance.py +19 -34
  33. ibm_watsonx_orchestrate/client/tools/tempus_client.py +3 -0
  34. ibm_watsonx_orchestrate/client/tools/tool_client.py +5 -2
  35. ibm_watsonx_orchestrate/client/utils.py +34 -2
  36. ibm_watsonx_orchestrate/docker/compose-lite.yml +14 -12
  37. ibm_watsonx_orchestrate/docker/default.env +17 -17
  38. ibm_watsonx_orchestrate/flow_builder/flows/flow.py +3 -1
  39. ibm_watsonx_orchestrate/flow_builder/types.py +252 -1
  40. ibm_watsonx_orchestrate/utils/docker_utils.py +280 -0
  41. ibm_watsonx_orchestrate/utils/environment.py +369 -0
  42. ibm_watsonx_orchestrate/utils/utils.py +1 -1
  43. {ibm_watsonx_orchestrate-1.11.0b0.dist-info → ibm_watsonx_orchestrate-1.12.0b0.dist-info}/METADATA +2 -2
  44. {ibm_watsonx_orchestrate-1.11.0b0.dist-info → ibm_watsonx_orchestrate-1.12.0b0.dist-info}/RECORD +47 -39
  45. {ibm_watsonx_orchestrate-1.11.0b0.dist-info → ibm_watsonx_orchestrate-1.12.0b0.dist-info}/WHEEL +0 -0
  46. {ibm_watsonx_orchestrate-1.11.0b0.dist-info → ibm_watsonx_orchestrate-1.12.0b0.dist-info}/entry_points.txt +0 -0
  47. {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 sanatize_app_id(app_id: str) -> str:
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ibm-watsonx-orchestrate
3
- Version: 1.11.0b0
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.0.8
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