ibm-watsonx-orchestrate 1.8.0b0__py3-none-any.whl → 1.8.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. ibm_watsonx_orchestrate/__init__.py +2 -1
  2. ibm_watsonx_orchestrate/agent_builder/agents/types.py +12 -0
  3. ibm_watsonx_orchestrate/agent_builder/connections/types.py +14 -2
  4. ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +61 -11
  5. ibm_watsonx_orchestrate/agent_builder/tools/types.py +7 -2
  6. ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +3 -3
  7. ibm_watsonx_orchestrate/cli/commands/channels/types.py +15 -2
  8. ibm_watsonx_orchestrate/cli/commands/channels/webchat/channels_webchat_controller.py +7 -7
  9. ibm_watsonx_orchestrate/cli/commands/connections/connections_command.py +14 -6
  10. ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +6 -8
  11. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_controller.py +111 -36
  12. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_server_controller.py +23 -7
  13. ibm_watsonx_orchestrate/cli/commands/environment/types.py +1 -1
  14. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_command.py +102 -37
  15. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_controller.py +20 -2
  16. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +10 -8
  17. ibm_watsonx_orchestrate/cli/commands/models/models_controller.py +5 -8
  18. ibm_watsonx_orchestrate/cli/commands/server/server_command.py +2 -10
  19. ibm_watsonx_orchestrate/client/connections/connections_client.py +5 -30
  20. ibm_watsonx_orchestrate/client/copilot/cpe/copilot_cpe_client.py +2 -1
  21. ibm_watsonx_orchestrate/client/utils.py +22 -20
  22. ibm_watsonx_orchestrate/docker/compose-lite.yml +12 -5
  23. ibm_watsonx_orchestrate/docker/default.env +13 -12
  24. ibm_watsonx_orchestrate/flow_builder/flows/__init__.py +8 -5
  25. ibm_watsonx_orchestrate/flow_builder/flows/flow.py +47 -7
  26. ibm_watsonx_orchestrate/flow_builder/node.py +7 -1
  27. ibm_watsonx_orchestrate/flow_builder/types.py +168 -66
  28. ibm_watsonx_orchestrate/flow_builder/utils.py +0 -1
  29. {ibm_watsonx_orchestrate-1.8.0b0.dist-info → ibm_watsonx_orchestrate-1.8.1.dist-info}/METADATA +2 -4
  30. {ibm_watsonx_orchestrate-1.8.0b0.dist-info → ibm_watsonx_orchestrate-1.8.1.dist-info}/RECORD +33 -34
  31. ibm_watsonx_orchestrate/agent_builder/utils/pydantic_utils.py +0 -149
  32. {ibm_watsonx_orchestrate-1.8.0b0.dist-info → ibm_watsonx_orchestrate-1.8.1.dist-info}/WHEEL +0 -0
  33. {ibm_watsonx_orchestrate-1.8.0b0.dist-info → ibm_watsonx_orchestrate-1.8.1.dist-info}/entry_points.txt +0 -0
  34. {ibm_watsonx_orchestrate-1.8.0b0.dist-info → ibm_watsonx_orchestrate-1.8.1.dist-info}/licenses/LICENSE +0 -0
@@ -11,8 +11,9 @@ from requests import ConnectionError
11
11
  from typing import List
12
12
  from ibm_watsonx_orchestrate.client.base_api_client import ClientAPIException
13
13
  from ibm_watsonx_orchestrate.agent_builder.tools import ToolSpec, ToolPermission, ToolRequestBody, ToolResponseBody
14
- from ibm_watsonx_orchestrate.cli.commands.agents.agents_controller import AgentsController, AgentKind
15
- from ibm_watsonx_orchestrate.agent_builder.agents.types import DEFAULT_LLM
14
+ from ibm_watsonx_orchestrate.cli.commands.agents.agents_controller import AgentsController, AgentKind, SpecVersion
15
+ from ibm_watsonx_orchestrate.agent_builder.agents.types import DEFAULT_LLM, BaseAgentSpec
16
+ from ibm_watsonx_orchestrate.client.agents.agent_client import AgentClient
16
17
  from ibm_watsonx_orchestrate.client.tools.tool_client import ToolClient
17
18
  from ibm_watsonx_orchestrate.client.copilot.cpe.copilot_cpe_client import CPEClient
18
19
  from ibm_watsonx_orchestrate.client.utils import instantiate_client
@@ -20,21 +21,24 @@ from ibm_watsonx_orchestrate.utils.exceptions import BadRequest
20
21
 
