ibm-watsonx-orchestrate 1.7.0a0__py3-none-any.whl → 1.8.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 (57) hide show
  1. ibm_watsonx_orchestrate/__init__.py +1 -1
  2. ibm_watsonx_orchestrate/agent_builder/agents/agent.py +3 -3
  3. ibm_watsonx_orchestrate/agent_builder/agents/assistant_agent.py +3 -2
  4. ibm_watsonx_orchestrate/agent_builder/agents/external_agent.py +3 -2
  5. ibm_watsonx_orchestrate/agent_builder/agents/types.py +26 -9
  6. ibm_watsonx_orchestrate/agent_builder/agents/webchat_customizations/prompts.py +1 -0
  7. ibm_watsonx_orchestrate/agent_builder/connections/connections.py +25 -10
  8. ibm_watsonx_orchestrate/agent_builder/connections/types.py +5 -9
  9. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/knowledge_base_requests.py +1 -22
  10. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +1 -17
  11. ibm_watsonx_orchestrate/agent_builder/tools/base_tool.py +2 -1
  12. ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +14 -13
  13. ibm_watsonx_orchestrate/agent_builder/tools/python_tool.py +136 -92
  14. ibm_watsonx_orchestrate/agent_builder/tools/types.py +10 -9
  15. ibm_watsonx_orchestrate/cli/commands/agents/agents_command.py +7 -7
  16. ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +35 -7
  17. ibm_watsonx_orchestrate/cli/commands/channels/webchat/channels_webchat_controller.py +33 -23
  18. ibm_watsonx_orchestrate/cli/commands/chat/chat_command.py +2 -0
  19. ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +6 -4
  20. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_command.py +65 -0
  21. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_controller.py +293 -0
  22. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_server_controller.py +154 -0
  23. ibm_watsonx_orchestrate/cli/commands/environment/environment_controller.py +6 -6
  24. ibm_watsonx_orchestrate/cli/commands/environment/types.py +2 -0
  25. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_command.py +118 -30
  26. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_controller.py +22 -9
  27. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_command.py +0 -18
  28. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +33 -19
  29. ibm_watsonx_orchestrate/cli/commands/models/models_command.py +1 -1
  30. ibm_watsonx_orchestrate/cli/commands/server/server_command.py +66 -9
  31. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_command.py +1 -1
  32. ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +93 -14
  33. ibm_watsonx_orchestrate/cli/config.py +3 -3
  34. ibm_watsonx_orchestrate/cli/init_helper.py +10 -1
  35. ibm_watsonx_orchestrate/cli/main.py +5 -0
  36. ibm_watsonx_orchestrate/client/base_api_client.py +12 -0
  37. ibm_watsonx_orchestrate/client/copilot/cpe/copilot_cpe_client.py +66 -0
  38. ibm_watsonx_orchestrate/client/knowledge_bases/knowledge_base_client.py +1 -1
  39. ibm_watsonx_orchestrate/client/local_service_instance.py +3 -1
  40. ibm_watsonx_orchestrate/client/service_instance.py +33 -7
  41. ibm_watsonx_orchestrate/client/utils.py +48 -9
  42. ibm_watsonx_orchestrate/docker/compose-lite.yml +16 -4
  43. ibm_watsonx_orchestrate/docker/default.env +25 -15
  44. ibm_watsonx_orchestrate/flow_builder/flows/__init__.py +3 -1
  45. ibm_watsonx_orchestrate/flow_builder/flows/decorators.py +4 -2
  46. ibm_watsonx_orchestrate/flow_builder/flows/events.py +10 -9
  47. ibm_watsonx_orchestrate/flow_builder/flows/flow.py +91 -20
  48. ibm_watsonx_orchestrate/flow_builder/node.py +12 -1
  49. ibm_watsonx_orchestrate/flow_builder/types.py +169 -16
  50. ibm_watsonx_orchestrate/flow_builder/utils.py +121 -6
  51. ibm_watsonx_orchestrate/utils/exceptions.py +23 -0
  52. {ibm_watsonx_orchestrate-1.7.0a0.dist-info → ibm_watsonx_orchestrate-1.8.0b0.dist-info}/METADATA +5 -5
  53. {ibm_watsonx_orchestrate-1.7.0a0.dist-info → ibm_watsonx_orchestrate-1.8.0b0.dist-info}/RECORD +56 -52
  54. ibm_watsonx_orchestrate/flow_builder/resources/flow_status.openapi.yml +0 -66
  55. {ibm_watsonx_orchestrate-1.7.0a0.dist-info → ibm_watsonx_orchestrate-1.8.0b0.dist-info}/WHEEL +0 -0
  56. {ibm_watsonx_orchestrate-1.7.0a0.dist-info → ibm_watsonx_orchestrate-1.8.0b0.dist-info}/entry_points.txt +0 -0
  57. {ibm_watsonx_orchestrate-1.7.0a0.dist-info → ibm_watsonx_orchestrate-1.8.0b0.dist-info}/licenses/LICENSE +0 -0
