ibm-watsonx-orchestrate 1.8.0b1__py3-none-any.whl → 1.9.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 (35) hide show
  1. ibm_watsonx_orchestrate/__init__.py +1 -1
  2. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +2 -2
  3. ibm_watsonx_orchestrate/agent_builder/models/types.py +5 -0
  4. ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +61 -11
  5. ibm_watsonx_orchestrate/agent_builder/tools/python_tool.py +18 -6
  6. ibm_watsonx_orchestrate/agent_builder/tools/types.py +12 -5
  7. ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +15 -3
  8. ibm_watsonx_orchestrate/cli/commands/channels/types.py +2 -2
  9. ibm_watsonx_orchestrate/cli/commands/channels/webchat/channels_webchat_controller.py +2 -3
  10. ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +6 -3
  11. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_controller.py +103 -23
  12. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_command.py +86 -31
  13. ibm_watsonx_orchestrate/cli/commands/models/model_provider_mapper.py +17 -13
  14. ibm_watsonx_orchestrate/cli/commands/models/models_controller.py +5 -8
  15. ibm_watsonx_orchestrate/cli/commands/server/server_command.py +147 -37
  16. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_command.py +4 -2
  17. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_controller.py +9 -1
  18. ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +1 -1
  19. ibm_watsonx_orchestrate/client/connections/connections_client.py +19 -32
  20. ibm_watsonx_orchestrate/client/copilot/cpe/copilot_cpe_client.py +5 -3
  21. ibm_watsonx_orchestrate/client/utils.py +17 -16
  22. ibm_watsonx_orchestrate/docker/compose-lite.yml +127 -12
  23. ibm_watsonx_orchestrate/docker/default.env +26 -21
  24. ibm_watsonx_orchestrate/flow_builder/flows/__init__.py +2 -2
  25. ibm_watsonx_orchestrate/flow_builder/flows/constants.py +2 -0
  26. ibm_watsonx_orchestrate/flow_builder/flows/flow.py +52 -10
  27. ibm_watsonx_orchestrate/flow_builder/node.py +34 -3
  28. ibm_watsonx_orchestrate/flow_builder/types.py +144 -26
  29. ibm_watsonx_orchestrate/flow_builder/utils.py +7 -5
  30. {ibm_watsonx_orchestrate-1.8.0b1.dist-info → ibm_watsonx_orchestrate-1.9.0.dist-info}/METADATA +1 -3
  31. {ibm_watsonx_orchestrate-1.8.0b1.dist-info → ibm_watsonx_orchestrate-1.9.0.dist-info}/RECORD +34 -35
  32. ibm_watsonx_orchestrate/agent_builder/utils/pydantic_utils.py +0 -149
  33. {ibm_watsonx_orchestrate-1.8.0b1.dist-info → ibm_watsonx_orchestrate-1.9.0.dist-info}/WHEEL +0 -0
  34. {ibm_watsonx_orchestrate-1.8.0b1.dist-info → ibm_watsonx_orchestrate-1.9.0.dist-info}/entry_points.txt +0 -0
  35. {ibm_watsonx_orchestrate-1.8.0b1.dist-info → ibm_watsonx_orchestrate-1.9.0.dist-info}/licenses/LICENSE +0 -0
@@ -2,11 +2,12 @@ import json
2
2
  import logging
3
3
  import typer
4
4
  import os
5
- import yaml
6
5
  import csv
7
6
  import rich
8
7
  import sys
9
8
  import shutil
9
+ import tempfile
10
+ import random
10
11
 
11
12
  from rich.panel import Panel
12
13
  from pathlib import Path
@@ -16,11 +17,37 @@ from typing_extensions import Annotated
16
17
 
17
18
  from ibm_watsonx_orchestrate import __version__
18
19
  from ibm_watsonx_orchestrate.cli.commands.evaluations.evaluations_controller import EvaluationsController
20
+ from ibm_watsonx_orchestrate.cli.commands.agents.agents_controller import AgentsController
19
21
 
20
22
  logger = logging.getLogger(__name__)
21
23
 
22
24
  evaluation_app = typer.Typer(no_args_is_help=True)
23
25
 
