Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .coderabbit/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
features:
docstrings: false
8 changes: 7 additions & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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

docker run -d \
--name "Block-Guard-AI" \
-p 8000:8000 \
-e GPT_API_KEY="${{ env.GPT_API_KEY }}" \
${{ env.DOCKER_REPO }}:latest
21 changes: 21 additions & 0 deletions app/api/fraud_analysis.py
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions app/config/setting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
gpt_api_key: str
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ› οΈ Refactor suggestion

API ν‚€ ν•„λ“œμ— λŒ€ν•œ 검증 κ°•ν™” ν•„μš”

API ν‚€λŠ” ν•„μˆ˜ ν•„λ“œμ΄λ―€λ‘œ λͺ…μ‹œμ μœΌλ‘œ μ„€μ •ν•˜κ³  기본값을 μ œκ±°ν•˜λŠ” 것이 μ’‹μŠ΅λ‹ˆλ‹€. λ˜ν•œ 빈 λ¬Έμžμ—΄ 검증도 μΆ”κ°€ν•΄μ•Ό ν•©λ‹ˆλ‹€.

λ‹€μŒκ³Ό 같이 κ°œμ„ ν•  수 μžˆμŠ΅λ‹ˆλ‹€:

+from pydantic import Field, validator
+
 class Settings(BaseSettings):
-    gpt_api_key: str
+    gpt_api_key: str = Field(..., min_length=1, description="OpenAI GPT API ν‚€")
+    
+    @validator('gpt_api_key')
+    def validate_api_key(cls, v):
+        if not v or not v.strip():
+            raise ValueError('API ν‚€λŠ” ν•„μˆ˜μž…λ‹ˆλ‹€')
+        return v.strip()
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
gpt_api_key: str
from pydantic import Field, validator
class Settings(BaseSettings):
gpt_api_key: str = Field(..., min_length=1, description="OpenAI GPT API ν‚€")
@validator('gpt_api_key')
def validate_api_key(cls, v):
if not v or not v.strip():
raise ValueError('API ν‚€λŠ” ν•„μˆ˜μž…λ‹ˆλ‹€')
return v.strip()
πŸ€– Prompt for AI Agents
In app/config/setting.py at line 4, the gpt_api_key field currently lacks
validation and a default value. To fix this, explicitly mark gpt_api_key as a
required field without a default and add validation to ensure it is not an empty
string. This will enforce that the API key must be provided and cannot be empty.


class Config:
env_file = ".env"

settings = Settings()
4 changes: 2 additions & 2 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -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():
Expand Down
9 changes: 9 additions & 0 deletions app/models/fraud_request.py
Original file line number Diff line number Diff line change
@@ -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
8 changes: 8 additions & 0 deletions app/models/fraud_response.py
Original file line number Diff line number Diff line change
@@ -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)")
28 changes: 28 additions & 0 deletions app/prompts/data/fraud_examples.py
Original file line number Diff line number Diff line change
@@ -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개 ν•­λͺ©)
]
9 changes: 9 additions & 0 deletions app/prompts/fraud_example.py
Original file line number Diff line number Diff line change
@@ -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
67 changes: 67 additions & 0 deletions app/prompts/fraud_prompts.py
Original file line number Diff line number Diff line change
@@ -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
7 changes: 0 additions & 7 deletions app/routers/fraud_analysis.py

This file was deleted.

37 changes: 37 additions & 0 deletions app/services/gpt_service.py
Original file line number Diff line number Diff line change
@@ -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()
8 changes: 7 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -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
Expand Down