ibm-watsonx-orchestrate 1.5.0b1__py3-none-any.whl → 1.6.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.
- ibm_watsonx_orchestrate/__init__.py +1 -1
- ibm_watsonx_orchestrate/agent_builder/agents/__init__.py +1 -1
- ibm_watsonx_orchestrate/agent_builder/agents/types.py +53 -3
- ibm_watsonx_orchestrate/agent_builder/model_policies/types.py +1 -1
- ibm_watsonx_orchestrate/agent_builder/models/types.py +0 -1
- ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +41 -3
- ibm_watsonx_orchestrate/agent_builder/tools/python_tool.py +2 -1
- ibm_watsonx_orchestrate/agent_builder/tools/types.py +7 -0
- ibm_watsonx_orchestrate/cli/commands/agents/agents_command.py +18 -1
- ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +97 -3
- ibm_watsonx_orchestrate/cli/commands/channels/webchat/channels_webchat_controller.py +0 -1
- ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +1 -1
- ibm_watsonx_orchestrate/cli/commands/environment/environment_command.py +29 -4
- ibm_watsonx_orchestrate/cli/commands/environment/environment_controller.py +74 -8
- ibm_watsonx_orchestrate/cli/commands/environment/types.py +1 -0
- ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_command.py +224 -0
- ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_controller.py +158 -0
- ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_command.py +2 -2
- ibm_watsonx_orchestrate/cli/commands/models/model_provider_mapper.py +31 -25
- ibm_watsonx_orchestrate/cli/commands/models/models_command.py +6 -6
- ibm_watsonx_orchestrate/cli/commands/models/models_controller.py +17 -8
- ibm_watsonx_orchestrate/cli/commands/server/server_command.py +25 -17
- ibm_watsonx_orchestrate/cli/commands/server/types.py +2 -1
- ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_controller.py +0 -3
- ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +14 -12
- ibm_watsonx_orchestrate/cli/config.py +2 -0
- ibm_watsonx_orchestrate/cli/main.py +6 -0
- ibm_watsonx_orchestrate/client/agents/agent_client.py +14 -8
- ibm_watsonx_orchestrate/client/agents/assistant_agent_client.py +3 -3
- ibm_watsonx_orchestrate/client/agents/external_agent_client.py +2 -2
- ibm_watsonx_orchestrate/client/base_api_client.py +9 -9
- ibm_watsonx_orchestrate/client/connections/connections_client.py +32 -6
- ibm_watsonx_orchestrate/client/connections/utils.py +1 -1
- ibm_watsonx_orchestrate/client/credentials.py +4 -0
- ibm_watsonx_orchestrate/client/model_policies/model_policies_client.py +2 -2
- ibm_watsonx_orchestrate/client/service_instance.py +42 -1
- ibm_watsonx_orchestrate/client/utils.py +27 -2
- ibm_watsonx_orchestrate/docker/compose-lite.yml +27 -17
- ibm_watsonx_orchestrate/docker/default.env +21 -15
- ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/flows/__init__.py +3 -2
- ibm_watsonx_orchestrate/flow_builder/flows/decorators.py +77 -0
- ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/flows/events.py +6 -1
- ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/flows/flow.py +70 -87
- ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/types.py +15 -6
- ibm_watsonx_orchestrate/flow_builder/utils.py +185 -0
- {ibm_watsonx_orchestrate-1.5.0b1.dist-info → ibm_watsonx_orchestrate-1.6.0b0.dist-info}/METADATA +2 -1
- {ibm_watsonx_orchestrate-1.5.0b1.dist-info → ibm_watsonx_orchestrate-1.6.0b0.dist-info}/RECORD +55 -53
- ibm_watsonx_orchestrate/experimental/flow_builder/flows/decorators.py +0 -144
- ibm_watsonx_orchestrate/experimental/flow_builder/utils.py +0 -115
- /ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/__init__.py +0 -0
- /ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/data_map.py +0 -0
- /ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/flows/constants.py +0 -0
- /ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/node.py +0 -0
- /ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/resources/flow_status.openapi.yml +0 -0
- {ibm_watsonx_orchestrate-1.5.0b1.dist-info → ibm_watsonx_orchestrate-1.6.0b0.dist-info}/WHEEL +0 -0
- {ibm_watsonx_orchestrate-1.5.0b1.dist-info → ibm_watsonx_orchestrate-1.6.0b0.dist-info}/entry_points.txt +0 -0
- {ibm_watsonx_orchestrate-1.5.0b1.dist-info → ibm_watsonx_orchestrate-1.6.0b0.dist-info}/licenses/LICENSE +0 -0
@@ -2,6 +2,7 @@ import logging
|
|
2
2
|
import rich
|
3
3
|
import jwt
|
4
4
|
import getpass
|
5
|
+
import sys
|
5
6
|
|
6
7
|
from ibm_watsonx_orchestrate.cli.commands.tools.types import RegistryType
|
7
8
|
from ibm_watsonx_orchestrate.cli.config import (
|
@@ -17,14 +18,15 @@ from ibm_watsonx_orchestrate.cli.config import (
|
|
17
18
|
ENV_IAM_URL_OPT,
|
18
19
|
ENVIRONMENTS_SECTION_HEADER,
|
19
20
|
PROTECTED_ENV_NAME,
|
20
|
-
ENV_AUTH_TYPE, PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_TYPE_OPT, PYTHON_REGISTRY_TEST_PACKAGE_VERSION_OVERRIDE_OPT,
|
21
|
+
ENV_AUTH_TYPE, PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_TYPE_OPT, PYTHON_REGISTRY_TEST_PACKAGE_VERSION_OVERRIDE_OPT, BYPASS_SSL, VERIFY,
|
21
22
|
DEFAULT_CONFIG_FILE_CONTENT
|
22
23
|
)
|
23
24
|
from ibm_watsonx_orchestrate.client.client import Client
|
24
25
|
from ibm_watsonx_orchestrate.client.client_errors import ClientError
|
26
|
+
from ibm_watsonx_orchestrate.client.agents.agent_client import AgentClient, ClientAPIException
|
25
27
|
from ibm_watsonx_orchestrate.client.credentials import Credentials
|
26
28
|
from threading import Lock
|
27
|
-
from ibm_watsonx_orchestrate.client.utils import is_local_dev, check_token_validity
|
29
|
+
from ibm_watsonx_orchestrate.client.utils import is_local_dev, check_token_validity, is_cpd_env
|
28
30
|
from ibm_watsonx_orchestrate.cli.commands.environment.types import EnvironmentAuthType
|
29
31
|
|
30
32
|
logger = logging.getLogger(__name__)
|
@@ -42,7 +44,33 @@ def _decode_token(token: str, is_local: bool = False) -> dict:
|
|
42
44
|
logger.error("Invalid token format")
|
43
45
|
raise e
|
44
46
|
|
45
|
-
|
47
|
+
|
48
|
+
def _validate_token_functionality(token: str, url: str) -> None:
|
49
|
+
'''
|
50
|
+
Validates a token by making a request to GET /agents
|
51
|
+
|
52
|
+
Args:
|
53
|
+
token: A JWT token
|
54
|
+
url: WXO instance URL
|
55
|
+
'''
|
56
|
+
is_cpd = is_cpd_env(url)
|
57
|
+
if is_cpd is True:
|
58
|
+
agent_client = AgentClient(base_url=url, api_key=token, is_local=is_local_dev(url), verify=False)
|
59
|
+
else:
|
60
|
+
agent_client = AgentClient(base_url=url, api_key=token, is_local=is_local_dev(url))
|
61
|
+
agent_client.api_key = token
|
62
|
+
|
63
|
+
try:
|
64
|
+
agent_client.get()
|
65
|
+
except ClientAPIException as e:
|
66
|
+
if e.response.status_code >= 400:
|
67
|
+
reason = e.response.reason
|
68
|
+
logger.error(f"Failed to authenticate to provided instance '{url}'. Reason: '{reason}'. Please ensure provider URL and API key are valid.")
|
69
|
+
sys.exit(1)
|
70
|
+
raise e
|
71
|
+
|
72
|
+
|
73
|
+
def _login(name: str, apikey: str = None, username: str = None, password: str = None) -> None:
|
46
74
|
cfg = Config()
|
47
75
|
auth_cfg = Config(AUTH_CONFIG_FILE_FOLDER, AUTH_CONFIG_FILE)
|
48
76
|
|
@@ -55,14 +83,43 @@ def _login(name: str, apikey: str = None) -> None:
|
|
55
83
|
except (KeyError, AttributeError):
|
56
84
|
auth_type = None
|
57
85
|
|
86
|
+
username = username
|
87
|
+
apikey = apikey
|
88
|
+
password = password
|
89
|
+
|
90
|
+
if is_cpd_env(url):
|
91
|
+
if username is None:
|
92
|
+
username = getpass.getpass("Please enter CPD Username: ")
|
93
|
+
|
94
|
+
if not apikey and not password:
|
95
|
+
apikey = getpass.getpass("Enter CPD API key (or leave blank to use password): ")
|
96
|
+
if not apikey and not password:
|
97
|
+
password = getpass.getpass("Enter CPD password (or leave blank if you used API key): ")
|
98
|
+
|
99
|
+
if apikey and password:
|
100
|
+
logger.error("For CPD, please use either an Apikey or a Password but not both.")
|
101
|
+
sys.exit(1)
|
102
|
+
|
103
|
+
if not apikey and not password:
|
104
|
+
logger.error("For CPD, you must provide either an API key or a password.")
|
105
|
+
sys.exit(1)
|
106
|
+
|
58
107
|
|
59
|
-
if apikey
|
108
|
+
if not apikey and not password and not is_local and auth_type != "cpd":
|
60
109
|
apikey = getpass.getpass("Please enter WXO API key: ")
|
61
110
|
|
62
111
|
try:
|
63
|
-
creds = Credentials(
|
112
|
+
creds = Credentials(
|
113
|
+
url=url,
|
114
|
+
api_key=apikey,
|
115
|
+
username=username,
|
116
|
+
password=password,
|
117
|
+
iam_url=iam_url,
|
118
|
+
auth_type=auth_type
|
119
|
+
)
|
64
120
|
client = Client(creds)
|
65
121
|
token = _decode_token(client.token, is_local)
|
122
|
+
_validate_token_functionality(token=token.get(AUTH_MCSP_TOKEN_OPT), url=url)
|
66
123
|
with lock:
|
67
124
|
auth_cfg.save(
|
68
125
|
{
|
@@ -74,7 +131,7 @@ def _login(name: str, apikey: str = None) -> None:
|
|
74
131
|
except ClientError as e:
|
75
132
|
raise ClientError(e)
|
76
133
|
|
77
|
-
def activate(name: str, apikey: str=None, registry: RegistryType=None, test_package_version_override=None) -> None:
|
134
|
+
def activate(name: str, apikey: str=None, username: str=None, password: str=None, registry: RegistryType=None, test_package_version_override=None) -> None:
|
78
135
|
cfg = Config()
|
79
136
|
auth_cfg = Config(AUTH_CONFIG_FILE_FOLDER, AUTH_CONFIG_FILE)
|
80
137
|
env_cfg = cfg.read(ENVIRONMENTS_SECTION_HEADER, name)
|
@@ -92,7 +149,7 @@ def activate(name: str, apikey: str=None, registry: RegistryType=None, test_pack
|
|
92
149
|
existing_token = existing_auth_config.get(AUTH_MCSP_TOKEN_OPT) if existing_auth_config else None
|
93
150
|
|
94
151
|
if not check_token_validity(existing_token) or is_local:
|
95
|
-
_login(name=name, apikey=apikey)
|
152
|
+
_login(name=name, apikey=apikey, username=username, password=password)
|
96
153
|
|
97
154
|
with lock:
|
98
155
|
cfg.write(CONTEXT_SECTION_HEADER, CONTEXT_ACTIVE_ENV_OPT, name)
|
@@ -104,8 +161,11 @@ def activate(name: str, apikey: str=None, registry: RegistryType=None, test_pack
|
|
104
161
|
cfg.write(PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_TEST_PACKAGE_VERSION_OVERRIDE_OPT, test_package_version_override)
|
105
162
|
|
106
163
|
logger.info(f"Environment '{name}' is now active")
|
164
|
+
is_cpd = is_cpd_env(url)
|
165
|
+
if is_cpd:
|
166
|
+
logger.warning("Support for CPD clusters is currently an early access preview")
|
107
167
|
|
108
|
-
def add(name: str, url: str, should_activate: bool=False, iam_url: str=None, type: EnvironmentAuthType=None) -> None:
|
168
|
+
def add(name: str, url: str, should_activate: bool=False, iam_url: str=None, type: EnvironmentAuthType=None, insecure: bool=None, verify: str=None) -> None:
|
109
169
|
if name == PROTECTED_ENV_NAME:
|
110
170
|
logger.error(f"The name '{PROTECTED_ENV_NAME}' is a reserved environment name. Please select a diffrent name or use `orchestrate env activate {PROTECTED_ENV_NAME}` to swap to '{PROTECTED_ENV_NAME}'")
|
111
171
|
return
|
@@ -124,6 +184,12 @@ def add(name: str, url: str, should_activate: bool=False, iam_url: str=None, typ
|
|
124
184
|
cfg.write(ENVIRONMENTS_SECTION_HEADER, name, {ENV_IAM_URL_OPT: iam_url})
|
125
185
|
if type:
|
126
186
|
cfg.write(ENVIRONMENTS_SECTION_HEADER, name, {ENV_AUTH_TYPE: str(type)})
|
187
|
+
if insecure:
|
188
|
+
cfg.write(ENVIRONMENTS_SECTION_HEADER, name, {BYPASS_SSL: insecure})
|
189
|
+
cfg.write(ENVIRONMENTS_SECTION_HEADER, name, {VERIFY: 'None'})
|
190
|
+
if verify:
|
191
|
+
cfg.write(ENVIRONMENTS_SECTION_HEADER, name, {VERIFY: verify})
|
192
|
+
cfg.write(ENVIRONMENTS_SECTION_HEADER, name, {BYPASS_SSL: False})
|
127
193
|
|
128
194
|
|
129
195
|
logger.info(f"Environment '{name}' has been created")
|
@@ -0,0 +1,224 @@
|
|
1
|
+
import json
|
2
|
+
import logging
|
3
|
+
import typer
|
4
|
+
import os
|
5
|
+
import yaml
|
6
|
+
import csv
|
7
|
+
import rich
|
8
|
+
from pathlib import Path
|
9
|
+
import sys
|
10
|
+
from dotenv import dotenv_values
|
11
|
+
|
12
|
+
from typing import Optional
|
13
|
+
from typing_extensions import Annotated
|
14
|
+
|
15
|
+
from ibm_watsonx_orchestrate import __version__
|
16
|
+
from ibm_watsonx_orchestrate.cli.commands.evaluations.evaluations_controller import EvaluationsController
|
17
|
+
|
18
|
+
logger = logging.getLogger(__name__)
|
19
|
+
|
20
|
+
evaluation_app = typer.Typer(no_args_is_help=True)
|
21
|
+
|
22
|
+
def read_env_file(env_path: Path|str) -> dict:
|
23
|
+
return dotenv_values(str(env_path))
|
24
|
+
|
25
|
+
def validate_watsonx_credentials(user_env_file: str) -> bool:
|
26
|
+
required_keys = ["WATSONX_SPACE_ID", "WATSONX_APIKEY"]
|
27
|
+
|
28
|
+
if all(key in os.environ for key in required_keys):
|
29
|
+
logger.info("WatsonX credentials validated successfully.")
|
30
|
+
return
|
31
|
+
|
32
|
+
if user_env_file is None:
|
33
|
+
logger.error("WatsonX credentials are not set. Please set WATSONX_SPACE_ID and WATSONX_APIKEY in your system environment variables or include them in your enviroment file and pass it with --env-file option.")
|
34
|
+
sys.exit(1)
|
35
|
+
|
36
|
+
if not Path(user_env_file).exists():
|
37
|
+
logger.error(f"Error: The specified environment file '{user_env_file}' does not exist.")
|
38
|
+
sys.exit(1)
|
39
|
+
|
40
|
+
user_env = read_env_file(user_env_file)
|
41
|
+
|
42
|
+
if not all(key in user_env for key in required_keys):
|
43
|
+
logger.error("Error: The environment file does not contain the required keys: WATSONX_SPACE_ID and WATSONX_APIKEY.")
|
44
|
+
sys.exit(1)
|
45
|
+
|
46
|
+
os.environ.update({key: user_env[key] for key in required_keys})
|
47
|
+
logger.info("WatsonX credentials validated successfully.")
|
48
|
+
|
49
|
+
|
50
|
+
@evaluation_app.command(name="evaluate", help="Evaluate an agent against a set of test cases")
|
51
|
+
def evaluate(
|
52
|
+
config_file: Annotated[
|
53
|
+
Optional[str],
|
54
|
+
typer.Option(
|
55
|
+
"--config", "-c",
|
56
|
+
help="Path to YAML configuration file containing evaluation settings."
|
57
|
+
)
|
58
|
+
] = None,
|
59
|
+
test_paths: Annotated[
|
60
|
+
Optional[str],
|
61
|
+
typer.Option(
|
62
|
+
"--test-paths", "-p",
|
63
|
+
help="Paths to the test files and/or directories to evaluate, separated by commas."
|
64
|
+
),
|
65
|
+
] = None,
|
66
|
+
output_dir: Annotated[
|
67
|
+
Optional[str],
|
68
|
+
typer.Option(
|
69
|
+
"--output-dir", "-o",
|
70
|
+
help="Directory to save the evaluation results."
|
71
|
+
)
|
72
|
+
] = None,
|
73
|
+
user_env_file: Annotated[
|
74
|
+
Optional[str],
|
75
|
+
typer.Option(
|
76
|
+
"--env-file", "-e",
|
77
|
+
help="Path to a .env file that overrides default.env. Then environment variables override both."
|
78
|
+
),
|
79
|
+
] = None
|
80
|
+
):
|
81
|
+
if not config_file:
|
82
|
+
if not test_paths or not output_dir:
|
83
|
+
logger.error("Error: Both --test-paths and --output-dir must be provided when not using a config file")
|
84
|
+
exit(1)
|
85
|
+
|
86
|
+
validate_watsonx_credentials(user_env_file)
|
87
|
+
controller = EvaluationsController()
|
88
|
+
controller.evaluate(config_file=config_file, test_paths=test_paths, output_dir=output_dir)
|
89
|
+
|
90
|
+
|
91
|
+
@evaluation_app.command(name="record", help="Record chat sessions and create test cases")
|
92
|
+
def record(
|
93
|
+
output_dir: Annotated[
|
94
|
+
Optional[str],
|
95
|
+
typer.Option(
|
96
|
+
"--output-dir", "-o",
|
97
|
+
help="Directory to save the recorded chats."
|
98
|
+
)
|
99
|
+
] = None,
|
100
|
+
user_env_file: Annotated[
|
101
|
+
Optional[str],
|
102
|
+
typer.Option(
|
103
|
+
"--env-file", "-e",
|
104
|
+
help="Path to a .env file that overrides default.env. Then environment variables override both."
|
105
|
+
),
|
106
|
+
] = None
|
107
|
+
):
|
108
|
+
validate_watsonx_credentials(user_env_file)
|
109
|
+
controller = EvaluationsController()
|
110
|
+
controller.record(output_dir=output_dir)
|
111
|
+
|
112
|
+
|
113
|
+
@evaluation_app.command(name="generate", help="Generate test cases from user stories and tools")
|
114
|
+
def generate(
|
115
|
+
stories_path: Annotated[
|
116
|
+
str,
|
117
|
+
typer.Option(
|
118
|
+
"--stories_path", "-s",
|
119
|
+
help="Path to the CSV file containing user stories for test case generation. "
|
120
|
+
"The file has 'story' and 'agent' columns."
|
121
|
+
)
|
122
|
+
],
|
123
|
+
tools_path: Annotated[
|
124
|
+
str,
|
125
|
+
typer.Option(
|
126
|
+
"--tools_path", "-t",
|
127
|
+
help="Path to the directory containing tool definitions."
|
128
|
+
)
|
129
|
+
],
|
130
|
+
output_dir: Annotated[
|
131
|
+
Optional[str],
|
132
|
+
typer.Option(
|
133
|
+
"--output_dir", "-o",
|
134
|
+
help="Directory to save the generated test cases."
|
135
|
+
)
|
136
|
+
] = None,
|
137
|
+
user_env_file: Annotated[
|
138
|
+
Optional[str],
|
139
|
+
typer.Option(
|
140
|
+
"--env-file", "-e",
|
141
|
+
help="Path to a .env file that overrides default.env. Then environment variables override both."
|
142
|
+
),
|
143
|
+
] = None
|
144
|
+
):
|
145
|
+
validate_watsonx_credentials(user_env_file)
|
146
|
+
controller = EvaluationsController()
|
147
|
+
controller.generate(stories_path=stories_path, tools_path=tools_path, output_dir=output_dir)
|
148
|
+
|
149
|
+
|
150
|
+
@evaluation_app.command(name="analyze", help="Analyze the results of an evaluation run")
|
151
|
+
def analyze(data_path: Annotated[
|
152
|
+
str,
|
153
|
+
typer.Option(
|
154
|
+
"--data_path", "-d",
|
155
|
+
help="Path to the directory that has the saved results"
|
156
|
+
)
|
157
|
+
],
|
158
|
+
user_env_file: Annotated[
|
159
|
+
Optional[str],
|
160
|
+
typer.Option(
|
161
|
+
"--env-file", "-e",
|
162
|
+
help="Path to a .env file that overrides default.env. Then environment variables override both."
|
163
|
+
),
|
164
|
+
] = None):
|
165
|
+
|
166
|
+
validate_watsonx_credentials(user_env_file)
|
167
|
+
controller = EvaluationsController()
|
168
|
+
controller.analyze(data_path=data_path)
|
169
|
+
|
170
|
+
|
171
|
+
@evaluation_app.command(name="validate_external", help="Validate an external agent against a set of inputs")
|
172
|
+
def validate_external(
|
173
|
+
data_path: Annotated[
|
174
|
+
str,
|
175
|
+
typer.Option(
|
176
|
+
"--csv", "-c",
|
177
|
+
help="Path to .csv file of inputs"
|
178
|
+
)
|
179
|
+
],
|
180
|
+
config: Annotated[
|
181
|
+
str,
|
182
|
+
typer.Option(
|
183
|
+
"--config", "-cf",
|
184
|
+
help="Path to the external agent yaml"
|
185
|
+
)
|
186
|
+
],
|
187
|
+
credential: Annotated[
|
188
|
+
str,
|
189
|
+
typer.Option(
|
190
|
+
"--credential", "-crd",
|
191
|
+
help="credential string"
|
192
|
+
)
|
193
|
+
],
|
194
|
+
output_dir: Annotated[
|
195
|
+
str,
|
196
|
+
typer.Option(
|
197
|
+
"--output", "-o",
|
198
|
+
help="where to save the validation results"
|
199
|
+
)
|
200
|
+
] = "./test_external_agent",
|
201
|
+
user_env_file: Annotated[
|
202
|
+
Optional[str],
|
203
|
+
typer.Option(
|
204
|
+
"--env-file", "-e",
|
205
|
+
help="Path to a .env file that overrides default.env. Then environment variables override both."
|
206
|
+
),
|
207
|
+
] = None
|
208
|
+
):
|
209
|
+
|
210
|
+
validate_watsonx_credentials(user_env_file)
|
211
|
+
with open(config, "r") as f:
|
212
|
+
config = yaml.safe_load(f)
|
213
|
+
controller = EvaluationsController()
|
214
|
+
test_data = []
|
215
|
+
with open(data_path, "r") as f:
|
216
|
+
csv_reader = csv.reader(f)
|
217
|
+
for line in csv_reader:
|
218
|
+
test_data.append(line[0])
|
219
|
+
results = controller.external_validate(config, test_data, credential)
|
220
|
+
os.makedirs(output_dir, exist_ok=True)
|
221
|
+
with open(os.path.join(output_dir, "validation_results.json"), "w") as f:
|
222
|
+
json.dump(results, f)
|
223
|
+
|
224
|
+
rich.print(f"[green] validation result is saved to {output_dir} [/green]")
|
@@ -0,0 +1,158 @@
|
|
1
|
+
import logging
|
2
|
+
from typing import List, Dict, Optional
|
3
|
+
import csv
|
4
|
+
from pathlib import Path
|
5
|
+
import rich
|
6
|
+
from wxo_agentic_evaluation import main as evaluate
|
7
|
+
from wxo_agentic_evaluation.tool_planner import build_snapshot
|
8
|
+
from wxo_agentic_evaluation.analyze_run import analyze
|
9
|
+
from wxo_agentic_evaluation.batch_annotate import generate_test_cases_from_stories
|
10
|
+
from wxo_agentic_evaluation.arg_configs import TestConfig, AuthConfig, LLMUserConfig, ChatRecordingConfig, AnalyzeConfig
|
11
|
+
from wxo_agentic_evaluation.record_chat import record_chats
|
12
|
+
from wxo_agentic_evaluation.external_agent.external_validate import ExternalAgentValidation
|
13
|
+
from ibm_watsonx_orchestrate import __version__
|
14
|
+
from ibm_watsonx_orchestrate.cli.config import Config, ENV_WXO_URL_OPT, AUTH_CONFIG_FILE, AUTH_CONFIG_FILE_FOLDER, AUTH_SECTION_HEADER, AUTH_MCSP_TOKEN_OPT
|
15
|
+
from ibm_watsonx_orchestrate.utils.utils import yaml_safe_load
|
16
|
+
from ibm_watsonx_orchestrate.cli.commands.agents.agents_controller import AgentsController
|
17
|
+
from ibm_watsonx_orchestrate.agent_builder.agents import AgentKind
|
18
|
+
|
19
|
+
|
20
|
+
logger = logging.getLogger(__name__)
|
21
|
+
|
22
|
+
|
23
|
+
class EvaluationsController:
|
24
|
+
def __init__(self):
|
25
|
+
pass
|
26
|
+
|
27
|
+
def _get_env_config(self) -> tuple[str, str, str | None]:
|
28
|
+
cfg = Config()
|
29
|
+
auth_cfg = Config(AUTH_CONFIG_FILE_FOLDER, AUTH_CONFIG_FILE)
|
30
|
+
|
31
|
+
url = cfg.get_active_env_config(ENV_WXO_URL_OPT)
|
32
|
+
tenant_name = cfg.get_active_env()
|
33
|
+
|
34
|
+
existing_auth_config = auth_cfg.get(AUTH_SECTION_HEADER).get(tenant_name, {})
|
35
|
+
token = existing_auth_config.get(AUTH_MCSP_TOKEN_OPT) if existing_auth_config else None
|
36
|
+
|
37
|
+
return url, tenant_name, token
|
38
|
+
|
39
|
+
def evaluate(self, config_file: Optional[str] = None, test_paths: Optional[str] = None, output_dir: Optional[str] = None) -> None:
|
40
|
+
url, tenant_name, token = self._get_env_config()
|
41
|
+
|
42
|
+
config_data = {
|
43
|
+
"wxo_lite_version": __version__,
|
44
|
+
"auth_config": AuthConfig(
|
45
|
+
url=url,
|
46
|
+
tenant_name=tenant_name,
|
47
|
+
token=token
|
48
|
+
)
|
49
|
+
}
|
50
|
+
|
51
|
+
if config_file:
|
52
|
+
logger.info(f"Loading configuration from {config_file}")
|
53
|
+
with open(config_file, 'r') as f:
|
54
|
+
file_config = yaml_safe_load(f) or {}
|
55
|
+
|
56
|
+
if "auth_config" in file_config:
|
57
|
+
auth_config_data = file_config.pop("auth_config")
|
58
|
+
config_data["auth_config"] = AuthConfig(**auth_config_data)
|
59
|
+
|
60
|
+
if "llm_user_config" in file_config:
|
61
|
+
llm_config_data = file_config.pop("llm_user_config")
|
62
|
+
config_data["llm_user_config"] = LLMUserConfig(**llm_config_data)
|
63
|
+
|
64
|
+
config_data.update(file_config)
|
65
|
+
|
66
|
+
if test_paths:
|
67
|
+
config_data["test_paths"] = test_paths.split(",")
|
68
|
+
logger.info(f"Using test paths: {config_data['test_paths']}")
|
69
|
+
if output_dir:
|
70
|
+
config_data["output_dir"] = output_dir
|
71
|
+
logger.info(f"Using output directory: {config_data['output_dir']}")
|
72
|
+
|
73
|
+
config = TestConfig(**config_data)
|
74
|
+
|
75
|
+
evaluate.main(config)
|
76
|
+
|
77
|
+
def record(self, output_dir) -> None:
|
78
|
+
url, tenant_name, token = self._get_env_config()
|
79
|
+
config_data = {
|
80
|
+
"output_dir": Path.cwd() if output_dir is None else Path(output_dir),
|
81
|
+
"service_url": url,
|
82
|
+
"tenant_name": tenant_name,
|
83
|
+
"token": token
|
84
|
+
}
|
85
|
+
|
86
|
+
config_data["output_dir"].mkdir(parents=True, exist_ok=True)
|
87
|
+
logger.info(f"Recording chat sessions to {config_data['output_dir']}")
|
88
|
+
|
89
|
+
record_chats(ChatRecordingConfig(**config_data))
|
90
|
+
|
91
|
+
def generate(self, stories_path: str, tools_path: str, output_dir: str) -> None:
|
92
|
+
stories_path = Path(stories_path)
|
93
|
+
tools_path = Path(tools_path)
|
94
|
+
|
95
|
+
if output_dir is None:
|
96
|
+
output_dir = stories_path.parent
|
97
|
+
else:
|
98
|
+
output_dir = Path(output_dir)
|
99
|
+
|
100
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
101
|
+
|
102
|
+
stories_by_agent = {}
|
103
|
+
with stories_path.open("r", encoding="utf-8", newline='') as f:
|
104
|
+
csv_reader = csv.DictReader(f)
|
105
|
+
for row in csv_reader:
|
106
|
+
agent_name = row["agent"]
|
107
|
+
if agent_name not in stories_by_agent:
|
108
|
+
stories_by_agent[agent_name] = []
|
109
|
+
stories_by_agent[agent_name].append(row["story"])
|
110
|
+
|
111
|
+
for agent_name, stories in stories_by_agent.items():
|
112
|
+
logger.info(f"Found {len(stories)} stories for agent '{agent_name}'")
|
113
|
+
try:
|
114
|
+
agent_controller = AgentsController()
|
115
|
+
agent = agent_controller.get_agent(agent_name, AgentKind.NATIVE)
|
116
|
+
allowed_tools = agent_controller.get_agent_tool_names(agent.tools)
|
117
|
+
except Exception as e:
|
118
|
+
logger.warning(f"Could not get tools for agent {agent_name}: {str(e)}")
|
119
|
+
allowed_tools = []
|
120
|
+
|
121
|
+
|
122
|
+
logger.info(f"Running tool planner for agent {agent_name}")
|
123
|
+
agent_snapshot_path = output_dir / f"{agent_name}_snapshot_llm.json"
|
124
|
+
build_snapshot(agent_name, tools_path, stories, agent_snapshot_path)
|
125
|
+
|
126
|
+
logger.info(f"Running batch annotate for agent {agent_name}")
|
127
|
+
generate_test_cases_from_stories(
|
128
|
+
agent_name=agent_name,
|
129
|
+
stories=stories,
|
130
|
+
tools_path=tools_path,
|
131
|
+
snapshot_path=agent_snapshot_path,
|
132
|
+
output_dir=output_dir / f"{agent_name}_test_cases",
|
133
|
+
allowed_tools=allowed_tools,
|
134
|
+
num_variants=2
|
135
|
+
)
|
136
|
+
|
137
|
+
logger.info("Test cases stored at: %s", output_dir)
|
138
|
+
|
139
|
+
def analyze(self, data_path: str) -> None:
|
140
|
+
config = AnalyzeConfig(data_path=data_path)
|
141
|
+
analyze(config)
|
142
|
+
|
143
|
+
def summarize(self) -> None:
|
144
|
+
pass
|
145
|
+
|
146
|
+
def external_validate(self, config: Dict, data: List[str], credential:str):
|
147
|
+
validator = ExternalAgentValidation(credential=credential,
|
148
|
+
auth_scheme=config["auth_scheme"],
|
149
|
+
service_url=config["api_url"])
|
150
|
+
summary = []
|
151
|
+
for entry in data:
|
152
|
+
results = validator.call_validation(entry)
|
153
|
+
if len(results) == 0:
|
154
|
+
rich.print(f"[red] No events are generated for input {entry} [/red]")
|
155
|
+
summary.append({entry: results})
|
156
|
+
|
157
|
+
return summary
|
158
|
+
|
@@ -50,7 +50,7 @@ def list_knowledge_bases(
|
|
50
50
|
controller = KnowledgeBaseController()
|
51
51
|
controller.list_knowledge_bases(verbose=verbose)
|
52
52
|
|
53
|
-
@knowledge_bases_app.command(name="remove", help="
|
53
|
+
@knowledge_bases_app.command(name="remove", help="Remove a knowledge base. Note that if your knowledge base was created by uploading documents (for built-in Milvus), the ingested information from your documents will also be deleted. If your knowledge base uses an external knowledge source through an index_config definition, your index will not be deleted.")
|
54
54
|
def remove_knowledge_base(
|
55
55
|
name: Annotated[
|
56
56
|
str,
|
@@ -64,7 +64,7 @@ def remove_knowledge_base(
|
|
64
64
|
controller = KnowledgeBaseController()
|
65
65
|
controller.remove_knowledge_base(id=id, name=name)
|
66
66
|
|
67
|
-
@knowledge_bases_app.command(name="status", help="Get the status of a
|
67
|
+
@knowledge_bases_app.command(name="status", help="Get the status of a knowledge base")
|
68
68
|
def knowledge_base_status(
|
69
69
|
name: Annotated[
|
70
70
|
str,
|
@@ -23,28 +23,34 @@ PROVIDER_EXTRA_PROPERTIES_LUT = {
|
|
23
23
|
# 'azure_ad_token',
|
24
24
|
# 'azure_model_name'
|
25
25
|
# },
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
26
|
+
ModelProvider.AZURE_OPENAI: {
|
27
|
+
'azure_resource_name',
|
28
|
+
'azure_deployment_id',
|
29
|
+
'azure_api_version',
|
30
|
+
'azure_model_name'
|
31
|
+
},
|
32
|
+
ModelProvider.BEDROCK: {
|
33
|
+
'aws_secret_access_key',
|
34
|
+
'aws_access_key_id',
|
35
|
+
'aws_session_token',
|
36
|
+
'aws_region',
|
37
|
+
'aws_auth_type',
|
38
|
+
'aws_role_arn',
|
39
|
+
'aws_external_id',
|
40
|
+
'aws_s3_bucket',
|
41
|
+
'aws_s3_object_key',
|
42
|
+
'aws_bedrock_model',
|
43
|
+
'aws_server_side_encryption',
|
44
|
+
'aws_server_side_encryption_kms_key_id'
|
45
|
+
},
|
46
|
+
ModelProvider.VERTEX_AI: {
|
47
|
+
'vertex_region',
|
48
|
+
'vertex_project_id',
|
49
|
+
'vertex_service_account_json',
|
50
|
+
'vertex_storage_bucket_name',
|
51
|
+
'vertex_model_name',
|
52
|
+
'filename'
|
53
|
+
},
|
48
54
|
# ModelProvider.HUGGINGFACE: {'huggingfaceBaseUrl'},
|
49
55
|
ModelProvider.MISTRAL_AI: {'mistral_fim_completion'},
|
50
56
|
# ModelProvider.STABILITY_AI: {'stability_client_id', 'stability_client_user_id', 'stability_client_version'},
|
@@ -93,7 +99,8 @@ PROVIDER_REQUIRED_FIELDS = {k:['api_key'] for k in ModelProvider}
|
|
93
99
|
# Use sets to denote when a requirement is 'or'
|
94
100
|
PROVIDER_REQUIRED_FIELDS.update({
|
95
101
|
ModelProvider.WATSONX: PROVIDER_REQUIRED_FIELDS[ModelProvider.WATSONX] + [{'watsonx_space_id', 'watsonx_project_id', 'watsonx_deployment_id'}],
|
96
|
-
ModelProvider.OLLAMA: PROVIDER_REQUIRED_FIELDS[ModelProvider.OLLAMA] + ['custom_host']
|
102
|
+
ModelProvider.OLLAMA: PROVIDER_REQUIRED_FIELDS[ModelProvider.OLLAMA] + ['custom_host'],
|
103
|
+
ModelProvider.BEDROCK: [],
|
97
104
|
})
|
98
105
|
|
99
106
|
# def env_file_to_model_ProviderConfig(model_name: str, env_file_path: str) -> ProviderConfig | None:
|
@@ -163,7 +170,7 @@ def _validate_requirements(provider: ModelProvider, cfg: ProviderConfig, app_id:
|
|
163
170
|
if not app_id:
|
164
171
|
missing_credentials_string = f"Missing configuration variable(s) required for the provider {provider}:"
|
165
172
|
else:
|
166
|
-
missing_credentials_string = f"
|
173
|
+
missing_credentials_string = f"Be sure to include the following required fields for provider '{provider}' in the connection '{app_id}':"
|
167
174
|
for cred in missing_credentials:
|
168
175
|
if isinstance(cred, set):
|
169
176
|
cred_str = ' or '.join(list(cred))
|
@@ -177,7 +184,6 @@ def _validate_requirements(provider: ModelProvider, cfg: ProviderConfig, app_id:
|
|
177
184
|
sys.exit(1)
|
178
185
|
else:
|
179
186
|
logger.info(missing_credentials_string)
|
180
|
-
logger.info(f"Please ensure these values are set in the connection '{app_id}'.")
|
181
187
|
|
182
188
|
|
183
189
|
def validate_ProviderConfig(cfg: ProviderConfig, app_id: str)-> None:
|
@@ -146,18 +146,18 @@ def models_policy_add(
|
|
146
146
|
ModelPolicyStrategyMode,
|
147
147
|
typer.Option('--strategy', '-s', help='How to spread traffic across models'),
|
148
148
|
],
|
149
|
+
retry_attempts: Annotated[
|
150
|
+
int,
|
151
|
+
typer.Option('--retry-attempts', help='The number of attempts to retry'),
|
152
|
+
],
|
149
153
|
strategy_on_code: Annotated[
|
150
154
|
List[int],
|
151
155
|
typer.Option('--strategy-on-code', help='The http status to consider invoking the strategy'),
|
152
|
-
],
|
156
|
+
] = None,
|
153
157
|
retry_on_code: Annotated[
|
154
158
|
List[int],
|
155
159
|
typer.Option('--retry-on-code', help='The http status to consider retrying the llm call'),
|
156
|
-
],
|
157
|
-
retry_attempts: Annotated[
|
158
|
-
int,
|
159
|
-
typer.Option('--retry-attempts', help='The number of attempts to retry'),
|
160
|
-
],
|
160
|
+
] = None,
|
161
161
|
display_name: Annotated[
|
162
162
|
str,
|
163
163
|
typer.Option('--display-name', help='What name should this llm appear as within the ui'),
|