ibm-watsonx-orchestrate 1.0.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 +28 -0
- ibm_watsonx_orchestrate/agent_builder/__init__.py +0 -0
- ibm_watsonx_orchestrate/agent_builder/agents/__init__.py +5 -0
- ibm_watsonx_orchestrate/agent_builder/agents/agent.py +27 -0
- ibm_watsonx_orchestrate/agent_builder/agents/assistant_agent.py +28 -0
- ibm_watsonx_orchestrate/agent_builder/agents/external_agent.py +28 -0
- ibm_watsonx_orchestrate/agent_builder/agents/types.py +204 -0
- ibm_watsonx_orchestrate/agent_builder/connections/__init__.py +27 -0
- ibm_watsonx_orchestrate/agent_builder/connections/connections.py +123 -0
- ibm_watsonx_orchestrate/agent_builder/connections/types.py +260 -0
- ibm_watsonx_orchestrate/agent_builder/knowledge_bases/knowledge_base.py +27 -0
- ibm_watsonx_orchestrate/agent_builder/knowledge_bases/knowledge_base_requests.py +59 -0
- ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +243 -0
- ibm_watsonx_orchestrate/agent_builder/tools/__init__.py +4 -0
- ibm_watsonx_orchestrate/agent_builder/tools/base_tool.py +36 -0
- ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +332 -0
- ibm_watsonx_orchestrate/agent_builder/tools/python_tool.py +195 -0
- ibm_watsonx_orchestrate/agent_builder/tools/types.py +162 -0
- ibm_watsonx_orchestrate/agent_builder/utils/__init__.py +0 -0
- ibm_watsonx_orchestrate/agent_builder/utils/pydantic_utils.py +149 -0
- ibm_watsonx_orchestrate/cli/__init__.py +0 -0
- ibm_watsonx_orchestrate/cli/commands/__init__.py +0 -0
- ibm_watsonx_orchestrate/cli/commands/agents/agents_command.py +192 -0
- ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +660 -0
- ibm_watsonx_orchestrate/cli/commands/channels/channels_command.py +15 -0
- ibm_watsonx_orchestrate/cli/commands/channels/channels_controller.py +16 -0
- ibm_watsonx_orchestrate/cli/commands/channels/types.py +15 -0
- ibm_watsonx_orchestrate/cli/commands/channels/webchat/channels_webchat_command.py +32 -0
- ibm_watsonx_orchestrate/cli/commands/channels/webchat/channels_webchat_controller.py +141 -0
- ibm_watsonx_orchestrate/cli/commands/chat/chat_command.py +43 -0
- ibm_watsonx_orchestrate/cli/commands/connections/connections_command.py +307 -0
- ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +517 -0
- ibm_watsonx_orchestrate/cli/commands/environment/environment_command.py +78 -0
- ibm_watsonx_orchestrate/cli/commands/environment/environment_controller.py +189 -0
- ibm_watsonx_orchestrate/cli/commands/environment/types.py +9 -0
- ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_command.py +79 -0
- ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +201 -0
- ibm_watsonx_orchestrate/cli/commands/login/login_command.py +17 -0
- ibm_watsonx_orchestrate/cli/commands/models/models_command.py +128 -0
- ibm_watsonx_orchestrate/cli/commands/server/server_command.py +623 -0
- ibm_watsonx_orchestrate/cli/commands/settings/__init__.py +0 -0
- ibm_watsonx_orchestrate/cli/commands/settings/observability/__init__.py +0 -0
- ibm_watsonx_orchestrate/cli/commands/settings/observability/langfuse/__init__.py +0 -0
- ibm_watsonx_orchestrate/cli/commands/settings/observability/langfuse/langfuse_command.py +175 -0
- ibm_watsonx_orchestrate/cli/commands/settings/observability/observability_command.py +11 -0
- ibm_watsonx_orchestrate/cli/commands/settings/settings_command.py +10 -0
- ibm_watsonx_orchestrate/cli/commands/tools/tools_command.py +85 -0
- ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +564 -0
- ibm_watsonx_orchestrate/cli/commands/tools/types.py +10 -0
- ibm_watsonx_orchestrate/cli/config.py +226 -0
- ibm_watsonx_orchestrate/cli/main.py +32 -0
- ibm_watsonx_orchestrate/client/__init__.py +0 -0
- ibm_watsonx_orchestrate/client/agents/agent_client.py +46 -0
- ibm_watsonx_orchestrate/client/agents/assistant_agent_client.py +38 -0
- ibm_watsonx_orchestrate/client/agents/external_agent_client.py +38 -0
- ibm_watsonx_orchestrate/client/analytics/__init__.py +0 -0
- ibm_watsonx_orchestrate/client/analytics/llm/__init__.py +0 -0
- ibm_watsonx_orchestrate/client/analytics/llm/analytics_llm_client.py +50 -0
- ibm_watsonx_orchestrate/client/base_api_client.py +113 -0
- ibm_watsonx_orchestrate/client/base_service_instance.py +10 -0
- ibm_watsonx_orchestrate/client/client.py +71 -0
- ibm_watsonx_orchestrate/client/client_errors.py +359 -0
- ibm_watsonx_orchestrate/client/connections/__init__.py +10 -0
- ibm_watsonx_orchestrate/client/connections/connections_client.py +162 -0
- ibm_watsonx_orchestrate/client/connections/utils.py +27 -0
- ibm_watsonx_orchestrate/client/credentials.py +123 -0
- ibm_watsonx_orchestrate/client/knowledge_bases/knowledge_base_client.py +46 -0
- ibm_watsonx_orchestrate/client/local_service_instance.py +91 -0
- ibm_watsonx_orchestrate/client/service_instance.py +73 -0
- ibm_watsonx_orchestrate/client/tools/tool_client.py +41 -0
- ibm_watsonx_orchestrate/client/utils.py +95 -0
- ibm_watsonx_orchestrate/docker/compose-lite.yml +595 -0
- ibm_watsonx_orchestrate/docker/default.env +125 -0
- ibm_watsonx_orchestrate/docker/sdk/ibm_watsonx_orchestrate-0.6.0-py3-none-any.whl +0 -0
- ibm_watsonx_orchestrate/docker/sdk/ibm_watsonx_orchestrate-0.6.0.tar.gz +0 -0
- ibm_watsonx_orchestrate/docker/start-up.sh +61 -0
- ibm_watsonx_orchestrate/docker/tempus/common-config.yaml +1 -0
- ibm_watsonx_orchestrate/run/__init__.py +0 -0
- ibm_watsonx_orchestrate/run/connections.py +40 -0
- ibm_watsonx_orchestrate/utils/__init__.py +0 -0
- ibm_watsonx_orchestrate/utils/logging/__init__.py +0 -0
- ibm_watsonx_orchestrate/utils/logging/logger.py +26 -0
- ibm_watsonx_orchestrate/utils/logging/logging.yaml +18 -0
- ibm_watsonx_orchestrate/utils/utils.py +15 -0
- ibm_watsonx_orchestrate-1.0.0.dist-info/METADATA +34 -0
- ibm_watsonx_orchestrate-1.0.0.dist-info/RECORD +89 -0
- ibm_watsonx_orchestrate-1.0.0.dist-info/WHEEL +4 -0
- ibm_watsonx_orchestrate-1.0.0.dist-info/entry_points.txt +2 -0
- ibm_watsonx_orchestrate-1.0.0.dist-info/licenses/LICENSE +22 -0
@@ -0,0 +1,189 @@
|
|
1
|
+
import logging
|
2
|
+
import rich
|
3
|
+
import jwt
|
4
|
+
import getpass
|
5
|
+
|
6
|
+
from ibm_watsonx_orchestrate.cli.commands.tools.types import RegistryType
|
7
|
+
from ibm_watsonx_orchestrate.cli.config import (
|
8
|
+
Config,
|
9
|
+
AUTH_CONFIG_FILE_FOLDER,
|
10
|
+
AUTH_CONFIG_FILE,
|
11
|
+
AUTH_SECTION_HEADER,
|
12
|
+
AUTH_MCSP_TOKEN_OPT,
|
13
|
+
AUTH_MCSP_TOKEN_EXPIRY_OPT,
|
14
|
+
CONTEXT_ACTIVE_ENV_OPT,
|
15
|
+
CONTEXT_SECTION_HEADER,
|
16
|
+
ENV_WXO_URL_OPT,
|
17
|
+
ENV_IAM_URL_OPT,
|
18
|
+
ENVIRONMENTS_SECTION_HEADER,
|
19
|
+
PROTECTED_ENV_NAME,
|
20
|
+
ENV_AUTH_TYPE, PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_TYPE_OPT, PYTHON_REGISTRY_TEST_PACKAGE_VERSION_OVERRIDE_OPT,
|
21
|
+
DEFAULT_CONFIG_FILE_CONTENT
|
22
|
+
)
|
23
|
+
from ibm_watsonx_orchestrate.client.client import Client
|
24
|
+
from ibm_watsonx_orchestrate.client.client_errors import ClientError
|
25
|
+
from ibm_watsonx_orchestrate.client.credentials import Credentials
|
26
|
+
from threading import Lock
|
27
|
+
from ibm_watsonx_orchestrate.client.utils import is_local_dev, check_token_validity
|
28
|
+
from ibm_watsonx_orchestrate.cli.commands.environment.types import EnvironmentAuthType
|
29
|
+
|
30
|
+
logger = logging.getLogger(__name__)
|
31
|
+
|
32
|
+
lock = Lock()
|
33
|
+
|
34
|
+
def _decode_token(token: str, is_local: bool = False) -> dict:
|
35
|
+
try:
|
36
|
+
claimset = jwt.decode(token, options={"verify_signature": False})
|
37
|
+
data = {AUTH_MCSP_TOKEN_OPT: token}
|
38
|
+
if not is_local:
|
39
|
+
data[AUTH_MCSP_TOKEN_EXPIRY_OPT] = claimset["exp"]
|
40
|
+
return data
|
41
|
+
except jwt.DecodeError as e:
|
42
|
+
logger.error("Invalid token format")
|
43
|
+
raise e
|
44
|
+
|
45
|
+
def _login(name: str, apikey: str = None) -> None:
|
46
|
+
cfg = Config()
|
47
|
+
auth_cfg = Config(AUTH_CONFIG_FILE_FOLDER, AUTH_CONFIG_FILE)
|
48
|
+
|
49
|
+
env_cfg = cfg.get(ENVIRONMENTS_SECTION_HEADER, name)
|
50
|
+
url = env_cfg.get(ENV_WXO_URL_OPT)
|
51
|
+
iam_url = env_cfg.get(ENV_IAM_URL_OPT)
|
52
|
+
is_local = name == PROTECTED_ENV_NAME or is_local_dev(url=url)
|
53
|
+
try:
|
54
|
+
auth_type = cfg.get(ENVIRONMENTS_SECTION_HEADER, name, ENV_AUTH_TYPE)
|
55
|
+
except (KeyError, AttributeError):
|
56
|
+
auth_type = None
|
57
|
+
|
58
|
+
|
59
|
+
if apikey is None and not is_local:
|
60
|
+
apikey = getpass.getpass("Please enter WXO API key: ")
|
61
|
+
|
62
|
+
try:
|
63
|
+
creds = Credentials(url=url, api_key=apikey, iam_url=iam_url, auth_type=auth_type)
|
64
|
+
client = Client(creds)
|
65
|
+
token = _decode_token(client.token, is_local)
|
66
|
+
with lock:
|
67
|
+
auth_cfg.save(
|
68
|
+
{
|
69
|
+
AUTH_SECTION_HEADER: {
|
70
|
+
name: token
|
71
|
+
},
|
72
|
+
}
|
73
|
+
)
|
74
|
+
except ClientError as e:
|
75
|
+
raise ClientError(e)
|
76
|
+
|
77
|
+
def activate(name: str, apikey: str=None, registry: RegistryType=None, test_package_version_override=None) -> None:
|
78
|
+
cfg = Config()
|
79
|
+
auth_cfg = Config(AUTH_CONFIG_FILE_FOLDER, AUTH_CONFIG_FILE)
|
80
|
+
env_cfg = cfg.read(ENVIRONMENTS_SECTION_HEADER, name)
|
81
|
+
url = cfg.get(ENVIRONMENTS_SECTION_HEADER, name, ENV_WXO_URL_OPT)
|
82
|
+
is_local = is_local_dev(url)
|
83
|
+
|
84
|
+
if not env_cfg:
|
85
|
+
logger.error(f"Environment '{name}' does not exist. Please create it with `orchestrate env add`")
|
86
|
+
return
|
87
|
+
elif not env_cfg.get(ENV_WXO_URL_OPT):
|
88
|
+
logger.error(f"Environment '{name}' is misconfigured. Please re-create it with `orchestrate env add`")
|
89
|
+
return
|
90
|
+
|
91
|
+
existing_auth_config = auth_cfg.get(AUTH_SECTION_HEADER).get(name, {})
|
92
|
+
existing_token = existing_auth_config.get(AUTH_MCSP_TOKEN_OPT) if existing_auth_config else None
|
93
|
+
|
94
|
+
if not check_token_validity(existing_token) or is_local:
|
95
|
+
_login(name=name, apikey=apikey)
|
96
|
+
|
97
|
+
with lock:
|
98
|
+
cfg.write(CONTEXT_SECTION_HEADER, CONTEXT_ACTIVE_ENV_OPT, name)
|
99
|
+
if registry is not None:
|
100
|
+
cfg.write(PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_TYPE_OPT, str(registry))
|
101
|
+
cfg.write(PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_TEST_PACKAGE_VERSION_OVERRIDE_OPT, test_package_version_override)
|
102
|
+
elif cfg.read(PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_TYPE_OPT) is None:
|
103
|
+
cfg.write(PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_TYPE_OPT, DEFAULT_CONFIG_FILE_CONTENT[PYTHON_REGISTRY_HEADER][PYTHON_REGISTRY_TYPE_OPT])
|
104
|
+
cfg.write(PYTHON_REGISTRY_HEADER, PYTHON_REGISTRY_TEST_PACKAGE_VERSION_OVERRIDE_OPT, test_package_version_override)
|
105
|
+
|
106
|
+
logger.info(f"Environment '{name}' is now active")
|
107
|
+
|
108
|
+
def add(name: str, url: str, should_activate: bool=False, iam_url: str=None, type: EnvironmentAuthType=None) -> None:
|
109
|
+
if name == PROTECTED_ENV_NAME:
|
110
|
+
logger.error(f"The name '{PROTECTED_ENV_NAME}' is a reserved environment name. Please select a diffrent name or use `orchestrate env activate {PROTECTED_ENV_NAME}` to swap to '{PROTECTED_ENV_NAME}'")
|
111
|
+
return
|
112
|
+
|
113
|
+
cfg = Config()
|
114
|
+
existing_env_cfg = cfg.read(ENVIRONMENTS_SECTION_HEADER, name)
|
115
|
+
if existing_env_cfg:
|
116
|
+
logger.info(f"Existing environment with name '{name}' found.")
|
117
|
+
update_response = input(f"Would you like to update the environment '{name}'? (Y/n)")
|
118
|
+
if update_response.lower() == "n":
|
119
|
+
logger.info(f"No changes made to environments")
|
120
|
+
return
|
121
|
+
with lock:
|
122
|
+
cfg.write(ENVIRONMENTS_SECTION_HEADER, name, {ENV_WXO_URL_OPT: url})
|
123
|
+
if iam_url:
|
124
|
+
cfg.write(ENVIRONMENTS_SECTION_HEADER, name, {ENV_IAM_URL_OPT: iam_url})
|
125
|
+
if type:
|
126
|
+
cfg.write(ENVIRONMENTS_SECTION_HEADER, name, {ENV_AUTH_TYPE: str(type)})
|
127
|
+
|
128
|
+
|
129
|
+
logger.info(f"Environment '{name}' has been created")
|
130
|
+
if should_activate:
|
131
|
+
activate(name)
|
132
|
+
|
133
|
+
def remove(name: str) -> None:
|
134
|
+
if name == PROTECTED_ENV_NAME:
|
135
|
+
logger.error(f"The environment '{PROTECTED_ENV_NAME}' is a default environment and cannot be removed.")
|
136
|
+
return
|
137
|
+
|
138
|
+
cfg = Config()
|
139
|
+
auth_cfg = Config(AUTH_CONFIG_FILE_FOLDER, AUTH_CONFIG_FILE)
|
140
|
+
active_env = cfg.read(CONTEXT_SECTION_HEADER, CONTEXT_ACTIVE_ENV_OPT)
|
141
|
+
existing_env_cfg = cfg.read(ENVIRONMENTS_SECTION_HEADER, name)
|
142
|
+
existing_auth_env_cfg = auth_cfg.read(AUTH_SECTION_HEADER, name)
|
143
|
+
|
144
|
+
if not existing_env_cfg:
|
145
|
+
logger.info(f"No environment named '{name}' exists")
|
146
|
+
return
|
147
|
+
|
148
|
+
if name == active_env:
|
149
|
+
remove_confirmation = input(f"The environment '{name}' is currently active, are you sure you wish to remove it? (y/N)")
|
150
|
+
if remove_confirmation.lower() != 'y':
|
151
|
+
logger.info("No changes made to environments")
|
152
|
+
return
|
153
|
+
cfg.write(CONTEXT_SECTION_HEADER, CONTEXT_ACTIVE_ENV_OPT, None)
|
154
|
+
|
155
|
+
cfg.delete(ENVIRONMENTS_SECTION_HEADER, name)
|
156
|
+
if existing_auth_env_cfg:
|
157
|
+
auth_cfg.delete(AUTH_SECTION_HEADER, name)
|
158
|
+
logger.info(f"Successfully removed environment '{name}'")
|
159
|
+
|
160
|
+
def list_envs() -> None:
|
161
|
+
cfg = Config()
|
162
|
+
active_env = cfg.read(CONTEXT_SECTION_HEADER, CONTEXT_ACTIVE_ENV_OPT)
|
163
|
+
envs = cfg.get(ENVIRONMENTS_SECTION_HEADER)
|
164
|
+
|
165
|
+
table = rich.table.Table(
|
166
|
+
show_header=False,
|
167
|
+
box=None
|
168
|
+
)
|
169
|
+
columns = ["Environment", "Url", ""]
|
170
|
+
for col in columns:
|
171
|
+
table.add_column(col)
|
172
|
+
|
173
|
+
# Make order active first followed alphabetically
|
174
|
+
env_names = []
|
175
|
+
if active_env:
|
176
|
+
env_names.append(active_env)
|
177
|
+
else:
|
178
|
+
logger.warning("No active environment is currently set. Use `orchestrate env activate` to set one")
|
179
|
+
envs_keys = list(envs.keys())
|
180
|
+
if active_env in envs_keys: envs_keys.remove(active_env)
|
181
|
+
envs_keys.sort()
|
182
|
+
env_names += envs_keys
|
183
|
+
|
184
|
+
for env in env_names:
|
185
|
+
active_tag = "[green](active)[/green]" if env == active_env else ""
|
186
|
+
table.add_row(env, envs.get(env, {}).get(ENV_WXO_URL_OPT, "N/A"), active_tag)
|
187
|
+
|
188
|
+
console = rich.console.Console()
|
189
|
+
console.print(table)
|
@@ -0,0 +1,79 @@
|
|
1
|
+
import typer
|
2
|
+
from typing_extensions import Annotated
|
3
|
+
from ibm_watsonx_orchestrate.cli.commands.knowledge_bases.knowledge_bases_controller import KnowledgeBaseController
|
4
|
+
|
5
|
+
knowledge_bases_app = typer.Typer(no_args_is_help=True)
|
6
|
+
|
7
|
+
|
8
|
+
@knowledge_bases_app.command(name="import", help="Import a knowledge-base by uploading documents, or providing an external vector index")
|
9
|
+
def knowledge_base_import(
|
10
|
+
file: Annotated[
|
11
|
+
str,
|
12
|
+
typer.Option("--file", "-f", help="YAML file with knowledge base definition"),
|
13
|
+
],
|
14
|
+
app_id: Annotated[
|
15
|
+
str, typer.Option(
|
16
|
+
'--app-id', '-a',
|
17
|
+
help='The app id of the connection to associate with this knowledge base. A application connection represents the authentication credentials needed to connection to the external Milvus or Elasticsearch instance (for example Api Keys, Basic, Bearer or OAuth credentials).'
|
18
|
+
)
|
19
|
+
] = None,
|
20
|
+
):
|
21
|
+
controller = KnowledgeBaseController()
|
22
|
+
controller.import_knowledge_base(file=file, app_id=app_id)
|
23
|
+
|
24
|
+
@knowledge_bases_app.command(name="patch", help="Patch a knowledge base by uploading documents, or providing an external vector index")
|
25
|
+
def knowledge_base_patch(
|
26
|
+
file: Annotated[
|
27
|
+
str,
|
28
|
+
typer.Option("--file", "-f", help="YAML file with knowledge base definition"),
|
29
|
+
],
|
30
|
+
name: Annotated[
|
31
|
+
str,
|
32
|
+
typer.Option("--name", "-n", help="Name of the knowledge base you wish to update"),
|
33
|
+
]=None,
|
34
|
+
id: Annotated[
|
35
|
+
str,
|
36
|
+
typer.Option("--id", "-i", help="ID of the knowledge base you wish to update"),
|
37
|
+
]=None
|
38
|
+
):
|
39
|
+
controller = KnowledgeBaseController()
|
40
|
+
controller.update_knowledge_base(id=id, name=name, file=file)
|
41
|
+
|
42
|
+
|
43
|
+
@knowledge_bases_app.command(name="list", help="List all knowledge bases")
|
44
|
+
def list_knowledge_bases(
|
45
|
+
verbose: Annotated[
|
46
|
+
bool,
|
47
|
+
typer.Option("--verbose", "-v", help="List full details of all knowledge bases in json format"),
|
48
|
+
] = False,
|
49
|
+
):
|
50
|
+
controller = KnowledgeBaseController()
|
51
|
+
controller.list_knowledge_bases(verbose=verbose)
|
52
|
+
|
53
|
+
@knowledge_bases_app.command(name="remove", help="Delete a knowlege base and all ingested documents")
|
54
|
+
def remove_knowledge_base(
|
55
|
+
name: Annotated[
|
56
|
+
str,
|
57
|
+
typer.Option("--name", "-n", help="Name of the knowledge base you wish to remove"),
|
58
|
+
]=None,
|
59
|
+
id: Annotated[
|
60
|
+
str,
|
61
|
+
typer.Option("--id", "-i", help="ID of the knowledge base you wish to remove"),
|
62
|
+
]=None
|
63
|
+
):
|
64
|
+
controller = KnowledgeBaseController()
|
65
|
+
controller.remove_knowledge_base(id=id, name=name)
|
66
|
+
|
67
|
+
@knowledge_bases_app.command(name="status", help="Get the status of a knowlege base")
|
68
|
+
def knowledge_base_status(
|
69
|
+
name: Annotated[
|
70
|
+
str,
|
71
|
+
typer.Option("--name", "-n", help="Name of the knowledge base you wish to get the status of"),
|
72
|
+
]=None,
|
73
|
+
id: Annotated[
|
74
|
+
str,
|
75
|
+
typer.Option("--id", "-i", help="ID of the knowledge base you wish to get the status of"),
|
76
|
+
]=None
|
77
|
+
):
|
78
|
+
controller = KnowledgeBaseController()
|
79
|
+
controller.knowledge_base_status(id=id, name=name)
|
@@ -0,0 +1,201 @@
|
|
1
|
+
import sys
|
2
|
+
import json
|
3
|
+
import rich
|
4
|
+
import requests
|
5
|
+
import logging
|
6
|
+
|
7
|
+
from ibm_watsonx_orchestrate.agent_builder.knowledge_bases.knowledge_base_requests import KnowledgeBaseCreateRequest, KnowledgeBaseUpdateRequest
|
8
|
+
from ibm_watsonx_orchestrate.agent_builder.knowledge_bases.knowledge_base import KnowledgeBase
|
9
|
+
from ibm_watsonx_orchestrate.client.knowledge_bases.knowledge_base_client import KnowledgeBaseClient
|
10
|
+
from ibm_watsonx_orchestrate.client.base_api_client import ClientAPIException
|
11
|
+
from ibm_watsonx_orchestrate.client.connections import get_connections_client
|
12
|
+
|
13
|
+
from ibm_watsonx_orchestrate.client.utils import instantiate_client
|
14
|
+
|
15
|
+
logger = logging.getLogger(__name__)
|
16
|
+
|
17
|
+
def to_column_name(col: str):
|
18
|
+
return " ".join([word.capitalize() if not word[0].isupper() else word for word in col.split("_")])
|
19
|
+
|
20
|
+
def get_file_name(path: str):
|
21
|
+
# This name prettifying currently screws up file type detection on ingestion
|
22
|
+
# return to_column_name(path.split("/")[-1].split(".")[0])
|
23
|
+
return path.split("/")[-1]
|
24
|
+
|
25
|
+
class KnowledgeBaseController:
|
26
|
+
def __init__(self):
|
27
|
+
self.client = None
|
28
|
+
self.connections_client = None
|
29
|
+
|
30
|
+
def get_client(self):
|
31
|
+
if not self.client:
|
32
|
+
self.client = instantiate_client(KnowledgeBaseClient)
|
33
|
+
return self.client
|
34
|
+
|
35
|
+
def import_knowledge_base(self, file: str, app_id: str):
|
36
|
+
client = self.get_client()
|
37
|
+
create_request = KnowledgeBaseCreateRequest.from_spec(file=file)
|
38
|
+
|
39
|
+
try:
|
40
|
+
if create_request.documents:
|
41
|
+
try:
|
42
|
+
file_dir = "/".join(file.split("/")[:-1])
|
43
|
+
files = [('files', (get_file_name(file_path), open(file_path if file_path.startswith("/") else f"{file_dir}/{file_path}", 'rb'))) for file_path in create_request.documents]
|
44
|
+
except Exception as e:
|
45
|
+
logger.error(f"Error importing knowledge base: {str(e).replace('[Errno 2] ', '')}")
|
46
|
+
sys.exit(1);
|
47
|
+
|
48
|
+
payload = create_request.model_dump(exclude_none=True);
|
49
|
+
payload.pop('documents');
|
50
|
+
|
51
|
+
client.create_built_in(payload=payload, files=files)
|
52
|
+
else:
|
53
|
+
if len(create_request.conversational_search_tool.index_config) != 1:
|
54
|
+
raise ValueError(f"Must provide exactly one conversational_search_tool.index_config. Provided {len(create_request.conversational_search_tool.index_config)}.")
|
55
|
+
|
56
|
+
if app_id:
|
57
|
+
connections_client = get_connections_client()
|
58
|
+
connection_id = None
|
59
|
+
if app_id is not None:
|
60
|
+
connections = connections_client.get_draft_by_app_id(app_id=app_id)
|
61
|
+
if not connections:
|
62
|
+
logger.error(f"No connection exists with the app-id '{app_id}'")
|
63
|
+
exit(1)
|
64
|
+
|
65
|
+
connection_id = connections.connection_id
|
66
|
+
create_request.conversational_search_tool.index_config[0].connection_id = connection_id
|
67
|
+
|
68
|
+
client.create(payload=create_request.model_dump(exclude_none=True))
|
69
|
+
|
70
|
+
logger.info(f"Successfully imported knowledge base '{create_request.name}'")
|
71
|
+
except ClientAPIException as e:
|
72
|
+
if "duplicate key value violates unique constraint" in e.response.text:
|
73
|
+
logger.error(f"A knowledge base with the name '{create_request.name}' already exists. Failed to import knowledge base")
|
74
|
+
else:
|
75
|
+
logger.error(f"Error importing knowledge base '{create_request.name}\n' {e.response.text}")
|
76
|
+
|
77
|
+
def get_id(
|
78
|
+
self, id: str, name: str
|
79
|
+
) -> str:
|
80
|
+
if id:
|
81
|
+
return id
|
82
|
+
|
83
|
+
if not name:
|
84
|
+
logger.error("Either 'id' or 'name' is required")
|
85
|
+
sys.exit(1)
|
86
|
+
|
87
|
+
response = self.get_client().get_by_name(name)
|
88
|
+
|
89
|
+
if not response:
|
90
|
+
logger.warning(f"No knowledge base '{name}' found")
|
91
|
+
sys.exit(1)
|
92
|
+
|
93
|
+
return response.get('id')
|
94
|
+
|
95
|
+
|
96
|
+
def update_knowledge_base(
|
97
|
+
self, id: str, name: str, file: str
|
98
|
+
) -> None:
|
99
|
+
knowledge_base_id = self.get_id(id, name)
|
100
|
+
update_request = KnowledgeBaseUpdateRequest.from_spec(file=file)
|
101
|
+
|
102
|
+
if update_request.documents:
|
103
|
+
file_dir = "/".join(file.split("/")[:-1])
|
104
|
+
files = [('files', (get_file_name(file_path), open(file_path if file_path.startswith("/") else f"{file_dir}/{file_path}", 'rb'))) for file_path in update_request.documents]
|
105
|
+
|
106
|
+
payload = update_request.model_dump(exclude_none=True);
|
107
|
+
payload.pop('documents');
|
108
|
+
|
109
|
+
self.get_client().update_with_documents(knowledge_base_id, payload=payload, files=files)
|
110
|
+
else:
|
111
|
+
self.get_client().update(knowledge_base_id, update_request.model_dump(exclude_none=True))
|
112
|
+
|
113
|
+
logEnding = f"with ID '{id}'" if id else f"'{name}'"
|
114
|
+
logger.info(f"Successfully updated knowledge base {logEnding}")
|
115
|
+
|
116
|
+
|
117
|
+
def knowledge_base_status( self, id: str, name: str) -> None:
|
118
|
+
knowledge_base_id = self.get_id(id, name)
|
119
|
+
response = self.get_client().status(knowledge_base_id)
|
120
|
+
|
121
|
+
if 'documents' in response:
|
122
|
+
response[f"documents ({len(response['documents'])})"] = ", ".join([str(doc.get('metadata', {}).get('original_file_name', '<Unnamed File>')) for doc in response['documents']])
|
123
|
+
response.pop('documents')
|
124
|
+
|
125
|
+
table = rich.table.Table(
|
126
|
+
show_header=True,
|
127
|
+
header_style="bold white",
|
128
|
+
show_lines=True
|
129
|
+
)
|
130
|
+
|
131
|
+
if "id" in response:
|
132
|
+
kbID = response["id"]
|
133
|
+
del response["id"]
|
134
|
+
|
135
|
+
response["id"] = kbID
|
136
|
+
|
137
|
+
[table.add_column(to_column_name(col), {}) for col in response.keys()]
|
138
|
+
table.add_row(*[str(val) for val in response.values()])
|
139
|
+
|
140
|
+
rich.print(table)
|
141
|
+
|
142
|
+
|
143
|
+
def list_knowledge_bases(self, verbose: bool=False):
|
144
|
+
response = self.get_client().get()
|
145
|
+
knowledge_bases = [KnowledgeBase.model_validate(knowledge_base) for knowledge_base in response]
|
146
|
+
|
147
|
+
knowledge_base_list = []
|
148
|
+
if verbose:
|
149
|
+
for kb in knowledge_bases:
|
150
|
+
knowledge_base_list.append(json.loads(kb.model_dump_json(exclude_none=True)))
|
151
|
+
rich.print(rich.json.JSON(json.dumps(knowledge_base_list, indent=4)))
|
152
|
+
else:
|
153
|
+
table = rich.table.Table(
|
154
|
+
show_header=True,
|
155
|
+
header_style="bold white",
|
156
|
+
show_lines=True
|
157
|
+
)
|
158
|
+
|
159
|
+
column_args = {
|
160
|
+
"Name": {},
|
161
|
+
"Description": {},
|
162
|
+
"App ID": {},
|
163
|
+
"ID": {}
|
164
|
+
}
|
165
|
+
|
166
|
+
for column in column_args:
|
167
|
+
table.add_column(column, **column_args[column])
|
168
|
+
|
169
|
+
for kb in knowledge_bases:
|
170
|
+
app_id = ""
|
171
|
+
|
172
|
+
if kb.conversational_search_tool is not None \
|
173
|
+
and kb.conversational_search_tool.index_config is not None \
|
174
|
+
and len(kb.conversational_search_tool.index_config) > 0 \
|
175
|
+
and kb.conversational_search_tool.index_config[0].connection_id is not None:
|
176
|
+
connections_client = get_connections_client()
|
177
|
+
app_id = str(connections_client.get_draft_by_id(kb.conversational_search_tool.index_config[0].connection_id))
|
178
|
+
|
179
|
+
table.add_row(
|
180
|
+
kb.name,
|
181
|
+
kb.description,
|
182
|
+
app_id,
|
183
|
+
str(kb.id)
|
184
|
+
)
|
185
|
+
|
186
|
+
rich.print(table)
|
187
|
+
|
188
|
+
|
189
|
+
def remove_knowledge_base(self, id: str, name: str):
|
190
|
+
knowledge_base_id = self.get_id(id, name)
|
191
|
+
logEnding = f"with ID '{id}'" if id else f"'{name}'"
|
192
|
+
|
193
|
+
try:
|
194
|
+
self.get_client().delete(knowledge_base_id=knowledge_base_id)
|
195
|
+
logger.info(f"Successfully removed knowledge base {logEnding}")
|
196
|
+
except requests.HTTPError as e:
|
197
|
+
if e.response.status_code == 404:
|
198
|
+
logger.warning(f"No knowledge base {logEnding} found")
|
199
|
+
logger.error(e.response.text)
|
200
|
+
exit(1)
|
201
|
+
|
@@ -0,0 +1,17 @@
|
|
1
|
+
import logging
|
2
|
+
import typer
|
3
|
+
from typing_extensions import Annotated
|
4
|
+
|
5
|
+
# REMOVE THIS FILE AFTER ONE MAJOR UPDATE
|
6
|
+
logger = logging.getLogger(__name__)
|
7
|
+
|
8
|
+
login_app = typer.Typer(no_args_is_help=True)
|
9
|
+
|
10
|
+
@login_app.command(name="login", add_help_option=False, hidden=True)
|
11
|
+
def login(
|
12
|
+
local: Annotated[bool, typer.Option("--local", help="local login ")] = False):
|
13
|
+
|
14
|
+
if local:
|
15
|
+
logger.error("The command `orchestrate login --local` has been deprecated. Please use `orchestrate env activate local` instead")
|
16
|
+
else:
|
17
|
+
logger.error("The command `orchestrate login` has been deprecated. Please use `orchestrate env activate <env>` instead")
|
@@ -0,0 +1,128 @@
|
|
1
|
+
import logging
|
2
|
+
import os
|
3
|
+
import requests
|
4
|
+
import sys
|
5
|
+
import rich.highlighter
|
6
|
+
import typer
|
7
|
+
import rich
|
8
|
+
import typer
|
9
|
+
from typing_extensions import Annotated
|
10
|
+
from ibm_watsonx_orchestrate.cli.commands.server.server_command import get_default_env_file, merge_env
|
11
|
+
|
12
|
+
logger = logging.getLogger(__name__)
|
13
|
+
models_app = typer.Typer(no_args_is_help=True)
|
14
|
+
|
15
|
+
WATSONX_URL = os.getenv("WATSONX_URL")
|
16
|
+
|
17
|
+
class ModelHighlighter(rich.highlighter.RegexHighlighter):
|
18
|
+
base_style = "model."
|
19
|
+
highlights = [r"(?P<name>watsonx\/.+\/.+):"]
|
20
|
+
|
21
|
+
@models_app.command(name="list")
|
22
|
+
def model_list(
|
23
|
+
print_raw: Annotated[
|
24
|
+
bool,
|
25
|
+
typer.Option("--raw", "-r", help="Display the list of models in a non-tabular format"),
|
26
|
+
] = False,
|
27
|
+
):
|
28
|
+
global WATSONX_URL
|
29
|
+
default_env_path = get_default_env_file()
|
30
|
+
merged_env_dict = merge_env(
|
31
|
+
default_env_path,
|
32
|
+
None
|
33
|
+
)
|
34
|
+
|
35
|
+
if 'WATSONX_URL' in merged_env_dict and merged_env_dict['WATSONX_URL']:
|
36
|
+
WATSONX_URL = merged_env_dict['WATSONX_URL']
|
37
|
+
|
38
|
+
watsonx_url = merged_env_dict.get("WATSONX_URL")
|
39
|
+
if not watsonx_url:
|
40
|
+
logger.error("Error: WATSONX_URL is required in the environment.")
|
41
|
+
sys.exit(1)
|
42
|
+
|
43
|
+
logger.info("Retrieving watsonx.ai models list...")
|
44
|
+
found_models = _get_wxai_foundational_models()
|
45
|
+
|
46
|
+
preferred_str = merged_env_dict.get('PREFERRED_MODELS', '')
|
47
|
+
incompatible_str = merged_env_dict.get('INCOMPATIBLE_MODELS', '')
|
48
|
+
|
49
|
+
preferred_list = _string_to_list(preferred_str)
|
50
|
+
incompatible_list = _string_to_list(incompatible_str)
|
51
|
+
|
52
|
+
models = found_models.get("resources", [])
|
53
|
+
if not models:
|
54
|
+
logger.error("No models found.")
|
55
|
+
else:
|
56
|
+
# Remove incompatible models
|
57
|
+
filtered_models = []
|
58
|
+
for model in models:
|
59
|
+
model_id = model.get("model_id", "")
|
60
|
+
short_desc = model.get("short_description", "")
|
61
|
+
if any(incomp in model_id.lower() for incomp in incompatible_list):
|
62
|
+
continue
|
63
|
+
if any(incomp in short_desc.lower() for incomp in incompatible_list):
|
64
|
+
continue
|
65
|
+
filtered_models.append(model)
|
66
|
+
|
67
|
+
# Sort to put preferred first
|
68
|
+
def sort_key(model):
|
69
|
+
model_id = model.get("model_id", "").lower()
|
70
|
+
is_preferred = any(pref in model_id for pref in preferred_list)
|
71
|
+
return (0 if is_preferred else 1, model_id)
|
72
|
+
|
73
|
+
sorted_models = sorted(filtered_models, key=sort_key)
|
74
|
+
|
75
|
+
if print_raw:
|
76
|
+
theme = rich.theme.Theme({"model.name": "bold cyan"})
|
77
|
+
console = rich.console.Console(highlighter=ModelHighlighter(), theme=theme)
|
78
|
+
console.print("[bold]Available Models:[/bold]")
|
79
|
+
for model in sorted_models:
|
80
|
+
model_id = model.get("model_id", "N/A")
|
81
|
+
short_desc = model.get("short_description", "No description provided.")
|
82
|
+
full_model_name = f"watsonx/{model_id}: {short_desc}"
|
83
|
+
marker = "★ " if any(pref in model_id.lower() for pref in preferred_list) else ""
|
84
|
+
console.print(f"- [yellow]{marker}[/yellow]{full_model_name}")
|
85
|
+
|
86
|
+
console.print("[yellow]★[/yellow] [italic dim]indicates a supported and preferred model[/italic dim]" )
|
87
|
+
else:
|
88
|
+
table = rich.table.Table(
|
89
|
+
show_header=True,
|
90
|
+
title="[bold]Available Models[/bold]",
|
91
|
+
caption="[yellow]★[/yellow] indicates a supported and preferred model",
|
92
|
+
show_lines=True)
|
93
|
+
columns = ["Model", "Description"]
|
94
|
+
for col in columns:
|
95
|
+
table.add_column(col)
|
96
|
+
|
97
|
+
for model in sorted_models:
|
98
|
+
model_id = model.get("model_id", "N/A")
|
99
|
+
short_desc = model.get("short_description", "No description provided.")
|
100
|
+
marker = "★ " if any(pref in model_id.lower() for pref in preferred_list) else ""
|
101
|
+
table.add_row(f"[yellow]{marker}[/yellow]watsonx/{model_id}", short_desc)
|
102
|
+
|
103
|
+
rich.print(table)
|
104
|
+
|
105
|
+
def _get_wxai_foundational_models():
|
106
|
+
foundation_models_url = WATSONX_URL + "/ml/v1/foundation_model_specs?version=2024-05-01"
|
107
|
+
|
108
|
+
try:
|
109
|
+
response = requests.get(foundation_models_url)
|
110
|
+
except requests.exceptions.RequestException as e:
|
111
|
+
logger.exception(f"Exception when connecting to Watsonx URL: {foundation_models_url}")
|
112
|
+
raise
|
113
|
+
|
114
|
+
if response.status_code != 200:
|
115
|
+
error_message = (
|
116
|
+
f"Failed to retrieve foundational models from {foundation_models_url}. "
|
117
|
+
f"Status code: {response.status_code}. Response: {response.content}"
|
118
|
+
)
|
119
|
+
raise Exception(error_message)
|
120
|
+
|
121
|
+
json_response = response.json()
|
122
|
+
return json_response
|
123
|
+
|
124
|
+
def _string_to_list(env_value):
|
125
|
+
return [item.strip().lower() for item in env_value.split(",") if item.strip()]
|
126
|
+
|
127
|
+
if __name__ == "__main__":
|
128
|
+
models_app()
|