ibm-watsonx-orchestrate 1.5.0b1__py3-none-any.whl → 1.6.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 (71) hide show
  1. ibm_watsonx_orchestrate/__init__.py +1 -1
  2. ibm_watsonx_orchestrate/agent_builder/agents/__init__.py +1 -1
  3. ibm_watsonx_orchestrate/agent_builder/agents/agent.py +1 -0
  4. ibm_watsonx_orchestrate/agent_builder/agents/types.py +58 -4
  5. ibm_watsonx_orchestrate/agent_builder/agents/webchat_customizations/__init__.py +2 -0
  6. ibm_watsonx_orchestrate/agent_builder/agents/webchat_customizations/prompts.py +34 -0
  7. ibm_watsonx_orchestrate/agent_builder/agents/webchat_customizations/welcome_content.py +20 -0
  8. ibm_watsonx_orchestrate/agent_builder/connections/__init__.py +2 -2
  9. ibm_watsonx_orchestrate/agent_builder/connections/types.py +38 -31
  10. ibm_watsonx_orchestrate/agent_builder/model_policies/types.py +1 -1
  11. ibm_watsonx_orchestrate/agent_builder/models/types.py +0 -1
  12. ibm_watsonx_orchestrate/agent_builder/tools/flow_tool.py +83 -0
  13. ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +41 -3
  14. ibm_watsonx_orchestrate/agent_builder/tools/python_tool.py +2 -1
  15. ibm_watsonx_orchestrate/agent_builder/tools/types.py +14 -1
  16. ibm_watsonx_orchestrate/cli/commands/agents/agents_command.py +18 -1
  17. ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +153 -21
  18. ibm_watsonx_orchestrate/cli/commands/channels/webchat/channels_webchat_controller.py +104 -22
  19. ibm_watsonx_orchestrate/cli/commands/chat/chat_command.py +2 -0
  20. ibm_watsonx_orchestrate/cli/commands/connections/connections_command.py +26 -18
  21. ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +61 -61
  22. ibm_watsonx_orchestrate/cli/commands/environment/environment_command.py +29 -4
  23. ibm_watsonx_orchestrate/cli/commands/environment/environment_controller.py +74 -8
  24. ibm_watsonx_orchestrate/cli/commands/environment/types.py +1 -0
  25. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_command.py +312 -0
  26. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_controller.py +171 -0
  27. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_command.py +2 -2
  28. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +2 -2
  29. ibm_watsonx_orchestrate/cli/commands/models/model_provider_mapper.py +31 -25
  30. ibm_watsonx_orchestrate/cli/commands/models/models_command.py +6 -6
  31. ibm_watsonx_orchestrate/cli/commands/models/models_controller.py +17 -8
  32. ibm_watsonx_orchestrate/cli/commands/server/server_command.py +147 -21
  33. ibm_watsonx_orchestrate/cli/commands/server/types.py +2 -1
  34. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_controller.py +9 -6
  35. ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +111 -32
  36. ibm_watsonx_orchestrate/cli/config.py +2 -0
  37. ibm_watsonx_orchestrate/cli/main.py +6 -0
  38. ibm_watsonx_orchestrate/client/agents/agent_client.py +83 -9
  39. ibm_watsonx_orchestrate/client/agents/assistant_agent_client.py +3 -3
  40. ibm_watsonx_orchestrate/client/agents/external_agent_client.py +2 -2
  41. ibm_watsonx_orchestrate/client/base_api_client.py +11 -10
  42. ibm_watsonx_orchestrate/client/connections/connections_client.py +49 -14
  43. ibm_watsonx_orchestrate/client/connections/utils.py +4 -2
  44. ibm_watsonx_orchestrate/client/credentials.py +4 -0
  45. ibm_watsonx_orchestrate/client/local_service_instance.py +1 -1
  46. ibm_watsonx_orchestrate/client/model_policies/model_policies_client.py +2 -2
  47. ibm_watsonx_orchestrate/client/service_instance.py +42 -1
  48. ibm_watsonx_orchestrate/client/tools/tempus_client.py +8 -3
  49. ibm_watsonx_orchestrate/client/utils.py +37 -2
  50. ibm_watsonx_orchestrate/docker/compose-lite.yml +252 -81
  51. ibm_watsonx_orchestrate/docker/default.env +40 -15
  52. ibm_watsonx_orchestrate/docker/proxy-config-single.yaml +12 -0
  53. ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/flows/__init__.py +3 -2
  54. ibm_watsonx_orchestrate/flow_builder/flows/decorators.py +77 -0
  55. ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/flows/events.py +6 -1
  56. ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/flows/flow.py +85 -92
  57. ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/types.py +15 -6
  58. ibm_watsonx_orchestrate/flow_builder/utils.py +215 -0
  59. ibm_watsonx_orchestrate/run/connections.py +4 -4
  60. {ibm_watsonx_orchestrate-1.5.0b1.dist-info → ibm_watsonx_orchestrate-1.6.0.dist-info}/METADATA +2 -1
  61. {ibm_watsonx_orchestrate-1.5.0b1.dist-info → ibm_watsonx_orchestrate-1.6.0.dist-info}/RECORD +69 -62
  62. ibm_watsonx_orchestrate/experimental/flow_builder/flows/decorators.py +0 -144
  63. ibm_watsonx_orchestrate/experimental/flow_builder/utils.py +0 -115
  64. /ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/__init__.py +0 -0
  65. /ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/data_map.py +0 -0
  66. /ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/flows/constants.py +0 -0
  67. /ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/node.py +0 -0
  68. /ibm_watsonx_orchestrate/{experimental/flow_builder → flow_builder}/resources/flow_status.openapi.yml +0 -0
  69. {ibm_watsonx_orchestrate-1.5.0b1.dist-info → ibm_watsonx_orchestrate-1.6.0.dist-info}/WHEEL +0 -0
  70. {ibm_watsonx_orchestrate-1.5.0b1.dist-info → ibm_watsonx_orchestrate-1.6.0.dist-info}/entry_points.txt +0 -0
  71. {ibm_watsonx_orchestrate-1.5.0b1.dist-info → ibm_watsonx_orchestrate-1.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,7 +1,79 @@
1
1
  from ibm_watsonx_orchestrate.client.base_api_client import BaseAPIClient, ClientAPIException
2
- from typing_extensions import List
2
+ from typing_extensions import List, Optional
3
3
  from ibm_watsonx_orchestrate.client.utils import is_local_dev
4
+ from pydantic import BaseModel
4
5
 
6
+ def transform_agents_from_flat_agent_spec(agents: dict | list[dict] ) -> dict | list[dict]:
7
+ if isinstance(agents,list):
8
+ new_agents = []
9
+ for agent in agents:
10
+ new_agents.append(_transform_agent_from_flat_agent_spec(agent))
11
+ agents = new_agents
12
+ else:
13
+ agents = _transform_agent_from_flat_agent_spec(agents)
14
+
15
+ return agents
16
+
17
+
18
+ def _transform_agent_from_flat_agent_spec(agent_spec: dict ) -> dict:
19
+ transformed = {"additional_properties": {}}
20
+ for key,value in agent_spec.items():
21
+ if key == "starter_prompts":
22
+ if value:
23
+ value.pop("is_default_prompts",None)
24
+ value["customize"] = value.pop("prompts", [])
25
+
26
+ transformed["additional_properties"] |= { key: value }
27
+
28
+ elif key == "welcome_content":
29
+ if value:
30
+ value.pop("is_default_message", None)
31
+
32
+ transformed["additional_properties"] |= { key: value }
33
+
34
+ else:
35
+ transformed |= { key: value }
36
+
37
+ return transformed
38
+
39
+ def transform_agents_to_flat_agent_spec(agents: dict | list[dict] ) -> dict | list[dict]:
40
+ if isinstance(agents,list):
41
+ new_agents = []
42
+ for agent in agents:
43
+ new_agents.append(_transform_agent_to_flat_agent_spec(agent))
44
+ agents = new_agents
45
+ else:
46
+ agents = _transform_agent_to_flat_agent_spec(agents)
47
+
48
+ return agents
49
+
50
+ def _transform_agent_to_flat_agent_spec(agent_spec: dict ) -> dict:
51
+ additional_properties = agent_spec.get("additional_properties", None)
52
+ if not additional_properties:
53
+ return agent_spec
54
+
55
+ transformed = agent_spec
56
+ for key,value in additional_properties.items():
57
+ if key == "starter_prompts":
58
+ if value:
59
+ value["is_default_prompts"] = False
60
+ value["prompts"] = value.pop("customize", [])
61
+
62
+ transformed[key] = value
63
+
64
+ elif key == "welcome_content":
65
+ if value:
66
+ value["is_default_message"] = False
67
+
68
+ transformed[key] = value
69
+
70
+ transformed.pop("additional_properties",None)
71
+
72
+ return transformed
73
+
74
+ class AgentUpsertResponse(BaseModel):
75
+ id: Optional[str] = None
76
+ warning: Optional[str] = None
5
77
 
6
78
  class AgentClient(BaseAPIClient):
7
79
  """
@@ -12,14 +84,16 @@ class AgentClient(BaseAPIClient):
12
84
  self.base_endpoint = "/orchestrate/agents" if is_local_dev(self.base_url) else "/agents"
13
85
 
14
86
 
15
- def create(self, payload: dict) -> dict:
16
- return self._post(self.base_endpoint, data=payload)
87
+ def create(self, payload: dict) -> AgentUpsertResponse:
88
+ response = self._post(self.base_endpoint, data=transform_agents_from_flat_agent_spec(payload))
89
+ return AgentUpsertResponse.model_validate(response)
17
90
 
18
91
  def get(self) -> dict:
19
- return self._get(self.base_endpoint)
92
+ return transform_agents_to_flat_agent_spec(self._get(f"{self.base_endpoint}?include_hidden=true"))
20
93
 
21
- def update(self, agent_id: str, data: dict) -> dict:
22
- return self._patch(f"{self.base_endpoint}/{agent_id}", data=data)
94
+ def update(self, agent_id: str, data: dict) -> AgentUpsertResponse:
95
+ response = self._patch(f"{self.base_endpoint}/{agent_id}", data=transform_agents_from_flat_agent_spec(data))
96
+ return AgentUpsertResponse.model_validate(response)
23
97
 
24
98
  def delete(self, agent_id: str) -> dict:
25
99
  return self._delete(f"{self.base_endpoint}/{agent_id}")
@@ -29,14 +103,14 @@ class AgentClient(BaseAPIClient):
29
103
 
30
104
  def get_drafts_by_names(self, agent_names: List[str]) -> List[dict]:
31
105
  formatted_agent_names = [f"names={x}" for x in agent_names]
32
- return self._get(f"{self.base_endpoint}?{'&'.join(formatted_agent_names)}")
106
+ return transform_agents_to_flat_agent_spec(self._get(f"{self.base_endpoint}?{'&'.join(formatted_agent_names)}&include_hidden=true"))
33
107
 
34
108
  def get_draft_by_id(self, agent_id: str) -> List[dict]:
35
109
  if agent_id is None:
36
110
  return ""
37
111
  else:
38
112
  try:
39
- agent = self._get(f"{self.base_endpoint}/{agent_id}")
113
+ agent = transform_agents_to_flat_agent_spec(self._get(f"{self.base_endpoint}/{agent_id}"))
40
114
  return agent
41
115
  except ClientAPIException as e:
42
116
  if e.response.status_code == 404 and "not found with the given name" in e.response.text:
@@ -45,5 +119,5 @@ class AgentClient(BaseAPIClient):
45
119
 
46
120
  def get_drafts_by_ids(self, agent_ids: List[str]) -> List[dict]:
47
121
  formatted_agent_ids = [f"ids={x}" for x in agent_ids]
48
- return self._get(f"{self.base_endpoint}?{'&'.join(formatted_agent_ids)}")
122
+ return transform_agents_to_flat_agent_spec(self._get(f"{self.base_endpoint}?{'&'.join(formatted_agent_ids)}&include_hidden=true"))
49
123
 
@@ -10,7 +10,7 @@ class AssistantAgentClient(BaseAPIClient):
10
10
  return self._post("/assistants/watsonx", data=payload)
11
11
 
12
12
  def get(self) -> dict:
13
- return self._get("/assistants/watsonx")
13
+ return self._get("/assistants/watsonx?include_hidden=true")
14
14
 
15
15
  def update(self, agent_id: str, data: dict) -> dict:
16
16
  return self._patch(f"/assistants/watsonx/{agent_id}", data=data)
@@ -23,7 +23,7 @@ class AssistantAgentClient(BaseAPIClient):
23
23
 
24
24
  def get_drafts_by_names(self, agent_names: List[str]) -> List[dict]:
25
25
  formatted_agent_names = [f"names={x}" for x in agent_names]
26
- return self._get(f"/assistants/watsonx?{'&'.join(formatted_agent_names)}")
26
+ return self._get(f"/assistants/watsonx?{'&'.join(formatted_agent_names)}&include_hidden=true")
27
27
 
28
28
  def get_draft_by_id(self, agent_id: str) -> dict | str:
29
29
  if agent_id is None:
@@ -39,4 +39,4 @@ class AssistantAgentClient(BaseAPIClient):
39
39
 
40
40
  def get_drafts_by_ids(self, agent_ids: List[str]) -> List[dict]:
41
41
  formatted_agent_ids = [f"ids={x}" for x in agent_ids]
42
- return self._get(f"/assistants/watsonx?{'&'.join(formatted_agent_ids)}")
42
+ return self._get(f"/assistants/watsonx?{'&'.join(formatted_agent_ids)}&include_hidden=true")
@@ -10,7 +10,7 @@ class ExternalAgentClient(BaseAPIClient):
10
10
  return self._post("/agents/external-chat", data=payload)
11
11
 
12
12
  def get(self) -> dict:
13
- return self._get("/agents/external-chat")
13
+ return self._get("/agents/external-chat?include_hidden=true")
14
14
 
15
15
  def update(self, agent_id: str, data: dict) -> dict:
16
16
  return self._patch(f"/agents/external-chat/{agent_id}", data=data)
@@ -39,4 +39,4 @@ class ExternalAgentClient(BaseAPIClient):
39
39
 
40
40
  def get_drafts_by_ids(self, agent_ids: List[str]) -> List[dict]:
41
41
  formatted_agent_ids = [f"ids={x}" for x in agent_ids]
42
- return self._get(f"/agents/external-chat?{'&'.join(formatted_agent_ids)}")
42
+ return self._get(f"/agents/external-chat?{'&'.join(formatted_agent_ids)}&include_hidden=true")
@@ -24,8 +24,7 @@ class ClientAPIException(requests.HTTPError):
24
24
 
25
25
 
26
26
  class BaseAPIClient:
27
-
28
- def __init__(self, base_url: str, api_key: str = None, is_local: bool = False, authenticator: MCSPAuthenticator = None):
27
+ def __init__(self, base_url: str, api_key: str = None, is_local: bool = False, verify: str = None, authenticator: MCSPAuthenticator = None):
29
28
  self.base_url = base_url.rstrip("/") # remove trailing slash
30
29
  self.api_key = api_key
31
30
  self.authenticator = authenticator
@@ -33,9 +32,12 @@ class BaseAPIClient:
33
32
  # api path can be re-written by api proxy when deployed
34
33
  # TO-DO: re-visit this when shipping to production
35
34
  self.is_local = is_local
35
+ self.verify = verify
36
36
 
37
37
  if not self.is_local:
38
38
  self.base_url = f"{self.base_url}/v1/orchestrate"
39
+ else:
40
+ self.base_url = f"{self.base_url}/v1"
39
41
 
40
42
  def _get_headers(self) -> dict:
41
43
  headers = {}
@@ -46,9 +48,8 @@ class BaseAPIClient:
46
48
  return headers
47
49
 
48
50
  def _get(self, path: str, params: dict = None, data=None, return_raw=False) -> dict:
49
-
50
51
  url = f"{self.base_url}{path}"
51
- response = requests.get(url, headers=self._get_headers(), params=params, data=data)
52
+ response = requests.get(url, headers=self._get_headers(), params=params, data=data, verify=self.verify)
52
53
  self._check_response(response)
53
54
  if not return_raw:
54
55
  return response.json()
@@ -57,39 +58,39 @@ class BaseAPIClient:
57
58
 
58
59
  def _post(self, path: str, data: dict = None, files: dict = None) -> dict:
59
60
  url = f"{self.base_url}{path}"
60
- response = requests.post(url, headers=self._get_headers(), json=data, files=files)
61
+ response = requests.post(url, headers=self._get_headers(), json=data, files=files, verify=self.verify)
61
62
  self._check_response(response)
62
63
  return response.json() if response.text else {}
63
64
 
64
65
  def _post_form_data(self, path: str, data: dict = None, files: dict = None) -> dict:
65
66
  url = f"{self.base_url}{path}"
66
67
  # Use data argument instead of json so data is encoded as application/x-www-form-urlencoded
67
- response = requests.post(url, headers=self._get_headers(), data=data, files=files)
68
+ response = requests.post(url, headers=self._get_headers(), data=data, files=files, verify=self.verify)
68
69
  self._check_response(response)
69
70
  return response.json() if response.text else {}
70
71
 
71
72
  def _put(self, path: str, data: dict = None) -> dict:
72
73
 
73
74
  url = f"{self.base_url}{path}"
74
- response = requests.put(url, headers=self._get_headers(), json=data)
75
+ response = requests.put(url, headers=self._get_headers(), json=data, verify=self.verify)
75
76
  self._check_response(response)
76
77
  return response.json() if response.text else {}
77
78
 
78
79
  def _patch(self, path: str, data: dict = None) -> dict:
79
80
  url = f"{self.base_url}{path}"
80
- response = requests.patch(url, headers=self._get_headers(), json=data)
81
+ response = requests.patch(url, headers=self._get_headers(), json=data, verify=self.verify)
81
82
  self._check_response(response)
82
83
  return response.json() if response.text else {}
83
84
 
84
85
  def _patch_form_data(self, path: str, data: dict = None, files = None) -> dict:
85
86
  url = f"{self.base_url}{path}"
86
- response = requests.patch(url, headers=self._get_headers(), data=data, files=files)
87
+ response = requests.patch(url, headers=self._get_headers(), data=data, files=files, verify=self.verify)
87
88
  self._check_response(response)
88
89
  return response.json() if response.text else {}
89
90
 
90
91
  def _delete(self, path: str, data=None) -> dict:
91
92
  url = f"{self.base_url}{path}"
92
- response = requests.delete(url, headers=self._get_headers(), json=data)
93
+ response = requests.delete(url, headers=self._get_headers(), json=data, verify=self.verify)
93
94
  self._check_response(response)
94
95
  return response.json() if response.text else {}
95
96
 
@@ -1,10 +1,12 @@
1
1
  from typing import List
2
2
 
3
+ from ibm_cloud_sdk_core.authenticators import MCSPAuthenticator
3
4
  from pydantic import BaseModel, ValidationError
4
5
  from typing import Optional
5
6
 
6
7
  from ibm_watsonx_orchestrate.client.base_api_client import BaseAPIClient, ClientAPIException
7
8
  from ibm_watsonx_orchestrate.agent_builder.connections.types import ConnectionEnvironment, ConnectionPreference, ConnectionAuthType, ConnectionSecurityScheme, IdpConfigData, AppConfigData, ConnectionType
9
+ from ibm_watsonx_orchestrate.client.utils import is_cpd_env, is_local_dev
8
10
 
9
11
  import logging
10
12
  logger = logging.getLogger(__name__)
@@ -41,6 +43,12 @@ class GetConnectionResponse(BaseModel):
41
43
 
42
44
 
43
45
  class ConnectionsClient(BaseAPIClient):
46
+ def __init__(self, base_url: str, api_key: str = None, is_local: bool = False, verify: str = None, authenticator: MCSPAuthenticator = None):
47
+ super(ConnectionsClient, self).__init__(base_url, api_key, is_local, verify, authenticator)
48
+ if is_local_dev(base_url):
49
+ self.base_url = f"{base_url.rstrip('/')}/api/v1/orchestrate"
50
+ else:
51
+ self.base_url = f"{base_url.rstrip('/')}/v1/orchestrate"
44
52
  """
45
53
  Client to handle CRUD operations for Connections endpoint
46
54
  """
@@ -53,9 +61,14 @@ class ConnectionsClient(BaseAPIClient):
53
61
  return self._delete(f"/connections/applications/{app_id}")
54
62
 
55
63
  # GET /api/v1/connections/applications/{app_id}
56
- def get(self, app_id: str) -> GetConnectionResponse:
64
+ def get(self, app_id: str) -> GetConnectionResponse | None:
57
65
  try:
58
- return GetConnectionResponse.model_validate(self._get(f"/connections/applications/{app_id}"))
66
+ path = (
67
+ f"/connections/applications/{app_id}"
68
+ if is_cpd_env(self.base_url)
69
+ else f"/connections/applications?app_id={app_id}"
70
+ )
71
+ return GetConnectionResponse.model_validate(self._get(path))
59
72
  except ClientAPIException as e:
60
73
  if e.response.status_code == 404:
61
74
  return None
@@ -65,7 +78,14 @@ class ConnectionsClient(BaseAPIClient):
65
78
  # GET api/v1/connections/applications
66
79
  def list(self) -> List[ListConfigsResponse]:
67
80
  try:
68
- res = self._get(f"/connections/applications")
81
+ path = (
82
+ f"/connections/applications"
83
+ if is_cpd_env(self.base_url)
84
+ else f"/connections/applications?include_details=true"
85
+ )
86
+ res = self._get(path)
87
+ import json
88
+ json.dumps(res)
69
89
  return [ListConfigsResponse.model_validate(conn) for conn in res.get("applications", [])]
70
90
  except ValidationError as e:
71
91
  logger.error("Recieved unexpected response from server")
@@ -96,28 +116,38 @@ class ConnectionsClient(BaseAPIClient):
96
116
 
97
117
  # POST /api/v1/connections/applications/{app_id}/configs/{env}/credentials
98
118
  # POST /api/v1/connections/applications/{app_id}/configs/{env}/runtime_credentials
99
- def create_credentials(self, app_id: str, env: ConnectionEnvironment, payload: dict, use_sso: bool) -> None:
100
- if use_sso:
119
+ def create_credentials(self, app_id: str, env: ConnectionEnvironment, payload: dict, use_app_credentials: bool) -> None:
120
+ if use_app_credentials:
101
121
  self._post(f"/connections/applications/{app_id}/configs/{env}/credentials", data=payload)
102
122
  else:
103
123
  self._post(f"/connections/applications/{app_id}/configs/{env}/runtime_credentials", data=payload)
104
124
 
105
125
  # PATCH /api/v1/connections/applications/{app_id}/configs/{env}/credentials
106
126
  # PATCH /api/v1/connections/applications/{app_id}/configs/{env}/runtime_credentials
107
- def update_credentials(self, app_id: str, env: ConnectionEnvironment, payload: dict, use_sso: bool) -> None:
108
- if use_sso:
127
+ def update_credentials(self, app_id: str, env: ConnectionEnvironment, payload: dict, use_app_credentials: bool) -> None:
128
+ if use_app_credentials:
109
129
  self._patch(f"/connections/applications/{app_id}/configs/{env}/credentials", data=payload)
110
130
  else:
111
131
  self._patch(f"/connections/applications/{app_id}/configs/{env}/runtime_credentials", data=payload)
112
132
 
113
133
  # GET /api/v1/connections/applications/{app_id}/configs/credentials?env={env}
114
134
  # GET /api/v1/connections/applications/{app_id}/configs/runtime_credentials?env={env}
115
- def get_credentials(self, app_id: str, env: ConnectionEnvironment, use_sso: bool) -> dict:
135
+ def get_credentials(self, app_id: str, env: ConnectionEnvironment, use_app_credentials: bool) -> dict:
116
136
  try:
117
- if use_sso:
118
- return self._get(f"/connections/applications/{app_id}/credentials?env={env}")
137
+ if use_app_credentials:
138
+ path = (
139
+ f"/connections/applications/{app_id}/credentials?env={env}"
140
+ if is_cpd_env(self.base_url)
141
+ else f"/connections/applications/{app_id}/credentials/{env}"
142
+ )
143
+ return self._get(path)
119
144
  else:
120
- return self._get(f"/connections/applications/{app_id}/configs/runtime_credentials?env={env}")
145
+ path = (
146
+ f"/connections/applications/{app_id}/configs/runtime_credentials?env={env}"
147
+ if is_cpd_env(self.base_url)
148
+ else f"/connections/applications/runtime_credentials?app_id={app_id}&env={env}"
149
+ )
150
+ return self._get(path)
121
151
  except ClientAPIException as e:
122
152
  if e.response.status_code == 404:
123
153
  return None
@@ -125,8 +155,8 @@ class ConnectionsClient(BaseAPIClient):
125
155
 
126
156
  # DELETE /api/v1/connections/applications/{app_id}/configs/{env}/credentials
127
157
  # DELETE /api/v1/connections/applications/{app_id}/configs/{env}/runtime_credentials
128
- def delete_credentials(self, app_id: str, env: ConnectionEnvironment, use_sso: bool) -> None:
129
- if use_sso:
158
+ def delete_credentials(self, app_id: str, env: ConnectionEnvironment, use_app_credentials: bool) -> None:
159
+ if use_app_credentials:
130
160
  self._delete(f"/connections/applications/{app_id}/configs/{env}/credentials")
131
161
  else:
132
162
  self._delete(f"/connections/applications/{app_id}/configs/{env}/runtime_credentials")
@@ -147,7 +177,12 @@ class ConnectionsClient(BaseAPIClient):
147
177
  if conn_id is None:
148
178
  return ""
149
179
  try:
150
- app_details = self._get(f"/connections/applications/id/{conn_id}")
180
+ path = (
181
+ f"/connections/applications/id/{conn_id}"
182
+ if is_cpd_env(self.base_url)
183
+ else f"/connections/applications?connection_id={conn_id}"
184
+ )
185
+ app_details = self._get(path)
151
186
  return app_details.get("app_id")
152
187
  except ClientAPIException as e:
153
188
  if e.response.status_code == 404:
@@ -17,14 +17,16 @@ def _get_connections_manager_url() -> str:
17
17
  url_parts = url.split(":")
18
18
  url_parts[-1] = str(LOCAL_CONNECTION_MANAGER_PORT)
19
19
  url = ":".join(url_parts)
20
- url = url + "/api/v1"
21
20
  return url
22
21
  return None
23
22
 
24
23
  def get_connections_client() -> ConnectionsClient:
25
24
  return instantiate_client(client=ConnectionsClient, url=_get_connections_manager_url())
26
25
 
27
- def get_connection_type(security_scheme: ConnectionSecurityScheme, auth_type: ConnectionAuthType) -> ConnectionType:
26
+ def get_connection_type(security_scheme: ConnectionSecurityScheme, auth_type: ConnectionAuthType) -> ConnectionType | None:
27
+ if security_scheme is None and auth_type is None:
28
+ return None
29
+
28
30
  if security_scheme != ConnectionSecurityScheme.OAUTH2:
29
31
  return ConnectionType(security_scheme)
30
32
  return ConnectionType(auth_type)
@@ -31,6 +31,8 @@ class Credentials:
31
31
  url: str | None = None,
32
32
  iam_url: str | None = None,
33
33
  api_key: str | None = None,
34
+ username: str | None = None,
35
+ password: str | None = None,
34
36
  token: str | None = None,
35
37
  verify: str | bool | None = None,
36
38
  auth_type: str | None = None,
@@ -39,6 +41,8 @@ class Credentials:
39
41
  self.url = url
40
42
  self.iam_url = iam_url
41
43
  self.api_key = api_key
44
+ self.username = username
45
+ self.password = password
42
46
  self.token = token
43
47
  self.local_global_token = None
44
48
  self.verify = verify
@@ -14,7 +14,7 @@ DEFAULT_TENANT = {
14
14
  DEFAULT_USER = {"username": "wxo.archer@ibm.com", "password": "watsonx"}
15
15
  DEFAULT_LOCAL_SERVICE_URL = "http://localhost:4321"
16
16
  DEFAULT_LOCAL_AUTH_ENDPOINT = f"{DEFAULT_LOCAL_SERVICE_URL}/api/v1/auth/token"
17
- DEFAULT_LOCAL_TENANT_URL = f"{DEFAULT_LOCAL_SERVICE_URL}/tenants"
17
+ DEFAULT_LOCAL_TENANT_URL = f"{DEFAULT_LOCAL_SERVICE_URL}/api/v1/tenants"
18
18
  DEFAULT_LOCAL_TENANT_AUTH_ENDPOINT = "{}/api/v1/auth/token?tenant_id={}"
19
19
 
20
20
 
@@ -17,11 +17,11 @@ class ModelPoliciesClient(BaseAPIClient):
17
17
  """
18
18
  # POST api/v1/model_policy
19
19
  def create(self, model: ModelPolicy) -> None:
20
- self._post("/model_policy", data=model.model_dump())
20
+ self._post("/model_policy", data=model.model_dump(exclude_none=True))
21
21
 
22
22
  # PUT api/v1/model_policy/{model_policy_id}
23
23
  def update(self, model_policy_id: str, model: ModelPolicy) -> None:
24
- self._put(f"/model_policy/{model_policy_id}", data=model.model_dump())
24
+ self._put(f"/model_policy/{model_policy_id}", data=model.model_dump(exclude_none=True))
25
25
 
26
26
  # DELETE api/v1/model_policy/{model_policy_id}
27
27
  def delete(self, model_policy_id: str) -> dict:
@@ -7,8 +7,9 @@ from __future__ import annotations
7
7
 
8
8
  from ibm_cloud_sdk_core.authenticators import MCSPAuthenticator
9
9
  from ibm_cloud_sdk_core.authenticators import IAMAuthenticator
10
+ from ibm_cloud_sdk_core.authenticators import CloudPakForDataAuthenticator
10
11
 
11
- from ibm_watsonx_orchestrate.client.utils import check_token_validity
12
+ from ibm_watsonx_orchestrate.client.utils import check_token_validity, is_cpd_env
12
13
  from ibm_watsonx_orchestrate.client.base_service_instance import BaseServiceInstance
13
14
  from ibm_watsonx_orchestrate.cli.commands.environment.types import EnvironmentAuthType
14
15
 
@@ -16,6 +17,16 @@ from ibm_watsonx_orchestrate.client.client_errors import (
16
17
  ClientError,
17
18
  )
18
19
 
20
+ import logging
21
+ logger = logging.getLogger(__name__)
22
+
23
+ from ibm_watsonx_orchestrate.cli.config import (
24
+ Config,
25
+ CONTEXT_SECTION_HEADER,
26
+ CONTEXT_ACTIVE_ENV_OPT,
27
+ ENVIRONMENTS_SECTION_HEADER,
28
+ ENV_WXO_URL_OPT
29
+ )
19
30
 
20
31
  class ServiceInstance(BaseServiceInstance):
21
32
  """Connect, get details, and check usage of a Watson Machine Learning service instance."""
@@ -40,8 +51,13 @@ class ServiceInstance(BaseServiceInstance):
40
51
  def _create_token(self) -> str:
41
52
  if not self._credentials.auth_type:
42
53
  if ".cloud.ibm.com" in self._credentials.url:
54
+ logger.warning("Using IBM IAM Auth Type. If this is incorrect please use the '--type' flag to explicitly choose one of 'ibm_iam' or 'mcsp' or 'cpd")
43
55
  return self._authenticate(EnvironmentAuthType.IBM_CLOUD_IAM)
56
+ elif is_cpd_env(self._credentials.url):
57
+ logger.warning("Using CPD Auth Type. If this is incorrect please use the '--type' flag to explicitly choose one of 'ibm_iam' or 'mcsp' or 'cpd")
58
+ return self._authenticate(EnvironmentAuthType.CPD)
44
59
  else:
60
+ logger.warning("Using MCSP Auth Type. If this is incorrect please use the '--type' flag to explicitly choose one of 'ibm_iam' or 'mcsp' or 'cpd' ")
45
61
  return self._authenticate(EnvironmentAuthType.MCSP)
46
62
  else:
47
63
  return self._authenticate(self._credentials.auth_type)
@@ -55,6 +71,31 @@ class ServiceInstance(BaseServiceInstance):
55
71
  authenticator = MCSPAuthenticator(apikey=self._credentials.api_key, url=url)
56
72
  case EnvironmentAuthType.IBM_CLOUD_IAM:
57
73
  authenticator = IAMAuthenticator(apikey=self._credentials.api_key, url=self._credentials.iam_url)
74
+ case EnvironmentAuthType.CPD:
75
+ url = ""
76
+ if self._credentials.iam_url is not None:
77
+ url = self._credentials.iam_url
78
+ else:
79
+ cfg = Config()
80
+ env_cfg = cfg.get(ENVIRONMENTS_SECTION_HEADER)
81
+ matching_wxo_url = next(
82
+ (env_config['wxo_url'] for env_config in env_cfg.values() if 'bypass_ssl' in env_config and 'verify' in env_config),
83
+ None
84
+ )
85
+ base_url = matching_wxo_url.split("/orchestrate")[0]
86
+ url = f"{base_url}/icp4d-api"
87
+
88
+ password = self._credentials.password if self._credentials.password is not None else None
89
+ api_key = self._credentials.api_key if self._credentials.api_key is not None else None
90
+ cpd_password=password if password else None
91
+ cpd_apikey=api_key if api_key else None
92
+ authenticator = CloudPakForDataAuthenticator(
93
+ username=self._credentials.username,
94
+ password=cpd_password,
95
+ apikey=cpd_apikey,
96
+ url=url,
97
+ disable_ssl_verification=True
98
+ )
58
99
  case _:
59
100
  raise ClientError(f"Unsupported authentication type: {auth_type}")
60
101
 
@@ -29,12 +29,17 @@ class TempusClient(BaseAPIClient):
29
29
  authenticator=authenticator
30
30
  )