26
+ def _native_agent_template():
27
+ return {
28
+ "spec_version": "v1",
29
+ "style": "default",
30
+ "llm": "watsonx/meta-llama/llama-3-405b-instruct",
31
+ "name": "",
32
+ "description": "Native agent for validating external agent",
33
+ "instructions": "Use the tools and external agent(s) provided to answer the user's question. If you do not have enough information to answer the question, say so. If you need more information, ask follow up questions.",
34
+ "collaborators": []
35
+ }
36
+
37
+ def _random_native_agent_name(external_agent_name):
38
+ """ Generate a native agent name in the following format to ensure uniqueness:
39
+
40
+ "external_agent_validation_{external_agent_name}_{random number}
41
+
42
+ So if the external agent name is, "QA_Agent", and the random number generated is, '100', the native agent name is:
43
+ "external_agent_validation_QA_Agent_100"
44
+
45
+ """
46
+ seed = 42
47
+ random.seed(seed)
48
+
49
+ return f"external_agent_validation_{external_agent_name}_{random.randint(0, 100)}"
50
+
24
51
  def read_env_file(env_path: Path|str) -> dict:
25
52
  return dotenv_values(str(env_path))
26
53
 
@@ -218,7 +245,7 @@ def validate_external(
218
245
  str,
219
246
  typer.Option(
220
247
  "--external-agent-config", "-ext",
221
- help="Path to the external agent yaml",
248
+ help="Path to the external agent json file",
222
249
 
223
250
  )
224
251
  ],
@@ -244,33 +271,65 @@ def validate_external(
244
271
  help="Path to a .env file that overrides default.env. Then environment variables override both."
245
272
  ),
246
273
  ] = None,
247
- agent_name: Annotated[
248
- str,
274
+ perf_test: Annotated[
275
+ bool,
249
276
  typer.Option(
250
- "--agent_name", "-a",
251
- 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)." \
252
- " If this parameter is pased, validation of the external agent is not run.",
253
- rich_help_panel="Parameters for Input Evaluation"
277
+ "--perf", "-p",
278
+ help="Performance test your external agent against the provide user stories.",
279
+ rich_help_panel="Parameters for Input Evaluation",
254
280
  )
255
- ] = None
281
+ ] = False
256
282
  ):
257
283
 
258
284
  validate_watsonx_credentials(user_env_file)
259
- Path(output_dir).mkdir(exist_ok=True)
260
- shutil.copy(data_path, os.path.join(output_dir, "input_sample.tsv"))
261
285
 
262
- if agent_name is not None:
263
- eval_dir = os.path.join(output_dir, "evaluation")
286
+ with open(external_agent_config, 'r') as f:
287
+ try:
288
+ external_agent_config = json.load(f)
289
+ except Exception:
290
+ rich.print(
291
+ f"[red]: Please provide a valid external agent spec in JSON format. See 'examples/evaluations/external_agent_validation/sample_external_agent_config.json' for an example."
292
+ )
293
+ sys.exit(1)
294
+
295
+ eval_dir = os.path.join(output_dir, "evaluations")
296
+ if perf_test:
264
297
  if os.path.exists(eval_dir):
265
298
  rich.print(f"[yellow]: found existing {eval_dir} in target directory. All content is removed.")
266
- shutil.rmtree(os.path.join(output_dir, "evaluation"))
267
- Path(eval_dir).mkdir(exist_ok=True)
299
+ shutil.rmtree(eval_dir)
300
+ Path(eval_dir).mkdir(exist_ok=True, parents=True)
268
301
  # save external agent config even though its not used for evaluation
269
302
  # it can help in later debugging customer agents
