a4e 0.1.5__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.
- a4e/__init__.py +0 -0
- a4e/cli.py +47 -0
- a4e/cli_commands/__init__.py +5 -0
- a4e/cli_commands/add.py +376 -0
- a4e/cli_commands/deploy.py +149 -0
- a4e/cli_commands/dev.py +162 -0
- a4e/cli_commands/info.py +206 -0
- a4e/cli_commands/init.py +211 -0
- a4e/cli_commands/list.py +227 -0
- a4e/cli_commands/mcp.py +504 -0
- a4e/cli_commands/remove.py +197 -0
- a4e/cli_commands/update.py +285 -0
- a4e/cli_commands/validate.py +117 -0
- a4e/core.py +109 -0
- a4e/dev_runner.py +425 -0
- a4e/server.py +86 -0
- a4e/templates/agent.md.j2 +168 -0
- a4e/templates/agent.py.j2 +15 -0
- a4e/templates/agents.md.j2 +99 -0
- a4e/templates/metadata.json.j2 +20 -0
- a4e/templates/prompt.md.j2 +20 -0
- a4e/templates/prompts/agent.md.j2 +206 -0
- a4e/templates/skills/agents.md.j2 +110 -0
- a4e/templates/skills/skill.md.j2 +120 -0
- a4e/templates/support_module.py.j2 +84 -0
- a4e/templates/tool.py.j2 +60 -0
- a4e/templates/tools/agent.md.j2 +192 -0
- a4e/templates/view.tsx.j2 +21 -0
- a4e/templates/views/agent.md.j2 +219 -0
- a4e/tools/__init__.py +70 -0
- a4e/tools/agent_tools/__init__.py +12 -0
- a4e/tools/agent_tools/add_support_module.py +95 -0
- a4e/tools/agent_tools/add_tool.py +115 -0
- a4e/tools/agent_tools/list_tools.py +28 -0
- a4e/tools/agent_tools/remove_tool.py +69 -0
- a4e/tools/agent_tools/update_tool.py +123 -0
- a4e/tools/deploy/__init__.py +8 -0
- a4e/tools/deploy/deploy.py +59 -0
- a4e/tools/dev/__init__.py +10 -0
- a4e/tools/dev/check_environment.py +79 -0
- a4e/tools/dev/dev_start.py +30 -0
- a4e/tools/dev/dev_stop.py +26 -0
- a4e/tools/project/__init__.py +10 -0
- a4e/tools/project/get_agent_info.py +66 -0
- a4e/tools/project/get_instructions.py +216 -0
- a4e/tools/project/initialize_project.py +231 -0
- a4e/tools/schemas/__init__.py +8 -0
- a4e/tools/schemas/generate_schemas.py +278 -0
- a4e/tools/skills/__init__.py +12 -0
- a4e/tools/skills/add_skill.py +105 -0
- a4e/tools/skills/helpers.py +137 -0
- a4e/tools/skills/list_skills.py +54 -0
- a4e/tools/skills/remove_skill.py +74 -0
- a4e/tools/skills/update_skill.py +150 -0
- a4e/tools/validation/__init__.py +8 -0
- a4e/tools/validation/validate.py +389 -0
- a4e/tools/views/__init__.py +12 -0
- a4e/tools/views/add_view.py +40 -0
- a4e/tools/views/helpers.py +91 -0
- a4e/tools/views/list_views.py +27 -0
- a4e/tools/views/remove_view.py +73 -0
- a4e/tools/views/update_view.py +124 -0
- a4e/utils/dev_manager.py +253 -0
- a4e/utils/schema_generator.py +255 -0
- a4e-0.1.5.dist-info/METADATA +427 -0
- a4e-0.1.5.dist-info/RECORD +70 -0
- a4e-0.1.5.dist-info/WHEEL +5 -0
- a4e-0.1.5.dist-info/entry_points.txt +2 -0
- a4e-0.1.5.dist-info/licenses/LICENSE +21 -0
- a4e-0.1.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Update skill - modify existing skill's properties.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Optional, List
|
|
7
|
+
|
|
8
|
+
from ...core import mcp, get_project_dir
|
|
9
|
+
from .helpers import create_skill
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@mcp.tool()
|
|
13
|
+
def update_skill(
|
|
14
|
+
skill_id: str,
|
|
15
|
+
name: Optional[str] = None,
|
|
16
|
+
description: Optional[str] = None,
|
|
17
|
+
intent_triggers: Optional[List[str]] = None,
|
|
18
|
+
output_view: Optional[str] = None,
|
|
19
|
+
internal_tools: Optional[List[str]] = None,
|
|
20
|
+
requires_auth: Optional[bool] = None,
|
|
21
|
+
agent_name: Optional[str] = None,
|
|
22
|
+
) -> dict:
|
|
23
|
+
"""
|
|
24
|
+
Update an existing skill's properties.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
skill_id: ID of the skill to update
|
|
28
|
+
name: New human-readable name (optional)
|
|
29
|
+
description: New description (optional)
|
|
30
|
+
intent_triggers: New list of trigger phrases (optional)
|
|
31
|
+
output_view: New view ID to render (optional)
|
|
32
|
+
internal_tools: New list of tool names (optional)
|
|
33
|
+
requires_auth: New auth requirement (optional)
|
|
34
|
+
agent_name: Optional agent ID if not in agent directory
|
|
35
|
+
"""
|
|
36
|
+
project_dir = get_project_dir(agent_name)
|
|
37
|
+
skills_dir = project_dir / "skills"
|
|
38
|
+
|
|
39
|
+
if not skills_dir.exists():
|
|
40
|
+
return {
|
|
41
|
+
"success": False,
|
|
42
|
+
"error": f"skills/ directory not found at {skills_dir}",
|
|
43
|
+
"fix": "Make sure you're in an agent project directory or specify agent_name",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Check if skill exists in schemas.json
|
|
47
|
+
schemas_file = skills_dir / "schemas.json"
|
|
48
|
+
if not schemas_file.exists():
|
|
49
|
+
return {
|
|
50
|
+
"success": False,
|
|
51
|
+
"error": "skills/schemas.json not found",
|
|
52
|
+
"fix": "Create a skill first with add_skill()",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
schemas = json.loads(schemas_file.read_text())
|
|
57
|
+
except json.JSONDecodeError:
|
|
58
|
+
return {
|
|
59
|
+
"success": False,
|
|
60
|
+
"error": "Failed to parse skills/schemas.json",
|
|
61
|
+
"fix": "Check the file for JSON syntax errors",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if skill_id not in schemas:
|
|
65
|
+
available = list(schemas.keys())
|
|
66
|
+
return {
|
|
67
|
+
"success": False,
|
|
68
|
+
"error": f"Skill '{skill_id}' not found",
|
|
69
|
+
"fix": f"Available skills: {', '.join(available) if available else 'none'}",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Get current skill data
|
|
73
|
+
current = schemas[skill_id]
|
|
74
|
+
|
|
75
|
+
# Check if anything to update
|
|
76
|
+
updates = {
|
|
77
|
+
"name": name,
|
|
78
|
+
"description": description,
|
|
79
|
+
"intent_triggers": intent_triggers,
|
|
80
|
+
"output_view": output_view,
|
|
81
|
+
"internal_tools": internal_tools,
|
|
82
|
+
"requires_auth": requires_auth,
|
|
83
|
+
}
|
|
84
|
+
if all(v is None for v in updates.values()):
|
|
85
|
+
return {
|
|
86
|
+
"success": False,
|
|
87
|
+
"error": "Nothing to update",
|
|
88
|
+
"fix": "Provide at least one of: name, description, intent_triggers, output_view, internal_tools, requires_auth",
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Merge with current values
|
|
92
|
+
final_name = name if name is not None else current.get("name", skill_id)
|
|
93
|
+
final_description = description if description is not None else current.get("description", "")
|
|
94
|
+
final_triggers = intent_triggers if intent_triggers is not None else current.get("intent_triggers", [])
|
|
95
|
+
final_view = output_view if output_view is not None else current.get("output", {}).get("view", "NONE")
|
|
96
|
+
final_tools = internal_tools if internal_tools is not None else current.get("internal_tools", [])
|
|
97
|
+
final_auth = requires_auth if requires_auth is not None else current.get("requires_auth", False)
|
|
98
|
+
|
|
99
|
+
warnings = []
|
|
100
|
+
|
|
101
|
+
# Validate output_view exists
|
|
102
|
+
view_props = {}
|
|
103
|
+
if final_view and final_view != "NONE":
|
|
104
|
+
view_dir = project_dir / "views" / final_view
|
|
105
|
+
view_schema_file = view_dir / "view.schema.json"
|
|
106
|
+
|
|
107
|
+
if not view_dir.exists():
|
|
108
|
+
return {
|
|
109
|
+
"success": False,
|
|
110
|
+
"error": f"View '{final_view}' not found",
|
|
111
|
+
"fix": f"Create it first with add_view() or use an existing view",
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if view_schema_file.exists():
|
|
115
|
+
try:
|
|
116
|
+
schema = json.loads(view_schema_file.read_text())
|
|
117
|
+
view_props = schema.get("props", {}).get("properties", {})
|
|
118
|
+
except Exception:
|
|
119
|
+
warnings.append(f"Could not parse view schema for '{final_view}'")
|
|
120
|
+
|
|
121
|
+
# Remove old skill directory if exists
|
|
122
|
+
skill_dir = skills_dir / skill_id
|
|
123
|
+
if skill_dir.exists():
|
|
124
|
+
import shutil
|
|
125
|
+
shutil.rmtree(skill_dir)
|
|
126
|
+
|
|
127
|
+
# Remove old entry from schemas
|
|
128
|
+
del schemas[skill_id]
|
|
129
|
+
schemas_file.write_text(json.dumps(schemas, indent=2))
|
|
130
|
+
|
|
131
|
+
# Create updated skill
|
|
132
|
+
result = create_skill(
|
|
133
|
+
skill_id=skill_id,
|
|
134
|
+
name=final_name,
|
|
135
|
+
description=final_description,
|
|
136
|
+
intent_triggers=final_triggers,
|
|
137
|
+
output_view=final_view,
|
|
138
|
+
internal_tools=final_tools,
|
|
139
|
+
requires_auth=final_auth,
|
|
140
|
+
view_props=view_props,
|
|
141
|
+
project_dir=project_dir,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if warnings and result.get("success"):
|
|
145
|
+
result["warnings"] = warnings
|
|
146
|
+
|
|
147
|
+
if result.get("success"):
|
|
148
|
+
result["message"] = f"Updated skill '{skill_id}'"
|
|
149
|
+
|
|
150
|
+
return result
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Validate agent structure tool.
|
|
3
|
+
|
|
4
|
+
Enhanced validation based on lessons learned from production deployments:
|
|
5
|
+
- Tool signature must use params: Dict[str, Any] pattern
|
|
6
|
+
- Schemas must be in dictionary format (not array)
|
|
7
|
+
- Support files must have __name__ compatibility for exec() context
|
|
8
|
+
- Agent prompt should include language policy
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Optional
|
|
12
|
+
from collections import defaultdict
|
|
13
|
+
import ast
|
|
14
|
+
import json
|
|
15
|
+
import re
|
|
16
|
+
|
|
17
|
+
from ...core import mcp, get_project_dir
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@mcp.tool()
|
|
21
|
+
def validate(strict: bool = True, agent_name: Optional[str] = None) -> dict:
|
|
22
|
+
"""
|
|
23
|
+
Validate agent structure before deployment
|
|
24
|
+
|
|
25
|
+
Checks:
|
|
26
|
+
- Required files exist
|
|
27
|
+
- Python syntax valid
|
|
28
|
+
- Tool signatures use params: Dict pattern (A4E compatibility)
|
|
29
|
+
- Schemas are in dictionary format (not array)
|
|
30
|
+
- Support files have exec() compatibility
|
|
31
|
+
- Skills integrity (triggers, dependencies, orphans)
|
|
32
|
+
- Agent prompt includes language policy
|
|
33
|
+
"""
|
|
34
|
+
project_dir = get_project_dir(agent_name)
|
|
35
|
+
required_files = [
|
|
36
|
+
"agent.py",
|
|
37
|
+
"metadata.json",
|
|
38
|
+
"prompts/agent.md",
|
|
39
|
+
"views/welcome/view.tsx",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
missing = []
|
|
43
|
+
for f in required_files:
|
|
44
|
+
if not (project_dir / f).exists():
|
|
45
|
+
missing.append(f)
|
|
46
|
+
|
|
47
|
+
if missing:
|
|
48
|
+
return {
|
|
49
|
+
"success": False,
|
|
50
|
+
"error": f"Missing required files in {project_dir}: {', '.join(missing)}",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
errors = []
|
|
54
|
+
warnings = []
|
|
55
|
+
|
|
56
|
+
tools_dir = project_dir / "tools"
|
|
57
|
+
views_dir = project_dir / "views"
|
|
58
|
+
|
|
59
|
+
if strict:
|
|
60
|
+
# 1. Check Python syntax and tool signatures
|
|
61
|
+
python_files = [project_dir / "agent.py"]
|
|
62
|
+
if tools_dir.exists():
|
|
63
|
+
python_files.extend(list(tools_dir.glob("*.py")))
|
|
64
|
+
|
|
65
|
+
for py_file in python_files:
|
|
66
|
+
if py_file.name == "__init__.py":
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
content = py_file.read_text()
|
|
71
|
+
tree = ast.parse(content)
|
|
72
|
+
|
|
73
|
+
# Check type hints and tool signatures
|
|
74
|
+
for node in ast.walk(tree):
|
|
75
|
+
if isinstance(node, ast.FunctionDef):
|
|
76
|
+
# Skip private functions or __init__
|
|
77
|
+
if node.name.startswith("_"):
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
# For tool files, check if using params: Dict pattern
|
|
81
|
+
if py_file.parent == tools_dir and py_file.name not in ["db.py", "models.py", "seed_data.py", "__init__.py"]:
|
|
82
|
+
tool_errors = _validate_tool_signature(node, py_file.name)
|
|
83
|
+
errors.extend(tool_errors)
|
|
84
|
+
|
|
85
|
+
# Check args for annotations
|
|
86
|
+
for arg in node.args.args:
|
|
87
|
+
if arg.annotation is None and arg.arg != "self":
|
|
88
|
+
errors.append(
|
|
89
|
+
f"Missing type hint for argument '{arg.arg}' in {py_file.name}:{node.name}"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Check exec() compatibility for support files
|
|
93
|
+
if py_file.name in ["db.py", "models.py", "seed_data.py"]:
|
|
94
|
+
compat_warnings = _check_exec_compatibility(content, py_file.name)
|
|
95
|
+
warnings.extend(compat_warnings)
|
|
96
|
+
|
|
97
|
+
except SyntaxError as e:
|
|
98
|
+
errors.append(f"Syntax error in {py_file.name}: {e}")
|
|
99
|
+
except Exception as e:
|
|
100
|
+
errors.append(f"Error analyzing {py_file.name}: {e}")
|
|
101
|
+
|
|
102
|
+
# 2. Check if schemas exist and are in correct format
|
|
103
|
+
if tools_dir.exists():
|
|
104
|
+
tool_files = [f for f in tools_dir.glob("*.py") if f.name not in ["__init__.py", "db.py", "models.py", "seed_data.py"]]
|
|
105
|
+
schemas_file = tools_dir / "schemas.json"
|
|
106
|
+
|
|
107
|
+
if not schemas_file.exists() and tool_files:
|
|
108
|
+
errors.append(
|
|
109
|
+
"Tools exist but tools/schemas.json is missing. Run generate_schemas."
|
|
110
|
+
)
|
|
111
|
+
elif schemas_file.exists():
|
|
112
|
+
schema_errors = _validate_tools_schema_format(schemas_file)
|
|
113
|
+
errors.extend(schema_errors)
|
|
114
|
+
|
|
115
|
+
# 3. Check view schemas
|
|
116
|
+
if views_dir.exists():
|
|
117
|
+
view_dirs = [
|
|
118
|
+
d
|
|
119
|
+
for d in views_dir.iterdir()
|
|
120
|
+
if d.is_dir() and (d / "view.tsx").exists()
|
|
121
|
+
]
|
|
122
|
+
if not (views_dir / "schemas.json").exists() and view_dirs:
|
|
123
|
+
errors.append(
|
|
124
|
+
"Views exist but views/schemas.json is missing. Run generate_schemas."
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# 4. Validate skills integrity
|
|
128
|
+
skills_dir = project_dir / "skills"
|
|
129
|
+
if skills_dir.exists():
|
|
130
|
+
skill_errors, skill_warnings = _validate_skills(
|
|
131
|
+
skills_dir, tools_dir, views_dir
|
|
132
|
+
)
|
|
133
|
+
errors.extend(skill_errors)
|
|
134
|
+
warnings.extend(skill_warnings)
|
|
135
|
+
|
|
136
|
+
# 5. Check agent prompt for language policy
|
|
137
|
+
agent_prompt = project_dir / "prompts" / "agent.md"
|
|
138
|
+
if agent_prompt.exists():
|
|
139
|
+
prompt_warnings = _check_agent_prompt(agent_prompt)
|
|
140
|
+
warnings.extend(prompt_warnings)
|
|
141
|
+
|
|
142
|
+
if errors:
|
|
143
|
+
result = {
|
|
144
|
+
"success": False,
|
|
145
|
+
"error": "Validation failed",
|
|
146
|
+
"details": errors,
|
|
147
|
+
}
|
|
148
|
+
if warnings:
|
|
149
|
+
result["warnings"] = warnings
|
|
150
|
+
return result
|
|
151
|
+
|
|
152
|
+
result = {"success": True, "message": "Agent structure is valid"}
|
|
153
|
+
if warnings:
|
|
154
|
+
result["warnings"] = warnings
|
|
155
|
+
return result
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _validate_tool_signature(func_node: ast.FunctionDef, filename: str) -> list:
|
|
159
|
+
"""
|
|
160
|
+
Validate that tool function uses the params: Dict[str, Any] pattern.
|
|
161
|
+
|
|
162
|
+
The A4E main application expects tools with signature:
|
|
163
|
+
def tool_name(params: Dict[str, Any]) -> Dict[str, Any]
|
|
164
|
+
"""
|
|
165
|
+
errors = []
|
|
166
|
+
func_name = func_node.name
|
|
167
|
+
|
|
168
|
+
# Get function arguments (excluding self)
|
|
169
|
+
args = [arg for arg in func_node.args.args if arg.arg != "self"]
|
|
170
|
+
|
|
171
|
+
# Check if using params: Dict pattern
|
|
172
|
+
if len(args) == 1 and args[0].arg == "params":
|
|
173
|
+
# Good - using params dict pattern
|
|
174
|
+
return errors
|
|
175
|
+
|
|
176
|
+
# Check if using multiple parameters (legacy pattern)
|
|
177
|
+
if len(args) > 1:
|
|
178
|
+
errors.append(
|
|
179
|
+
f"Tool '{func_name}' in {filename} uses individual parameters. "
|
|
180
|
+
f"A4E requires params: Dict[str, Any] pattern. "
|
|
181
|
+
f"Fix: Change signature to 'def {func_name}(params: Dict[str, Any]) -> Dict[str, Any]'"
|
|
182
|
+
)
|
|
183
|
+
elif len(args) == 1 and args[0].arg != "params":
|
|
184
|
+
errors.append(
|
|
185
|
+
f"Tool '{func_name}' in {filename} has single param named '{args[0].arg}'. "
|
|
186
|
+
f"A4E expects the param to be named 'params'. "
|
|
187
|
+
f"Fix: Rename to 'params: Dict[str, Any]'"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
return errors
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _check_exec_compatibility(content: str, filename: str) -> list:
|
|
194
|
+
"""
|
|
195
|
+
Check if support file has exec() context compatibility.
|
|
196
|
+
|
|
197
|
+
Support files (db.py, models.py, etc.) need:
|
|
198
|
+
1. __name__ definition for exec() context
|
|
199
|
+
2. sys.path manipulation for imports
|
|
200
|
+
"""
|
|
201
|
+
warnings = []
|
|
202
|
+
|
|
203
|
+
# Check for __name__ compatibility
|
|
204
|
+
if "if '__name__' not in" not in content and "if \"__name__\" not in" not in content:
|
|
205
|
+
warnings.append(
|
|
206
|
+
f"{filename} may not work in exec() context. "
|
|
207
|
+
f"Add at top: if '__name__' not in dir(): __name__ = \"{filename.replace('.py', '')}\""
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Check for if __name__ == "__main__" pattern
|
|
211
|
+
if 'if __name__ == "__main__"' in content or "if __name__ == '__main__'" in content:
|
|
212
|
+
# Make sure it's using globals().get pattern
|
|
213
|
+
if "globals().get('__name__')" not in content:
|
|
214
|
+
warnings.append(
|
|
215
|
+
f"{filename} uses 'if __name__ == \"__main__\"' which may fail in exec() context. "
|
|
216
|
+
f"Change to: if globals().get('__name__') == '__main__'"
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
return warnings
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _validate_tools_schema_format(schema_file) -> list:
|
|
223
|
+
"""
|
|
224
|
+
Validate that tools/schemas.json is in dictionary format.
|
|
225
|
+
|
|
226
|
+
The A4E main application expects:
|
|
227
|
+
{
|
|
228
|
+
"tool_name": {
|
|
229
|
+
"function": {...},
|
|
230
|
+
"returns": {...}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
NOT an array format:
|
|
235
|
+
[{"name": "tool_name", ...}]
|
|
236
|
+
"""
|
|
237
|
+
errors = []
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
with open(schema_file) as f:
|
|
241
|
+
schemas = json.load(f)
|
|
242
|
+
|
|
243
|
+
if isinstance(schemas, list):
|
|
244
|
+
errors.append(
|
|
245
|
+
"tools/schemas.json is in array format but A4E expects dictionary format. "
|
|
246
|
+
"Run 'generate_schemas(force=True)' to regenerate in correct format."
|
|
247
|
+
)
|
|
248
|
+
elif isinstance(schemas, dict):
|
|
249
|
+
# Check each tool has the correct structure
|
|
250
|
+
for tool_name, tool_schema in schemas.items():
|
|
251
|
+
if "function" not in tool_schema:
|
|
252
|
+
errors.append(
|
|
253
|
+
f"Tool '{tool_name}' schema missing 'function' key. "
|
|
254
|
+
"Expected format: {\"function\": {...}, \"returns\": {...}}"
|
|
255
|
+
)
|
|
256
|
+
except json.JSONDecodeError as e:
|
|
257
|
+
errors.append(f"Invalid JSON in tools/schemas.json: {e}")
|
|
258
|
+
except Exception as e:
|
|
259
|
+
errors.append(f"Error reading tools/schemas.json: {e}")
|
|
260
|
+
|
|
261
|
+
return errors
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _check_agent_prompt(prompt_file) -> list:
|
|
265
|
+
"""
|
|
266
|
+
Check agent prompt for recommended patterns.
|
|
267
|
+
"""
|
|
268
|
+
warnings = []
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
content = prompt_file.read_text()
|
|
272
|
+
|
|
273
|
+
# Check for language policy
|
|
274
|
+
language_patterns = [
|
|
275
|
+
"english only",
|
|
276
|
+
"respond in english",
|
|
277
|
+
"language policy",
|
|
278
|
+
"always respond in english",
|
|
279
|
+
]
|
|
280
|
+
has_language_policy = any(
|
|
281
|
+
pattern in content.lower() for pattern in language_patterns
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
if not has_language_policy:
|
|
285
|
+
warnings.append(
|
|
286
|
+
"Agent prompt (prompts/agent.md) doesn't specify language policy. "
|
|
287
|
+
"Consider adding: '## Language Policy\\n**ALWAYS respond in English only.**'"
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
except Exception:
|
|
291
|
+
pass
|
|
292
|
+
|
|
293
|
+
return warnings
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _validate_skills(skills_dir, tools_dir, views_dir) -> tuple:
|
|
297
|
+
"""
|
|
298
|
+
Validate skills for integrity issues.
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
Tuple of (errors, warnings)
|
|
302
|
+
"""
|
|
303
|
+
errors = []
|
|
304
|
+
warnings = []
|
|
305
|
+
|
|
306
|
+
schema_file = skills_dir / "schemas.json"
|
|
307
|
+
|
|
308
|
+
# Check if schemas.json exists when skill folders exist
|
|
309
|
+
skill_folders = [d for d in skills_dir.iterdir() if d.is_dir()]
|
|
310
|
+
if skill_folders and not schema_file.exists():
|
|
311
|
+
errors.append("Skills exist but skills/schemas.json is missing.")
|
|
312
|
+
return errors, warnings
|
|
313
|
+
|
|
314
|
+
if not schema_file.exists():
|
|
315
|
+
return errors, warnings
|
|
316
|
+
|
|
317
|
+
try:
|
|
318
|
+
schemas = json.loads(schema_file.read_text())
|
|
319
|
+
except json.JSONDecodeError as e:
|
|
320
|
+
errors.append(f"Invalid JSON in skills/schemas.json: {e}")
|
|
321
|
+
return errors, warnings
|
|
322
|
+
|
|
323
|
+
# Get existing tools and views for dependency validation
|
|
324
|
+
existing_tools = set()
|
|
325
|
+
if tools_dir.exists():
|
|
326
|
+
tools_schema = tools_dir / "schemas.json"
|
|
327
|
+
if tools_schema.exists():
|
|
328
|
+
try:
|
|
329
|
+
ts = json.loads(tools_schema.read_text())
|
|
330
|
+
if isinstance(ts, dict):
|
|
331
|
+
existing_tools = set(ts.keys())
|
|
332
|
+
else:
|
|
333
|
+
existing_tools = {t.get("name") for t in ts if isinstance(t, dict)}
|
|
334
|
+
except Exception:
|
|
335
|
+
pass
|
|
336
|
+
|
|
337
|
+
existing_views = set()
|
|
338
|
+
if views_dir.exists():
|
|
339
|
+
existing_views = {d.name for d in views_dir.iterdir() if d.is_dir() and (d / "view.tsx").exists()}
|
|
340
|
+
|
|
341
|
+
# Track triggers for duplicate detection
|
|
342
|
+
trigger_to_skills = defaultdict(list)
|
|
343
|
+
|
|
344
|
+
for skill_id, skill_data in schemas.items():
|
|
345
|
+
# Check SKILL.md exists
|
|
346
|
+
skill_md = skills_dir / skill_id / "SKILL.md"
|
|
347
|
+
if not skill_md.exists():
|
|
348
|
+
warnings.append(f"Skill '{skill_id}' missing SKILL.md documentation")
|
|
349
|
+
else:
|
|
350
|
+
# Check SKILL.md content for correct tool calling pattern
|
|
351
|
+
try:
|
|
352
|
+
skill_content = skill_md.read_text()
|
|
353
|
+
if "render_view(" in skill_content:
|
|
354
|
+
warnings.append(
|
|
355
|
+
f"Skill '{skill_id}' SKILL.md references non-existent 'render_view()' function. "
|
|
356
|
+
"Update to use actual tool names with params dict pattern."
|
|
357
|
+
)
|
|
358
|
+
except Exception:
|
|
359
|
+
pass
|
|
360
|
+
|
|
361
|
+
# Check output view exists
|
|
362
|
+
output = skill_data.get("output", {})
|
|
363
|
+
view_id = output.get("view")
|
|
364
|
+
if view_id and view_id != "NONE" and view_id not in existing_views:
|
|
365
|
+
errors.append(f"Skill '{skill_id}' references non-existent view '{view_id}'")
|
|
366
|
+
|
|
367
|
+
# Check internal tools exist
|
|
368
|
+
internal_tools = skill_data.get("internal_tools", [])
|
|
369
|
+
for tool in internal_tools:
|
|
370
|
+
if tool not in existing_tools:
|
|
371
|
+
warnings.append(f"Skill '{skill_id}' references tool '{tool}' not in schemas")
|
|
372
|
+
|
|
373
|
+
# Collect triggers for duplicate detection
|
|
374
|
+
triggers = skill_data.get("intent_triggers", [])
|
|
375
|
+
for trigger in triggers:
|
|
376
|
+
trigger_lower = trigger.lower().strip()
|
|
377
|
+
trigger_to_skills[trigger_lower].append(skill_id)
|
|
378
|
+
|
|
379
|
+
# Check for orphan SKILL.md (folder exists but not in schemas.json)
|
|
380
|
+
for skill_folder in skill_folders:
|
|
381
|
+
if skill_folder.name not in schemas:
|
|
382
|
+
warnings.append(f"Orphan skill folder '{skill_folder.name}' not in schemas.json")
|
|
383
|
+
|
|
384
|
+
# Detect duplicate triggers
|
|
385
|
+
for trigger, skills in trigger_to_skills.items():
|
|
386
|
+
if len(skills) > 1:
|
|
387
|
+
warnings.append(f"Duplicate trigger '{trigger}' in skills: {', '.join(skills)}")
|
|
388
|
+
|
|
389
|
+
return errors, warnings
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Views management tools.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .add_view import add_view
|
|
6
|
+
from .list_views import list_views
|
|
7
|
+
from .remove_view import remove_view
|
|
8
|
+
from .update_view import update_view
|
|
9
|
+
from .helpers import create_view
|
|
10
|
+
|
|
11
|
+
__all__ = ["add_view", "list_views", "remove_view", "update_view", "create_view"]
|
|
12
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Add view tool.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from ...core import mcp, get_project_dir
|
|
8
|
+
from .helpers import create_view
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@mcp.tool()
|
|
12
|
+
def add_view(
|
|
13
|
+
view_id: str, description: str, props: dict, agent_name: Optional[str] = None
|
|
14
|
+
) -> dict:
|
|
15
|
+
"""
|
|
16
|
+
Add a new React view
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
view_id: ID of the view (snake_case, e.g., "meal_plan")
|
|
20
|
+
description: View purpose
|
|
21
|
+
props: Dictionary of props with types
|
|
22
|
+
agent_name: Optional agent ID if not in agent directory
|
|
23
|
+
"""
|
|
24
|
+
# Validate view ID
|
|
25
|
+
if not view_id.replace("_", "").isalnum():
|
|
26
|
+
return {
|
|
27
|
+
"success": False,
|
|
28
|
+
"error": "View ID must be alphanumeric with underscores",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
project_dir = get_project_dir(agent_name)
|
|
32
|
+
result = create_view(view_id, description, props, project_dir)
|
|
33
|
+
|
|
34
|
+
# Auto-generate schemas after adding view
|
|
35
|
+
if result.get("success"):
|
|
36
|
+
from ..schemas import generate_schemas
|
|
37
|
+
generate_schemas(force=False, agent_name=agent_name)
|
|
38
|
+
|
|
39
|
+
return result
|
|
40
|
+
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Helper functions for view management.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from ...core import jinja_env
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def create_view(
|
|
12
|
+
view_id: str, description: str, props: dict, project_dir: Path
|
|
13
|
+
) -> dict:
|
|
14
|
+
"""
|
|
15
|
+
Helper to create a view directory with view.tsx and view.schema.json files.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
view_id: ID of the view (snake_case)
|
|
19
|
+
description: View purpose
|
|
20
|
+
props: Dictionary of props with types (e.g., {"title": "string", "count": "number"})
|
|
21
|
+
project_dir: Path to the agent project directory
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Result dictionary with success status
|
|
25
|
+
"""
|
|
26
|
+
views_dir = project_dir / "views"
|
|
27
|
+
|
|
28
|
+
if not views_dir.exists():
|
|
29
|
+
return {
|
|
30
|
+
"success": False,
|
|
31
|
+
"error": f"views/ directory not found at {views_dir}",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
view_dir = views_dir / view_id
|
|
35
|
+
if view_dir.exists():
|
|
36
|
+
return {"success": False, "error": f"View '{view_id}' already exists"}
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
view_dir.mkdir()
|
|
40
|
+
|
|
41
|
+
# Convert snake_case to PascalCase for component name
|
|
42
|
+
view_name = "".join(word.title() for word in view_id.split("_"))
|
|
43
|
+
|
|
44
|
+
# Create view.tsx
|
|
45
|
+
template = jinja_env.get_template("view.tsx.j2")
|
|
46
|
+
code = template.render(
|
|
47
|
+
view_name=view_name, description=description, props=props
|
|
48
|
+
)
|
|
49
|
+
(view_dir / "view.tsx").write_text(code)
|
|
50
|
+
|
|
51
|
+
# Create view.schema.json (required by A4E View Renderer)
|
|
52
|
+
schema_properties = {}
|
|
53
|
+
required_props = []
|
|
54
|
+
for prop_name, prop_type in props.items():
|
|
55
|
+
# Handle both simple types ("string") and detailed types ({"type": "string", "description": "..."})
|
|
56
|
+
if isinstance(prop_type, dict):
|
|
57
|
+
schema_properties[prop_name] = {
|
|
58
|
+
"type": prop_type.get("type", "string"),
|
|
59
|
+
"description": prop_type.get("description", f"The {prop_name} prop"),
|
|
60
|
+
}
|
|
61
|
+
if prop_type.get("required", True):
|
|
62
|
+
required_props.append(prop_name)
|
|
63
|
+
else:
|
|
64
|
+
schema_properties[prop_name] = {
|
|
65
|
+
"type": prop_type,
|
|
66
|
+
"description": f"The {prop_name} prop",
|
|
67
|
+
}
|
|
68
|
+
required_props.append(prop_name)
|
|
69
|
+
|
|
70
|
+
view_schema = {
|
|
71
|
+
"name": view_id,
|
|
72
|
+
"description": description,
|
|
73
|
+
"props": {
|
|
74
|
+
"type": "object",
|
|
75
|
+
"properties": schema_properties,
|
|
76
|
+
"required": required_props,
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
(view_dir / "view.schema.json").write_text(
|
|
80
|
+
json.dumps(view_schema, indent=2)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
"success": True,
|
|
85
|
+
"message": f"Created view '{view_id}' with view.tsx and view.schema.json",
|
|
86
|
+
"path": str(view_dir),
|
|
87
|
+
"files": ["view.tsx", "view.schema.json"],
|
|
88
|
+
}
|
|
89
|
+
except Exception as e:
|
|
90
|
+
return {"success": False, "error": str(e)}
|
|
91
|
+
|