ibm-watsonx-orchestrate 1.11.0b0__py3-none-any.whl → 1.12.0b0__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 (47) hide show
  1. ibm_watsonx_orchestrate/__init__.py +1 -1
  2. ibm_watsonx_orchestrate/agent_builder/agents/types.py +30 -0
  3. ibm_watsonx_orchestrate/agent_builder/connections/connections.py +8 -5
  4. ibm_watsonx_orchestrate/agent_builder/connections/types.py +14 -0
  5. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +25 -10
  6. ibm_watsonx_orchestrate/agent_builder/tools/__init__.py +1 -0
  7. ibm_watsonx_orchestrate/agent_builder/tools/langflow_tool.py +124 -0
  8. ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +3 -3
  9. ibm_watsonx_orchestrate/agent_builder/tools/types.py +20 -2
  10. ibm_watsonx_orchestrate/cli/commands/agents/agents_command.py +10 -2
  11. ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +421 -177
  12. ibm_watsonx_orchestrate/cli/commands/connections/connections_command.py +18 -0
  13. ibm_watsonx_orchestrate/cli/commands/connections/connections_controller.py +114 -0
  14. ibm_watsonx_orchestrate/cli/commands/copilot/copilot_server_controller.py +24 -91
  15. ibm_watsonx_orchestrate/cli/commands/environment/environment_command.py +1 -1
  16. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_command.py +223 -2
  17. ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_controller.py +93 -9
  18. ibm_watsonx_orchestrate/cli/commands/models/models_controller.py +3 -3
  19. ibm_watsonx_orchestrate/cli/commands/partners/offering/partners_offering_command.py +56 -0
  20. ibm_watsonx_orchestrate/cli/commands/partners/offering/partners_offering_controller.py +458 -0
  21. ibm_watsonx_orchestrate/cli/commands/partners/offering/types.py +107 -0
  22. ibm_watsonx_orchestrate/cli/commands/partners/partners_command.py +12 -0
  23. ibm_watsonx_orchestrate/cli/commands/partners/partners_controller.py +0 -0
  24. ibm_watsonx_orchestrate/cli/commands/server/server_command.py +114 -635
  25. ibm_watsonx_orchestrate/cli/commands/server/types.py +1 -1
  26. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_controller.py +2 -2
  27. ibm_watsonx_orchestrate/cli/commands/tools/tools_command.py +2 -3
  28. ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +206 -43
  29. ibm_watsonx_orchestrate/cli/main.py +2 -0
  30. ibm_watsonx_orchestrate/client/base_api_client.py +31 -10
  31. ibm_watsonx_orchestrate/client/connections/connections_client.py +18 -1
  32. ibm_watsonx_orchestrate/client/service_instance.py +19 -34
  33. ibm_watsonx_orchestrate/client/tools/tempus_client.py +3 -0
  34. ibm_watsonx_orchestrate/client/tools/tool_client.py +5 -2
  35. ibm_watsonx_orchestrate/client/utils.py +34 -2
  36. ibm_watsonx_orchestrate/docker/compose-lite.yml +14 -12
  37. ibm_watsonx_orchestrate/docker/default.env +17 -17
  38. ibm_watsonx_orchestrate/flow_builder/flows/flow.py +3 -1
  39. ibm_watsonx_orchestrate/flow_builder/types.py +252 -1
  40. ibm_watsonx_orchestrate/utils/docker_utils.py +280 -0
  41. ibm_watsonx_orchestrate/utils/environment.py +369 -0
  42. ibm_watsonx_orchestrate/utils/utils.py +1 -1
  43. {ibm_watsonx_orchestrate-1.11.0b0.dist-info → ibm_watsonx_orchestrate-1.12.0b0.dist-info}/METADATA +2 -2
  44. {ibm_watsonx_orchestrate-1.11.0b0.dist-info → ibm_watsonx_orchestrate-1.12.0b0.dist-info}/RECORD +47 -39
  45. {ibm_watsonx_orchestrate-1.11.0b0.dist-info → ibm_watsonx_orchestrate-1.12.0b0.dist-info}/WHEEL +0 -0
  46. {ibm_watsonx_orchestrate-1.11.0b0.dist-info → ibm_watsonx_orchestrate-1.12.0b0.dist-info}/entry_points.txt +0 -0
  47. {ibm_watsonx_orchestrate-1.11.0b0.dist-info → ibm_watsonx_orchestrate-1.12.0b0.dist-info}/licenses/LICENSE +0 -0
