ibm-watsonx-orchestrate 1.7.0a0__py3-none-any.whl → 1.8.0b0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ibm_watsonx_orchestrate/__init__.py +1 -1
- ibm_watsonx_orchestrate/agent_builder/agents/agent.py +3 -3
- ibm_watsonx_orchestrate/agent_builder/agents/assistant_agent.py +3 -2
- ibm_watsonx_orchestrate/agent_builder/agents/external_agent.py +3 -2
- ibm_watsonx_orchestrate/agent_builder/agents/types.py +26 -9
- ibm_watsonx_orchestrate/agent_builder/agents/webchat_customizations/prompts.py +1 -0
- ibm_watsonx_orchestrate/agent_builder/connections/connections.py +25 -10
- ibm_watsonx_orchestrate/agent_builder/connections/types.py +5 -9
- ibm_watsonx_orchestrate/agent_builder/knowledge_bases/knowledge_base_requests.py +1 -22
- ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +1 -17
- ibm_watsonx_orchestrate/agent_builder/tools/base_tool.py +2 -1
- ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +14 -13
- ibm_watsonx_orchestrate/agent_builder/tools/python_tool.py +136 -92
- ibm_watsonx_orchestrate/agent_builder/tools/types.py +10 -9
- ibm_watsonx_orchestrate/cli/commands/agents/agents_command.py +7 -7
- ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +35 -7
- ibm_watsonx_orchestrate/cli/commands/channels/webchat/channels_webchat_controller.py +33 -23
- ibm_watsonx_orchestrate/cli/commands/chat/chat_command.py +2 -0
- ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +6 -4
- ibm_watsonx_orchestrate/cli/commands/copilot/copilot_command.py +65 -0
- ibm_watsonx_orchestrate/cli/commands/copilot/copilot_controller.py +293 -0
- ibm_watsonx_orchestrate/cli/commands/copilot/copilot_server_controller.py +154 -0
- ibm_watsonx_orchestrate/cli/commands/environment/environment_controller.py +6 -6
- ibm_watsonx_orchestrate/cli/commands/environment/types.py +2 -0
- ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_command.py +118 -30
- ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_controller.py +22 -9
- ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_command.py +0 -18
- ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +33 -19
- ibm_watsonx_orchestrate/cli/commands/models/models_command.py +1 -1
- ibm_watsonx_orchestrate/cli/commands/server/server_command.py +66 -9
- ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_command.py +1 -1
- ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +93 -14
- ibm_watsonx_orchestrate/cli/config.py +3 -3
- ibm_watsonx_orchestrate/cli/init_helper.py +10 -1
- ibm_watsonx_orchestrate/cli/main.py +5 -0
- ibm_watsonx_orchestrate/client/base_api_client.py +12 -0
- ibm_watsonx_orchestrate/client/copilot/cpe/copilot_cpe_client.py +66 -0
- ibm_watsonx_orchestrate/client/knowledge_bases/knowledge_base_client.py +1 -1
- ibm_watsonx_orchestrate/client/local_service_instance.py +3 -1
- ibm_watsonx_orchestrate/client/service_instance.py +33 -7
- ibm_watsonx_orchestrate/client/utils.py +48 -9
- ibm_watsonx_orchestrate/docker/compose-lite.yml +16 -4
- ibm_watsonx_orchestrate/docker/default.env +25 -15
- ibm_watsonx_orchestrate/flow_builder/flows/__init__.py +3 -1
- ibm_watsonx_orchestrate/flow_builder/flows/decorators.py +4 -2
- ibm_watsonx_orchestrate/flow_builder/flows/events.py +10 -9
- ibm_watsonx_orchestrate/flow_builder/flows/flow.py +91 -20
- ibm_watsonx_orchestrate/flow_builder/node.py +12 -1
- ibm_watsonx_orchestrate/flow_builder/types.py +169 -16
- ibm_watsonx_orchestrate/flow_builder/utils.py +121 -6
- ibm_watsonx_orchestrate/utils/exceptions.py +23 -0
- {ibm_watsonx_orchestrate-1.7.0a0.dist-info → ibm_watsonx_orchestrate-1.8.0b0.dist-info}/METADATA +5 -5
- {ibm_watsonx_orchestrate-1.7.0a0.dist-info → ibm_watsonx_orchestrate-1.8.0b0.dist-info}/RECORD +56 -52
- ibm_watsonx_orchestrate/flow_builder/resources/flow_status.openapi.yml +0 -66
- {ibm_watsonx_orchestrate-1.7.0a0.dist-info → ibm_watsonx_orchestrate-1.8.0b0.dist-info}/WHEEL +0 -0
- {ibm_watsonx_orchestrate-1.7.0a0.dist-info → ibm_watsonx_orchestrate-1.8.0b0.dist-info}/entry_points.txt +0 -0
- {ibm_watsonx_orchestrate-1.7.0a0.dist-info → ibm_watsonx_orchestrate-1.8.0b0.dist-info}/licenses/LICENSE +0 -0
@@ -5,9 +5,6 @@ import os
|
|
5
5
|
from typing import Any, Callable, Dict, List, get_type_hints
|
6
6
|
import logging
|
7
7
|
|
8
|
-
import docstring_parser
|
9
|
-
from langchain_core.tools.base import create_schema_from_function
|
10
|
-
from langchain_core.utils.json_schema import dereference_refs
|
11
8
|
from pydantic import TypeAdapter, BaseModel
|
12
9
|
|
13
10
|
from ibm_watsonx_orchestrate.utils.utils import yaml_safe_load
|
@@ -15,6 +12,7 @@ from ibm_watsonx_orchestrate.agent_builder.connections import ExpectedCredential
|
|
15
12
|
from .base_tool import BaseTool
|
16
13
|
from .types import PythonToolKind, ToolSpec, ToolPermission, ToolRequestBody, ToolResponseBody, JsonSchemaObject, ToolBinding, \
|
17
14
|
PythonToolBinding
|
15
|
+
from ibm_watsonx_orchestrate.utils.exceptions import BadRequest
|
18
16
|
|
19
17
|
_all_tools = []
|
20
18
|
logger = logging.getLogger(__name__)
|
@@ -25,15 +23,136 @@ JOIN_TOOL_PARAMS = {
|
|
25
23
|
'messages': List[Dict[str, Any]],
|
26
24
|
}
|
27
25
|
|
26
|
+
def _parse_expected_credentials(expected_credentials: ExpectedCredentials | dict):
|
27
|
+
parsed_expected_credentials = []
|
28
|
+
if expected_credentials:
|
29
|
+
for credential in expected_credentials:
|
30
|
+
if isinstance(credential, ExpectedCredentials):
|
31
|
+
parsed_expected_credentials.append(credential)
|
32
|
+
else:
|
33
|
+
parsed_expected_credentials.append(ExpectedCredentials.model_validate(credential))
|
34
|
+
|
35
|
+
return parsed_expected_credentials
|
36
|
+
|
28
37
|
class PythonTool(BaseTool):
|
29
|
-
def __init__(self,
|
30
|
-
|
38
|
+
def __init__(self,
|
39
|
+
fn,
|
40
|
+
name: str = None,
|
41
|
+
description: str = None,
|
42
|
+
input_schema: ToolRequestBody = None,
|
43
|
+
output_schema: ToolResponseBody = None,
|
44
|
+
permission: ToolPermission = ToolPermission.READ_ONLY,
|
45
|
+
expected_credentials: List[ExpectedCredentials] = None,
|
46
|
+
display_name: str = None,
|
47
|
+
kind: PythonToolKind = PythonToolKind.TOOL,
|
48
|
+
spec=None
|
49
|
+
):
|
31
50
|
self.fn = fn
|
32
|
-
self.
|
51
|
+
self.name = name
|
52
|
+
self.description = description
|
53
|
+
self.input_schema = input_schema
|
54
|
+
self.output_schema = output_schema
|
55
|
+
self.permission = permission
|
56
|
+
self.display_name = display_name
|
57
|
+
self.kind = kind
|
58
|
+
self.expected_credentials=_parse_expected_credentials(expected_credentials)
|
59
|
+
self._spec = None
|
60
|
+
if spec:
|
61
|
+
self._spec = spec
|
33
62
|
|
34
63
|
def __call__(self, *args, **kwargs):
|
35
64
|
return self.fn(*args, **kwargs)
|
65
|
+
|
66
|
+
@property
|
67
|
+
def __tool_spec__(self):
|
68
|
+
if self._spec:
|
69
|
+
return self._spec
|
70
|
+
|
71
|
+
import docstring_parser
|
72
|
+
from langchain_core.tools.base import create_schema_from_function
|
73
|
+
from langchain_core.utils.json_schema import dereference_refs
|
74
|
+
|
75
|
+
if self.fn.__doc__ is not None:
|
76
|
+
doc = docstring_parser.parse(self.fn.__doc__)
|
77
|
+
else:
|
78
|
+
doc = None
|
79
|
+
|
80
|
+
_desc = self.description
|
81
|
+
if self.description is None and doc is not None:
|
82
|
+
_desc = doc.description
|
83
|
+
|
84
|
+
|
85
|
+
spec = ToolSpec(
|
86
|
+
name=self.name or self.fn.__name__,
|
87
|
+
display_name=self.display_name,
|
88
|
+
description=_desc,
|
89
|
+
permission=self.permission
|
90
|
+
)
|
91
|
+
|
92
|
+
spec.binding = ToolBinding(python=PythonToolBinding(function=''))
|
93
|
+
|
94
|
+
linux_friendly_os_cwd = os.getcwd().replace("\\", "/")
|
95
|
+
function_binding = (inspect.getsourcefile(self.fn)
|
96
|
+
.replace("\\", "/")
|
97
|
+
.replace(linux_friendly_os_cwd+'/', '')
|
98
|
+
.replace('.py', '')
|
99
|
+
.replace('/','.') +
|
100
|
+
f":{self.fn.__name__}")
|
101
|
+
spec.binding.python.function = function_binding
|
102
|
+
|
103
|
+
sig = inspect.signature(self.fn)
|
104
|
+
|
105
|
+
# If the function is a join tool, validate its signature matches the expected parameters. If not, raise error with details.
|
106
|
+
if self.kind == PythonToolKind.JOIN_TOOL:
|
107
|
+
_validate_join_tool_func(self.fn, sig, spec.name)
|
108
|
+
|
109
|
+
if not self.input_schema:
|
110
|
+
try:
|
111
|
+
input_schema_model: type[BaseModel] = create_schema_from_function(spec.name, self.fn, parse_docstring=True)
|
112
|
+
except:
|
113
|
+
logger.warning("Unable to properly parse parameter descriptions due to incorrectly formatted docstring. This may result in degraded agent performance. To fix this, please ensure the docstring conforms to Google's docstring format.")
|
114
|
+
input_schema_model: type[BaseModel] = create_schema_from_function(spec.name, self.fn, parse_docstring=False)
|
115
|
+
input_schema_json = input_schema_model.model_json_schema()
|
116
|
+
input_schema_json = dereference_refs(input_schema_json)
|
117
|
+
|
118
|
+
# Convert the input schema to a JsonSchemaObject
|
119
|
+
input_schema_obj = JsonSchemaObject(**input_schema_json)
|
120
|
+
input_schema_obj = _fix_optional(input_schema_obj)
|
121
|
+
|
122
|
+
spec.input_schema = ToolRequestBody(
|
123
|
+
type='object',
|
124
|
+
properties=input_schema_obj.properties or {},
|
125
|
+
required=input_schema_obj.required or []
|
126
|
+
)
|
127
|
+
else:
|
128
|
+
spec.input_schema = self.input_schema
|
129
|
+
|
130
|
+
_validate_input_schema(spec.input_schema)
|
131
|
+
|
132
|
+
if not self.output_schema:
|
133
|
+
ret = sig.return_annotation
|
134
|
+
if ret != sig.empty:
|
135
|
+
_schema = dereference_refs(TypeAdapter(ret).json_schema())
|
136
|
+
if '$defs' in _schema:
|
137
|
+
_schema.pop('$defs')
|
138
|
+
spec.output_schema = _fix_optional(ToolResponseBody(**_schema))
|
139
|
+
else:
|
140
|
+
spec.output_schema = ToolResponseBody()
|
141
|
+
|
142
|
+
if doc is not None and doc.returns is not None and doc.returns.description is not None:
|
143
|
+
spec.output_schema.description = doc.returns.description
|
144
|
+
|
145
|
+
else:
|
146
|
+
spec.output_schema = ToolResponseBody()
|
147
|
+
|
148
|
+
# Validate the generated schema still conforms to the requirement for a join tool
|
149
|
+
if self.kind == PythonToolKind.JOIN_TOOL:
|
150
|
+
if not spec.is_custom_join_tool():
|
151
|
+
raise ValueError(f"Join tool '{spec.name}' does not conform to the expected join tool schema. Please ensure the input schema has the required fields: {JOIN_TOOL_PARAMS.keys()} and the output schema is a string.")
|
36
152
|
|
153
|
+
self._spec = spec
|
154
|
+
return spec
|
155
|
+
|
37
156
|
@staticmethod
|
38
157
|
def from_spec(file: str) -> 'PythonTool':
|
39
158
|
with open(file, 'r') as f:
|
@@ -42,10 +161,10 @@ class PythonTool(BaseTool):
|
|
42
161
|
elif file.endswith('.json'):
|
43
162
|
spec = ToolSpec.model_validate(json.load(f))
|
44
163
|
else:
|
45
|
-
raise
|
164
|
+
raise BadRequest('file must end in .json, .yaml, or .yml')
|
46
165
|
|
47
166
|
if spec.binding.python is None:
|
48
|
-
raise
|
167
|
+
raise BadRequest('failed to load python tool as the tool had no python binding')
|
49
168
|
|
50
169
|
[module, fn_name] = spec.binding.python.function.split(':')
|
51
170
|
fn = getattr(importlib.import_module(module), fn_name)
|
@@ -147,92 +266,17 @@ def tool(
|
|
147
266
|
"""
|
148
267
|
# inspiration: https://github.com/pydantic/pydantic/blob/main/pydantic/validate_call_decorator.py
|
149
268
|
def _tool_decorator(fn):
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
spec = ToolSpec(
|
161
|
-
name=name or fn.__name__,
|
269
|
+
t = PythonTool(
|
270
|
+
fn=fn,
|
271
|
+
name=name,
|
272
|
+
description=description,
|
273
|
+
input_schema=input_schema,
|
274
|
+
output_schema=output_schema,
|
275
|
+
permission=permission,
|
276
|
+
expected_credentials=expected_credentials,
|
162
277
|
display_name=display_name,
|
163
|
-
|
164
|
-
permission=permission
|
278
|
+
kind=kind
|
165
279
|
)
|
166
|
-
|
167
|
-
parsed_expected_credentials = []
|
168
|
-
if expected_credentials:
|
169
|
-
for credential in expected_credentials:
|
170
|
-
if isinstance(credential, ExpectedCredentials):
|
171
|
-
parsed_expected_credentials.append(credential)
|
172
|
-
else:
|
173
|
-
parsed_expected_credentials.append(ExpectedCredentials.model_validate(credential))
|
174
|
-
|
175
|
-
t = PythonTool(fn=fn, spec=spec, expected_credentials=parsed_expected_credentials)
|
176
|
-
spec.binding = ToolBinding(python=PythonToolBinding(function=''))
|
177
|
-
|
178
|
-
linux_friendly_os_cwd = os.getcwd().replace("\\", "/")
|
179
|
-
function_binding = (inspect.getsourcefile(fn)
|
180
|
-
.replace("\\", "/")
|
181
|
-
.replace(linux_friendly_os_cwd+'/', '')
|
182
|
-
.replace('.py', '')
|
183
|
-
.replace('/','.') +
|
184
|
-
f":{fn.__name__}")
|
185
|
-
spec.binding.python.function = function_binding
|
186
|
-
|
187
|
-
sig = inspect.signature(fn)
|
188
|
-
|
189
|
-
# If the function is a join tool, validate its signature matches the expected parameters. If not, raise error with details.
|
190
|
-
if kind == PythonToolKind.JOIN_TOOL:
|
191
|
-
_validate_join_tool_func(fn, sig, spec.name)
|
192
|
-
|
193
|
-
if not input_schema:
|
194
|
-
try:
|
195
|
-
input_schema_model: type[BaseModel] = create_schema_from_function(spec.name, fn, parse_docstring=True)
|
196
|
-
except:
|
197
|
-
logger.warning("Unable to properly parse parameter descriptions due to incorrectly formatted docstring. This may result in degraded agent performance. To fix this, please ensure the docstring conforms to Google's docstring format.")
|
198
|
-
input_schema_model: type[BaseModel] = create_schema_from_function(spec.name, fn, parse_docstring=False)
|
199
|
-
input_schema_json = input_schema_model.model_json_schema()
|
200
|
-
input_schema_json = dereference_refs(input_schema_json)
|
201
|
-
|
202
|
-
# Convert the input schema to a JsonSchemaObject
|
203
|
-
input_schema_obj = JsonSchemaObject(**input_schema_json)
|
204
|
-
input_schema_obj = _fix_optional(input_schema_obj)
|
205
|
-
|
206
|
-
spec.input_schema = ToolRequestBody(
|
207
|
-
type='object',
|
208
|
-
properties=input_schema_obj.properties or {},
|
209
|
-
required=input_schema_obj.required or []
|
210
|
-
)
|
211
|
-
else:
|
212
|
-
spec.input_schema = input_schema
|
213
|
-
|
214
|
-
_validate_input_schema(spec.input_schema)
|
215
|
-
|
216
|
-
if not output_schema:
|
217
|
-
ret = sig.return_annotation
|
218
|
-
if ret != sig.empty:
|
219
|
-
_schema = dereference_refs(TypeAdapter(ret).json_schema())
|
220
|
-
if '$defs' in _schema:
|
221
|
-
_schema.pop('$defs')
|
222
|
-
spec.output_schema = _fix_optional(ToolResponseBody(**_schema))
|
223
|
-
else:
|
224
|
-
spec.output_schema = ToolResponseBody()
|
225
|
-
|
226
|
-
if doc is not None and doc.returns is not None and doc.returns.description is not None:
|
227
|
-
spec.output_schema.description = doc.returns.description
|
228
|
-
|
229
|
-
else:
|
230
|
-
spec.output_schema = ToolResponseBody()
|
231
|
-
|
232
|
-
# Validate the generated schema still conforms to the requirement for a join tool
|
233
|
-
if kind == PythonToolKind.JOIN_TOOL:
|
234
|
-
if not spec.is_custom_join_tool():
|
235
|
-
raise ValueError(f"Join tool '{spec.name}' does not conform to the expected join tool schema. Please ensure the input schema has the required fields: {JOIN_TOOL_PARAMS.keys()} and the output schema is a string.")
|
236
280
|
|
237
281
|
_all_tools.append(t)
|
238
282
|
return t
|
@@ -2,6 +2,7 @@ from enum import Enum
|
|
2
2
|
from typing import List, Any, Dict, Literal, Optional, Union
|
3
3
|
|
4
4
|
from pydantic import BaseModel, model_validator, ConfigDict, Field, AliasChoices
|
5
|
+
from ibm_watsonx_orchestrate.utils.exceptions import BadRequest
|
5
6
|
|
6
7
|
|
7
8
|
class ToolPermission(str, Enum):
|
@@ -74,19 +75,19 @@ class OpenApiSecurityScheme(BaseModel):
|
|
74
75
|
@model_validator(mode='after')
|
75
76
|
def validate_security_scheme(self) -> 'OpenApiSecurityScheme':
|
76
77
|
if self.type == 'http' and self.scheme is None:
|
77
|
-
raise
|
78
|
+
raise BadRequest("'scheme' is required when type is 'http'")
|
78
79
|
|
79
80
|
if self.type == 'oauth2' and self.flows is None:
|
80
|
-
raise
|
81
|
+
raise BadRequest("'flows' is required when type is 'oauth2'")
|
81
82
|
|
82
83
|
if self.type == 'openIdConnect' and self.open_id_connect_url is None:
|
83
|
-
raise
|
84
|
+
raise BadRequest("'open_id_connect_url' is required when type is 'openIdConnect'")
|
84
85
|
|
85
86
|
if self.type == 'apiKey':
|
86
87
|
if self.name is None:
|
87
|
-
raise
|
88
|
+
raise BadRequest("'name' is required when type is 'apiKey'")
|
88
89
|
if self.in_field is None:
|
89
|
-
raise
|
90
|
+
raise BadRequest("'in_field' is required when type is 'apiKey'")
|
90
91
|
|
91
92
|
return self
|
92
93
|
|
@@ -111,7 +112,7 @@ class OpenApiToolBinding(BaseModel):
|
|
111
112
|
@model_validator(mode='after')
|
112
113
|
def validate_openapi_tool_binding(self):
|
113
114
|
if len(self.servers) != 1:
|
114
|
-
raise
|
115
|
+
raise BadRequest("OpenAPI definition must include exactly one server")
|
115
116
|
return self
|
116
117
|
|
117
118
|
|
@@ -129,7 +130,7 @@ class WxFlowsToolBinding(BaseModel):
|
|
129
130
|
@model_validator(mode='after')
|
130
131
|
def validate_security_scheme(self) -> 'WxFlowsToolBinding':
|
131
132
|
if self.security.type != 'apiKey':
|
132
|
-
raise
|
133
|
+
raise BadRequest("'security' scheme must be of type 'apiKey'")
|
133
134
|
return self
|
134
135
|
|
135
136
|
|
@@ -173,9 +174,9 @@ class ToolBinding(BaseModel):
|
|
173
174
|
self.flow is not None
|
174
175
|
]
|
175
176
|
if sum(bindings) == 0:
|
176
|
-
raise
|
177
|
+
raise BadRequest("One binding must be set")
|
177
178
|
if sum(bindings) > 1:
|
178
|
-
raise
|
179
|
+
raise BadRequest("Only one binding can be set")
|
179
180
|
return self
|
180
181
|
|
181
182
|
class ToolSpec(BaseModel):
|
@@ -31,6 +31,13 @@ def agent_create(
|
|
31
31
|
str,
|
32
32
|
typer.Option("--name", "-n", help="Name of the agent you wish to create"),
|
33
33
|
],
|
34
|
+
description: Annotated[
|
35
|
+
str,
|
36
|
+
typer.Option(
|
37
|
+
"--description",
|
38
|
+
help="Description of the agent",
|
39
|
+
),
|
40
|
+
],
|
34
41
|
title: Annotated[
|
35
42
|
str,
|
36
43
|
typer.Option("--title", "-t", help="Title of the agent you wish to create. Only needed for External and Assistant Agents"),
|
@@ -87,13 +94,6 @@ def agent_create(
|
|
87
94
|
str,
|
88
95
|
typer.Option("--app-id", help="Application ID for the agent"),
|
89
96
|
] = None,
|
90
|
-
description: Annotated[
|
91
|
-
str,
|
92
|
-
typer.Option(
|
93
|
-
"--description",
|
94
|
-
help="Description of the agent",
|
95
|
-
),
|
96
|
-
] = None,
|
97
97
|
llm: Annotated[
|
98
98
|
str,
|
99
99
|
typer.Option(
|
@@ -29,6 +29,7 @@ from ibm_watsonx_orchestrate.client.agents.agent_client import AgentClient, Agen
|
|
29
29
|
from ibm_watsonx_orchestrate.client.agents.external_agent_client import ExternalAgentClient
|
30
30
|
from ibm_watsonx_orchestrate.client.agents.assistant_agent_client import AssistantAgentClient
|
31
31
|
from ibm_watsonx_orchestrate.client.tools.tool_client import ToolClient
|
32
|
+
from ibm_watsonx_orchestrate.utils.exceptions import BadRequest
|
32
33
|
from ibm_watsonx_orchestrate.client.connections import get_connections_client
|
33
34
|
from ibm_watsonx_orchestrate.client.knowledge_bases.knowledge_base_client import KnowledgeBaseClient
|
34
35
|
|
@@ -71,7 +72,7 @@ def create_agent_from_spec(file:str, kind:str) -> Agent | ExternalAgent | Assist
|
|
71
72
|
case AgentKind.ASSISTANT:
|
72
73
|
agent = AssistantAgent.from_spec(file)
|
73
74
|
case _:
|
74
|
-
raise
|
75
|
+
raise BadRequest("'kind' must be either 'native' or 'external'")
|
75
76
|
|
76
77
|
return agent
|
77
78
|
|
@@ -88,7 +89,7 @@ def parse_file(file: str) -> List[Agent | ExternalAgent | AssistantAgent]:
|
|
88
89
|
agents = import_python_agent(file)
|
89
90
|
return agents
|
90
91
|
else:
|
91
|
-
raise
|
92
|
+
raise BadRequest("file must end in .json, .yaml, .yml or .py")
|
92
93
|
|
93
94
|
def parse_create_native_args(name: str, kind: AgentKind, description: str | None, **args) -> dict:
|
94
95
|
agent_details = {
|
@@ -747,14 +748,25 @@ class AgentsController:
|
|
747
748
|
return knowledge_bases
|
748
749
|
|
749
750
|
def list_agents(self, kind: AgentKind=None, verbose: bool=False):
|
751
|
+
parse_errors = []
|
752
|
+
|
750
753
|
if kind == AgentKind.NATIVE or kind is None:
|
751
754
|
response = self.get_native_client().get()
|
752
|
-
native_agents = [
|
755
|
+
native_agents = []
|
756
|
+
for agent in response:
|
757
|
+
try:
|
758
|
+
native_agents.append(Agent.model_validate(agent))
|
759
|
+
except Exception as e:
|
760
|
+
name = agent.get('name', None)
|
761
|
+
parse_errors.append([
|
762
|
+
f"Agent '{name}' could not be parsed",
|
763
|
+
json.dumps(agent),
|
764
|
+
e
|
765
|
+
])
|
753
766
|
|
754
767
|
if verbose:
|
755
768
|
agents_list = []
|
756
769
|
for agent in native_agents:
|
757
|
-
|
758
770
|
agents_list.append(json.loads(agent.dumps_spec()))
|
759
771
|
|
760
772
|
rich.print(rich.json.JSON(json.dumps(agents_list, indent=4)))
|
@@ -799,7 +811,13 @@ class AgentsController:
|
|
799
811
|
if kind == AgentKind.EXTERNAL or kind is None:
|
800
812
|
response = self.get_external_client().get()
|
801
813
|
|
802
|
-
external_agents = [
|
814
|
+
external_agents = []
|
815
|
+
for agent in response:
|
816
|
+
try:
|
817
|
+
external_agents.append(ExternalAgent.model_validate(agent))
|
818
|
+
except Exception as e:
|
819
|
+
name = agent.get('name', None)
|
820
|
+
parse_errors.append([f"External Agent {name} could not be parsed", e])
|
803
821
|
|
804
822
|
response_dict = {agent["id"]: agent for agent in response}
|
805
823
|
|
@@ -859,7 +877,13 @@ class AgentsController:
|
|
859
877
|
if kind == AgentKind.ASSISTANT or kind is None:
|
860
878
|
response = self.get_assistant_client().get()
|
861
879
|
|
862
|
-
assistant_agents = [
|
880
|
+
assistant_agents = []
|
881
|
+
for agent in response:
|
882
|
+
try:
|
883
|
+
assistant_agents.append(AssistantAgent.model_validate(agent))
|
884
|
+
except Exception as e:
|
885
|
+
name = agent.get('name', None)
|
886
|
+
parse_errors.append([f"Assistant Agent {name} could not be parsed", e])
|
863
887
|
|
864
888
|
response_dict = {agent["id"]: agent for agent in response}
|
865
889
|
|
@@ -914,6 +938,10 @@ class AgentsController:
|
|
914
938
|
)
|
915
939
|
rich.print(assistants_table)
|
916
940
|
|
941
|
+
for error in parse_errors:
|
942
|
+
for l in error:
|
943
|
+
logger.error(l)
|
944
|
+
|
917
945
|
def remove_agent(self, name: str, kind: AgentKind):
|
918
946
|
try:
|
919
947
|
if kind == AgentKind.NATIVE:
|
@@ -923,7 +951,7 @@ class AgentsController:
|
|
923
951
|
elif kind == AgentKind.ASSISTANT:
|
924
952
|
client = self.get_assistant_client()
|
925
953
|
else:
|
926
|
-
raise
|
954
|
+
raise BadRequest("'kind' must be 'native'")
|
927
955
|
|
928
956
|
draft_agents = client.get_draft_by_name(name)
|
929
957
|
if len(draft_agents) > 1:
|
@@ -4,7 +4,7 @@ import jwt
|
|
4
4
|
import sys
|
5
5
|
|
6
6
|
from ibm_watsonx_orchestrate.cli.config import Config, ENV_WXO_URL_OPT, ENVIRONMENTS_SECTION_HEADER, CONTEXT_SECTION_HEADER, CONTEXT_ACTIVE_ENV_OPT, CHAT_UI_PORT
|
7
|
-
from ibm_watsonx_orchestrate.client.utils import is_local_dev,
|
7
|
+
from ibm_watsonx_orchestrate.client.utils import is_local_dev, is_ibm_cloud_platform, get_environment, get_cpd_instance_id_from_url, is_saas_env, AUTH_CONFIG_FILE_FOLDER, AUTH_SECTION_HEADER, AUTH_MCSP_TOKEN_OPT, AUTH_CONFIG_FILE
|
8
8
|
|
9
9
|
from ibm_watsonx_orchestrate.client.agents.agent_client import AgentClient
|
10
10
|
|
@@ -22,7 +22,7 @@ class ChannelsWebchatController:
|
|
22
22
|
return self.native_client
|
23
23
|
|
24
24
|
def extract_tenant_id_from_crn(self, crn: str) -> str:
|
25
|
-
is_ibm_cloud_env =
|
25
|
+
is_ibm_cloud_env = is_ibm_cloud_platform()
|
26
26
|
if is_ibm_cloud_env:
|
27
27
|
try:
|
28
28
|
parts = crn.split("a/")[1].split(":")
|
@@ -77,6 +77,7 @@ class ChannelsWebchatController:
|
|
77
77
|
agent_environments = agent.get("environments", [])
|
78
78
|
|
79
79
|
is_local = is_local_dev()
|
80
|
+
is_saas = is_saas_env()
|
80
81
|
target_env = env or 'draft'
|
81
82
|
|
82
83
|
if is_local:
|
@@ -91,7 +92,7 @@ class ChannelsWebchatController:
|
|
91
92
|
logger.error(f'This agent does not exist in the {env} environment. You need to deploy it to {env} before you can embed the agent')
|
92
93
|
exit(1)
|
93
94
|
|
94
|
-
if target_env == 'draft' and
|
95
|
+
if target_env == 'draft' and is_saas == True:
|
95
96
|
logger.error(f'For SAAS, please ensure this agent exists in a Live Environment')
|
96
97
|
exit(1)
|
97
98
|
|
@@ -99,7 +100,7 @@ class ChannelsWebchatController:
|
|
99
100
|
|
100
101
|
return filtered_environments[0].get("id")
|
101
102
|
|
102
|
-
def
|
103
|
+
def get_tenant_id(self):
|
103
104
|
auth_cfg = Config(AUTH_CONFIG_FILE_FOLDER, AUTH_CONFIG_FILE)
|
104
105
|
|
105
106
|
cfg = Config()
|
@@ -154,30 +155,40 @@ class ChannelsWebchatController:
|
|
154
155
|
|
155
156
|
def create_webchat_embed_code(self):
|
156
157
|
crn = None
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
158
|
+
environment = get_environment()
|
159
|
+
|
160
|
+
match (environment):
|
161
|
+
case "local":
|
162
|
+
tenant_id = self.get_tenant_id_local()
|
163
|
+
|
164
|
+
case "cpd":
|
165
|
+
tenant_id = get_cpd_instance_id_from_url()
|
166
|
+
|
167
|
+
case "ibmcloud":
|
168
|
+
crn = input("Please enter your CRN which can be retrieved from the IBM Cloud UI: ")
|
169
|
+
if crn == "":
|
170
|
+
logger.error("You must enter your CRN for IBM Cloud instances")
|
171
|
+
sys.exit(1)
|
172
|
+
is_crn_correct = self.check_crn_is_correct(crn)
|
173
|
+
if is_crn_correct == False:
|
174
|
+
logger.error("Invalid CRN format provided.")
|
175
|
+
sys.exit(1)
|
176
|
+
tenant_id = self.extract_tenant_id_from_crn(crn)
|
177
|
+
|
178
|
+
case "ga":
|
179
|
+
tenant_id = self.get_tenant_id()
|
180
|
+
|
181
|
+
case _:
|
182
|
+
logger.error("Environment not recognized")
|
163
183
|
sys.exit(1)
|
164
|
-
|
165
|
-
if is_crn_correct == False:
|
166
|
-
logger.error("Invalid CRN format provided.")
|
167
|
-
|
168
|
-
if is_ibm_cloud_env and crn is not None:
|
169
|
-
tenant_id = self.extract_tenant_id_from_crn(crn)
|
170
|
-
elif is_ibm_cloud_env is False and is_local is False:
|
171
|
-
tenant_id = self.get_tennent_id()
|
172
|
-
elif is_local:
|
173
|
-
tenant_id = self.get_tenant_id_local()
|
184
|
+
|
174
185
|
host_url = self.get_host_url()
|
175
186
|
agent_id = self.get_agent_id(self.agent_name)
|
176
187
|
agent_env_id = self.get_environment_id(self.agent_name, self.env)
|
177
188
|
|
178
189
|
script_path = (
|
179
190
|
"/wxoLoader.js?embed=true"
|
180
|
-
if
|
191
|
+
if environment == "local"
|
181
192
|
else "/wxochat/wxoLoader.js?embed=true"
|
182
193
|
)
|
183
194
|
|
@@ -189,9 +200,8 @@ class ChannelsWebchatController:
|
|
189
200
|
]
|
190
201
|
|
191
202
|
# Conditional fields for IBM Cloud
|
192
|
-
if
|
203
|
+
if environment == "ibmcloud":
|
193
204
|
config_lines.append(f'crn: "{crn}"')
|
194
|
-
if is_ibm_cloud_env:
|
195
205
|
config_lines.append(f'deploymentPlatform: "ibmcloud"')
|
196
206
|
|
197
207
|
config_lines.append(f"""chatOptions: {{
|
@@ -24,6 +24,8 @@ def chat_start(
|
|
24
24
|
url = "http://localhost:3000/chat-lite"
|
25
25
|
webbrowser.open(url)
|
26
26
|
logger.info(f"Opening chat interface at {url}")
|
27
|
+
# TODO: Remove when connections UI is added
|
28
|
+
logger.warning("When using local chat, requests that the user 'Connect Apps' must be resolved by running `orchestrate connections set-credentials`")
|
27
29
|
else:
|
28
30
|
logger.error("Unable to start orchestrate UI chat service. Please check error messages and logs")
|
29
31
|
|
@@ -346,7 +346,7 @@ def add_connection(app_id: str) -> None:
|
|
346
346
|
status_code = response.status_code
|
347
347
|
try:
|
348
348
|
if status_code == 409:
|
349
|
-
response_text = f"Failed to create connection. A connection with the App ID '{app_id}' already exists. Please select a
|
349
|
+
response_text = f"Failed to create connection. A connection with the App ID '{app_id}' already exists. Please select a different App ID or delete the existing resource."
|
350
350
|
else:
|
351
351
|
resp = json.loads(response_text)
|
352
352
|
response_text = resp.get('detail')
|
@@ -370,12 +370,14 @@ def remove_connection(app_id: str) -> None:
|
|
370
370
|
|
371
371
|
def list_connections(environment: ConnectionEnvironment | None, verbose: bool = False) -> None:
|
372
372
|
client = get_connections_client()
|
373
|
-
|
374
373
|
connections = client.list()
|
375
|
-
|
374
|
+
is_local = is_local_dev()
|
375
|
+
|
376
376
|
if verbose:
|
377
377
|
connections_list = []
|
378
378
|
for conn in connections:
|
379
|
+
if is_local and conn.environment == ConnectionEnvironment.LIVE:
|
380
|
+
continue
|
379
381
|
connections_list.append(json.loads(conn.model_dump_json()))
|
380
382
|
|
381
383
|
rich.print_json(json.dumps(connections_list, indent=4))
|
@@ -414,7 +416,7 @@ def list_connections(environment: ConnectionEnvironment | None, verbose: bool =
|
|
414
416
|
conn.preference,
|
415
417
|
"✅" if conn.credentials_entered else "❌"
|
416
418
|
)
|
417
|
-
elif conn.environment == ConnectionEnvironment.LIVE:
|
419
|
+
elif conn.environment == ConnectionEnvironment.LIVE and not is_local:
|
418
420
|
live_table.add_row(
|
419
421
|
conn.app_id,
|
420
422
|
connection_type,
|