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
@@ -0,0 +1,158 @@
1
+ import logging
2
+ import yaml
3
+ from typing import Mapping, Any
4
+ from enum import StrEnum
5
+ from pathlib import Path
6
+
7
+ from ibm_watsonx_orchestrate.cli.commands.agents.agents_controller import (
8
+ AgentsController,
9
+ Agent,
10
+ ExternalAgent,
11
+ AssistantAgent,
12
+ )
13
+ from ibm_watsonx_orchestrate.cli.commands.tools.tools_controller import (
14
+ ToolsController,
15
+ BaseTool,
16
+ )
17
+ from ibm_watsonx_orchestrate.cli.commands.knowledge_bases.knowledge_bases_controller import (
18
+ KnowledgeBaseController,
19
+ KnowledgeBase,
20
+ )
21
+ from ibm_watsonx_orchestrate.cli.commands.knowledge_bases.knowledge_bases_controller import (
22
+ parse_file as kb_parse_file,
23
+ )
24
+ from ibm_watsonx_orchestrate.cli.commands.evaluations.evaluations_controller import (
25
+ EvaluationsController,
26
+ EvaluateMode,
27
+ )
28
+
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ class ArtifactTypes(StrEnum):
34
+ """The allowed artifacts in the environment manager path.
35
+
36
+ The environment manager config looks like this:
37
+ ```json
38
+ env1:
39
+ agent:
40
+ agents_path: None
41
+ tools:
42
+ tools_path: None
43
+ tool_kind: None
44
+ # any other tool flags
45
+ knowledge:
46
+ knowledge_base_path: None
47
+ test_config: # path to config.yaml
48
+ clean_up: True
49
+ ```
50
+ The allowed artifacts/keys are "agent", "tools", "knowledge"
51
+ """
52
+
53
+ agent = "agent"
54
+ tools = "tools"
55
+ knowledge = "knowledge"
56
+
57
+
58
+ class TestCaseManager:
59
+ def __init__(
60
+ self,
61
+ env_settings: Mapping[str, Any],
62
+ output_dir: str,
63
+ mode: EvaluateMode = EvaluateMode.default,
64
+ ):
65
+ self.env_settings = env_settings
66
+ self.cleanup = env_settings.get("clean_up", False)
67
+ self.output_dir = output_dir
68
+ self.mode = mode
69
+
70
+ self.agent_controller = AgentsController()
71
+ self.knowledge_controller = KnowledgeBaseController()
72
+ self.tool_controller = None
73
+ if (tool_settings := env_settings.get(ArtifactTypes.tools)):
74
+ self.tool_controller = ToolsController(
75
+ tool_kind=tool_settings.get("kind"),
76
+ file=tool_settings.get("file"),
77
+ requirements_file=tool_settings.get("requirements_file")
78
+ )
79
+
80
+ self.imported_artifacts = []
81
+
82
+ def __enter__(self):
83
+ for artifact in [
84
+ ArtifactTypes.tools,
85
+ ArtifactTypes.knowledge,
86
+ ArtifactTypes.agent,
87
+ ]:
88
+ if artifact not in self.env_settings:
89
+ continue
90
+
91
+ artifact_settings = self.env_settings.get(artifact)
92
+ if artifact == ArtifactTypes.tools:
93
+ tools = ToolsController.import_tool(**artifact_settings)
94
+ # import_tool returns Iterator[BaseTool], copy the iterator into a list for preservation
95
+ # this is needed if user wants environment cleanup
96
+ tools = [tool for tool in tools]
97
+ self.imported_artifacts.append(tools)
98
+ self.tool_controller.publish_or_update_tools(tools)
99
+ elif artifact == ArtifactTypes.knowledge:
100
+ KnowledgeBaseController.import_knowledge_base(**artifact_settings)
101
+ kb_spec = kb_parse_file(artifact_settings.get("file"))
102
+ self.imported_artifacts.append(kb_spec)
103
+ elif artifact == ArtifactTypes.agent:
104
+ artifact_settings["app_id"] = artifact_settings.get("app_id", None)
105
+ agents = AgentsController.import_agent(**artifact_settings)
106
+ self.agent_controller.publish_or_update_agents(agents)
107
+ self.imported_artifacts.append(agents)
108
+
109
+ eval = EvaluationsController()
110
+ eval.evaluate(
111
+ test_paths=self.env_settings.get("test_paths"),
112
+ output_dir=self.output_dir,
113
+ mode=self.mode,
114
+ )
115
+
116
+ return self
117
+
118
+ def __exit__(self, exc_type, exc_val, exc_tb):
119
+ if self.cleanup:
120
+ logger.info("Cleaning environment")
121
+ for artifact in self.imported_artifacts:
122
+ # artifact can be a list of agents, tools
123
+ for item in artifact:
124
+ if isinstance(item, BaseTool):
125
+ self.tool_controller.remove_tool(item.__tool_spec__.name)
126
+ if isinstance(item, KnowledgeBase):
127
+ self.knowledge_controller.remove_knowledge_base(
128
+ item.id, item.name
129
+ )
130
+ if isinstance(item, (Agent, AssistantAgent, ExternalAgent)):
131
+ self.agent_controller.remove_agent(item.name, item.kind)
132
+
133
+
134
+ def run_environment_manager(
135
+ environment_manager_path: str,
136
+ mode: EvaluateMode = EvaluateMode.default,
137
+ output_dir: str = None,
138
+ ):
139
+ with open(environment_manager_path, encoding="utf-8", mode="r") as f:
140
+ env_settings = yaml.load(f, Loader=yaml.SafeLoader)
141
+
142
+ for env in env_settings:
143
+ if not env_settings.get(env).get("enabled"):
144
+ continue
145
+ results_folder = Path(output_dir) / env
146
+ results_folder.mkdir(parents=True, exist_ok=True)
147
+ logger.info(
148
+ "Processing environment: '%s'. Results will be saved to '%s'",
149
+ env,
150
+ results_folder,
151
+ )
152
+
153
+ with TestCaseManager(
154
+ env_settings=env_settings.get(env),
155
+ output_dir=str(results_folder),
156
+ mode=mode,
157
+ ):
158
+ logger.info("Finished evaluation for environment: '%s'", env)
@@ -59,3 +59,29 @@ def knowledge_base_status(
59
59
  ):