21
22
  logger = logging.getLogger(__name__)
22
23
 
24
+
23
25
  def _validate_output_file(output_file: str, dry_run_flag: bool) -> None:
24
26
  if not output_file and not dry_run_flag:
25
- logger.error("Please provide a valid yaml output file. Or use the `--dry-run` flag to output generated agent content to terminal")
27
+ logger.error(
28
+ "Please provide a valid yaml output file. Or use the `--dry-run` flag to output generated agent content to terminal")
26
29
  sys.exit(1)
27
-
30
+
28
31
  if output_file and dry_run_flag:
29
32
  logger.error("Cannot set output file when performing a dry run")
30
33
  sys.exit(1)
31
-
34
+
32
35
  if output_file:
33
36
  _, file_extension = os.path.splitext(output_file)
34
- if file_extension not in {".yaml", ".yml", ".json"}:
37
+ if file_extension not in {".yaml", ".yml", ".json"}:
35
38
  logger.error("Output file must be of type '.yaml', '.yml' or '.json'")
36
39
  sys.exit(1)
37
40
 
41
+
38
42
  def _get_progress_spinner() -> Progress:
39
43
  console = Console()
40
44
  return Progress(
@@ -44,16 +48,22 @@ def _get_progress_spinner() -> Progress:
44
48
  console=console,
45
49
  )
46
50
 
51
+
47
52
  def _get_incomplete_tool_from_name(tool_name: str) -> dict:
48
53
  input_schema = ToolRequestBody(**{"type": "object", "properties": {}})
49
54
  output_schema = ToolResponseBody(**{"description": "None"})
50
- spec = ToolSpec(**{"name": tool_name, "description": tool_name, "permission": ToolPermission.ADMIN, "input_schema": input_schema, "output_schema": output_schema})
55
+ spec = ToolSpec(**{"name": tool_name, "description": tool_name, "permission": ToolPermission.ADMIN,
56
+ "input_schema": input_schema, "output_schema": output_schema})
57
+ return spec.model_dump()
58
+
59
+ def _get_incomplete_agent_from_name(agent_name: str) -> dict:
60
+ spec = BaseAgentSpec(**{"name": agent_name, "description": agent_name, "kind": AgentKind.NATIVE})
51
61
  return spec.model_dump()
52
62
 
53
63
  def _get_tools_from_names(tool_names: List[str]) -> List[dict]:
54
64
  if not len(tool_names):
55
65
  return []
56
-
66
+
57
67
  tool_client = get_tool_client()
58
68
 
59
69
  try:
@@ -61,25 +71,63 @@ def _get_tools_from_names(tool_names: List[str]) -> List[dict]:
61
71
  task = progress.add_task(description="Fetching tools", total=None)
62
72
  tools = tool_client.get_drafts_by_names(tool_names)
63
73
  found_tools = {tool.get("name") for tool in tools}
64
- rich.print("\n")
74
+ progress.remove_task(task)
75
+ progress.refresh()
65
76
  for tool_name in tool_names:
66
77
  if tool_name not in found_tools:
67
- logger.warning(f"Failed to find tool named '{tool_name}'. Falling back to incomplete tool definition. Copilot performance maybe effected.")
78
+ logger.warning(
79
+ f"Failed to find tool named '{tool_name}'. Falling back to incomplete tool definition. Copilot performance maybe effected.")
68
80
  tools.append(_get_incomplete_tool_from_name(tool_name))
69
- progress.remove_task(task)
70
81
  except ConnectionError:
71
- logger.warning(f"Failed to fetch tools from server. For optimal results please start the server and import the relevant tools {', '.join(tool_names)}.")
82
+ logger.warning(
83
+ f"Failed to fetch tools from server. For optimal results please start the server and import the relevant tools {', '.join(tool_names)}.")
72
84
  tools = []
73
85
  for tool_name in tool_names:
74
86
  tools.append(_get_incomplete_tool_from_name(tool_name))
75
87
 
76
88
  return tools
77
89
 
