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.
- ibm_watsonx_orchestrate/__init__.py +1 -1
- ibm_watsonx_orchestrate/agent_builder/agents/types.py +30 -0
- ibm_watsonx_orchestrate/agent_builder/connections/connections.py +8 -5
- ibm_watsonx_orchestrate/agent_builder/connections/types.py +14 -0
- ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +25 -10
- ibm_watsonx_orchestrate/agent_builder/tools/__init__.py +1 -0
- ibm_watsonx_orchestrate/agent_builder/tools/langflow_tool.py +124 -0
- ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +3 -3
- ibm_watsonx_orchestrate/agent_builder/tools/types.py +20 -2
- ibm_watsonx_orchestrate/cli/commands/agents/agents_command.py +10 -2
- ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +421 -177
- 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_server_controller.py +24 -91
- ibm_watsonx_orchestrate/cli/commands/environment/environment_command.py +1 -1
- ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_command.py +223 -2
- ibm_watsonx_orchestrate/cli/commands/evaluations/evaluations_controller.py +93 -9
- 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 +114 -635
- ibm_watsonx_orchestrate/cli/commands/server/types.py +1 -1
- 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/base_api_client.py +31 -10
- ibm_watsonx_orchestrate/client/connections/connections_client.py +18 -1
- ibm_watsonx_orchestrate/client/service_instance.py +19 -34
- 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 +34 -2
- ibm_watsonx_orchestrate/docker/compose-lite.yml +14 -12
- ibm_watsonx_orchestrate/docker/default.env +17 -17
- ibm_watsonx_orchestrate/flow_builder/flows/flow.py +3 -1
- ibm_watsonx_orchestrate/flow_builder/types.py +252 -1
- 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.0b0.dist-info → ibm_watsonx_orchestrate-1.12.0b0.dist-info}/METADATA +2 -2
- {ibm_watsonx_orchestrate-1.11.0b0.dist-info → ibm_watsonx_orchestrate-1.12.0b0.dist-info}/RECORD +47 -39
- {ibm_watsonx_orchestrate-1.11.0b0.dist-info → ibm_watsonx_orchestrate-1.12.0b0.dist-info}/WHEEL +0 -0
- {ibm_watsonx_orchestrate-1.11.0b0.dist-info → ibm_watsonx_orchestrate-1.12.0b0.dist-info}/entry_points.txt +0 -0
- {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
|
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
|
-
|
94
|
-
|
95
|
-
|
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(
|
165
|
-
|
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
|
+
|