원 레포랑 완전 분리
This commit is contained in:
25
workspace/routers/__init__.py
Normal file
25
workspace/routers/__init__.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from .api_key_router import router as api_key_router
|
||||
from .download_router import router as download_router
|
||||
from .dummy_router import router as dummy_router
|
||||
from .extract_router import router as extract_router
|
||||
from .general_router import router as general_router
|
||||
from .guide_router import router as guide_router
|
||||
from .llm_summation import router as llm_summation
|
||||
from .model_router import router as model_router
|
||||
from .ocr_router import router as ocr_router
|
||||
from .stt_router import router as stt_router
|
||||
from .yolo_router import router as yolo_router
|
||||
|
||||
__all__ = [
|
||||
"api_key_router",
|
||||
"download_router",
|
||||
"dummy_router",
|
||||
"extract_router",
|
||||
"general_router",
|
||||
"guide_router",
|
||||
"model_router",
|
||||
"ocr_router",
|
||||
"stt_router",
|
||||
"llm_summation",
|
||||
"yolo_router",
|
||||
]
|
||||
42
workspace/routers/api_key_router.py
Normal file
42
workspace/routers/api_key_router.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from fastapi import APIRouter, Body, HTTPException
|
||||
from services import api_key_service
|
||||
|
||||
router = APIRouter(prefix="/manage", tags=["API Key Management"])
|
||||
|
||||
|
||||
@router.post("/keys", summary="Create a new API Key")
|
||||
def create_key(
|
||||
client_name: str = Body(
|
||||
...,
|
||||
embed=True,
|
||||
description="Name of the client or service that will use this key.",
|
||||
),
|
||||
):
|
||||
"""
|
||||
새로운 API 키를 생성하고 시스템에 등록합니다.
|
||||
"""
|
||||
if not client_name:
|
||||
raise HTTPException(status_code=400, detail="Client name is required.")
|
||||
|
||||
new_key_info = api_key_service.create_api_key(client_name)
|
||||
return {"message": "API Key created successfully", "key_info": new_key_info}
|
||||
|
||||
|
||||
@router.get("/keys", summary="List all API Keys")
|
||||
def list_keys():
|
||||
"""
|
||||
현재 시스템에 등록된 모든 API 키의 정보를 조회합니다.
|
||||
"""
|
||||
keys = api_key_service.list_api_keys()
|
||||
return {"keys": keys}
|
||||
|
||||
|
||||
@router.delete("/keys/{api_key}", summary="Revoke an API Key")
|
||||
def revoke_key(api_key: str):
|
||||
"""
|
||||
지정된 API 키를 시스템에서 영구적으로 삭제(폐기)합니다.
|
||||
"""
|
||||
success = api_key_service.revoke_api_key(api_key)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="API Key not found.")
|
||||
return {"message": f"API Key '{api_key}' has been revoked."}
|
||||
22
workspace/routers/download_router.py
Normal file
22
workspace/routers/download_router.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from fastapi import APIRouter
|
||||
from services.download_service import DownloadService
|
||||
|
||||
router = APIRouter(tags=["Model Management"])
|
||||
|
||||
|
||||
# ✅ GET:기본 프롬프트 다운로드
|
||||
@router.get("/default_prompt", summary="기본 프롬프트 파일 다운로드")
|
||||
async def download_default_prompt():
|
||||
return DownloadService.download_default_prompt()
|
||||
|
||||
|
||||
# ✅ GET:구조화 프롬프트 파일 다운로드
|
||||
@router.get("/structured_prompt", summary="구조화 프롬프트 파일 다운로드")
|
||||
async def download_structured_prompt():
|
||||
return DownloadService.download_structured_prompt()
|
||||
|
||||
|
||||
# ✅ GET:구조화 필드 정의 파일 다운로드
|
||||
@router.get("/structured_schema", summary="구조화 포맷 정의 파일 다운로드")
|
||||
async def download_structured_schema():
|
||||
return DownloadService.download_structured_schema()
|
||||
68
workspace/routers/dummy_router.py
Normal file
68
workspace/routers/dummy_router.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Form, Request
|
||||
from services.inference_service import InferenceHandler
|
||||
from utils.checking_keys import get_api_key
|
||||
|
||||
router = APIRouter(prefix="/dummy", tags=["Dummy"])
|
||||
|
||||
|
||||
# ✅ POST:DUMMY
|
||||
@router.post(
|
||||
"/extract/outer",
|
||||
summary="더미 응답 생성",
|
||||
description="""### **요약**
|
||||
실제 모델 추론이나 파일 업로드 없이, 지정된 모델의 응답 형식을 테스트하기 위한 더미(dummy) 결과를 생성합니다.
|
||||
|
||||
### **작동 방식**
|
||||
- 요청 시, 시스템에 미리 저장된 더미 응답(`dummy_response.json`)을 즉시 반환합니다.
|
||||
- 실제 OCR, LLM 추론 등 어떠한 백그라운드 작업도 수행하지 않습니다.
|
||||
- 네트워크나 모델 성능에 관계없이 API 응답 구조를 빠르게 확인하는 용도로 사용됩니다.
|
||||
|
||||
### **입력 (multipart/form-data)**
|
||||
- `model` (선택): 응답 형식의 기준이 될 모델 이름. (기본값: `dummy`)
|
||||
- 이 값은 실제 추론에 사용되지 않으며, 형식 테스트용으로만 기능합니다.
|
||||
|
||||
### **출력 (application/json)**
|
||||
- **즉시 반환**:
|
||||
```json
|
||||
{
|
||||
"filename": "dummy_input.pdf",
|
||||
"dummy_model": {
|
||||
"ocr_model": "dummy",
|
||||
"llm_model": "dummy",
|
||||
"api_url": "dummy"
|
||||
},
|
||||
"time": {
|
||||
"duration_sec": "0.00",
|
||||
"started_at": "...",
|
||||
"ended_at": "..."
|
||||
},
|
||||
"fields": {},
|
||||
"parsed": "dummy",
|
||||
"generated": "dummy",
|
||||
"processed": {
|
||||
"dummy response"
|
||||
}
|
||||
}
|
||||
```
|
||||
""",
|
||||
)
|
||||
async def extract_outer(
|
||||
request_info: Request,
|
||||
model: Optional[str] = Form(
|
||||
default="dummy", description="실제 추론 없이 포맷 테스트용으로 사용됩니다."
|
||||
),
|
||||
api_key: str = Depends(get_api_key),
|
||||
):
|
||||
return await InferenceHandler.handle_extract_background(
|
||||
request_id=None,
|
||||
result_id=None,
|
||||
input_file=None,
|
||||
schema_file=None,
|
||||
prompt_file=None,
|
||||
mode="dummy",
|
||||
model_list=[model],
|
||||
request_info=request_info,
|
||||
api_key=api_key,
|
||||
)
|
||||
728
workspace/routers/extract_router.py
Normal file
728
workspace/routers/extract_router.py
Normal file
@@ -0,0 +1,728 @@
|
||||
import asyncio
|
||||
import io
|
||||
import json
|
||||
from typing import Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
from config.setting import (
|
||||
D6C_PROMPT_PATH,
|
||||
I18N_PROMPT_PATH,
|
||||
PGN_REDIS_DB,
|
||||
PGN_REDIS_HOST,
|
||||
PGN_REDIS_PORT,
|
||||
)
|
||||
from fastapi import APIRouter, Depends, File, Form, Request, UploadFile
|
||||
from fastapi.responses import JSONResponse
|
||||
from redis import Redis
|
||||
from services.inference_service import InferenceHandler
|
||||
from utils.checking_files import (
|
||||
clone_upload_file,
|
||||
validate_all_files,
|
||||
)
|
||||
from utils.checking_keys import create_key, get_api_key
|
||||
from utils.minio_utils import fetch_result_from_minio
|
||||
|
||||
|
||||
# Redis 클라이언트 (LLM Gateway 전용)
|
||||
redis_client = Redis(
|
||||
host=PGN_REDIS_HOST, port=PGN_REDIS_PORT, db=PGN_REDIS_DB, decode_responses=True
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/extract", tags=["Extraction"])
|
||||
|
||||
|
||||
# ✅ 공통 비동기 추론 엔드포인트 생성기
|
||||
def register_extract_route(
|
||||
path: str, mode: str, default_model: str, summary: str, description: str
|
||||
):
|
||||
@router.post(path, summary=summary, description=description)
|
||||
async def extract_endpoint(
|
||||
request_info: Request,
|
||||
input_file: UploadFile = File(...),
|
||||
prompt_file: Optional[UploadFile] = File(
|
||||
default=None,
|
||||
description="⚠️ prompt_file 업로드하지 않을 경우, **'Send empty value'** 체크박스를 반드시 해제해주세요.",
|
||||
),
|
||||
model: Optional[str] = Form(default=default_model),
|
||||
api_key: str = Depends(get_api_key),
|
||||
):
|
||||
validate_all_files(input_file)
|
||||
|
||||
# ✅ 고유한 요청 ID 생성
|
||||
request_id = create_key()
|
||||
result_id = create_key()
|
||||
|
||||
cloned_input = clone_upload_file(input_file) if input_file else None
|
||||
cloned_prompt = clone_upload_file(prompt_file) if prompt_file else None
|
||||
|
||||
# ✅ 백그라운드에서 작업 실행
|
||||
asyncio.create_task(
|
||||
InferenceHandler.handle_extract_background(
|
||||
request_id=request_id,
|
||||
result_id=result_id,
|
||||
input_file=cloned_input,
|
||||
schema_file=None,
|
||||
prompt_file=cloned_prompt,
|
||||
mode=mode,
|
||||
model_list=[model],
|
||||
request_info=request_info,
|
||||
api_key=api_key,
|
||||
)
|
||||
)
|
||||
|
||||
# ✅ request_id → result_id 매핑 저장
|
||||
redis_client.hset("pipeline_result_mapping", request_id, result_id)
|
||||
|
||||
return JSONResponse(
|
||||
content={
|
||||
"message": "문서 추출 및 생성형 응답 작업이 백그라운드에서 실행 중입니다.",
|
||||
"request_id": request_id,
|
||||
"status_check_url": f"/extract/progress/{request_id}",
|
||||
}
|
||||
)
|
||||
|
||||
# FastAPI 문서화용 정보 부여
|
||||
extract_endpoint.__name__ = f"extract_{mode}"
|
||||
extract_endpoint.__doc__ = description
|
||||
return extract_endpoint
|
||||
|
||||
|
||||
# ✅ 내부 모델용 등록
|
||||
extract_inner = register_extract_route(
|
||||
path="/inner",
|
||||
mode="inner",
|
||||
default_model="gemma3:27b",
|
||||
summary="내부 LLM 기반 문서 정보 추출 (비동기)",
|
||||
description="""### **요약**
|
||||
내부망에 배포된 LLM(Ollama 기반)을 사용하여 문서(PDF, 이미지 등)에서 정보를 추출하고 응답을 생성합니다. 이 엔드포인트는 사전 정의된 기본 프롬프트를 사용하며, 비동기적으로 처리됩니다.
|
||||
|
||||
### **작동 방식**
|
||||
1. **요청 접수**: `input_file`을 받아 고유 `request_id`를 생성하고 즉시 반환합니다.
|
||||
2. **백그라운드 처리**:
|
||||
- `input_file`에 대해 **OCR API**를 호출하여 텍스트를 추출합니다.
|
||||
- 시스템에 내장된 기본 프롬프트와 추출된 텍스트를 조합합니다. (`prompt_file`을 업로드하여 기본 프롬프트를 대체할 수 있습니다.)
|
||||
- 내부 LLM(Ollama)에 추론을 요청합니다.
|
||||
3. **상태 및 결과 확인**: `GET /extract/progress/{request_id}`로 작업 상태와 최종 결과를 조회합니다.
|
||||
|
||||
### **입력 (multipart/form-data)**
|
||||
- `input_file` (**필수**): 정보 추출의 대상이 될 문서 파일.
|
||||
- 지원 형식: `.pdf`, `.docx`, `.jpg`, `.png`, `.jpeg` 등.
|
||||
- `prompt_file` (선택): 기본 프롬프트 대신 사용할 사용자 정의 `.txt` 프롬프트 파일.
|
||||
- `model` (선택): 사용할 내부 LLM 모델 이름. (기본값: `gemma3:27b`)
|
||||
|
||||
### **출력 (application/json)**
|
||||
- **초기 응답**:
|
||||
```json
|
||||
{
|
||||
"message": "작업이 백그라운드에서 실행 중입니다.",
|
||||
"request_id": "고유한 요청 ID",
|
||||
"status_check_url": "/extract/progress/고유한 요청 ID"
|
||||
}
|
||||
```
|
||||
- **최종 결과**: `GET /extract/progress/{request_id}`를 통해 확인 가능.
|
||||
""",
|
||||
)
|
||||
|
||||
# ✅ 외부 모델용 등록
|
||||
extract_outer = register_extract_route(
|
||||
path="/outer",
|
||||
mode="outer",
|
||||
default_model="gemini-2.5-flash",
|
||||
summary="외부 LLM 기반 문서 정보 추출 (비동기)",
|
||||
description="""### **요약**
|
||||
외부 상용 LLM(예: GPT, Gemini)을 사용하여 문서에서 정보를 추출하고 응답을 생성합니다. 내부 LLM 엔드포인트와 작동 방식은 동일하나, 외부 API를 호출합니다.
|
||||
|
||||
### **작동 방식**
|
||||
1. **요청 접수**: `input_file`을 받아 `request_id`를 생성 후 즉시 반환합니다.
|
||||
2. **백그라운드 처리**:
|
||||
- `input_file`에서 **OCR API**를 통해 텍스트를 추출합니다.
|
||||
- 내장된 기본 프롬프트(또는 사용자 정의 `prompt_file`)와 텍스트를 조합합니다.
|
||||
- 외부 LLM API(OpenAI, Google 등)에 추론을 요청합니다.
|
||||
3. **상태 및 결과 확인**: `GET /extract/progress/{request_id}`로 작업 상태와 최종 결과를 조회합니다.
|
||||
|
||||
### **입력 (multipart/form-data)**
|
||||
- `input_file` (**필수**): 정보 추출 대상 문서 파일.
|
||||
- `prompt_file` (선택): 기본 프롬프트 대신 사용할 `.txt` 파일.
|
||||
- `model` (선택): 사용할 외부 LLM 모델 이름. (기본값: `gemini-2.5-flash`)
|
||||
|
||||
### **출력 (application/json)**
|
||||
- **초기 응답**:
|
||||
```json
|
||||
{
|
||||
"message": "작업이 백그라운드에서 실행 중입니다.",
|
||||
"request_id": "고유한 요청 ID",
|
||||
"status_check_url": "/extract/progress/고유한 요청 ID"
|
||||
}
|
||||
```
|
||||
- **최종 결과**: `GET /extract/progress/{request_id}`를 통해 확인 가능.
|
||||
""",
|
||||
)
|
||||
|
||||
# ✅ 멀티모달 GPT 테스트용 등록
|
||||
extract_outer_gpt = register_extract_route(
|
||||
path="/outer/gpt",
|
||||
mode="multimodal",
|
||||
default_model="gpt-4o",
|
||||
summary="멀티모달 GPT 테스트용",
|
||||
description="""### **요약**
|
||||
GPT-4o와 같은 멀티모달 모델을 사용하여, 문서(PDF, 이미지 등)에서 정보를 추출하고 응답을 생성합니다.
|
||||
|
||||
### **작동 방식**
|
||||
- 다른 추출 엔드포인트와 동일한 비동기 파이프라인을 따릅니다.
|
||||
- 추론 단계에서 시스템은 멀티모달 출력을 생성하도록 특화된 프롬프트(`multimodal_prompt`)를 사용합니다.
|
||||
- LLM은 `input_file`의 내용과 `prompt_file`의 구조를 바탕으로 JSON 객체를 생성합니다.
|
||||
|
||||
### **입력 (multipart/form-data)**
|
||||
- `input_file` (**필수**): 정보 추출 대상 문서 파일.
|
||||
- `prompt_file` (**선택**): 구조화용 기본 프롬프트 대신 사용할 `.txt` 파일.
|
||||
- `model` (선택): 사용할 LLM 모델 이름.
|
||||
|
||||
### **출력 (application/json)**
|
||||
- **초기 응답**: `request_id` 포함.
|
||||
- **최종 결과**: `GET /extract/progress/{request_id}` 조회 시, 지정된 스키마를 따르는 JSON 객체가 반환됩니다.
|
||||
""",
|
||||
)
|
||||
|
||||
# ✅ 멀티모달 Gemini 테스트용 등록
|
||||
extract_outer_gemini = register_extract_route(
|
||||
path="/outer/gemini",
|
||||
mode="multimodal",
|
||||
default_model="gemini-2.5-flash",
|
||||
summary="멀티모달 Gemini 테스트용",
|
||||
description="""### **요약**
|
||||
Gemini와 같은 멀티모달 모델을 사용하여, 문서(PDF, 이미지 등)에서 정보를 추출하고 응답을 생성합니다.
|
||||
|
||||
### **작동 방식**
|
||||
- 다른 추출 엔드포인트와 동일한 비동기 파이프라인을 따릅니다.
|
||||
- 추론 단계에서 시스템은 멀티모달 출력을 생성하도록 특화된 프롬프트(`multimodal_prompt`)를 사용합니다.
|
||||
- LLM은 `input_file`의 내용과 `prompt_file`의 구조를 바탕으로 JSON 객체를 생성합니다.
|
||||
|
||||
### **입력 (multipart/form-data)**
|
||||
- `input_file` (**필수**): 정보 추출 대상 문서 파일.
|
||||
- `prompt_file` (**선택**): 구조화용 기본 프롬프트 대신 사용할 `.txt` 파일.
|
||||
- `model` (선택): 사용할 LLM 모델 이름.
|
||||
|
||||
### **출력 (application/json)**
|
||||
- **초기 응답**: `request_id` 포함.
|
||||
- **최종 결과**: `GET /extract/progress/{request_id}` 조회 시, 지정된 스키마를 따르는 JSON 객체가 반환됩니다.
|
||||
""",
|
||||
)
|
||||
|
||||
@router.post(
|
||||
"/inner/d6c",
|
||||
summary="국내 문서 테스트용",
|
||||
)
|
||||
async def extract_d6c(
|
||||
request_info: Request,
|
||||
input_file: UploadFile = File(...),
|
||||
model: Optional[str] = Form(default="gemma3:27b"),
|
||||
api_key: str = Depends(get_api_key),
|
||||
):
|
||||
validate_all_files(input_file)
|
||||
|
||||
request_id = create_key()
|
||||
result_id = create_key()
|
||||
|
||||
cloned_input = clone_upload_file(input_file) if input_file else None
|
||||
|
||||
# 설정에 정의된 기본 I18N 프롬프트 파일을 항상 사용
|
||||
with open(I18N_PROMPT_PATH, "rb") as f:
|
||||
content = f.read()
|
||||
|
||||
# 메모리 내 파일 객체 생성
|
||||
spooled_file = io.BytesIO(content)
|
||||
|
||||
# UploadFile과 유사한 객체를 생성하여 백그라운드 핸들러로 전달
|
||||
dummy_prompt_file = UploadFile(filename=I18N_PROMPT_PATH.name, file=spooled_file)
|
||||
cloned_prompt = clone_upload_file(dummy_prompt_file)
|
||||
|
||||
asyncio.create_task(
|
||||
InferenceHandler.handle_extract_background(
|
||||
request_id=request_id,
|
||||
result_id=result_id,
|
||||
input_file=cloned_input,
|
||||
schema_file=None,
|
||||
prompt_file=cloned_prompt,
|
||||
mode="inner",
|
||||
model_list=[model],
|
||||
request_info=request_info,
|
||||
api_key=api_key,
|
||||
)
|
||||
)
|
||||
|
||||
redis_client.hset("pipeline_result_mapping", request_id, result_id)
|
||||
|
||||
return JSONResponse(
|
||||
content={
|
||||
"message": "문서 추출 및 생성형 응답 작업이 백그라운드에서 실행 중입니다.",
|
||||
"request_id": request_id,
|
||||
"status_check_url": f"/extract/progress/{request_id}",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/inner/i18n",
|
||||
summary="해외 문서 테스트용",
|
||||
)
|
||||
async def extract_i18n(
|
||||
request_info: Request,
|
||||
input_file: UploadFile = File(...),
|
||||
model: Optional[str] = Form(default="gemma3:27b"),
|
||||
api_key: str = Depends(get_api_key),
|
||||
):
|
||||
validate_all_files(input_file)
|
||||
|
||||
request_id = create_key()
|
||||
result_id = create_key()
|
||||
|
||||
cloned_input = clone_upload_file(input_file) if input_file else None
|
||||
|
||||
# 설정에 정의된 기본 I18N 프롬프트 파일을 항상 사용
|
||||
with open(D6C_PROMPT_PATH, "rb") as f:
|
||||
content = f.read()
|
||||
|
||||
# 메모리 내 파일 객체 생성
|
||||
spooled_file = io.BytesIO(content)
|
||||
|
||||
# UploadFile과 유사한 객체를 생성하여 백그라운드 핸들러로 전달
|
||||
dummy_prompt_file = UploadFile(filename=D6C_PROMPT_PATH.name, file=spooled_file)
|
||||
cloned_prompt = clone_upload_file(dummy_prompt_file)
|
||||
|
||||
asyncio.create_task(
|
||||
InferenceHandler.handle_extract_background(
|
||||
request_id=request_id,
|
||||
result_id=result_id,
|
||||
input_file=cloned_input,
|
||||
schema_file=None,
|
||||
prompt_file=cloned_prompt,
|
||||
mode="inner",
|
||||
model_list=[model],
|
||||
request_info=request_info,
|
||||
api_key=api_key,
|
||||
)
|
||||
)
|
||||
|
||||
redis_client.hset("pipeline_result_mapping", request_id, result_id)
|
||||
|
||||
return JSONResponse(
|
||||
content={
|
||||
"message": "문서 추출 및 생성형 응답 작업이 백그라운드에서 실행 중입니다.",
|
||||
"request_id": request_id,
|
||||
"status_check_url": f"/extract/progress/{request_id}",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# ✅ structured 모드: 구조화 JSON 스키마 기반 추론
|
||||
@router.post(
|
||||
"/inner/structured",
|
||||
summary="구조화된 JSON 정보 추출 (비동기)",
|
||||
description="""### **요약**
|
||||
사용자가 제공한 `schema_file`에 정의된 JSON 스키마에 따라, 문서에서 정보를 추출하여 구조화된 JSON으로 반환합니다.
|
||||
|
||||
### **작동 방식**
|
||||
- 다른 추출 엔드포인트와 동일한 비동기 파이프라인을 따릅니다.
|
||||
- 추론 단계에서 시스템은 구조화된 출력을 생성하도록 특화된 프롬프트(`structured_prompt`)를 사용합니다.
|
||||
- LLM은 `input_file`의 내용과 `schema_file`의 구조를 바탕으로 JSON 객체를 생성합니다.
|
||||
|
||||
### **입력 (multipart/form-data)**
|
||||
- `input_file` (**필수**): 정보 추출 대상 문서 파일.
|
||||
- `schema_file` (**필수**): 원하는 출력 JSON 구조를 정의하는 `.json` 파일.
|
||||
- `prompt_file` (**필수**): 구조화용 기본 프롬프트 대신 사용할 `.txt` 파일.
|
||||
- `model` (선택): 사용할 LLM 모델 이름.
|
||||
|
||||
### **출력 (application/json)**
|
||||
- **초기 응답**: `request_id` 포함.
|
||||
- **최종 결과**: `GET /extract/progress/{request_id}` 조회 시, 지정된 스키마를 따르는 JSON 객체가 반환됩니다.
|
||||
""",
|
||||
)
|
||||
async def extract_structured_inner(
|
||||
request_info: Request,
|
||||
input_file: UploadFile = File(...),
|
||||
schema_file: UploadFile = File(...),
|
||||
prompt_file: UploadFile = File(...),
|
||||
model: Optional[str] = Form(default="gemma3:27b"),
|
||||
api_key: str = Depends(get_api_key),
|
||||
):
|
||||
validate_all_files(input_file)
|
||||
|
||||
# ✅ 고유한 요청 ID 생성
|
||||
request_id = create_key()
|
||||
result_id = create_key()
|
||||
|
||||
cloned_input = clone_upload_file(input_file) if input_file else None
|
||||
cloned_schema = clone_upload_file(schema_file) if schema_file else None
|
||||
cloned_prompt = clone_upload_file(prompt_file) if prompt_file else None
|
||||
|
||||
# ✅ 백그라운드에서 작업 실행
|
||||
asyncio.create_task(
|
||||
InferenceHandler.handle_extract_background(
|
||||
request_id=request_id,
|
||||
result_id=result_id,
|
||||
input_file=cloned_input,
|
||||
schema_file=cloned_schema,
|
||||
prompt_file=cloned_prompt,
|
||||
mode="structured",
|
||||
model_list=[model],
|
||||
request_info=request_info,
|
||||
api_key=api_key,
|
||||
)
|
||||
)
|
||||
|
||||
# ✅ request_id → result_id 매핑 저장
|
||||
redis_client.hset("pipeline_result_mapping", request_id, result_id)
|
||||
|
||||
return JSONResponse(
|
||||
content={
|
||||
"message": "문서 추출 및 생성형 응답 작업이 백그라운드에서 실행 중입니다.",
|
||||
"request_id": request_id,
|
||||
"status_check_url": f"/extract/progress/{request_id}",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# ✅ structured 모드: 구조화 JSON 스키마 기반 추론
|
||||
@router.post(
|
||||
"/outer/structured",
|
||||
summary="구조화된 JSON 정보 추출 (비동기)",
|
||||
description="""### **요약**
|
||||
사용자가 제공한 `schema_file`에 정의된 JSON 스키마에 따라, 문서에서 정보를 추출하여 구조화된 JSON으로 반환합니다.
|
||||
|
||||
### **작동 방식**
|
||||
- 다른 추출 엔드포인트와 동일한 비동기 파이프라인을 따릅니다.
|
||||
- 추론 단계에서 시스템은 구조화된 출력을 생성하도록 특화된 프롬프트(`structured_prompt`)를 사용합니다.
|
||||
- LLM은 `input_file`의 내용과 `schema_file`의 구조를 바탕으로 JSON 객체를 생성합니다.
|
||||
|
||||
### **입력 (multipart/form-data)**
|
||||
- `input_file` (**필수**): 정보 추출 대상 문서 파일.
|
||||
- `schema_file` (**선택**): 원하는 출력 JSON 구조를 정의하는 `.json` 파일.
|
||||
- `prompt_file` (**선택**): 구조화용 기본 프롬프트 대신 사용할 `.txt` 파일.
|
||||
- `model` (선택): 사용할 LLM 모델 이름.
|
||||
|
||||
### **출력 (application/json)**
|
||||
- **초기 응답**: `request_id` 포함.
|
||||
- **최종 결과**: `GET /extract/progress/{request_id}` 조회 시, 지정된 스키마를 따르는 JSON 객체가 반환됩니다.
|
||||
""",
|
||||
)
|
||||
async def extract_structured_outer(
|
||||
request_info: Request,
|
||||
input_file: UploadFile = File(...),
|
||||
schema_file: Optional[UploadFile] = File(
|
||||
default=None,
|
||||
description="⚠️ schema_file 업로드하지 않을 경우, **'Send empty value'** 체크박스를 반드시 해제해주세요.",
|
||||
),
|
||||
prompt_file: Optional[UploadFile] = File(
|
||||
default=None,
|
||||
description="⚠️ prompt_file 업로드하지 않을 경우, **'Send empty value'** 체크박스를 반드시 해제해주세요.",
|
||||
),
|
||||
model: Optional[str] = Form(default="gemma3:27b"),
|
||||
api_key: str = Depends(get_api_key),
|
||||
):
|
||||
validate_all_files(input_file)
|
||||
|
||||
# ✅ 고유한 요청 ID 생성
|
||||
request_id = create_key()
|
||||
result_id = create_key()
|
||||
|
||||
cloned_input = clone_upload_file(input_file) if input_file else None
|
||||
cloned_schema = clone_upload_file(schema_file) if schema_file else None
|
||||
cloned_prompt = clone_upload_file(prompt_file) if prompt_file else None
|
||||
|
||||
# ✅ 백그라운드에서 작업 실행
|
||||
asyncio.create_task(
|
||||
InferenceHandler.handle_extract_background(
|
||||
request_id=request_id,
|
||||
result_id=result_id,
|
||||
input_file=cloned_input,
|
||||
schema_file=cloned_schema,
|
||||
prompt_file=cloned_prompt,
|
||||
mode="structured",
|
||||
model_list=[model],
|
||||
request_info=request_info,
|
||||
api_key=api_key,
|
||||
)
|
||||
)
|
||||
|
||||
# ✅ request_id → result_id 매핑 저장
|
||||
redis_client.hset("pipeline_result_mapping", request_id, result_id)
|
||||
|
||||
return JSONResponse(
|
||||
content={
|
||||
"message": "문서 추출 및 생성형 응답 작업이 백그라운드에서 실행 중입니다.",
|
||||
"request_id": request_id,
|
||||
"status_check_url": f"/extract/progress/{request_id}",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# ✅ 상태 로그 조회 API
|
||||
@router.get(
|
||||
"/progress/{request_id}",
|
||||
summary="정보 추출 작업 상태 및 결과 조회",
|
||||
description="""### **요약**
|
||||
`POST /extract/*` 계열 엔드포인트 요청 시 반환된 `request_id`를 사용하여, 해당 정보 추출 작업의 진행 상태와 최종 결과를 조회합니다.
|
||||
|
||||
### **작동 방식**
|
||||
- `request_id`를 기반으로 Redis에 저장된 작업 로그와 결과 데이터를 조회합니다.
|
||||
- 작업이 진행 중일 때는 현재까지의 로그를, 완료되었을 때는 로그와 함께 최종 결과(`final_result`)를 반환합니다.
|
||||
|
||||
### **입력**
|
||||
- `request_id`: 조회할 작업의 고유 ID.
|
||||
|
||||
### **출력 (application/json)**
|
||||
- **성공 시**:
|
||||
```json
|
||||
{
|
||||
"request_id": "요청 시 사용된 ID",
|
||||
"progress_logs": [
|
||||
{ "timestamp": "...", "status": "OCR 시작", "details": "..." },
|
||||
{ "timestamp": "...", "status": "입력 길이 검사 시작", "details": "..." },
|
||||
{ "timestamp": "...", "status": "LLM 추론 시작", "details": "..." },
|
||||
{ "timestamp": "...", "status": "LLM 추론 완료 및 후처리 시작", "details": "..." },
|
||||
{ "timestamp": "...", "status": "후처리 완료 및 결과 반환"", "details": "..." }
|
||||
],
|
||||
"final_result": {
|
||||
"filename": "입력 파일",
|
||||
"processed": "LLM의 최종 응답 내용"
|
||||
}
|
||||
}
|
||||
```
|
||||
- **ID가 유효하지 않을 경우 (404 Not Found)**:
|
||||
```json
|
||||
{
|
||||
"message": "{request_id}에 대한 상태 로그가 없습니다."
|
||||
}
|
||||
```
|
||||
""",
|
||||
)
|
||||
async def get_pipeline_status(request_id: str):
|
||||
redis_key = f"pipeline_status:{request_id}"
|
||||
|
||||
# 1. 상태 로그 조회
|
||||
logs = redis_client.lrange(redis_key, 0, -1)
|
||||
if not logs:
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content={"message": f"{request_id}에 대한 상태 로그가 없습니다."},
|
||||
)
|
||||
parsed_logs = [json.loads(log) for log in logs] if logs else []
|
||||
|
||||
# 2. request_id → result_id 매핑 조회
|
||||
result_id = redis_client.hget("pipeline_result_mapping", request_id)
|
||||
|
||||
final_result = None
|
||||
if result_id:
|
||||
# 3. Redis에서 최종 결과 조회
|
||||
result_key = f"pipeline_result:{result_id}"
|
||||
result_str = redis_client.get(result_key)
|
||||
if result_str:
|
||||
try:
|
||||
final_result = json.loads(result_str)
|
||||
return JSONResponse(
|
||||
content={
|
||||
"request_id": request_id,
|
||||
"progress_logs": parsed_logs,
|
||||
"final_result": final_result,
|
||||
}
|
||||
)
|
||||
except json.JSONDecodeError:
|
||||
final_result = {
|
||||
"massage": "[REDIS] 결과 존재하지만, 디코딩에 실패했습니다."
|
||||
}
|
||||
else:
|
||||
print(f"[REDIS] request_id {request_id} 가 Redis에 없습니다.")
|
||||
|
||||
# 4. Redis에 결과가 없으면 MinIO에서 조회
|
||||
try:
|
||||
print(f"[MINIO] MinIO에서 결과를 가져오는 중: {request_id}")
|
||||
final_result = fetch_result_from_minio(request_id)
|
||||
if final_result:
|
||||
return JSONResponse(
|
||||
content={
|
||||
"request_id": request_id,
|
||||
"progress_logs": parsed_logs,
|
||||
"final_result": final_result,
|
||||
}
|
||||
)
|
||||
else:
|
||||
# MinIO에서 결과가 없으면 작업 진행 상태 실시간 확인
|
||||
return JSONResponse(
|
||||
content={
|
||||
"request_id": request_id,
|
||||
"progress_logs": parsed_logs,
|
||||
"final_result": "작업이 진행 중입니다. 결과는 아직 생성되지 않았습니다.",
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"[MINIO] MinIO 결과 조회 중 실패했습니다: {e}")
|
||||
|
||||
## 조찬영
|
||||
@router.post(
|
||||
"/inner2/d6c",
|
||||
summary="국내 문서 테스트용",
|
||||
)
|
||||
async def extract2_d6c(
|
||||
request_info: Request,
|
||||
minio_url: str = Form(...),
|
||||
model: Optional[str] = Form(default="qwen3:30b"),
|
||||
api_key: str = Depends(get_api_key),
|
||||
):
|
||||
try:
|
||||
response = requests.get(minio_url)
|
||||
response.raise_for_status() # 4xx/5xx 응답에 대해 HTTPError 발생
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if e.response.status_code == 403:
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={
|
||||
"message": "제공된 MinIO URL이 만료되었거나 접근 권한이 없습니다."
|
||||
},
|
||||
)
|
||||
else:
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={
|
||||
"message": f"URL에서 파일을 가져오는 데 실패했습니다: {e.response.status_code} {e.response.reason}"
|
||||
},
|
||||
)
|
||||
except requests.exceptions.RequestException as e:
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={"message": f"URL에 연결하는 중 오류가 발생했습니다: {e}"},
|
||||
)
|
||||
|
||||
# URL에서 쿼리 파라미터를 제외한 파일 이름 추출
|
||||
parsed_url = urlparse(minio_url)
|
||||
file_name = parsed_url.path.split("/")[-1]
|
||||
|
||||
# 다운로드한 파일 데이터로 UploadFile 객체 생성
|
||||
input_file = UploadFile(filename=file_name, file=io.BytesIO(response.content))
|
||||
validate_all_files(input_file)
|
||||
|
||||
request_id = create_key()
|
||||
result_id = create_key()
|
||||
|
||||
cloned_input = clone_upload_file(input_file) if input_file else None
|
||||
|
||||
# 설정에 정의된 기본 I18N 프롬프트 파일을 항상 사용
|
||||
with open(I18N_PROMPT_PATH, "rb") as f:
|
||||
content = f.read()
|
||||
|
||||
# 메모리 내 파일 객체 생성
|
||||
spooled_file = io.BytesIO(content)
|
||||
|
||||
# UploadFile과 유사한 객체를 생성하여 백그라운드 핸들러로 전달
|
||||
dummy_prompt_file = UploadFile(filename=I18N_PROMPT_PATH.name, file=spooled_file)
|
||||
cloned_prompt = clone_upload_file(dummy_prompt_file)
|
||||
|
||||
asyncio.create_task(
|
||||
InferenceHandler.handle_extract_background(
|
||||
request_id=request_id,
|
||||
result_id=result_id,
|
||||
input_file=cloned_input,
|
||||
schema_file=None,
|
||||
prompt_file=cloned_prompt,
|
||||
mode="inner",
|
||||
model_list=[model],
|
||||
request_info=request_info,
|
||||
api_key=api_key,
|
||||
)
|
||||
)
|
||||
|
||||
redis_client.hset("pipeline_result_mapping", request_id, result_id)
|
||||
|
||||
return JSONResponse(
|
||||
content={
|
||||
"message": "문서 추출 및 생성형 응답 작업이 백그라운드에서 실행 중입니다.",
|
||||
"request_id": request_id,
|
||||
"status_check_url": f"/extract/progress/{request_id}",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/inner2/i18n",
|
||||
summary="해외 문서 테스트용",
|
||||
)
|
||||
async def extract2_i18n(
|
||||
request_info: Request,
|
||||
minio_url: str = Form(...),
|
||||
model: Optional[str] = Form(default="qwen3:30b"),
|
||||
api_key: str = Depends(get_api_key),
|
||||
):
|
||||
try:
|
||||
response = requests.get(minio_url)
|
||||
response.raise_for_status() # 4xx/5xx 응답에 대해 HTTPError 발생
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if e.response.status_code == 403:
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={
|
||||
"message": "제공된 MinIO URL이 만료되었거나 접근 권한이 없습니다."
|
||||
},
|
||||
)
|
||||
else:
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={
|
||||
"message": f"URL에서 파일을 가져오는 데 실패했습니다: {e.response.status_code} {e.response.reason}"
|
||||
},
|
||||
)
|
||||
except requests.exceptions.RequestException as e:
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={"message": f"URL에 연결하는 중 오류가 발생했습니다: {e}"},
|
||||
)
|
||||
|
||||
# URL에서 쿼리 파라미터를 제외한 파일 이름 추출
|
||||
parsed_url = urlparse(minio_url)
|
||||
file_name = parsed_url.path.split("/")[-1]
|
||||
|
||||
# 다운로드한 파일 데이터로 UploadFile 객체 생성
|
||||
input_file = UploadFile(filename=file_name, file=io.BytesIO(response.content))
|
||||
validate_all_files(input_file)
|
||||
|
||||
request_id = create_key()
|
||||
result_id = create_key()
|
||||
|
||||
cloned_input = clone_upload_file(input_file) if input_file else None
|
||||
|
||||
# 설정에 정의된 기본 I18N 프롬프트 파일을 항상 사용
|
||||
with open(D6C_PROMPT_PATH, "rb") as f:
|
||||
content = f.read()
|
||||
|
||||
# 메모리 내 파일 객체 생성
|
||||
spooled_file = io.BytesIO(content)
|
||||
|
||||
# UploadFile과 유사한 객체를 생성하여 백그라운드 핸들러로 전달
|
||||
dummy_prompt_file = UploadFile(filename=D6C_PROMPT_PATH.name, file=spooled_file)
|
||||
cloned_prompt = clone_upload_file(dummy_prompt_file)
|
||||
|
||||
asyncio.create_task(
|
||||
InferenceHandler.handle_extract_background(
|
||||
request_id=request_id,
|
||||
result_id=result_id,
|
||||
input_file=cloned_input,
|
||||
schema_file=None,
|
||||
prompt_file=cloned_prompt,
|
||||
mode="inner",
|
||||
model_list=[model],
|
||||
request_info=request_info,
|
||||
api_key=api_key,
|
||||
)
|
||||
)
|
||||
|
||||
redis_client.hset("pipeline_result_mapping", request_id, result_id)
|
||||
|
||||
return JSONResponse(
|
||||
content={
|
||||
"message": "문서 추출 및 생성형 응답 작업이 백그라운드에서 실행 중입니다.",
|
||||
"request_id": request_id,
|
||||
"status_check_url": f"/extract/progress/{request_id}",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
## 조찬영
|
||||
235
workspace/routers/general_router.py
Normal file
235
workspace/routers/general_router.py
Normal file
@@ -0,0 +1,235 @@
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
from config.setting import (
|
||||
PGN_REDIS_DB,
|
||||
PGN_REDIS_HOST,
|
||||
PGN_REDIS_PORT,
|
||||
)
|
||||
from fastapi import APIRouter, Depends, File, Form, Request, UploadFile
|
||||
from fastapi.responses import JSONResponse
|
||||
from redis import Redis
|
||||
from services.inference_service import InferenceHandler
|
||||
from utils.checking_files import (
|
||||
clone_upload_file,
|
||||
validate_all_files,
|
||||
)
|
||||
from utils.checking_keys import create_key, get_api_key
|
||||
|
||||
# Redis 클라이언트 (LLM Gateway 전용)
|
||||
redis_client = Redis(
|
||||
host=PGN_REDIS_HOST, port=PGN_REDIS_PORT, db=PGN_REDIS_DB, decode_responses=True
|
||||
)
|
||||
|
||||
|
||||
router = APIRouter(prefix="/general", tags=["General"])
|
||||
|
||||
|
||||
# ✅ 공통 비동기 추론 엔드포인트 생성기
|
||||
def register_general_route(
|
||||
path: str, mode: str, default_model: str, summary: str, description: str
|
||||
):
|
||||
@router.post(path, summary=summary, description=description)
|
||||
async def general_endpoint(
|
||||
request_info: Request,
|
||||
input_file: UploadFile = File(...),
|
||||
prompt_file: UploadFile = File(...),
|
||||
schema_file: Optional[UploadFile] = File(default=None),
|
||||
model: Optional[str] = Form(default=default_model),
|
||||
api_key: str = Depends(get_api_key),
|
||||
):
|
||||
validate_all_files(input_file)
|
||||
|
||||
# ✅ 고유한 요청 ID 생성
|
||||
request_id = create_key()
|
||||
result_id = create_key()
|
||||
|
||||
cloned_input = clone_upload_file(input_file) if input_file else None
|
||||
cloned_prompt = clone_upload_file(prompt_file) if prompt_file else None
|
||||
cloned_schema = clone_upload_file(schema_file) if schema_file else None
|
||||
|
||||
effective_mode = "structured" if schema_file and schema_file.filename else mode
|
||||
|
||||
# ✅ 백그라운드에서 작업 실행
|
||||
asyncio.create_task(
|
||||
InferenceHandler.handle_general_background(
|
||||
request_id=request_id,
|
||||
result_id=result_id,
|
||||
input_file=cloned_input,
|
||||
schema_file=cloned_schema,
|
||||
prompt_file=cloned_prompt,
|
||||
mode=effective_mode,
|
||||
model=model,
|
||||
request_info=request_info,
|
||||
api_key=api_key,
|
||||
)
|
||||
)
|
||||
|
||||
# ✅ request_id → result_id 매핑 저장
|
||||
redis_client.hset("pipeline_result_mapping", request_id, result_id)
|
||||
|
||||
return JSONResponse(
|
||||
content={
|
||||
"message": "문서 추출 및 생성형 응답 작업이 백그라운드에서 실행 중입니다.",
|
||||
"request_id": request_id,
|
||||
"status_check_url": f"/general/progress/{request_id}",
|
||||
}
|
||||
)
|
||||
|
||||
# FastAPI 문서화용 정보 부여
|
||||
general_endpoint.__name__ = f"general_{mode}"
|
||||
general_endpoint.__doc__ = description
|
||||
return general_endpoint
|
||||
|
||||
|
||||
# ✅ 내부 모델용 등록
|
||||
general_inner = register_general_route(
|
||||
path="/inner",
|
||||
mode="inner",
|
||||
default_model="gemma3:27b",
|
||||
summary="내부 LLM 기반 범용 추론 요청 (비동기)",
|
||||
description="""### **요약**
|
||||
내부망에 배포된 LLM(Ollama 기반)을 사용하여 문서 기반의 범용 추론을 비동기적으로 요청합니다. 이 엔드포인트는 파일(PDF, 이미지 등)에서 텍스트를 추출하고, 사용자가 제공한 프롬프트를 적용하여 결과를 생성합니다.
|
||||
|
||||
### **작동 방식**
|
||||
1. **요청 접수**: `input_file`, `prompt_file` 등을 받아 고유한 `request_id`를 생성하고 즉시 반환합니다.
|
||||
2. **백그라운드 처리**:
|
||||
- `input_file`이 문서나 이미지일 경우, **OCR API**를 호출하여 텍스트를 추출합니다.
|
||||
- 추출된 텍스트와 `prompt_file`의 내용을 조합하여 최종 프롬프트를 구성합니다.
|
||||
- 내부 LLM(Ollama)에 추론을 요청합니다.
|
||||
- `schema_file`이 제공되면, LLM이 스키마에 맞는 JSON을 생성하도록 요청합니다.
|
||||
3. **상태 및 결과 확인**: 반환된 `request_id`를 사용하여 `GET /general/progress/{request_id}` 엔드포인트에서 작업 진행 상태와 최종 결과를 조회할 수 있습니다.
|
||||
|
||||
### **입력 (multipart/form-data)**
|
||||
- `input_file` (**필수**): 추론의 기반이 될 문서 파일.
|
||||
- 지원 형식: `.pdf`, `.docx`, `.jpg`, `.png`, `.jpeg` 등.
|
||||
- 내부적으로 OCR을 통해 텍스트가 자동 추출됩니다.
|
||||
- `prompt_file` (**필수**): LLM에 전달할 명령어(프롬프트)가 포함된 `.txt` 파일.
|
||||
- `schema_file` (선택): 결과물의 구조를 정의하는 `.json` 스키마 파일. 제공 시, 출력은 이 스키마를 따르는 JSON 형식으로 강제됩니다.
|
||||
- `model` (선택): 사용할 내부 LLM 모델 이름. (기본값: `gemma3:27b`)
|
||||
|
||||
### **출력 (application/json)**
|
||||
- **초기 응답**:
|
||||
```json
|
||||
{
|
||||
"message": "작업이 백그라운드에서 실행 중입니다.",
|
||||
"request_id": "고유한 요청 ID",
|
||||
"status_check_url": "/general/progress/고유한 요청 ID"
|
||||
}
|
||||
```
|
||||
- **최종 결과**: `GET /general/progress/{request_id}`를 통해 확인 가능.
|
||||
""",
|
||||
)
|
||||
|
||||
# ✅ 외부 모델용 등록
|
||||
general_outer = register_general_route(
|
||||
path="/outer",
|
||||
mode="outer",
|
||||
default_model="gemini-2.5-flash",
|
||||
summary="외부 LLM 기반 범용 추론 요청 (비동기)",
|
||||
description="""### **요약**
|
||||
외부 상용 LLM(예: GPT, Gemini, Claude)을 사용하여 문서 기반의 범용 추론을 비동기적으로 요청합니다. 기능과 작동 방식은 내부 LLM용 엔드포인트와 동일하나, 외부 API를 호출하는 점이 다릅니다.
|
||||
|
||||
### **작동 방식**
|
||||
1. **요청 접수**: `input_file`, `prompt_file` 등을 받아 고유한 `request_id`를 생성하고 즉시 반환합니다.
|
||||
2. **백그라운드 처리**:
|
||||
- `input_file`에서 **OCR API**를 통해 텍스트를 추출합니다.
|
||||
- 추출된 텍스트와 `prompt_file`의 내용을 조합하여 최종 프롬프트를 구성합니다.
|
||||
- 외부 LLM API(OpenAI, Google, Anthropic 등)에 추론을 요청합니다.
|
||||
- `schema_file`이 제공되면, LLM이 스키마에 맞는 JSON을 생성하도록 요청합니다.
|
||||
3. **상태 및 결과 확인**: 반환된 `request_id`를 사용하여 `GET /general/progress/{request_id}` 엔드포인트에서 작업 진행 상태와 최종 결과를 조회할 수 있습니다.
|
||||
|
||||
### **입력 (multipart/form-data)**
|
||||
- `input_file` (**필수**): 추론의 기반이 될 문서 파일.
|
||||
- 지원 형식: `.pdf`, `.docx`, `.jpg`, `.png`, `.jpeg` 등.
|
||||
- `prompt_file` (**필수**): LLM에 전달할 프롬프트가 포함된 `.txt` 파일.
|
||||
- `schema_file` (선택): 결과물의 구조를 정의하는 `.json` 스키마 파일.
|
||||
- `model` (선택): 사용할 외부 LLM 모델 이름. (기본값: `gemini-2.5-flash`)
|
||||
|
||||
### **출력 (application/json)**
|
||||
- **초기 응답**:
|
||||
```json
|
||||
{
|
||||
"message": "작업이 백그라운드에서 실행 중입니다.",
|
||||
"request_id": "고유한 요청 ID",
|
||||
"status_check_url": "/general/progress/고유한 요청 ID"
|
||||
}
|
||||
```
|
||||
- **최종 결과**: `GET /general/progress/{request_id}`를 통해 확인 가능.
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
# ✅ 상태 로그 조회 API
|
||||
@router.get(
|
||||
"/progress/{request_id}",
|
||||
summary="범용 추론 작업 상태 및 결과 조회",
|
||||
description="""### **요약**
|
||||
`POST /general/inner` 또는 `POST /general/outer` 요청 시 반환된 `request_id`를 사용하여, 해당 작업의 진행 상태와 최종 결과를 조회합니다.
|
||||
|
||||
### **작동 방식**
|
||||
- `request_id`를 기반으로 Redis에 저장된 작업 로그와 결과 데이터를 조회합니다.
|
||||
- 작업이 진행 중일 때는 현재까지의 로그를, 완료되었을 때는 로그와 함께 최종 결과(`final_result`)를 반환합니다.
|
||||
|
||||
### **입력**
|
||||
- `request_id`: 조회할 작업의 고유 ID.
|
||||
|
||||
### **출력 (application/json)**
|
||||
- **성공 시**:
|
||||
```json
|
||||
{
|
||||
"request_id": "요청 시 사용된 ID",
|
||||
"progress_logs": [
|
||||
{ "timestamp": "...", "status": "OCR 시작", "details": "..." },
|
||||
{ "timestamp": "...", "status": "입력 길이 검사 시작", "details": "..." },
|
||||
{ "timestamp": "...", "status": "LLM 추론 시작", "details": "..." },
|
||||
{ "timestamp": "...", "status": "LLM 추론 완료 및 후처리 시작", "details": "..." },
|
||||
{ "timestamp": "...", "status": "후처리 완료 및 결과 반환"", "details": "..." }
|
||||
],
|
||||
"final_result": {
|
||||
"filename": "입력 파일",
|
||||
"processed": "LLM의 최종 응답 내용"
|
||||
}
|
||||
}
|
||||
```
|
||||
- **ID가 유효하지 않을 경우 (404 Not Found)**:
|
||||
```json
|
||||
{
|
||||
"message": "{request_id}에 대한 상태 로그가 없습니다."
|
||||
}
|
||||
```
|
||||
""",
|
||||
)
|
||||
async def get_pipeline_status(request_id: str):
|
||||
# 상태 로그 조회
|
||||
redis_key = f"pipeline_status:{request_id}"
|
||||
logs = redis_client.lrange(redis_key, 0, -1)
|
||||
if not logs:
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content={"message": f"{request_id}에 대한 상태 로그가 없습니다."},
|
||||
)
|
||||
parsed_logs = [json.loads(log) for log in logs] if logs else []
|
||||
|
||||
# request_id → result_id 매핑 조회
|
||||
result_id = redis_client.hget("pipeline_result_mapping", request_id)
|
||||
|
||||
final_result = None
|
||||
if result_id:
|
||||
# 최종 결과 조회
|
||||
result_key = f"pipeline_result:{result_id}"
|
||||
result_str = redis_client.get(result_key)
|
||||
if result_str:
|
||||
try:
|
||||
final_result = json.loads(result_str)
|
||||
except json.JSONDecodeError:
|
||||
final_result = {"error": "결과 디코딩 실패"}
|
||||
|
||||
return JSONResponse(
|
||||
content={
|
||||
"request_id": request_id,
|
||||
"progress_logs": parsed_logs,
|
||||
"final_result": final_result,
|
||||
}
|
||||
)
|
||||
46
workspace/routers/guide_router.py
Normal file
46
workspace/routers/guide_router.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from config.setting import (
|
||||
EXTRACT_DEFAULT_PATH,
|
||||
GENERAL_GUIDE_PATH,
|
||||
SCHEMA_FILE_PATH,
|
||||
)
|
||||
from fastapi import APIRouter
|
||||
from fastapi.responses import FileResponse, HTMLResponse
|
||||
|
||||
router = APIRouter(tags=["Guide Book"])
|
||||
|
||||
|
||||
# ✅ /schema_json 가이드 HTML
|
||||
@router.get(
|
||||
"/schema_file_guide",
|
||||
summary="schema 파일 작성 가이드북 HTML 보기",
|
||||
description=(
|
||||
"📄 본 가이드북은 <strong>/general</strong> 및 <strong>/extract/structured</strong> "
|
||||
"엔드포인트에 첨부되는 <strong>schema_file</strong> 작성법을 설명합니다.<br><br>"
|
||||
"가이드북은 <a href='/schema_file_guide' target='_blank'>여기</a>에서 확인하세요."
|
||||
),
|
||||
response_class=HTMLResponse,
|
||||
)
|
||||
async def schema_guide():
|
||||
return FileResponse(SCHEMA_FILE_PATH, media_type="text/html")
|
||||
|
||||
|
||||
# ✅ /general 가이드 HTML
|
||||
@router.get(
|
||||
"/general_guide",
|
||||
summary="/general 가이드북 HTML 보기",
|
||||
description="가이드북을 <a href='/general_guide' target='_blank'>여기</a>에서 확인하세요.",
|
||||
response_class=HTMLResponse,
|
||||
)
|
||||
async def general_guide():
|
||||
return FileResponse(GENERAL_GUIDE_PATH, media_type="text/html")
|
||||
|
||||
|
||||
# ✅ /extract 가이드 HTML
|
||||
@router.get(
|
||||
"/extract_guide",
|
||||
summary="/extract 가이드북 HTML 보기",
|
||||
description="가이드북을 <a href='/extract_guide' target='_blank'>여기</a>에서 확인하세요.",
|
||||
response_class=HTMLResponse,
|
||||
)
|
||||
async def extract_guide():
|
||||
return FileResponse(EXTRACT_DEFAULT_PATH, media_type="text/html")
|
||||
86
workspace/routers/llm_summation.py
Normal file
86
workspace/routers/llm_summation.py
Normal file
@@ -0,0 +1,86 @@
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends
|
||||
from pydantic import BaseModel
|
||||
from services.report import (
|
||||
ask_ollama_qwen,
|
||||
dialog_ask_gemini,
|
||||
run_all_models,
|
||||
tasks_store,
|
||||
total_summation,
|
||||
)
|
||||
from utils.checking_keys import create_key
|
||||
from utils.logging_utils import EndpointLogger
|
||||
|
||||
# ------------------------------------------
|
||||
|
||||
# 로깅 설정
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(tags=["summary"])
|
||||
|
||||
|
||||
class SummaryRequest(BaseModel):
|
||||
text: str
|
||||
|
||||
|
||||
@router.post("/summary") # STT 요약 모델
|
||||
async def summarize(
|
||||
request: SummaryRequest, endpoint_logger: EndpointLogger = Depends(EndpointLogger)
|
||||
):
|
||||
endpoint_logger.log(
|
||||
model="gpt-4.1-mini, qwen3:custom, gemini-2.5-flash, claude-3-7-sonnet-latest",
|
||||
input_filename="None",
|
||||
prompt_filename="None",
|
||||
context_length=len(request.text),
|
||||
)
|
||||
|
||||
results = await total_summation(request.text)
|
||||
return {"summary_results": results}
|
||||
|
||||
|
||||
@router.post("/ollama_summary") # ollama 모델 전용
|
||||
async def ollama_summary(
|
||||
request: SummaryRequest, endpoint_logger: EndpointLogger = Depends(EndpointLogger)
|
||||
):
|
||||
endpoint_logger.log(
|
||||
model="qwen3:custom",
|
||||
input_filename="None",
|
||||
prompt_filename="None",
|
||||
context_length=len(request.text),
|
||||
)
|
||||
|
||||
results = await ask_ollama_qwen(request.text)
|
||||
return {"summary_results": results}
|
||||
|
||||
|
||||
@router.post("/gemini_summary")
|
||||
async def gemini_summary(request: SummaryRequest):
|
||||
results = await dialog_ask_gemini(request.text)
|
||||
return {"summary_results": results}
|
||||
|
||||
|
||||
@router.post("/task_summary") # 모델 별 전체 요약
|
||||
async def task_summary(
|
||||
request: SummaryRequest,
|
||||
background_tasks: BackgroundTasks,
|
||||
endpoint_logger: EndpointLogger = Depends(EndpointLogger),
|
||||
):
|
||||
endpoint_logger.log(
|
||||
model="gpt-4.1-mini, qwen3:custom, gemini-2.5-flash, claude-3-7-sonnet-latest",
|
||||
input_filename="None",
|
||||
prompt_filename="None",
|
||||
context_length=len(request.text),
|
||||
)
|
||||
|
||||
task_id = create_key()
|
||||
background_tasks.add_task(run_all_models, request.text, task_id)
|
||||
return {"task_id": task_id}
|
||||
|
||||
|
||||
@router.get("/task_summary/{task_id}") # 모델 별 요약 조회
|
||||
async def get_status(task_id: str):
|
||||
task = tasks_store.get(task_id)
|
||||
if not task:
|
||||
return {"error": "Invalid task_id"}
|
||||
return task
|
||||
17
workspace/routers/model_router.py
Normal file
17
workspace/routers/model_router.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from fastapi import APIRouter
|
||||
from services.model_service import ModelInfoService
|
||||
|
||||
router = APIRouter(tags=["Model Management"])
|
||||
|
||||
|
||||
# ✅ GET:사용 가능한 모델 조회 API
|
||||
@router.get(
|
||||
"/info",
|
||||
summary="'/extract', '/general' 에서 사용 가능한 모델 목록 확인",
|
||||
description="""
|
||||
✅ 'inner(내부용)' 와 'outer(외부용)' 모델의 사용 가능한 목록을 확인합니다.<br>
|
||||
✅ 'Try it out' → 'Execute' 순서로 클릭합니다.<br>
|
||||
""",
|
||||
)
|
||||
async def get_model_info():
|
||||
return await ModelInfoService.get_model_info()
|
||||
168
workspace/routers/ocr_router.py
Normal file
168
workspace/routers/ocr_router.py
Normal file
@@ -0,0 +1,168 @@
|
||||
import logging
|
||||
|
||||
import httpx
|
||||
from config.setting import (
|
||||
MINIO_BUCKET_NAME,
|
||||
OCR_API_URL,
|
||||
)
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
||||
from fastapi.responses import JSONResponse
|
||||
from utils.checking_files import validate_all_files
|
||||
from utils.checking_keys import create_key
|
||||
from utils.logging_utils import EndpointLogger
|
||||
from utils.minio_utils import upload_file_to_minio_v2 # ✅ MinIO 유틸 함수 import
|
||||
|
||||
router = APIRouter(prefix="/ocr", tags=["OCR"])
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.post(
|
||||
"",
|
||||
summary="문서 OCR 요청 (비동기)",
|
||||
description="""### **요약**
|
||||
문서 파일(PDF, 이미지 등)을 받아 텍스트를 추출하는 OCR(광학 문자 인식) 작업을 비동기적으로 요청합니다.
|
||||
|
||||
### **작동 방식**
|
||||
1. **요청 접수**: `file`을 받아 고유 `request_id`를 생성하고 즉시 반환합니다.
|
||||
2. **백그라운드 처리**:
|
||||
- 업로드된 파일을 내부 저장소(MinIO)에 저장합니다.
|
||||
- 별도의 OCR 서버에 텍스트 추출 작업을 요청합니다.
|
||||
3. **상태 및 결과 확인**: 반환된 `request_id`를 사용하여 `GET /ocr/progress/{request_id}`로 작업 상태를, `GET /ocr/result/{request_id}`로 최종 텍스트 결과를 조회할 수 있습니다.
|
||||
|
||||
### **입력 (multipart/form-data)**
|
||||
- `file` (**필수**): 텍스트를 추출할 문서 파일.
|
||||
- 지원 형식: `.pdf`, `.jpg`, `.png`, `.jpeg` 등 OCR 서버가 지원하는 형식.
|
||||
|
||||
### **출력 (application/json)**
|
||||
- **초기 응답**:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"request_id": "고유한 요청 ID",
|
||||
"status": "작업 접수",
|
||||
"message": "아래 URL을 통해 작업 상태 및 결과를 확인하세요."
|
||||
}
|
||||
]
|
||||
```
|
||||
- **최종 결과**: `GET /ocr/result/{request_id}`를 통해 확인 가능.
|
||||
""",
|
||||
)
|
||||
async def ocr_only(
|
||||
file: UploadFile = File(...),
|
||||
endpoint_logger: EndpointLogger = Depends(EndpointLogger),
|
||||
):
|
||||
validate_all_files(file)
|
||||
results = []
|
||||
endpoint_logger.log(
|
||||
model="paddle-ocr",
|
||||
input_filename=file.filename,
|
||||
context_length=0, # OCR은 context_length가 필요하지 않음
|
||||
)
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
# ✅ 1. 고유 ID 생성
|
||||
request_id = create_key()
|
||||
bucket_name = MINIO_BUCKET_NAME
|
||||
object_name = f"{request_id}/{file.filename}"
|
||||
|
||||
# ✅ 2. MinIO에 파일 업로드 후 presigned URL 생성
|
||||
# presigned_url = upload_file_to_minio(file, request_id)
|
||||
presigned_url = upload_file_to_minio_v2(
|
||||
file=file, bucket_name=bucket_name, object_name=object_name
|
||||
)
|
||||
logger.info(f"[MinIO] ✅ presigned URL 생성 완료: {presigned_url}")
|
||||
|
||||
try:
|
||||
# ✅ 3. OCR API에 presigned URL 전달
|
||||
resp = await client.post(
|
||||
OCR_API_URL,
|
||||
json=[
|
||||
{
|
||||
"file_url": presigned_url,
|
||||
"filename": file.filename,
|
||||
}
|
||||
],
|
||||
timeout=None,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
|
||||
# except httpx.ReadTimeout:
|
||||
# logger.error("[OCR] OCR 서버 지연 가능성")
|
||||
# raise HTTPException(
|
||||
# status_code=504, detail="OCR 서버 응답이 지연되고 있습니다."
|
||||
# )
|
||||
|
||||
# except httpx.HTTPStatusError as e:
|
||||
# logger.error(
|
||||
# f"[OCR] ❌ HTTP 에러 발생: {e.response.status_code} - {e.response.text}"
|
||||
# )
|
||||
# raise HTTPException(
|
||||
# status_code=e.response.status_code, detail="OCR 서버 오류 발생"
|
||||
# )
|
||||
|
||||
except Exception:
|
||||
logger.exception("[OCR] ❌ 예기치 못한 오류 발생")
|
||||
raise HTTPException(
|
||||
status_code=500, detail="OCR 요청 처리 중 내부 오류 발생"
|
||||
)
|
||||
|
||||
# ✅ 4. OCR 응답에서 request_id 추출
|
||||
for item in resp.json().get("results", []):
|
||||
ocr_request_id = item.get("request_id")
|
||||
|
||||
result_item = {
|
||||
"request_id": ocr_request_id,
|
||||
"status": "작업 접수",
|
||||
"message": "아래 URL을 통해 작업 상태 및 결과를 확인하세요.",
|
||||
}
|
||||
results.append(result_item)
|
||||
|
||||
return JSONResponse(content=results)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/progress/{request_id}",
|
||||
summary="OCR 작업 상태 조회",
|
||||
description="""### **요약**
|
||||
`POST /ocr` 요청 시 반환된 `request_id`를 사용하여 OCR 작업의 현재 진행 상태를 조회합니다.
|
||||
|
||||
### **작동 방식**
|
||||
- `request_id`를 OCR 서버에 전달하여 해당 작업의 상태를 가져옵니다.
|
||||
- 상태는 보통 'PENDING', 'IN_PROGRESS', 'SUCCESS', 'FAILURE' 등으로 표시됩니다.
|
||||
|
||||
### **입력**
|
||||
- `request_id`: 조회할 OCR 작업의 고유 ID.
|
||||
|
||||
### **출력 (application/json)**
|
||||
- **성공 시**:
|
||||
```json
|
||||
{
|
||||
"request_id": "요청 시 사용된 ID",
|
||||
"progress_logs": [
|
||||
{ "timestamp": "...", "status": "OCR 시작", "details": "..." },
|
||||
{ "timestamp": "...", "status": "입력 길이 검사 시작", "details": "..." },
|
||||
{ "timestamp": "...", "status": "LLM 추론 시작", "details": "..." },
|
||||
{ "timestamp": "...", "status": "LLM 추론 완료 및 후처리 시작", "details": "..." },
|
||||
{ "timestamp": "...", "status": "후처리 완료 및 결과 반환"", "details": "..." }
|
||||
],
|
||||
"final_result": {
|
||||
"filename": "입력 파일",
|
||||
"parsed": "OCR 결과 내용"
|
||||
}
|
||||
}
|
||||
```
|
||||
- **ID가 유효하지 않을 경우 (404 Not Found)**:
|
||||
```json
|
||||
{
|
||||
"detail": "Meeting ID {request_id} 작업 없음"
|
||||
}
|
||||
```
|
||||
""",
|
||||
)
|
||||
async def get_pipeline_status(request_id: str):
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(f"{OCR_API_URL}/progress/{request_id}")
|
||||
return JSONResponse(content=response.json(), status_code=response.status_code)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"OCR 상태 조회 실패: {str(e)}")
|
||||
144
workspace/routers/stt_router.py
Normal file
144
workspace/routers/stt_router.py
Normal file
@@ -0,0 +1,144 @@
|
||||
# llmgateway/routers/stt_proxy.py
|
||||
import logging
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile
|
||||
from fastapi.responses import JSONResponse
|
||||
from utils.checking_keys import create_key
|
||||
from utils.logging_utils import EndpointLogger
|
||||
from utils.minio_utils import upload_file_to_minio_v2
|
||||
|
||||
router = APIRouter(tags=["STT Gateway"])
|
||||
|
||||
STT_API_BASE_URL = "http://stt_fastapi:8899/ccp" # docker-compose 내 서비스명 기반
|
||||
MULTI_STT_API_BASE_URL = (
|
||||
"http://stt_fastapi:8899/dialog" # docker-compose 내 서비스명 기반
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# 파일 업로드 → stt_api에 Presigned URL 전달
|
||||
@router.post("/audio")
|
||||
async def proxy_audio(
|
||||
audio_file: UploadFile = File(...),
|
||||
endpoint_logger: EndpointLogger = Depends(EndpointLogger),
|
||||
):
|
||||
request_id = create_key()
|
||||
bucket_name = "stt-gateway"
|
||||
object_name = f"{request_id}/{audio_file.filename}"
|
||||
|
||||
try:
|
||||
# upload_file_to_minio_v2는 presigned URL을 반환합니다.
|
||||
presigned_url = upload_file_to_minio_v2(
|
||||
file=audio_file,
|
||||
bucket_name=bucket_name,
|
||||
object_name=object_name,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"MinIO upload failed: {e}")
|
||||
raise HTTPException(status_code=500, detail="File upload to storage failed.")
|
||||
|
||||
# 로깅
|
||||
endpoint_logger.log(model="N/A", input_filename=audio_file.filename)
|
||||
|
||||
# stt_fastapi에 Presigned URL 정보 전달
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
payload = {
|
||||
"file_url": presigned_url,
|
||||
"language": "ko",
|
||||
}
|
||||
response = await client.post(f"{STT_API_BASE_URL}/audio", json=payload)
|
||||
return JSONResponse(content=response.json(), status_code=response.status_code)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"STT API 호출 실패: {str(e)}")
|
||||
|
||||
|
||||
# 상태 조회 → stt_api에 중계 및 오류 로깅
|
||||
@router.get("/progress/{request_id}")
|
||||
async def proxy_progress(request_id: str):
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(f"{STT_API_BASE_URL}/progress/{request_id}")
|
||||
response.raise_for_status() # HTTP 오류 발생 시 예외 처리
|
||||
|
||||
# 응답 데이터 확인 및 로깅
|
||||
data = response.json()
|
||||
if data.get("celery_status") == "FAILURE":
|
||||
# 상세 오류 정보를 포함하여 에러 로그 기록
|
||||
error_details = data.get("progress_logs", [])
|
||||
logger.error(f"[ERROR] STT task failed for request_id {request_id}. Details: {error_details}")
|
||||
|
||||
return JSONResponse(content=data, status_code=response.status_code)
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(f"STT progress check failed with status {e.response.status_code} for request_id {request_id}: {e.response.text}")
|
||||
raise HTTPException(status_code=e.response.status_code, detail=f"STT 상태 조회 실패: {e.response.text}")
|
||||
except Exception as e:
|
||||
logger.error(f"An unexpected error occurred while checking STT progress for request_id {request_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"STT 상태 조회 실패: {str(e)}")
|
||||
|
||||
|
||||
# 다중 입력 회의 → stt_api에 Presigned URL 전달
|
||||
@router.post("/dialog_processing")
|
||||
async def proxy_dialog_processing(
|
||||
audio_file: UploadFile = File(...),
|
||||
meeting_tag: str = Form(...),
|
||||
endpoint_logger: EndpointLogger = Depends(EndpointLogger),
|
||||
):
|
||||
bucket_name = "stt-gateway"
|
||||
request_id = create_key()
|
||||
object_name = f"{meeting_tag}_{request_id}/{audio_file.filename}"
|
||||
|
||||
try:
|
||||
presigned_url = upload_file_to_minio_v2(
|
||||
file=audio_file,
|
||||
bucket_name=bucket_name,
|
||||
object_name=object_name,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"MinIO upload failed for dialog_processing: {e}")
|
||||
raise HTTPException(status_code=500, detail="File upload to storage failed.")
|
||||
|
||||
# 로깅
|
||||
endpoint_logger.log(model="N/A", input_filename=audio_file.filename)
|
||||
|
||||
# stt_fastapi에 Presigned URL 정보 전달
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
payload = {
|
||||
"file_url": presigned_url,
|
||||
"meeting_tag": meeting_tag,
|
||||
}
|
||||
resp = await client.post(
|
||||
f"{MULTI_STT_API_BASE_URL}/dialog_processing", json=payload
|
||||
)
|
||||
return JSONResponse(status_code=resp.status_code, content=resp.json())
|
||||
except httpx.RequestError as e:
|
||||
raise HTTPException(status_code=500, detail=f"내부 서버 요청 실패: {e}")
|
||||
|
||||
|
||||
@router.get("/start_parallel_stt/{meeting_tag}")
|
||||
async def proxy_start_parallel_stt(meeting_tag: str):
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
resp = await client.get(
|
||||
f"{MULTI_STT_API_BASE_URL}/start_parallel_stt/{meeting_tag}"
|
||||
)
|
||||
except httpx.RequestError as e:
|
||||
raise HTTPException(status_code=500, detail=f"내부 서버 요청 실패: {e}")
|
||||
|
||||
return JSONResponse(status_code=resp.status_code, content=resp.json())
|
||||
|
||||
|
||||
@router.get("/dialog_result/{task_id}")
|
||||
async def proxy_get_progress(task_id: str):
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
resp = await client.get(
|
||||
f"{MULTI_STT_API_BASE_URL}/result/parallel/{task_id}"
|
||||
)
|
||||
except httpx.RequestError as e:
|
||||
raise HTTPException(status_code=500, detail=f"내부 서버 요청 실패: {e}")
|
||||
|
||||
return JSONResponse(status_code=resp.status_code, content=resp.json())
|
||||
80
workspace/routers/yolo_router.py
Normal file
80
workspace/routers/yolo_router.py
Normal file
@@ -0,0 +1,80 @@
|
||||
# llmgateway/routers/stt_proxy.py
|
||||
import io
|
||||
import logging
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
from utils.checking_keys import create_key
|
||||
from utils.logging_utils import EndpointLogger
|
||||
from utils.minio_utils import upload_file_to_minio_v2
|
||||
|
||||
router = APIRouter(tags=["YOLO Gateway"])
|
||||
|
||||
YOLO_BASE_URL = "http://yolo_gateway:8891" # docker-compose 내 서비스명 기반
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.post("/detect_view")
|
||||
async def proxy_audio(
|
||||
request_info: Request,
|
||||
image_file: UploadFile = File(...),
|
||||
endpoint_logger: EndpointLogger = Depends(EndpointLogger),
|
||||
):
|
||||
request_id = create_key()
|
||||
bucket_name = "yolo-gateway"
|
||||
object_name = f"{request_id}/{image_file.filename}"
|
||||
|
||||
try:
|
||||
presigned_url = upload_file_to_minio_v2(
|
||||
file=image_file,
|
||||
bucket_name=bucket_name,
|
||||
object_name=object_name,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"MinIO upload failed: {e}")
|
||||
raise HTTPException(status_code=500, detail="File upload to storage failed.")
|
||||
|
||||
endpoint_logger.log(
|
||||
model="yolo11x", input_filename=image_file.filename, context_length=0
|
||||
)
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
payload = {
|
||||
"request_id": request_id,
|
||||
"file_url": presigned_url,
|
||||
}
|
||||
response = await client.post(f"{YOLO_BASE_URL}/detect", json=payload)
|
||||
return JSONResponse(content=response.json(), status_code=response.status_code)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"YOLO API 호출 실패: {str(e)}")
|
||||
|
||||
|
||||
# YOLO 서버의 이미지 프록시
|
||||
@router.get("/detect_view/images/{request_id}")
|
||||
async def proxy_get_image(request_id: str):
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
yolo_url = f"{YOLO_BASE_URL}/images/{request_id}"
|
||||
response = await client.get(yolo_url)
|
||||
response.raise_for_status()
|
||||
except httpx.HTTPError as e:
|
||||
raise HTTPException(status_code=500, detail=f"YOLO 이미지 요청 실패: {str(e)}")
|
||||
|
||||
return StreamingResponse(io.BytesIO(response.content), media_type="image/jpeg")
|
||||
|
||||
|
||||
# YOLO 서버의 JSON 결과 프록시
|
||||
@router.get("/detect_view/results/{request_id}")
|
||||
async def proxy_get_results(request_id: str):
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
yolo_url = f"{YOLO_BASE_URL}/results/{request_id}"
|
||||
response = await client.get(yolo_url)
|
||||
response.raise_for_status()
|
||||
except httpx.HTTPError as e:
|
||||
raise HTTPException(status_code=500, detail=f"YOLO 결과 요청 실패: {str(e)}")
|
||||
|
||||
return JSONResponse(content=response.json(), status_code=response.status_code)
|
||||
Reference in New Issue
Block a user