From 428c43d688c4dadb239a4c19b95abc9b688f8a3c Mon Sep 17 00:00:00 2001 From: yeonjy73 Date: Sun, 20 Jul 2025 17:58:20 +0900 Subject: [PATCH 01/11] docs: add requirements openai --- requirements.txt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c2ad132..ed3a45b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,16 +1,23 @@ annotated-types==0.7.0 anyio==4.9.0 -click==8.1.3 +certifi==2025.7.14 +click==8.2.1 +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.0 pydantic==2.11.7 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 From f575a6f3e2d3ed42c4347544d789cde8a653e52a Mon Sep 17 00:00:00 2001 From: yeonjy73 Date: Sun, 20 Jul 2025 18:25:36 +0900 Subject: [PATCH 02/11] fix: fraud-analysis api endpoint --- app/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/main.py b/app/main.py index 7245ae5..8e8d742 100644 --- a/app/main.py +++ b/app/main.py @@ -4,7 +4,7 @@ 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(): From bf880bef75d5e54f1521bed3405cd1a9c6f54972 Mon Sep 17 00:00:00 2001 From: yeonjy73 Date: Fri, 25 Jul 2025 11:34:10 +0900 Subject: [PATCH 03/11] feat: setting gpt api --- .github/workflows/deploy.yml | 8 +++++++- app/api/fraud_analysis.py | 12 ++++++++++++ app/config/setting.py | 9 +++++++++ app/main.py | 2 +- app/models/gpt_request.py | 5 +++++ app/routers/fraud_analysis.py | 7 ------- app/services/gpt_service.py | 28 ++++++++++++++++++++++++++++ requirements.txt | 4 +++- 8 files changed, 65 insertions(+), 10 deletions(-) create mode 100644 app/api/fraud_analysis.py create mode 100644 app/config/setting.py create mode 100644 app/models/gpt_request.py delete mode 100644 app/routers/fraud_analysis.py create mode 100644 app/services/gpt_service.py 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..e169ba2 --- /dev/null +++ b/app/api/fraud_analysis.py @@ -0,0 +1,12 @@ +from fastapi import APIRouter, HTTPException +from app.services.gpt_service import call_gpt_with_image + +router = APIRouter() + +@router.get("") +async def fraud_analysis(): + try: + answer = await call_gpt_with_image() + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + return {"result": answer} \ No newline at end of file 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 8e8d742..e955943 100644 --- a/app/main.py +++ b/app/main.py @@ -1,5 +1,5 @@ from fastapi import FastAPI -from app.routers import fraud_analysis +from app.api import fraud_analysis app = FastAPI() diff --git a/app/models/gpt_request.py b/app/models/gpt_request.py new file mode 100644 index 0000000..4def738 --- /dev/null +++ b/app/models/gpt_request.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + +class GPTRequest(BaseModel): + prompt: str + max_tokens: int = 150 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..74bba50 --- /dev/null +++ b/app/services/gpt_service.py @@ -0,0 +1,28 @@ +from openai import OpenAI +from app.config.setting import settings + +client = OpenAI( + api_key = settings.gpt_api_key + ) + +async def call_gpt_with_image(): + try: + response = client.responses.create( + model="gpt-4o-mini", + input=[ + { + "role": "developer", + "content": "너는 사기분석 AI야. 사용자의 말을 토대로 사기 유형을 판단하고 그렇게 생각한 이유를 말해줘" + }, + { + "role": "user", + "content": "나의 딸이 다른 번호로 연락이 와서 핸드폰 수리 요금을 이체해달라고 해." + } + ] + ) + print(response) + + except Exception as e: + return f"GPT API 호출 실패: {e}" + + return response.output_text diff --git a/requirements.txt b/requirements.txt index ed3a45b..292e8fe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ anyio==4.9.0 certifi==2025.7.14 click==8.2.1 distro==1.9.0 +dotenv==0.9.9 fastapi==0.116.0 h11==0.16.0 httpcore==1.0.9 @@ -10,8 +11,9 @@ httptools==0.6.4 httpx==0.28.1 idna==3.10 jiter==0.10.0 -openai==1.97.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 From 0974eb27dab36b66484c25c529a69365d7579750 Mon Sep 17 00:00:00 2001 From: yeonjy73 Date: Fri, 25 Jul 2025 11:37:50 +0900 Subject: [PATCH 04/11] refactor: rename func --- app/api/fraud_analysis.py | 4 ++-- app/models/gpt_request.py | 5 ----- app/services/gpt_service.py | 2 +- 3 files changed, 3 insertions(+), 8 deletions(-) delete mode 100644 app/models/gpt_request.py diff --git a/app/api/fraud_analysis.py b/app/api/fraud_analysis.py index e169ba2..3ec0aba 100644 --- a/app/api/fraud_analysis.py +++ b/app/api/fraud_analysis.py @@ -1,12 +1,12 @@ from fastapi import APIRouter, HTTPException -from app.services.gpt_service import call_gpt_with_image +from app.services.gpt_service import call_gpt router = APIRouter() @router.get("") async def fraud_analysis(): try: - answer = await call_gpt_with_image() + answer = await call_gpt() except Exception as e: raise HTTPException(status_code=500, detail=str(e)) return {"result": answer} \ No newline at end of file diff --git a/app/models/gpt_request.py b/app/models/gpt_request.py deleted file mode 100644 index 4def738..0000000 --- a/app/models/gpt_request.py +++ /dev/null @@ -1,5 +0,0 @@ -from pydantic import BaseModel - -class GPTRequest(BaseModel): - prompt: str - max_tokens: int = 150 diff --git a/app/services/gpt_service.py b/app/services/gpt_service.py index 74bba50..312f907 100644 --- a/app/services/gpt_service.py +++ b/app/services/gpt_service.py @@ -5,7 +5,7 @@ api_key = settings.gpt_api_key ) -async def call_gpt_with_image(): +async def call_gpt(): try: response = client.responses.create( model="gpt-4o-mini", From 50f0f41b8af16f5b6b7959cededde7ef4bae35ed Mon Sep 17 00:00:00 2001 From: yeonjy73 Date: Fri, 25 Jul 2025 11:50:08 +0900 Subject: [PATCH 05/11] =?UTF-8?q?feat:=20max=20token=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/services/gpt_service.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/services/gpt_service.py b/app/services/gpt_service.py index 312f907..399f3be 100644 --- a/app/services/gpt_service.py +++ b/app/services/gpt_service.py @@ -13,16 +13,19 @@ async def call_gpt(): { "role": "developer", "content": "너는 사기분석 AI야. 사용자의 말을 토대로 사기 유형을 판단하고 그렇게 생각한 이유를 말해줘" + # 추가적인 설정 추가 }, { "role": "user", "content": "나의 딸이 다른 번호로 연락이 와서 핸드폰 수리 요금을 이체해달라고 해." } - ] + ], + max_output_tokens = 200 ) print(response) except Exception as e: return f"GPT API 호출 실패: {e}" - return response.output_text + # 파싱 필요 + return response.output_text.strip() From d50d67fba3f28037abd833fa375e6784475b8e8a Mon Sep 17 00:00:00 2001 From: yeonjy73 Date: Fri, 25 Jul 2025 14:49:59 +0900 Subject: [PATCH 06/11] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=AC=ED=94=84?= =?UTF-8?q?=ED=8A=B8=20=EC=9E=AC=EA=B0=9C=EB=B0=9C=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=ED=98=95=EC=8B=9D=20=EB=B3=80=EA=B2=BD=20=ED=94=84=EB=A1=AC?= =?UTF-8?q?=ED=94=84=ED=8A=B8=20=EB=B8=94=EB=A1=9D=20=EB=B3=84=EB=8F=84=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=EC=97=90=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/fraud_analysis.py | 18 +++++++++---- app/models/fraud_request.py | 4 +++ app/models/fraud_response.py | 8 ++++++ app/prompts/fraud_prompts.py | 49 ++++++++++++++++++++++++++++++++++++ app/services/gpt_service.py | 29 +++++++++------------ 5 files changed, 86 insertions(+), 22 deletions(-) create mode 100644 app/models/fraud_request.py create mode 100644 app/models/fraud_response.py create mode 100644 app/prompts/fraud_prompts.py diff --git a/app/api/fraud_analysis.py b/app/api/fraud_analysis.py index 3ec0aba..22ab209 100644 --- a/app/api/fraud_analysis.py +++ b/app/api/fraud_analysis.py @@ -1,12 +1,20 @@ 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.get("") -async def fraud_analysis(): +@router.post( + "", + response_model=FraudResponse, + summary="사기 유형 분석" +) +async def fraud_analysis(request: FraudRequest): try: - answer = await call_gpt() + answer = await call_gpt(request.user_input) + response = FraudResponse.model_validate_json(answer) + return response + except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - return {"result": answer} \ No newline at end of file + raise HTTPException(status_code=500, detail=f"사기분석 실패: {e}") diff --git a/app/models/fraud_request.py b/app/models/fraud_request.py new file mode 100644 index 0000000..ff64714 --- /dev/null +++ b/app/models/fraud_request.py @@ -0,0 +1,4 @@ +from pydantic import BaseModel + +class FraudRequest(BaseModel): + user_input: str \ 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..b91d672 --- /dev/null +++ b/app/models/fraud_response.py @@ -0,0 +1,8 @@ +from pydantic import BaseModel, conint +from typing import List + +class FraudResponse(BaseModel): + fraud_type: str # 분류된 사기 유형 + keywords: List[str] # 주요 위험 키워드 (최대 3개) + reason: str # 해당 유형으로 판단한 이유 + risk_score: float # 위험도(0~100) \ 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..a4098cb --- /dev/null +++ b/app/prompts/fraud_prompts.py @@ -0,0 +1,49 @@ +from typing import List, Dict + +def get_fraud_detection_prompt(user_text: str) -> List[Dict[str, str]]: + system = { + "role": "system", + "content": ( + "당신은 사기 탐지 어시스턴트입니다. " + "입력된 텍스트를 미리 정의된 사기 유형 중 하나로 분류하고, " + "최소 1개에서 최대 3개의 주요 위험 키워드를 추출하며, 그 이유를 설명하고, " + "위험 점수(0–100%)를 제공해야 합니다.\n" + "출력은 반드시 valid JSON 객체로만 응답하세요. 아래는 응답 예시입니다:\n" + "{\n" + " \"fraud_type\": \"복권 사기\",\n" + " \"keywords\": [\"키워드1\", \"키워드2\"],\n" + " \"reason\": \"...\",\n" + " \"risk_score\": 92.4\n" + "}\n" + ) + } + + assistant = { + "role": "assistant", + "content": ( + "사기 유형 및 예시:\n\n" + "1. 피싱: '귀하의 계정이 잠길 수 있습니다. 비밀번호를 확인하려면 여기를 클릭하세요.'\n" + "2. 투자 사기: '2주 안에 보장된 30% 수익, 지금 가입하세요.'\n" + "3. 복권 사기: '당신은 1,000,000달러에 당첨되었습니다! 수수료를 지불하면 수령할 수 있습니다.'\n\n" + "예시 1:\n" + "입력: '축하합니다! 당첨되셨습니다. 수령을 위해 은행 정보를 보내주세요.'\n" + "유형: 복권 사기\n" + "키워드: ['당첨되셨습니다', '은행 정보', '수령']\n" + "이유: 상금을 제안하고 은행 정보를 요청하는 전형적인 복권 사기 패턴입니다.\n" + "위험 점수: 92.8\n\n" + "예시 2:\n" + "입력: '고객님, 구독이 만료되었습니다. 이 링크에서 갱신하세요.'\n" + "유형: 피싱\n" + "키워드: ['구독이 만료되었습니다', '갱신하세요', '링크']\n" + "이유: 서비스 중단을 위협하며 링크 클릭을 유도합니다.\n" + "위험 점수: 85.9\n\n" + "이제 아래 입력을 같은 형식으로 분류하세요:" + ) + } + + user = { + "role": "user", + "content": user_text + } + + return [system, assistant, user] diff --git a/app/services/gpt_service.py b/app/services/gpt_service.py index 399f3be..dab69d8 100644 --- a/app/services/gpt_service.py +++ b/app/services/gpt_service.py @@ -1,31 +1,26 @@ -from openai import OpenAI +from openai import OpenAI, OpenAIError from app.config.setting import settings +from app.prompts.fraud_prompts import get_fraud_detection_prompt client = OpenAI( api_key = settings.gpt_api_key ) -async def call_gpt(): +async def call_gpt(user_input: str): + messages = get_fraud_detection_prompt(user_input) + try: response = client.responses.create( model="gpt-4o-mini", - input=[ - { - "role": "developer", - "content": "너는 사기분석 AI야. 사용자의 말을 토대로 사기 유형을 판단하고 그렇게 생각한 이유를 말해줘" - # 추가적인 설정 추가 - }, - { - "role": "user", - "content": "나의 딸이 다른 번호로 연락이 와서 핸드폰 수리 요금을 이체해달라고 해." - } - ], + input = messages, + temperature = 0.5, # 생성된 텍스트의 무작위성을 결정 max_output_tokens = 200 ) - print(response) - + print(response.output_text.strip()) + + except OpenAIError as e: + raise RuntimeError(f"GPT API 호출 실패: {e}") except Exception as e: - return f"GPT API 호출 실패: {e}" + return f"서버 오류 발생: {e}" - # 파싱 필요 return response.output_text.strip() From eba9e38fd2ca88e35f85cc484623288ffaffefbf Mon Sep 17 00:00:00 2001 From: yeonjy73 Date: Fri, 25 Jul 2025 16:21:09 +0900 Subject: [PATCH 07/11] docs: add coderabbit config --- .coderabbit/config.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .coderabbit/config.yml 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 From d7fd1c6d13213cf84b2194cf7f27aa4dec164769 Mon Sep 17 00:00:00 2001 From: yeonjy73 Date: Fri, 25 Jul 2025 22:31:13 +0900 Subject: [PATCH 08/11] feat: add additionalDescription --- app/api/fraud_analysis.py | 4 +-- app/models/fraud_request.py | 3 +- app/models/fraud_response.py | 6 ++-- app/prompts/fraud_prompts.py | 55 +++++++++++++++++++++--------------- app/services/gpt_service.py | 8 ++++-- 5 files changed, 46 insertions(+), 30 deletions(-) diff --git a/app/api/fraud_analysis.py b/app/api/fraud_analysis.py index 22ab209..90590f9 100644 --- a/app/api/fraud_analysis.py +++ b/app/api/fraud_analysis.py @@ -6,13 +6,13 @@ router = APIRouter() @router.post( - "", + path = "", response_model=FraudResponse, summary="사기 유형 분석" ) async def fraud_analysis(request: FraudRequest): try: - answer = await call_gpt(request.user_input) + answer = await call_gpt(request) response = FraudResponse.model_validate_json(answer) return response diff --git a/app/models/fraud_request.py b/app/models/fraud_request.py index ff64714..debdd0b 100644 --- a/app/models/fraud_request.py +++ b/app/models/fraud_request.py @@ -1,4 +1,5 @@ from pydantic import BaseModel class FraudRequest(BaseModel): - user_input: str \ No newline at end of file + messageContent: str + additionalDescription: str \ No newline at end of file diff --git a/app/models/fraud_response.py b/app/models/fraud_response.py index b91d672..2eb7f78 100644 --- a/app/models/fraud_response.py +++ b/app/models/fraud_response.py @@ -2,7 +2,7 @@ from typing import List class FraudResponse(BaseModel): - fraud_type: str # 분류된 사기 유형 + estimatedFraudType: str # 분류된 사기 유형 keywords: List[str] # 주요 위험 키워드 (최대 3개) - reason: str # 해당 유형으로 판단한 이유 - risk_score: float # 위험도(0~100) \ No newline at end of file + explanation: str # 해당 유형으로 판단한 이유 + score: float # 위험도(0~100) \ No newline at end of file diff --git a/app/prompts/fraud_prompts.py b/app/prompts/fraud_prompts.py index a4098cb..c65afc6 100644 --- a/app/prompts/fraud_prompts.py +++ b/app/prompts/fraud_prompts.py @@ -1,19 +1,22 @@ from typing import List, Dict -def get_fraud_detection_prompt(user_text: str) -> List[Dict[str, str]]: +def get_fraud_detection_prompt( + message_content: str, + additional_description: str +) -> List[Dict[str, str]]: system = { "role": "system", "content": ( "당신은 사기 탐지 어시스턴트입니다. " - "입력된 텍스트를 미리 정의된 사기 유형 중 하나로 분류하고, " + "입력된 텍스트를 반드시 미리 정의된 사기 유형 중 하나로 분류하고, " "최소 1개에서 최대 3개의 주요 위험 키워드를 추출하며, 그 이유를 설명하고, " "위험 점수(0–100%)를 제공해야 합니다.\n" "출력은 반드시 valid JSON 객체로만 응답하세요. 아래는 응답 예시입니다:\n" "{\n" - " \"fraud_type\": \"복권 사기\",\n" + " \"estimatedFraudType\": \"복권 사기\",\n" " \"keywords\": [\"키워드1\", \"키워드2\"],\n" - " \"reason\": \"...\",\n" - " \"risk_score\": 92.4\n" + " \"explanation\": \"...\",\n" + " \"score\": 92.4\n" "}\n" ) } @@ -22,28 +25,36 @@ def get_fraud_detection_prompt(user_text: str) -> List[Dict[str, str]]: "role": "assistant", "content": ( "사기 유형 및 예시:\n\n" - "1. 피싱: '귀하의 계정이 잠길 수 있습니다. 비밀번호를 확인하려면 여기를 클릭하세요.'\n" - "2. 투자 사기: '2주 안에 보장된 30% 수익, 지금 가입하세요.'\n" - "3. 복권 사기: '당신은 1,000,000달러에 당첨되었습니다! 수수료를 지불하면 수령할 수 있습니다.'\n\n" - "예시 1:\n" - "입력: '축하합니다! 당첨되셨습니다. 수령을 위해 은행 정보를 보내주세요.'\n" - "유형: 복권 사기\n" - "키워드: ['당첨되셨습니다', '은행 정보', '수령']\n" - "이유: 상금을 제안하고 은행 정보를 요청하는 전형적인 복권 사기 패턴입니다.\n" - "위험 점수: 92.8\n\n" - "예시 2:\n" - "입력: '고객님, 구독이 만료되었습니다. 이 링크에서 갱신하세요.'\n" - "유형: 피싱\n" - "키워드: ['구독이 만료되었습니다', '갱신하세요', '링크']\n" - "이유: 서비스 중단을 위협하며 링크 클릭을 유도합니다.\n" - "위험 점수: 85.9\n\n" - "이제 아래 입력을 같은 형식으로 분류하세요:" + "1. 피싱:\n" + " messageContent: '귀하의 계정이 잠길 수 있습니다. 비밀번호를 확인하려면 여기를 클릭하세요.'\n" + " additionalDescription: '늦은 밤, 낯선 번호로 온 SMS를 우연히 열어보았습니다.'\n" + " 출력 JSON 예시:\n" + " {\n" + " \"estimatedFraudType\": \"피싱\",\n" + " \"keywords\": [\"계정이 잠길\", \"비밀번호 확인\"],\n" + " \"explanation\": \"긴급성을 조성하며 비밀번호 확인 링크를 제공하는 전형적인 피싱 패턴입니다.\",\n" + " \"score\": 88.5\n" + " }\n\n" + "2. 투자 사기:\n" + " messageContent: '2주 만에 30% 수익 보장! 지금 투자하세요.'\n" + " additionalDescription: '점심시간 카페에서 친구가 공유한 링크를 클릭했습니다.'\n" + " 출력 JSON 예시:\n" + " {\n" + " \"estimatedFraudType\": \"투자 사기\",\n" + " \"keywords\": [\"30% 수익 보장\", \"지금 투자\"],\n" + " \"explanation\": \"높은 수익을 짧은 기간에 보장한다는 과장된 약속이 전형적인 투자 사기 신호입니다.\",\n" + " \"score\": 90.2\n" + " }\n\n" + "이제 다음 입력을 같은 형식으로 분류하세요:\n" ) } user = { "role": "user", - "content": user_text + "content": ( + f"messageContent: '{message_content}'\n" + f"additionalDescription: '{additional_description}'" + ) } return [system, assistant, user] diff --git a/app/services/gpt_service.py b/app/services/gpt_service.py index dab69d8..af8f012 100644 --- a/app/services/gpt_service.py +++ b/app/services/gpt_service.py @@ -1,13 +1,17 @@ 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 client = OpenAI( api_key = settings.gpt_api_key ) -async def call_gpt(user_input: str): - messages = get_fraud_detection_prompt(user_input) +async def call_gpt(request: FraudRequest): + messages = get_fraud_detection_prompt( + message_content = request.messageContent, + additional_description = request.additionalDescription + ) try: response = client.responses.create( From 218e02fd38f31dd9457e0b7261797ea4394a43e5 Mon Sep 17 00:00:00 2001 From: yeonjy73 Date: Sat, 26 Jul 2025 17:04:33 +0900 Subject: [PATCH 09/11] fix: request dto and fraud example --- app/models/fraud_request.py | 5 ++- app/prompts/data/fraud_examples.py | 28 ++++++++++++++ app/prompts/fraud_example.py | 9 +++++ app/prompts/fraud_prompts.py | 60 +++++++++++++++++------------- app/services/gpt_service.py | 6 ++- 5 files changed, 79 insertions(+), 29 deletions(-) create mode 100644 app/prompts/data/fraud_examples.py create mode 100644 app/prompts/fraud_example.py diff --git a/app/models/fraud_request.py b/app/models/fraud_request.py index debdd0b..ab4ed12 100644 --- a/app/models/fraud_request.py +++ b/app/models/fraud_request.py @@ -1,5 +1,8 @@ from pydantic import BaseModel +from typing import List class FraudRequest(BaseModel): messageContent: str - additionalDescription: str \ No newline at end of file + keywords: List[str] + additionalDescription: str + imageContent: str \ 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 index c65afc6..680b8eb 100644 --- a/app/prompts/fraud_prompts.py +++ b/app/prompts/fraud_prompts.py @@ -1,9 +1,15 @@ 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 + additional_description: str, + keywords: List[str], + image_content: str, + examples: List[FraudExample] = FRAUD_EXAMPLES ) -> List[Dict[str, str]]: + system = { "role": "system", "content": ( @@ -21,40 +27,42 @@ def get_fraud_detection_prompt( ) } + example_lines = build_example_lines(examples) + assistant_content = "".join(example_lines) + assistant_content += ( + "이제 아래 입력을 같은 형식으로 분류하세요:\n" + ) + print(assistant_content) + assistant = { "role": "assistant", - "content": ( - "사기 유형 및 예시:\n\n" - "1. 피싱:\n" - " messageContent: '귀하의 계정이 잠길 수 있습니다. 비밀번호를 확인하려면 여기를 클릭하세요.'\n" - " additionalDescription: '늦은 밤, 낯선 번호로 온 SMS를 우연히 열어보았습니다.'\n" - " 출력 JSON 예시:\n" - " {\n" - " \"estimatedFraudType\": \"피싱\",\n" - " \"keywords\": [\"계정이 잠길\", \"비밀번호 확인\"],\n" - " \"explanation\": \"긴급성을 조성하며 비밀번호 확인 링크를 제공하는 전형적인 피싱 패턴입니다.\",\n" - " \"score\": 88.5\n" - " }\n\n" - "2. 투자 사기:\n" - " messageContent: '2주 만에 30% 수익 보장! 지금 투자하세요.'\n" - " additionalDescription: '점심시간 카페에서 친구가 공유한 링크를 클릭했습니다.'\n" - " 출력 JSON 예시:\n" - " {\n" - " \"estimatedFraudType\": \"투자 사기\",\n" - " \"keywords\": [\"30% 수익 보장\", \"지금 투자\"],\n" - " \"explanation\": \"높은 수익을 짧은 기간에 보장한다는 과장된 약속이 전형적인 투자 사기 신호입니다.\",\n" - " \"score\": 90.2\n" - " }\n\n" - "이제 다음 입력을 같은 형식으로 분류하세요:\n" - ) + "content": assistant_content } user = { "role": "user", "content": ( f"messageContent: '{message_content}'\n" - f"additionalDescription: '{additional_description}'" + f"additionalDescription: '{additional_description}'\n" + f"keywords: '{keywords}'\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/services/gpt_service.py b/app/services/gpt_service.py index af8f012..1e2fb0d 100644 --- a/app/services/gpt_service.py +++ b/app/services/gpt_service.py @@ -10,7 +10,9 @@ async def call_gpt(request: FraudRequest): messages = get_fraud_detection_prompt( message_content = request.messageContent, - additional_description = request.additionalDescription + additional_description = request.additionalDescription, + keywords = request.keywords, + image_content = request.imageContent ) try: @@ -20,7 +22,7 @@ async def call_gpt(request: FraudRequest): temperature = 0.5, # 생성된 텍스트의 무작위성을 결정 max_output_tokens = 200 ) - print(response.output_text.strip()) + print(response) except OpenAIError as e: raise RuntimeError(f"GPT API 호출 실패: {e}") From 38b06f6ca00dd234474a8044cd10cfaf86a1d24f Mon Sep 17 00:00:00 2001 From: yeonjy73 Date: Sat, 26 Jul 2025 17:24:32 +0900 Subject: [PATCH 10/11] =?UTF-8?q?fix:=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 292e8fe..e231f8e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,6 @@ annotated-types==0.7.0 anyio==4.9.0 certifi==2025.7.14 -click==8.2.1 distro==1.9.0 dotenv==0.9.9 fastapi==0.116.0 From e190004b78665f6186bb23dfc62dd588d4b15feb Mon Sep 17 00:00:00 2001 From: yeonjy73 Date: Sat, 26 Jul 2025 17:57:24 +0900 Subject: [PATCH 11/11] fix: exception and logging --- app/api/fraud_analysis.py | 5 +++-- app/models/fraud_request.py | 7 ++++--- app/models/fraud_response.py | 6 +++--- app/prompts/fraud_prompts.py | 3 +-- app/services/gpt_service.py | 11 ++++++++--- requirements.txt | 2 -- 6 files changed, 19 insertions(+), 15 deletions(-) diff --git a/app/api/fraud_analysis.py b/app/api/fraud_analysis.py index 90590f9..9bbb7da 100644 --- a/app/api/fraud_analysis.py +++ b/app/api/fraud_analysis.py @@ -15,6 +15,7 @@ async def fraud_analysis(request: FraudRequest): 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}") + raise HTTPException(status_code=500, detail=f"응답 파싱 실패: {e}") from e diff --git a/app/models/fraud_request.py b/app/models/fraud_request.py index ab4ed12..9a70a3a 100644 --- a/app/models/fraud_request.py +++ b/app/models/fraud_request.py @@ -1,8 +1,9 @@ from pydantic import BaseModel from typing import List +from typing import Optional class FraudRequest(BaseModel): - messageContent: str + messageContent: Optional[str] = None keywords: List[str] - additionalDescription: str - imageContent: str \ No newline at end of file + 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 index 2eb7f78..4fb0ea3 100644 --- a/app/models/fraud_response.py +++ b/app/models/fraud_response.py @@ -1,8 +1,8 @@ -from pydantic import BaseModel, conint +from pydantic import BaseModel, Field from typing import List class FraudResponse(BaseModel): estimatedFraudType: str # 분류된 사기 유형 - keywords: List[str] # 주요 위험 키워드 (최대 3개) + keywords: List[str] = Field(..., min_items=1, max_items=3, description="주요 위험 키워드 (최대 3개)") explanation: str # 해당 유형으로 판단한 이유 - score: float # 위험도(0~100) \ No newline at end of file + score: float = Field(..., ge=0, le=100, description="위험도(0~100)") \ No newline at end of file diff --git a/app/prompts/fraud_prompts.py b/app/prompts/fraud_prompts.py index 680b8eb..4733a15 100644 --- a/app/prompts/fraud_prompts.py +++ b/app/prompts/fraud_prompts.py @@ -32,7 +32,6 @@ def get_fraud_detection_prompt( assistant_content += ( "이제 아래 입력을 같은 형식으로 분류하세요:\n" ) - print(assistant_content) assistant = { "role": "assistant", @@ -44,7 +43,7 @@ def get_fraud_detection_prompt( "content": ( f"messageContent: '{message_content}'\n" f"additionalDescription: '{additional_description}'\n" - f"keywords: '{keywords}'\n" + f"keywords: {', '.join(keywords) if keywords else 'None'}\n" f"imageContent: '{image_content}'" ) } diff --git a/app/services/gpt_service.py b/app/services/gpt_service.py index 1e2fb0d..19722b0 100644 --- a/app/services/gpt_service.py +++ b/app/services/gpt_service.py @@ -2,6 +2,9 @@ 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 @@ -22,11 +25,13 @@ async def call_gpt(request: FraudRequest): temperature = 0.5, # 생성된 텍스트의 무작위성을 결정 max_output_tokens = 200 ) - print(response) + logger.info(f"GPT API 호출 성공: {response}") except OpenAIError as e: - raise RuntimeError(f"GPT API 호출 실패: {e}") + logger.error(f"GPT API 호출 실패: {e}", exc_info=True) + raise RuntimeError(f"GPT API 호출 실패: {e}") from e except Exception as e: - return f"서버 오류 발생: {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 e231f8e..2485208 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,6 @@ annotated-types==0.7.0 anyio==4.9.0 -certifi==2025.7.14 distro==1.9.0 -dotenv==0.9.9 fastapi==0.116.0 h11==0.16.0 httpcore==1.0.9