ibm-watsonx-orchestrate 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. ibm_watsonx_orchestrate/__init__.py +28 -0
  2. ibm_watsonx_orchestrate/agent_builder/__init__.py +0 -0
  3. ibm_watsonx_orchestrate/agent_builder/agents/__init__.py +5 -0
  4. ibm_watsonx_orchestrate/agent_builder/agents/agent.py +27 -0
  5. ibm_watsonx_orchestrate/agent_builder/agents/assistant_agent.py +28 -0
  6. ibm_watsonx_orchestrate/agent_builder/agents/external_agent.py +28 -0
  7. ibm_watsonx_orchestrate/agent_builder/agents/types.py +204 -0
  8. ibm_watsonx_orchestrate/agent_builder/connections/__init__.py +27 -0
  9. ibm_watsonx_orchestrate/agent_builder/connections/connections.py +123 -0
  10. ibm_watsonx_orchestrate/agent_builder/connections/types.py +260 -0
  11. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/knowledge_base.py +27 -0
  12. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/knowledge_base_requests.py +59 -0
  13. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +243 -0
  14. ibm_watsonx_orchestrate/agent_builder/tools/__init__.py +4 -0
  15. ibm_watsonx_orchestrate/agent_builder/tools/base_tool.py +36 -0
  16. ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +332 -0
  17. ibm_watsonx_orchestrate/agent_builder/tools/python_tool.py +195 -0
  18. ibm_watsonx_orchestrate/agent_builder/tools/types.py +162 -0
  19. ibm_watsonx_orchestrate/agent_builder/utils/__init__.py +0 -0
  20. ibm_watsonx_orchestrate/agent_builder/utils/pydantic_utils.py +149 -0
  21. ibm_watsonx_orchestrate/cli/__init__.py +0 -0
  22. ibm_watsonx_orchestrate/cli/commands/__init__.py +0 -0
  23. ibm_watsonx_orchestrate/cli/commands/agents/agents_command.py +192 -0
  24. ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +660 -0
  25. ibm_watsonx_orchestrate/cli/commands/channels/channels_command.py +15 -0
  26. ibm_watsonx_orchestrate/cli/commands/channels/channels_controller.py +16 -0
  27. ibm_watsonx_orchestrate/cli/commands/channels/types.py +15 -0
  28. ibm_watsonx_orchestrate/cli/commands/channels/webchat/channels_webchat_command.py +32 -0
  29. ibm_watsonx_orchestrate/cli/commands/channels/webchat/channels_webchat_controller.py +141 -0
  30. ibm_watsonx_orchestrate/cli/commands/chat/chat_command.py +43 -0
  31. ibm_watsonx_orchestrate/cli/commands/connections/connections_command.py +307 -0
  32. ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +517 -0
  33. ibm_watsonx_orchestrate/cli/commands/environment/environment_command.py +78 -0
  34. ibm_watsonx_orchestrate/cli/commands/environment/environment_controller.py +189 -0
  35. ibm_watsonx_orchestrate/cli/commands/environment/types.py +9 -0
  36. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_command.py +79 -0
  37. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +201 -0
  38. ibm_watsonx_orchestrate/cli/commands/login/login_command.py +17 -0
  39. ibm_watsonx_orchestrate/cli/commands/models/models_command.py +128 -0
  40. ibm_watsonx_orchestrate/cli/commands/server/server_command.py +623 -0
  41. ibm_watsonx_orchestrate/cli/commands/settings/__init__.py +0 -0
  42. ibm_watsonx_orchestrate/cli/commands/settings/observability/__init__.py +0 -0
  43. ibm_watsonx_orchestrate/cli/commands/settings/observability/langfuse/__init__.py +0 -0
  44. ibm_watsonx_orchestrate/cli/commands/settings/observability/langfuse/langfuse_command.py +175 -0
  45. ibm_watsonx_orchestrate/cli/commands/settings/observability/observability_command.py +11 -0
  46. ibm_watsonx_orchestrate/cli/commands/settings/settings_command.py +10 -0
  47. ibm_watsonx_orchestrate/cli/commands/tools/tools_command.py +85 -0
  48. ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +564 -0
  49. ibm_watsonx_orchestrate/cli/commands/tools/types.py +10 -0
  50. ibm_watsonx_orchestrate/cli/config.py +226 -0
  51. ibm_watsonx_orchestrate/cli/main.py +32 -0
  52. ibm_watsonx_orchestrate/client/__init__.py +0 -0
  53. ibm_watsonx_orchestrate/client/agents/agent_client.py +46 -0
  54. ibm_watsonx_orchestrate/client/agents/assistant_agent_client.py +38 -0
  55. ibm_watsonx_orchestrate/client/agents/external_agent_client.py +38 -0
  56. ibm_watsonx_orchestrate/client/analytics/__init__.py +0 -0
  57. ibm_watsonx_orchestrate/client/analytics/llm/__init__.py +0 -0
  58. ibm_watsonx_orchestrate/client/analytics/llm/analytics_llm_client.py +50 -0
  59. ibm_watsonx_orchestrate/client/base_api_client.py +113 -0
  60. ibm_watsonx_orchestrate/client/base_service_instance.py +10 -0
  61. ibm_watsonx_orchestrate/client/client.py +71 -0
  62. ibm_watsonx_orchestrate/client/client_errors.py +359 -0
  63. ibm_watsonx_orchestrate/client/connections/__init__.py +10 -0
  64. ibm_watsonx_orchestrate/client/connections/connections_client.py +162 -0
  65. ibm_watsonx_orchestrate/client/connections/utils.py +27 -0
  66. ibm_watsonx_orchestrate/client/credentials.py +123 -0
  67. ibm_watsonx_orchestrate/client/knowledge_bases/knowledge_base_client.py +46 -0
  68. ibm_watsonx_orchestrate/client/local_service_instance.py +91 -0
  69. ibm_watsonx_orchestrate/client/service_instance.py +73 -0
  70. ibm_watsonx_orchestrate/client/tools/tool_client.py +41 -0
  71. ibm_watsonx_orchestrate/client/utils.py +95 -0
  72. ibm_watsonx_orchestrate/docker/compose-lite.yml +595 -0
  73. ibm_watsonx_orchestrate/docker/default.env +125 -0
  74. ibm_watsonx_orchestrate/docker/sdk/ibm_watsonx_orchestrate-0.6.0-py3-none-any.whl +0 -0
  75. ibm_watsonx_orchestrate/docker/sdk/ibm_watsonx_orchestrate-0.6.0.tar.gz +0 -0
  76. ibm_watsonx_orchestrate/docker/start-up.sh +61 -0
  77. ibm_watsonx_orchestrate/docker/tempus/common-config.yaml +1 -0
  78. ibm_watsonx_orchestrate/run/__init__.py +0 -0
  79. ibm_watsonx_orchestrate/run/connections.py +40 -0
  80. ibm_watsonx_orchestrate/utils/__init__.py +0 -0
  81. ibm_watsonx_orchestrate/utils/logging/__init__.py +0 -0
  82. ibm_watsonx_orchestrate/utils/logging/logger.py +26 -0
  83. ibm_watsonx_orchestrate/utils/logging/logging.yaml +18 -0
  84. ibm_watsonx_orchestrate/utils/utils.py +15 -0
  85. ibm_watsonx_orchestrate-1.0.0.dist-info/METADATA +34 -0
  86. ibm_watsonx_orchestrate-1.0.0.dist-info/RECORD +89 -0
  87. ibm_watsonx_orchestrate-1.0.0.dist-info/WHEEL +4 -0
  88. ibm_watsonx_orchestrate-1.0.0.dist-info/entry_points.txt +2 -0
  89. ibm_watsonx_orchestrate-1.0.0.dist-info/licenses/LICENSE +22 -0
