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