90
+
91
+ def _get_agents_from_names(collaborators_names: List[str]) -> List[dict]:
92
+ if not len(collaborators_names):
93
+ return []
94
+
95
+ native_agents_client = get_native_client()
96
+
97
+ try:
98
+ with _get_progress_spinner() as progress:
99
+ task = progress.add_task(description="Fetching agents", total=None)
100
+ agents = native_agents_client.get_drafts_by_names(collaborators_names)
101
+ found_agents = {tool.get("name") for tool in agents}
102
+ progress.remove_task(task)
103
+ progress.refresh()
104
+ for collaborator_name in collaborators_names:
105
+ if collaborator_name not in found_agents:
106
+ logger.warning(
107
+ f"Failed to find agent named '{collaborator_name}'. Falling back to incomplete agent definition. Copilot performance maybe effected.")
108
+ agents.append(_get_incomplete_agent_from_name(collaborator_name))
109
+ except ConnectionError:
110
+ logger.warning(
111
+ f"Failed to fetch tools from server. For optimal results please start the server and import the relevant tools {', '.join(collaborators_names)}.")
112
+ agents = []
113
+ for collaborator_name in collaborators_names:
114
+ agents.append(_get_incomplete_agent_from_name(collaborator_name))
115
+
116
+ return agents
117
+
78
118
  def get_cpe_client() -> CPEClient:
79
119
  url = os.getenv('CPE_URL', "http://localhost:8081")
80
120
  return instantiate_client(client=CPEClient, url=url)
81
121
 
82
122
 
123
+ def get_tool_client(*args, **kwargs):
124
+ return instantiate_client(ToolClient)
125
+
126
+
127
+ def get_native_client(*args, **kwargs):
128
+ return instantiate_client(AgentClient)
129
+
130
+
83
131
  def gather_utterances(max: int) -> list[str]:
84
132
  utterances = []
85
133
  logger.info("Please provide 3 sample utterances you expect your agent to handle:")
@@ -98,7 +146,16 @@ def gather_utterances(max: int) -> list[str]:
98
146
 
99
147
  return utterances
100
148
 
101
- def pre_cpe_step(cpe_client, tool_client):
149
+
150
+ def get_deployed_tools_agents():
151
+ all_tools = find_tools_by_description(tool_client=get_tool_client(), description=None)
152
+ # TODO: this brings only the "native" agents. Can external and assistant agents also be collaborators?
153
+ all_agents = find_agents(agent_client=get_native_client())
154
+ return {"tools": all_tools, "agents": all_agents}
155
+
156
+
157
+ def pre_cpe_step(cpe_client):
158
+ tools_agents = get_deployed_tools_agents()
102
159
  user_message = ""
103
160
  with _get_progress_spinner() as progress:
104
161
  task = progress.add_task(description="Initilizing Prompt Engine", total=None)
@@ -113,31 +170,47 @@ def pre_cpe_step(cpe_client, tool_client):
113
170
  message_content = {"user_message": user_message}
114
171
  elif "description" in response and response["description"]:
115
172
  res["description"] = response["description"]
116
- tools = find_tools_by_description(res["description"], tool_client)
117
- message_content = {"tools": tools}
173
+ message_content = tools_agents
118
174
  elif "metadata" in response:
119
175
  res["agent_name"] = response["metadata"]["agent_name"]
120
176
  res["agent_style"] = response["metadata"]["style"]
121
- res["tools"] = [t for t in tools if t["name"] in response["metadata"]["tools"]]
177
+ res["tools"] = [t for t in tools_agents["tools"] if t["name"] in response["metadata"]["tools"]]
178
+ res["collaborators"] = [a for a in tools_agents["agents"] if
179
+ a["name"] in response["metadata"]["collaborators"]]
122
180
  return res
123
181
  with _get_progress_spinner() as progress:
124
182
  task = progress.add_task(description="Thinking...", total=None)
125
183
  response = cpe_client.submit_pre_cpe_chat(**message_content)
126
184
  progress.remove_task(task)
127
185
 
128
- # TODO: Add description RAG search
186
+
129
187
  def find_tools_by_description(description, tool_client):
130
188
  with _get_progress_spinner() as progress:
131
189
  task = progress.add_task(description="Fetching Tools", total=None)
132
190
  try:
133
191
  tools = tool_client.get()
192
+ progress.remove_task(task)
134
193
  except ConnectionError:
135
194
  tools = []
136
- rich.print("\n")
195
+ progress.remove_task(task)
196
+ progress.refresh()
137
197
  logger.warning("Failed to contact wxo server to fetch tools. Proceeding with empty tool list")
138
- progress.remove_task(task)
139
198
  return tools
140
199
 
200
+ def find_agents(agent_client):
201
+ with _get_progress_spinner() as progress:
202
+ task = progress.add_task(description="Fetching Agents", total=None)
203
+ try:
204
+ agents = agent_client.get()
205
+ progress.remove_task(task)
206
+ except ConnectionError:
207
+ agents = []
208
+ progress.remove_task(task)
209
+ progress.refresh()
210
+ logger.warning("Failed to contact wxo server to fetch agents. Proceeding with empty agent list")
211
+ return agents
212
+
213
+
141
214
  def gather_examples(samples_file=None):
