Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v1.1.0: 테스트 기간 종료 후 구글 플레이스토어 정식 출시에 따른 변경 및 수정사항 반영 #114

Merged
merged 31 commits into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
5816766
feat: 식습관 분석 상세보기 API 구현을 위한 DIET_ANALYSIS_TB 정의
Kyeong6 Jan 17, 2025
8a09129
fix: gender 남자:1, 여자:2로 변경
Kyeong6 Jan 18, 2025
20e6306
feat: RAG 파이프라인 구축에 의한 스키마 정의
Kyeong6 Jan 18, 2025
d322be8
feat: RAG 파이프라인 구축에 의한 프롬프트 정의
Kyeong6 Jan 19, 2025
ea2df62
feat: Langchain을 이용한 Multi-Chain 구현(기능 구현만 완료 추후 코드 일부 수정 필요)
Kyeong6 Jan 19, 2025
562ba33
feat: Dockerfile에 컨테이너 시간대 한국으로 설정(음식 조회시 에러 발생 해결)
Kyeong6 Jan 20, 2025
e698eb2
refator: 환경에 따른 환경변수 설정 리팩토링 진행(core/config.py)
Kyeong6 Jan 20, 2025
d0f8e7e
refactor: 식습관 분석 코드 모듈화 진행(함수화 리팩토링 필요)
Kyeong6 Jan 20, 2025
8ae79bd
feat: 식습관분석 스크립트(food_analysis.py) 예외처리 및 로그 설정
Kyeong6 Jan 20, 2025
f842b6c
feat: 식습관 분석 상세보기 API 구현 및 Swagger 반영
Kyeong6 Jan 20, 2025
5af77c6
Merge pull request #95 from Kyeong6/feat/#75
Kyeong6 Jan 20, 2025
9f28a2f
feat: 음식 이미지 분석 API 성능 테스트 진행을 위한 테스트 자동화 코드 구현
Kyeong6 Jan 23, 2025
75e29c3
Merge pull request #97 from Kyeong6/feat/#96
Kyeong6 Jan 26, 2025
41891f6
feat: G-Eval 평가방식과 A/B 테스트를 이용한 식습관 분석 API 성능 테스트 자동화 시스템 구현
Kyeong6 Jan 30, 2025
59b854c
Merge pull request #104 from Kyeong6/feat/#86
Kyeong6 Jan 30, 2025
1c8f8ac
feat: 이미지와 텍스트 순서 변경(OpenAI API에 이미지 먼저 제공)
Kyeong6 Feb 3, 2025
ebd9ee5
feat: 식습관 분석 상세보기 API 삭제 후 분석(/diet)로 통합, AOS 통신 테스트를 위한 푸쉬 진행
Kyeong6 Feb 3, 2025
460b398
Merge pull request #106 from Kyeong6/feat/#105
Kyeong6 Feb 3, 2025
d1d064b
Merge branch 'develop' into feat
Kyeong6 Feb 3, 2025
82a1866
Merge pull request #107 from JNU-econovation/feat
Kyeong6 Feb 3, 2025
5056ea2
refactor: 식습관 분석 로직에 존재하는 PromptTemplate 내용 templates 디렉토리로 이전
Kyeong6 Feb 3, 2025
8402c10
feat: 음식명 유사도 로직 수정 및 Embedding 방식 변경(프롬프트 내용 수정 전)
Kyeong6 Feb 4, 2025
a3c16f1
feat: 음식 이미지 탐지 API 프롬프트 최적화 진행
Kyeong6 Feb 4, 2025
cd69fdf
Merge pull request #109 from Kyeong6/feat/#105
Kyeong6 Feb 4, 2025
1d7dc55
feat: 음식 이미지 탐지 API 비동기 처리 및 API 통신 테스트 진행 후 발생한 오류 수정
Kyeong6 Feb 4, 2025
84e881f
feat: Redis를 이용한 프롬프트 캐싱 도입
Kyeong6 Feb 5, 2025
21704e8
Merge pull request #110 from Kyeong6/feat/#108
Kyeong6 Feb 5, 2025
3e63e61
Merge pull request #111 from JNU-econovation/feat
Kyeong6 Feb 5, 2025
7f6e4a4
feat: 음식 이미지 탐지 API 라우터 변경
Kyeong6 Feb 5, 2025
0a42518
Merge pull request #112 from Kyeong6/feat/#108
Kyeong6 Feb 5, 2025
5ce1210
Merge pull request #113 from JNU-econovation/feat
Kyeong6 Feb 5, 2025
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ redis.conf

# test/data
server/test/image
server/test/test_image

# food.csv(대용량)
server/data/food.csv
381 changes: 236 additions & 145 deletions server/apis/food_analysis.py

