ibm-watsonx-orchestrate 1.8.0b0__py3-none-any.whl → 1.8.0b1__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 (27) hide show
  1. ibm_watsonx_orchestrate/__init__.py +1 -1
  2. ibm_watsonx_orchestrate/agent_builder/agents/types.py +12 -0
  3. ibm_watsonx_orchestrate/agent_builder/connections/types.py +14 -2
  4. ibm_watsonx_orchestrate/cli/commands/channels/types.py +15 -2
  5. ibm_watsonx_orchestrate/cli/commands/channels/webchat/channels_webchat_controller.py +5 -4
  6. ibm_watsonx_orchestrate/cli/commands/connections/connections_command.py +14 -6
  7. ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +6 -8
  8. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_controller.py +111 -36
  9. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_server_controller.py +23 -7
  10. ibm_watsonx_orchestrate/cli/commands/environment/types.py +1 -1
  11. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_command.py +16 -6
  12. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_controller.py +20 -2
  13. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +10 -8
  14. ibm_watsonx_orchestrate/cli/commands/server/server_command.py +1 -9
  15. ibm_watsonx_orchestrate/client/copilot/cpe/copilot_cpe_client.py +2 -1
  16. ibm_watsonx_orchestrate/client/utils.py +5 -4
  17. ibm_watsonx_orchestrate/docker/compose-lite.yml +9 -2
  18. ibm_watsonx_orchestrate/docker/default.env +6 -5
  19. ibm_watsonx_orchestrate/flow_builder/flows/__init__.py +8 -5
  20. ibm_watsonx_orchestrate/flow_builder/flows/flow.py +47 -7
  21. ibm_watsonx_orchestrate/flow_builder/node.py +7 -1
  22. ibm_watsonx_orchestrate/flow_builder/types.py +168 -65
  23. {ibm_watsonx_orchestrate-1.8.0b0.dist-info → ibm_watsonx_orchestrate-1.8.0b1.dist-info}/METADATA +2 -2
  24. {ibm_watsonx_orchestrate-1.8.0b0.dist-info → ibm_watsonx_orchestrate-1.8.0b1.dist-info}/RECORD +27 -27
  25. {ibm_watsonx_orchestrate-1.8.0b0.dist-info → ibm_watsonx_orchestrate-1.8.0b1.dist-info}/WHEEL +0 -0
  26. {ibm_watsonx_orchestrate-1.8.0b0.dist-info → ibm_watsonx_orchestrate-1.8.0b1.dist-info}/entry_points.txt +0 -0
  27. {ibm_watsonx_orchestrate-1.8.0b0.dist-info → ibm_watsonx_orchestrate-1.8.0b1.dist-info}/licenses/LICENSE +0 -0
@@ -5,7 +5,7 @@
5
5
 
6
6
  pkg_name = "ibm-watsonx-orchestrate"
7
7
 
8
- __version__ = "1.8.0b0"
8
+ __version__ = "1.8.0b1"
9
9
 
10
10
 
11
11
 
@@ -29,6 +29,12 @@ yaml.representer.SafeRepresenter.add_representer(str, str_presenter) # to use wi
29
29
  class SpecVersion(str, Enum):
30
30
  V1 = "v1"
31
31
 
32
+ def __str__(self):
33
+ return self.value
34
+
35
+ def __repr__(self):
36
+ return repr(self.value)
37
+
32
38
 
33
39
  class AgentKind(str, Enum):
34
40
  NATIVE = "native"
@@ -104,6 +110,12 @@ class AgentStyle(str, Enum):
104
110
  REACT = "react"
105
111
  PLANNER = "planner"
106
112
 
113
+ def __str__(self):
114
+ return self.value
115
+
116
+ def __repr__(self):
117
+ return repr(self.value)
118
+
107
119
  class AgentGuideline(BaseModel):
108
120
  model_config = ConfigDict(arbitrary_types_allowed=True)
109
121
 
@@ -75,6 +75,16 @@ class ConnectionType(str, Enum):
75
75
 
76
76
  def __repr__(self):
77
77
  return repr(self.value)
