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
@@ -1,34 +1,26 @@
1
- import importlib.resources as resources
2
1
  import logging
3
2
  import os
4
3
  import platform
5
- import subprocess
6
4
  import sys
7
5
  import shutil
8
- import tempfile
9
6
  import time
10
7
  from pathlib import Path
11
- from urllib.parse import urlparse
12
8
 
13
9
  import re
14
10
  import jwt
15
11
  import requests
16
12
  import typer
17
- from dotenv import dotenv_values
18
13
 
19
14
  from ibm_watsonx_orchestrate.client.utils import instantiate_client
20
15
 
21
- from ibm_watsonx_orchestrate.cli.commands.server.types import WatsonXAIEnvConfig, ModelGatewayEnvConfig
22
-
23
16
  from ibm_watsonx_orchestrate.cli.commands.environment.environment_controller import _login
24
17
 
25
- from ibm_watsonx_orchestrate.cli.config import LICENSE_HEADER, \
26
- ENV_ACCEPT_LICENSE
27
-
28
18
  from ibm_watsonx_orchestrate.cli.config import PROTECTED_ENV_NAME, clear_protected_env_credentials_token, Config, \
29
19
  AUTH_CONFIG_FILE_FOLDER, AUTH_CONFIG_FILE, AUTH_MCSP_TOKEN_OPT, AUTH_SECTION_HEADER, USER_ENV_CACHE_HEADER, LICENSE_HEADER, \
30
20
  ENV_ACCEPT_LICENSE
31
21
  from ibm_watsonx_orchestrate.client.agents.agent_client import AgentClient
22
+ from ibm_watsonx_orchestrate.utils.docker_utils import DockerLoginService, DockerComposeCore, DockerUtils
23
+ from ibm_watsonx_orchestrate.utils.environment import EnvService
32
24
 
33
25
  logger = logging.getLogger(__name__)
34
26
 
@@ -42,305 +34,6 @@ _EXPORT_FILE_TYPES: set[str] = {
42
34
  'env'
43
35
  }
44
36
 
