ibm-watsonx-orchestrate 1.3.0__py3-none-any.whl → 1.5.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 (54) hide show
  1. ibm_watsonx_orchestrate/__init__.py +2 -1
  2. ibm_watsonx_orchestrate/agent_builder/agents/types.py +2 -0
  3. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +10 -2
  4. ibm_watsonx_orchestrate/agent_builder/toolkits/base_toolkit.py +32 -0
  5. ibm_watsonx_orchestrate/agent_builder/toolkits/types.py +42 -0
  6. ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +10 -1
  7. ibm_watsonx_orchestrate/agent_builder/tools/python_tool.py +4 -2
  8. ibm_watsonx_orchestrate/agent_builder/tools/types.py +2 -1
  9. ibm_watsonx_orchestrate/cli/commands/agents/agents_command.py +29 -0
  10. ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +271 -12
  11. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +17 -2
  12. ibm_watsonx_orchestrate/cli/commands/models/env_file_model_provider_mapper.py +180 -0
  13. ibm_watsonx_orchestrate/cli/commands/models/models_command.py +199 -8
  14. ibm_watsonx_orchestrate/cli/commands/server/server_command.py +117 -48
  15. ibm_watsonx_orchestrate/cli/commands/server/types.py +105 -0
  16. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_command.py +55 -7
  17. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_controller.py +123 -42
  18. ibm_watsonx_orchestrate/cli/commands/tools/tools_command.py +22 -1
  19. ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +197 -12
  20. ibm_watsonx_orchestrate/client/agents/agent_client.py +4 -1
  21. ibm_watsonx_orchestrate/client/agents/assistant_agent_client.py +5 -1
  22. ibm_watsonx_orchestrate/client/agents/external_agent_client.py +5 -1
  23. ibm_watsonx_orchestrate/client/analytics/llm/analytics_llm_client.py +2 -6
  24. ibm_watsonx_orchestrate/client/base_api_client.py +5 -2
  25. ibm_watsonx_orchestrate/client/connections/connections_client.py +3 -9
  26. ibm_watsonx_orchestrate/client/model_policies/__init__.py +0 -0
  27. ibm_watsonx_orchestrate/client/model_policies/model_policies_client.py +47 -0
  28. ibm_watsonx_orchestrate/client/model_policies/types.py +36 -0
  29. ibm_watsonx_orchestrate/client/models/__init__.py +0 -0
  30. ibm_watsonx_orchestrate/client/models/models_client.py +46 -0
  31. ibm_watsonx_orchestrate/client/models/types.py +189 -0
  32. ibm_watsonx_orchestrate/client/toolkit/toolkit_client.py +20 -6
  33. ibm_watsonx_orchestrate/client/tools/tempus_client.py +40 -0
  34. ibm_watsonx_orchestrate/client/tools/tool_client.py +8 -0
  35. ibm_watsonx_orchestrate/docker/compose-lite.yml +68 -13
  36. ibm_watsonx_orchestrate/docker/default.env +22 -12
  37. ibm_watsonx_orchestrate/docker/tempus/common-config.yaml +1 -1
  38. ibm_watsonx_orchestrate/experimental/flow_builder/__init__.py +0 -0
  39. ibm_watsonx_orchestrate/experimental/flow_builder/data_map.py +19 -0
  40. ibm_watsonx_orchestrate/experimental/flow_builder/flows/__init__.py +42 -0
  41. ibm_watsonx_orchestrate/experimental/flow_builder/flows/constants.py +19 -0
  42. ibm_watsonx_orchestrate/experimental/flow_builder/flows/decorators.py +144 -0
  43. ibm_watsonx_orchestrate/experimental/flow_builder/flows/events.py +72 -0
  44. ibm_watsonx_orchestrate/experimental/flow_builder/flows/flow.py +1310 -0
  45. ibm_watsonx_orchestrate/experimental/flow_builder/node.py +116 -0
  46. ibm_watsonx_orchestrate/experimental/flow_builder/resources/flow_status.openapi.yml +66 -0
  47. ibm_watsonx_orchestrate/experimental/flow_builder/types.py +765 -0
  48. ibm_watsonx_orchestrate/experimental/flow_builder/utils.py +115 -0
  49. ibm_watsonx_orchestrate/utils/utils.py +5 -2
  50. {ibm_watsonx_orchestrate-1.3.0.dist-info → ibm_watsonx_orchestrate-1.5.0b0.dist-info}/METADATA +4 -1
  51. {ibm_watsonx_orchestrate-1.3.0.dist-info → ibm_watsonx_orchestrate-1.5.0b0.dist-info}/RECORD +54 -32
  52. {ibm_watsonx_orchestrate-1.3.0.dist-info → ibm_watsonx_orchestrate-1.5.0b0.dist-info}/WHEEL +0 -0
  53. {ibm_watsonx_orchestrate-1.3.0.dist-info → ibm_watsonx_orchestrate-1.5.0b0.dist-info}/entry_points.txt +0 -0
  54. {ibm_watsonx_orchestrate-1.3.0.dist-info → ibm_watsonx_orchestrate-1.5.0b0.dist-info}/licenses/LICENSE +0 -0