@@ -0,0 +1,623 @@
1
+ import logging
2
+ import sys
3
+ import subprocess
4
+ import tempfile
5
+ from pathlib import Path
6
+ import requests
7
+ import time
8
+ import os
9
+ import platform
10
+
11
+
12
+ import typer
13
+ import importlib.resources as resources
14
+ import jwt
15
+
16
+ from dotenv import dotenv_values, load_dotenv
17
+
18
+ from ibm_watsonx_orchestrate.client.agents.agent_client import AgentClient
19
+ from ibm_watsonx_orchestrate.client.analytics.llm.analytics_llm_client import AnalyticsLLMClient, AnalyticsLLMConfig, \
20
+ AnalyticsLLMUpsertToolIdentifier
21
+ from ibm_watsonx_orchestrate.client.utils import instantiate_client, check_token_validity, is_local_dev
22
+
23
+ from ibm_watsonx_orchestrate.cli.commands.environment.environment_controller import _login, _decode_token
24
+ from ibm_watsonx_orchestrate.cli.config import PROTECTED_ENV_NAME, clear_protected_env_credentials_token, Config, \
25
+ AUTH_CONFIG_FILE_FOLDER, AUTH_CONFIG_FILE, AUTH_MCSP_TOKEN_OPT, ENVIRONMENTS_SECTION_HEADER, ENV_WXO_URL_OPT, \
26
+ CONTEXT_SECTION_HEADER, CONTEXT_ACTIVE_ENV_OPT, AUTH_SECTION_HEADER
27
+ from dotenv import dotenv_values, load_dotenv
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ server_app = typer.Typer(no_args_is_help=True)
32
+
33
+
34
+ def ensure_docker_installed() -> None:
35
+ try:
36
+ subprocess.run(["docker", "--version"], check=True, capture_output=True)
37
+ except (FileNotFoundError, subprocess.CalledProcessError):
38
+ logger.error("Unable to find an installed docker")
39
+ sys.exit(1)
40
+
41
+ def ensure_docker_compose_installed() -> list:
42
+ try:
43
+ subprocess.run(["docker", "compose", "version"], check=True, capture_output=True)
44
+ return ["docker", "compose"]
45
+ except (FileNotFoundError, subprocess.CalledProcessError):
46
+ pass
47
+
48
+ try:
49
+ subprocess.run(["docker-compose", "version"], check=True, capture_output=True)
50
+ return ["docker-compose"]
51
+ except (FileNotFoundError, subprocess.CalledProcessError):
52
+ typer.echo("Unable to find an installed docker-compose or docker compose")
53
+ sys.exit(1)
54
+
55
+ def docker_login(iam_api_key: str, registry_url: str) -> None:
56
+ logger.info(f"Logging into Docker registry: {registry_url} ...")
57
+ result = subprocess.run(
58
+ ["docker", "login", "-u", "iamapikey", "--password-stdin", registry_url],
59
+ input=iam_api_key.encode("utf-8"),
60
+ capture_output=True,
61
+ )
62
+ if result.returncode != 0:
63
+ logger.error(f"Error logging into Docker:\n{result.stderr.decode('utf-8')}")
64
+ sys.exit(1)
65
+ logger.info("Successfully logged in to Docker.")
66
+
67
+
68
+ def get_compose_file() -> Path:
69
+ with resources.as_file(
70
+ resources.files("ibm_watsonx_orchestrate.docker").joinpath("compose-lite.yml")
71
+ ) as compose_file:
72
+ return compose_file
73
+
74
+
75
+ def get_default_env_file() -> Path:
76
+ with resources.as_file(
77
+ resources.files("ibm_watsonx_orchestrate.docker").joinpath("default.env")
78
+ ) as env_file:
79
+ return env_file
80
+
81
+
82
+ def read_env_file(env_path: Path|str) -> dict:
83
+ return dotenv_values(str(env_path))
84
+
85
+ def merge_env(
86
+ default_env_path: Path,
87
+ user_env_path: Path | None
88
+ ) -> dict:
89
+
90
+ merged = dotenv_values(str(default_env_path))
91
+
92
+ if user_env_path is not None:
93
+ user_env = dotenv_values(str(user_env_path))
94
+ merged.update(user_env)
95
+
96
+
97
+ return merged
98
+
99
+
100
+ def apply_llm_api_key_defaults(env_dict: dict) -> None:
101
+ llm_value = env_dict.get("WATSONX_APIKEY")
102
+ if llm_value:
103
+ env_dict.setdefault("ASSISTANT_LLM_API_KEY", llm_value)
104
+ env_dict.setdefault("ASSISTANT_EMBEDDINGS_API_KEY", llm_value)
105
+ env_dict.setdefault("ROUTING_LLM_API_KEY", llm_value)
106
+ env_dict.setdefault("BAM_API_KEY", llm_value)
107
+ env_dict.setdefault("WXAI_API_KEY", llm_value)
108
+ space_value = env_dict.get("WATSONX_SPACE_ID")
109
+ if space_value:
110
+ env_dict.setdefault("ASSISTANT_LLM_SPACE_ID", space_value)
111
+ env_dict.setdefault("ASSISTANT_EMBEDDINGS_SPACE_ID", space_value)
112
+ env_dict.setdefault("ROUTING_LLM_SPACE_ID", space_value)
113
+
114
+ def write_merged_env_file(merged_env: dict) -> Path:
115
+ tmp = tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".env")
116
+ with tmp:
117
+ for key, val in merged_env.items():
118
+ tmp.write(f"{key}={val}\n")
119
+ return Path(tmp.name)
120
+
121
+
122
+ def get_dbtag_from_architecture(merged_env_dict: dict) -> str:
123
+ """Detects system architecture and returns the corresponding DBTAG."""
124
+ arch = platform.machine()
125
+
126
+ arm64_tag = merged_env_dict.get("ARM64DBTAG")
127
+ amd_tag = merged_env_dict.get("AMDDBTAG")
128
+
129
+ if arch in ["aarch64", "arm64"]:
130
+ return arm64_tag
131
+ else:
132
+ return amd_tag
133
+
134
+ def refresh_local_credentials() -> None:
135
+ """
136
+ Refresh the local credentials
137
+ """
138
+ clear_protected_env_credentials_token()
139
+ _login(name=PROTECTED_ENV_NAME, apikey=None)
140
+
141
+
142
+
143
+ def run_compose_lite(final_env_file: Path, experimental_with_langfuse=False, with_flow_runtime=False) -> None:
144
+ compose_path = get_compose_file()
145
+ compose_command = ensure_docker_compose_installed()
146
+ db_tag = read_env_file(final_env_file).get('DBTAG', None)
147
+ logger.info(f"Detected architecture: {platform.machine()}, using DBTAG: {db_tag}")
148
+
149
+ # Step 1: Start only the DB container
150
+ db_command = compose_command + [
151
+ "-f", str(compose_path),
152
+ "--env-file", str(final_env_file),
153
+ "up",
154
+ "-d",
155
+ "--remove-orphans",
156
+ "wxo-server-db"
157
+ ]
158
+
159
+ logger.info("Starting database container...")
160
+ result = subprocess.run(db_command, env=os.environ, capture_output=False)
161
+
162
+ if result.returncode != 0:
163
+ logger.error(f"Error starting DB container: {result.stderr}")
164
+ sys.exit(1)
165
+
166
+ logger.info("Database container started successfully. Now starting other services...")
167
+
168
+
169
+ # Step 2: Start all remaining services (except DB)
170
+ if experimental_with_langfuse:
171
+ command = compose_command + [
172
+ '--profile',
173
+ 'langfuse'
174
+ ]
175
+ else:
176
+ command = compose_command
177
+
178
+ # Check if we start the server with tempus-runtime.
179
+ if with_flow_runtime:
180
+ command += ['--profile', 'with-tempus-runtime']
181
+
182
+ command += [
183
+ "-f", str(compose_path),
184
+ "--env-file", str(final_env_file),
185
+ "up",
186
+ "--scale",
187
+ "ui=0",
188
+ "-d",
189
+ "--remove-orphans",
190
+ ]
191
+
192
+ logger.info("Starting docker-compose services...")
193
+ result = subprocess.run(command, capture_output=False)
194
+
195
+ if result.returncode == 0:
196
+ logger.info("Services started successfully.")
197
+ # Remove the temp file if successful
198
+ if final_env_file.exists():
199
+ final_env_file.unlink()
200
+ else:
201
+ error_message = result.stderr.decode('utf-8') if result.stderr else "Error occurred."
202
+ logger.error(
203
+ f"Error running docker-compose (temporary env file left at {final_env_file}):\n{error_message}"
204
+ )
205
+ sys.exit(1)
206
+
207
+ def wait_for_wxo_server_health_check(health_user, health_pass, timeout_seconds=90, interval_seconds=2):
208
+ url = "http://localhost:4321/api/v1/auth/token"
209
+ headers = {
210
+ 'Content-Type': 'application/x-www-form-urlencoded'
211
+ }
212
+ data = {
213
+ 'username': health_user,
214
+ 'password': health_pass
215
+ }
216
+
217
+ start_time = time.time()
218
+ errormsg = None
219
+ while time.time() - start_time <= timeout_seconds:
220
+ try:
221
+ response = requests.post(url, headers=headers, data=data)
222
+ if 200 <= response.status_code < 300:
223
+ return True
224
+ else:
225
+ logger.debug(f"Response code from healthcheck {response.status_code}")
226
+ except requests.RequestException as e:
227
+ errormsg = e
228
+ #print(f"Request failed: {e}")
229
+
230
+ time.sleep(interval_seconds)
231
+ if errormsg:
232
+ logger.error(f"Health check request failed: {errormsg}")
233
+ return False
234
+
235
+ def wait_for_wxo_ui_health_check(timeout_seconds=45, interval_seconds=2):
236
+ url = "http://localhost:3000/chat-lite"
237
+ logger.info("Waiting for UI component to be initialized...")
238
+ start_time = time.time()
239
+ while time.time() - start_time <= timeout_seconds:
240
+ try:
241
+ response = requests.get(url)
242
+ if 200 <= response.status_code < 300:
243
+ return True
244
+ else:
245
+ pass
246
+ #print(f"Response code from UI healthcheck {response.status_code}")
247
+ except requests.RequestException as e:
248
+ pass
249
+ #print(f"Request failed for UI: {e}")
250
+
251
+ time.sleep(interval_seconds)
252
+ logger.info("UI component is initialized")
253
+ return False
254
+
255
+ def run_compose_lite_ui(user_env_file: Path) -> bool:
256
+ compose_path = get_compose_file()
257
+ compose_command = ensure_docker_compose_installed()
258
+ ensure_docker_installed()
259
+ default_env_path = get_default_env_file()
260
+ logger.debug(f"user env file: {user_env_file}")
261
+ merged_env_dict = merge_env(
262
+ default_env_path,
263
+ user_env_file if user_env_file else None
264
+ )
265
+
266
+ _login(name=PROTECTED_ENV_NAME)
267
+ auth_cfg = Config(AUTH_CONFIG_FILE_FOLDER, AUTH_CONFIG_FILE)
268
+ existing_auth_config = auth_cfg.get(AUTH_SECTION_HEADER).get(PROTECTED_ENV_NAME, {})
269
+ existing_token = existing_auth_config.get(AUTH_MCSP_TOKEN_OPT) if existing_auth_config else None
270
+ token = jwt.decode(existing_token, options={"verify_signature": False})
271
+ tenant_id = token.get('woTenantId', None)
272
+ merged_env_dict['REACT_APP_TENANT_ID'] = tenant_id
273
+
274
+
275
+ registry_url = merged_env_dict.get("REGISTRY_URL")
276
+ if not registry_url:
277
+ logger.error("Error: REGISTRY_URL is required in the environment file.")
278
+ sys.exit(1)
279
+
280
+ agent_client = instantiate_client(AgentClient)
281
+ agents = agent_client.get()
282
+ if not agents:
283
+ logger.error("No agents found for the current environment. Please create an agent before starting the chat.")
284
+ sys.exit(1)
285
+
286
+ iam_api_key = merged_env_dict.get("DOCKER_IAM_KEY")
287
+ if iam_api_key:
288
+ docker_login(iam_api_key, registry_url)
289
+
290
+ #These are to removed warning and not used in UI component
291
+ if not 'WATSONX_SPACE_ID' in merged_env_dict:
292
+ merged_env_dict['WATSONX_SPACE_ID']='X'
293
+ if not 'WATSONX_APIKEY' in merged_env_dict:
294
+ merged_env_dict['WATSONX_APIKEY']='X'
295
+ apply_llm_api_key_defaults(merged_env_dict)
296
+
297
+ final_env_file = write_merged_env_file(merged_env_dict)
298
+
299
+ logger.info("Waiting for orchestrate server to be fully started and ready...")
300
+
301
+ health_check_timeout = int(merged_env_dict["HEALTH_TIMEOUT"]) if "HEALTH_TIMEOUT" in merged_env_dict else 120
302
+ is_successful_server_healthcheck = wait_for_wxo_server_health_check(merged_env_dict['WXO_USER'], merged_env_dict['WXO_PASS'], timeout_seconds=health_check_timeout)
303
+ if not is_successful_server_healthcheck:
304
+ logger.error("Healthcheck failed orchestrate server. Make sure you start the server components with `orchestrate server start` before trying to start the chat UI")
305
+ return False
306
+
307
+ command = compose_command + [
308
+ "-f", str(compose_path),
309
+ "--env-file", str(final_env_file),
310
+ "up",
311
+ "ui",
312
+ "-d",
313
+ "--remove-orphans"
314
+ ]
315
+
316
+ logger.info(f"Starting docker-compose UI service...")
317
+ result = subprocess.run(command, capture_output=False)
318
+
319
+ if result.returncode == 0:
320
+ logger.info("Chat UI Service started successfully.")
321
+ # Remove the temp file if successful
322
+ if final_env_file.exists():
323
+ final_env_file.unlink()
324
+ else:
325
+ error_message = result.stderr.decode('utf-8') if result.stderr else "Error occurred."
326
+ logger.error(
327
+ f"Error running docker-compose (temporary env file left at {final_env_file}):\n{error_message}"
328
+ )
329
+ return False
330
+
331
+ is_successful_ui_healthcheck = wait_for_wxo_ui_health_check()
332
+ if not is_successful_ui_healthcheck:
333
+ logger.error("The Chat UI service did not initialize within the expected time. Check the logs for any errors.")
334
+
335
+ return True
336
+
337
+ def run_compose_lite_down_ui(user_env_file: Path, is_reset: bool = False) -> None:
338
+ compose_path = get_compose_file()
339
+ compose_command = ensure_docker_compose_installed()
340
+
341
+
342
+ ensure_docker_installed()
343
+ default_env_path = get_default_env_file()
344
+ merged_env_dict = merge_env(
345
+ default_env_path,
346
+ user_env_file
347
+ )
348
+ merged_env_dict['WATSONX_SPACE_ID']='X'
349
+ merged_env_dict['WATSONX_APIKEY']='X'
350
+ apply_llm_api_key_defaults(merged_env_dict)
351
+ final_env_file = write_merged_env_file(merged_env_dict)
352
+
353
+ command = compose_command + [
354
+ "-f", str(compose_path),
355
+ "--env-file", str(final_env_file),
356
+ "down",
357
+ "ui"
358
+ ]
359
+
360
+ if is_reset:
361
+ command.append("--volumes")
362
+ logger.info("Stopping docker-compose UI service and resetting volumes...")
363
+ else:
364
+ logger.info("Stopping docker-compose UI service...")
365
+
366
+ result = subprocess.run(command, capture_output=False)
367
+
368
+ if result.returncode == 0:
369
+ logger.info("UI service stopped successfully.")
370
+ # Remove the temp file if successful
371
+ if final_env_file.exists():
372
+ final_env_file.unlink()
373
+ else:
374
+ error_message = result.stderr.decode('utf-8') if result.stderr else "Error occurred."
375
+ logger.error(
376
+ f"Error running docker-compose (temporary env file left at {final_env_file}):\n{error_message}"
377
+ )
378
+ sys.exit(1)
379
+
380
+ def run_compose_lite_down(final_env_file: Path, is_reset: bool = False) -> None:
381
+ compose_path = get_compose_file()
382
+ compose_command = ensure_docker_compose_installed()
383
+
384
+ command = compose_command + [
385
+ '--profile', '*',
386
+ "-f", str(compose_path),
387
+ "--env-file", str(final_env_file),
388
+ "down"
389
+ ]
390
+
391
+ if is_reset:
392
+ command.append("--volumes")
393
+ logger.info("Stopping docker-compose services and resetting volumes...")
394
+ else:
395
+ logger.info("Stopping docker-compose services...")
396
+
397
+ result = subprocess.run(command, capture_output=False)
398
+
399
+ if result.returncode == 0:
400
+ logger.info("Services stopped successfully.")
401
+ # Remove the temp file if successful
402
+ if final_env_file.exists():
403
+ final_env_file.unlink()
404
+ else:
405
+ error_message = result.stderr.decode('utf-8') if result.stderr else "Error occurred."
406
+ logger.error(
407
+ f"Error running docker-compose (temporary env file left at {final_env_file}):\n{error_message}"
408
+ )
409
+ sys.exit(1)
410
+
411
+
412
+ def run_compose_lite_logs(final_env_file: Path, is_reset: bool = False) -> None:
413
+ compose_path = get_compose_file()
414
+ compose_command = ensure_docker_compose_installed()
415
+
416
+ command = compose_command + [
417
+ "-f", str(compose_path),
418
+ "--env-file", str(final_env_file),
419
+ "logs",
420
+ "-f"
421
+ ]
422
+
423
+ logger.info("Docker Logs...")
424
+
425
+ result = subprocess.run(command, capture_output=False)
426
+
427
+ if result.returncode == 0:
428
+ logger.info("End of docker logs")
429
+ # Remove the temp file if successful
430
+ if final_env_file.exists():
431
+ final_env_file.unlink()
432
+ else:
433
+ error_message = result.stderr.decode('utf-8') if result.stderr else "Error occurred."
434
+ logger.error(
435
+ f"Error running docker-compose (temporary env file left at {final_env_file}):\n{error_message}"
436
+ )
437
+ sys.exit(1)
438
+
439
+ @server_app.command(name="start")
440
+ def server_start(
441
+ user_env_file: str = typer.Option(
442
+ None,
443
+ "--env-file", '-e',
444
+ help="Path to a .env file that overrides default.env. Then environment variables override both."
445
+ ),
446
+ experimental_with_langfuse: bool = typer.Option(
447
+ False,
448
+ '--with-langfuse', '-l',
449
+ help=''
450
+ ),
451
+ with_flow_runtime: bool = typer.Option(
452
+ False,
453
+ '--with-tempus-runtime', '-f',
454
+ help='Option to start server with tempus-runtime.',
455
+ hidden=True
456
+ )
457
+ ):
458
+ if user_env_file and not Path(user_env_file).exists():
459
+ logger.error(f"Error: The specified environment file '{user_env_file}' does not exist.")
460
+ sys.exit(1)
461
+ ensure_docker_installed()
462
+
463
+ default_env_path = get_default_env_file()
464
+
465
+ merged_env_dict = merge_env(
466
+ default_env_path,
467
+ Path(user_env_file) if user_env_file else None
468
+ )
469
+
470
+ merged_env_dict['DBTAG'] = get_dbtag_from_architecture(merged_env_dict=merged_env_dict)
471
+
472
+ iam_api_key = merged_env_dict.get("DOCKER_IAM_KEY")
473
+ if not iam_api_key:
474
+ logger.error("Error: DOCKER_IAM_KEY is required in the environment file.")
475
+ sys.exit(1)
476
+
477
+ registry_url = merged_env_dict.get("REGISTRY_URL")
478
+ if not registry_url:
479
+ logger.error("Error: REGISTRY_URL is required in the environment file.")
480
+ sys.exit(1)
481
+
482
+ docker_login(iam_api_key, registry_url)
483
+
484
+ apply_llm_api_key_defaults(merged_env_dict)
485
+
486
+
487
+ final_env_file = write_merged_env_file(merged_env_dict)
488
+ run_compose_lite(final_env_file=final_env_file, experimental_with_langfuse=experimental_with_langfuse, with_flow_runtime=with_flow_runtime)
489
+
490
+ run_db_migration()
491
+
492
+ logger.info("Waiting for orchestrate server to be fully initialized and ready...")
493
+
494
+ health_check_timeout = int(merged_env_dict["HEALTH_TIMEOUT"]) if "HEALTH_TIMEOUT" in merged_env_dict else (7 * 60)
495
+ is_successful_server_healthcheck = wait_for_wxo_server_health_check(merged_env_dict['WXO_USER'], merged_env_dict['WXO_PASS'], timeout_seconds=health_check_timeout)
496
+ if is_successful_server_healthcheck:
497
+ logger.info("Orchestrate services initialized successfully")
498
+ else:
499
+ logger.error(
500
+ "The server did not successfully start within the given timeout. This is either an indication that something "
501
+ f"went wrong, or that the server simply did not start within {health_check_timeout} seconds. Please check the logs with "
502
+ "`orchestrate server logs`, or consider increasing the timeout by adding `HEALTH_TIMEOUT=number-of-seconds` "
503
+ "to your env file."
504
+ )
505
+ exit(1)
506
+
507
+ try:
508
+ refresh_local_credentials()
509
+ except:
510
+ logger.warning("Failed to refresh local credentials, please run `orchestrate env activate local`")
511
+
512
+ logger.info(f"You can run `orchestrate env activate local` to set your environment or `orchestrate chat start` to start the UI service and begin chatting.")
513
+
514
+ if with_flow_runtime:
515
+ logger.info(f"Starting with flow runtime")
516
+
517
+ @server_app.command(name="stop")
518
+ def server_stop(
519
+ user_env_file: str = typer.Option(
520
+ None,
521
+ "--env-file", '-e',
522
+ help="Path to a .env file that overrides default.env. Then environment variables override both."
523
+ )
524
+ ):
525
+ ensure_docker_installed()
526
+ default_env_path = get_default_env_file()
527
+ merged_env_dict = merge_env(
528
+ default_env_path,
529
+ Path(user_env_file) if user_env_file else None
530
+ )
531
+ merged_env_dict['WATSONX_SPACE_ID']='X'
532
+ merged_env_dict['WATSONX_APIKEY']='X'
533
+ apply_llm_api_key_defaults(merged_env_dict)
534
+ final_env_file = write_merged_env_file(merged_env_dict)
535
+ run_compose_lite_down(final_env_file=final_env_file)
536
+
537
+ @server_app.command(name="reset")
538
+ def server_reset(
539
+ user_env_file: str = typer.Option(
540
+ None,
541
+ "--env-file", '-e',
542
+ help="Path to a .env file that overrides default.env. Then environment variables override both."
543
+ )
544
+ ):
545
+
546
+ ensure_docker_installed()
547
+ default_env_path = get_default_env_file()
548
+ merged_env_dict = merge_env(
549
+ default_env_path,
550
+ Path(user_env_file) if user_env_file else None
551
+ )
552
+ merged_env_dict['WATSONX_SPACE_ID']='X'
553
+ merged_env_dict['WATSONX_APIKEY']='X'
554
+ apply_llm_api_key_defaults(merged_env_dict)
555
+ final_env_file = write_merged_env_file(merged_env_dict)
556
+ run_compose_lite_down(final_env_file=final_env_file, is_reset=True)
557
+
558
+ @server_app.command(name="logs")
559
+ def server_logs(
560
+ user_env_file: str = typer.Option(
561
+ None,
562
+ "--env-file", '-e',
563
+ help="Path to a .env file that overrides default.env. Then environment variables override both."
564
+ )
565
+ ):
566
+ ensure_docker_installed()
567
+ default_env_path = get_default_env_file()
568
+ merged_env_dict = merge_env(
569
+ default_env_path,
570
+ Path(user_env_file) if user_env_file else None
571
+ )
572
+ merged_env_dict['WATSONX_SPACE_ID']='X'
573
+ merged_env_dict['WATSONX_APIKEY']='X'
574
+ apply_llm_api_key_defaults(merged_env_dict)
575
+ final_env_file = write_merged_env_file(merged_env_dict)
576
+ run_compose_lite_logs(final_env_file=final_env_file)
577
+
578
+ def run_db_migration() -> None:
579
+ compose_path = get_compose_file()
580
+ compose_command = ensure_docker_compose_installed()
581
+
582
+ command = compose_command + [
583
+ "-f", str(compose_path),
584
+ "exec",
585
+ "wxo-server-db",
586
+ "bash",
587
+ "-c",
588
+ '''
589
+ APPLIED_MIGRATIONS_FILE="/var/lib/postgresql/applied_migrations/applied_migrations.txt"
590
+ touch "$APPLIED_MIGRATIONS_FILE"
591
+
592
+ for file in /docker-entrypoint-initdb.d/*.sql; do
593
+ filename=$(basename "$file")
594
+
595
+ if grep -Fxq "$filename" "$APPLIED_MIGRATIONS_FILE"; then
596
+ echo "Skipping already applied migration: $filename"
597
+ else
598
+ echo "Applying migration: $filename"
599
+ if psql -U postgres -d postgres -q -f "$file" > /dev/null 2>&1; then
600
+ echo "$filename" >> "$APPLIED_MIGRATIONS_FILE"
601
+ else
602
+ echo "Error applying $filename. Stopping migrations."
603
+ exit 1
604
+ fi
605
+ fi
606
+ done
607
+ '''
608
+ ]
609
+
610
+ logger.info("Running Database Migration...")
611
+ result = subprocess.run(command, capture_output=False)
612
+
613
+ if result.returncode == 0:
614
+ logger.info("Migration ran successfully.")
615
+ else:
616
+ error_message = result.stderr.decode('utf-8') if result.stderr else "Error occurred."
617
+ logger.error(
618
+ f"Error running database migration):\n{error_message}"
619
+ )
620
+ sys.exit(1)
621
+
622
+ if __name__ == "__main__":
623
+ server_app()