Large diffs are not rendered by default.

149 changes: 92 additions & 57 deletions server/apis/food_image.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
import os
import base64
import redis
import aiofiles
import time
from datetime import datetime, timedelta
from openai import OpenAI
from openai import AsyncOpenAI
from pinecone.grpc import PineconeGRPC as Pinecone
from core.config import settings
from errors.business_exception import RateLimitExceeded, ImageAnalysisError, ImageProcessingError
from errors.server_exception import FileAccessError, ServiceConnectionError, ExternalAPIError
from logs.logger_config import get_logger
import time

# 환경에 따른 설정 파일 로드
if os.getenv("APP_ENV") == "prod":
from core.config_prod import settings
elif os.getenv("APP_ENV") == "dev":
from core.config_dev import settings
else:
from core.config_local import settings

# 환경에 따른 설정 파일 로드
if os.getenv("APP_ENV") in ["prod", "dev"]:
Expand All @@ -39,11 +33,20 @@
# 요청 제한 설정
RATE_LIMIT = settings.RATE_LIMIT # 하루 최대 요청 가능 횟수

# 프롬프트 캐싱
CACHE_TTL = 3600

# 공용 로거
logger = get_logger()

# Chatgpt API 사용
client = OpenAI(api_key = settings.OPENAI_API_KEY)
# OpenAI API 사용
client = AsyncOpenAI(api_key = settings.OPENAI_API_KEY)

# Upsage API 사용
upstage = AsyncOpenAI(
api_key = settings.UPSTAGE_API_KEY,
base_url="https://api.upstage.ai/v1/solar"
)

# Pinecone 설정
pc = Pinecone(api_key=settings.PINECONE_API_KEY)
Expand Down Expand Up @@ -91,48 +94,71 @@ async def process_image_to_base64(file):


# prompt를 불러오기
def read_prompt(filename):
with open(filename, 'r', encoding='utf-8') as file:
prompt = file.read().strip()
return prompt
async def read_prompt(filename):

# Redis에서 캐싱된 프롬프트 확인
cached_prompt = redis_client.get(f"prompt:{filename}")

if cached_prompt:
# logger.info(f"Redis 캐싱 프롬프트 사용: {filename}")
return cached_prompt

try:
async with aiofiles.open(filename, 'r', encoding='utf-8') as file:
prompt = (await file.read()).strip()

if not prompt:
logger.error("프롬프트 파일 비어있음")
raise FileAccessError()

# Redis에 프롬프트 캐싱(TTL : 1 hr)
redis_client.setex(f"prompt:{filename}", CACHE_TTL, prompt)
logger.info(f"Redis 프롬프트 캐싱 완료: {filename}")

return prompt

except Exception as e:
logger.error(f"프롬프트 파일 읽기 실패: {e}")
raise FileAccessError()


# 음식 이미지 분석 API: prompt_type은 함수명과 동일
def food_image_analyze(image_base64: str):
async def food_image_analyze(image_base64: str):

# prompt 타입 설정
prompt_file = os.path.join(settings.PROMPT_PATH, "food_image_analyze.txt")
prompt = read_prompt(prompt_file)
prompt_file = os.path.join(settings.PROMPT_PATH, "image_detection.txt")
prompt = await read_prompt(prompt_file)

# prompt 내용 없을 경우
if not prompt:
logger.error("food_image_analyze.txt에 prompt 내용 미존재")
logger.error("image_detection.txt에 prompt 내용 미존재")
raise FileAccessError()

# OpenAI API 호출
response = client.chat.completions.create(
response = await client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": prompt},
{
"role": "user",
"content": [
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{image_base64}"
"url": f"data:image/jpeg;base64,{image_base64}",
# 성능이 좋아지지만, token 소모 큼(tradeoff): 검증 필요
# "detail": "high"
}
}
]
}
},
{"role": "system", "content": prompt}
],
temperature=0.0,
max_tokens=300
)

result = response.choices[0].message.content
# print(result)

# 음식명(반환값)이 존재하지 않을 경우
if not result:
Expand All @@ -143,50 +169,59 @@ def food_image_analyze(image_base64: str):
return result


# 제공받은 음식의 벡터 임베딩 값 변환 작업 수행
def get_embedding(text, model="text-embedding-3-small"):
text = text.replace("\n", " ")
embedding = client.embeddings.create(input=[text], model=model).data[0].embedding
return embedding
# 제공받은 음식의 벡터 임베딩 값 변환 작업 수행(Upstage-Embedding 사용)
async def get_embedding(text, model="embedding-query"):
try:
text = text.replace("\n", " ")
response = await upstage.embeddings.create(input=[text], model=model)
return response.data[0].embedding
except Exception as e:
logger.error(f"텍스트 임베딩 변환 실패: {e}")
raise ExternalAPIError()


