ibm-watsonx-orchestrate 1.11.0b0__py3-none-any.whl → 1.11.1__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 (27) hide show
  1. ibm_watsonx_orchestrate/__init__.py +2 -1
  2. ibm_watsonx_orchestrate/agent_builder/agents/types.py +13 -0
  3. ibm_watsonx_orchestrate/agent_builder/connections/connections.py +5 -2
  4. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +25 -10
  5. ibm_watsonx_orchestrate/cli/commands/agents/agents_command.py +10 -2
  6. ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +404 -173
  7. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_controller.py +6 -2
  8. ibm_watsonx_orchestrate/cli/commands/environment/environment_command.py +6 -2
  9. ibm_watsonx_orchestrate/cli/commands/environment/environment_controller.py +6 -3
  10. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_command.py +174 -2
  11. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_controller.py +93 -9
  12. ibm_watsonx_orchestrate/cli/commands/server/types.py +1 -1
  13. ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +8 -5
  14. ibm_watsonx_orchestrate/cli/config.py +3 -1
  15. ibm_watsonx_orchestrate/client/base_api_client.py +31 -10
  16. ibm_watsonx_orchestrate/client/connections/connections_client.py +14 -0
  17. ibm_watsonx_orchestrate/client/service_instance.py +19 -34
  18. ibm_watsonx_orchestrate/client/utils.py +3 -1
  19. ibm_watsonx_orchestrate/docker/compose-lite.yml +12 -11
  20. ibm_watsonx_orchestrate/docker/default.env +13 -13
  21. ibm_watsonx_orchestrate/flow_builder/flows/flow.py +3 -1
  22. ibm_watsonx_orchestrate/flow_builder/types.py +252 -1
  23. {ibm_watsonx_orchestrate-1.11.0b0.dist-info → ibm_watsonx_orchestrate-1.11.1.dist-info}/METADATA +2 -2
  24. {ibm_watsonx_orchestrate-1.11.0b0.dist-info → ibm_watsonx_orchestrate-1.11.1.dist-info}/RECORD +27 -27
  25. {ibm_watsonx_orchestrate-1.11.0b0.dist-info → ibm_watsonx_orchestrate-1.11.1.dist-info}/WHEEL +0 -0
  26. {ibm_watsonx_orchestrate-1.11.0b0.dist-info → ibm_watsonx_orchestrate-1.11.1.dist-info}/entry_points.txt +0 -0
  27. {ibm_watsonx_orchestrate-1.11.0b0.dist-info → ibm_watsonx_orchestrate-1.11.1.dist-info}/licenses/LICENSE +0 -0
@@ -43,9 +43,13 @@ def activate_env(
43
43
  test_package_version_override: Annotated[
44
44
  str,
45
45
  typer.Option("--test-package-version-override", help="Which prereleased package version to reference when using --registry testpypi", hidden=True),
46
+ ] = None,
47
+ skip_version_check: Annotated[
48
+ bool,
49
+ typer.Option('--skip-version-check/--enable-version-check', help='Use this flag to skip validating that adk version in use exists in pypi (for clients who mirror the ADK to a local registry and do not have local access to pypi).')
46
50
  ] = None
47
51
  ):
48
- environment_controller.activate(name=name, apikey=apikey,username=username, password=password, registry=registry, test_package_version_override=test_package_version_override)
52
+ environment_controller.activate(name=name, apikey=apikey, username=username, password=password, registry=registry, test_package_version_override=test_package_version_override, skip_version_check=skip_version_check)
49
53
 
50
54
 
51
55
  @environment_app.command(name="add")
@@ -70,7 +74,7 @@ def add_env(
70
74
  ] = None,
71
75
  type: Annotated[
72
76
  EnvironmentAuthType,
73
- typer.Option("--type", "-t", help="The type of auth you wish to use"),
77
+ typer.Option("--type", "-t", help="The type of auth you wish to use. This overrides the auth type that is inferred from the url"),
74
78
  ] = None,
