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
7 changes: 2 additions & 5 deletions app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,11 @@
load_dotenv()

redis_url = os.getenv("REDIS_SERVER_URL")
bucket_name = os.getenv("BUCKET_NAME")
s3_region = os.getenv("S3_REGION")

openai_key = os.getenv("OPENAI_API_KEY")

class Settings(BaseSettings):
REDIS_URL: str = redis_url
S3_REGION: str = bucket_name
BUCKET_NAME: str = s3_region
OPENAI_KEY: str = openai_key

STREAM_JOB: str = "image.jobs" # SpringBoot에서 job 발행 (FastAPI에서 listen)
STREAM_RESULT: str = "image.results" # FastAPI에서 결과 발행 (SpringBoot에서 listen)
Expand Down
66 changes: 66 additions & 0 deletions app/services/openai_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from app.core.config import settings
from openai import OpenAI
import json

client = OpenAI(api_key=settings.OPENAI_KEY)

class PregnancySafetyChecker:
def __init__(self, client: OpenAI):
self.client = client

"""
- isSafe: 안전하면 1, 안전하지 않으면 0
- description: 복용 가능 여부 설명
"""
def ask_chatgpt_about_pregnancy_safety(self, pill_name: str) -> tuple[str, int]:

Choose a reason for hiding this comment

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

medium

현재 ask_chatgpt_about_pregnancy_safety 메서드는 동기적으로 작동합니다. 이 애플리케이션은 asyncio를 기반으로 하므로, openai 라이브러리의 AsyncOpenAI 클라이언트를 사용하여 이 메서드를 async로 구현하는 것이 더 자연스럽고 효율적입니다. 이렇게 하면 호출부에서 asyncio.to_thread를 사용할 필요가 없어지고, 코드 전체의 비동기 일관성을 유지할 수 있습니다.

prompt = f"""
약 이름: {pill_name}
질문: 이 약은 임산부가 복용해도 안전한가요? 복용 가능 여부와 주의사항을 알려주세요.
description 안에는 문장마다 \\n 을 적용하세요.
결과를 JSON 형식으로 정확히 반환하세요. 설명이나 다른 텍스트를 절대 덧붙이지 마세요.
스키마:
{{
"description": "복용 가능 여부 및 주의사항에 대한 설명",
"isSafe": 1 또는 0
}}
"""
Comment on lines +16 to +26

Choose a reason for hiding this comment

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

medium

프롬프트 문자열이 메서드 내에 하드코딩되어 있습니다. 프롬프트가 길고 복잡해질 경우, 가독성과 유지보수성이 떨어질 수 있습니다. 프롬프트를 파일 상단의 상수로 분리하거나, 별도의 템플릿 파일로 관리하는 것을 고려해 보세요. 이렇게 하면 프롬프트의 재사용 및 수정이 용이해집니다.


response = self.client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=0,
max_tokens=600,
response_format={"type": "json_object"}
)
print("GPT Asking 성공...")

Choose a reason for hiding this comment

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

medium

print 문은 디버깅에는 유용하지만, 프로덕션 환경에서는 로그 레벨 관리, 포맷팅, 출력 위치 제어 등이 가능한 표준 logging 라이브러리를 사용하는 것이 좋습니다. 파일 상단에 import logging을 추가하고 logging.info()와 같은 함수를 사용하도록 변경하는 것을 권장합니다.

Suggested change
print("GPT Asking 성공...")
logging.info("GPT Asking 성공...")

raw = response.choices[0].message.content.strip()

try:
data = json.loads(raw)
except json.JSONDecodeError:
start = raw.find("{")
end = raw.rfind("}")
if start != -1 and end != -1 and start < end:
data = json.loads(raw[start:end+1])
else:
# 디버깅
preview = raw[:200].replace("\n", "\\n")
raise ValueError(f"응답이 유효한 JSON이 아닙니다. preview='{preview}'")

description = data.get("description")
isSafe = data.get("isSafe")