31
31
 
32
+ def get_tempus_endpoint(self) -> str:
33
+ """
34
+ Returns the Tempus endpoint URL
35
+ """
36
+ return self.base_url
32
37
  def create_update_flow_model(self, flow_id: str, model: dict) -> dict:
33
- return self._post(f"/v1/flow-models/{flow_id}", data=model)
38
+ return self._post(f"/flow-models/{flow_id}", data=model)
34
39
 
35
40
  def run_flow(self, flow_id: str, input: dict) -> dict:
36
- return self._post(f"/v1/flows/{flow_id}/versions/TIP/run", data=input)
41
+ return self._post(f"/flows/{flow_id}/versions/TIP/run", data=input)
37
42
 
38
43
  def arun_flow(self, flow_id: str, input: dict) -> dict:
39
- return self._post(f"/v1/flows/{flow_id}/versions/TIP/run/async", data=input)
44
+ return self._post(f"/flows/{flow_id}/versions/TIP/run/async", data=input)
40
45
 
@@ -9,7 +9,9 @@ from ibm_watsonx_orchestrate.cli.config import (
9
9
  CONTEXT_SECTION_HEADER,
10
10
  CONTEXT_ACTIVE_ENV_OPT,
11
11
  ENVIRONMENTS_SECTION_HEADER,
12
- ENV_WXO_URL_OPT
12
+ ENV_WXO_URL_OPT,
13
+ BYPASS_SSL,
14
+ VERIFY
13
15
  )
