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