weni-cli 3.7.0a0__tar.gz → 3.7.2__tar.gz
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.
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/PKG-INFO +2 -2
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/pyproject.toml +2 -2
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/cli.py +9 -2
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/clients/cli_client.py +16 -4
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/commands/project_push.py +39 -9
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/commands/run.py +14 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/formatter/formatter.py +11 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/formatter/tests/test_formatter.py +18 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/validators/agent_definition.py +23 -0
- weni_cli-3.7.2/weni_cli/validators/source.py +134 -0
- weni_cli-3.7.2/weni_cli/validators/tests/conftest.py +19 -0
- weni_cli-3.7.2/weni_cli/validators/tests/test_source.py +156 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/README.md +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/__init__.py +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/auth.py +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/clients/__init__.py +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/clients/common.py +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/clients/response_handlers/__init__.py +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/clients/response_handlers/handlers.py +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/clients/tests/__init__.py +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/clients/tests/test_cli_client.py +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/clients/tests/test_weni_client.py +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/clients/weni_client.py +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/commands/channel_create.py +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/commands/eval_init.py +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/commands/eval_run.py +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/commands/init.py +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/commands/login.py +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/commands/logs.py +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/commands/project_current.py +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/commands/project_list.py +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/commands/project_use.py +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/formatter/__init__.py +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/handler.py +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/packager/__init__.py +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/packager/loader.py +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/packager/packager.py +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/packager/tests/__init__.py +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/packager/tests/test_loader.py +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/packager/tests/test_packager.py +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/spinner/__init__.py +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/spinner/spinners.py +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/store.py +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/utils.py +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/validators/__init__.py +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/validators/channel_definition.py +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/validators/tests/__init__.py +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/validators/tests/test_active_test_definition.py +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/validators/tests/test_channel_definition.py +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/validators/tests/test_definition.py +0 -0
- {weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/wsgi.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: weni-cli
|
|
3
|
-
Version: 3.7.
|
|
3
|
+
Version: 3.7.2
|
|
4
4
|
Summary:
|
|
5
5
|
Author: Paulo Bernardo
|
|
6
6
|
Author-email: paulo.bernardo@weni.ai
|
|
@@ -20,7 +20,7 @@ Requires-Dist: rich (>=13.9.4,<14.0.0)
|
|
|
20
20
|
Requires-Dist: rich-click (>=1.8.6,<2.0.0)
|
|
21
21
|
Requires-Dist: waitress (>=3.0.2,<4.0.0)
|
|
22
22
|
Requires-Dist: weni-agenteval (>=1.1.0,<2.0.0)
|
|
23
|
-
Requires-Dist: weni-agents-toolkit (==2.6.
|
|
23
|
+
Requires-Dist: weni-agents-toolkit (==2.6.4)
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
25
|
|
|
26
26
|
# Weni-CLI
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "weni-cli"
|
|
3
|
-
version = "3.7.
|
|
3
|
+
version = "3.7.2"
|
|
4
4
|
description = ""
|
|
5
5
|
authors = ["Paulo Bernardo <paulo.bernardo@weni.ai>", "Matheus Leal <matheus.cardoso@vtex.com>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -14,7 +14,7 @@ waitress = "^3.0.2"
|
|
|
14
14
|
pyyaml = "^6.0.2"
|
|
15
15
|
python-slugify = "^8.0.4"
|
|
16
16
|
regex = "^2024.11.6"
|
|
17
|
-
weni-agents-toolkit = "2.6.
|
|
17
|
+
weni-agents-toolkit = "2.6.4"
|
|
18
18
|
rich-click = "^1.8.6"
|
|
19
19
|
rich = "^13.9.4"
|
|
20
20
|
weni-agenteval = "^1.1.0"
|
|
@@ -101,7 +101,9 @@ def current_project():
|
|
|
101
101
|
@project.command("push")
|
|
102
102
|
@click.argument("definition", required=True, type=click.Path(exists=True, dir_okay=False))
|
|
103
103
|
@click.option("--force-update", is_flag=True, help="Force update to the project")
|
|
104
|
-
|
|
104
|
+
@click.option("--use-apm", is_flag=True, help="Enable Elastic APM instrumentation for passive agent tool lambdas")
|
|
105
|
+
@click.option("--remove-apm", is_flag=True, help="Remove Elastic APM instrumentation from passive agent tool lambdas")
|
|
106
|
+
def push_project(definition, force_update, use_apm, remove_apm):
|
|
105
107
|
"""Push an Agent definition to the current project
|
|
106
108
|
|
|
107
109
|
DEFINITION: The path to the YAML agent definition file
|
|
@@ -109,7 +111,12 @@ def push_project(definition, force_update):
|
|
|
109
111
|
from weni_cli.commands.project_push import ProjectPushHandler
|
|
110
112
|
|
|
111
113
|
try:
|
|
112
|
-
ProjectPushHandler().execute(
|
|
114
|
+
ProjectPushHandler().execute(
|
|
115
|
+
definition=definition,
|
|
116
|
+
force_update=force_update,
|
|
117
|
+
use_apm=use_apm,
|
|
118
|
+
remove_apm=remove_apm,
|
|
119
|
+
)
|
|
113
120
|
except Exception as e:
|
|
114
121
|
click.echo(f"Error: {e}")
|
|
115
122
|
|
|
@@ -50,15 +50,22 @@ def get_cli_version() -> str:
|
|
|
50
50
|
return importlib.metadata.version("weni-cli")
|
|
51
51
|
|
|
52
52
|
|
|
53
|
-
def create_default_payload(
|
|
53
|
+
def create_default_payload(
|
|
54
|
+
project_uuid: str, definition: Dict, agent_type: str, apm_instrumentation: Optional[str] = None
|
|
55
|
+
) -> Dict[str, str]:
|
|
54
56
|
"""Create a default payload for API requests."""
|
|
55
|
-
|
|
57
|
+
payload = {
|
|
56
58
|
"project_uuid": project_uuid,
|
|
57
59
|
"definition": json.dumps(definition, ensure_ascii=False),
|
|
58
60
|
"toolkit_version": get_toolkit_version(),
|
|
59
61
|
"type": agent_type,
|
|
60
62
|
}
|
|
61
63
|
|
|
64
|
+
if apm_instrumentation:
|
|
65
|
+
payload["apm_instrumentation"] = apm_instrumentation
|
|
66
|
+
|
|
67
|
+
return payload
|
|
68
|
+
|
|
62
69
|
|
|
63
70
|
class CLIClient:
|
|
64
71
|
"""Client for interacting with the Weni CLI API."""
|
|
@@ -179,10 +186,15 @@ class CLIClient:
|
|
|
179
186
|
raise RequestError(f"Failed to check project permission: {e.message}")
|
|
180
187
|
|
|
181
188
|
def push_agents(
|
|
182
|
-
self,
|
|
189
|
+
self,
|
|
190
|
+
project_uuid: str,
|
|
191
|
+
agents_definition: Dict,
|
|
192
|
+
resources_folder: Dict[str, BinaryIO],
|
|
193
|
+
agent_type: str,
|
|
194
|
+
apm_instrumentation: Optional[str] = None,
|
|
183
195
|
) -> None:
|
|
184
196
|
"""Push agents to the API."""
|
|
185
|
-
data = create_default_payload(project_uuid, agents_definition, agent_type)
|
|
197
|
+
data = create_default_payload(project_uuid, agents_definition, agent_type, apm_instrumentation)
|
|
186
198
|
|
|
187
199
|
with spinner():
|
|
188
200
|
try:
|
|
@@ -19,12 +19,36 @@ from weni_cli.validators.agent_definition import (
|
|
|
19
19
|
|
|
20
20
|
CONTACT_FIELD_NAME_REGEX = r"^[a-z][a-z0-9_]*$"
|
|
21
21
|
|
|
22
|
+
APM_OBSERVABILITY_WARNING = (
|
|
23
|
+
"Use Elastic APM instrumentation only for observability—for example, while debugging or "
|
|
24
|
+
"investigating an issue. APM adds Lambda layers and environment variables that increase cold "
|
|
25
|
+
"start time and runtime overhead.\n\n"
|
|
26
|
+
"When you no longer need it, disable instrumentation with:\n"
|
|
27
|
+
" weni project push <definition> --remove-apm"
|
|
28
|
+
)
|
|
29
|
+
|
|
22
30
|
|
|
23
31
|
class ProjectPushHandler(Handler):
|
|
24
32
|
def execute(self, **kwargs):
|
|
25
33
|
force_update = self.load_param(kwargs, "force_update", False)
|
|
34
|
+
use_apm = self.load_param(kwargs, "use_apm", False)
|
|
35
|
+
remove_apm = self.load_param(kwargs, "remove_apm", False)
|
|
26
36
|
definition_path = self.load_param(kwargs, "definition", None, True)
|
|
27
37
|
|
|
38
|
+
if use_apm and remove_apm:
|
|
39
|
+
formatter = Formatter()
|
|
40
|
+
formatter.print_error_panel(
|
|
41
|
+
"Cannot use --use-apm and --remove-apm together. Choose one option.",
|
|
42
|
+
title="Invalid APM options",
|
|
43
|
+
)
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
apm_instrumentation = None
|
|
47
|
+
if use_apm:
|
|
48
|
+
apm_instrumentation = "enabled"
|
|
49
|
+
elif remove_apm:
|
|
50
|
+
apm_instrumentation = "disabled"
|
|
51
|
+
|
|
28
52
|
store = Store()
|
|
29
53
|
project_uuid = store.get(STORE_PROJECT_UUID_KEY)
|
|
30
54
|
|
|
@@ -55,11 +79,11 @@ class ProjectPushHandler(Handler):
|
|
|
55
79
|
return
|
|
56
80
|
|
|
57
81
|
if agent_type == "passive":
|
|
58
|
-
self.push_passive_agent(force_update, project_uuid, definition_data)
|
|
82
|
+
self.push_passive_agent(force_update, project_uuid, definition_data, apm_instrumentation)
|
|
59
83
|
elif agent_type == "active":
|
|
60
|
-
self.push_active_agent(force_update, project_uuid, definition_data)
|
|
84
|
+
self.push_active_agent(force_update, project_uuid, definition_data, apm_instrumentation)
|
|
61
85
|
|
|
62
|
-
def push_passive_agent(self, force_update, project_uuid, definition):
|
|
86
|
+
def push_passive_agent(self, force_update, project_uuid, definition, apm_instrumentation=None):
|
|
63
87
|
formatter = Formatter()
|
|
64
88
|
error = validate_agent_definition_schema(definition)
|
|
65
89
|
if error:
|
|
@@ -74,9 +98,9 @@ class ProjectPushHandler(Handler):
|
|
|
74
98
|
return
|
|
75
99
|
|
|
76
100
|
definition = format_definition(definition)
|
|
77
|
-
self.push_definition(force_update, "passive", project_uuid, definition, tools_folders_map)
|
|
101
|
+
self.push_definition(force_update, "passive", project_uuid, definition, tools_folders_map, apm_instrumentation)
|
|
78
102
|
|
|
79
|
-
def push_active_agent(self, force_update, project_uuid, definition):
|
|
103
|
+
def push_active_agent(self, force_update, project_uuid, definition, apm_instrumentation=None):
|
|
80
104
|
formatter = Formatter()
|
|
81
105
|
error = validate_active_agent_definition_schema(definition)
|
|
82
106
|
if error:
|
|
@@ -98,7 +122,7 @@ class ProjectPushHandler(Handler):
|
|
|
98
122
|
rules_folders_map.update(preprocessing_folders_map)
|
|
99
123
|
resources_folders_map = rules_folders_map
|
|
100
124
|
definition = format_definition(definition)
|
|
101
|
-
self.push_definition(force_update, "active", project_uuid, definition, resources_folders_map)
|
|
125
|
+
self.push_definition(force_update, "active", project_uuid, definition, resources_folders_map, None)
|
|
102
126
|
|
|
103
127
|
def load_param(self, params, key, default=None, required=False):
|
|
104
128
|
value = params.get(key, default)
|
|
@@ -115,13 +139,19 @@ class ProjectPushHandler(Handler):
|
|
|
115
139
|
def load_preprocessing_folder(self, definition) -> tuple[Optional[dict], Optional[str]]:
|
|
116
140
|
return _load_preprocessing_folder(definition)
|
|
117
141
|
|
|
118
|
-
def push_definition(
|
|
142
|
+
def push_definition(
|
|
143
|
+
self, force_update, agent_type, project_uuid, definition, resources_folder_map, apm_instrumentation=None
|
|
144
|
+
):
|
|
119
145
|
client = CLIClient()
|
|
146
|
+
formatter = Formatter()
|
|
120
147
|
|
|
121
148
|
try:
|
|
122
|
-
client.push_agents(
|
|
149
|
+
client.push_agents(
|
|
150
|
+
project_uuid, definition, resources_folder_map, agent_type, apm_instrumentation=apm_instrumentation
|
|
151
|
+
)
|
|
123
152
|
except Exception as e:
|
|
124
|
-
formatter = Formatter()
|
|
125
153
|
formatter.print_error_panel(f"Failed to push definition: {e}")
|
|
126
154
|
else:
|
|
127
155
|
click.echo("Definition pushed successfully")
|
|
156
|
+
if apm_instrumentation == "enabled":
|
|
157
|
+
formatter.print_warning_panel(APM_OBSERVABILITY_WARNING, title="APM instrumentation enabled")
|
|
@@ -18,7 +18,9 @@ from weni_cli.validators.agent_definition import (
|
|
|
18
18
|
format_definition,
|
|
19
19
|
load_agent_definition,
|
|
20
20
|
load_test_definition,
|
|
21
|
+
validate_active_agent_definition_schema,
|
|
21
22
|
validate_active_test_definition,
|
|
23
|
+
validate_agent_definition_schema,
|
|
22
24
|
)
|
|
23
25
|
|
|
24
26
|
DEFAULT_TEST_DEFINITION_FILE = "test_definition.yaml"
|
|
@@ -94,6 +96,18 @@ class RunHandler(Handler):
|
|
|
94
96
|
|
|
95
97
|
agent_type = detect_agent_type(definition_data)
|
|
96
98
|
|
|
99
|
+
schema_validator = (
|
|
100
|
+
validate_active_agent_definition_schema
|
|
101
|
+
if agent_type == ACTIVE_TYPE
|
|
102
|
+
else validate_agent_definition_schema
|
|
103
|
+
)
|
|
104
|
+
if validation_error := schema_validator(definition_data):
|
|
105
|
+
formatter.print_error_panel(
|
|
106
|
+
f"Invalid agent definition YAML file format, error:\n{validation_error}",
|
|
107
|
+
title="Failed to load definition file",
|
|
108
|
+
)
|
|
109
|
+
return
|
|
110
|
+
|
|
97
111
|
if agent_type == ACTIVE_TYPE:
|
|
98
112
|
self._execute_active(
|
|
99
113
|
definition_data=definition_data,
|
|
@@ -32,3 +32,14 @@ class Formatter:
|
|
|
32
32
|
expand=False,
|
|
33
33
|
)
|
|
34
34
|
print(success_panel)
|
|
35
|
+
|
|
36
|
+
def print_warning_panel(self, message, title="Warning"):
|
|
37
|
+
warning_panel = Panel(
|
|
38
|
+
self._to_renderable(message),
|
|
39
|
+
title=f"[bold yellow]{title}[/bold yellow]",
|
|
40
|
+
title_align="left",
|
|
41
|
+
style="bold yellow",
|
|
42
|
+
expand=False,
|
|
43
|
+
padding=(1, 1),
|
|
44
|
+
)
|
|
45
|
+
print(warning_panel)
|
|
@@ -53,6 +53,24 @@ def test_print_success_panel(formatter, mocker):
|
|
|
53
53
|
assert panel_arg.expand is False
|
|
54
54
|
|
|
55
55
|
|
|
56
|
+
def test_print_warning_panel(formatter, mocker):
|
|
57
|
+
"""Test the warning panel creation and printing."""
|
|
58
|
+
mock_print = mocker.patch("weni_cli.formatter.formatter.print")
|
|
59
|
+
|
|
60
|
+
warning_message = "This is a warning message"
|
|
61
|
+
|
|
62
|
+
formatter.print_warning_panel(warning_message, title="Custom warning")
|
|
63
|
+
|
|
64
|
+
mock_print.assert_called_once()
|
|
65
|
+
|
|
66
|
+
panel_arg = mock_print.call_args[0][0]
|
|
67
|
+
assert isinstance(panel_arg, Panel)
|
|
68
|
+
assert panel_arg.title == "[bold yellow]Custom warning[/bold yellow]"
|
|
69
|
+
assert panel_arg.renderable == warning_message
|
|
70
|
+
assert panel_arg.style == "bold yellow"
|
|
71
|
+
assert panel_arg.expand is False
|
|
72
|
+
|
|
73
|
+
|
|
56
74
|
def test_print_error_panel_with_exception(formatter, mocker):
|
|
57
75
|
"""Test printing an error panel with an exception object."""
|
|
58
76
|
mock_print = mocker.patch("weni_cli.formatter.formatter.print")
|
|
@@ -5,6 +5,8 @@ import yaml
|
|
|
5
5
|
|
|
6
6
|
from slugify import slugify
|
|
7
7
|
|
|
8
|
+
from weni_cli.validators.source import validate_entrypoint
|
|
9
|
+
|
|
8
10
|
MIN_INSTRUCTION_LENGTH = 40
|
|
9
11
|
MIN_GUARDRAIL_LENGTH = 40
|
|
10
12
|
MAX_AGENT_NAME_LENGTH = 55
|
|
@@ -174,6 +176,13 @@ def validate_agent_definition_schema(data):
|
|
|
174
176
|
if not isinstance(tool_data["source"]["entrypoint"], str):
|
|
175
177
|
return f"Agent '{agent_key}': tool '{tool_name}': 'source.entrypoint' must be a string in the agent definition file"
|
|
176
178
|
|
|
179
|
+
if error := validate_entrypoint(
|
|
180
|
+
context=f"Agent '{agent_key}': tool '{tool_name}'",
|
|
181
|
+
source_path=tool_data["source"]["path"],
|
|
182
|
+
entrypoint=tool_data["source"]["entrypoint"],
|
|
183
|
+
):
|
|
184
|
+
return error
|
|
185
|
+
|
|
177
186
|
# Validate source path_test if present (must be string)
|
|
178
187
|
if "path_test" in tool_data["source"] and not isinstance(tool_data["source"]["path_test"], str):
|
|
179
188
|
return f"Agent '{agent_key}': tool '{tool_name}': 'source.path_test' must be a string in the agent definition file"
|
|
@@ -419,6 +428,13 @@ def validate_active_agent_definition_schema(data):
|
|
|
419
428
|
if not isinstance(rule_data["source"]["entrypoint"], str):
|
|
420
429
|
return f"Agent '{agent_key}': rule '{rule_key}': 'source.entrypoint' must be a string in the agent definition file"
|
|
421
430
|
|
|
431
|
+
if error := validate_entrypoint(
|
|
432
|
+
context=f"Agent '{agent_key}': rule '{rule_key}'",
|
|
433
|
+
source_path=rule_data["source"]["path"],
|
|
434
|
+
entrypoint=rule_data["source"]["entrypoint"],
|
|
435
|
+
):
|
|
436
|
+
return error
|
|
437
|
+
|
|
422
438
|
# Validate start_condition (required, must be a string)
|
|
423
439
|
if not rule_data.get("start_condition"):
|
|
424
440
|
return f"Agent '{agent_key}': rule '{rule_key}' is missing required field 'start_condition' in the agent definition file"
|
|
@@ -461,6 +477,13 @@ def validate_active_agent_definition_schema(data):
|
|
|
461
477
|
if not isinstance(agent_data["pre_processing"]["source"]["entrypoint"], str):
|
|
462
478
|
return f"Agent '{agent_key}': 'pre_processing.source.entrypoint' must be a string in the agent definition file"
|
|
463
479
|
|
|
480
|
+
if error := validate_entrypoint(
|
|
481
|
+
context=f"Agent '{agent_key}': pre_processing",
|
|
482
|
+
source_path=agent_data["pre_processing"]["source"]["path"],
|
|
483
|
+
entrypoint=agent_data["pre_processing"]["source"]["entrypoint"],
|
|
484
|
+
):
|
|
485
|
+
return error
|
|
486
|
+
|
|
464
487
|
# Validate result_examples_file (required, must be a string with a .json in suffix)
|
|
465
488
|
if "result_examples_file" not in agent_data["pre_processing"]:
|
|
466
489
|
return f"Agent '{agent_key}': 'pre_processing' is missing required field 'result_examples_file' in the agent definition file"
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Filesystem and AST-based validation for ``source.entrypoint`` definitions.
|
|
2
|
+
|
|
3
|
+
The schema validator in :mod:`weni_cli.validators.agent_definition` only checks
|
|
4
|
+
type and shape of YAML fields. This module complements it by verifying that
|
|
5
|
+
the ``source.path`` actually exists on disk, that the referenced module file
|
|
6
|
+
is present, and that the class named in ``entrypoint`` is declared in that
|
|
7
|
+
module — all without importing or executing any user code.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import ast
|
|
13
|
+
import os
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
ENTRYPOINT_FORMAT = "module.ClassName"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _is_valid_python_identifier(name: str) -> bool:
|
|
21
|
+
return name.isidentifier()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _parse_entrypoint(entrypoint: str) -> Optional[tuple[str, str]]:
|
|
25
|
+
"""Return ``(module, class_name)`` if entrypoint matches ``module.ClassName``.
|
|
26
|
+
|
|
27
|
+
Rules:
|
|
28
|
+
- Exactly one dot separating two non-empty parts.
|
|
29
|
+
- Both parts must be valid Python identifiers (no digits leading, no spaces).
|
|
30
|
+
"""
|
|
31
|
+
if not isinstance(entrypoint, str):
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
parts = entrypoint.split(".")
|
|
35
|
+
if len(parts) != 2:
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
module, class_name = parts
|
|
39
|
+
if not module or not class_name:
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
if not _is_valid_python_identifier(module):
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
if not _is_valid_python_identifier(class_name):
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
return module, class_name
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _class_exists_in_module(module_file: str, class_name: str) -> tuple[bool, Optional[str]]:
|
|
52
|
+
"""Parse ``module_file`` with AST and check whether ``class_name`` is declared.
|
|
53
|
+
|
|
54
|
+
Returns ``(found, syntax_error)``. ``syntax_error`` is set only when the
|
|
55
|
+
file cannot be parsed as Python.
|
|
56
|
+
"""
|
|
57
|
+
try:
|
|
58
|
+
with open(module_file, "r", encoding="utf-8") as handle:
|
|
59
|
+
source_code = handle.read()
|
|
60
|
+
except OSError as error:
|
|
61
|
+
return False, str(error)
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
tree = ast.parse(source_code, filename=module_file)
|
|
65
|
+
except SyntaxError as error:
|
|
66
|
+
return False, f"{error.msg} (line {error.lineno})"
|
|
67
|
+
|
|
68
|
+
for node in tree.body:
|
|
69
|
+
if isinstance(node, ast.ClassDef) and node.name == class_name:
|
|
70
|
+
return True, None
|
|
71
|
+
|
|
72
|
+
return False, None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def validate_entrypoint(
|
|
76
|
+
context: str,
|
|
77
|
+
source_path: str,
|
|
78
|
+
entrypoint: str,
|
|
79
|
+
base_dir: Optional[str] = None,
|
|
80
|
+
) -> Optional[str]:
|
|
81
|
+
"""Validate ``entrypoint`` against the local filesystem.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
context: Human-readable prefix for the error message (e.g.
|
|
85
|
+
``"Agent 'cep_agent': tool 'get_address'"``).
|
|
86
|
+
source_path: Path to the resource folder, as written in the YAML.
|
|
87
|
+
entrypoint: ``module.ClassName`` string from ``source.entrypoint``.
|
|
88
|
+
base_dir: Directory to resolve relative paths against. Defaults to the
|
|
89
|
+
current working directory, matching how the packager resolves paths.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
``None`` if everything checks out, otherwise a user-facing error
|
|
93
|
+
message describing the first problem encountered.
|
|
94
|
+
"""
|
|
95
|
+
parsed = _parse_entrypoint(entrypoint)
|
|
96
|
+
if parsed is None:
|
|
97
|
+
return (
|
|
98
|
+
f"{context}: entrypoint '{entrypoint}' must follow the format "
|
|
99
|
+
f"'{ENTRYPOINT_FORMAT}'"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
module, class_name = parsed
|
|
103
|
+
|
|
104
|
+
resolved_root = base_dir if base_dir is not None else os.getcwd()
|
|
105
|
+
resolved_source_path = (
|
|
106
|
+
source_path
|
|
107
|
+
if os.path.isabs(source_path)
|
|
108
|
+
else os.path.join(resolved_root, source_path)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if not os.path.isdir(resolved_source_path):
|
|
112
|
+
return f"{context}: source path '{source_path}' does not exist"
|
|
113
|
+
|
|
114
|
+
module_file = os.path.join(resolved_source_path, f"{module}.py")
|
|
115
|
+
if not os.path.isfile(module_file):
|
|
116
|
+
return (
|
|
117
|
+
f"{context}: module file '{source_path}{os.sep}{module}.py' not found "
|
|
118
|
+
f"for entrypoint '{entrypoint}'"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
found, syntax_error = _class_exists_in_module(module_file, class_name)
|
|
122
|
+
if syntax_error is not None:
|
|
123
|
+
return (
|
|
124
|
+
f"{context}: failed to parse '{source_path}{os.sep}{module}.py' for "
|
|
125
|
+
f"entrypoint '{entrypoint}': {syntax_error}"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if not found:
|
|
129
|
+
return (
|
|
130
|
+
f"{context}: class '{class_name}' not found in "
|
|
131
|
+
f"'{source_path}{os.sep}{module}.py' for entrypoint '{entrypoint}'"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return None
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Shared fixtures for validator tests.
|
|
2
|
+
|
|
3
|
+
The schema tests in :mod:`test_definition` operate on synthetic agent
|
|
4
|
+
definitions and don't ship with real source paths or module files on disk.
|
|
5
|
+
The newly introduced :func:`validate_entrypoint` would otherwise reject every
|
|
6
|
+
one of them. Auto-mock it to ``None`` here so schema-shape tests keep their
|
|
7
|
+
focus. Tests that exercise the entrypoint validator itself should import it
|
|
8
|
+
directly from :mod:`weni_cli.validators.source`, which is not patched.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.fixture(autouse=True)
|
|
15
|
+
def _bypass_entrypoint_validation(mocker):
|
|
16
|
+
mocker.patch(
|
|
17
|
+
"weni_cli.validators.agent_definition.validate_entrypoint",
|
|
18
|
+
return_value=None,
|
|
19
|
+
)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Tests for :mod:`weni_cli.validators.source`.
|
|
2
|
+
|
|
3
|
+
The autouse fixture in this folder's ``conftest.py`` patches
|
|
4
|
+
``weni_cli.validators.agent_definition.validate_entrypoint``. The function
|
|
5
|
+
under test here is imported directly from ``weni_cli.validators.source``, so
|
|
6
|
+
those tests bypass the schema-level patch and exercise the real validator.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
from click.testing import CliRunner
|
|
15
|
+
|
|
16
|
+
from weni_cli.validators.source import (
|
|
17
|
+
ENTRYPOINT_FORMAT,
|
|
18
|
+
validate_entrypoint,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
CONTEXT = "Agent 'cep_agent': tool 'get_address'"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _write_module(path: str, body: str) -> None:
|
|
26
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
27
|
+
with open(path, "w") as handle:
|
|
28
|
+
handle.write(body)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.mark.parametrize(
|
|
32
|
+
"bad_entrypoint",
|
|
33
|
+
[
|
|
34
|
+
"main",
|
|
35
|
+
"main.Foo.Bar",
|
|
36
|
+
"123.Foo",
|
|
37
|
+
".Foo",
|
|
38
|
+
"main.",
|
|
39
|
+
"main Foo.Bar",
|
|
40
|
+
"",
|
|
41
|
+
],
|
|
42
|
+
)
|
|
43
|
+
def test_invalid_entrypoint_format_is_rejected(bad_entrypoint):
|
|
44
|
+
runner = CliRunner()
|
|
45
|
+
with runner.isolated_filesystem():
|
|
46
|
+
os.makedirs("tools/get_address", exist_ok=True)
|
|
47
|
+
|
|
48
|
+
error = validate_entrypoint(CONTEXT, "tools/get_address", bad_entrypoint)
|
|
49
|
+
|
|
50
|
+
assert error is not None
|
|
51
|
+
assert ENTRYPOINT_FORMAT in error
|
|
52
|
+
assert CONTEXT in error
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_missing_source_path_is_rejected():
|
|
56
|
+
runner = CliRunner()
|
|
57
|
+
with runner.isolated_filesystem():
|
|
58
|
+
error = validate_entrypoint(CONTEXT, "tools/get_address", "main.GetAddress")
|
|
59
|
+
|
|
60
|
+
assert error is not None
|
|
61
|
+
assert "source path 'tools/get_address' does not exist" in error
|
|
62
|
+
assert CONTEXT in error
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_missing_module_file_is_rejected():
|
|
66
|
+
runner = CliRunner()
|
|
67
|
+
with runner.isolated_filesystem():
|
|
68
|
+
os.makedirs("tools/get_address", exist_ok=True)
|
|
69
|
+
|
|
70
|
+
error = validate_entrypoint(CONTEXT, "tools/get_address", "main.GetAddress")
|
|
71
|
+
|
|
72
|
+
assert error is not None
|
|
73
|
+
assert "module file" in error
|
|
74
|
+
assert "main.py" in error
|
|
75
|
+
assert CONTEXT in error
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_missing_class_is_rejected():
|
|
79
|
+
runner = CliRunner()
|
|
80
|
+
with runner.isolated_filesystem():
|
|
81
|
+
_write_module("tools/get_address/main.py", "class SomethingElse:\n pass\n")
|
|
82
|
+
|
|
83
|
+
error = validate_entrypoint(CONTEXT, "tools/get_address", "main.GetAddress")
|
|
84
|
+
|
|
85
|
+
assert error is not None
|
|
86
|
+
assert "class 'GetAddress' not found" in error
|
|
87
|
+
assert CONTEXT in error
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_syntax_error_in_module_is_reported():
|
|
91
|
+
runner = CliRunner()
|
|
92
|
+
with runner.isolated_filesystem():
|
|
93
|
+
_write_module("tools/get_address/main.py", "class :::: pass\n")
|
|
94
|
+
|
|
95
|
+
error = validate_entrypoint(CONTEXT, "tools/get_address", "main.GetAddress")
|
|
96
|
+
|
|
97
|
+
assert error is not None
|
|
98
|
+
assert "failed to parse" in error
|
|
99
|
+
assert CONTEXT in error
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_happy_path_returns_none():
|
|
103
|
+
runner = CliRunner()
|
|
104
|
+
with runner.isolated_filesystem():
|
|
105
|
+
_write_module(
|
|
106
|
+
"tools/get_address/main.py",
|
|
107
|
+
"class GetAddress:\n def execute(self, context):\n return 'ok'\n",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
assert (
|
|
111
|
+
validate_entrypoint(CONTEXT, "tools/get_address", "main.GetAddress")
|
|
112
|
+
is None
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_class_nested_inside_function_is_not_detected():
|
|
117
|
+
"""Only top-level ``class`` declarations count, mirroring how the backend
|
|
118
|
+
imports the module to grab the entrypoint."""
|
|
119
|
+
runner = CliRunner()
|
|
120
|
+
with runner.isolated_filesystem():
|
|
121
|
+
_write_module(
|
|
122
|
+
"tools/get_address/main.py",
|
|
123
|
+
"def factory():\n class GetAddress:\n pass\n return GetAddress\n",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
error = validate_entrypoint(CONTEXT, "tools/get_address", "main.GetAddress")
|
|
127
|
+
|
|
128
|
+
assert error is not None
|
|
129
|
+
assert "class 'GetAddress' not found" in error
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_absolute_source_path_is_supported(tmp_path):
|
|
133
|
+
module_dir = tmp_path / "tools" / "get_address"
|
|
134
|
+
module_dir.mkdir(parents=True)
|
|
135
|
+
(module_dir / "main.py").write_text("class GetAddress: pass\n")
|
|
136
|
+
|
|
137
|
+
assert (
|
|
138
|
+
validate_entrypoint(CONTEXT, str(module_dir), "main.GetAddress")
|
|
139
|
+
is None
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_base_dir_resolves_relative_paths(tmp_path):
|
|
144
|
+
module_dir = tmp_path / "tools" / "get_address"
|
|
145
|
+
module_dir.mkdir(parents=True)
|
|
146
|
+
(module_dir / "main.py").write_text("class GetAddress: pass\n")
|
|
147
|
+
|
|
148
|
+
assert (
|
|
149
|
+
validate_entrypoint(
|
|
150
|
+
CONTEXT,
|
|
151
|
+
"tools/get_address",
|
|
152
|
+
"main.GetAddress",
|
|
153
|
+
base_dir=str(tmp_path),
|
|
154
|
+
)
|
|
155
|
+
is None
|
|
156
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{weni_cli-3.7.0a0 → weni_cli-3.7.2}/weni_cli/validators/tests/test_active_test_definition.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|