14
16
  from threading import Lock
15
17
  from ibm_watsonx_orchestrate.client.base_api_client import BaseAPIClient
@@ -45,6 +47,21 @@ def is_local_dev(url: str | None = None) -> bool:
45
47
 
46
48
  return False
47
49
 
50
+ def is_ibm_cloud():
51
+ cfg = Config()
52
+ active_env = cfg.read(CONTEXT_SECTION_HEADER, CONTEXT_ACTIVE_ENV_OPT)
53
+ url = cfg.get(ENVIRONMENTS_SECTION_HEADER, active_env, ENV_WXO_URL_OPT)
54
+
55
+ if url.__contains__("cloud.ibm.com"):
56
+ return True
57
+ return False
58
+
59
+
60
+ def is_cpd_env(url: str) -> bool:
61
+ if url.lower().startswith("https://cpd"):
62
+ return True
63
+ return False
64
+
48
65
  def check_token_validity(token: str) -> bool:
49
66
  try:
50
67
  token_claimset = jwt.decode(token, options={"verify_signature": False})
@@ -65,6 +82,17 @@ def instantiate_client(client: type[T] , url: str | None=None) -> T:
65
82
  with open(os.path.join(DEFAULT_CONFIG_FILE_FOLDER, DEFAULT_CONFIG_FILE), "r") as f:
