ibm-watsonx-orchestrate 1.11.0b1__py3-none-any.whl → 1.12.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ibm_watsonx_orchestrate/__init__.py +1 -2
- ibm_watsonx_orchestrate/agent_builder/agents/types.py +22 -5
- ibm_watsonx_orchestrate/agent_builder/connections/connections.py +3 -3
- ibm_watsonx_orchestrate/agent_builder/connections/types.py +14 -0
- ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +1 -1
- ibm_watsonx_orchestrate/agent_builder/models/types.py +1 -0
- ibm_watsonx_orchestrate/agent_builder/toolkits/base_toolkit.py +1 -1
- ibm_watsonx_orchestrate/agent_builder/tools/__init__.py +1 -0
- ibm_watsonx_orchestrate/agent_builder/tools/base_tool.py +1 -1
- ibm_watsonx_orchestrate/agent_builder/tools/langflow_tool.py +124 -0
- ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +9 -3
- ibm_watsonx_orchestrate/agent_builder/tools/types.py +20 -2
- ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +19 -6
- ibm_watsonx_orchestrate/cli/commands/connections/connections_command.py +18 -0
- ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +114 -0
- ibm_watsonx_orchestrate/cli/commands/copilot/copilot_controller.py +2 -6
- ibm_watsonx_orchestrate/cli/commands/copilot/copilot_server_controller.py +24 -91
- ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_command.py +49 -0
- ibm_watsonx_orchestrate/cli/commands/models/model_provider_mapper.py +23 -4
- ibm_watsonx_orchestrate/cli/commands/models/models_controller.py +3 -3
- ibm_watsonx_orchestrate/cli/commands/partners/offering/partners_offering_command.py +56 -0
- ibm_watsonx_orchestrate/cli/commands/partners/offering/partners_offering_controller.py +458 -0
- ibm_watsonx_orchestrate/cli/commands/partners/offering/types.py +107 -0
- ibm_watsonx_orchestrate/cli/commands/partners/partners_command.py +12 -0
- ibm_watsonx_orchestrate/cli/commands/partners/partners_controller.py +0 -0
- ibm_watsonx_orchestrate/cli/commands/server/server_command.py +124 -637
- ibm_watsonx_orchestrate/cli/commands/server/types.py +1 -1
- ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_command.py +2 -2
- ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_controller.py +2 -2
- ibm_watsonx_orchestrate/cli/commands/tools/tools_command.py +2 -3
- ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +206 -43
- ibm_watsonx_orchestrate/cli/main.py +2 -0
- ibm_watsonx_orchestrate/client/connections/connections_client.py +4 -1
- ibm_watsonx_orchestrate/client/tools/tempus_client.py +3 -0
- ibm_watsonx_orchestrate/client/tools/tool_client.py +5 -2
- ibm_watsonx_orchestrate/client/utils.py +31 -1
- ibm_watsonx_orchestrate/docker/compose-lite.yml +68 -17
- ibm_watsonx_orchestrate/docker/default.env +21 -18
- ibm_watsonx_orchestrate/flow_builder/flows/decorators.py +8 -2
- ibm_watsonx_orchestrate/flow_builder/flows/flow.py +31 -7
- ibm_watsonx_orchestrate/flow_builder/node.py +1 -1
- ibm_watsonx_orchestrate/flow_builder/types.py +18 -3
- ibm_watsonx_orchestrate/utils/docker_utils.py +280 -0
- ibm_watsonx_orchestrate/utils/environment.py +369 -0
- ibm_watsonx_orchestrate/utils/utils.py +1 -1
- {ibm_watsonx_orchestrate-1.11.0b1.dist-info → ibm_watsonx_orchestrate-1.12.0b1.dist-info}/METADATA +2 -2
- {ibm_watsonx_orchestrate-1.11.0b1.dist-info → ibm_watsonx_orchestrate-1.12.0b1.dist-info}/RECORD +50 -42
- {ibm_watsonx_orchestrate-1.11.0b1.dist-info → ibm_watsonx_orchestrate-1.12.0b1.dist-info}/WHEEL +0 -0
- {ibm_watsonx_orchestrate-1.11.0b1.dist-info → ibm_watsonx_orchestrate-1.12.0b1.dist-info}/entry_points.txt +0 -0
- {ibm_watsonx_orchestrate-1.11.0b1.dist-info → ibm_watsonx_orchestrate-1.12.0b1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,458 @@
|
|
1
|
+
import json
|
2
|
+
import yaml
|
3
|
+
import zipfile
|
4
|
+
import logging
|
5
|
+
import sys
|
6
|
+
from pathlib import Path
|
7
|
+
import tempfile
|
8
|
+
import zipfile
|
9
|
+
import shutil
|
10
|
+
from shutil import make_archive
|
11
|
+
from ibm_watsonx_orchestrate.agent_builder.tools.types import ToolSpec
|
12
|
+
from ibm_watsonx_orchestrate.client.agents.agent_client import AgentClient
|
13
|
+
from ibm_watsonx_orchestrate.client.agents.external_agent_client import ExternalAgentClient
|
14
|
+
from ibm_watsonx_orchestrate.client.tools.tool_client import ToolClient
|
15
|
+
from ibm_watsonx_orchestrate.cli.commands.agents.agents_controller import AgentsController, AgentKind, parse_create_native_args, parse_create_external_args
|
16
|
+
from ibm_watsonx_orchestrate.client.utils import instantiate_client
|
17
|
+
from ibm_watsonx_orchestrate.agent_builder.agents import (
|
18
|
+
Agent,
|
19
|
+
ExternalAgent,
|
20
|
+
AgentKind,
|
21
|
+
)
|
22
|
+
from ibm_watsonx_orchestrate.client.connections import get_connections_client
|
23
|
+
from ibm_watsonx_orchestrate.agent_builder.connections.types import ConnectionEnvironment
|
24
|
+
from ibm_watsonx_orchestrate.cli.commands.connections.connections_controller import export_connection
|
25
|
+
from ibm_watsonx_orchestrate.cli.commands.tools.tools_controller import ToolsController
|
26
|
+
from .types import *
|
27
|
+
|
28
|
+
APPLICATIONS_FILE_VERSION = '1.16.0'
|
29
|
+
|
30
|
+
|
31
|
+
logger = logging.getLogger(__name__)
|
32
|
+
|
33
|
+
def get_tool_bindings(tool_names: list[str]) -> dict[str, dict]:
|
34
|
+
"""
|
35
|
+
Return the raw binding (e.g. python function, connections, requirements)
|
36
|
+
for each tool name.
|
37
|
+
"""
|
38
|
+
tools_controller = ToolsController()
|
39
|
+
client = tools_controller.get_client()
|
40
|
+
|
41
|
+
results = {}
|
42
|
+
|
43
|
+
for name in tool_names:
|
44
|
+
draft_tools = client.get_draft_by_name(tool_name=name)
|
45
|
+
if not draft_tools:
|
46
|
+
logger.warning(f"No tool named {name} found")
|
47
|
+
continue
|
48
|
+
if len(draft_tools) > 1:
|
49
|
+
logger.warning(f"Multiple tools found with name {name}, using first")
|
50
|
+
|
51
|
+
draft_tool = draft_tools[0]
|
52
|
+
binding = draft_tool.get("binding", {})
|
53
|
+
results[name] = binding
|
54
|
+
|
55
|
+
return results
|
56
|
+
|
57
|
+
def _patch_agent_yamls(project_root: Path, publisher_name: str):
|
58
|
+
agents_dir = project_root / "agents"
|
59
|
+
if not agents_dir.exists():
|
60
|
+
return
|
61
|
+
|
62
|
+
for agent_yaml in agents_dir.glob("*.yaml"):
|
63
|
+
with open(agent_yaml, "r") as f:
|
64
|
+
agent_data = yaml.safe_load(f) or {}
|
65
|
+
|
66
|
+
if "tags" not in agent_data:
|
67
|
+
agent_data["tags"] = []
|
68
|
+
if "publisher" not in agent_data:
|
69
|
+
agent_data["publisher"] = publisher_name
|
70
|
+
if "language_support" not in agent_data:
|
71
|
+
agent_data["language_support"] = ["English"]
|
72
|
+
if "icon" not in agent_data:
|
73
|
+
agent_data["icon"] = "inline-svg-of-icon"
|
74
|
+
if "category" not in agent_data:
|
75
|
+
agent_data["category"] = "agent"
|
76
|
+
if "supported_apps" not in agent_data:
|
77
|
+
agent_data["supported_apps"] = []
|
78
|
+
|
79
|
+
with open(agent_yaml, "w") as f:
|
80
|
+
yaml.safe_dump(agent_data, f, sort_keys=False)
|
81
|
+
|
82
|
+
def _create_applications_entry(connection_config: dict) -> dict:
|
83
|
+
return {
|
84
|
+
'app_id': connection_config.get('app_id'),
|
85
|
+
'name': connection_config.get('catalog',{}).get('name','applications_file'),
|
86
|
+
'description': connection_config.get('catalog',{}).get('description',''),
|
87
|
+
'icon': connection_config.get('catalog',{}).get('icon','')
|
88
|
+
}
|
89
|
+
|
90
|
+
|
91
|
+
|
92
|
+
|
93
|
+
class PartnersOfferingController:
|
94
|
+
def __init__(self):
|
95
|
+
self.root = Path.cwd()
|
96
|
+
|
97
|
+
def get_native_client(self):
|
98
|
+
self.native_client = instantiate_client(AgentClient)
|
99
|
+
return self.native_client
|
100
|
+
|
101
|
+
def get_external_client(self):
|
102
|
+
self.native_client = instantiate_client(ExternalAgentClient)
|
103
|
+
return self.native_client
|
104
|
+
|
105
|
+
def get_tool_client(self):
|
106
|
+
self.tool_client = instantiate_client(ToolClient)
|
107
|
+
return self.tool_client
|
108
|
+
|
109
|
+
def _to_agent_kind(self, kind_str: str) -> AgentKind:
|
110
|
+
s = (kind_str or "").strip().lower()
|
111
|
+
if s in ("native", "agentkind.native"):
|
112
|
+
return AgentKind.NATIVE
|
113
|
+
if s in ("external", "agentkind.external"):
|
114
|
+
return AgentKind.EXTERNAL
|
115
|
+
logger.error(f"Agent kind '{kind_str}' is not currently supported. Expected 'native' or 'external'.")
|
116
|
+
sys.exit(1)
|
117
|
+
|
118
|
+
def create(self, offering: str, publisher_name: str, agent_type: str, agent_name: str):
|
119
|
+
# Create parent project folder
|
120
|
+
project_root = self.root / offering
|
121
|
+
|
122
|
+
# Check if the folder already exists — skip the whole thing
|
123
|
+
if project_root.exists():
|
124
|
+
logger.error(f"Offering folder '{offering}' already exists. Skipping creation.")
|
125
|
+
sys.exit(1)
|
126
|
+
|
127
|
+
project_root.mkdir(parents=True, exist_ok=True)
|
128
|
+
|
129
|
+
# Scaffold subfolders that aren’t provided by Agent export
|
130
|
+
for folder in [
|
131
|
+
project_root / "connections",
|
132
|
+
project_root / "offerings",
|
133
|
+
project_root / "evaluations",
|
134
|
+
]:
|
135
|
+
folder.mkdir(parents=True, exist_ok=True)
|
136
|
+
|
137
|
+
# Export the agent (includes tools + collaborators) to a temp zip-----------------------------------
|
138
|
+
output_zip = project_root / f"{offering}.zip" # drives top-level folder inside zip
|
139
|
+
agents_controller = AgentsController()
|
140
|
+
kind_enum = self._to_agent_kind(agent_type)
|
141
|
+
agents_controller.export_agent(
|
142
|
+
name=agent_name,
|
143
|
+
kind=kind_enum,
|
144
|
+
output_path=str(output_zip),
|
145
|
+
agent_only_flag=False,
|
146
|
+
with_tool_spec_file=True
|
147
|
+
)
|
148
|
+
|
149
|
+
# Unzip into project_root
|
150
|
+
with zipfile.ZipFile(output_zip, "r") as zf:
|
151
|
+
zf.extractall(project_root)
|
152
|
+
|
153
|
+
# Flatten "<offering>/" top-level from the zip into project_root
|
154
|
+
extracted_root = project_root / output_zip.stem
|
155
|
+
if extracted_root.exists() and extracted_root.is_dir():
|
156
|
+
for child in extracted_root.iterdir():
|
157
|
+
dest = project_root / child.name
|
158
|
+
|
159
|
+
# Special case: flatten away "agents/native" (or "agents/external")
|
160
|
+
if child.name == "agents":
|
161
|
+
agents_dir = project_root / "agents"
|
162
|
+
agents_dir.mkdir(exist_ok=True)
|
163
|
+
nested = child / kind_enum.value.lower()
|
164
|
+
if nested.exists() and nested.is_dir():
|
165
|
+
for agent_child in nested.iterdir():
|
166
|
+
shutil.move(str(agent_child), str(agents_dir))
|
167
|
+
shutil.rmtree(nested, ignore_errors=True)
|
168
|
+
continue
|
169
|
+
|
170
|
+
if dest.exists():
|
171
|
+
if dest.is_dir():
|
172
|
+
shutil.rmtree(dest)
|
173
|
+
else:
|
174
|
+
dest.unlink()
|
175
|
+
shutil.move(str(child), str(dest))
|
176
|
+
shutil.rmtree(extracted_root, ignore_errors=True)
|
177
|
+
|
178
|
+
# Remove the temp zip
|
179
|
+
output_zip.unlink(missing_ok=True)
|
180
|
+
|
181
|
+
# Patch the agent yamls with publisher, tags, icon, etc.
|
182
|
+
_patch_agent_yamls(project_root, publisher_name)
|
183
|
+
|
184
|
+
|
185
|
+
# Create offering.yaml file -------------------------------------------------------
|
186
|
+
native_client = self.get_native_client()
|
187
|
+
external_client = self.get_external_client()
|
188
|
+
|
189
|
+
existing_native_agents = native_client.get_draft_by_name(agent_name)
|
190
|
+
existing_native_agents = [Agent.model_validate(agent) for agent in existing_native_agents]
|
191
|
+
existing_external_clients = external_client.get_draft_by_name(agent_name)
|
192
|
+
existing_external_clients = [ExternalAgent.model_validate(agent) for agent in existing_external_clients]
|
193
|
+
|
194
|
+
all_existing_agents = existing_external_clients + existing_native_agents
|
195
|
+
|
196
|
+
if len(all_existing_agents) > 0:
|
197
|
+
existing_agent = all_existing_agents[0]
|
198
|
+
|
199
|
+
tool_client = self.get_tool_client()
|
200
|
+
tool_names = []
|
201
|
+
if hasattr(existing_agent,'tools') and existing_agent.tools:
|
202
|
+
matching_tools = tool_client.get_drafts_by_ids(existing_agent.tools)
|
203
|
+
tool_names = [tool['name'] for tool in matching_tools if 'name' in tool]
|
204
|
+
|
205
|
+
all_agents_names = []
|
206
|
+
all_agents_names.append(agent_name)
|
207
|
+
all_tools_names = []
|
208
|
+
all_tools_names.extend(tool_names)
|
209
|
+
|
210
|
+
if hasattr(existing_agent,'collaborators') and existing_agent.collaborators:
|
211
|
+
collaborator_agents = existing_agent.collaborators
|
212
|
+
for agent_id in collaborator_agents:
|
213
|
+
native_collaborator_agent = native_client.get_draft_by_id(agent_id)
|
214
|
+
external_collaborator_agent = external_client.get_draft_by_id(agent_id)
|
215
|
+
|
216
|
+
# collect names of collaborators
|
217
|
+
if native_collaborator_agent and "name" in native_collaborator_agent:
|
218
|
+
all_agents_names.append(native_collaborator_agent["name"])
|
219
|
+
if external_collaborator_agent and "name" in external_collaborator_agent:
|
220
|
+
all_agents_names.append(external_collaborator_agent["name"])
|
221
|
+
|
222
|
+
# collect tools of collaborators
|
223
|
+
collaborator_tool_ids = []
|
224
|
+
|
225
|
+
if native_collaborator_agent and "tools" in native_collaborator_agent:
|
226
|
+
collaborator_tool_ids.extend(native_collaborator_agent["tools"])
|
227
|
+
if external_collaborator_agent and "tools" in external_collaborator_agent:
|
228
|
+
collaborator_tool_ids.extend(external_collaborator_agent["tools"])
|
229
|
+
|
230
|
+
for tool_id in collaborator_tool_ids:
|
231
|
+
tool = tool_client.get_draft_by_id(tool_id)
|
232
|
+
if tool and "name" in tool:
|
233
|
+
all_tools_names.append(tool["name"])
|
234
|
+
|
235
|
+
if not existing_agent.display_name:
|
236
|
+
if hasattr(existing_agent,'title') and existing_agent.title:
|
237
|
+
existing_agent.display_name = existing_agent.title
|
238
|
+
elif hasattr(existing_agent,'nickname') and existing_agent.nickname:
|
239
|
+
existing_agent.display_name = existing_agent.nickname
|
240
|
+
else:
|
241
|
+
existing_agent.display_name = ""
|
242
|
+
|
243
|
+
offering_file = project_root / "offerings" / f"{offering}.yaml"
|
244
|
+
if not offering_file.exists():
|
245
|
+
offering = Offering(
|
246
|
+
name=agent_name,
|
247
|
+
display_name=existing_agent.display_name,
|
248
|
+
publisher=publisher_name,
|
249
|
+
description=existing_agent.description,
|
250
|
+
agents=all_agents_names,
|
251
|
+
tools=all_tools_names
|
252
|
+
)
|
253
|
+
offering_file.write_text(yaml.safe_dump(offering.model_dump(exclude_none=True), sort_keys=False))
|
254
|
+
logger.info("Successfully created Offerings yaml file.")
|
255
|
+
|
256
|
+
# Connection Yaml------------------------------------------------------------------
|
257
|
+
bindings = get_tool_bindings(all_tools_names)
|
258
|
+
seen_connections = set() # track only unique connections by app+conn_id
|
259
|
+
|
260
|
+
for _, binding in bindings.items():
|
261
|
+
if "python" in binding and "connections" in binding["python"]:
|
262
|
+
for app_id, conn_id in binding["python"]["connections"].items():
|
263
|
+
key = (app_id, conn_id)
|
264
|
+
if key in seen_connections:
|
265
|
+
continue
|
266
|
+
seen_connections.add(key)
|
267
|
+
|
268
|
+
conn_file = project_root / "connections" / f"{app_id}.yaml"
|
269
|
+
|
270
|
+
# Using connection Id instead of app_id because app_id has been sanitized in the binding
|
271
|
+
export_connection(connection_id=conn_id, output_file=conn_file)
|
272
|
+
|
273
|
+
|
274
|
+
def package(self, offering: str, folder_path: Optional[str] = None):
|
275
|
+
# Root folder
|
276
|
+
if folder_path:
|
277
|
+
root_folder = Path(folder_path)
|
278
|
+
else:
|
279
|
+
root_folder = Path.cwd()
|
280
|
+
|
281
|
+
if not root_folder.exists():
|
282
|
+
raise ValueError(f"Folder '{str(root_folder)}' does not exist")
|
283
|
+
|
284
|
+
project_root = root_folder / offering
|
285
|
+
|
286
|
+
# Resilience in case path to project folder is passed as root
|
287
|
+
if not project_root.exists() and str(root_folder).lower().endswith(offering.lower()):
|
288
|
+
project_root = Path(root_folder)
|
289
|
+
root_folder = Path(str(root_folder)[:-len(offering)])
|
290
|
+
|
291
|
+
offering_file = project_root / "offerings" / f"{offering}.yaml"
|
292
|
+
|
293
|
+
if not offering_file.exists():
|
294
|
+
raise FileNotFoundError(f"Offering file '{offering_file}' does not exist")
|
295
|
+
|
296
|
+
# Load offering data
|
297
|
+
with open(offering_file) as f:
|
298
|
+
offering_obj = Offering(**yaml.safe_load(f))
|
299
|
+
|
300
|
+
# Validate offering
|
301
|
+
offering_obj.validate_ready_for_packaging()
|
302
|
+
offering_data = offering_obj.model_dump()
|
303
|
+
|
304
|
+
publisher_name = offering_obj.publisher or "default_publisher"
|
305
|
+
zip_name = f"{offering}-{offering_obj.version}.zip"
|
306
|
+
zip_path = root_folder / zip_name # Zip created at root
|
307
|
+
|
308
|
+
|
309
|
+
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
310
|
+
top_level_folder = offering
|
311
|
+
|
312
|
+
# --- Add offering YAML as JSON ---
|
313
|
+
offering_json_path = f"{top_level_folder}/offerings/{offering}/config.json"
|
314
|
+
zf.writestr(offering_json_path, json.dumps(offering_data, indent=2))
|
315
|
+
|
316
|
+
# --- Add & validate agents ---
|
317
|
+
agents = offering_data.get("assets", {}).get(publisher_name, {}).get("agents", [])
|
318
|
+
for agent_name in agents:
|
319
|
+
agent_file = project_root / "agents" / f"{agent_name}.yaml"
|
320
|
+
if not agent_file.exists():
|
321
|
+
logger.error(f"Agent {agent_name} not found")
|
322
|
+
sys.exit(1)
|
323
|
+
|
324
|
+
with open(agent_file) as f:
|
325
|
+
agent_data = yaml.safe_load(f)
|
326
|
+
|
327
|
+
# Validate agent spec
|
328
|
+
agent_kind = agent_data.get("kind")
|
329
|
+
if agent_kind not in ("native", "external"):
|
330
|
+
logger.error(f"Agent {agent_name} has invalid kind: {agent_kind}")
|
331
|
+
sys.exit(1)
|
332
|
+
|
333
|
+
# Agent validation
|
334
|
+
match agent_kind:
|
335
|
+
case AgentKind.NATIVE:
|
336
|
+
agent_details = parse_create_native_args(
|
337
|
+
**agent_data
|
338
|
+
)
|
339
|
+
agent = Agent.model_validate(agent_details)
|
340
|
+
AgentsController().persist_record(agent=agent)
|
341
|
+
case AgentKind.EXTERNAL:
|
342
|
+
agent_details = parse_create_external_args(
|
343
|
+
**agent_data
|
344
|
+
)
|
345
|
+
agent = ExternalAgent.model_validate(agent_details)
|
346
|
+
|
347
|
+
agent_json_path = f"{top_level_folder}/agents/{agent_name}/config.json"
|
348
|
+
zf.writestr(agent_json_path, json.dumps(agent_data, indent=2))
|
349
|
+
|
350
|
+
# --- Add & validate tools ---
|
351
|
+
tools_client = instantiate_client(ToolClient)
|
352
|
+
tools = offering_data.get("assets", {}).get(publisher_name, {}).get("tools", [])
|
353
|
+
for tool_name in tools:
|
354
|
+
tool_dir = project_root / "tools" / tool_name
|
355
|
+
if not tool_dir.exists():
|
356
|
+
logger.error(f"Tool {tool_name} not found")
|
357
|
+
sys.exit(1)
|
358
|
+
|
359
|
+
spec_file = tool_dir / "config.json"
|
360
|
+
if not spec_file.exists():
|
361
|
+
logger.warning(f"No spec file found for tool '{tool_name}', checking orchestrate")
|
362
|
+
tool_data = tools_client.get_draft_by_name(tool_name)
|
363
|
+
if not tool_data or not len(tool_data):
|
364
|
+
logger.error(f"Unable to locate tool '{tool_name}' in current env")
|
365
|
+
sys.exit(1)
|
366
|
+
|
367
|
+
tool_data = ToolSpec.model_validate(tool_data[0]).model_dump(exclude_unset=True)
|
368
|
+
else:
|
369
|
+
with open(spec_file) as f:
|
370
|
+
tool_data = json.load(f)
|
371
|
+
|
372
|
+
# Validate tool
|
373
|
+
if not tool_data.get("binding",{}).get("python"):
|
374
|
+
logger.error(f"Tool {tool_name} is not a Python tool")
|
375
|
+
sys.exit(1)
|
376
|
+
if "name" not in tool_data or tool_data["name"] != tool_name:
|
377
|
+
logger.error(f"Tool {tool_name} has invalid or missing name in spec")
|
378
|
+
sys.exit(1)
|
379
|
+
|
380
|
+
# Write tool spec directly into zip
|
381
|
+
tool_zip_path = f"{top_level_folder}/tools/{tool_name}/config.json"
|
382
|
+
zf.writestr(tool_zip_path, json.dumps(tool_data, indent=2))
|
383
|
+
|
384
|
+
# --- Build artifact zip in-memory instead of source ---
|
385
|
+
artifact_zip_path = f"{top_level_folder}/tools/{tool_name}/attachments/{tool_name}.zip"
|
386
|
+
py_files = [p for p in tool_dir.glob("*.py")]
|
387
|
+
if py_files:
|
388
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
389
|
+
tmp_zip = Path(tmpdir) / f"{tool_name}.zip"
|
390
|
+
make_archive(str(tmp_zip.with_suffix('')), 'zip', root_dir=tool_dir, base_dir='.')
|
391
|
+
zf.write(tmp_zip, artifact_zip_path)
|
392
|
+
else:
|
393
|
+
logger.error(f"No Python files found for tool {tool_name}.")
|
394
|
+
sys.exit(1)
|
395
|
+
|
396
|
+
# --- Add & validate connections(applications) ---
|
397
|
+
applications_file_path = f"{top_level_folder}/applications/config.json"
|
398
|
+
applications_file_data = {
|
399
|
+
'name': 'applications_file',
|
400
|
+
'version': APPLICATIONS_FILE_VERSION,
|
401
|
+
'description': None
|
402
|
+
}
|
403
|
+
applications = []
|
404
|
+
|
405
|
+
connections_folder_path = project_root / "connections"
|
406
|
+
for connection_file in connections_folder_path.glob('*.yaml'):
|
407
|
+
with open(connection_file,"r") as f:
|
408
|
+
connection_data = yaml.safe_load(f)
|
409
|
+
applications.append(
|
410
|
+
_create_applications_entry(connection_data)
|
411
|
+
)
|
412
|
+
|
413
|
+
applications_file_data['applications'] = applications
|
414
|
+
|
415
|
+
zf.writestr(applications_file_path, json.dumps(applications_file_data, indent=2))
|
416
|
+
|
417
|
+
|
418
|
+
|
419
|
+
logger.info(f"Successfully packed Offering into {zip_path}")
|
420
|
+
|
421
|
+
|
422
|
+
|
423
|
+
|
424
|
+
|
425
|
+
|
426
|
+
|
427
|
+
|
428
|
+
|
429
|
+
|
430
|
+
|
431
|
+
|
432
|
+
|
433
|
+
|
434
|
+
|
435
|
+
|
436
|
+
|
437
|
+
|
438
|
+
|
439
|
+
|
440
|
+
|
441
|
+
|
442
|
+
|
443
|
+
|
444
|
+
|
445
|
+
|
446
|
+
|
447
|
+
|
448
|
+
|
449
|
+
|
450
|
+
|
451
|
+
|
452
|
+
|
453
|
+
|
454
|
+
|
455
|
+
|
456
|
+
|
457
|
+
|
458
|
+
|
@@ -0,0 +1,107 @@
|
|
1
|
+
from enum import Enum
|
2
|
+
from typing import Optional
|
3
|
+
import logging
|
4
|
+
|
5
|
+
from pydantic import BaseModel, model_validator
|
6
|
+
|
7
|
+
logger = logging.getLogger(__name__)
|
8
|
+
|
9
|
+
CATALOG_PLACEHOLDERS = {
|
10
|
+
'domain' : 'HR',
|
11
|
+
'version' : '1.0',
|
12
|
+
'part_number': 'my-part-number',
|
13
|
+
'form_factor': 'free',
|
14
|
+
'tenant_type': {
|
15
|
+
'trial': 'free'
|
16
|
+
}
|
17
|
+
}
|
18
|
+
|
19
|
+
CATALOG_ONLY_FIELDS = [
|
20
|
+
'publisher',
|
21
|
+
'language_support',
|
22
|
+
'icon',
|
23
|
+
'category',
|
24
|
+
'supported_apps'
|
25
|
+
]
|
26
|
+
|
27
|
+
class AgentKind(str, Enum):
|
28
|
+
NATIVE = "native"
|
29
|
+
EXTERNAL = "external"
|
30
|
+
|
31
|
+
def __str__(self):
|
32
|
+
return self.value
|
33
|
+
|
34
|
+
def __repr__(self):
|
35
|
+
return repr(self.value)
|
36
|
+
|
37
|
+
class OfferingFormFactor(BaseModel):
|
38
|
+
aws: Optional[str] = CATALOG_PLACEHOLDERS['form_factor']
|
39
|
+
ibm_cloud: Optional[str] = CATALOG_PLACEHOLDERS['form_factor']
|
40
|
+
cp4d: Optional[str] = CATALOG_PLACEHOLDERS['form_factor']
|
41
|
+
|
42
|
+
class OfferingPartNumber(BaseModel):
|
43
|
+
aws: Optional[str] = CATALOG_PLACEHOLDERS['part_number']
|
44
|
+
ibm_cloud: Optional[str] = CATALOG_PLACEHOLDERS['part_number']
|
45
|
+
cp4d: Optional[str] = None
|
46
|
+
|
47
|
+
class OfferingScope(BaseModel):
|
48
|
+
form_factor: Optional[OfferingFormFactor] = OfferingFormFactor()
|
49
|
+
tenant_type: Optional[dict] = CATALOG_PLACEHOLDERS['tenant_type']
|
50
|
+
|
51
|
+
class Offering(BaseModel):
|
52
|
+
name: str
|
53
|
+
display_name: str
|
54
|
+
domain: Optional[str] = CATALOG_PLACEHOLDERS['domain']
|
55
|
+
publisher: str
|
56
|
+
version: Optional[str] = CATALOG_PLACEHOLDERS['version']
|
57
|
+
description: str
|
58
|
+
assets: dict
|
59
|
+
part_number: Optional[OfferingPartNumber] = OfferingPartNumber()
|
60
|
+
scope: Optional[OfferingScope] = OfferingScope()
|
61
|
+
|
62
|
+
def __init__(self, *args, **kwargs):
|
63
|
+
# set asset details
|
64
|
+
if not kwargs.get('assets'):
|
65
|
+
kwargs['assets'] = {
|
66
|
+
kwargs.get('publisher','default_publisher'): {
|
67
|
+
"agents": kwargs.get('agents',[]),
|
68
|
+
"tools": kwargs.get('tools',[])
|
69
|
+
}
|
70
|
+
}
|
71
|
+
super().__init__(**kwargs)
|
72
|
+
|
73
|
+
@model_validator(mode="before")
|
74
|
+
def validate_values(cls,values):
|
75
|
+
publisher = values.get('publisher')
|
76
|
+
if not publisher:
|
77
|
+
raise ValueError(f"An offering cannot be packaged without a publisher")
|
78
|
+
|
79
|
+
assets = values.get('assets')
|
80
|
+
if not assets or not assets.get(publisher):
|
81
|
+
raise ValueError(f"An offering cannot be packaged without assets")
|
82
|
+
|
83
|
+
agents = assets.get(publisher).get('agents')
|
84
|
+
if not agents:
|
85
|
+
raise ValueError(f"An offering requires at least one agent to be provided")
|
86
|
+
|
87
|
+
return values
|
88
|
+
|
89
|
+
def validate_ready_for_packaging(self):
|
90
|
+
self.test_for_placeholder_values()
|
91
|
+
|
92
|
+
def test_for_placeholder_values(self):
|
93
|
+
placholders = False
|
94
|
+
# part numbers
|
95
|
+
if not self.part_number:
|
96
|
+
raise ValueError(f"Offering '{self.name}' does not have valid part numbers")
|
97
|
+
|
98
|
+
for (k,v) in self.part_number.model_dump().items():
|
99
|
+
if v == CATALOG_PLACEHOLDERS['part_number']:
|
100
|
+
logger.warning(f"Placeholder part number detected for platform '{k}', please ensure valid part numbers are entered before packaging.")
|
101
|
+
placholders = True
|
102
|
+
|
103
|
+
if placholders:
|
104
|
+
raise ValueError(f"Offering '{self.name}' cannot be packaged with placeholder values")
|
105
|
+
|
106
|
+
|
107
|
+
|
@@ -0,0 +1,12 @@
|
|
1
|
+
import typer
|
2
|
+
|
3
|
+
from ibm_watsonx_orchestrate.cli.commands.partners import partners_controller
|
4
|
+
from ibm_watsonx_orchestrate.cli.commands.partners.offering.partners_offering_command import partners_offering
|
5
|
+
|
6
|
+
partners_app = typer.Typer(no_args_is_help=True)
|
7
|
+
|
8
|
+
partners_app.add_typer(
|
9
|
+
partners_offering,
|
10
|
+
name="offering",
|
11
|
+
help="Tools for partners to create and package offerings"
|
12
|
+
)
|
File without changes
|