@@ -1,17 +1,23 @@
1
1
  import logging
2
2
  import os.path
3
3
  from typing import List, Dict, Optional, Tuple
4
+ from enum import StrEnum
4
5
  import csv
5
6
  from pathlib import Path
6
7
  import sys
7
8
  from wxo_agentic_evaluation import main as evaluate
9
+ from wxo_agentic_evaluation import quick_eval
8
10
  from wxo_agentic_evaluation.tool_planner import build_snapshot
9
- from wxo_agentic_evaluation.analyze_run import analyze
11
+ from wxo_agentic_evaluation.analyze_run import Analyzer
10
12
  from wxo_agentic_evaluation.batch_annotate import generate_test_cases_from_stories
11
- from wxo_agentic_evaluation.arg_configs import TestConfig, AuthConfig, LLMUserConfig, ChatRecordingConfig, AnalyzeConfig, ProviderConfig
13
+ from wxo_agentic_evaluation.arg_configs import TestConfig, AuthConfig, LLMUserConfig, ChatRecordingConfig, AnalyzeConfig, ProviderConfig, AttackConfig, QuickEvalConfig
12
14
  from wxo_agentic_evaluation.record_chat import record_chats
13
15
  from wxo_agentic_evaluation.external_agent.external_validate import ExternalAgentValidation
14
16
  from wxo_agentic_evaluation.external_agent.performance_test import ExternalAgentPerformanceTest
17
+ from wxo_agentic_evaluation.red_teaming.attack_list import print_attacks
18
+ from wxo_agentic_evaluation.red_teaming import attack_generator
19
+ from wxo_agentic_evaluation.red_teaming.attack_runner import run_attacks
20
+ from wxo_agentic_evaluation.arg_configs import AttackGeneratorConfig
15
21
  from ibm_watsonx_orchestrate import __version__
16
22
  from ibm_watsonx_orchestrate.cli.config import Config, ENV_WXO_URL_OPT, AUTH_CONFIG_FILE, AUTH_CONFIG_FILE_FOLDER, AUTH_SECTION_HEADER, AUTH_MCSP_TOKEN_OPT
17
23
  from ibm_watsonx_orchestrate.utils.utils import yaml_safe_load
@@ -21,6 +27,9 @@ import uuid
21
27
 
22
28
  logger = logging.getLogger(__name__)
23
29
 
30
+ class EvaluateMode(StrEnum):
31
+ default = "default" # referenceFUL evaluation
32
+ referenceless = "referenceless"
24
33
 
25
34
  class EvaluationsController:
26
35
  def __init__(self):
@@ -38,7 +47,7 @@ class EvaluationsController:
38
47
 
39
48
  return url, tenant_name, token
40
49
 
41
- def evaluate(self, config_file: Optional[str] = None, test_paths: Optional[str] = None, output_dir: Optional[str] = None) -> None:
50
+ def evaluate(self, config_file: Optional[str] = None, test_paths: Optional[str] = None, output_dir: Optional[str] = None, tools_path: str = None, mode: str = EvaluateMode.default) -> None:
42
51
  url, tenant_name, token = self._get_env_config()
43
52
 
44
53
  if "WATSONX_SPACE_ID" in os.environ and "WATSONX_APIKEY" in os.environ:
@@ -90,9 +99,13 @@ class EvaluationsController:
90
99
  config_data["output_dir"] = output_dir
91
100
  logger.info(f"Using output directory: {config_data['output_dir']}")
92
101
 
