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.
- ibm_watsonx_orchestrate/__init__.py +2 -1
- ibm_watsonx_orchestrate/agent_builder/connections/types.py +34 -3
- ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +11 -2
- ibm_watsonx_orchestrate/agent_builder/models/types.py +17 -1
- ibm_watsonx_orchestrate/agent_builder/toolkits/types.py +14 -2
- ibm_watsonx_orchestrate/agent_builder/tools/__init__.py +1 -1
- ibm_watsonx_orchestrate/agent_builder/tools/langflow_tool.py +61 -1
- ibm_watsonx_orchestrate/agent_builder/tools/types.py +21 -3
- ibm_watsonx_orchestrate/agent_builder/voice_configurations/__init__.py +1 -1
- ibm_watsonx_orchestrate/agent_builder/voice_configurations/types.py +11 -0
- ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +27 -51
- ibm_watsonx_orchestrate/cli/commands/connections/connections_command.py +2 -2
- ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +54 -28
- ibm_watsonx_orchestrate/cli/commands/copilot/copilot_command.py +25 -2
- ibm_watsonx_orchestrate/cli/commands/copilot/copilot_controller.py +249 -14
- ibm_watsonx_orchestrate/cli/commands/copilot/copilot_server_controller.py +4 -4
- ibm_watsonx_orchestrate/cli/commands/environment/environment_command.py +5 -1
- ibm_watsonx_orchestrate/cli/commands/environment/environment_controller.py +6 -3
- ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_command.py +3 -2
- ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_controller.py +1 -1
- ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +45 -16
- ibm_watsonx_orchestrate/cli/commands/models/models_command.py +2 -2
- ibm_watsonx_orchestrate/cli/commands/models/models_controller.py +29 -10
- ibm_watsonx_orchestrate/cli/commands/partners/offering/partners_offering_controller.py +21 -4
- ibm_watsonx_orchestrate/cli/commands/partners/offering/types.py +7 -15
- ibm_watsonx_orchestrate/cli/commands/server/server_command.py +19 -17
- ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_controller.py +139 -27
- ibm_watsonx_orchestrate/cli/commands/tools/tools_command.py +2 -2
- ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +79 -36
- ibm_watsonx_orchestrate/cli/commands/voice_configurations/voice_configurations_controller.py +23 -11
- ibm_watsonx_orchestrate/cli/common.py +26 -0
- ibm_watsonx_orchestrate/cli/config.py +33 -2
- ibm_watsonx_orchestrate/client/connections/connections_client.py +1 -14
- ibm_watsonx_orchestrate/client/copilot/cpe/copilot_cpe_client.py +34 -1
- ibm_watsonx_orchestrate/client/knowledge_bases/knowledge_base_client.py +6 -2
- ibm_watsonx_orchestrate/client/model_policies/model_policies_client.py +1 -1
- ibm_watsonx_orchestrate/client/models/models_client.py +1 -1
- ibm_watsonx_orchestrate/client/threads/threads_client.py +34 -0
- ibm_watsonx_orchestrate/client/utils.py +29 -7
- ibm_watsonx_orchestrate/docker/compose-lite.yml +2 -2
- ibm_watsonx_orchestrate/docker/default.env +15 -9
- ibm_watsonx_orchestrate/flow_builder/flows/decorators.py +2 -0
- ibm_watsonx_orchestrate/flow_builder/flows/flow.py +59 -9
- ibm_watsonx_orchestrate/flow_builder/node.py +13 -1
- ibm_watsonx_orchestrate/flow_builder/types.py +39 -0
- ibm_watsonx_orchestrate/langflow/__init__.py +0 -0
- ibm_watsonx_orchestrate/langflow/langflow_utils.py +195 -0
- ibm_watsonx_orchestrate/langflow/lfx_deps.py +84 -0
- ibm_watsonx_orchestrate/utils/async_helpers.py +31 -0
- ibm_watsonx_orchestrate/utils/docker_utils.py +1177 -33
- ibm_watsonx_orchestrate/utils/environment.py +165 -20
- ibm_watsonx_orchestrate/utils/exceptions.py +1 -1
- ibm_watsonx_orchestrate/utils/tokens.py +51 -0
- ibm_watsonx_orchestrate/utils/utils.py +63 -4
- {ibm_watsonx_orchestrate-1.12.0b1.dist-info → ibm_watsonx_orchestrate-1.13.0b0.dist-info}/METADATA +2 -2
- {ibm_watsonx_orchestrate-1.12.0b1.dist-info → ibm_watsonx_orchestrate-1.13.0b0.dist-info}/RECORD +59 -52
- {ibm_watsonx_orchestrate-1.12.0b1.dist-info → ibm_watsonx_orchestrate-1.13.0b0.dist-info}/WHEEL +0 -0
- {ibm_watsonx_orchestrate-1.12.0b1.dist-info → ibm_watsonx_orchestrate-1.13.0b0.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
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
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
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"] =
|
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
|
-
|
91
|
-
|
92
|
-
|
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,
|
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
|
-
|
42
|
-
|
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
|
-
|
55
|
-
db_tag =
|
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.
|
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
|
-
|
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
|
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.
|
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
|
-
|
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
|
-
|
324
|
-
toolkit.__toolkit_spec__.
|
325
|
-
|
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
|
-
|
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",
|