ibm-watsonx-orchestrate 1.12.2__py3-none-any.whl → 1.13.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 (53) hide show
  1. ibm_watsonx_orchestrate/__init__.py +1 -1
  2. ibm_watsonx_orchestrate/agent_builder/connections/types.py +34 -3
  3. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +13 -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/types.py +21 -3
  8. ibm_watsonx_orchestrate/agent_builder/voice_configurations/__init__.py +1 -1
  9. ibm_watsonx_orchestrate/agent_builder/voice_configurations/types.py +11 -0
  10. ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +31 -53
  11. ibm_watsonx_orchestrate/cli/commands/connections/connections_command.py +2 -2
  12. ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +54 -28
  13. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_command.py +36 -2
  14. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_controller.py +270 -26
  15. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_server_controller.py +4 -4
  16. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_command.py +30 -3
  17. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_environment_manager.py +158 -0
  18. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_command.py +26 -0
  19. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +150 -34
  20. ibm_watsonx_orchestrate/cli/commands/models/models_command.py +2 -2
  21. ibm_watsonx_orchestrate/cli/commands/models/models_controller.py +29 -10
  22. ibm_watsonx_orchestrate/cli/commands/server/server_command.py +50 -18
  23. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_controller.py +139 -27
  24. ibm_watsonx_orchestrate/cli/commands/tools/tools_command.py +2 -2
  25. ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +43 -29
  26. ibm_watsonx_orchestrate/cli/commands/voice_configurations/voice_configurations_controller.py +23 -11
  27. ibm_watsonx_orchestrate/cli/common.py +26 -0
  28. ibm_watsonx_orchestrate/cli/config.py +30 -1
  29. ibm_watsonx_orchestrate/client/agents/agent_client.py +1 -1
  30. ibm_watsonx_orchestrate/client/connections/connections_client.py +1 -14
  31. ibm_watsonx_orchestrate/client/copilot/cpe/copilot_cpe_client.py +55 -11
  32. ibm_watsonx_orchestrate/client/knowledge_bases/knowledge_base_client.py +6 -2
  33. ibm_watsonx_orchestrate/client/model_policies/model_policies_client.py +1 -1
  34. ibm_watsonx_orchestrate/client/models/models_client.py +1 -1
  35. ibm_watsonx_orchestrate/client/threads/threads_client.py +34 -0
  36. ibm_watsonx_orchestrate/client/tools/tempus_client.py +4 -2
  37. ibm_watsonx_orchestrate/client/utils.py +29 -7
  38. ibm_watsonx_orchestrate/docker/compose-lite.yml +3 -2
  39. ibm_watsonx_orchestrate/docker/default.env +15 -10
  40. ibm_watsonx_orchestrate/flow_builder/flows/flow.py +28 -12
  41. ibm_watsonx_orchestrate/flow_builder/types.py +25 -0
  42. ibm_watsonx_orchestrate/flow_builder/utils.py +1 -9
  43. ibm_watsonx_orchestrate/utils/async_helpers.py +31 -0
  44. ibm_watsonx_orchestrate/utils/docker_utils.py +1177 -33
  45. ibm_watsonx_orchestrate/utils/environment.py +165 -20
  46. ibm_watsonx_orchestrate/utils/exceptions.py +1 -1
  47. ibm_watsonx_orchestrate/utils/tokens.py +51 -0
  48. ibm_watsonx_orchestrate/utils/utils.py +57 -2
  49. {ibm_watsonx_orchestrate-1.12.2.dist-info → ibm_watsonx_orchestrate-1.13.0b1.dist-info}/METADATA +2 -2
  50. {ibm_watsonx_orchestrate-1.12.2.dist-info → ibm_watsonx_orchestrate-1.13.0b1.dist-info}/RECORD +53 -48
  51. {ibm_watsonx_orchestrate-1.12.2.dist-info → ibm_watsonx_orchestrate-1.13.0b1.dist-info}/WHEEL +0 -0
  52. {ibm_watsonx_orchestrate-1.12.2.dist-info → ibm_watsonx_orchestrate-1.13.0b1.dist-info}/entry_points.txt +0 -0
  53. {ibm_watsonx_orchestrate-1.12.2.dist-info → ibm_watsonx_orchestrate-1.13.0b1.dist-info}/licenses/LICENSE +0 -0
@@ -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
 
@@ -550,7 +552,6 @@ def run_db_migration() -> None:
550
552
  migration_command = f'''
551
553
  APPLIED_MIGRATIONS_FILE="/var/lib/postgresql/applied_migrations/applied_migrations.txt"
552
554
  touch "$APPLIED_MIGRATIONS_FILE"
553
-
554
555
  for file in /docker-entrypoint-initdb.d/*.sql; do
555
556
  filename=$(basename "$file")
556
557
 
@@ -566,11 +567,42 @@ def run_db_migration() -> None:
566
567
  fi
567
568
  fi
568
569
  done
570
+
571
+ # Create wxo_observability database if it doesn't exist
572
+ if psql -U {pg_user} -lqt | cut -d \\| -f 1 | grep -qw wxo_observability; then
573
+ echo 'Existing wxo_observability DB found'
574
+ else
575
+ echo 'Creating wxo_observability DB'
576
+ createdb -U "{pg_user}" -O "{pg_user}" wxo_observability;
577
+ psql -U {pg_user} -q -d postgres -c "GRANT CONNECT ON DATABASE wxo_observability TO {pg_user}";
578
+ fi
579
+
580
+ # Run observability-specific migrations
581
+ OBSERVABILITY_MIGRATIONS_FILE="/var/lib/postgresql/applied_migrations/observability_migrations.txt"
582
+ touch "$OBSERVABILITY_MIGRATIONS_FILE"
583
+
584
+ for file in /docker-entrypoint-initdb.d/observability/*.sql; do
585
+ if [ -f "$file" ]; then
586
+ filename=$(basename "$file")
587
+
588
+ if grep -Fxq "$filename" "$OBSERVABILITY_MIGRATIONS_FILE"; then
589
+ echo "Skipping already applied observability migration: $filename"
590
+ else
591
+ echo "Applying observability migration: $filename"
592
+ if psql -U {pg_user} -d wxo_observability -q -f "$file" > /dev/null 2>&1; then
593
+ echo "$filename" >> "$OBSERVABILITY_MIGRATIONS_FILE"
594
+ else
595
+ echo "Error applying observability migration: $filename. Stopping migrations."
596
+ exit 1
597
+ fi
598
+ fi
599
+ fi
600
+ done
569
601
  '''
570
602
 
571
603
  cli_config = Config()
572
604
  env_service = EnvService(cli_config)
573
- compose_core = DockerComposeCore(env_service)
605
+ compose_core = DockerComposeCore(env_service=env_service)
574
606
 