93
- config = TestConfig(**config_data)
94
-
95
- evaluate.main(config)
102
+ if mode == EvaluateMode.default:
103
+ config = TestConfig(**config_data)
104
+ evaluate.main(config)
105
+ elif mode == EvaluateMode.referenceless:
106
+ config_data["tools_path"] = tools_path
107
+ config = QuickEvalConfig(**config_data)
108
+ quick_eval.main(config)
96
109
 
97
110
  def record(self, output_dir) -> None:
98
111
 
@@ -160,9 +173,13 @@ class EvaluationsController:
160
173
 
161
174
  logger.info("Test cases stored at: %s", output_dir)
162
175
 
163
- def analyze(self, data_path: str) -> None:
164
- config = AnalyzeConfig(data_path=data_path)
165
- analyze(config)
176
+ def analyze(self, data_path: str, tool_definition_path: str) -> None:
177
+ config = AnalyzeConfig(
178
+ data_path=data_path,
179
+ tool_definition_path=tool_definition_path
180
+ )
181
+ analyzer = Analyzer()
182
+ analyzer.analyze(config)
166
183
 
167
184
  def summarize(self) -> None:
168
185
  pass
@@ -187,3 +204,70 @@ class EvaluationsController:
187
204
  generated_performance_tests = performance_test.generate_tests()
188
205
 
189
206
  return generated_performance_tests
207
+
208
+ def list_red_teaming_attacks(self):
209
+ print_attacks()
210
+
211
+ def generate_red_teaming_attacks(
212
+ self,
213
+ attacks_list: str,
214
+ datasets_path: str,
215
+ agents_path: str,
216
+ target_agent_name: str,
217
+ output_dir: Optional[str] = None,
218
+ max_variants: Optional[int] = None,
219
+ ):
220
+ if output_dir is None:
221
+ output_dir = os.path.join(os.getcwd(), "red_teaming_attacks")
222
+ os.makedirs(output_dir, exist_ok=True)
223
+ logger.info(f"No output directory specified. Using default: {output_dir}")
224
+
225
+ results = attack_generator.main(
226
+ AttackGeneratorConfig(
227
+ attacks_list=attacks_list.split(","),
228
+ datasets_path=datasets_path.split(","),
229
+ agents_path=agents_path,
230
+ target_agent_name=target_agent_name,
231
+ output_dir=output_dir,
232
+ max_variants=max_variants,
233
+ )
234
+ )
235
+ logger.info(f"Generated {len(results)} attacks and saved to {output_dir}")
236
+
237
+ def run_red_teaming_attacks(self, attack_paths: str, output_dir: Optional[str] = None) -> None:
238
+ url, tenant_name, token = self._get_env_config()
239
+
240
+ if "WATSONX_SPACE_ID" in os.environ and "WATSONX_APIKEY" in os.environ:
241
+ provider = "watsonx"
242
+ elif "WO_INSTANCE" in os.environ and "WO_API_KEY" in os.environ:
243
+ provider = "model_proxy"
244
+ else:
245
+ logger.error(
246
+ "No provider found. Please either provide a config_file or set either WATSONX_SPACE_ID and WATSONX_APIKEY or WO_INSTANCE and WO_API_KEY in your system environment variables."
247
+ )
248
+ sys.exit(1)
249
+
250
+ config_data = {
251
+ "auth_config": AuthConfig(
252
+ url=url,
253
+ tenant_name=tenant_name,
254
+ token=token,
255
+ ),
256
+ "provider_config": ProviderConfig(
257
+ provider=provider,
258
+ model_id="meta-llama/llama-3-405b-instruct",
259
+ ),
260
+ }
261
+
262
+ config_data["attack_paths"] = attack_paths.split(",")
263
+ if output_dir:
264
+ config_data["output_dir"] = output_dir
265
+ else:
266
+ config_data["output_dir"] = os.path.join(os.getcwd(), "red_teaming_results")
267
+ os.makedirs(config_data["output_dir"], exist_ok=True)
268
+ logger.info(f"No output directory specified. Using default: {config_data['output_dir']}")
269
+
270
+
271
+ config = AttackConfig(**config_data)
272
+
273
+ run_attacks(config)
@@ -12,7 +12,6 @@ import requests
12
12
  import rich
