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.
- ibm_watsonx_orchestrate/__init__.py +2 -1
- ibm_watsonx_orchestrate/agent_builder/agents/types.py +13 -0
- ibm_watsonx_orchestrate/agent_builder/connections/connections.py +5 -2
- ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +25 -10
- ibm_watsonx_orchestrate/cli/commands/agents/agents_command.py +10 -2
- ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +404 -173
- ibm_watsonx_orchestrate/cli/commands/copilot/copilot_controller.py +6 -2
- ibm_watsonx_orchestrate/cli/commands/environment/environment_command.py +6 -2
- ibm_watsonx_orchestrate/cli/commands/environment/environment_controller.py +6 -3
- ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_command.py +174 -2
- ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_controller.py +93 -9
- ibm_watsonx_orchestrate/cli/commands/server/types.py +1 -1
- ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +8 -5
- ibm_watsonx_orchestrate/cli/config.py +3 -1
- ibm_watsonx_orchestrate/client/base_api_client.py +31 -10
- ibm_watsonx_orchestrate/client/connections/connections_client.py +14 -0
- ibm_watsonx_orchestrate/client/service_instance.py +19 -34
- ibm_watsonx_orchestrate/client/utils.py +3 -1
- ibm_watsonx_orchestrate/docker/compose-lite.yml +12 -11
- ibm_watsonx_orchestrate/docker/default.env +13 -13
- ibm_watsonx_orchestrate/flow_builder/flows/flow.py +3 -1
- ibm_watsonx_orchestrate/flow_builder/types.py +252 -1
- {ibm_watsonx_orchestrate-1.11.0b0.dist-info → ibm_watsonx_orchestrate-1.11.1.dist-info}/METADATA +2 -2
- {ibm_watsonx_orchestrate-1.11.0b0.dist-info → ibm_watsonx_orchestrate-1.11.1.dist-info}/RECORD +27 -27
- {ibm_watsonx_orchestrate-1.11.0b0.dist-info → ibm_watsonx_orchestrate-1.11.1.dist-info}/WHEEL +0 -0
- {ibm_watsonx_orchestrate-1.11.0b0.dist-info → ibm_watsonx_orchestrate-1.11.1.dist-info}/entry_points.txt +0 -0
- {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,
|
22
|
-
|
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(
|
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
|
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
|
-
|
94
|
-
|
95
|
-
|
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(
|
165
|
-
|
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
|
-
|
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
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
80
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
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
|
-
|
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
|