Files
conai/backend/app/api/rag.py
sinmb79 2a4950d8a0 feat: CONAI Phase 1 MVP 초기 구현
소형 건설업체(100억 미만)를 위한 AI 기반 토목공사 통합관리 플랫폼

Backend (FastAPI):
- SQLAlchemy 모델 13개 (users, projects, wbs, tasks, daily_reports, reports, inspections, quality, weather, permits, rag, settings)
- API 라우터 11개 (auth, projects, tasks, daily_reports, reports, inspections, weather, rag, kakao, permits, settings)
- Services: Claude AI 래퍼, CPM Gantt 계산, 기상청 API, RAG(pgvector), 카카오 Skill API
- Alembic 마이그레이션 (pgvector 포함)
- pytest 테스트 (CPM, 날씨 경보)

Frontend (Next.js 15):
- 11개 페이지 (대시보드, 프로젝트, Gantt, 일보, 검측, 품질, 날씨, 인허가, RAG, 설정)
- TanStack Query + Zustand + Tailwind CSS

인프라:
- Docker Compose (PostgreSQL pgvector + backend + frontend)
- 한국어 README 및 설치 가이드

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 20:06:36 +09:00

74 lines
2.6 KiB
Python

import uuid
from fastapi import APIRouter, HTTPException, status, UploadFile, File
from sqlalchemy import select, func
from app.deps import CurrentUser, DB
from app.models.rag import RagSource, RagChunk
from app.schemas.rag import RagAskRequest, RagAskResponse, RagSourceCreate, RagSourceResponse, RagSource as RagSourceSchema
from app.services.rag_service import ask
router = APIRouter(prefix="/rag", tags=["법규/시방서 Q&A (RAG)"])
@router.post("/ask", response_model=RagAskResponse)
async def ask_question(data: RagAskRequest, db: DB, current_user: CurrentUser):
"""Ask a question about construction laws and specifications."""
source_types = [st.value for st in data.source_types] if data.source_types else None
result = await ask(db, data.question, data.top_k, source_types)
sources = [
RagSourceSchema(
id=uuid.UUID(s["id"]),
title=s["title"],
source_type=s["source_type"],
chunk_content=s["content"][:500], # Truncate for response
relevance_score=s["relevance_score"],
)
for s in result.get("sources", [])
]
return RagAskResponse(
question=result["question"],
answer=result["answer"],
sources=sources,
)
@router.get("/sources", response_model=list[RagSourceResponse])
async def list_sources(db: DB, current_user: CurrentUser):
"""List all indexed RAG sources with chunk counts."""
result = await db.execute(
select(RagSource, func.count(RagChunk.id).label("chunk_count"))
.outerjoin(RagChunk, RagChunk.source_id == RagSource.id)
.group_by(RagSource.id)
.order_by(RagSource.created_at.desc())
)
rows = result.fetchall()
return [
RagSourceResponse(
id=row.RagSource.id,
title=row.RagSource.title,
source_type=row.RagSource.source_type,
source_url=row.RagSource.source_url,
chunk_count=row.chunk_count,
created_at=row.RagSource.created_at,
)
for row in rows
]
@router.post("/sources", response_model=RagSourceResponse, status_code=status.HTTP_201_CREATED)
async def create_source(data: RagSourceCreate, db: DB, current_user: CurrentUser):
"""Register a new RAG source (metadata only; content indexed separately)."""
source = RagSource(**data.model_dump())
db.add(source)
await db.commit()
await db.refresh(source)
return RagSourceResponse(
id=source.id,
title=source.title,
source_type=source.source_type,
source_url=source.source_url,
chunk_count=0,
created_at=source.created_at,
)