ibm-watsonx-orchestrate 1.5.1__py3-none-any.whl → 1.6.0__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 +1 -1
- ibm_watsonx_orchestrate/agent_builder/agents/__init__.py +1 -1
- ibm_watsonx_orchestrate/agent_builder/agents/agent.py +1 -0
- ibm_watsonx_orchestrate/agent_builder/agents/types.py +58 -4
- ibm_watsonx_orchestrate/agent_builder/agents/webchat_customizations/__init__.py +2 -0
- ibm_watsonx_orchestrate/agent_builder/agents/webchat_customizations/prompts.py +34 -0
- ibm_watsonx_orchestrate/agent_builder/agents/webchat_customizations/welcome_content.py +20 -0
- ibm_watsonx_orchestrate/agent_builder/connections/__init__.py +2 -2
- ibm_watsonx_orchestrate/agent_builder/connections/types.py +38 -31
- ibm_watsonx_orchestrate/agent_builder/model_policies/types.py +1 -1
- ibm_watsonx_orchestrate/agent_builder/models/types.py +0 -1
- ibm_watsonx_orchestrate/agent_builder/tools/flow_tool.py +83 -0
- ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +41 -3
- ibm_watsonx_orchestrate/agent_builder/tools/python_tool.py +2 -1
- ibm_watsonx_orchestrate/agent_builder/tools/types.py +14 -1
- ibm_watsonx_orchestrate/cli/commands/agents/agents_command.py +18 -1
- ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +153 -21
- ibm_watsonx_orchestrate/cli/commands/channels/webchat/channels_webchat_controller.py +104 -22
- ibm_watsonx_orchestrate/cli/commands/chat/chat_command.py +2 -0
- ibm_watsonx_orchestrate/cli/commands/connections/connections_command.py +26 -18
- ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +61 -61
- ibm_watsonx_orchestrate/cli/commands/environment/environment_command.py +29 -4
- ibm_watsonx_orchestrate/cli/commands/environment/environment_controller.py +74 -8
- ibm_watsonx_orchestrate/cli/commands/environment/types.py +1 -0
- ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_command.py +312 -0
- ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_controller.py +171 -0
- ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_command.py +2 -2
- ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +2 -2
- ibm_watsonx_orchestrate/cli/commands/models/model_provider_mapper.py +31 -25
- ibm_watsonx_orchestrate/cli/commands/models/models_command.py +6 -6
- ibm_watsonx_orchestrate/cli/commands/models/models_controller.py +17 -8
- ibm_watsonx_orchestrate/cli/commands/server/server_command.py +147 -21
- ibm_watsonx_orchestrate/cli/commands/server/types.py +2 -1
- ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_controller.py +9 -6
- ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +111 -32
- ibm_watsonx_orchestrate/cli/config.py +2 -0
- ibm_watsonx_orchestrate/cli/main.py +6 -0
- ibm_watsonx_orchestrate/client/agents/agent_client.py +83 -9
- ibm_watsonx_orchestrate/client/agents/assistant_agent_client.py +3 -3
- ibm_watsonx_orchestrate/client/agents/external_agent_client.py +2 -2
- ibm_watsonx_orchestrate/client/base_api_client.py +11 -10
- ibm_watsonx_orchestrate/client/connections/connections_client.py +49 -14
- ibm_watsonx_orchestrate/client/connections/utils.py +4 -2
- ibm_watsonx_orchestrate/client/credentials.py +4 -0
- ibm_watsonx_orchestrate/client/local_service_instance.py +1 -1
- ibm_watsonx_orchestrate/client/model_policies/model_policies_client.py +2 -2
- ibm_watsonx_orchestrate/client/service_instance.py +42 -1
- ibm_watsonx_orchestrate/client/tools/tempus_client.py +8 -3
- ibm_watsonx_orchestrate/client/utils.py +37 -2
- ibm_watsonx_orchestrate/docker/compose-lite.yml +252 -81
- ibm_watsonx_orchestrate/docker/default.env +40 -15
- ibm_watsonx_orchestrate/docker/proxy-config-single.yaml +12 -0
- ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/flows/__init__.py +3 -2
- ibm_watsonx_orchestrate/flow_builder/flows/decorators.py +77 -0
- ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/flows/events.py +6 -1
- ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/flows/flow.py +85 -92
- ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/types.py +15 -6
- ibm_watsonx_orchestrate/flow_builder/utils.py +215 -0
- ibm_watsonx_orchestrate/run/connections.py +4 -4
- {ibm_watsonx_orchestrate-1.5.1.dist-info → ibm_watsonx_orchestrate-1.6.0.dist-info}/METADATA +2 -1
- {ibm_watsonx_orchestrate-1.5.1.dist-info → ibm_watsonx_orchestrate-1.6.0.dist-info}/RECORD +69 -62
- ibm_watsonx_orchestrate/experimental/flow_builder/flows/decorators.py +0 -144
- ibm_watsonx_orchestrate/experimental/flow_builder/utils.py +0 -115
- /ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/__init__.py +0 -0
- /ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/data_map.py +0 -0
- /ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/flows/constants.py +0 -0
- /ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/node.py +0 -0
- /ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/resources/flow_status.openapi.yml +0 -0
- {ibm_watsonx_orchestrate-1.5.1.dist-info → ibm_watsonx_orchestrate-1.6.0.dist-info}/WHEEL +0 -0
- {ibm_watsonx_orchestrate-1.5.1.dist-info → ibm_watsonx_orchestrate-1.6.0.dist-info}/entry_points.txt +0 -0
- {ibm_watsonx_orchestrate-1.5.1.dist-info → ibm_watsonx_orchestrate-1.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -18,7 +18,7 @@ from ibm_watsonx_orchestrate.agent_builder.model_policies.types import ModelPoli
|
|
18
18
|
ModelPolicyRetry, ModelPolicyStrategy, ModelPolicyStrategyMode, ModelPolicyTarget
|
19
19
|
from ibm_watsonx_orchestrate.client.models.models_client import ModelsClient
|
20
20
|
from ibm_watsonx_orchestrate.agent_builder.models.types import VirtualModel, ProviderConfig, ModelType, ANTHROPIC_DEFAULT_MAX_TOKENS
|
21
|
-
from ibm_watsonx_orchestrate.client.utils import instantiate_client
|
21
|
+
from ibm_watsonx_orchestrate.client.utils import instantiate_client, is_cpd_env
|
22
22
|
from ibm_watsonx_orchestrate.client.connections import get_connection_id, ConnectionType
|
23
23
|
|
24
24
|
logger = logging.getLogger(__name__)
|
@@ -85,6 +85,11 @@ def import_python_policy(file: str) -> List[ModelPolicy]:
|
|
85
85
|
models.append(obj)
|
86
86
|
return models
|
87
87
|
|
88
|
+
def validate_spec_content(content: dict) -> None:
|
89
|
+
if not content.get("spec_version"):
|
90
|
+
logger.error(f"Field 'spec_version' not provided. Please ensure provided spec conforms to a valid spec format")
|
91
|
+
sys.exit(1)
|
92
|
+
|
88
93
|
def parse_model_file(file: str) -> List[VirtualModel]:
|
89
94
|
if file.endswith('.yaml') or file.endswith('.yml') or file.endswith(".json"):
|
90
95
|
with open(file, 'r') as f:
|
@@ -92,6 +97,7 @@ def parse_model_file(file: str) -> List[VirtualModel]:
|
|
92
97
|
content = json.load(f)
|
93
98
|
else:
|
94
99
|
content = yaml.load(f, Loader=yaml.SafeLoader)
|
100
|
+
validate_spec_content(content)
|
95
101
|
model = create_model_from_spec(spec=content)
|
96
102
|
return [model]
|
97
103
|
elif file.endswith('.py'):
|
@@ -107,6 +113,7 @@ def parse_policy_file(file: str) -> List[ModelPolicy]:
|
|
107
113
|
content = json.load(f)
|
108
114
|
else:
|
109
115
|
content = yaml.load(f, Loader=yaml.SafeLoader)
|
116
|
+
validate_spec_content(content)
|
110
117
|
policy = create_policy_from_spec(spec=content)
|
111
118
|
return [policy]
|
112
119
|
elif file.endswith('.py'):
|
@@ -160,13 +167,15 @@ class ModelsController:
|
|
160
167
|
logger.error("Error: WATSONX_URL is required in the environment.")
|
161
168
|
sys.exit(1)
|
162
169
|
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
170
|
+
if is_cpd_env(models_client.base_url):
|
171
|
+
virtual_models = []
|
172
|
+
virtual_model_policies = []
|
173
|
+
else:
|
174
|
+
logger.info("Retrieving virtual-model models list...")
|
175
|
+
virtual_models = models_client.list()
|
167
176
|
|
168
|
-
|
169
|
-
|
177
|
+
logger.info("Retrieving virtual-policies models list...")
|
178
|
+
virtual_model_policies = model_policies_client.list()
|
170
179
|
|
171
180
|
logger.info("Retrieving watsonx.ai models list...")
|
172
181
|
found_models = _get_wxai_foundational_models()
|
@@ -382,7 +391,7 @@ class ModelsController:
|
|
382
391
|
mode=strategy,
|
383
392
|
on_status_codes=strategy_on_code
|
384
393
|
)
|
385
|
-
inner.targets = [ModelPolicyTarget(
|
394
|
+
inner.targets = [ModelPolicyTarget(model_name=m) for m in models]
|
386
395
|
if retry_on_code:
|
387
396
|
inner.retry = ModelPolicyRetry(
|
388
397
|
on_status_codes=retry_on_code,
|
@@ -67,23 +67,28 @@ def docker_login(api_key: str, registry_url: str, username:str = "iamapikey") ->
|
|
67
67
|
logger.info("Successfully logged in to Docker.")
|
68
68
|
|
69
69
|
def docker_login_by_dev_edition_source(env_dict: dict, source: str) -> None:
|
70
|
-
if
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
70
|
+
if env_dict.get('WO_DEVELOPER_EDITION_SKIP_LOGIN', None) == 'true':
|
71
|
+
logger.info('WO_DEVELOPER_EDITION_SKIP_LOGIN is set to true, skipping login.')
|
72
|
+
logger.warning('If the developer edition images are not already pulled this call will fail without first setting WO_DEVELOPER_EDITION_SKIP_LOGIN=false')
|
73
|
+
else:
|
74
|
+
if not env_dict.get("REGISTRY_URL"):
|
75
|
+
raise ValueError("REGISTRY_URL is not set.")
|
76
|
+
registry_url = env_dict["REGISTRY_URL"].split("/")[0]
|
77
|
+
if source == "internal":
|
78
|
+
iam_api_key = env_dict.get("DOCKER_IAM_KEY")
|
79
|
+
if not iam_api_key:
|
80
|
+
raise ValueError("DOCKER_IAM_KEY is required in the environment file if WO_DEVELOPER_EDITION_SOURCE is set to 'internal'.")
|
81
|
+
docker_login(iam_api_key, registry_url, "iamapikey")
|
82
|
+
elif source == "myibm":
|
83
|
+
wo_entitlement_key = env_dict.get("WO_ENTITLEMENT_KEY")
|
84
|
+
if not wo_entitlement_key:
|
85
|
+
raise ValueError("WO_ENTITLEMENT_KEY is required in the environment file.")
|
86
|
+
docker_login(wo_entitlement_key, registry_url, "cp")
|
87
|
+
elif source == "orchestrate":
|
88
|
+
wo_auth_type = env_dict.get("WO_AUTH_TYPE")
|
89
|
+
api_key, username = get_docker_cred_by_wo_auth_type(env_dict, wo_auth_type)
|
90
|
+
docker_login(api_key, registry_url, username)
|
91
|
+
|
87
92
|
|
88
93
|
def get_compose_file() -> Path:
|
89
94
|
with resources.as_file(
|
@@ -165,6 +170,8 @@ def get_docker_cred_by_wo_auth_type(env_dict: dict, auth_type: str | None) -> tu
|
|
165
170
|
auth_type = "ibm_iam"
|
166
171
|
elif ".ibm.com" in instance_url:
|
167
172
|
auth_type = "mcsp"
|
173
|
+
elif "https://cpd" in instance_url:
|
174
|
+
auth_type = "cpd"
|
168
175
|
|
169
176
|
if auth_type in {"mcsp", "ibm_iam"}:
|
170
177
|
wo_api_key = env_dict.get("WO_API_KEY")
|
@@ -233,6 +240,27 @@ def apply_llm_api_key_defaults(env_dict: dict) -> None:
|
|
233
240
|
env_dict.setdefault("ASSISTANT_EMBEDDINGS_SPACE_ID", space_value)
|
234
241
|
env_dict.setdefault("ROUTING_LLM_SPACE_ID", space_value)
|
235
242
|
|
243
|
+
def _is_docker_container_running(container_name):
|
244
|
+
ensure_docker_installed()
|
245
|
+
command = [ "docker",
|
246
|
+
"ps",
|
247
|
+
"-f",
|
248
|
+
f"name={container_name}"
|
249
|
+
]
|
250
|
+
result = subprocess.run(command, env=os.environ, capture_output=True)
|
251
|
+
if container_name in str(result.stdout):
|
252
|
+
return True
|
253
|
+
return False
|
254
|
+
|
255
|
+
def _check_exclusive_observibility(langfuse_enabled: bool, ibm_tele_enabled: bool):
|
256
|
+
if langfuse_enabled and ibm_tele_enabled:
|
257
|
+
return False
|
258
|
+
if langfuse_enabled and _is_docker_container_running("docker-frontend-server-1"):
|
259
|
+
return False
|
260
|
+
if ibm_tele_enabled and _is_docker_container_running("docker-langfuse-web-1"):
|
261
|
+
return False
|
262
|
+
return True
|
263
|
+
|
236
264
|
def write_merged_env_file(merged_env: dict) -> Path:
|
237
265
|
tmp = tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".env")
|
238
266
|
with tmp:
|
@@ -285,7 +313,8 @@ def get_persisted_user_env() -> dict | None:
|
|
285
313
|
user_env = cfg.get(USER_ENV_CACHE_HEADER) if cfg.get(USER_ENV_CACHE_HEADER) else None
|
286
314
|
return user_env
|
287
315
|
|
288
|
-
|
316
|
+
|
317
|
+
def run_compose_lite(final_env_file: Path, experimental_with_langfuse=False, experimental_with_ibm_telemetry=False) -> None:
|
289
318
|
compose_path = get_compose_file()
|
290
319
|
compose_command = ensure_docker_compose_installed()
|
291
320
|
db_tag = read_env_file(final_env_file).get('DBTAG', None)
|
@@ -317,9 +346,15 @@ def run_compose_lite(final_env_file: Path, experimental_with_langfuse=False) ->
|
|
317
346
|
'--profile',
|
318
347
|
'langfuse'
|
319
348
|
]
|
349
|
+
elif experimental_with_ibm_telemetry:
|
350
|
+
command = compose_command + [
|
351
|
+
'--profile',
|
352
|
+
'ibm-telemetry'
|
353
|
+
]
|
320
354
|
else:
|
321
355
|
command = compose_command
|
322
356
|
|
357
|
+
|
323
358
|
command += [
|
324
359
|
"-f", str(compose_path),
|
325
360
|
"--env-file", str(final_env_file),
|
@@ -437,6 +472,9 @@ def run_compose_lite_ui(user_env_file: Path) -> bool:
|
|
437
472
|
# do nothing, as the docker login here is not mandatory
|
438
473
|
pass
|
439
474
|
|
475
|
+
# Auto-configure callback IP for async tools
|
476
|
+
merged_env_dict = auto_configure_callback_ip(merged_env_dict)
|
477
|
+
|
440
478
|
#These are to removed warning and not used in UI component
|
441
479
|
if not 'WATSONX_SPACE_ID' in merged_env_dict:
|
442
480
|
merged_env_dict['WATSONX_SPACE_ID']='X'
|
@@ -614,8 +652,80 @@ def confirm_accepts_license_agreement(accepts_by_argument: bool):
|
|
614
652
|
logger.error('The terms and conditions were not accepted, exiting.')
|
615
653
|
exit(1)
|
616
654
|
|
617
|
-
|
618
|
-
|
655
|
+
def auto_configure_callback_ip(merged_env_dict: dict) -> dict:
|
656
|
+
"""
|
657
|
+
Automatically detect and configure CALLBACK_HOST_URL if it's empty.
|
658
|
+
|
659
|
+
Args:
|
660
|
+
merged_env_dict: The merged environment dictionary
|
661
|
+
|
662
|
+
Returns:
|
663
|
+
Updated environment dictionary with CALLBACK_HOST_URL set
|
664
|
+
"""
|
665
|
+
callback_url = merged_env_dict.get('CALLBACK_HOST_URL', '').strip()
|
666
|
+
|
667
|
+
# Only auto-configure if CALLBACK_HOST_URL is empty
|
668
|
+
if not callback_url:
|
669
|
+
logger.info("Auto-detecting local IP address for async tool callbacks...")
|
670
|
+
|
671
|
+
system = platform.system()
|
672
|
+
ip = None
|
673
|
+
|
674
|
+
try:
|
675
|
+
if system in ("Linux", "Darwin"):
|
676
|
+
result = subprocess.run(["ifconfig"], capture_output=True, text=True, check=True)
|
677
|
+
lines = result.stdout.splitlines()
|
678
|
+
|
679
|
+
for line in lines:
|
680
|
+
line = line.strip()
|
681
|
+
# Unix ifconfig output format: "inet 192.168.1.100 netmask 0xffffff00 broadcast 192.168.1.255"
|
682
|
+
if line.startswith("inet ") and "127.0.0.1" not in line:
|
683
|
+
candidate_ip = line.split()[1]
|
684
|
+
# Validate IP is not loopback or link-local
|
685
|
+
if (candidate_ip and
|
686
|
+
not candidate_ip.startswith("127.") and
|
687
|
+
not candidate_ip.startswith("169.254")):
|
688
|
+
ip = candidate_ip
|
689
|
+
break
|
690
|
+
|
691
|
+
elif system == "Windows":
|
692
|
+
result = subprocess.run(["ipconfig"], capture_output=True, text=True, check=True)
|
693
|
+
lines = result.stdout.splitlines()
|
694
|
+
|
695
|
+
for line in lines:
|
696
|
+
line = line.strip()
|
697
|
+
# Windows ipconfig output format: " IPv4 Address. . . . . . . . . . . : 192.168.1.100"
|
698
|
+
if "IPv4 Address" in line and ":" in line:
|
699
|
+
candidate_ip = line.split(":")[-1].strip()
|
700
|
+
# Validate IP is not loopback or link-local
|
701
|
+
if (candidate_ip and
|
702
|
+
not candidate_ip.startswith("127.") and
|
703
|
+
not candidate_ip.startswith("169.254")):
|
704
|
+
ip = candidate_ip
|
705
|
+
break
|
706
|
+
|
707
|
+
else:
|
708
|
+
logger.warning(f"Unsupported platform: {system}")
|
709
|
+
ip = None
|
710
|
+
|
711
|
+
except Exception as e:
|
712
|
+
logger.debug(f"IP detection failed on {system}: {e}")
|
713
|
+
ip = None
|
714
|
+
|
715
|
+
if ip:
|
716
|
+
callback_url = f"http://{ip}:4321"
|
717
|
+
merged_env_dict['CALLBACK_HOST_URL'] = callback_url
|
718
|
+
logger.info(f"Auto-configured CALLBACK_HOST_URL to: {callback_url}")
|
719
|
+
else:
|
720
|
+
# Fallback for localhost
|
721
|
+
callback_url = "http://host.docker.internal:4321"
|
722
|
+
merged_env_dict['CALLBACK_HOST_URL'] = callback_url
|
723
|
+
logger.info(f"Using Docker internal URL: {callback_url}")
|
724
|
+
logger.info("For external tools, consider using ngrok or similar tunneling service.")
|
725
|
+
else:
|
726
|
+
logger.info(f"Using existing CALLBACK_HOST_URL: {callback_url}")
|
727
|
+
|
728
|
+
return merged_env_dict
|
619
729
|
|
620
730
|
@server_app.command(name="start")
|
621
731
|
def server_start(
|
@@ -629,6 +739,11 @@ def server_start(
|
|
629
739
|
'--with-langfuse', '-l',
|
630
740
|
help='Option to enable Langfuse support.'
|
631
741
|
),
|
742
|
+
experimental_with_ibm_telemetry: bool = typer.Option(
|
743
|
+
False,
|
744
|
+
'--with-ibm-telemetry', '-i',
|
745
|
+
help=''
|
746
|
+
),
|
632
747
|
persist_env_secrets: bool = typer.Option(
|
633
748
|
False,
|
634
749
|
'--persist-env-secrets', '-p',
|
@@ -668,9 +783,18 @@ def server_start(
|
|
668
783
|
|
669
784
|
merged_env_dict = apply_server_env_dict_defaults(merged_env_dict)
|
670
785
|
|
786
|
+
# Auto-configure callback IP for async tools
|
787
|
+
merged_env_dict = auto_configure_callback_ip(merged_env_dict)
|
788
|
+
if not _check_exclusive_observibility(experimental_with_langfuse, experimental_with_ibm_telemetry):
|
789
|
+
logger.error("Please select either langfuse or ibm telemetry for observability not both")
|
790
|
+
sys.exit(1)
|
791
|
+
|
671
792
|
# Add LANGFUSE_ENABLED into the merged_env_dict, for tempus to pick up.
|
672
793
|
if experimental_with_langfuse:
|
673
794
|
merged_env_dict['LANGFUSE_ENABLED'] = 'true'
|
795
|
+
|
796
|
+
if experimental_with_ibm_telemetry:
|
797
|
+
merged_env_dict['USE_IBM_TELEMETRY'] = 'true'
|
674
798
|
|
675
799
|
try:
|
676
800
|
docker_login_by_dev_edition_source(merged_env_dict, dev_edition_source)
|
@@ -682,7 +806,9 @@ def server_start(
|
|
682
806
|
|
683
807
|
|
684
808
|
final_env_file = write_merged_env_file(merged_env_dict)
|
685
|
-
run_compose_lite(final_env_file=final_env_file,
|
809
|
+
run_compose_lite(final_env_file=final_env_file,
|
810
|
+
experimental_with_langfuse=experimental_with_langfuse,
|
811
|
+
experimental_with_ibm_telemetry=experimental_with_ibm_telemetry)
|
686
812
|
|
687
813
|
run_db_migration()
|
688
814
|
|
@@ -26,7 +26,8 @@ def _infer_auth_type_from_instance_url(instance_url: str) -> WoAuthType:
|
|
26
26
|
return WoAuthType.IBM_IAM
|
27
27
|
if ".ibm.com" in instance_url:
|
28
28
|
return WoAuthType.MCSP
|
29
|
-
|
29
|
+
if "https://cpd" in instance_url:
|
30
|
+
return WoAuthType.CPD
|
30
31
|
|
31
32
|
|
32
33
|
class WatsonXAIEnvConfig(BaseModel):
|
@@ -213,9 +213,6 @@ class ToolkitController:
|
|
213
213
|
|
214
214
|
|
215
215
|
def remove_toolkit(self, name: str):
|
216
|
-
if not is_local_dev():
|
217
|
-
logger.error("This functionality is only available for Local Environments")
|
218
|
-
sys.exit(1)
|
219
216
|
try:
|
220
217
|
client = self.get_client()
|
221
218
|
draft_toolkits = client.get_draft_by_name(toolkit_name=name)
|
@@ -246,9 +243,15 @@ class ToolkitController:
|
|
246
243
|
rich.print(JSON(json.dumps(tools_list, indent=4)))
|
247
244
|
else:
|
248
245
|
table = rich.table.Table(show_header=True, header_style="bold white", show_lines=True)
|
249
|
-
|
250
|
-
|
251
|
-
|
246
|
+
column_args = {
|
247
|
+
"Name": {"overflow": "fold"},
|
248
|
+
"Kind": {},
|
249
|
+
"Description": {},
|
250
|
+
"Tools": {},
|
251
|
+
"App ID": {"overflow": "fold"}
|
252
|
+
}
|
253
|
+
for column in column_args:
|
254
|
+
table.add_column(column,**column_args[column])
|
252
255
|
|
253
256
|
tools_client = instantiate_client(ToolClient)
|
254
257
|
|
@@ -24,6 +24,7 @@ from rich.console import Console
|
|
24
24
|
from rich.panel import Panel
|
25
25
|
|
26
26
|
from ibm_watsonx_orchestrate.agent_builder.tools import BaseTool, ToolSpec
|
27
|
+
from ibm_watsonx_orchestrate.agent_builder.tools.flow_tool import create_flow_json_tool
|
27
28
|
from ibm_watsonx_orchestrate.agent_builder.tools.openapi_tool import create_openapi_json_tools_from_uri,create_openapi_json_tools_from_content
|
28
29
|
from ibm_watsonx_orchestrate.cli.commands.models.models_controller import ModelHighlighter
|
29
30
|
from ibm_watsonx_orchestrate.cli.commands.tools.types import RegistryType
|
@@ -33,15 +34,14 @@ from ibm_watsonx_orchestrate.cli.config import Config, CONTEXT_SECTION_HEADER, C
|
|
33
34
|
PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_TYPE_OPT, PYTHON_REGISTRY_TEST_PACKAGE_VERSION_OVERRIDE_OPT, \
|
34
35
|
DEFAULT_CONFIG_FILE_CONTENT
|
35
36
|
from ibm_watsonx_orchestrate.agent_builder.connections import ConnectionSecurityScheme, ExpectedCredentials
|
36
|
-
from ibm_watsonx_orchestrate.
|
37
|
+
from ibm_watsonx_orchestrate.flow_builder.flows.decorators import FlowWrapper
|
37
38
|
from ibm_watsonx_orchestrate.client.tools.tool_client import ToolClient
|
38
39
|
from ibm_watsonx_orchestrate.client.toolkit.toolkit_client import ToolKitClient
|
39
40
|
from ibm_watsonx_orchestrate.client.connections import get_connections_client, get_connection_type
|
40
41
|
from ibm_watsonx_orchestrate.client.utils import instantiate_client, is_local_dev
|
42
|
+
from ibm_watsonx_orchestrate.flow_builder.utils import import_flow_support_tools
|
41
43
|
from ibm_watsonx_orchestrate.utils.utils import sanatize_app_id
|
42
44
|
from ibm_watsonx_orchestrate.client.utils import is_local_dev
|
43
|
-
from ibm_watsonx_orchestrate.client.tools.tempus_client import TempusClient
|
44
|
-
from ibm_watsonx_orchestrate.experimental.flow_builder.utils import import_flow_model
|
45
45
|
|
46
46
|
from ibm_watsonx_orchestrate import __version__
|
47
47
|
|
@@ -57,6 +57,12 @@ class ToolKind(str, Enum):
|
|
57
57
|
flow = "flow"
|
58
58
|
# skill = "skill"
|
59
59
|
|
60
|
+
def _get_connection_environments() -> List[ConnectionEnvironment]:
|
61
|
+
if is_local_dev():
|
62
|
+
return [ConnectionEnvironment.DRAFT]
|
63
|
+
else:
|
64
|
+
return [env.value for env in ConnectionEnvironment]
|
65
|
+
|
60
66
|
def validate_app_ids(kind: ToolKind, **args) -> None:
|
61
67
|
app_ids = args.get("app_id")
|
62
68
|
if not app_ids:
|
@@ -71,7 +77,14 @@ def validate_app_ids(kind: ToolKind, **args) -> None:
|
|
71
77
|
connections_client = get_connections_client()
|
72
78
|
|
73
79
|
imported_connections_list = connections_client.list()
|
74
|
-
imported_connections = {
|
80
|
+
imported_connections = {}
|
81
|
+
for conn in imported_connections_list:
|
82
|
+
app_id = conn.app_id
|
83
|
+
conn_env = conn.environment
|
84
|
+
if app_id in imported_connections:
|
85
|
+
imported_connections[app_id][conn_env] = conn
|
86
|
+
else:
|
87
|
+
imported_connections[app_id] = {conn_env: conn}
|
75
88
|
|
76
89
|
for app_id in app_ids:
|
77
90
|
if kind == ToolKind.python:
|
@@ -89,9 +102,23 @@ def validate_app_ids(kind: ToolKind, **args) -> None:
|
|
89
102
|
if app_id not in imported_connections:
|
90
103
|
logger.warning(f"No connection found for provided app-id '{app_id}'. Please create the connection using `orchestrate connections add`")
|
91
104
|
else:
|
92
|
-
|
93
|
-
|
94
|
-
|
105
|
+
environments = _get_connection_environments()
|
106
|
+
|
107
|
+
imported_connection = imported_connections.get(app_id)
|
108
|
+
|
109
|
+
for conn_environment in environments:
|
110
|
+
conn = imported_connection.get(conn_environment)
|
111
|
+
|
112
|
+
if conn is None or conn.security_scheme is None:
|
113
|
+
logger.error(f"Connection '{app_id}' is not configured in the '{conn_environment}' environment.")
|
114
|
+
if conn_environment == ConnectionEnvironment.DRAFT:
|
115
|
+
sys.exit(1)
|
116
|
+
logger.error("If you deploy this tool without setting the live configuration the tool will error during execution.")
|
117
|
+
continue
|
118
|
+
|
119
|
+
if kind == ToolKind.openapi and conn.security_scheme == ConnectionSecurityScheme.KEY_VALUE:
|
120
|
+
logger.error(f"Key value application connections can not be bound to an openapi tool")
|
121
|
+
exit(1)
|
95
122
|
|
96
123
|
def validate_params(kind: ToolKind, **args) -> None:
|
97
124
|
if kind in {"openapi", "python"} and args["file"] is None:
|
@@ -157,7 +184,14 @@ def validate_python_connections(tool: BaseTool):
|
|
157
184
|
|
158
185
|
provided_connections = list(connections.keys()) if connections else []
|
159
186
|
imported_connections_list = connections_client.list()
|
160
|
-
imported_connections = {
|
187
|
+
imported_connections = {}
|
188
|
+
for conn in imported_connections_list:
|
189
|
+
conn_id = conn.connection_id
|
190
|
+
conn_env = conn.environment
|
191
|
+
if conn_id in imported_connections:
|
192
|
+
imported_connections[conn_id][conn_env] = conn
|
193
|
+
else:
|
194
|
+
imported_connections[conn_id] = {conn_env: conn}
|
161
195
|
|
162
196
|
validation_failed = False
|
163
197
|
|
@@ -186,15 +220,28 @@ def validate_python_connections(tool: BaseTool):
|
|
186
220
|
|
187
221
|
connection_id = connections.get(sanatized_expected_tool_app_id)
|
188
222
|
imported_connection = imported_connections.get(connection_id)
|
189
|
-
imported_connection_auth_type = get_connection_type(security_scheme=imported_connection.security_scheme, auth_type=imported_connection.auth_type)
|
190
223
|
|
191
224
|
if connection_id and not imported_connection:
|
192
225
|
logger.error(f"The expected connection id '{connection_id}' does not match any known connection. This is likely caused by the connection being deleted. Please rec-reate the connection and re-import the tool")
|
193
226
|
validation_failed = True
|
227
|
+
|
228
|
+
environments = _get_connection_environments()
|
229
|
+
|
230
|
+
for conn_environment in environments:
|
231
|
+
conn = imported_connection.get(conn_environment)
|
232
|
+
conn_identifier = conn.app_id if conn is not None else connection_id
|
233
|
+
if conn is None or conn.security_scheme is None:
|
234
|
+
logger.error(f"Connection '{conn_identifier}' is not configured in the '{conn_environment}' environment.")
|
235
|
+
if conn_environment == ConnectionEnvironment.DRAFT:
|
236
|
+
sys.exit(1)
|
237
|
+
logger.error("If you deploy this tool without setting the live configuration the tool will error during execution.")
|
238
|
+
continue
|
194
239
|
|
195
|
-
|
196
|
-
|
197
|
-
|
240
|
+
imported_connection_auth_type = get_connection_type(security_scheme=conn.security_scheme, auth_type=conn.auth_type)
|
241
|
+
|
242
|
+
if conn and len(expected_tool_conn_types) and imported_connection_auth_type not in expected_tool_conn_types:
|
243
|
+
logger.error(f"The app-id '{conn.app_id}' is of type '{imported_connection_auth_type.value}' in the '{conn_environment}' environment. The tool '{tool.__tool_spec__.name}' accepts connections of the following types '{', '.join(expected_tool_conn_types)}'. Use `orchestrate connections list` to view current connections and use `orchestrate connections add` to create the relevent connection")
|
244
|
+
validation_failed = True
|
198
245
|
|
199
246
|
if validation_failed:
|
200
247
|
exit(1)
|
@@ -339,24 +386,18 @@ async def import_flow_tool(file: str) -> None:
|
|
339
386
|
theme = rich.theme.Theme({"model.name": "bold cyan"})
|
340
387
|
console = rich.console.Console(highlighter=ModelHighlighter(), theme=theme)
|
341
388
|
|
342
|
-
message = f"""[bold cyan]Flow Tools
|
389
|
+
message = f"""[bold cyan]Flow Tools[/bold cyan]
|
343
390
|
|
344
391
|
The [bold]flow tool[/bold] is being imported from [green]`{file}`[/green].
|
345
392
|
|
346
393
|
[bold cyan]Additional information:[/bold cyan]
|
347
394
|
|
348
|
-
-
|
349
|
-
- The [bold green]get_flow_status[/bold green] tool is being imported to support flow tools. Ensure [bold]both this tools and the one you are importing are added to your agent[/bold] to retrieve the flow output.
|
350
|
-
- Include additional instructions in your agent to call the [bold green]get_flow_status[/bold green] tool to retrieve the flow output. For example: [green]"If you get an instance_id, use the tool get_flow_status to retrieve the current status of a flow."[/green]
|
395
|
+
- The [bold green]Get flow status[/bold green] tool is being imported to support flow tools. This tool can query the status of a flow tool instance. You can add it to your agent using the UI or including the following tool name in your agent definition: [green]i__get_flow_status_intrinsic_tool__[/green].
|
351
396
|
|
352
397
|
"""
|
353
398
|
|
354
399
|
console.print(Panel(message, title="[bold blue]Flow tool support information[/bold blue]", border_style="bright_blue"))
|
355
400
|
|
356
|
-
|
357
|
-
if not is_local_dev():
|
358
|
-
raise typer.BadParameter(f"Flow tools are only supported in local environment.")
|
359
|
-
|
360
401
|
model = None
|
361
402
|
|
362
403
|
# Load the Flow JSON model from the file
|
@@ -423,7 +464,16 @@ The [bold]flow tool[/bold] is being imported from [green]`{file}`[/green].
|
|
423
464
|
except Exception as e:
|
424
465
|
raise typer.BadParameter(f"Failed to load model from file {file}: {e}")
|
425
466
|
|
426
|
-
|
467
|
+
tool = create_flow_json_tool(name=model["spec"]["name"],
|
468
|
+
description=model["spec"]["description"],
|
469
|
+
permission="read_only",
|
470
|
+
flow_model=model)
|
471
|
+
|
472
|
+
tools = import_flow_support_tools()
|
473
|
+
|
474
|
+
tools.append(tool)
|
475
|
+
|
476
|
+
return tools
|
427
477
|
|
428
478
|
|
429
479
|
async def import_openapi_tool(file: str, connection_id: str) -> List[BaseTool]:
|
@@ -438,6 +488,10 @@ def _get_kind_from_spec(spec: dict) -> ToolKind:
|
|
438
488
|
return ToolKind.python
|
439
489
|
elif ToolKind.openapi in tool_binding:
|
440
490
|
return ToolKind.openapi
|
491
|
+
elif ToolKind.mcp in tool_binding:
|
492
|
+
return ToolKind.mcp
|
493
|
+
elif 'wxflows' in tool_binding:
|
494
|
+
return ToolKind.flow
|
441
495
|
else:
|
442
496
|
logger.error(f"Could not determine 'kind' of tool '{name}'")
|
443
497
|
sys.exit(1)
|
@@ -500,7 +554,20 @@ class ToolsController:
|
|
500
554
|
|
501
555
|
def list_tools(self, verbose=False):
|
502
556
|
response = self.get_client().get()
|
503
|
-
tool_specs = [
|
557
|
+
tool_specs = []
|
558
|
+
parse_errors = []
|
559
|
+
|
560
|
+
for tool in response:
|
561
|
+
try:
|
562
|
+
tool_specs.append(ToolSpec.model_validate(tool))
|
563
|
+
except Exception as e:
|
564
|
+
name = tool.get('name', None)
|
565
|
+
parse_errors.append([
|
566
|
+
f"Tool '{name}' could not be parsed",
|
567
|
+
json.dumps(tool),
|
568
|
+
e
|
569
|
+
])
|
570
|
+
|
504
571
|
tools = [BaseTool(spec=spec) for spec in tool_specs]
|
505
572
|
|
506
573
|
if verbose:
|
@@ -511,9 +578,16 @@ class ToolsController:
|
|
511
578
|
rich.print(JSON(json.dumps(tools_list, indent=4)))
|
512
579
|
else:
|
513
580
|
table = rich.table.Table(show_header=True, header_style="bold white", show_lines=True)
|
514
|
-
|
515
|
-
|
516
|
-
|
581
|
+
column_args = {
|
582
|
+
"Name": {"overflow": "fold"},
|
583
|
+
"Description": {},
|
584
|
+
"Permission": {},
|
585
|
+
"Type": {},
|
586
|
+
"Toolkit": {},
|
587
|
+
"App ID": {"overflow": "fold"}
|
588
|
+
}
|
589
|
+
for column in column_args:
|
590
|
+
table.add_column(column,**column_args[column])
|
517
591
|
|
518
592
|
connections_client = get_connections_client()
|
519
593
|
connections = connections_client.list()
|
@@ -555,19 +629,20 @@ class ToolsController:
|
|
555
629
|
tool_type=ToolKind.openapi
|
556
630
|
elif tool_binding.mcp is not None:
|
557
631
|
tool_type=ToolKind.mcp
|
632
|
+
elif tool_binding.flow is not None:
|
633
|
+
tool_type=ToolKind.flow
|
558
634
|
else:
|
559
635
|
tool_type="Unknown"
|
560
636
|
|
561
637
|
toolkit_name = ""
|
562
638
|
|
563
|
-
if
|
639
|
+
if tool.__tool_spec__.toolkit_id:
|
564
640
|
toolkit_client = instantiate_client(ToolKitClient)
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
toolkit_name = str(toolkit)
|
641
|
+
toolkit = toolkit_client.get_draft_by_id(tool.__tool_spec__.toolkit_id)
|
642
|
+
if isinstance(toolkit, dict) and "name" in toolkit:
|
643
|
+
toolkit_name = toolkit["name"]
|
644
|
+
elif toolkit:
|
645
|
+
toolkit_name = str(toolkit)
|
571
646
|
|
572
647
|
|
573
648
|
table.add_row(
|
@@ -581,6 +656,10 @@ class ToolsController:
|
|
581
656
|
|
582
657
|
rich.print(table)
|
583
658
|
|
659
|
+
for error in parse_errors:
|
660
|
+
for l in error:
|
661
|
+
logger.error(l)
|
662
|
+
|
584
663
|
def get_all_tools(self) -> dict:
|
585
664
|
return {entry["name"]: entry["id"] for entry in self.get_client().get()}
|
586
665
|
|
@@ -26,6 +26,8 @@ ENV_WXO_URL_OPT = "wxo_url"
|
|
26
26
|
ENV_IAM_URL_OPT = "iam_url"
|
27
27
|
PROTECTED_ENV_NAME = "local"
|
28
28
|
ENV_AUTH_TYPE = "auth_type"
|
29
|
+
BYPASS_SSL = "bypass_ssl"
|
30
|
+
VERIFY = "verify"
|
29
31
|
ENV_ACCEPT_LICENSE = 'accepts_license_agreements'
|
30
32
|
|
31
33
|
DEFAULT_LOCAL_SERVICE_URL = "http://localhost:4321"
|
@@ -12,8 +12,13 @@ from ibm_watsonx_orchestrate.cli.commands.environment.environment_command import
|
|
12
12
|
from ibm_watsonx_orchestrate.cli.commands.channels.channels_command import channel_app
|
13
13
|
from ibm_watsonx_orchestrate.cli.commands.knowledge_bases.knowledge_bases_command import knowledge_bases_app
|
14
14
|
from ibm_watsonx_orchestrate.cli.commands.toolkit.toolkit_command import toolkits_app
|
15
|
+
from ibm_watsonx_orchestrate.cli.commands.evaluations.evaluations_command import evaluation_app
|
15
16
|
from ibm_watsonx_orchestrate.cli.init_helper import init_callback
|
16
17
|
|
18
|
+
import urllib3
|
19
|
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
20
|
+
|
21
|
+
|
17
22
|
app = typer.Typer(
|
18
23
|
no_args_is_help=True,
|
19
24
|
pretty_exceptions_enable=False,
|
@@ -30,6 +35,7 @@ app.add_typer(server_app, name="server", help='Manipulate your local Orchestrate
|
|
30
35
|
app.add_typer(chat_app, name="chat", help='Launch the chat ui for your local Developer Edition server [requires entitlement]')
|
31
36
|
app.add_typer(models_app, name="models", help='List the available large language models (llms) that can be used in your agent definitions')
|
32
37
|
app.add_typer(channel_app, name="channels", help="Configure channels where your agent can exist on (such as embedded webchat)")
|
38
|
+
app.add_typer(evaluation_app, name="evaluations", help='Evaluate the performance of your agents in your active env')
|
33
39
|
app.add_typer(settings_app, name="settings", help='Configure the settings for your active env')
|
34
40
|
|
35
41
|
if __name__ == "__main__":
|