feat: scaffold v2 project structure with Docker, FastAPI, and config
동작하는 최소 코드 수준의 v2 스캐폴딩: - docker-compose.yml: postgres, fastapi, kordoc, frontend, caddy - app/: FastAPI 백엔드 (main, core, models, ai, prompts) - services/kordoc/: Node.js 문서 파싱 마이크로서비스 - gpu-server/: AI Gateway + GPU docker-compose - frontend/: SvelteKit 기본 구조 - migrations/: PostgreSQL 초기 스키마 (documents, tasks, processing_queue) - tests/: pytest conftest 기본 설정 - config.yaml, Caddyfile, credentials.env.example 갱신 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
0
app/ai/__init__.py
Normal file
0
app/ai/__init__.py
Normal file
79
app/ai/client.py
Normal file
79
app/ai/client.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""AI 추상화 레이어 — 통합 클라이언트. 기본값은 항상 Qwen3.5."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
|
||||
from core.config import settings
|
||||
|
||||
# 프롬프트 로딩
|
||||
PROMPTS_DIR = Path(__file__).parent.parent / "prompts"
|
||||
|
||||
|
||||
def _load_prompt(name: str) -> str:
|
||||
return (PROMPTS_DIR / name).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
CLASSIFY_PROMPT = _load_prompt("classify.txt") if (PROMPTS_DIR / "classify.txt").exists() else ""
|
||||
|
||||
|
||||
class AIClient:
|
||||
"""AI Gateway를 통한 통합 클라이언트. 기본값은 항상 Qwen3.5."""
|
||||
|
||||
def __init__(self):
|
||||
self.ai = settings.ai
|
||||
self._http = httpx.AsyncClient(timeout=120)
|
||||
|
||||
async def classify(self, text: str) -> dict:
|
||||
"""문서 분류 — 항상 primary(Qwen3.5) 사용"""
|
||||
prompt = CLASSIFY_PROMPT.replace("{document_text}", text)
|
||||
response = await self._call_chat(self.ai.primary, prompt)
|
||||
return response
|
||||
|
||||
async def summarize(self, text: str, force_premium: bool = False) -> str:
|
||||
"""문서 요약 — 기본 Qwen3.5, 장문이거나 명시적 요청 시만 Claude"""
|
||||
model = self.ai.primary
|
||||
if force_premium or len(text) > 15000:
|
||||
model = self.ai.premium
|
||||
return await self._call_chat(model, f"다음 문서를 500자 이내로 요약해주세요:\n\n{text}")
|
||||
|
||||
async def embed(self, text: str) -> list[float]:
|
||||
"""벡터 임베딩 — GPU 서버 전용"""
|
||||
response = await self._http.post(
|
||||
self.ai.embedding.endpoint,
|
||||
json={"model": self.ai.embedding.model, "prompt": text},
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()["embedding"]
|
||||
|
||||
async def ocr(self, image_bytes: bytes) -> str:
|
||||
"""이미지 OCR — GPU 서버 전용"""
|
||||
# TODO: Qwen2.5-VL-7B 비전 모델 호출 구현
|
||||
raise NotImplementedError("OCR는 Phase 1에서 구현")
|
||||
|
||||
async def _call_chat(self, model_config, prompt: str) -> str:
|
||||
"""OpenAI 호환 API 호출 + 자동 폴백"""
|
||||
try:
|
||||
return await self._request(model_config, prompt)
|
||||
except (httpx.TimeoutException, httpx.ConnectError):
|
||||
if model_config == self.ai.primary:
|
||||
return await self._request(self.ai.fallback, prompt)
|
||||
raise
|
||||
|
||||
async def _request(self, model_config, prompt: str) -> str:
|
||||
"""단일 모델 API 호출"""
|
||||
response = await self._http.post(
|
||||
model_config.endpoint,
|
||||
json={
|
||||
"model": model_config.model,
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"max_tokens": model_config.max_tokens,
|
||||
},
|
||||
timeout=model_config.timeout,
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
return data["choices"][0]["message"]["content"]
|
||||
|
||||
async def close(self):
|
||||
await self._http.aclose()
|
||||
Reference in New Issue
Block a user