270
- with open(os.path.join(eval_dir, "external_agent_cfg.yaml"), "w+") as f:
271
- with open(external_agent_config, "r") as cfg:
272
- external_agent_config = yaml.safe_load(cfg)
273
- yaml.safe_dump(external_agent_config, f, indent=4)
303
+ with open(os.path.join(eval_dir, f"external_agent_cfg.json"), "w+") as f:
304
+ json.dump(external_agent_config, f, indent=4)
305
+
306
+ logger.info("Registering External Agent")
307
+ agent_controller = AgentsController()
308
+
309
+ external_agent_config["title"] = external_agent_config["name"]
310
+ external_agent_config["auth_config"] = {"token": credential}
311
+ external_agent_config["spec_version"] = external_agent_config.get("spec_version", "v1")
312
+ external_agent_config["provider"] = "external_chat"
313
+
314
+ with tempfile.NamedTemporaryFile(mode="w+", encoding="utf-8", suffix=".json", delete=True) as fp:
315
+ json.dump(external_agent_config, fp, indent=4)
316
+ fp.flush()
317
+ agents = agent_controller.import_agent(file=os.path.abspath(fp.name), app_id=None)
318
+ agent_controller.publish_or_update_agents(agents)
319
+
320
+ logger.info("Registering Native Agent")
321
+
322
+ native_agent_template = _native_agent_template()
323
+ agent_name = _random_native_agent_name(external_agent_config["name"])
324
+ rich.print(f"[blue][b]Generated native agent name is: [i]{agent_name}[/i][/b]")
325
+ native_agent_template["name"] = agent_name
326
+ native_agent_template["collaborators"] = [external_agent_config["name"]]
327
+
328
+ with tempfile.NamedTemporaryFile(mode="w+", encoding="utf-8", suffix=".json", delete=True) as fp:
329
+ json.dump(native_agent_template, fp, indent=4)
330
+ fp.flush()
331
+ agents = agent_controller.import_agent(file=os.path.abspath(fp.name), app_id=None)
332
+ agent_controller.publish_or_update_agents(agents)
274
333
 
275
334
  rich.print(f"[gold3]Starting evaluation of inputs in '{data_path}' against '{agent_name}'[/gold3]")
