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
a4e/dev_runner.py
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
import importlib.util
|
|
4
|
+
import inspect
|
|
5
|
+
import json
|
|
6
|
+
import argparse
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from types import ModuleType
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Mock a4e SDK and autogen
|
|
13
|
+
def _mock_dependencies():
|
|
14
|
+
"""Mock a4e.sdk and autogen_agentchat to allow agent.py to load"""
|
|
15
|
+
if "a4e" not in sys.modules:
|
|
16
|
+
a4e = ModuleType("a4e")
|
|
17
|
+
sdk = ModuleType("a4e.sdk")
|
|
18
|
+
|
|
19
|
+
class MockAgentFactory:
|
|
20
|
+
@staticmethod
|
|
21
|
+
async def create_agent(*args, **kwargs):
|
|
22
|
+
return "MockAgentInstance"
|
|
23
|
+
|
|
24
|
+
sdk.AgentFactory = MockAgentFactory
|
|
25
|
+
|
|
26
|
+
# Mock tool decorator
|
|
27
|
+
def tool(func):
|
|
28
|
+
func._is_tool = True
|
|
29
|
+
return func
|
|
30
|
+
|
|
31
|
+
sdk.tool = tool
|
|
32
|
+
|
|
33
|
+
a4e.sdk = sdk
|
|
34
|
+
sys.modules["a4e"] = a4e
|
|
35
|
+
sys.modules["a4e.sdk"] = sdk
|
|
36
|
+
|
|
37
|
+
if "autogen_agentchat" not in sys.modules:
|
|
38
|
+
autogen = ModuleType("autogen_agentchat")
|
|
39
|
+
agents = ModuleType("autogen_agentchat.agents")
|
|
40
|
+
|
|
41
|
+
class AssistantAgent:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
agents.AssistantAgent = AssistantAgent
|
|
45
|
+
autogen.agents = agents
|
|
46
|
+
sys.modules["autogen_agentchat"] = autogen
|
|
47
|
+
sys.modules["autogen_agentchat.agents"] = agents
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def run_agent_server(agent_path: Path, port: int):
|
|
51
|
+
"""Run the agent in a FastMCP server with REST API endpoints"""
|
|
52
|
+
from mcp.server.fastmcp import FastMCP
|
|
53
|
+
from starlette.applications import Starlette
|
|
54
|
+
from starlette.routing import Route, Mount
|
|
55
|
+
from starlette.responses import JSONResponse, Response
|
|
56
|
+
from starlette.middleware.cors import CORSMiddleware
|
|
57
|
+
import zipfile
|
|
58
|
+
import io
|
|
59
|
+
|
|
60
|
+
_mock_dependencies()
|
|
61
|
+
|
|
62
|
+
agent_name = agent_path.name
|
|
63
|
+
mcp = FastMCP(name=agent_name)
|
|
64
|
+
|
|
65
|
+
# Load metadata
|
|
66
|
+
metadata_path = agent_path / "metadata.json"
|
|
67
|
+
metadata = {}
|
|
68
|
+
if metadata_path.exists():
|
|
69
|
+
metadata = json.loads(metadata_path.read_text())
|
|
70
|
+
|
|
71
|
+
# Load tools schemas
|
|
72
|
+
tools_schemas_path = agent_path / "tools" / "schemas.json"
|
|
73
|
+
tools_schemas = {}
|
|
74
|
+
if tools_schemas_path.exists():
|
|
75
|
+
tools_schemas = json.loads(tools_schemas_path.read_text())
|
|
76
|
+
|
|
77
|
+
# Load views schemas
|
|
78
|
+
views_schemas_path = agent_path / "views" / "schemas.json"
|
|
79
|
+
views_schemas = {}
|
|
80
|
+
if views_schemas_path.exists():
|
|
81
|
+
views_schemas = json.loads(views_schemas_path.read_text())
|
|
82
|
+
|
|
83
|
+
# Load skills schemas
|
|
84
|
+
skills_schemas_path = agent_path / "skills" / "schemas.json"
|
|
85
|
+
skills_schemas = {}
|
|
86
|
+
if skills_schemas_path.exists():
|
|
87
|
+
skills_schemas = json.loads(skills_schemas_path.read_text())
|
|
88
|
+
|
|
89
|
+
# Load system prompt
|
|
90
|
+
prompt_path = agent_path / "prompts" / "agent.md"
|
|
91
|
+
system_prompt = ""
|
|
92
|
+
if prompt_path.exists():
|
|
93
|
+
system_prompt = prompt_path.read_text()
|
|
94
|
+
|
|
95
|
+
# Load agent tools into MCP
|
|
96
|
+
tools_dir = agent_path / "tools"
|
|
97
|
+
if tools_dir.exists():
|
|
98
|
+
sys.path.insert(0, str(agent_path))
|
|
99
|
+
for tool_file in tools_dir.glob("*.py"):
|
|
100
|
+
if tool_file.name == "__init__.py":
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
spec = importlib.util.spec_from_file_location(tool_file.stem, tool_file)
|
|
105
|
+
if spec and spec.loader:
|
|
106
|
+
module = importlib.util.module_from_spec(spec)
|
|
107
|
+
spec.loader.exec_module(module)
|
|
108
|
+
|
|
109
|
+
for name, obj in inspect.getmembers(module):
|
|
110
|
+
if inspect.isfunction(obj):
|
|
111
|
+
if (
|
|
112
|
+
getattr(obj, "_is_tool", False)
|
|
113
|
+
or name == tool_file.stem
|
|
114
|
+
):
|
|
115
|
+
# Register with FastMCP
|
|
116
|
+
mcp.tool()(obj)
|
|
117
|
+
print(f"Registered tool: {name}")
|
|
118
|
+
except Exception as e:
|
|
119
|
+
print(f"Failed to load tool {tool_file}: {e}")
|
|
120
|
+
|
|
121
|
+
# Add system prompt resource
|
|
122
|
+
@mcp.resource("agent://system_prompt")
|
|
123
|
+
def get_system_prompt() -> str:
|
|
124
|
+
return system_prompt or "You are a helpful assistant."
|
|
125
|
+
|
|
126
|
+
# REST API Endpoints
|
|
127
|
+
async def agent_info(request):
|
|
128
|
+
return JSONResponse(metadata)
|
|
129
|
+
|
|
130
|
+
async def get_tools(request):
|
|
131
|
+
# Convert schema format to frontend expected format
|
|
132
|
+
# tools_schemas is a dict: {"tool_name": {"name": "...", "parameters": {...}}}
|
|
133
|
+
tools = []
|
|
134
|
+
for tool_name, schema in tools_schemas.items():
|
|
135
|
+
params = []
|
|
136
|
+
# Handle both "parameters" and "inputSchema" formats
|
|
137
|
+
param_schema = schema.get("parameters") or schema.get("inputSchema", {})
|
|
138
|
+
if "properties" in param_schema:
|
|
139
|
+
required = param_schema.get("required", [])
|
|
140
|
+
for param_name, param_info in param_schema["properties"].items():
|
|
141
|
+
params.append(
|
|
142
|
+
{
|
|
143
|
+
"name": param_name,
|
|
144
|
+
"type": param_info.get("type", "string"),
|
|
145
|
+
"description": param_info.get("description", ""),
|
|
146
|
+
"required": param_name in required,
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
tools.append(
|
|
150
|
+
{
|
|
151
|
+
"name": schema.get("name", tool_name),
|
|
152
|
+
"description": schema.get("description", ""),
|
|
153
|
+
"parameters": params,
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
return JSONResponse(tools)
|
|
157
|
+
|
|
158
|
+
async def get_views(request):
|
|
159
|
+
# Convert views schema to frontend expected format
|
|
160
|
+
views = []
|
|
161
|
+
for view_id, view_data in views_schemas.items():
|
|
162
|
+
props = []
|
|
163
|
+
if "params" in view_data:
|
|
164
|
+
for prop_name, prop_info in view_data["params"].items():
|
|
165
|
+
props.append(
|
|
166
|
+
{
|
|
167
|
+
"name": prop_name,
|
|
168
|
+
"type": prop_info.get("type", "string"),
|
|
169
|
+
"required": True, # Default to required
|
|
170
|
+
"description": prop_info.get("description", ""),
|
|
171
|
+
}
|
|
172
|
+
)
|
|
173
|
+
views.append(
|
|
174
|
+
{
|
|
175
|
+
"id": view_data.get("id", view_id),
|
|
176
|
+
"description": view_data.get("description", ""),
|
|
177
|
+
"props": props,
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
return JSONResponse(views)
|
|
181
|
+
|
|
182
|
+
async def get_skills(request):
|
|
183
|
+
# Convert skills schema to frontend expected format
|
|
184
|
+
skills = []
|
|
185
|
+
for skill_id, skill_data in skills_schemas.items():
|
|
186
|
+
skills.append(
|
|
187
|
+
{
|
|
188
|
+
"id": skill_data.get("id", skill_id),
|
|
189
|
+
"name": skill_data.get("name", skill_id),
|
|
190
|
+
"description": skill_data.get("description", ""),
|
|
191
|
+
"intent_triggers": skill_data.get("intent_triggers", []),
|
|
192
|
+
"requires_auth": skill_data.get("requires_auth", False),
|
|
193
|
+
"internal_tools": skill_data.get("internal_tools", []),
|
|
194
|
+
"output": skill_data.get("output", {}),
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
return JSONResponse(skills)
|
|
198
|
+
|
|
199
|
+
async def get_view_source(request):
|
|
200
|
+
"""Get source code for a specific view"""
|
|
201
|
+
view_id = request.path_params["view_id"]
|
|
202
|
+
view_file = agent_path / "views" / view_id / "view.tsx"
|
|
203
|
+
|
|
204
|
+
if not view_file.exists():
|
|
205
|
+
return JSONResponse({"error": "View not found"}, status_code=404)
|
|
206
|
+
|
|
207
|
+
return Response(content=view_file.read_text(), media_type="text/plain")
|
|
208
|
+
|
|
209
|
+
async def download_source(request):
|
|
210
|
+
"""Download the entire agent source as a zip file"""
|
|
211
|
+
buffer = io.BytesIO()
|
|
212
|
+
with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
|
|
213
|
+
for root, dirs, files in os.walk(agent_path):
|
|
214
|
+
for file in files:
|
|
215
|
+
# Skip __pycache__ and hidden files
|
|
216
|
+
if "__pycache__" in root or file.startswith("."):
|
|
217
|
+
continue
|
|
218
|
+
|
|
219
|
+
file_path = os.path.join(root, file)
|
|
220
|
+
archive_name = os.path.relpath(file_path, agent_path)
|
|
221
|
+
zip_file.write(file_path, archive_name)
|
|
222
|
+
|
|
223
|
+
buffer.seek(0)
|
|
224
|
+
return Response(
|
|
225
|
+
content=buffer.getvalue(),
|
|
226
|
+
media_type="application/zip",
|
|
227
|
+
headers={"Content-Disposition": f'attachment; filename="{agent_name}.zip"'},
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
async def get_prompt(request):
|
|
231
|
+
return JSONResponse({"prompt": system_prompt})
|
|
232
|
+
|
|
233
|
+
async def unified_stream(request):
|
|
234
|
+
from sse_starlette.sse import EventSourceResponse
|
|
235
|
+
import asyncio
|
|
236
|
+
import json
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
payload = await request.json()
|
|
240
|
+
print(f"[DEV] Received payload: {json.dumps(payload, indent=2)}")
|
|
241
|
+
except Exception as e:
|
|
242
|
+
print(f"[DEV] Error parsing payload: {e}")
|
|
243
|
+
payload = {}
|
|
244
|
+
|
|
245
|
+
# Extract user message - handle both formats:
|
|
246
|
+
# Format 1 (playground): {"message": "Hello", "currentState": {...}, "metadata": {...}}
|
|
247
|
+
# Format 2 (legacy): {"messages": [{"content": "Hello"}]}
|
|
248
|
+
last_message = payload.get("message", "")
|
|
249
|
+
if not last_message:
|
|
250
|
+
messages = payload.get("messages", [])
|
|
251
|
+
if messages:
|
|
252
|
+
last_message = messages[-1].get("content", "")
|
|
253
|
+
if not last_message:
|
|
254
|
+
last_message = "Hello!"
|
|
255
|
+
|
|
256
|
+
print(f"[DEV] Processing message: {last_message}")
|
|
257
|
+
|
|
258
|
+
# Check for view switch commands
|
|
259
|
+
message_lower = last_message.lower()
|
|
260
|
+
view_to_show = None
|
|
261
|
+
view_props = {}
|
|
262
|
+
|
|
263
|
+
if "show profile" in message_lower or "profile view" in message_lower:
|
|
264
|
+
view_to_show = "profile"
|
|
265
|
+
view_props = {
|
|
266
|
+
"userName": "Test User",
|
|
267
|
+
"email": "test@example.com",
|
|
268
|
+
"role": "Developer"
|
|
269
|
+
}
|
|
270
|
+
elif "show results" in message_lower or "results view" in message_lower:
|
|
271
|
+
view_to_show = "results"
|
|
272
|
+
view_props = {
|
|
273
|
+
"title": "Analysis Complete",
|
|
274
|
+
"summary": "Here are your test results",
|
|
275
|
+
"items": [
|
|
276
|
+
{"title": "Test 1", "description": "First test passed", "value": "100%", "status": "success"},
|
|
277
|
+
{"title": "Test 2", "description": "Minor issues found", "value": "85%", "status": "warning"},
|
|
278
|
+
{"title": "Test 3", "description": "Needs attention", "value": "60%", "status": "error"}
|
|
279
|
+
]
|
|
280
|
+
}
|
|
281
|
+
elif "show error" in message_lower or "error view" in message_lower:
|
|
282
|
+
view_to_show = "error"
|
|
283
|
+
view_props = {
|
|
284
|
+
"title": "Test Error",
|
|
285
|
+
"message": "This is a test error message to demonstrate the error view.",
|
|
286
|
+
"errorCode": "TEST_001",
|
|
287
|
+
"suggestion": "This is just a demo - no action needed!"
|
|
288
|
+
}
|
|
289
|
+
elif "show welcome" in message_lower or "welcome view" in message_lower:
|
|
290
|
+
view_to_show = "welcome"
|
|
291
|
+
view_props = {
|
|
292
|
+
"title": "Welcome Back!",
|
|
293
|
+
"subtitle": "Ready to help you",
|
|
294
|
+
"userName": "Developer"
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async def event_generator():
|
|
298
|
+
# Simulate thinking delay
|
|
299
|
+
await asyncio.sleep(0.5)
|
|
300
|
+
|
|
301
|
+
# 1. Send status
|
|
302
|
+
yield {
|
|
303
|
+
"data": json.dumps(
|
|
304
|
+
{"type": "status", "content": "Processing request..."}
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
await asyncio.sleep(0.5)
|
|
308
|
+
|
|
309
|
+
# 2. If view switch requested, send view event
|
|
310
|
+
if view_to_show:
|
|
311
|
+
response_text = f"Switching to {view_to_show} view..."
|
|
312
|
+
words = response_text.split(" ")
|
|
313
|
+
for word in words:
|
|
314
|
+
yield {
|
|
315
|
+
"data": json.dumps(
|
|
316
|
+
{"type": "chat", "content": word + " ", "complete": False}
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
await asyncio.sleep(0.05)
|
|
320
|
+
|
|
321
|
+
yield {
|
|
322
|
+
"data": json.dumps({"type": "chat", "content": "", "complete": True})
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
await asyncio.sleep(0.3)
|
|
326
|
+
|
|
327
|
+
# Send view event
|
|
328
|
+
yield {
|
|
329
|
+
"data": json.dumps({
|
|
330
|
+
"type": "view",
|
|
331
|
+
"viewId": view_to_show,
|
|
332
|
+
"props": view_props
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
else:
|
|
336
|
+
# 3. Stream text response
|
|
337
|
+
response_text = f"I received your message: '{last_message}'. Try saying 'show profile', 'show results', 'show error', or 'show welcome' to switch views!"
|
|
338
|
+
|
|
339
|
+
words = response_text.split(" ")
|
|
340
|
+
for word in words:
|
|
341
|
+
yield {
|
|
342
|
+
"data": json.dumps(
|
|
343
|
+
{"type": "chat", "content": word + " ", "complete": False}
|
|
344
|
+
)
|
|
345
|
+
}
|
|
346
|
+
await asyncio.sleep(0.05)
|
|
347
|
+
|
|
348
|
+
# Complete chat
|
|
349
|
+
yield {
|
|
350
|
+
"data": json.dumps({"type": "chat", "content": "", "complete": True})
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
# 4. Done signal
|
|
354
|
+
yield {"data": json.dumps({"type": "done", "content": "Stream complete"})}
|
|
355
|
+
|
|
356
|
+
# SSE Done
|
|
357
|
+
yield {"data": "[DONE]"}
|
|
358
|
+
|
|
359
|
+
return EventSourceResponse(event_generator())
|
|
360
|
+
|
|
361
|
+
print(f"Starting agent server on port {port}...")
|
|
362
|
+
# FastMCP uses starlette/uvicorn internally for SSE
|
|
363
|
+
import uvicorn
|
|
364
|
+
|
|
365
|
+
# Get the SSE ASGI app from FastMCP
|
|
366
|
+
sse_app = mcp.sse_app()
|
|
367
|
+
|
|
368
|
+
# Create combined app with REST endpoints + MCP SSE
|
|
369
|
+
# Note: Mount must come last as it catches all unmatched routes
|
|
370
|
+
app = Starlette(
|
|
371
|
+
routes=[
|
|
372
|
+
Route("/agent-info", agent_info, methods=["GET"]),
|
|
373
|
+
Route("/tools", get_tools, methods=["GET"]),
|
|
374
|
+
Route("/views", get_views, methods=["GET"]),
|
|
375
|
+
Route("/views/{view_id}/source", get_view_source, methods=["GET"]),
|
|
376
|
+
Route("/skills", get_skills, methods=["GET"]),
|
|
377
|
+
Route("/system-prompt", get_prompt, methods=["GET"]),
|
|
378
|
+
Route("/download", download_source, methods=["GET"]),
|
|
379
|
+
Route(
|
|
380
|
+
"/api/agents/{agent_name}/unified-stream",
|
|
381
|
+
unified_stream,
|
|
382
|
+
methods=["POST"],
|
|
383
|
+
),
|
|
384
|
+
# Alternative endpoints the playground might call
|
|
385
|
+
Route("/chat", unified_stream, methods=["POST"]),
|
|
386
|
+
Route("/stream", unified_stream, methods=["POST"]),
|
|
387
|
+
Route("/api/chat", unified_stream, methods=["POST"]),
|
|
388
|
+
Route("/api/stream", unified_stream, methods=["POST"]),
|
|
389
|
+
Mount("/mcp", sse_app), # Mount MCP SSE at /mcp to avoid route conflicts
|
|
390
|
+
]
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
# Add CORS middleware with expose_headers for ngrok compatibility
|
|
394
|
+
app.add_middleware(
|
|
395
|
+
CORSMiddleware,
|
|
396
|
+
allow_origins=["*"],
|
|
397
|
+
allow_credentials=True,
|
|
398
|
+
allow_methods=["*"],
|
|
399
|
+
allow_headers=["*"],
|
|
400
|
+
expose_headers=["*"],
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
# Add request logging middleware
|
|
404
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
405
|
+
|
|
406
|
+
class LoggingMiddleware(BaseHTTPMiddleware):
|
|
407
|
+
async def dispatch(self, request, call_next):
|
|
408
|
+
print(f"[DEV] {request.method} {request.url.path}")
|
|
409
|
+
response = await call_next(request)
|
|
410
|
+
print(f"[DEV] Response status: {response.status_code}")
|
|
411
|
+
return response
|
|
412
|
+
|
|
413
|
+
app.add_middleware(LoggingMiddleware)
|
|
414
|
+
|
|
415
|
+
# Run uvicorn directly with our port
|
|
416
|
+
uvicorn.run(app, host="0.0.0.0", port=port)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
if __name__ == "__main__":
|
|
420
|
+
parser = argparse.ArgumentParser()
|
|
421
|
+
parser.add_argument("--agent-path", required=True, help="Path to agent directory")
|
|
422
|
+
parser.add_argument("--port", type=int, default=5000, help="Port to run on")
|
|
423
|
+
args = parser.parse_args()
|
|
424
|
+
|
|
425
|
+
run_agent_server(Path(args.agent_path), args.port)
|
a4e/server.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A4E MCP Server - Main entry point.
|
|
3
|
+
|
|
4
|
+
This server provides tools for creating and managing A4E agents.
|
|
5
|
+
All tools are organized in the tools/ directory by category.
|
|
6
|
+
|
|
7
|
+
IMPORTANT: This server communicates via stdio (stdin/stdout).
|
|
8
|
+
All logging MUST go to stderr to avoid breaking the MCP protocol.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
import argparse
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
from .core import mcp, set_project_dir
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _log_error(message: str) -> None:
|
|
19
|
+
"""Log error to stderr (never stdout, which is reserved for MCP protocol)."""
|
|
20
|
+
print(f"[a4e] {message}", file=sys.stderr)
|
|
21
|
+
|
|
22
|
+
# Import all tools to register them with the MCP server
|
|
23
|
+
# Each tool uses the @mcp.tool() decorator from core.py
|
|
24
|
+
from .tools import (
|
|
25
|
+
# Project
|
|
26
|
+
initialize_project,
|
|
27
|
+
get_agent_info,
|
|
28
|
+
get_instructions,
|
|
29
|
+
# Agent tools
|
|
30
|
+
add_tool,
|
|
31
|
+
list_tools,
|
|
32
|
+
remove_tool,
|
|
33
|
+
update_tool,
|
|
34
|
+
# Views
|
|
35
|
+
add_view,
|
|
36
|
+
list_views,
|
|
37
|
+
remove_view,
|
|
38
|
+
update_view,
|
|
39
|
+
# Skills
|
|
40
|
+
add_skill,
|
|
41
|
+
list_skills,
|
|
42
|
+
remove_skill,
|
|
43
|
+
update_skill,
|
|
44
|
+
# Schemas
|
|
45
|
+
generate_schemas,
|
|
46
|
+
# Validation
|
|
47
|
+
validate,
|
|
48
|
+
# Development
|
|
49
|
+
dev_start,
|
|
50
|
+
dev_stop,
|
|
51
|
+
check_environment,
|
|
52
|
+
# Deployment
|
|
53
|
+
deploy,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def main():
|
|
58
|
+
"""Entry point for the CLI"""
|
|
59
|
+
# Parse CLI arguments (standard MCP pattern)
|
|
60
|
+
parser = argparse.ArgumentParser(
|
|
61
|
+
description="A4E MCP Server for agent creation and management"
|
|
62
|
+
)
|
|
63
|
+
parser.add_argument(
|
|
64
|
+
"--project-dir",
|
|
65
|
+
type=str,
|
|
66
|
+
help="Root directory for agent projects (standard MCP pattern). "
|
|
67
|
+
"Agents will be created in {project-dir}/file-store/agent-store/",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
args, unknown = parser.parse_known_args()
|
|
71
|
+
|
|
72
|
+
# Set global project directory
|
|
73
|
+
if args.project_dir:
|
|
74
|
+
project_dir = Path(args.project_dir).resolve()
|
|
75
|
+
# Validate that it exists
|
|
76
|
+
if not project_dir.exists():
|
|
77
|
+
_log_error(f"Project directory does not exist: {project_dir}")
|
|
78
|
+
sys.exit(1)
|
|
79
|
+
set_project_dir(project_dir)
|
|
80
|
+
|
|
81
|
+
# Run MCP server
|
|
82
|
+
mcp.run()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
if __name__ == "__main__":
|
|
86
|
+
main()
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# AGENTS.md — A4E Agent Project
|
|
2
|
+
|
|
3
|
+
> This file provides context and instructions for AI coding agents working on A4E agent projects.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
A4E agents are AI assistants that combine natural language understanding with custom capabilities defined by **Tools** and **Views**. This template provides guidance for developing and maintaining A4E agents.
|
|
8
|
+
|
|
9
|
+
## Project Structure
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
{agent-name}/
|
|
13
|
+
├── AGENTS.md # This file - context for AI coding agents
|
|
14
|
+
├── agent.py # Agent factory and initialization
|
|
15
|
+
├── metadata.json # Agent metadata for the marketplace
|
|
16
|
+
├── prompts/
|
|
17
|
+
│ ├── AGENTS.md # Prompt development guide
|
|
18
|
+
│ ├── agent.md # Main agent personality and instructions
|
|
19
|
+
│ ├── reviewer.md # Review/validation prompts
|
|
20
|
+
│ └── view_renderer.md # View rendering instructions
|
|
21
|
+
├── tools/
|
|
22
|
+
│ ├── AGENTS.md # Tool development guide
|
|
23
|
+
│ ├── schemas.json # Auto-generated tool schemas
|
|
24
|
+
│ └── *.py # Python tool files
|
|
25
|
+
├── views/
|
|
26
|
+
│ ├── AGENTS.md # View development guide
|
|
27
|
+
│ ├── schemas.json # Auto-generated view schemas
|
|
28
|
+
│ └── */view.tsx # React view components
|
|
29
|
+
└── skills/ # Optional: reusable skill modules
|
|
30
|
+
└── AGENTS.md # Skill development guide
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
| Task | How to Do It |
|
|
36
|
+
| ----------------------- | ------------------------------------------- |
|
|
37
|
+
| Add a tool | Create `tools/<name>.py` with `@tool` |
|
|
38
|
+
| Add a view | Create `views/<id>/view.tsx` |
|
|
39
|
+
| Edit personality | Modify `prompts/agent.md` |
|
|
40
|
+
| Update metadata | Edit `metadata.json` |
|
|
41
|
+
| Generate schemas | Run `generate_schemas` MCP tool |
|
|
42
|
+
| Start dev server | Run `dev_start` MCP tool |
|
|
43
|
+
| Deploy to Hub | Run `deploy` MCP tool |
|
|
44
|
+
|
|
45
|
+
## Code Style
|
|
46
|
+
|
|
47
|
+
### Python (Tools)
|
|
48
|
+
|
|
49
|
+
- Python 3.11+ required
|
|
50
|
+
- Type hints for ALL parameters and return types
|
|
51
|
+
- Use `Optional[T]` for optional params
|
|
52
|
+
- Snake_case for functions and variables
|
|
53
|
+
- Return `dict` with `status` and data
|
|
54
|
+
|
|
55
|
+
### TypeScript (Views)
|
|
56
|
+
|
|
57
|
+
- Functional components with hooks
|
|
58
|
+
- Explicit props interfaces
|
|
59
|
+
- camelCase for variables
|
|
60
|
+
- PascalCase for components
|
|
61
|
+
- Tailwind CSS for styling
|
|
62
|
+
|
|
63
|
+
### Markdown (Prompts)
|
|
64
|
+
|
|
65
|
+
- Clear section headings
|
|
66
|
+
- Bullet points for guidelines
|
|
67
|
+
- Examples for expected behavior
|
|
68
|
+
- Template variables where needed
|
|
69
|
+
|
|
70
|
+
## Key Files
|
|
71
|
+
|
|
72
|
+
### agent.py
|
|
73
|
+
|
|
74
|
+
Factory function that creates the agent instance:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from a4e.sdk import AgentFactory
|
|
78
|
+
|
|
79
|
+
async def create_agent(model_client, agent_id):
|
|
80
|
+
return await AgentFactory.create_agent(
|
|
81
|
+
agent_path=agent_id,
|
|
82
|
+
model_client=model_client
|
|
83
|
+
)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### metadata.json
|
|
87
|
+
|
|
88
|
+
Agent marketplace metadata:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"id": "agent-id",
|
|
93
|
+
"name": "Display Name",
|
|
94
|
+
"category": "Category",
|
|
95
|
+
"description": "...",
|
|
96
|
+
"version": "1.0.0"
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### prompts/agent.md
|
|
101
|
+
|
|
102
|
+
Main personality and instructions:
|
|
103
|
+
|
|
104
|
+
```markdown
|
|
105
|
+
# Agent Name
|
|
106
|
+
|
|
107
|
+
You are [Agent Name], a specialized AI assistant...
|
|
108
|
+
|
|
109
|
+
## Your Mission
|
|
110
|
+
## Capabilities
|
|
111
|
+
## Guidelines
|
|
112
|
+
## Constraints
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Schema Generation
|
|
116
|
+
|
|
117
|
+
Schemas are automatically generated from code:
|
|
118
|
+
|
|
119
|
+
- **Tools**: Parsed from Python `@tool` decorators and docstrings
|
|
120
|
+
- **Views**: Parsed from TypeScript props interfaces
|
|
121
|
+
|
|
122
|
+
**Never edit schema files manually** — run `generate_schemas` after changes.
|
|
123
|
+
|
|
124
|
+
## Development Workflow
|
|
125
|
+
|
|
126
|
+
1. **Create** - Add tools, views, or prompts
|
|
127
|
+
2. **Generate** - Run `generate_schemas` to update schemas
|
|
128
|
+
3. **Test** - Use `dev_start` to run locally
|
|
129
|
+
4. **Iterate** - Refine based on testing
|
|
130
|
+
5. **Deploy** - Use `deploy` to publish
|
|
131
|
+
|
|
132
|
+
## Security
|
|
133
|
+
|
|
134
|
+
- Never hardcode API keys or secrets
|
|
135
|
+
- Use environment variables for sensitive data
|
|
136
|
+
- Validate all user inputs in tools
|
|
137
|
+
- Sanitize data before rendering in views
|
|
138
|
+
- Handle errors gracefully
|
|
139
|
+
|
|
140
|
+
## Troubleshooting
|
|
141
|
+
|
|
142
|
+
### Tool not appearing
|
|
143
|
+
|
|
144
|
+
1. Check `@tool` decorator is present
|
|
145
|
+
2. Verify type hints and docstring
|
|
146
|
+
3. Run `generate_schemas`
|
|
147
|
+
|
|
148
|
+
### View not rendering
|
|
149
|
+
|
|
150
|
+
1. Check default export
|
|
151
|
+
2. Verify props interface
|
|
152
|
+
3. Check for TypeScript errors
|
|
153
|
+
|
|
154
|
+
### Agent not responding correctly
|
|
155
|
+
|
|
156
|
+
1. Review `prompts/agent.md`
|
|
157
|
+
2. Check tool implementations
|
|
158
|
+
3. Verify schema generation ran
|
|
159
|
+
|
|
160
|
+
## Subdirectory Guides
|
|
161
|
+
|
|
162
|
+
Each subdirectory has its own `AGENTS.md` with specific guidance:
|
|
163
|
+
|
|
164
|
+
- `tools/AGENTS.md` - Tool development patterns
|
|
165
|
+
- `views/AGENTS.md` - View component patterns
|
|
166
|
+
- `prompts/AGENTS.md` - Prompt engineering tips
|
|
167
|
+
- `skills/AGENTS.md` - Skill module development
|
|
168
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from a4e.sdk import AgentFactory
|
|
2
|
+
from autogen_agentchat.agents import AssistantAgent
|
|
3
|
+
|
|
4
|
+
async def create_agent(model_client, agent_id):
|
|
5
|
+
"""
|
|
6
|
+
Create the agent instance
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
model_client: The LLM client
|
|
10
|
+
agent_id: The ID of the agent
|
|
11
|
+
"""
|
|
12
|
+
return await AgentFactory.create_agent(
|
|
13
|
+
agent_path=agent_id,
|
|
14
|
+
model_client=model_client
|
|
15
|
+
)
|