ibm-watsonx-orchestrate 1.11.0b1__py3-none-any.whl → 1.12.0b1__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 (50) hide show
  1. ibm_watsonx_orchestrate/__init__.py +1 -2
  2. ibm_watsonx_orchestrate/agent_builder/agents/types.py +22 -5
  3. ibm_watsonx_orchestrate/agent_builder/connections/connections.py +3 -3
  4. ibm_watsonx_orchestrate/agent_builder/connections/types.py +14 -0
  5. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +1 -1
  6. ibm_watsonx_orchestrate/agent_builder/models/types.py +1 -0
  7. ibm_watsonx_orchestrate/agent_builder/toolkits/base_toolkit.py +1 -1
  8. ibm_watsonx_orchestrate/agent_builder/tools/__init__.py +1 -0
  9. ibm_watsonx_orchestrate/agent_builder/tools/base_tool.py +1 -1
  10. ibm_watsonx_orchestrate/agent_builder/tools/langflow_tool.py +124 -0
  11. ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +9 -3
  12. ibm_watsonx_orchestrate/agent_builder/tools/types.py +20 -2
  13. ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +19 -6
  14. ibm_watsonx_orchestrate/cli/commands/connections/connections_command.py +18 -0
  15. ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +114 -0
  16. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_controller.py +2 -6
  17. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_server_controller.py +24 -91
  18. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_command.py +49 -0
  19. ibm_watsonx_orchestrate/cli/commands/models/model_provider_mapper.py +23 -4
  20. ibm_watsonx_orchestrate/cli/commands/models/models_controller.py +3 -3
  21. ibm_watsonx_orchestrate/cli/commands/partners/offering/partners_offering_command.py +56 -0
  22. ibm_watsonx_orchestrate/cli/commands/partners/offering/partners_offering_controller.py +458 -0
  23. ibm_watsonx_orchestrate/cli/commands/partners/offering/types.py +107 -0
  24. ibm_watsonx_orchestrate/cli/commands/partners/partners_command.py +12 -0
  25. ibm_watsonx_orchestrate/cli/commands/partners/partners_controller.py +0 -0
  26. ibm_watsonx_orchestrate/cli/commands/server/server_command.py +124 -637
  27. ibm_watsonx_orchestrate/cli/commands/server/types.py +1 -1
  28. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_command.py +2 -2
  29. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_controller.py +2 -2
  30. ibm_watsonx_orchestrate/cli/commands/tools/tools_command.py +2 -3
  31. ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +206 -43
  32. ibm_watsonx_orchestrate/cli/main.py +2 -0
  33. ibm_watsonx_orchestrate/client/connections/connections_client.py +4 -1
  34. ibm_watsonx_orchestrate/client/tools/tempus_client.py +3 -0
  35. ibm_watsonx_orchestrate/client/tools/tool_client.py +5 -2
  36. ibm_watsonx_orchestrate/client/utils.py +31 -1
  37. ibm_watsonx_orchestrate/docker/compose-lite.yml +68 -17
  38. ibm_watsonx_orchestrate/docker/default.env +21 -18
  39. ibm_watsonx_orchestrate/flow_builder/flows/decorators.py +8 -2
  40. ibm_watsonx_orchestrate/flow_builder/flows/flow.py +31 -7
  41. ibm_watsonx_orchestrate/flow_builder/node.py +1 -1
  42. ibm_watsonx_orchestrate/flow_builder/types.py +18 -3
  43. ibm_watsonx_orchestrate/utils/docker_utils.py +280 -0
  44. ibm_watsonx_orchestrate/utils/environment.py +369 -0
  45. ibm_watsonx_orchestrate/utils/utils.py +1 -1
  46. {ibm_watsonx_orchestrate-1.11.0b1.dist-info → ibm_watsonx_orchestrate-1.12.0b1.dist-info}/METADATA +2 -2
  47. {ibm_watsonx_orchestrate-1.11.0b1.dist-info → ibm_watsonx_orchestrate-1.12.0b1.dist-info}/RECORD +50 -42
  48. {ibm_watsonx_orchestrate-1.11.0b1.dist-info → ibm_watsonx_orchestrate-1.12.0b1.dist-info}/WHEEL +0 -0
  49. {ibm_watsonx_orchestrate-1.11.0b1.dist-info → ibm_watsonx_orchestrate-1.12.0b1.dist-info}/entry_points.txt +0 -0
  50. {ibm_watsonx_orchestrate-1.11.0b1.dist-info → ibm_watsonx_orchestrate-1.12.0b1.dist-info}/licenses/LICENSE +0 -0
@@ -327,11 +327,7 @@ def talk_to_cpe(cpe_client, samples_file=None, context_data=None):
327
327
 
328
328
 
329
329
  def prompt_tune(agent_spec: str, output_file: str | None, samples_file: str | None, dry_run_flag: bool) -> None:
330
- agents = AgentsController.import_agent(file=agent_spec, app_id=None)
331
- if not agents:
332
- logger.error("Invalid agent spec file provided, no agent found.")
333
- sys.exit(1)
334
- agent = agents[0]
330
+ agent = AgentsController.import_agent(file=agent_spec, app_id=None)[0]
335
331
  agent_kind = agent.kind
336
332
 
337
333
  if agent_kind != AgentKind.NATIVE:
@@ -346,7 +342,7 @@ def prompt_tune(agent_spec: str, output_file: str | None, samples_file: str | No
346
342
 
347
343
  client = get_cpe_client()
348
344
 
349
- instr = agent.instructions if agent.instructions else ""
345
+ instr = agent.instructions
350
346
 
351
347
  tools = _get_tools_from_names(agent.tools)
352
348
 
@@ -1,24 +1,13 @@
1
1
  import logging
2
2
  import sys
3
3
  from pathlib import Path
4
- import subprocess
5
4
  import time
6
5
  import requests
7
6
  from urllib.parse import urlparse
8
- from ibm_watsonx_orchestrate.cli.commands.server.server_command import (
9
- get_compose_file,
10
- ensure_docker_compose_installed,
11
- _prepare_clean_env,
12
- ensure_docker_installed,
13
- read_env_file,
14
- get_default_env_file,
15
- get_persisted_user_env,
16
- get_dev_edition_source,
17
- get_default_registry_env_vars_by_dev_edition_source,
18
- docker_login_by_dev_edition_source,
19
- write_merged_env_file,
20
- apply_server_env_dict_defaults
21
- )
7
+
8
+ from ibm_watsonx_orchestrate.cli.config import Config
9
+ from ibm_watsonx_orchestrate.utils.docker_utils import DockerLoginService, DockerComposeCore, DockerUtils
10
+ from ibm_watsonx_orchestrate.utils.environment import EnvService
22
11
 
23
12
  logger = logging.getLogger(__name__)
24
13
 
@@ -39,71 +28,26 @@ def wait_for_wxo_cpe_health_check(timeout_seconds=45, interval_seconds=2):
39
28
  time.sleep(interval_seconds)
40
29
  return False
41
30
 
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
-
60
31
  def run_compose_lite_cpe(user_env_file: Path) -> bool:
61
- compose_path = get_compose_file()
62
- compose_command = ensure_docker_compose_installed()
63
- _prepare_clean_env(user_env_file)
64
- ensure_docker_installed()
65
-
66
- default_env = read_env_file(get_default_env_file())
67
- user_env = read_env_file(user_env_file) if user_env_file else {}
68
- if not user_env:
69
- user_env = get_persisted_user_env() or {}
70
-
71
- dev_edition_source = get_dev_edition_source(user_env)
72
- default_registry_vars = get_default_registry_env_vars_by_dev_edition_source(default_env, user_env, source=dev_edition_source)
73
-
74
- # Update the default environment with the default registry variables only if they are not already set
75
- for key in default_registry_vars:
76
- if key not in default_env or not default_env[key]:
77
- default_env[key] = default_registry_vars[key]
78
-
79
- # Merge the default environment with the user environment
80
- merged_env_dict = {
81
- **default_env,
82
- **user_env,
83
- }
84
-
85
- merged_env_dict = apply_server_env_dict_defaults(merged_env_dict)
86
- merged_env_dict = _trim_authorization_urls(merged_env_dict)
32
+ DockerUtils.ensure_docker_installed()
33
+
34
+ cli_config = Config()
35
+ env_service = EnvService(cli_config)
36
+ env_service.prepare_clean_env(user_env_file)
37
+ user_env = env_service.get_user_env(user_env_file)
38
+ merged_env_dict = env_service.prepare_server_env_vars(user_env=user_env, should_drop_auth_routes=True)
87
39
 
88
40
  try:
89
- docker_login_by_dev_edition_source(merged_env_dict, dev_edition_source)
41
+ DockerLoginService(env_service=env_service).login_by_dev_edition_source(merged_env_dict)
90
42
  except ValueError as ignored:
91
43
  # do nothing, as the docker login here is not mandatory
92
44
  pass
93
45
 
94
- final_env_file = write_merged_env_file(merged_env_dict)
46
+ final_env_file = env_service.write_merged_env_file(merged_env_dict)
95
47
 
96
- command = compose_command + [
97
- "-f", str(compose_path),
98
- "--env-file", str(final_env_file),
99
- "up",
100
- "cpe",
101
- "-d",
102
- "--remove-orphans"
103
- ]
48
+ compose_core = DockerComposeCore(env_service)
104
49
 
105
- logger.info(f"Starting docker-compose Copilot service...")
106
- result = subprocess.run(command, capture_output=False)
50
+ result = compose_core.service_up(service_name="cpe", friendly_name="Copilot", final_env_file=final_env_file)
107
51
 
108
52
  if result.returncode == 0:
109
53
  logger.info("Copilot Service started successfully.")
@@ -124,27 +68,16 @@ def run_compose_lite_cpe(user_env_file: Path) -> bool:
124
68
  return True
125
69
 
126
70
  def run_compose_lite_cpe_down(is_reset: bool = False) -> None:
127
- compose_path = get_compose_file()
128
- compose_command = ensure_docker_compose_installed()
129
- ensure_docker_installed()
130
-
131
- default_env = read_env_file(get_default_env_file())
132
- final_env_file = write_merged_env_file(default_env)
133
-
134
- command = compose_command + [
135
- "-f", str(compose_path),
136
- "--env-file", final_env_file,
137
- "down",
138
- "cpe"
139
- ]
140
-
141
- if is_reset:
142
- command.append("--volumes")
143
- logger.info("Stopping docker-compose Copilot service and resetting volumes...")
144
- else:
145
- logger.info("Stopping docker-compose Copilot service...")
71
+ DockerUtils.ensure_docker_installed()
72
+
73
+ default_env = EnvService.read_env_file(EnvService.get_default_env_file())
74
+ final_env_file = EnvService.write_merged_env_file(default_env)
75
+
76
+ cli_config = Config()
77
+ env_service = EnvService(cli_config)
78
+ compose_core = DockerComposeCore(env_service)
146
79
 
147
- result = subprocess.run(command, capture_output=False)
80
+ result = compose_core.service_down(service_name="cpe", friendly_name="Copilot", final_env_file=final_env_file, is_reset=is_reset)
148
81
 
149
82
  if result.returncode == 0:
150
83
  logger.info("Copilot service stopped successfully.")
@@ -386,6 +386,55 @@ def validate_external(
386
386
 
387
387
  rich.print(Panel(msg))
388
388
 
389
+ @evaluation_app.command(name="validate-native", help="Validate native agents against a set of inputs")
390
+ def validate_native(
391
+ data_path: Annotated[
392
+ str,
393
+ typer.Option(
394
+ "--tsv", "-t",
395
+ help="Path to .tsv file of inputs. The first column of the TSV is the user story, the second column is the expected final output from the agent, and the third column is the name of the agent"
396
+ )
397
+ ],
398
+ output_dir: Annotated[
399
+ str,
400
+ typer.Option(
401
+ "--output", "-o",
402
+ help="where to save the validation results"
403
+ )
404
+ ] = "./test_native_agent",
405
+ user_env_file: Annotated[
406
+ Optional[str],
407
+ typer.Option(
408
+ "--env-file", "-e",
409
+ help="Path to a .env file that overrides default.env. Then environment variables override both."
410
+ ),
411
+ ] = None,
412
+ ):
413
+ validate_watsonx_credentials(user_env_file)
414
+
415
+ eval_dir = os.path.join(output_dir, "native_agent_evaluations")
416
+ if os.path.exists(eval_dir):
417
+ rich.print(f"[yellow]: found existing {eval_dir} in target directory. All content is removed.")
418
+ shutil.rmtree(eval_dir)
419
+ Path(eval_dir).mkdir(exist_ok=True, parents=True)
420
+
421
+ test_data_path = Path(eval_dir) / "generated_test_data"
422
+ test_data_path.mkdir(exist_ok=True, parents=True)
423
+
424
+ controller = EvaluationsController()
425
+ test_data = read_csv(data_path) # tab seperated file containing the user story, the final outcome, and the agent name
426
+
427
+ for idx, row in enumerate(test_data):
428
+ agent_name = row[2] # agent name
429
+ dataset = [row[0:2]] # user story, expected final outcome
430
+ generated_test_data = controller.generate_performance_test(agent_name=agent_name, test_data=dataset)
431
+ for test_data in generated_test_data:
432
+ test_name = f"native_agent_evaluation_test_{idx}.json"
433
+ with open(test_data_path / test_name, encoding="utf-8", mode="w+") as f:
434
+ json.dump(test_data, f, indent=4)
435
+
436
+ evaluate(output_dir=eval_dir, test_paths=str(test_data_path))
437
+
389
438
  @evaluation_app.command(name="quick-eval",
390
439
  short_help="Evaluate agent against a suite of static metrics and LLM-as-a-judge metrics",
391
440
  help="""
@@ -101,10 +101,11 @@ PROVIDER_EXTRA_PROPERTIES_LUT = {
101
101
  PROVIDER_REQUIRED_FIELDS = {k:['api_key'] for k in ModelProvider}
102
102
  # Update required fields for each provider
103
103
  # Use sets to denote when a requirement is 'or'
104
+ # Tuples denote combined requirements like 'and'
104
105
  PROVIDER_REQUIRED_FIELDS.update({
105
106
  ModelProvider.WATSONX: PROVIDER_REQUIRED_FIELDS[ModelProvider.WATSONX] + [{'watsonx_space_id', 'watsonx_project_id', 'watsonx_deployment_id'}],
106
107
  ModelProvider.OLLAMA: PROVIDER_REQUIRED_FIELDS[ModelProvider.OLLAMA] + ['custom_host'],
107
- ModelProvider.BEDROCK: [],
108
+ ModelProvider.BEDROCK: [{'api_key', ('aws_secret_access_key', 'aws_access_key_id')}],
108
109
  })
109
110
 
110
111
  # def env_file_to_model_ProviderConfig(model_name: str, env_file_path: str) -> ProviderConfig | None:
@@ -158,16 +159,34 @@ def _validate_extra_fields(provider: ModelProvider, cfg: ProviderConfig) -> None
158
159
  if cfg.__dict__.get(attr) is not None and attr not in accepted_fields:
159
160
  logger.warning(f"The config option '{attr}' is not used by provider '{provider}'")
160
161
 
162
+ def _check_credential_provided(cred: str | tuple, provided_creds: set) -> bool:
163
+ if isinstance(cred, tuple):
164
+ return all(item in provided_creds for item in cred)
165
+ else:
166
+ return cred in provided_creds
167
+
168
+ def _format_missing_credential(missing_credential: set) -> str:
169
+ parts = []
170
+ for cred in missing_credential:
171
+ if isinstance(cred, tuple):
172
+ formatted = " and ".join(f"{x}" for x in cred)
173
+ parts.append(f"({formatted})")
174
+ else:
175
+ parts.append(f"{cred}")
176
+
177
+ return " or ".join(parts)
178
+
179
+
161
180
  def _validate_requirements(provider: ModelProvider, cfg: ProviderConfig, app_id: str = None) -> None:
162
181
  provided_credentials = set([k for k,v in dict(cfg).items() if v is not None])
163
182
  required_creds = PROVIDER_REQUIRED_FIELDS[provider]
164
183
  missing_credentials = []
165
184
  for cred in required_creds:
166
185
  if isinstance(cred, set):
167
- if not any(c in provided_credentials for c in cred):
186
+ if not any(_check_credential_provided(c, provided_credentials) for c in cred):
168
187
  missing_credentials.append(cred)
169
188
  else:
170
- if cred not in provided_credentials:
189
+ if not _check_credential_provided(cred, provided_credentials):
171
190
  missing_credentials.append(cred)
172
191
 
173
192
  if len(missing_credentials) > 0:
@@ -177,7 +196,7 @@ def _validate_requirements(provider: ModelProvider, cfg: ProviderConfig, app_id:
177
196
  missing_credentials_string = f"Be sure to include the following required fields for provider '{provider}' in the connection '{app_id}':"
178
197
  for cred in missing_credentials:
179
198
  if isinstance(cred, set):
180
- cred_str = ' or '.join(list(cred))
199
+ cred_str = _format_missing_credential(cred)
181
200
  else:
182
201
  cred_str = cred
183
202
  missing_credentials_string += f"\n\t - {cred_str}"
@@ -12,7 +12,6 @@ import requests
12
12
  import rich
13
13
  import rich.highlighter
14
14
 
15
- from ibm_watsonx_orchestrate.cli.commands.server.server_command import get_default_env_file, merge_env
16
15
  from ibm_watsonx_orchestrate.client.model_policies.model_policies_client import ModelPoliciesClient
17
16
  from ibm_watsonx_orchestrate.agent_builder.model_policies.types import ModelPolicy, ModelPolicyInner, \
18
17
  ModelPolicyRetry, ModelPolicyStrategy, ModelPolicyStrategyMode, ModelPolicyTarget
@@ -20,6 +19,7 @@ from ibm_watsonx_orchestrate.client.models.models_client import ModelsClient
20
19
  from ibm_watsonx_orchestrate.agent_builder.models.types import VirtualModel, ProviderConfig, ModelType, ANTHROPIC_DEFAULT_MAX_TOKENS
21
20
  from ibm_watsonx_orchestrate.client.utils import instantiate_client, is_cpd_env
22
21
  from ibm_watsonx_orchestrate.client.connections import get_connection_id, ConnectionType
22
+ from ibm_watsonx_orchestrate.utils.environment import EnvService
23
23
 
24
24
  logger = logging.getLogger(__name__)
25
25
 
@@ -153,8 +153,8 @@ class ModelsController:
153
153
  models_client: ModelsClient = self.get_models_client()
154
154
  model_policies_client: ModelPoliciesClient = self.get_model_policies_client()
155
155
  global WATSONX_URL
156
- default_env_path = get_default_env_file()
157
- merged_env_dict = merge_env(
156
+ default_env_path = EnvService.get_default_env_file()
157
+ merged_env_dict = EnvService.merge_env(
158
158
  default_env_path,
159
159
  None
160
160
  )
@@ -0,0 +1,56 @@
1
+ import typer
2
+ from typing_extensions import Annotated
3
+ from ibm_watsonx_orchestrate.cli.commands.partners.offering.partners_offering_controller import PartnersOfferingController
4
+ from typing_extensions import Annotated
5
+ from ibm_watsonx_orchestrate.cli.commands.partners.offering.types import AgentKind
6
+
7
+
8
+ partners_offering = typer.Typer(no_args_is_help=True)
9
+
10
+ @partners_offering.command(
11
+ name="create",
12
+ help="Export Items from your environment to create an offering"
13
+ )
14
+ def create_offering(
15
+ offering: Annotated[
16
+ str,
17
+ typer.Option("--offering", "-o", help="Name of the offering"),
18
+ ],
19
+ publisher_name: Annotated[
20
+ str,
21
+ typer.Option("--publisher", "-p", help="Publisher name"),
22
+ ],
23
+ type: Annotated[
24
+ AgentKind,
25
+ typer.Option("--type", "-t", help="Type of agent: native|external"),
26
+ ],
27
+ agent_name: Annotated[
28
+ str,
29
+ typer.Option("--agent-name", "-a", help="Agent name to create"),
30
+ ],
31
+ ):
32
+ controller = PartnersOfferingController()
33
+ controller.create(
34
+ offering=offering,
35
+ publisher_name=publisher_name,
36
+ agent_type=type,
37
+ agent_name=agent_name,
38
+ )
39
+
40
+
41
+ @partners_offering.command(
42
+ name="package",
43
+ help="Validate your exported offering and package for upload"
44
+ )
45
+ def package_offering(
46
+ offering: Annotated[
47
+ str,
48
+ typer.Option("--offering", "-o", help="Name of the offering to package"),
49
+ ],
50
+ folder_path: Annotated[
51
+ str,
52
+ typer.Option("--folder", "-f", help="Path to folder containing the specified offering")
53
+ ] = None
54
+ ):
55
+ controller = PartnersOfferingController()
56
+ controller.package(offering=offering, folder_path=folder_path)