# 벡터 임베딩을 통한 유사도 분석 진행(Pinecone)
def search_similar_food(query_name, top_k=3, score_threshold=0.7):

async def search_similar_food(query_name, top_k=3, score_threshold=0.7, candidate_multiplier=2):
try:
query_vector = get_embedding(query_name)
except Exception as e:
logger.error(f"OpenAI API 텍스트 임베딩 실패: {e}")
raise ExternalAPIError()
# 음식명 Embedding Vector 변환
query_vector = await get_embedding(query_name)

# Pinecone에서 유사도 검색
results = index.query(
vector=query_vector,
top_k=top_k * candidate_multiplier,
include_metadata=True
)

# 결과 처리 (점수 필터링 적용)
candidates = [
{
'food_pk': match['id'],
'food_name': match['metadata']['food_name'],
'score': match['score']
}
for match in results['matches'] if match['score'] >= score_threshold
]

# Pinecone에서 유사도 검색
results = index.query(
vector=query_vector,
# 결과값 갯수 설정
top_k=top_k,
# 메타데이터 포함 유무
include_metadata=True
)
# 유사도 점수를 기준으로 내림차순 정렬
sorted_candidates = sorted(candidates, key=lambda x: x["score"], reverse=True)

# 결과 처리 (점수 필터링 적용)
similar_foods = [
{
'food_pk': match['id'],
'food_name': match['metadata']['food_name'],
'score': match['score']
}
for match in results['matches'] if match['score'] >= score_threshold
]
# 상위 top_k개 선택
final_results = sorted_candidates[:top_k]

# null로 채워서 항상 top_k 크기로 반환
while len(similar_foods) < top_k:
similar_foods.append({'food_name': None, 'food_pk': None})
# null로 채워서 항상 top_k 크기로 반환
while len(final_results) < top_k:
final_results.append({'food_name': None, 'food_pk': None})

return similar_foods[:top_k]
return final_results

except Exception as e:
logger.error(f"유사도 검색 실패: {e}")
raise ExternalAPIError()


# Redis의 정의된 잔여 기능 횟수 확인
def get_remaining_requests(member_id: int):
async def get_remaining_requests(member_id: int):

try:
# Redis 키 생성
Expand Down
9 changes: 1 addition & 8 deletions server/apis/swagger.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,7 @@
from fastapi import Depends, HTTPException
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from starlette.status import HTTP_401_UNAUTHORIZED

# 환경에 따른 설정 파일 로드
if os.getenv("APP_ENV") == "prod":
from core.config_prod import settings
elif os.getenv("APP_ENV") == "dev":
from core.config_dev import settings
else:
from core.config_local import settings
from core.config import settings

# HTTP 기본 인증을 사용하는 Security 객체 생성
security = HTTPBasic()
Expand Down
18 changes: 18 additions & 0 deletions server/core/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import os

# 환경 설정 로드 함수
def load_settings():

# 기본값을 'local'로 설정
env = os.getenv("APP_ENV", "local").lower()

if env == "prod":
from core.config_prod import settings
elif env == "dev":
from core.config_dev import settings
else:
from core.config_local import settings

return settings

settings = load_settings()
3 changes: 3 additions & 0 deletions server/core/config_dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ class Settings:
# OpenAI
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")

# Upstage
UPSTAGE_API_KEY = os.getenv("UPSTAGE_API_KEY")

# Data
DATA_PATH = os.getenv("DATA_PATH")
DOCKER_DATA_PATH = os.getenv("DOCKER_DATA_PATH")
Expand Down
6 changes: 6 additions & 0 deletions server/core/config_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,17 @@ class Settings:
# OpenAI
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")

# Upstage
UPSTAGE_API_KEY = os.getenv("UPSTAGE_API_KEY")

# Data
DATA_PATH = os.getenv("DATA_PATH")
DOCKER_DATA_PATH = os.getenv("DOCKER_DATA_PATH")
PROMPT_PATH = os.getenv("PROMPT_PATH")

# Test
TEST_PATH = os.getenv("TEST_PATH")

# Log
LOG_PATH = os.getenv("LOG_PATH")

Expand Down
3 changes: 3 additions & 0 deletions server/core/config_prod.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ class Settings:
# OpenAI
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")

# Upstage
UPSTAGE_API_KEY = os.getenv("UPSTAGE_API_KEY")

# Data
DATA_PATH = os.getenv("DATA_PATH")
PROMPT_PATH = os.getenv("PROMPT_PATH")
Expand Down
Loading