@@ -5,10 +5,12 @@ import os
5
5
  import yaml
6
6
  import csv
7
7
  import rich
8
- from pathlib import Path
9
8
  import sys
10
- from dotenv import dotenv_values
9
+ import shutil
11
10
 
11
+ from rich.panel import Panel
12
+ from pathlib import Path
13
+ from dotenv import dotenv_values
12
14
  from typing import Optional
13
15
  from typing_extensions import Annotated
14
16
 
@@ -46,6 +48,32 @@ def validate_watsonx_credentials(user_env_file: str) -> bool:
46
48
  os.environ.update({key: user_env[key] for key in required_keys})
47
49
  logger.info("WatsonX credentials validated successfully.")
48
50
 
51
+ def read_csv(data_path: str, delimiter="\t"):
52
+ data = []
53
+ with open(data_path, "r") as f:
54
+ tsv_reader = csv.reader(f, delimiter=delimiter)
55
+ for line in tsv_reader:
56
+ data.append(line)
57
+
58
+ return data
59
+
60
+ def performance_test(agent_name, data_path, output_dir = None, user_env_file = None):
61
+ test_data = read_csv(data_path)
62
+
63
+ controller = EvaluationsController()
64
+ generated_performance_tests = controller.generate_performance_test(agent_name, test_data)
65
+
66
+ generated_perf_test_dir = Path(output_dir) / "generated_performance_tests"
67
+ generated_perf_test_dir.mkdir(exist_ok=True, parents=True)
68
+
69
+ for idx, test in enumerate(generated_performance_tests):
70
+ test_name = f"validate_external_agent_evaluation_test_{idx}.json"
71
+ with open(generated_perf_test_dir / test_name, encoding="utf-8", mode="w+") as f:
72
+ json.dump(test, f, indent=4)
73
+
74
+ rich.print(f"Performance test cases saved at path '{str(generated_perf_test_dir)}'")
75
+ rich.print("[gold3]Running Performance Test")
76
+ evaluate(output_dir=output_dir, test_paths=str(generated_perf_test_dir))
49
77
 
50
78
  @evaluation_app.command(name="evaluate", help="Evaluate an agent against a set of test cases")