45
- _ALWAYS_UNSET: set[str] = {
46
- "WO_API_KEY",
47
- "WO_INSTANCE",
48
- "DOCKER_IAM_KEY",
49
- "WO_DEVELOPER_EDITION_SOURCE",
50
- "WATSONX_SPACE_ID",
51
- "WATSONX_APIKEY",
52
- "WO_USERNAME",
53
- "WO_PASSWORD",
54
- }
55
-
56
- NON_SECRET_ENV_ITEMS: set[str] = {
57
- "WO_DEVELOPER_EDITION_SOURCE",
58
- "WO_INSTANCE",
59
- "USE_SAAS_ML_TOOLS_RUNTIME",
60
- "AUTHORIZATION_URL",
61
- "OPENSOURCE_REGISTRY_PROXY",
62
- "SAAS_WDU_RUNTIME",
63
- "LATEST_ENV_FILE",
64
- }
65
-
66
- def define_saas_wdu_runtime(value: str = "none") -> None:
67
- cfg = Config()
68
- cfg.write(USER_ENV_CACHE_HEADER,"SAAS_WDU_RUNTIME",value)
69
-
70
- def set_compose_file_path_in_env(path: str = None) -> None:
71
- Config().save(
72
- {
73
- USER_ENV_CACHE_HEADER: {
74
- "DOCKER_COMPOSE_FILE_PATH" : path
75
- }
76
- }
77
- )
78
-
79
- def get_compose_file_path_from_env() -> str:
80
- return Config().read(USER_ENV_CACHE_HEADER,"DOCKER_COMPOSE_FILE_PATH")
81
-
82
-
83
- def ensure_docker_installed() -> None:
84
- try:
85
- subprocess.run(["docker", "--version"], check=True, capture_output=True)
86
- except (FileNotFoundError, subprocess.CalledProcessError):
87
- logger.error("Unable to find an installed docker")
88
- sys.exit(1)
89
-
90
- def ensure_docker_compose_installed() -> list:
91
- try:
92
- subprocess.run(["docker", "compose", "version"], check=True, capture_output=True)
93
- return ["docker", "compose"]
94
- except (FileNotFoundError, subprocess.CalledProcessError):
95
- pass
96
-
97
- try:
98
- subprocess.run(["docker-compose", "version"], check=True, capture_output=True)
99
- return ["docker-compose"]
100
- except (FileNotFoundError, subprocess.CalledProcessError):
101
- typer.echo("Unable to find an installed docker-compose or docker compose")
102
- sys.exit(1)
103
-
104
- def docker_login(api_key: str, registry_url: str, username:str = "iamapikey") -> None:
105
- logger.info(f"Logging into Docker registry: {registry_url} ...")
106
- result = subprocess.run(
107
- ["docker", "login", "-u", username, "--password-stdin", registry_url],
108
- input=api_key.encode("utf-8"),
109
- capture_output=True,
110
- )
111
- if result.returncode != 0:
112
- logger.error(f"Error logging into Docker:\n{result.stderr.decode('utf-8')}")
113
- sys.exit(1)
114
- logger.info("Successfully logged in to Docker.")
115
-
116
- def docker_login_by_dev_edition_source(env_dict: dict, source: str) -> None:
117
- if env_dict.get('WO_DEVELOPER_EDITION_SKIP_LOGIN', None) == 'true':
118
- logger.info('WO_DEVELOPER_EDITION_SKIP_LOGIN is set to true, skipping login.')
119
- logger.warning('If the developer edition images are not already pulled this call will fail without first setting WO_DEVELOPER_EDITION_SKIP_LOGIN=false')
120
- else:
121
- if not env_dict.get("REGISTRY_URL"):
122
- raise ValueError("REGISTRY_URL is not set.")
123
- registry_url = env_dict["REGISTRY_URL"].split("/")[0]
124
- if source == "internal":
125
- iam_api_key = env_dict.get("DOCKER_IAM_KEY")
126
- if not iam_api_key:
127
- raise ValueError("DOCKER_IAM_KEY is required in the environment file if WO_DEVELOPER_EDITION_SOURCE is set to 'internal'.")
128
- docker_login(iam_api_key, registry_url, "iamapikey")
129
- elif source == "myibm":
130
- wo_entitlement_key = env_dict.get("WO_ENTITLEMENT_KEY")
131
- if not wo_entitlement_key:
132
- raise ValueError("WO_ENTITLEMENT_KEY is required in the environment file.")
133
- docker_login(wo_entitlement_key, registry_url, "cp")
134
- elif source == "orchestrate":
135
- wo_auth_type = env_dict.get("WO_AUTH_TYPE")
136
- api_key, username = get_docker_cred_by_wo_auth_type(env_dict, wo_auth_type)
137
- docker_login(api_key, registry_url, username)
138
-
139
-
140
- def get_compose_file() -> Path:
141
- custom_compose_path = get_compose_file_path_from_env()
142
- return Path(custom_compose_path) if custom_compose_path else get_default_compose_file()
143
-
144
-
145
- def get_default_compose_file() -> Path:
146
- with resources.as_file(
147
- resources.files("ibm_watsonx_orchestrate.docker").joinpath("compose-lite.yml")
148
- ) as compose_file:
149
- return compose_file
150
-
151
-
152
- def get_default_env_file() -> Path:
153
- with resources.as_file(
154
- resources.files("ibm_watsonx_orchestrate.docker").joinpath("default.env")
155
- ) as env_file:
156
- return env_file
157
-
158
-
159
- def read_env_file(env_path: Path|str) -> dict:
160
- return dotenv_values(str(env_path))
161
-
162
- def merge_env(
163
- default_env_path: Path,
164
- user_env_path: Path | None
165
- ) -> dict:
166
-
167
- merged = dotenv_values(str(default_env_path))
168
-
169
- if user_env_path is not None:
170
- user_env = dotenv_values(str(user_env_path))
171
- merged.update(user_env)
172
-
173
- return merged
174
-
175
- def get_default_registry_env_vars_by_dev_edition_source(default_env: dict, user_env:dict, source: str) -> dict[str,str]:
176
- component_registry_var_names = {key for key in default_env if key.endswith("_REGISTRY")} | {'REGISTRY_URL'}
177
-
178
- registry_url = user_env.get("REGISTRY_URL", None)
179
- if not registry_url:
180
- if source == "internal":
181
- registry_url = "us.icr.io/watson-orchestrate-private"
182
- elif source == "myibm":
183
- registry_url = "cp.icr.io/cp/wxo-lite"
184
- elif source == "orchestrate":
185
- # extract the hostname from the WO_INSTANCE URL, and replace the "api." prefix with "registry." to construct the registry URL per region
186
- wo_url = user_env.get("WO_INSTANCE")
187
-
188
- if not wo_url:
189
- raise ValueError("WO_INSTANCE is required in the environment file if the developer edition source is set to 'orchestrate'.")
190
-
191
- parsed = urlparse(wo_url)
192
- hostname = parsed.hostname
193
-
194
- registry_url = f"registry.{hostname[4:]}/cp/wxo-lite"
195
- else:
196
- raise ValueError(f"Unknown value for developer edition source: {source}. Must be one of ['internal', 'myibm', 'orchestrate'].")
197
-
198
- result = {name: registry_url for name in component_registry_var_names}
199
- return result
200
-
201
- def get_dev_edition_source(env_dict: dict | None) -> str:
202
- if not env_dict:
203
- return "myibm"
204
-
205
- source = env_dict.get("WO_DEVELOPER_EDITION_SOURCE")
206
-
207
- if source:
208
- return source
209
- if env_dict.get("WO_INSTANCE"):
210
- return "orchestrate"
211
- return "myibm"
212
-
213
- def get_docker_cred_by_wo_auth_type(env_dict: dict, auth_type: str | None) -> tuple[str, str]:
214
- # Try infer the auth type if not provided
215
- if not auth_type:
216
- instance_url = env_dict.get("WO_INSTANCE")
217
- if instance_url:
218
- if ".cloud.ibm.com" in instance_url:
219
- auth_type = "ibm_iam"
220
- elif ".ibm.com" in instance_url:
221
- auth_type = "mcsp"
222
- elif "https://cpd" in instance_url:
223
- auth_type = "cpd"
224
-
225
- if auth_type in {"mcsp", "ibm_iam"}:
226
- wo_api_key = env_dict.get("WO_API_KEY")
227
- if not wo_api_key:
228
- raise ValueError("WO_API_KEY is required in the environment file if the WO_AUTH_TYPE is set to 'mcsp' or 'ibm_iam'.")
229
- instance_url = env_dict.get("WO_INSTANCE")
230
- if not instance_url:
231
- raise ValueError("WO_INSTANCE is required in the environment file if the WO_AUTH_TYPE is set to 'mcsp' or 'ibm_iam'.")
232
- path = urlparse(instance_url).path
233
- if not path or '/' not in path:
234
- raise ValueError(f"Invalid WO_INSTANCE URL: '{instance_url}'. It should contain the instance (tenant) id.")
235
- tenant_id = path.split('/')[-1]
236
- return wo_api_key, f"wxouser-{tenant_id}"
237
- elif auth_type == "cpd":
238
- wo_api_key = env_dict.get("WO_API_KEY")
239
- wo_password = env_dict.get("WO_PASSWORD")
240
- if not wo_api_key and not wo_password:
241
- raise ValueError("WO_API_KEY or WO_PASSWORD is required in the environment file if the WO_AUTH_TYPE is set to 'cpd'.")
242
- wo_username = env_dict.get("WO_USERNAME")
243
- if not wo_username:
244
- raise ValueError("WO_USERNAME is required in the environment file if the WO_AUTH_TYPE is set to 'cpd'.")
245
- return wo_api_key or wo_password, wo_username # type: ignore[return-value]
246
- else:
247
- raise ValueError(f"Unknown value for WO_AUTH_TYPE: '{auth_type}'. Must be one of ['mcsp', 'ibm_iam', 'cpd'].")
248
-
249
- def apply_server_env_dict_defaults(provided_env_dict: dict) -> dict:
250
-
251
- env_dict = provided_env_dict.copy()
252
-
253
- env_dict['DBTAG'] = get_dbtag_from_architecture(merged_env_dict=env_dict)
254
-
255
- model_config = None
256
- try:
257
- use_model_proxy = env_dict.get("USE_SAAS_ML_TOOLS_RUNTIME")
258
- if not use_model_proxy or use_model_proxy.lower() != 'true':
259
- model_config = WatsonXAIEnvConfig.model_validate(env_dict)
260
- except ValueError:
261
- pass
262
-
263
- # If no watsonx ai detials are found, try build model gateway config
264
- if not model_config:
265
- try:
266
- model_config = ModelGatewayEnvConfig.model_validate(env_dict)
267
- except ValueError as e :
268
- pass
269
-
270
- if not model_config:
271
- logger.error("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'")
272
- sys.exit(1)
273
-
274
- env_dict.update(model_config.model_dump(exclude_none=True))
275
-
276
- return env_dict
277
-
278
- def apply_llm_api_key_defaults(env_dict: dict) -> None:
279
- llm_value = env_dict.get("WATSONX_APIKEY")
280
- if llm_value:
281
- env_dict.setdefault("ASSISTANT_LLM_API_KEY", llm_value)
282
- env_dict.setdefault("ASSISTANT_EMBEDDINGS_API_KEY", llm_value)
283
- env_dict.setdefault("ROUTING_LLM_API_KEY", llm_value)
284
- env_dict.setdefault("BAM_API_KEY", llm_value)
285
- env_dict.setdefault("WXAI_API_KEY", llm_value)
286
- space_value = env_dict.get("WATSONX_SPACE_ID")
287
- if space_value:
288
- env_dict.setdefault("ASSISTANT_LLM_SPACE_ID", space_value)
289
- env_dict.setdefault("ASSISTANT_EMBEDDINGS_SPACE_ID", space_value)
290
- env_dict.setdefault("ROUTING_LLM_SPACE_ID", space_value)
291
-
292
- def _is_docker_container_running(container_name):
293
- ensure_docker_installed()
294
- command = [ "docker",
295
- "ps",
296
- "-f",
297
- f"name={container_name}"
298
- ]
299
- result = subprocess.run(command, env=os.environ, capture_output=True)
300
- if container_name in str(result.stdout):
301
- return True
302
- return False
303
-
304
- def _check_exclusive_observibility(langfuse_enabled: bool, ibm_tele_enabled: bool):
305
- if langfuse_enabled and ibm_tele_enabled:
306
- return False
307
- if langfuse_enabled and _is_docker_container_running("docker-frontend-server-1"):
308
- return False
309
- if ibm_tele_enabled and _is_docker_container_running("docker-langfuse-web-1"):
310
- return False
311
- return True
312
-
313
- def _prepare_clean_env(env_file: Path) -> None:
314
- """Remove env vars so terminal definitions don't override"""
315
- keys_from_file = set(dotenv_values(str(env_file)).keys())
316
- keys_to_unset = keys_from_file | _ALWAYS_UNSET
317
- for key in keys_to_unset:
318
- os.environ.pop(key, None)
319
-
320
- def write_merged_env_file(merged_env: dict, target_path: str = None) -> Path:
321
-
322
- if target_path:
323
- file = open(target_path,"w")
324
- else:
325
- file = tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".env")
326
-
327
- with file:
328
- for key, val in merged_env.items():
329
- file.write(f"{key}={val}\n")
330
- return Path(file.name)
331
-
332
- def get_dbtag_from_architecture(merged_env_dict: dict) -> str:
333
- """Detects system architecture and returns the corresponding DBTAG."""
334
- arch = platform.machine()
335
-
336
- arm64_tag = merged_env_dict.get("ARM64DBTAG")
337
- amd_tag = merged_env_dict.get("AMDDBTAG")
338
-
339
- if arch in ["aarch64", "arm64"]:
340
- return arm64_tag
341
- else:
342
- return amd_tag
343
-
344
37
  def refresh_local_credentials() -> None:
345
38
  """
346
39
  Refresh the local credentials
@@ -348,51 +41,23 @@ def refresh_local_credentials() -> None:
348
41
  clear_protected_env_credentials_token()
349
42
  _login(name=PROTECTED_ENV_NAME, apikey=None)
350
43
 
351
- def persist_user_env(env: dict, include_secrets: bool = False) -> None:
352
- if include_secrets:
353
- persistable_env = env
354
- else:
355
- persistable_env = {k:env[k] for k in NON_SECRET_ENV_ITEMS if k in env}
356
-
357
- cfg = Config()
358
- cfg.save(
359
- {
360
- USER_ENV_CACHE_HEADER: persistable_env
361
- }
362
- )
363
-
364
- def get_persisted_user_env() -> dict | None:
365
- cfg = Config()
366
- user_env = cfg.get(USER_ENV_CACHE_HEADER) if cfg.get(USER_ENV_CACHE_HEADER) else None
367
- return user_env
368
-
369
44
  def run_compose_lite(
370
- final_env_file: Path,
45
+ final_env_file: Path,
46
+ env_service: EnvService,
371
47
  experimental_with_langfuse=False,
372
48
  experimental_with_ibm_telemetry=False,
373
49
  with_doc_processing=False,
374
50
  with_voice=False,
375
- experimental_with_langflow=False,
51
+ with_langflow=False,
376
52
  ) -> None:
377
- compose_path = get_compose_file()
378
-
379
- compose_command = ensure_docker_compose_installed()
380
- _prepare_clean_env(final_env_file)
381
- db_tag = read_env_file(final_env_file).get('DBTAG', None)
53
+ EnvService.prepare_clean_env(final_env_file)
54
+ db_tag = EnvService.read_env_file(final_env_file).get('DBTAG', None)
382
55
  logger.info(f"Detected architecture: {platform.machine()}, using DBTAG: {db_tag}")
383
56
 
57
+ compose_core = DockerComposeCore(env_service)
58
+
384
59
  # Step 1: Start only the DB container
385
- db_command = compose_command + [
386
- "-f", str(compose_path),
387
- "--env-file", str(final_env_file),
388
- "up",
389
- "-d",
390
- "--remove-orphans",
391
- "wxo-server-db"
392
- ]
393
-
394
- logger.info("Starting database container...")
395
- result = subprocess.run(db_command, env=os.environ, capture_output=False)
60
+ result = compose_core.service_up(service_name="wxo-server-db", friendly_name="WxO Server DB", final_env_file=final_env_file, compose_env=os.environ)
396
61
 
397
62
  if result.returncode != 0:
398
63
  logger.error(f"Error starting DB container: {result.stderr}")
@@ -402,7 +67,7 @@ def run_compose_lite(
402
67
 
403
68
 
404
69
  # Step 2: Create Langflow DB (if enabled)
405
- if experimental_with_langflow:
70
+ if with_langflow:
406
71
  create_langflow_db()
407
72
 
408
73
  # Step 3: Start all remaining services (except DB)
@@ -415,27 +80,10 @@ def run_compose_lite(
415
80
  profiles.append("docproc")
416
81
  if with_voice:
417
82
  profiles.append("voice")
418
- if experimental_with_langflow:
83
+ if with_langflow:
419
84
  profiles.append("langflow")
420
85
 
421
- command = compose_command[:]
422
- for profile in profiles:
423
- command += ["--profile", profile]
424
-
425
- command += [
426
- "-f", str(compose_path),
427
- "--env-file", str(final_env_file),
428
- "up",
429
- "--scale",
430
- "ui=0",
431
- "--scale",
432
- "cpe=0",
433
- "-d",
434
- "--remove-orphans",
435
- ]
436
-
437
- logger.info("Starting docker-compose services...")
438
- result = subprocess.run(command, capture_output=False)
86
+ result = compose_core.services_up(profiles, final_env_file, ["--scale", "ui=0", "--scale", "cpe=0"])
439
87
 
440
88
  if result.returncode == 0:
441
89
  logger.info("Services started successfully.")
@@ -498,29 +146,13 @@ def wait_for_wxo_ui_health_check(timeout_seconds=45, interval_seconds=2):
498
146
  return False
499
147
 
500
148
  def run_compose_lite_ui(user_env_file: Path) -> bool:
501
- compose_path = get_compose_file()
502
- compose_command = ensure_docker_compose_installed()
503
- _prepare_clean_env(user_env_file)
504
- ensure_docker_installed()
505
-
506
- default_env = read_env_file(get_default_env_file())
507
- user_env = read_env_file(user_env_file) if user_env_file else {}
508
- if not user_env:
509
- user_env = get_persisted_user_env() or {}
510
-
511
- dev_edition_source = get_dev_edition_source(user_env)
512
- default_registry_vars = get_default_registry_env_vars_by_dev_edition_source(default_env, user_env, source=dev_edition_source)
513
-
514
- # Update the default environment with the default registry variables only if they are not already set
515
- for key in default_registry_vars:
516
- if key not in default_env or not default_env[key]:
517
- default_env[key] = default_registry_vars[key]
518
-
519
- # Merge the default environment with the user environment
520
- merged_env_dict = {
521
- **default_env,
522
- **user_env,
523
- }
149
+ DockerUtils.ensure_docker_installed()
150
+
151
+ cli_config = Config()
152
+ env_service = EnvService(cli_config)
153
+ env_service.prepare_clean_env(user_env_file)
154
+ user_env = env_service.get_user_env(user_env_file)
155
+ merged_env_dict = env_service.prepare_server_env_vars_minimal(user_env=user_env)
524
156
 
525
157
  _login(name=PROTECTED_ENV_NAME)
526
158
  auth_cfg = Config(AUTH_CONFIG_FILE_FOLDER, AUTH_CONFIG_FILE)
@@ -537,22 +169,22 @@ def run_compose_lite_ui(user_env_file: Path) -> bool:
537
169
  sys.exit(1)
538
170
 
539
171
  try:
540
- docker_login_by_dev_edition_source(merged_env_dict, dev_edition_source)
172
+ DockerLoginService(env_service=env_service).login_by_dev_edition_source(merged_env_dict)
541
173
  except ValueError as ignored:
542
174
  # do nothing, as the docker login here is not mandatory
543
175
  pass
544
176
 
545
177
  # Auto-configure callback IP for async tools
546
- merged_env_dict = auto_configure_callback_ip(merged_env_dict)
178
+ merged_env_dict = env_service.auto_configure_callback_ip(merged_env_dict)
547
179
 
548
180
  #These are to removed warning and not used in UI component
549
181
  if not 'WATSONX_SPACE_ID' in merged_env_dict:
550
182
  merged_env_dict['WATSONX_SPACE_ID']='X'
551
183
  if not 'WATSONX_APIKEY' in merged_env_dict:
552
184
  merged_env_dict['WATSONX_APIKEY']='X'
553
- apply_llm_api_key_defaults(merged_env_dict)
185
+ env_service.apply_llm_api_key_defaults(merged_env_dict)
554
186
 
555
- final_env_file = write_merged_env_file(merged_env_dict)
187
+ final_env_file = env_service.write_merged_env_file(merged_env_dict)
556
188
 
557
189
  logger.info("Waiting for orchestrate server to be fully started and ready...")
558
190
 
@@ -562,17 +194,9 @@ def run_compose_lite_ui(user_env_file: Path) -> bool:
562
194
  logger.error("Healthcheck failed orchestrate server. Make sure you start the server components with `orchestrate server start` before trying to start the chat UI")
563
195
  return False
564
196
 
565
- command = compose_command + [
566
- "-f", str(compose_path),
567
- "--env-file", str(final_env_file),
568
- "up",
569
- "ui",
570
- "-d",
571
- "--remove-orphans"
572
- ]
197
+ compose_core = DockerComposeCore(env_service)
573
198
 
574
- logger.info(f"Starting docker-compose UI service...")
575
- result = subprocess.run(command, capture_output=False)
199
+ result = compose_core.service_up(service_name="ui", friendly_name="UI", final_env_file=final_env_file)
576
200
 
577
201
  if result.returncode == 0:
578
202
  logger.info("Chat UI Service started successfully.")
@@ -593,36 +217,23 @@ def run_compose_lite_ui(user_env_file: Path) -> bool:
593
217
  return True
594
218
 
595
219
  def run_compose_lite_down_ui(user_env_file: Path, is_reset: bool = False) -> None:
596
- compose_path = get_compose_file()
597
- compose_command = ensure_docker_compose_installed()
598
- _prepare_clean_env(user_env_file)
599
-
600
-
601
- ensure_docker_installed()
602
- default_env_path = get_default_env_file()
603
- merged_env_dict = merge_env(
220
+ EnvService.prepare_clean_env(user_env_file)
221
+ DockerUtils.ensure_docker_installed()
222
+ default_env_path = EnvService.get_default_env_file()
223
+ merged_env_dict = EnvService.merge_env(
604
224
  default_env_path,
605
225
  user_env_file
606
226
  )
607
227
  merged_env_dict['WATSONX_SPACE_ID']='X'
608
228
  merged_env_dict['WATSONX_APIKEY']='X'
609
- apply_llm_api_key_defaults(merged_env_dict)
610
- final_env_file = write_merged_env_file(merged_env_dict)
611
-
612
- command = compose_command + [
613
- "-f", str(compose_path),
614
- "--env-file", str(final_env_file),
615
- "down",
616
- "ui"
617
- ]
618
-
619
- if is_reset:
620
- command.append("--volumes")
621
- logger.info("Stopping docker-compose UI service and resetting volumes...")
622
- else:
623
- logger.info("Stopping docker-compose UI service...")
229
+ EnvService.apply_llm_api_key_defaults(merged_env_dict)
230
+ final_env_file = EnvService.write_merged_env_file(merged_env_dict)
231
+
232
+ cli_config = Config()
233
+ env_service = EnvService(cli_config)
234
+ compose_core = DockerComposeCore(env_service)
624
235
 
625
- result = subprocess.run(command, capture_output=False)
236
+ result = compose_core.service_down(service_name="ui", friendly_name="UI", final_env_file=final_env_file, is_reset=is_reset)
626
237
 
627
238
  if result.returncode == 0:
628
239
  logger.info("UI service stopped successfully.")
@@ -637,24 +248,13 @@ def run_compose_lite_down_ui(user_env_file: Path, is_reset: bool = False) -> Non
637
248
  sys.exit(1)
638
249
 
639
250
  def run_compose_lite_down(final_env_file: Path, is_reset: bool = False) -> None:
640
- compose_path = get_compose_file()
641
- compose_command = ensure_docker_compose_installed()
642
- _prepare_clean_env(final_env_file)
643
-
644
- command = compose_command + [
645
- '--profile', '*',
646
- "-f", str(compose_path),
647
- "--env-file", str(final_env_file),
648
- "down"
649
- ]
650
-
651
- if is_reset:
652
- command.append("--volumes")
653
- logger.info("Stopping docker-compose services and resetting volumes...")
654
- else:
655
- logger.info("Stopping docker-compose services...")
251
+ EnvService.prepare_clean_env(final_env_file)
252
+
253
+ cli_config = Config()
254
+ env_service = EnvService(cli_config)
255
+ compose_core = DockerComposeCore(env_service)
656
256
 
657
- result = subprocess.run(command, capture_output=False)
257
+ result = compose_core.services_down(final_env_file=final_env_file, is_reset=is_reset)
658
258
 
659
259
  if result.returncode == 0:
660
260
  logger.info("Services stopped successfully.")
@@ -668,23 +268,14 @@ def run_compose_lite_down(final_env_file: Path, is_reset: bool = False) -> None:
668
268
  )
669
269
  sys.exit(1)
670
270
 
671
- def run_compose_lite_logs(final_env_file: Path, is_reset: bool = False) -> None:
672
- compose_path = get_compose_file()
673
- compose_command = ensure_docker_compose_installed()
674
- _prepare_clean_env(final_env_file)
675
-
676
- command = compose_command + [
677
- "-f", str(compose_path),
678
- "--env-file", str(final_env_file),
679
- "--profile",
680
- "*",
681
- "logs",
682
- "-f"
683
- ]
271
+ def run_compose_lite_logs(final_env_file: Path) -> None:
272
+ EnvService.prepare_clean_env(final_env_file)
684
273
 
685
- logger.info("Docker Logs...")
274
+ cli_config = Config()
275
+ env_service = EnvService(cli_config)
276
+ compose_core = DockerComposeCore(env_service)
686
277
 
687
- result = subprocess.run(command, capture_output=False)
278
+ result = compose_core.services_logs(final_env_file=final_env_file, should_follow=True)
688
279
 
689
280
  if result.returncode == 0:
690
281
  logger.info("End of docker logs")
@@ -698,13 +289,12 @@ def run_compose_lite_logs(final_env_file: Path, is_reset: bool = False) -> None:
698
289
  )
699
290
  sys.exit(1)
700
291
 
701
- def confirm_accepts_license_agreement(accepts_by_argument: bool):
702
- cfg = Config()
292
+ def confirm_accepts_license_agreement(accepts_by_argument: bool, cfg: Config):
703
293
  accepts_license = cfg.read(LICENSE_HEADER, ENV_ACCEPT_LICENSE)
704
294
  if accepts_license != True:
705
295
  logger.warning(('''
706
296
  By running the following command your machine will install IBM watsonx Orchestrate Developer Edition, which is governed by the following IBM license agreement:
707
- - * https://www.ibm.com/support/customer/csol/terms/?id=L-YRMZ-PB6MHM&lc=en
297
+ - * https://www.ibm.com/support/customer/csol/terms/?id=L-GLQU-5KA4PY&lc=en
708
298
  Additionally, the following prerequisite open source programs will be obtained from Docker Hub and will be installed on your machine. Each of the below programs are Separately Licensed Code, and are governed by the separate license agreements identified below, and not by the IBM license agreement:
709
299
  * redis (7.2) - https://github.com/redis/redis/blob/7.2.7/COPYING
710
300
  * minio - https://github.com/minio/minio/blob/master/LICENSE
@@ -712,6 +302,7 @@ def confirm_accepts_license_agreement(accepts_by_argument: bool):
712
302
  * etcd - https://github.com/etcd-io/etcd/blob/main/LICENSE
713
303
  * clickhouse-server - https://github.com/ClickHouse/ClickHouse/blob/master/LICENSE
714
304
  * langfuse - https://github.com/langfuse/langfuse/blob/main/LICENSE
305
+ * langflow - https://github.com/langflow-ai/langflow/blob/main/LICENSE
715
306
  After installation, you are solely responsible for obtaining and installing updates and fixes, including security patches, for the above prerequisite open source programs. To update images the customer will run `orchestrate server reset && orchestrate server start -e .env`.
716
307
  ''').strip())
717
308
  if not accepts_by_argument:
@@ -724,107 +315,6 @@ def confirm_accepts_license_agreement(accepts_by_argument: bool):
724
315
  logger.error('The terms and conditions were not accepted, exiting.')
725
316
  exit(1)
726
317
 
727
- def auto_configure_callback_ip(merged_env_dict: dict) -> dict:
728
- """
729
- Automatically detect and configure CALLBACK_HOST_URL if it's empty.
730
-
731
- Args:
732
- merged_env_dict: The merged environment dictionary
733
-
734
- Returns:
735
- Updated environment dictionary with CALLBACK_HOST_URL set
736
- """
737
- callback_url = merged_env_dict.get('CALLBACK_HOST_URL', '').strip()
738
-
739
- # Only auto-configure if CALLBACK_HOST_URL is empty
740
- if not callback_url:
741
- logger.info("Auto-detecting local IP address for async tool callbacks...")
742
-
743
- system = platform.system()
744
- ip = None
745
-
746
- try:
747
- if system in ("Linux", "Darwin"):
748
- result = subprocess.run(["ifconfig"], capture_output=True, text=True, check=True)
749
- lines = result.stdout.splitlines()
750
-
751
- for line in lines:
752
- line = line.strip()
753
- # Unix ifconfig output format: "inet 192.168.1.100 netmask 0xffffff00 broadcast 192.168.1.255"
754
- if line.startswith("inet ") and "127.0.0.1" not in line:
755
- candidate_ip = line.split()[1]
756
- # Validate IP is not loopback or link-local
757
- if (candidate_ip and
758
- not candidate_ip.startswith("127.") and
759
- not candidate_ip.startswith("169.254")):
760
- ip = candidate_ip
761
- break
762
-
763
- elif system == "Windows":
764
- result = subprocess.run(["ipconfig"], capture_output=True, text=True, check=True)
765
- lines = result.stdout.splitlines()
766
-
767
- for line in lines:
768
- line = line.strip()
769
- # Windows ipconfig output format: " IPv4 Address. . . . . . . . . . . : 192.168.1.100"
770
- if "IPv4 Address" in line and ":" in line:
771
- candidate_ip = line.split(":")[-1].strip()
772
- # Validate IP is not loopback or link-local
773
- if (candidate_ip and
774
- not candidate_ip.startswith("127.") and
775
- not candidate_ip.startswith("169.254")):
776
- ip = candidate_ip
777
- break
778
-
779
- else:
780
- logger.warning(f"Unsupported platform: {system}")
781
- ip = None
782
-
783
- except Exception as e:
784
- logger.debug(f"IP detection failed on {system}: {e}")
785
- ip = None
786
-
787
- if ip:
788
- callback_url = f"http://{ip}:4321"
789
- merged_env_dict['CALLBACK_HOST_URL'] = callback_url
790
- logger.info(f"Auto-configured CALLBACK_HOST_URL to: {callback_url}")
791
- else:
792
- # Fallback for localhost
793
- callback_url = "http://host.docker.internal:4321"
794
- merged_env_dict['CALLBACK_HOST_URL'] = callback_url
795
- logger.info(f"Using Docker internal URL: {callback_url}")
796
- logger.info("For external tools, consider using ngrok or similar tunneling service.")
797
- else:
798
- logger.info(f"Using existing CALLBACK_HOST_URL: {callback_url}")
799
-
800
- return merged_env_dict
801
-
802
- def prepare_server_env_vars(user_env: dict = {}):
803
-
804
- default_env = read_env_file(get_default_env_file())
805
- dev_edition_source = get_dev_edition_source(user_env)
806
- default_registry_vars = get_default_registry_env_vars_by_dev_edition_source(default_env, user_env, source=dev_edition_source)
807
-
808
- # Update the default environment with the default registry variables only if they are not already set
809
- for key in default_registry_vars:
810
- if key not in default_env or not default_env[key]:
811
- default_env[key] = default_registry_vars[key]
812
-
813
- # Merge the default environment with the user environment
814
- merged_env_dict = {
815
- **default_env,
816
- **user_env,
817
- }
818
-
819
- merged_env_dict = apply_server_env_dict_defaults(merged_env_dict)
820
-
821
- # Auto-configure callback IP for async tools
822
- merged_env_dict = auto_configure_callback_ip(merged_env_dict)
823
-
824
- apply_llm_api_key_defaults(merged_env_dict)
825
-
826
- return merged_env_dict
827
-
828
318
  @server_app.command(name="start")
829
319
  def server_start(
830
320
  user_env_file: str = typer.Option(
@@ -868,18 +358,17 @@ def server_start(
868
358
  '--with-voice', '-v',
869
359
  help='Enable voice controller to interact with the chat via voice channels'
870
360
  ),
871
- experimental_with_langflow: bool = typer.Option(
361
+ with_langflow: bool = typer.Option(
872
362
  False,
873
- '--experimental-with-langflow',
874
- help='(Experimental) Enable Langflow UI, available at http://localhost:7861',
363
+ '--with-langflow',
364
+ help='Enable Langflow UI, available at http://localhost:7861',
875
365
  hidden=True
876
366
  ),
877
367
  ):
878
- confirm_accepts_license_agreement(accept_terms_and_conditions)
368
+ cli_config = Config()
369
+ confirm_accepts_license_agreement(accept_terms_and_conditions, cli_config)
879
370
 
880
- define_saas_wdu_runtime()
881
-
882
- ensure_docker_installed()
371
+ DockerUtils.ensure_docker_installed()
883
372
 
884
373
  if user_env_file and not Path(user_env_file).exists():
885
374
  logger.error(f"The specified environment file '{user_env_file}' does not exist.")
@@ -891,16 +380,20 @@ def server_start(
891
380
  else:
892
381
  logger.error(f"The specified docker-compose file '{custom_compose_file}' does not exist.")
893
382
  sys.exit(1)
383
+
384
+ env_service = EnvService(cli_config)
385
+
386
+ env_service.define_saas_wdu_runtime()
894
387
 
895
388
  #Run regardless, to allow this to set compose as 'None' when not in use
896
- set_compose_file_path_in_env(custom_compose_file)
389
+ env_service.set_compose_file_path_in_env(custom_compose_file)
897
390
 
898
- user_env = read_env_file(user_env_file) if user_env_file else {}
899
- persist_user_env(user_env, include_secrets=persist_env_secrets)
391
+ user_env = env_service.get_user_env(user_env_file=user_env_file, fallback_to_persisted_env=False)
392
+ env_service.persist_user_env(user_env, include_secrets=persist_env_secrets)
900
393
 
901
- merged_env_dict = prepare_server_env_vars(user_env)
394
+ merged_env_dict = env_service.prepare_server_env_vars(user_env=user_env, should_drop_auth_routes=False)
902
395
 
903
- if not _check_exclusive_observibility(experimental_with_langfuse, experimental_with_ibm_telemetry):
396
+ if not DockerUtils.check_exclusive_observability(experimental_with_langfuse, experimental_with_ibm_telemetry):
904
397
  logger.error("Please select either langfuse or ibm telemetry for observability not both")
905
398
  sys.exit(1)
906
399
 
@@ -910,30 +403,29 @@ def server_start(
910
403
 
911
404
  if with_doc_processing:
912
405
  merged_env_dict['DOCPROC_ENABLED'] = 'true'
913
- define_saas_wdu_runtime("local")
406
+ env_service.define_saas_wdu_runtime("local")
914
407
 
915
408
  if experimental_with_ibm_telemetry:
916
409
  merged_env_dict['USE_IBM_TELEMETRY'] = 'true'
917
410
 
918
- if experimental_with_langflow:
411
+ if with_langflow:
919
412
  merged_env_dict['LANGFLOW_ENABLED'] = 'true'
920
413
 
921
414
 
922
415
  try:
923
- dev_edition_source = get_dev_edition_source(merged_env_dict)
924
- docker_login_by_dev_edition_source(merged_env_dict, dev_edition_source)
416
+ DockerLoginService(env_service=env_service).login_by_dev_edition_source(merged_env_dict)
925
417
  except ValueError as e:
926
418
  logger.error(f"Error: {e}")
927
419
  sys.exit(1)
928
420
 
929
- final_env_file = write_merged_env_file(merged_env_dict)
421
+ final_env_file = env_service.write_merged_env_file(merged_env_dict)
930
422
 
931
423
  run_compose_lite(final_env_file=final_env_file,
932
424
  experimental_with_langfuse=experimental_with_langfuse,
933
425
  experimental_with_ibm_telemetry=experimental_with_ibm_telemetry,
934
426
  with_doc_processing=with_doc_processing,
935
427
  with_voice=with_voice,
936
- experimental_with_langflow=experimental_with_langflow)
428
+ with_langflow=with_langflow, env_service=env_service)
937
429
 
938
430
  run_db_migration()
939
431
 
@@ -963,7 +455,7 @@ def server_start(
963
455
  logger.info(f"You can access the observability platform Langfuse at http://localhost:3010, username: orchestrate@ibm.com, password: orchestrate")
964
456
  if with_doc_processing:
965
457
  logger.info(f"Document processing in Flows (Public Preview) has been enabled.")
966
- if experimental_with_langflow:
458
+ if with_langflow:
967
459
  logger.info("Langflow has been enabled, the Langflow UI is available at http://localhost:7861")
968
460
 
969
461
  @server_app.command(name="stop")
@@ -975,16 +467,16 @@ def server_stop(
975
467
  )
976
468
  ):
977
469
 
978
- ensure_docker_installed()
979
- default_env_path = get_default_env_file()
980
- merged_env_dict = merge_env(
470
+ DockerUtils.ensure_docker_installed()
471
+ default_env_path = EnvService.get_default_env_file()
472
+ merged_env_dict = EnvService.merge_env(
981
473
  default_env_path,
982
474
  Path(user_env_file) if user_env_file else None
983
475
  )
984
476
  merged_env_dict['WATSONX_SPACE_ID']='X'
985
477
  merged_env_dict['WATSONX_APIKEY']='X'
986
- apply_llm_api_key_defaults(merged_env_dict)
987
- final_env_file = write_merged_env_file(merged_env_dict)
478
+ EnvService.apply_llm_api_key_defaults(merged_env_dict)
479
+ final_env_file = EnvService.write_merged_env_file(merged_env_dict)
988
480
  run_compose_lite_down(final_env_file=final_env_file)
989
481
 
990
482
  @server_app.command(name="reset")
@@ -996,16 +488,16 @@ def server_reset(
996
488
  )
997
489
  ):
998
490
 
999
- ensure_docker_installed()
1000
- default_env_path = get_default_env_file()
1001
- merged_env_dict = merge_env(
491
+ DockerUtils.ensure_docker_installed()
492
+ default_env_path = EnvService.get_default_env_file()
493
+ merged_env_dict = EnvService.merge_env(
1002
494
  default_env_path,
1003
495
  Path(user_env_file) if user_env_file else None
1004
496
  )
1005
497
  merged_env_dict['WATSONX_SPACE_ID']='X'
1006
498
  merged_env_dict['WATSONX_APIKEY']='X'
1007
- apply_llm_api_key_defaults(merged_env_dict)
1008
- final_env_file = write_merged_env_file(merged_env_dict)
499
+ EnvService.apply_llm_api_key_defaults(merged_env_dict)
500
+ final_env_file = EnvService.write_merged_env_file(merged_env_dict)
1009
501
  run_compose_lite_down(final_env_file=final_env_file, is_reset=True)
1010
502
 
1011
503
  @server_app.command(name="logs")
@@ -1016,23 +508,21 @@ def server_logs(
1016
508
  help="Path to a .env file that overrides default.env. Then environment variables override both."
1017
509
  )
1018
510
  ):
1019
- ensure_docker_installed()
1020
- default_env_path = get_default_env_file()
1021
- merged_env_dict = merge_env(
511
+ DockerUtils.ensure_docker_installed()
512
+ default_env_path = EnvService.get_default_env_file()
513
+ merged_env_dict = EnvService.merge_env(
1022
514
  default_env_path,
1023
515
  Path(user_env_file) if user_env_file else None
1024
516
  )
1025
517
  merged_env_dict['WATSONX_SPACE_ID']='X'
1026
518
  merged_env_dict['WATSONX_APIKEY']='X'
1027
- apply_llm_api_key_defaults(merged_env_dict)
1028
- final_env_file = write_merged_env_file(merged_env_dict)
519
+ EnvService.apply_llm_api_key_defaults(merged_env_dict)
520
+ final_env_file = EnvService.write_merged_env_file(merged_env_dict)
1029
521
  run_compose_lite_logs(final_env_file=final_env_file)
1030
522
 
1031
523
  def run_db_migration() -> None:
1032
- compose_path = get_compose_file()
1033
- compose_command = ensure_docker_compose_installed()
1034
- default_env_path = get_default_env_file()
1035
- merged_env_dict = merge_env(default_env_path, user_env_path=None)
524
+ default_env_path = EnvService.get_default_env_file()
525
+ merged_env_dict = EnvService.merge_env(default_env_path, user_env_path=None)
1036
526
  merged_env_dict['WATSONX_SPACE_ID']='X'
1037
527
  merged_env_dict['WATSONX_APIKEY']='X'
1038
528
  merged_env_dict['WXAI_API_KEY'] = ''
@@ -1044,7 +534,7 @@ def run_db_migration() -> None:
1044
534
  merged_env_dict['ASSISTANT_EMBEDDINGS_SPACE_ID'] = ''
1045
535
  merged_env_dict['ROUTING_LLM_API_KEY'] = ''
1046
536
  merged_env_dict['ASSISTANT_LLM_API_KEY'] = ''
1047
- final_env_file = write_merged_env_file(merged_env_dict)
537
+ final_env_file = EnvService.write_merged_env_file(merged_env_dict)
1048
538
 
1049
539
 
1050
540
  pg_user = merged_env_dict.get("POSTGRES_USER","postgres")
@@ -1070,18 +560,13 @@ def run_db_migration() -> None:
1070
560
  done
1071
561
  '''
1072
562
 
1073
- command = compose_command + [
1074
- "-f", str(compose_path),
1075
- "--env-file", str(final_env_file),
1076
- "exec",
1077
- "wxo-server-db",
1078
- "bash",
1079
- "-c",
1080
- migration_command
1081
- ]
563
+ cli_config = Config()
564
+ env_service = EnvService(cli_config)
565
+ compose_core = DockerComposeCore(env_service)
1082
566
 
1083
- logger.info("Running Database Migration...")
1084
- result = subprocess.run(command, capture_output=False)
567
+ result = compose_core.service_container_bash_exec(service_name="wxo-server-db",
568
+ log_message="Running Database Migration...",
569
+ final_env_file=final_env_file, bash_command=migration_command)
1085
570
 
1086
571
  if result.returncode == 0:
1087
572
  logger.info("Migration ran successfully.")
@@ -1093,10 +578,8 @@ def run_db_migration() -> None:
1093
578
  sys.exit(1)
1094
579
 
1095
580
  def create_langflow_db() -> None:
1096
- compose_path = get_compose_file()
1097
- compose_command = ensure_docker_compose_installed()
1098
- default_env_path = get_default_env_file()
1099
- merged_env_dict = merge_env(default_env_path, user_env_path=None)
581
+ default_env_path = EnvService.get_default_env_file()
582
+ merged_env_dict = EnvService.merge_env(default_env_path, user_env_path=None)
1100
583
  merged_env_dict['WATSONX_SPACE_ID']='X'
1101
584
  merged_env_dict['WATSONX_APIKEY']='X'
1102
585
  merged_env_dict['WXAI_API_KEY'] = ''
@@ -1108,7 +591,7 @@ def create_langflow_db() -> None:
1108
591
  merged_env_dict['ASSISTANT_EMBEDDINGS_SPACE_ID'] = ''
1109
592
  merged_env_dict['ROUTING_LLM_API_KEY'] = ''
1110
593
  merged_env_dict['ASSISTANT_LLM_API_KEY'] = ''
1111
- final_env_file = write_merged_env_file(merged_env_dict)
594
+ final_env_file = EnvService.write_merged_env_file(merged_env_dict)
1112
595
 
1113
596
  pg_timeout = merged_env_dict.get('POSTGRES_READY_TIMEOUT','10')
1114
597
 
@@ -1130,18 +613,14 @@ def create_langflow_db() -> None:
1130
613
  psql -U {pg_user} -q -d postgres -c "GRANT CONNECT ON DATABASE langflow TO {pg_user}";
1131
614
  fi
1132
615
  """