13
13
  import rich.highlighter
14
14
 
15
- from ibm_watsonx_orchestrate.cli.commands.server.server_command import get_default_env_file, merge_env
16
15
  from ibm_watsonx_orchestrate.client.model_policies.model_policies_client import ModelPoliciesClient
17
16
  from ibm_watsonx_orchestrate.agent_builder.model_policies.types import ModelPolicy, ModelPolicyInner, \
18
17
  ModelPolicyRetry, ModelPolicyStrategy, ModelPolicyStrategyMode, ModelPolicyTarget
@@ -20,6 +19,7 @@ from ibm_watsonx_orchestrate.client.models.models_client import ModelsClient
20
19
  from ibm_watsonx_orchestrate.agent_builder.models.types import VirtualModel, ProviderConfig, ModelType, ANTHROPIC_DEFAULT_MAX_TOKENS
21
20
  from ibm_watsonx_orchestrate.client.utils import instantiate_client, is_cpd_env
22
21
  from ibm_watsonx_orchestrate.client.connections import get_connection_id, ConnectionType
22
+ from ibm_watsonx_orchestrate.utils.environment import EnvService
23
23
 
24
24
  logger = logging.getLogger(__name__)
25
25
 
@@ -153,8 +153,8 @@ class ModelsController:
153
153
  models_client: ModelsClient = self.get_models_client()
154
154
  model_policies_client: ModelPoliciesClient = self.get_model_policies_client()
155
155
  global WATSONX_URL
156
- default_env_path = get_default_env_file()
157
- merged_env_dict = merge_env(
156
+ default_env_path = EnvService.get_default_env_file()
157
+ merged_env_dict = EnvService.merge_env(
158
158
  default_env_path,
159
159
  None
160
160
  )
@@ -0,0 +1,56 @@
1
+ import typer
2
+ from typing_extensions import Annotated
3
+ from ibm_watsonx_orchestrate.cli.commands.partners.offering.partners_offering_controller import PartnersOfferingController
4
+ from typing_extensions import Annotated
5
+ from ibm_watsonx_orchestrate.cli.commands.partners.offering.types import AgentKind
6
+
7
+
8
+ partners_offering = typer.Typer(no_args_is_help=True)
9
+
10
+ @partners_offering.command(
11
+ name="create",
12
+ help="Export Items from your environment to create an offering"
13
+ )
14
+ def create_offering(
15
+ offering: Annotated[
16
+ str,
17
+ typer.Option("--offering", "-o", help="Name of the offering"),
18
+ ],
19
+ publisher_name: Annotated[
20
+ str,
21
+ typer.Option("--publisher", "-p", help="Publisher name"),
22
+ ],
23
+ type: Annotated[
24
+ AgentKind,
25
+ typer.Option("--type", "-t", help="Type of agent: native|external"),
26
+ ],
27
+ agent_name: Annotated[
28
+ str,
29
+ typer.Option("--agent-name", "-a", help="Agent name to create"),
30
+ ],
31
+ ):
32
+ controller = PartnersOfferingController()
33
+ controller.create(
34
+ offering=offering,
35
+ publisher_name=publisher_name,
36
+ agent_type=type,
37
+ agent_name=agent_name,
38
+ )
39
+
40
+
41
+ @partners_offering.command(
42
+ name="package",
43
+ help="Validate your exported offering and package for upload"
44
+ )
45
+ def package_offering(
46
+ offering: Annotated[
47
+ str,
48
+ typer.Option("--offering", "-o", help="Name of the offering to package"),
49
+ ],
50
+ folder_path: Annotated[
51
+ str,
52
+ typer.Option("--folder", "-f", help="Path to folder containing the specified offering")
53
+ ] = None
54
+ ):
55
+ controller = PartnersOfferingController()
56
+ controller.package(offering=offering, folder_path=folder_path)
@@ -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
+