276
335
  performance_test(
@@ -281,8 +340,6 @@ def validate_external(
281
340
  )
282
341
 
283
342
  else:
284
- with open(external_agent_config, "r") as f:
285
- external_agent_config = yaml.safe_load(f)
286
343
  controller = EvaluationsController()
287
344
  test_data = []
288
345
  with open(data_path, "r") as f:
@@ -290,31 +347,29 @@ def validate_external(
290
347
  for line in csv_reader:
291
348
  test_data.append(line[0])
292
349
 
293
- # save validation results in "validation_results" sub-dir
294
- validation_folder = Path(output_dir) / "validation_results"
350
+ # save validation results in "validate_external" sub-dir
351
+ validation_folder = Path(output_dir) / "validate_external"
295
352
  if os.path.exists(validation_folder):
296
353
  rich.print(f"[yellow]: found existing {validation_folder} in target directory. All content is removed.")
297
354
  shutil.rmtree(validation_folder)
298
355
  validation_folder.mkdir(exist_ok=True, parents=True)
356
+ shutil.copy(data_path, os.path.join(validation_folder, "input_sample.tsv"))
299
357
 
300
358
  # validate the inputs in the provided csv file
301
359
  summary = controller.external_validate(external_agent_config, test_data, credential)
302
- with open(validation_folder / "validation_results.json", "w") as f:
303
- json.dump(summary, f, indent=4)
304
-
305
360
  # validate sample block inputs
306
- rich.print("[gold3]Validating external agent to see if it can handle an array of messages.")
361
+ rich.print("[gold3]Validating external agent against an array of messages.")
307
362
  block_input_summary = controller.external_validate(external_agent_config, test_data, credential, add_context=True)
308
- with open(validation_folder / "sample_block_validation_results.json", "w") as f:
309
- json.dump(block_input_summary, f, indent=4)
310
-
363
+
364
+ with open(validation_folder / "validation_results.json", "w") as f:
365
+ json.dump([summary, block_input_summary], f, indent=4)
366
+
311
367
  user_validation_successful = all([item["success"] for item in summary])
312
368
  block_validation_successful = all([item["success"] for item in block_input_summary])
313
369
 
314
370
  if user_validation_successful and block_validation_successful:
315
371
  msg = (
316
372
  f"[green]Validation is successful. The result is saved to '{str(validation_folder)}'.[/green]\n"
317
- "You can add the external agent as a collaborator agent. See: https://developer.watson-orchestrate.ibm.com/agents/build_agent#native-agents."
318
373
  )
319
374
  else:
320
375
  msg = f"[dark_orange]Schema validation did not succeed. See '{str(validation_folder)}' for failures.[/dark_orange]"
@@ -10,19 +10,23 @@ _BASIC_PROVIDER_CONFIG_KEYS = {'provider', 'api_key', 'custom_host', 'url_to_fet
10
10
 
11
11
  PROVIDER_EXTRA_PROPERTIES_LUT = {
12
12
  ModelProvider.ANTHROPIC: {'anthropic_beta', 'anthropic_version'},
13
- # ModelProvider.AZURE_AI: {
14
- # 'azure_resource_name',
15
- # 'azure_deployment_id',
16
- # 'azure_api_version',
17
- # 'ad_auth',
18
- # 'azure_auth_mode',
19
- # 'azure_managed_client_id',
20
- # 'azure_entra_client_id',
21
- # 'azure_entra_client_secret',
22
- # 'azure_entra_tenant_id',
23
- # 'azure_ad_token',
24
- # 'azure_model_name'
25
- # },
13
+ ModelProvider.AZURE_AI: {
14
+ 'azure_resource_name',
15
+ 'azure_deployment_id',
16
+ 'azure_api_version',
17
+ 'ad_auth',
18
+ 'azure_auth_mode',
19
+ 'azure_managed_client_id',
20
+ 'azure_entra_client_id',
21
+ 'azure_entra_client_secret',
22
+ 'azure_entra_tenant_id',
23
+ 'azure_ad_token',
24
+ 'azure_model_name',
25
+ 'azure_inference_deployment_name',
26
+ 'azure_inference_api_version',
27
+ 'azure_inference_extra_params',
28
+ 'azure_inference_foundry_url'
29
+ },
26
30
  ModelProvider.AZURE_OPENAI: {
27
31
  'azure_resource_name',
28
32
  'azure_deployment_id',
@@ -167,15 +167,12 @@ class ModelsController:
167
167
  logger.error("Error: WATSONX_URL is required in the environment.")
168
168
  sys.exit(1)
169
169
 
170
- if is_cpd_env(models_client.base_url):
171
- virtual_models = []
172
- virtual_model_policies = []
173
- else:
174
- logger.info("Retrieving virtual-model models list...")
175
- virtual_models = models_client.list()
170
+
171
+ logger.info("Retrieving virtual-model models list...")
172
+ virtual_models = models_client.list()
176
173
 
177
- logger.info("Retrieving virtual-policies models list...")
178
- virtual_model_policies = model_policies_client.list()
174
+ logger.info("Retrieving virtual-policies models list...")
175
+ virtual_model_policies = model_policies_client.list()
179
176
 
180
177
  logger.info("Retrieving watsonx.ai models list...")
181
178
  found_models = _get_wxai_foundational_models()
@@ -4,11 +4,13 @@ import os
4
4
  import platform
5
5
  import subprocess
6
6
  import sys
7
+ import shutil
7
8
  import tempfile
8
9
  import time
9
10
  from pathlib import Path
10
11
  from urllib.parse import urlparse
11
12
 
13
+ import re
12
14
  import jwt
13
15
  import requests
14
16
  import typer
@@ -32,6 +34,13 @@ logger = logging.getLogger(__name__)
32
34
 
33
35
  server_app = typer.Typer(no_args_is_help=True)
34
36
 
37
+ _EXPORT_FILE_TYPES: set[str] = {
38
+ 'py',
39
+ 'yaml',
40
+ 'yml',
41
+ 'json',
42
+ 'env'
43
+ }
35
44
 
36
45
  _ALWAYS_UNSET: set[str] = {
37
46
  "WO_API_KEY",
@@ -43,11 +52,34 @@ _ALWAYS_UNSET: set[str] = {
43
52
  "WO_USERNAME",
44
53
  "WO_PASSWORD",
45
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
+ }
46
65
 
47
66
  def define_saas_wdu_runtime(value: str = "none") -> None:
48
67
  cfg = Config()
49
68
  cfg.write(USER_ENV_CACHE_HEADER,"SAAS_WDU_RUNTIME",value)
50
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
+
51
83
  def ensure_docker_installed() -> None:
52
84
  try:
53
85
  subprocess.run(["docker", "--version"], check=True, capture_output=True)
@@ -106,6 +138,11 @@ def docker_login_by_dev_edition_source(env_dict: dict, source: str) -> None:
106
138
 
107
139
 
108
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:
109
146
  with resources.as_file(
110
147
  resources.files("ibm_watsonx_orchestrate.docker").joinpath("compose-lite.yml")
111
148
  ) as compose_file:
@@ -283,12 +320,17 @@ def _prepare_clean_env(env_file: Path) -> None:
283
320
  for key in keys_to_unset:
284
321
  os.environ.pop(key, None)
285
322
 
286
- def write_merged_env_file(merged_env: dict) -> Path:
287
- tmp = tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".env")
288
- with tmp:
323
+ def write_merged_env_file(merged_env: dict, target_path: str = None) -> Path:
324
+
325
+ if target_path:
326
+ file = open(target_path,"w")
327
+ else:
328
+ file = tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".env")
329
+
330
+ with file:
289
331
  for key, val in merged_env.items():
290
- tmp.write(f"{key}={val}\n")
291
- return Path(tmp.name)
332
+ file.write(f"{key}={val}\n")
333
+ return Path(file.name)
292
334
 
293
335
 
294
336
  def get_dbtag_from_architecture(merged_env_dict: dict) -> str:
@@ -310,14 +352,6 @@ def refresh_local_credentials() -> None:
310
352
  clear_protected_env_credentials_token()
311
353
  _login(name=PROTECTED_ENV_NAME, apikey=None)
312
354
 
313
- NON_SECRET_ENV_ITEMS = {
314
- "WO_DEVELOPER_EDITION_SOURCE",
315
- "WO_INSTANCE",
316
- "USE_SAAS_ML_TOOLS_RUNTIME",
317
- "AUTHORIZATION_URL",
318
- "OPENSOURCE_REGISTRY_PROXY",
319
- "SAAS_WDU_RUNTIME"
320
- }
321
355
  def persist_user_env(env: dict, include_secrets: bool = False) -> None:
322
356
  if include_secrets:
323
357
  persistable_env = env
@@ -337,7 +371,10 @@ def get_persisted_user_env() -> dict | None:
337
371
  return user_env
338
372
 
339
373
  def run_compose_lite(final_env_file: Path, experimental_with_langfuse=False, experimental_with_ibm_telemetry=False, with_doc_processing=False) -> None:
374
+
375
+
340
376
  compose_path = get_compose_file()
377
+
341
378
  compose_command = ensure_docker_compose_installed()
342
379
  _prepare_clean_env(final_env_file)
343
380
  db_tag = read_env_file(final_env_file).get('DBTAG', None)
@@ -754,6 +791,32 @@ def auto_configure_callback_ip(merged_env_dict: dict) -> dict:
754
791
 
755
792
  return merged_env_dict
756
793
 
794
+ def prepare_server_env_vars(user_env: dict = {}):
795
+
796
+ default_env = read_env_file(get_default_env_file())
797
+ dev_edition_source = get_dev_edition_source(user_env)
798
+ default_registry_vars = get_default_registry_env_vars_by_dev_edition_source(default_env, user_env, source=dev_edition_source)
799
+
800
+ # Update the default environment with the default registry variables only if they are not already set
801
+ for key in default_registry_vars:
802
+ if key not in default_env or not default_env[key]:
803
+ default_env[key] = default_registry_vars[key]
804
+
805
+ # Merge the default environment with the user environment
806
+ merged_env_dict = {
807
+ **default_env,
808
+ **user_env,
809
+ }
810
+
811
+ merged_env_dict = apply_server_env_dict_defaults(merged_env_dict)
812
+
813
+ # Auto-configure callback IP for async tools
814
+ merged_env_dict = auto_configure_callback_ip(merged_env_dict)
815
+
816
+ apply_llm_api_key_defaults(merged_env_dict)
817
+
818
+ return merged_env_dict
819
+
757
820
  @server_app.command(name="start")
758
821
  def server_start(
759
822
  user_env_file: str = typer.Option(
@@ -787,38 +850,37 @@ def server_start(
787
850
  '--with-doc-processing', '-d',
788
851
  help='Enable IBM Document Processing to extract information from your business documents. Enabling this activates the Watson Document Understanding service.'
789
852
  ),
853
+ custom_compose_file: str = typer.Option(
854
+ None,
855
+ '--compose-file', '-f',
856
+ help='Provide the path to a custom docker-compose file to use instead of the default compose file'
857
+ ),
790
858
  ):
791
859
  confirm_accepts_license_agreement(accept_terms_and_conditions)
792
860
 
793
861
  define_saas_wdu_runtime()
794
862
 
863
+ ensure_docker_installed()
864
+
795
865
  if user_env_file and not Path(user_env_file).exists():
796
- logger.error(f"Error: The specified environment file '{user_env_file}' does not exist.")
866
+ logger.error(f"The specified environment file '{user_env_file}' does not exist.")
797
867
  sys.exit(1)
798
- ensure_docker_installed()
799
868
 
800
- default_env = read_env_file(get_default_env_file())
869
+ if custom_compose_file:
870
+ if Path(custom_compose_file).exists():
871
+ logger.warning("You are using a custom docker compose file, official support will not be available for this configuration")
872
+ else:
873
+ logger.error(f"The specified docker-compose file '{custom_compose_file}' does not exist.")
874
+ sys.exit(1)
875
+
876
+ #Run regardless, to allow this to set compose as 'None' when not in use
877
+ set_compose_file_path_in_env(custom_compose_file)
878
+
801
879
  user_env = read_env_file(user_env_file) if user_env_file else {}
802
880
  persist_user_env(user_env, include_secrets=persist_env_secrets)
803
881
 
804
- dev_edition_source = get_dev_edition_source(user_env)
805
- default_registry_vars = get_default_registry_env_vars_by_dev_edition_source(default_env, user_env, source=dev_edition_source)
806
-
807
- # Update the default environment with the default registry variables only if they are not already set
808
- for key in default_registry_vars:
809
- if key not in default_env or not default_env[key]:
810
- default_env[key] = default_registry_vars[key]
882
+ merged_env_dict = prepare_server_env_vars(user_env)
811
883
 
812
- # Merge the default environment with the user environment
813
- merged_env_dict = {
814
- **default_env,
815
- **user_env,
816
- }
817
-
818
- merged_env_dict = apply_server_env_dict_defaults(merged_env_dict)
819
-
820
- # Auto-configure callback IP for async tools
821
- merged_env_dict = auto_configure_callback_ip(merged_env_dict)
822
884
  if not _check_exclusive_observibility(experimental_with_langfuse, experimental_with_ibm_telemetry):
823
885
  logger.error("Please select either langfuse or ibm telemetry for observability not both")
824
886
  sys.exit(1)
@@ -835,14 +897,12 @@ def server_start(
835
897
  merged_env_dict['USE_IBM_TELEMETRY'] = 'true'
836
898
 
837
899
  try:
900
+ dev_edition_source = get_dev_edition_source(merged_env_dict)
838
901
  docker_login_by_dev_edition_source(merged_env_dict, dev_edition_source)
839
902
  except ValueError as e:
840
903
  logger.error(f"Error: {e}")
841
904
  sys.exit(1)
842
905
 
843
- apply_llm_api_key_defaults(merged_env_dict)
844
-
845
-
846
906
  final_env_file = write_merged_env_file(merged_env_dict)
847
907
 
848
908
  run_compose_lite(final_env_file=final_env_file,
@@ -877,7 +937,7 @@ def server_start(
877
937
  if experimental_with_langfuse:
878
938
  logger.info(f"You can access the observability platform Langfuse at http://localhost:3010, username: orchestrate@ibm.com, password: orchestrate")
879
939
  if with_doc_processing:
880
- 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.")
940
+ logger.info(f"Document processing in Flows (Public Preview) has been enabled.")
881
941
 
882
942
  @server_app.command(name="stop")
883
943
  def server_stop(
@@ -1000,5 +1060,55 @@ def run_db_migration() -> None:
1000
1060
  )
1001
1061
  sys.exit(1)
1002
1062
 
1063
+
1064
+ def bump_file_iteration(filename: str) -> str:
1065
+ regex = re.compile(f"^(?P<name>[^\\(\\s\\.\\)]+)(\\((?P<num>\\d+)\\))?(?P<type>\\.(?:{'|'.join(_EXPORT_FILE_TYPES)}))?$")
1066
+ _m = regex.match(filename)
1067
+ iter = int(_m['num']) + 1 if (_m and _m['num']) else 1
1068
+ return f"{_m['name']}({iter}){_m['type'] or ''}"
1069
+
1070
+ def get_next_free_file_iteration(filename: str) -> str:
1071
+ while Path(filename).exists():
1072
+ filename = bump_file_iteration(filename)
1073
+ return filename
1074
+
1075
+ @server_app.command(name="eject", help="output the docker-compose file and associated env file used to run the server")
1076
+ def server_eject(
1077
+ user_env_file: str = typer.Option(
1078
+ None,
1079
+ "--env-file",
1080
+ "-e",
1081
+ help="Path to a .env file that overrides default.env. Then environment variables override both."
1082
+ )
1083
+ ):
1084
+
1085
+ if not user_env_file:
1086
+ logger.error(f"To use 'server eject' you need to specify an env file with '--env-file' or '-e'")
1087
+ sys.exit(1)
1088
+
1089
+ if not Path(user_env_file).exists():
1090
+ logger.error(f"The specified environment file '{user_env_file}' does not exist.")
1091
+ sys.exit(1)
1092
+
1093
+ logger.warning("Changes to your docker compose file are not supported")
1094
+
1095
+ compose_file_path = get_compose_file()
1096
+
1097
+ compose_output_file = get_next_free_file_iteration('docker-compose.yml')
1098
+ logger.info(f"Exporting docker compose file to '{compose_output_file}'")
1099
+
1100
+ shutil.copyfile(compose_file_path,compose_output_file)
1101
+
1102
+
1103
+ user_env = read_env_file(user_env_file)
1104
+ merged_env_dict = prepare_server_env_vars(user_env)
1105
+
1106
+ env_output_file = get_next_free_file_iteration('server.env')
1107
+ logger.info(f"Exporting env file to '{env_output_file}'")
1108
+
1109
+ write_merged_env_file(merged_env=merged_env_dict,target_path=env_output_file)
1110
+
1111
+ logger.info(f"To make use of the exported configuration file run \"orchestrate server start -e {env_output_file} -f {compose_output_file}\"")
1112
+
1003
1113
  if __name__ == "__main__":
1004
1114
  server_app()
@@ -64,8 +64,8 @@ def import_toolkit(
64
64
  else:
65
65
  tool_list = None
66
66
 
67
- if not package and not package_root:
68
- logger.error("You must provide either '--package' or '--package-root'.")
67
+ if not package and not package_root and not command:
68
+ logger.error("You must provide either '--package', '--package-root' or '--command'.")
69
69
  sys.exit(1)
70
70
 
71
71
  if package_root and not command:
@@ -85,6 +85,8 @@ def import_toolkit(
85
85
  else:
86
86
  logger.error("Unable to infer start up command: '--language' flag must be either 'node' or 'python' when using the '--package' flag without '--command' flag.")
87
87
  sys.exit(1)
88
+ else:
89
+ logger.warning(f"Default package installation command for package '{package}' overridden by '--command {command}'.")
88
90
 
89
91
 
90
92
  toolkit_controller = ToolkitController(
@@ -70,7 +70,7 @@ class ToolkitController:
70
70
  self.client = None
71
71
 
72
72
  self.source: ToolkitSource = (
73
- ToolkitSource.PUBLIC_REGISTRY if package else ToolkitSource.FILES
73
+ ToolkitSource.FILES if package_root else ToolkitSource.PUBLIC_REGISTRY
74
74
  )
75
75
 
76
76
  def get_client(self) -> ToolKitClient:
@@ -105,6 +105,14 @@ class ToolkitController:
105
105
  command = command_parts[0]
106
106
  args = command_parts[1:]
107
107
 
108
+ if self.package_root:
109
+ is_folder = os.path.isdir(self.package_root)
110
+ is_zip_file = os.path.isfile(self.package_root) and zipfile.is_zipfile(self.package_root)
111
+
112
+ if not is_folder and not is_zip_file:
113
+ logger.error(f"Unable to find a valid directory or zip file at location '{self.package_root}'")
114
+ sys.exit(1)
115
+
108
116
  console = Console()
109
117
 
110
118
  with tempfile.TemporaryDirectory() as tmpdir:
@@ -590,7 +590,7 @@ class ToolsController:
590
590
  for tool in tools:
591
591
  tools_list.append(json.loads(tool.dumps_spec()))
592
592
 
593
- rich.print(JSON(json.dumps(tools_list, indent=4)))
593
+ rich.print_json(json.dumps(tools_list, indent=4))
594
594
  else:
595
595
  table = rich.table.Table(show_header=True, header_style="bold white", show_lines=True)
596
596
  column_args = {