1133
- command = compose_command + [
1134
- "-f", str(compose_path),
1135
- "--env-file", str(final_env_file),
1136
- "exec",
1137
- "wxo-server-db",
1138
- "bash",
1139
- "-c",
1140
- creation_command
1141
- ]
1142
-
1143
- logger.info("Preparing Langflow resources...")
1144
- result = subprocess.run(command, capture_output=False)
616
+
617
+ cli_config = Config()
618
+ env_service = EnvService(cli_config)
619
+ compose_core = DockerComposeCore(env_service)
620
+
621
+ result = compose_core.service_container_bash_exec(service_name="wxo-server-db",
622
+ log_message="Preparing Langflow resources...",
623
+ final_env_file=final_env_file, bash_command=creation_command)
1145
624
 
1146
625
  if result.returncode == 0:
1147
626
  logger.info("Langflow resources sucessfully created")
@@ -1182,22 +661,22 @@ def server_eject(
1182
661
  sys.exit(1)
1183
662
 
1184
663
  logger.warning("Changes to your docker compose file are not supported")
1185
-
1186
- compose_file_path = get_compose_file()
1187
664
 
665
+ cli_config = Config()
666
+ env_service = EnvService(cli_config)
667
+ compose_file_path = env_service.get_compose_file()
1188
668
  compose_output_file = get_next_free_file_iteration('docker-compose.yml')
1189
669
  logger.info(f"Exporting docker compose file to '{compose_output_file}'")
1190
670
 
1191
671
  shutil.copyfile(compose_file_path,compose_output_file)
1192
672
 
1193
-
1194
- user_env = read_env_file(user_env_file)
1195
- merged_env_dict = prepare_server_env_vars(user_env)
673
+ user_env = env_service.get_user_env(user_env_file=user_env_file, fallback_to_persisted_env=False)
674
+ merged_env_dict = env_service.prepare_server_env_vars(user_env=user_env, should_drop_auth_routes=False)
1196
675
 
1197
676
  env_output_file = get_next_free_file_iteration('server.env')
1198
677
  logger.info(f"Exporting env file to '{env_output_file}'")
1199
678
 
1200
- write_merged_env_file(merged_env=merged_env_dict,target_path=env_output_file)
679
+ env_service.write_merged_env_file(merged_env=merged_env_dict,target_path=env_output_file)
1201
680
 
1202
681
  logger.info(f"To make use of the exported configuration file run \"orchestrate server start -e {env_output_file} -f {compose_output_file}\"")
1203
682