kcsc-mcp / src /app.py
nicefree19's picture
Upload 105 files
0ee3c92 verified
Raw
History Blame Contribute Delete
22.7 kB
"""
KCSC MCP ์„œ๋ฒ„ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜
ํ•œ๊ตญ๊ฑด์„ค๊ธฐ์ค€์„ผํ„ฐ(KCSC) API์™€ Model Context Protocol(MCP)์„ ์—ฐ๋™ํ•œ
์ž์—ฐ์–ด ๊ฒ€์ƒ‰ ๋ฐ ์งˆ์˜์‘๋‹ต API ์„œ๋ฒ„
"""
import logging
import os
import json
import time
import uuid
from datetime import datetime
from typing import List, Optional, Dict, Any, Union
from fastapi import FastAPI, HTTPException, Depends, Request, BackgroundTasks
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.openapi.docs import get_swagger_ui_html
from fastapi.openapi.utils import get_openapi
from pydantic import BaseModel, Field, validator
from src import config
from src.kcsc_api_client import KCSCApiClient
from src.vector_db_client import KCSCVectorDB
from src.mcp_processor import MCPProcessor
from src.utils import (
format_error_response,
KCSCBaseException,
sanitize_string
)
# ์„ค์ • ์ดˆ๊ธฐํ™”
config.init()
logger = config.setup_logger("app")
# FastAPI ์•ฑ ์ดˆ๊ธฐํ™”
app = FastAPI(
title="KCSC MCP API",
description="ํ•œ๊ตญ๊ฑด์„ค๊ธฐ์ค€์„ผํ„ฐ(KCSC) API์™€ Model Context Protocol(MCP)์„ ์—ฐ๋™ํ•œ ์ž์—ฐ์–ด ๊ฒ€์ƒ‰ ๋ฐ ์งˆ์˜์‘๋‹ต API",
version="1.0.1",
docs_url=None, # ๊ธฐ๋ณธ ๋ฌธ์„œ ๊ฒฝ๋กœ ๋น„ํ™œ์„ฑํ™” (์ปค์Šคํ…€ ๊ฒฝ๋กœ ์‚ฌ์šฉ)
redoc_url=None, # Redoc ๋น„ํ™œ์„ฑํ™”
)
# CORS ์„ค์ •
origins = [
"http://localhost",
"http://localhost:8000",
"http://127.0.0.1",
"http://127.0.0.1:8000",
# ํ•„์š”์— ๋”ฐ๋ผ ์ถ”๊ฐ€ ๋„๋ฉ”์ธ ํ—ˆ์šฉ
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ๋ชจ๋ธ ์ •์˜
class SearchQuery(BaseModel):
"""๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ ๋ชจ๋ธ"""
query: str = Field(..., description="๊ฒ€์ƒ‰์–ด")
doc_types: Optional[List[str]] = Field(None, description="๋ฌธ์„œ ์œ ํ˜• ๋ชฉ๋ก")
limit: Optional[int] = Field(5, description="๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ˆ˜", ge=1, le=20)
@validator('query')
def validate_query(cls, v):
if not v or not v.strip():
raise ValueError('๊ฒ€์ƒ‰์–ด๋Š” ๋น„์›Œ๋‘˜ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.')
return sanitize_string(v.strip())
class MCPQuery(BaseModel):
"""MCP ์ฟผ๋ฆฌ ๋ชจ๋ธ"""
query: str = Field(..., description="์งˆ์˜ ๋‚ด์šฉ")
conversation_id: Optional[str] = Field(None, description="๋Œ€ํ™” ID")
enable_rag: bool = Field(True, description="RAG(๊ฒ€์ƒ‰ ์ฆ๊ฐ• ์ƒ์„ฑ) ์‚ฌ์šฉ ์—ฌ๋ถ€")
system_prompt: Optional[str] = Field(None, description="์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ")
temperature: Optional[float] = Field(0.2, description="์ƒ์„ฑ ์˜จ๋„(์ฐฝ์˜์„ฑ)", ge=0.0, le=1.0)
max_tokens: Optional[int] = Field(2000, description="์ตœ๋Œ€ ์ƒ์„ฑ ํ† ํฐ ์ˆ˜", ge=1, le=4096)
@validator('query')
def validate_query(cls, v):
if not v or not v.strip():
raise ValueError('์งˆ์˜ ๋‚ด์šฉ์€ ๋น„์›Œ๋‘˜ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.')
return sanitize_string(v.strip())
class CodeDetailRequest(BaseModel):
"""์ฝ”๋“œ ์ƒ์„ธ ์ •๋ณด ์š”์ฒญ ๋ชจ๋ธ"""
code: str = Field(..., description="์ฝ”๋“œ ๋ฒˆํ˜ธ")
doc_type: str = Field(..., description="๋ฌธ์„œ ์œ ํ˜•")
@validator('code', 'doc_type')
def validate_fields(cls, v):
if not v or not v.strip():
raise ValueError('ํ•„๋“œ๋Š” ๋น„์›Œ๋‘˜ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.')
return v.strip()
class SearchResult(BaseModel):
"""๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๋ชจ๋ธ"""
id: str = Field(..., description="๊ฒฐ๊ณผ ID")
text: str = Field(..., description="๊ฒฐ๊ณผ ํ…์ŠคํŠธ")
metadata: Dict[str, Any] = Field(..., description="๋ฉ”ํƒ€๋ฐ์ดํ„ฐ")
relevance: float = Field(..., description="๊ด€๋ จ๋„ ์ ์ˆ˜", ge=0.0, le=1.0)
class SearchResponse(BaseModel):
"""๊ฒ€์ƒ‰ ์‘๋‹ต ๋ชจ๋ธ"""
results: List[SearchResult] = Field(..., description="๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๋ชฉ๋ก")
query: str = Field(..., description="์›๋ณธ ๊ฒ€์ƒ‰์–ด")
timestamp: str = Field(..., description="ํƒ€์ž„์Šคํƒฌํ”„")
class CollectRequest(BaseModel):
"""๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์š”์ฒญ ๋ชจ๋ธ"""
doc_types: Optional[List[str]] = Field(["KDS", "KCS"], description="์ˆ˜์ง‘ํ•  ๋ฌธ์„œ ์œ ํ˜• ๋ชฉ๋ก")
class IndexParams(BaseModel):
"""์ธ๋ฑ์Šค ๊ตฌ์ถ• ํŒŒ๋ผ๋ฏธํ„ฐ ๋ชจ๋ธ"""
chunk_size: Optional[int] = Field(None, description="์ฒญํฌ ํฌ๊ธฐ")
overlap: Optional[int] = Field(None, description="์ฒญํฌ ๊ฐ„ ๊ฒน์น˜๋Š” ๋ฌธ์ž ์ˆ˜")
reset: Optional[bool] = Field(True, description="๊ธฐ์กด ๋ฐ์ดํ„ฐ ๋ฆฌ์…‹ ์—ฌ๋ถ€")
# ์ข…์†์„ฑ ์ฃผ์ž… - ์‹ฑ๊ธ€ํ„ด ๊ฐ์ฒด
def get_kcsc_api():
"""KCSC API ํด๋ผ์ด์–ธํŠธ ๋ฐ˜ํ™˜"""
return KCSCApiClient()
def get_vector_db():
"""๋ฒกํ„ฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํด๋ผ์ด์–ธํŠธ ๋ฐ˜ํ™˜"""
return KCSCVectorDB()
def get_mcp_processor():
"""MCP ํ”„๋กœ์„ธ์„œ ๋ฐ˜ํ™˜"""
return MCPProcessor()
# ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—…
def background_data_collection(api_client, doc_types):
"""๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์ž‘์—…"""
try:
logger.info(f"๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์‹œ์ž‘: {doc_types}")
all_codes = api_client.collect_codes(doc_types)
for doc_type, codes in all_codes.items():
logger.info(f"{doc_type} ์ƒ์„ธ ์ •๋ณด ์ˆ˜์ง‘ ์‹œ์ž‘ (์ด {len(codes)}๊ฐœ ์ฝ”๋“œ)")
api_client.fetch_code_details(doc_type, codes)
logger.info(f"๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์™„๋ฃŒ")
except Exception as e:
logger.error(f"๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}")
def background_index_building(vector_db, params):
"""๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ธ๋ฑ์Šค ๊ตฌ์ถ• ์ž‘์—…"""
try:
logger.info("๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ธ๋ฑ์Šค ๊ตฌ์ถ• ์‹œ์ž‘")
vector_db.build_index_from_files(
chunk_size=params.get("chunk_size"),
overlap=params.get("overlap"),
reset=params.get("reset", True)
)
logger.info("๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ธ๋ฑ์Šค ๊ตฌ์ถ• ์™„๋ฃŒ")
except Exception as e:
logger.error(f"๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ธ๋ฑ์Šค ๊ตฌ์ถ• ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}")
# ์—๋Ÿฌ ํ•ธ๋“ค๋Ÿฌ
@app.exception_handler(KCSCBaseException)
async def kcsc_exception_handler(request: Request, exc: KCSCBaseException):
"""KCSC ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ธฐ"""
return JSONResponse(
status_code=500,
content=format_error_response(exc)
)
@app.exception_handler(Exception)
async def general_exception_handler(request: Request, exc: Exception):
"""์ผ๋ฐ˜ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ธฐ"""
logger.error(f"์š”์ฒญ ์ฒ˜๋ฆฌ ์ค‘ ์˜ˆ์™ธ ๋ฐœ์ƒ: {str(exc)}")
return JSONResponse(
status_code=500,
content={
"status": "error",
"error": {
"type": exc.__class__.__name__,
"message": str(exc),
"timestamp": datetime.now().isoformat()
}
}
)
# ์„œ๋ฒ„ ์‹œ์ž‘ ์‹œ ์ดˆ๊ธฐํ™”
@app.on_event("startup")
async def startup_event():
"""์„œ๋ฒ„ ์‹œ์ž‘ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ"""
logger.info("์„œ๋ฒ„ ์‹œ์ž‘ ์ค‘...")
try:
# ํ•„์š”ํ•œ ๋””๋ ‰ํ† ๋ฆฌ ์ƒ์„ฑ
os.makedirs(config.DATA_DIR, exist_ok=True)
os.makedirs(config.LOGS_DIR, exist_ok=True)
os.makedirs(config.VECTOR_DB_DIR, exist_ok=True)
# ๋ชจ๋“ˆ ์ดˆ๊ธฐํ™”
kcsc_api = get_kcsc_api()
vector_db = get_vector_db()
mcp_processor = get_mcp_processor()
# ๋ฒกํ„ฐ DB ์ดˆ๊ธฐํ™” ์ƒํƒœ ํ™•์ธ
stats = vector_db.get_collection_stats()
has_data = any(count > 0 for count in stats.values())
if not has_data:
logger.info("๋ฒกํ„ฐ DB์— ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ๋ฐ ์ธ๋ฑ์‹ฑ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.")
logger.info("์„œ๋ฒ„ ์‹œ์ž‘ ์™„๋ฃŒ")
except Exception as e:
logger.error(f"์„œ๋ฒ„ ์‹œ์ž‘ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}")
# ์˜ค๋ฅ˜๊ฐ€ ์žˆ๋”๋ผ๋„ ์„œ๋ฒ„๋Š” ์‹คํ–‰ (ํ•„์š”์‹œ ์ผ๋ถ€ ๊ธฐ๋Šฅ๋งŒ ์ œํ•œ)
# API ์—”๋“œํฌ์ธํŠธ ๊ตฌํ˜„
@app.get("/")
async def root():
"""๋ฃจํŠธ ์—”๋“œํฌ์ธํŠธ"""
return {
"status": "online",
"message": "KCSC MCP API ์„œ๋ฒ„ ์‹คํ–‰ ์ค‘",
"version": app.version,
"timestamp": datetime.now().isoformat()
}
# ๋ฌธ์„œ ์ปค์Šคํ…€ ์—”๋“œํฌ์ธํŠธ
@app.get("/docs", include_in_schema=False)
async def custom_swagger_ui_html():
"""์ปค์Šคํ…€ Swagger UI"""
return get_swagger_ui_html(
openapi_url="/openapi.json",
title=app.title + " - API ๋ฌธ์„œ",
swagger_js_url="https://cdn.jsdelivr.net/npm/swagger-ui-dist@4/swagger-ui-bundle.js",
swagger_css_url="https://cdn.jsdelivr.net/npm/swagger-ui-dist@4/swagger-ui.css",
)
@app.get("/openapi.json", include_in_schema=False)
async def get_open_api_endpoint():
"""OpenAPI ์Šคํ‚ค๋งˆ"""
return get_openapi(
title=app.title,
version=app.version,
description=app.description,
routes=app.routes,
)
# MCP ํ”„๋กœํ† ์ฝœ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์—”๋“œํฌ์ธํŠธ
@app.get("/mcp")
async def mcp_metadata():
"""MCP ์„œ๋ฒ„ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ - Claude Desktop์—์„œ ์„œ๋ฒ„ ๊ฒ€์ฆ์— ์‚ฌ์šฉ"""
return {
"name": "KCSC API",
"description": "ํ•œ๊ตญ๊ฑด์„ค๊ธฐ์ค€์„ผํ„ฐ(KCSC) API๋ฅผ ํ†ตํ•œ ๊ฒ€์ƒ‰ ๋ฐ ์ปจํ…์ŠคํŠธ ์ œ๊ณต",
"version": app.version,
"actions": [
{
"name": "search",
"description": "๊ฑด์„ค๊ธฐ์ค€ ์ •๋ณด ๊ฒ€์ƒ‰",
"parameters": {
"query": "๊ฒ€์ƒ‰์–ด",
"doc_types": "๋ฌธ์„œ ์œ ํ˜• (์„ ํƒ์ )",
"limit": "๊ฒฐ๊ณผ ์ˆ˜ (์„ ํƒ์ )"
}
},
{
"name": "code_detail",
"description": "ํŠน์ • ์ฝ”๋“œ์˜ ์ƒ์„ธ ์ •๋ณด ์กฐํšŒ",
"parameters": {
"code": "์ฝ”๋“œ ๋ฒˆํ˜ธ",
"doc_type": "๋ฌธ์„œ ์œ ํ˜•"
}
}
]
}
# MCP ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ์—”๋“œํฌ์ธํŠธ - Claude Desktop๊ณผ ํ†ต์‹ ํ•˜๋Š” ํ•ต์‹ฌ ๋ถ€๋ถ„
@app.post("/mcp/messages")
async def handle_mcp_message(
request: Request,
vector_db: KCSCVectorDB = Depends(get_vector_db),
kcsc_api: KCSCApiClient = Depends(get_kcsc_api),
mcp_processor: MCPProcessor = Depends(get_mcp_processor)
):
"""
Claude Desktop๊ณผ ํ˜ธํ™˜๋˜๋Š” MCP ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ
์ด ์—”๋“œํฌ์ธํŠธ๋Š” MCP ํ”„๋กœํ† ์ฝœ์„ ํ†ตํ•ด Claude Desktop๊ณผ ํ†ต์‹ ํ•ฉ๋‹ˆ๋‹ค.
"""
try:
# ์š”์ฒญ ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ
data = await request.json()
message = data.get("message", "")
conversation_id = data.get("conversation_id")
action = data.get("action")
action_params = data.get("action_params", {})
logger.info(f"MCP ๋ฉ”์‹œ์ง€ ์ˆ˜์‹ : action={action}, message_prefix={message[:30] if message else 'None'}...")
# ์•ก์…˜์ด ์—†์œผ๋ฉด ๊ธฐ๋ณธ ๊ฒ€์ƒ‰ ์ˆ˜ํ–‰
if not action:
# ๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ๋กœ ์ฒ˜๋ฆฌ
search_results = vector_db.search(
query=message,
limit=3
)
# ์ปจํ…์ŠคํŠธ ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜
context = []
for i, result in enumerate(search_results):
context.append({
"id": f"result-{i}",
"content": result["text"],
"metadata": {
"source": "KCSC",
"code": result["metadata"]["code"],
"name": result["metadata"]["name"],
"doc_type": result["metadata"]["doc_type"],
"relevance": result["relevance"]
}
})
logger.info(f"๊ธฐ๋ณธ ๊ฒ€์ƒ‰ ์™„๋ฃŒ: {len(context)}๊ฐœ ๊ฒฐ๊ณผ")
# ์‘๋‹ต ์ƒ์„ฑ
return {
"content": "๋‹ค์Œ์€ ํ•œ๊ตญ๊ฑด์„ค๊ธฐ์ค€์„ผํ„ฐ(KCSC)์—์„œ ๊ฒ€์ƒ‰ํ•œ ๊ด€๋ จ ์ •๋ณด์ž…๋‹ˆ๋‹ค:",
"context": context,
"conversation_id": conversation_id
}
# ์•ก์…˜๋ณ„ ์ฒ˜๋ฆฌ
elif action == "search":
# ๊ฒ€์ƒ‰ ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”์ถœ
query = action_params.get("query", message)
doc_types = action_params.get("doc_types")
limit = int(action_params.get("limit", 5))
# ๊ฒ€์ƒ‰ ์ˆ˜ํ–‰
search_results = vector_db.search(
query=query,
doc_types=doc_types,
limit=limit
)
# ์ปจํ…์ŠคํŠธ ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜
context = []
for i, result in enumerate(search_results):
context.append({
"id": f"result-{i}",
"content": result["text"],
"metadata": {
"source": "KCSC",
"code": result["metadata"]["code"],
"name": result["metadata"]["name"],
"doc_type": result["metadata"]["doc_type"],
"relevance": result["relevance"]
}
})
logger.info(f"๊ฒ€์ƒ‰ ์•ก์…˜ ์™„๋ฃŒ: '{query}'์— ๋Œ€ํ•ด {len(context)}๊ฐœ ๊ฒฐ๊ณผ")
# ์‘๋‹ต ์ƒ์„ฑ
return {
"content": f"'{query}'์— ๋Œ€ํ•œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์ž…๋‹ˆ๋‹ค:",
"context": context,
"conversation_id": conversation_id
}
elif action == "code_detail":
# ์ฝ”๋“œ ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”์ถœ
code = action_params.get("code")
doc_type = action_params.get("doc_type")
if not code or not doc_type:
return {
"content": "์ฝ”๋“œ ๋ฒˆํ˜ธ์™€ ๋ฌธ์„œ ์œ ํ˜•์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.",
"conversation_id": conversation_id
}
# ์ฝ”๋“œ ์ƒ์„ธ ์ •๋ณด ์กฐํšŒ
detail = kcsc_api.get_code_details(doc_type, code)
if not detail:
return {
"content": f"์ฝ”๋“œ {doc_type}/{code}์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.",
"conversation_id": conversation_id
}
# ํฌ๋งทํŒ…๋œ ์ƒ์„ธ ์ •๋ณด
formatted_detail = f"""
์ฝ”๋“œ: {detail.get('Code')}
์ด๋ฆ„: {detail.get('Name')}
๋ฒ„์ „: {detail.get('Version')}
์—…๋ฐ์ดํŠธ: {detail.get('UpdateDate')}
๋‚ด์šฉ: {detail.get('Contents')}
"""
# ์‘๋‹ต ์ƒ์„ฑ
return {
"content": formatted_detail,
"context": [{
"id": f"{doc_type}-{code}",
"content": detail.get('Contents', ''),
"metadata": {
"source": "KCSC",
"code": detail.get('Code'),
"name": detail.get('Name'),
"doc_type": doc_type
}
}],
"conversation_id": conversation_id
}
else:
return {
"content": f"์ง€์›ํ•˜์ง€ ์•Š๋Š” ์•ก์…˜์ž…๋‹ˆ๋‹ค: {action}",
"conversation_id": conversation_id
}
except Exception as e:
logger.error(f"MCP ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}")
return {
"content": f"์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}",
"conversation_id": conversation_id
}
@app.get("/status")
async def get_status(
kcsc_api: KCSCApiClient = Depends(get_kcsc_api),
vector_db: KCSCVectorDB = Depends(get_vector_db)
):
"""์„œ๋ฒ„ ์ƒํƒœ ํ™•์ธ"""
try:
# ๋ฒกํ„ฐ DB ์ƒํƒœ ํ™•์ธ
db_stats = vector_db.get_collection_stats()
return {
"status": "online",
"vector_db": db_stats,
"timestamp": datetime.now().isoformat(),
"version": app.version
}
except Exception as e:
logger.error(f"์ƒํƒœ ํ™•์ธ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/collect")
async def collect_data(
request: CollectRequest,
background_tasks: BackgroundTasks,
kcsc_api: KCSCApiClient = Depends(get_kcsc_api)
):
"""
KCSC API์—์„œ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์‹œ์ž‘
๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘ํ•˜์—ฌ ์„œ๋ฒ„ ์‘๋‹ต์„ ์ฆ‰์‹œ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
"""
try:
doc_types = request.doc_types
logger.info(f"๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์š”์ฒญ: {doc_types}")
# ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์‹œ์ž‘
background_tasks.add_task(background_data_collection, kcsc_api, doc_types)
return {
"status": "accepted",
"message": "๋ฐ์ดํ„ฐ ์ˆ˜์ง‘์ด ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์‹œ์ž‘๋˜์—ˆ์Šต๋‹ˆ๋‹ค.",
"doc_types": doc_types,
"timestamp": datetime.now().isoformat()
}
except Exception as e:
logger.error(f"๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์š”์ฒญ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/index")
async def build_index(
params: IndexParams = None,
background_tasks: BackgroundTasks = None,
vector_db: KCSCVectorDB = Depends(get_vector_db)
):
"""
๋ฒกํ„ฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ธ๋ฑ์Šค ๊ตฌ์ถ•
๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์ธ๋ฑ์Šค๋ฅผ ๊ตฌ์ถ•ํ•˜์—ฌ ์„œ๋ฒ„ ์‘๋‹ต์„ ์ฆ‰์‹œ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
"""
try:
logger.info("๋ฒกํ„ฐ ์ธ๋ฑ์Šค ๊ตฌ์ถ• ์š”์ฒญ")
params_dict = params.dict() if params else {}
# ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์ธ๋ฑ์Šค ๊ตฌ์ถ• ์‹œ์ž‘
background_tasks.add_task(background_index_building, vector_db, params_dict)
return {
"status": "accepted",
"message": "๋ฒกํ„ฐ ์ธ๋ฑ์Šค ๊ตฌ์ถ•์ด ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์‹œ์ž‘๋˜์—ˆ์Šต๋‹ˆ๋‹ค.",
"parameters": params_dict,
"timestamp": datetime.now().isoformat()
}
except Exception as e:
logger.error(f"์ธ๋ฑ์Šค ๊ตฌ์ถ• ์š”์ฒญ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/search", response_model=SearchResponse)
async def search(
query: SearchQuery,
vector_db: KCSCVectorDB = Depends(get_vector_db)
):
"""
์ž์—ฐ์–ด ๊ฒ€์ƒ‰ ์ˆ˜ํ–‰
๋ฌธ์„œ ๋‚ด์šฉ์„ ์ž์—ฐ์–ด ์ฟผ๋ฆฌ๋กœ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค.
"""
try:
logger.info(f"๊ฒ€์ƒ‰ ์š”์ฒญ: {query.query}")
# ๊ฒ€์ƒ‰ ์ˆ˜ํ–‰
results = vector_db.search(
query=query.query,
doc_types=query.doc_types,
limit=query.limit
)
# ์‘๋‹ต ํ˜•์‹ ๋ณ€ํ™˜
formatted_results = []
for result in results:
formatted_results.append(SearchResult(
id=result["id"],
text=result["text"],
metadata=result["metadata"],
relevance=result["relevance"]
))
return SearchResponse(
results=formatted_results,
query=query.query,
timestamp=datetime.now().isoformat()
)
except Exception as e:
logger.error(f"๊ฒ€์ƒ‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/code_detail")
async def get_code_detail(
request: CodeDetailRequest,
kcsc_api: KCSCApiClient = Depends(get_kcsc_api)
):
"""
ํŠน์ • ์ฝ”๋“œ์˜ ์ƒ์„ธ ์ •๋ณด ์กฐํšŒ
์ฝ”๋“œ ๋ฒˆํ˜ธ์™€ ๋ฌธ์„œ ์œ ํ˜•์œผ๋กœ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.
"""
try:
logger.info(f"์ฝ”๋“œ ์ƒ์„ธ ์ •๋ณด ์š”์ฒญ: {request.doc_type}/{request.code}")
# ์ฝ”๋“œ ์ƒ์„ธ ์ •๋ณด ์กฐํšŒ
detail = kcsc_api.get_code_details(request.doc_type, request.code)
if not detail:
raise HTTPException(status_code=404, detail="ํ•ด๋‹น ์ฝ”๋“œ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
return detail
except HTTPException:
raise
except Exception as e:
logger.error(f"์ฝ”๋“œ ์ƒ์„ธ ์ •๋ณด ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/mcp_query")
async def process_mcp_query(
query: MCPQuery,
vector_db: KCSCVectorDB = Depends(get_vector_db),
mcp_processor: MCPProcessor = Depends(get_mcp_processor)
):
"""
MCP๋ฅผ ํ†ตํ•œ ์ฟผ๋ฆฌ ์ฒ˜๋ฆฌ
RAG(๊ฒ€์ƒ‰ ์ฆ๊ฐ• ์ƒ์„ฑ)์„ ํ™œ์šฉํ•˜์—ฌ ์งˆ์˜์— ๋‹ต๋ณ€ํ•ฉ๋‹ˆ๋‹ค.
"""
try:
logger.info(f"MCP ์ฟผ๋ฆฌ ์š”์ฒญ: {query.query[:50]}...")
# ์ปจํ…์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ค€๋น„ (RAG ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ)
context_data = None
if query.enable_rag:
search_results = vector_db.search(query.query, limit=3)
if search_results:
context_data = [
{
"id": result["id"],
"content": result["text"],
"metadata": {
"title": result["metadata"]["name"],
"code": result["metadata"]["code"],
"doc_type": result["metadata"]["doc_type"],
"relevance": result["relevance"]
}
}
for result in search_results
]
logger.debug(f"์ปจํ…์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ค€๋น„ ์™„๋ฃŒ: {len(context_data)}๊ฐœ ํ•ญ๋ชฉ")
# MCP๋ฅผ ํ†ตํ•œ ์ฒ˜๋ฆฌ
result = mcp_processor.process_query(
query=query.query,
context_data=context_data,
conversation_id=query.conversation_id,
system_prompt=query.system_prompt,
temperature=query.temperature,
max_tokens=query.max_tokens
)
return {
"response": result["content"],
"conversation_id": result["conversation_id"],
"used_context": result["used_context"],
"timestamp": datetime.now().isoformat()
}
except Exception as e:
logger.error(f"MCP ์ฟผ๋ฆฌ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/health")
async def health_check():
"""
ํ—ฌ์Šค ์ฒดํฌ ์—”๋“œํฌ์ธํŠธ
์„œ๋ฒ„ ์ƒํƒœ์˜ ๊ฐ„๋‹จํ•œ ํ—ฌ์Šค ์ฒดํฌ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
"""
return {
"status": "healthy",
"timestamp": datetime.now().isoformat(),
"service": "KCSC MCP API",
"version": app.version
}
# ๋ฉ”์ธ ์‹คํ–‰
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"src.app:app",
host=config.HOST,
port=config.PORT,
reload=config.DEBUG
)