ibm-watsonx-orchestrate 1.6.3__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 (58) 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 +3 -2
  17. ibm_watsonx_orchestrate/cli/commands/channels/webchat/channels_webchat_controller.py +1 -2
  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 +2 -0
  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/server/server_command.py +94 -36
  31. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_command.py +1 -1
  32. ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +11 -4
  33. ibm_watsonx_orchestrate/cli/config.py +3 -3
  34. ibm_watsonx_orchestrate/cli/init_helper.py +10 -1
  35. ibm_watsonx_orchestrate/cli/main.py +5 -0
  36. ibm_watsonx_orchestrate/client/base_api_client.py +12 -0
  37. ibm_watsonx_orchestrate/client/copilot/cpe/copilot_cpe_client.py +67 -0
  38. ibm_watsonx_orchestrate/client/knowledge_bases/knowledge_base_client.py +1 -1
  39. ibm_watsonx_orchestrate/client/local_service_instance.py +3 -1
  40. ibm_watsonx_orchestrate/client/service_instance.py +33 -7
  41. ibm_watsonx_orchestrate/client/utils.py +15 -13
  42. ibm_watsonx_orchestrate/docker/compose-lite.yml +198 -6
  43. ibm_watsonx_orchestrate/docker/default.env +36 -12
  44. ibm_watsonx_orchestrate/flow_builder/flows/__init__.py +9 -4
  45. ibm_watsonx_orchestrate/flow_builder/flows/decorators.py +4 -2
  46. ibm_watsonx_orchestrate/flow_builder/flows/events.py +10 -9
  47. ibm_watsonx_orchestrate/flow_builder/flows/flow.py +131 -20
  48. ibm_watsonx_orchestrate/flow_builder/node.py +18 -1
  49. ibm_watsonx_orchestrate/flow_builder/types.py +271 -16
  50. ibm_watsonx_orchestrate/flow_builder/utils.py +120 -6
  51. ibm_watsonx_orchestrate/utils/exceptions.py +23 -0
  52. {ibm_watsonx_orchestrate-1.6.3.dist-info → ibm_watsonx_orchestrate-1.6.4.dist-info}/METADATA +3 -7
  53. {ibm_watsonx_orchestrate-1.6.3.dist-info → ibm_watsonx_orchestrate-1.6.4.dist-info}/RECORD +56 -53
  54. ibm_watsonx_orchestrate/agent_builder/utils/pydantic_utils.py +0 -149
  55. ibm_watsonx_orchestrate/flow_builder/resources/flow_status.openapi.yml +0 -66
  56. {ibm_watsonx_orchestrate-1.6.3.dist-info → ibm_watsonx_orchestrate-1.6.4.dist-info}/WHEEL +0 -0
  57. {ibm_watsonx_orchestrate-1.6.3.dist-info → ibm_watsonx_orchestrate-1.6.4.dist-info}/entry_points.txt +0 -0
  58. {ibm_watsonx_orchestrate-1.6.3.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:
@@ -13,7 +13,8 @@ class ChannelType(str, Enum):
13
13
 
14
14
  def __str__(self):
15
15
  return self.value
16
-
16
+
17
+
17
18
  class RuntimeEnvironmentType(str, Enum):
18
19
  LOCAL = 'local'
19
20
  CPD = 'cpd'
@@ -22,6 +23,6 @@ class RuntimeEnvironmentType(str, Enum):
22
23
 
23
24
  def __str__(self):
24
25
  return self.value
25
-
26
+
26
27
  def __repr__(self):
27
28
  return self.value
@@ -5,7 +5,6 @@ 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
7
  from ibm_watsonx_orchestrate.cli.commands.channels.types import RuntimeEnvironmentType
8
-
9
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
10
9
 
11
10
  from ibm_watsonx_orchestrate.client.agents.agent_client import AgentClient
@@ -98,7 +97,7 @@ class ChannelsWebchatController:
98
97
  if target_env == 'draft' and is_saas == True:
99
98
  logger.error(f'For SAAS, please ensure this agent exists in a Live Environment')
100
99
  exit(1)
101
-
100
+
102
101
  return filtered_environments[0].get("id")
103
102
 
104
103
  def get_tenant_id(self):
@@ -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"),
@@ -0,0 +1,65 @@
1
+ import typer
2
+ from typing_extensions import Annotated
3
+ from pathlib import Path
4
+ from ibm_watsonx_orchestrate.cli.commands.copilot.copilot_controller import prompt_tune, create_agent
5
+ from ibm_watsonx_orchestrate.cli.commands.copilot.copilot_server_controller import start_server, stop_server
6
+
7
+ copilot_app = typer.Typer(no_args_is_help=True)
8
+
9
+ # Server Commands
10
+ @copilot_app.command(name="start", help='Start the copilot server')
11
+ def start_server_command(
12
+ user_env_file: str = typer.Option(
13
+ None,
14
+ "--env-file", "-e",
15
+ help="Path to a .env file that overrides default.env. Then environment variables override both."
16
+ )
17
+ ):
18
+ user_env_file_path = Path(user_env_file) if user_env_file else None
19
+
20
+ start_server(user_env_file_path=user_env_file_path)
21
+
22
+ @copilot_app.command(name="stop", help='Stop the copilot server')
23
+ def stop_server_command():
24
+ stop_server()
25
+
26
+ # Functional Commands
27
+ @copilot_app.command(name="prompt-tune", help='Tune the instructions of an Agent using IBM Conversational Prompt Engineering (CPE) to improve agent performance')
28
+ def prompt_tume_command(
29
+ file: Annotated[
30
+ str,
31
+ typer.Option("--file", "-f", help="Path to agent spec file"),
32
+ ] = None,
33
+ output_file: Annotated[
34
+ str,
35
+ typer.Option("--output-file", "-o", help="Optional output file to avoid overwriting existing agent spec"),
36
+ ] = None,
37
+ dry_run_flag: Annotated[
38
+ bool,
39
+ typer.Option("--dry-run", help="Dry run will prevent the tuned content being saved and output the results to console"),
40
+ ] = False,
41
+ llm: Annotated[
42
+ str,
43
+ typer.Option("--llm", help="Select the agent LLM"),
44
+ ] = None,
45
+ samples: Annotated[
46
+ str,
47
+ typer.Option("--samples", "-s", help="Path to utterances sample file (txt file where each line is a utterance, or csv file with a single \"input\" column)"),
48
+ ] = None
49
+ ):
50
+ if file is None:
51
+ # create agent yaml from scratch
52
+ create_agent(
53
+ llm=llm,
54
+ output_file=output_file,
55
+ samples_file=samples,
56
+ dry_run_flag=dry_run_flag
57
+ )
58
+ else:
59
+ # improve existing agent instruction
60
+ prompt_tune(
61
+ agent_spec=file,
62
+ samples_file=samples,
63
+ output_file=output_file,
64
+ dry_run_flag=dry_run_flag,
65
+ )