ibm-watsonx-orchestrate 1.3.0__py3-none-any.whl → 1.4.2__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 +1 -1
- ibm_watsonx_orchestrate/agent_builder/agents/types.py +2 -0
- ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +9 -2
- ibm_watsonx_orchestrate/agent_builder/toolkits/base_toolkit.py +32 -0
- ibm_watsonx_orchestrate/agent_builder/toolkits/types.py +42 -0
- ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +10 -1
- ibm_watsonx_orchestrate/agent_builder/tools/python_tool.py +4 -2
- ibm_watsonx_orchestrate/agent_builder/tools/types.py +2 -1
- ibm_watsonx_orchestrate/cli/commands/agents/agents_command.py +29 -0
- ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +271 -12
- ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +17 -2
- ibm_watsonx_orchestrate/cli/commands/models/env_file_model_provider_mapper.py +180 -0
- ibm_watsonx_orchestrate/cli/commands/models/models_command.py +194 -8
- ibm_watsonx_orchestrate/cli/commands/server/server_command.py +117 -48
- ibm_watsonx_orchestrate/cli/commands/server/types.py +105 -0
- ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_command.py +55 -7
- ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_controller.py +123 -42
- ibm_watsonx_orchestrate/cli/commands/tools/tools_command.py +22 -1
- ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +197 -12
- ibm_watsonx_orchestrate/client/agents/agent_client.py +4 -1
- ibm_watsonx_orchestrate/client/agents/assistant_agent_client.py +5 -1
- ibm_watsonx_orchestrate/client/agents/external_agent_client.py +5 -1
- ibm_watsonx_orchestrate/client/analytics/llm/analytics_llm_client.py +2 -6
- ibm_watsonx_orchestrate/client/base_api_client.py +5 -2
- ibm_watsonx_orchestrate/client/connections/connections_client.py +3 -9
- ibm_watsonx_orchestrate/client/model_policies/__init__.py +0 -0
- ibm_watsonx_orchestrate/client/model_policies/model_policies_client.py +47 -0
- ibm_watsonx_orchestrate/client/model_policies/types.py +36 -0
- ibm_watsonx_orchestrate/client/models/__init__.py +0 -0
- ibm_watsonx_orchestrate/client/models/models_client.py +46 -0
- ibm_watsonx_orchestrate/client/models/types.py +177 -0
- ibm_watsonx_orchestrate/client/toolkit/toolkit_client.py +15 -6
- ibm_watsonx_orchestrate/client/tools/tempus_client.py +40 -0
- ibm_watsonx_orchestrate/client/tools/tool_client.py +8 -0
- ibm_watsonx_orchestrate/docker/compose-lite.yml +68 -13
- ibm_watsonx_orchestrate/docker/default.env +22 -12
- ibm_watsonx_orchestrate/docker/tempus/common-config.yaml +1 -1
- ibm_watsonx_orchestrate/experimental/flow_builder/__init__.py +0 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/flows/__init__.py +41 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/flows/constants.py +17 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/flows/data_map.py +91 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/flows/decorators.py +143 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/flows/events.py +72 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/flows/flow.py +1288 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/node.py +97 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/resources/flow_status.openapi.yml +98 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/types.py +492 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/utils.py +113 -0
- ibm_watsonx_orchestrate/utils/utils.py +5 -2
- {ibm_watsonx_orchestrate-1.3.0.dist-info → ibm_watsonx_orchestrate-1.4.2.dist-info}/METADATA +4 -1
- {ibm_watsonx_orchestrate-1.3.0.dist-info → ibm_watsonx_orchestrate-1.4.2.dist-info}/RECORD +54 -32
- {ibm_watsonx_orchestrate-1.3.0.dist-info → ibm_watsonx_orchestrate-1.4.2.dist-info}/WHEEL +0 -0
- {ibm_watsonx_orchestrate-1.3.0.dist-info → ibm_watsonx_orchestrate-1.4.2.dist-info}/entry_points.txt +0 -0
- {ibm_watsonx_orchestrate-1.3.0.dist-info → ibm_watsonx_orchestrate-1.4.2.dist-info}/licenses/LICENSE +0 -0
@@ -46,6 +46,7 @@ class BaseAgentSpec(BaseModel):
|
|
46
46
|
kind: AgentKind
|
47
47
|
id: Optional[Annotated[str, Field(json_schema_extra={"min_length_str": 1})]] = None
|
48
48
|
name: Annotated[str, Field(json_schema_extra={"min_length_str":1})]
|
49
|
+
display_name: Annotated[Optional[str], Field(json_schema_extra={"min_length_str":1})] = None
|
49
50
|
description: Annotated[str, Field(json_schema_extra={"min_length_str":1})]
|
50
51
|
|
51
52
|
def dump_spec(self, file: str) -> None:
|
@@ -69,6 +70,7 @@ class BaseAgentSpec(BaseModel):
|
|
69
70
|
class AgentStyle(str, Enum):
|
70
71
|
DEFAULT = "default"
|
71
72
|
REACT = "react"
|
73
|
+
PLANNER = "planner"
|
72
74
|
|
73
75
|
class AgentSpec(BaseAgentSpec):
|
74
76
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
@@ -3,8 +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,
|
7
|
-
|
6
|
+
from pydantic import BaseModel, model_validator
|
8
7
|
|
9
8
|
class SpecVersion(str, Enum):
|
10
9
|
V1 = "v1"
|
@@ -213,6 +212,14 @@ class PatchKnowledgeBase(BaseModel):
|
|
213
212
|
conversational_search_tool: Optional[ConversationalSearchConfig] = None
|
214
213
|
prioritize_built_in_index: Optional[bool] = None
|
215
214
|
representation: Optional[KnowledgeBaseRepresentation] = None
|
215
|
+
|
216
|
+
@model_validator(mode="after")
|
217
|
+
def validate_fields(self):
|
218
|
+
if self.documents and self.conversational_search_tool and self.conversational_search_tool.index_config:
|
219
|
+
raise ValueError("Must not provide both \"documents\" or \"conversational_search_tool.index_config\"")
|
220
|
+
if self.conversational_search_tool and self.conversational_search_tool.index_config and len(self.conversational_search_tool.index_config) != 1:
|
221
|
+
raise ValueError(f"Must provide exactly one conversational_search_tool.index_config. Provided {len(self.conversational_search_tool.index_config)}.")
|
222
|
+
return self
|
216
223
|
|
217
224
|
class KnowledgeBaseSpec(BaseModel):
|
218
225
|
"""Schema for a complete knowledge-base."""
|
@@ -0,0 +1,32 @@
|
|
1
|
+
import json
|
2
|
+
|
3
|
+
import yaml
|
4
|
+
|
5
|
+
from .types import ToolkitSpec
|
6
|
+
|
7
|
+
|
8
|
+
class BaseToolkit:
|
9
|
+
__toolkit_spec__: ToolkitSpec
|
10
|
+
|
11
|
+
def __init__(self, spec: ToolkitSpec):
|
12
|
+
self.__toolkit_spec__ = spec
|
13
|
+
|
14
|
+
def __call__(self, **kwargs):
|
15
|
+
pass
|
16
|
+
|
17
|
+
def dump_spec(self, file: str) -> None:
|
18
|
+
dumped = self.__toolkit_spec__.model_dump(mode='json', exclude_unset=True, exclude_none=True, by_alias=True)
|
19
|
+
with open(file, 'w') as f:
|
20
|
+
if file.endswith('.yaml') or file.endswith('.yml'):
|
21
|
+
yaml.dump(dumped, f)
|
22
|
+
elif file.endswith('.json'):
|
23
|
+
json.dump(dumped, f, indent=2)
|
24
|
+
else:
|
25
|
+
raise ValueError('file must end in .json, .yaml, or .yml')
|
26
|
+
|
27
|
+
def dumps_spec(self) -> str:
|
28
|
+
dumped = self.__toolkit_spec__.model_dump(mode='json', exclude_unset=True, exclude_none=True, by_alias=True)
|
29
|
+
return json.dumps(dumped, indent=2)
|
30
|
+
|
31
|
+
def __repr__(self):
|
32
|
+
return f"Toolkit(name='{self.__toolkit_spec__.name}', description='{self.__toolkit_spec__.description}')"
|
@@ -0,0 +1,42 @@
|
|
1
|
+
from typing import List, Dict, Optional
|
2
|
+
from enum import Enum
|
3
|
+
from pydantic import BaseModel, model_validator
|
4
|
+
|
5
|
+
class ToolkitKind(str, Enum):
|
6
|
+
MCP = "mcp"
|
7
|
+
|
8
|
+
class Language(str, Enum):
|
9
|
+
NODE = "node"
|
10
|
+
PYTHON ="python"
|
11
|
+
|
12
|
+
class ToolkitSource(str, Enum):
|
13
|
+
FILES = "files"
|
14
|
+
PUBLIC_REGISTRY = "public-registry"
|
15
|
+
|
16
|
+
|
17
|
+
|
18
|
+
class McpModel(BaseModel):
|
19
|
+
source: str
|
20
|
+
command: str
|
21
|
+
args: List[str]
|
22
|
+
tools: List[str]
|
23
|
+
connections: Dict[str, str]
|
24
|
+
|
25
|
+
|
26
|
+
class ToolkitSpec(BaseModel):
|
27
|
+
id: str
|
28
|
+
tenant_id: str
|
29
|
+
name: str
|
30
|
+
description: Optional[str]
|
31
|
+
created_on: str
|
32
|
+
updated_at: str
|
33
|
+
created_by: str
|
34
|
+
created_by_username: str
|
35
|
+
tools: List[str] | None
|
36
|
+
mcp: McpModel
|
37
|
+
|
38
|
+
@model_validator(mode='after')
|
39
|
+
def validate_tools_and_mcp(self) -> 'ToolkitSpec':
|
40
|
+
if self.mcp.source not in {"files", "public-registry"}:
|
41
|
+
raise ValueError("MCP source must be either 'files' or 'public-registry'.")
|
42
|
+
return self
|
@@ -293,7 +293,7 @@ async def create_openapi_json_tool_from_uri(
|
|
293
293
|
description=description,
|
294
294
|
input_schema=input_schema,
|
295
295
|
output_schema=output_schema,
|
296
|
-
|
296
|
+
connection_id=app_id
|
297
297
|
)
|
298
298
|
|
299
299
|
|
@@ -302,6 +302,15 @@ async def create_openapi_json_tools_from_uri(
|
|
302
302
|
connection_id: str = None
|
303
303
|
) -> List[OpenAPITool]:
|
304
304
|
openapi_contents = await _get_openapi_spec_from_uri(openapi_uri)
|
305
|
+
tools: List[OpenAPITool] = await create_openapi_json_tools_from_content(openapi_contents, connection_id)
|
306
|
+
|
307
|
+
return tools
|
308
|
+
|
309
|
+
async def create_openapi_json_tools_from_content(
|
310
|
+
openapi_contents: dict,
|
311
|
+
connection_id: str = None
|
312
|
+
) -> List[OpenAPITool]:
|
313
|
+
|
305
314
|
tools: List[OpenAPITool] = []
|
306
315
|
|
307
316
|
for path, methods in openapi_contents.get('paths', {}).items():
|
@@ -47,7 +47,7 @@ class PythonTool(BaseTool):
|
|
47
47
|
return PythonTool(fn=fn, spec=spec)
|
48
48
|
|
49
49
|
def __repr__(self):
|
50
|
-
return f"PythonTool(fn={self.__tool_spec__.binding.python.function}, name='{self.__tool_spec__.name}', description='{self.__tool_spec__.description}')"
|
50
|
+
return f"PythonTool(fn={self.__tool_spec__.binding.python.function}, name='{self.__tool_spec__.name}', display_name='{self.__tool_spec__.display_name or ''}', description='{self.__tool_spec__.description}')"
|
51
51
|
|
52
52
|
def __str__(self):
|
53
53
|
return self.__repr__()
|
@@ -99,7 +99,8 @@ def tool(
|
|
99
99
|
input_schema: ToolRequestBody = None,
|
100
100
|
output_schema: ToolResponseBody = None,
|
101
101
|
permission: ToolPermission = ToolPermission.READ_ONLY,
|
102
|
-
expected_credentials: List[ExpectedCredentials] = None
|
102
|
+
expected_credentials: List[ExpectedCredentials] = None,
|
103
|
+
display_name: str = None
|
103
104
|
) -> Callable[[{__name__, __doc__}], PythonTool]:
|
104
105
|
"""
|
105
106
|
Decorator to convert a python function into a callable tool.
|
@@ -125,6 +126,7 @@ def tool(
|
|
125
126
|
|
126
127
|
spec = ToolSpec(
|
127
128
|
name=name or fn.__name__,
|
129
|
+
display_name=display_name,
|
128
130
|
description=_desc,
|
129
131
|
permission=permission
|
130
132
|
)
|
@@ -137,7 +137,7 @@ class ClientSideToolBinding(BaseModel):
|
|
137
137
|
class McpToolBinding(BaseModel):
|
138
138
|
server_url: Optional[str] = None
|
139
139
|
source: str
|
140
|
-
connections: Dict[str, str]
|
140
|
+
connections: Dict[str, str] | None
|
141
141
|
|
142
142
|
class ToolBinding(BaseModel):
|
143
143
|
openapi: OpenApiToolBinding = None
|
@@ -166,6 +166,7 @@ class ToolBinding(BaseModel):
|
|
166
166
|
|
167
167
|
class ToolSpec(BaseModel):
|
168
168
|
name: str
|
169
|
+
display_name: str | None = None
|
169
170
|
description: str
|
170
171
|
permission: ToolPermission
|
171
172
|
input_schema: ToolRequestBody = None
|
@@ -190,3 +190,32 @@ def remove_agent(
|
|
190
190
|
):
|
191
191
|
agents_controller = AgentsController()
|
192
192
|
agents_controller.remove_agent(name=name, kind=kind)
|
193
|
+
|
194
|
+
@agents_app.command(name="export", help='Export an agent and its dependencies to a zip file or yaml')
|
195
|
+
def export_agent(
|
196
|
+
name: Annotated[
|
197
|
+
str,
|
198
|
+
typer.Option("--name", "-n", help="Name of the agent you wish to export"),
|
199
|
+
],
|
200
|
+
kind: Annotated[
|
201
|
+
AgentKind,
|
202
|
+
typer.Option("--kind", "-k", help="The kind of agent you wish to export"),
|
203
|
+
],
|
204
|
+
output_file: Annotated[
|
205
|
+
str,
|
206
|
+
typer.Option(
|
207
|
+
"--output",
|
208
|
+
"-o",
|
209
|
+
help="Path to a where the file containing the exported data should be saved",
|
210
|
+
),
|
211
|
+
],
|
212
|
+
agent_only_flag: Annotated[
|
213
|
+
bool,
|
214
|
+
typer.Option(
|
215
|
+
"--agent-only",
|
216
|
+
help="Export only the yaml to the specified agent, excluding its dependencies",
|
217
|
+
),
|
218
|
+
]=False
|
219
|
+
):
|
220
|
+
agents_controller = AgentsController()
|
221
|
+
agents_controller.export_agent(name=name, kind=kind, output_path=output_file, agent_only_flag=agent_only_flag)
|
@@ -4,13 +4,15 @@ import rich
|
|
4
4
|
import requests
|
5
5
|
import importlib
|
6
6
|
import inspect
|
7
|
+
import zipfile
|
7
8
|
import sys
|
9
|
+
import io
|
8
10
|
import logging
|
9
11
|
from pathlib import Path
|
10
12
|
from copy import deepcopy
|
11
13
|
|
12
14
|
from typing import Iterable, List
|
13
|
-
from ibm_watsonx_orchestrate.cli.commands.tools.tools_controller import import_python_tool
|
15
|
+
from ibm_watsonx_orchestrate.cli.commands.tools.tools_controller import import_python_tool, ToolsController
|
14
16
|
from ibm_watsonx_orchestrate.cli.commands.knowledge_bases.knowledge_bases_controller import import_python_knowledge_base
|
15
17
|
|
16
18
|
from ibm_watsonx_orchestrate.agent_builder.agents import (
|
@@ -28,6 +30,7 @@ from ibm_watsonx_orchestrate.client.connections import get_connections_client
|
|
28
30
|
from ibm_watsonx_orchestrate.client.knowledge_bases.knowledge_base_client import KnowledgeBaseClient
|
29
31
|
|
30
32
|
from ibm_watsonx_orchestrate.client.utils import instantiate_client
|
33
|
+
from ibm_watsonx_orchestrate.utils.utils import check_file_in_zip
|
31
34
|
|
32
35
|
logger = logging.getLogger(__name__)
|
33
36
|
|
@@ -142,10 +145,29 @@ def get_conn_id_from_app_id(app_id: str) -> str:
|
|
142
145
|
connections_client = get_connections_client()
|
143
146
|
connection = connections_client.get_draft_by_app_id(app_id=app_id)
|
144
147
|
if not connection:
|
145
|
-
logger.error(f"No connection
|
148
|
+
logger.error(f"No connection exists with the app-id '{app_id}'")
|
146
149
|
exit(1)
|
147
150
|
return connection.connection_id
|
148
151
|
|
152
|
+
def get_app_id_from_conn_id(conn_id: str) -> str:
|
153
|
+
connections_client = get_connections_client()
|
154
|
+
app_id = connections_client.get_draft_by_id(conn_id=conn_id)
|
155
|
+
if not app_id or app_id == conn_id:
|
156
|
+
logger.error(f"No connection exists with the connection id '{conn_id}'")
|
157
|
+
exit(1)
|
158
|
+
return app_id
|
159
|
+
|
160
|
+
def get_agent_details(name: str, client: AgentClient | ExternalAgentClient | AssistantAgentClient) -> dict:
|
161
|
+
agent_specs = client.get_draft_by_name(name)
|
162
|
+
if len(agent_specs) > 1:
|
163
|
+
logger.error(f"Multiple agents with the name '{name}' found. Failed to get agent")
|
164
|
+
sys.exit(1)
|
165
|
+
if len(agent_specs) == 0:
|
166
|
+
logger.error(f"No agents with the name '{name}' found. Failed to get agent")
|
167
|
+
sys.exit(1)
|
168
|
+
|
169
|
+
return agent_specs[0]
|
170
|
+
|
149
171
|
class AgentsController:
|
150
172
|
def __init__(self):
|
151
173
|
self.native_client = None
|
@@ -227,6 +249,7 @@ class AgentsController:
|
|
227
249
|
matching_external_agents = external_client.get_drafts_by_names(deref_agent.collaborators)
|
228
250
|
matching_assistant_agents = assistant_client.get_drafts_by_names(deref_agent.collaborators)
|
229
251
|
matching_agents = matching_native_agents + matching_external_agents + matching_assistant_agents
|
252
|
+
|
230
253
|
name_id_lookup = {}
|
231
254
|
for a in matching_agents:
|
232
255
|
if a.get("name") in name_id_lookup:
|
@@ -245,6 +268,35 @@ class AgentsController:
|
|
245
268
|
|
246
269
|
return deref_agent
|
247
270
|
|
271
|
+
def reference_collaborators(self, agent: Agent) -> Agent:
|
272
|
+
native_client = self.get_native_client()
|
273
|
+
external_client = self.get_external_client()
|
274
|
+
assistant_client = self.get_assistant_client()
|
275
|
+
|
276
|
+
ref_agent = deepcopy(agent)
|
277
|
+
matching_native_agents = native_client.get_drafts_by_ids(ref_agent.collaborators)
|
278
|
+
matching_external_agents = external_client.get_drafts_by_ids(ref_agent.collaborators)
|
279
|
+
matching_assistant_agents = assistant_client.get_drafts_by_ids(ref_agent.collaborators)
|
280
|
+
matching_agents = matching_native_agents + matching_external_agents + matching_assistant_agents
|
281
|
+
|
282
|
+
id_name_lookup = {}
|
283
|
+
for a in matching_agents:
|
284
|
+
if a.get("id") in id_name_lookup:
|
285
|
+
logger.error(f"Duplicate draft entries for collaborator '{a.get('id')}'")
|
286
|
+
sys.exit(1)
|
287
|
+
id_name_lookup[a.get("id")] = a.get("name")
|
288
|
+
|
289
|
+
ref_collaborators = []
|
290
|
+
for id in agent.collaborators:
|
291
|
+
name = id_name_lookup.get(id)
|
292
|
+
if not name:
|
293
|
+
logger.error(f"Failed to find collaborator. No agents found with the id '{id}'")
|
294
|
+
sys.exit(1)
|
295
|
+
ref_collaborators.append(name)
|
296
|
+
ref_agent.collaborators = ref_collaborators
|
297
|
+
|
298
|
+
return ref_agent
|
299
|
+
|
248
300
|
def dereference_tools(self, agent: Agent) -> Agent:
|
249
301
|
tool_client = self.get_tool_client()
|
250
302
|
|
@@ -254,7 +306,7 @@ class AgentsController:
|
|
254
306
|
name_id_lookup = {}
|
255
307
|
for tool in matching_tools:
|
256
308
|
if tool.get("name") in name_id_lookup:
|
257
|
-
logger.error(f"Duplicate draft entries for
|
309
|
+
logger.error(f"Duplicate draft entries for tool '{tool.get('name')}'")
|
258
310
|
sys.exit(1)
|
259
311
|
name_id_lookup[tool.get("name")] = tool.get("id")
|
260
312
|
|
@@ -269,6 +321,30 @@ class AgentsController:
|
|
269
321
|
|
270
322
|
return deref_agent
|
271
323
|
|
324
|
+
def reference_tools(self, agent: Agent) -> Agent:
|
325
|
+
tool_client = self.get_tool_client()
|
326
|
+
|
327
|
+
ref_agent = deepcopy(agent)
|
328
|
+
matching_tools = tool_client.get_drafts_by_ids(ref_agent.tools)
|
329
|
+
|
330
|
+
id_name_lookup = {}
|
331
|
+
for tool in matching_tools:
|
332
|
+
if tool.get("id") in id_name_lookup:
|
333
|
+
logger.error(f"Duplicate draft entries for tool '{tool.get('id')}'")
|
334
|
+
sys.exit(1)
|
335
|
+
id_name_lookup[tool.get("id")] = tool.get("name")
|
336
|
+
|
337
|
+
ref_tools = []
|
338
|
+
for id in agent.tools:
|
339
|
+
name = id_name_lookup[id]
|
340
|
+
if not name:
|
341
|
+
logger.error(f"Failed to find tool. No tools found with the id '{id}'")
|
342
|
+
sys.exit(1)
|
343
|
+
ref_tools.append(name)
|
344
|
+
ref_agent.tools = ref_tools
|
345
|
+
|
346
|
+
return ref_agent
|
347
|
+
|
272
348
|
def dereference_knowledge_bases(self, agent: Agent) -> Agent:
|
273
349
|
client = self.get_knowledge_base_client()
|
274
350
|
|
@@ -293,6 +369,22 @@ class AgentsController:
|
|
293
369
|
|
294
370
|
return deref_agent
|
295
371
|
|
372
|
+
def reference_knowledge_bases(self, agent: Agent) -> Agent:
|
373
|
+
client = self.get_knowledge_base_client()
|
374
|
+
|
375
|
+
ref_agent = deepcopy(agent)
|
376
|
+
|
377
|
+
ref_knowledge_bases = []
|
378
|
+
for id in agent.knowledge_base:
|
379
|
+
matching_knowledge_base = client.get_by_id(id)
|
380
|
+
name = matching_knowledge_base.get("name")
|
381
|
+
if not name:
|
382
|
+
logger.error(f"Failed to find knowledge base. No knowledge base found with the id '{id}'")
|
383
|
+
sys.exit(1)
|
384
|
+
ref_knowledge_bases.append(name)
|
385
|
+
ref_agent.knowledge_base = ref_knowledge_bases
|
386
|
+
return ref_agent
|
387
|
+
|
296
388
|
@staticmethod
|
297
389
|
def dereference_app_id(agent: ExternalAgent | AssistantAgent) -> ExternalAgent | AssistantAgent:
|
298
390
|
if agent.kind == AgentKind.EXTERNAL:
|
@@ -301,6 +393,17 @@ class AgentsController:
|
|
301
393
|
agent.config.connection_id = get_conn_id_from_app_id(agent.config.app_id)
|
302
394
|
|
303
395
|
return agent
|
396
|
+
|
397
|
+
@staticmethod
|
398
|
+
def reference_app_id(agent: ExternalAgent | AssistantAgent) -> ExternalAgent | AssistantAgent:
|
399
|
+
if agent.kind == AgentKind.EXTERNAL:
|
400
|
+
agent.app_id = get_app_id_from_conn_id(agent.connection_id)
|
401
|
+
agent.connection_id = None
|
402
|
+
else:
|
403
|
+
agent.config.app_id = get_app_id_from_conn_id(agent.config.connection_id)
|
404
|
+
agent.config.connection_id = None
|
405
|
+
|
406
|
+
return agent
|
304
407
|
|
305
408
|
|
306
409
|
def dereference_native_agent_dependencies(self, agent: Agent) -> Agent:
|
@@ -313,6 +416,16 @@ class AgentsController:
|
|
313
416
|
|
314
417
|
return agent
|
315
418
|
|
419
|
+
def reference_native_agent_dependencies(self, agent: Agent) -> Agent:
|
420
|
+
if agent.collaborators and len(agent.collaborators):
|
421
|
+
agent = self.reference_collaborators(agent)
|
422
|
+
if agent.tools and len(agent.tools):
|
423
|
+
agent = self.reference_tools(agent)
|
424
|
+
if agent.knowledge_base and len(agent.knowledge_base):
|
425
|
+
agent = self.reference_knowledge_bases(agent)
|
426
|
+
|
427
|
+
return agent
|
428
|
+
|
316
429
|
def dereference_external_or_assistant_agent_dependencies(self, agent: ExternalAgent | AssistantAgent) -> ExternalAgent | AssistantAgent:
|
317
430
|
agent_dict = agent.model_dump()
|
318
431
|
|
@@ -320,13 +433,28 @@ class AgentsController:
|
|
320
433
|
agent = self.dereference_app_id(agent)
|
321
434
|
|
322
435
|
return agent
|
323
|
-
|
324
|
-
def
|
436
|
+
|
437
|
+
def reference_external_or_assistant_agent_dependencies(self, agent: ExternalAgent | AssistantAgent) -> ExternalAgent | AssistantAgent:
|
438
|
+
agent_dict = agent.model_dump()
|
439
|
+
|
440
|
+
if agent_dict.get("connection_id") or agent.config.model_dump().get("connection_id"):
|
441
|
+
agent = self.reference_app_id(agent)
|
442
|
+
|
443
|
+
return agent
|
444
|
+
|
445
|
+
# Convert all names used in an agent to the corresponding ids
|
446
|
+
def dereference_agent_dependencies(self, agent: Agent | ExternalAgent | AssistantAgent ) -> Agent | ExternalAgent | AssistantAgent:
|
325
447
|
if isinstance(agent, Agent):
|
326
448
|
return self.dereference_native_agent_dependencies(agent)
|
327
449
|
if isinstance(agent, ExternalAgent) or isinstance(agent, AssistantAgent):
|
328
450
|
return self.dereference_external_or_assistant_agent_dependencies(agent)
|
329
|
-
|
451
|
+
|
452
|
+
# Convert all ids used in an agent to the corresponding names
|
453
|
+
def reference_agent_dependencies(self, agent: Agent | ExternalAgent | AssistantAgent ) -> Agent | ExternalAgent | AssistantAgent:
|
454
|
+
if isinstance(agent, Agent):
|
455
|
+
return self.reference_native_agent_dependencies(agent)
|
456
|
+
if isinstance(agent, ExternalAgent) or isinstance(agent, AssistantAgent):
|
457
|
+
return self.reference_external_or_assistant_agent_dependencies(agent)
|
330
458
|
|
331
459
|
def publish_or_update_agents(
|
332
460
|
self, agents: Iterable[Agent]
|
@@ -368,13 +496,13 @@ class AgentsController:
|
|
368
496
|
|
369
497
|
def publish_agent(self, agent: Agent, **kwargs) -> None:
|
370
498
|
if isinstance(agent, Agent):
|
371
|
-
self.get_native_client().create(agent.model_dump())
|
499
|
+
self.get_native_client().create(agent.model_dump(exclude_none=True))
|
372
500
|
logger.info(f"Agent '{agent.name}' imported successfully")
|
373
501
|
if isinstance(agent, ExternalAgent):
|
374
|
-
self.get_external_client().create(agent.model_dump())
|
502
|
+
self.get_external_client().create(agent.model_dump(exclude_none=True))
|
375
503
|
logger.info(f"External Agent '{agent.name}' imported successfully")
|
376
504
|
if isinstance(agent, AssistantAgent):
|
377
|
-
self.get_assistant_client().create(agent.model_dump(by_alias=True))
|
505
|
+
self.get_assistant_client().create(agent.model_dump(exclude_none=True, by_alias=True))
|
378
506
|
logger.info(f"Assistant Agent '{agent.name}' imported successfully")
|
379
507
|
|
380
508
|
def update_agent(
|
@@ -382,15 +510,15 @@ class AgentsController:
|
|
382
510
|
) -> None:
|
383
511
|
if isinstance(agent, Agent):
|
384
512
|
logger.info(f"Existing Agent '{agent.name}' found. Updating...")
|
385
|
-
self.get_native_client().update(agent_id, agent.model_dump())
|
513
|
+
self.get_native_client().update(agent_id, agent.model_dump(exclude_none=True))
|
386
514
|
logger.info(f"Agent '{agent.name}' updated successfully")
|
387
515
|
if isinstance(agent, ExternalAgent):
|
388
516
|
logger.info(f"Existing External Agent '{agent.name}' found. Updating...")
|
389
|
-
self.get_external_client().update(agent_id, agent.model_dump())
|
517
|
+
self.get_external_client().update(agent_id, agent.model_dump(exclude_none=True))
|
390
518
|
logger.info(f"External Agent '{agent.name}' updated successfully")
|
391
519
|
if isinstance(agent, AssistantAgent):
|
392
520
|
logger.info(f"Existing Assistant Agent '{agent.name}' found. Updating...")
|
393
|
-
self.get_assistant_client().update(agent_id, agent.model_dump(by_alias=True))
|
521
|
+
self.get_assistant_client().update(agent_id, agent.model_dump(exclude_none=True, by_alias=True))
|
394
522
|
logger.info(f"Assistant Agent '{agent.name}' updated successfully")
|
395
523
|
|
396
524
|
@staticmethod
|
@@ -659,4 +787,135 @@ class AgentsController:
|
|
659
787
|
except requests.HTTPError as e:
|
660
788
|
logger.error(e.response.text)
|
661
789
|
exit(1)
|
790
|
+
|
791
|
+
def get_spec_file_content(self, agent: Agent | ExternalAgent | AssistantAgent):
|
792
|
+
ref_agent = self.reference_agent_dependencies(agent)
|
793
|
+
agent_spec = ref_agent.model_dump(mode='json', exclude_none=True)
|
794
|
+
return agent_spec
|
795
|
+
|
796
|
+
def get_agent(self, name: str, kind: AgentKind) -> Agent | ExternalAgent | AssistantAgent:
|
797
|
+
match kind:
|
798
|
+
case AgentKind.NATIVE:
|
799
|
+
client = self.get_native_client()
|
800
|
+
agent_details = get_agent_details(name=name, client=client)
|
801
|
+
agent = Agent.model_validate(agent_details)
|
802
|
+
case AgentKind.EXTERNAL:
|
803
|
+
client = self.get_external_client()
|
804
|
+
agent_details = get_agent_details(name=name, client=client)
|
805
|
+
agent = ExternalAgent.model_validate(agent_details)
|
806
|
+
case AgentKind.ASSISTANT:
|
807
|
+
client = self.get_assistant_client()
|
808
|
+
agent_details = get_agent_details(name=name, client=client)
|
809
|
+
agent = AssistantAgent.model_validate(agent_details)
|
810
|
+
|
811
|
+
return agent
|
812
|
+
|
813
|
+
def get_agent_by_id(self, id: str) -> Agent | ExternalAgent | AssistantAgent:
|
814
|
+
native_client = self.get_native_client()
|
815
|
+
external_client = self.get_external_client()
|
816
|
+
assistant_client = self.get_assistant_client()
|
817
|
+
|
818
|
+
native_result = native_client.get_draft_by_id(id)
|
819
|
+
external_result = external_client.get_draft_by_id(id)
|
820
|
+
assistant_result = assistant_client.get_draft_by_id(id)
|
821
|
+
|
822
|
+
if native_result:
|
823
|
+
return Agent.model_validate(native_result)
|
824
|
+
if external_result:
|
825
|
+
return ExternalAgent.model_validate(external_result)
|
826
|
+
if assistant_result:
|
827
|
+
return AssistantAgent.model_validate(assistant_result)
|
828
|
+
|
829
|
+
|
830
|
+
def export_agent(self, name: str, kind: AgentKind, output_path: str, agent_only_flag: bool=False, zip_file_out: zipfile.ZipFile | None = None) -> None:
|
831
|
+
|
832
|
+
output_file = Path(output_path)
|
833
|
+
output_file_extension = output_file.suffix
|
834
|
+
output_file_name = output_file.stem
|
835
|
+
if not agent_only_flag and output_file_extension != ".zip":
|
836
|
+
logger.error(f"Output file must end with the extension '.zip'. Provided file '{output_path}' ends with '{output_file_extension}'")
|
837
|
+
sys.exit(1)
|
838
|
+
elif agent_only_flag and (output_file_extension != ".yaml" and output_file_extension != ".yml"):
|
839
|
+
logger.error(f"Output file must end with the extension '.yaml' or '.yml'. Provided file '{output_path}' ends with '{output_file_extension}'")
|
840
|
+
sys.exit(1)
|
841
|
+
|
842
|
+
agent = self.get_agent(name, kind)
|
843
|
+
agent_spec_file_content = self.get_spec_file_content(agent)
|
844
|
+
|
845
|
+
agent_spec_file_content.pop("hidden", None)
|
846
|
+
agent_spec_file_content.pop("id", None)
|
847
|
+
agent_spec_file_content["spec_version"] = SpecVersion.V1.value
|
848
|
+
|
849
|
+
if agent_only_flag:
|
850
|
+
logger.info(f"Exported agent definition for '{name}' to '{output_path}'")
|
851
|
+
with open(output_path, 'w') as outfile:
|
852
|
+
yaml.dump(agent_spec_file_content, outfile, sort_keys=False, default_flow_style=False)
|
853
|
+
return
|
854
|
+
|
855
|
+
close_file_flag = False
|
856
|
+
if zip_file_out is None:
|
857
|
+
close_file_flag = True
|
858
|
+
zip_file_out = zipfile.ZipFile(output_path, "w")
|
859
|
+
|
860
|
+
logger.info(f"Exporting agent definition for '{name}'")
|
861
|
+
|
862
|
+
agent_spec_yaml = yaml.dump(agent_spec_file_content, sort_keys=False, default_flow_style=False)
|
863
|
+
agent_spec_yaml_bytes = agent_spec_yaml.encode("utf-8")
|
864
|
+
agent_spec_yaml_file = io.BytesIO(agent_spec_yaml_bytes)
|
865
|
+
|
866
|
+
# Skip processing an agent if its already been saved
|
867
|
+
agent_file_path = f"{output_file_name}/agents/{agent_spec_file_content.get('kind', 'unknown')}/{agent_spec_file_content.get('name')}.yaml"
|
868
|
+
if check_file_in_zip(file_path=agent_file_path, zip_file=zip_file_out):
|
869
|
+
logger.warning(f"Skipping {agent_spec_file_content.get('name')}, agent with that name already exists in the output folder")
|
870
|
+
if close_file_flag:
|
871
|
+
zip_file_out.close()
|
872
|
+
return
|
873
|
+
|
874
|
+
zip_file_out.writestr(
|
875
|
+
agent_file_path,
|
876
|
+
agent_spec_yaml_file.getvalue()
|
877
|
+
)
|
878
|
+
|
879
|
+
tools_contoller = ToolsController()
|
880
|
+
for tool_name in agent_spec_file_content.get("tools", []):
|
881
|
+
|
882
|
+
base_tool_file_path = f"{output_file_name}/tools/{tool_name}/"
|
883
|
+
if check_file_in_zip(file_path=base_tool_file_path, zip_file=zip_file_out):
|
884
|
+
continue
|
885
|
+
|
886
|
+
logger.info(f"Exporting tool '{tool_name}'")
|
887
|
+
tool_artifact_bytes = tools_contoller.download_tool(tool_name)
|
888
|
+
if not tool_artifact_bytes:
|
889
|
+
continue
|
890
|
+
|
891
|
+
with zipfile.ZipFile(io.BytesIO(tool_artifact_bytes), "r") as zip_file_in:
|
892
|
+
for item in zip_file_in.infolist():
|
893
|
+
buffer = zip_file_in.read(item.filename)
|
894
|
+
if (item.filename != 'bundle-format'):
|
895
|
+
zip_file_out.writestr(
|
896
|
+
f"{base_tool_file_path}{item.filename}",
|
897
|
+
buffer
|
898
|
+
)
|
899
|
+
|
900
|
+
for kb_name in agent_spec_file_content.get("knowledge_base", []):
|
901
|
+
logger.warning(f"Skipping {kb_name}, knowledge_bases are currently unsupported by export")
|
902
|
+
|
903
|
+
if kind == AgentKind.NATIVE:
|
904
|
+
for collaborator_id in agent.collaborators:
|
905
|
+
collaborator = self.get_agent_by_id(collaborator_id)
|
906
|
+
|
907
|
+
if not collaborator:
|
908
|
+
logger.warning(f"Skipping {collaborator_id}, no agent with id {collaborator_id} found")
|
909
|
+
continue
|
910
|
+
|
911
|
+
self.export_agent(
|
912
|
+
name=collaborator.name,
|
913
|
+
kind=collaborator.kind,
|
914
|
+
output_path=output_path,
|
915
|
+
agent_only_flag=False,
|
916
|
+
zip_file_out=zip_file_out)
|
917
|
+
|
918
|
+
if close_file_flag:
|
919
|
+
logger.info(f"Successfully wrote agents and tools to '{output_path}'")
|
920
|
+
zip_file_out.close()
|
662
921
|
|