@@ -7,18 +7,24 @@ import sys
7
7
  import tempfile
8
8
  import time
9
9
  from pathlib import Path
10
+ from urllib.parse import urlparse
10
11
 
11
12
  import jwt
12
13
  import requests
13
14
  import typer
14
15
  from dotenv import dotenv_values
15
16
 
16
- from ibm_watsonx_orchestrate.client.utils import instantiate_client, check_token_validity, is_local_dev
17
+ from ibm_watsonx_orchestrate.client.utils import instantiate_client
18
+
19
+ from ibm_watsonx_orchestrate.cli.commands.server.types import WatsonXAIEnvConfig, ModelGatewayEnvConfig
20
+
17
21
  from ibm_watsonx_orchestrate.cli.commands.environment.environment_controller import _login
22
+
18
23
  from ibm_watsonx_orchestrate.cli.config import LICENSE_HEADER, \
19
24
  ENV_ACCEPT_LICENSE
20
25
  from ibm_watsonx_orchestrate.cli.config import PROTECTED_ENV_NAME, clear_protected_env_credentials_token, Config, \
21
- AUTH_CONFIG_FILE_FOLDER, AUTH_CONFIG_FILE, AUTH_MCSP_TOKEN_OPT, AUTH_SECTION_HEADER, USER_ENV_CACHE_HEADER
26
+ AUTH_CONFIG_FILE_FOLDER, AUTH_CONFIG_FILE, AUTH_MCSP_TOKEN_OPT, AUTH_SECTION_HEADER, USER_ENV_CACHE_HEADER, LICENSE_HEADER, \
27
+ ENV_ACCEPT_LICENSE
22
28
  from ibm_watsonx_orchestrate.client.agents.agent_client import AgentClient
23
29
 
24
30
  logger = logging.getLogger(__name__)
@@ -60,7 +66,9 @@ def docker_login(api_key: str, registry_url: str, username:str = "iamapikey") ->
60
66
  logger.info("Successfully logged in to Docker.")
61
67
 
62
68
  def docker_login_by_dev_edition_source(env_dict: dict, source: str) -> None:
63
- registry_url = env_dict["REGISTRY_URL"]
69
+ if not env_dict.get("REGISTRY_URL"):
70
+ raise ValueError("REGISTRY_URL is not set.")
71
+ registry_url = env_dict["REGISTRY_URL"].split("/")[0]
64
72
  if source == "internal":
65
73
  iam_api_key = env_dict.get("DOCKER_IAM_KEY")
66
74
  if not iam_api_key:
@@ -73,8 +81,6 @@ def docker_login_by_dev_edition_source(env_dict: dict, source: str) -> None:
73
81
  docker_login(wo_entitlement_key, registry_url, "cp")
74
82
  elif source == "orchestrate":
75
83
  wo_auth_type = env_dict.get("WO_AUTH_TYPE")
