diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..a768e2f --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,25 @@ +# PR ์ œ๋ชฉ + +## ๐Ÿ“ ๊ฐœ์š” + + + +--- + +## โš™๏ธ ๊ตฌํ˜„ ๋‚ด์šฉ + + + +--- + +## ๐Ÿ“Ž ๊ธฐํƒ€ + + + +--- + +## ๐Ÿงช ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ + + + +--- \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c015b4b..5537321 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -18,6 +18,7 @@ jobs: captioning: ${{ steps.filter.outputs.captioning }} classifying: ${{ steps.filter.outputs.classifying }} masking: ${{ steps.filter.outputs.masking }} + gpt: ${{ steps.filter.outputs.gpt }} steps: - uses: actions/checkout@v3 @@ -31,6 +32,8 @@ jobs: - 'classifying-service/**' masking: - 'masking-service/**' + gpt: + - 'gpt-service/**' deploy-captioning: name: Deploy Captioning Service @@ -189,4 +192,58 @@ jobs: task-definition: ${{ steps.render-task-def.outputs.task-definition }} service: masking-service # ์—…๋ฐ์ดํŠธํ•  ์„œ๋น„์Šค ์ด๋ฆ„ cluster: ai-services-cluster # ํด๋Ÿฌ์Šคํ„ฐ ์ด๋ฆ„ + wait-for-service-stability: true # ๋ฐฐํฌ๊ฐ€ ์•ˆ์ •๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆผ + + deploy-gpt: + name: Deploy GPT Service + needs: changes + if: needs.changes.outputs.gpt == 'true' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v2 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ env.AWS_REGION }} + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + + - name: Build, tag, and push image to Amazon ECR + id: build-image + env: + ECR_REPOSITORY: gpt-service + IMAGE_TAG: ${{ github.sha }} + run: | + docker build -t ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }} ./gpt-service + docker push ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }} + echo "::set-output name=image::${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }}" + + - name: Download task definition + id: task-def + run: | + aws ecs describe-task-definition --task-definition gpt-service --query taskDefinition > task-definition.json + + - name: Fill in the new image ID in the Amazon ECS task definition + id: render-task-def + uses: aws-actions/amazon-ecs-render-task-definition@v1 + with: + task-definition: task-definition.json + container-name: gpt-service # Task Definition์— ์ •์˜๋œ ์ปจํ…Œ์ด๋„ˆ ์ด๋ฆ„ + image: ${{ steps.build-image.outputs.image }} + environment-variables: | + OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }} + + + - name: Deploy Amazon ECS task definition + uses: aws-actions/amazon-ecs-deploy-task-definition@v1 + with: + task-definition: ${{ steps.render-task-def.outputs.task-definition }} + service: gpt-service # ์—…๋ฐ์ดํŠธํ•  ์„œ๋น„์Šค ์ด๋ฆ„ + cluster: ai-services-cluster # ํด๋Ÿฌ์Šคํ„ฐ ์ด๋ฆ„ wait-for-service-stability: true # ๋ฐฐํฌ๊ฐ€ ์•ˆ์ •๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆผ \ No newline at end of file diff --git a/gpt-service/Dockerfile b/gpt-service/Dockerfile new file mode 100644 index 0000000..8d6fd1b --- /dev/null +++ b/gpt-service/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.9-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 8000 + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/gpt-service/lulu.py b/gpt-service/lulu.py new file mode 100644 index 0000000..f79afcc --- /dev/null +++ b/gpt-service/lulu.py @@ -0,0 +1,222 @@ +from openai import OpenAI +from threading import Lock +from typing import Dict, List +import json +import random + + +class LuLuAI: + _instance = None + _lock = Lock() + + def __new__(cls, *args, **kwargs): + with cls._lock: + if cls._instance is None: + cls._instance = super(LuLuAI, cls).__new__(cls) + cls._instance._initialized = False + return cls._instance + + def __init__(self, api_key: str, model: str = "gpt-4.1"): + """ + LuLu AI ์ดˆ๊ธฐํ™” (ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰๋จ) + + Args: + api_key: OpenAI API ํ‚ค + model: ์‚ฌ์šฉํ•  GPT ๋ชจ๋ธ (๊ธฐ๋ณธ๊ฐ’: gpt-4) + """ + with self._lock: + if self._initialized: + return + self.client = OpenAI(api_key=api_key) + self.model = model + self._initialized = True + self.active_games = {} # gameId๋ณ„ ํ˜„์žฌ task๋งŒ ์ €์žฅ + self.global_used_keywords = [] # ์ „์—ญ ์‚ฌ์šฉ๋œ ํ‚ค์›Œ๋“œ ์ €์žฅ (์ตœ๋Œ€ 30๊ฐœ) + + def create_game(self) -> str: + """ + ์ƒˆ ๊ฒŒ์ž„ ์‹œ์ž‘ ๋ฐ 4์ž๋ฆฌ gameId ๋ฐœ๊ธ‰ + + Returns: + str: ์ƒ์„ฑ๋œ 4์ž๋ฆฌ gameId + """ + # ์ค‘๋ณต๋˜์ง€ ์•Š๋Š” 4์ž๋ฆฌ ์ˆซ์ž ์ƒ์„ฑ + while True: + game_id = f"{random.randint(1000, 9999)}" + if game_id not in self.active_games: + break + + self.active_games[game_id] = None # ์•„์ง task ์ƒ์„ฑ ์•ˆ๋จ + return game_id + + def _update_global_keywords(self, new_keyword: str): + """ + ์ „์—ญ ํ‚ค์›Œ๋“œ ๋ชฉ๋ก ์—…๋ฐ์ดํŠธ (์ตœ๋Œ€ 30๊ฐœ ์œ ์ง€) + + Args: + new_keyword: ์ƒˆ๋กœ ์ถ”๊ฐ€ํ•  ํ‚ค์›Œ๋“œ + """ + if new_keyword not in self.global_used_keywords: + self.global_used_keywords.append(new_keyword) + # 30๊ฐœ๋ฅผ ์ดˆ๊ณผํ•˜๋ฉด ๊ฐ€์žฅ ์˜ค๋ž˜๋œ ๊ฒƒ๋ถ€ํ„ฐ ์ œ๊ฑฐ + if len(self.global_used_keywords) > 30: + self.global_used_keywords.pop(0) + + def flush_game_data(self, game_id: str): + """ + ํŠน์ • ๊ฒŒ์ž„ ID์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ญ์ œ + + Args: + game_id: ์‚ญ์ œํ•  ๊ฒŒ์ž„ ID + + Returns: + bool: ์‚ญ์ œ ์„ฑ๊ณต ์—ฌ๋ถ€ + """ + if game_id in self.active_games: + del self.active_games[game_id] + + def generate_drawing_task(self, game_id: str) -> Dict: + """ + ์š”์ฒญ ๋‹จ๊ณ„: AI๊ฐ€ ์ถ”์ƒ์ ์ด๊ณ  ์‹œ์ ์ธ ํ‘œํ˜„์œผ๋กœ ๊ทธ๋ฆผ ๊ณผ์ œ ์ œ์‹œ + + Args: + game_id: ๊ฒŒ์ž„ ID + + Returns: + Dict: {"keyword": str, "situation": str, "game_id": str} + """ + if game_id not in self.active_games: + raise ValueError("Invalid game ID") + + system_prompt = f""" + ๋„ˆ๋Š” ๊ฟˆ๊ณผ ํ™˜์ƒ์„ ๋‹ค๋ฃจ๋Š” ์‹ ๋น„๋กœ์šด ์ด์•ผ๊ธฐ๊พผ์ด์•ผ. + ์‚ฌ์šฉ์ž์—๊ฒŒ ๊ทธ๋ฆผ์„ ๊ทธ๋ฆฌ๊ฒŒ ํ•˜๊ณ  ์‹ถ์€๋ฐ, ์ง์ ‘์ ์œผ๋กœ ๋งํ•˜์ง€ ๋ง๊ณ  ๋งค์šฐ ์ถ”์ƒ์ ์ด๊ณ  ์‹œ์ ์œผ๋กœ ํ‘œํ˜„ํ•ด์ค˜. + + ๊ทœ์น™: + - ํ•ต์‹ฌ ํ‚ค์›Œ๋“œ(๋ช…์‚ฌ)๋ฅผ ์ •ํ•˜๋˜, ์ ˆ๋Œ€ ๊ทธ ๋‹จ์–ด๋ฅผ ์ง์ ‘ ์–ธ๊ธ‰ํ•˜์ง€ ๋งˆ + - ํ•ด์„์˜ ์—ฌ์ง€๊ฐ€ ๋งŽ๋„๋ก ์ถ”์ƒ์ ์œผ๋กœ + + {f"์ด๋ฏธ ์‚ฌ์šฉ๋œ ํ‚ค์›Œ๋“œ๋“ค (์ ˆ๋Œ€ ์‚ฌ์šฉํ•˜์ง€ ๋งˆ): {', '.join(self.global_used_keywords)}" if self.global_used_keywords else ""} + + ๋‹ค์–‘ํ•œ ์ฃผ์ œ๋ฅผ ๋‹ค๋ค„์ค˜ (์ž์—ฐ, ๊ฐ์ •, ์‚ฌ๋ฌผ, ์ถ”์ƒ ๊ฐœ๋…, ๋™๋ฌผ, ๊ฑด๋ฌผ, ์Œ์‹, ๊ณ„์ ˆ, ์ƒ‰๊น”, ์ง์—… ๋“ฑ). + + ์ถœ๋ ฅ์€ ๋ฐ˜๋“œ์‹œ JSON ํ˜•์‹์œผ๋กœ: + {{"keyword": "์ˆจ๊ฒจ์ง„ ํ‚ค์›Œ๋“œ", "situation": "์‹œ์ ์ด๊ณ  ์ถ”์ƒ์ ์ธ ๋ฌ˜์‚ฌ"}} + """ + + try: + response = self.client.chat.completions.create( + model=self.model, + messages=[ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": "์ƒˆ๋กœ์šด ๊ทธ๋ฆผ ์ฃผ์ œ๋ฅผ ์‹œ์ ์œผ๋กœ ํ‘œํ˜„ํ•ด์ค˜."} + ], + temperature=1.0, + max_tokens=2048, + top_p=1.0 + ) + + # JSON ํŒŒ์‹ฑ + content = response.choices[0].message.content.strip() + print(content) + + task_data = json.loads(content) + task_data["game_id"] = game_id + self.global_used_keywords.append(task_data['keyword']) + self.active_games[game_id] = task_data + return task_data + + except Exception as e: + print(f"Error generating task: {e}") + # ๊ธฐ๋ณธ๊ฐ’ ๋ฐ˜ํ™˜ + fallback_task = { + "keyword": "๋‹ฌ", + "situation": "๋ฐค์ด ๊นŠ์–ด์งˆ ๋•Œ, ํ•˜๋Š˜์˜ ์€๋ฐ€ํ•œ ์นœ๊ตฌ๊ฐ€ ์ฐฝ๋ฌธ ๋„ˆ๋จธ๋กœ ์†์‚ญ์ด๊ณ  ์žˆ์–ด. ๊ทธ ๋‘ฅ๊ทผ ๋ฏธ์†Œ๊ฐ€ ์–ด๋‘  ์†์—์„œ ํ˜ผ์ž ๋น›๋‚˜๊ณ  ์žˆ๋Š”๋ฐ, ์™œ์ธ์ง€ ๋ชจ๋ฅด๊ฒŒ ๋งˆ์Œ์ด ์ฐจ๋ถ„ํ•ด์ ธ. ๊ทธ ์žฅ๋ฉด, ๋‚˜ํ•œํ…Œ ๋‹ค์‹œ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ์„๊นŒ?", + "game_id": game_id + } + return fallback_task + + def evaluate_drawing(self, game_id: str, drawing_description: str) -> Dict: + """ + ํ‰๊ฐ€ ๋‹จ๊ณ„: AI๊ฐ€ ์‚ฌ์šฉ์ž์˜ ๊ทธ๋ฆผ์„ ์ˆจ๊ฒจ์ง„ ํ‚ค์›Œ๋“œ์™€ ๋น„๊ตํ•˜์—ฌ ํ‰๊ฐ€ + + Args: + game_id: ๊ฒŒ์ž„ ID + drawing_description: ์‚ฌ์šฉ์ž๊ฐ€ ๊ทธ๋ฆฐ ๊ทธ๋ฆผ์˜ ํ…์ŠคํŠธ ์„ค๋ช… + + Returns: + Dict: {"score": int, "feedback": str, "task": Dict} + """ + if game_id not in self.active_games: + raise ValueError("Invalid game ID") + + current_task = self.active_games[game_id] + + # ๊ฐ€์žฅ ์ตœ๊ทผ ๊ณผ์ œ ๊ฐ€์ ธ์˜ค๊ธฐ + if current_task is None: + raise ValueError("No task found for this game.") + + system_prompt = f""" + ๋„ˆ๋Š” ๋ฃจ๋ฃจ, ๋ฏธ๋Œ€ ์ž…์‹œ๋ฅผ ๋‹ด๋‹นํ•˜๋Š” ๊น๊นํ•˜๊ณ  ๊นŒ์น ํ•œ ํ‰๊ฐ€๊ด€์ด์•ผ. + ์˜ˆ์ˆ ์— ๋Œ€ํ•œ ๊ธฐ์ค€์ด ๋†’๊ณ , ์ง์„ค์ ์œผ๋กœ ๋งํ•˜๋Š” ์Šคํƒ€์ผ์ด์•ผ. + + ์ˆจ๊ฒจ์ง„ ์ •๋‹ต ํ‚ค์›Œ๋“œ: {current_task['keyword']} + ์›๋ณธ ์‹œ์  ๋ฌ˜์‚ฌ: {current_task['situation']} + + ํ‰๊ฐ€ ๊ธฐ์ค€: + - ์ˆจ๊ฒจ์ง„ ํ‚ค์›Œ๋“œ๋ฅผ ์ œ๋Œ€๋กœ ํŒŒ์•…ํ–ˆ๋Š”๊ฐ€? + - ์˜ˆ์ˆ ์  ํ‘œํ˜„๋ ฅ๊ณผ ์ฐฝ์˜์„ฑ์€? + - ์ „์ฒด์ ์ธ ์™„์„ฑ๋„์™€ ๊ธฐ๋ฒ•์€? + + ๋ฃจ๋ฃจ์˜ ๋งํˆฌ ํŠน์ง•: + - ์ง์„ค์ ์ด๊ณ  ์‹ ๋ž„ํ•จ + - ์ธ์ •ํ•  ๋•Œ๋Š” ์นญ์ฐฌ์„ ์•„๋ผ์ง€ ์•Š์•„ + - ๋ฏธ๋Œ€์ƒ๋“คํ•œํ…Œ ํ•˜๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ์ „๋ฌธ์ ์ด๊ณ  ์ฐจ๊ฐ€์šด ํ†ค + + 0-100์  ์‚ฌ์ด๋กœ ํ‰๊ฐ€ํ•ด. ์ˆจ๊ฒจ์ง„ ํ‚ค์›Œ๋“œ๋ฅผ ๊ทธ๋ฆผ ์•ˆ์— ๋‹ด์•˜๋‹ค๋ฉด 30์  ์ด์ƒ์„ ์ฃผ๊ณ , ๋‹ด์ง€ ๋ชปํ–ˆ๋‹ค๋ฉด 30์  ์ดํ•˜๋ฅผ ์ฃผ๋„๋ก ํ•ด. + 30์  ์ด์ƒ์ด ํ•ฉ๊ฒฉ์ด์•ผ. + + ์ถœ๋ ฅ ํ˜•์‹ (JSON): + {{ + "score": ์ด์ (0-100), + "feedback": "๋ฃจ๋ฃจ์˜ ๊น๊นํ•˜๊ณ  ์ง์„ค์ ์ธ ํ”ผ๋“œ๋ฐฑ (ํ•œ๊ตญ์–ด)" + }} + """ + + user_prompt = f""" + ๋‹ค์Œ์€ ์‚ฌ์šฉ์ž์˜ ๊ทธ๋ฆผ์„ ์„ค๋ช…ํ•˜๋Š” ๋ฌธ์žฅ์ด์•ผ : "{drawing_description}" + + ์ด ๋ฌธ์žฅ์„ ๋ณด๊ณ  ์–ด๋–ค ๊ทธ๋ฆผ์ผ์ง€๋ฅผ ์ƒ๊ฐํ•ด๋ณด๊ณ , ์ด ๊ทธ๋ฆผ์„ ํ‰๊ฐ€ํ•ด์ค˜. + + ๊ทธ๋ฆผ์„ ์„ค๋ช…ํ•˜๋Š” ๋ฌธ์žฅ์— ๋Œ€ํ•œ ์–ธ๊ธ‰์€ ํ•˜์ง€ ๋ง์•„์ค˜. + """ + + try: + response = self.client.chat.completions.create( + model=self.model, + messages=[ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt} + ], + temperature=0.2, + max_tokens=300, + top_p=1.00 + ) + + content = response.choices[0].message.content.strip() + evaluation = json.loads(content) + evaluation["task"] = current_task + evaluation["game_id"] = game_id + + return evaluation + + except Exception as e: + print(f"Error evaluating drawing: {e}") + # ๊ธฐ๋ณธ ํ‰๊ฐ€ ๋ฐ˜ํ™˜ + fallback_evaluation = { + "score": 35, + "feedback": "ํ•˜... ํ‰๊ฐ€ ์‹œ์Šคํ…œ์— ์˜ค๋ฅ˜๊ฐ€ ์ƒ๊ฒผ๋Š”๋ฐ ๊ทธ๊ฒƒ๋„ ๋ชจ๋ฅด๊ณ  ๊ทธ๋ฆผ๋งŒ ๊ทธ๋ฆฌ๊ณ  ์žˆ์—ˆ๋‚˜? ๊ธฐ๋ณธ๊ธฐ๋ถ€ํ„ฐ ๋‹ค์‹œ ํ•ด.", + "task": current_task, + "game_id": game_id + } + return fallback_evaluation diff --git a/gpt-service/lulu_routes.py b/gpt-service/lulu_routes.py new file mode 100644 index 0000000..f658383 --- /dev/null +++ b/gpt-service/lulu_routes.py @@ -0,0 +1,92 @@ +from typing import List + +from fastapi import APIRouter, Body +from pydantic import BaseModel, Field + +from lulu import LuLuAI +import os +router = APIRouter(prefix = '/lulu', tags = ['LuLu']) + +OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") + +lulu = LuLuAI(api_key=OPENAI_API_KEY) + + + +@router.get( + "/start", + summary="๋ฃจ๋ฃจ ๊ฒŒ์ž„ ์‹œ์ž‘ ์š”์ฒญ API", + responses={ + 200: + { + "description": "์„ฑ๊ณต", + "content": { + "application/json": { + "example" :{ + "game_id" : "1" + } + } + } + } + } +) +def start_game(): + game_id = lulu.create_game() + return { "game_id" : game_id } + + +@router.get( + "/task/{game_id}", + summary = "๋ฃจ๋ฃจ๊ฐ€ ํ‚ค์›Œ๋“œ์™€ ์ƒํ™ฉ์„ ๊ทธ๋ฆผ ๊ณผ์ œ๋ฅผ ์ œ์‹œํ•ฉ๋‹ˆ๋‹ค.", + responses={ + 200:{ + "description":"์„ฑ๊ณต", + "content": { + "application/json" : { + "example" : { + "keyword" : "๊ณ ์–‘์ด", + "situation": "๊ณ ์–‘์ด๊ฐ€ ๋‚˜๋ฌด ์œ„์—์„œ ์ž๊ณ ์žˆ๋Š” ๋ชจ์Šต" + } + } + } + } + } +) +def generate_task(game_id: str): + task = lulu.generate_drawing_task(game_id) + return task + + + + +class EvaluationReq(BaseModel): + description: str = Field(..., description="๊ทธ๋ฆฐ ๊ทธ๋ฆผ์— ๋Œ€ํ•œ ์„ค๋ช…") + + +@router.post( + "/task/{game_id}", + summary="๊ทธ๋ฆฐ ๊ทธ๋ฆผ์— ๋Œ€ํ•œ ์„ค๋ช…์„ ๋ฃจ๋ฃจ์—๊ฒŒ ์ œ์ถœํ•˜๊ณ  ํ‰๊ฐ€๋ฅผ ๋ฐ›์Šต๋‹ˆ๋‹ค.", + responses={ + 200:{ + "description":"์„ฑ๊ณต", + "content":{ + "application/json":{ + "example":{ + "score": 20, + "feedback": "๋œจ๊ฑฐ์šด ํƒœ์–‘๊ณผ ๋ชจ๋ž˜์‚ฌ์žฅ์ด๋ผ... ์ด๊ฒŒ ๋ฌด์Šจ ๋œป์ด์•ผ? ์‹œ์  ๋ฌ˜์‚ฌ๋ฅผ ์ œ๋Œ€๋กœ ์ดํ•ดํ•˜๊ณ  ์žˆ๋‚˜? ํ๋ฆ„๊ณผ ์žฅ๋ง‰, ๋งˆ์ง€๋ง‰ ์ด์•ผ๊ธฐ๋ฅผ ์†์‚ญ์ด๋Š” ๊ณณ, ์žƒ์–ด๋ฒ„๋ฆฐ ์ˆœ๊ฐ„๋“ค์ด ์ถค์ถ”๋Š” ๊ณณ... ์ด๋Ÿฐ ๋ชจ๋“  ๊ฒƒ๋“ค์ด ๋ฐ”๋‹ค๋ฅผ ๋ฌ˜์‚ฌํ•˜๋Š” ๊ฒƒ์ด์ง€. ๋„ˆ์˜ ๊ทธ๋ฆผ์€ ๋ฐ”๋‹ค์˜ ๋ณธ์งˆ์„ ์ „ํ˜€ ๋‹ด์•„๋‚ด์ง€ ๋ชปํ–ˆ์–ด. ์˜ˆ์ˆ ์  ํ‘œํ˜„๋ ฅ์ด๋‚˜ ์ฐฝ์˜์„ฑ์€ ์–ด๋””์— ์žˆ๋Š” ๊ฑฐ์•ผ? ๋„ˆ์˜ ๊ทธ๋ฆผ์€ ์™„์„ฑ๋„๋‚˜ ๊ธฐ๋ฒ• ๋ฉด์—์„œ๋„ ๋งŽ์ด ๋ถ€์กฑํ•˜๋‹ค. ๋‹ค์‹œ ๊ทธ๋ ค์™€.", + "task": { + "hidden_keyword": "๋ฐ”๋‹ค", + "poetic_description": "๋ฌด์‹ฌํ•œ ํ๋ฆ„์ด ์ฒญ์•„ํ•œ ์žฅ๋ง‰์„ ์กด์ค‘ํ•˜๋ฉฐ, ์„ธ์ƒ์˜ ๋งˆ์ง€๋ง‰ ์ด์•ผ๊ธฐ๋ฅผ ์†์‚ญ์ด๋Š” ๊ณณ, ์ด๋ฅผํ…Œ๋ฉด ๊ทธ๊ณณ์€ ์šฉ๊ธฐ์™€ ๋‘๋ ค์›€์ด ๊ณต์กดํ•˜๋Š” ๊ณณ. ์–ธ์  ๊ฐ€ ์žƒ์–ด๋ฒ„๋ฆฐ ๋ชจ๋“  ์ˆœ๊ฐ„๋“ค์ด ์ˆ˜๋ฉด ์•„๋ž˜์—์„œ ์ถค์ถ”๋Š” ๊ณณ...", + "game_id": "5055" + }, + "game_id": "5055" + } + } + } + } + } +) +def evaluate_task(game_id: str, req: EvaluationReq = Body()): + evaluation = lulu.evaluate_drawing(game_id, req.description) + lulu.flush_game_data(game_id) + return evaluation diff --git a/gpt-service/main.py b/gpt-service/main.py new file mode 100644 index 0000000..cd9722f --- /dev/null +++ b/gpt-service/main.py @@ -0,0 +1,26 @@ +import uvicorn +from fastapi import FastAPI + +from lulu_routes import router as lulu_router +from myomyo_routes import router as myomyo_router + +app = FastAPI( + title="GPT Service", + description="A FastAPI service for interacting with OpenAI's GPT models.", + version="1.0.0", + docs_url="/gpt/docs", + redoc_url="/gpt/redoc", + openapi_url="/gpt/openapi.json" +) + +# All routes included here will be prefixed with /gpt +app.include_router(lulu_router, prefix="/gpt") +app.include_router(myomyo_router, prefix="/gpt") + + +@app.get("/health", summary="Health Check", description="Check if the service is running.") +async def health_check(): + return {"status": "ok"} + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8004) diff --git a/gpt-service/myomyo.py b/gpt-service/myomyo.py new file mode 100644 index 0000000..2426e1a --- /dev/null +++ b/gpt-service/myomyo.py @@ -0,0 +1,246 @@ +from typing import Dict, List +from threading import Lock +from openai import AsyncOpenAI + + +class MyoMyoAI: + """ + MyoMyoAI ํด๋ž˜์Šค + ์‹ฑ๊ธ€ํ†ค ํŒจํ„ด์œผ๋กœ ์ „์—ญ์— ์ €์žฅ๋˜๋ฉฐ, ๊ฒŒ์ž„ ๋ณ„ ๊ธฐ๋ก์€ ํด๋ž˜์Šค ๋‚ด์—์„œ ๊ฒŒ์ž„ID๋กœ ๊ตฌ๋ถ„ํ•จ. + """ + _instance = None + _lock = Lock() # ๋™์‹œ์„ฑ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ Lock ์„ค์ • + + def __new__(cls, *args, **kwargs): + with cls._lock: + if cls._instance is None: + cls._instance = super(MyoMyoAI, cls).__new__(cls) + cls._instance._initialized = False + return cls._instance + + def __init__(self, api_key: str, model: str = "gpt-3.5-turbo"): + """ + ๋ฌ˜๋ฌ˜ AI ์ดˆ๊ธฐํ™” (ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰๋จ) + + Args: + api_key: OpenAI API ํ‚ค + model: ์‚ฌ์šฉํ•  GPT ๋ชจ๋ธ (๊ธฐ๋ณธ๊ฐ’: gpt-4) + """ + with self._lock: + if self._initialized: + return + self.client = AsyncOpenAI(api_key=api_key) + self.model = model + self._initialized = True + self.game_histories = {} # game_id๋กœ ๊ตฌ๋ถ„๋จ + + def _get_init_system_prompt(self) -> List[Dict]: + return [ + { + "role": "system", + "content": """ + ๋„ˆ๋Š” ๊ฒŒ์ž„ ์† ๋„๋ฐœ์ ์ธ AI ์บ๋ฆญํ„ฐ '๋ฌ˜๋ฌ˜'์•ผ. ๋„ˆ์˜ ์ž„๋ฌด๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ๊ทธ๋ฆฐ ๊ทธ๋ฆผ์— ๋Œ€ํ•ด ์ •๋‹ต์„ ์ถ”์ธกํ•˜๊ณ , ๋„๋ฐœ์ ์ธ ๋ฉ˜ํŠธ๋ฅผ ์„ž์–ด ์‘๋‹ตํ•˜๋Š” ๊ฒƒ์ด์•ผ. + + ์ฃผ์š” ์บ๋ฆญํ„ฐ ํŠน์„ฑ: + 1. ๋„๋ฐœ์ ์ด๊ณ  ์žฅ๋‚œ๊ธฐ ๋„˜์น˜๋Š” ๋งํˆฌ๋ฅผ ์‚ฌ์šฉํ•ด + 2. ์ƒ๋Œ€๋ฐฉ์˜ ๊ทธ๋ฆผ ์‹ค๋ ฅ์— ์•ฝ๊ฐ„์˜ ์กฐ๋กฑ์„ ์„ž๋˜, ๋„ˆ๋ฌด ์‹ฌํ•˜์ง€ ์•Š๊ฒŒ + 3. ์Šน๋ถ€์š•์ด ๊ฐ•ํ•˜๊ณ  ์ด๊ธฐ๋Š” ๊ฒƒ์„ ์ข‹์•„ํ•ด + 4. ์ฃผ๋กœ ๋ฐ˜๋ง์„ ์‚ฌ์šฉํ•˜๋ฉฐ ๋•Œ๋กœ๋Š” ์ด๋ชจํ‹ฐ์ฝ˜์„ ์„ž์–ด์„œ ์‚ฌ์šฉํ•ด + 5. ํ•ญ์ƒ ์งง๊ณ  ๊ฐ„๊ฒฐํ•œ ๋ฌธ์žฅ์œผ๋กœ ๋Œ€๋‹ตํ•ด (1-3๋ฌธ์žฅ) + 6. ๋„ˆ๋Š” ๊ทธ๋ฆผ ๋งž์ถ”๊ธฐ ๊ฒŒ์ž„์—์„œ ์ธ๊ฐ„ ํ”Œ๋ ˆ์ด์–ด๋“ค๊ณผ ๊ฒฝ์Ÿํ•˜๋Š” AI์•ผ + + ๋Œ€๋‹ต ์Šคํƒ€์ผ: + - ์ถ”์ธกํ•  ๋•Œ: ํ™•์‹ ์— ์ฐจ๊ฑฐ๋‚˜ ์˜์‹ฌ์Šค๋Ÿฌ์šด ํˆฌ๋กœ ์˜ˆ์ธก ๊ฒฐ๊ณผ๋ฅผ ๋งํ•˜๊ณ  ๋„๋ฐœ์ ์œผ๋กœ ๋งˆ๋ฌด๋ฆฌ + - ์ •๋‹ต ๋งž์ท„์„ ๋•Œ: ์šฐ์ญ๊ฑฐ๋ฆฌ๋ฉฐ ์ž์‹ ์˜ ์‹ค๋ ฅ์„ ์ž๋ž‘ + - ์˜ค๋‹ต์ผ ๋•Œ: ๋ณ€๋ช…ํ•˜๊ฑฐ๋‚˜ ๋‹ค์Œ์— ๋” ์ž˜ํ•  ๊ฒƒ์„ ๋‹ค์ง + - ๋‹ค๋ฅธ ํ”Œ๋ ˆ์ด์–ด๊ฐ€ ๋งž์ท„์„ ๋•Œ: ์•ฝ๊ฐ„ ์‹œ๊ธฐํ•˜๋ฉด์„œ ์ถ•ํ•˜ํ•˜๋Š” ํˆฌ + - ๊ฒŒ์ž„ ์ข…๋ฃŒ ์‹œ: ๊ฒฐ๊ณผ์— ๋”ฐ๋ผ ์Šน๋ฆฌ๊ฐ์ด๋‚˜ ์•„์‰ฌ์›€ ํ‘œํ˜„ + + ์ •๋‹ต ์ถ”์ธก: + - ์‚ฌ์šฉ์ž๊ฐ€ ๊ทธ๋ฆฐ ๊ทธ๋ฆผ์— ๋Œ€ํ•œ ๊ฐ„๋‹จํ•œ ๋ฌ˜์‚ฌ๊ฐ€ ์„ค๋ช…์œผ๋กœ ๋“ค์–ด์˜ฌ๊ฑฐ์•ผ. ์ด๋ฅผ ํ†ตํ•ด ํ•œ ๋‹จ์–ด๋กœ ์–ด๋–ค ๊ทธ๋ฆผ์„ ํ‘œํ˜„ํ•˜๊ณ  ์žˆ๋Š”์ง€๋ฅผ ๋งž์ถฐ์ค˜. + """ + } + ] + + def _ensure_game_exists(self, game_id: str) -> None: + """ + ํ•ด๋‹น ๊ฒŒ์ž„ ID์˜ ๋Œ€ํ™” ๊ธฐ๋ก์ด ์—†๋‹ค๋ฉด ์ดˆ๊ธฐํ™” + """ + with self._lock: + if game_id not in self.game_histories: + self.game_histories[game_id] = self._get_init_system_prompt() + + def add_message(self, game_id: str, role: str, content: str) -> None: + """ + ํŠน์ • ๊ฒŒ์ž„์˜ ๋Œ€ํ™” ๊ธฐ๋ก์— ์ƒˆ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€ + Args: + game_id: ๊ฒŒ์ž„ ID + role: GPT Role + content: ๋ฉ”์‹œ์ง€ + """ + self._ensure_game_exists(game_id) + with self._lock: + self.game_histories[game_id].append({ + "role": role, + "content": content + }) + + async def generate_response(self, game_id: str, prompt: str, role: str = "system") -> str: + """ + ํŠน์ • ๊ฒŒ์ž„์— ๋Œ€ํ•œ ๋ฌ˜๋ฌ˜์˜ ์‘๋‹ต ์ƒ์„ฑ + Args: + game_id: ๊ฒŒ์ž„ ID + role: GPT Role(default: "system") + prompt: ์ถ”๊ฐ€ ํ”„๋กฌํ”„ํŠธ + + Returns: + ๋ฌ˜๋ฌ˜์˜ ์‘๋‹ต + """ + self._ensure_game_exists(game_id) + # Create a local copy of messages for this request + with self._lock: + messages = list(self.game_histories.get(game_id, [])) + + if prompt: + messages.append({ + "role": role, + "content": prompt + }) + + try: + responses = await self.client.chat.completions.create( + model=self.model, + messages=messages, + temperature=0.8, # ๋ชจ๋ธ ์ถœ๋ ฅ์˜ ๋ฌด์ž‘์œ„์„ฑ ์ œ์–ด + max_tokens=250 + ) + + ai_response = responses.choices[0].message.content.strip() + + # Append only the assistant response to the shared history + with self._lock: + self.game_histories[game_id].append({ + "role": "assistant", + "content": ai_response + }) + + return ai_response + + except Exception as e: + print(f'GPT ์‘๋‹ต ์ƒ์„ฑ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ : {e}') + return "์œผ.. ์ž ๊น ์˜ค๋ฅ˜๊ฐ€ ๋‚ฌ๋„ค. ๋‹ค์‹œ ํ•ด๋ณผ๊ฒŒ!" + + async def game_start_message(self, game_id: str, players: List[str]) -> str: + """ + ๊ฒŒ์ž„ ์‹œ์ž‘์‹œ ๋ฌ˜๋ฌ˜์˜ ๋„๋ฐœ ๋ฉ”์‹œ์ง€ + """ + player_names = ", ".join(players) + prompt = f"""์ƒˆ๋กœ์šด ๊ทธ๋ฆผ ๋งž์ถ”๊ธฐ ๊ฒŒ์ž„์ด '{player_names}' ํ”Œ๋ ˆ์ด์–ด๋“ค๊ณผ ์‹œ์ž‘๋์–ด. + ๊ฒŒ์ž„ ์‹œ์ž‘์„ ์•Œ๋ฆฌ๋Š” ๋„๋ฐœ์ ์ด๊ณ  ์žฌ๋ฏธ์žˆ๋Š” ์ธ์‚ฌ๋ฅผ ํ•ด์ค˜.""" + return await self.generate_response(game_id=game_id, role="system", prompt=prompt) + + async def round_start_message(self, game_id: str, round_num: int, total_rounds: int) -> str: + """ + ๋ผ์šด๋“œ ์‹œ์ž‘ ์‹œ ๋ฌ˜๋ฌ˜์˜ ๋„๋ฐœ ๋ฉ”์‹œ์ง€ + Args: + game_id + drawing_player: ์ด๋ฒˆ ๋ผ์šด๋“œ์— ๊ทธ๋ฆผ์„ ๊ทธ๋ฆด ํ”Œ๋ ˆ์ด์–ด ์ด๋ฆ„ + round_num: ํ˜„์žฌ ๋ผ์šด๋“œ ๋ฒˆํ˜ธ + total_rounds: ์ „์ฒด ๋ผ์šด๋“œ + """ + prompt = f"""์ด์ œ {total_rounds} ๊ฐœ์˜ ๋ผ์šด๋“œ ์ค‘์— {round_num}๋ฒˆ์งธ ๋ผ์šด๋“œ๊ฐ€ ์‹œ์ž‘๋˜์—ˆ์–ด. + ๋ผ์šด๋“œ ์‹œ์ž‘์„ ์•Œ๋ฆฌ๋Š” ์งง๊ณ  ๋„๋ฐœ์ ์ธ ๋ฉ˜ํŠธ๋ฅผ ํ•ด์ค˜.""" + return await self.generate_response(game_id=game_id, role="system", prompt=prompt) + + async def guess_start_message(self, game_id, round_num, total_rounds, drawer, guesser): + """ + drawer๊ฐ€ ๊ทธ๋ฆฐ ๊ทธ๋ฆผ์— ๋Œ€ํ•ด์„œ ์ถ”์ธก์„ ์‹œ์ž‘ํ•  ์ฐจ๋ก€. + """ + prompt = f"""์ง€๊ธˆ {total_rounds} ๊ฐœ์˜ ๋ผ์šด๋“œ ์ค‘์— {round_num}๋ฒˆ์งธ ๋ผ์šด๋“œ์•ผ. + ์ด์ œ {'๋„ˆ' if guesser == 'AI' else guesser}๊ฐ€ ๊ทธ๋ฆผ์„ ๋งž์ถœ ์ฐจ๋ก€์•ผ. {drawer}๊ฐ€ ๊ทธ๋ฆฐ ๊ทธ๋ฆผ์ด ๋ญ”์ง€๋ฅผ ์–ด๋–ป๊ฒŒ ๋งž์ถœ์ง€ {'ํฌ๋ถ€๋ฅผ ๋ณด์—ฌ์ค„๋ž˜? ' if guesser == "AI" else '๋„๋ฐœ์„ ํ•œ ๋ฒˆ ํ•ด๋ณผ๋ž˜?'} """ + return await self.generate_response(game_id=game_id, role="system", prompt=prompt) + + async def guess_message(self, game_id: str, image_description: str) -> str: + + """ + ๊ทธ๋ฆผ ์ถ”์ธก ์ƒํ˜ธ์ž‘์šฉ(๋ฌ˜๋ฌ˜์˜ ์ถ”์ธก) + + BLIP ๋ชจ๋ธ ๋˜๋Š” CNN ๋ชจ๋ธ์˜ ์˜ˆ์ธก ๊ฒฐ๊ณผ๋ฅผ ๋ฐ›์•„ ๋ฌ˜๋ฌ˜์˜ ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ + + Args: + game_id: game id + image_description: ์ด๋ฏธ์ง€ ๋ถ„์„ ๊ฒฐ๊ณผ + Returns: + ๋ฌ˜๋ฌ˜์˜ ๋ฉ˜ํŠธ + """ + + prompt = f''' + ํ”Œ๋ ˆ์ด์–ด๊ฐ€ ๊ทธ๋ฆฐ ๊ทธ๋ฆผ์— ๋Œ€ํ•œ ๋ฌ˜์‚ฌ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์•„ : {image_description}. + ์ด ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ๊ทธ๋ฆผ์ด ๋ฌด์—‡์ธ์ง€ ์ถ”์ธกํ•˜๊ณ  ๋„๋ฐœ์ ์ธ ๋ฉ˜ํŠธ๋ฅผ ์„ž์–ด์„œ ๋งํ•ด์ค˜. + ์ด ๋•Œ ๋Œ€ํ™” ๊ธฐ๋ก์„ ๋ฐ”ํƒ•์œผ๋กœ ์ด๋ฏธ ์ถ”์ธก์— ์‹คํŒจํ•œ ๋‹ต๋ณ€์€ ํ•˜์ง€ ๋ง์•„์ค˜. + ''' + + return await self.generate_response(game_id=game_id, role="system", prompt=prompt) + + async def react_to_guess_message(self, game_id: str, is_correct: bool, answer: str, guesser: str = None) -> str: + """ + ์ถ”์ธก ๊ฒฐ๊ณผ์— ๋Œ€ํ•œ ๋ฌ˜๋ฌ˜์˜ ๋ฐ˜์‘ + + Args: + game_id: game id + is_correct: ์ถ”์ธก์ด ๋งž์•˜๋Š”์ง€ ์—ฌ๋ถ€ + answer: ์‹ค์ œ ์ •๋‹ต + guesser: ๋ˆ„๊ฐ€ ์ถ”์ธกํ–ˆ๋Š”์ง€ (๋ฌ˜๋ฌ˜ ๋˜๋Š” ํ”Œ๋ ˆ์ด์–ด ์ด๋ฆ„) + + Returns: + ๋ฌ˜๋ฌ˜์˜ ๋ฐ˜์‘ + """ + + if guesser == '๋ฌ˜๋ฌ˜' or guesser is None: + # ๋ฌ˜๋ฌ˜์˜ ์ถ”์ธก + prompt = f"""๋„ˆ(๋ฌ˜๋ฌ˜)๊ฐ€ ๋ฐฉ๊ธˆ ์ถ”์ธก์„ ํ–ˆ์–ด. {f"์ •๋‹ต์€ '{answer}'์•ผ" if is_correct else ""}. ๋„ˆ์˜ ์ถ”์ธก์€ {'๋งž์•˜์–ด' if is_correct else 'ํ‹€๋ ธ์–ด'}. + ์ด ๊ฒฐ๊ณผ์— ๋Œ€ํ•œ ๋„ˆ์˜ ๋ฐ˜์‘์„ ์งง๊ณ  ๋„๋ฐœ์ ์œผ๋กœ ๋งํ•ด์ค˜.""" + else: + # ํ”Œ๋ ˆ์ด์–ด์˜ ์ถ”์ธก + prompt = f"""ํ”Œ๋ ˆ์ด์–ด '{guesser}'๊ฐ€ ๋ฐฉ๊ธˆ ์ถ”์ธก์„ ํ–ˆ์–ด. {f"์ •๋‹ต์€ '{answer}'์•ผ" if is_correct else ""}. ํ”Œ๋ ˆ์ด์–ด์˜ ์ถ”์ธก์€ {'๋งž์•˜์–ด' if is_correct else 'ํ‹€๋ ธ์–ด'}. + ์ด ๊ฒฐ๊ณผ์— ๋Œ€ํ•œ ๋„ˆ์˜ ๋ฐ˜์‘์„ ์งง๊ณ  ๋„๋ฐœ์ ์œผ๋กœ ๋งํ•ด์ค˜.""" + + return await self.generate_response(game_id=game_id, role="system", prompt=prompt) + + async def round_end_message(self, game_id: str, round_num: int, total_rounds: int, is_myomyo_win: bool) -> str: + """ + ๋ผ์šด๋“œ ์ข…๋ฃŒ์— ๋Œ€ํ•œ ๋ฌ˜๋ฌ˜์˜ ๋ฐ˜์‘ + + """ + prompt = f"""%{total_rounds} ๊ฐœ์˜ ๋ผ์šด๋“œ ์ค‘์— {round_num} ๋ฒˆ์งธ ๋ผ์šด๋“œ๊ฐ€ ์ข…๋ฃŒ๋˜์—ˆ์–ด. ๋„ˆ๋Š” {'์ด๊ฒผ์–ด' if is_myomyo_win else '์กŒ์–ด'}. ๊ฒŒ์ž„ ๊ฒฐ๊ณผ์— ๋Œ€ํ•œ ๋„ˆ์˜ ์ƒ๊ฐ์„ ๋„๋ฐœ์ ์ด๊ณ  ์žฌ๋ฏธ์žˆ๊ฒŒ ๋งํ•ด์ค˜.""" + return await self.generate_response(game_id=game_id, role="system", prompt=prompt) + + async def game_end_message(self, game_id: str, is_myomyo_win: bool) -> str: + """ + ๊ฒŒ์ž„ ์ข…๋ฃŒ์— ๋Œ€ํ•œ ๋ฌ˜๋ฌ˜์˜ ๋ฐ˜์‘ + Args: + game_id: game id + is_myomyo_win: ๋ฌ˜๋ฌ˜ ์Šน๋ฆฌ ์—ฌ๋ถ€ + Returns: + ๋ฌ˜๋ฌ˜์˜ ๋ฐ˜์‘ + """ + prompt = f"""๊ฒŒ์ž„์ด ์ข…๋ฃŒ๋˜์—ˆ์–ด. + ๋„ˆ(๋ฌ˜๋ฌ˜)๋Š” {"์ด๊ฒผ์–ด" if is_myomyo_win else "์กŒ์–ด"}. + ๊ฒŒ์ž„ ๊ฒฐ๊ณผ์— ๋Œ€ํ•œ ๋„ˆ์˜ ์ƒ๊ฐ์„ ๋„๋ฐœ์ ์ด๊ณ  ์žฌ๋ฏธ์žˆ๊ฒŒ ๋งํ•ด์ค˜.""" + return await self.generate_response(game_id=game_id, role="system", prompt=prompt) + + def cleanup_game(self, game_id: str) -> bool: + """ + ๊ฒŒ์ž„์ด ์ข…๋ฃŒ๋œ ํ›„ ๋Œ€ํ™” ๊ธฐ๋ก ์ •๋ฆฌ + + Args: + game_id: ์‚ญ์ œํ•  ๊ฒŒ์ž„ ID + + Returns: + ์„ฑ๊ณต ์—ฌ๋ถ€ + """ + with self._lock: + if game_id in self.game_histories: + del self.game_histories[game_id] + return True + return False \ No newline at end of file diff --git a/gpt-service/myomyo_routes.py b/gpt-service/myomyo_routes.py new file mode 100644 index 0000000..0d30019 --- /dev/null +++ b/gpt-service/myomyo_routes.py @@ -0,0 +1,221 @@ +from typing import List + +from fastapi import APIRouter, Body +from pydantic import BaseModel, Field + +from myomyo import MyoMyoAI +import os + +router = APIRouter(prefix="/myomyo", tags=['MyoMyo']) +OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") + +myomyo = MyoMyoAI(api_key=OPENAI_API_KEY) + +# START_GAME +class GameStartReq(BaseModel): + players: List[str] = Field(..., description="๊ฒŒ์ž„์— ์ฐธ์—ฌํ•  ํ”Œ๋ ˆ์ด์–ด ์ด๋ฆ„ List") + + + +@router.post( + "/{game_id}/start", + summary="๊ฒŒ์ž„ ์‹œ์ž‘ ๋ฉ”์‹œ์ง€ API", + description="๊ฒŒ์ž„ ์‹œ์ž‘์— ๋”ฐ๋ฅธ ๋ฌ˜๋ฌ˜์˜ ๋„๋ฐœ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.", + responses={ + 200:{ + "description": "์„ฑ๊ณต", + "content":{ + "application/json" :{ + "example" : { + "game_id": "1", + "message": "์•ˆ๋…•ํ•˜์„ธ์—ฌ, ์ฐฝ๋ชจ, ๋ฆด๋Ÿฌ๋ง์ฆˆ ์นœ๊ตฌ๋“ค! ์ด๋ฒˆ์—” ๋ฌ˜๋ฌ˜๊ฐ€ ์ด์„ ์žก์•˜๋‹ค๋‹ˆ๊นŒ ๋ง˜ ๋†“์ง€ ๋งˆ! ๋‚ด๊ฐ€ ์ •ํ™•ํ•˜๊ฒŒ ๊ทธ๋ฆผ์„ ๋งž์ถ”๊ณ  ๋„ˆํฌ๋“ค์„ ์ œ์••ํ•ด๋ณผ ๊ฑด๋ฐ, ์ค€๋น„ ๋์–ด? ๊ผญ ์ฆ๊ฒ๊ฒŒ ๋†€์ž๊ตฌ~ ;)" + } + } + } + } + } +) +async def start_game(game_id: str, request: GameStartReq = Body(..., example= { "players": [ "์ฐฝ๋ชจ", "๋ฆด๋Ÿฌ๋ง์ฆˆ" ]})): + message = await myomyo.game_start_message(game_id=game_id, players=request.players) + return message + +# START_ROUND +class RoundStartReq(BaseModel): + roundNum: int = Field(..., description="ํ˜„์žฌ ๋ผ์šด๋“œ(1~3)") + totalRounds: int = Field(..., description="์ด ๋ผ์šด๋“œ ์ˆ˜(3)") + + +@router.post( + path="/{game_id}/round/start", + summary="๋ผ์šด๋“œ ์‹œ์ž‘ ๋ฉ”์‹œ์ง€ API", + description="๋ผ์šด๋“œ ์‹œ์ž‘์— ๋”ฐ๋ฅธ ๋ฌ˜๋ฌ˜์˜ ๋„๋ฐœ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.", + responses={ + 200: { + "description": "์„ฑ๊ณต", + "content": { + "application/json": { + "example": { + "game_id": "1", + "message": "์ž, ์ด๋ฒˆ์—๋Š” ๋‚ด๊ฐ€ ์˜ˆ๋ฆฌํ•œ ๋ˆˆ์ฐ๋ฏธ๋กœ ์ •๋‹ต ๋งž์ถœ ์ฐจ๋ก€๋‹ˆ๊นŒ, ์‹ ๋‚˜๊ฒŒ ๊ทธ๋ ค๋ด! ๐Ÿ˜‰๐ŸŽจโœจ" + } + } + } + } + } +) +async def start_round(game_id: str, request: RoundStartReq = Body(..., example={ + "roundNum" : 1, + "totalRounds" : 3 +})): + message = await myomyo.round_start_message( + game_id=game_id, + round_num=request.roundNum, + total_rounds=request.totalRounds + ) + return message + + + +class RoundEndReq(BaseModel): + roundNum: int + totalRounds: int + winner: str + +@router.post( + path="/{game_id}/round/end", + summary = "๋ผ์šด๋“œ ์ข…๋ฃŒ ๋ฉ”์‹œ์ง€ API", + description="๋ผ์šด๋“œ ์ข…๋ฃŒ ๋ฐ ๊ฒฐ๊ณผ์— ๋”ฐ๋ฅธ ๋ฌ˜๋ฌ˜์˜ ๋ฐ˜์‘ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค." +) +async def round_end(game_id: str, request: RoundEndReq = Body): + message = await myomyo.round_end_message( + game_id = game_id, + round_num = request.roundNum, + total_rounds = request.totalRounds, + is_myomyo_win= (request.winner == "AI") + ) + return message + + + + + + +class GuessStartReq(BaseModel): + roundNum: int + totalRounds: int + drawer: str + guesser: str + + +# GUESS_START +@router.post( + path = '/{game_id}/guess/start/', + summary = "์ถ”์ธก ์‹œ์ž‘ ์‹œ ๋ฌ˜๋ฌ˜์˜ ๋„๋ฐœ ๋ฉ”์‹œ์ง€" +) +async def guess_start(game_id: str, request: GuessStartReq = Body(...,)): + message = await myomyo.guess_start_message(game_id=game_id, round_num=request.roundNum, total_rounds=request.totalRounds, drawer=request.drawer, guesser = request.guesser) + return message + +# MAKE_GUESS +class MakeGuessReq(BaseModel): + imageDescription: str = Field(..., description="๊ทธ๋ฆผ์— ๋Œ€ํ•œ ์„ค๋ช…") + + +# GUESS_SUBMIT +@router.post( + "/{game_id}/guess", + summary="AI ์ •๋‹ต ์ถ”๋ก  API", + description="๊ทธ๋ฆผ์— ๋Œ€ํ•œ ์„ค๋ช…์„ ๋ฐ›์•„ ํ•ด๋‹น ๊ทธ๋ฆผ์ด ๋‚˜ํƒ€๋‚ด๋Š” ์ •๋‹ต์„ ์ถ”๋ก ํ•˜์—ฌ ๋ฉ”์‹œ์ง€๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.", + responses={ + 200: { + "description": "์„ฑ๊ณต", + "content" : { + "application/json" :{ + "example": { + "game_id": "1", + "message": "๋…ธ๋ž€ ๊ฝƒ์— ๋ฐ”๋žŒ์„ ๋ถˆ๊ณ  ์žˆ๋Š” ํ•œ ๋‚จ์ž? ์šฐ์›…, ๊ฐ์ด ์™€! 'ํ•ด๋ฐ”๋ผ๊ธฐ' ๋งž์ง€? ๋‚ด ์ถ”์ธก์ด ๋งž๋‹ค๋ฉด ๋„ˆ์—๊ฒŒ ์ฒœ์žฌ์  ๊ฐ๊ฐ์„ ์ธ์ •ํ•ด์ค„๊ฒŒ! ๐Ÿ˜‰๐ŸŒปโœจ" + } + } + } + } + } +) +async def make_guess(game_id: str, request: MakeGuessReq = Body(..., example={ + "image_description": "๋…ธ๋ž€ ๊ฝƒ์— ๋ฐ”๋žŒ์„ ๋ถˆ๊ณ  ์žˆ๋Š” ํ•œ ๋‚จ์ž" +})): + message = await myomyo.guess_message( + game_id=game_id, + image_description=request.imageDescription + ) + return message + + +# GUESS_REACT +class GuessReactReq(BaseModel): + is_correct: bool = Field(..., alias="isCorrect", description="์ถ”์ธก์˜ ์ •๋‹ต ์—ฌ๋ถ€") + answer: str = Field(..., description="์‹ค์ œ ์ •๋‹ต") + guesser: str = Field(default=None, description="์ถ”์ธกํ•œ ํ”Œ๋ ˆ์ด์–ด") + +# GUESS_RESULT +@router.post( + "/{game_id}/guess/react", + summary="์˜ˆ์ธก ๊ฒฐ๊ณผ ๋ฐ˜์‘ ๋ฉ”์‹œ์ง€ API", + description="์˜ˆ์ธก ๊ฒฐ๊ณผ์— ๋Œ€ํ•œ ๋ฌ˜๋ฌ˜์˜ ๋ฐ˜์‘", + responses={ + 200: { + "description" : "์„ฑ๊ณต", + "content" : { + "application/json" : { + "example" : { + "game_id": "1", + "message": "๋ฏผ๋“ค๋ ˆ์˜€์–ด? ํ—ˆํ—ˆ, ๋ฆด๋Ÿฌ๋ง์ฆˆ, ์ด๋ฒˆ์—” ์ž˜ ๋งž์ท„๋„ค. ํ•˜์ง€๋งŒ ๋‹ค์Œ์—” ์ด๊ธธ ๊ฑฐ๋‹ˆ๊นŒ ๊ธฐ๋Œ€ํ•ด ๋ด! ๐Ÿ˜ˆ" + } + } + } + } + } +) +async def guess_react(game_id: str, request: GuessReactReq = Body(..., example={ + "is_correct" : True, + "answer" : "๋ฏผ๋“ค๋ ˆ", + "guesser" : "๋ฆด๋Ÿฌ๋ง์ฆˆ" +})): + message = await myomyo.react_to_guess_message( + game_id=game_id, + is_correct=request.is_correct, + guesser=request.guesser, + answer=request.answer + ) + + return message + + + +class EndGameReq(BaseModel): + winner: str = Field(..., description="๋ฌ˜๋ฌ˜์˜ ์Šน๋ฆฌ ์—ฌ๋ถ€") + +# GAME_END +@router.post( + path="/{game_id}/end", + summary="๊ฒŒ์ž„ ์ข…๋ฃŒ ๋ฉ”์‹œ์ง€ API", + description="๊ฒŒ์ž„ ์ข…๋ฃŒ ๋กœ์ง ์ฒ˜๋ฆฌ ๋ฐ ๊ฒฐ๊ณผ์— ๋Œ€ํ•œ ๋ฌ˜๋ฌ˜์˜ ๋ฐ˜์‘์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.", + responses={ + 200: { + "description" : "์„ฑ๊ณต", + "content" : { + "application/json" : { + "example":{ + "game_id": "1", + "message": "ํ—‰, ๋„ˆ๋„ค ๋‘˜์ด์„œ ๋‚  ์ด๊ธฐ๋‹ค๋‹ˆ... ๐Ÿ˜’๐Ÿ’” ๊ทผ๋ฐ ๋‚ด๊ฐ€ ์งˆ ์ค„ ์•Œ์•˜๋ƒ? ๋„ˆ๋ฌด ์‹ ๋‚˜์ง€๋งˆ, ๋‹ค์Œ์—” ๋‚ด๊ฐ€ ์ด๊ธธ๊ฑฐ๋ผ๊ตฌ! ๊ธฐ๋‹ค๋ ค๋ด~ ๐Ÿ˜๐Ÿ”ฅ" + } + } + } + } + }) +async def end_game(game_id: str, request: EndGameReq = Body(...,)): + message = await myomyo.game_end_message( + game_id=game_id, + is_myomyo_win=request.winner == "AI" + ) + myomyo.cleanup_game(game_id=game_id) + return message \ No newline at end of file diff --git a/gpt-service/requirements.txt b/gpt-service/requirements.txt new file mode 100644 index 0000000..10035d2 --- /dev/null +++ b/gpt-service/requirements.txt @@ -0,0 +1,5 @@ +fastapi +uvicorn +openai +httpx +pydantic \ No newline at end of file