66
83
  config = yaml_safe_load(f)
67
84
  active_env = config.get(CONTEXT_SECTION_HEADER, {}).get(CONTEXT_ACTIVE_ENV_OPT)
85
+ bypass_ssl = (
86
+ config.get(ENVIRONMENTS_SECTION_HEADER, {})
87
+ .get(active_env, {})
88
+ .get(BYPASS_SSL, None)
89
+ )
90
+
91
+ verify = (
92
+ config.get(ENVIRONMENTS_SECTION_HEADER, {})
93
+ .get(active_env, {})
94
+ .get(VERIFY, None)
95
+ )
68
96
 
69
97
  if not url:
70
98
  url = config.get(ENVIRONMENTS_SECTION_HEADER, {}).get(active_env, {}).get(ENV_WXO_URL_OPT)
@@ -86,7 +114,14 @@ def instantiate_client(client: type[T] , url: str | None=None) -> T:
86
114
  if not check_token_validity(token):
87
115
  logger.error(f"The token found for environment '{active_env}' is missing or expired. Use `orchestrate env activate {active_env}` to fetch a new one")
88
116
  exit(1)
89
- client_instance = client(base_url=url, api_key=token, is_local=is_local_dev(url))
117
+ is_cpd = is_cpd_env(url)
118
+ if is_cpd:
119
+ if bypass_ssl is True:
120
+ client_instance = client(base_url=url, api_key=token, is_local=is_local_dev(url), verify=False)
121
+ elif verify is not None:
122
+ client_instance = client(base_url=url, api_key=token, is_local=is_local_dev(url), verify=verify)
123
+ else:
124
+ client_instance = client(base_url=url, api_key=token, is_local=is_local_dev(url))
90
125
 
91
126
  return client_instance
92
127
  except FileNotFoundError as e: