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.
- ibm_watsonx_orchestrate/__init__.py +28 -0
- ibm_watsonx_orchestrate/agent_builder/__init__.py +0 -0
- ibm_watsonx_orchestrate/agent_builder/agents/__init__.py +5 -0
- ibm_watsonx_orchestrate/agent_builder/agents/agent.py +27 -0
- ibm_watsonx_orchestrate/agent_builder/agents/assistant_agent.py +28 -0
- ibm_watsonx_orchestrate/agent_builder/agents/external_agent.py +28 -0
- ibm_watsonx_orchestrate/agent_builder/agents/types.py +204 -0
- ibm_watsonx_orchestrate/agent_builder/connections/__init__.py +27 -0
- ibm_watsonx_orchestrate/agent_builder/connections/connections.py +123 -0
- ibm_watsonx_orchestrate/agent_builder/connections/types.py +260 -0
- ibm_watsonx_orchestrate/agent_builder/knowledge_bases/knowledge_base.py +27 -0
- ibm_watsonx_orchestrate/agent_builder/knowledge_bases/knowledge_base_requests.py +59 -0
- ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +243 -0
- ibm_watsonx_orchestrate/agent_builder/tools/__init__.py +4 -0
- ibm_watsonx_orchestrate/agent_builder/tools/base_tool.py +36 -0
- ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +332 -0
- ibm_watsonx_orchestrate/agent_builder/tools/python_tool.py +195 -0
- ibm_watsonx_orchestrate/agent_builder/tools/types.py +162 -0
- ibm_watsonx_orchestrate/agent_builder/utils/__init__.py +0 -0
- ibm_watsonx_orchestrate/agent_builder/utils/pydantic_utils.py +149 -0
- ibm_watsonx_orchestrate/cli/__init__.py +0 -0
- ibm_watsonx_orchestrate/cli/commands/__init__.py +0 -0
- ibm_watsonx_orchestrate/cli/commands/agents/agents_command.py +192 -0
- ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +660 -0
- ibm_watsonx_orchestrate/cli/commands/channels/channels_command.py +15 -0
- ibm_watsonx_orchestrate/cli/commands/channels/channels_controller.py +16 -0
- ibm_watsonx_orchestrate/cli/commands/channels/types.py +15 -0
- ibm_watsonx_orchestrate/cli/commands/channels/webchat/channels_webchat_command.py +32 -0
- ibm_watsonx_orchestrate/cli/commands/channels/webchat/channels_webchat_controller.py +141 -0
- ibm_watsonx_orchestrate/cli/commands/chat/chat_command.py +43 -0
- ibm_watsonx_orchestrate/cli/commands/connections/connections_command.py +307 -0
- ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +517 -0
- ibm_watsonx_orchestrate/cli/commands/environment/environment_command.py +78 -0
- ibm_watsonx_orchestrate/cli/commands/environment/environment_controller.py +189 -0
- ibm_watsonx_orchestrate/cli/commands/environment/types.py +9 -0
- ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_command.py +79 -0
- ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +201 -0
- ibm_watsonx_orchestrate/cli/commands/login/login_command.py +17 -0
- ibm_watsonx_orchestrate/cli/commands/models/models_command.py +128 -0
- ibm_watsonx_orchestrate/cli/commands/server/server_command.py +623 -0
- ibm_watsonx_orchestrate/cli/commands/settings/__init__.py +0 -0
- ibm_watsonx_orchestrate/cli/commands/settings/observability/__init__.py +0 -0
- ibm_watsonx_orchestrate/cli/commands/settings/observability/langfuse/__init__.py +0 -0
- ibm_watsonx_orchestrate/cli/commands/settings/observability/langfuse/langfuse_command.py +175 -0
- ibm_watsonx_orchestrate/cli/commands/settings/observability/observability_command.py +11 -0
- ibm_watsonx_orchestrate/cli/commands/settings/settings_command.py +10 -0
- ibm_watsonx_orchestrate/cli/commands/tools/tools_command.py +85 -0
- ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +564 -0
- ibm_watsonx_orchestrate/cli/commands/tools/types.py +10 -0
- ibm_watsonx_orchestrate/cli/config.py +226 -0
- ibm_watsonx_orchestrate/cli/main.py +32 -0
- ibm_watsonx_orchestrate/client/__init__.py +0 -0
- ibm_watsonx_orchestrate/client/agents/agent_client.py +46 -0
- ibm_watsonx_orchestrate/client/agents/assistant_agent_client.py +38 -0
- ibm_watsonx_orchestrate/client/agents/external_agent_client.py +38 -0
- ibm_watsonx_orchestrate/client/analytics/__init__.py +0 -0
- ibm_watsonx_orchestrate/client/analytics/llm/__init__.py +0 -0
- ibm_watsonx_orchestrate/client/analytics/llm/analytics_llm_client.py +50 -0
- ibm_watsonx_orchestrate/client/base_api_client.py +113 -0
- ibm_watsonx_orchestrate/client/base_service_instance.py +10 -0
- ibm_watsonx_orchestrate/client/client.py +71 -0
- ibm_watsonx_orchestrate/client/client_errors.py +359 -0
- ibm_watsonx_orchestrate/client/connections/__init__.py +10 -0
- ibm_watsonx_orchestrate/client/connections/connections_client.py +162 -0
- ibm_watsonx_orchestrate/client/connections/utils.py +27 -0
- ibm_watsonx_orchestrate/client/credentials.py +123 -0
- ibm_watsonx_orchestrate/client/knowledge_bases/knowledge_base_client.py +46 -0
- ibm_watsonx_orchestrate/client/local_service_instance.py +91 -0
- ibm_watsonx_orchestrate/client/service_instance.py +73 -0
- ibm_watsonx_orchestrate/client/tools/tool_client.py +41 -0
- ibm_watsonx_orchestrate/client/utils.py +95 -0
- ibm_watsonx_orchestrate/docker/compose-lite.yml +595 -0
- ibm_watsonx_orchestrate/docker/default.env +125 -0
- ibm_watsonx_orchestrate/docker/sdk/ibm_watsonx_orchestrate-0.6.0-py3-none-any.whl +0 -0
- ibm_watsonx_orchestrate/docker/sdk/ibm_watsonx_orchestrate-0.6.0.tar.gz +0 -0
- ibm_watsonx_orchestrate/docker/start-up.sh +61 -0
- ibm_watsonx_orchestrate/docker/tempus/common-config.yaml +1 -0
- ibm_watsonx_orchestrate/run/__init__.py +0 -0
- ibm_watsonx_orchestrate/run/connections.py +40 -0
- ibm_watsonx_orchestrate/utils/__init__.py +0 -0
- ibm_watsonx_orchestrate/utils/logging/__init__.py +0 -0
- ibm_watsonx_orchestrate/utils/logging/logger.py +26 -0
- ibm_watsonx_orchestrate/utils/logging/logging.yaml +18 -0
- ibm_watsonx_orchestrate/utils/utils.py +15 -0
- ibm_watsonx_orchestrate-1.0.0.dist-info/METADATA +34 -0
- ibm_watsonx_orchestrate-1.0.0.dist-info/RECORD +89 -0
- ibm_watsonx_orchestrate-1.0.0.dist-info/WHEEL +4 -0
- ibm_watsonx_orchestrate-1.0.0.dist-info/entry_points.txt +2 -0
- 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()
|
File without changes
|
File without changes
|
File without changes
|