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