if isinstance(isSafe, bool):
isSafe = 1 if isSafe else 0
elif isinstance(isSafe, str):
isSafe = 1 if isSafe.strip() in {"1", "true", "True"} else 0
elif not isinstance(isSafe, int):
isSafe = 0
Comment on lines +53 to +58

Choose a reason for hiding this comment

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

medium

isSafe 값을 정수로 변환하는 로직을 더 간결하게 개선할 수 있습니다. 다양한 타입(bool, str, int)을 처리하는 현재 로직은 방어적이지만, 아래와 같이 한 줄로 표현하여 가독성을 높일 수 있습니다. 이 코드는 isSafe 값이 1, True, "1", "true" 등일 경우 1을, 그 외의 모든 경우(None, 0, False, 다른 숫자나 문자열 등)에는 0을 반환합니다.

        isSafe = 1 if str(isSafe).strip().lower() in ("1", "true") else 0


if not isinstance(description, str):
description = "" # 안전장치

return description, int(isSafe)


checker = PregnancySafetyChecker(client)
18 changes: 3 additions & 15 deletions app/services/s3_service.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,15 @@

import boto3
from botocore.config import Config as BotoConfig
import requests
from io import BytesIO

from app.core.config import settings

class S3Service:
def __init__(self):
self.client = boto3.client(
"s3",
region_name=settings.S3_REGION,
config=BotoConfig(
retries={"max_attempts": 5, "mode": "standard"},
read_timeout=30,
connect_timeout=5,
),
)
pass

def download_file_from_presigned_url(self, presigned_url: str) -> BytesIO:
response = requests.get(presigned_url)
response.raise_for_status()

return BytesIO(response.content) # response 안의 content Stream으로 처리
# response의 content를 BytesIO로 감싸 반환
return BytesIO(response.content)

s3_service = S3Service()
8 changes: 3 additions & 5 deletions app/worker/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from app.core.config import settings
from app.schemas.job import ImageJob, JobResult
from app.services.openai_service import checker
from app.services.predictor_service import predictor_service
from app.services.s3_service import s3_service

Expand All @@ -27,11 +28,8 @@ async def process_image_scan(job: ImageJob, redis_client: redis.Redis):
predictor_service.predict,
stream_file
)

# TODO: ChatGPT에 요청 결과 출력

isSafe = 0
description = "일단은 테스트입니다. 추후에 GPT 부분 추가할 예정"
print(f"[task] Start Asking GPT for job_id={correlationId}")

Choose a reason for hiding this comment

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

medium

디버깅 및 로깅을 위해 print를 사용하는 것보다 표준 logging 모듈을 사용하는 것이 좋습니다. 이를 통해 로그 레벨, 포맷, 출력 대상을 유연하게 관리할 수 있습니다. 파일 상단에 import logging을 추가하고 logging.info()와 같은 함수를 사용해 주세요.

Suggested change
print(f"[task] Start Asking GPT for job_id={correlationId}")
logging.info(f"[task] Start Asking GPT for job_id={correlationId}")

description, isSafe = checker.ask_chatgpt_about_pregnancy_safety(pillName)

Choose a reason for hiding this comment

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

critical

async 함수 내에서 동기 함수인 ask_chatgpt_about_pregnancy_safety를 직접 호출하고 있습니다. 이 방식은 이벤트 루프를 블로킹하여 전체 애플리케이션의 성능을 심각하게 저하시킬 수 있습니다. 다른 I/O 바운드 작업과 마찬가지로 asyncio.to_thread를 사용하여 비동기적으로 실행해야 합니다.

Suggested change
description, isSafe = checker.ask_chatgpt_about_pregnancy_safety(pillName)
description, isSafe = await asyncio.to_thread(checker.ask_chatgpt_about_pregnancy_safety, pillName)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

이미 실행 중인 이벤트 루프가 비동기적으로 동작하므로, 별도의 스레드를 두는 대신 동기 방식 처리를 선택

finishedAt = datetime.utcnow().isoformat()

result = JobResult(
Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ uvicorn
redis
pydantic
pydantic-settings
boto3
requests
torch==2.8.0
torchvision==0.23.0
Pillow==11.3.0
dotenv
dotenv
openai