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.
Files changed (60) hide show
  1. ibm_watsonx_orchestrate/__init__.py +2 -1
  2. ibm_watsonx_orchestrate/agent_builder/agents/agent.py +3 -3
  3. ibm_watsonx_orchestrate/agent_builder/agents/assistant_agent.py +3 -2
  4. ibm_watsonx_orchestrate/agent_builder/agents/external_agent.py +3 -2
  5. ibm_watsonx_orchestrate/agent_builder/agents/types.py +38 -9
  6. ibm_watsonx_orchestrate/agent_builder/connections/connections.py +4 -3
  7. ibm_watsonx_orchestrate/agent_builder/connections/types.py +14 -2
  8. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/knowledge_base_requests.py +1 -22
  9. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +1 -17
  10. ibm_watsonx_orchestrate/agent_builder/tools/base_tool.py +2 -1
  11. ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +75 -24
  12. ibm_watsonx_orchestrate/agent_builder/tools/python_tool.py +136 -92
  13. ibm_watsonx_orchestrate/agent_builder/tools/types.py +17 -11
  14. ibm_watsonx_orchestrate/cli/commands/agents/agents_command.py +7 -7
  15. ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +7 -6
  16. ibm_watsonx_orchestrate/cli/commands/channels/types.py +15 -2
  17. ibm_watsonx_orchestrate/cli/commands/channels/webchat/channels_webchat_controller.py +35 -25
  18. ibm_watsonx_orchestrate/cli/commands/connections/connections_command.py +14 -6
  19. ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +6 -8
  20. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_command.py +65 -0
  21. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_controller.py +368 -0
  22. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_server_controller.py +170 -0
  23. ibm_watsonx_orchestrate/cli/commands/environment/environment_controller.py +5 -5
  24. ibm_watsonx_orchestrate/cli/commands/environment/types.py +3 -1
  25. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_command.py +102 -37
  26. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_controller.py +20 -2
  27. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_command.py +0 -18
  28. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +36 -20
  29. ibm_watsonx_orchestrate/cli/commands/models/models_command.py +1 -1
  30. ibm_watsonx_orchestrate/cli/commands/models/models_controller.py +5 -8
  31. ibm_watsonx_orchestrate/cli/commands/server/server_command.py +94 -36
  32. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_command.py +1 -1
  33. ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +11 -4
  34. ibm_watsonx_orchestrate/cli/config.py +3 -3
  35. ibm_watsonx_orchestrate/cli/init_helper.py +10 -1
  36. ibm_watsonx_orchestrate/cli/main.py +5 -0
  37. ibm_watsonx_orchestrate/client/base_api_client.py +12 -0
  38. ibm_watsonx_orchestrate/client/connections/connections_client.py +5 -30
  39. ibm_watsonx_orchestrate/client/copilot/cpe/copilot_cpe_client.py +67 -0
  40. ibm_watsonx_orchestrate/client/knowledge_bases/knowledge_base_client.py +1 -1
  41. ibm_watsonx_orchestrate/client/local_service_instance.py +3 -1
  42. ibm_watsonx_orchestrate/client/service_instance.py +33 -7
  43. ibm_watsonx_orchestrate/client/utils.py +49 -8
  44. ibm_watsonx_orchestrate/docker/compose-lite.yml +198 -6
  45. ibm_watsonx_orchestrate/docker/default.env +36 -12
  46. ibm_watsonx_orchestrate/flow_builder/flows/__init__.py +9 -4
  47. ibm_watsonx_orchestrate/flow_builder/flows/decorators.py +4 -2
  48. ibm_watsonx_orchestrate/flow_builder/flows/events.py +10 -9
  49. ibm_watsonx_orchestrate/flow_builder/flows/flow.py +131 -20
  50. ibm_watsonx_orchestrate/flow_builder/node.py +18 -1
  51. ibm_watsonx_orchestrate/flow_builder/types.py +271 -16
  52. ibm_watsonx_orchestrate/flow_builder/utils.py +120 -6
  53. ibm_watsonx_orchestrate/utils/exceptions.py +23 -0
  54. {ibm_watsonx_orchestrate-1.6.2.dist-info → ibm_watsonx_orchestrate-1.6.4.dist-info}/METADATA +3 -7
  55. {ibm_watsonx_orchestrate-1.6.2.dist-info → ibm_watsonx_orchestrate-1.6.4.dist-info}/RECORD +58 -55
  56. ibm_watsonx_orchestrate/agent_builder/utils/pydantic_utils.py +0 -149
  57. ibm_watsonx_orchestrate/flow_builder/resources/flow_status.openapi.yml +0 -66
  58. {ibm_watsonx_orchestrate-1.6.2.dist-info → ibm_watsonx_orchestrate-1.6.4.dist-info}/WHEEL +0 -0
  59. {ibm_watsonx_orchestrate-1.6.2.dist-info → ibm_watsonx_orchestrate-1.6.4.dist-info}/entry_points.txt +0 -0
  60. {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, fn, spec: ToolSpec, expected_credentials: List[ExpectedCredentials]=None):
