-
Notifications
You must be signed in to change notification settings - Fork 0
DEPLOY v1.0.0 #23
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
DEPLOY v1.0.0 #23
Changes from all commits
35ebb67
eb49336
999c6ce
494d3c1
b5a1236
71b6f13
a02453e
3982bac
ff22c6e
4e68318
fcb9b54
2dbc3a9
c4d5482
a47a056
f7376e2
b915cb5
46c32a1
437caf4
6a8e492
5839810
39bb4ff
43cb041
fd8dea8
35cf9a4
e8e0651
bfe07a6
e76098c
bb4eb0a
38eaf14
db90904
e885d49
022e9f3
0541d27
2d1b01e
5d56d32
1125d0d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -4,30 +4,43 @@ | |||||||
| import redis.asyncio as redis | ||||||||
| import uuid | ||||||||
| from datetime import datetime | ||||||||
| import json | ||||||||
|
|
||||||||
| router = APIRouter() | ||||||||
|
|
||||||||
| def get_redis_client(request: Request) -> redis.Redis: | ||||||||
| return request.app.state.redis_client | ||||||||
|
|
||||||||
| """ | ||||||||
| ํ ์คํธ๋ฅผ ์ํ ์์์ ์ธ API | ||||||||
| """ | ||||||||
| @router.post("/predict", status_code=202) | ||||||||
| async def create_prediction_job(job_request: JobRequest, redis_client: redis.Redis = Depends(get_redis_client)): | ||||||||
| correlation_id = str(uuid.uuid4()) | ||||||||
| correlationId = str(uuid.uuid4()) | ||||||||
| job = ImageJob( | ||||||||
| correlationId=correlationId, | ||||||||
| presignedUrl=job_request.presigned_url, | ||||||||
| presignedUrl=job_request.presignedUrl, | ||||||||
| replyQueue=settings.STREAM_RESULT, | ||||||||
| callbackUrl=None, | ||||||||
| contentType="image/jpeg", | ||||||||
| contentType=job_request.contentType, | ||||||||
| createdAt=datetime.utcnow().isoformat(), | ||||||||
| ttlSec=3600, | ||||||||
| ) | ||||||||
|
|
||||||||
| await redis_client.xadd( | ||||||||
| # ํ์ ์ ๋ง๋๋ก ๋ฃ์ด์ฃผ๊ธฐ | ||||||||
| correlationId = job.correlationId | ||||||||
| payload = json.dumps(job.dict()) | ||||||||
|
Comment on lines
+30
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ์ด ๋ถ๋ถ์๋ ๋ ๊ฐ์ง ๊ฐ์ ์ ์ด ์์ต๋๋ค:
Suggested change
|
||||||||
| print("๋ถ์ ๊ฒฐ๊ณผ ๋ฐํ ์์...") | ||||||||
| entry_id = await redis_client.xadd( | ||||||||
| settings.STREAM_JOB, | ||||||||
| {"json": job.model_dump_json()}, | ||||||||
| { | ||||||||
| "type": "image_results", | ||||||||
| "payload": payload, | ||||||||
| "correlationId": correlationId, | ||||||||
| }, | ||||||||
|
Comment on lines
+35
to
+39
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Redis Stream์ ๋ฐํํ๋ ์์
์ {
"type": "image_jobs",
"payload": payload,
"correlationId": correlationId,
} |
||||||||
| maxlen=10_000, | ||||||||
| approximate=True, | ||||||||
| ) | ||||||||
|
|
||||||||
| return {"job_id": correlation_id} | ||||||||
| print("๋ถ์ ๊ฒฐ๊ณผ ๋ฐํ ์๋ฃ...") | ||||||||
|
|
||||||||
| return {"job_id": correlationId} | ||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,26 +1,24 @@ | ||||||||||||||||||
|
|
||||||||||||||||||
| import asyncio | ||||||||||||||||||
| from contextlib import asynccontextmanager | ||||||||||||||||||
| import redis.asyncio as redis | ||||||||||||||||||
|
|
||||||||||||||||||
| import anyio.to_thread | ||||||||||||||||||
| import redis | ||||||||||||||||||
| from fastapi import FastAPI | ||||||||||||||||||
|
|
||||||||||||||||||
| from app.worker.redis_client import redis_client | ||||||||||||||||||
| from app.core.config import settings | ||||||||||||||||||
| from app.worker.worker import JobWorker | ||||||||||||||||||
|
|
||||||||||||||||||
| @asynccontextmanager | ||||||||||||||||||
| async def lifespan(app: FastAPI): | ||||||||||||||||||
| redis_client = redis.from_url(settings.REDIS_URL, decode_responses=True) | ||||||||||||||||||
| limiter = anyio.to_thread.current_default_thread_limiter() | ||||||||||||||||||
| limiter.total_tokens = 100 | ||||||||||||||||||
|
|
||||||||||||||||||
| try: | ||||||||||||||||||
| await redis_client.xgroup_create( | ||||||||||||||||||
| name=settings.STREAM_JOB, | ||||||||||||||||||
| groupname=settings.GROUP_NAME, | ||||||||||||||||||
| id="$", | ||||||||||||||||||
| mkstream=True | ||||||||||||||||||
| ) | ||||||||||||||||||
| except redis.ResponseError as e: | ||||||||||||||||||
| if "BUSYGROUP" not in str(e): | ||||||||||||||||||
| raise | ||||||||||||||||||
| redis_client.xgroup_create( | ||||||||||||||||||
| stream_name=settings.STREAM_JOB, | ||||||||||||||||||
| group_name=settings.GROUP_NAME, | ||||||||||||||||||
| ) | ||||||||||||||||||
|
Comment on lines
+18
to
+21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||
|
|
||||||||||||||||||
| worker = JobWorker(redis_client) | ||||||||||||||||||
| worker_task = asyncio.create_task(worker.run()) | ||||||||||||||||||
|
|
||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,20 +2,25 @@ | |
| from pydantic import BaseModel, Field | ||
|
|
||
| class JobRequest(BaseModel): | ||
| presigned_url: str = Field(..., description="๋ค์ด๋ก๋ํ ์ด๋ฏธ์ง์ Presigned URL") | ||
| presignedUrl: str = Field(..., description="๋ค์ด๋ก๋ํ ์ด๋ฏธ์ง์ Presigned URL") | ||
|
|
||
| """ | ||
| FastAPI๊ฐ image.results Stream์ ๋ฐํํ๋ ๋ฉ์์ง | ||
| """ | ||
| class JobResult(BaseModel): | ||
| pill_name: str | ||
| correlation_id: str | ||
| label: str | ||
| confidence: float | ||
| finished_at: str | ||
| correlationId: str | ||
| pillName: str | ||
| isSafe: int | ||
| description: str | ||
| finishedAt: str | ||
|
|
||
| """ | ||
| Spring์ผ๋ก๋ถํฐ ๋ฐ๋ Job(image.jobs ๊ตฌ๋ ) | ||
| """ | ||
| class ImageJob(BaseModel): | ||
| correlation_id: str = Field(alias="correlationId") | ||
| presigned_url: str = Field(alias="presignedUrl") | ||
| reply_queue: str = Field(alias="replyQueue") | ||
| callback_url: str | None = Field(alias="callbackUrl") | ||
| content_type: str = Field(alias="contentType") | ||
| created_at: str = Field(alias="createdAt") | ||
| ttl_sec: int = Field(alias="ttlSec") | ||
| correlationId: str = Field(alias="correlationId") | ||
| presignedUrl: str = Field(alias="presignedUrl") | ||
| replyQueue: str = Field(alias="replyQueue") | ||
| contentType: str = Field(alias="contentType") | ||
| createdAt: str = Field(alias="createdAt") | ||
| ttlSec: int = Field(alias="ttlSec") | ||
|
Comment on lines
4
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pydantic ๋ชจ๋ธ๋ค์ ํ๋๋ช
์ด class JobRequest(BaseModel):
presigned_url: str = Field(..., alias="presignedUrl", description="๋ค์ด๋ก๋ํ ์ด๋ฏธ์ง์ Presigned URL")
"""
FastAPI๊ฐ image.results Stream์ ๋ฐํํ๋ ๋ฉ์์ง
"""
class JobResult(BaseModel):
correlation_id: str = Field(alias="correlationId")
pill_name: str = Field(alias="pillName")
is_safe: int = Field(alias="isSafe")
description: str
finished_at: str = Field(alias="finishedAt")
"""
Spring์ผ๋ก๋ถํฐ ๋ฐ๋ Job(image.jobs ๊ตฌ๋
)
"""
class ImageJob(BaseModel):
correlation_id: str = Field(alias="correlationId")
presigned_url: str = Field(alias="presignedUrl")
reply_queue: str = Field(alias="replyQueue")
content_type: str = Field(alias="contentType")
created_at: str = Field(alias="createdAt")
ttl_sec: int = Field(alias="ttlSec") |
||
| 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]: | ||
| prompt = f""" | ||
| ์ฝ ์ด๋ฆ: {pill_name} | ||
| ์ง๋ฌธ: ์ด ์ฝ์ ์์ฐ๋ถ๊ฐ ๋ณต์ฉํด๋ ์์ ํ๊ฐ์? ๋ณต์ฉ ๊ฐ๋ฅ ์ฌ๋ถ์ ์ฃผ์์ฌํญ์ ์๋ ค์ฃผ์ธ์. | ||
| description ์์๋ ๋ฌธ์ฅ๋ง๋ค \\n ์ ์ ์ฉํ์ธ์. | ||
| ๊ฒฐ๊ณผ๋ฅผ JSON ํ์์ผ๋ก ์ ํํ ๋ฐํํ์ธ์. ์ค๋ช ์ด๋ ๋ค๋ฅธ ํ ์คํธ๋ฅผ ์ ๋ ๋ง๋ถ์ด์ง ๋ง์ธ์. | ||
| ์คํค๋ง: | ||
| {{ | ||
| "description": "๋ณต์ฉ ๊ฐ๋ฅ ์ฌ๋ถ ๋ฐ ์ฃผ์์ฌํญ์ ๋ํ ์ค๋ช ", | ||
| "isSafe": 1 ๋๋ 0 | ||
| }} | ||
| """ | ||
|
|
||
| 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 ์ฑ๊ณต...") | ||
| 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 | ||
|
|
||
| if not isinstance(description, str): | ||
| description = "" # ์์ ์ฅ์น | ||
|
|
||
| return description, int(isSafe) | ||
|
|
||
|
|
||
| checker = PregnancySafetyChecker(client) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,27 +1,15 @@ | ||
|
|
||
| import boto3 | ||
| from botocore.config import Config as BotoConfig | ||
| from pathlib import Path | ||
| import requests | ||
|
|
||
| from app.core.config import settings | ||
| from io import BytesIO | ||
|
|
||
| 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, destination: Path): | ||
| def download_file_from_presigned_url(self, presigned_url: str) -> BytesIO: | ||
| response = requests.get(presigned_url) | ||
| response.raise_for_status() | ||
| with open(destination, "wb") as f: | ||
| f.write(response.content) | ||
| # response์ content๋ฅผ BytesIO๋ก ๊ฐ์ธ ๋ฐํ | ||
| return BytesIO(response.content) | ||
|
|
||
| s3_service = S3Service() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
job_request๊ฐ์ฒด (ํ์ :JobRequest)์๋contentType์์ฑ์ด ์์ต๋๋ค.JobRequest๋ชจ๋ธ ์ ์์contentTypeํ๋๊ฐ ๋๋ฝ๋ ๊ฒ ๊ฐ์ต๋๋ค. ์ด ์ฝ๋๋ ์คํ ์AttributeError๋ฅผ ๋ฐ์์ํต๋๋ค.JobRequest์คํค๋ง์contentType์ ์ถ๊ฐํ๊ณ ์์ฒญ ๋ณธ๋ฌธ์ ํด๋น ํ๋๋ฅผ ํฌํจํ๋๋ก ์์ ํด์ผ ํฉ๋๋ค.