ibm-watsonx-orchestrate 1.7.0a0__py3-none-any.whl → 1.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. ibm_watsonx_orchestrate/__init__.py +1 -1
  2. ibm_watsonx_orchestrate/agent_builder/agents/agent.py +3 -3
  3. ibm_watsonx_orchestrate/agent_builder/agents/assistant_agent.py +3 -2
  4. ibm_watsonx_orchestrate/agent_builder/agents/external_agent.py +3 -2
  5. ibm_watsonx_orchestrate/agent_builder/agents/types.py +38 -9
  6. ibm_watsonx_orchestrate/agent_builder/agents/webchat_customizations/prompts.py +1 -0
  7. ibm_watsonx_orchestrate/agent_builder/connections/connections.py +25 -10
  8. ibm_watsonx_orchestrate/agent_builder/connections/types.py +19 -11
  9. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/knowledge_base_requests.py +1 -22
  10. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +1 -17
  11. ibm_watsonx_orchestrate/agent_builder/tools/base_tool.py +2 -1
  12. ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +14 -13
  13. ibm_watsonx_orchestrate/agent_builder/tools/python_tool.py +136 -92
  14. ibm_watsonx_orchestrate/agent_builder/tools/types.py +10 -9
  15. ibm_watsonx_orchestrate/cli/commands/agents/agents_command.py +7 -7
  16. ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +35 -7
  17. ibm_watsonx_orchestrate/cli/commands/channels/types.py +15 -2
  18. ibm_watsonx_orchestrate/cli/commands/channels/webchat/channels_webchat_controller.py +35 -25
  19. ibm_watsonx_orchestrate/cli/commands/chat/chat_command.py +2 -0
  20. ibm_watsonx_orchestrate/cli/commands/connections/connections_command.py +14 -6
  21. ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +12 -12
  22. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_command.py +65 -0
  23. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_controller.py +368 -0
  24. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_server_controller.py +170 -0
  25. ibm_watsonx_orchestrate/cli/commands/environment/environment_controller.py +6 -6
  26. ibm_watsonx_orchestrate/cli/commands/environment/types.py +3 -1
  27. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_command.py +134 -36
  28. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_controller.py +42 -11
  29. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_command.py +0 -18
  30. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +36 -20
  31. ibm_watsonx_orchestrate/cli/commands/models/models_command.py +1 -1
  32. ibm_watsonx_orchestrate/cli/commands/models/models_controller.py +5 -8
  33. ibm_watsonx_orchestrate/cli/commands/server/server_command.py +59 -10
  34. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_command.py +1 -1
  35. ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +93 -14
  36. ibm_watsonx_orchestrate/cli/config.py +3 -3
  37. ibm_watsonx_orchestrate/cli/init_helper.py +10 -1
  38. ibm_watsonx_orchestrate/cli/main.py +5 -0
  39. ibm_watsonx_orchestrate/client/base_api_client.py +12 -0
  40. ibm_watsonx_orchestrate/client/connections/connections_client.py +5 -30
  41. ibm_watsonx_orchestrate/client/copilot/cpe/copilot_cpe_client.py +67 -0
  42. ibm_watsonx_orchestrate/client/knowledge_bases/knowledge_base_client.py +1 -1
  43. ibm_watsonx_orchestrate/client/local_service_instance.py +3 -1
  44. ibm_watsonx_orchestrate/client/service_instance.py +33 -7
  45. ibm_watsonx_orchestrate/client/utils.py +49 -8
  46. ibm_watsonx_orchestrate/docker/compose-lite.yml +25 -6
  47. ibm_watsonx_orchestrate/docker/default.env +26 -15
  48. ibm_watsonx_orchestrate/flow_builder/flows/__init__.py +9 -4
  49. ibm_watsonx_orchestrate/flow_builder/flows/decorators.py +4 -2
  50. ibm_watsonx_orchestrate/flow_builder/flows/events.py +10 -9
  51. ibm_watsonx_orchestrate/flow_builder/flows/flow.py +131 -20
  52. ibm_watsonx_orchestrate/flow_builder/node.py +18 -1
  53. ibm_watsonx_orchestrate/flow_builder/types.py +271 -15
  54. ibm_watsonx_orchestrate/flow_builder/utils.py +121 -6
  55. ibm_watsonx_orchestrate/utils/exceptions.py +23 -0
  56. {ibm_watsonx_orchestrate-1.7.0a0.dist-info → ibm_watsonx_orchestrate-1.8.0.dist-info}/METADATA +5 -5
  57. {ibm_watsonx_orchestrate-1.7.0a0.dist-info → ibm_watsonx_orchestrate-1.8.0.dist-info}/RECORD +60 -56
  58. ibm_watsonx_orchestrate/flow_builder/resources/flow_status.openapi.yml +0 -66
  59. {ibm_watsonx_orchestrate-1.7.0a0.dist-info → ibm_watsonx_orchestrate-1.8.0.dist-info}/WHEEL +0 -0
  60. {ibm_watsonx_orchestrate-1.7.0a0.dist-info → ibm_watsonx_orchestrate-1.8.0.dist-info}/entry_points.txt +0 -0
  61. {ibm_watsonx_orchestrate-1.7.0a0.dist-info → ibm_watsonx_orchestrate-1.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -23,7 +23,7 @@ from ibm_watsonx_orchestrate.cli.config import (
23
23
  )
24
24
  from ibm_watsonx_orchestrate.client.client import Client
25
25
  from ibm_watsonx_orchestrate.client.client_errors import ClientError
26
- from ibm_watsonx_orchestrate.client.agents.agent_client import AgentClient, ClientAPIException
26
+ from ibm_watsonx_orchestrate.client.knowledge_bases.knowledge_base_client import KnowledgeBaseClient, ClientAPIException
27
27
  from ibm_watsonx_orchestrate.client.credentials import Credentials
28
28
  from threading import Lock
29
29
  from ibm_watsonx_orchestrate.client.utils import is_local_dev, check_token_validity, is_cpd_env
@@ -55,13 +55,13 @@ def _validate_token_functionality(token: str, url: str) -> None:
55
55
  '''
56
56
  is_cpd = is_cpd_env(url)
57
57
  if is_cpd is True:
58
- agent_client = AgentClient(base_url=url, api_key=token, is_local=is_local_dev(url), verify=False)
58
+ knowledge_base_client = KnowledgeBaseClient(base_url=url, api_key=token, is_local=is_local_dev(url), verify=False)
59
59
  else:
60
- agent_client = AgentClient(base_url=url, api_key=token, is_local=is_local_dev(url))
61
- agent_client.api_key = token
60
+ knowledge_base_client = KnowledgeBaseClient(base_url=url, api_key=token, is_local=is_local_dev(url))
61
+ knowledge_base_client.api_key = token
62
62
 
63
63
  try:
64
- agent_client.get()
64
+ knowledge_base_client.get()
65
65
  except ClientAPIException as e:
66
66
  if e.response.status_code >= 400:
67
67
  reason = e.response.reason
@@ -167,7 +167,7 @@ def activate(name: str, apikey: str=None, username: str=None, password: str=None
167
167
 
168
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:
169
169
  if name == PROTECTED_ENV_NAME:
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}'")
170
+ logger.error(f"The name '{PROTECTED_ENV_NAME}' is a reserved environment name. Please select a different name or use `orchestrate env activate {PROTECTED_ENV_NAME}` to swap to '{PROTECTED_ENV_NAME}'")
171
171
  return
172
172
 
173
173
  cfg = Config()
@@ -4,7 +4,9 @@ from enum import Enum
4
4
  class EnvironmentAuthType(str, Enum):
5
5
  IBM_CLOUD_IAM = 'ibm_iam'
6
6
  MCSP = 'mcsp'
7
+ MCSP_V1 = 'mcsp_v1'
8
+ MCSP_V2 = 'mcsp_v2'
7
9
  CPD = 'cpd'
8
10
 
9
11
  def __str__(self):
10
- return self.value
12
+ return self.value
@@ -5,10 +5,12 @@ import os
5
5
  import yaml
6
6
  import csv
7
7
  import rich
8
- from pathlib import Path
9
8
  import sys
10
- from dotenv import dotenv_values
9
+ import shutil
11
10
 
11
+ from rich.panel import Panel
12
+ from pathlib import Path
13
+ from dotenv import dotenv_values
12
14
  from typing import Optional
13
15
  from typing_extensions import Annotated
14
16
 
@@ -23,14 +25,20 @@ def read_env_file(env_path: Path|str) -> dict:
23
25
  return dotenv_values(str(env_path))
24
26
 
25
27
  def validate_watsonx_credentials(user_env_file: str) -> bool:
26
- required_keys = ["WATSONX_SPACE_ID", "WATSONX_APIKEY"]
28
+ required_sets = [
29
+ ["WATSONX_SPACE_ID", "WATSONX_APIKEY"],
30
+ ["WO_INSTANCE", "WO_API_KEY"]
31
+ ]
27
32
 
28
- if all(key in os.environ for key in required_keys):
33
+ def has_valid_keys(env: dict) -> bool:
34
+ return any(all(key in env for key in key_set) for key_set in required_sets)
35
+
36
+ if has_valid_keys(os.environ):
29
37
  logger.info("WatsonX credentials validated successfully.")
30
38
  return
31
39
 
32
40
  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.")
41
+ logger.error("WatsonX credentials are not set. Please set either WATSONX_SPACE_ID and WATSONX_APIKEY or WO_INSTANCE and WO_API_KEY in your system environment variables or include them in your environment file and pass it with --env-file option.")
34
42
  sys.exit(1)
35
43
 
36
44
  if not Path(user_env_file).exists():
@@ -39,13 +47,43 @@ def validate_watsonx_credentials(user_env_file: str) -> bool:
39
47
 
40
48
  user_env = read_env_file(user_env_file)
41
49
 
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.")
50
+ if not has_valid_keys(user_env):
51
+ logger.error("Error: The environment file does not contain the required keys: either WATSONX_SPACE_ID and WATSONX_APIKEY or WO_INSTANCE and WO_API_KEY.")
44
52
  sys.exit(1)
45
53
 
46
- os.environ.update({key: user_env[key] for key in required_keys})
54
+ # Update os.environ with whichever set is present
55
+ for key_set in required_sets:
56
+ if all(key in user_env for key in key_set):
57
+ os.environ.update({key: user_env[key] for key in key_set})
58
+ break
47
59
  logger.info("WatsonX credentials validated successfully.")
48
60
 
61
+ def read_csv(data_path: str, delimiter="\t"):
62
+ data = []
63
+ with open(data_path, "r") as f:
64
+ tsv_reader = csv.reader(f, delimiter=delimiter)
65
+ for line in tsv_reader:
66
+ data.append(line)
67
+
68
+ return data
69
+
70
+ def performance_test(agent_name, data_path, output_dir = None, user_env_file = None):
71
+ test_data = read_csv(data_path)
72
+
73
+ controller = EvaluationsController()
74
+ generated_performance_tests = controller.generate_performance_test(agent_name, test_data)
75
+
76
+ generated_perf_test_dir = Path(output_dir) / "generated_performance_tests"
77
+ generated_perf_test_dir.mkdir(exist_ok=True, parents=True)
78
+
79
+ for idx, test in enumerate(generated_performance_tests):
80
+ test_name = f"validate_external_agent_evaluation_test_{idx}.json"
81
+ with open(generated_perf_test_dir / test_name, encoding="utf-8", mode="w+") as f:
82
+ json.dump(test, f, indent=4)
83
+
84
+ rich.print(f"Performance test cases saved at path '{str(generated_perf_test_dir)}'")
85
+ rich.print("[gold3]Running Performance Test")
86
+ evaluate(output_dir=output_dir, test_paths=str(generated_perf_test_dir))
49
87
 
50
88
  @evaluation_app.command(name="evaluate", help="Evaluate an agent against a set of test cases")
51
89
  def evaluate(
@@ -115,7 +153,7 @@ def generate(
115
153
  stories_path: Annotated[
116
154
  str,
117
155
  typer.Option(
118
- "--stories_path", "-s",
156
+ "--stories-path", "-s",
119
157
  help="Path to the CSV file containing user stories for test case generation. "
120
158
  "The file has 'story' and 'agent' columns."
121
159
  )
@@ -123,14 +161,14 @@ def generate(
123
161
  tools_path: Annotated[
124
162
  str,
125
163
  typer.Option(
126
- "--tools_path", "-t",
164
+ "--tools-path", "-t",
127
165
  help="Path to the directory containing tool definitions."
128
166
  )
129
167
  ],
130
168
  output_dir: Annotated[
131
169
  Optional[str],
132
170
  typer.Option(
133
- "--output_dir", "-o",
171
+ "--output-dir", "-o",
134
172
  help="Directory to save the generated test cases."
135
173
  )
136
174
  ] = None,
@@ -151,7 +189,7 @@ def generate(
151
189
  def analyze(data_path: Annotated[
152
190
  str,
153
191
  typer.Option(
154
- "--data_path", "-d",
192
+ "--data-path", "-d",
155
193
  help="Path to the directory that has the saved results"
156
194
  )
157
195
  ],
@@ -167,30 +205,31 @@ def analyze(data_path: Annotated[
167
205
  controller = EvaluationsController()
168
206
  controller.analyze(data_path=data_path)
169
207
 
170
-
171
- @evaluation_app.command(name="validate_external", help="Validate an external agent against a set of inputs")
208
+ @evaluation_app.command(name="validate-external", help="Validate an external agent against a set of inputs")
172
209
  def validate_external(
173
210
  data_path: Annotated[
174
211
  str,
175
212
  typer.Option(
176
- "--csv", "-c",
177
- help="Path to .csv file of inputs"
213
+ "--tsv", "-t",
214
+ help="Path to .tsv file of inputs"
178
215
  )
179
216
  ],
180
- config: Annotated[
217
+ external_agent_config: Annotated[
181
218
  str,
182
219
  typer.Option(
183
- "--config", "-cf",
184
- help="Path to the external agent yaml"
220
+ "--external-agent-config", "-ext",
221
+ help="Path to the external agent yaml",
222
+
185
223
  )
186
224
  ],
187
225
  credential: Annotated[
188
226
  str,
189
227
  typer.Option(
190
228
  "--credential", "-crd",
191
- help="credential string"
229
+ help="credential string",
230
+ rich_help_panel="Parameters for Validation"
192
231
  )
193
- ],
232
+ ] = None,
194
233
  output_dir: Annotated[
195
234
  str,
196
235
  typer.Option(
@@ -204,21 +243,80 @@ def validate_external(
204
243
  "--env-file", "-e",
205
244
  help="Path to a .env file that overrides default.env. Then environment variables override both."
206
245
  ),
246
+ ] = None,
247
+ agent_name: Annotated[
248
+ str,
249
+ typer.Option(
250
+ "--agent_name", "-a",
251
+ help="Name of the native agent which has the external agent to test registered as a collaborater. See: https://developer.watson-orchestrate.ibm.com/agents/build_agent#native-agents)." \
252
+ " If this parameter is pased, validation of the external agent is not run.",
253
+ rich_help_panel="Parameters for Input Evaluation"
254
+ )
207
255
  ] = None
208
256
  ):
209
-
257
+
210
258
  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]")
259
+ Path(output_dir).mkdir(exist_ok=True)
260
+ shutil.copy(data_path, os.path.join(output_dir, "input_sample.tsv"))
261
+
262
+ if agent_name is not None:
263
+ eval_dir = os.path.join(output_dir, "evaluation")
264
+ if os.path.exists(eval_dir):
265
+ rich.print(f"[yellow]: found existing {eval_dir} in target directory. All content is removed.")
266
+ shutil.rmtree(os.path.join(output_dir, "evaluation"))
267
+ Path(eval_dir).mkdir(exist_ok=True)
268
+ # save external agent config even though its not used for evaluation
269
+ # it can help in later debugging customer agents
270
+ with open(os.path.join(eval_dir, "external_agent_cfg.yaml"), "w+") as f:
271
+ with open(external_agent_config, "r") as cfg:
272
+ external_agent_config = yaml.safe_load(cfg)
273
+ yaml.safe_dump(external_agent_config, f, indent=4)
274
+
275
+ rich.print(f"[gold3]Starting evaluation of inputs in '{data_path}' against '{agent_name}'[/gold3]")
276
+ performance_test(
277
+ agent_name=agent_name,
278
+ data_path=data_path,
279
+ output_dir=eval_dir,
280
+ user_env_file=user_env_file
281
+ )
282
+
283
+ else:
284
+ with open(external_agent_config, "r") as f:
285
+ external_agent_config = yaml.safe_load(f)
286
+ controller = EvaluationsController()
287
+ test_data = []
288
+ with open(data_path, "r") as f:
289
+ csv_reader = csv.reader(f, delimiter="\t")
290
+ for line in csv_reader:
291
+ test_data.append(line[0])
292
+
293
+ # save validation results in "validation_results" sub-dir
294
+ validation_folder = Path(output_dir) / "validation_results"
295
+ if os.path.exists(validation_folder):
296
+ rich.print(f"[yellow]: found existing {validation_folder} in target directory. All content is removed.")
297
+ shutil.rmtree(validation_folder)
298
+ validation_folder.mkdir(exist_ok=True, parents=True)
299
+
300
+ # validate the inputs in the provided csv file
301
+ summary = controller.external_validate(external_agent_config, test_data, credential)
302
+ with open(validation_folder / "validation_results.json", "w") as f:
303
+ json.dump(summary, f, indent=4)
304
+
305
+ # validate sample block inputs
306
+ rich.print("[gold3]Validating external agent to see if it can handle an array of messages.")
307
+ block_input_summary = controller.external_validate(external_agent_config, test_data, credential, add_context=True)
308
+ with open(validation_folder / "sample_block_validation_results.json", "w") as f:
309
+ json.dump(block_input_summary, f, indent=4)
310
+
311
+ user_validation_successful = all([item["success"] for item in summary])
312
+ block_validation_successful = all([item["success"] for item in block_input_summary])
313
+
314
+ if user_validation_successful and block_validation_successful:
315
+ msg = (
316
+ f"[green]Validation is successful. The result is saved to '{str(validation_folder)}'.[/green]\n"
317
+ "You can add the external agent as a collaborator agent. See: https://developer.watson-orchestrate.ibm.com/agents/build_agent#native-agents."
318
+ )
319
+ else:
320
+ msg = f"[dark_orange]Schema validation did not succeed. See '{str(validation_folder)}' for failures.[/dark_orange]"
321
+
322
+ rich.print(Panel(msg))
@@ -1,21 +1,23 @@
1
1
  import logging
2
- from typing import List, Dict, Optional
2
+ import os.path
3
+ from typing import List, Dict, Optional, Tuple
3
4
  import csv
4
5
  from pathlib import Path
5
- import rich
6
+ import sys
6
7
  from wxo_agentic_evaluation import main as evaluate
7
8
  from wxo_agentic_evaluation.tool_planner import build_snapshot
8
9
  from wxo_agentic_evaluation.analyze_run import analyze
9
10
  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.arg_configs import TestConfig, AuthConfig, LLMUserConfig, ChatRecordingConfig, AnalyzeConfig, ProviderConfig
11
12
  from wxo_agentic_evaluation.record_chat import record_chats
12
13
  from wxo_agentic_evaluation.external_agent.external_validate import ExternalAgentValidation
14
+ from wxo_agentic_evaluation.external_agent.performance_test import ExternalAgentPerformanceTest
13
15
  from ibm_watsonx_orchestrate import __version__
14
16
  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
17
  from ibm_watsonx_orchestrate.utils.utils import yaml_safe_load
16
18
  from ibm_watsonx_orchestrate.cli.commands.agents.agents_controller import AgentsController
17
19
  from ibm_watsonx_orchestrate.agent_builder.agents import AgentKind
18
-
20
+ import uuid
19
21
 
20
22
  logger = logging.getLogger(__name__)
21
23
 
@@ -39,12 +41,26 @@ class EvaluationsController:
39
41
  def evaluate(self, config_file: Optional[str] = None, test_paths: Optional[str] = None, output_dir: Optional[str] = None) -> None:
40
42
  url, tenant_name, token = self._get_env_config()
41
43
 
44
+ if "WATSONX_SPACE_ID" in os.environ and "WATSONX_APIKEY" in os.environ:
45
+ provider = "watsonx"
46
+ elif "WO_INSTANCE" in os.environ and "WO_API_KEY" in os.environ:
47
+ provider = "model_proxy"
48
+ else:
49
+ logger.error(
50
+ "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."
51
+ )
52
+ sys.exit(1)
53
+
42
54
  config_data = {
43
55
  "wxo_lite_version": __version__,
44
56
  "auth_config": AuthConfig(
45
57
  url=url,
46
58
  tenant_name=tenant_name,
47
59
  token=token
60
+ ),
61
+ "provider_config": ProviderConfig(
62
+ provider=provider,
63
+ model_id="meta-llama/llama-3-405b-instruct",
48
64
  )
49
65
  }
50
66
 
@@ -60,6 +76,10 @@ class EvaluationsController:
60
76
  if "llm_user_config" in file_config:
61
77
  llm_config_data = file_config.pop("llm_user_config")
62
78
  config_data["llm_user_config"] = LLMUserConfig(**llm_config_data)
79
+
80
+ if "provider_config" in file_config:
81
+ provider_config_data = file_config.pop("provider_config")
82
+ config_data["provider_config"] = ProviderConfig(**provider_config_data)
63
83
 
64
84
  config_data.update(file_config)
65
85
 
@@ -75,9 +95,13 @@ class EvaluationsController:
75
95
  evaluate.main(config)
76
96
 
77
97
  def record(self, output_dir) -> None:
98
+
99
+
100
+ random_uuid = str(uuid.uuid4())
101
+
78
102
  url, tenant_name, token = self._get_env_config()
79
103
  config_data = {
80
- "output_dir": Path.cwd() if output_dir is None else Path(output_dir),
104
+ "output_dir": Path(os.path.join(Path.cwd(), random_uuid)) if output_dir is None else Path(os.path.join(output_dir,random_uuid)),
81
105
  "service_url": url,
82
106
  "tenant_name": tenant_name,
83
107
  "token": token
@@ -143,16 +167,23 @@ class EvaluationsController:
143
167
  def summarize(self) -> None:
144
168
  pass
145
169
 
146
- def external_validate(self, config: Dict, data: List[str], credential:str):
170
+ def external_validate(self, config: Dict, data: List[str], credential:str, add_context: bool = False):
147
171
  validator = ExternalAgentValidation(credential=credential,
148
172
  auth_scheme=config["auth_scheme"],
149
173
  service_url=config["api_url"])
174
+
150
175
  summary = []
151
176
  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})
177
+ results = validator.call_validation(entry, add_context)
178
+ summary.append(results)
156
179
 
157
180
  return summary
158
-
181
+
182
+ def generate_performance_test(self, agent_name: str, test_data: List[Tuple[str, str]]):
183
+ performance_test = ExternalAgentPerformanceTest(
184
+ agent_name=agent_name,
185
+ test_data=test_data
186
+ )
187
+ generated_performance_tests = performance_test.generate_tests()
188
+
189
+ return generated_performance_tests
@@ -21,24 +21,6 @@ def knowledge_base_import(
21
21
  controller = KnowledgeBaseController()
22
22
  controller.import_knowledge_base(file=file, app_id=app_id)
23
23
 
24
- @knowledge_bases_app.command(name="patch", help="Patch a knowledge base by uploading documents, or providing an external vector index")
25
- def knowledge_base_patch(
26
- file: Annotated[
27
- str,
28
- typer.Option("--file", "-f", help="YAML or JSON file with knowledge base definition"),
29
- ],
30
- name: Annotated[
31
- str,
32
- typer.Option("--name", "-n", help="Name of the knowledge base you wish to update"),
33
- ]=None,
34
- id: Annotated[
35
- str,
36
- typer.Option("--id", "-i", help="ID of the knowledge base you wish to update"),
37
- ]=None
38
- ):
39
- controller = KnowledgeBaseController()
40
- controller.update_knowledge_base(id=id, name=name, file=file)
41
-
42
24
 
43
25
  @knowledge_bases_app.command(name="list", help="List all knowledge bases")
44
26
  def list_knowledge_bases(
@@ -8,7 +8,6 @@ import inspect
8
8
  from pathlib import Path
9
9
  from typing import List
10
10
 
11
- from ibm_watsonx_orchestrate.agent_builder.knowledge_bases.knowledge_base_requests import KnowledgeBaseUpdateRequest
12
11
  from ibm_watsonx_orchestrate.agent_builder.knowledge_bases.knowledge_base import KnowledgeBase
13
12
  from ibm_watsonx_orchestrate.client.knowledge_bases.knowledge_base_client import KnowledgeBaseClient
14
13
  from ibm_watsonx_orchestrate.client.base_api_client import ClientAPIException
@@ -72,11 +71,21 @@ class KnowledgeBaseController:
72
71
  client = self.get_client()
73
72
 
74
73
  knowledge_bases = parse_file(file=file)
74
+
75
+ existing_knowledge_bases = client.get_by_names([kb.name for kb in knowledge_bases])
76
+
75
77
  for kb in knowledge_bases:
76
78
  try:
79
+ file_dir = "/".join(file.split("/")[:-1])
80
+
81
+ existing = list(filter(lambda ex: ex.get('name') == kb.name, existing_knowledge_bases))
82
+ if len(existing) > 0:
83
+ logger.info(f"Existing knowledge base '{kb.name}' found. Updating...")
84
+ self.update_knowledge_base(existing[0].get("id"), kb=kb, file_dir=file_dir)
85
+ continue
86
+
77
87
  kb.validate_documents_or_index_exists()
78
88
  if kb.documents:
79
- file_dir = "/".join(file.split("/")[:-1])
80
89
  files = [('files', (get_file_name(file_path), open(get_relative_file_path(file_path, file_dir), 'rb'))) for file_path in kb.documents]
81
90
 
82
91
  kb.prioritize_built_in_index = True
@@ -106,10 +115,7 @@ class KnowledgeBaseController:
106
115
 
107
116
  logger.info(f"Successfully imported knowledge base '{kb.name}'")
108
117
  except ClientAPIException as e:
109
- if "duplicate key value violates unique constraint" in e.response.text:
110
- logger.error(f"A knowledge base with the name '{kb.name}' already exists. Failed to import knowledge base")
111
- else:
112
- logger.error(f"Error importing knowledge base '{kb.name}\n' {e.response.text}")
118
+ logger.error(f"Error importing knowledge base '{kb.name}\n' {e.response.text}")
113
119
 
114
120
  def get_id(
115
121
  self, id: str, name: str
@@ -131,27 +137,37 @@ class KnowledgeBaseController:
131
137
 
132
138
 
133
139
  def update_knowledge_base(
134
- self, id: str, name: str, file: str
135
- ) -> None:
136
- knowledge_base_id = self.get_id(id, name)
137
- update_request = KnowledgeBaseUpdateRequest.from_spec(file=file)
140
+ self, knowledge_base_id: str, kb: KnowledgeBase, file_dir: str
141
+ ) -> None:
142
+ if kb.documents:
143
+ status = self.get_client().status(knowledge_base_id)
144
+ existing_docs = [doc.get("metadata", {}).get("original_file_name", "") for doc in status.get("documents", [])]
145
+
146
+ removed_docs = existing_docs[:]
147
+ for filepath in kb.documents:
148
+ filename = get_file_name(filepath)
149
+
150
+ if filename in existing_docs:
151
+ logger.warning(f'Document \"{filename}\" already exists in knowledge base. Updating...')
152
+ removed_docs.remove(filename)
153
+
154
+ for filename in removed_docs:
155
+ logger.warning(f'Document \"{filename}\" removed from knowledge base.')
138
156
 
139
- if update_request.documents:
140
- file_dir = "/".join(file.split("/")[:-1])
141
- files = [('files', (get_file_name(file_path), open(get_relative_file_path(file_path, file_dir), 'rb'))) for file_path in update_request.documents]
157
+
158
+ files = [('files', (get_file_name(file_path), open(get_relative_file_path(file_path, file_dir), 'rb'))) for file_path in kb.documents]
142
159
 
143
- update_request.prioritize_built_in_index = True
144
- payload = update_request.model_dump(exclude_none=True);
160
+ kb.prioritize_built_in_index = True
161
+ payload = kb.model_dump(exclude_none=True);
145
162
  payload.pop('documents');
146
163
 
147
164
  self.get_client().update_with_documents(knowledge_base_id, payload=payload, files=files)
148
165
  else:
149
- if update_request.conversational_search_tool and update_request.conversational_search_tool.index_config:
150
- update_request.prioritize_built_in_index = False
151
- self.get_client().update(knowledge_base_id, update_request.model_dump(exclude_none=True))
166
+ if kb.conversational_search_tool and kb.conversational_search_tool.index_config:
167
+ kb.prioritize_built_in_index = False
168
+ self.get_client().update(knowledge_base_id, kb.model_dump(exclude_none=True))
152
169
 
153
- logEnding = f"with ID '{id}'" if id else f"'{name}'"
154
- logger.info(f"Successfully updated knowledge base {logEnding}")
170
+ logger.info(f"Knowledge base '{kb.name}' updated successfully")
155
171
 
156
172
 
157
173
  def knowledge_base_status( self, id: str, name: str) -> None:
@@ -149,7 +149,7 @@ def models_policy_add(
149
149
  retry_attempts: Annotated[
150
150
  int,
151
151
  typer.Option('--retry-attempts', help='The number of attempts to retry'),
152
- ],
152
+ ] = None,
153
153
  strategy_on_code: Annotated[
154
154
  List[int],
155
155
  typer.Option('--strategy-on-code', help='The http status to consider invoking the strategy'),
@@ -167,15 +167,12 @@ class ModelsController:
167
167
  logger.error("Error: WATSONX_URL is required in the environment.")
168
168
  sys.exit(1)
169
169
 
170
- if is_cpd_env(models_client.base_url):
171
- virtual_models = []
172
- virtual_model_policies = []
173
- else:
174
- logger.info("Retrieving virtual-model models list...")
175
- virtual_models = models_client.list()
170
+
171
+ logger.info("Retrieving virtual-model models list...")
172
+ virtual_models = models_client.list()
176
173
 
177
- logger.info("Retrieving virtual-policies models list...")
178
- virtual_model_policies = model_policies_client.list()
174
+ logger.info("Retrieving virtual-policies models list...")
175
+ virtual_model_policies = model_policies_client.list()
179
176
 
180
177
  logger.info("Retrieving watsonx.ai models list...")
181
178
  found_models = _get_wxai_foundational_models()