ibm-watsonx-orchestrate 1.7.0a0__py3-none-any.whl → 1.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. ibm_watsonx_orchestrate/__init__.py +1 -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/agents/webchat_customizations/prompts.py +1 -0
  7. ibm_watsonx_orchestrate/agent_builder/connections/connections.py +25 -10
  8. ibm_watsonx_orchestrate/agent_builder/connections/types.py +19 -11
  9. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/knowledge_base_requests.py +1 -22
  10. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +1 -17
  11. ibm_watsonx_orchestrate/agent_builder/tools/base_tool.py +2 -1
  12. ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +14 -13
  13. ibm_watsonx_orchestrate/agent_builder/tools/python_tool.py +136 -92
  14. ibm_watsonx_orchestrate/agent_builder/tools/types.py +10 -9
  15. ibm_watsonx_orchestrate/cli/commands/agents/agents_command.py +7 -7
  16. ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +35 -7
  17. ibm_watsonx_orchestrate/cli/commands/channels/types.py +15 -2
  18. ibm_watsonx_orchestrate/cli/commands/channels/webchat/channels_webchat_controller.py +35 -25
  19. ibm_watsonx_orchestrate/cli/commands/chat/chat_command.py +2 -0
  20. ibm_watsonx_orchestrate/cli/commands/connections/connections_command.py +14 -6
  21. ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +12 -12
  22. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_command.py +65 -0
  23. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_controller.py +368 -0
  24. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_server_controller.py +170 -0
  25. ibm_watsonx_orchestrate/cli/commands/environment/environment_controller.py +6 -6
  26. ibm_watsonx_orchestrate/cli/commands/environment/types.py +3 -1
  27. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_command.py +134 -36
  28. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_controller.py +42 -11
  29. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_command.py +0 -18
  30. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +36 -20
  31. ibm_watsonx_orchestrate/cli/commands/models/models_command.py +1 -1
  32. ibm_watsonx_orchestrate/cli/commands/models/models_controller.py +5 -8
  33. ibm_watsonx_orchestrate/cli/commands/server/server_command.py +59 -10
  34. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_command.py +1 -1
  35. ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +93 -14
  36. ibm_watsonx_orchestrate/cli/config.py +3 -3
  37. ibm_watsonx_orchestrate/cli/init_helper.py +10 -1
  38. ibm_watsonx_orchestrate/cli/main.py +5 -0
  39. ibm_watsonx_orchestrate/client/base_api_client.py +12 -0
  40. ibm_watsonx_orchestrate/client/connections/connections_client.py +5 -30
  41. ibm_watsonx_orchestrate/client/copilot/cpe/copilot_cpe_client.py +67 -0
  42. ibm_watsonx_orchestrate/client/knowledge_bases/knowledge_base_client.py +1 -1
  43. ibm_watsonx_orchestrate/client/local_service_instance.py +3 -1
  44. ibm_watsonx_orchestrate/client/service_instance.py +33 -7
  45. ibm_watsonx_orchestrate/client/utils.py +49 -8
  46. ibm_watsonx_orchestrate/docker/compose-lite.yml +25 -6
  47. ibm_watsonx_orchestrate/docker/default.env +26 -15
  48. ibm_watsonx_orchestrate/flow_builder/flows/__init__.py +9 -4
  49. ibm_watsonx_orchestrate/flow_builder/flows/decorators.py +4 -2
  50. ibm_watsonx_orchestrate/flow_builder/flows/events.py +10 -9
  51. ibm_watsonx_orchestrate/flow_builder/flows/flow.py +131 -20
  52. ibm_watsonx_orchestrate/flow_builder/node.py +18 -1
  53. ibm_watsonx_orchestrate/flow_builder/types.py +271 -15
  54. ibm_watsonx_orchestrate/flow_builder/utils.py +121 -6
  55. ibm_watsonx_orchestrate/utils/exceptions.py +23 -0
  56. {ibm_watsonx_orchestrate-1.7.0a0.dist-info → ibm_watsonx_orchestrate-1.8.0.dist-info}/METADATA +5 -5
  57. {ibm_watsonx_orchestrate-1.7.0a0.dist-info → ibm_watsonx_orchestrate-1.8.0.dist-info}/RECORD +60 -56
  58. ibm_watsonx_orchestrate/flow_builder/resources/flow_status.openapi.yml +0 -66
  59. {ibm_watsonx_orchestrate-1.7.0a0.dist-info → ibm_watsonx_orchestrate-1.8.0.dist-info}/WHEEL +0 -0
  60. {ibm_watsonx_orchestrate-1.7.0a0.dist-info → ibm_watsonx_orchestrate-1.8.0.dist-info}/entry_points.txt +0 -0
  61. {ibm_watsonx_orchestrate-1.7.0a0.dist-info → ibm_watsonx_orchestrate-1.8.0.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
 
@@ -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 ValueError("OpenAPI definition must include exactly one server")
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 ValueError("'security' scheme must be of type 'apiKey'")
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 ValueError("One binding must be set")
177
+ raise BadRequest("One binding must be set")
177
178
  if sum(bindings) > 1:
178
- raise ValueError("Only one binding can be set")
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 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 = {
@@ -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 = [Agent.model_validate(agent) for agent in response]
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 = [ExternalAgent.model_validate(agent) for agent in response]
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 = [AssistantAgent.model_validate(agent) for agent in response]
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 ValueError("'kind' must be 'native'")
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:
@@ -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: {{
@@ -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
 
@@ -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