30
- BaseTool.__init__(self, spec=spec)
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.expected_credentials=expected_credentials
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 ValueError('file must end in .json, .yaml, or .yml')
164
+ raise BadRequest('file must end in .json, .yaml, or .yml')
46
165
 
47
166
  if spec.binding.python is None:
48
- raise ValueError('failed to load python tool as the tool had no python binding')
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
- if fn.__doc__ is not None:
151
- doc = docstring_parser.parse(fn.__doc__)
152
- else:
153
- doc = None
154
-
155
- _desc = description
156
- if description is None and doc is not None:
157
- _desc = doc.description
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
- description=_desc,
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 ValueError("'scheme' is required when type is 'http'")
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 ValueError("'flows' is required when type is 'oauth2'")
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 ValueError("'open_id_connect_url' is required when type is 'openIdConnect'")
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 ValueError("'name' is required when type is 'apiKey'")
88
+ raise BadRequest("'name' is required when type is 'apiKey'")
88
89
  if self.in_field is None:
89
- raise ValueError("'in_field' is required when type is 'apiKey'")
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 ValueError("OpenAPI definition must include exactly one server")
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 ValueError("'security' scheme must be of type 'apiKey'")
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 ValueError("One binding must be set")
182
+ raise BadRequest("One binding must be set")
177
183
  if sum(bindings) > 1:
178
- raise ValueError("Only one binding can be set")
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 ValueError("'kind' must be either 'native' or 'external'")
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 ValueError("file must end in .json, .yaml, .yml or .py")
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.print(rich.json.JSON(json.dumps(agents_list, indent=4)))
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.print(rich.json.JSON(json.dumps(external_agents_list, indent=4)))
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.print(agent.dumps_spec())
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 ValueError("'kind' must be 'native'")
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 EnvironmentType(str, Enum):
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 ChannelType(str, Enum):
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.client.utils import is_local_dev, is_ibm_cloud, AUTH_CONFIG_FILE_FOLDER, AUTH_SECTION_HEADER, AUTH_MCSP_TOKEN_OPT, AUTH_CONFIG_FILE
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 = is_ibm_cloud()
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 is_local == False:
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 get_tennent_id(self):
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
- is_ibm_cloud_env = is_ibm_cloud()
158
- is_local = is_local_dev()
159
- if is_ibm_cloud_env is True:
160
- crn = input("Please enter your CRN which can be gotten from the IBM Cloud UI: ")
161
- if crn == "":
162
- logger.error("You must enter your CRN for IBM Cloud instances")
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
- is_crn_correct = self.check_crn_is_correct(crn)
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 is_local
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 is_ibm_cloud_env:
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
- scopes: Annotated[
227
- List[str],
233
+ scope: Annotated[
234
+ str,
228
235
  typer.Option(
229
- '--scopes',
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
- scopes=scopes,
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
- scopes=kwargs.get("scopes")
200
+ scope=kwargs.get("scope")
201
201
  )
202
202
  case ConnectionType.OAUTH2_CLIENT_CREDS:
203
- return OAuth2ClientCredentials(
204
- client_id=kwargs.get("client_id"),
205
- client_secret=kwargs.get("client_secret"),
206
- token_url=kwargs.get("token_url"),
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"),