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.
This commit is contained in:
Ivan Tytar
2025-11-15 14:16:38 +01:00
commit 3cacf67cf5
62 changed files with 10625 additions and 0 deletions

22
gateway-bot/Dockerfile Normal file
View File

@@ -0,0 +1,22 @@
# Bot Gateway Dockerfile
FROM python:3.11-slim
LABEL maintainer="DAARION.city Team"
LABEL description="Bot Gateway - Telegram/Discord webhook handler"
LABEL version="0.2.0"
WORKDIR /app
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
COPY ../requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 9300
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:9300/health || exit 1
CMD ["python", "-m", "gateway_bot.main", "--host", "0.0.0.0", "--port", "9300"]

0
gateway-bot/__init__.py Normal file
View File

200
gateway-bot/http_api.py Normal file
View File

@@ -0,0 +1,200 @@
"""
Bot Gateway HTTP API
Handles incoming webhooks from Telegram, Discord, etc.
"""
import logging
from typing import Dict, Any, Optional
from datetime import datetime
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from .router_client import send_to_router
logger = logging.getLogger(__name__)
router = APIRouter()
# ========================================
# Request Models
# ========================================
class TelegramUpdate(BaseModel):
"""Simplified Telegram update model"""
update_id: Optional[int] = None
message: Optional[Dict[str, Any]] = None
class DiscordMessage(BaseModel):
"""Simplified Discord message model"""
content: Optional[str] = None
author: Optional[Dict[str, Any]] = None
channel_id: Optional[str] = None
guild_id: Optional[str] = None
# ========================================
# DAO Mapping (temporary)
# ========================================
# Map chat/channel ID to DAO ID
# TODO: Move to database or config
CHAT_TO_DAO = {
"default": "greenfood-dao",
# Add mappings: "telegram:12345": "specific-dao",
}
def get_dao_id(chat_id: str, source: str) -> str:
"""Get DAO ID from chat ID"""
key = f"{source}:{chat_id}"
return CHAT_TO_DAO.get(key, CHAT_TO_DAO["default"])
# ========================================
# Endpoints
# ========================================
@router.post("/telegram/webhook")
async def telegram_webhook(update: TelegramUpdate):
"""
Handle Telegram webhook.
Telegram update format:
{
"update_id": 123,
"message": {
"message_id": 456,
"from": {"id": 12345, "username": "alice"},
"chat": {"id": 12345, "type": "private"},
"text": "Hello!"
}
}
"""
try:
if not update.message:
raise HTTPException(status_code=400, detail="No message in update")
# Extract message details
text = update.message.get("text", "")
from_user = update.message.get("from", {})
chat = update.message.get("chat", {})
user_id = str(from_user.get("id", "unknown"))
chat_id = str(chat.get("id", "unknown"))
username = from_user.get("username", "")
# Get DAO ID for this chat
dao_id = get_dao_id(chat_id, "telegram")
logger.info(f"Telegram message from {username} (tg:{user_id}) in chat {chat_id}: {text[:50]}")
# Build request to Router
router_request = {
"mode": "chat",
"source": "telegram",
"dao_id": dao_id,
"user_id": f"tg:{user_id}",
"session_id": f"tg:{chat_id}:{dao_id}",
"message": text,
"payload": {
"message": text,
"username": username,
"chat_id": chat_id,
"timestamp": datetime.now().isoformat()
}
}
# Send to Router
router_response = await send_to_router(router_request)
# TODO: Send response back to Telegram via Bot API
# For now, just return the router response
return {
"status": "ok",
"processed": True,
"router_response": router_response
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Telegram webhook error: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/discord/webhook")
async def discord_webhook(message: DiscordMessage):
"""
Handle Discord webhook.
Discord message format:
{
"content": "Hello!",
"author": {"id": "123", "username": "alice"},
"channel_id": "456",
"guild_id": "789"
}
"""
try:
if not message.content:
raise HTTPException(status_code=400, detail="No content in message")
# Extract message details
text = message.content
author = message.author or {}
channel_id = message.channel_id or "unknown"
guild_id = message.guild_id or "unknown"
user_id = author.get("id", "unknown")
username = author.get("username", "")
# Get DAO ID for this guild/channel
dao_id = get_dao_id(guild_id, "discord")
logger.info(f"Discord message from {username} (dc:{user_id}) in guild {guild_id}: {text[:50]}")
# Build request to Router
router_request = {
"mode": "chat",
"source": "discord",
"dao_id": dao_id,
"user_id": f"dc:{user_id}",
"session_id": f"dc:{channel_id}:{dao_id}",
"message": text,
"payload": {
"message": text,
"username": username,
"channel_id": channel_id,
"guild_id": guild_id,
"timestamp": datetime.now().isoformat()
}
}
# Send to Router
router_response = await send_to_router(router_request)
# TODO: Send response back to Discord via Bot API
return {
"status": "ok",
"processed": True,
"router_response": router_response
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Discord webhook error: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/health")
async def health():
"""Health check endpoint"""
return {
"status": "healthy",
"service": "bot-gateway"
}

79
gateway-bot/main.py Normal file
View File

@@ -0,0 +1,79 @@
"""
Bot Gateway Service
Entry point for Telegram/Discord webhook handling
"""
import logging
import argparse
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import uvicorn
from .http_api import router as gateway_router
# Configure logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
)
logger = logging.getLogger(__name__)
def create_app() -> FastAPI:
"""Create FastAPI application"""
app = FastAPI(
title="Bot Gateway",
version="1.0.0",
description="Gateway service for Telegram/Discord bots → DAGI Router"
)
# CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Include gateway routes
app.include_router(gateway_router, prefix="", tags=["gateway"])
@app.get("/")
async def root():
return {
"service": "bot-gateway",
"version": "1.0.0",
"endpoints": [
"POST /telegram/webhook",
"POST /discord/webhook",
"GET /health"
]
}
return app
def main():
parser = argparse.ArgumentParser(description="Bot Gateway Service")
parser.add_argument("--host", default="127.0.0.1", help="Host to bind to")
parser.add_argument("--port", type=int, default=9300, help="Port to bind to")
parser.add_argument("--reload", action="store_true", help="Enable auto-reload")
args = parser.parse_args()
logger.info(f"Starting Bot Gateway on {args.host}:{args.port}")
app = create_app()
uvicorn.run(
app,
host=args.host,
port=args.port,
reload=args.reload,
log_level="info"
)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,42 @@
"""
DAGI Router Client
Sends requests to DAGI Router from Bot Gateway
"""
import logging
import httpx
from typing import Dict, Any
logger = logging.getLogger(__name__)
# Router configuration
ROUTER_URL = "http://127.0.0.1:9102/route"
ROUTER_TIMEOUT = 30.0
async def send_to_router(body: Dict[str, Any]) -> Dict[str, Any]:
"""
Send request to DAGI Router.
Args:
body: Request payload with mode, message, dao_id, etc.
Returns:
Router response as dict
Raises:
httpx.HTTPError: if router request fails
"""
logger.info(f"Sending to Router: mode={body.get('mode')}, dao_id={body.get('dao_id')}")
try:
async with httpx.AsyncClient(timeout=ROUTER_TIMEOUT) as client:
response = await client.post(ROUTER_URL, json=body)
response.raise_for_status()
result = response.json()
logger.info(f"Router response: ok={result.get('ok')}")
return result
except httpx.HTTPError as e:
logger.error(f"Router request failed: {e}")
raise