ibm-watsonx-orchestrate 1.12.0b1__py3-none-any.whl → 1.13.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.
Files changed (59) hide show
  1. ibm_watsonx_orchestrate/__init__.py +2 -1
  2. ibm_watsonx_orchestrate/agent_builder/connections/types.py +34 -3
  3. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +11 -2
  4. ibm_watsonx_orchestrate/agent_builder/models/types.py +17 -1
  5. ibm_watsonx_orchestrate/agent_builder/toolkits/types.py +14 -2
  6. ibm_watsonx_orchestrate/agent_builder/tools/__init__.py +1 -1
  7. ibm_watsonx_orchestrate/agent_builder/tools/langflow_tool.py +61 -1
  8. ibm_watsonx_orchestrate/agent_builder/tools/types.py +21 -3
  9. ibm_watsonx_orchestrate/agent_builder/voice_configurations/__init__.py +1 -1
  10. ibm_watsonx_orchestrate/agent_builder/voice_configurations/types.py +11 -0
  11. ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +27 -51
  12. ibm_watsonx_orchestrate/cli/commands/connections/connections_command.py +2 -2
  13. ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +54 -28
  14. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_command.py +25 -2
  15. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_controller.py +249 -14
  16. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_server_controller.py +4 -4
  17. ibm_watsonx_orchestrate/cli/commands/environment/environment_command.py +5 -1
  18. ibm_watsonx_orchestrate/cli/commands/environment/environment_controller.py +6 -3
  19. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_command.py +3 -2
  20. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_controller.py +1 -1
  21. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +45 -16
  22. ibm_watsonx_orchestrate/cli/commands/models/models_command.py +2 -2
  23. ibm_watsonx_orchestrate/cli/commands/models/models_controller.py +29 -10
  24. ibm_watsonx_orchestrate/cli/commands/partners/offering/partners_offering_controller.py +21 -4
  25. ibm_watsonx_orchestrate/cli/commands/partners/offering/types.py +7 -15
  26. ibm_watsonx_orchestrate/cli/commands/server/server_command.py +19 -17
  27. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_controller.py +139 -27
  28. ibm_watsonx_orchestrate/cli/commands/tools/tools_command.py +2 -2
  29. ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +79 -36
  30. ibm_watsonx_orchestrate/cli/commands/voice_configurations/voice_configurations_controller.py +23 -11
  31. ibm_watsonx_orchestrate/cli/common.py +26 -0
  32. ibm_watsonx_orchestrate/cli/config.py +33 -2
  33. ibm_watsonx_orchestrate/client/connections/connections_client.py +1 -14
  34. ibm_watsonx_orchestrate/client/copilot/cpe/copilot_cpe_client.py +34 -1
  35. ibm_watsonx_orchestrate/client/knowledge_bases/knowledge_base_client.py +6 -2
  36. ibm_watsonx_orchestrate/client/model_policies/model_policies_client.py +1 -1
  37. ibm_watsonx_orchestrate/client/models/models_client.py +1 -1
  38. ibm_watsonx_orchestrate/client/threads/threads_client.py +34 -0
  39. ibm_watsonx_orchestrate/client/utils.py +29 -7
  40. ibm_watsonx_orchestrate/docker/compose-lite.yml +2 -2
  41. ibm_watsonx_orchestrate/docker/default.env +15 -9
  42. ibm_watsonx_orchestrate/flow_builder/flows/decorators.py +2 -0
  43. ibm_watsonx_orchestrate/flow_builder/flows/flow.py +59 -9
  44. ibm_watsonx_orchestrate/flow_builder/node.py +13 -1
  45. ibm_watsonx_orchestrate/flow_builder/types.py +39 -0
  46. ibm_watsonx_orchestrate/langflow/__init__.py +0 -0
  47. ibm_watsonx_orchestrate/langflow/langflow_utils.py +195 -0
  48. ibm_watsonx_orchestrate/langflow/lfx_deps.py +84 -0
  49. ibm_watsonx_orchestrate/utils/async_helpers.py +31 -0
  50. ibm_watsonx_orchestrate/utils/docker_utils.py +1177 -33
  51. ibm_watsonx_orchestrate/utils/environment.py +165 -20
  52. ibm_watsonx_orchestrate/utils/exceptions.py +1 -1
  53. ibm_watsonx_orchestrate/utils/tokens.py +51 -0
  54. ibm_watsonx_orchestrate/utils/utils.py +63 -4
  55. {ibm_watsonx_orchestrate-1.12.0b1.dist-info → ibm_watsonx_orchestrate-1.13.0b0.dist-info}/METADATA +2 -2
  56. {ibm_watsonx_orchestrate-1.12.0b1.dist-info → ibm_watsonx_orchestrate-1.13.0b0.dist-info}/RECORD +59 -52
  57. {ibm_watsonx_orchestrate-1.12.0b1.dist-info → ibm_watsonx_orchestrate-1.13.0b0.dist-info}/WHEEL +0 -0
  58. {ibm_watsonx_orchestrate-1.12.0b1.dist-info → ibm_watsonx_orchestrate-1.13.0b0.dist-info}/entry_points.txt +0 -0
  59. {ibm_watsonx_orchestrate-1.12.0b1.dist-info → ibm_watsonx_orchestrate-1.13.0b0.dist-info}/licenses/LICENSE +0 -0
@@ -6,7 +6,7 @@ import yaml
6
6
  import importlib
7
7
  import inspect
8
8
  from pathlib import Path
9
- from typing import List
9
+ from typing import List, Optional
10
10
 
11
11
  import requests
12
12
  import rich
@@ -16,10 +16,11 @@ from ibm_watsonx_orchestrate.client.model_policies.model_policies_client import
16
16
  from ibm_watsonx_orchestrate.agent_builder.model_policies.types import ModelPolicy, ModelPolicyInner, \
17
17
  ModelPolicyRetry, ModelPolicyStrategy, ModelPolicyStrategyMode, ModelPolicyTarget
18
18
  from ibm_watsonx_orchestrate.client.models.models_client import ModelsClient
19
- from ibm_watsonx_orchestrate.agent_builder.models.types import VirtualModel, ProviderConfig, ModelType, ANTHROPIC_DEFAULT_MAX_TOKENS
19
+ from ibm_watsonx_orchestrate.agent_builder.models.types import VirtualModel, ProviderConfig, ModelType, ANTHROPIC_DEFAULT_MAX_TOKENS, ModelListEntry
20
20
  from ibm_watsonx_orchestrate.client.utils import instantiate_client, is_cpd_env
21
21
  from ibm_watsonx_orchestrate.client.connections import get_connection_id, ConnectionType
22
22
  from ibm_watsonx_orchestrate.utils.environment import EnvService
23
+ from ibm_watsonx_orchestrate.cli.common import ListFormats, rich_table_to_markdown
23
24
 
24
25
  logger = logging.getLogger(__name__)
25
26
 
@@ -149,7 +150,7 @@ class ModelsController:
149
150
  self.model_policies_client = instantiate_client(ModelPoliciesClient)
150
151
  return self.model_policies_client
151
152
 
152
- def list_models(self, print_raw: bool = False) -> None:
153
+ def list_models(self, print_raw: bool = False, format: Optional[ListFormats] = None) -> List[ModelListEntry] | str |None:
153
154
  models_client: ModelsClient = self.get_models_client()
154
155
  model_policies_client: ModelPoliciesClient = self.get_model_policies_client()
155
156
  global WATSONX_URL
@@ -224,6 +225,7 @@ class ModelsController:
224
225
 
225
226
  console.print("[yellow]★[/yellow] [italic dim]indicates a supported and preferred model[/italic dim]\n[blue dim]✨️[/blue dim] [italic dim]indicates a model from a custom provider[/italic dim]" )
226
227
  else:
228
+ model_details = []
227
229
  table = rich.table.Table(
228
230
  show_header=True,
229
231
  title="[bold]Available Models[/bold]",
@@ -234,15 +236,32 @@ class ModelsController:
234
236
  table.add_column(col)
235
237
 
236
238
  for model in (virtual_models + virtual_model_policies):
237
- table.add_row(f"✨️ {model.name}", model.description or 'No description provided.')
239
+ entry = ModelListEntry(
240
+ name=model.name,
241
+ description=model.description,
242
+ is_custom=True
243
+ )
244
+ model_details.append(entry)
245
+ table.add_row(*entry.get_row_details())
238
246
 
239
247
  for model in sorted_models:
240
- model_id = model.get("model_id", "N/A")
241
- short_desc = model.get("short_description", "No description provided.")
242
- marker = "★ " if any(pref in model_id.lower() for pref in preferred_list) else ""
243
- table.add_row(f"[yellow]{marker}[/yellow]watsonx/{model_id}", short_desc)
244
-
245
- rich.print(table)
248
+ name = model.get("model_id", "N/A")
249
+ entry = ModelListEntry(
250
+ name=name,
251
+ description=model.get("short_description"),
252
+ is_custom=False,
253
+ recommended=any(pref in name.lower() for pref in preferred_list)
254
+ )
255
+ model_details.append(entry)
256
+ table.add_row(*entry.get_row_details())
257
+
258
+ match format:
259
+ case ListFormats.JSON:
260
+ return model_details
261
+ case ListFormats.Table:
262
+ return rich_table_to_markdown(table)
263
+ case _:
264
+ rich.print(table)
246
265
 
247
266
  def import_model(self, file: str, app_id: str | None) -> List[VirtualModel]:
248
267
  from ibm_watsonx_orchestrate.cli.commands.models.model_provider_mapper import validate_ProviderConfig # lazily import this because the lut building is expensive
@@ -23,6 +23,7 @@ from ibm_watsonx_orchestrate.client.connections import get_connections_client
23
23
  from ibm_watsonx_orchestrate.agent_builder.connections.types import ConnectionEnvironment
24
24
  from ibm_watsonx_orchestrate.cli.commands.connections.connections_controller import export_connection
25
25
  from ibm_watsonx_orchestrate.cli.commands.tools.tools_controller import ToolsController
26
+ from ibm_watsonx_orchestrate.utils.utils import sanitize_catalog_label
26
27
  from .types import *
27
28
 
28
29
  APPLICATIONS_FILE_VERSION = '1.16.0'
@@ -54,7 +55,7 @@ def get_tool_bindings(tool_names: list[str]) -> dict[str, dict]:
54
55
 
55
56
  return results
56
57
 
57
- def _patch_agent_yamls(project_root: Path, publisher_name: str):
58
+ def _patch_agent_yamls(project_root: Path, publisher_name: str, parent_agent_name: str):
58
59
  agents_dir = project_root / "agents"
59
60
  if not agents_dir.exists():
60
61
  return
@@ -70,15 +71,18 @@ def _patch_agent_yamls(project_root: Path, publisher_name: str):
70
71
  if "language_support" not in agent_data:
71
72
  agent_data["language_support"] = ["English"]
72
73
  if "icon" not in agent_data:
73
- agent_data["icon"] = "inline-svg-of-icon"
74
+ agent_data["icon"] = AGENT_CATALOG_ONLY_PLACEHOLDERS['icon']
74
75
  if "category" not in agent_data:
75
76
  agent_data["category"] = "agent"
76
77
  if "supported_apps" not in agent_data:
77
78
  agent_data["supported_apps"] = []
79
+ if "agent_role" not in agent_data:
80
+ agent_data["agent_role"] = "manager" if agent_data.get("name") == parent_agent_name else "collaborator"
78
81
 
79
82
  with open(agent_yaml, "w") as f:
80
83
  yaml.safe_dump(agent_data, f, sort_keys=False)
81
84
 
85
+
82
86
  def _create_applications_entry(connection_config: dict) -> dict:
83
87
  return {
84
88
  'app_id': connection_config.get('app_id'),
@@ -116,6 +120,15 @@ class PartnersOfferingController:
116
120
  sys.exit(1)
117
121
 
118
122
  def create(self, offering: str, publisher_name: str, agent_type: str, agent_name: str):
123
+
124
+ # Sanitize offering name
125
+ original_offering = offering
126
+ offering = sanitize_catalog_label(offering)
127
+
128
+ if offering != original_offering:
129
+ logger.warning("Offering name must contain only alpahnumeric characters or underscore")
130
+ logger.info(f"Offering '{original_offering}' has been updated to '{offering}'")
131
+
119
132
  # Create parent project folder
120
133
  project_root = self.root / offering
121
134
 
@@ -179,7 +192,7 @@ class PartnersOfferingController:
179
192
  output_zip.unlink(missing_ok=True)
180
193
 
181
194
  # Patch the agent yamls with publisher, tags, icon, etc.
182
- _patch_agent_yamls(project_root, publisher_name)
195
+ _patch_agent_yamls(project_root=project_root, publisher_name=publisher_name, parent_agent_name=agent_name)
183
196
 
184
197
 
185
198
  # Create offering.yaml file -------------------------------------------------------
@@ -337,12 +350,16 @@ class PartnersOfferingController:
337
350
  **agent_data
338
351
  )
339
352
  agent = Agent.model_validate(agent_details)
340
- AgentsController().persist_record(agent=agent)
341
353
  case AgentKind.EXTERNAL:
342
354
  agent_details = parse_create_external_args(
343
355
  **agent_data
344
356
  )
345
357
  agent = ExternalAgent.model_validate(agent_details)
358
+
359
+ # Placeholder detection
360
+ for label,placeholder in AGENT_CATALOG_ONLY_PLACEHOLDERS.items():
361
+ if agent_data.get(label) == placeholder:
362
+ logger.warning(f"Placeholder '{label}' detected for agent '{agent_name}', please ensure '{label}' is correct before packaging.")
346
363
 
347
364
  agent_json_path = f"{top_level_folder}/agents/{agent_name}/config.json"
348
365
  zf.writestr(agent_json_path, json.dumps(agent_data, indent=2))
@@ -24,6 +24,10 @@ CATALOG_ONLY_FIELDS = [
24
24
  'supported_apps'
25
25
  ]
26
26
 
27
+ AGENT_CATALOG_ONLY_PLACEHOLDERS = {
28
+ 'icon': "inline-svg-of-icon",
29
+ }
30
+
27
31
  class AgentKind(str, Enum):
28
32
  NATIVE = "native"
29
33
  EXTERNAL = "external"
@@ -87,21 +91,9 @@ class Offering(BaseModel):
87
91
  return values
88
92
 
89
93
  def validate_ready_for_packaging(self):
90
- self.test_for_placeholder_values()
91
-
92
- def test_for_placeholder_values(self):
93
- placholders = False
94
- # part numbers
95
- if not self.part_number:
96
- raise ValueError(f"Offering '{self.name}' does not have valid part numbers")
97
-
98
- for (k,v) in self.part_number.model_dump().items():
99
- if v == CATALOG_PLACEHOLDERS['part_number']:
100
- logger.warning(f"Placeholder part number detected for platform '{k}', please ensure valid part numbers are entered before packaging.")
101
- placholders = True
102
-
103
- if placholders:
104
- raise ValueError(f"Offering '{self.name}' cannot be packaged with placeholder values")
94
+ # Leaving this fn here in case we want to reintroduce validation
95
+ pass
96
+
105
97
 
106
98
 
107
99
 
@@ -16,7 +16,7 @@ from ibm_watsonx_orchestrate.client.utils import instantiate_client
16
16
  from ibm_watsonx_orchestrate.cli.commands.environment.environment_controller import _login
17
17
 
18
18
  from ibm_watsonx_orchestrate.cli.config import PROTECTED_ENV_NAME, clear_protected_env_credentials_token, Config, \
19
- AUTH_CONFIG_FILE_FOLDER, AUTH_CONFIG_FILE, AUTH_MCSP_TOKEN_OPT, AUTH_SECTION_HEADER, USER_ENV_CACHE_HEADER, LICENSE_HEADER, \
19
+ AUTH_CONFIG_FILE_FOLDER, AUTH_CONFIG_FILE, AUTH_MCSP_TOKEN_OPT, AUTH_SECTION_HEADER, LICENSE_HEADER, \
20
20
  ENV_ACCEPT_LICENSE
21
21
  from ibm_watsonx_orchestrate.client.agents.agent_client import AgentClient
22
22
  from ibm_watsonx_orchestrate.utils.docker_utils import DockerLoginService, DockerComposeCore, DockerUtils
@@ -38,8 +38,12 @@ def refresh_local_credentials() -> None:
38
38
  """
39
39
  Refresh the local credentials
40
40
  """
41
- clear_protected_env_credentials_token()
42
- _login(name=PROTECTED_ENV_NAME, apikey=None)
41
+ try:
42
+ clear_protected_env_credentials_token()
43
+ _login(name=PROTECTED_ENV_NAME, apikey=None)
44
+
45
+ except:
46
+ logger.warning("Failed to refresh local credentials, please run `orchestrate env activate local`")
43
47
 
44
48
  def run_compose_lite(
45
49
  final_env_file: Path,
@@ -51,11 +55,11 @@ def run_compose_lite(
51
55
  with_connections_ui=False,
52
56
  with_langflow=False,
53
57
  ) -> None:
54
- EnvService.prepare_clean_env(final_env_file)
55
- db_tag = EnvService.read_env_file(final_env_file).get('DBTAG', None)
58
+ env_service.prepare_clean_env(final_env_file)
59
+ db_tag = env_service.read_env_file(final_env_file).get('DBTAG', None)
56
60
  logger.info(f"Detected architecture: {platform.machine()}, using DBTAG: {db_tag}")
57
61
 
58
- compose_core = DockerComposeCore(env_service)
62
+ compose_core = DockerComposeCore(env_service=env_service)
59
63
 
60
64
  # Step 1: Start only the DB container
61
65
  result = compose_core.service_up(service_name="wxo-server-db", friendly_name="WxO Server DB", final_env_file=final_env_file, compose_env=os.environ)
@@ -197,7 +201,7 @@ def run_compose_lite_ui(user_env_file: Path) -> bool:
197
201
  logger.error("Healthcheck failed orchestrate server. Make sure you start the server components with `orchestrate server start` before trying to start the chat UI")
198
202
  return False
199
203
 
200
- compose_core = DockerComposeCore(env_service)
204
+ compose_core = DockerComposeCore(env_service=env_service)
201
205
 
202
206
  result = compose_core.service_up(service_name="ui", friendly_name="UI", final_env_file=final_env_file)
203
207
 
@@ -234,7 +238,7 @@ def run_compose_lite_down_ui(user_env_file: Path, is_reset: bool = False) -> Non
234
238
 
235
239
  cli_config = Config()
236
240
  env_service = EnvService(cli_config)
237
- compose_core = DockerComposeCore(env_service)
241
+ compose_core = DockerComposeCore(env_service=env_service)
238
242
 
239
243
  result = compose_core.service_down(service_name="ui", friendly_name="UI", final_env_file=final_env_file, is_reset=is_reset)
240
244
 
@@ -255,7 +259,7 @@ def run_compose_lite_down(final_env_file: Path, is_reset: bool = False) -> None:
255
259
 
256
260
  cli_config = Config()
257
261
  env_service = EnvService(cli_config)
258
- compose_core = DockerComposeCore(env_service)
262
+ compose_core = DockerComposeCore(env_service=env_service)
259
263
 
260
264
  result = compose_core.services_down(final_env_file=final_env_file, is_reset=is_reset)
261
265
 
@@ -276,7 +280,7 @@ def run_compose_lite_logs(final_env_file: Path) -> None:
276
280
 
277
281
  cli_config = Config()
278
282
  env_service = EnvService(cli_config)
279
- compose_core = DockerComposeCore(env_service)
283
+ compose_core = DockerComposeCore(env_service=env_service)
280
284
 
281
285
  result = compose_core.services_logs(final_env_file=final_env_file, should_follow=True)
282
286
 
@@ -395,7 +399,8 @@ def server_start(
395
399
  env_service.set_compose_file_path_in_env(custom_compose_file)
396
400
 
397
401
  user_env = env_service.get_user_env(user_env_file=user_env_file, fallback_to_persisted_env=False)
398
- env_service.persist_user_env(user_env, include_secrets=persist_env_secrets)
402
+ developer_edition_source = env_service.get_dev_edition_source_core(user_env)
403
+ env_service.persist_user_env(user_env, include_secrets=persist_env_secrets, source=developer_edition_source)
399
404
 
400
405
  merged_env_dict = env_service.prepare_server_env_vars(user_env=user_env, should_drop_auth_routes=False)
401
406
 
@@ -451,10 +456,7 @@ def server_start(
451
456
  )
452
457
  exit(1)
453
458
 
454
- try:
455
- refresh_local_credentials()
456
- except:
457
- logger.warning("Failed to refresh local credentials, please run `orchestrate env activate local`")
459
+ refresh_local_credentials()
458
460
 
459
461
  logger.info(f"You can run `orchestrate env activate local` to set your environment or `orchestrate chat start` to start the UI service and begin chatting.")
460
462
 
@@ -570,7 +572,7 @@ def run_db_migration() -> None:
570
572
 
571
573
  cli_config = Config()
572
574
  env_service = EnvService(cli_config)
573
- compose_core = DockerComposeCore(env_service)
575
+ compose_core = DockerComposeCore(env_service=env_service)
574
576
 
575
577
  result = compose_core.service_container_bash_exec(service_name="wxo-server-db",
576
578
  log_message="Running Database Migration...",
@@ -624,7 +626,7 @@ def create_langflow_db() -> None:
624
626
 
625
627
  cli_config = Config()
626
628
  env_service = EnvService(cli_config)
627
- compose_core = DockerComposeCore(env_service)
629
+ compose_core = DockerComposeCore(env_service=env_service)
628
630
 
629
631
  result = compose_core.service_container_bash_exec(service_name="wxo-server-db",
630
632
  log_message="Preparing Langflow resources...",
@@ -1,8 +1,8 @@
1
1
  import os
2
2
  import zipfile
3
3
  import tempfile
4
- from typing import List, Optional
5
- from enum import Enum
4
+ from typing import List, Optional, Any
5
+ from pydantic import BaseModel
6
6
  import logging
7
7
  import sys
8
8
  import re
@@ -10,7 +10,7 @@ import requests
10
10
  from ibm_watsonx_orchestrate.client.toolkit.toolkit_client import ToolKitClient
11
11
  from ibm_watsonx_orchestrate.client.tools.tool_client import ToolClient
12
12
  from ibm_watsonx_orchestrate.agent_builder.toolkits.base_toolkit import BaseToolkit, ToolkitSpec
13
- from ibm_watsonx_orchestrate.agent_builder.toolkits.types import ToolkitKind, Language, ToolkitSource, ToolkitTransportKind
13
+ from ibm_watsonx_orchestrate.agent_builder.toolkits.types import ToolkitKind, Language, ToolkitSource, ToolkitTransportKind, ToolkitListEntry
14
14
  from ibm_watsonx_orchestrate.client.utils import instantiate_client
15
15
  from ibm_watsonx_orchestrate.utils.utils import sanitize_app_id
16
16
  from ibm_watsonx_orchestrate.client.connections import get_connections_client
@@ -18,7 +18,7 @@ import typer
18
18
  import json
19
19
  from rich.console import Console
20
20
  from rich.progress import Progress, SpinnerColumn, TextColumn
21
- from ibm_watsonx_orchestrate.client.utils import is_local_dev
21
+ from ibm_watsonx_orchestrate.cli.common import ListFormats, rich_table_to_markdown
22
22
  from rich.json import JSON
23
23
  import rich
24
24
  import rich.table
@@ -264,8 +264,119 @@ class ToolkitController:
264
264
  except requests.HTTPError as e:
265
265
  logger.error(e.response.text)
266
266
  exit(1)
267
+
268
+ def _lookup_toolkit_resource_value(
269
+ self,
270
+ toolkit: BaseToolkit,
271
+ lookup_table: dict[str, str],
272
+ target_attr: str,
273
+ target_attr_display_name: str
274
+ ) -> List[str] | str | None:
275
+ """
276
+ Using a lookup table convert all the strings in a given field of an agent into their equivalent in the lookup table
277
+ Example: lookup_table={1: obj1, 2: obj2} agent=Toolkit(tools=[1,2]) return. [obj1, obj2]
278
+
279
+ Args:
280
+ toolkit: A toolkit
281
+ lookup_table: A dictionary that maps one value to another
282
+ target_attr: The field to convert on the provided agent
283
+ target_attr_display_name: The name of the field to be displayed in the event of an error
284
+ """
285
+ attr_value = getattr(toolkit, target_attr, None)
286
+ if not attr_value:
287
+ return
288
+
289
+ if isinstance(attr_value, list):
290
+ new_resource_list=[]
291
+ for value in attr_value:
292
+ if value in lookup_table:
293
+ new_resource_list.append(lookup_table[value])
294
+ else:
295
+ logger.warning(f"{target_attr_display_name} with ID '{value}' not found. Returning {target_attr_display_name} ID")
296
+ new_resource_list.append(value)
297
+ return new_resource_list
298
+ else:
299
+ if attr_value in lookup_table:
300
+ return lookup_table[attr_value]
301
+ else:
302
+ logger.warning(f"{target_attr_display_name} with ID '{attr_value}' not found. Returning {target_attr_display_name} ID")
303
+ return attr_value
304
+
305
+ def _construct_lut_toolkit_resource(self, resource_list: List[dict], key_attr: str, value_attr) -> dict:
306
+ """
307
+ Given a list of dictionaries build a key -> value look up table
308
+ Example [{id: 1, name: obj1}, {id: 2, name: obj2}] return {1: obj1, 2: obj2}
309
+
310
+ Args:
311
+ resource_list: A list of dictionries from which to build the lookup table from
312
+ key_attr: The name of the field whose value will form the key of the lookup table
313
+ value_attrL The name of the field whose value will form the value of the lookup table
314
+
315
+ Returns:
316
+ A lookup table
317
+ """
318
+ lut = {}
319
+ for resource in resource_list:
320
+ if isinstance(resource, BaseModel):
321
+ resource = resource.model_dump()
322
+ lut[resource.get(key_attr, None)] = resource.get(value_attr, None)
323
+ return lut
324
+
325
+ def _batch_request_resource(self, client_fn, ids, batch_size=50) -> List[dict]:
326
+ resources = []
327
+ for i in range(0, len(ids), batch_size):
328
+ chunk = ids[i:i + batch_size]
329
+ resources += (client_fn(chunk))
330
+ return resources
331
+
332
+ def _get_all_unique_toolkit_resources(self, toolkits: List[BaseToolkit], target_attr: str) -> List[str]:
333
+ """
334
+ Given a list of toolkits get all the unique values of a certain field
335
+ Example: tk1.tools = [1 ,2 ,3] and tk2.tools = [2, 4, 5] then return [1, 2, 3, 4, 5]
336
+ Example: tk1.id = "123" and tk2.id = "456" then return ["123", "456"]
337
+
338
+ Args:
339
+ toolkits: List of toolkits
340
+ target_attr: The name of the field to access and get unique elements
341
+
342
+ Returns:
343
+ A list of unique elements from across all toolkits
344
+ """
345
+ all_ids = set()
346
+ for toolkit in toolkits:
347
+ attr_value = getattr(toolkit, target_attr, None)
348
+ if attr_value:
349
+ if isinstance(attr_value, list):
350
+ all_ids.update(attr_value)
351
+ else:
352
+ all_ids.add(attr_value)
353
+ return list(all_ids)
354
+
355
+ def _bulk_resolve_toolkit_tools(self, toolkits: List[BaseToolkit]) -> List[BaseToolkit]:
356
+ new_toolkit_specs = [tk.__toolkit_spec__ for tk in toolkits].copy()
357
+ all_tools_ids = self._get_all_unique_toolkit_resources(new_toolkit_specs, "tools")
358
+ if not all_tools_ids:
359
+ return toolkits
360
+
361
+ tool_client = instantiate_client(ToolClient)
362
+
363
+ all_tools = self._batch_request_resource(tool_client.get_drafts_by_ids, all_tools_ids)
364
+
365
+ tool_lut = self._construct_lut_toolkit_resource(all_tools, "id", "name")
366
+
367
+ new_toolkits = []
368
+ for toolkit_spec in new_toolkit_specs:
369
+ tool_names = self._lookup_toolkit_resource_value(toolkit_spec, tool_lut, "tools", "Tool")
370
+ if tool_names:
371
+ toolkit_spec.tools = tool_names
372
+ new_toolkits.append(BaseToolkit(toolkit_spec))
373
+ return new_toolkits
374
+
375
+ def list_toolkits(self, verbose=False, format: ListFormats| None = None) -> List[dict[str, Any]] | List[ToolkitListEntry] | str | None:
376
+ if verbose and format:
377
+ logger.error("For toolkits list, `--verbose` and `--format` are mutually exclusive options")
378
+ sys.exit(1)
267
379
 
268
- def list_toolkits(self, verbose=False):
269
380
  client = self.get_client()
270
381
  response = client.get()
271
382
  toolkit_spec = [ToolkitSpec.model_validate(toolkit) for toolkit in response]
@@ -276,7 +387,10 @@ class ToolkitController:
276
387
  for toolkit in toolkits:
277
388
  tools_list.append(json.loads(toolkit.dumps_spec()))
278
389
  rich.print(JSON(json.dumps(tools_list, indent=4)))
390
+ return tools_list
279
391
  else:
392
+ toolkit_details = []
393
+
280
394
  table = rich.table.Table(show_header=True, header_style="bold white", show_lines=True)
281
395
  column_args = {
282
396
  "Name": {"overflow": "fold"},
@@ -288,23 +402,14 @@ class ToolkitController:
288
402
  for column in column_args:
289
403
  table.add_column(column,**column_args[column])
290
404
 
291
- tools_client = instantiate_client(ToolClient)
292
-
293
405
  connections_client = get_connections_client()
294
406
  connections = connections_client.list()
295
407
 
296
408
  connections_dict = {conn.connection_id: conn for conn in connections}
297
409
 
298
- for toolkit in toolkits:
299
- tool_ids = toolkit.__toolkit_spec__.tools or []
300
- tool_names = []
301
- if len(tool_ids) == 0:
302
- logger.warning("This toolkit contains no tools.")
303
-
304
- for tool_id in tool_ids:
305
- tool = tools_client.get_draft_by_id(tool_id)
306
- tool_names.append(tool["name"])
410
+ resolved_toolkits = self._bulk_resolve_toolkit_tools(toolkits)
307
411
 
412
+ for toolkit in resolved_toolkits:
308
413
  app_ids = []
309
414
  connection_ids = toolkit.__toolkit_spec__.mcp.connections.values()
310
415
 
@@ -317,15 +422,22 @@ class ToolkitController:
317
422
  else:
318
423
  app_id = ""
319
424
  app_ids.append(app_id)
320
-
321
-
322
-
323
- table.add_row(
324
- toolkit.__toolkit_spec__.name,
325
- "MCP",
326
- toolkit.__toolkit_spec__.description,
327
- ", ".join(tool_names),
328
- ", ".join(app_ids),
425
+
426
+ entry = ToolkitListEntry(
427
+ name = toolkit.__toolkit_spec__.name,
428
+ description = toolkit.__toolkit_spec__.description,
429
+ tools = toolkit.__toolkit_spec__.tools,
430
+ app_ids = app_ids
329
431
  )
330
-
331
- rich.print(table)
432
+ if format == ListFormats.JSON:
433
+ toolkit_details.append(entry)
434
+ else:
435
+ table.add_row(*entry.get_row_details())
436
+
437
+ match format:
438
+ case ListFormats.JSON:
439
+ return toolkit_details
440
+ case ListFormats.Table:
441
+ return rich_table_to_markdown(table)
442
+ case _:
443
+ rich.print(table)
@@ -1,6 +1,6 @@
1
1
  import typer
2
2
  from typing import List
3
- from typing_extensions import Annotated
3
+ from typing_extensions import Annotated, Optional
4
4
  from ibm_watsonx_orchestrate.cli.commands.tools.tools_controller import ToolsController, ToolKind
5
5
  tools_app= typer.Typer(no_args_is_help=True)
6
6
 
@@ -34,7 +34,7 @@ def tool_import(
34
34
  )
35
35
  ] = None,
36
36
  requirements_file: Annotated[
37
- str,
37
+ Optional[str],
38
38
  typer.Option(
39
39
  "--requirements-file",
40
40
  "-r",