manual-deploy 2026-06-07 08:13

This commit is contained in:
DESKTOP-TKLFCPR\ython 2026-06-07 08:13:43 +09:00
parent 9f2344604e
commit 2b62703df9
12 changed files with 3128 additions and 0 deletions

20
main.py
View File

@ -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")

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)}