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.
Files changed (50) hide show
  1. ibm_watsonx_orchestrate/__init__.py +1 -2
  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 +124 -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/evaluations/evaluations_command.py +49 -0
  19. ibm_watsonx_orchestrate/cli/commands/models/model_provider_mapper.py +23 -4
  20. ibm_watsonx_orchestrate/cli/commands/models/models_controller.py +3 -3
  21. ibm_watsonx_orchestrate/cli/commands/partners/offering/partners_offering_command.py +56 -0
  22. ibm_watsonx_orchestrate/cli/commands/partners/offering/partners_offering_controller.py +458 -0
  23. ibm_watsonx_orchestrate/cli/commands/partners/offering/types.py +107 -0
  24. ibm_watsonx_orchestrate/cli/commands/partners/partners_command.py +12 -0
  25. ibm_watsonx_orchestrate/cli/commands/partners/partners_controller.py +0 -0
  26. ibm_watsonx_orchestrate/cli/commands/server/server_command.py +124 -637
  27. ibm_watsonx_orchestrate/cli/commands/server/types.py +1 -1
  28. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_command.py +2 -2
  29. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_controller.py +2 -2
  30. ibm_watsonx_orchestrate/cli/commands/tools/tools_command.py +2 -3
  31. ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +206 -43
  32. ibm_watsonx_orchestrate/cli/main.py +2 -0
  33. ibm_watsonx_orchestrate/client/connections/connections_client.py +4 -1
  34. ibm_watsonx_orchestrate/client/tools/tempus_client.py +3 -0
  35. ibm_watsonx_orchestrate/client/tools/tool_client.py +5 -2
  36. ibm_watsonx_orchestrate/client/utils.py +31 -1
  37. ibm_watsonx_orchestrate/docker/compose-lite.yml +68 -17
  38. ibm_watsonx_orchestrate/docker/default.env +21 -18
  39. ibm_watsonx_orchestrate/flow_builder/flows/decorators.py +8 -2
  40. ibm_watsonx_orchestrate/flow_builder/flows/flow.py +31 -7
  41. ibm_watsonx_orchestrate/flow_builder/node.py +1 -1
  42. ibm_watsonx_orchestrate/flow_builder/types.py +18 -3
  43. ibm_watsonx_orchestrate/utils/docker_utils.py +280 -0
  44. ibm_watsonx_orchestrate/utils/environment.py +369 -0
  45. ibm_watsonx_orchestrate/utils/utils.py +1 -1
  46. {ibm_watsonx_orchestrate-1.11.0b1.dist-info → ibm_watsonx_orchestrate-1.12.0b1.dist-info}/METADATA +2 -2
  47. {ibm_watsonx_orchestrate-1.11.0b1.dist-info → ibm_watsonx_orchestrate-1.12.0b1.dist-info}/RECORD +50 -42
  48. {ibm_watsonx_orchestrate-1.11.0b1.dist-info → ibm_watsonx_orchestrate-1.12.0b1.dist-info}/WHEEL +0 -0
  49. {ibm_watsonx_orchestrate-1.11.0b1.dist-info → ibm_watsonx_orchestrate-1.12.0b1.dist-info}/entry_points.txt +0 -0
  50. {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
+ )