60
60
  controller = KnowledgeBaseController()
61
61
  controller.knowledge_base_status(id=id, name=name)
62
+
63
+ @knowledge_bases_app.command(name="export", help='Export a knowledge base spec to a yaml')
64
+ def knowledge_base_export(
65
+ output_file: Annotated[
66
+ str,
67
+ typer.Option(
68
+ "--output",
69
+ "-o",
70
+ help="Path to a where the zip file containing the exported data should be saved",
71
+ ),
72
+ ],
73
+ name: Annotated[
74
+ str,
75
+ typer.Option("--name", "-n", help="The name of the knowledge base you want to export"),
76
+ ]=None,
77
+ id: Annotated[
78
+ str,
79
+ typer.Option("--id", "-i", help="The ID of the knowledge base you wish export"),
80
+ ]=None,
81
+ ):
82
+ controller = KnowledgeBaseController()
83
+ controller.knowledge_base_export(
84
+ id=id,
85
+ name=name,
86
+ output_path=output_file
87
+ )
@@ -5,15 +5,20 @@ import requests
5
5
  import logging
6
6
  import importlib
7
7
  import inspect
8
+ import yaml
8
9
  from pathlib import Path
9
- from typing import List
10
+ from typing import List, Any, Optional
11
+ from zipfile import ZipFile
12
+ from io import BytesIO
10
13
 
11
14
  from ibm_watsonx_orchestrate.agent_builder.knowledge_bases.knowledge_base import KnowledgeBase
12
15
  from ibm_watsonx_orchestrate.client.knowledge_bases.knowledge_base_client import KnowledgeBaseClient
13
16
  from ibm_watsonx_orchestrate.client.base_api_client import ClientAPIException
