diff --git a/.coderabbit/config.yml b/.coderabbit/config.yml new file mode 100644 index 0000000..647d280 --- /dev/null +++ b/.coderabbit/config.yml @@ -0,0 +1,2 @@ +features: + docstrings: false \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index afd7075..4ad5e42 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -14,6 +14,7 @@ env: AWS_HOST: ${{ secrets.AWS_HOST }} AWS_SSH_KEY: ${{ secrets.AWS_SSH_KEY }} AWS_USER: ${{ secrets.AWS_USER }} + GPT_API_KEY: ${{ secrets.GPT_API_KEY }} jobs: @@ -55,4 +56,9 @@ jobs: fi docker pull "${{ env.DOCKER_REPO }}:latest" - docker run -d --name "Block-Guard-AI" -p 8000:8000 ${{ env.DOCKER_REPO }}:latest \ No newline at end of file + + docker run -d \ + --name "Block-Guard-AI" \ + -p 8000:8000 \ + -e GPT_API_KEY="${{ env.GPT_API_KEY }}" \ + ${{ env.DOCKER_REPO }}:latest \ No newline at end of file diff --git a/app/api/fraud_analysis.py b/app/api/fraud_analysis.py new file mode 100644 index 0000000..9bbb7da --- /dev/null +++ b/app/api/fraud_analysis.py @@ -0,0 +1,21 @@ +from fastapi import APIRouter, HTTPException +from app.services.gpt_service import call_gpt +from app.models.fraud_request import FraudRequest +from app.models.fraud_response import FraudResponse + +router = APIRouter() + +@router.post( + path = "", + response_model=FraudResponse, + summary="사기 유형 분석" +) +async def fraud_analysis(request: FraudRequest): + try: + answer = await call_gpt(request) + response = FraudResponse.model_validate_json(answer) + return response + except RuntimeError as e: + raise HTTPException(status_code=500, detail=f"사기분석 실패: {e}") from e + except Exception as e: + raise HTTPException(status_code=500, detail=f"응답 파싱 실패: {e}") from e diff --git a/app/config/setting.py b/app/config/setting.py new file mode 100644 index 0000000..4d36a7f --- /dev/null +++ b/app/config/setting.py @@ -0,0 +1,9 @@ +from pydantic_settings import BaseSettings + +class Settings(BaseSettings): + gpt_api_key: str + + class Config: + env_file = ".env" + +settings = Settings() diff --git a/app/main.py b/app/main.py index 7245ae5..e955943 100644 --- a/app/main.py +++ b/app/main.py @@ -1,10 +1,10 @@ from fastapi import FastAPI -from app.routers import fraud_analysis +from app.api import fraud_analysis app = FastAPI() # router 등록 -app.include_router(fraud_analysis.router, prefix="/api/fraud_analysis", tags=["fraud_analysis"]) +app.include_router(fraud_analysis.router, prefix="/api/fraud-analysis", tags=["fraud-analysis"]) @app.get("/api/hello") async def hello(): diff --git a/app/models/fraud_request.py b/app/models/fraud_request.py new file mode 100644 index 0000000..9a70a3a --- /dev/null +++ b/app/models/fraud_request.py @@ -0,0 +1,9 @@ +from pydantic import BaseModel +from typing import List +from typing import Optional + +class FraudRequest(BaseModel): + messageContent: Optional[str] = None + keywords: List[str] + additionalDescription: Optional[str] = None + imageContent: Optional[str] = None \ No newline at end of file diff --git a/app/models/fraud_response.py b/app/models/fraud_response.py new file mode 100644 index 0000000..4fb0ea3 --- /dev/null +++ b/app/models/fraud_response.py @@ -0,0 +1,8 @@ +from pydantic import BaseModel, Field +from typing import List + +class FraudResponse(BaseModel): + estimatedFraudType: str # 분류된 사기 유형 + keywords: List[str] = Field(..., min_items=1, max_items=3, description="주요 위험 키워드 (최대 3개)") + explanation: str # 해당 유형으로 판단한 이유 + score: float = Field(..., ge=0, le=100, description="위험도(0~100)") \ No newline at end of file diff --git a/app/prompts/data/fraud_examples.py b/app/prompts/data/fraud_examples.py new file mode 100644 index 0000000..7be5e4f --- /dev/null +++ b/app/prompts/data/fraud_examples.py @@ -0,0 +1,28 @@ +from typing import List +from app.prompts.fraud_prompts import FraudExample + +# 여기에 18개 유형 × 3~4개 예시만큼 리스트를 정의 +FRAUD_EXAMPLES: List[FraudExample] = [ + FraudExample( + type_name="피싱", + message_content="귀하의 계정이 잠길 수 있습니다. 비밀번호를 확인하려면 여기를 클릭하세요.", + additional_description="늦은 밤, 낯선 번호로 온 SMS를 우연히 열어보았습니다.", + keywords=["계정 잠금", "비밀번호 확인", "SMS 링크"], + image_content="은행알림 Bot: 당신의 계좌가 비정상적으로 활동되어 잠길 예정입니다. https://bit.ly/secure-link" + ), + FraudExample( + type_name="투자 사기", + message_content="2주 만에 30% 수익 보장! 지금 투자하세요.", + additional_description="점심시간 카페에서 친구가 공유한 링크를 클릭했습니다.", + keywords=["30% 수익", "지금 투자", "보장된 수익"], + image_content="InvestPro: 한정 시간 30% 수익, 투자하기 ▶ http://invest.example.com" + ), + FraudExample( + type_name="복권 사기", + message_content="축하합니다! 당신은 1,000,000달러에 당첨되었습니다. 수수료를 지불하면 수령 가능합니다.", + additional_description="출근길 지하철에서 받은 이메일에 포함된 링크를 클릭했습니다.", + keywords=["당첨", "수수료", "이메일 링크"], + image_content="LotteryKingdom: Congratulations! You've won $1,000,000. Click here https://lotto.fake/claim" + ), + # … (총 60~70개 항목) +] diff --git a/app/prompts/fraud_example.py b/app/prompts/fraud_example.py new file mode 100644 index 0000000..6d768bf --- /dev/null +++ b/app/prompts/fraud_example.py @@ -0,0 +1,9 @@ +from pydantic import BaseModel +from typing import List + +class FraudExample(BaseModel): + type_name: str + message_content: str + keywords: List[str] + additional_description: str + image_content: str \ No newline at end of file diff --git a/app/prompts/fraud_prompts.py b/app/prompts/fraud_prompts.py new file mode 100644 index 0000000..4733a15 --- /dev/null +++ b/app/prompts/fraud_prompts.py @@ -0,0 +1,67 @@ +from typing import List, Dict +from app.prompts.fraud_example import FraudExample +from app.prompts.data.fraud_examples import FRAUD_EXAMPLES + +def get_fraud_detection_prompt( + message_content: str, + additional_description: str, + keywords: List[str], + image_content: str, + examples: List[FraudExample] = FRAUD_EXAMPLES +) -> List[Dict[str, str]]: + + system = { + "role": "system", + "content": ( + "당신은 사기 탐지 어시스턴트입니다. " + "입력된 텍스트를 반드시 미리 정의된 사기 유형 중 하나로 분류하고, " + "최소 1개에서 최대 3개의 주요 위험 키워드를 추출하며, 그 이유를 설명하고, " + "위험 점수(0–100%)를 제공해야 합니다.\n" + "출력은 반드시 valid JSON 객체로만 응답하세요. 아래는 응답 예시입니다:\n" + "{\n" + " \"estimatedFraudType\": \"복권 사기\",\n" + " \"keywords\": [\"키워드1\", \"키워드2\"],\n" + " \"explanation\": \"...\",\n" + " \"score\": 92.4\n" + "}\n" + ) + } + + example_lines = build_example_lines(examples) + assistant_content = "".join(example_lines) + assistant_content += ( + "이제 아래 입력을 같은 형식으로 분류하세요:\n" + ) + + assistant = { + "role": "assistant", + "content": assistant_content + } + + user = { + "role": "user", + "content": ( + f"messageContent: '{message_content}'\n" + f"additionalDescription: '{additional_description}'\n" + f"keywords: {', '.join(keywords) if keywords else 'None'}\n" + f"imageContent: '{image_content}'" + ) + } + + return [system, assistant, user] + +def build_example_lines(examples): + example_lines = ["사기 유형 및 예시:\n"] + for idx, ex in enumerate(examples, start=1): + example_lines.append(f"{idx}. {ex.type_name}:\n") + example_lines.append(f" messageContent: '{ex.message_content}'\n") + example_lines.append(f" additionalDescription: '{ex.additional_description}'\n") + example_lines.append(f" keywords: '{ex.keywords}'\n") + example_lines.append(f" imageContent: '{ex.image_content}'\n") + example_lines.append(" 출력 JSON 예시:\n") + example_lines.append( + f" {{\"estimatedFraudType\": \"{ex.type_name}\", " + f"\"keywords\": [...], \"explanation\": \"...\", \"score\": ...}}\n\n" + ) + + return example_lines diff --git a/app/routers/fraud_analysis.py b/app/routers/fraud_analysis.py deleted file mode 100644 index 276d6c6..0000000 --- a/app/routers/fraud_analysis.py +++ /dev/null @@ -1,7 +0,0 @@ -from fastapi import APIRouter, HTTPException - -router = APIRouter() - -@router.get("") -async def fraud_analysis(): - return {"message": "사기분석 API"} \ No newline at end of file diff --git a/app/services/gpt_service.py b/app/services/gpt_service.py new file mode 100644 index 0000000..19722b0 --- /dev/null +++ b/app/services/gpt_service.py @@ -0,0 +1,37 @@ +from openai import OpenAI, OpenAIError +from app.config.setting import settings +from app.models.fraud_request import FraudRequest +from app.prompts.fraud_prompts import get_fraud_detection_prompt +import logging + +logger = logging.getLogger(__name__) + +client = OpenAI( + api_key = settings.gpt_api_key + ) + +async def call_gpt(request: FraudRequest): + messages = get_fraud_detection_prompt( + message_content = request.messageContent, + additional_description = request.additionalDescription, + keywords = request.keywords, + image_content = request.imageContent + ) + + try: + response = client.responses.create( + model="gpt-4o-mini", + input = messages, + temperature = 0.5, # 생성된 텍스트의 무작위성을 결정 + max_output_tokens = 200 + ) + logger.info(f"GPT API 호출 성공: {response}") + + except OpenAIError as e: + logger.error(f"GPT API 호출 실패: {e}", exc_info=True) + raise RuntimeError(f"GPT API 호출 실패: {e}") from e + except Exception as e: + logger.error(f"서버 오류 발생: {e}", exc_info=True) + raise RuntimeError(f"서버 오류 발생: {e}") from e + + return response.output_text.strip() diff --git a/requirements.txt b/requirements.txt index c2ad132..2485208 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,16 +1,22 @@ annotated-types==0.7.0 anyio==4.9.0 -click==8.1.3 +distro==1.9.0 fastapi==0.116.0 h11==0.16.0 +httpcore==1.0.9 httptools==0.6.4 +httpx==0.28.1 idna==3.10 +jiter==0.10.0 +openai==1.97.1 pydantic==2.11.7 +pydantic-settings==2.10.1 pydantic_core==2.33.2 python-dotenv==1.1.1 PyYAML==6.0.2 sniffio==1.3.1 starlette==0.46.2 +tqdm==4.67.1 typing-inspection==0.4.1 typing_extensions==4.14.1 uvicorn==0.35.0