78
+
79
+ class ConnectionSendVia(str,Enum):
80
+ HEADER = 'header'
81
+ BODY = 'body'
82
+
83
+ def __str__(self):
84
+ return self.value
85
+
86
+ def __repr__(self):
87
+ return repr(self.value)
78
88
 
79
89
  OAUTH_CONNECTION_TYPES = {
80
90
  ConnectionType.OAUTH2_AUTH_CODE,
@@ -177,7 +187,7 @@ class OAuth2AuthCodeCredentials(BaseModel):
177
187
  client_secret: str
178
188
  token_url: str
179
189
  authorization_url: str
180
- scopes : Optional[List[str]] = None
190
+ scope : Optional[str] = None
181
191
 
182
192
  # class OAuth2ImplicitCredentials(BaseModel):
183
193
  # client_id: str
@@ -193,7 +203,9 @@ class OAuth2ClientCredentials(BaseModel):
193
203
  client_id: str
194
204
  client_secret: str
195
205
  token_url: str
196
- scopes : Optional[List[str]] = None
206
+ scope : Optional[str] = None
207
+ send_via: ConnectionSendVia = ConnectionSendVia.HEADER
208
+ grant_type: str = "client_credentials"
197
209
 
198
210
  class OAuthOnBehalfOfCredentials(BaseModel):
199
211
  client_id: str
@@ -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,6 +4,7 @@ import jwt
4
4
  import sys
5
5
 
6
6
  from ibm_watsonx_orchestrate.cli.config import Config, ENV_WXO_URL_OPT, ENVIRONMENTS_SECTION_HEADER, CONTEXT_SECTION_HEADER, CONTEXT_ACTIVE_ENV_OPT, CHAT_UI_PORT
7
+ from ibm_watsonx_orchestrate.cli.commands.channels.types import RuntimeEnvironmentType
7
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
@@ -158,13 +159,13 @@ class ChannelsWebchatController:
158
159
  environment = get_environment()
159
160
 
160
161
  match (environment):
161
- case "local":
162
+ case RuntimeEnvironmentType.LOCAL:
162
163
  tenant_id = self.get_tenant_id_local()
163
164
 
164
- case "cpd":
165
+ case RuntimeEnvironmentType.CPD:
165
166
  tenant_id = get_cpd_instance_id_from_url()
166
167
 
167
- case "ibmcloud":
168
+ case RuntimeEnvironmentType.IBM_CLOUD:
168
169
  crn = input("Please enter your CRN which can be retrieved from the IBM Cloud UI: ")
169
170
  if crn == "":
170
171
  logger.error("You must enter your CRN for IBM Cloud instances")
@@ -175,7 +176,7 @@ class ChannelsWebchatController:
175
176
  sys.exit(1)
176
177
  tenant_id = self.extract_tenant_id_from_crn(crn)
177
178
 
178
- case "ga":
179
+ case RuntimeEnvironmentType.AWS:
179
180
  tenant_id = self.get_tenant_id()
180
181
 
181
182
  case _:
@@ -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"),
@@ -11,8 +11,9 @@ from requests import ConnectionError
11
11
  from typing import List
12
12
  from ibm_watsonx_orchestrate.client.base_api_client import ClientAPIException
13
13
  from ibm_watsonx_orchestrate.agent_builder.tools import ToolSpec, ToolPermission, ToolRequestBody, ToolResponseBody
14
- from ibm_watsonx_orchestrate.cli.commands.agents.agents_controller import AgentsController, AgentKind
15
- from ibm_watsonx_orchestrate.agent_builder.agents.types import DEFAULT_LLM
14
+ from ibm_watsonx_orchestrate.cli.commands.agents.agents_controller import AgentsController, AgentKind, SpecVersion
15
+ from ibm_watsonx_orchestrate.agent_builder.agents.types import DEFAULT_LLM, BaseAgentSpec
16
+ from ibm_watsonx_orchestrate.client.agents.agent_client import AgentClient
16
17
  from ibm_watsonx_orchestrate.client.tools.tool_client import ToolClient
17
18
  from ibm_watsonx_orchestrate.client.copilot.cpe.copilot_cpe_client import CPEClient
18
19
  from ibm_watsonx_orchestrate.client.utils import instantiate_client
@@ -20,21 +21,24 @@ from ibm_watsonx_orchestrate.utils.exceptions import BadRequest
20
21
 
21
22
  logger = logging.getLogger(__name__)
22
23
 
24
+
23
25
  def _validate_output_file(output_file: str, dry_run_flag: bool) -> None:
24
26
  if not output_file and not dry_run_flag:
25
- logger.error("Please provide a valid yaml output file. Or use the `--dry-run` flag to output generated agent content to terminal")
27
+ logger.error(
28
+ "Please provide a valid yaml output file. Or use the `--dry-run` flag to output generated agent content to terminal")
26
29
  sys.exit(1)
27
-
30
+
28
31
  if output_file and dry_run_flag:
29
32
  logger.error("Cannot set output file when performing a dry run")
30
33
  sys.exit(1)
31
-
34
+
32
35
  if output_file:
33
36
  _, file_extension = os.path.splitext(output_file)
34
- if file_extension not in {".yaml", ".yml", ".json"}:
37
+ if file_extension not in {".yaml", ".yml", ".json"}:
35
38
  logger.error("Output file must be of type '.yaml', '.yml' or '.json'")
36
39
  sys.exit(1)
37
40
 
41
+
38
42
  def _get_progress_spinner() -> Progress:
39
43
  console = Console()
40
44
  return Progress(
@@ -44,16 +48,22 @@ def _get_progress_spinner() -> Progress:
44
48
  console=console,
45
49
  )
46
50
 
51
+
47
52
  def _get_incomplete_tool_from_name(tool_name: str) -> dict:
48
53
  input_schema = ToolRequestBody(**{"type": "object", "properties": {}})
49
54
  output_schema = ToolResponseBody(**{"description": "None"})
50
- spec = ToolSpec(**{"name": tool_name, "description": tool_name, "permission": ToolPermission.ADMIN, "input_schema": input_schema, "output_schema": output_schema})
55
+ spec = ToolSpec(**{"name": tool_name, "description": tool_name, "permission": ToolPermission.ADMIN,
56
+ "input_schema": input_schema, "output_schema": output_schema})
57
+ return spec.model_dump()
58
+
59
+ def _get_incomplete_agent_from_name(agent_name: str) -> dict:
60
+ spec = BaseAgentSpec(**{"name": agent_name, "description": agent_name, "kind": AgentKind.NATIVE})
51
61
  return spec.model_dump()
52
62
 
53
63
  def _get_tools_from_names(tool_names: List[str]) -> List[dict]:
54
64
  if not len(tool_names):
55
65
  return []
56
-
66
+
57
67
  tool_client = get_tool_client()
58
68
 
59
69
  try:
@@ -61,25 +71,63 @@ def _get_tools_from_names(tool_names: List[str]) -> List[dict]:
61
71
  task = progress.add_task(description="Fetching tools", total=None)
62
72
  tools = tool_client.get_drafts_by_names(tool_names)
63
73
  found_tools = {tool.get("name") for tool in tools}
64
- rich.print("\n")
74
+ progress.remove_task(task)
75
+ progress.refresh()
65
76
  for tool_name in tool_names:
66
77
  if tool_name not in found_tools:
67
- logger.warning(f"Failed to find tool named '{tool_name}'. Falling back to incomplete tool definition. Copilot performance maybe effected.")
78
+ logger.warning(
79
+ f"Failed to find tool named '{tool_name}'. Falling back to incomplete tool definition. Copilot performance maybe effected.")
68
80
  tools.append(_get_incomplete_tool_from_name(tool_name))
69
- progress.remove_task(task)
70
81
  except ConnectionError:
71
- logger.warning(f"Failed to fetch tools from server. For optimal results please start the server and import the relevant tools {', '.join(tool_names)}.")
82
+ logger.warning(
83
+ f"Failed to fetch tools from server. For optimal results please start the server and import the relevant tools {', '.join(tool_names)}.")
72
84
  tools = []
73
85
  for tool_name in tool_names:
74
86
  tools.append(_get_incomplete_tool_from_name(tool_name))
75
87
 
76
88
  return tools
77
89
 
90
+
91
+ def _get_agents_from_names(collaborators_names: List[str]) -> List[dict]:
92
+ if not len(collaborators_names):
93
+ return []
94
+
95
+ native_agents_client = get_native_client()
96
+
97
+ try:
98
+ with _get_progress_spinner() as progress:
99
+ task = progress.add_task(description="Fetching agents", total=None)
100
+ agents = native_agents_client.get_drafts_by_names(collaborators_names)
101
+ found_agents = {tool.get("name") for tool in agents}
102
+ progress.remove_task(task)
103
+ progress.refresh()
104
+ for collaborator_name in collaborators_names:
105
+ if collaborator_name not in found_agents:
106
+ logger.warning(
107
+ f"Failed to find agent named '{collaborator_name}'. Falling back to incomplete agent definition. Copilot performance maybe effected.")
108
+ agents.append(_get_incomplete_agent_from_name(collaborator_name))
109
+ except ConnectionError:
110
+ logger.warning(
111
+ f"Failed to fetch tools from server. For optimal results please start the server and import the relevant tools {', '.join(collaborators_names)}.")
112
+ agents = []
113
+ for collaborator_name in collaborators_names:
114
+ agents.append(_get_incomplete_agent_from_name(collaborator_name))
115
+
116
+ return agents
117
+
78
118
  def get_cpe_client() -> CPEClient:
79
119
  url = os.getenv('CPE_URL', "http://localhost:8081")
80
120
  return instantiate_client(client=CPEClient, url=url)
81
121
 
82
122
 
123
+ def get_tool_client(*args, **kwargs):
124
+ return instantiate_client(ToolClient)
125
+
126
+
127
+ def get_native_client(*args, **kwargs):
128
+ return instantiate_client(AgentClient)
129
+
130
+
83
131
  def gather_utterances(max: int) -> list[str]:
84
132
  utterances = []
85
133
  logger.info("Please provide 3 sample utterances you expect your agent to handle:")
@@ -98,7 +146,16 @@ def gather_utterances(max: int) -> list[str]:
98
146
 
99
147
  return utterances
100
148
 
101
- def pre_cpe_step(cpe_client, tool_client):
149
+
150
+ def get_deployed_tools_agents():
151
+ all_tools = find_tools_by_description(tool_client=get_tool_client(), description=None)
152
+ # TODO: this brings only the "native" agents. Can external and assistant agents also be collaborators?
153
+ all_agents = find_agents(agent_client=get_native_client())
154
+ return {"tools": all_tools, "agents": all_agents}
155
+
156
+
157
+ def pre_cpe_step(cpe_client):
158
+ tools_agents = get_deployed_tools_agents()
102
159
  user_message = ""
103
160
  with _get_progress_spinner() as progress:
104
161
  task = progress.add_task(description="Initilizing Prompt Engine", total=None)
@@ -113,31 +170,47 @@ def pre_cpe_step(cpe_client, tool_client):
113
170
  message_content = {"user_message": user_message}
114
171
  elif "description" in response and response["description"]:
115
172
  res["description"] = response["description"]
116
- tools = find_tools_by_description(res["description"], tool_client)
117
- message_content = {"tools": tools}
173
+ message_content = tools_agents
118
174
  elif "metadata" in response:
119
175
  res["agent_name"] = response["metadata"]["agent_name"]
120
176
  res["agent_style"] = response["metadata"]["style"]
121
- res["tools"] = [t for t in tools if t["name"] in response["metadata"]["tools"]]
177
+ res["tools"] = [t for t in tools_agents["tools"] if t["name"] in response["metadata"]["tools"]]
178
+ res["collaborators"] = [a for a in tools_agents["agents"] if
179
+ a["name"] in response["metadata"]["collaborators"]]
122
180
  return res
123
181
  with _get_progress_spinner() as progress:
124
182
  task = progress.add_task(description="Thinking...", total=None)
125
183
  response = cpe_client.submit_pre_cpe_chat(**message_content)
126
184
  progress.remove_task(task)
127
185
 
128
- # TODO: Add description RAG search
186
+
129
187
  def find_tools_by_description(description, tool_client):
130
188
  with _get_progress_spinner() as progress:
131
189
  task = progress.add_task(description="Fetching Tools", total=None)
132
190
  try:
133
191
  tools = tool_client.get()
192
+ progress.remove_task(task)
134
193
  except ConnectionError:
135
194
  tools = []
136
- rich.print("\n")
195
+ progress.remove_task(task)
196
+ progress.refresh()
137
197
  logger.warning("Failed to contact wxo server to fetch tools. Proceeding with empty tool list")
138
- progress.remove_task(task)
139
198
  return tools
140
199
 
200
+ def find_agents(agent_client):
201
+ with _get_progress_spinner() as progress:
202
+ task = progress.add_task(description="Fetching Agents", total=None)
203
+ try:
204
+ agents = agent_client.get()
205
+ progress.remove_task(task)
206
+ except ConnectionError:
207
+ agents = []
208
+ progress.remove_task(task)
209
+ progress.refresh()
210
+ logger.warning("Failed to contact wxo server to fetch agents. Proceeding with empty agent list")
211
+ return agents
212
+
213
+
141
214
  def gather_examples(samples_file=None):
142
215
  if samples_file:
143
216
  if samples_file.endswith('.txt'):
@@ -167,7 +240,7 @@ def talk_to_cpe(cpe_client, samples_file=None, context_data=None):
167
240
  examples = gather_examples(samples_file)
168
241
  # upload or gather input examples
169
242
  context_data['examples'] = examples
170
- response=None
243
+ response = None
171
244
  with _get_progress_spinner() as progress:
172
245
  task = progress.add_task(description="Thinking...", total=None)
173
246
  response = cpe_client.init_with_context(context_data=context_data)
@@ -199,20 +272,23 @@ def prompt_tune(agent_spec: str, output_file: str | None, samples_file: str | No
199
272
 
200
273
  if not output_file and not dry_run_flag:
201
274
  output_file = agent_spec
202
-
275
+
203
276
  _validate_output_file(output_file, dry_run_flag)
204
277
 
205
278
  client = get_cpe_client()
206
279
 
207
280
  instr = agent.instructions
208
- prompt = 'My current prompt is:\n' + instr if instr else "I don't have an initial prompt."
209
281
 
210
282
  tools = _get_tools_from_names(agent.tools)
211
283
 
284
+ collaborators = _get_agents_from_names(agent.collaborators)
212
285
  try:
213
- new_prompt = talk_to_cpe(cpe_client=client, samples_file=samples_file, context_data={"prompt": prompt, 'tools': tools, 'description': agent.description})
286
+ new_prompt = talk_to_cpe(cpe_client=client, samples_file=samples_file,
287
+ context_data={"initial_instruction": instr, 'tools': tools, 'description': agent.description,
288
+ "collaborators": collaborators})
214
289
  except ConnectionError:
215
- logger.error("Failed to connect to Copilot server. Please ensure Copilot is running via `orchestrate copilot start`")
290
+ logger.error(
291
+ "Failed to connect to Copilot server. Please ensure Copilot is running via `orchestrate copilot start`")
216
292
  sys.exit(1)
217
293
  except ClientAPIException:
218
294
  logger.error("An unexpected server error has occur with in the Copilot server. Please check the logs via `orchestrate server logs`")
@@ -223,59 +299,58 @@ def prompt_tune(agent_spec: str, output_file: str | None, samples_file: str | No
223
299
  agent.instructions = new_prompt
224
300
 
225
301
  if dry_run_flag:
226
- rich.print(agent.model_dump())
302
+ rich.print(agent.model_dump(exclude_none=True))
227
303
  else:
228
304
  if os.path.dirname(output_file):
229
305
  os.makedirs(os.path.dirname(output_file), exist_ok=True)
230
306
  AgentsController.persist_record(agent, output_file=output_file)
231
307
 
232
308
 
233
- def get_tool_client(*args, **kwargs):
234
- return instantiate_client(ToolClient)
235
-
236
309
  def create_agent(output_file: str, llm: str, samples_file: str | None, dry_run_flag: bool = False) -> None:
237
310
  _validate_output_file(output_file, dry_run_flag)
238
311
  # 1. prepare the clients
239
312
  cpe_client = get_cpe_client()
240
- tool_client = get_tool_client()
241
313
 
242
314
  # 2. Pre-CPE stage:
243
315
  try:
244
- res = pre_cpe_step(cpe_client, tool_client)
316
+ res = pre_cpe_step(cpe_client)
245
317
  except ConnectionError:
246
- logger.error("Failed to connect to Copilot server. Please ensure Copilot is running via `orchestrate copilot start`")
318
+ logger.error(
319
+ "Failed to connect to Copilot server. Please ensure Copilot is running via `orchestrate copilot start`")
247
320
  sys.exit(1)
248
321
  except ClientAPIException:
249
322
  logger.error("An unexpected server error has occur with in the Copilot server. Please check the logs via `orchestrate server logs`")
250
323
  sys.exit(1)
251
324
 
252
325
  tools = res["tools"]
326
+ collaborators = res["collaborators"]
253
327
  description = res["description"]
254
328
  agent_name = res["agent_name"]
255
329
  agent_style = res["agent_style"]
256
330
 
257
331
  # 4. discuss the instructions
258
- instructions = talk_to_cpe(cpe_client, samples_file, {'tools': tools, 'description': description})
332
+ instructions = talk_to_cpe(cpe_client, samples_file, {'description': description, 'tools': tools, 'collaborators': collaborators})
259
333
 
260
334
  # 6. create and save the agent
261
335
  llm = llm if llm else DEFAULT_LLM
262
336
  params = {
263
337
  'style': agent_style,
264
338
  'tools': [t['name'] for t in tools],
265
- 'llm': llm
339
+ 'llm': llm,
340
+ 'collaborators': [c['name'] for c in collaborators]
266
341
  }
267
342
  agent = AgentsController.generate_agent_spec(agent_name, AgentKind.NATIVE, description, **params)
268
343
  agent.instructions = instructions
344
+ agent.spec_version = SpecVersion.V1
269
345
 
270
346
  if dry_run_flag:
271
- rich.print(agent.model_dump())
347
+ rich.print(agent.model_dump(exclude_none=True))
272
348
  return
273
349
 
274
350
  if os.path.dirname(output_file):
275
351
  os.makedirs(os.path.dirname(output_file), exist_ok=True)
276
352
  AgentsController.persist_record(agent, output_file=output_file)
277
353
 
278
-
279
354
  message_lines = [
280
355
  "Your agent building session finished successfully!",
281
356
  f"Agent YAML saved in file:",
@@ -290,4 +365,4 @@ def create_agent(output_file: str, llm: str, samples_file: str | None, dry_run_f
290
365
  rich.print("╔" + "═" * frame_width + "╗")
291
366
  for line in message_lines:
292
367
  rich.print("║ " + line.ljust(max_length) + " ║")
293
- rich.print("╚" + "═" * frame_width + "╝")
368
+ rich.print("╚" + "═" * frame_width + "╝")
@@ -4,6 +4,7 @@ from pathlib import Path
4
4
  import subprocess
5
5
  import time
6
6
  import requests
7
+ from urllib.parse import urlparse
7
8
  from ibm_watsonx_orchestrate.cli.commands.server.server_command import (
8
9
  get_compose_file,
9
10
  ensure_docker_compose_installed,
@@ -16,15 +17,11 @@ from ibm_watsonx_orchestrate.cli.commands.server.server_command import (
16
17
  get_default_registry_env_vars_by_dev_edition_source,
17
18
  docker_login_by_dev_edition_source,
18
19
  write_merged_env_file,
20
+ apply_server_env_dict_defaults
19
21
  )
20
22
 
21
23
  logger = logging.getLogger(__name__)
22
24
 
23
- def _verify_env_contents(env: dict) -> None:
24
- if not env.get("WATSONX_APIKEY") or not env.get("WATSONX_SPACE_ID"):
25
- logger.error("The Copilot feature requires wx.ai credentials to passed through the provided env file. Please set 'WATSONX_SPACE_ID' and 'WATSONX_APIKEY'")
26
- sys.exit(1)
27
-
28
25
  def wait_for_wxo_cpe_health_check(timeout_seconds=45, interval_seconds=2):
29
26
  url = "http://localhost:8081/version"
30
27
  logger.info("Waiting for Copilot component to be initialized...")
@@ -42,6 +39,24 @@ def wait_for_wxo_cpe_health_check(timeout_seconds=45, interval_seconds=2):
42
39
  time.sleep(interval_seconds)
43
40
  return False
44
41
 
42
+ def _trim_authorization_urls(env_dict: dict) -> dict:
43
+ auth_url_key = "AUTHORIZATION_URL"
44
+ env_dict_copy = env_dict.copy()
45
+
46
+ auth_url = env_dict_copy.get(auth_url_key)
47
+ if not auth_url:
48
+ return env_dict_copy
49
+
50
+
51
+ parsed_url = urlparse(auth_url)
52
+ new_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
53
+ env_dict_copy[auth_url_key] = new_url
54
+
55
+ return env_dict_copy
56
+
57
+
58
+
59
+
45
60
  def run_compose_lite_cpe(user_env_file: Path) -> bool:
46
61
  compose_path = get_compose_file()
47
62
  compose_command = ensure_docker_compose_installed()
@@ -66,8 +81,9 @@ def run_compose_lite_cpe(user_env_file: Path) -> bool:
66
81
  **default_env,
67
82
  **user_env,
68
83
  }
69
-
70
- _verify_env_contents(merged_env_dict)
84
+
85
+ merged_env_dict = apply_server_env_dict_defaults(merged_env_dict)
86
+ merged_env_dict = _trim_authorization_urls(merged_env_dict)
71
87
 
72
88
  try:
73
89
  docker_login_by_dev_edition_source(merged_env_dict, dev_edition_source)
@@ -9,4 +9,4 @@ class EnvironmentAuthType(str, Enum):
9
9
  CPD = 'cpd'
10
10
 
11
11
  def __str__(self):
12
- return self.value
12
+ return self.value
@@ -25,14 +25,20 @@ def read_env_file(env_path: Path|str) -> dict:
25
25
  return dotenv_values(str(env_path))
26
26
 
27
27
  def validate_watsonx_credentials(user_env_file: str) -> bool:
28
- required_keys = ["WATSONX_SPACE_ID", "WATSONX_APIKEY"]
28
+ required_sets = [
29
+ ["WATSONX_SPACE_ID", "WATSONX_APIKEY"],
30
+ ["WO_INSTANCE", "WO_API_KEY"]
31
+ ]
29
32
 
30
- if all(key in os.environ for key in required_keys):
33
+ def has_valid_keys(env: dict) -> bool:
34
+ return any(all(key in env for key in key_set) for key_set in required_sets)
35
+
36
+ if has_valid_keys(os.environ):
31
37
  logger.info("WatsonX credentials validated successfully.")
32
38
  return
33
39
 
34
40
  if user_env_file is None:
35
- logger.error("WatsonX credentials are not set. Please set WATSONX_SPACE_ID and WATSONX_APIKEY in your system environment variables or include them in your enviroment file and pass it with --env-file option.")
41
+ logger.error("WatsonX credentials are not set. Please set either WATSONX_SPACE_ID and WATSONX_APIKEY or WO_INSTANCE and WO_API_KEY in your system environment variables or include them in your environment file and pass it with --env-file option.")
36
42
  sys.exit(1)
37
43
 
38
44
  if not Path(user_env_file).exists():
@@ -41,11 +47,15 @@ def validate_watsonx_credentials(user_env_file: str) -> bool:
41
47
 
42
48
  user_env = read_env_file(user_env_file)
43
49
 
44
- if not all(key in user_env for key in required_keys):
45
- logger.error("Error: The environment file does not contain the required keys: WATSONX_SPACE_ID and WATSONX_APIKEY.")
50
+ if not has_valid_keys(user_env):
51
+ logger.error("Error: The environment file does not contain the required keys: either WATSONX_SPACE_ID and WATSONX_APIKEY or WO_INSTANCE and WO_API_KEY.")
46
52
  sys.exit(1)
47
53
 
48
- os.environ.update({key: user_env[key] for key in required_keys})
54
+ # Update os.environ with whichever set is present
55
+ for key_set in required_sets:
56
+ if all(key in user_env for key in key_set):
57
+ os.environ.update({key: user_env[key] for key in key_set})
58
+ break
49
59
  logger.info("WatsonX credentials validated successfully.")
50
60
 
51
61
  def read_csv(data_path: str, delimiter="\t"):