feat(phase6-9): Complete modular architecture and Agent Teams
Phase 6.4 - Modular Architecture: - Add lewooogo-brain adapters for LLM providers - Add lewooogo-data dual memory (Redis + PostgreSQL) - Implement consensus engine for multi-agent decisions - Add incident memory service for historical context Phase 9 - Agent Teams (Claude Agent SDK): - Add base agent class with Claude Sonnet 4 integration - Implement action planner, blast radius, and security agents - Add agent API endpoints and proposal workflow - Integrate ADR-009 OpenClaw Agent Teams architecture DevOps & CI/CD: - Add GitHub Actions CI/CD workflows (ci.yaml, cd.yaml) - Add pre-commit hooks and secrets baseline - Add docker-compose for local development - Update Kubernetes network policies Frontend Improvements: - Add auto-healing error boundary component - Update i18n messages for agent features - Enhance dual-state incident card with execution feedback Documentation: - Add 7 ADRs covering MCP, design system, architecture decisions - Update ARCHITECTURE_MEMORY.md with modular design - Add GLOBAL_RULES.md and SOUL.md for project identity Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,11 +7,19 @@ Endpoints:
|
||||
- GET /health - Full health check with components
|
||||
- GET /health/ready - K8s readinessProbe
|
||||
- GET /health/live - K8s livenessProbe
|
||||
|
||||
統帥鐵律 2026-03-23:
|
||||
- 禁止假數據 (必須真實連接資源)
|
||||
- 每個檢查 2 秒超時
|
||||
- 失敗不導致 API 崩潰
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from typing import Literal
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -21,6 +29,9 @@ from src.core.logging import get_logger
|
||||
router = APIRouter()
|
||||
logger = get_logger("awoooi.health")
|
||||
|
||||
# Health check timeout (seconds)
|
||||
HEALTH_CHECK_TIMEOUT = 2.0
|
||||
|
||||
|
||||
class ComponentStatus(BaseModel):
|
||||
"""Individual component status"""
|
||||
@@ -39,6 +50,140 @@ class HealthResponse(BaseModel):
|
||||
components: dict[str, Literal["up", "down", "degraded"]]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Real Health Check Functions (統帥鐵律: 禁止假數據)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
async def check_database() -> Literal["up", "down"]:
|
||||
"""
|
||||
Check PostgreSQL connection using asyncpg
|
||||
|
||||
統帥鐵律: 真實執行 SELECT 1,禁止假數據
|
||||
"""
|
||||
try:
|
||||
import asyncpg
|
||||
|
||||
# Parse DATABASE_URL for asyncpg (remove +asyncpg suffix)
|
||||
db_url = settings.DATABASE_URL.replace("postgresql+asyncpg://", "postgresql://")
|
||||
|
||||
conn = await asyncio.wait_for(
|
||||
asyncpg.connect(db_url),
|
||||
timeout=HEALTH_CHECK_TIMEOUT,
|
||||
)
|
||||
try:
|
||||
result = await asyncio.wait_for(
|
||||
conn.fetchval("SELECT 1"),
|
||||
timeout=HEALTH_CHECK_TIMEOUT,
|
||||
)
|
||||
if result == 1:
|
||||
logger.debug("health_check_database", status="up")
|
||||
return "up"
|
||||
else:
|
||||
logger.warning("health_check_database", status="down", reason="unexpected_result")
|
||||
return "down"
|
||||
finally:
|
||||
await conn.close()
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning("health_check_database", status="down", reason="timeout")
|
||||
return "down"
|
||||
except Exception as e:
|
||||
logger.warning("health_check_database", status="down", error=str(e))
|
||||
return "down"
|
||||
|
||||
|
||||
async def check_redis() -> Literal["up", "down"]:
|
||||
"""
|
||||
Check Redis connection using redis.ping()
|
||||
|
||||
統帥鐵律: 真實執行 PING,禁止假數據
|
||||
"""
|
||||
try:
|
||||
import redis.asyncio as redis_lib
|
||||
|
||||
# Create temporary connection for health check (avoid pool dependency)
|
||||
client = redis_lib.from_url(
|
||||
settings.REDIS_URL,
|
||||
encoding="utf-8",
|
||||
decode_responses=True,
|
||||
socket_timeout=HEALTH_CHECK_TIMEOUT,
|
||||
socket_connect_timeout=HEALTH_CHECK_TIMEOUT,
|
||||
)
|
||||
try:
|
||||
result = await asyncio.wait_for(
|
||||
client.ping(),
|
||||
timeout=HEALTH_CHECK_TIMEOUT,
|
||||
)
|
||||
if result:
|
||||
logger.debug("health_check_redis", status="up")
|
||||
return "up"
|
||||
else:
|
||||
logger.warning("health_check_redis", status="down", reason="ping_failed")
|
||||
return "down"
|
||||
finally:
|
||||
await client.close()
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning("health_check_redis", status="down", reason="timeout")
|
||||
return "down"
|
||||
except Exception as e:
|
||||
logger.warning("health_check_redis", status="down", error=str(e))
|
||||
return "down"
|
||||
|
||||
|
||||
async def check_ollama() -> Literal["up", "down"]:
|
||||
"""
|
||||
Check Ollama service via /api/tags endpoint
|
||||
|
||||
統帥鐵律: 真實 HTTP 請求,禁止假數據
|
||||
"""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=HEALTH_CHECK_TIMEOUT) as client:
|
||||
response = await client.get(f"{settings.OLLAMA_URL}/api/tags")
|
||||
if response.status_code == 200:
|
||||
logger.debug("health_check_ollama", status="up")
|
||||
return "up"
|
||||
else:
|
||||
logger.warning(
|
||||
"health_check_ollama",
|
||||
status="down",
|
||||
status_code=response.status_code,
|
||||
)
|
||||
return "down"
|
||||
except httpx.TimeoutException:
|
||||
logger.warning("health_check_ollama", status="down", reason="timeout")
|
||||
return "down"
|
||||
except Exception as e:
|
||||
logger.warning("health_check_ollama", status="down", error=str(e))
|
||||
return "down"
|
||||
|
||||
|
||||
async def check_openclaw() -> Literal["up", "down"]:
|
||||
"""
|
||||
Check OpenClaw service via /health endpoint
|
||||
|
||||
統帥鐵律: 真實 HTTP 請求,禁止假數據
|
||||
"""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=HEALTH_CHECK_TIMEOUT) as client:
|
||||
response = await client.get(f"{settings.OPENCLAW_URL}/health")
|
||||
if response.status_code == 200:
|
||||
logger.debug("health_check_openclaw", status="up")
|
||||
return "up"
|
||||
else:
|
||||
logger.warning(
|
||||
"health_check_openclaw",
|
||||
status="down",
|
||||
status_code=response.status_code,
|
||||
)
|
||||
return "down"
|
||||
except httpx.TimeoutException:
|
||||
logger.warning("health_check_openclaw", status="down", reason="timeout")
|
||||
return "down"
|
||||
except Exception as e:
|
||||
logger.warning("health_check_openclaw", status="down", error=str(e))
|
||||
return "down"
|
||||
|
||||
|
||||
@router.get("/health", response_model=HealthResponse)
|
||||
async def get_health() -> HealthResponse:
|
||||
"""
|
||||
@@ -46,14 +191,34 @@ async def get_health() -> HealthResponse:
|
||||
|
||||
Returns overall system health and individual component statuses.
|
||||
Used for monitoring dashboards and alerting.
|
||||
|
||||
統帥鐵律 2026-03-23: 禁止假數據,所有檢查必須真實連接
|
||||
"""
|
||||
# TODO: Implement actual async health checks
|
||||
components = {
|
||||
"api": "up",
|
||||
"database": "up", # TODO: asyncpg ping
|
||||
"redis": "up", # TODO: redis ping
|
||||
"ollama": "up", # TODO: httpx check
|
||||
"clawbot": "up", # TODO: httpx check
|
||||
# API is always up if this endpoint responds
|
||||
api_status: Literal["up", "down", "degraded"] = "up"
|
||||
|
||||
# Run all health checks concurrently with timeout protection
|
||||
start_time = time.monotonic()
|
||||
|
||||
db_task = asyncio.create_task(check_database())
|
||||
redis_task = asyncio.create_task(check_redis())
|
||||
ollama_task = asyncio.create_task(check_ollama())
|
||||
openclaw_task = asyncio.create_task(check_openclaw())
|
||||
|
||||
# Wait for all tasks (each has internal timeout)
|
||||
db_status, redis_status, ollama_status, openclaw_status = await asyncio.gather(
|
||||
db_task, redis_task, ollama_task, openclaw_task,
|
||||
return_exceptions=False,
|
||||
)
|
||||
|
||||
elapsed_ms = (time.monotonic() - start_time) * 1000
|
||||
|
||||
components: dict[str, Literal["up", "down", "degraded"]] = {
|
||||
"api": api_status,
|
||||
"database": db_status,
|
||||
"redis": redis_status,
|
||||
"ollama": ollama_status,
|
||||
"openclaw": openclaw_status,
|
||||
}
|
||||
|
||||
# Determine overall status
|
||||
@@ -67,10 +232,11 @@ async def get_health() -> HealthResponse:
|
||||
else:
|
||||
overall_status = "healthy"
|
||||
|
||||
logger.debug(
|
||||
logger.info(
|
||||
"health_check",
|
||||
status=overall_status,
|
||||
components=components,
|
||||
elapsed_ms=round(elapsed_ms, 2),
|
||||
)
|
||||
|
||||
return HealthResponse(
|
||||
|
||||
Reference in New Issue
Block a user