14
17
  from ibm_watsonx_orchestrate.client.connections import get_connections_client
15
18
  from ibm_watsonx_orchestrate.client.utils import instantiate_client
16
- from ibm_watsonx_orchestrate.agent_builder.knowledge_bases.types import FileUpload
19
+ from ibm_watsonx_orchestrate.agent_builder.knowledge_bases.types import FileUpload, KnowledgeBaseListEntry
20
+ from ibm_watsonx_orchestrate.cli.common import ListFormats, rich_table_to_markdown
21
+ from ibm_watsonx_orchestrate.agent_builder.knowledge_bases.types import KnowledgeBaseKind, IndexConnection, SpecVersion
17
22
 
18
23
  logger = logging.getLogger(__name__)
19
24
 
@@ -63,6 +68,32 @@ def build_file_object(file_dir: str, file: str | FileUpload):
63
68
  return ('files', (get_file_name(file.path), open(get_relative_file_path(file.path, file_dir), 'rb')))
64
69
  return ('files', (get_file_name(file), open(get_relative_file_path(file, file_dir), 'rb')))
65
70
 
71
+ def build_connections_map(key_attr: str) -> dict:
72
+ connections_client = get_connections_client()
73
+ connections = connections_client.list()
74
+
75
+ return {getattr(conn, key_attr): conn for conn in connections}
76
+
77
+ def get_index_config(kb: KnowledgeBase, index: int = 0) -> IndexConnection | None:
78
+ if kb.conversational_search_tool is not None \
79
+ and kb.conversational_search_tool.index_config is not None \
80
+ and len(kb.conversational_search_tool.index_config) > index:
81
+
82
+ return kb.conversational_search_tool.index_config[index]
83
+ return None
84
+
85
+ def get_kb_app_id(kb: KnowledgeBase) -> str | None:
86
+ index_config = get_index_config(kb)
87
+ if not index_config:
88
+ return
89
+ return index_config.app_id
90
+
91
+ def get_kb_connection_id(kb: KnowledgeBase) -> str | None:
92
+ index_config = get_index_config(kb)
93
+ if not index_config:
94
+ return
95
+ return index_config.connection_id
96
+
66
97
  class KnowledgeBaseController:
67
98
  def __init__(self):
68
99
  self.client = None
@@ -78,24 +109,23 @@ class KnowledgeBaseController:
78
109
 
79
110
  knowledge_bases = parse_file(file=file)
80
111
 
81
- if app_id:
82
- connections_client = get_connections_client()
83
- connection_id = None
84
-
85
- connections = connections_client.get_draft_by_app_id(app_id=app_id)
86
- if not connections:
87
- logger.error(f"No connection exists with the app-id '{app_id}'")
88
- exit(1)
89
-
90
- connection_id = connections.connection_id
91
-
92
- for kb in knowledge_bases:
93
- if kb.conversational_search_tool and kb.conversational_search_tool.index_config and len(kb.conversational_search_tool.index_config) > 0:
94
- kb.conversational_search_tool.index_config[0].connection_id = connection_id
112
+ connections_map = None
95
113
 
96
114
  existing_knowledge_bases = client.get_by_names([kb.name for kb in knowledge_bases])
97
115
 
98
116
  for kb in knowledge_bases:
117
+ app_id = app_id if app_id else get_kb_app_id(kb)
118
+ if app_id:
119
+ if not connections_map:
120
+ connections_map = build_connections_map("app_id")
121
+ conn = connections_map.get(app_id)
122
+ if conn:
123
+ index_config = get_index_config(kb)
124
+ if index_config:
125
+ index_config.connection_id = conn.connection_id
126
+ else:
127
+ logger.error(f"No connection exists with the app-id '{app_id}'")
128
+ exit(1)
99
129
  try:
100
130
  file_dir = "/".join(file.split("/")[:-1])
101
131
 
@@ -198,8 +228,7 @@ class KnowledgeBaseController:
198
228
 
199
229
  logger.info(f"Knowledge base '{kb.name}' updated successfully")
200
230
 