142
215
  if samples_file:
143
216
  if samples_file.endswith('.txt'):
@@ -167,7 +240,7 @@ def talk_to_cpe(cpe_client, samples_file=None, context_data=None):
167
240
  examples = gather_examples(samples_file)
168
241
  # upload or gather input examples
169
242
  context_data['examples'] = examples
170
- response=None
243
+ response = None
171
244
  with _get_progress_spinner() as progress:
172
245
  task = progress.add_task(description="Thinking...", total=None)
173
246
  response = cpe_client.init_with_context(context_data=context_data)
@@ -199,20 +272,23 @@ def prompt_tune(agent_spec: str, output_file: str | None, samples_file: str | No
199
272
 
200
273
  if not output_file and not dry_run_flag:
201
274
  output_file = agent_spec
202
-
275
+
203
276
  _validate_output_file(output_file, dry_run_flag)
204
277
 
205
278
  client = get_cpe_client()
206
279
 
207
280
  instr = agent.instructions
208
- prompt = 'My current prompt is:\n' + instr if instr else "I don't have an initial prompt."
209
281
 
210
282
  tools = _get_tools_from_names(agent.tools)
211
283
 
284
+ collaborators = _get_agents_from_names(agent.collaborators)
212
285
  try:
213
- new_prompt = talk_to_cpe(cpe_client=client, samples_file=samples_file, context_data={"prompt": prompt, 'tools': tools, 'description': agent.description})
286
+ new_prompt = talk_to_cpe(cpe_client=client, samples_file=samples_file,
287
+ context_data={"initial_instruction": instr, 'tools': tools, 'description': agent.description,
288
+ "collaborators": collaborators})
214
289
  except ConnectionError:
215
- logger.error("Failed to connect to Copilot server. Please ensure Copilot is running via `orchestrate copilot start`")
290
+ logger.error(
291
+ "Failed to connect to Copilot server. Please ensure Copilot is running via `orchestrate copilot start`")
216
292
  sys.exit(1)
217
293
  except ClientAPIException:
218
294
  logger.error("An unexpected server error has occur with in the Copilot server. Please check the logs via `orchestrate server logs`")
@@ -223,59 +299,58 @@ def prompt_tune(agent_spec: str, output_file: str | None, samples_file: str | No
223
299
  agent.instructions = new_prompt
224
300
 
225
301
  if dry_run_flag:
226
- rich.print(agent.model_dump())
302
+ rich.print(agent.model_dump(exclude_none=True))
227
303
  else:
228
304
  if os.path.dirname(output_file):
229
305
  os.makedirs(os.path.dirname(output_file), exist_ok=True)
230
306
  AgentsController.persist_record(agent, output_file=output_file)
231
307
 
232
308
 
233
- def get_tool_client(*args, **kwargs):
234
- return instantiate_client(ToolClient)
235
-
236
309
  def create_agent(output_file: str, llm: str, samples_file: str | None, dry_run_flag: bool = False) -> None:
237
310
  _validate_output_file(output_file, dry_run_flag)
238
311
  # 1. prepare the clients
239
312
  cpe_client = get_cpe_client()
240
- tool_client = get_tool_client()
241
313
 
242
314
  # 2. Pre-CPE stage:
243
315
  try:
244
- res = pre_cpe_step(cpe_client, tool_client)
316
+ res = pre_cpe_step(cpe_client)
245
317
  except ConnectionError:
246
- logger.error("Failed to connect to Copilot server. Please ensure Copilot is running via `orchestrate copilot start`")
318
+ logger.error(
319
+ "Failed to connect to Copilot server. Please ensure Copilot is running via `orchestrate copilot start`")
247
320
  sys.exit(1)
248
321
  except ClientAPIException:
249
322
  logger.error("An unexpected server error has occur with in the Copilot server. Please check the logs via `orchestrate server logs`")
250
323
  sys.exit(1)
251
324
 
252
325
  tools = res["tools"]
326
+ collaborators = res["collaborators"]
253
327
  description = res["description"]
254
328
  agent_name = res["agent_name"]
255
329
  agent_style = res["agent_style"]
256
330
 
257
331
  # 4. discuss the instructions
258
- instructions = talk_to_cpe(cpe_client, samples_file, {'tools': tools, 'description': description})
332
+ instructions = talk_to_cpe(cpe_client, samples_file, {'description': description, 'tools': tools, 'collaborators': collaborators})
259
333
 
260
334
  # 6. create and save the agent
261
335
  llm = llm if llm else DEFAULT_LLM
262
336
  params = {
263
337
  'style': agent_style,
264
338
  'tools': [t['name'] for t in tools],
265
- 'llm': llm
339
+ 'llm': llm,
340
+ 'collaborators': [c['name'] for c in collaborators]
266
341
  }
267
342
  agent = AgentsController.generate_agent_spec(agent_name, AgentKind.NATIVE, description, **params)
268
343
  agent.instructions = instructions
344
+ agent.spec_version = SpecVersion.V1
269
345
 
270
346
  if dry_run_flag:
271
- rich.print(agent.model_dump())
347
+ rich.print(agent.model_dump(exclude_none=True))
272
348
  return
273
349
 
274
350
  if os.path.dirname(output_file):
275
351
  os.makedirs(os.path.dirname(output_file), exist_ok=True)
276
352
  AgentsController.persist_record(agent, output_file=output_file)
277
353
 
278
-
279
354
  message_lines = [
280
355
  "Your agent building session finished successfully!",
281
356
  f"Agent YAML saved in file:",
@@ -290,4 +365,4 @@ def create_agent(output_file: str, llm: str, samples_file: str | None, dry_run_f
290
365
  rich.print("╔" + "═" * frame_width + "╗")
291
366
  for line in message_lines:
292
367
  rich.print("║ " + line.ljust(max_length) + " ║")
293
- rich.print("╚" + "═" * frame_width + "╝")
368
+ rich.print("╚" + "═" * frame_width + "╝")
@@ -4,6 +4,7 @@ from pathlib import Path
4
4
  import subprocess
5
5
  import time
6
6
  import requests
7
+ from urllib.parse import urlparse
7
8
  from ibm_watsonx_orchestrate.cli.commands.server.server_command import (
8
9
  get_compose_file,
9
10
  ensure_docker_compose_installed,
@@ -16,15 +17,11 @@ from ibm_watsonx_orchestrate.cli.commands.server.server_command import (
16
17
  get_default_registry_env_vars_by_dev_edition_source,
17
18
  docker_login_by_dev_edition_source,
18
19
  write_merged_env_file,
20
+ apply_server_env_dict_defaults
19
21
  )
20
22
 
21
23
  logger = logging.getLogger(__name__)
22
24
 
23
- def _verify_env_contents(env: dict) -> None:
24
- if not env.get("WATSONX_APIKEY") or not env.get("WATSONX_SPACE_ID"):
25
- logger.error("The Copilot feature requires wx.ai credentials to passed through the provided env file. Please set 'WATSONX_SPACE_ID' and 'WATSONX_APIKEY'")
26
- sys.exit(1)
27
-
28
25
  def wait_for_wxo_cpe_health_check(timeout_seconds=45, interval_seconds=2):
29
26
  url = "http://localhost:8081/version"
30
27
  logger.info("Waiting for Copilot component to be initialized...")
@@ -42,6 +39,24 @@ def wait_for_wxo_cpe_health_check(timeout_seconds=45, interval_seconds=2):
42
39
  time.sleep(interval_seconds)
43
40
  return False
44
41
 
42
+ def _trim_authorization_urls(env_dict: dict) -> dict:
43
+ auth_url_key = "AUTHORIZATION_URL"
44
+ env_dict_copy = env_dict.copy()
45
+
46
+ auth_url = env_dict_copy.get(auth_url_key)
47
+ if not auth_url:
48
+ return env_dict_copy
49
+
50
+
51
+ parsed_url = urlparse(auth_url)
52
+ new_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
53
+ env_dict_copy[auth_url_key] = new_url
54
+
55
+ return env_dict_copy
56
+
57
+
58
+
59
+
45
60
  def run_compose_lite_cpe(user_env_file: Path) -> bool:
46
61
  compose_path = get_compose_file()
47
62
  compose_command = ensure_docker_compose_installed()
@@ -66,8 +81,9 @@ def run_compose_lite_cpe(user_env_file: Path) -> bool:
66
81
  **default_env,
67
82
  **user_env,
68
83
  }
69
-
70
- _verify_env_contents(merged_env_dict)
84
+
85
+ merged_env_dict = apply_server_env_dict_defaults(merged_env_dict)
86
+ merged_env_dict = _trim_authorization_urls(merged_env_dict)
71
87
 
72
88
  try:
73
89
  docker_login_by_dev_edition_source(merged_env_dict, dev_edition_source)
@@ -9,4 +9,4 @@ class EnvironmentAuthType(str, Enum):
9
9
  CPD = 'cpd'
10
10
 
11
11
  def __str__(self):
12
- return self.value
12
+ return self.value
@@ -2,11 +2,12 @@ import json
2
2
  import logging
3
3
  import typer
4
4
  import os
5
- import yaml
6
5
  import csv
7
6
  import rich
8
7
  import sys
9
8
  import shutil
9
+ import tempfile
10
+ import random
10
11
 
11
12
  from rich.panel import Panel
12
13
  from pathlib import Path
@@ -16,23 +17,55 @@ from typing_extensions import Annotated
16
17
 
17
18
  from ibm_watsonx_orchestrate import __version__
18
19
  from ibm_watsonx_orchestrate.cli.commands.evaluations.evaluations_controller import EvaluationsController
20
+ from ibm_watsonx_orchestrate.cli.commands.agents.agents_controller import AgentsController
19
21
 
20
22
  logger = logging.getLogger(__name__)
21
23
 
22
24
  evaluation_app = typer.Typer(no_args_is_help=True)
23
25
 
26
+ def _native_agent_template():
27
+ return {
28
+ "spec_version": "v1",
29
+ "style": "default",
30
+ "llm": "watsonx/meta-llama/llama-3-405b-instruct",
31
+ "name": "",
32
+ "description": "Native agent for validating external agent",
33
+ "instructions": "Use the tools and external agent(s) provided to answer the user's question. If you do not have enough information to answer the question, say so. If you need more information, ask follow up questions.",
34
+ "collaborators": []
35
+ }
36
+
37
+ def _random_native_agent_name(external_agent_name):
38
+ """ Generate a native agent name in the following format to ensure uniqueness:
39
+
40
+ "external_agent_validation_{external_agent_name}_{random number}
41
+
42
+ So if the external agent name is, "QA_Agent", and the random number generated is, '100', the native agent name is:
43
+ "external_agent_validation_QA_Agent_100"
44
+
45
+ """
46
+ seed = 42
47
+ random.seed(seed)
48
+
49
+ return f"external_agent_validation_{external_agent_name}_{random.randint(0, 100)}"
50
+
24
51
  def read_env_file(env_path: Path|str) -> dict:
25
52
  return dotenv_values(str(env_path))
26
53
 
27
54
  def validate_watsonx_credentials(user_env_file: str) -> bool:
28
- required_keys = ["WATSONX_SPACE_ID", "WATSONX_APIKEY"]
55
+ required_sets = [
56
+ ["WATSONX_SPACE_ID", "WATSONX_APIKEY"],
57
+ ["WO_INSTANCE", "WO_API_KEY"]
58
+ ]
29
59
 
30
- if all(key in os.environ for key in required_keys):
60
+ def has_valid_keys(env: dict) -> bool:
61
+ return any(all(key in env for key in key_set) for key_set in required_sets)
62
+
63
+ if has_valid_keys(os.environ):
31
64
  logger.info("WatsonX credentials validated successfully.")
32
65
  return
33
66
 
34
67
  if user_env_file is None:
35
- 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.")
68
+ 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.")
36
69
  sys.exit(1)
37
70
 
38
71
  if not Path(user_env_file).exists():
@@ -41,11 +74,15 @@ def validate_watsonx_credentials(user_env_file: str) -> bool:
41
74
 
42
75
  user_env = read_env_file(user_env_file)
43
76
 
44
- if not all(key in user_env for key in required_keys):
45
- logger.error("Error: The environment file does not contain the required keys: WATSONX_SPACE_ID and WATSONX_APIKEY.")
77
+ if not has_valid_keys(user_env):
78
+ 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.")
46
79
  sys.exit(1)
47
80
 
48
- os.environ.update({key: user_env[key] for key in required_keys})
81
+ # Update os.environ with whichever set is present
82
+ for key_set in required_sets:
83
+ if all(key in user_env for key in key_set):
84
+ os.environ.update({key: user_env[key] for key in key_set})
85
+ break
49
86
  logger.info("WatsonX credentials validated successfully.")
50
87
 
51
88
  def read_csv(data_path: str, delimiter="\t"):
@@ -208,7 +245,7 @@ def validate_external(
208
245
  str,
209
246
  typer.Option(
210
247
  "--external-agent-config", "-ext",
211
- help="Path to the external agent yaml",
248
+ help="Path to the external agent json file",
212
249
 
213
250
  )
214
251
  ],
@@ -234,33 +271,65 @@ def validate_external(
234
271
  help="Path to a .env file that overrides default.env. Then environment variables override both."
235
272
  ),
236
273
  ] = None,
237
- agent_name: Annotated[
238
- str,
274
+ perf_test: Annotated[
275
+ bool,
239
276
  typer.Option(
240
- "--agent_name", "-a",
241
- 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)." \
242
- " If this parameter is pased, validation of the external agent is not run.",
243
- rich_help_panel="Parameters for Input Evaluation"
277
+ "--perf", "-p",
278
+ help="Performance test your external agent against the provide user stories.",
279
+ rich_help_panel="Parameters for Input Evaluation",
244
280
  )
245
- ] = None
281
+ ] = False
246
282
  ):
247
283
 
248
284
  validate_watsonx_credentials(user_env_file)
249
- Path(output_dir).mkdir(exist_ok=True)
250
- shutil.copy(data_path, os.path.join(output_dir, "input_sample.tsv"))
251
285
 
252
- if agent_name is not None:
253
- eval_dir = os.path.join(output_dir, "evaluation")
286
+ with open(external_agent_config, 'r') as f:
287
+ try:
288
+ external_agent_config = json.load(f)
289
+ except Exception:
290
+ rich.print(
291
+ f"[red]: Please provide a valid external agent spec in JSON format. See 'examples/evaluations/external_agent_validation/sample_external_agent_config.json' for an example."
292
+ )
293
+ sys.exit(1)
294
+
295
+ eval_dir = os.path.join(output_dir, "evaluations")
296
+ if perf_test:
254
297
  if os.path.exists(eval_dir):
255
298
  rich.print(f"[yellow]: found existing {eval_dir} in target directory. All content is removed.")
256
- shutil.rmtree(os.path.join(output_dir, "evaluation"))
257
- Path(eval_dir).mkdir(exist_ok=True)
299
+ shutil.rmtree(eval_dir)
300
+ Path(eval_dir).mkdir(exist_ok=True, parents=True)
258
301
  # save external agent config even though its not used for evaluation
259
302
  # it can help in later debugging customer agents
260
- with open(os.path.join(eval_dir, "external_agent_cfg.yaml"), "w+") as f:
261
- with open(external_agent_config, "r") as cfg:
262
- external_agent_config = yaml.safe_load(cfg)
263
- yaml.safe_dump(external_agent_config, f, indent=4)
303
+ with open(os.path.join(eval_dir, f"external_agent_cfg.json"), "w+") as f:
304
+ json.dump(external_agent_config, f, indent=4)
305
+
306
+ logger.info("Registering External Agent")
307
+ agent_controller = AgentsController()
308
+
309
+ external_agent_config["title"] = external_agent_config["name"]
310
+ external_agent_config["auth_config"] = {"token": credential}
311
+ external_agent_config["spec_version"] = external_agent_config.get("spec_version", "v1")
312
+ external_agent_config["provider"] = "external_chat"
313
+
314
+ with tempfile.NamedTemporaryFile(mode="w+", encoding="utf-8", suffix=".json", delete=True) as fp:
315
+ json.dump(external_agent_config, fp, indent=4)
316
+ fp.flush()
317
+ agents = agent_controller.import_agent(file=os.path.abspath(fp.name), app_id=None)
318
+ agent_controller.publish_or_update_agents(agents)
319
+
320
+ logger.info("Registering Native Agent")
321
+
322
+ native_agent_template = _native_agent_template()
323
+ agent_name = _random_native_agent_name(external_agent_config["name"])
324
+ rich.print(f"[blue][b]Generated native agent name is: [i]{agent_name}[/i][/b]")
325
+ native_agent_template["name"] = agent_name
326
+ native_agent_template["collaborators"] = [external_agent_config["name"]]
327
+
328
+ with tempfile.NamedTemporaryFile(mode="w+", encoding="utf-8", suffix=".json", delete=True) as fp:
329
+ json.dump(native_agent_template, fp, indent=4)
330
+ fp.flush()
331
+ agents = agent_controller.import_agent(file=os.path.abspath(fp.name), app_id=None)
332
+ agent_controller.publish_or_update_agents(agents)
264
333
 
265
334
  rich.print(f"[gold3]Starting evaluation of inputs in '{data_path}' against '{agent_name}'[/gold3]")
266
335
  performance_test(
@@ -271,8 +340,6 @@ def validate_external(
271
340
  )
272
341
 
273
342
  else:
274
- with open(external_agent_config, "r") as f:
275
- external_agent_config = yaml.safe_load(f)
276
343
  controller = EvaluationsController()
277
344
  test_data = []
278
345
  with open(data_path, "r") as f:
@@ -280,31 +347,29 @@ def validate_external(
280
347
  for line in csv_reader:
281
348
  test_data.append(line[0])
282
349
 
283
- # save validation results in "validation_results" sub-dir
284
- validation_folder = Path(output_dir) / "validation_results"
350
+ # save validation results in "validate_external" sub-dir
351
+ validation_folder = Path(output_dir) / "validate_external"
285
352
  if os.path.exists(validation_folder):
286
353
  rich.print(f"[yellow]: found existing {validation_folder} in target directory. All content is removed.")
287
354
  shutil.rmtree(validation_folder)
288
355
  validation_folder.mkdir(exist_ok=True, parents=True)
356
+ shutil.copy(data_path, os.path.join(validation_folder, "input_sample.tsv"))
289
357
 
290
358
  # validate the inputs in the provided csv file
291
359
  summary = controller.external_validate(external_agent_config, test_data, credential)
292
- with open(validation_folder / "validation_results.json", "w") as f:
293
- json.dump(summary, f, indent=4)
294
-
295
360
  # validate sample block inputs
296
- rich.print("[gold3]Validating external agent to see if it can handle an array of messages.")
361
+ rich.print("[gold3]Validating external agent against an array of messages.")
297
362
  block_input_summary = controller.external_validate(external_agent_config, test_data, credential, add_context=True)
298
- with open(validation_folder / "sample_block_validation_results.json", "w") as f:
299
- json.dump(block_input_summary, f, indent=4)
300
-
363
+
364
+ with open(validation_folder / "validation_results.json", "w") as f:
365
+ json.dump([summary, block_input_summary], f, indent=4)
366
+
301
367
  user_validation_successful = all([item["success"] for item in summary])
302
368
  block_validation_successful = all([item["success"] for item in block_input_summary])
303
369
 
304
370
  if user_validation_successful and block_validation_successful:
305
371
  msg = (
306
372
  f"[green]Validation is successful. The result is saved to '{str(validation_folder)}'.[/green]\n"
307
- "You can add the external agent as a collaborator agent. See: https://developer.watson-orchestrate.ibm.com/agents/build_agent#native-agents."
308
373
  )
309
374
  else:
310
375
  msg = f"[dark_orange]Schema validation did not succeed. See '{str(validation_folder)}' for failures.[/dark_orange]"
@@ -3,12 +3,12 @@ import os.path
3
3
  from typing import List, Dict, Optional, Tuple
4
4
  import csv
5
5
  from pathlib import Path
6
- import rich
6
+ import sys
7
7
  from wxo_agentic_evaluation import main as evaluate
8
8
  from wxo_agentic_evaluation.tool_planner import build_snapshot
9
9
  from wxo_agentic_evaluation.analyze_run import analyze
10
10
  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
11
+ from wxo_agentic_evaluation.arg_configs import TestConfig, AuthConfig, LLMUserConfig, ChatRecordingConfig, AnalyzeConfig, ProviderConfig
12
12
  from wxo_agentic_evaluation.record_chat import record_chats
13
13
  from wxo_agentic_evaluation.external_agent.external_validate import ExternalAgentValidation
14
14
  from wxo_agentic_evaluation.external_agent.performance_test import ExternalAgentPerformanceTest
@@ -41,12 +41,26 @@ class EvaluationsController:
41
41
  def evaluate(self, config_file: Optional[str] = None, test_paths: Optional[str] = None, output_dir: Optional[str] = None) -> None:
42
42
  url, tenant_name, token = self._get_env_config()
43
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
+
44
54
  config_data = {
45
55
  "wxo_lite_version": __version__,
46
56
  "auth_config": AuthConfig(
47
57
  url=url,
48
58
  tenant_name=tenant_name,
49
59
  token=token
60
+ ),
61
+ "provider_config": ProviderConfig(
62
+ provider=provider,
63
+ model_id="meta-llama/llama-3-405b-instruct",
50
64
  )
51
65
  }
52
66
 
@@ -62,6 +76,10 @@ class EvaluationsController:
62
76
  if "llm_user_config" in file_config:
63
77
  llm_config_data = file_config.pop("llm_user_config")
64
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)
65
83
 
66
84
  config_data.update(file_config)
67
85