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.
- ibm_watsonx_orchestrate/__init__.py +2 -1
- ibm_watsonx_orchestrate/agent_builder/agents/agent.py +3 -3
- ibm_watsonx_orchestrate/agent_builder/agents/assistant_agent.py +3 -2
- ibm_watsonx_orchestrate/agent_builder/agents/external_agent.py +3 -2
- ibm_watsonx_orchestrate/agent_builder/agents/types.py +38 -9
- ibm_watsonx_orchestrate/agent_builder/connections/connections.py +4 -3
- ibm_watsonx_orchestrate/agent_builder/connections/types.py +14 -2
- ibm_watsonx_orchestrate/agent_builder/knowledge_bases/knowledge_base_requests.py +1 -22
- ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +1 -17
- ibm_watsonx_orchestrate/agent_builder/tools/base_tool.py +2 -1
- ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +75 -24
- ibm_watsonx_orchestrate/agent_builder/tools/python_tool.py +136 -92
- ibm_watsonx_orchestrate/agent_builder/tools/types.py +17 -11
- ibm_watsonx_orchestrate/cli/commands/agents/agents_command.py +7 -7
- ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +7 -6
- ibm_watsonx_orchestrate/cli/commands/channels/types.py +3 -2
- ibm_watsonx_orchestrate/cli/commands/channels/webchat/channels_webchat_controller.py +1 -2
- ibm_watsonx_orchestrate/cli/commands/connections/connections_command.py +14 -6
- ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +6 -8
- ibm_watsonx_orchestrate/cli/commands/copilot/copilot_command.py +65 -0
- ibm_watsonx_orchestrate/cli/commands/copilot/copilot_controller.py +368 -0
- ibm_watsonx_orchestrate/cli/commands/copilot/copilot_server_controller.py +170 -0
- ibm_watsonx_orchestrate/cli/commands/environment/environment_controller.py +5 -5
- ibm_watsonx_orchestrate/cli/commands/environment/types.py +2 -0
- ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_command.py +102 -37
- ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_controller.py +20 -2
- ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_command.py +0 -18
- ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +36 -20
- ibm_watsonx_orchestrate/cli/commands/models/models_command.py +1 -1
- ibm_watsonx_orchestrate/cli/commands/server/server_command.py +94 -36
- ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_command.py +1 -1
- ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +11 -4
- ibm_watsonx_orchestrate/cli/config.py +3 -3
- ibm_watsonx_orchestrate/cli/init_helper.py +10 -1
- ibm_watsonx_orchestrate/cli/main.py +5 -0
- ibm_watsonx_orchestrate/client/base_api_client.py +12 -0
- ibm_watsonx_orchestrate/client/copilot/cpe/copilot_cpe_client.py +67 -0
- ibm_watsonx_orchestrate/client/knowledge_bases/knowledge_base_client.py +1 -1
- ibm_watsonx_orchestrate/client/local_service_instance.py +3 -1
- ibm_watsonx_orchestrate/client/service_instance.py +33 -7
- ibm_watsonx_orchestrate/client/utils.py +15 -13
- ibm_watsonx_orchestrate/docker/compose-lite.yml +198 -6
- ibm_watsonx_orchestrate/docker/default.env +36 -12
- ibm_watsonx_orchestrate/flow_builder/flows/__init__.py +9 -4
- ibm_watsonx_orchestrate/flow_builder/flows/decorators.py +4 -2
- ibm_watsonx_orchestrate/flow_builder/flows/events.py +10 -9
- ibm_watsonx_orchestrate/flow_builder/flows/flow.py +131 -20
- ibm_watsonx_orchestrate/flow_builder/node.py +18 -1
- ibm_watsonx_orchestrate/flow_builder/types.py +271 -16
- ibm_watsonx_orchestrate/flow_builder/utils.py +120 -6
- ibm_watsonx_orchestrate/utils/exceptions.py +23 -0
- {ibm_watsonx_orchestrate-1.6.3.dist-info → ibm_watsonx_orchestrate-1.6.4.dist-info}/METADATA +3 -7
- {ibm_watsonx_orchestrate-1.6.3.dist-info → ibm_watsonx_orchestrate-1.6.4.dist-info}/RECORD +56 -53
- ibm_watsonx_orchestrate/agent_builder/utils/pydantic_utils.py +0 -149
- ibm_watsonx_orchestrate/flow_builder/resources/flow_status.openapi.yml +0 -66
- {ibm_watsonx_orchestrate-1.6.3.dist-info → ibm_watsonx_orchestrate-1.6.4.dist-info}/WHEEL +0 -0
- {ibm_watsonx_orchestrate-1.6.3.dist-info → ibm_watsonx_orchestrate-1.6.4.dist-info}/entry_points.txt +0 -0
- {ibm_watsonx_orchestrate-1.6.3.dist-info → ibm_watsonx_orchestrate-1.6.4.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,7 @@
|
|
1
1
|
import json
|
2
2
|
from ibm_watsonx_orchestrate.utils.utils import yaml_safe_load
|
3
3
|
from .types import AgentSpec
|
4
|
+
from ibm_watsonx_orchestrate.utils.exceptions import BadRequest
|
4
5
|
|
5
6
|
|
6
7
|
class Agent(AgentSpec):
|
@@ -13,10 +14,9 @@ class Agent(AgentSpec):
|
|
13
14
|
elif file.endswith('.json'):
|
14
15
|
content = json.load(f)
|
15
16
|
else:
|
16
|
-
raise
|
17
|
-
|
17
|
+
raise BadRequest('file must end in .json, .yaml, or .yml')
|
18
18
|
if not content.get("spec_version"):
|
19
|
-
raise
|
19
|
+
raise BadRequest(f"Field 'spec_version' not provided. Please ensure provided spec conforms to a valid spec format")
|
20
20
|
agent = Agent.model_validate(content)
|
21
21
|
|
22
22
|
return agent
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import json
|
2
2
|
from ibm_watsonx_orchestrate.utils.utils import yaml_safe_load
|
3
3
|
from .types import AssistantAgentSpec
|
4
|
+
from ibm_watsonx_orchestrate.utils.exceptions import BadRequest
|
4
5
|
|
5
6
|
|
6
7
|
class AssistantAgent(AssistantAgentSpec):
|
@@ -13,10 +14,10 @@ class AssistantAgent(AssistantAgentSpec):
|
|
13
14
|
elif file.endswith('.json'):
|
14
15
|
content = json.load(f)
|
15
16
|
else:
|
16
|
-
raise
|
17
|
+
raise BadRequest('file must end in .json, .yaml, or .yml')
|
17
18
|
|
18
19
|
if not content.get("spec_version"):
|
19
|
-
raise
|
20
|
+
raise BadRequest(f"Field 'spec_version' not provided. Please ensure provided spec conforms to a valid spec format")
|
20
21
|
agent = AssistantAgent.model_validate(content)
|
21
22
|
|
22
23
|
return agent
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import json
|
2
2
|
from ibm_watsonx_orchestrate.utils.utils import yaml_safe_load
|
3
3
|
from .types import ExternalAgentSpec
|
4
|
+
from ibm_watsonx_orchestrate.utils.exceptions import BadRequest
|
4
5
|
|
5
6
|
|
6
7
|
class ExternalAgent(ExternalAgentSpec):
|
@@ -13,10 +14,10 @@ class ExternalAgent(ExternalAgentSpec):
|
|
13
14
|
elif file.endswith('.json'):
|
14
15
|
content = json.load(f)
|
15
16
|
else:
|
16
|
-
raise
|
17
|
+
raise BadRequest('file must end in .json, .yaml, or .yml')
|
17
18
|
|
18
19
|
if not content.get("spec_version"):
|
19
|
-
raise
|
20
|
+
raise BadRequest(f"Field 'spec_version' not provided. Please ensure provided spec conforms to a valid spec format")
|
20
21
|
agent = ExternalAgent.model_validate(content)
|
21
22
|
|
22
23
|
return agent
|
@@ -9,21 +9,44 @@ from ibm_watsonx_orchestrate.agent_builder.knowledge_bases.knowledge_base import
|
|
9
9
|
from ibm_watsonx_orchestrate.agent_builder.agents.webchat_customizations import StarterPrompts, WelcomeContent
|
10
10
|
from pydantic import Field, AliasChoices
|
11
11
|
from typing import Annotated
|
12
|
+
from ibm_watsonx_orchestrate.utils.exceptions import BadRequest
|
12
13
|
|
13
14
|
from ibm_watsonx_orchestrate.agent_builder.tools.types import JsonSchemaObject
|
14
15
|
|
15
16
|
# TO-DO: this is just a placeholder. Will update this later to align with backend
|
16
|
-
DEFAULT_LLM = "watsonx/meta-llama/llama-3-
|
17
|
+
DEFAULT_LLM = "watsonx/meta-llama/llama-3-2-90b-vision-instruct"
|
18
|
+
|
19
|
+
# Handles yaml formatting for multiline strings to improve readability
|
20
|
+
def str_presenter(dumper, data):
|
21
|
+
if len(data.splitlines()) > 1: # check for multiline string
|
22
|
+
data = "\n".join([line.rstrip() for line in data.splitlines()])
|
23
|
+
return dumper.represent_scalar('tag:yaml.org,2002:str', data, style='|')
|
24
|
+
return dumper.represent_scalar('tag:yaml.org,2002:str', data)
|
25
|
+
|
26
|
+
yaml.add_representer(str, str_presenter)
|
27
|
+
yaml.representer.SafeRepresenter.add_representer(str, str_presenter) # to use with safe_dum
|
17
28
|
|
18
29
|
class SpecVersion(str, Enum):
|
19
30
|
V1 = "v1"
|
20
31
|
|
32
|
+
def __str__(self):
|
33
|
+
return self.value
|
34
|
+
|
35
|
+
def __repr__(self):
|
36
|
+
return repr(self.value)
|
37
|
+
|
21
38
|
|
22
39
|
class AgentKind(str, Enum):
|
23
40
|
NATIVE = "native"
|
24
41
|
EXTERNAL = "external"
|
25
42
|
ASSISTANT = "assistant"
|
26
43
|
|
44
|
+
def __str__(self):
|
45
|
+
return self.value
|
46
|
+
|
47
|
+
def __repr__(self):
|
48
|
+
return repr(self.value)
|
49
|
+
|
27
50
|
class ExternalAgentAuthScheme(str, Enum):
|
28
51
|
BEARER_TOKEN = 'BEARER_TOKEN'
|
29
52
|
API_KEY = "API_KEY"
|
@@ -63,7 +86,7 @@ class BaseAgentSpec(BaseModel):
|
|
63
86
|
elif file.endswith('.json'):
|
64
87
|
json.dump(dumped, f, indent=2)
|
65
88
|
else:
|
66
|
-
raise
|
89
|
+
raise BadRequest('file must end in .json, .yaml, or .yml')
|
67
90
|
|
68
91
|
def dumps_spec(self) -> str:
|
69
92
|
dumped = self.model_dump(mode='json', exclude_none=True)
|
@@ -87,6 +110,12 @@ class AgentStyle(str, Enum):
|
|
87
110
|
REACT = "react"
|
88
111
|
PLANNER = "planner"
|
89
112
|
|
113
|
+
def __str__(self):
|
114
|
+
return self.value
|
115
|
+
|
116
|
+
def __repr__(self):
|
117
|
+
return repr(self.value)
|
118
|
+
|
90
119
|
class AgentGuideline(BaseModel):
|
91
120
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
92
121
|
|
@@ -136,7 +165,7 @@ class AgentSpec(BaseAgentSpec):
|
|
136
165
|
@model_validator(mode="after")
|
137
166
|
def validate_kind(self):
|
138
167
|
if self.kind != AgentKind.NATIVE:
|
139
|
-
raise
|
168
|
+
raise BadRequest(f"The specified kind '{self.kind}' cannot be used to create a native agent.")
|
140
169
|
return self
|
141
170
|
|
142
171
|
def validate_agent_fields(values: dict) -> dict:
|
@@ -144,13 +173,13 @@ def validate_agent_fields(values: dict) -> dict:
|
|
144
173
|
for field in ["id", "name", "kind", "description", "collaborators", "tools", "knowledge_base"]:
|
145
174
|
value = values.get(field)
|
146
175
|
if value and not str(value).strip():
|
147
|
-
raise
|
176
|
+
raise BadRequest(f"{field} cannot be empty or just whitespace")
|
148
177
|
|
149
178
|
name = values.get("name")
|
150
179
|
collaborators = values.get("collaborators", []) if values.get("collaborators", []) else []
|
151
180
|
for collaborator in collaborators:
|
152
181
|
if collaborator == name:
|
153
|
-
raise
|
182
|
+
raise BadRequest(f"Circular reference detected. The agent '{name}' cannot contain itself as a collaborator")
|
154
183
|
|
155
184
|
if values.get("style") == AgentStyle.PLANNER:
|
156
185
|
if values.get("custom_join_tool") and values.get("structured_output"):
|
@@ -197,7 +226,7 @@ class ExternalAgentSpec(BaseAgentSpec):
|
|
197
226
|
@model_validator(mode="after")
|
198
227
|
def validate_kind_for_external(self):
|
199
228
|
if self.kind != AgentKind.EXTERNAL:
|
200
|
-
raise
|
229
|
+
raise BadRequest(f"The specified kind '{self.kind}' cannot be used to create an external agent.")
|
201
230
|
return self
|
202
231
|
|
203
232
|
def validate_external_agent_fields(values: dict) -> dict:
|
@@ -205,7 +234,7 @@ def validate_external_agent_fields(values: dict) -> dict:
|
|
205
234
|
for field in ["name", "kind", "description", "title", "tags", "api_url", "chat_params", "nickname", "app_id"]:
|
206
235
|
value = values.get(field)
|
207
236
|
if value and not str(value).strip():
|
208
|
-
raise
|
237
|
+
raise BadRequest(f"{field} cannot be empty or just whitespace")
|
209
238
|
|
210
239
|
context_variables = values.get("context_variables")
|
211
240
|
if context_variables is not None:
|
@@ -250,7 +279,7 @@ class AssistantAgentSpec(BaseAgentSpec):
|
|
250
279
|
@model_validator(mode="after")
|
251
280
|
def validate_kind_for_external(self):
|
252
281
|
if self.kind != AgentKind.ASSISTANT:
|
253
|
-
raise
|
282
|
+
raise BadRequest(f"The specified kind '{self.kind}' cannot be used to create an assistant agent.")
|
254
283
|
return self
|
255
284
|
|
256
285
|
def validate_assistant_agent_fields(values: dict) -> dict:
|
@@ -258,7 +287,7 @@ def validate_assistant_agent_fields(values: dict) -> dict:
|
|
258
287
|
for field in ["name", "kind", "description", "title", "tags", "nickname", "app_id"]:
|
259
288
|
value = values.get(field)
|
260
289
|
if value and not str(value).strip():
|
261
|
-
raise
|
290
|
+
raise BadRequest(f"{field} cannot be empty or just whitespace")
|
262
291
|
|
263
292
|
# Validate context_variables if provided
|
264
293
|
context_variables = values.get("context_variables")
|
@@ -14,6 +14,7 @@ from ibm_watsonx_orchestrate.agent_builder.connections.types import (
|
|
14
14
|
)
|
15
15
|
|
16
16
|
from ibm_watsonx_orchestrate.utils.utils import sanatize_app_id
|
17
|
+
from ibm_watsonx_orchestrate.utils.exceptions import BadRequest
|
17
18
|
|
18
19
|
logger = logging.getLogger(__name__)
|
19
20
|
|
@@ -61,7 +62,7 @@ def _clean_env_vars(vars: dict[str:str], requirements: List[str], app_id: str) -
|
|
61
62
|
missing_requirements_str = ", ".join(missing_requirements)
|
62
63
|
message = f"Missing requirement environment variables '{missing_requirements_str}' for connection '{app_id}'"
|
63
64
|
logger.error(message)
|
64
|
-
raise
|
65
|
+
raise BadRequest(message)
|
65
66
|
|
66
67
|
return required_env_vars
|
67
68
|
|
@@ -114,7 +115,7 @@ def get_connection_type(app_id: str) -> ConnectionSecurityScheme:
|
|
114
115
|
if not expected_schema:
|
115
116
|
message = f"No credentials found for connections '{app_id}'"
|
116
117
|
logger.error(message)
|
117
|
-
raise
|
118
|
+
raise BadRequest(message)
|
118
119
|
|
119
120
|
auth_types = {e.value for e in ConnectionSecurityScheme}
|
120
121
|
if expected_schema not in auth_types:
|
@@ -132,6 +133,6 @@ def get_application_connection_credentials(type: ConnectionType, app_id: str) ->
|
|
132
133
|
if not _validate_schema_type(requested_type=requested_schema, expected_type=expected_schema):
|
133
134
|
message = f"The requested type '{requested_schema}' does not match the type '{expected_schema}' for the connection '{app_id}'"
|
134
135
|
logger.error(message)
|
135
|
-
raise
|
136
|
+
raise BadRequest(message)
|
136
137
|
|
137
138
|
return _get_credentials_model(connection_type=requested_schema, app_id=sanitized_app_id)
|
@@ -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
|
-
|
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
|
-
|
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,7 +1,6 @@
|
|
1
1
|
import json
|
2
2
|
from ibm_watsonx_orchestrate.utils.utils import yaml_safe_load
|
3
|
-
from .types import KnowledgeBaseSpec
|
4
|
-
|
3
|
+
from .types import KnowledgeBaseSpec
|
5
4
|
|
6
5
|
class KnowledgeBaseCreateRequest(KnowledgeBaseSpec):
|
7
6
|
|
@@ -21,23 +20,3 @@ class KnowledgeBaseCreateRequest(KnowledgeBaseSpec):
|
|
21
20
|
knowledge_base = KnowledgeBaseSpec.model_validate(content)
|
22
21
|
|
23
22
|
return knowledge_base
|
24
|
-
|
25
|
-
|
26
|
-
class KnowledgeBaseUpdateRequest(PatchKnowledgeBase):
|
27
|
-
|
28
|
-
@staticmethod
|
29
|
-
def from_spec(file: str) -> 'PatchKnowledgeBase':
|
30
|
-
with open(file, 'r') as f:
|
31
|
-
if file.endswith('.yaml') or file.endswith('.yml'):
|
32
|
-
content = yaml_safe_load(f)
|
33
|
-
elif file.endswith('.json'):
|
34
|
-
content = json.load(f)
|
35
|
-
else:
|
36
|
-
raise ValueError('file must end in .json, .yaml, or .yml')
|
37
|
-
|
38
|
-
if not content.get("spec_version"):
|
39
|
-
raise ValueError(f"Field 'spec_version' not provided. Please ensure provided spec conforms to a valid spec format")
|
40
|
-
|
41
|
-
patch = PatchKnowledgeBase.model_validate(content)
|
42
|
-
|
43
|
-
return patch
|
@@ -3,7 +3,7 @@ from datetime import datetime
|
|
3
3
|
from uuid import UUID
|
4
4
|
from enum import Enum
|
5
5
|
|
6
|
-
from pydantic import BaseModel
|
6
|
+
from pydantic import BaseModel
|
7
7
|
|
8
8
|
class SpecVersion(str, Enum):
|
9
9
|
V1 = "v1"
|
@@ -219,22 +219,6 @@ class KnowledgeBaseBuiltInVectorIndexConfig(BaseModel):
|
|
219
219
|
chunk_overlap: Optional[int] = None
|
220
220
|
limit: Optional[int] = None
|
221
221
|
|
222
|
-
class PatchKnowledgeBase(BaseModel):
|
223
|
-
"""request payload schema"""
|
224
|
-
description: Optional[str] = None
|
225
|
-
documents: list[str] = None
|
226
|
-
conversational_search_tool: Optional[ConversationalSearchConfig] = None
|
227
|
-
prioritize_built_in_index: Optional[bool] = None
|
228
|
-
representation: Optional[KnowledgeBaseRepresentation] = None
|
229
|
-
|
230
|
-
@model_validator(mode="after")
|
231
|
-
def validate_fields(self):
|
232
|
-
if self.documents and self.conversational_search_tool and self.conversational_search_tool.index_config:
|
233
|
-
raise ValueError("Must not provide both \"documents\" or \"conversational_search_tool.index_config\"")
|
234
|
-
if self.conversational_search_tool and self.conversational_search_tool.index_config and len(self.conversational_search_tool.index_config) != 1:
|
235
|
-
raise ValueError(f"Must provide exactly one conversational_search_tool.index_config. Provided {len(self.conversational_search_tool.index_config)}.")
|
236
|
-
return self
|
237
|
-
|
238
222
|
class KnowledgeBaseSpec(BaseModel):
|
239
223
|
"""Schema for a complete knowledge-base."""
|
240
224
|
spec_version: SpecVersion = None
|
@@ -4,6 +4,7 @@ import yaml
|
|
4
4
|
|
5
5
|
from .types import ToolSpec
|
6
6
|
|
7
|
+
from ibm_watsonx_orchestrate.utils.exceptions import BadRequest
|
7
8
|
|
8
9
|
class BaseTool:
|
9
10
|
__tool_spec__: ToolSpec
|
@@ -22,7 +23,7 @@ class BaseTool:
|
|
22
23
|
elif file.endswith('.json'):
|
23
24
|
json.dump(dumped, f, indent=2)
|
24
25
|
else:
|
25
|
-
raise
|
26
|
+
raise BadRequest('file must end in .json, .yaml, or .yml')
|
26
27
|
|
27
28
|
def dumps_spec(self) -> str:
|
28
29
|
dumped = self.__tool_spec__.model_dump(mode='json', exclude_unset=True, exclude_none=True, by_alias=True)
|
@@ -3,6 +3,7 @@ import json
|
|
3
3
|
import os.path
|
4
4
|
import logging
|
5
5
|
from typing import Dict, Any, List
|
6
|
+
from ibm_watsonx_orchestrate.utils.exceptions import BadRequest
|
6
7
|
|
7
8
|
import yaml
|
8
9
|
import yaml.constructor
|
@@ -13,7 +14,7 @@ from ibm_watsonx_orchestrate.utils.utils import yaml_safe_load
|
|
13
14
|
from .types import ToolSpec
|
14
15
|
from .base_tool import BaseTool
|
15
16
|
from .types import HTTP_METHOD, ToolPermission, ToolRequestBody, ToolResponseBody, \
|
16
|
-
OpenApiToolBinding, \
|
17
|
+
OpenApiToolBinding, AcknowledgementBinding, \
|
17
18
|
JsonSchemaObject, ToolBinding, OpenApiSecurityScheme, CallbackBinding
|
18
19
|
|
19
20
|
import json
|
@@ -40,7 +41,7 @@ class OpenAPITool(BaseTool):
|
|
40
41
|
BaseTool.__init__(self, spec=spec)
|
41
42
|
|
42
43
|
if self.__tool_spec__.binding.openapi is None:
|
43
|
-
raise
|
44
|
+
raise BadRequest('Missing openapi binding')
|
44
45
|
|
45
46
|
async def __call__(self, **kwargs):
|
46
47
|
raise RuntimeError('OpenAPI Tools are only available when deployed onto watson orchestrate or the watson '
|
@@ -54,10 +55,10 @@ class OpenAPITool(BaseTool):
|
|
54
55
|
elif file.endswith('.json'):
|
55
56
|
spec = ToolSpec.model_validate(json.load(f))
|
56
57
|
else:
|
57
|
-
raise
|
58
|
+
raise BadRequest('file must end in .json, .yaml, or .yml')
|
58
59
|
|
59
60
|
if spec.binding.openapi is None or spec.binding.openapi is None:
|
60
|
-
raise
|
61
|
+
raise BadRequest('failed to load python tool as the tool had no openapi binding')
|
61
62
|
|
62
63
|
return OpenAPITool(spec=spec)
|
63
64
|
|
@@ -108,11 +109,11 @@ def create_openapi_json_tool(
|
|
108
109
|
paths = openapi_contents.get('paths', {})
|
109
110
|
route = paths.get(http_path)
|
110
111
|
if route is None:
|
111
|
-
raise
|
112
|
+
raise BadRequest(f"Path {http_path} not found in paths. Available endpoints are: {list(paths.keys())}")
|
112
113
|
|
113
114
|
route_spec = route.get(http_method.lower(), route.get(http_method.upper()))
|
114
115
|
if route_spec is None:
|
115
|
-
raise
|
116
|
+
raise BadRequest(
|
116
117
|
f"Path {http_path} did not have an http_method {http_method}. Available methods are {list(route.keys())}")
|
117
118
|
|
118
119
|
operation_id = re.sub( r'(\W|_)+', '_', route_spec.get('operationId') ) \
|
@@ -121,12 +122,12 @@ def create_openapi_json_tool(
|
|
121
122
|
spec_name = name or operation_id
|
122
123
|
spec_permission = permission or _action_to_perm(route_spec.get('x-ibm-operation', {}).get('action'))
|
123
124
|
if spec_name is None:
|
124
|
-
raise
|
125
|
-
f"
|
125
|
+
raise BadRequest(
|
126
|
+
f"Failed to import tool from endpoint {http_method}: {http_path} as no operationId was provided. An operationId must be provided to generate the name of the tool.")
|
126
127
|
|
127
128
|
spec_description = description or route_spec.get('description')
|
128
129
|
if spec_description is None:
|
129
|
-
raise
|
130
|
+
raise BadRequest(
|
130
131
|
f"No description provided for tool. {http_method}: {http_path} did not specify a description field, and no description was provided")
|
131
132
|
|
132
133
|
spec = ToolSpec(
|
@@ -199,42 +200,89 @@ def create_openapi_json_tool(
|
|
199
200
|
for needed_security in route_spec.get('security', []) + openapi_spec.get('security', []):
|
200
201
|
name = next(iter(needed_security.keys()), None)
|
201
202
|
if name is None or name not in security_schemes_map:
|
202
|
-
raise
|
203
|
+
raise BadRequest(f"Invalid openapi spec, {HTTP_METHOD} {http_path} asks for a security scheme of {name}, "
|
203
204
|
f"but no such security scheme was configured in the .security section of the spec")
|
204
205
|
|
205
206
|
security.append(security_schemes_map[name])
|
206
207
|
|
207
208
|
# If it's an async tool, add callback binding
|
208
209
|
if spec.is_async:
|
209
|
-
|
210
|
-
|
211
210
|
callbacks = route_spec.get('callbacks', {})
|
212
211
|
callback_name = next(iter(callbacks.keys()))
|
213
212
|
callback_spec = callbacks[callback_name]
|
214
213
|
callback_path = next(iter(callback_spec.keys()))
|
215
214
|
callback_method = next(iter(callback_spec[callback_path].keys()))
|
215
|
+
callback_operation = callback_spec[callback_path][callback_method]
|
216
216
|
|
217
|
-
#
|
218
|
-
# Note: Currently assuming the callback URL parameter will be named 'callbackUrl' in the OpenAPI spec
|
219
|
-
# Future phases will handle other naming conventions
|
217
|
+
# Extract callback input schema from the callback requestBody
|
220
218
|
callback_input_schema = ToolRequestBody(
|
219
|
+
type='object',
|
220
|
+
properties={},
|
221
|
+
required=[]
|
222
|
+
)
|
223
|
+
|
224
|
+
# Handle callback parameters (query, path, header params)
|
225
|
+
callback_parameters = callback_operation.get('parameters') or []
|
226
|
+
for parameter in callback_parameters:
|
227
|
+
name = f"{parameter['in']}_{parameter['name']}"
|
228
|
+
if parameter.get('required'):
|
229
|
+
callback_input_schema.required.append(name)
|
230
|
+
parameter['schema']['title'] = parameter['name']
|
231
|
+
parameter['schema']['description'] = parameter.get('description', None)
|
232
|
+
callback_input_schema.properties[name] = JsonSchemaObject.model_validate(parameter['schema'])
|
233
|
+
callback_input_schema.properties[name].in_field = parameter['in']
|
234
|
+
callback_input_schema.properties[name].aliasName = parameter['name']
|
235
|
+
|
236
|
+
# Handle callback request body
|
237
|
+
callback_request_body_params = callback_operation.get('requestBody', {}).get('content', {}).get(http_response_content_type, {}).get('schema', None)
|
238
|
+
if callback_request_body_params is not None:
|
239
|
+
callback_input_schema.required.append('__requestBody__')
|
240
|
+
callback_request_body_params = copy.deepcopy(callback_request_body_params)
|
241
|
+
callback_request_body_params['in'] = 'body'
|
242
|
+
if callback_request_body_params.get('title') is None:
|
243
|
+
callback_request_body_params['title'] = 'CallbackRequestBody'
|
244
|
+
if callback_request_body_params.get('description') is None:
|
245
|
+
callback_request_body_params['description'] = 'The callback request body used for this async operation.'
|
246
|
+
|
247
|
+
callback_input_schema.properties['__requestBody__'] = JsonSchemaObject.model_validate(callback_request_body_params)
|
248
|
+
|
249
|
+
# Extract callback output schema
|
250
|
+
callback_responses = callback_operation.get('responses', {})
|
251
|
+
callback_response = callback_responses.get(str(http_success_response_code), {})
|
252
|
+
callback_response_description = callback_response.get('description')
|
253
|
+
callback_response_schema = callback_response.get('content', {}).get(http_response_content_type, {}).get('schema', {})
|
254
|
+
|
255
|
+
callback_response_schema['required'] = []
|
256
|
+
callback_output_schema = ToolResponseBody.model_validate(callback_response_schema)
|
257
|
+
callback_output_schema.description = callback_response_description
|
258
|
+
|
259
|
+
# Remove callbackUrl parameter from main tool input schema
|
260
|
+
original_input_schema = ToolRequestBody(
|
221
261
|
type='object',
|
222
262
|
properties={k: v for k, v in spec.input_schema.properties.items() if not k.endswith('_callbackUrl')},
|
223
263
|
required=[r for r in spec.input_schema.required if not r.endswith('_callbackUrl')]
|
224
264
|
)
|
265
|
+
spec.input_schema = original_input_schema
|
225
266
|
|
226
|
-
|
227
|
-
|
228
|
-
|
267
|
+
original_response_schema = spec.output_schema
|
268
|
+
|
229
269
|
callback_binding = CallbackBinding(
|
230
270
|
callback_url=callback_path,
|
231
271
|
method=callback_method.upper(),
|
232
|
-
|
233
|
-
output_schema=spec.output_schema
|
272
|
+
output_schema=callback_output_schema
|
234
273
|
)
|
235
274
|
|
275
|
+
# Create acknowledgement binding with the original response schema
|
276
|
+
acknowledgement_binding = AcknowledgementBinding(
|
277
|
+
output_schema=original_response_schema
|
278
|
+
)
|
279
|
+
|
280
|
+
# For async tools, set the main tool's output_schema to the callback's input_schema
|
281
|
+
spec.output_schema = callback_input_schema
|
282
|
+
|
236
283
|
else:
|
237
284
|
callback_binding = None
|
285
|
+
acknowledgement_binding = None
|
238
286
|
|
239
287
|
openapi_binding = OpenApiToolBinding(
|
240
288
|
http_path=http_path,
|
@@ -247,6 +295,9 @@ def create_openapi_json_tool(
|
|
247
295
|
if callback_binding is not None:
|
248
296
|
openapi_binding.callback = callback_binding
|
249
297
|
|
298
|
+
if acknowledgement_binding is not None:
|
299
|
+
openapi_binding.acknowledgement = acknowledgement_binding
|
300
|
+
|
250
301
|
spec.binding = ToolBinding(openapi=openapi_binding)
|
251
302
|
|
252
303
|
return OpenAPITool(spec=spec)
|
@@ -260,23 +311,23 @@ async def _get_openapi_spec_from_uri(openapi_uri: str) -> Dict[str, Any]:
|
|
260
311
|
elif openapi_uri.endswith('.yaml') or openapi_uri.endswith('.yml'):
|
261
312
|
openapi_contents = yaml_safe_load(fp)
|
262
313
|
else:
|
263
|
-
raise
|
314
|
+
raise BadRequest(
|
264
315
|
f"Unexpected file extension for file {openapi_uri}, expected one of [.json, .yaml, .yml]")
|
265
316
|
elif openapi_uri.endswith('.json'):
|
266
317
|
async with httpx.AsyncClient() as client:
|
267
318
|
r = await client.get(openapi_uri)
|
268
319
|
if r.status_code != 200:
|
269
|
-
raise
|
320
|
+
raise BadRequest(f"Failed to fetch an openapi spec from {openapi_uri}, status code: {r.status_code}")
|
270
321
|
openapi_contents = r.json()
|
271
322
|
elif openapi_uri.endswith('.yaml'):
|
272
323
|
async with httpx.AsyncClient() as client:
|
273
324
|
r = await client.get(openapi_uri)
|
274
325
|
if r.status_code != 200:
|
275
|
-
raise
|
326
|
+
raise BadRequest(f"Failed to fetch an openapi spec from {openapi_uri}, status code: {r.status_code}")
|
276
327
|
openapi_contents = yaml_safe_load(r.text)
|
277
328
|
|
278
329
|
if openapi_contents is None:
|
279
|
-
raise
|
330
|
+
raise BadRequest(f"Unrecognized path or uri {openapi_uri}")
|
280
331
|
|
281
332
|
return openapi_contents
|
282
333
|
|