575
607
  result = compose_core.service_container_bash_exec(service_name="wxo-server-db",
576
608
  log_message="Running Database Migration...",
@@ -624,7 +656,7 @@ def create_langflow_db() -> None:
624
656
 
625
657
  cli_config = Config()
626
658
  env_service = EnvService(cli_config)
627
- compose_core = DockerComposeCore(env_service)
659
+ compose_core = DockerComposeCore(env_service=env_service)
628
660
 
629
661
  result = compose_core.service_container_bash_exec(service_name="wxo-server-db",
630
662
  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",
@@ -1,5 +1,4 @@
1
1
  import logging
2
- import asyncio
3
2
  import importlib
4
3
  import inspect
5
4
  import sys
@@ -11,25 +10,24 @@ import zipfile
11
10
  from enum import Enum
12
11
  from os import path
13
12
  from pathlib import Path
14
- from typing import Iterable, List, cast
13
+ from typing import Iterable, List, Any, Optional, cast
15
14
  import rich
16
15
  import json
17
- from rich.json import JSON
18
16
  import glob
19
17
 
20
18
  import rich.table
21
19
  import typer
22
20
 
23
- from rich.console import Console
24
21
  from rich.panel import Panel
25
22
 
26
- from ibm_watsonx_orchestrate.agent_builder.tools import BaseTool, ToolSpec
23
+ from ibm_watsonx_orchestrate.agent_builder.tools import BaseTool, ToolSpec, ToolListEntry
27
24
  from ibm_watsonx_orchestrate.agent_builder.tools.flow_tool import create_flow_json_tool
28
25
  from ibm_watsonx_orchestrate.agent_builder.tools.langflow_tool import LangflowTool, create_langflow_tool
29
26
  from ibm_watsonx_orchestrate.agent_builder.tools.openapi_tool import create_openapi_json_tools_from_uri,create_openapi_json_tools_from_content
30
27
  from ibm_watsonx_orchestrate.cli.commands.models.models_controller import ModelHighlighter
31
28
  from ibm_watsonx_orchestrate.cli.commands.tools.types import RegistryType
32
29
  from ibm_watsonx_orchestrate.cli.commands.connections.connections_controller import configure_connection, remove_connection, add_connection
30
+ from ibm_watsonx_orchestrate.cli.common import ListFormats, rich_table_to_markdown
33
31
  from ibm_watsonx_orchestrate.agent_builder.connections.types import ConnectionType, ConnectionEnvironment, ConnectionPreference
34
32
  from ibm_watsonx_orchestrate.cli.config import Config, CONTEXT_SECTION_HEADER, CONTEXT_ACTIVE_ENV_OPT, \
35
33
  PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_TYPE_OPT, PYTHON_REGISTRY_TEST_PACKAGE_VERSION_OVERRIDE_OPT, \
@@ -42,6 +40,7 @@ from ibm_watsonx_orchestrate.client.connections import get_connections_client, g
42
40
  from ibm_watsonx_orchestrate.client.utils import instantiate_client, is_local_dev
43
41
  from ibm_watsonx_orchestrate.flow_builder.utils import import_flow_support_tools
44
42
  from ibm_watsonx_orchestrate.utils.utils import sanitize_app_id
43
+ from ibm_watsonx_orchestrate.utils.async_helpers import run_coroutine_sync
45
44
  from ibm_watsonx_orchestrate.utils.exceptions import BadRequest
46
45
  from ibm_watsonx_orchestrate.client.tools.tempus_client import TempusClient
47
46
 
@@ -620,7 +619,7 @@ def get_whl_in_registry(registry_url: str, version: str) -> str| None:
620
619
  return wheel_file
621
620
 
622
621
  class ToolsController:
623
- def __init__(self, tool_kind: ToolKind = None, file: str = None, requirements_file: str = None):
622
+ def __init__(self, tool_kind: ToolKind = None, file: str = None, requirements_file: Optional[str] = None):
624
623
  self.client = None
625
624
  self.tool_kind = tool_kind
626
625
  self.file = file
@@ -656,14 +655,14 @@ class ToolsController:
656
655
  app_id = app_id[0]
657
656
  connection = connections_client.get_draft_by_app_id(app_id=app_id)
658
657
  connection_id = connection.connection_id
659
- tools = asyncio.run(import_openapi_tool(file=args["file"], connection_id=connection_id))
658
+ tools = run_coroutine_sync(import_openapi_tool(file=args["file"], connection_id=connection_id))
660
659
  case "flow":
661
- tools = asyncio.run(import_flow_tool(file=args["file"]))
660
+ tools = run_coroutine_sync(import_flow_tool(file=args["file"]))
662
661
  case "skill":
663
662
  tools = []
664
663
  logger.warning("Skill Import not implemented yet")
665
664
  case "langflow":
666
- tools = asyncio.run(import_langflow_tool(file=args["file"],app_id=args.get('app_id',None)))
665
+ tools = run_coroutine_sync(import_langflow_tool(file=args["file"],app_id=args.get('app_id',None)))
667
666
  case _:
668
667
  raise BadRequest("Invalid kind selected")
669
668
 
@@ -674,14 +673,18 @@ class ToolsController:
674
673
  yield tool
675
674
 
676
675
 
677
- def list_tools(self, verbose=False):
676
+ def list_tools(self, verbose=False, format: ListFormats| None = None) -> List[dict[str, Any]] | str | None:
677
+ if verbose and format:
678
+ logger.error("For tools list, `--verbose` and `--format` are mutually exclusive options")
679
+ sys.exit(1)
680
+
678
681
  response = self.get_client().get()
679
682
  tool_specs = []
680
683
  parse_errors = []
681
684
 
682
685
  for tool in response:
683
686
  try:
684
- tool_specs.append(ToolSpec.model_validate(tool))
687
+ tool_specs.append(ToolSpec.model_validate(tool, context="list"))
685
688
  except Exception as e:
686
689
  name = tool.get('name', None)
687
690
  parse_errors.append([
@@ -698,12 +701,19 @@ class ToolsController:
698
701
  tools_list.append(json.loads(tool.dumps_spec()))
699
702
 
700
703
  rich.print_json(json.dumps(tools_list, indent=4))
704
+ return tools_list
701
705
  else:
706
+ tool_details = []
707
+
708
+ connections_client = get_connections_client()
709
+ connections = connections_client.list()
710
+
711
+ connections_dict = {conn.connection_id: conn for conn in connections}
712
+
702
713
  table = rich.table.Table(show_header=True, header_style="bold white", show_lines=True)
703
714
  column_args = {
704
715
  "Name": {"overflow": "fold"},
705
716
  "Description": {},
706
- "Permission": {},
707
717
  "Type": {},
708
718
  "Toolkit": {},
709
719
  "App ID": {"overflow": "fold"}
@@ -711,11 +721,6 @@ class ToolsController:
711
721
  for column in column_args:
712
722
  table.add_column(column,**column_args[column])
713
723
 
714
- connections_client = get_connections_client()
715
- connections = connections_client.list()
716
-
717
- connections_dict = {conn.connection_id: conn for conn in connections}
718
-
719
724
  for tool in tools:
720
725
  tool_binding = tool.__tool_spec__.binding
721
726
 
@@ -773,22 +778,31 @@ class ToolsController:
773
778
  toolkit_name = toolkit["name"]
774
779
  elif toolkit:
775
780
  toolkit_name = str(toolkit)
776
-
777
781
 
778
- table.add_row(
779
- tool.__tool_spec__.name,
780
- tool.__tool_spec__.description,
781
- tool.__tool_spec__.permission,
782
- tool_type,
783
- toolkit_name,
784
- ", ".join(app_ids),
782
+ entry = ToolListEntry(
783
+ name=tool.__tool_spec__.name,
784
+ description=tool.__tool_spec__.description,
785
+ type=tool_type,
786
+ toolkit=toolkit_name,
787
+ app_ids=app_ids
785
788
  )
786
789
 
787
- rich.print(table)
790
+ if format == ListFormats.JSON:
791
+ tool_details.append(entry)
792
+ else:
793
+ table.add_row(*entry.get_row_details())
794
+
795
+ match format:
796
+ case ListFormats.JSON:
797
+ return tool_details
798
+ case ListFormats.Table:
799
+ return rich_table_to_markdown(table)
800
+ case _:
801
+ rich.print(table)
788
802
 
789
- for error in parse_errors:
790
- for l in error:
791
- logger.error(l)
803
+ for error in parse_errors:
804
+ for l in error:
805
+ logger.error(l)
792
806
 
793
807
  def get_all_tools(self) -> dict:
794
808
  return {entry["name"]: entry["id"] for entry in self.get_client().get()}
@@ -3,10 +3,12 @@ import sys
3
3
  import rich
4
4
  import yaml
5
5
  import logging
6
- from ibm_watsonx_orchestrate.agent_builder.voice_configurations import VoiceConfiguration
6
+ from typing import Optional, List, Any
7
+ from ibm_watsonx_orchestrate.agent_builder.voice_configurations import VoiceConfiguration, VoiceConfigurationListEntry
7
8
  from ibm_watsonx_orchestrate.client.utils import instantiate_client
8
9
  from ibm_watsonx_orchestrate.client.voice_configurations.voice_configurations_client import VoiceConfigurationsClient
9
10
  from ibm_watsonx_orchestrate.utils.exceptions import BadRequest
11
+ from ibm_watsonx_orchestrate.cli.common import ListFormats, rich_table_to_markdown
10
12
 
11
13
  logger = logging.getLogger(__name__)
12
14
 
@@ -69,12 +71,13 @@ class VoiceConfigurationsController:
69
71
 
70
72
  return configs[0]
71
73
 
72
- def list_voice_configs(self, verbose: bool) -> None:
74
+ def list_voice_configs(self, verbose: bool, format: Optional[ListFormats]=None) -> List[dict[str | Any]] | List[VoiceConfigurationListEntry] | str | None:
73
75
  voice_configs = self.fetch_voice_configs()
74
76
 
75
77
  if verbose:
76
78
  json_configs = [json.loads(x.dumps_spec()) for x in voice_configs]
77
79
  rich.print_json(json.dumps(json_configs, indent=4))
80
+ return json_configs
78
81
  else:
79
82
  config_table = rich.table.Table(
80
83
  show_header=True,
@@ -94,18 +97,27 @@ class VoiceConfigurationsController:
94
97
  for column in column_args:
95
98
  config_table.add_column(column, **column_args[column])
96
99
 
100
+ config_details = []
101
+
97
102
  for config in voice_configs:
98
- attached_agents = [x.display_name or x.name or x.id for x in config.attached_agents]
99
- config_table.add_row(
100
- config.name,
101
- config.voice_configuration_id,
102
- config.speech_to_text.provider,
103
- config.text_to_speech.provider,
104
- ",".join(attached_agents)
103
+ attached_agents = [x.name or x.id for x in config.attached_agents]
104
+ entry = VoiceConfigurationListEntry(
105
+ name=config.name,
106
+ id=config.voice_configuration_id,
107
+ speech_to_text_provider=config.speech_to_text.provider,
108
+ text_to_speech_provider=config.text_to_speech.provider,
109
+ attached_agents=attached_agents
105
110
  )
111
+ config_details.append(entry)
112
+ config_table.add_row(*entry.get_row_details())
106
113
 
107
- rich.print(config_table)
108
-
114
+ match format:
115
+ case ListFormats.JSON:
116
+ return config_details
117
+ case ListFormats.Table:
118
+ return rich_table_to_markdown(config_table)
119
+ case _:
120
+ rich.print(config_table)
109
121
 
110
122
  def create_voice_config(self, voice_config: VoiceConfiguration) -> str | None:
111
123
  client = self.get_voice_configurations_client()
@@ -0,0 +1,26 @@
1
+ from enum import Enum
2
+ from rich.table import Table
3
+
4
+ class ListFormats(str, Enum):
5
+ Table = "table"
6
+ JSON = "json"
7
+
8
+ def __str__(self):
9
+ return self.value
10
+
11
+ def __repr__(self):
12
+ return repr(self.value)
13
+
14
+ def rich_table_to_markdown(table: Table) -> str:
15
+ headers = [column.header for column in table.columns]
16
+ cols = [[cell for cell in col.cells] for col in table.columns]
17
+ rows = list(map(list, zip(*cols)))
18
+
19
+ # Header row
20
+ md = "| " + " | ".join(headers) + " |\n"
21
+ # Separator row
22
+ md += "| " + " | ".join(["---"] * len(headers)) + " |\n"
23
+ # # Data rows
24
+ for row in rows:
25
+ md += "| " + " | ".join(row) + " |\n"
26
+ return md