diff --git a/main.py b/main.py index b1c3c60..f45d37a 100644 --- a/main.py +++ b/main.py @@ -72,6 +72,9 @@ from routers import ( patches, stats as stats_router, cicd, + data_sync, + # Gen6 추가 (2026-06-07) + mcp_agents, platform_eng, advanced_security2, data_ai2, public_sector2, infra_native, ) @@ -549,6 +552,23 @@ app.include_router(mobile2_ext.citizen_router) # /api/citizen/* app.include_router(mobile2_ext.pub_router) # /api/public-sector/* app.include_router(mobile2_ext.esign_router) # /api/approvals (전자서명 확장) +# ── 크로스 시스템 데이터 동기화 허브 ────────────────────────────────────────── +app.include_router(data_sync.router) # ITSM ↔ Manager ↔ Messenger 실시간 이벤트 버스 + +# ── 바이브코딩 + 디자인 AI (온프레미스 Ollama 전용) ─────────────────────────── +from routers import vibe_coding, design_studio, agent_collab, gs_certification +app.include_router(vibe_coding.router) # 자연어 → 코드 생성 (Ollama codellama) +app.include_router(design_studio.router) # 온프레미스 디자인 스튜디오 (토큰·컴포넌트·CSS) +app.include_router(agent_collab.router) # 에이전트 간 의견 교환 · 협업 채널 +app.include_router(gs_certification.router) # GS인증 요건 자동 점검·보고 + +# ── Gen6 확장 (2026-06-07) — MCP·플랫폼엔지니어링·고급보안2·데이터AI·공공특화2·클라우드네이티브 ── +app.include_router(mcp_agents.router) # MCP 에이전트 메시 (tool-calling·오케스트레이션) +app.include_router(platform_eng.router) # 플랫폼 엔지니어링 (IDP·골든패스·카탈로그·SLO) +app.include_router(advanced_security2.router) # 고급 보안 v2 (ZTNA v2·SBOM v2·포렌식·IAM감사) +app.include_router(data_ai2.router) # 데이터 AI v2 (벡터DB·RAG·LoRA API·임베딩) +app.include_router(public_sector2.router) # 공공기관 특화 v2 (K-CSAP·나라장터·GPKI·ISP) +app.include_router(infra_native.router) # 클라우드 네이티브 (eBPF·Wasm·서비스메시·이벤트소싱) # ── 개방망 보안 헤더 미들웨어 ──────────────────────────────────────────────── @app.middleware("http") diff --git a/routers/advanced_security2.py b/routers/advanced_security2.py new file mode 100644 index 0000000..3b28b14 --- /dev/null +++ b/routers/advanced_security2.py @@ -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)} diff --git a/routers/agent_collab.py b/routers/agent_collab.py new file mode 100644 index 0000000..ca9d95d --- /dev/null +++ b/routers/agent_collab.py @@ -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} diff --git a/routers/data_ai2.py b/routers/data_ai2.py new file mode 100644 index 0000000..de19394 --- /dev/null +++ b/routers/data_ai2.py @@ -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)} diff --git a/routers/data_sync.py b/routers/data_sync.py new file mode 100644 index 0000000..1bde146 --- /dev/null +++ b/routers/data_sync.py @@ -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()}, + ], + } diff --git a/routers/design_studio.py b/routers/design_studio.py new file mode 100644 index 0000000..2350e58 --- /dev/null +++ b/routers/design_studio.py @@ -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)} diff --git a/routers/gs_certification.py b/routers/gs_certification.py new file mode 100644 index 0000000..8a17594 --- /dev/null +++ b/routers/gs_certification.py @@ -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"] != "낮음"], + } diff --git a/routers/infra_native.py b/routers/infra_native.py new file mode 100644 index 0000000..1cbbb84 --- /dev/null +++ b/routers/infra_native.py @@ -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()} diff --git a/routers/mcp_agents.py b/routers/mcp_agents.py new file mode 100644 index 0000000..3c1fdc6 --- /dev/null +++ b/routers/mcp_agents.py @@ -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} diff --git a/routers/platform_eng.py b/routers/platform_eng.py new file mode 100644 index 0000000..f35b668 --- /dev/null +++ b/routers/platform_eng.py @@ -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} diff --git a/routers/public_sector2.py b/routers/public_sector2.py new file mode 100644 index 0000000..64977bd --- /dev/null +++ b/routers/public_sector2.py @@ -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)} diff --git a/routers/vibe_coding.py b/routers/vibe_coding.py new file mode 100644 index 0000000..229c8e8 --- /dev/null +++ b/routers/vibe_coding.py @@ -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)}