76
- if not wo_auth_type:
77
- raise ValueError("WO_AUTH_TYPE is required in the environment file if WO_DEVELOPER_EDITION_SOURCE is set to 'orchestrate'.")
78
84
  api_key, username = get_docker_cred_by_wo_auth_type(env_dict, wo_auth_type)
79
85
  docker_login(api_key, registry_url, username)
80
86
 
@@ -108,24 +114,39 @@ def merge_env(
108
114
 
109
115
  return merged
110
116
 
111
- def get_default_registry_env_vars_by_dev_edition_source(env_dict: dict, source: str) -> dict[str,str]:
112
- component_registry_var_names = {key for key in env_dict if key.endswith("_REGISTRY")}
113
-
114
- result = {}
115
- if source == "internal":
116
- result["REGISTRY_URL"] = "us.icr.io"
117
- for name in component_registry_var_names:
118
- result[name] = "us.icr.io/watson-orchestrate-private"
119
- elif source == "myibm":
120
- result["REGISTRY_URL"] = "cp.icr.io"
121
- for name in component_registry_var_names:
122
- result[name] = "cp.icr.io/cp/wxo-lite"
123
- elif source == "orchestrate":
124
- raise NotImplementedError("The 'orchestrate' source is not implemented yet.")
125
- # TODO: confirm with Tej about the registry url for orchestrate source
117
+ def get_default_registry_env_vars_by_dev_edition_source(default_env: dict, user_env:dict, source: str) -> dict[str,str]:
118
+ component_registry_var_names = {key for key in default_env if key.endswith("_REGISTRY")} | {'REGISTRY_URL'}
119
+
120
+ registry_url = user_env.get("REGISTRY_URL", None)
121
+ if not registry_url:
122
+ if source == "internal":
123
+ registry_url = "us.icr.io/watson-orchestrate-private"
124
+ elif source == "myibm":
125
+ registry_url = "cp.icr.io/cp/wxo-lite"
126
+ elif source == "orchestrate":
127
+ # extract the hostname from the WO_INSTANCE URL, and replace the "api." prefix with "registry." to construct the registry URL per region
128
+ wo_url = user_env.get("WO_INSTANCE")
129
+
130
+ if not wo_url:
131
+ raise ValueError("WO_INSTANCE is required in the environment file if the developer edition source is set to 'orchestrate'.")
132
+
133
+ parsed = urlparse(wo_url)
134
+ hostname = parsed.hostname
135
+
136
+ if not hostname or not hostname.startswith("api."):
137
+ raise ValueError(f"Invalid WO_INSTANCE URL: '{wo_url}'. It should starts with 'api.'")
138
+
139
+ registry_url = f"registry.{hostname[4:]}/cp/wxo-lite"
140
+ else:
141
+ raise ValueError(f"Unknown value for developer edition source: {source}. Must be one of ['internal', 'myibm', 'orchestrate'].")
142
+
143
+ result = {name: registry_url for name in component_registry_var_names}
126
144
  return result
127
145
 
128
- def get_dev_edition_source(env_dict: dict) -> str:
146
+ def get_dev_edition_source(env_dict: dict | None) -> str:
147
+ if not env_dict:
148
+ return "myibm"
149
+
129
150
  source = env_dict.get("WO_DEVELOPER_EDITION_SOURCE")
130
151
 
131
152
  if source:
@@ -134,12 +155,28 @@ def get_dev_edition_source(env_dict: dict) -> str:
134
155
  return "orchestrate"
135
156
  return "myibm"
136
157
 
137
- def get_docker_cred_by_wo_auth_type(env_dict: dict, auth_type: str) -> tuple[str, str]:
158
+ def get_docker_cred_by_wo_auth_type(env_dict: dict, auth_type: str | None) -> tuple[str, str]:
159
+ # Try infer the auth type if not provided
160
+ if not auth_type:
161
+ instance_url = env_dict.get("WO_INSTANCE")
162
+ if instance_url:
163
+ if ".cloud.ibm.com" in instance_url:
164
+ auth_type = "ibm_iam"
165
+ elif ".ibm.com" in instance_url:
166
+ auth_type = "mcsp"
167
+
138
168
  if auth_type in {"mcsp", "ibm_iam"}:
139
169
  wo_api_key = env_dict.get("WO_API_KEY")
140
170
  if not wo_api_key:
141
171
  raise ValueError("WO_API_KEY is required in the environment file if the WO_AUTH_TYPE is set to 'mcsp' or 'ibm_iam'.")
142
- return wo_api_key, "wouser"
172
+ instance_url = env_dict.get("WO_INSTANCE")
173
+ if not instance_url:
174
+ raise ValueError("WO_INSTANCE is required in the environment file if the WO_AUTH_TYPE is set to 'mcsp' or 'ibm_iam'.")
175
+ path = urlparse(instance_url).path
176
+ if not path or '/' not in path:
177
+ raise ValueError(f"Invalid WO_INSTANCE URL: '{instance_url}'. It should contain the instance (tenant) id.")
178
+ tenant_id = path.split('/')[-1]
179
+ return wo_api_key, f"wxouser-{tenant_id}"
143
180
  elif auth_type == "cpd":
144
181
  wo_api_key = env_dict.get("WO_API_KEY")
145
182
  wo_password = env_dict.get("WO_PASSWORD")
@@ -150,7 +187,36 @@ def get_docker_cred_by_wo_auth_type(env_dict: dict, auth_type: str) -> tuple[str
150
187
  raise ValueError("WO_USERNAME is required in the environment file if the WO_AUTH_TYPE is set to 'cpd'.")
151
188
  return wo_api_key or wo_password, wo_username # type: ignore[return-value]
152
189
  else:
153
- raise ValueError(f"Unknown value for WO_AUTH_TYPE: {auth_type}. Must be one of ['mcsp', 'ibm_iam', 'cpd'].")
190
+ raise ValueError(f"Unknown value for WO_AUTH_TYPE: '{auth_type}'. Must be one of ['mcsp', 'ibm_iam', 'cpd'].")
191
+
192
+ def apply_server_env_dict_defaults(provided_env_dict: dict) -> dict:
193
+
194
+ env_dict = provided_env_dict.copy()
195
+
196
+ env_dict['DBTAG'] = get_dbtag_from_architecture(merged_env_dict=env_dict)
197
+
198
+ model_config = None
199
+ try:
200
+ use_model_proxy = env_dict.get("USE_SAAS_ML_TOOLS_RUNTIME")
201
+ if not use_model_proxy or use_model_proxy.lower() != 'true':
202
+ model_config = WatsonXAIEnvConfig.model_validate(env_dict)
203
+ except ValueError:
204
+ pass
205
+
206
+ # If no watsonx ai detials are found, try build model gateway config
207
+ if not model_config:
208
+ try:
209
+ model_config = ModelGatewayEnvConfig.model_validate(env_dict)
210
+ except ValueError as e :
211
+ pass
212
+
213
+ if not model_config:
214
+ logger.error("Missing required model access environment variables. Please set Watson Orchestrate credentials 'WO_INSTANCE' and 'WO_API_KEY'. For CPD, set 'WO_INSTANCE', 'WO_USERNAME' and either 'WO_API_KEY' or 'WO_PASSWORD'. Alternatively, you can set WatsonX AI credentials directly using 'WATSONX_SPACE_ID' and 'WATSONX_APIKEY'")
215
+ sys.exit(1)
216
+
217
+ env_dict.update(model_config.model_dump(exclude_none=True))
218
+
219
+ return env_dict
154
220
 
155
221
  def apply_llm_api_key_defaults(env_dict: dict) -> None:
156
222
  llm_value = env_dict.get("WATSONX_APIKEY")
@@ -197,7 +263,7 @@ NON_SECRET_ENV_ITEMS = {
197
263
  "WO_DEVELOPER_EDITION_SOURCE",
198
264
  "WO_INSTANCE",
199
265
  "USE_SAAS_ML_TOOLS_RUNTIME",
200
- "WXO_MCSP_EXCHANGE_URL",
266
+ "AUTHORIZATION_URL",
201
267
  "OPENSOURCE_REGISTRY_PROXY"
202
268
  }
203
269
  def persist_user_env(env: dict, include_secrets: bool = False) -> None:
@@ -218,7 +284,7 @@ def get_persisted_user_env() -> dict | None:
218
284
  user_env = cfg.get(USER_ENV_CACHE_HEADER) if cfg.get(USER_ENV_CACHE_HEADER) else None
219
285
  return user_env
220
286
 
221
- def run_compose_lite(final_env_file: Path, experimental_with_langfuse=False, with_flow_runtime=False) -> None:
287
+ def run_compose_lite(final_env_file: Path, experimental_with_langfuse=False) -> None:
222
288
  compose_path = get_compose_file()
223
289
  compose_command = ensure_docker_compose_installed()
224
290
  db_tag = read_env_file(final_env_file).get('DBTAG', None)
@@ -253,10 +319,6 @@ def run_compose_lite(final_env_file: Path, experimental_with_langfuse=False, wit
253
319
  else:
254
320
  command = compose_command
255
321
 
256
- # Check if we start the server with tempus-runtime.
257
- if with_flow_runtime:
258
- command += ['--profile', 'with-tempus-runtime']
259
-
260
322
  command += [
261
323
  "-f", str(compose_path),
262
324
  "--env-file", str(final_env_file),
@@ -338,14 +400,19 @@ def run_compose_lite_ui(user_env_file: Path) -> bool:
338
400
  default_env = read_env_file(get_default_env_file())
339
401
  user_env = read_env_file(user_env_file) if user_env_file else {}
340
402
  if not user_env:
341
- user_env = get_persisted_user_env()
403
+ user_env = get_persisted_user_env() or {}
342
404
 
343
405
  dev_edition_source = get_dev_edition_source(user_env)
344
- default_registry_vars = get_default_registry_env_vars_by_dev_edition_source(default_env, source=dev_edition_source)
406
+ default_registry_vars = get_default_registry_env_vars_by_dev_edition_source(default_env, user_env, source=dev_edition_source)
407
+
408
+ # Update the default environment with the default registry variables only if they are not already set
409
+ for key in default_registry_vars:
410
+ if key not in default_env or not default_env[key]:
411
+ default_env[key] = default_registry_vars[key]
345
412
 
413
+ # Merge the default environment with the user environment
346
414
  merged_env_dict = {
347
415
  **default_env,
348
- **default_registry_vars,
349
416
  **user_env,
350
417
  }
351
418
 
@@ -498,6 +565,8 @@ def run_compose_lite_logs(final_env_file: Path, is_reset: bool = False) -> None:
498
565
  command = compose_command + [
499
566
  "-f", str(compose_path),
500
567
  "--env-file", str(final_env_file),
568
+ "--profile",
569
+ "*",
501
570
  "logs",
502
571
  "-f"
503
572
  ]
@@ -557,15 +626,8 @@ def server_start(
557
626
  experimental_with_langfuse: bool = typer.Option(
558
627
  False,
559
628
  '--with-langfuse', '-l',
560
- help=''
629
+ help='Option to enable Langfuse support.'
561
630
  ),
562
- with_flow_runtime: bool = typer.Option(
563
- False,
564
- '--with-tempus-runtime', '-f',
565
- help='Option to start server with tempus-runtime.',
566
- hidden=True
567
- )
568
- ,
569
631
  persist_env_secrets: bool = typer.Option(
570
632
  False,
571
633
  '--persist-env-secrets', '-p',
@@ -588,16 +650,26 @@ def server_start(
588
650
  default_env = read_env_file(get_default_env_file())
589
651
  user_env = read_env_file(user_env_file) if user_env_file else {}
590
652
  persist_user_env(user_env, include_secrets=persist_env_secrets)
653
+
591
654
  dev_edition_source = get_dev_edition_source(user_env)
592
- default_registry_vars = get_default_registry_env_vars_by_dev_edition_source(default_env, source=dev_edition_source)
655
+ default_registry_vars = get_default_registry_env_vars_by_dev_edition_source(default_env, user_env, source=dev_edition_source)
593
656
 
657
+ # Update the default environment with the default registry variables only if they are not already set
658
+ for key in default_registry_vars:
659
+ if key not in default_env or not default_env[key]:
660
+ default_env[key] = default_registry_vars[key]
661
+
662
+ # Merge the default environment with the user environment
594
663
  merged_env_dict = {
595
664
  **default_env,
596
- **default_registry_vars,
597
665
  **user_env,
598
666
  }
599
667
 
600
- merged_env_dict['DBTAG'] = get_dbtag_from_architecture(merged_env_dict=merged_env_dict)
668
+ merged_env_dict = apply_server_env_dict_defaults(merged_env_dict)
669
+
670
+ # Add LANGFUSE_ENABLED into the merged_env_dict, for tempus to pick up.
671
+ if experimental_with_langfuse:
672
+ merged_env_dict['LANGFUSE_ENABLED'] = 'true'
601
673
 
602
674
  try:
603
675
  docker_login_by_dev_edition_source(merged_env_dict, dev_edition_source)
@@ -609,7 +681,7 @@ def server_start(
609
681
 
610
682
 
611
683
  final_env_file = write_merged_env_file(merged_env_dict)
612
- run_compose_lite(final_env_file=final_env_file, experimental_with_langfuse=experimental_with_langfuse, with_flow_runtime=with_flow_runtime)
684
+ run_compose_lite(final_env_file=final_env_file, experimental_with_langfuse=experimental_with_langfuse)
613
685
 
614
686
  run_db_migration()
615
687
 
@@ -638,9 +710,6 @@ def server_start(
638
710
  if experimental_with_langfuse:
639
711
  logger.info(f"You can access the observability platform Langfuse at http://localhost:3010, username: orchestrate@ibm.com, password: orchestrate")
640
712
 
641
- if with_flow_runtime:
642
- logger.info(f"Starting with flow runtime")
643
-
644
713
  @server_app.command(name="stop")
645
714
  def server_stop(
646
715
  user_env_file: str = typer.Option(
@@ -0,0 +1,105 @@
1
+ import logging
2
+ import sys
3
+ from enum import Enum
4
+ from pydantic import BaseModel, model_validator, ConfigDict
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ class WoAuthType(str, Enum):
9
+ MCSP="mcsp"
10
+ IBM_IAM="ibm_iam"
11
+ CPD="cpd"
12
+
13
+ def __str__(self):
14
+ return self.value
15
+
16
+ def __repr__(self):
17
+ return repr(self.value)
18
+
19
+ AUTH_TYPE_DEFAULT_URL_MAPPING = {
20
+ WoAuthType.MCSP: "https://iam.platform.saas.ibm.com/siusermgr/api/1.0/apikeys/token",
21
+ WoAuthType.IBM_IAM: "https://iam.cloud.ibm.com/identity/token",
22
+ }
23
+
24
+ def _infer_auth_type_from_instance_url(instance_url: str) -> WoAuthType:
25
+ if ".cloud.ibm.com" in instance_url:
26
+ return WoAuthType.IBM_IAM
27
+ if ".ibm.com" in instance_url:
28
+ return WoAuthType.MCSP
29
+ return WoAuthType.CPD
30
+
31
+
32
+ class WatsonXAIEnvConfig(BaseModel):
33
+ WATSONX_SPACE_ID: str
34
+ WATSONX_APIKEY: str
35
+ USE_SAAS_ML_TOOLS_RUNTIME: bool
36
+
37
+ @model_validator(mode="before")
38
+ def validate_wxai_config(values):
39
+ relevant_fields = WatsonXAIEnvConfig.model_fields.keys()
40
+ config = {k: values.get(k) for k in relevant_fields}
41
+
42
+ if not config.get("WATSONX_SPACE_ID") and not config.get("WATSONX_APIKEY"):
43
+ raise ValueError("Missing configuration requirements 'WATSONX_SPACE_ID' and 'WATSONX_APIKEY'")
44
+
45
+ if config.get("WATSONX_SPACE_ID") and not config.get("WATSONX_APIKEY"):
46
+ logger.error("Cannot use env var 'WATSONX_SPACE_ID' without setting the corresponding 'WATSONX_APIKEY'")
47
+ sys.exit(1)
48
+
49
+ if not config.get("WATSONX_SPACE_ID") and config.get("WATSONX_APIKEY"):
50
+ logger.error("Cannot use env var 'WATSONX_APIKEY' without setting the corresponding 'WATSONX_SPACE_ID'")
51
+ sys.exit(1)
52
+
53
+ config["USE_SAAS_ML_TOOLS_RUNTIME"] = False
54
+ return config
55
+
56
+
57
+ class ModelGatewayEnvConfig(BaseModel):
58
+ WO_API_KEY: str | None = None
59
+ WO_USERNAME: str | None = None
60
+ WO_PASSWORD: str | None = None
61
+ WO_INSTANCE: str
62
+ AUTHORIZATION_URL: str
63
+ USE_SAAS_ML_TOOLS_RUNTIME: bool
64
+ WO_AUTH_TYPE: WoAuthType
65
+ WATSONX_SPACE_ID: str
66
+
67
+ @model_validator(mode="before")
68
+ def validate_model_gateway_config(values):
69
+ relevant_fields = ModelGatewayEnvConfig.model_fields.keys()
70
+ config = {k: values.get(k) for k in relevant_fields}
71
+
72
+ if not config.get("WO_INSTANCE"):
73
+ raise ValueError("Missing configuration requirements 'WO_INSTANCE'")
74
+
75
+ if not config.get("WO_AUTH_TYPE"):
76
+ inferred_auth_type = _infer_auth_type_from_instance_url(config.get("WO_INSTANCE"))
77
+ if not inferred_auth_type:
78
+ logger.error(f"Could not infer auth type from 'WO_INSTANCE'. Please set the 'WO_AUTH_TYPE' explictly")
79
+ sys.exit(1)
80
+ config["WO_AUTH_TYPE"] = inferred_auth_type
81
+ auth_type = config.get("WO_AUTH_TYPE")
82
+
83
+ if not config.get("AUTHORIZATION_URL"):
84
+ inferred_auth_url = AUTH_TYPE_DEFAULT_URL_MAPPING.get(auth_type)
85
+ if not inferred_auth_url:
86
+ logger.error(f"No 'AUTHORIZATION_URL' found. Auth type '{auth_type}' does not support defaulting. Please set the 'AUTHORIZATION_URL' explictly")
87
+ sys.exit(1)
88
+ config["AUTHORIZATION_URL"] = inferred_auth_url
89
+
90
+ if auth_type != WoAuthType.CPD:
91
+ if not config.get("WO_API_KEY"):
92
+ logger.error(f"Auth type '{auth_type}' requires 'WO_API_KEY' to be set as an env var.")
93
+ sys.exit(1)
94
+ else:
95
+ if not config.get("WO_USERNAME"):
96
+ logger.error("Auth type 'cpd' requires 'WO_USERNAME' to be set as an env var.")
97
+ sys.exit(1)
98
+ if not config.get("WO_API_KEY") and not config.get("WO_PASSWORD"):
99
+ logger.error("Auth type 'cpd' requires either 'WO_API_KEY' or 'WO_PASSWORD' to be set as env vars.")
100
+ sys.exit(1)
101
+
102
+ config["USE_SAAS_ML_TOOLS_RUNTIME"] = True
103
+ # Fake (but valid) UUIDv4 for knowledgebase check
104
+ config["WATSONX_SPACE_ID"] = "aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaaaa"
105
+ return config
@@ -1,7 +1,12 @@
1
1
  import typer
2
2
  from typing import List
3
3
  from typing_extensions import Annotated, Optional
4
- from ibm_watsonx_orchestrate.cli.commands.toolkit.toolkit_controller import ToolkitController, ToolkitKind
4
+ from ibm_watsonx_orchestrate.agent_builder.toolkits.types import ToolkitKind, Language
5
+ from ibm_watsonx_orchestrate.cli.commands.toolkit.toolkit_controller import ToolkitController
6
+ import logging
7
+ import sys
8
+
9
+ logger = logging.getLogger(__name__)
5
10
 
6
11
  toolkits_app = typer.Typer(no_args_is_help=True)
7
12
 
@@ -19,10 +24,18 @@ def import_toolkit(
19
24
  str,
20
25
  typer.Option("--description", help="Description of the toolkit"),
21
26
  ],
27
+ package: Annotated[
28
+ str,
29
+ typer.Option("--package", help="NPM or Python package of the MCP server"),
30
+ ] = None,
22
31
  package_root: Annotated[
23
32
  str,
24
- typer.Option("--package-root", "-p", help="Root directory of the MCP server package"),
25
- ],
33
+ typer.Option("--package-root", help="Root directory of the MCP server package"),
34
+ ] = None,
35
+ language: Annotated[
36
+ Language,
37
+ typer.Option("--language", "-l", help="Language your package is based on")
38
+ ] = None,
26
39
  command: Annotated[
27
40
  str,
28
41
  typer.Option(
@@ -31,7 +44,7 @@ def import_toolkit(
31
44
  "or a JSON-style list of arguments (e.g. '[\"node\", \"dist/index.js\", \"--transport\", \"stdio\"]'). "
32
45
  "The first argument will be used as the executable, the rest as its arguments."
33
46
  ),
34
- ],
47
+ ] = None,
35
48
  tools: Annotated[
36
49
  Optional[str],
37
50
  typer.Option("--tools", "-t", help="Comma-separated list of tools to import. Or you can use `*` to use all tools"),
@@ -40,26 +53,61 @@ def import_toolkit(
40
53
  List[str],
41
54
  typer.Option(
42
55
  "--app-id", "-a",
43
- help='The app id of the connection to associate with this tool. A application connection represents the server authentication credentials needed to connect to this tool. Only type key_value is currently supported for MCP.'
56
+ help='The app ids of the connections to associate with this tool. A application connection represents the server authentication credentials needed to connect to this tool. Only type key_value is currently supported for MCP.'
44
57
  )
45
58
  ] = None
46
59
  ):
47
- if tools == "*":
48
- tool_list = ["*"] # Wildcard to use all tools
60
+ if tools == "*": # Wildcard to use all tools for MCP
61
+ tool_list = ["*"]
49
62
  elif tools:
50
63
  tool_list = [tool.strip() for tool in tools.split(",")]
51
64
  else:
52
65
  tool_list = None
53
66
 
67
+ if not package and not package_root:
68
+ logger.error("You must provide either '--package' or '--package-root'.")
69
+ sys.exit(1)
70
+
71
+ if package_root and not command:
72
+ logger.error("Error: '--command' flag must be provided when '--package-root' is specified.")
73
+ sys.exit(1)
74
+
75
+ if package_root and package:
76
+ logger.error("Please choose either '--package-root' or '--package' but not both.")
77
+ sys.exit(1)
78
+
79
+ if package and not package_root:
80
+ if not command:
81
+ if language == Language.NODE:
82
+ command = f"npx -y {package}"
83
+ elif language == Language.PYTHON:
84
+ command = f"python -m {package}"
85
+ else:
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
+ sys.exit(1)
88
+
89
+
54
90
  toolkit_controller = ToolkitController(
55
91
  kind=kind,
56
92
  name=name,
57
93
  description=description,
94
+ package=package,
58
95
  package_root=package_root,
96
+ language=language,
59
97
  command=command,
60
98
  )
61
99
  toolkit_controller.import_toolkit(tools=tool_list, app_id=app_id)
62
100
 
101
+ @toolkits_app.command(name="list")
102
+ def list_toolkits(
103
+ verbose: Annotated[
104
+ bool,
105
+ typer.Option("--verbose", "-v", help="List full details of all toolkits as json"),
106
+ ] = False,
107
+ ):
108
+ toolkit_controller = ToolkitController()
109
+ toolkit_controller.list_toolkits(verbose=verbose)
110
+
63
111
  @toolkits_app.command(name="remove")
64
112
  def remove_toolkit(
65
113
  name: Annotated[