ibm-watsonx-orchestrate 1.6.2__py3-none-any.whl → 1.6.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ibm_watsonx_orchestrate/__init__.py +2 -1
- ibm_watsonx_orchestrate/agent_builder/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 +38 -9
- ibm_watsonx_orchestrate/agent_builder/connections/connections.py +4 -3
- ibm_watsonx_orchestrate/agent_builder/connections/types.py +14 -2
- 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 +75 -24
- ibm_watsonx_orchestrate/agent_builder/tools/python_tool.py +136 -92
- ibm_watsonx_orchestrate/agent_builder/tools/types.py +17 -11
- ibm_watsonx_orchestrate/cli/commands/agents/agents_command.py +7 -7
- ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +7 -6
- ibm_watsonx_orchestrate/cli/commands/channels/types.py +15 -2
- ibm_watsonx_orchestrate/cli/commands/channels/webchat/channels_webchat_controller.py +35 -25
- ibm_watsonx_orchestrate/cli/commands/connections/connections_command.py +14 -6
- ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +6 -8
- ibm_watsonx_orchestrate/cli/commands/copilot/copilot_command.py +65 -0
- ibm_watsonx_orchestrate/cli/commands/copilot/copilot_controller.py +368 -0
- ibm_watsonx_orchestrate/cli/commands/copilot/copilot_server_controller.py +170 -0
- ibm_watsonx_orchestrate/cli/commands/environment/environment_controller.py +5 -5
- ibm_watsonx_orchestrate/cli/commands/environment/types.py +3 -1
- ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_command.py +102 -37
- ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_controller.py +20 -2
- ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_command.py +0 -18
- ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +36 -20
- ibm_watsonx_orchestrate/cli/commands/models/models_command.py +1 -1
- ibm_watsonx_orchestrate/cli/commands/models/models_controller.py +5 -8
- ibm_watsonx_orchestrate/cli/commands/server/server_command.py +94 -36
- ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_command.py +1 -1
- ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +11 -4
- 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/connections/connections_client.py +5 -30
- ibm_watsonx_orchestrate/client/copilot/cpe/copilot_cpe_client.py +67 -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 +49 -8
- ibm_watsonx_orchestrate/docker/compose-lite.yml +198 -6
- ibm_watsonx_orchestrate/docker/default.env +36 -12
- ibm_watsonx_orchestrate/flow_builder/flows/__init__.py +9 -4
- 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 +131 -20
- ibm_watsonx_orchestrate/flow_builder/node.py +18 -1
- ibm_watsonx_orchestrate/flow_builder/types.py +271 -16
- ibm_watsonx_orchestrate/flow_builder/utils.py +120 -6
- ibm_watsonx_orchestrate/utils/exceptions.py +23 -0
- {ibm_watsonx_orchestrate-1.6.2.dist-info → ibm_watsonx_orchestrate-1.6.4.dist-info}/METADATA +3 -7
- {ibm_watsonx_orchestrate-1.6.2.dist-info → ibm_watsonx_orchestrate-1.6.4.dist-info}/RECORD +58 -55
- ibm_watsonx_orchestrate/agent_builder/utils/pydantic_utils.py +0 -149
- ibm_watsonx_orchestrate/flow_builder/resources/flow_status.openapi.yml +0 -66
- {ibm_watsonx_orchestrate-1.6.2.dist-info → ibm_watsonx_orchestrate-1.6.4.dist-info}/WHEEL +0 -0
- {ibm_watsonx_orchestrate-1.6.2.dist-info → ibm_watsonx_orchestrate-1.6.4.dist-info}/entry_points.txt +0 -0
- {ibm_watsonx_orchestrate-1.6.2.dist-info → ibm_watsonx_orchestrate-1.6.4.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
|
|
@@ -96,9 +97,13 @@ HTTP_METHOD = Literal['GET', 'POST', 'PUT', 'PATCH', 'DELETE']
|
|
96
97
|
class CallbackBinding(BaseModel):
|
97
98
|
callback_url: str
|
98
99
|
method: HTTP_METHOD
|
99
|
-
input_schema: ToolRequestBody
|
100
|
+
input_schema: Optional[ToolRequestBody] = None
|
100
101
|
output_schema: ToolResponseBody
|
101
102
|
|
103
|
+
class AcknowledgementBinding(BaseModel):
|
104
|
+
output_schema: ToolResponseBody
|
105
|
+
|
106
|
+
|
102
107
|
class OpenApiToolBinding(BaseModel):
|
103
108
|
http_method: HTTP_METHOD
|
104
109
|
http_path: str
|
@@ -106,12 +111,13 @@ class OpenApiToolBinding(BaseModel):
|
|
106
111
|
security: Optional[List[OpenApiSecurityScheme]] = None
|
107
112
|
servers: Optional[List[str]] = None
|
108
113
|
connection_id: str | None = None
|
109
|
-
callback: CallbackBinding = None
|
114
|
+
callback: Optional[CallbackBinding] = None
|
115
|
+
acknowledgement: Optional[AcknowledgementBinding] = None
|
110
116
|
|
111
117
|
@model_validator(mode='after')
|
112
118
|
def validate_openapi_tool_binding(self):
|
113
119
|
if len(self.servers) != 1:
|
114
|
-
raise
|
120
|
+
raise BadRequest("OpenAPI definition must include exactly one server")
|
115
121
|
return self
|
116
122
|
|
117
123
|
|
@@ -129,7 +135,7 @@ class WxFlowsToolBinding(BaseModel):
|
|
129
135
|
@model_validator(mode='after')
|
130
136
|
def validate_security_scheme(self) -> 'WxFlowsToolBinding':
|
131
137
|
if self.security.type != 'apiKey':
|
132
|
-
raise
|
138
|
+
raise BadRequest("'security' scheme must be of type 'apiKey'")
|
133
139
|
return self
|
134
140
|
|
135
141
|
|
@@ -173,9 +179,9 @@ class ToolBinding(BaseModel):
|
|
173
179
|
self.flow is not None
|
174
180
|
]
|
175
181
|
if sum(bindings) == 0:
|
176
|
-
raise
|
182
|
+
raise BadRequest("One binding must be set")
|
177
183
|
if sum(bindings) > 1:
|
178
|
-
raise
|
184
|
+
raise BadRequest("Only one binding can be set")
|
179
185
|
return self
|
180
186
|
|
181
187
|
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 = {
|
@@ -768,7 +769,7 @@ class AgentsController:
|
|
768
769
|
for agent in native_agents:
|
769
770
|
agents_list.append(json.loads(agent.dumps_spec()))
|
770
771
|
|
771
|
-
rich.
|
772
|
+
rich.print_json(json.dumps(agents_list, indent=4))
|
772
773
|
else:
|
773
774
|
native_table = rich.table.Table(
|
774
775
|
show_header=True,
|
@@ -831,7 +832,7 @@ class AgentsController:
|
|
831
832
|
if verbose:
|
832
833
|
for agent in external_agents:
|
833
834
|
external_agents_list.append(json.loads(agent.dumps_spec()))
|
834
|
-
rich.
|
835
|
+
rich.print_json(json.dumps(external_agents_list, indent=4))
|
835
836
|
else:
|
836
837
|
external_table = rich.table.Table(
|
837
838
|
show_header=True,
|
@@ -899,7 +900,7 @@ class AgentsController:
|
|
899
900
|
|
900
901
|
if verbose:
|
901
902
|
for agent in assistant_agents:
|
902
|
-
rich.
|
903
|
+
rich.print_json(agent.dumps_spec())
|
903
904
|
else:
|
904
905
|
assistants_table = rich.table.Table(
|
905
906
|
show_header=True,
|
@@ -950,7 +951,7 @@ class AgentsController:
|
|
950
951
|
elif kind == AgentKind.ASSISTANT:
|
951
952
|
client = self.get_assistant_client()
|
952
953
|
else:
|
953
|
-
raise
|
954
|
+
raise BadRequest("'kind' must be 'native'")
|
954
955
|
|
955
956
|
draft_agents = client.get_draft_by_name(name)
|
956
957
|
if len(draft_agents) > 1:
|
@@ -1,6 +1,6 @@
|
|
1
1
|
from enum import Enum
|
2
2
|
|
3
|
-
class
|
3
|
+
class EnvironmentType(str, Enum):
|
4
4
|
DRAFT ='draft'
|
5
5
|
LIVE = 'live'
|
6
6
|
|
@@ -8,8 +8,21 @@ class EnvironmentType(str, Enum):
|
|
8
8
|
return self.value
|
9
9
|
|
10
10
|
|
11
|
-
class
|
11
|
+
class ChannelType(str, Enum):
|
12
12
|
WEBCHAT ='webchat'
|
13
13
|
|
14
14
|
def __str__(self):
|
15
|
+
return self.value
|
16
|
+
|
17
|
+
|
18
|
+
class RuntimeEnvironmentType(str, Enum):
|
19
|
+
LOCAL = 'local'
|
20
|
+
CPD = 'cpd'
|
21
|
+
IBM_CLOUD = 'ibmcloud'
|
22
|
+
AWS = 'aws'
|
23
|
+
|
24
|
+
def __str__(self):
|
25
|
+
return self.value
|
26
|
+
|
27
|
+
def __repr__(self):
|
15
28
|
return self.value
|
@@ -4,12 +4,14 @@ 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.
|
7
|
+
from ibm_watsonx_orchestrate.cli.commands.channels.types import RuntimeEnvironmentType
|
8
|
+
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
9
|
|
9
10
|
from ibm_watsonx_orchestrate.client.agents.agent_client import AgentClient
|
10
11
|
|
11
12
|
from ibm_watsonx_orchestrate.client.utils import instantiate_client
|
12
13
|
|
14
|
+
|
13
15
|
logger = logging.getLogger(__name__)
|
14
16
|
|
15
17
|
class ChannelsWebchatController:
|
@@ -22,7 +24,7 @@ class ChannelsWebchatController:
|
|
22
24
|
return self.native_client
|
23
25
|
|
24
26
|
def extract_tenant_id_from_crn(self, crn: str) -> str:
|
25
|
-
is_ibm_cloud_env =
|
27
|
+
is_ibm_cloud_env = is_ibm_cloud_platform()
|
26
28
|
if is_ibm_cloud_env:
|
27
29
|
try:
|
28
30
|
parts = crn.split("a/")[1].split(":")
|
@@ -77,6 +79,7 @@ class ChannelsWebchatController:
|
|
77
79
|
agent_environments = agent.get("environments", [])
|
78
80
|
|
79
81
|
is_local = is_local_dev()
|
82
|
+
is_saas = is_saas_env()
|
80
83
|
target_env = env or 'draft'
|
81
84
|
|
82
85
|
if is_local:
|
@@ -91,15 +94,13 @@ class ChannelsWebchatController:
|
|
91
94
|
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
95
|
exit(1)
|
93
96
|
|
94
|
-
if target_env == 'draft' and
|
97
|
+
if target_env == 'draft' and is_saas == True:
|
95
98
|
logger.error(f'For SAAS, please ensure this agent exists in a Live Environment')
|
96
99
|
exit(1)
|
97
|
-
|
98
|
-
|
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 RuntimeEnvironmentType.LOCAL:
|
162
|
+
tenant_id = self.get_tenant_id_local()
|
163
|
+
|
164
|
+
case RuntimeEnvironmentType.CPD:
|
165
|
+
tenant_id = get_cpd_instance_id_from_url()
|
166
|
+
|
167
|
+
case RuntimeEnvironmentType.IBM_CLOUD:
|
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 RuntimeEnvironmentType.AWS:
|
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: {{
|
@@ -201,6 +201,13 @@ def set_credentials_connection_command(
|
|
201
201
|
help='For oauth_auth_client_credentials_flow, the client_secret to authenticate with'
|
202
202
|
)
|
203
203
|
] = None,
|
204
|
+
send_via: Annotated[
|
205
|
+
str,
|
206
|
+
typer.Option(
|
207
|
+
'--send-via',
|
208
|
+
help='For oauth_auth_client_credentials_flow, how the token will be sent to the server. Defaults to using `header`'
|
209
|
+
)
|
210
|
+
] = None,
|
204
211
|
token_url: Annotated[
|
205
212
|
str,
|
206
213
|
typer.Option(
|
@@ -220,14 +227,14 @@ def set_credentials_connection_command(
|
|
220
227
|
str,
|
221
228
|
typer.Option(
|
222
229
|
'--grant-type',
|
223
|
-
help='For oauth_auth_on_behalf_of_flow, the grant type used by the application token server'
|
230
|
+
help='For oauth_auth_on_behalf_of_flow and oauth_auth_client_credentials_flow, the grant type used by the application token server'
|
224
231
|
)
|
225
232
|
] = None,
|
226
|
-
|
227
|
-
|
233
|
+
scope: Annotated[
|
234
|
+
str,
|
228
235
|
typer.Option(
|
229
|
-
'--
|
230
|
-
help='For oauth_auth_code_flow and oauth_auth_client_credentials_flow, the optional scopes used by the application token server'
|
236
|
+
'--scope',
|
237
|
+
help='For oauth_auth_code_flow and oauth_auth_client_credentials_flow, the optional scopes used by the application token server. Should be in the form of a space seperated string.'
|
231
238
|
)
|
232
239
|
] = None,
|
233
240
|
entries: Annotated[
|
@@ -247,10 +254,11 @@ def set_credentials_connection_command(
|
|
247
254
|
api_key=api_key,
|
248
255
|
client_id=client_id,
|
249
256
|
client_secret=client_secret,
|
257
|
+
send_via=send_via,
|
250
258
|
token_url=token_url,
|
251
259
|
auth_url=auth_url,
|
252
260
|
grant_type=grant_type,
|
253
|
-
|
261
|
+
scope=scope,
|
254
262
|
entries=entries
|
255
263
|
)
|
256
264
|
|
@@ -160,13 +160,13 @@ def _validate_connection_params(type: ConnectionType, **args) -> None:
|
|
160
160
|
f"Missing flags --token-url is required for type {type}"
|
161
161
|
)
|
162
162
|
|
163
|
-
|
164
163
|
if type == ConnectionType.OAUTH_ON_BEHALF_OF_FLOW and (
|
165
164
|
args.get('grant_type') is None
|
166
165
|
):
|
167
166
|
raise typer.BadParameter(
|
168
167
|
f"Missing flags --grant-type is required for type {type}"
|
169
168
|
)
|
169
|
+
|
170
170
|
|
171
171
|
def _parse_entry(entry: str) -> dict[str,str]:
|
172
172
|
split_entry = entry.split('=')
|
@@ -197,15 +197,13 @@ def _get_credentials(type: ConnectionType, **kwargs):
|
|
197
197
|
client_id=kwargs.get("client_id"),
|
198
198
|
client_secret=kwargs.get("client_secret"),
|
199
199
|
token_url=kwargs.get("token_url"),
|
200
|
-
|
200
|
+
scope=kwargs.get("scope")
|
201
201
|
)
|
202
202
|
case ConnectionType.OAUTH2_CLIENT_CREDS:
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
scopes=kwargs.get("scopes")
|
208
|
-
)
|
203
|
+
# using filtered args as default values will not be set if 'None' is passed, causing validation errors
|
204
|
+
keys = ["client_id","client_secret","token_url","grant_type","send_via", "scope"]
|
205
|
+
filtered_args = { key_name: kwargs[key_name] for key_name in keys if kwargs.get(key_name) }
|
206
|
+
return OAuth2ClientCredentials(**filtered_args)
|
209
207
|
# case ConnectionType.OAUTH2_IMPLICIT:
|
210
208
|
# return OAuth2ImplicitCredentials(
|
211
209
|
# authorization_url=kwargs.get("auth_url"),
|