201
-
202
- def knowledge_base_status( self, id: str, name: str) -> None:
231
+ def knowledge_base_status( self, id: str, name: str, format: ListFormats = None) -> dict | str | None:
203
232
  knowledge_base_id = self.get_id(id, name)
204
233
  response = self.get_client().status(knowledge_base_id)
205
234
 
@@ -219,13 +248,25 @@ class KnowledgeBaseController:
219
248
 
220
249
  response["id"] = kbID
221
250
 
251
+ if format == ListFormats.JSON:
252
+ return response
253
+
254
+
222
255
  [table.add_column(to_column_name(col), {}) for col in response.keys()]
223
256
  table.add_row(*[str(val) for val in response.values()])
224
257
 
258
+ if format == ListFormats.Table:
259
+ return rich_table_to_markdown(table)
260
+
225
261
  rich.print(table)
226
262
 
227
263
 
228
- def list_knowledge_bases(self, verbose: bool=False):
264
+ def list_knowledge_bases(self, verbose: bool=False, format: ListFormats=None)-> List[dict[str, Any]] | List[KnowledgeBaseListEntry] | str | None:
265
+
266
+ if verbose and format:
267
+ logger.error("For knowledge base list, `--verbose` and `--format` are mutually exclusive options")
268
+ sys.exit(1)
269
+
229
270
  response = self.get_client().get()
230
271
  knowledge_bases = [KnowledgeBase.model_validate(knowledge_base) for knowledge_base in response]
231
272
 
@@ -234,7 +275,9 @@ class KnowledgeBaseController:
234
275
  for kb in knowledge_bases:
235
276
  knowledge_base_list.append(json.loads(kb.model_dump_json(exclude_none=True)))
236
277
  rich.print(rich.json.JSON(json.dumps(knowledge_base_list, indent=4)))
278
+ return knowledge_base_list
237
279
  else:
