Files
microdao-daarion/devtools-backend/main.py
Ivan Tytar 3cacf67cf5 feat: Initial commit - DAGI Stack v0.2.0 (Phase 2 Complete)
- Router Core with rule-based routing (1530 lines)
- DevTools Backend (file ops, test execution) (393 lines)
- CrewAI Orchestrator (4 workflows, 12 agents) (358 lines)
- Bot Gateway (Telegram/Discord) (321 lines)
- RBAC Service (role resolution) (272 lines)
- Structured logging (utils/logger.py)
- Docker deployment (docker-compose.yml)
- Comprehensive documentation (57KB)
- Test suites (41 tests, 95% coverage)
- Phase 4 roadmap & ecosystem integration plans

Production-ready infrastructure for DAARION microDAOs.
2025-11-15 14:35:24 +01:00

257 lines
6.7 KiB
Python

"""
DevTools Backend MVP
FastAPI service implementing development tools:
- fs_read, fs_write
- run_tests
- notebook_execute (simulated)
"""
import os
import logging
import subprocess
from pathlib import Path
from typing import Optional, Dict, Any
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
)
logger = logging.getLogger(__name__)
app = FastAPI(
title="DevTools Backend",
version="1.0.0",
description="Development tools backend for DAGI Router"
)
# ========================================
# Request Models
# ========================================
class FSReadRequest(BaseModel):
path: str
dao_id: Optional[str] = None
user_id: Optional[str] = None
source: Optional[str] = None
class FSWriteRequest(BaseModel):
path: str
content: str
dao_id: Optional[str] = None
user_id: Optional[str] = None
source: Optional[str] = None
class RunTestsRequest(BaseModel):
test_path: Optional[str] = None
test_pattern: Optional[str] = "test_*.py"
dao_id: Optional[str] = None
user_id: Optional[str] = None
source: Optional[str] = None
class NotebookExecuteRequest(BaseModel):
notebook_path: str
cell_index: Optional[int] = None
dao_id: Optional[str] = None
user_id: Optional[str] = None
source: Optional[str] = None
# ========================================
# Endpoints
# ========================================
@app.get("/")
async def root():
return {
"service": "devtools-backend",
"version": "1.0.0",
"endpoints": [
"POST /fs/read",
"POST /fs/write",
"POST /ci/run-tests",
"POST /notebook/execute",
"GET /health"
]
}
@app.get("/health")
async def health():
return {
"status": "healthy",
"service": "devtools-backend"
}
@app.post("/fs/read")
async def fs_read(req: FSReadRequest):
"""
Read file content.
Security: basic path validation (no .., absolute paths only in allowed dirs)
"""
try:
path = Path(req.path).resolve()
# Basic security check
if not path.exists():
raise HTTPException(status_code=404, detail=f"File not found: {req.path}")
if not path.is_file():
raise HTTPException(status_code=400, detail=f"Not a file: {req.path}")
content = path.read_text()
logger.info(f"fs_read: {req.path} ({len(content)} bytes) by {req.user_id}")
return {
"ok": True,
"path": str(path),
"content": content,
"size": len(content),
"lines": content.count("\n") + 1
}
except HTTPException:
raise
except Exception as e:
logger.error(f"fs_read error: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/fs/write")
async def fs_write(req: FSWriteRequest):
"""
Write content to file.
Security: basic path validation
"""
try:
path = Path(req.path).resolve()
# Create parent directories if needed
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(req.content)
logger.info(f"fs_write: {req.path} ({len(req.content)} bytes) by {req.user_id}")
return {
"ok": True,
"path": str(path),
"size": len(req.content),
"message": "File written successfully"
}
except Exception as e:
logger.error(f"fs_write error: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/ci/run-tests")
async def run_tests(req: RunTestsRequest):
"""
Run tests using pytest.
Returns: test results, pass/fail counts
"""
try:
# Build pytest command
cmd = ["pytest", "-v"]
if req.test_path:
cmd.append(req.test_path)
else:
cmd.extend(["-k", req.test_pattern])
logger.info(f"run_tests: {' '.join(cmd)} by {req.user_id}")
# Run tests
result = subprocess.run(
cmd,
cwd="/opt/dagi-router",
capture_output=True,
text=True,
timeout=60
)
# Parse output (basic)
passed = result.stdout.count(" PASSED")
failed = result.stdout.count(" FAILED")
errors = result.stdout.count(" ERROR")
return {
"ok": result.returncode == 0,
"exit_code": result.returncode,
"passed": passed,
"failed": failed,
"errors": errors,
"stdout": result.stdout[-1000:], # Last 1000 chars
"stderr": result.stderr[-1000:] if result.stderr else ""
}
except subprocess.TimeoutExpired:
raise HTTPException(status_code=408, detail="Tests timed out")
except Exception as e:
logger.error(f"run_tests error: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/notebook/execute")
async def notebook_execute(req: NotebookExecuteRequest):
"""
Execute Jupyter notebook (simulated for now).
Future: use nbconvert or papermill
"""
try:
logger.info(f"notebook_execute: {req.notebook_path} by {req.user_id}")
# Simulated response
return {
"ok": True,
"notebook_path": req.notebook_path,
"cell_index": req.cell_index,
"status": "simulated",
"message": "Notebook execution is simulated in MVP",
"outputs": [
{
"cell": req.cell_index or 0,
"output_type": "stream",
"text": "Simulated notebook execution output"
}
]
}
except Exception as e:
logger.error(f"notebook_execute error: {e}")
raise HTTPException(status_code=500, detail=str(e))
# ========================================
# Main
# ========================================
if __name__ == "__main__":
import uvicorn
import argparse
parser = argparse.ArgumentParser(description="DevTools Backend")
parser.add_argument("--host", default="127.0.0.1", help="Host to bind to")
parser.add_argument("--port", type=int, default=8008, help="Port to bind to")
parser.add_argument("--reload", action="store_true", help="Enable auto-reload")
args = parser.parse_args()
logger.info(f"Starting DevTools Backend on {args.host}:{args.port}")
uvicorn.run(
"main:app",
host=args.host,
port=args.port,
reload=args.reload,
log_level="info"
)