51
79
  def evaluate(
@@ -115,7 +143,7 @@ def generate(
115
143
  stories_path: Annotated[
116
144
  str,
117
145
  typer.Option(
118
- "--stories_path", "-s",
146
+ "--stories-path", "-s",
119
147
  help="Path to the CSV file containing user stories for test case generation. "
120
148
  "The file has 'story' and 'agent' columns."
121
149
  )
@@ -123,14 +151,14 @@ def generate(
123
151
  tools_path: Annotated[
124
152
  str,
125
153
  typer.Option(
126
- "--tools_path", "-t",
154
+ "--tools-path", "-t",
127
155
  help="Path to the directory containing tool definitions."
128
156
  )
129
157
  ],
130
158
  output_dir: Annotated[
131
159
  Optional[str],
132
160
  typer.Option(
133
- "--output_dir", "-o",
161
+ "--output-dir", "-o",
134
162
  help="Directory to save the generated test cases."
135
163
  )
136
164
  ] = None,
@@ -151,7 +179,7 @@ def generate(
151
179
  def analyze(data_path: Annotated[
152
180
  str,
153
181
  typer.Option(
154
- "--data_path", "-d",
182
+ "--data-path", "-d",
155
183
  help="Path to the directory that has the saved results"
156
184
  )
157
185
  ],
@@ -167,30 +195,31 @@ def analyze(data_path: Annotated[
167
195
  controller = EvaluationsController()
168
196
  controller.analyze(data_path=data_path)
169
197
 
170
-
171
- @evaluation_app.command(name="validate_external", help="Validate an external agent against a set of inputs")
198
+ @evaluation_app.command(name="validate-external", help="Validate an external agent against a set of inputs")
172
199
  def validate_external(
173
200
  data_path: Annotated[
174
201
  str,
175
202
  typer.Option(
176
- "--csv", "-c",
177
- help="Path to .csv file of inputs"
203
+ "--tsv", "-t",
204
+ help="Path to .tsv file of inputs"
178
205
  )
179
206
  ],
180
- config: Annotated[
207
+ external_agent_config: Annotated[
181
208
  str,
182
209
  typer.Option(
183
- "--config", "-cf",
184
- help="Path to the external agent yaml"
210
+ "--external-agent-config", "-ext",
211
+ help="Path to the external agent yaml",
212
+
185
213
  )
186
214
  ],
187
215
  credential: Annotated[
188
216
  str,
189
217
  typer.Option(
190
218
  "--credential", "-crd",
191
- help="credential string"
219
+ help="credential string",
220
+ rich_help_panel="Parameters for Validation"
192
221
  )
193
- ],
222
+ ] = None,
194
223
  output_dir: Annotated[
195
224
  str,
196
225
  typer.Option(
@@ -204,21 +233,80 @@ def validate_external(
204
233
  "--env-file", "-e",
205
234
  help="Path to a .env file that overrides default.env. Then environment variables override both."
206
235
  ),
236
+ ] = None,
237
+ agent_name: Annotated[
238
+ str,
239
+ typer.Option(
240
+ "--agent_name", "-a",
241
+ help="Name of the native agent which has the external agent to test registered as a collaborater. See: https://developer.watson-orchestrate.ibm.com/agents/build_agent#native-agents)." \
242
+ " If this parameter is pased, validation of the external agent is not run.",
243
+ rich_help_panel="Parameters for Input Evaluation"
244
+ )
207
245
  ] = None
208
246
  ):
209
-
247
+
210
248
  validate_watsonx_credentials(user_env_file)
211
- with open(config, "r") as f:
212
- config = yaml.safe_load(f)
213
- controller = EvaluationsController()
214
- test_data = []
215
- with open(data_path, "r") as f:
216
- csv_reader = csv.reader(f)
217
- for line in csv_reader:
218
- test_data.append(line[0])
219
- results = controller.external_validate(config, test_data, credential)
220
- os.makedirs(output_dir, exist_ok=True)
221
- with open(os.path.join(output_dir, "validation_results.json"), "w") as f:
222
- json.dump(results, f)
223
-
224
- rich.print(f"[green] validation result is saved to {output_dir} [/green]")
249
+ Path(output_dir).mkdir(exist_ok=True)
250
+ shutil.copy(data_path, os.path.join(output_dir, "input_sample.tsv"))
251
+
252
+ if agent_name is not None:
253
+ eval_dir = os.path.join(output_dir, "evaluation")
254
+ if os.path.exists(eval_dir):
255
+ rich.print(f"[yellow]: found existing {eval_dir} in target directory. All content is removed.")
256
+ shutil.rmtree(os.path.join(output_dir, "evaluation"))
257
+ Path(eval_dir).mkdir(exist_ok=True)
258
+ # save external agent config even though its not used for evaluation
259
+ # it can help in later debugging customer agents
260
+ with open(os.path.join(eval_dir, "external_agent_cfg.yaml"), "w+") as f:
261
+ with open(external_agent_config, "r") as cfg:
262
+ external_agent_config = yaml.safe_load(cfg)
263
+ yaml.safe_dump(external_agent_config, f, indent=4)
264
+
265
+ rich.print(f"[gold3]Starting evaluation of inputs in '{data_path}' against '{agent_name}'[/gold3]")
266
+ performance_test(
267
+ agent_name=agent_name,
268
+ data_path=data_path,
269
+ output_dir=eval_dir,
270
+ user_env_file=user_env_file
271
+ )
272
+
273
+ else:
274
+ with open(external_agent_config, "r") as f:
275
+ external_agent_config = yaml.safe_load(f)
276
+ controller = EvaluationsController()
277
+ test_data = []
278
+ with open(data_path, "r") as f:
279
+ csv_reader = csv.reader(f, delimiter="\t")
280
+ for line in csv_reader:
281
+ test_data.append(line[0])
282
+
283
+ # save validation results in "validation_results" sub-dir
284
+ validation_folder = Path(output_dir) / "validation_results"
285
+ if os.path.exists(validation_folder):
286
+ rich.print(f"[yellow]: found existing {validation_folder} in target directory. All content is removed.")
287
+ shutil.rmtree(validation_folder)
288
+ validation_folder.mkdir(exist_ok=True, parents=True)
289
+
290
+ # validate the inputs in the provided csv file
291
+ summary = controller.external_validate(external_agent_config, test_data, credential)
292
+ with open(validation_folder / "validation_results.json", "w") as f:
293
+ json.dump(summary, f, indent=4)
294
+
295
+ # validate sample block inputs
296
+ rich.print("[gold3]Validating external agent to see if it can handle an array of messages.")
297
+ block_input_summary = controller.external_validate(external_agent_config, test_data, credential, add_context=True)
298
+ with open(validation_folder / "sample_block_validation_results.json", "w") as f:
299
+ json.dump(block_input_summary, f, indent=4)
300
+
301
+ user_validation_successful = all([item["success"] for item in summary])
302
+ block_validation_successful = all([item["success"] for item in block_input_summary])
303
+
304
+ if user_validation_successful and block_validation_successful:
305
+ msg = (
306
+ f"[green]Validation is successful. The result is saved to '{str(validation_folder)}'.[/green]\n"
307
+ "You can add the external agent as a collaborator agent. See: https://developer.watson-orchestrate.ibm.com/agents/build_agent#native-agents."
308
+ )
309
+ else:
310
+ msg = f"[dark_orange]Schema validation did not succeed. See '{str(validation_folder)}' for failures.[/dark_orange]"
311
+
312
+ rich.print(Panel(msg))
@@ -1,5 +1,6 @@
1
1
  import logging
2
- from typing import List, Dict, Optional
2
+ import os.path
3
+ from typing import List, Dict, Optional, Tuple
3
4
  import csv
4
5
  from pathlib import Path
5
6
  import rich
@@ -10,12 +11,13 @@ from wxo_agentic_evaluation.batch_annotate import generate_test_cases_from_stori
10
11
  from wxo_agentic_evaluation.arg_configs import TestConfig, AuthConfig, LLMUserConfig, ChatRecordingConfig, AnalyzeConfig
11
12
  from wxo_agentic_evaluation.record_chat import record_chats
12
13
  from wxo_agentic_evaluation.external_agent.external_validate import ExternalAgentValidation
14
+ from wxo_agentic_evaluation.external_agent.performance_test import ExternalAgentPerformanceTest
13
15
  from ibm_watsonx_orchestrate import __version__
14
16
  from ibm_watsonx_orchestrate.cli.config import Config, ENV_WXO_URL_OPT, AUTH_CONFIG_FILE, AUTH_CONFIG_FILE_FOLDER, AUTH_SECTION_HEADER, AUTH_MCSP_TOKEN_OPT
15
17
  from ibm_watsonx_orchestrate.utils.utils import yaml_safe_load
16
18
  from ibm_watsonx_orchestrate.cli.commands.agents.agents_controller import AgentsController
17
19
  from ibm_watsonx_orchestrate.agent_builder.agents import AgentKind
18
-
20
+ import uuid
19
21
 
20
22
  logger = logging.getLogger(__name__)
21
23
 
@@ -75,9 +77,13 @@ class EvaluationsController:
75
77
  evaluate.main(config)
76
78
 
77
79
  def record(self, output_dir) -> None:
80
+
81
+
82
+ random_uuid = str(uuid.uuid4())
83
+
78
84
  url, tenant_name, token = self._get_env_config()
79
85
  config_data = {
80
- "output_dir": Path.cwd() if output_dir is None else Path(output_dir),
86
+ "output_dir": Path(os.path.join(Path.cwd(), random_uuid)) if output_dir is None else Path(os.path.join(output_dir,random_uuid)),
81
87
  "service_url": url,
82
88
  "tenant_name": tenant_name,
83
89
  "token": token
@@ -143,16 +149,23 @@ class EvaluationsController:
143
149
  def summarize(self) -> None:
144
150
  pass
145
151
 
146
- def external_validate(self, config: Dict, data: List[str], credential:str):
152
+ def external_validate(self, config: Dict, data: List[str], credential:str, add_context: bool = False):
147
153
  validator = ExternalAgentValidation(credential=credential,
148
154
  auth_scheme=config["auth_scheme"],
149
155
  service_url=config["api_url"])
156
+
150
157
  summary = []
151
158
  for entry in data:
152
- results = validator.call_validation(entry)
153
- if len(results) == 0:
154
- rich.print(f"[red] No events are generated for input {entry} [/red]")
155
- summary.append({entry: results})
159
+ results = validator.call_validation(entry, add_context)
160
+ summary.append(results)
156
161
 
157
162
  return summary
158
-
163
+
164
+ def generate_performance_test(self, agent_name: str, test_data: List[Tuple[str, str]]):
165
+ performance_test = ExternalAgentPerformanceTest(
166
+ agent_name=agent_name,
167
+ test_data=test_data
168
+ )
169
+ generated_performance_tests = performance_test.generate_tests()
170
+
171
+ return generated_performance_tests
@@ -21,24 +21,6 @@ def knowledge_base_import(
21
21
  controller = KnowledgeBaseController()
22
22
  controller.import_knowledge_base(file=file, app_id=app_id)
23
23
 
24
- @knowledge_bases_app.command(name="patch", help="Patch a knowledge base by uploading documents, or providing an external vector index")
25
- def knowledge_base_patch(
26
- file: Annotated[
27
- str,
28
- typer.Option("--file", "-f", help="YAML or JSON file with knowledge base definition"),
29
- ],
30
- name: Annotated[
31
- str,
32
- typer.Option("--name", "-n", help="Name of the knowledge base you wish to update"),
33
- ]=None,
34
- id: Annotated[
35
- str,
36
- typer.Option("--id", "-i", help="ID of the knowledge base you wish to update"),
37
- ]=None
38
- ):
39
- controller = KnowledgeBaseController()
40
- controller.update_knowledge_base(id=id, name=name, file=file)
41
-
42
24
 
43
25
  @knowledge_bases_app.command(name="list", help="List all knowledge bases")
44
26
  def list_knowledge_bases(
@@ -8,7 +8,6 @@ import inspect
8
8
  from pathlib import Path
9
9
  from typing import List
10
10
 
11
- from ibm_watsonx_orchestrate.agent_builder.knowledge_bases.knowledge_base_requests import KnowledgeBaseUpdateRequest
12
11
  from ibm_watsonx_orchestrate.agent_builder.knowledge_bases.knowledge_base import KnowledgeBase
13
12
  from ibm_watsonx_orchestrate.client.knowledge_bases.knowledge_base_client import KnowledgeBaseClient
14
13
  from ibm_watsonx_orchestrate.client.base_api_client import ClientAPIException
@@ -72,11 +71,20 @@ class KnowledgeBaseController:
72
71
  client = self.get_client()
73
72
 
74
73
  knowledge_bases = parse_file(file=file)
74
+ existing_knowledge_bases = client.get_by_names([kb.name for kb in knowledge_bases])
75
+
75
76
  for kb in knowledge_bases:
76
77
  try:
78
+ file_dir = "/".join(file.split("/")[:-1])
79
+
80
+ existing = list(filter(lambda ex: ex.get('name') == kb.name, existing_knowledge_bases))
81
+ if len(existing) > 0:
82
+ logger.info(f"Existing knowledge base '{kb.name}' found. Updating...")
83
+ self.update_knowledge_base(existing[0].get("id"), kb=kb, file_dir=file_dir)
84
+ continue
85
+
77
86
  kb.validate_documents_or_index_exists()
78
87
  if kb.documents:
79
- file_dir = "/".join(file.split("/")[:-1])
80
88
  files = [('files', (get_file_name(file_path), open(get_relative_file_path(file_path, file_dir), 'rb'))) for file_path in kb.documents]
81
89
 
82
90
  kb.prioritize_built_in_index = True
@@ -106,10 +114,7 @@ class KnowledgeBaseController:
106
114
 
107
115
  logger.info(f"Successfully imported knowledge base '{kb.name}'")
108
116
  except ClientAPIException as e:
109
- if "duplicate key value violates unique constraint" in e.response.text:
110
- logger.error(f"A knowledge base with the name '{kb.name}' already exists. Failed to import knowledge base")
111
- else:
112
- logger.error(f"Error importing knowledge base '{kb.name}\n' {e.response.text}")
117
+ logger.error(f"Error importing knowledge base '{kb.name}\n' {e.response.text}")
113
118
 
114
119
  def get_id(
115
120
  self, id: str, name: str
@@ -131,27 +136,36 @@ class KnowledgeBaseController:
131
136
 
132
137
 
133
138
  def update_knowledge_base(
134
- self, id: str, name: str, file: str
139
+ self, knowledge_base_id: str, kb: KnowledgeBase, file_dir: str
135
140
  ) -> None:
136
- knowledge_base_id = self.get_id(id, name)
137
- update_request = KnowledgeBaseUpdateRequest.from_spec(file=file)
141
+ filtered_files = []
142
+
143
+ if kb.documents:
144
+ status = self.get_client().status(knowledge_base_id)
145
+ existing_docs = [doc.get("metadata", {}).get("original_file_name", "") for doc in status.get("documents", [])]
146
+
147
+ for filepath in kb.documents:
148
+ filename = get_file_name(filepath)
138
149
 
139
- if update_request.documents:
140
- file_dir = "/".join(file.split("/")[:-1])
141
- files = [('files', (get_file_name(file_path), open(get_relative_file_path(file_path, file_dir), 'rb'))) for file_path in update_request.documents]
150
+ if filename in existing_docs:
151
+ logger.warning(f'Document \"{filename}\" already exists in knowledge base, skipping.')
152
+ else:
153
+ filtered_files.append(filepath)
154
+
155
+ if filtered_files:
156
+ files = [('files', (get_file_name(file_path), open(get_relative_file_path(file_path, file_dir), 'rb'))) for file_path in filtered_files]
142
157
 
143
- update_request.prioritize_built_in_index = True
144
- payload = update_request.model_dump(exclude_none=True);
158
+ kb.prioritize_built_in_index = True
159
+ payload = kb.model_dump(exclude_none=True);
145
160
  payload.pop('documents');
146
161
 
147
162
  self.get_client().update_with_documents(knowledge_base_id, payload=payload, files=files)
148
163
  else:
149
- if update_request.conversational_search_tool and update_request.conversational_search_tool.index_config:
150
- update_request.prioritize_built_in_index = False
151
- self.get_client().update(knowledge_base_id, update_request.model_dump(exclude_none=True))
164
+ if kb.conversational_search_tool and kb.conversational_search_tool.index_config:
165
+ kb.prioritize_built_in_index = False
166
+ self.get_client().update(knowledge_base_id, kb.model_dump(exclude_none=True))
152
167
 
153
- logEnding = f"with ID '{id}'" if id else f"'{name}'"
154
- logger.info(f"Successfully updated knowledge base {logEnding}")
168
+ logger.info(f"Knowledge base '{kb.name}' updated successfully")
155
169
 
156
170
 
157
171
  def knowledge_base_status( self, id: str, name: str) -> None:
@@ -149,7 +149,7 @@ def models_policy_add(
149
149
  retry_attempts: Annotated[
150
150
  int,
151
151
  typer.Option('--retry-attempts', help='The number of attempts to retry'),
152
- ],
152
+ ] = None,
153
153
  strategy_on_code: Annotated[
154
154
  List[int],
155
155
  typer.Option('--strategy-on-code', help='The http status to consider invoking the strategy'),
@@ -33,6 +33,29 @@ logger = logging.getLogger(__name__)
33
33
  server_app = typer.Typer(no_args_is_help=True)
34
34
 
35
35
 
36
+ _ALWAYS_UNSET: set[str] = {
37
+ "WO_API_KEY",
38
+ "WO_INSTANCE",
39
+ "DOCKER_IAM_KEY",
40
+ "WO_DEVELOPER_EDITION_SOURCE",
41
+ "WATSONX_SPACE_ID",
42
+ "WATSONX_APIKEY",
43
+ "WO_USERNAME",
44
+ "WO_PASSWORD",
45
+ }
46
+
47
+ def define_saas_wdu_runtime(value: str = "none") -> None:
48
+ cfg = Config()
49
+
50
+ current_config_file_values = cfg.get(USER_ENV_CACHE_HEADER)
51
+ current_config_file_values["SAAS_WDU_RUNTIME"] = value
52
+
53
+ cfg.save(
54
+ {
55
+ USER_ENV_CACHE_HEADER: current_config_file_values
56
+ }
57
+ )
58
+
36
59
  def ensure_docker_installed() -> None:
37
60
  try:
38
61
  subprocess.run(["docker", "--version"], check=True, capture_output=True)
@@ -261,6 +284,13 @@ def _check_exclusive_observibility(langfuse_enabled: bool, ibm_tele_enabled: boo
261
284
  return False
262
285
  return True
263
286
 
287
+ def _prepare_clean_env(env_file: Path) -> None:
288
+ """Remove env vars so terminal definitions don't override"""
289
+ keys_from_file = set(dotenv_values(str(env_file)).keys())
290
+ keys_to_unset = keys_from_file | _ALWAYS_UNSET
291
+ for key in keys_to_unset:
292
+ os.environ.pop(key, None)
293
+
264
294
  def write_merged_env_file(merged_env: dict) -> Path:
265
295
  tmp = tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".env")
266
296
  with tmp:
@@ -293,7 +323,8 @@ NON_SECRET_ENV_ITEMS = {
293
323
  "WO_INSTANCE",
294
324
  "USE_SAAS_ML_TOOLS_RUNTIME",
295
325
  "AUTHORIZATION_URL",
296
- "OPENSOURCE_REGISTRY_PROXY"
326
+ "OPENSOURCE_REGISTRY_PROXY",
327
+ "SAAS_WDU_RUNTIME"
297
328
  }
298
329
  def persist_user_env(env: dict, include_secrets: bool = False) -> None:
299
330
  if include_secrets:
@@ -313,9 +344,10 @@ def get_persisted_user_env() -> dict | None:
313
344
  user_env = cfg.get(USER_ENV_CACHE_HEADER) if cfg.get(USER_ENV_CACHE_HEADER) else None
314
345
  return user_env
315
346
 
316
- def run_compose_lite(final_env_file: Path, experimental_with_langfuse=False, experimental_with_ibm_telemetry=False, with_docproc=False) -> None:
347
+ def run_compose_lite(final_env_file: Path, experimental_with_langfuse=False, experimental_with_ibm_telemetry=False, with_doc_processing=False) -> None:
317
348
  compose_path = get_compose_file()
318
349
  compose_command = ensure_docker_compose_installed()
350
+ _prepare_clean_env(final_env_file)
319
351
  db_tag = read_env_file(final_env_file).get('DBTAG', None)
320
352
  logger.info(f"Detected architecture: {platform.machine()}, using DBTAG: {db_tag}")
321
353
 
@@ -345,7 +377,7 @@ def run_compose_lite(final_env_file: Path, experimental_with_langfuse=False, exp
345
377
  profiles.append("langfuse")
346
378
  if experimental_with_ibm_telemetry:
347
379
  profiles.append("ibm-telemetry")
348
- if with_docproc:
380
+ if with_doc_processing:
349
381
  profiles.append("docproc")
350
382
 
351
383
  command = compose_command[:]
@@ -358,6 +390,8 @@ def run_compose_lite(final_env_file: Path, experimental_with_langfuse=False, exp
358
390
  "up",
359
391
  "--scale",
360
392
  "ui=0",
393
+ "--scale",
394
+ "cpe=0",
361
395
  "-d",
362
396
  "--remove-orphans",
363
397
  ]
@@ -428,6 +462,7 @@ def wait_for_wxo_ui_health_check(timeout_seconds=45, interval_seconds=2):
428
462
  def run_compose_lite_ui(user_env_file: Path) -> bool:
429
463
  compose_path = get_compose_file()
430
464
  compose_command = ensure_docker_compose_installed()
465
+ _prepare_clean_env(user_env_file)
431
466
  ensure_docker_installed()
432
467
 
433
468
  default_env = read_env_file(get_default_env_file())
@@ -522,6 +557,7 @@ def run_compose_lite_ui(user_env_file: Path) -> bool:
522
557
  def run_compose_lite_down_ui(user_env_file: Path, is_reset: bool = False) -> None:
523
558
  compose_path = get_compose_file()
524
559
  compose_command = ensure_docker_compose_installed()
560
+ _prepare_clean_env(user_env_file)
525
561
 
526
562
 
527
563
  ensure_docker_installed()
@@ -565,6 +601,7 @@ def run_compose_lite_down_ui(user_env_file: Path, is_reset: bool = False) -> Non
565
601
  def run_compose_lite_down(final_env_file: Path, is_reset: bool = False) -> None:
566
602
  compose_path = get_compose_file()
567
603
  compose_command = ensure_docker_compose_installed()
604
+ _prepare_clean_env(final_env_file)
568
605
 
569
606
  command = compose_command + [
570
607
  '--profile', '*',
@@ -597,6 +634,7 @@ def run_compose_lite_down(final_env_file: Path, is_reset: bool = False) -> None:
597
634
  def run_compose_lite_logs(final_env_file: Path, is_reset: bool = False) -> None:
598
635
  compose_path = get_compose_file()
599
636
  compose_command = ensure_docker_compose_installed()
637
+ _prepare_clean_env(final_env_file)
600
638
 
601
639
  command = compose_command + [
602
640
  "-f", str(compose_path),
@@ -752,14 +790,16 @@ def server_start(
752
790
  "--accept-terms-and-conditions",
753
791
  help="By providing this flag you accept the terms and conditions outlined in the logs on server start."
754
792
  ),
755
- with_docproc: bool = typer.Option(
793
+ with_doc_processing: bool = typer.Option(
756
794
  False,
757
- '--with-docproc', '-d',
795
+ '--with-doc-processing', '-d',
758
796
  help='Enable IBM Document Processing to extract information from your business documents. Enabling this activates the Watson Document Understanding service.'
759
797
  ),
760
798
  ):
761
799
  confirm_accepts_license_agreement(accept_terms_and_conditions)
762
800
 
801
+ define_saas_wdu_runtime()
802
+
763
803
  if user_env_file and not Path(user_env_file).exists():
764
804
  logger.error(f"Error: The specified environment file '{user_env_file}' does not exist.")
765
805
  sys.exit(1)
@@ -795,8 +835,9 @@ def server_start(
795
835
  if experimental_with_langfuse:
796
836
  merged_env_dict['LANGFUSE_ENABLED'] = 'true'
797
837
 
798
- if with_docproc:
838
+ if with_doc_processing:
799
839
  merged_env_dict['DOCPROC_ENABLED'] = 'true'
840
+ define_saas_wdu_runtime("local")
800
841
 
801
842
  if experimental_with_ibm_telemetry:
802
843
  merged_env_dict['USE_IBM_TELEMETRY'] = 'true'
@@ -815,8 +856,8 @@ def server_start(
815
856
  run_compose_lite(final_env_file=final_env_file,
816
857
  experimental_with_langfuse=experimental_with_langfuse,
817
858
  experimental_with_ibm_telemetry=experimental_with_ibm_telemetry,
818
- with_docproc=with_docproc)
819
-
859
+ with_doc_processing=with_doc_processing)
860
+
820
861
  run_db_migration()
821
862
 
822
863
  logger.info("Waiting for orchestrate server to be fully initialized and ready...")
@@ -843,7 +884,7 @@ def server_start(
843
884
 
844
885
  if experimental_with_langfuse:
845
886
  logger.info(f"You can access the observability platform Langfuse at http://localhost:3010, username: orchestrate@ibm.com, password: orchestrate")
846
- if with_docproc:
887
+ if with_doc_processing:
847
888
  logger.info(f"Document processing capabilities are now available for use in Flows (both ADK and runtime). Note: This option is currently available only in the Developer edition.")
848
889
 
849
890
  @server_app.command(name="stop")
@@ -854,6 +895,7 @@ def server_stop(
854
895
  help="Path to a .env file that overrides default.env. Then environment variables override both."
855
896
  )
856
897
  ):
898
+
857
899
  ensure_docker_installed()
858
900
  default_env_path = get_default_env_file()
859
901
  merged_env_dict = merge_env(
@@ -910,9 +952,24 @@ def server_logs(
910
952
  def run_db_migration() -> None:
911
953
  compose_path = get_compose_file()
912
954
  compose_command = ensure_docker_compose_installed()
955
+ default_env_path = get_default_env_file()
956
+ merged_env_dict = merge_env(default_env_path, user_env_path=None)
957
+ merged_env_dict['WATSONX_SPACE_ID']='X'
958
+ merged_env_dict['WATSONX_APIKEY']='X'
959
+ merged_env_dict['WXAI_API_KEY'] = ''
960
+ merged_env_dict['ASSISTANT_EMBEDDINGS_API_KEY'] = ''
961
+ merged_env_dict['ASSISTANT_LLM_SPACE_ID'] = ''
962
+ merged_env_dict['ROUTING_LLM_SPACE_ID'] = ''
963
+ merged_env_dict['USE_SAAS_ML_TOOLS_RUNTIME'] = ''
964
+ merged_env_dict['BAM_API_KEY'] = ''
965
+ merged_env_dict['ASSISTANT_EMBEDDINGS_SPACE_ID'] = ''
966
+ merged_env_dict['ROUTING_LLM_API_KEY'] = ''
967
+ merged_env_dict['ASSISTANT_LLM_API_KEY'] = ''
968
+ final_env_file = write_merged_env_file(merged_env_dict)
913
969
 
914
970
  command = compose_command + [
915
971
  "-f", str(compose_path),
972
+ "--env-file", str(final_env_file),
916
973
  "exec",
917
974
  "wxo-server-db",
918
975
  "bash",
@@ -47,7 +47,7 @@ def import_toolkit(
47
47
  ] = None,
48
48
  tools: Annotated[
49
49
  Optional[str],
50
- typer.Option("--tools", "-t", help="Comma-separated list of tools to import. Or you can use `*` to use all tools"),
50
+ typer.Option("--tools", "-t", help="Comma-separated list of tools to import. Or you can use \"*\" to use all tools"),
51
51
  ] = None,
52
52
  app_id: Annotated[
53
53
  List[str],