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.
Files changed (70) hide show
  1. a4e/__init__.py +0 -0
  2. a4e/cli.py +47 -0
  3. a4e/cli_commands/__init__.py +5 -0
  4. a4e/cli_commands/add.py +376 -0
  5. a4e/cli_commands/deploy.py +149 -0
  6. a4e/cli_commands/dev.py +162 -0
  7. a4e/cli_commands/info.py +206 -0
  8. a4e/cli_commands/init.py +211 -0
  9. a4e/cli_commands/list.py +227 -0
  10. a4e/cli_commands/mcp.py +504 -0
  11. a4e/cli_commands/remove.py +197 -0
  12. a4e/cli_commands/update.py +285 -0
  13. a4e/cli_commands/validate.py +117 -0
  14. a4e/core.py +109 -0
  15. a4e/dev_runner.py +425 -0
  16. a4e/server.py +86 -0
  17. a4e/templates/agent.md.j2 +168 -0
  18. a4e/templates/agent.py.j2 +15 -0
  19. a4e/templates/agents.md.j2 +99 -0
  20. a4e/templates/metadata.json.j2 +20 -0
  21. a4e/templates/prompt.md.j2 +20 -0
  22. a4e/templates/prompts/agent.md.j2 +206 -0
  23. a4e/templates/skills/agents.md.j2 +110 -0
  24. a4e/templates/skills/skill.md.j2 +120 -0
  25. a4e/templates/support_module.py.j2 +84 -0
  26. a4e/templates/tool.py.j2 +60 -0
  27. a4e/templates/tools/agent.md.j2 +192 -0
  28. a4e/templates/view.tsx.j2 +21 -0
  29. a4e/templates/views/agent.md.j2 +219 -0
  30. a4e/tools/__init__.py +70 -0
  31. a4e/tools/agent_tools/__init__.py +12 -0
  32. a4e/tools/agent_tools/add_support_module.py +95 -0
  33. a4e/tools/agent_tools/add_tool.py +115 -0
  34. a4e/tools/agent_tools/list_tools.py +28 -0
  35. a4e/tools/agent_tools/remove_tool.py +69 -0
  36. a4e/tools/agent_tools/update_tool.py +123 -0
  37. a4e/tools/deploy/__init__.py +8 -0
  38. a4e/tools/deploy/deploy.py +59 -0
  39. a4e/tools/dev/__init__.py +10 -0
  40. a4e/tools/dev/check_environment.py +79 -0
  41. a4e/tools/dev/dev_start.py +30 -0
  42. a4e/tools/dev/dev_stop.py +26 -0
  43. a4e/tools/project/__init__.py +10 -0
  44. a4e/tools/project/get_agent_info.py +66 -0
  45. a4e/tools/project/get_instructions.py +216 -0
  46. a4e/tools/project/initialize_project.py +231 -0
  47. a4e/tools/schemas/__init__.py +8 -0
  48. a4e/tools/schemas/generate_schemas.py +278 -0
  49. a4e/tools/skills/__init__.py +12 -0
  50. a4e/tools/skills/add_skill.py +105 -0
  51. a4e/tools/skills/helpers.py +137 -0
  52. a4e/tools/skills/list_skills.py +54 -0
  53. a4e/tools/skills/remove_skill.py +74 -0
  54. a4e/tools/skills/update_skill.py +150 -0
  55. a4e/tools/validation/__init__.py +8 -0
  56. a4e/tools/validation/validate.py +389 -0
  57. a4e/tools/views/__init__.py +12 -0
  58. a4e/tools/views/add_view.py +40 -0
  59. a4e/tools/views/helpers.py +91 -0
  60. a4e/tools/views/list_views.py +27 -0
  61. a4e/tools/views/remove_view.py +73 -0
  62. a4e/tools/views/update_view.py +124 -0
  63. a4e/utils/dev_manager.py +253 -0
  64. a4e/utils/schema_generator.py +255 -0
  65. a4e-0.1.5.dist-info/METADATA +427 -0
  66. a4e-0.1.5.dist-info/RECORD +70 -0
  67. a4e-0.1.5.dist-info/WHEEL +5 -0
  68. a4e-0.1.5.dist-info/entry_points.txt +2 -0
  69. a4e-0.1.5.dist-info/licenses/LICENSE +21 -0
  70. 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,8 @@
1
+ """
2
+ Validation tools.
3
+ """
4
+
5
+ from .validate import validate
6
+
7
+ __all__ = ["validate"]
8
+
@@ -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
+