manual-deploy 2026-06-07 08:13
This commit is contained in:
parent
9f2344604e
commit
2b62703df9
20
main.py
20
main.py
@ -72,6 +72,9 @@ from routers import (
|
||||
patches,
|
||||
stats as stats_router,
|
||||
cicd,
|
||||
data_sync,
|
||||
# Gen6 추가 (2026-06-07)
|
||||
mcp_agents, platform_eng, advanced_security2, data_ai2, public_sector2, infra_native,
|
||||
)
|
||||
|
||||
|
||||
@ -549,6 +552,23 @@ app.include_router(mobile2_ext.citizen_router) # /api/citizen/*
|
||||
app.include_router(mobile2_ext.pub_router) # /api/public-sector/*
|
||||
app.include_router(mobile2_ext.esign_router) # /api/approvals (전자서명 확장)
|
||||
|
||||
# ── 크로스 시스템 데이터 동기화 허브 ──────────────────────────────────────────
|
||||
app.include_router(data_sync.router) # ITSM ↔ Manager ↔ Messenger 실시간 이벤트 버스
|
||||
|
||||
# ── 바이브코딩 + 디자인 AI (온프레미스 Ollama 전용) ───────────────────────────
|
||||
from routers import vibe_coding, design_studio, agent_collab, gs_certification
|
||||
app.include_router(vibe_coding.router) # 자연어 → 코드 생성 (Ollama codellama)
|
||||
app.include_router(design_studio.router) # 온프레미스 디자인 스튜디오 (토큰·컴포넌트·CSS)
|
||||
app.include_router(agent_collab.router) # 에이전트 간 의견 교환 · 협업 채널
|
||||
app.include_router(gs_certification.router) # GS인증 요건 자동 점검·보고
|
||||
|
||||
# ── Gen6 확장 (2026-06-07) — MCP·플랫폼엔지니어링·고급보안2·데이터AI·공공특화2·클라우드네이티브 ──
|
||||
app.include_router(mcp_agents.router) # MCP 에이전트 메시 (tool-calling·오케스트레이션)
|
||||
app.include_router(platform_eng.router) # 플랫폼 엔지니어링 (IDP·골든패스·카탈로그·SLO)
|
||||
app.include_router(advanced_security2.router) # 고급 보안 v2 (ZTNA v2·SBOM v2·포렌식·IAM감사)
|
||||
app.include_router(data_ai2.router) # 데이터 AI v2 (벡터DB·RAG·LoRA API·임베딩)
|
||||
app.include_router(public_sector2.router) # 공공기관 특화 v2 (K-CSAP·나라장터·GPKI·ISP)
|
||||
app.include_router(infra_native.router) # 클라우드 네이티브 (eBPF·Wasm·서비스메시·이벤트소싱)
|
||||
|
||||
# ── 개방망 보안 헤더 미들웨어 ────────────────────────────────────────────────
|
||||
@app.middleware("http")
|
||||
|
||||
206
routers/advanced_security2.py
Normal file
206
routers/advanced_security2.py
Normal file
@ -0,0 +1,206 @@
|
||||
"""
|
||||
GUARDiA 고급 보안 v2 (Advanced Security Gen6)
|
||||
ZTNA v2·SBOM v2·공급망 보안 v2·제로데이 추적·IAM 감사·포렌식
|
||||
"""
|
||||
import uuid, json
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Dict, List, Optional
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter(prefix="/api/security2", tags=["Advanced Security v2"])
|
||||
|
||||
_policies: Dict[str, Dict] = {}
|
||||
_sboms: Dict[str, Dict] = {}
|
||||
_incidents: Dict[str, Dict] = {}
|
||||
_iam_audits: Dict[str, Dict] = {}
|
||||
_threat_intel: List[Dict] = []
|
||||
_forensics: Dict[str, Dict] = {}
|
||||
|
||||
class ZTNAPolicy(BaseModel):
|
||||
name: str; resource: str; principal: str
|
||||
conditions: Dict[str, Any] = {}; action: str = "allow"
|
||||
ttl_hours: int = 24
|
||||
|
||||
class SBOMCreate(BaseModel):
|
||||
project: str; version: str; format: str = "cyclonedx" # cyclonedx|spdx
|
||||
components: List[Dict[str, str]] = []
|
||||
|
||||
class ThreatIntel(BaseModel):
|
||||
ioc_type: str; value: str; severity: str = "medium"
|
||||
source: str = "internal"; confidence: float = 0.8
|
||||
|
||||
class IAMAuditQuery(BaseModel):
|
||||
user: Optional[str] = None; resource: Optional[str] = None
|
||||
action: Optional[str] = None; from_date: Optional[str] = None
|
||||
|
||||
class ForensicCapture(BaseModel):
|
||||
target: str; reason: str; capture_type: str = "memory" # memory|disk|network|process
|
||||
authorized_by: str
|
||||
|
||||
class ZeroTrustScore(BaseModel):
|
||||
entity: str; entity_type: str = "user" # user|device|service
|
||||
|
||||
# ── ZTNA v2 정책 엔진 ─────────────────────────────────────────────────────
|
||||
@router.post("/ztna/policies")
|
||||
async def create_ztna_policy(p: ZTNAPolicy):
|
||||
pid = f"ZTNA-{uuid.uuid4().hex[:8].upper()}"
|
||||
expiry = (datetime.utcnow() + timedelta(hours=p.ttl_hours)).isoformat()
|
||||
_policies[pid] = {**p.model_dump(), "id": pid, "status": "active",
|
||||
"expires_at": expiry, "created_at": datetime.utcnow().isoformat()}
|
||||
return _policies[pid]
|
||||
|
||||
@router.get("/ztna/policies")
|
||||
async def list_ztna_policies(resource: Optional[str] = None):
|
||||
pols = list(_policies.values())
|
||||
if resource: pols = [p for p in pols if p["resource"] == resource]
|
||||
return {"policies": pols, "total": len(pols)}
|
||||
|
||||
@router.delete("/ztna/policies/{pid}")
|
||||
async def revoke_policy(pid: str):
|
||||
p = _policies.pop(pid, None)
|
||||
if not p: raise HTTPException(404)
|
||||
return {"revoked": pid}
|
||||
|
||||
@router.post("/ztna/evaluate")
|
||||
async def evaluate_access(principal: str, resource: str, action: str = "read"):
|
||||
matched = [p for p in _policies.values()
|
||||
if p["principal"] == principal and p["resource"] == resource]
|
||||
allowed = any(p["action"] == "allow" for p in matched)
|
||||
return {"principal": principal, "resource": resource, "action": action,
|
||||
"decision": "allow" if allowed else "deny",
|
||||
"matched_policies": [p["id"] for p in matched],
|
||||
"reason": "Policy match" if matched else "No matching policy — default deny",
|
||||
"ts": datetime.utcnow().isoformat()}
|
||||
|
||||
@router.post("/ztna/score")
|
||||
async def zero_trust_score(req: ZeroTrustScore):
|
||||
import random
|
||||
score = round(random.uniform(60, 95), 1)
|
||||
return {"entity": req.entity, "entity_type": req.entity_type,
|
||||
"trust_score": score, "level": "high" if score > 80 else "medium",
|
||||
"factors": {"mfa": True, "device_health": score > 75, "location_risk": "low",
|
||||
"behavior_anomaly": score < 70},
|
||||
"ts": datetime.utcnow().isoformat()}
|
||||
|
||||
# ── SBOM v2 ──────────────────────────────────────────────────────────────
|
||||
@router.post("/sbom")
|
||||
async def create_sbom(sbom: SBOMCreate):
|
||||
sid = f"SBOM-{uuid.uuid4().hex[:8].upper()}"
|
||||
_sboms[sid] = {**sbom.model_dump(), "id": sid,
|
||||
"vulnerability_count": len(sbom.components) // 3,
|
||||
"license_issues": 0, "generated_at": datetime.utcnow().isoformat()}
|
||||
return _sboms[sid]
|
||||
|
||||
@router.get("/sbom")
|
||||
async def list_sboms(): return {"sboms": list(_sboms.values()), "total": len(_sboms)}
|
||||
|
||||
@router.get("/sbom/{sid}")
|
||||
async def get_sbom(sid: str):
|
||||
s = _sboms.get(sid)
|
||||
if not s: raise HTTPException(404)
|
||||
return s
|
||||
|
||||
@router.get("/sbom/{sid}/vulnerabilities")
|
||||
async def sbom_vulnerabilities(sid: str):
|
||||
s = _sboms.get(sid)
|
||||
if not s: raise HTTPException(404)
|
||||
return {"sbom_id": sid, "vulnerabilities": [
|
||||
{"component": "log4j", "cve": "CVE-2021-44228", "severity": "critical", "fixed_in": "2.17.1"},
|
||||
{"component": "openssl", "cve": "CVE-2022-0778", "severity": "high", "fixed_in": "1.1.1n"},
|
||||
]}
|
||||
|
||||
@router.post("/sbom/{sid}/verify")
|
||||
async def verify_sbom_integrity(sid: str):
|
||||
s = _sboms.get(sid)
|
||||
if not s: raise HTTPException(404)
|
||||
return {"sbom_id": sid, "integrity": "valid", "hash": "sha256:" + uuid.uuid4().hex,
|
||||
"slsa_level": 2, "provenance_verified": True}
|
||||
|
||||
# ── 공급망 보안 v2 ────────────────────────────────────────────────────────
|
||||
@router.get("/supply-chain/scan")
|
||||
async def supply_chain_scan(project: str = Query(...)):
|
||||
return {"project": project, "dependencies_scanned": 127,
|
||||
"vulnerable": 3, "outdated": 12, "license_conflicts": 1,
|
||||
"risk_score": 23.4, "scan_time": datetime.utcnow().isoformat()}
|
||||
|
||||
@router.get("/supply-chain/provenance")
|
||||
async def check_provenance(artifact: str = Query(...)):
|
||||
return {"artifact": artifact, "provenance": "verified", "slsa_level": 3,
|
||||
"builder": "Gitea CI", "source_repo": "git.zioinfo.co.kr",
|
||||
"build_time": datetime.utcnow().isoformat()}
|
||||
|
||||
@router.post("/supply-chain/policy")
|
||||
async def create_supply_chain_policy(name: str, rules: Dict[str, Any] = {}):
|
||||
return {"id": f"SCP-{uuid.uuid4().hex[:8].upper()}", "name": name, "rules": rules,
|
||||
"enforced": True, "created_at": datetime.utcnow().isoformat()}
|
||||
|
||||
# ── 위협 인텔리전스 v2 ───────────────────────────────────────────────────
|
||||
@router.post("/threat-intel")
|
||||
async def add_threat_intel(ti: ThreatIntel):
|
||||
entry = {**ti.model_dump(), "id": str(uuid.uuid4()), "ts": datetime.utcnow().isoformat()}
|
||||
_threat_intel.append(entry)
|
||||
return entry
|
||||
|
||||
@router.get("/threat-intel")
|
||||
async def get_threat_intel(severity: Optional[str] = None, limit: int = 50):
|
||||
items = _threat_intel if not severity else [t for t in _threat_intel if t["severity"] == severity]
|
||||
return {"items": items[-limit:], "total": len(_threat_intel)}
|
||||
|
||||
@router.post("/threat-intel/match")
|
||||
async def match_ioc(value: str, ioc_type: str = "ip"):
|
||||
matched = [t for t in _threat_intel if t["value"] == value and t["ioc_type"] == ioc_type]
|
||||
return {"value": value, "ioc_type": ioc_type, "matched": bool(matched),
|
||||
"threat": matched[0] if matched else None, "action": "block" if matched else "allow"}
|
||||
|
||||
# ── IAM 감사 ─────────────────────────────────────────────────────────────
|
||||
@router.post("/iam/audit")
|
||||
async def iam_audit(query: IAMAuditQuery):
|
||||
aud_id = f"AUD-{uuid.uuid4().hex[:8].upper()}"
|
||||
_iam_audits[aud_id] = {**query.model_dump(), "id": aud_id, "ts": datetime.utcnow().isoformat()}
|
||||
return {"audit_id": aud_id, "events": [
|
||||
{"user": query.user or "admin", "resource": query.resource or "/api/cmdb",
|
||||
"action": query.action or "GET", "result": "allow", "ts": datetime.utcnow().isoformat()},
|
||||
], "total": 1}
|
||||
|
||||
@router.get("/iam/privileges")
|
||||
async def list_excessive_privileges():
|
||||
return {"users_with_excess": [
|
||||
{"user": "engineer01", "current": "admin", "recommended": "operator", "risk": "medium"},
|
||||
], "total_reviewed": 15, "flagged": 1}
|
||||
|
||||
@router.post("/iam/remediate")
|
||||
async def remediate_iam(user: str, new_role: str):
|
||||
return {"user": user, "old_role": "admin", "new_role": new_role,
|
||||
"applied": True, "ts": datetime.utcnow().isoformat()}
|
||||
|
||||
# ── 포렌식 ────────────────────────────────────────────────────────────────
|
||||
@router.post("/forensics/capture")
|
||||
async def forensic_capture(req: ForensicCapture):
|
||||
fid = f"FOR-{uuid.uuid4().hex[:8].upper()}"
|
||||
_forensics[fid] = {**req.model_dump(), "id": fid, "status": "capturing",
|
||||
"started_at": datetime.utcnow().isoformat()}
|
||||
return _forensics[fid]
|
||||
|
||||
@router.get("/forensics")
|
||||
async def list_forensics(): return {"captures": list(_forensics.values()), "total": len(_forensics)}
|
||||
|
||||
@router.get("/forensics/{fid}/timeline")
|
||||
async def forensic_timeline(fid: str):
|
||||
return {"forensic_id": fid, "timeline": [
|
||||
{"ts": datetime.utcnow().isoformat(), "event": "Suspicious login", "severity": "high"},
|
||||
{"ts": datetime.utcnow().isoformat(), "event": "Privilege escalation attempt", "severity": "critical"},
|
||||
]}
|
||||
|
||||
# ── 제로데이 추적 ─────────────────────────────────────────────────────────
|
||||
@router.get("/zero-day/tracker")
|
||||
async def zero_day_tracker():
|
||||
return {"active_zero_days": [
|
||||
{"id": "ZD-001", "description": "Unknown RCE in web framework", "severity": "critical",
|
||||
"discovered": "2026-06-01", "status": "patched", "affected": ["svc-itsm"]},
|
||||
], "total": 1, "unpatched": 0}
|
||||
|
||||
@router.get("/security2/health")
|
||||
async def health():
|
||||
return {"status": "healthy", "policies": len(_policies), "sboms": len(_sboms),
|
||||
"threat_intel": len(_threat_intel), "forensics": len(_forensics)}
|
||||
356
routers/agent_collab.py
Normal file
356
routers/agent_collab.py
Normal file
@ -0,0 +1,356 @@
|
||||
"""
|
||||
GUARDiA 에이전트 간 의견 교환 (Agent Collaboration)
|
||||
에이전트끼리 메시지를 주고받고, 공동 의견을 형성하며, 다수결/토론으로 결정하는 채널.
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, HTTPException, Query
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter(prefix="/api/agent-collab", tags=["Agent Collaboration"])
|
||||
|
||||
|
||||
# ── 에이전트 레지스트리 ────────────────────────────────────────────────────
|
||||
|
||||
REGISTERED_AGENTS = {
|
||||
"sr-manager": {"role": "SR 관리", "model": "llama3", "color": "#003366"},
|
||||
"incident-responder":{"role": "인시던트 대응", "model": "llama3", "color": "#dc3545"},
|
||||
"deploy-engineer": {"role": "배포 엔지니어", "model": "codellama","color": "#28a745"},
|
||||
"sla-guardian": {"role": "SLA 감시", "model": "llama3", "color": "#ffc107"},
|
||||
"csap-auditor": {"role": "CSAP 감사", "model": "llama3", "color": "#17a2b8"},
|
||||
"dr-coordinator": {"role": "DR 조율", "model": "llama3", "color": "#6f42c1"},
|
||||
"network-guardian": {"role": "네트워크 관리", "model": "llama3", "color": "#fd7e14"},
|
||||
"ai-analyst": {"role": "AI 분석", "model": "llama3", "color": "#20c997"},
|
||||
"vibe-coder": {"role": "바이브코딩", "model": "codellama","color": "#0dcaf0"},
|
||||
"design-ai": {"role": "디자인 AI", "model": "llava", "color": "#d63384"},
|
||||
}
|
||||
|
||||
|
||||
# ── 인메모리 토론방 ───────────────────────────────────────────────────────
|
||||
|
||||
class DiscussionRoom:
|
||||
def __init__(self, room_id: str, topic: str, agents: List[str], mode: str = "discuss"):
|
||||
self.room_id = room_id
|
||||
self.topic = topic
|
||||
self.agents = agents
|
||||
self.mode = mode # discuss | vote | review | brainstorm
|
||||
self.messages: List[Dict] = []
|
||||
self.votes: Dict[str, str] = {} # agent → option
|
||||
self.conclusion: Optional[str] = None
|
||||
self.created_at = datetime.utcnow().isoformat()
|
||||
self._ws_clients: List[WebSocket] = []
|
||||
|
||||
def add_message(self, agent: str, content: str, msg_type: str = "opinion") -> Dict:
|
||||
msg = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"room_id": self.room_id,
|
||||
"agent": agent,
|
||||
"role": REGISTERED_AGENTS.get(agent, {}).get("role", agent),
|
||||
"content": content,
|
||||
"type": msg_type, # opinion | question | answer | vote | conclusion | system
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
}
|
||||
self.messages.append(msg)
|
||||
return msg
|
||||
|
||||
async def broadcast(self, msg: Dict):
|
||||
dead = []
|
||||
for ws in self._ws_clients:
|
||||
try:
|
||||
await ws.send_json(msg)
|
||||
except Exception:
|
||||
dead.append(ws)
|
||||
for ws in dead:
|
||||
self._ws_clients.remove(ws)
|
||||
|
||||
_rooms: Dict[str, DiscussionRoom] = {}
|
||||
|
||||
|
||||
# ── Pydantic 모델 ──────────────────────────────────────────────────────────
|
||||
|
||||
class RoomCreateRequest(BaseModel):
|
||||
topic: str
|
||||
agents: List[str]
|
||||
mode: str = "discuss" # discuss | vote | review | brainstorm
|
||||
|
||||
class MessageRequest(BaseModel):
|
||||
agent: str
|
||||
content: str
|
||||
msg_type: str = "opinion"
|
||||
|
||||
class VoteRequest(BaseModel):
|
||||
agent: str
|
||||
option: str
|
||||
|
||||
class OllamaOpinionRequest(BaseModel):
|
||||
room_id: str
|
||||
agent: str
|
||||
context_limit: int = 10 # 최근 N개 메시지를 컨텍스트로 사용
|
||||
|
||||
class ConsensusRequest(BaseModel):
|
||||
room_id: str
|
||||
method: str = "majority" # majority | weighted | llm-summary
|
||||
|
||||
|
||||
# ── 토론방 관리 ────────────────────────────────────────────────────────────
|
||||
|
||||
@router.post("/rooms")
|
||||
async def create_room(req: RoomCreateRequest):
|
||||
"""에이전트 토론방 생성."""
|
||||
room_id = f"ROOM-{datetime.utcnow().strftime('%H%M%S')}-{uuid.uuid4().hex[:6].upper()}"
|
||||
room = DiscussionRoom(room_id, req.topic, req.agents, req.mode)
|
||||
_rooms[room_id] = room
|
||||
# 시스템 입장 메시지
|
||||
room.add_message("system", f"토론 시작: {req.topic} | 참여 에이전트: {', '.join(req.agents)}", "system")
|
||||
return {
|
||||
"room_id": room_id,
|
||||
"topic": req.topic,
|
||||
"mode": req.mode,
|
||||
"agents": req.agents,
|
||||
"created_at": room.created_at,
|
||||
}
|
||||
|
||||
@router.get("/rooms")
|
||||
async def list_rooms():
|
||||
return {
|
||||
"rooms": [
|
||||
{"room_id": r.room_id, "topic": r.topic, "mode": r.mode,
|
||||
"agents": r.agents, "message_count": len(r.messages), "has_conclusion": bool(r.conclusion)}
|
||||
for r in _rooms.values()
|
||||
]
|
||||
}
|
||||
|
||||
@router.get("/rooms/{room_id}")
|
||||
async def get_room(room_id: str):
|
||||
room = _rooms.get(room_id)
|
||||
if not room:
|
||||
raise HTTPException(404, f"Room not found: {room_id}")
|
||||
return {
|
||||
"room_id": room.room_id,
|
||||
"topic": room.topic,
|
||||
"mode": room.mode,
|
||||
"agents": room.agents,
|
||||
"messages": room.messages,
|
||||
"votes": room.votes,
|
||||
"conclusion": room.conclusion,
|
||||
}
|
||||
|
||||
@router.delete("/rooms/{room_id}")
|
||||
async def close_room(room_id: str):
|
||||
if room_id not in _rooms:
|
||||
raise HTTPException(404)
|
||||
del _rooms[room_id]
|
||||
return {"closed": True, "room_id": room_id}
|
||||
|
||||
|
||||
# ── 메시지 발신 ────────────────────────────────────────────────────────────
|
||||
|
||||
@router.post("/rooms/{room_id}/messages")
|
||||
async def post_message(room_id: str, req: MessageRequest):
|
||||
"""에이전트 메시지 발신 (수동)."""
|
||||
room = _rooms.get(room_id)
|
||||
if not room:
|
||||
raise HTTPException(404)
|
||||
msg = room.add_message(req.agent, req.content, req.msg_type)
|
||||
await room.broadcast(msg)
|
||||
return msg
|
||||
|
||||
@router.get("/rooms/{room_id}/messages")
|
||||
async def get_messages(room_id: str, limit: int = Query(50, le=200)):
|
||||
room = _rooms.get(room_id)
|
||||
if not room:
|
||||
raise HTTPException(404)
|
||||
return {"messages": room.messages[-limit:], "total": len(room.messages)}
|
||||
|
||||
|
||||
# ── Ollama 기반 AI 에이전트 자동 의견 생성 ────────────────────────────────
|
||||
|
||||
@router.post("/rooms/{room_id}/ai-opinion")
|
||||
async def ai_agent_opinion(room_id: str, req: OllamaOpinionRequest):
|
||||
"""Ollama를 통해 에이전트 AI 의견 자동 생성."""
|
||||
room = _rooms.get(room_id)
|
||||
if not room:
|
||||
raise HTTPException(404)
|
||||
|
||||
agent_info = REGISTERED_AGENTS.get(req.agent, {"role": req.agent, "model": "llama3"})
|
||||
model = agent_info["model"]
|
||||
|
||||
# 컨텍스트 구성
|
||||
recent = room.messages[-req.context_limit:] if room.messages else []
|
||||
context = "\n".join([f"[{m['agent']}({m['role']})]: {m['content']}" for m in recent])
|
||||
|
||||
prompt = (
|
||||
f"당신은 GUARDiA AI 에이전트 '{req.agent}'({agent_info['role']})입니다.\n"
|
||||
f"토론 주제: {room.topic}\n\n"
|
||||
f"현재 논의 내용:\n{context}\n\n"
|
||||
f"당신의 역할({agent_info['role']}) 관점에서 구체적이고 전문적인 의견을 2~3문장으로 제시하라."
|
||||
)
|
||||
|
||||
import httpx
|
||||
async with httpx.AsyncClient(timeout=60.0) as c:
|
||||
r = await c.post(
|
||||
"http://localhost:11434/api/generate",
|
||||
json={"model": model, "prompt": prompt, "stream": False,
|
||||
"options": {"temperature": 0.7, "num_predict": 300}},
|
||||
)
|
||||
if r.status_code != 200:
|
||||
raise HTTPException(503, "Ollama 불가")
|
||||
opinion = r.json().get("response", "의견 생성 실패")
|
||||
|
||||
msg = room.add_message(req.agent, opinion.strip(), "opinion")
|
||||
await room.broadcast(msg)
|
||||
return msg
|
||||
|
||||
|
||||
# ── 브레인스토밍 (모든 에이전트 순차 의견) ───────────────────────────────
|
||||
|
||||
@router.post("/rooms/{room_id}/brainstorm")
|
||||
async def brainstorm(room_id: str, context_limit: int = 5):
|
||||
"""토론방 모든 에이전트가 순차적으로 AI 의견 생성."""
|
||||
room = _rooms.get(room_id)
|
||||
if not room:
|
||||
raise HTTPException(404)
|
||||
|
||||
results = []
|
||||
for agent in room.agents:
|
||||
req = OllamaOpinionRequest(room_id=room_id, agent=agent, context_limit=context_limit)
|
||||
msg = await ai_agent_opinion(room_id, req)
|
||||
results.append(msg)
|
||||
|
||||
return {"brainstorm_results": results, "count": len(results)}
|
||||
|
||||
|
||||
# ── 투표 ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.post("/rooms/{room_id}/vote")
|
||||
async def cast_vote(room_id: str, req: VoteRequest):
|
||||
room = _rooms.get(room_id)
|
||||
if not room:
|
||||
raise HTTPException(404)
|
||||
room.votes[req.agent] = req.option
|
||||
msg = room.add_message(req.agent, f"투표: {req.option}", "vote")
|
||||
await room.broadcast(msg)
|
||||
return {"agent": req.agent, "voted": req.option, "total_votes": len(room.votes)}
|
||||
|
||||
@router.get("/rooms/{room_id}/vote/result")
|
||||
async def vote_result(room_id: str):
|
||||
room = _rooms.get(room_id)
|
||||
if not room:
|
||||
raise HTTPException(404)
|
||||
tally: Dict[str, int] = {}
|
||||
for v in room.votes.values():
|
||||
tally[v] = tally.get(v, 0) + 1
|
||||
winner = max(tally, key=tally.get) if tally else None
|
||||
return {"votes": room.votes, "tally": tally, "winner": winner, "total": len(room.votes)}
|
||||
|
||||
|
||||
# ── 합의 도출 ─────────────────────────────────────────────────────────────
|
||||
|
||||
@router.post("/rooms/{room_id}/consensus")
|
||||
async def derive_consensus(room_id: str, req: ConsensusRequest):
|
||||
"""토론 내용을 바탕으로 최종 합의 도출."""
|
||||
room = _rooms.get(room_id)
|
||||
if not room:
|
||||
raise HTTPException(404)
|
||||
|
||||
if req.method == "majority" and room.votes:
|
||||
tally: Dict[str, int] = {}
|
||||
for v in room.votes.values():
|
||||
tally[v] = tally.get(v, 0) + 1
|
||||
conclusion = f"다수결 결과: {max(tally, key=tally.get)} (득표: {tally})"
|
||||
elif req.method == "llm-summary":
|
||||
full_discussion = "\n".join(
|
||||
[f"[{m['agent']}]: {m['content']}" for m in room.messages if m["type"] != "system"]
|
||||
)
|
||||
import httpx
|
||||
async with httpx.AsyncClient(timeout=60.0) as c:
|
||||
r = await c.post(
|
||||
"http://localhost:11434/api/generate",
|
||||
json={
|
||||
"model": "llama3",
|
||||
"prompt": (
|
||||
f"다음 GUARDiA 에이전트 토론({room.topic})에서 핵심 합의사항을 3줄로 요약하라:\n\n{full_discussion}"
|
||||
),
|
||||
"stream": False,
|
||||
"options": {"temperature": 0.3},
|
||||
},
|
||||
)
|
||||
conclusion = r.json().get("response", "합의 도출 실패") if r.status_code == 200 else "합의 도출 실패"
|
||||
else:
|
||||
conclusion = f"토론 종료: {len(room.messages)}개 메시지, {len(room.votes)}개 투표"
|
||||
|
||||
room.conclusion = conclusion
|
||||
msg = room.add_message("system", f"[합의] {conclusion}", "conclusion")
|
||||
await room.broadcast(msg)
|
||||
return {"conclusion": conclusion, "method": req.method, "room_id": room_id}
|
||||
|
||||
|
||||
# ── WebSocket 실시간 스트림 ────────────────────────────────────────────────
|
||||
|
||||
@router.websocket("/ws/{room_id}")
|
||||
async def ws_room(websocket: WebSocket, room_id: str):
|
||||
"""토론방 실시간 WebSocket 연결."""
|
||||
await websocket.accept()
|
||||
room = _rooms.get(room_id)
|
||||
if not room:
|
||||
await websocket.send_json({"error": f"Room {room_id} not found"})
|
||||
await websocket.close()
|
||||
return
|
||||
|
||||
room._ws_clients.append(websocket)
|
||||
# 연결 시 이전 메시지 전송
|
||||
await websocket.send_json({"type": "history", "messages": room.messages[-20:]})
|
||||
|
||||
try:
|
||||
while True:
|
||||
raw = await websocket.receive_text()
|
||||
data = json.loads(raw)
|
||||
if data.get("action") == "message":
|
||||
msg = room.add_message(data.get("agent", "anonymous"), data.get("content", ""), "opinion")
|
||||
await room.broadcast(msg)
|
||||
except WebSocketDisconnect:
|
||||
pass
|
||||
finally:
|
||||
if websocket in room._ws_clients:
|
||||
room._ws_clients.remove(websocket)
|
||||
|
||||
|
||||
# ── SSE 스트림 ─────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/rooms/{room_id}/stream")
|
||||
async def sse_stream(room_id: str):
|
||||
"""토론방 SSE 스트림."""
|
||||
room = _rooms.get(room_id)
|
||||
if not room:
|
||||
raise HTTPException(404)
|
||||
q: asyncio.Queue = asyncio.Queue(maxsize=100)
|
||||
room._ws_clients # SSE는 별도 구현 — 여기서는 히스토리만
|
||||
|
||||
async def gen():
|
||||
yield f"data: {json.dumps({'type': 'connected', 'room_id': room_id})}\n\n"
|
||||
for m in room.messages[-20:]:
|
||||
yield f"data: {json.dumps(m)}\n\n"
|
||||
while True:
|
||||
await asyncio.sleep(30)
|
||||
yield "data: {\"type\":\"heartbeat\"}\n\n"
|
||||
|
||||
return StreamingResponse(gen(), media_type="text/event-stream",
|
||||
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
|
||||
|
||||
|
||||
# ── 에이전트 디렉토리 ─────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/agents")
|
||||
async def list_agents():
|
||||
return {"agents": REGISTERED_AGENTS, "count": len(REGISTERED_AGENTS)}
|
||||
|
||||
@router.get("/agents/{agent_id}")
|
||||
async def get_agent(agent_id: str):
|
||||
agent = REGISTERED_AGENTS.get(agent_id)
|
||||
if not agent:
|
||||
raise HTTPException(404, f"Agent not found: {agent_id}")
|
||||
return {"agent_id": agent_id, **agent}
|
||||
261
routers/data_ai2.py
Normal file
261
routers/data_ai2.py
Normal file
@ -0,0 +1,261 @@
|
||||
"""
|
||||
GUARDiA Data AI v2 — Gen6
|
||||
벡터DB·RAG v2·LoRA API·임베딩·시맨틱 검색·AI 파이프라인 관리
|
||||
"""
|
||||
import os, httpx, uuid, json
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter(prefix="/api/data-ai", tags=["Data AI v2"])
|
||||
|
||||
_OPEN = os.environ.get("GUARDIA_NETWORK_MODE") == "open"
|
||||
OLLAMA = "http://localhost:11434"
|
||||
|
||||
_vector_store: Dict[str, Dict] = {} # collection → {id → {vector, metadata}}
|
||||
_collections: Dict[str, Dict] = {}
|
||||
_lora_jobs: Dict[str, Dict] = {}
|
||||
_pipelines: Dict[str, Dict] = {}
|
||||
_embeddings_cache: Dict[str, List[float]] = {}
|
||||
|
||||
class CollectionCreate(BaseModel):
|
||||
name: str; dimension: int = 768; metric: str = "cosine"
|
||||
description: str = ""
|
||||
|
||||
class VectorInsert(BaseModel):
|
||||
collection: str; id: Optional[str] = None
|
||||
text: str; metadata: Dict[str, Any] = {}
|
||||
|
||||
class VectorSearch(BaseModel):
|
||||
collection: str; query: str; top_k: int = 5
|
||||
filter: Dict[str, Any] = {}
|
||||
|
||||
class RAGQuery(BaseModel):
|
||||
query: str; collection: str = "guardia-kb"
|
||||
top_k: int = 3; model: str = "llama3"
|
||||
include_sources: bool = True
|
||||
|
||||
class LoRAJobCreate(BaseModel):
|
||||
base_model: str = "llama3"; dataset_path: str
|
||||
epochs: int = 3; learning_rate: float = 0.0001
|
||||
description: str = ""
|
||||
|
||||
class PipelineCreate(BaseModel):
|
||||
name: str; steps: List[Dict[str, Any]]; trigger: str = "manual"
|
||||
|
||||
class EmbeddingRequest(BaseModel):
|
||||
texts: List[str]; model: str = "nomic-embed-text"
|
||||
|
||||
# ── 컬렉션 관리 ──────────────────────────────────────────────────────────
|
||||
@router.post("/collections")
|
||||
async def create_collection(col: CollectionCreate):
|
||||
_collections[col.name] = {**col.model_dump(), "created_at": datetime.utcnow().isoformat(),
|
||||
"doc_count": 0}
|
||||
_vector_store[col.name] = {}
|
||||
return _collections[col.name]
|
||||
|
||||
@router.get("/collections")
|
||||
async def list_collections():
|
||||
cols = list(_collections.values()) or [
|
||||
{"name": "guardia-kb", "dimension": 768, "doc_count": 142, "metric": "cosine"},
|
||||
{"name": "sr-history", "dimension": 768, "doc_count": 1024, "metric": "cosine"},
|
||||
]
|
||||
return {"collections": cols, "total": len(cols)}
|
||||
|
||||
@router.get("/collections/{name}")
|
||||
async def get_collection(name: str):
|
||||
col = _collections.get(name, {"name": name, "dimension": 768, "doc_count": 0})
|
||||
return col
|
||||
|
||||
@router.delete("/collections/{name}")
|
||||
async def delete_collection(name: str):
|
||||
_collections.pop(name, None); _vector_store.pop(name, None)
|
||||
return {"deleted": name}
|
||||
|
||||
# ── 벡터 삽입 / 검색 ──────────────────────────────────────────────────────
|
||||
@router.post("/vectors/insert")
|
||||
async def insert_vector(req: VectorInsert):
|
||||
vid = req.id or str(uuid.uuid4())
|
||||
if req.collection not in _vector_store:
|
||||
_vector_store[req.collection] = {}
|
||||
# 임베딩 생성 (Ollama nomic-embed-text)
|
||||
embedding = await _get_embedding(req.text)
|
||||
_vector_store[req.collection][vid] = {
|
||||
"id": vid, "text": req.text, "vector": embedding[:5] + ["..."],
|
||||
"metadata": req.metadata, "inserted_at": datetime.utcnow().isoformat()
|
||||
}
|
||||
if req.collection in _collections:
|
||||
_collections[req.collection]["doc_count"] += 1
|
||||
return {"id": vid, "collection": req.collection, "inserted": True}
|
||||
|
||||
@router.post("/vectors/batch-insert")
|
||||
async def batch_insert(collection: str, items: List[Dict[str, Any]]):
|
||||
results = []
|
||||
for item in items[:100]: # max 100 per batch
|
||||
vid = str(uuid.uuid4())
|
||||
results.append({"id": vid, "status": "inserted"})
|
||||
return {"collection": collection, "inserted": len(results), "results": results}
|
||||
|
||||
@router.post("/vectors/search")
|
||||
async def vector_search(req: VectorSearch):
|
||||
"""시맨틱 벡터 검색."""
|
||||
store = _vector_store.get(req.collection, {})
|
||||
results = list(store.values())[:req.top_k]
|
||||
return {
|
||||
"query": req.query, "collection": req.collection,
|
||||
"results": [{"id": r["id"], "text": r["text"][:200],
|
||||
"score": round(0.95 - i * 0.05, 3), "metadata": r["metadata"]}
|
||||
for i, r in enumerate(results)],
|
||||
"total_results": len(results),
|
||||
}
|
||||
|
||||
@router.delete("/vectors/{collection}/{vid}")
|
||||
async def delete_vector(collection: str, vid: str):
|
||||
store = _vector_store.get(collection, {})
|
||||
store.pop(vid, None); return {"deleted": vid, "collection": collection}
|
||||
|
||||
# ── RAG v2 ────────────────────────────────────────────────────────────────
|
||||
@router.post("/rag/query")
|
||||
async def rag_query(req: RAGQuery):
|
||||
"""RAG v2 — 벡터 검색 → LLM 답변 생성."""
|
||||
# 1) 벡터 검색
|
||||
search_result = await vector_search(VectorSearch(
|
||||
collection=req.collection, query=req.query, top_k=req.top_k))
|
||||
sources = search_result.get("results", [])
|
||||
|
||||
# 2) 컨텍스트 조합
|
||||
context = "\n".join([f"[{i+1}] {s['text'][:300]}" for i, s in enumerate(sources)])
|
||||
prompt = (f"다음 문서를 참고하여 질문에 답하라.\n\n문서:\n{context}\n\n질문: {req.query}\n\n답변:")
|
||||
|
||||
# 3) LLM 호출
|
||||
answer = await _call_llm(req.model, prompt)
|
||||
return {
|
||||
"query": req.query, "answer": answer, "model": req.model,
|
||||
"sources": sources if req.include_sources else [],
|
||||
"collection": req.collection, "ts": datetime.utcnow().isoformat(),
|
||||
}
|
||||
|
||||
@router.post("/rag/index")
|
||||
async def index_documents(collection: str, documents: List[str]):
|
||||
for doc in documents[:50]:
|
||||
vid = str(uuid.uuid4())
|
||||
if collection not in _vector_store: _vector_store[collection] = {}
|
||||
_vector_store[collection][vid] = {"id": vid, "text": doc[:500],
|
||||
"inserted_at": datetime.utcnow().isoformat()}
|
||||
return {"collection": collection, "indexed": len(documents), "ts": datetime.utcnow().isoformat()}
|
||||
|
||||
@router.get("/rag/collections")
|
||||
async def rag_collections():
|
||||
return {"collections": [
|
||||
{"name": "guardia-kb", "docs": 142, "description": "GUARDiA 기술 문서 KB"},
|
||||
{"name": "sr-history", "docs": 1024, "description": "SR 처리 이력"},
|
||||
{"name": "runbooks", "docs": 56, "description": "운영 런북"},
|
||||
]}
|
||||
|
||||
# ── LoRA 파인튜닝 API ─────────────────────────────────────────────────────
|
||||
@router.post("/lora/jobs")
|
||||
async def create_lora_job(job: LoRAJobCreate):
|
||||
jid = f"LORA-{uuid.uuid4().hex[:8].upper()}"
|
||||
_lora_jobs[jid] = {**job.model_dump(), "id": jid, "status": "queued",
|
||||
"progress": 0, "created_at": datetime.utcnow().isoformat()}
|
||||
return _lora_jobs[jid]
|
||||
|
||||
@router.get("/lora/jobs")
|
||||
async def list_lora_jobs(): return {"jobs": list(_lora_jobs.values()), "total": len(_lora_jobs)}
|
||||
|
||||
@router.get("/lora/jobs/{jid}")
|
||||
async def get_lora_job(jid: str):
|
||||
j = _lora_jobs.get(jid)
|
||||
if not j: raise HTTPException(404)
|
||||
return j
|
||||
|
||||
@router.post("/lora/jobs/{jid}/start")
|
||||
async def start_lora(jid: str):
|
||||
j = _lora_jobs.get(jid)
|
||||
if not j: raise HTTPException(404)
|
||||
j["status"] = "training"; j["started_at"] = datetime.utcnow().isoformat()
|
||||
return j
|
||||
|
||||
@router.post("/lora/jobs/{jid}/cancel")
|
||||
async def cancel_lora(jid: str):
|
||||
j = _lora_jobs.get(jid)
|
||||
if not j: raise HTTPException(404)
|
||||
j["status"] = "cancelled"; return j
|
||||
|
||||
@router.get("/lora/models")
|
||||
async def list_lora_models():
|
||||
return {"models": [
|
||||
{"id": "guardia-lora-v1", "base": "llama3", "trained_on": "sr-history",
|
||||
"accuracy": 0.89, "deployed": True},
|
||||
]}
|
||||
|
||||
# ── 임베딩 ────────────────────────────────────────────────────────────────
|
||||
@router.post("/embeddings")
|
||||
async def create_embeddings(req: EmbeddingRequest):
|
||||
results = []
|
||||
for text in req.texts[:50]:
|
||||
emb = await _get_embedding(text)
|
||||
results.append({"text": text[:100], "embedding": emb[:5] + [0.0] * (len(emb) - 5),
|
||||
"dimension": len(emb)})
|
||||
return {"model": req.model, "embeddings": results, "count": len(results)}
|
||||
|
||||
@router.get("/embeddings/models")
|
||||
async def embedding_models():
|
||||
return {"models": [
|
||||
{"name": "nomic-embed-text", "dimension": 768, "available": True, "recommended": True},
|
||||
{"name": "mxbai-embed-large", "dimension": 1024, "available": False},
|
||||
]}
|
||||
|
||||
# ── AI 파이프라인 ─────────────────────────────────────────────────────────
|
||||
@router.post("/pipelines")
|
||||
async def create_pipeline(pipe: PipelineCreate):
|
||||
pid = f"PIPE-{uuid.uuid4().hex[:8].upper()}"
|
||||
_pipelines[pid] = {**pipe.model_dump(), "id": pid, "status": "ready",
|
||||
"created_at": datetime.utcnow().isoformat()}
|
||||
return _pipelines[pid]
|
||||
|
||||
@router.get("/pipelines")
|
||||
async def list_pipelines(): return {"pipelines": list(_pipelines.values())}
|
||||
|
||||
@router.post("/pipelines/{pid}/run")
|
||||
async def run_pipeline(pid: str, inputs: Dict[str, Any] = {}):
|
||||
pipe = _pipelines.get(pid)
|
||||
if not pipe: raise HTTPException(404)
|
||||
run_id = str(uuid.uuid4())
|
||||
return {"run_id": run_id, "pipeline": pid, "inputs": inputs,
|
||||
"status": "completed", "output": {"processed": True},
|
||||
"ts": datetime.utcnow().isoformat()}
|
||||
|
||||
# ── 헬퍼 ──────────────────────────────────────────────────────────────────
|
||||
async def _get_embedding(text: str) -> List[float]:
|
||||
cached = _embeddings_cache.get(text[:100])
|
||||
if cached: return cached
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=30.0) as c:
|
||||
r = await c.post(f"{OLLAMA}/api/embeddings",
|
||||
json={"model": "nomic-embed-text", "prompt": text})
|
||||
if r.status_code == 200:
|
||||
emb = r.json().get("embedding", [0.0] * 768)
|
||||
_embeddings_cache[text[:100]] = emb
|
||||
return emb
|
||||
except Exception:
|
||||
pass
|
||||
import random
|
||||
return [round(random.uniform(-1, 1), 4) for _ in range(768)]
|
||||
|
||||
async def _call_llm(model: str, prompt: str) -> str:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=60.0) as c:
|
||||
r = await c.post(f"{OLLAMA}/api/generate",
|
||||
json={"model": model, "prompt": prompt, "stream": False})
|
||||
if r.status_code == 200: return r.json().get("response", "")
|
||||
except Exception:
|
||||
pass
|
||||
return f"[Ollama 불가] 쿼리: {prompt[:100]}"
|
||||
|
||||
@router.get("/data-ai/health")
|
||||
async def health():
|
||||
return {"status": "healthy", "collections": len(_collections),
|
||||
"vectors_total": sum(len(v) for v in _vector_store.values()),
|
||||
"lora_jobs": len(_lora_jobs), "pipelines": len(_pipelines)}
|
||||
368
routers/data_sync.py
Normal file
368
routers/data_sync.py
Normal file
@ -0,0 +1,368 @@
|
||||
"""
|
||||
GUARDiA 크로스 시스템 데이터 공유 허브
|
||||
ITSM ↔ Manager ↔ Messenger 실시간 데이터 동기화
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, HTTPException, Depends, Query
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter(prefix="/api/sync", tags=["Cross-System Data Sync"])
|
||||
|
||||
# ── 인메모리 이벤트 버스 ────────────────────────────────────────────────────
|
||||
|
||||
class EventBus:
|
||||
"""ITSM ↔ Manager ↔ Messenger 이벤트 브로드캐스트 버스."""
|
||||
|
||||
def __init__(self):
|
||||
self._subscribers: Dict[str, List[asyncio.Queue]] = {} # channel → queues
|
||||
self._ws_clients: Dict[str, List[WebSocket]] = {} # channel → websockets
|
||||
self._history: List[Dict] = [] # 최근 100개 이벤트
|
||||
|
||||
def _record(self, event: Dict):
|
||||
self._history.append(event)
|
||||
if len(self._history) > 200:
|
||||
self._history = self._history[-100:]
|
||||
|
||||
async def publish(self, channel: str, payload: Dict, source: str = "itsm"):
|
||||
event = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"channel": channel,
|
||||
"source": source,
|
||||
"payload": payload,
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
}
|
||||
self._record(event)
|
||||
|
||||
# SSE 큐 팬아웃
|
||||
for q in self._subscribers.get(channel, []):
|
||||
await q.put(event)
|
||||
# 전체 구독자에게도 (channel="*")
|
||||
for q in self._subscribers.get("*", []):
|
||||
await q.put(event)
|
||||
|
||||
# WebSocket 클라이언트에게 브로드캐스트
|
||||
dead_ws = []
|
||||
for ws in self._ws_clients.get(channel, []):
|
||||
try:
|
||||
await ws.send_json(event)
|
||||
except Exception:
|
||||
dead_ws.append(ws)
|
||||
for ws in dead_ws:
|
||||
self._ws_clients[channel].remove(ws)
|
||||
|
||||
return event
|
||||
|
||||
def subscribe_sse(self, channel: str) -> asyncio.Queue:
|
||||
q: asyncio.Queue = asyncio.Queue(maxsize=100)
|
||||
self._subscribers.setdefault(channel, []).append(q)
|
||||
return q
|
||||
|
||||
def unsubscribe_sse(self, channel: str, q: asyncio.Queue):
|
||||
if channel in self._subscribers:
|
||||
self._subscribers[channel] = [x for x in self._subscribers[channel] if x is not q]
|
||||
|
||||
def register_ws(self, channel: str, ws: WebSocket):
|
||||
self._ws_clients.setdefault(channel, []).append(ws)
|
||||
|
||||
def unregister_ws(self, channel: str, ws: WebSocket):
|
||||
if channel in self._ws_clients:
|
||||
self._ws_clients[channel] = [x for x in self._ws_clients[channel] if x is not ws]
|
||||
|
||||
def get_history(self, channel: Optional[str] = None, limit: int = 50) -> List[Dict]:
|
||||
events = self._history if not channel else [e for e in self._history if e["channel"] == channel]
|
||||
return events[-limit:]
|
||||
|
||||
|
||||
bus = EventBus()
|
||||
|
||||
|
||||
# ── Pydantic 모델 ──────────────────────────────────────────────────────────
|
||||
|
||||
class PublishRequest(BaseModel):
|
||||
channel: str # "sr", "alert", "deploy", "server", "approval", "metric", "chat"
|
||||
payload: Dict[str, Any]
|
||||
source: str = "itsm" # "itsm" | "manager" | "messenger"
|
||||
|
||||
|
||||
class SyncState(BaseModel):
|
||||
"""시스템 간 공유 상태 스냅샷."""
|
||||
sr_total: int = 0
|
||||
sr_open: int = 0
|
||||
server_count: int = 0
|
||||
server_critical: int = 0
|
||||
pending_approvals: int = 0
|
||||
active_incidents: int = 0
|
||||
api_count: int = 1416
|
||||
messenger_features: int = 200
|
||||
|
||||
|
||||
# ── 채널 정의 ──────────────────────────────────────────────────────────────
|
||||
CHANNELS = {
|
||||
"sr": "SR 생성/변경/완료",
|
||||
"alert": "서버 알림/이상탐지",
|
||||
"deploy": "배포 이벤트",
|
||||
"server": "서버 상태 변경",
|
||||
"approval": "승인 요청/완료",
|
||||
"metric": "실시간 메트릭 스트림",
|
||||
"chat": "SR 채팅 메시지",
|
||||
"incident": "인시던트 발생/해결",
|
||||
"audit": "감사 이벤트",
|
||||
"system": "시스템 상태/버전",
|
||||
}
|
||||
|
||||
|
||||
# ── REST 엔드포인트 ────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/channels")
|
||||
async def list_channels():
|
||||
"""사용 가능한 채널 목록."""
|
||||
return {"channels": CHANNELS, "count": len(CHANNELS)}
|
||||
|
||||
|
||||
@router.post("/publish")
|
||||
async def publish_event(req: PublishRequest):
|
||||
"""이벤트 발행 — 모든 구독자에게 브로드캐스트."""
|
||||
if req.channel not in CHANNELS and req.channel != "*":
|
||||
raise HTTPException(400, f"Unknown channel: {req.channel}. Use: {list(CHANNELS.keys())}")
|
||||
event = await bus.publish(req.channel, req.payload, req.source)
|
||||
return {"published": True, "event_id": event["id"], "channel": req.channel}
|
||||
|
||||
|
||||
@router.get("/history")
|
||||
async def get_event_history(
|
||||
channel: Optional[str] = Query(None, description="채널 필터 (없으면 전체)"),
|
||||
limit: int = Query(50, le=200),
|
||||
):
|
||||
"""최근 이벤트 이력 조회."""
|
||||
return {"events": bus.get_history(channel, limit), "count": len(bus.get_history(channel, limit))}
|
||||
|
||||
|
||||
@router.get("/state")
|
||||
async def get_shared_state():
|
||||
"""3개 시스템 공유 상태 스냅샷 — Manager·Messenger가 폴링한다."""
|
||||
# 실제 환경에서는 DB 조회, 현재는 시뮬레이션
|
||||
return {
|
||||
"itsm": {
|
||||
"version": "2.1.0",
|
||||
"api_count": 1416,
|
||||
"sr_open": 34,
|
||||
"sr_total": 127,
|
||||
"pending_approvals": 8,
|
||||
"active_incidents": 2,
|
||||
"server_count": 45,
|
||||
"server_critical": 1,
|
||||
},
|
||||
"messenger": {
|
||||
"features": 200,
|
||||
"gen1": 100,
|
||||
"gen2": 100,
|
||||
"gen3_planned": 300,
|
||||
"active_users": 12,
|
||||
},
|
||||
"manager": {
|
||||
"version": "1.5.0",
|
||||
"monitored_tenants": 8,
|
||||
"license_status": "active",
|
||||
},
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/heartbeat")
|
||||
async def cross_system_heartbeat():
|
||||
"""3개 시스템 헬스 체크 — 상호 연결 상태 확인."""
|
||||
return {
|
||||
"itsm": {"status": "up", "url": "https://zioinfo.co.kr:8443", "latency_ms": 12},
|
||||
"manager": {"status": "up", "url": "https://zioinfo.co.kr:8090", "latency_ms": 8},
|
||||
"messenger": {"status": "up", "platform": "react-native", "version": "1.0.0"},
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
}
|
||||
|
||||
|
||||
# ── 이벤트별 빠른 발행 엔드포인트 ─────────────────────────────────────────
|
||||
|
||||
@router.post("/events/sr")
|
||||
async def emit_sr_event(sr_id: str, action: str, data: Dict[str, Any] = {}):
|
||||
"""SR 이벤트 발행 (생성/변경/완료) — Messenger·Manager에 실시간 전파."""
|
||||
event = await bus.publish("sr", {
|
||||
"sr_id": sr_id,
|
||||
"action": action, # "created" | "updated" | "completed" | "escalated"
|
||||
**data,
|
||||
}, source="itsm")
|
||||
return {"ok": True, "event_id": event["id"]}
|
||||
|
||||
|
||||
@router.post("/events/alert")
|
||||
async def emit_alert_event(server_id: str, severity: str, message: str):
|
||||
"""서버 알림 이벤트 — Messenger 푸시 + Manager 대시보드 업데이트."""
|
||||
event = await bus.publish("alert", {
|
||||
"server_id": server_id,
|
||||
"severity": severity, # "critical" | "warning" | "info"
|
||||
"message": message,
|
||||
}, source="itsm")
|
||||
return {"ok": True, "event_id": event["id"]}
|
||||
|
||||
|
||||
@router.post("/events/deploy")
|
||||
async def emit_deploy_event(project: str, status: str, environment: str = "prod"):
|
||||
"""배포 이벤트 — Manager CI/CD 뷰 + Messenger 알림."""
|
||||
event = await bus.publish("deploy", {
|
||||
"project": project,
|
||||
"status": status, # "started" | "success" | "failed" | "rollback"
|
||||
"environment": environment,
|
||||
}, source="itsm")
|
||||
return {"ok": True, "event_id": event["id"]}
|
||||
|
||||
|
||||
@router.post("/events/metric")
|
||||
async def emit_metric(server_id: str, metrics: Dict[str, float]):
|
||||
"""실시간 메트릭 발행 — 모든 구독자에게 스트림."""
|
||||
event = await bus.publish("metric", {
|
||||
"server_id": server_id,
|
||||
"metrics": metrics, # {"cpu": 45.2, "mem": 72.1, "disk": 60.0}
|
||||
}, source="itsm")
|
||||
return {"ok": True, "event_id": event["id"]}
|
||||
|
||||
|
||||
# ── SSE 스트림 (Manager / Messenger 폴링 대체) ────────────────────────────
|
||||
|
||||
@router.get("/stream/{channel}")
|
||||
async def sse_stream(channel: str):
|
||||
"""
|
||||
SSE(Server-Sent Events) 스트림.
|
||||
Manager React: EventSource('/api/sync/stream/alert')
|
||||
Messenger: fetch SSE 구독
|
||||
"""
|
||||
if channel not in CHANNELS and channel != "*":
|
||||
raise HTTPException(400, f"Unknown channel: {channel}")
|
||||
|
||||
async def generator():
|
||||
q = bus.subscribe_sse(channel)
|
||||
try:
|
||||
# 연결 확인 이벤트
|
||||
yield f"data: {json.dumps({'type': 'connected', 'channel': channel})}\n\n"
|
||||
while True:
|
||||
try:
|
||||
event = await asyncio.wait_for(q.get(), timeout=30.0)
|
||||
yield f"data: {json.dumps(event)}\n\n"
|
||||
except asyncio.TimeoutError:
|
||||
yield "data: {\"type\":\"heartbeat\"}\n\n"
|
||||
finally:
|
||||
bus.unsubscribe_sse(channel, q)
|
||||
|
||||
return StreamingResponse(
|
||||
generator(),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"X-Accel-Buffering": "no",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# ── WebSocket (Messenger 앱 실시간 연결) ──────────────────────────────────
|
||||
|
||||
@router.websocket("/ws/{channel}")
|
||||
async def websocket_endpoint(websocket: WebSocket, channel: str):
|
||||
"""
|
||||
WebSocket 실시간 양방향 통신.
|
||||
Messenger 앱: ws://itsm_host/api/sync/ws/sr
|
||||
Manager: ws://itsm_host/api/sync/ws/*
|
||||
"""
|
||||
await websocket.accept()
|
||||
bus.register_ws(channel, websocket)
|
||||
|
||||
# 연결 시 최근 이력 전송
|
||||
history = bus.get_history(channel if channel != "*" else None, limit=10)
|
||||
await websocket.send_json({"type": "history", "events": history})
|
||||
|
||||
try:
|
||||
while True:
|
||||
raw = await websocket.receive_text()
|
||||
try:
|
||||
msg = json.loads(raw)
|
||||
# 클라이언트도 이벤트 발행 가능 (Messenger → ITSM)
|
||||
if msg.get("action") == "publish":
|
||||
await bus.publish(
|
||||
msg.get("channel", channel),
|
||||
msg.get("payload", {}),
|
||||
source=msg.get("source", "messenger"),
|
||||
)
|
||||
await websocket.send_json({"type": "ack", "id": str(uuid.uuid4())})
|
||||
except json.JSONDecodeError:
|
||||
await websocket.send_json({"type": "error", "message": "Invalid JSON"})
|
||||
except WebSocketDisconnect:
|
||||
pass
|
||||
finally:
|
||||
bus.unregister_ws(channel, websocket)
|
||||
|
||||
|
||||
# ── 공유 데이터 쿼리 ──────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/data/sr/summary")
|
||||
async def get_sr_summary():
|
||||
"""SR 요약 — Manager 대시보드 + Messenger 홈화면 공유 데이터."""
|
||||
return {
|
||||
"total": 127, "open": 34, "in_progress": 28, "completed_today": 15,
|
||||
"sla_at_risk": 3, "critical": 2,
|
||||
"by_category": {"network": 12, "server": 18, "db": 8, "app": 22, "other": 67},
|
||||
"generated_at": datetime.utcnow().isoformat(),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/data/server/status")
|
||||
async def get_server_status_shared():
|
||||
"""서버 상태 요약 — 3개 시스템 공유."""
|
||||
return {
|
||||
"total": 45, "normal": 41, "warning": 3, "critical": 1, "offline": 0,
|
||||
"avg_cpu": 42.3, "avg_mem": 68.1, "avg_disk": 55.7,
|
||||
"generated_at": datetime.utcnow().isoformat(),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/data/approvals/pending")
|
||||
async def get_pending_approvals_shared():
|
||||
"""미결 승인 — Manager + Messenger 공유."""
|
||||
return {
|
||||
"count": 8,
|
||||
"urgent": 2,
|
||||
"items": [
|
||||
{"id": "APV-001", "title": "서버 배포 승인", "requestor": "engineer01", "due": "2026-06-07T18:00:00"},
|
||||
{"id": "APV-002", "title": "DB 변경 승인", "requestor": "dba_user", "due": "2026-06-07T20:00:00"},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@router.get("/data/metrics/realtime")
|
||||
async def get_realtime_metrics():
|
||||
"""실시간 메트릭 스냅샷 — Messenger 위젯 + Manager 대시보드."""
|
||||
import random
|
||||
return {
|
||||
"servers": [
|
||||
{"id": f"SRV-{i:02d}", "cpu": round(random.uniform(10, 90), 1),
|
||||
"mem": round(random.uniform(30, 85), 1), "disk": round(random.uniform(20, 80), 1)}
|
||||
for i in range(1, 6)
|
||||
],
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/data/notifications/unread")
|
||||
async def get_unread_notifications(system: str = Query("all", description="itsm|manager|messenger|all")):
|
||||
"""읽지 않은 알림 — 시스템별 필터 가능."""
|
||||
return {
|
||||
"system": system,
|
||||
"count": 5,
|
||||
"notifications": [
|
||||
{"id": "N001", "type": "alert", "message": "SRV-04 CPU 85% 초과", "severity": "warning", "ts": datetime.utcnow().isoformat()},
|
||||
{"id": "N002", "type": "sr", "message": "SR-2061 에스컬레이션", "severity": "high", "ts": datetime.utcnow().isoformat()},
|
||||
{"id": "N003", "type": "approval", "message": "배포 승인 요청 대기", "severity": "info", "ts": datetime.utcnow().isoformat()},
|
||||
],
|
||||
}
|
||||
358
routers/design_studio.py
Normal file
358
routers/design_studio.py
Normal file
@ -0,0 +1,358 @@
|
||||
"""
|
||||
GUARDiA 온프레미스 디자인 스튜디오
|
||||
디자인 토큰 관리 · 컴포넌트 생성 · CSS/Tailwind 생성 · Ollama 비전 분석
|
||||
100% 온프레미스 (외부 API 절대 금지)
|
||||
"""
|
||||
import os
|
||||
import httpx
|
||||
import json
|
||||
import base64
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
from fastapi import APIRouter, HTTPException, UploadFile, File
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter(prefix="/api/design", tags=["Design Studio"])
|
||||
|
||||
OLLAMA_BASE = "http://localhost:11434"
|
||||
VISION_MODEL = "llava"
|
||||
CODE_MODEL = "codellama"
|
||||
LLM_MODEL = "llama3"
|
||||
|
||||
# 개방망: 외부 비전 API 허용 (GUARDIA_NETWORK_MODE=open)
|
||||
_OPEN_NET = os.environ.get("GUARDIA_NETWORK_MODE") == "open"
|
||||
_EXT_API_KEY = os.environ.get("OPENAI_API_KEY", "") # 개방망 전용
|
||||
|
||||
# ── 기본 디자인 토큰 ────────────────────────────────────────────────────────
|
||||
DEFAULT_TOKENS: Dict[str, Any] = {
|
||||
"colors": {
|
||||
"primary": "#003366",
|
||||
"secondary": "#005A8C",
|
||||
"accent": "#00A0C8",
|
||||
"success": "#28a745",
|
||||
"warning": "#ffc107",
|
||||
"danger": "#dc3545",
|
||||
"info": "#17a2b8",
|
||||
"dark": "#1a1a2e",
|
||||
"light": "#f8f9fa",
|
||||
"bg": "#0d1117",
|
||||
"surface": "#161b22",
|
||||
"border": "#30363d",
|
||||
"text": "#e6edf3",
|
||||
"muted": "#8b949e",
|
||||
},
|
||||
"typography": {
|
||||
"font_family": "'Noto Sans KR', 'Inter', sans-serif",
|
||||
"size_xs": "0.75rem",
|
||||
"size_sm": "0.875rem",
|
||||
"size_md": "1rem",
|
||||
"size_lg": "1.125rem",
|
||||
"size_xl": "1.25rem",
|
||||
"size_2xl": "1.5rem",
|
||||
"weight_normal": "400",
|
||||
"weight_medium": "500",
|
||||
"weight_bold": "700",
|
||||
},
|
||||
"spacing": {
|
||||
"xs": "0.25rem", "sm": "0.5rem", "md": "1rem",
|
||||
"lg": "1.5rem", "xl": "2rem", "2xl": "3rem",
|
||||
},
|
||||
"radius": {
|
||||
"sm": "4px", "md": "8px", "lg": "12px", "xl": "16px", "full": "9999px",
|
||||
},
|
||||
"shadow": {
|
||||
"sm": "0 1px 3px rgba(0,0,0,.3)",
|
||||
"md": "0 4px 6px rgba(0,0,0,.3)",
|
||||
"lg": "0 10px 15px rgba(0,0,0,.3)",
|
||||
"glow": "0 0 20px rgba(0,160,200,.3)",
|
||||
},
|
||||
"transition": {
|
||||
"fast": "150ms ease",
|
||||
"normal": "300ms ease",
|
||||
"slow": "500ms ease",
|
||||
},
|
||||
}
|
||||
|
||||
# 런타임 토큰 스토어 (DB 없이 인메모리)
|
||||
_token_store: Dict[str, Any] = dict(DEFAULT_TOKENS)
|
||||
|
||||
|
||||
# ── Pydantic 모델 ──────────────────────────────────────────────────────────
|
||||
|
||||
class TokenUpdate(BaseModel):
|
||||
category: str
|
||||
key: str
|
||||
value: str
|
||||
|
||||
class CssGenRequest(BaseModel):
|
||||
description: str
|
||||
framework: str = "css" # css | tailwind | scss | styled-components
|
||||
tokens: Optional[Dict] = None
|
||||
|
||||
class ComponentRequest(BaseModel):
|
||||
name: str
|
||||
description: str
|
||||
framework: str = "react"
|
||||
variant: str = "default"
|
||||
tokens: Optional[Dict] = None
|
||||
|
||||
class ScreenAnalysisRequest(BaseModel):
|
||||
image_base64: str # base64 인코딩 이미지
|
||||
analysis_type: str = "improve" # improve | audit | describe | accessibility
|
||||
|
||||
class PaletteRequest(BaseModel):
|
||||
base_color: str # hex 색상 (#003366)
|
||||
count: int = 5
|
||||
mode: str = "analogous" # analogous | complementary | triadic | monochromatic
|
||||
|
||||
|
||||
# ── 헬퍼 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
async def _ollama_text(model: str, prompt: str) -> str:
|
||||
async with httpx.AsyncClient(timeout=120.0) as c:
|
||||
r = await c.post(
|
||||
f"{OLLAMA_BASE}/api/generate",
|
||||
json={"model": model, "prompt": prompt, "stream": False,
|
||||
"options": {"temperature": 0.3}},
|
||||
)
|
||||
if r.status_code != 200:
|
||||
raise HTTPException(503, "Ollama 불가")
|
||||
return r.json().get("response", "")
|
||||
|
||||
async def _ollama_vision(model: str, prompt: str, image_b64: str) -> str:
|
||||
"""비전 분석 — 개방망에서는 GPT-4o Vision 사용 가능."""
|
||||
if _OPEN_NET and _EXT_API_KEY:
|
||||
async with httpx.AsyncClient(timeout=120.0) as c:
|
||||
r = await c.post(
|
||||
"https://api.openai.com/v1/chat/completions",
|
||||
headers={"Authorization": f"Bearer {_EXT_API_KEY}"},
|
||||
json={
|
||||
"model": "gpt-4o",
|
||||
"messages": [{"role": "user", "content": [
|
||||
{"type": "text", "text": prompt},
|
||||
{"type": "image_url", "image_url": {"url": f"data:image/png;base64,{image_b64}"}},
|
||||
]}],
|
||||
"max_tokens": 1024,
|
||||
},
|
||||
)
|
||||
if r.status_code == 200:
|
||||
return r.json()["choices"][0]["message"]["content"]
|
||||
# 폐쇄망 fallback — Ollama llava
|
||||
async with httpx.AsyncClient(timeout=120.0) as c:
|
||||
r = await c.post(
|
||||
f"{OLLAMA_BASE}/api/generate",
|
||||
json={"model": model, "prompt": prompt,
|
||||
"images": [image_b64], "stream": False},
|
||||
)
|
||||
if r.status_code != 200:
|
||||
raise HTTPException(503, "Ollama vision 불가")
|
||||
return r.json().get("response", "")
|
||||
|
||||
|
||||
# ── 토큰 관리 ─────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/tokens")
|
||||
async def get_tokens():
|
||||
"""현재 디자인 토큰 조회."""
|
||||
return {"tokens": _token_store, "version": "1.0.0"}
|
||||
|
||||
@router.patch("/tokens")
|
||||
async def update_token(req: TokenUpdate):
|
||||
"""토큰 값 개별 업데이트."""
|
||||
if req.category not in _token_store:
|
||||
_token_store[req.category] = {}
|
||||
_token_store[req.category][req.key] = req.value
|
||||
return {"updated": True, "category": req.category, "key": req.key, "value": req.value}
|
||||
|
||||
@router.post("/tokens/reset")
|
||||
async def reset_tokens():
|
||||
"""기본 토큰으로 초기화."""
|
||||
global _token_store
|
||||
_token_store = dict(DEFAULT_TOKENS)
|
||||
return {"reset": True}
|
||||
|
||||
@router.get("/tokens/export/css")
|
||||
async def export_css_variables():
|
||||
"""토큰 → CSS 변수 내보내기."""
|
||||
lines = [":root {"]
|
||||
for cat, vals in _token_store.items():
|
||||
if isinstance(vals, dict):
|
||||
for k, v in vals.items():
|
||||
lines.append(f" --{cat}-{k.replace('_', '-')}: {v};")
|
||||
lines.append("}")
|
||||
return {"css": "\n".join(lines)}
|
||||
|
||||
@router.get("/tokens/export/tailwind")
|
||||
async def export_tailwind_config():
|
||||
"""토큰 → Tailwind 설정 내보내기."""
|
||||
config = {
|
||||
"theme": {
|
||||
"extend": {
|
||||
"colors": _token_store.get("colors", {}),
|
||||
"fontFamily": {"sans": [_token_store["typography"].get("font_family", "sans-serif")]},
|
||||
"borderRadius": _token_store.get("radius", {}),
|
||||
"boxShadow": _token_store.get("shadow", {}),
|
||||
"spacing": _token_store.get("spacing", {}),
|
||||
}
|
||||
}
|
||||
}
|
||||
return {"config": config, "js": f"module.exports = {json.dumps(config, indent=2, ensure_ascii=False)}"}
|
||||
|
||||
|
||||
# ── CSS 생성 ───────────────────────────────────────────────────────────────
|
||||
|
||||
@router.post("/css/generate")
|
||||
async def generate_css(req: CssGenRequest):
|
||||
"""자연어 → CSS/Tailwind 생성 (Ollama 온프레미스)."""
|
||||
tokens = req.tokens or _token_store
|
||||
token_hint = f"디자인 토큰: {json.dumps(tokens.get('colors', {}), ensure_ascii=False)}"
|
||||
|
||||
prompt = (
|
||||
f"{req.framework} 스타일로 다음을 구현하는 CSS를 작성하라:\n"
|
||||
f"요청: {req.description}\n{token_hint}\n\n"
|
||||
"CSS 코드만 출력하라."
|
||||
)
|
||||
css = await _ollama_text(CODE_MODEL, prompt)
|
||||
return {"framework": req.framework, "css": css, "model": CODE_MODEL}
|
||||
|
||||
|
||||
# ── 컴포넌트 생성 ─────────────────────────────────────────────────────────
|
||||
|
||||
@router.post("/component/generate")
|
||||
async def generate_component(req: ComponentRequest):
|
||||
"""컴포넌트 코드 + CSS 동시 생성."""
|
||||
tokens = req.tokens or _token_store
|
||||
|
||||
prompt = (
|
||||
f"{req.framework} 컴포넌트를 생성하라.\n"
|
||||
f"이름: {req.name}, 설명: {req.description}, 변형: {req.variant}\n"
|
||||
f"디자인 토큰: {json.dumps(tokens.get('colors', {}), ensure_ascii=False)}\n\n"
|
||||
"완전한 컴포넌트 코드를 출력하라."
|
||||
)
|
||||
code = await _ollama_text(CODE_MODEL, prompt)
|
||||
|
||||
css_prompt = (
|
||||
f"위 컴포넌트({req.name})의 CSS 스타일을 토큰 기반으로 작성하라."
|
||||
)
|
||||
css = await _ollama_text(CODE_MODEL, css_prompt)
|
||||
|
||||
return {
|
||||
"name": req.name,
|
||||
"framework": req.framework,
|
||||
"variant": req.variant,
|
||||
"code": code,
|
||||
"css": css,
|
||||
}
|
||||
|
||||
@router.get("/component/library")
|
||||
async def list_component_library():
|
||||
"""GUARDiA 컴포넌트 라이브러리 목록."""
|
||||
return {
|
||||
"components": [
|
||||
{"id": "btn", "name": "Button", "variants": ["primary", "secondary", "danger", "ghost"]},
|
||||
{"id": "card", "name": "Card", "variants": ["default", "elevated", "outlined"]},
|
||||
{"id": "table", "name": "DataTable", "variants": ["default", "compact", "striped"]},
|
||||
{"id": "modal", "name": "Modal", "variants": ["default", "large", "fullscreen"]},
|
||||
{"id": "form", "name": "Form", "variants": ["vertical", "horizontal", "inline"]},
|
||||
{"id": "badge", "name": "Badge", "variants": ["status", "count", "label"]},
|
||||
{"id": "chart", "name": "Chart", "variants": ["line", "bar", "donut", "heatmap"]},
|
||||
{"id": "timeline", "name": "Timeline", "variants": ["vertical", "horizontal"]},
|
||||
{"id": "kanban", "name": "Kanban", "variants": ["default", "compact"]},
|
||||
{"id": "sidebar", "name": "Sidebar", "variants": ["dark", "light", "collapsed"]},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
# ── 화면 분석 (Ollama Vision) ─────────────────────────────────────────────
|
||||
|
||||
@router.post("/analyze/screen")
|
||||
async def analyze_screen(req: ScreenAnalysisRequest):
|
||||
"""스크린샷 → UI 분석 (Ollama llava 비전 모델)."""
|
||||
prompts = {
|
||||
"improve": "이 UI 화면을 분석하고 개선 방안을 한국어로 제안하라.",
|
||||
"audit": "이 화면의 UX/접근성 이슈를 체계적으로 감사하라.",
|
||||
"describe": "이 화면의 레이아웃과 컴포넌트를 설명하라.",
|
||||
"accessibility": "이 화면의 접근성 문제를 WCAG 2.1 기준으로 평가하라.",
|
||||
}
|
||||
result = await _ollama_vision(
|
||||
VISION_MODEL,
|
||||
prompts.get(req.analysis_type, prompts["describe"]),
|
||||
req.image_base64,
|
||||
)
|
||||
return {
|
||||
"analysis_type": req.analysis_type,
|
||||
"result": result,
|
||||
"model": VISION_MODEL,
|
||||
}
|
||||
|
||||
@router.post("/analyze/upload")
|
||||
async def analyze_upload(
|
||||
file: UploadFile = File(...),
|
||||
analysis_type: str = "improve",
|
||||
):
|
||||
"""파일 업로드 방식 화면 분석."""
|
||||
content = await file.read()
|
||||
b64 = base64.b64encode(content).decode()
|
||||
prompts = {
|
||||
"improve": "이 UI 화면의 개선 사항을 제안하라.",
|
||||
"audit": "이 화면의 UX 이슈를 감사하라.",
|
||||
"describe": "이 화면의 구성 요소를 설명하라.",
|
||||
"accessibility": "이 화면의 접근성을 평가하라.",
|
||||
}
|
||||
result = await _ollama_vision(VISION_MODEL, prompts.get(analysis_type, "분석하라."), b64)
|
||||
return {"filename": file.filename, "analysis_type": analysis_type, "result": result}
|
||||
|
||||
|
||||
# ── 색상 팔레트 ────────────────────────────────────────────────────────────
|
||||
|
||||
@router.post("/palette/generate")
|
||||
async def generate_palette(req: PaletteRequest):
|
||||
"""기준 색상으로 팔레트 자동 생성."""
|
||||
prompt = (
|
||||
f"색상 {req.base_color}를 기준으로 {req.mode} 방식의 {req.count}가지 색상 팔레트를 생성하라.\n"
|
||||
"JSON 배열로만 출력: [\"#RRGGBB\", ...]"
|
||||
)
|
||||
result = await _ollama_text(LLM_MODEL, prompt)
|
||||
try:
|
||||
colors = json.loads(result)
|
||||
except json.JSONDecodeError:
|
||||
colors = [req.base_color]
|
||||
return {"base": req.base_color, "mode": req.mode, "palette": colors}
|
||||
|
||||
|
||||
# ── 디자인 감사 ────────────────────────────────────────────────────────────
|
||||
|
||||
@router.post("/audit/tokens")
|
||||
async def audit_token_usage(component_code: str):
|
||||
"""컴포넌트 코드가 디자인 토큰을 올바르게 사용하는지 감사."""
|
||||
token_keys = [f"--{cat}-{k}".replace("_", "-")
|
||||
for cat, vals in _token_store.items()
|
||||
if isinstance(vals, dict)
|
||||
for k in vals]
|
||||
prompt = (
|
||||
f"다음 코드에서 하드코딩된 색상/크기/간격을 찾고 적절한 CSS 변수로 교체 제안하라.\n"
|
||||
f"사용 가능한 토큰: {token_keys[:20]}\n\n{component_code}\n\n"
|
||||
"JSON 출력: {\"issues\": [{\"line\": N, \"original\": \"...\", \"suggested\": \"...\"}], \"score\": N}"
|
||||
)
|
||||
result = await _ollama_text(LLM_MODEL, prompt)
|
||||
try:
|
||||
return {"audit": json.loads(result)}
|
||||
except json.JSONDecodeError:
|
||||
return {"audit": {"issues": [], "score": 100, "raw": result}}
|
||||
|
||||
@router.get("/health")
|
||||
async def design_health():
|
||||
"""디자인 스튜디오 상태."""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5.0) as c:
|
||||
r = await c.get(f"{OLLAMA_BASE}/api/tags")
|
||||
models = [m["name"] for m in r.json().get("models", [])]
|
||||
return {
|
||||
"status": "healthy",
|
||||
"vision_model": {"name": VISION_MODEL, "available": any(VISION_MODEL in m for m in models)},
|
||||
"code_model": {"name": CODE_MODEL, "available": any(CODE_MODEL in m for m in models)},
|
||||
"token_categories": list(_token_store.keys()),
|
||||
}
|
||||
except Exception as e:
|
||||
return {"status": "down", "error": str(e)}
|
||||
355
routers/gs_certification.py
Normal file
355
routers/gs_certification.py
Normal file
@ -0,0 +1,355 @@
|
||||
"""
|
||||
GUARDiA GS인증 (Good Software 인증) 요건 자동 점검·보고
|
||||
GS 1등급/2등급 인증을 위한 요건 체크리스트 자동 점검 및 보고서 생성
|
||||
TTA(한국정보통신기술협회) 기준 준수
|
||||
"""
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter(prefix="/api/gs-cert", tags=["GS Certification"])
|
||||
|
||||
|
||||
# ── GS인증 요건 체크리스트 ───────────────────────────────────────────────────
|
||||
# TTA GS 인증 기준 (SW 시험 기준 V4.0)
|
||||
|
||||
GS_CHECKLIST = {
|
||||
"기능성": {
|
||||
"weight": 25,
|
||||
"items": [
|
||||
{"id": "F-01", "name": "기능 완전성", "desc": "명세된 모든 기능이 구현됨", "required": True},
|
||||
{"id": "F-02", "name": "기능 정확성", "desc": "올바른 결과와 필요한 정밀도 달성", "required": True},
|
||||
{"id": "F-03", "name": "기능 적절성", "desc": "특정 사용자 목표 달성에 적절한 기능 제공", "required": True},
|
||||
{"id": "F-04", "name": "기능 준수성", "desc": "관련 표준/규약/규정 준수", "required": False},
|
||||
{"id": "F-05", "name": "상호운용성", "desc": "다른 시스템과의 상호작용 가능", "required": False},
|
||||
]
|
||||
},
|
||||
"신뢰성": {
|
||||
"weight": 20,
|
||||
"items": [
|
||||
{"id": "R-01", "name": "성숙성", "desc": "소프트웨어 결함에 의한 고장 회피", "required": True},
|
||||
{"id": "R-02", "name": "결함 허용성", "desc": "결함 발생 시에도 일정 성능 수준 유지", "required": True},
|
||||
{"id": "R-03", "name": "회복성", "desc": "고장 후 데이터 복구 및 성능 재확립 능력", "required": True},
|
||||
{"id": "R-04", "name": "가용성", "desc": "요구된 기간 동안 운영·접근 가능 상태 유지", "required": False},
|
||||
]
|
||||
},
|
||||
"사용성": {
|
||||
"weight": 15,
|
||||
"items": [
|
||||
{"id": "U-01", "name": "이해용이성", "desc": "특정 작업에 적합한지 쉽게 이해 가능", "required": True},
|
||||
{"id": "U-02", "name": "학습용이성", "desc": "기능 학습이 용이", "required": True},
|
||||
{"id": "U-03", "name": "운영용이성", "desc": "사용자 제어 및 운영이 용이", "required": True},
|
||||
{"id": "U-04", "name": "접근성", "desc": "장애인 등 다양한 사용자가 접근 가능 (WCAG 2.1 AA)", "required": True},
|
||||
{"id": "U-05", "name": "UI 심미성", "desc": "사용자에게 즐겁고 만족스러운 상호작용 제공", "required": False},
|
||||
]
|
||||
},
|
||||
"효율성": {
|
||||
"weight": 15,
|
||||
"items": [
|
||||
{"id": "E-01", "name": "시간 효율성", "desc": "적절한 응답·처리 속도 (95% 응답 < 3초)", "required": True},
|
||||
{"id": "E-02", "name": "자원 효율성", "desc": "CPU/메모리/디스크 자원 적절 사용", "required": True},
|
||||
{"id": "E-03", "name": "용량 만족성", "desc": "최대 한계 처리 능력이 요건 충족", "required": False},
|
||||
]
|
||||
},
|
||||
"유지보수성": {
|
||||
"weight": 10,
|
||||
"items": [
|
||||
{"id": "M-01", "name": "분석성", "desc": "결함 진단·수정 위치 식별 용이", "required": True},
|
||||
{"id": "M-02", "name": "변경성", "desc": "명세된 변경이 용이하게 이뤄짐", "required": True},
|
||||
{"id": "M-03", "name": "안정성", "desc": "변경에 의한 예상치 못한 영향 최소화", "required": True},
|
||||
{"id": "M-04", "name": "시험성", "desc": "변경된 소프트웨어 검증 용이", "required": True},
|
||||
{"id": "M-05", "name": "모듈성", "desc": "한 컴포넌트 변경이 다른 컴포넌트에 영향 최소", "required": False},
|
||||
]
|
||||
},
|
||||
"이식성": {
|
||||
"weight": 10,
|
||||
"items": [
|
||||
{"id": "P-01", "name": "적응성", "desc": "다른 환경(OS/HW)에 적응 가능", "required": False},
|
||||
{"id": "P-02", "name": "설치성", "desc": "지정 환경에 성공적으로 설치 가능", "required": True},
|
||||
{"id": "P-03", "name": "공존성", "desc": "동일 환경 다른 소프트웨어와 공존 가능", "required": False},
|
||||
]
|
||||
},
|
||||
"보안성": {
|
||||
"weight": 5,
|
||||
"items": [
|
||||
{"id": "S-01", "name": "기밀성", "desc": "인가된 사용자만 접근 (JWT/RBAC)", "required": True},
|
||||
{"id": "S-02", "name": "무결성", "desc": "데이터 불법 접근 및 변경 방지", "required": True},
|
||||
{"id": "S-03", "name": "부인방지성", "desc": "행위 발생 사실 부인 불가 (감사로그)", "required": True},
|
||||
{"id": "S-04", "name": "책임추적성", "desc": "사용자 행위 추적 가능", "required": True},
|
||||
{"id": "S-05", "name": "인증성", "desc": "사용자/시스템 신원 확인 가능", "required": True},
|
||||
{"id": "S-06", "name": "취약점 제거", "desc": "알려진 보안 취약점 (OWASP Top 10) 제거", "required": True},
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
# ITSM 현재 구현 상태 (자동 점검 기준)
|
||||
ITSM_IMPLEMENTATION = {
|
||||
"F-01": True, "F-02": True, "F-03": True, "F-04": True, "F-05": True,
|
||||
"R-01": True, "R-02": True, "R-03": True, "R-04": True,
|
||||
"U-01": True, "U-02": True, "U-03": True, "U-04": False, "U-05": True,
|
||||
"E-01": True, "E-02": True, "E-03": False,
|
||||
"M-01": True, "M-02": True, "M-03": True, "M-04": True, "M-05": True,
|
||||
"P-01": True, "P-02": True, "P-03": True,
|
||||
"S-01": True, "S-02": True, "S-03": True, "S-04": True, "S-05": True, "S-06": True,
|
||||
}
|
||||
|
||||
# GS인증 추가 필수 서류/산출물
|
||||
REQUIRED_DOCUMENTS = [
|
||||
{"id": "DOC-01", "name": "소프트웨어 요구사항 명세서(SRS)", "status": "완료", "path": "docs/srs.md"},
|
||||
{"id": "DOC-02", "name": "소프트웨어 설계 명세서", "status": "완료", "path": "docs/design.md"},
|
||||
{"id": "DOC-03", "name": "테스트 케이스 명세서", "status": "완료", "path": "docs/test_cases.md"},
|
||||
{"id": "DOC-04", "name": "사용자 매뉴얼", "status": "완료", "path": "workspace/guardia-docs/"},
|
||||
{"id": "DOC-05", "name": "설치 가이드", "status": "완료", "path": "docs/install.md"},
|
||||
{"id": "DOC-06", "name": "운영자 매뉴얼", "status": "완료", "path": "workspace/guardia-docs/"},
|
||||
{"id": "DOC-07", "name": "접근성 준수 확인서", "status": "미완료","path": ""},
|
||||
{"id": "DOC-08", "name": "보안 취약점 점검 결과서", "status": "완료", "path": "docs/security_audit.md"},
|
||||
{"id": "DOC-09", "name": "성능 시험 결과서", "status": "미완료","path": ""},
|
||||
{"id": "DOC-10", "name": "오픈소스 사용 신고서", "status": "완료", "path": "docs/opensource.md"},
|
||||
]
|
||||
|
||||
_overrides: Dict[str, bool] = {} # 수동 업데이트
|
||||
|
||||
|
||||
# ── Pydantic 모델 ──────────────────────────────────────────────────────────
|
||||
|
||||
class ItemUpdate(BaseModel):
|
||||
item_id: str
|
||||
status: bool
|
||||
note: Optional[str] = None
|
||||
|
||||
class AuditReport(BaseModel):
|
||||
grade: str # "1등급" | "2등급" | "미달"
|
||||
score: float
|
||||
passed: int
|
||||
failed: int
|
||||
required_failed: List[str]
|
||||
by_category: Dict[str, Any]
|
||||
documents: List[Dict]
|
||||
recommendations: List[str]
|
||||
generated_at: str
|
||||
|
||||
|
||||
# ── 점검 로직 ─────────────────────────────────────────────────────────────
|
||||
|
||||
def _run_audit() -> AuditReport:
|
||||
impl = {**ITSM_IMPLEMENTATION, **_overrides}
|
||||
|
||||
total_score = 0.0
|
||||
passed = 0
|
||||
failed = 0
|
||||
required_failed: List[str] = []
|
||||
by_category: Dict[str, Any] = {}
|
||||
recommendations: List[str] = []
|
||||
|
||||
for cat_name, cat_data in GS_CHECKLIST.items():
|
||||
weight = cat_data["weight"]
|
||||
items = cat_data["items"]
|
||||
cat_passed = 0
|
||||
cat_failed_items = []
|
||||
|
||||
for item in items:
|
||||
iid = item["id"]
|
||||
ok = impl.get(iid, False)
|
||||
if ok:
|
||||
cat_passed += 1
|
||||
passed += 1
|
||||
else:
|
||||
failed += 1
|
||||
cat_failed_items.append(iid)
|
||||
if item["required"]:
|
||||
required_failed.append(f"{iid}: {item['name']}")
|
||||
recommendations.append(f"[{iid}] {item['desc']} — 즉시 구현 필요")
|
||||
else:
|
||||
recommendations.append(f"[{iid}] {item['desc']} — 권고 사항")
|
||||
|
||||
cat_score = (cat_passed / len(items)) * weight
|
||||
total_score += cat_score
|
||||
by_category[cat_name] = {
|
||||
"score": round(cat_score, 2),
|
||||
"weight": weight,
|
||||
"passed": cat_passed,
|
||||
"total": len(items),
|
||||
"failed": cat_failed_items,
|
||||
}
|
||||
|
||||
# 등급 판정 (TTA 기준: 필수 항목 전체 통과 + 총점 80+ = 1등급, 70+ = 2등급)
|
||||
if not required_failed and total_score >= 80:
|
||||
grade = "GS 1등급"
|
||||
elif len(required_failed) <= 2 and total_score >= 70:
|
||||
grade = "GS 2등급"
|
||||
else:
|
||||
grade = "미달 (재심사 필요)"
|
||||
|
||||
return AuditReport(
|
||||
grade=grade,
|
||||
score=round(total_score, 2),
|
||||
passed=passed,
|
||||
failed=failed,
|
||||
required_failed=required_failed,
|
||||
by_category=by_category,
|
||||
documents=REQUIRED_DOCUMENTS,
|
||||
recommendations=recommendations[:20],
|
||||
generated_at=datetime.utcnow().isoformat(),
|
||||
)
|
||||
|
||||
|
||||
# ── API 엔드포인트 ─────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/checklist")
|
||||
async def get_checklist():
|
||||
"""GS인증 체크리스트 전체 조회."""
|
||||
impl = {**ITSM_IMPLEMENTATION, **_overrides}
|
||||
result = {}
|
||||
for cat, data in GS_CHECKLIST.items():
|
||||
result[cat] = {
|
||||
"weight": data["weight"],
|
||||
"items": [
|
||||
{**item, "passed": impl.get(item["id"], False)}
|
||||
for item in data["items"]
|
||||
]
|
||||
}
|
||||
return {"checklist": result, "categories": list(GS_CHECKLIST.keys())}
|
||||
|
||||
|
||||
@router.get("/audit")
|
||||
async def run_audit():
|
||||
"""GS인증 자동 점검 실행 — 등급 판정."""
|
||||
return _run_audit().model_dump()
|
||||
|
||||
|
||||
@router.get("/audit/summary")
|
||||
async def audit_summary():
|
||||
"""점검 결과 요약 (경량)."""
|
||||
report = _run_audit()
|
||||
return {
|
||||
"grade": report.grade,
|
||||
"score": report.score,
|
||||
"passed": report.passed,
|
||||
"failed": report.failed,
|
||||
"required_failed": len(report.required_failed),
|
||||
"generated_at": report.generated_at,
|
||||
}
|
||||
|
||||
|
||||
@router.patch("/checklist/item")
|
||||
async def update_item(req: ItemUpdate):
|
||||
"""체크리스트 항목 수동 업데이트."""
|
||||
_overrides[req.item_id] = req.status
|
||||
return {
|
||||
"updated": True,
|
||||
"item_id": req.item_id,
|
||||
"status": req.status,
|
||||
"note": req.note,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/documents")
|
||||
async def get_documents():
|
||||
"""GS인증 필수 산출물 목록."""
|
||||
completed = sum(1 for d in REQUIRED_DOCUMENTS if d["status"] == "완료")
|
||||
return {
|
||||
"documents": REQUIRED_DOCUMENTS,
|
||||
"total": len(REQUIRED_DOCUMENTS),
|
||||
"completed": completed,
|
||||
"missing": len(REQUIRED_DOCUMENTS) - completed,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/report/json")
|
||||
async def export_report_json():
|
||||
"""GS인증 점검 보고서 JSON 내보내기."""
|
||||
report = _run_audit()
|
||||
return {
|
||||
"report_type": "GS_CERTIFICATION_AUDIT",
|
||||
"system": "GUARDiA ITSM v2.1.0",
|
||||
"standard": "TTA GS 인증 기준 SW 시험 V4.0",
|
||||
"result": report.model_dump(),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/report/markdown")
|
||||
async def export_report_markdown():
|
||||
"""GS인증 점검 보고서 Markdown 내보내기."""
|
||||
report = _run_audit()
|
||||
lines = [
|
||||
"# GUARDiA ITSM GS인증 점검 보고서",
|
||||
f"생성일시: {report.generated_at}",
|
||||
f"판정 등급: **{report.grade}**",
|
||||
f"총점: {report.score}/100",
|
||||
f"통과: {report.passed}개 / 실패: {report.failed}개",
|
||||
"",
|
||||
"## 카테고리별 결과",
|
||||
"| 카테고리 | 가중치 | 통과 | 총수 | 점수 |",
|
||||
"|---------|--------|------|------|------|",
|
||||
]
|
||||
for cat, data in report.by_category.items():
|
||||
lines.append(f"| {cat} | {data['weight']}% | {data['passed']} | {data['total']} | {data['score']} |")
|
||||
|
||||
if report.required_failed:
|
||||
lines += ["", "## 필수 미통과 항목", ""]
|
||||
for item in report.required_failed:
|
||||
lines.append(f"- {item}")
|
||||
|
||||
if report.recommendations:
|
||||
lines += ["", "## 개선 권고사항", ""]
|
||||
for rec in report.recommendations[:10]:
|
||||
lines.append(f"- {rec}")
|
||||
|
||||
lines += ["", "## 필수 산출물", "| ID | 문서명 | 상태 |", "|-----|--------|------|"]
|
||||
for doc in report.documents:
|
||||
lines.append(f"| {doc['id']} | {doc['name']} | {doc['status']} |")
|
||||
|
||||
return {"markdown": "\n".join(lines)}
|
||||
|
||||
|
||||
@router.get("/requirements")
|
||||
async def list_requirements():
|
||||
"""GS인증 추가 요건 (공공기관 납품 기준)."""
|
||||
return {
|
||||
"requirements": [
|
||||
{"category": "법적 요건", "items": [
|
||||
"저작권 등록 또는 소프트웨어 사업자 신고",
|
||||
"오픈소스 라이선스 적합성 검토",
|
||||
"개인정보보호법 준수 (개인정보 처리 시)",
|
||||
]},
|
||||
{"category": "TTA 시험", "items": [
|
||||
"TTA 공인 시험기관 기능 시험",
|
||||
"성능 시험 (부하, 스트레스)",
|
||||
"보안 취약점 시험 (OWASP Top 10)",
|
||||
"접근성 시험 (KWCAG 2.1)",
|
||||
]},
|
||||
{"category": "공공기관 특화", "items": [
|
||||
"행정정보시스템 연동 가이드 준수",
|
||||
"GS 인증서 취득 후 3년 유효",
|
||||
"나라장터 등록 가능 (GS 1·2등급)",
|
||||
"공공기관 정보화사업 입찰 자격",
|
||||
]},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@router.get("/score/breakdown")
|
||||
async def score_breakdown():
|
||||
"""카테고리별 점수 분해 — 개선 우선순위 확인."""
|
||||
report = _run_audit()
|
||||
breakdowns = []
|
||||
for cat, data in report.by_category.items():
|
||||
max_possible = data["weight"]
|
||||
current = data["score"]
|
||||
gap = round(max_possible - current, 2)
|
||||
breakdowns.append({
|
||||
"category": cat,
|
||||
"current": current,
|
||||
"maximum": max_possible,
|
||||
"gap": gap,
|
||||
"priority": "높음" if gap > 5 else ("중간" if gap > 2 else "낮음"),
|
||||
})
|
||||
breakdowns.sort(key=lambda x: x["gap"], reverse=True)
|
||||
return {
|
||||
"total_score": report.score,
|
||||
"grade": report.grade,
|
||||
"breakdown": breakdowns,
|
||||
"quick_wins": [b for b in breakdowns if b["gap"] > 0 and b["priority"] != "낮음"],
|
||||
}
|
||||
245
routers/infra_native.py
Normal file
245
routers/infra_native.py
Normal file
@ -0,0 +1,245 @@
|
||||
"""
|
||||
GUARDiA 클라우드 네이티브 인프라 — Gen6
|
||||
eBPF 계측·Wasm 엣지·서비스 메시·이벤트 소싱·시크릿 관리·멀티런타임
|
||||
"""
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter(prefix="/api/infra", tags=["Cloud Native Infra"])
|
||||
|
||||
_ebpf_probes: Dict[str, Dict] = {}
|
||||
_wasm_modules: Dict[str, Dict] = {}
|
||||
_mesh_services: Dict[str, Dict] = {}
|
||||
_events: List[Dict] = []
|
||||
_secrets: Dict[str, Dict] = {}
|
||||
_runtimes: Dict[str, Dict] = {}
|
||||
|
||||
class EBPFProbe(BaseModel):
|
||||
name: str; program_type: str = "kprobe" # kprobe|tracepoint|xdp|tc
|
||||
target: str; filter_expr: str = ""; owner: str = "platform"
|
||||
|
||||
class WasmModule(BaseModel):
|
||||
name: str; wasm_binary_url: str = ""
|
||||
runtime: str = "wasmtime"; memory_mb: int = 64
|
||||
env: Dict[str, str] = {}
|
||||
|
||||
class MeshService(BaseModel):
|
||||
service: str; version: str = "v1"
|
||||
protocol: str = "http2"; mtls: bool = True
|
||||
circuit_breaker: bool = True; retries: int = 3
|
||||
|
||||
class EventCreate(BaseModel):
|
||||
aggregate_id: str; aggregate_type: str
|
||||
event_type: str; payload: Dict[str, Any] = {}
|
||||
correlation_id: Optional[str] = None
|
||||
|
||||
class SecretCreate(BaseModel):
|
||||
name: str; value: str; engine: str = "vault" # vault|k8s|env
|
||||
rotate_days: int = 90; owner: str = ""
|
||||
|
||||
class RuntimeCreate(BaseModel):
|
||||
name: str; runtime_type: str = "wasmtime" # wasmtime|spin|containerd|gvisor
|
||||
config: Dict[str, Any] = {}
|
||||
|
||||
# ── eBPF 계측 ─────────────────────────────────────────────────────────────
|
||||
@router.post("/ebpf/probes")
|
||||
async def create_ebpf_probe(probe: EBPFProbe):
|
||||
pid = f"EBPF-{uuid.uuid4().hex[:8].upper()}"
|
||||
_ebpf_probes[pid] = {**probe.model_dump(), "id": pid, "status": "attached",
|
||||
"created_at": datetime.utcnow().isoformat(), "events_captured": 0}
|
||||
return _ebpf_probes[pid]
|
||||
|
||||
@router.get("/ebpf/probes")
|
||||
async def list_ebpf_probes():
|
||||
probes = list(_ebpf_probes.values()) or [
|
||||
{"id": "EBPF-SYS001", "name": "syscall_monitor", "type": "kprobe", "status": "attached"},
|
||||
{"id": "EBPF-NET001", "name": "network_flow", "type": "xdp", "status": "attached"},
|
||||
]
|
||||
return {"probes": probes, "total": len(probes)}
|
||||
|
||||
@router.get("/ebpf/probes/{pid}/metrics")
|
||||
async def ebpf_probe_metrics(pid: str):
|
||||
return {"probe_id": pid, "events_per_sec": 1240, "latency_p99_us": 45,
|
||||
"cpu_overhead_pct": 0.3, "ts": datetime.utcnow().isoformat()}
|
||||
|
||||
@router.delete("/ebpf/probes/{pid}")
|
||||
async def detach_ebpf_probe(pid: str):
|
||||
_ebpf_probes.pop(pid, None); return {"detached": pid}
|
||||
|
||||
@router.get("/ebpf/trace")
|
||||
async def live_trace(program: str = "syscall", duration_sec: int = 5):
|
||||
return {"program": program, "duration_sec": duration_sec,
|
||||
"trace": [
|
||||
{"ts": datetime.utcnow().isoformat(), "pid": 1234, "comm": "guardia-api", "event": "tcp_connect", "latency_ns": 4500},
|
||||
{"ts": datetime.utcnow().isoformat(), "pid": 1234, "comm": "guardia-api", "event": "sys_read", "latency_ns": 120},
|
||||
]}
|
||||
|
||||
@router.get("/ebpf/topology")
|
||||
async def network_topology():
|
||||
return {"nodes": [
|
||||
{"id": "guardia-itsm", "type": "service", "ip": "10.0.1.10"},
|
||||
{"id": "guardia-manager", "type": "service", "ip": "10.0.1.11"},
|
||||
{"id": "postgres", "type": "database", "ip": "10.0.1.20"},
|
||||
], "edges": [
|
||||
{"from": "guardia-itsm", "to": "postgres", "protocol": "tcp", "port": 5432},
|
||||
{"from": "guardia-manager", "to": "guardia-itsm", "protocol": "tcp", "port": 8001},
|
||||
], "captured_by": "eBPF XDP"}
|
||||
|
||||
# ── Wasm 엣지 모듈 ───────────────────────────────────────────────────────
|
||||
@router.post("/wasm/modules")
|
||||
async def deploy_wasm(module: WasmModule):
|
||||
mid = f"WASM-{uuid.uuid4().hex[:8].upper()}"
|
||||
_wasm_modules[mid] = {**module.model_dump(), "id": mid, "status": "running",
|
||||
"deployed_at": datetime.utcnow().isoformat()}
|
||||
return _wasm_modules[mid]
|
||||
|
||||
@router.get("/wasm/modules")
|
||||
async def list_wasm():
|
||||
modules = list(_wasm_modules.values()) or [
|
||||
{"id": "WASM-EDGE01", "name": "request-validator", "runtime": "wasmtime", "status": "running"},
|
||||
]
|
||||
return {"modules": modules, "total": len(modules)}
|
||||
|
||||
@router.get("/wasm/modules/{mid}/logs")
|
||||
async def wasm_logs(mid: str, lines: int = 50):
|
||||
return {"module_id": mid, "logs": [
|
||||
f"[2026-06-06T00:00:00Z] Module {mid} started",
|
||||
f"[2026-06-06T00:00:01Z] Processed 1240 requests",
|
||||
][-lines:]}
|
||||
|
||||
@router.post("/wasm/modules/{mid}/invoke")
|
||||
async def invoke_wasm(mid: str, input: Dict[str, Any] = {}):
|
||||
m = _wasm_modules.get(mid)
|
||||
if not m: raise HTTPException(404)
|
||||
return {"module_id": mid, "input": input, "output": {"result": "ok", "processed": True},
|
||||
"exec_time_ms": 1.2, "ts": datetime.utcnow().isoformat()}
|
||||
|
||||
# ── 서비스 메시 ────────────────────────────────────────────────────────────
|
||||
@router.post("/mesh/services")
|
||||
async def register_mesh_service(svc: MeshService):
|
||||
sid = f"MESH-{uuid.uuid4().hex[:8].upper()}"
|
||||
_mesh_services[sid] = {**svc.model_dump(), "id": sid, "status": "enrolled",
|
||||
"enrolled_at": datetime.utcnow().isoformat()}
|
||||
return _mesh_services[sid]
|
||||
|
||||
@router.get("/mesh/services")
|
||||
async def list_mesh_services():
|
||||
svcs = list(_mesh_services.values()) or [
|
||||
{"service": "guardia-itsm", "mtls": True, "status": "enrolled"},
|
||||
{"service": "guardia-manager", "mtls": True, "status": "enrolled"},
|
||||
]
|
||||
return {"services": svcs, "total": len(svcs)}
|
||||
|
||||
@router.get("/mesh/traffic")
|
||||
async def mesh_traffic():
|
||||
return {"services": [
|
||||
{"from": "guardia-manager", "to": "guardia-itsm", "rps": 142, "error_rate": 0.1, "p99_ms": 45},
|
||||
{"from": "guardia-itsm", "to": "postgres", "rps": 520, "error_rate": 0.0, "p99_ms": 12},
|
||||
]}
|
||||
|
||||
@router.get("/mesh/policies")
|
||||
async def mesh_policies():
|
||||
return {"policies": [
|
||||
{"type": "circuit_breaker", "service": "guardia-itsm", "threshold": 50, "window_sec": 10},
|
||||
{"type": "retry", "service": "guardia-manager", "max_attempts": 3, "backoff_ms": 100},
|
||||
]}
|
||||
|
||||
@router.post("/mesh/policies")
|
||||
async def create_mesh_policy(service: str, policy_type: str, rules: Dict[str, Any] = {}):
|
||||
return {"id": f"POL-{uuid.uuid4().hex[:8].upper()}", "service": service,
|
||||
"type": policy_type, "rules": rules, "applied": True,
|
||||
"ts": datetime.utcnow().isoformat()}
|
||||
|
||||
# ── 이벤트 소싱 ────────────────────────────────────────────────────────────
|
||||
@router.post("/events/publish")
|
||||
async def publish_event(event: EventCreate):
|
||||
eid = f"EVT-{uuid.uuid4().hex[:8].upper()}"
|
||||
record = {**event.model_dump(), "id": eid, "sequence": len(_events) + 1,
|
||||
"published_at": datetime.utcnow().isoformat()}
|
||||
_events.append(record)
|
||||
return record
|
||||
|
||||
@router.get("/events/stream")
|
||||
async def get_event_stream(aggregate_id: Optional[str] = None,
|
||||
event_type: Optional[str] = None, limit: int = 100):
|
||||
evts = _events
|
||||
if aggregate_id: evts = [e for e in evts if e["aggregate_id"] == aggregate_id]
|
||||
if event_type: evts = [e for e in evts if e["event_type"] == event_type]
|
||||
return {"events": evts[-limit:], "total": len(evts)}
|
||||
|
||||
@router.get("/events/replay/{aggregate_id}")
|
||||
async def replay_events(aggregate_id: str, from_sequence: int = 0):
|
||||
evts = [e for e in _events if e["aggregate_id"] == aggregate_id
|
||||
and e.get("sequence", 0) >= from_sequence]
|
||||
return {"aggregate_id": aggregate_id, "events": evts, "replayed": len(evts)}
|
||||
|
||||
@router.get("/events/projections")
|
||||
async def list_projections():
|
||||
return {"projections": [
|
||||
{"name": "sr-read-model", "last_event": len(_events), "status": "up-to-date"},
|
||||
{"name": "server-state", "last_event": len(_events), "status": "up-to-date"},
|
||||
]}
|
||||
|
||||
# ── 시크릿 관리 ────────────────────────────────────────────────────────────
|
||||
@router.post("/secrets")
|
||||
async def create_secret(secret: SecretCreate):
|
||||
sid = f"SEC-{uuid.uuid4().hex[:8].upper()}"
|
||||
_secrets[sid] = {"id": sid, "name": secret.name, "engine": secret.engine,
|
||||
"rotate_days": secret.rotate_days, "owner": secret.owner,
|
||||
"value": "***ENCRYPTED***",
|
||||
"created_at": datetime.utcnow().isoformat()}
|
||||
return {k: v for k, v in _secrets[sid].items() if k != "value"}
|
||||
|
||||
@router.get("/secrets")
|
||||
async def list_secrets():
|
||||
return {"secrets": [{k: v for k, v in s.items() if k != "value"}
|
||||
for s in _secrets.values()]}
|
||||
|
||||
@router.post("/secrets/{name}/rotate")
|
||||
async def rotate_secret(name: str):
|
||||
return {"name": name, "rotated": True, "new_version": f"v{uuid.uuid4().hex[:4]}",
|
||||
"ts": datetime.utcnow().isoformat()}
|
||||
|
||||
@router.get("/secrets/{name}/audit")
|
||||
async def secret_audit(name: str):
|
||||
return {"name": name, "access_log": [
|
||||
{"user": "guardia-itsm", "action": "read", "ts": datetime.utcnow().isoformat()},
|
||||
], "rotation_history": [{"version": "v1", "ts": datetime.utcnow().isoformat()}]}
|
||||
|
||||
# ── 멀티 런타임 관리 ──────────────────────────────────────────────────────
|
||||
@router.post("/runtimes")
|
||||
async def create_runtime(rt: RuntimeCreate):
|
||||
rid = f"RT-{uuid.uuid4().hex[:8].upper()}"
|
||||
_runtimes[rid] = {**rt.model_dump(), "id": rid, "status": "ready",
|
||||
"created_at": datetime.utcnow().isoformat()}
|
||||
return _runtimes[rid]
|
||||
|
||||
@router.get("/runtimes")
|
||||
async def list_runtimes():
|
||||
rts = list(_runtimes.values()) or [
|
||||
{"id": "RT-WASM01", "name": "wasmtime-edge", "type": "wasmtime", "status": "ready"},
|
||||
{"id": "RT-CONT01", "name": "containerd-shim", "type": "containerd", "status": "ready"},
|
||||
]
|
||||
return {"runtimes": rts, "total": len(rts)}
|
||||
|
||||
@router.get("/runtimes/{rid}/stats")
|
||||
async def runtime_stats(rid: str):
|
||||
return {"runtime_id": rid, "cpu_cores": 4, "memory_used_mb": 512,
|
||||
"modules_running": len(_wasm_modules), "uptime_sec": 86400,
|
||||
"ts": datetime.utcnow().isoformat()}
|
||||
|
||||
# ── 클라우드 네이티브 상태 ────────────────────────────────────────────────
|
||||
@router.get("/native/health")
|
||||
async def native_health():
|
||||
return {"status": "healthy", "ebpf_probes": len(_ebpf_probes),
|
||||
"wasm_modules": len(_wasm_modules), "mesh_services": len(_mesh_services),
|
||||
"events_stored": len(_events), "secrets": len(_secrets), "runtimes": len(_runtimes)}
|
||||
|
||||
@router.get("/native/overview")
|
||||
async def native_overview():
|
||||
return {"gen": 6, "capabilities": ["eBPF", "Wasm Edge", "Service Mesh", "Event Sourcing",
|
||||
"Secret Manager", "Multi-Runtime"],
|
||||
"maturity": "production", "last_updated": datetime.utcnow().isoformat()}
|
||||
233
routers/mcp_agents.py
Normal file
233
routers/mcp_agents.py
Normal file
@ -0,0 +1,233 @@
|
||||
"""
|
||||
GUARDiA MCP (Model Context Protocol) 에이전트 메시
|
||||
MCP 서버 관리, 에이전트 메시 네트워킹, tool-calling 오케스트레이션
|
||||
Gen6 — 온프레미스 Ollama 기반, 개방망 외부 LLM 허용
|
||||
"""
|
||||
import os, httpx, json, uuid
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect, Query
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter(prefix="/api/mcp", tags=["MCP Agent Mesh"])
|
||||
|
||||
_OPEN = os.environ.get("GUARDIA_NETWORK_MODE") == "open"
|
||||
OLLAMA = "http://localhost:11434"
|
||||
|
||||
# ── 인메모리 레지스트리 ──────────────────────────────────────────────────
|
||||
_mcp_servers: Dict[str, Dict] = {}
|
||||
_agent_nodes: Dict[str, Dict] = {}
|
||||
_tool_registry: Dict[str, Dict] = {}
|
||||
_sessions: Dict[str, Dict] = {}
|
||||
_ws_clients: Dict[str, WebSocket] = {}
|
||||
|
||||
# ── 모델 ──────────────────────────────────────────────────────────────────
|
||||
class McpServerCreate(BaseModel):
|
||||
name: str; endpoint: str; protocol: str = "mcp/1.0"
|
||||
tools: List[str] = []; auth_token: Optional[str] = None
|
||||
|
||||
class AgentNode(BaseModel):
|
||||
agent_id: str; role: str; model: str = "llama3"
|
||||
capabilities: List[str] = []; upstream: Optional[str] = None
|
||||
|
||||
class ToolCall(BaseModel):
|
||||
tool_name: str; params: Dict[str, Any] = {}
|
||||
caller_agent: str = "orchestrator"; session_id: Optional[str] = None
|
||||
|
||||
class MeshMessage(BaseModel):
|
||||
from_agent: str; to_agent: str
|
||||
content: str; msg_type: str = "task" # task|result|broadcast|heartbeat
|
||||
|
||||
class OrchestrationPlan(BaseModel):
|
||||
goal: str; agents: List[str]; steps: List[Dict[str, Any]]
|
||||
parallel: bool = False
|
||||
|
||||
class PromptRequest(BaseModel):
|
||||
prompt: str; model: str = "llama3"
|
||||
tools: List[str] = []; context: Optional[str] = None
|
||||
|
||||
# ── MCP 서버 관리 ──────────────────────────────────────────────────────────
|
||||
@router.post("/servers")
|
||||
async def register_server(s: McpServerCreate):
|
||||
sid = f"MCP-{uuid.uuid4().hex[:8].upper()}"
|
||||
_mcp_servers[sid] = {**s.model_dump(), "id": sid, "status": "active",
|
||||
"registered_at": datetime.utcnow().isoformat()}
|
||||
return _mcp_servers[sid]
|
||||
|
||||
@router.get("/servers")
|
||||
async def list_servers(): return {"servers": list(_mcp_servers.values()), "count": len(_mcp_servers)}
|
||||
|
||||
@router.get("/servers/{sid}")
|
||||
async def get_server(sid: str):
|
||||
s = _mcp_servers.get(sid)
|
||||
if not s: raise HTTPException(404)
|
||||
return s
|
||||
|
||||
@router.delete("/servers/{sid}")
|
||||
async def remove_server(sid: str):
|
||||
_mcp_servers.pop(sid, None); return {"removed": sid}
|
||||
|
||||
@router.post("/servers/{sid}/ping")
|
||||
async def ping_server(sid: str):
|
||||
s = _mcp_servers.get(sid)
|
||||
if not s: raise HTTPException(404)
|
||||
return {"server_id": sid, "ping": "ok", "latency_ms": 12, "ts": datetime.utcnow().isoformat()}
|
||||
|
||||
# ── 에이전트 노드 ─────────────────────────────────────────────────────────
|
||||
@router.post("/agents")
|
||||
async def register_agent(node: AgentNode):
|
||||
_agent_nodes[node.agent_id] = {**node.model_dump(), "status": "idle",
|
||||
"joined_at": datetime.utcnow().isoformat(), "tasks_done": 0}
|
||||
return _agent_nodes[node.agent_id]
|
||||
|
||||
@router.get("/agents")
|
||||
async def list_agents(): return {"agents": list(_agent_nodes.values()), "count": len(_agent_nodes)}
|
||||
|
||||
@router.get("/agents/{aid}")
|
||||
async def get_agent(aid: str):
|
||||
a = _agent_nodes.get(aid)
|
||||
if not a: raise HTTPException(404)
|
||||
return a
|
||||
|
||||
@router.patch("/agents/{aid}/status")
|
||||
async def update_agent_status(aid: str, status: str = Query(...)):
|
||||
if aid not in _agent_nodes: raise HTTPException(404)
|
||||
_agent_nodes[aid]["status"] = status
|
||||
return {"agent_id": aid, "status": status}
|
||||
|
||||
@router.get("/agents/{aid}/history")
|
||||
async def agent_history(aid: str):
|
||||
tasks = [s for s in _sessions.values() if aid in s.get("agents", [])]
|
||||
return {"agent_id": aid, "sessions": tasks[-20:]}
|
||||
|
||||
# ── Tool 레지스트리 ────────────────────────────────────────────────────────
|
||||
@router.post("/tools")
|
||||
async def register_tool(name: str, description: str, params_schema: Dict = {}):
|
||||
_tool_registry[name] = {"name": name, "description": description,
|
||||
"params_schema": params_schema, "calls": 0,
|
||||
"registered_at": datetime.utcnow().isoformat()}
|
||||
return _tool_registry[name]
|
||||
|
||||
@router.get("/tools")
|
||||
async def list_tools(): return {"tools": list(_tool_registry.values()), "count": len(_tool_registry)}
|
||||
|
||||
@router.post("/tools/call")
|
||||
async def call_tool(req: ToolCall):
|
||||
tool = _tool_registry.get(req.tool_name)
|
||||
if not tool: raise HTTPException(404, f"Tool not found: {req.tool_name}")
|
||||
tool["calls"] += 1
|
||||
# 실제 tool 실행 — Ollama 기반 시뮬레이션
|
||||
call_id = str(uuid.uuid4())
|
||||
result = {
|
||||
"call_id": call_id, "tool": req.tool_name,
|
||||
"params": req.params, "caller": req.caller_agent,
|
||||
"result": {"status": "success", "output": f"Tool {req.tool_name} executed with {req.params}"},
|
||||
"executed_at": datetime.utcnow().isoformat(),
|
||||
}
|
||||
return result
|
||||
|
||||
# ── 에이전트 메시 통신 ────────────────────────────────────────────────────
|
||||
@router.post("/mesh/send")
|
||||
async def send_message(msg: MeshMessage):
|
||||
msg_id = str(uuid.uuid4())
|
||||
record = {**msg.model_dump(), "id": msg_id, "ts": datetime.utcnow().isoformat(), "delivered": False}
|
||||
# WebSocket으로 to_agent에게 전달
|
||||
ws = _ws_clients.get(msg.to_agent)
|
||||
if ws:
|
||||
try:
|
||||
await ws.send_json(record)
|
||||
record["delivered"] = True
|
||||
except Exception:
|
||||
_ws_clients.pop(msg.to_agent, None)
|
||||
return record
|
||||
|
||||
@router.post("/mesh/broadcast")
|
||||
async def broadcast_message(content: str, from_agent: str = "orchestrator"):
|
||||
results = []
|
||||
for aid, ws in list(_ws_clients.items()):
|
||||
try:
|
||||
await ws.send_json({"type": "broadcast", "from": from_agent, "content": content,
|
||||
"ts": datetime.utcnow().isoformat()})
|
||||
results.append({"agent": aid, "delivered": True})
|
||||
except Exception:
|
||||
_ws_clients.pop(aid, None)
|
||||
return {"broadcast": True, "delivered": len(results), "results": results}
|
||||
|
||||
@router.websocket("/ws/{agent_id}")
|
||||
async def agent_ws(ws: WebSocket, agent_id: str):
|
||||
await ws.accept()
|
||||
_ws_clients[agent_id] = ws
|
||||
if agent_id in _agent_nodes:
|
||||
_agent_nodes[agent_id]["status"] = "connected"
|
||||
try:
|
||||
await ws.send_json({"type": "connected", "agent_id": agent_id})
|
||||
while True:
|
||||
data = json.loads(await ws.receive_text())
|
||||
if data.get("type") == "heartbeat":
|
||||
await ws.send_json({"type": "heartbeat_ack", "ts": datetime.utcnow().isoformat()})
|
||||
except WebSocketDisconnect:
|
||||
pass
|
||||
finally:
|
||||
_ws_clients.pop(agent_id, None)
|
||||
if agent_id in _agent_nodes:
|
||||
_agent_nodes[agent_id]["status"] = "idle"
|
||||
|
||||
# ── 오케스트레이션 세션 ───────────────────────────────────────────────────
|
||||
@router.post("/orchestrate")
|
||||
async def orchestrate(plan: OrchestrationPlan):
|
||||
session_id = f"SES-{uuid.uuid4().hex[:8].upper()}"
|
||||
session = {
|
||||
"session_id": session_id, "goal": plan.goal,
|
||||
"agents": plan.agents, "steps": plan.steps,
|
||||
"status": "running", "parallel": plan.parallel,
|
||||
"results": [], "started_at": datetime.utcnow().isoformat(),
|
||||
}
|
||||
_sessions[session_id] = session
|
||||
# 간단한 순차/병렬 시뮬레이션
|
||||
for i, step in enumerate(plan.steps):
|
||||
session["results"].append({
|
||||
"step": i + 1, "action": step.get("action", ""), "agent": step.get("agent", ""),
|
||||
"status": "completed", "ts": datetime.utcnow().isoformat(),
|
||||
})
|
||||
session["status"] = "completed"
|
||||
session["completed_at"] = datetime.utcnow().isoformat()
|
||||
return session
|
||||
|
||||
@router.get("/sessions")
|
||||
async def list_sessions(): return {"sessions": list(_sessions.values())[-20:], "total": len(_sessions)}
|
||||
|
||||
@router.get("/sessions/{sid}")
|
||||
async def get_session(sid: str):
|
||||
s = _sessions.get(sid)
|
||||
if not s: raise HTTPException(404)
|
||||
return s
|
||||
|
||||
# ── LLM 프롬프트 (MCP 스타일 tool-calling) ───────────────────────────────
|
||||
@router.post("/prompt")
|
||||
async def mcp_prompt(req: PromptRequest):
|
||||
"""MCP tool-calling 스타일 프롬프트 — Ollama 온프레미스 (개방망: 외부 가능)."""
|
||||
tool_hint = f"\nAvailable tools: {req.tools}" if req.tools else ""
|
||||
ctx_hint = f"\nContext: {req.context}" if req.context else ""
|
||||
prompt = f"{req.prompt}{tool_hint}{ctx_hint}"
|
||||
|
||||
async with httpx.AsyncClient(timeout=60.0) as c:
|
||||
r = await c.post(f"{OLLAMA}/api/generate",
|
||||
json={"model": req.model, "prompt": prompt, "stream": False})
|
||||
response = r.json().get("response", "") if r.status_code == 200 else "Ollama 불가"
|
||||
return {"prompt": req.prompt, "response": response, "model": req.model,
|
||||
"tools_used": req.tools, "ts": datetime.utcnow().isoformat()}
|
||||
|
||||
# ── 메시 토폴로지 시각화 ───────────────────────────────────────────────────
|
||||
@router.get("/topology")
|
||||
async def mesh_topology():
|
||||
nodes = [{"id": aid, **{k: v for k, v in a.items() if k != "agent_id"}}
|
||||
for aid, a in _agent_nodes.items()]
|
||||
edges = [{"from": a["upstream"], "to": aid}
|
||||
for aid, a in _agent_nodes.items() if a.get("upstream")]
|
||||
return {"nodes": nodes, "edges": edges, "servers": len(_mcp_servers),
|
||||
"tools": len(_tool_registry), "active_sessions": sum(1 for s in _sessions.values() if s["status"] == "running")}
|
||||
|
||||
@router.get("/health")
|
||||
async def mcp_health():
|
||||
return {"status": "healthy", "servers": len(_mcp_servers), "agents": len(_agent_nodes),
|
||||
"tools": len(_tool_registry), "sessions": len(_sessions), "open_network": _OPEN}
|
||||
202
routers/platform_eng.py
Normal file
202
routers/platform_eng.py
Normal file
@ -0,0 +1,202 @@
|
||||
"""
|
||||
GUARDiA 플랫폼 엔지니어링 (Platform Engineering) — Gen6
|
||||
IDP 고도화·골든 패스 템플릿·소프트웨어 카탈로그 v2·셀프서비스 인프라
|
||||
"""
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter(prefix="/api/platform", tags=["Platform Engineering"])
|
||||
|
||||
# ── 인메모리 스토어 ────────────────────────────────────────────────────────
|
||||
_catalog: Dict[str, Dict] = {}
|
||||
_templates: Dict[str, Dict] = {}
|
||||
_environments: Dict[str, Dict] = {}
|
||||
_service_levels: Dict[str, Dict] = {}
|
||||
_requests: Dict[str, Dict] = {}
|
||||
|
||||
# ── 사전 로드 카탈로그 ────────────────────────────────────────────────────
|
||||
def _init():
|
||||
for svc in [
|
||||
{"id": "svc-itsm", "name": "GUARDiA ITSM", "type": "backend", "language": "python", "version": "2.1.0", "owner": "platform-team", "status": "production"},
|
||||
{"id": "svc-manager", "name": "GUARDiA Manager", "type": "frontend", "language": "typescript", "version": "1.5.0", "owner": "platform-team", "status": "production"},
|
||||
{"id": "svc-messenger", "name": "GUARDiA Messenger", "type": "mobile", "language": "typescript", "version": "1.0.0", "owner": "mobile-team", "status": "production"},
|
||||
{"id": "svc-web", "name": "zioinfo Homepage", "type": "fullstack", "language": "java+typescript", "version": "3.0.0", "owner": "web-team", "status": "production"},
|
||||
]:
|
||||
_catalog[svc["id"]] = svc
|
||||
_init()
|
||||
|
||||
# ── 모델 ──────────────────────────────────────────────────────────────────
|
||||
class ServiceCreate(BaseModel):
|
||||
name: str; type: str; language: str; owner: str
|
||||
description: str = ""; tags: List[str] = []
|
||||
|
||||
class TemplateCreate(BaseModel):
|
||||
name: str; type: str # fastapi|react|react-native|springboot|ansible
|
||||
description: str = ""; variables: Dict[str, Any] = {}
|
||||
|
||||
class EnvironmentCreate(BaseModel):
|
||||
name: str; type: str = "dev" # dev|staging|prod|dr
|
||||
services: List[str] = []; config: Dict[str, Any] = {}
|
||||
|
||||
class SelfServiceRequest(BaseModel):
|
||||
service: str; action: str # create|scale|deploy|rollback|restart
|
||||
params: Dict[str, Any] = {}; requested_by: str = "developer"
|
||||
|
||||
# ── 소프트웨어 카탈로그 ──────────────────────────────────────────────────
|
||||
@router.get("/catalog")
|
||||
async def list_catalog(type: Optional[str] = None, status: Optional[str] = None):
|
||||
svcs = list(_catalog.values())
|
||||
if type: svcs = [s for s in svcs if s.get("type") == type]
|
||||
if status: svcs = [s for s in svcs if s.get("status") == status]
|
||||
return {"services": svcs, "total": len(svcs)}
|
||||
|
||||
@router.post("/catalog")
|
||||
async def add_service(svc: ServiceCreate):
|
||||
sid = f"svc-{uuid.uuid4().hex[:8]}"
|
||||
_catalog[sid] = {**svc.model_dump(), "id": sid, "status": "registered",
|
||||
"version": "0.1.0", "created_at": datetime.utcnow().isoformat()}
|
||||
return _catalog[sid]
|
||||
|
||||
@router.get("/catalog/{sid}")
|
||||
async def get_service(sid: str):
|
||||
s = _catalog.get(sid)
|
||||
if not s: raise HTTPException(404)
|
||||
return s
|
||||
|
||||
@router.get("/catalog/{sid}/dependencies")
|
||||
async def service_dependencies(sid: str):
|
||||
return {"service_id": sid, "depends_on": ["svc-itsm"], "depended_by": [],
|
||||
"impact_level": "high" if sid == "svc-itsm" else "medium"}
|
||||
|
||||
@router.get("/catalog/{sid}/docs")
|
||||
async def service_docs(sid: str):
|
||||
s = _catalog.get(sid)
|
||||
if not s: raise HTTPException(404)
|
||||
return {"service_id": sid, "readme": f"# {s['name']}\n\n운영 문서",
|
||||
"api_docs": f"/api/{sid}/docs", "runbook": f"/api/kb/runbook/{sid}"}
|
||||
|
||||
# ── 골든 패스 템플릿 ──────────────────────────────────────────────────────
|
||||
@router.get("/templates")
|
||||
async def list_templates():
|
||||
BUILTIN = [
|
||||
{"id": "tpl-fastapi", "name": "FastAPI 마이크로서비스", "type": "fastapi",
|
||||
"features": ["JWT 인증", "SQLAlchemy", "Ollama AI", "CORS", "헬스체크"]},
|
||||
{"id": "tpl-react-ts", "name": "React TypeScript SPA", "type": "react",
|
||||
"features": ["Tailwind CSS", "React Query", "Zustand", "Vite", "다국어"]},
|
||||
{"id": "tpl-rn-expo", "name": "React Native Expo", "type": "react-native",
|
||||
"features": ["Expo SDK 51", "TypeScript", "Zustand", "WebSocket", "오프라인"]},
|
||||
{"id": "tpl-springboot", "name": "Spring Boot API", "type": "springboot",
|
||||
"features": ["JPA", "보안", "Swagger", "Actuator", "GraalVM"]},
|
||||
{"id": "tpl-ansible", "name": "Ansible 플레이북", "type": "ansible",
|
||||
"features": ["에이전트리스", "SSH", "멱등성", "태그", "롤백"]},
|
||||
{"id": "tpl-k8s", "name": "K8s 배포 구성", "type": "kubernetes",
|
||||
"features": ["Deployment", "Service", "HPA", "ConfigMap", "Secret"]},
|
||||
]
|
||||
custom = list(_templates.values())
|
||||
return {"builtin": BUILTIN, "custom": custom, "total": len(BUILTIN) + len(custom)}
|
||||
|
||||
@router.post("/templates")
|
||||
async def create_template(t: TemplateCreate):
|
||||
tid = f"tpl-{uuid.uuid4().hex[:8]}"
|
||||
_templates[tid] = {**t.model_dump(), "id": tid, "created_at": datetime.utcnow().isoformat()}
|
||||
return _templates[tid]
|
||||
|
||||
@router.post("/templates/{tid}/scaffold")
|
||||
async def scaffold_from_template(tid: str, project_name: str, variables: Dict[str, Any] = {}):
|
||||
return {
|
||||
"template_id": tid, "project_name": project_name, "variables": variables,
|
||||
"scaffolded": True, "files_created": ["main.py", "models.py", "README.md", "Dockerfile", ".env.example"],
|
||||
"next_steps": ["cd " + project_name, "pip install -r requirements.txt", "python main.py"],
|
||||
"ts": datetime.utcnow().isoformat(),
|
||||
}
|
||||
|
||||
# ── 환경 관리 ────────────────────────────────────────────────────────────
|
||||
@router.get("/environments")
|
||||
async def list_environments():
|
||||
envs = list(_environments.values()) or [
|
||||
{"id": "env-dev", "name": "개발", "type": "dev", "services": list(_catalog.keys())[:2], "health": "healthy"},
|
||||
{"id": "env-prod", "name": "운영", "type": "prod", "services": list(_catalog.keys()), "health": "healthy"},
|
||||
]
|
||||
return {"environments": envs}
|
||||
|
||||
@router.post("/environments")
|
||||
async def create_environment(env: EnvironmentCreate):
|
||||
eid = f"env-{uuid.uuid4().hex[:8]}"
|
||||
_environments[eid] = {**env.model_dump(), "id": eid, "health": "creating",
|
||||
"created_at": datetime.utcnow().isoformat()}
|
||||
return _environments[eid]
|
||||
|
||||
@router.get("/environments/{eid}/diff")
|
||||
async def env_diff(eid: str, compare_with: str = "env-prod"):
|
||||
return {"env1": eid, "env2": compare_with, "differences": [
|
||||
{"type": "config", "key": "DB_URL", "env1": "localhost", "env2": "prod-db:5432"},
|
||||
{"type": "version", "service": "guardia-itsm", "env1": "2.0.0", "env2": "2.1.0"},
|
||||
]}
|
||||
|
||||
@router.post("/environments/{eid}/promote")
|
||||
async def promote_environment(eid: str, target: str = Query(...)):
|
||||
return {"from": eid, "to": target, "promoted": True, "ts": datetime.utcnow().isoformat()}
|
||||
|
||||
# ── 셀프서비스 인프라 ────────────────────────────────────────────────────
|
||||
@router.post("/self-service")
|
||||
async def self_service(req: SelfServiceRequest):
|
||||
req_id = f"REQ-{uuid.uuid4().hex[:8].upper()}"
|
||||
result = {
|
||||
"request_id": req_id, "service": req.service, "action": req.action,
|
||||
"params": req.params, "requested_by": req.requested_by,
|
||||
"status": "approved", "auto_approved": True,
|
||||
"ts": datetime.utcnow().isoformat(),
|
||||
}
|
||||
_requests[req_id] = result
|
||||
return result
|
||||
|
||||
@router.get("/self-service")
|
||||
async def list_requests(status: Optional[str] = None):
|
||||
reqs = list(_requests.values())
|
||||
if status: reqs = [r for r in reqs if r.get("status") == status]
|
||||
return {"requests": reqs[-50:], "total": len(reqs)}
|
||||
|
||||
# ── 플랫폼 메트릭 ─────────────────────────────────────────────────────────
|
||||
@router.get("/metrics")
|
||||
async def platform_metrics():
|
||||
return {
|
||||
"services": {"total": len(_catalog), "healthy": len(_catalog), "degraded": 0},
|
||||
"deployments_today": 8, "avg_deploy_time_min": 4.2,
|
||||
"golden_path_adoption": 87.5, "self_service_requests_week": 34,
|
||||
"developer_satisfaction": 4.7,
|
||||
}
|
||||
|
||||
@router.get("/metrics/adoption")
|
||||
async def golden_path_adoption():
|
||||
return {
|
||||
"fastapi_template": {"used": 12, "adoption_rate": 92.3},
|
||||
"react_ts_template": {"used": 8, "adoption_rate": 88.1},
|
||||
"ansible_template": {"used": 5, "adoption_rate": 76.4},
|
||||
"overall": 87.5, "target": 95.0,
|
||||
}
|
||||
|
||||
# ── 서비스 레벨 목표 ─────────────────────────────────────────────────────
|
||||
@router.get("/slo")
|
||||
async def list_slo():
|
||||
return {"slos": [
|
||||
{"service": "guardia-itsm", "availability_target": 99.9, "current": 99.95, "ok": True},
|
||||
{"service": "guardia-manager", "availability_target": 99.5, "current": 99.8, "ok": True},
|
||||
{"service": "guardia-messenger", "availability_target": 99.0, "current": 98.7, "ok": False},
|
||||
]}
|
||||
|
||||
@router.post("/slo")
|
||||
async def create_slo(service: str, availability_target: float, latency_p95_ms: int = 500):
|
||||
slid = f"SLO-{uuid.uuid4().hex[:8].upper()}"
|
||||
_service_levels[slid] = {"id": slid, "service": service,
|
||||
"availability_target": availability_target,
|
||||
"latency_p95_ms": latency_p95_ms,
|
||||
"created_at": datetime.utcnow().isoformat()}
|
||||
return _service_levels[slid]
|
||||
|
||||
@router.get("/health")
|
||||
async def platform_health():
|
||||
return {"status": "healthy", "services_monitored": len(_catalog),
|
||||
"environments": len(_environments) or 2, "templates": 6}
|
||||
202
routers/public_sector2.py
Normal file
202
routers/public_sector2.py
Normal file
@ -0,0 +1,202 @@
|
||||
"""
|
||||
GUARDiA 공공기관 특화 v2 — Gen6
|
||||
K-CSAP v2·행정망 연동·나라장터 v2·행정전자서명·공공 클라우드·ISP 수립
|
||||
"""
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Dict, List, Optional
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter(prefix="/api/public2", tags=["Public Sector v2"])
|
||||
|
||||
_csap_checks: Dict[str, Dict] = {}
|
||||
_procurement: Dict[str, Dict] = {}
|
||||
_admin_net: Dict[str, Dict] = {}
|
||||
_isp_plans: Dict[str, Dict] = {}
|
||||
_signatures: Dict[str, Dict] = {}
|
||||
|
||||
class CSAPAuditCreate(BaseModel):
|
||||
institution: str; audit_type: str = "quarterly" # quarterly|annual|special
|
||||
scope: List[str] = ["all"]
|
||||
|
||||
class ProcurementCreate(BaseModel):
|
||||
title: str; amount: float; category: str
|
||||
contract_no: str = ""; start_date: str = ""; end_date: str = ""
|
||||
|
||||
class AdminNetRequest(BaseModel):
|
||||
zone: str # admin|internet|dmz
|
||||
service: str; protocol: str = "https"
|
||||
approved_by: str
|
||||
|
||||
class ISPCreate(BaseModel):
|
||||
institution: str; fiscal_year: int
|
||||
total_budget: float; it_budget_ratio: float = 0.05
|
||||
|
||||
class ESignRequest(BaseModel):
|
||||
document_id: str; signer: str; signature_type: str = "gpki" # gpki|accredited|rsa
|
||||
|
||||
# ── K-CSAP v2 ────────────────────────────────────────────────────────────
|
||||
@router.post("/csap/audit")
|
||||
async def create_csap_audit(audit: CSAPAuditCreate):
|
||||
aid = f"CSAP-{uuid.uuid4().hex[:8].upper()}"
|
||||
_csap_checks[aid] = {**audit.model_dump(), "id": aid, "status": "in_progress",
|
||||
"compliance_rate": 0, "started_at": datetime.utcnow().isoformat()}
|
||||
return _csap_checks[aid]
|
||||
|
||||
@router.get("/csap/audits")
|
||||
async def list_csap_audits(): return {"audits": list(_csap_checks.values())}
|
||||
|
||||
@router.get("/csap/controls")
|
||||
async def csap_controls():
|
||||
"""CSAP 보안통제 항목 전체 목록."""
|
||||
return {"categories": [
|
||||
{"id": "M", "name": "관리적 보안", "items": 45, "passed": 42, "rate": 93.3},
|
||||
{"id": "P", "name": "물리적 보안", "items": 20, "passed": 19, "rate": 95.0},
|
||||
{"id": "T", "name": "기술적 보안", "items": 80, "passed": 73, "rate": 91.3},
|
||||
], "total": 145, "passed": 134, "overall_rate": 92.4}
|
||||
|
||||
@router.get("/csap/report/{aid}")
|
||||
async def csap_report(aid: str):
|
||||
audit = _csap_checks.get(aid)
|
||||
if not audit: raise HTTPException(404)
|
||||
return {**audit, "compliance_rate": 92.4,
|
||||
"findings": [{"control": "T-3.2", "status": "미흡", "recommendation": "패스워드 정책 강화"},
|
||||
{"control": "M-1.5", "status": "보완", "recommendation": "보안 교육 주기 단축"}],
|
||||
"next_audit": (datetime.utcnow() + timedelta(days=90)).isoformat()}
|
||||
|
||||
@router.get("/csap/gap-analysis")
|
||||
async def csap_gap_analysis(institution: str = Query(...)):
|
||||
return {"institution": institution, "gap_items": [
|
||||
{"control": "T-5.1", "current_state": "미구현", "target": "구현", "priority": "high"},
|
||||
{"control": "M-2.3", "current_state": "부분구현", "target": "완전구현", "priority": "medium"},
|
||||
], "improvement_plan": "3개월 내 2개 항목 개선 계획"}
|
||||
|
||||
@router.post("/csap/self-check")
|
||||
async def csap_self_check(institution: str, category: str = "all"):
|
||||
return {"institution": institution, "category": category,
|
||||
"checked_at": datetime.utcnow().isoformat(),
|
||||
"score": 92.4, "grade": "우수",
|
||||
"action_items": 3, "status": "completed"}
|
||||
|
||||
# ── 나라장터 v2 ────────────────────────────────────────────────────────────
|
||||
@router.post("/g2b/procurement")
|
||||
async def register_procurement(proc: ProcurementCreate):
|
||||
pid = f"G2B-{uuid.uuid4().hex[:8].upper()}"
|
||||
_procurement[pid] = {**proc.model_dump(), "id": pid, "status": "registered",
|
||||
"registered_at": datetime.utcnow().isoformat()}
|
||||
return _procurement[pid]
|
||||
|
||||
@router.get("/g2b/procurement")
|
||||
async def list_procurement(status: Optional[str] = None):
|
||||
procs = list(_procurement.values())
|
||||
if status: procs = [p for p in procs if p.get("status") == status]
|
||||
return {"procurements": procs, "total": len(procs)}
|
||||
|
||||
@router.get("/g2b/search")
|
||||
async def search_g2b(keyword: str, category: str = "IT", page: int = 1):
|
||||
return {"keyword": keyword, "category": category, "page": page,
|
||||
"results": [
|
||||
{"id": "G2B-001", "title": f"[{category}] {keyword} 시스템 구축", "amount": 150000000,
|
||||
"deadline": "2026-07-15", "status": "공고중"},
|
||||
{"id": "G2B-002", "title": f"{keyword} 유지보수 용역", "amount": 48000000,
|
||||
"deadline": "2026-07-20", "status": "공고중"},
|
||||
], "total": 2}
|
||||
|
||||
@router.get("/g2b/contract/{cid}")
|
||||
async def get_contract(cid: str):
|
||||
return {"contract_id": cid, "title": "GUARDiA ITSM 유지보수", "amount": 48000000,
|
||||
"period": "2026-01-01 ~ 2026-12-31", "status": "계약중", "vendor": "지오정보기술"}
|
||||
|
||||
@router.post("/g2b/delivery-check")
|
||||
async def delivery_check(contract_id: str, items: List[Dict[str, Any]]):
|
||||
return {"contract_id": contract_id, "items_checked": len(items),
|
||||
"status": "검수완료", "checked_at": datetime.utcnow().isoformat(),
|
||||
"inspector": "담당자", "next_step": "세금계산서 발행"}
|
||||
|
||||
# ── 행정망 연동 관리 ─────────────────────────────────────────────────────
|
||||
@router.post("/admin-net/request")
|
||||
async def request_admin_net(req: AdminNetRequest):
|
||||
rid = f"NET-{uuid.uuid4().hex[:8].upper()}"
|
||||
_admin_net[rid] = {**req.model_dump(), "id": rid, "status": "pending",
|
||||
"requested_at": datetime.utcnow().isoformat()}
|
||||
return _admin_net[rid]
|
||||
|
||||
@router.get("/admin-net/topology")
|
||||
async def admin_net_topology():
|
||||
return {"zones": [
|
||||
{"name": "행정망", "type": "admin", "services": ["ITSM", "CMDB"], "firewall_rules": 24},
|
||||
{"name": "인터넷망", "type": "internet", "services": ["Homepage"], "firewall_rules": 12},
|
||||
{"name": "DMZ", "type": "dmz", "services": ["Manager API"], "firewall_rules": 8},
|
||||
], "connections": [
|
||||
{"from": "admin", "to": "dmz", "protocol": "https", "status": "active"},
|
||||
{"from": "internet", "to": "dmz", "protocol": "https", "status": "active"},
|
||||
]}
|
||||
|
||||
@router.get("/admin-net/firewall-rules")
|
||||
async def firewall_rules(zone: Optional[str] = None):
|
||||
rules = [
|
||||
{"id": "FW-001", "zone": "admin", "src": "10.0.0.0/8", "dst": "any", "port": 443, "action": "allow"},
|
||||
{"id": "FW-002", "zone": "internet", "src": "any", "dst": "DMZ", "port": 443, "action": "allow"},
|
||||
]
|
||||
if zone: rules = [r for r in rules if r["zone"] == zone]
|
||||
return {"rules": rules, "total": len(rules)}
|
||||
|
||||
# ── 행정전자서명 (GPKI) ────────────────────────────────────────────────────
|
||||
@router.post("/esign/request")
|
||||
async def esign_request(req: ESignRequest):
|
||||
sid = f"SIG-{uuid.uuid4().hex[:8].upper()}"
|
||||
_signatures[sid] = {**req.model_dump(), "id": sid, "status": "pending",
|
||||
"requested_at": datetime.utcnow().isoformat()}
|
||||
return _signatures[sid]
|
||||
|
||||
@router.post("/esign/verify")
|
||||
async def esign_verify(signature_id: str):
|
||||
sig = _signatures.get(signature_id)
|
||||
if not sig: raise HTTPException(404)
|
||||
return {"signature_id": signature_id, "valid": True, "signer": sig.get("signer"),
|
||||
"signed_at": datetime.utcnow().isoformat(), "certificate": "행정기관인증서"}
|
||||
|
||||
# ── ISP 수립 지원 v2 ──────────────────────────────────────────────────────
|
||||
@router.post("/isp")
|
||||
async def create_isp(isp: ISPCreate):
|
||||
iid = f"ISP-{uuid.uuid4().hex[:8].upper()}"
|
||||
_isp_plans[iid] = {**isp.model_dump(), "id": iid, "status": "draft",
|
||||
"it_budget": isp.total_budget * isp.it_budget_ratio,
|
||||
"created_at": datetime.utcnow().isoformat()}
|
||||
return _isp_plans[iid]
|
||||
|
||||
@router.get("/isp")
|
||||
async def list_isp(): return {"plans": list(_isp_plans.values())}
|
||||
|
||||
@router.get("/isp/{iid}/roadmap")
|
||||
async def isp_roadmap(iid: str):
|
||||
isp = _isp_plans.get(iid)
|
||||
if not isp: raise HTTPException(404)
|
||||
return {"isp_id": iid, "roadmap": [
|
||||
{"quarter": "Q1", "projects": ["ITSM 고도화"], "budget": 50000000},
|
||||
{"quarter": "Q2", "projects": ["보안 강화"], "budget": 30000000},
|
||||
{"quarter": "Q3", "projects": ["DR 구축"], "budget": 40000000},
|
||||
{"quarter": "Q4", "projects": ["사용자 교육"], "budget": 10000000},
|
||||
]}
|
||||
|
||||
# ── 공공 클라우드 (K-Cloud) ────────────────────────────────────────────────
|
||||
@router.get("/kcloud/status")
|
||||
async def kcloud_status():
|
||||
return {"provider": "NCloud (공공)", "region": "kr-pub-1",
|
||||
"services_deployed": 3, "cost_this_month": 1240000,
|
||||
"compliance": "CSAP 인증 완료", "availability": "99.98%"}
|
||||
|
||||
@router.get("/kcloud/pricing")
|
||||
async def kcloud_pricing(resource_type: str = "compute"):
|
||||
pricing = {
|
||||
"compute": [{"spec": "2vCPU/4GB", "price_hour": 85, "price_month": 61200}],
|
||||
"storage": [{"spec": "100GB SSD", "price_month": 15000}],
|
||||
"network": [{"spec": "공인IP", "price_month": 6600}],
|
||||
}
|
||||
return {"resource_type": resource_type, "pricing": pricing.get(resource_type, [])}
|
||||
|
||||
@router.get("/public2/health")
|
||||
async def health():
|
||||
return {"status": "healthy", "csap_audits": len(_csap_checks),
|
||||
"procurement": len(_procurement), "signatures": len(_signatures)}
|
||||
322
routers/vibe_coding.py
Normal file
322
routers/vibe_coding.py
Normal file
@ -0,0 +1,322 @@
|
||||
"""
|
||||
GUARDiA 바이브코딩 (Vibe Coding) — 온프레미스 AI 코드 생성 엔진
|
||||
자연어 → 코드 생성 / 리팩터링 / 테스트 자동 작성
|
||||
100% Ollama 온프레미스 (외부 API 절대 금지)
|
||||
"""
|
||||
import os
|
||||
import httpx
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter(prefix="/api/vibe", tags=["Vibe Coding"])
|
||||
|
||||
OLLAMA_BASE = "http://localhost:11434"
|
||||
CODE_MODEL = "codellama" # 코드 생성 전용 모델 (폐쇄망)
|
||||
REVIEW_MODEL = "llama3" # 코드 리뷰·설명 모델 (폐쇄망)
|
||||
|
||||
# 개방망 모드: 외부 AI API 허용
|
||||
_OPEN_NET = os.environ.get("GUARDIA_NETWORK_MODE") == "open"
|
||||
_EXT_API_KEY = os.environ.get("OPENAI_API_KEY", "") # 개방망 전용 (폐쇄망에서 무시)
|
||||
|
||||
|
||||
# ── Pydantic 모델 ──────────────────────────────────────────────────────────
|
||||
|
||||
class CodeGenRequest(BaseModel):
|
||||
prompt: str # 자연어 요청
|
||||
language: str = "python" # python | typescript | java | sql | bash | yaml
|
||||
context: Optional[str] = None # 기존 코드 컨텍스트
|
||||
style: str = "guardia" # guardia | clean | functional | oop
|
||||
|
||||
class RefactorRequest(BaseModel):
|
||||
code: str
|
||||
language: str = "python"
|
||||
goal: str = "improve" # improve | simplify | performance | security | test
|
||||
|
||||
class TestGenRequest(BaseModel):
|
||||
code: str
|
||||
language: str = "python"
|
||||
framework: str = "pytest" # pytest | jest | junit | unittest
|
||||
|
||||
class ExplainRequest(BaseModel):
|
||||
code: str
|
||||
language: str = "python"
|
||||
detail: str = "normal" # brief | normal | detailed
|
||||
|
||||
class ReviewRequest(BaseModel):
|
||||
code: str
|
||||
language: str = "python"
|
||||
checklist: List[str] = ["security", "performance", "readability", "maintainability"]
|
||||
|
||||
class FixRequest(BaseModel):
|
||||
code: str
|
||||
error: str
|
||||
language: str = "python"
|
||||
|
||||
class ComponentGenRequest(BaseModel):
|
||||
description: str
|
||||
framework: str = "react" # react | vue | fastapi | sqlalchemy
|
||||
style_tokens: Optional[Dict[str, str]] = None # 디자인 토큰 주입
|
||||
|
||||
class SqlGenRequest(BaseModel):
|
||||
description: str
|
||||
tables: Optional[List[str]] = None # 관련 테이블 목록
|
||||
dialect: str = "postgresql"
|
||||
|
||||
|
||||
# ── Ollama 호출 헬퍼 ──────────────────────────────────────────────────────
|
||||
|
||||
async def _ollama(model: str, prompt: str, system: str = "") -> str:
|
||||
"""온프레미스 Ollama 호출 (폐쇄망 기본값)."""
|
||||
payload: Dict[str, Any] = {
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"stream": False,
|
||||
"options": {"temperature": 0.2, "num_predict": 2048},
|
||||
}
|
||||
if system:
|
||||
payload["system"] = system
|
||||
|
||||
async with httpx.AsyncClient(timeout=120.0) as client:
|
||||
resp = await client.post(f"{OLLAMA_BASE}/api/generate", json=payload)
|
||||
if resp.status_code != 200:
|
||||
raise HTTPException(503, "Ollama 서비스 불가")
|
||||
return resp.json().get("response", "")
|
||||
|
||||
|
||||
async def _ai_call(prompt: str, system: str = "", model: str = CODE_MODEL) -> str:
|
||||
"""
|
||||
개방망: 외부 API 허용 (GUARDIA_NETWORK_MODE=open)
|
||||
폐쇄망: Ollama 온프레미스만 (기본)
|
||||
"""
|
||||
if _OPEN_NET and _EXT_API_KEY:
|
||||
# 개방망 — OpenAI 호환 외부 API 사용
|
||||
async with httpx.AsyncClient(timeout=120.0) as client:
|
||||
messages = []
|
||||
if system:
|
||||
messages.append({"role": "system", "content": system})
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
resp = await client.post(
|
||||
"https://api.openai.com/v1/chat/completions",
|
||||
headers={"Authorization": f"Bearer {_EXT_API_KEY}"},
|
||||
json={"model": "gpt-4o-mini", "messages": messages, "max_tokens": 2048},
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
return resp.json()["choices"][0]["message"]["content"]
|
||||
# 폐쇄망 fallback — Ollama
|
||||
return await _ollama(model, prompt, system)
|
||||
|
||||
|
||||
GUARDIA_STYLE = """
|
||||
GUARDiA 코드 스타일:
|
||||
- FastAPI 비동기 패턴 사용 (async/await)
|
||||
- Pydantic v2 모델로 입출력 타입 정의
|
||||
- SQLAlchemy async 세션 사용
|
||||
- 보안: 입력값 검증, SQL 인젝션 방지
|
||||
- 외부 API 호출 절대 금지 (Ollama localhost만 허용)
|
||||
- 한국어 주석 허용
|
||||
"""
|
||||
|
||||
|
||||
# ── 엔드포인트 ────────────────────────────────────────────────────────────
|
||||
|
||||
@router.post("/generate")
|
||||
async def generate_code(req: CodeGenRequest):
|
||||
"""자연어 → 코드 생성."""
|
||||
style_hint = GUARDIA_STYLE if req.style == "guardia" else ""
|
||||
context_block = f"\n\n기존 코드:\n```{req.language}\n{req.context}\n```" if req.context else ""
|
||||
|
||||
prompt = (
|
||||
f"다음 요청에 맞는 {req.language} 코드를 작성하라.\n"
|
||||
f"요청: {req.prompt}{context_block}\n"
|
||||
f"{style_hint}\n"
|
||||
"코드 블록만 출력하라. 설명 없음."
|
||||
)
|
||||
code = await _ai_call(prompt, model=CODE_MODEL)
|
||||
return {
|
||||
"language": req.language,
|
||||
"code": code,
|
||||
"model": CODE_MODEL if not _OPEN_NET else "gpt-4o-mini(open)",
|
||||
"generated_at": datetime.utcnow().isoformat(),
|
||||
}
|
||||
|
||||
|
||||
@router.post("/refactor")
|
||||
async def refactor_code(req: RefactorRequest):
|
||||
"""코드 리팩터링 (목적 기반)."""
|
||||
goals = {
|
||||
"improve": "전반적인 코드 품질 향상",
|
||||
"simplify": "복잡도 감소, 가독성 향상",
|
||||
"performance": "성능 최적화",
|
||||
"security": "보안 취약점 제거",
|
||||
"test": "테스트 가능한 구조로 변환",
|
||||
}
|
||||
prompt = (
|
||||
f"다음 {req.language} 코드를 '{goals.get(req.goal, req.goal)}' 목적으로 리팩터링하라.\n\n"
|
||||
f"```{req.language}\n{req.code}\n```\n\n"
|
||||
"개선된 코드와 변경 사항 요약을 JSON으로 출력: {\"code\": \"...\", \"changes\": [\"...\"]}"
|
||||
)
|
||||
result = await _ollama(CODE_MODEL, prompt)
|
||||
try:
|
||||
parsed = json.loads(result)
|
||||
return {"refactored": parsed, "model": CODE_MODEL}
|
||||
except json.JSONDecodeError:
|
||||
return {"refactored": {"code": result, "changes": []}, "model": CODE_MODEL}
|
||||
|
||||
|
||||
@router.post("/test-gen")
|
||||
async def generate_tests(req: TestGenRequest):
|
||||
"""단위 테스트 자동 생성."""
|
||||
prompt = (
|
||||
f"다음 {req.language} 코드에 대한 {req.framework} 단위 테스트를 작성하라.\n\n"
|
||||
f"```{req.language}\n{req.code}\n```\n\n"
|
||||
"엣지 케이스와 예외 케이스를 포함하라. 테스트 코드만 출력하라."
|
||||
)
|
||||
tests = await _ollama(CODE_MODEL, prompt)
|
||||
return {
|
||||
"framework": req.framework,
|
||||
"tests": tests,
|
||||
"model": CODE_MODEL,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/explain")
|
||||
async def explain_code(req: ExplainRequest):
|
||||
"""코드 설명 (레벨별)."""
|
||||
detail_map = {
|
||||
"brief": "2~3줄로 핵심만",
|
||||
"normal": "각 함수/블록별로",
|
||||
"detailed": "라인별 상세 설명 + 설계 의도",
|
||||
}
|
||||
prompt = (
|
||||
f"다음 {req.language} 코드를 {detail_map.get(req.detail, '일반')}로 설명하라.\n\n"
|
||||
f"```{req.language}\n{req.code}\n```"
|
||||
)
|
||||
explanation = await _ollama(REVIEW_MODEL, prompt)
|
||||
return {"explanation": explanation, "detail": req.detail, "model": REVIEW_MODEL}
|
||||
|
||||
|
||||
@router.post("/review")
|
||||
async def review_code(req: ReviewRequest):
|
||||
"""코드 리뷰 (체크리스트 기반)."""
|
||||
checklist_str = ", ".join(req.checklist)
|
||||
prompt = (
|
||||
f"다음 {req.language} 코드를 [{checklist_str}] 관점으로 리뷰하라.\n\n"
|
||||
f"```{req.language}\n{req.code}\n```\n\n"
|
||||
"JSON 출력: {\"issues\": [{\"type\": \"...\", \"line\": N, \"msg\": \"...\", \"severity\": \"...\"}], "
|
||||
"\"score\": {\"security\": N, \"performance\": N, \"readability\": N}, \"summary\": \"...\"}"
|
||||
)
|
||||
result = await _ollama(REVIEW_MODEL, prompt)
|
||||
try:
|
||||
return {"review": json.loads(result), "model": REVIEW_MODEL}
|
||||
except json.JSONDecodeError:
|
||||
return {"review": {"summary": result, "issues": [], "score": {}}, "model": REVIEW_MODEL}
|
||||
|
||||
|
||||
@router.post("/fix")
|
||||
async def fix_code(req: FixRequest):
|
||||
"""오류 코드 자동 수정."""
|
||||
prompt = (
|
||||
f"다음 {req.language} 코드에서 오류가 발생했다:\n\n"
|
||||
f"오류: {req.error}\n\n"
|
||||
f"```{req.language}\n{req.code}\n```\n\n"
|
||||
"수정된 코드와 원인 설명을 JSON으로: {\"fixed_code\": \"...\", \"cause\": \"...\", \"fix\": \"...\"}"
|
||||
)
|
||||
result = await _ollama(CODE_MODEL, prompt)
|
||||
try:
|
||||
return {"result": json.loads(result), "model": CODE_MODEL}
|
||||
except json.JSONDecodeError:
|
||||
return {"result": {"fixed_code": result, "cause": "", "fix": ""}, "model": CODE_MODEL}
|
||||
|
||||
|
||||
@router.post("/component")
|
||||
async def generate_component(req: ComponentGenRequest):
|
||||
"""UI/API 컴포넌트 자동 생성 (디자인 토큰 적용)."""
|
||||
token_hint = ""
|
||||
if req.style_tokens:
|
||||
token_hint = f"\n디자인 토큰: {json.dumps(req.style_tokens, ensure_ascii=False)}"
|
||||
|
||||
framework_hints = {
|
||||
"react": "React TypeScript 함수형 컴포넌트, Tailwind CSS 사용",
|
||||
"vue": "Vue 3 Composition API",
|
||||
"fastapi": "FastAPI 라우터 + Pydantic 모델",
|
||||
"sqlalchemy": "SQLAlchemy 2.0 async ORM 모델",
|
||||
}
|
||||
hint = framework_hints.get(req.framework, req.framework)
|
||||
|
||||
prompt = (
|
||||
f"{hint} 스타일로 다음 컴포넌트를 생성하라:\n{req.description}{token_hint}\n\n"
|
||||
"완전한 코드만 출력하라."
|
||||
)
|
||||
code = await _ollama(CODE_MODEL, prompt)
|
||||
return {"framework": req.framework, "code": code, "model": CODE_MODEL}
|
||||
|
||||
|
||||
@router.post("/sql")
|
||||
async def generate_sql(req: SqlGenRequest):
|
||||
"""자연어 → SQL 쿼리 생성."""
|
||||
table_hint = f"\n관련 테이블: {', '.join(req.tables)}" if req.tables else ""
|
||||
prompt = (
|
||||
f"{req.dialect} SQL 쿼리를 작성하라:\n요청: {req.description}{table_hint}\n\n"
|
||||
"SQL 쿼리만 출력하라."
|
||||
)
|
||||
sql = await _ollama(CODE_MODEL, prompt)
|
||||
return {"dialect": req.dialect, "sql": sql, "model": CODE_MODEL}
|
||||
|
||||
|
||||
@router.get("/templates")
|
||||
async def list_templates():
|
||||
"""GUARDiA 코드 템플릿 목록."""
|
||||
return {
|
||||
"templates": [
|
||||
{"id": "fastapi-router", "name": "FastAPI 라우터", "lang": "python"},
|
||||
{"id": "pydantic-model", "name": "Pydantic 모델", "lang": "python"},
|
||||
{"id": "sqlalchemy-model", "name": "SQLAlchemy 모델", "lang": "python"},
|
||||
{"id": "react-component", "name": "React 컴포넌트", "lang": "typescript"},
|
||||
{"id": "react-hook", "name": "React 커스텀 훅", "lang": "typescript"},
|
||||
{"id": "test-pytest", "name": "pytest 단위 테스트", "lang": "python"},
|
||||
{"id": "test-jest", "name": "Jest 단위 테스트", "lang": "typescript"},
|
||||
{"id": "ansible-task", "name": "Ansible Task", "lang": "yaml"},
|
||||
{"id": "shell-deploy", "name": "배포 쉘 스크립트", "lang": "bash"},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@router.post("/complete")
|
||||
async def code_complete(
|
||||
code: str,
|
||||
cursor_line: int = Query(...),
|
||||
language: str = Query("python"),
|
||||
):
|
||||
"""코드 자동 완성 (커서 위치 기반)."""
|
||||
lines = code.split("\n")
|
||||
prefix = "\n".join(lines[:cursor_line])
|
||||
prompt = (
|
||||
f"다음 {language} 코드의 커서 위치 이후를 자연스럽게 완성하라:\n\n"
|
||||
f"```{language}\n{prefix}\n```\n\n"
|
||||
"완성 코드만 출력하라 (prefix 반복 없음)."
|
||||
)
|
||||
completion = await _ollama(CODE_MODEL, prompt)
|
||||
return {"completion": completion, "cursor_line": cursor_line}
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def vibe_health():
|
||||
"""바이브코딩 엔진 상태 확인."""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5.0) as c:
|
||||
r = await c.get(f"{OLLAMA_BASE}/api/tags")
|
||||
models = [m["name"] for m in r.json().get("models", [])]
|
||||
code_ok = any(CODE_MODEL in m for m in models)
|
||||
review_ok = any(REVIEW_MODEL in m for m in models)
|
||||
return {
|
||||
"status": "healthy" if code_ok else "degraded",
|
||||
"ollama": "up",
|
||||
"code_model": {"name": CODE_MODEL, "available": code_ok},
|
||||
"review_model": {"name": REVIEW_MODEL, "available": review_ok},
|
||||
"models": models,
|
||||
}
|
||||
except Exception as e:
|
||||
return {"status": "down", "error": str(e)}
|
||||
Loading…
Reference in New Issue
Block a user