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.
Files changed (89) hide show
  1. ibm_watsonx_orchestrate/__init__.py +28 -0
  2. ibm_watsonx_orchestrate/agent_builder/__init__.py +0 -0
  3. ibm_watsonx_orchestrate/agent_builder/agents/__init__.py +5 -0
  4. ibm_watsonx_orchestrate/agent_builder/agents/agent.py +27 -0
  5. ibm_watsonx_orchestrate/agent_builder/agents/assistant_agent.py +28 -0
  6. ibm_watsonx_orchestrate/agent_builder/agents/external_agent.py +28 -0
  7. ibm_watsonx_orchestrate/agent_builder/agents/types.py +204 -0
  8. ibm_watsonx_orchestrate/agent_builder/connections/__init__.py +27 -0
  9. ibm_watsonx_orchestrate/agent_builder/connections/connections.py +123 -0
  10. ibm_watsonx_orchestrate/agent_builder/connections/types.py +260 -0
  11. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/knowledge_base.py +27 -0
  12. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/knowledge_base_requests.py +59 -0
  13. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +243 -0
  14. ibm_watsonx_orchestrate/agent_builder/tools/__init__.py +4 -0
  15. ibm_watsonx_orchestrate/agent_builder/tools/base_tool.py +36 -0
  16. ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +332 -0
  17. ibm_watsonx_orchestrate/agent_builder/tools/python_tool.py +195 -0
  18. ibm_watsonx_orchestrate/agent_builder/tools/types.py +162 -0
  19. ibm_watsonx_orchestrate/agent_builder/utils/__init__.py +0 -0
  20. ibm_watsonx_orchestrate/agent_builder/utils/pydantic_utils.py +149 -0
  21. ibm_watsonx_orchestrate/cli/__init__.py +0 -0
  22. ibm_watsonx_orchestrate/cli/commands/__init__.py +0 -0
  23. ibm_watsonx_orchestrate/cli/commands/agents/agents_command.py +192 -0
  24. ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +660 -0
  25. ibm_watsonx_orchestrate/cli/commands/channels/channels_command.py +15 -0
  26. ibm_watsonx_orchestrate/cli/commands/channels/channels_controller.py +16 -0
  27. ibm_watsonx_orchestrate/cli/commands/channels/types.py +15 -0
  28. ibm_watsonx_orchestrate/cli/commands/channels/webchat/channels_webchat_command.py +32 -0
  29. ibm_watsonx_orchestrate/cli/commands/channels/webchat/channels_webchat_controller.py +141 -0
  30. ibm_watsonx_orchestrate/cli/commands/chat/chat_command.py +43 -0
  31. ibm_watsonx_orchestrate/cli/commands/connections/connections_command.py +307 -0
  32. ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +517 -0
  33. ibm_watsonx_orchestrate/cli/commands/environment/environment_command.py +78 -0
  34. ibm_watsonx_orchestrate/cli/commands/environment/environment_controller.py +189 -0
  35. ibm_watsonx_orchestrate/cli/commands/environment/types.py +9 -0
  36. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_command.py +79 -0
  37. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +201 -0
  38. ibm_watsonx_orchestrate/cli/commands/login/login_command.py +17 -0
  39. ibm_watsonx_orchestrate/cli/commands/models/models_command.py +128 -0
  40. ibm_watsonx_orchestrate/cli/commands/server/server_command.py +623 -0
  41. ibm_watsonx_orchestrate/cli/commands/settings/__init__.py +0 -0
  42. ibm_watsonx_orchestrate/cli/commands/settings/observability/__init__.py +0 -0
  43. ibm_watsonx_orchestrate/cli/commands/settings/observability/langfuse/__init__.py +0 -0
  44. ibm_watsonx_orchestrate/cli/commands/settings/observability/langfuse/langfuse_command.py +175 -0
  45. ibm_watsonx_orchestrate/cli/commands/settings/observability/observability_command.py +11 -0
  46. ibm_watsonx_orchestrate/cli/commands/settings/settings_command.py +10 -0
  47. ibm_watsonx_orchestrate/cli/commands/tools/tools_command.py +85 -0
  48. ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +564 -0
  49. ibm_watsonx_orchestrate/cli/commands/tools/types.py +10 -0
  50. ibm_watsonx_orchestrate/cli/config.py +226 -0
  51. ibm_watsonx_orchestrate/cli/main.py +32 -0
  52. ibm_watsonx_orchestrate/client/__init__.py +0 -0
  53. ibm_watsonx_orchestrate/client/agents/agent_client.py +46 -0
  54. ibm_watsonx_orchestrate/client/agents/assistant_agent_client.py +38 -0
  55. ibm_watsonx_orchestrate/client/agents/external_agent_client.py +38 -0
  56. ibm_watsonx_orchestrate/client/analytics/__init__.py +0 -0
  57. ibm_watsonx_orchestrate/client/analytics/llm/__init__.py +0 -0
  58. ibm_watsonx_orchestrate/client/analytics/llm/analytics_llm_client.py +50 -0
  59. ibm_watsonx_orchestrate/client/base_api_client.py +113 -0
  60. ibm_watsonx_orchestrate/client/base_service_instance.py +10 -0
  61. ibm_watsonx_orchestrate/client/client.py +71 -0
  62. ibm_watsonx_orchestrate/client/client_errors.py +359 -0
  63. ibm_watsonx_orchestrate/client/connections/__init__.py +10 -0
  64. ibm_watsonx_orchestrate/client/connections/connections_client.py +162 -0
  65. ibm_watsonx_orchestrate/client/connections/utils.py +27 -0
  66. ibm_watsonx_orchestrate/client/credentials.py +123 -0
  67. ibm_watsonx_orchestrate/client/knowledge_bases/knowledge_base_client.py +46 -0
  68. ibm_watsonx_orchestrate/client/local_service_instance.py +91 -0
  69. ibm_watsonx_orchestrate/client/service_instance.py +73 -0
  70. ibm_watsonx_orchestrate/client/tools/tool_client.py +41 -0
  71. ibm_watsonx_orchestrate/client/utils.py +95 -0
  72. ibm_watsonx_orchestrate/docker/compose-lite.yml +595 -0
  73. ibm_watsonx_orchestrate/docker/default.env +125 -0
  74. ibm_watsonx_orchestrate/docker/sdk/ibm_watsonx_orchestrate-0.6.0-py3-none-any.whl +0 -0
  75. ibm_watsonx_orchestrate/docker/sdk/ibm_watsonx_orchestrate-0.6.0.tar.gz +0 -0
  76. ibm_watsonx_orchestrate/docker/start-up.sh +61 -0
  77. ibm_watsonx_orchestrate/docker/tempus/common-config.yaml +1 -0
  78. ibm_watsonx_orchestrate/run/__init__.py +0 -0
  79. ibm_watsonx_orchestrate/run/connections.py +40 -0
  80. ibm_watsonx_orchestrate/utils/__init__.py +0 -0
  81. ibm_watsonx_orchestrate/utils/logging/__init__.py +0 -0
  82. ibm_watsonx_orchestrate/utils/logging/logger.py +26 -0
  83. ibm_watsonx_orchestrate/utils/logging/logging.yaml +18 -0
  84. ibm_watsonx_orchestrate/utils/utils.py +15 -0
  85. ibm_watsonx_orchestrate-1.0.0.dist-info/METADATA +34 -0
  86. ibm_watsonx_orchestrate-1.0.0.dist-info/RECORD +89 -0
  87. ibm_watsonx_orchestrate-1.0.0.dist-info/WHEEL +4 -0
  88. ibm_watsonx_orchestrate-1.0.0.dist-info/entry_points.txt +2 -0
  89. 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,9 @@
1
+ from enum import Enum
2
+
3
+
4
+ class EnvironmentAuthType(str, Enum):
5
+ IBM_CLOUD_IAM = 'ibm_iam'
6
+ MCSP = 'mcsp'
7
+
8
+ def __str__(self):
9
+ return self.value
@@ -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()