75
79
  insecure: Annotated[
76
80
  bool,
@@ -18,8 +18,9 @@ from ibm_watsonx_orchestrate.cli.config import (
18
18
  ENV_IAM_URL_OPT,
19
19
  ENVIRONMENTS_SECTION_HEADER,
20
20
  PROTECTED_ENV_NAME,
21
- ENV_AUTH_TYPE, PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_TYPE_OPT, PYTHON_REGISTRY_TEST_PACKAGE_VERSION_OVERRIDE_OPT, BYPASS_SSL, VERIFY,
22
- DEFAULT_CONFIG_FILE_CONTENT
21
+ ENV_AUTH_TYPE, PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_TYPE_OPT, PYTHON_REGISTRY_TEST_PACKAGE_VERSION_OVERRIDE_OPT,
22
+ BYPASS_SSL, VERIFY,
23
+ DEFAULT_CONFIG_FILE_CONTENT, PYTHON_REGISTRY_SKIP_VERSION_CHECK_OPT
23
24
  )
24
25
  from ibm_watsonx_orchestrate.client.client import Client
25
26
  from ibm_watsonx_orchestrate.client.client_errors import ClientError
@@ -131,7 +132,7 @@ def _login(name: str, apikey: str = None, username: str = None, password: str =
131
132
  except ClientError as e:
132
133
  raise ClientError(e)
133
134
 
134
- def activate(name: str, apikey: str=None, username: str=None, password: str=None, registry: RegistryType=None, test_package_version_override=None) -> None:
135
+ def activate(name: str, apikey: str=None, username: str=None, password: str=None, registry: RegistryType=None, test_package_version_override=None, skip_version_check=None) -> None:
135
136
  cfg = Config()
136
137
  auth_cfg = Config(AUTH_CONFIG_FILE_FOLDER, AUTH_CONFIG_FILE)
137
138
  env_cfg = cfg.read(ENVIRONMENTS_SECTION_HEADER, name)
@@ -159,6 +160,8 @@ def activate(name: str, apikey: str=None, username: str=None, password: str=None
159
160
  elif cfg.read(PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_TYPE_OPT) is None:
160
161
  cfg.write(PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_TYPE_OPT, DEFAULT_CONFIG_FILE_CONTENT[PYTHON_REGISTRY_HEADER][PYTHON_REGISTRY_TYPE_OPT])
161
162
  cfg.write(PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_TEST_PACKAGE_VERSION_OVERRIDE_OPT, test_package_version_override)
163
+ if skip_version_check is not None:
164
+ cfg.write(PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_SKIP_VERSION_CHECK_OPT, skip_version_check)
162
165
 
163
166
  logger.info(f"Environment '{name}' is now active")
164
167
  is_cpd = is_cpd_env(url)
@@ -16,7 +16,7 @@ from typing import Optional
16
16
  from typing_extensions import Annotated
17
17
 
18
18
  from ibm_watsonx_orchestrate import __version__
19
- from ibm_watsonx_orchestrate.cli.commands.evaluations.evaluations_controller import EvaluationsController
19
+ from ibm_watsonx_orchestrate.cli.commands.evaluations.evaluations_controller import EvaluationsController, EvaluateMode
20
20
  from ibm_watsonx_orchestrate.cli.commands.agents.agents_controller import AgentsController
21
21
 
22
22
  logger = logging.getLogger(__name__)
@@ -220,6 +220,13 @@ def analyze(data_path: Annotated[
220
220
  help="Path to the directory that has the saved results"
221
221
  )
222
222
  ],
223
+ tool_definition_path: Annotated[
224
+ Optional[str],
225
+ typer.Option(
226
+ "--tools-path", "-t",
227
+ help="Path to the directory containing tool definitions."
228
+ )
229
+ ] = None,
223
230
  user_env_file: Annotated[
224
231
  Optional[str],
225
232
  typer.Option(
@@ -230,7 +237,10 @@ def analyze(data_path: Annotated[
230
237
 
231
238
  validate_watsonx_credentials(user_env_file)
232
239
  controller = EvaluationsController()
233
- controller.analyze(data_path=data_path)
240
+ controller.analyze(
241
+ data_path=data_path,
242
+ tool_definition_path=tool_definition_path
243
+ )
234
244
 
235
245
  @evaluation_app.command(name="validate-external", help="Validate an external agent against a set of inputs")
236
246
  def validate_external(
@@ -375,3 +385,165 @@ def validate_external(
375
385
  msg = f"[dark_orange]Schema validation did not succeed. See '{str(validation_folder)}' for failures.[/dark_orange]"
376
386
 
377
387
  rich.print(Panel(msg))
388
+
389
+ @evaluation_app.command(name="quick-eval",
390
+ short_help="Evaluate agent against a suite of static metrics and LLM-as-a-judge metrics",
391
+ help="""
392
+ Use the quick-eval command to evaluate your agent against a suite of static metrics and LLM-as-a-judge metrics.
393
+ """)
394
+ def quick_eval(
395
+ config_file: Annotated[
396
+ Optional[str],
397
+ typer.Option(
398
+ "--config", "-c",
399
+ help="Path to YAML configuration file containing evaluation settings."
400
+ )
401
+ ] = None,
402
+ test_paths: Annotated[
403
+ Optional[str],
404
+ typer.Option(
405
+ "--test-paths", "-p",
406
+ help="Paths to the test files and/or directories to evaluate, separated by commas."
407
+ ),
408
+ ] = None,
409
+ tools_path: Annotated[
410
+ str,
411
+ typer.Option(
412
+ "--tools-path", "-t",
413
+ help="Path to the directory containing tool definitions."
414
+ )
415
+ ] = None,
416
+ output_dir: Annotated[
417
+ Optional[str],
418
+ typer.Option(
419
+ "--output-dir", "-o",
420
+ help="Directory to save the evaluation results."
421
+ )
422
+ ] = None,
423
+ user_env_file: Annotated[
424
+ Optional[str],
425
+ typer.Option(
426
+ "--env-file", "-e",
427
+ help="Path to a .env file that overrides default.env. Then environment variables override both."
428
+ ),
429
+ ] = None
430
+ ):
431
+ if not config_file:
432
+ if not test_paths or not output_dir:
433
+ logger.error("Error: Both --test-paths and --output-dir must be provided when not using a config file")
434
+ exit(1)
435
+
436
+ validate_watsonx_credentials(user_env_file)
437
+
438
+ if tools_path is None:
439
+ logger.error("When running `quick-eval`, please provide the path to your tools file.")
440
+ sys.exit(1)
441
+
442
+ controller = EvaluationsController()
443
+ controller.evaluate(
444
+ config_file=config_file,
445
+ test_paths=test_paths,
446
+ output_dir=output_dir,
447
+ tools_path=tools_path, mode=EvaluateMode.referenceless
448
+ )
449
+
450
+
451
+ red_teaming_app = typer.Typer(no_args_is_help=True)
452
+ evaluation_app.add_typer(red_teaming_app, name="red-teaming")
453
+
454
+
455
+ @red_teaming_app.command("list", help="List available red-teaming attack plans")
456
+ def list_plans():
457
+ controller = EvaluationsController()
458
+ controller.list_red_teaming_attacks()
459
+
460
+
461
+ @red_teaming_app.command("plan", help="Generate red-teaming attacks")
462
+ def plan(
463
+ attacks_list: Annotated[
464
+ str,
465
+ typer.Option(
466
+ "--attacks-list",
467
+ "-a",
468
+ help="Comma-separated list of red-teaming attacks to generate.",
469
+ ),
470
+ ],
471
+ datasets_path: Annotated[
472
+ str,
473
+ typer.Option(
474
+ "--datasets-path",
475
+ "-d",
476
+ help="Path to datasets for red-teaming. This can also be a comma-separated list of files or directories.",
477
+ ),
478
+ ],
479
+ agents_path: Annotated[
480
+ str, typer.Option("--agents-path", "-g", help="Path to the directory containing all agent definitions.")
481
+ ],
482
+ target_agent_name: Annotated[
483
+ str,
484
+ typer.Option(
485
+ "--target-agent-name",
486
+ "-t",
487
+ help="Name of the target agent to attack (should be present in agents-path).",
488
+ ),
489
+ ],
490
+ output_dir: Annotated[
491
+ Optional[str],
492
+ typer.Option("--output-dir", "-o", help="Directory to save generated attacks.")
493
+ ]=None,
494
+ user_env_file: Annotated[
495
+ Optional[str],
496
+ typer.Option(
497
+ "--env-file",
498
+ "-e",
499
+ help="Path to a .env file that overrides default.env. Then environment variables override both.",
500
+ ),
501
+ ] = None,
502
+ max_variants: Annotated[
503
+ Optional[int],
504
+ typer.Option(
505
+ "--max_variants",
506
+ "-n",
507
+ help="Number of variants to generate per attack type.",
508
+ ),
509
+ ] = None,
510
+
511
+ ):
512
+ validate_watsonx_credentials(user_env_file)
513
+ controller = EvaluationsController()
514
+ controller.generate_red_teaming_attacks(
515
+ attacks_list=attacks_list,
516
+ datasets_path=datasets_path,
517
+ agents_path=agents_path,
518
+ target_agent_name=target_agent_name,
519
+ output_dir=output_dir,
520
+ max_variants=max_variants,
521
+ )
522
+
523
+
524
+ @red_teaming_app.command("run", help="Run red-teaming attacks")
525
+ def run(
526
+ attack_paths: Annotated[
527
+ str,
528
+ typer.Option(
529
+ "--attack-paths",
530
+ "-a",
531
+ help="Path or list of paths (comma-separated) to directories containing attack files.",
532
+ ),
533
+ ],
534
+ output_dir: Annotated[
535
+ Optional[str],
536
+ typer.Option("--output-dir", "-o", help="Directory to save attack results."),
537
+ ] = None,
538
+ user_env_file: Annotated[
539
+ Optional[str],
540
+ typer.Option(
541
+ "--env-file",
542
+ "-e",
543
+ help="Path to a .env file that overrides default.env. Then environment variables override both.",
544
+ ),
545
+ ] = None,
546
+ ):
547
+ validate_watsonx_credentials(user_env_file)
548
+ controller = EvaluationsController()
549
+ controller.run_red_teaming_attacks(attack_paths=attack_paths, output_dir=output_dir)
@@ -1,17 +1,23 @@
1
1
  import logging
2
2
  import os.path
3
3
  from typing import List, Dict, Optional, Tuple
4
+ from enum import StrEnum
4
5
  import csv
5
6
  from pathlib import Path
6
7
  import sys
7
8
  from wxo_agentic_evaluation import main as evaluate
9
+ from wxo_agentic_evaluation import quick_eval
8
10
  from wxo_agentic_evaluation.tool_planner import build_snapshot
9
- from wxo_agentic_evaluation.analyze_run import analyze
11
+ from wxo_agentic_evaluation.analyze_run import Analyzer
10
12
  from wxo_agentic_evaluation.batch_annotate import generate_test_cases_from_stories
11
- from wxo_agentic_evaluation.arg_configs import TestConfig, AuthConfig, LLMUserConfig, ChatRecordingConfig, AnalyzeConfig, ProviderConfig
13
+ from wxo_agentic_evaluation.arg_configs import TestConfig, AuthConfig, LLMUserConfig, ChatRecordingConfig, AnalyzeConfig, ProviderConfig, AttackConfig, QuickEvalConfig
12
14
  from wxo_agentic_evaluation.record_chat import record_chats
13
15
  from wxo_agentic_evaluation.external_agent.external_validate import ExternalAgentValidation
14
16
  from wxo_agentic_evaluation.external_agent.performance_test import ExternalAgentPerformanceTest
17
+ from wxo_agentic_evaluation.red_teaming.attack_list import print_attacks
18
+ from wxo_agentic_evaluation.red_teaming import attack_generator
19
+ from wxo_agentic_evaluation.red_teaming.attack_runner import run_attacks
20
+ from wxo_agentic_evaluation.arg_configs import AttackGeneratorConfig
15
21
  from ibm_watsonx_orchestrate import __version__
16
22
  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
17
23
  from ibm_watsonx_orchestrate.utils.utils import yaml_safe_load
@@ -21,6 +27,9 @@ import uuid
21
27
 
22
28
  logger = logging.getLogger(__name__)
23
29
 
30
+ class EvaluateMode(StrEnum):
31
+ default = "default" # referenceFUL evaluation
32
+ referenceless = "referenceless"
24
33
 
25
34
  class EvaluationsController:
26
35
  def __init__(self):
@@ -38,7 +47,7 @@ class EvaluationsController:
38
47
 
39
48
  return url, tenant_name, token
40
49
 
41
- def evaluate(self, config_file: Optional[str] = None, test_paths: Optional[str] = None, output_dir: Optional[str] = None) -> None:
50
+ def evaluate(self, config_file: Optional[str] = None, test_paths: Optional[str] = None, output_dir: Optional[str] = None, tools_path: str = None, mode: str = EvaluateMode.default) -> None:
42
51
  url, tenant_name, token = self._get_env_config()
43
52
 
44
53
  if "WATSONX_SPACE_ID" in os.environ and "WATSONX_APIKEY" in os.environ:
@@ -90,9 +99,13 @@ class EvaluationsController:
90
99
  config_data["output_dir"] = output_dir
91
100
  logger.info(f"Using output directory: {config_data['output_dir']}")
92
101
 
93
- config = TestConfig(**config_data)
94
-
95
- evaluate.main(config)
102
+ if mode == EvaluateMode.default:
103
+ config = TestConfig(**config_data)
104
+ evaluate.main(config)
105
+ elif mode == EvaluateMode.referenceless:
106
+ config_data["tools_path"] = tools_path
107
+ config = QuickEvalConfig(**config_data)
108
+ quick_eval.main(config)
96
109
 
97
110
  def record(self, output_dir) -> None:
98
111
 
@@ -160,9 +173,13 @@ class EvaluationsController:
160
173
 
161
174
  logger.info("Test cases stored at: %s", output_dir)
162
175
 
163
- def analyze(self, data_path: str) -> None:
164
- config = AnalyzeConfig(data_path=data_path)
165
- analyze(config)
176
+ def analyze(self, data_path: str, tool_definition_path: str) -> None:
177
+ config = AnalyzeConfig(
178
+ data_path=data_path,
179
+ tool_definition_path=tool_definition_path
180
+ )
181
+ analyzer = Analyzer()
182
+ analyzer.analyze(config)
166
183
 
167
184
  def summarize(self) -> None:
168
185
  pass
@@ -187,3 +204,70 @@ class EvaluationsController:
187
204
  generated_performance_tests = performance_test.generate_tests()
188
205
 
189
206
  return generated_performance_tests
207
+
208
+ def list_red_teaming_attacks(self):
209
+ print_attacks()
210
+
211
+ def generate_red_teaming_attacks(
212
+ self,
213
+ attacks_list: str,
214
+ datasets_path: str,
215
+ agents_path: str,
216
+ target_agent_name: str,
217
+ output_dir: Optional[str] = None,
218
+ max_variants: Optional[int] = None,
219
+ ):
220
+ if output_dir is None:
221
+ output_dir = os.path.join(os.getcwd(), "red_teaming_attacks")
222
+ os.makedirs(output_dir, exist_ok=True)
223
+ logger.info(f"No output directory specified. Using default: {output_dir}")
224
+
225
+ results = attack_generator.main(
226
+ AttackGeneratorConfig(
227
+ attacks_list=attacks_list.split(","),
228
+ datasets_path=datasets_path.split(","),
229
+ agents_path=agents_path,
230
+ target_agent_name=target_agent_name,
231
+ output_dir=output_dir,
232
+ max_variants=max_variants,
233
+ )
234
+ )
235
+ logger.info(f"Generated {len(results)} attacks and saved to {output_dir}")
236
+
237
+ def run_red_teaming_attacks(self, attack_paths: str, output_dir: Optional[str] = None) -> None:
238
+ url, tenant_name, token = self._get_env_config()
239
+
240
+ if "WATSONX_SPACE_ID" in os.environ and "WATSONX_APIKEY" in os.environ:
241
+ provider = "watsonx"
242
+ elif "WO_INSTANCE" in os.environ and "WO_API_KEY" in os.environ:
243
+ provider = "model_proxy"
244
+ else:
245
+ logger.error(
246
+ "No provider found. Please either provide a config_file or set either WATSONX_SPACE_ID and WATSONX_APIKEY or WO_INSTANCE and WO_API_KEY in your system environment variables."
247
+ )
248
+ sys.exit(1)
249
+
250
+ config_data = {
251
+ "auth_config": AuthConfig(
252
+ url=url,
253
+ tenant_name=tenant_name,
254
+ token=token,
255
+ ),
256
+ "provider_config": ProviderConfig(
257
+ provider=provider,
258
+ model_id="meta-llama/llama-3-405b-instruct",
259
+ ),
260
+ }
261
+
262
+ config_data["attack_paths"] = attack_paths.split(",")
263
+ if output_dir:
264
+ config_data["output_dir"] = output_dir
265
+ else:
266
+ config_data["output_dir"] = os.path.join(os.getcwd(), "red_teaming_results")
267
+ os.makedirs(config_data["output_dir"], exist_ok=True)
268
+ logger.info(f"No output directory specified. Using default: {config_data['output_dir']}")
269
+
270
+
271
+ config = AttackConfig(**config_data)
272
+
273
+ run_attacks(config)
@@ -92,7 +92,7 @@ class ModelGatewayEnvConfig(BaseModel):
92
92
  inferred_auth_url = config.get("WO_INSTANCE") + '/icp4d-api/v1/authorize'
93
93
  else:
94
94
  logger.error(f"No 'AUTHORIZATION_URL' found. Auth type '{auth_type}' does not support defaulting. Please set the 'AUTHORIZATION_URL' explictly")
95
- sys.exit(1)
95
+ sys.exit(1)
96
96
  config["AUTHORIZATION_URL"] = inferred_auth_url
97
97
 
98
98
  if auth_type != WoAuthType.CPD:
@@ -32,7 +32,7 @@ from ibm_watsonx_orchestrate.cli.commands.connections.connections_controller imp
32
32
  from ibm_watsonx_orchestrate.agent_builder.connections.types import ConnectionType, ConnectionEnvironment, ConnectionPreference
33
33
  from ibm_watsonx_orchestrate.cli.config import Config, CONTEXT_SECTION_HEADER, CONTEXT_ACTIVE_ENV_OPT, \
34
34
  PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_TYPE_OPT, PYTHON_REGISTRY_TEST_PACKAGE_VERSION_OVERRIDE_OPT, \
35
- DEFAULT_CONFIG_FILE_CONTENT
35
+ DEFAULT_CONFIG_FILE_CONTENT, PYTHON_REGISTRY_SKIP_VERSION_CHECK_OPT
36
36
  from ibm_watsonx_orchestrate.agent_builder.connections import ConnectionSecurityScheme, ExpectedCredentials
37
37
  from ibm_watsonx_orchestrate.flow_builder.flows.decorators import FlowWrapper
38
38
  from ibm_watsonx_orchestrate.client.tools.tool_client import ToolClient
@@ -746,15 +746,18 @@ class ToolsController:
746
746
 
747
747
  cfg = Config()
748
748
  registry_type = cfg.read(PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_TYPE_OPT) or DEFAULT_CONFIG_FILE_CONTENT[PYTHON_REGISTRY_HEADER][PYTHON_REGISTRY_TYPE_OPT]
749
+ skip_version_check = cfg.read(PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_SKIP_VERSION_CHECK_OPT) or DEFAULT_CONFIG_FILE_CONTENT[PYTHON_REGISTRY_HEADER][PYTHON_REGISTRY_SKIP_VERSION_CHECK_OPT]
749
750
 
750
751
  version = __version__
751
752
  if registry_type == RegistryType.LOCAL:
753
+ logger.warning(f"Using a local registry which is for development purposes only")
752
754
  requirements.append(f"/packages/ibm_watsonx_orchestrate-0.6.0-py3-none-any.whl\n")
753
755
  elif registry_type == RegistryType.PYPI:
754
- wheel_file = get_whl_in_registry(registry_url='https://pypi.org/simple/ibm-watsonx-orchestrate', version=version)
755
- if not wheel_file:
756
- logger.error(f"Could not find ibm-watsonx-orchestrate@{version} on https://pypi.org/project/ibm-watsonx-orchestrate")
757
- exit(1)
756
+ if not skip_version_check:
757
+ wheel_file = get_whl_in_registry(registry_url='https://pypi.org/simple/ibm-watsonx-orchestrate', version=version)
758
+ if not wheel_file:
759
+ logger.error(f"Could not find ibm-watsonx-orchestrate@{version} on https://pypi.org/project/ibm-watsonx-orchestrate")
760
+ exit(1)
758
761
  requirements.append(f"ibm-watsonx-orchestrate=={version}\n")
759
762
  elif registry_type == RegistryType.TESTPYPI:
760
763
  override_version = cfg.get(PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_TEST_PACKAGE_VERSION_OVERRIDE_OPT) or version
@@ -22,6 +22,7 @@ AUTH_MCSP_TOKEN_OPT = "wxo_mcsp_token"
22
22
  AUTH_MCSP_TOKEN_EXPIRY_OPT = "wxo_mcsp_token_expiry"
23
23
  CONTEXT_ACTIVE_ENV_OPT = "active_environment"
24
24
  PYTHON_REGISTRY_TYPE_OPT = "type"
25
+ PYTHON_REGISTRY_SKIP_VERSION_CHECK_OPT = "skip_version_check"
25
26
  PYTHON_REGISTRY_TEST_PACKAGE_VERSION_OVERRIDE_OPT = "test_package_version_override"
26
27
  ENV_WXO_URL_OPT = "wxo_url"
27
28
  ENV_IAM_URL_OPT = "iam_url"
@@ -40,7 +41,8 @@ DEFAULT_CONFIG_FILE_CONTENT = {
40
41
  CONTEXT_SECTION_HEADER: {CONTEXT_ACTIVE_ENV_OPT: None},
41
42
  PYTHON_REGISTRY_HEADER: {
42
43
  PYTHON_REGISTRY_TYPE_OPT: str(RegistryType.PYPI),
43
- PYTHON_REGISTRY_TEST_PACKAGE_VERSION_OVERRIDE_OPT: None
44
+ PYTHON_REGISTRY_TEST_PACKAGE_VERSION_OVERRIDE_OPT: None,
45
+ PYTHON_REGISTRY_SKIP_VERSION_CHECK_OPT: False
44
46
  },
45
47
  ENVIRONMENTS_SECTION_HEADER: {
46
48
  PROTECTED_ENV_NAME: {
@@ -1,9 +1,22 @@
1
1
  import json
2
-
2
+ from ibm_watsonx_orchestrate.utils.exceptions import BadRequest
3
3
  import requests
4
4
  from abc import ABC, abstractmethod
5
5
  from ibm_cloud_sdk_core.authenticators import MCSPAuthenticator
6
6
  from typing_extensions import List
7
+ from contextlib import contextmanager
8
+
9
+ @contextmanager
10
+ def ssl_handler():
11
+ try:
12
+ yield
13
+ except requests.exceptions.SSLError as e:
14
+ error_message = str(e)
15
+ if "self-signed certificate in certificate chain" in error_message:
16
+ reason = "[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate in certificate chain"
17
+ else:
18
+ reason = error_message
19
+ raise BadRequest(f"SSL handshake failed for request '{e.request.path_url}'. Reason: '{reason}'")
7
20
 
8
21
 
9
22
  class ClientAPIException(requests.HTTPError):
@@ -50,7 +63,8 @@ class BaseAPIClient:
50
63
 
51
64
  def _get(self, path: str, params: dict = None, data=None, return_raw=False) -> dict:
52
65
  url = f"{self.base_url}{path}"
53
- response = requests.get(url, headers=self._get_headers(), params=params, data=data, verify=self.verify)
66
+ with ssl_handler():
67
+ response = requests.get(url, headers=self._get_headers(), params=params, data=data, verify=self.verify)
54
68
  self._check_response(response)
55
69
  if not return_raw:
56
70
  return response.json()
@@ -59,13 +73,15 @@ class BaseAPIClient:
59
73
 
60
74
  def _post(self, path: str, data: dict = None, files: dict = None) -> dict:
61
75
  url = f"{self.base_url}{path}"
62
- response = requests.post(url, headers=self._get_headers(), json=data, files=files, verify=self.verify)
76
+ with ssl_handler():
77
+ response = requests.post(url, headers=self._get_headers(), json=data, files=files, verify=self.verify)
63
78
  self._check_response(response)
64
79
  return response.json() if response.text else {}
65
80
 
66
81
  def _post_nd_json(self, path: str, data: dict = None, files: dict = None) -> List[dict]:
67
82
  url = f"{self.base_url}{path}"
68
- response = requests.post(url, headers=self._get_headers(), json=data, files=files)
83
+ with ssl_handler():
84
+ response = requests.post(url, headers=self._get_headers(), json=data, files=files)
69
85
  self._check_response(response)
70
86
 
71
87
  res = []
@@ -76,33 +92,38 @@ class BaseAPIClient:
76
92
 
77
93
  def _post_form_data(self, path: str, data: dict = None, files: dict = None) -> dict:
78
94
  url = f"{self.base_url}{path}"
79
- # Use data argument instead of json so data is encoded as application/x-www-form-urlencoded
80
- response = requests.post(url, headers=self._get_headers(), data=data, files=files, verify=self.verify)
95
+ with ssl_handler():
96
+ # Use data argument instead of json so data is encoded as application/x-www-form-urlencoded
97
+ response = requests.post(url, headers=self._get_headers(), data=data, files=files, verify=self.verify)
81
98
  self._check_response(response)
82
99
  return response.json() if response.text else {}
83
100
 
84
101
  def _put(self, path: str, data: dict = None) -> dict:
85
102
 
86
103
  url = f"{self.base_url}{path}"
87
- response = requests.put(url, headers=self._get_headers(), json=data, verify=self.verify)
104
+ with ssl_handler():
105
+ response = requests.put(url, headers=self._get_headers(), json=data, verify=self.verify)
88
106
  self._check_response(response)
89
107
  return response.json() if response.text else {}
90
108
 
91
109
  def _patch(self, path: str, data: dict = None) -> dict:
92
110
  url = f"{self.base_url}{path}"
93
- response = requests.patch(url, headers=self._get_headers(), json=data, verify=self.verify)
111
+ with ssl_handler():
112
+ response = requests.patch(url, headers=self._get_headers(), json=data, verify=self.verify)
94
113
  self._check_response(response)
95
114
  return response.json() if response.text else {}
96
115
 
97
116
  def _patch_form_data(self, path: str, data: dict = None, files = None) -> dict:
98
117
  url = f"{self.base_url}{path}"
99
- response = requests.patch(url, headers=self._get_headers(), data=data, files=files, verify=self.verify)
118
+ with ssl_handler():
119
+ response = requests.patch(url, headers=self._get_headers(), data=data, files=files, verify=self.verify)
100
120
  self._check_response(response)
101
121
  return response.json() if response.text else {}
102
122
 
103
123
  def _delete(self, path: str, data=None) -> dict:
104
124
  url = f"{self.base_url}{path}"
105
- response = requests.delete(url, headers=self._get_headers(), json=data, verify=self.verify)
125
+ with ssl_handler():
126
+ response = requests.delete(url, headers=self._get_headers(), json=data, verify=self.verify)
106
127
  self._check_response(response)
107
128
  return response.json() if response.text else {}
108
129
 
@@ -177,3 +177,17 @@ class ConnectionsClient(BaseAPIClient):
177
177
  logger.warning(f"Connections not found. Returning connection ID: {conn_id}")
178
178
  return conn_id
179
179
  raise e
180
+
181
+ def get_drafts_by_ids(self, conn_ids) -> List[ListConfigsResponse]:
182
+ try:
183
+ res = self._get(f"/connections/applications?connectionIds={','.join(conn_ids)}")
184
+ import json
185
+ json.dumps(res)
186
+ return [ListConfigsResponse.model_validate(conn) for conn in res.get("applications", [])]
187
+ except ValidationError as e:
188
+ logger.error("Recieved unexpected response from server")
189
+ raise e
190
+ except ClientAPIException as e:
191
+ if e.response.status_code == 404:
192
+ return []
193
+ raise e
@@ -10,7 +10,7 @@ from ibm_cloud_sdk_core.authenticators import MCSPV2Authenticator
10
10
  from ibm_cloud_sdk_core.authenticators import IAMAuthenticator
11
11
  from ibm_cloud_sdk_core.authenticators import CloudPakForDataAuthenticator
12
12
 
13
- from ibm_watsonx_orchestrate.client.utils import check_token_validity, is_cpd_env
13
+ from ibm_watsonx_orchestrate.client.utils import check_token_validity, is_cpd_env, is_ibm_cloud_platform
14
14
  from ibm_watsonx_orchestrate.client.base_service_instance import BaseServiceInstance
15
15
  from ibm_watsonx_orchestrate.cli.commands.environment.types import EnvironmentAuthType
16
16
 
@@ -21,14 +21,6 @@ from ibm_watsonx_orchestrate.client.client_errors import (
21
21
  import logging
22
22
  logger = logging.getLogger(__name__)
23
23
 
24
- from ibm_watsonx_orchestrate.cli.config import (
25
- Config,
26
- CONTEXT_SECTION_HEADER,
27
- CONTEXT_ACTIVE_ENV_OPT,
28
- ENVIRONMENTS_SECTION_HEADER,
29
- ENV_WXO_URL_OPT
30
- )
31
-
32
24
  class ServiceInstance(BaseServiceInstance):
33
25
  """Connect, get details, and check usage of a Watson Machine Learning service instance."""
34
26
 
@@ -50,29 +42,28 @@ class ServiceInstance(BaseServiceInstance):
50
42
  return self._client.token
51
43
 
52
44
  def _create_token(self) -> str:
53
- if not self._credentials.auth_type:
54
- if ".cloud.ibm.com" in self._credentials.url:
55
- logger.warning("Using IBM IAM Auth Type. If this is incorrect please use the '--type' flag to explicitly choose one of 'mcsp', 'mcsp_v1', 'mcsp_v2' or 'cpd' ")
56
- return self._authenticate(EnvironmentAuthType.IBM_CLOUD_IAM)
57
- elif is_cpd_env(self._credentials.url):
58
- logger.warning("Using CPD Auth Type. If this is incorrect please use the '--type' flag to explicitly choose one of 'ibm_iam', 'mcsp', 'mcsp_v1' or 'mcsp_v2' ")
59
- return self._authenticate(EnvironmentAuthType.CPD)
60
- else:
61
- logger.warning("Using MCSP Auth Type. If this is incorrect please use the '--type' flag to explicitly choose one of 'ibm_iam', 'mcsp_v1', 'mcsp_v2' or 'cpd' ")
62
- try:
63
- return self._authenticate(EnvironmentAuthType.MCSP_V1)
64
- except:
65
- return self._authenticate(EnvironmentAuthType.MCSP_V2)
66
- auth_type = self._credentials.auth_type.lower()
45
+ inferred_auth_type = None
46
+ if is_ibm_cloud_platform(self._credentials.url):
47
+ inferred_auth_type = EnvironmentAuthType.IBM_CLOUD_IAM
48
+ elif is_cpd_env(self._credentials.url):
49
+ inferred_auth_type = EnvironmentAuthType.CPD
50
+ else:
51
+ inferred_auth_type = EnvironmentAuthType.MCSP
52
+
53
+ if self._credentials.auth_type:
54
+ if self._credentials.auth_type != inferred_auth_type:
55
+ logger.warning(f"Overriding the default authentication type '{inferred_auth_type}' for url '{self._credentials.url}' with '{self._credentials.auth_type.lower()}'")
56
+ auth_type = self._credentials.auth_type.lower()
57
+ else:
58
+ inferred_type_options = [t for t in EnvironmentAuthType if t != inferred_auth_type]
59
+ logger.warning(f"Using '{inferred_auth_type}' Auth Type. If this is incorrect please use the '--type' flag to explicitly choose one of {', '.join(inferred_type_options[:-1])} or {inferred_type_options[-1]}")
60
+ auth_type = inferred_auth_type
61
+
67
62
  if auth_type == "mcsp":
68
63
  try:
69
64
  return self._authenticate(EnvironmentAuthType.MCSP_V1)
70
65
  except:
71
66
  return self._authenticate(EnvironmentAuthType.MCSP_V2)
72
- elif auth_type == "mcsp_v1":
73
- return self._authenticate(EnvironmentAuthType.MCSP_V1)
74
- elif auth_type == "mcsp_v2":
75
- return self._authenticate(EnvironmentAuthType.MCSP_V2)
76
67
  else:
77
68
  return self._authenticate(auth_type)
78
69
 
@@ -100,13 +91,7 @@ class ServiceInstance(BaseServiceInstance):
100
91
  if self._credentials.iam_url is not None:
101
92
  url = self._credentials.iam_url
102
93
  else:
103
- cfg = Config()
104
- env_cfg = cfg.get(ENVIRONMENTS_SECTION_HEADER)
105
- matching_wxo_url = next(
106
- (env_config['wxo_url'] for env_config in env_cfg.values() if 'bypass_ssl' in env_config and 'verify' in env_config),
107
- None
108
- )
109
- base_url = matching_wxo_url.split("/orchestrate")[0]
94
+ base_url = self._credentials.url.split("/orchestrate")[0]
110
95
  url = f"{base_url}/icp4d-api"
111
96
 
112
97
  password = self._credentials.password if self._credentials.password is not None else None