280
+ knowledge_base_details=[]
238
281
  table = rich.table.Table(
239
282
  show_header=True,
240
283
  header_style="bold white",
@@ -251,25 +294,34 @@ class KnowledgeBaseController:
251
294
  for column in column_args:
252
295
  table.add_column(column, **column_args[column])
253
296
 
297
+ connections_dict = build_connections_map("connection_id")
298
+
254
299
  for kb in knowledge_bases:
255
300
  app_id = ""
256
-
257
- if kb.conversational_search_tool is not None \
258
- and kb.conversational_search_tool.index_config is not None \
259
- and len(kb.conversational_search_tool.index_config) > 0 \
260
- and kb.conversational_search_tool.index_config[0].connection_id is not None:
261
- connections_client = get_connections_client()
262
- app_id = str(connections_client.get_draft_by_id(kb.conversational_search_tool.index_config[0].connection_id))
263
-
264
- table.add_row(
265
- kb.name,
266
- kb.description,
267
- app_id,
268
- str(kb.id)
301
+ connection_id = get_kb_connection_id(kb)
302
+ if connection_id is not None:
303
+ conn = connections_dict.get(connection_id)
304
+ if conn:
305
+ app_id = conn.app_id
306
+
307
+ entry = KnowledgeBaseListEntry(
308
+ name=kb.name,
309
+ id=str(kb.id),
310
+ description=kb.description,
311
+ app_id=app_id
269
312
  )
313
+ if format == ListFormats.JSON:
314
+ knowledge_base_details.append(entry)
315
+ else:
316
+ table.add_row(*entry.get_row_details())
270
317
 
271
- rich.print(table)
272
-
318
+ match format:
319
+ case ListFormats.JSON:
320
+ return knowledge_base_details
321
+ case ListFormats.Table:
322
+ return rich_table_to_markdown(table)
323
+ case _:
324
+ rich.print(table)
273
325
 
274
326
  def remove_knowledge_base(self, id: str, name: str):
275
327
  knowledge_base_id = self.get_id(id, name)
@@ -283,4 +335,68 @@ class KnowledgeBaseController:
283
335
  logger.warning(f"No knowledge base {logEnding} found")
284
336
  logger.error(e.response.text)
285
337
  exit(1)
338
+
339
+ def get_knowledge_base(self, id) -> KnowledgeBase:
340
+ client = self.get_client()
341
+ try:
342
+ return KnowledgeBase.model_validate(client.get_by_id(id))
343
+ except requests.HTTPError as e:
344
+ if e.response.status_code == 404:
345
+ logger.error(f"No knowledge base {id} found")
346
+ else:
347
+ logger.error(e.response.text)
348
+ exit(1)
349
+
350
+
351
+ def knowledge_base_export(self,
352
+ output_path: str,
353
+ id: Optional[str] = None,
354
+ name: Optional[str] = None,
355
+ zip_file_out: Optional[ZipFile] = None) -> None:
356
+ output_file = Path(output_path)
357
+ output_file_extension = output_file.suffix
358
+ if output_file_extension not in {".yaml", ".yml"} :
359
+ logger.error(f"Output file must end with the extension '.yaml'/'.yml'. Provided file '{output_path}' ends with '{output_file_extension}'")
360
+ sys.exit(1)
361
+
362
+ knowledge_base_id = self.get_id(id, name)
363
+ logEnding = f"with ID '{id}'" if id else f"'{name}'"
364
+
365
+ logger.info(f"Exporting spec for knowledge base {logEnding}'")
366
+
367
+ knowledge_base = self.get_knowledge_base(knowledge_base_id)
286
368
 
369
+ if not knowledge_base:
370
+ logger.error(f"Knowledge base'{knowledge_base_id}' not found.'")
371
+ return
372
+
373
+ knowledge_base.tenant_id = None
374
+ knowledge_base.id = None
375
+ knowledge_base.spec_version = SpecVersion.V1
376
+ knowledge_base.kind = KnowledgeBaseKind.KNOWLEDGE_BASE
377
+
378
+ connection_id = get_kb_connection_id(knowledge_base)
379
+ if connection_id:
380
+ connections_map = build_connections_map("connection_id")
381
+ conn = connections_map.get(connection_id)
382
+ if conn:
383
+ index_config = get_index_config(knowledge_base)
384
+ index_config.app_id = conn.app_id
385
+ index_config.connection_id = None
386
+ else:
387
+ logger.warning(f"Connection '{connection_id}' not found, unable to resolve app_id for Knowledge base {logEnding}")
388
+
389
+ knowledge_base_spec = knowledge_base.model_dump(mode="json", exclude_none=True, exclude_unset=True)
390
+ if zip_file_out:
391
+ knowledge_base_spec_yaml = yaml.dump(knowledge_base_spec, sort_keys=False, default_flow_style=False, allow_unicode=True)
392
+ knowledge_base_spec_yaml_bytes = knowledge_base_spec_yaml.encode("utf-8")
393
+ knowledge_base_spec_yaml_file = BytesIO(knowledge_base_spec_yaml_bytes)
394
+ zip_file_out.writestr(
395
+ output_path,
396
+ knowledge_base_spec_yaml_file.getvalue()
397
+ )
398
+ else:
399
+ with open(output_path, 'w') as outfile:
400
+ yaml.dump(knowledge_base_spec, outfile, sort_keys=False, default_flow_style=False, allow_unicode=True)
401
+
402
+ logger.info(f"Successfully exported for knowledge base {logEnding} to '{output_path}'")
@@ -136,11 +136,11 @@ def models_policy_import(
136
136
  def models_policy_add(
137
137
  name: Annotated[
138
138
  str,
139
- typer.Option("--name", "-n", help="The name of the model to remove"),
139
+ typer.Option("--name", "-n", help="The name of the model policy to add"),
140
140
  ],
141
141
  models: Annotated[
142
142
  List[str],
143
- typer.Option('--model', '-m', help='The name of the model to add'),
143
+ typer.Option('--model', '-m', help='A list of model names the policy should contain'),
144
144
  ],
145
145
  strategy: Annotated[
146
146
  ModelPolicyStrategyMode,
@@ -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