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,285 @@
1
+ # a4e/cli_commands/update.py
2
+ """
3
+ Commands for updating tools, views, and skills in an agent.
4
+ """
5
+
6
+ import typer
7
+ import json
8
+ from pathlib import Path
9
+ from typing import Optional
10
+
11
+ from rich.console import Console
12
+ from rich.prompt import Prompt, Confirm
13
+
14
+ console = Console()
15
+
16
+ # Create a 'Typer' app for the 'update' command group
17
+ app = typer.Typer(
18
+ no_args_is_help=True,
19
+ help="Update existing tools, views, or skills.",
20
+ )
21
+
22
+ TYPE_OPTIONS = ["string", "number", "integer", "boolean", "array", "object"]
23
+
24
+
25
+ def find_agent_dir(agent_name: Optional[str] = None) -> Optional[Path]:
26
+ """Find the agent directory from name or current working directory."""
27
+ cwd = Path.cwd()
28
+
29
+ if agent_name:
30
+ if Path(agent_name).is_absolute() and Path(agent_name).exists():
31
+ return Path(agent_name)
32
+ if (cwd / agent_name).exists():
33
+ return cwd / agent_name
34
+ agent_store = cwd / "file-store" / "agent-store" / agent_name
35
+ if agent_store.exists():
36
+ return agent_store
37
+ return None
38
+
39
+ if (cwd / "agent.py").exists() and (cwd / "metadata.json").exists():
40
+ return cwd
41
+
42
+ return None
43
+
44
+
45
+ @app.command("tool")
46
+ def update_tool(
47
+ tool_name: Optional[str] = typer.Argument(None, help="Name of the tool to update"),
48
+ description: Optional[str] = typer.Option(
49
+ None, "--description", "-d", help="New description"
50
+ ),
51
+ parameters_json: Optional[str] = typer.Option(
52
+ None, "--parameters", "-p", help="New parameters as JSON string"
53
+ ),
54
+ agent_name: Optional[str] = typer.Option(
55
+ None, "--agent", "-a", help="Agent name/path"
56
+ ),
57
+ ) -> None:
58
+ """
59
+ Update an existing tool's description or parameters.
60
+
61
+ Example:
62
+ a4e update tool calculate_bmi -d "New description"
63
+ a4e update tool calculate_bmi -p '{"weight": "number", "height": "number"}'
64
+ """
65
+ agent_dir = find_agent_dir(agent_name)
66
+ if not agent_dir:
67
+ console.print("[red]Error: Not in an agent directory. Use --agent to specify the agent.[/red]")
68
+ raise typer.Exit(code=1)
69
+
70
+ # List available tools if none specified
71
+ if not tool_name:
72
+ tools_dir = agent_dir / "tools"
73
+ if tools_dir.exists():
74
+ available = [f.stem for f in tools_dir.glob("*.py") if f.stem != "__init__"]
75
+ if available:
76
+ console.print("[bold]Available tools:[/bold]")
77
+ for t in available:
78
+ console.print(f" • {t}")
79
+ tool_name = Prompt.ask("[bold]Tool to update[/bold]")
80
+ else:
81
+ console.print("[red]No tools directory found[/red]")
82
+ raise typer.Exit(code=1)
83
+
84
+ if not description and not parameters_json:
85
+ console.print("[yellow]What would you like to update?[/yellow]")
86
+ update_desc = Confirm.ask("Update description?", default=False)
87
+ if update_desc:
88
+ description = Prompt.ask("New description")
89
+
90
+ update_params = Confirm.ask("Update parameters?", default=False)
91
+ if update_params:
92
+ console.print("[dim]Enter parameters as JSON or use interactive mode[/dim]")
93
+ parameters_json = Prompt.ask("Parameters JSON", default="{}")
94
+
95
+ parameters = None
96
+ if parameters_json:
97
+ try:
98
+ parameters = json.loads(parameters_json)
99
+ except json.JSONDecodeError:
100
+ console.print("[red]Invalid JSON for parameters[/red]")
101
+ raise typer.Exit(code=1)
102
+
103
+ try:
104
+ from ..tools.agent_tools.update_tool import update_tool as mcp_update_tool
105
+
106
+ result = mcp_update_tool(
107
+ tool_name=tool_name,
108
+ description=description,
109
+ parameters=parameters,
110
+ agent_name=str(agent_dir),
111
+ )
112
+
113
+ if result.get("success"):
114
+ console.print(f"\n[green]✓ Tool '{tool_name}' updated![/green]")
115
+ console.print(f" Path: {result.get('path')}")
116
+ else:
117
+ console.print(f"\n[red]Error: {result.get('error')}[/red]")
118
+ if result.get("fix"):
119
+ console.print(f" [yellow]Fix: {result.get('fix')}[/yellow]")
120
+ raise typer.Exit(code=1)
121
+
122
+ except ImportError as e:
123
+ console.print(f"[red]Error importing tools: {e}[/red]")
124
+ raise typer.Exit(code=1)
125
+
126
+
127
+ @app.command("view")
128
+ def update_view(
129
+ view_id: Optional[str] = typer.Argument(None, help="ID of the view to update"),
130
+ description: Optional[str] = typer.Option(
131
+ None, "--description", "-d", help="New description"
132
+ ),
133
+ props_json: Optional[str] = typer.Option(
134
+ None, "--props", "-p", help="New props as JSON string"
135
+ ),
136
+ agent_name: Optional[str] = typer.Option(
137
+ None, "--agent", "-a", help="Agent name/path"
138
+ ),
139
+ ) -> None:
140
+ """
141
+ Update an existing view's description or props.
142
+
143
+ Example:
144
+ a4e update view dashboard -d "New description"
145
+ a4e update view dashboard -p '{"title": "string", "items": "array"}'
146
+ """
147
+ agent_dir = find_agent_dir(agent_name)
148
+ if not agent_dir:
149
+ console.print("[red]Error: Not in an agent directory. Use --agent to specify the agent.[/red]")
150
+ raise typer.Exit(code=1)
151
+
152
+ # List available views if none specified
153
+ if not view_id:
154
+ views_dir = agent_dir / "views"
155
+ if views_dir.exists():
156
+ available = [d.name for d in views_dir.iterdir() if d.is_dir() and not d.name.startswith("_")]
157
+ if available:
158
+ console.print("[bold]Available views:[/bold]")
159
+ for v in available:
160
+ console.print(f" • {v}")
161
+ view_id = Prompt.ask("[bold]View to update[/bold]")
162
+ else:
163
+ console.print("[red]No views directory found[/red]")
164
+ raise typer.Exit(code=1)
165
+
166
+ if not description and not props_json:
167
+ console.print("[yellow]What would you like to update?[/yellow]")
168
+ update_desc = Confirm.ask("Update description?", default=False)
169
+ if update_desc:
170
+ description = Prompt.ask("New description")
171
+
172
+ update_props = Confirm.ask("Update props?", default=False)
173
+ if update_props:
174
+ props_json = Prompt.ask("Props JSON", default="{}")
175
+
176
+ props = None
177
+ if props_json:
178
+ try:
179
+ props = json.loads(props_json)
180
+ except json.JSONDecodeError:
181
+ console.print("[red]Invalid JSON for props[/red]")
182
+ raise typer.Exit(code=1)
183
+
184
+ try:
185
+ from ..tools.views.update_view import update_view as mcp_update_view
186
+
187
+ result = mcp_update_view(
188
+ view_id=view_id,
189
+ description=description,
190
+ props=props,
191
+ agent_name=str(agent_dir),
192
+ )
193
+
194
+ if result.get("success"):
195
+ console.print(f"\n[green]✓ View '{view_id}' updated![/green]")
196
+ console.print(f" Path: {result.get('path')}")
197
+ else:
198
+ console.print(f"\n[red]Error: {result.get('error')}[/red]")
199
+ if result.get("fix"):
200
+ console.print(f" [yellow]Fix: {result.get('fix')}[/yellow]")
201
+ raise typer.Exit(code=1)
202
+
203
+ except ImportError as e:
204
+ console.print(f"[red]Error importing tools: {e}[/red]")
205
+ raise typer.Exit(code=1)
206
+
207
+
208
+ @app.command("skill")
209
+ def update_skill(
210
+ skill_id: Optional[str] = typer.Argument(None, help="ID of the skill to update"),
211
+ name: Optional[str] = typer.Option(None, "--name", "-n", help="New display name"),
212
+ description: Optional[str] = typer.Option(None, "--description", "-d", help="New description"),
213
+ intent_triggers: Optional[str] = typer.Option(
214
+ None, "--triggers", "-t", help="New comma-separated triggers"
215
+ ),
216
+ output_view: Optional[str] = typer.Option(None, "--view", "-v", help="New output view"),
217
+ internal_tools: Optional[str] = typer.Option(
218
+ None, "--tools", help="New comma-separated internal tools"
219
+ ),
220
+ requires_auth: Optional[bool] = typer.Option(None, "--auth/--no-auth", help="Requires auth"),
221
+ agent_name: Optional[str] = typer.Option(None, "--agent", "-a", help="Agent name/path"),
222
+ ) -> None:
223
+ """
224
+ Update an existing skill's properties.
225
+
226
+ Example:
227
+ a4e update skill show_results --name "Display Results" --view new-results
228
+ """
229
+ agent_dir = find_agent_dir(agent_name)
230
+ if not agent_dir:
231
+ console.print("[red]Error: Not in an agent directory. Use --agent to specify the agent.[/red]")
232
+ raise typer.Exit(code=1)
233
+
234
+ # List available skills if none specified
235
+ if not skill_id:
236
+ skills_file = agent_dir / "skills" / "schemas.json"
237
+ if skills_file.exists():
238
+ try:
239
+ schemas = json.loads(skills_file.read_text())
240
+ available = list(schemas.keys())
241
+ if available:
242
+ console.print("[bold]Available skills:[/bold]")
243
+ for s in available:
244
+ console.print(f" • {s}")
245
+ skill_id = Prompt.ask("[bold]Skill to update[/bold]")
246
+ except json.JSONDecodeError:
247
+ console.print("[red]Could not parse skills/schemas.json[/red]")
248
+ raise typer.Exit(code=1)
249
+ else:
250
+ console.print("[red]No skills/schemas.json found[/red]")
251
+ raise typer.Exit(code=1)
252
+
253
+ # Parse list options
254
+ triggers_list = [t.strip() for t in intent_triggers.split(",")] if intent_triggers else None
255
+ tools_list = [t.strip() for t in internal_tools.split(",") if t.strip()] if internal_tools else None
256
+
257
+ try:
258
+ from ..tools.skills.update_skill import update_skill as mcp_update_skill
259
+
260
+ result = mcp_update_skill(
261
+ skill_id=skill_id,
262
+ name=name,
263
+ description=description,
264
+ intent_triggers=triggers_list,
265
+ output_view=output_view,
266
+ internal_tools=tools_list,
267
+ requires_auth=requires_auth,
268
+ agent_name=str(agent_dir),
269
+ )
270
+
271
+ if result.get("success"):
272
+ console.print(f"\n[green]✓ Skill '{skill_id}' updated![/green]")
273
+ console.print(f" Path: {result.get('path')}")
274
+ if result.get("warnings"):
275
+ for w in result["warnings"]:
276
+ console.print(f" [yellow]Warning: {w}[/yellow]")
277
+ else:
278
+ console.print(f"\n[red]Error: {result.get('error')}[/red]")
279
+ if result.get("fix"):
280
+ console.print(f" [yellow]Fix: {result.get('fix')}[/yellow]")
281
+ raise typer.Exit(code=1)
282
+
283
+ except ImportError as e:
284
+ console.print(f"[red]Error importing tools: {e}[/red]")
285
+ raise typer.Exit(code=1)
@@ -0,0 +1,117 @@
1
+ # a4e/cli_commands/validate.py
2
+ """
3
+ Command for validating an agent project.
4
+ """
5
+
6
+ import typer
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+ from rich.console import Console
11
+ from rich.table import Table
12
+ from rich.panel import Panel
13
+
14
+ console = Console()
15
+
16
+ # Create a 'Typer' app for the 'validate' command
17
+ app = typer.Typer(
18
+ no_args_is_help=False,
19
+ help="Validate your agent project for errors and warnings.",
20
+ )
21
+
22
+
23
+ def find_agent_dir(agent_name: Optional[str] = None) -> Optional[Path]:
24
+ """Find the agent directory from name or current working directory."""
25
+ cwd = Path.cwd()
26
+
27
+ if agent_name:
28
+ if Path(agent_name).is_absolute() and Path(agent_name).exists():
29
+ return Path(agent_name)
30
+ if (cwd / agent_name).exists():
31
+ return cwd / agent_name
32
+ agent_store = cwd / "file-store" / "agent-store" / agent_name
33
+ if agent_store.exists():
34
+ return agent_store
35
+ return None
36
+
37
+ if (cwd / "agent.py").exists() and (cwd / "metadata.json").exists():
38
+ return cwd
39
+
40
+ return None
41
+
42
+
43
+ @app.callback(invoke_without_command=True)
44
+ def validate(
45
+ ctx: typer.Context,
46
+ agent_name: Optional[str] = typer.Option(
47
+ None, "--agent", "-a", help="Agent name/path (defaults to current directory)"
48
+ ),
49
+ strict: bool = typer.Option(
50
+ False, "--strict", "-s", help="Treat warnings as errors"
51
+ ),
52
+ ) -> None:
53
+ """
54
+ Validate the agent project structure and code.
55
+
56
+ Checks for:
57
+ - Required files (agent.py, metadata.json, prompts/agent.md, etc.)
58
+ - Python syntax errors
59
+ - Type hints on public functions
60
+ - Schema existence
61
+ - Skill dependencies (views, tools)
62
+
63
+ Example:
64
+ a4e validate
65
+ a4e validate --agent my-agent --strict
66
+ """
67
+ agent_dir = find_agent_dir(agent_name)
68
+ if not agent_dir:
69
+ console.print("[red]Error: Not in an agent directory. Use --agent to specify the agent.[/red]")
70
+ raise typer.Exit(code=1)
71
+
72
+ console.print(Panel.fit(
73
+ f"[bold]Validating agent:[/bold] {agent_dir.name}",
74
+ border_style="blue"
75
+ ))
76
+
77
+ try:
78
+ from ..tools.validation.validate import validate as mcp_validate
79
+
80
+ result = mcp_validate(agent_name=str(agent_dir))
81
+
82
+ if result.get("success"):
83
+ errors = result.get("errors", [])
84
+ warnings = result.get("warnings", [])
85
+
86
+ # Show errors
87
+ if errors:
88
+ console.print(f"\n[red bold]Errors ({len(errors)}):[/red bold]")
89
+ for error in errors:
90
+ console.print(f" [red]✗[/red] {error}")
91
+
92
+ # Show warnings
93
+ if warnings:
94
+ console.print(f"\n[yellow bold]Warnings ({len(warnings)}):[/yellow bold]")
95
+ for warning in warnings:
96
+ console.print(f" [yellow]⚠[/yellow] {warning}")
97
+
98
+ # Summary
99
+ console.print("")
100
+ if errors:
101
+ console.print(f"[red bold]Validation failed with {len(errors)} error(s)[/red bold]")
102
+ raise typer.Exit(code=1)
103
+ elif warnings and strict:
104
+ console.print(f"[yellow bold]Validation failed with {len(warnings)} warning(s) (strict mode)[/yellow bold]")
105
+ raise typer.Exit(code=1)
106
+ elif warnings:
107
+ console.print(f"[green]✓ Validation passed with {len(warnings)} warning(s)[/green]")
108
+ else:
109
+ console.print("[green bold]✓ Validation passed - no issues found![/green bold]")
110
+
111
+ else:
112
+ console.print(f"[red]Error: {result.get('error')}[/red]")
113
+ raise typer.Exit(code=1)
114
+
115
+ except ImportError as e:
116
+ console.print(f"[red]Error importing tools: {e}[/red]")
117
+ raise typer.Exit(code=1)
a4e/core.py ADDED
@@ -0,0 +1,109 @@
1
+ """
2
+ Core module with shared utilities and MCP instance.
3
+ """
4
+
5
+ from mcp.server.fastmcp import FastMCP
6
+ from pathlib import Path
7
+ from typing import Optional
8
+ import os
9
+ import re
10
+ from jinja2 import Environment, FileSystemLoader
11
+
12
+ # Global project directory (set by CLI args)
13
+ _PROJECT_DIR: Optional[Path] = None
14
+
15
+ # Initialize MCP server
16
+ mcp = FastMCP(name="a4e-agent-creator")
17
+
18
+ # Load templates
19
+ template_dir = Path(__file__).parent / "templates"
20
+ jinja_env = Environment(
21
+ loader=FileSystemLoader(template_dir),
22
+ autoescape=False, # Explicit for code generation
23
+ )
24
+
25
+
26
+ def set_project_dir(path: Path) -> None:
27
+ """Set the global project directory."""
28
+ global _PROJECT_DIR
29
+ _PROJECT_DIR = path
30
+
31
+
32
+ def get_configured_project_dir() -> Optional[Path]:
33
+ """Get the configured project directory."""
34
+ return _PROJECT_DIR
35
+
36
+
37
+ def sanitize_input(value: str, allowed_chars: str = r"a-zA-Z0-9_-") -> str:
38
+ """
39
+ Sanitize user input to prevent template injection.
40
+
41
+ Args:
42
+ value: Input string to sanitize
43
+ allowed_chars: Regex character class of allowed characters
44
+
45
+ Returns:
46
+ Sanitized string with only allowed characters
47
+ """
48
+ pattern = f"[^{allowed_chars}]"
49
+ return re.sub(pattern, "", value)
50
+
51
+
52
+ def get_project_dir(agent_name: Optional[str] = None) -> Path:
53
+ """
54
+ Resolve the agent project directory.
55
+
56
+ Priority (highest to lowest):
57
+ 1. --project-dir CLI arg (explicit override)
58
+ 2. A4E_WORKSPACE env var (set by editor via ${workspaceFolder})
59
+ 3. Path.cwd() (fallback)
60
+
61
+ Note: Tools should prefer using explicit project_path parameter when available,
62
+ as this function relies on environment context that may not be available
63
+ in all MCP clients.
64
+
65
+ Args:
66
+ agent_name: Optional agent ID to resolve path for
67
+
68
+ Returns:
69
+ Path to agent directory or project root
70
+
71
+ Raises:
72
+ ValueError: If agent creation attempted in invalid location
73
+ """
74
+ global _PROJECT_DIR
75
+
76
+ root = None
77
+ workspace_env = os.environ.get("A4E_WORKSPACE", "")
78
+
79
+ # Priority 1: Explicit CLI override
80
+ if _PROJECT_DIR:
81
+ root = _PROJECT_DIR
82
+ # Priority 2: Workspace from editor (if properly expanded)
83
+ elif workspace_env and "${" not in workspace_env:
84
+ # Only use if the variable was expanded and path exists
85
+ workspace_path = Path(workspace_env)
86
+ if workspace_path.exists() and workspace_path.is_dir():
87
+ root = workspace_path.resolve()
88
+
89
+ # Priority 3: Fallback to cwd
90
+ if root is None:
91
+ root = Path.cwd()
92
+
93
+ if not agent_name:
94
+ return root
95
+
96
+ # Safety: Warn if creating in HOME directory without explicit path
97
+ if root == Path.home():
98
+ raise ValueError(
99
+ f"Cannot create agent in HOME directory.\n"
100
+ f"\n"
101
+ f"Please specify project_path in your tool call:\n"
102
+ f' initialize_project(name="{agent_name}", project_path="/path/to/your/project", ...)\n'
103
+ f"\n"
104
+ f"The agent will be created at: {{project_path}}/{agent_name}/"
105
+ )
106
+
107
+ # Create agent directly in the workspace root
108
+ return root / agent_name
109
+