weni-cli 3.7.0__tar.gz → 3.7.1__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.
Files changed (51) hide show
  1. {weni_cli-3.7.0 → weni_cli-3.7.1}/PKG-INFO +2 -2
  2. {weni_cli-3.7.0 → weni_cli-3.7.1}/pyproject.toml +2 -2
  3. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/commands/run.py +14 -0
  4. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/validators/agent_definition.py +23 -0
  5. weni_cli-3.7.1/weni_cli/validators/source.py +134 -0
  6. weni_cli-3.7.1/weni_cli/validators/tests/conftest.py +19 -0
  7. weni_cli-3.7.1/weni_cli/validators/tests/test_source.py +156 -0
  8. {weni_cli-3.7.0 → weni_cli-3.7.1}/README.md +0 -0
  9. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/__init__.py +0 -0
  10. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/auth.py +0 -0
  11. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/cli.py +0 -0
  12. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/clients/__init__.py +0 -0
  13. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/clients/cli_client.py +0 -0
  14. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/clients/common.py +0 -0
  15. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/clients/response_handlers/__init__.py +0 -0
  16. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/clients/response_handlers/handlers.py +0 -0
  17. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/clients/tests/__init__.py +0 -0
  18. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/clients/tests/test_cli_client.py +0 -0
  19. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/clients/tests/test_weni_client.py +0 -0
  20. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/clients/weni_client.py +0 -0
  21. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/commands/channel_create.py +0 -0
  22. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/commands/eval_init.py +0 -0
  23. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/commands/eval_run.py +0 -0
  24. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/commands/init.py +0 -0
  25. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/commands/login.py +0 -0
  26. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/commands/logs.py +0 -0
  27. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/commands/project_current.py +0 -0
  28. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/commands/project_list.py +0 -0
  29. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/commands/project_push.py +0 -0
  30. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/commands/project_use.py +0 -0
  31. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/formatter/__init__.py +0 -0
  32. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/formatter/formatter.py +0 -0
  33. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/formatter/tests/test_formatter.py +0 -0
  34. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/handler.py +0 -0
  35. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/packager/__init__.py +0 -0
  36. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/packager/loader.py +0 -0
  37. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/packager/packager.py +0 -0
  38. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/packager/tests/__init__.py +0 -0
  39. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/packager/tests/test_loader.py +0 -0
  40. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/packager/tests/test_packager.py +0 -0
  41. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/spinner/__init__.py +0 -0
  42. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/spinner/spinners.py +0 -0
  43. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/store.py +0 -0
  44. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/utils.py +0 -0
  45. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/validators/__init__.py +0 -0
  46. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/validators/channel_definition.py +0 -0
  47. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/validators/tests/__init__.py +0 -0
  48. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/validators/tests/test_active_test_definition.py +0 -0
  49. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/validators/tests/test_channel_definition.py +0 -0
  50. {weni_cli-3.7.0 → weni_cli-3.7.1}/weni_cli/validators/tests/test_definition.py +0 -0
  51. {weni_cli-3.7.0 → weni_cli-3.7.1}/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.0
3
+ Version: 3.7.1
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.3)
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.0"
3
+ version = "3.7.1"
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.3"
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"
@@ -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,
@@ -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