From f07ab6d8384a202dbdcb8694b6756a46edb83b08 Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Sat, 26 Apr 2025 22:07:35 +0900 Subject: [PATCH 01/72] [add]ci.yml --- .github/workflows/ci.yml | 84 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..35cc250 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,84 @@ +name: Build and Test FastAPI Backend + +on: + push: + branches: + - main + - develop + paths: + - 'backend/**' + - '.github/workflows/backend-build-test.yml' + pull_request: + branches: + - main + - develop + paths: + - 'backend/**' + workflow_dispatch: + +jobs: + build-and-test: + name: Build and Test + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: 'pip' + cache-dependency-path: 'backend/requirements.txt' + + - name: Install dependencies + working-directory: backend + run: | + python -m pip install --upgrade pip + pip install pytest flake8 + pip install -r requirements.txt + + - name: Lint with flake8 + working-directory: backend + run: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + + - name: Test with pytest + working-directory: backend + run: pytest || echo "No tests configured" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build API Docker image (local) + uses: docker/build-push-action@v5 + with: + context: ./backend/api + push: false + load: true + tags: saburo-api:local-test + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Build Migration Docker image (local) + uses: docker/build-push-action@v5 + with: + context: ./backend/migration + push: false + load: true + tags: saburo-migration:local-test + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Save commit info + if: github.event_name != 'pull_request' + run: | + mkdir -p /tmp/artifacts + echo "${{ github.sha }}" > /tmp/artifacts/commit-sha + + - name: Upload commit SHA + if: github.event_name != 'pull_request' + uses: actions/upload-artifact@v4 + with: + name: backend-commit-info + path: /tmp/artifacts/commit-sha + retention-days: 7 \ No newline at end of file From 920788d17fa18cef21e3aa3a9b4e51ecee40414e Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Sat, 26 Apr 2025 22:07:49 +0900 Subject: [PATCH 02/72] [add]cd.yml --- .github/workflows/cd.yml | 102 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 .github/workflows/cd.yml diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 0000000..d7c58b4 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,102 @@ +name: Deploy FastAPI to ECR and ECS + +on: + workflow_run: + workflows: ["Build and Test FastAPI Backend"] + types: + - completed + branches: + - main + workflow_dispatch: + inputs: + commit_sha: + description: 'Commit SHA to deploy' + required: false + type: string + +jobs: + deploy-to-ecr-ecs: + name: Deploy to ECR and ECS + runs-on: ubuntu-latest + if: ${{ (github.event_name == 'workflow_dispatch') || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'main') }} + environment: production + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download commit info + if: github.event.inputs.commit_sha == '' + uses: actions/download-artifact@v4 + with: + name: backend-commit-info + path: /tmp/artifacts + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} + + - name: Set commit SHA + id: set-sha + run: | + if [ -n "${{ github.event.inputs.commit_sha }}" ]; then + echo "commit_sha=${{ github.event.inputs.commit_sha }}" >> "$GITHUB_OUTPUT" + elif [ -f "/tmp/artifacts/commit-sha" ]; then + SHA=$(cat /tmp/artifacts/commit-sha) + echo "commit_sha=$SHA" >> "$GITHUB_OUTPUT" + else + echo "commit_sha=${{ github.sha }}" >> "$GITHUB_OUTPUT" + fi + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ap-northeast-1 + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push API image + uses: docker/build-push-action@v5 + with: + context: ./backend/api + push: true + tags: ${{ steps.login-ecr.outputs.registry }}/saburo-api:${{ steps.set-sha.outputs.commit_sha }},${{ steps.login-ecr.outputs.registry }}/saburo-api:latest + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Build and push Migration image + uses: docker/build-push-action@v5 + with: + context: ./backend/migration + push: true + tags: ${{ steps.login-ecr.outputs.registry }}/saburo-migration:${{ steps.set-sha.outputs.commit_sha }},${{ steps.login-ecr.outputs.registry }}/saburo-migration:latest + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Update ECS API task definition + id: task-def-api + uses: aws-actions/amazon-ecs-render-task-definition@v1 + with: + task-definition: backend/task-definitions/api.json + container-name: api + image: ${{ steps.login-ecr.outputs.registry }}/saburo-api:${{ steps.set-sha.outputs.commit_sha }} + + - name: Deploy ECS API task definition + uses: aws-actions/amazon-ecs-deploy-task-definition@v1 + with: + task-definition: ${{ steps.task-def-api.outputs.task-definition }} + service: saburo-api-service + cluster: saburo-cluster + wait-for-service-stability: true + + - name: Run DB Migration + run: | + aws ecs run-task \ + --cluster saburo-cluster \ + --task-definition saburo-migration \ + --launch-type FARGATE \ + --network-configuration "awsvpcConfiguration={subnets=[${{ secrets.ECS_SUBNETS }}],securityGroups=[${{ secrets.ECS_SECURITY_GROUPS }}],assignPublicIp=ENABLED}" \ No newline at end of file From 4c257c57c3b6936c06603be7880a88185390b66c Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Sat, 26 Apr 2025 23:39:05 +0900 Subject: [PATCH 03/72] [add]ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 35cc250..586914b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ on: - develop paths: - 'backend/**' - - '.github/workflows/backend-build-test.yml' + - '.github/workflows/ci.yml' pull_request: branches: - main From 53927a715363a4baba505d3c85b28edd6f99311f Mon Sep 17 00:00:00 2001 From: sasakifuruta Date: Sun, 27 Apr 2025 11:28:29 +0900 Subject: [PATCH 04/72] =?UTF-8?q?=E3=83=81=E3=83=A3=E3=83=83=E3=83=88?= =?UTF-8?q?=E3=81=AECRUD=E5=87=A6=E7=90=86=E4=BD=9C=E6=88=90=E3=81=A8?= =?UTF-8?q?=E5=B1=A5=E6=AD=B4=E8=A1=A8=E7=A4=BA=E7=94=A8schema=E8=BF=BD?= =?UTF-8?q?=E8=A8=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/schemas/session.py | 16 +++++++++++++ app/servises/message.py | 33 +++++++++++++++++++++++++ app/servises/session.py | 53 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+) create mode 100644 app/servises/message.py create mode 100644 app/servises/session.py diff --git a/app/schemas/session.py b/app/schemas/session.py index 218a986..96c7eea 100644 --- a/app/schemas/session.py +++ b/app/schemas/session.py @@ -1,3 +1,4 @@ +from fastapi import Query from enum import Enum from pydantic import BaseModel from datetime import datetime @@ -8,6 +9,12 @@ class CharacterMode(str, Enum): BIJYO = "bijyo" ANGER_MOM = "anger_mom" +class SessionQueryParams(BaseModel): + skip: int = 0 + limit: int = 20 + favorite_only: bool = False + keyword: Optional[str] = None + class SessionBase(BaseModel): user_id: int character_mode: CharacterMode @@ -20,5 +27,14 @@ class SessionResponse(SessionBase): created_at: datetime updated_at: datetime + class Config: + from_attributes = True + +class SessionSummaryResponse(BaseModel): + session_id: int + character_mode: CharacterMode + first_message: Optional[str] = "" + created_at: datetime + class Config: from_attributes = True \ No newline at end of file diff --git a/app/servises/message.py b/app/servises/message.py new file mode 100644 index 0000000..f58e1c6 --- /dev/null +++ b/app/servises/message.py @@ -0,0 +1,33 @@ +from fastapi import HTTPException +from sqlalchemy.orm import Session +from app.models.message import Message +from app.schemas.message import MessageCreate + + +def create_message(db: Session, message: MessageCreate) -> Message: + db_message = Message(**message.dict()) + db.add(db_message) + db.commit() + db.refresh(db_message) + return db_message + + +def get_message(db: Session, session_id: int) -> list[Message]: + return db.query(Message).filter(Message.session_id == session_id).order_by(Message.created_at).all() + + +def update_message(db: Session, message_id: int, content: str) -> Message: + db_message = db.query(Message).filter(Message.id == message_id).first() + if db_message is None: + raise HTTPException(status_code=404, detail="該当するメッセージが見つかりません") # もしくはreturn None + db_message.content = content + db.commit() + db.refresh(db_message) + return db_message + + +def delete_message(db: Session, message_id: int) -> None: + db_message = db.query(Message).filter(Message.id == message_id).first() + if db_message: + db.delete(db_message) + db.commit() diff --git a/app/servises/session.py b/app/servises/session.py new file mode 100644 index 0000000..8af3466 --- /dev/null +++ b/app/servises/session.py @@ -0,0 +1,53 @@ +from sqlalchemy.orm import Session +from app.models.session import Session as SessionModel +from app.models.message import Message +from app.schemas.session import SessionSummaryResponse + + +def get_sessions_with_first_message( + db: Session, + user_id: int, + skip: int = 0, + limit: int = 100, + favorite_only: bool = False, + keyword: str | None = None, + ): + """ + 指定したユーザーIDに紐づくセッション一覧を取得します。 + セッションごとに、最初のメッセージの内容と作成日時をまとめて返します。 + + Args: + db (Session): データベースセッション + user_id (int): ユーザーID + skip (int, optional): 取得開始位置(デフォルト0) + limit (int, optional): 取得件数(デフォルト100) + favorite_only (bool, optional): お気に入りメッセージが存在するセッションのみ取得する場合True(デフォルトFalse) + keyword (str | None, optional): メッセージ本文に含まれるキーワードでフィルタリングする場合に指定(デフォルトNone) + + Returns: + セッション情報(session_id、character_mode、first_message、created_at)をまとめたリスト + """ + query = db.query(SessionModel).filter(SessionModel.user_id == user_id) + + if favorite_only: + query = query.join(SessionModel.messages).join(Message.favorites).distinct() + + if keyword: + query = query.join(SessionModel.messages).filter(Message.content.ilike(f"%{keyword}%")).distinct() + + sessions = query.offset(skip).limit(limit).all() + result = [] + for session in sessions: + first_message = ( + db.query(Message) + .filter(Message.session_id == session.id) + .order_by(Message.created_at.asc()) + .first() + ) + result.append(SessionSummaryResponse( + session_id=session.id, + character_mode=session.character_mode, + first_message=first_message.content[:20] if first_message else "", + created_at=session.created_at, + )) + return result \ No newline at end of file From 10ddd687015251c43b38ecf7871b10ab7e6b05c1 Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Sun, 27 Apr 2025 17:19:13 +0900 Subject: [PATCH 05/72] =?UTF-8?q?[add]SECRET=5FKEY=20=E3=82=92=20.env=20?= =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=81=8B=E3=82=89=E8=AA=AD?= =?UTF-8?q?=E3=81=BF=E8=BE=BC=E3=82=80=E3=81=9F=E3=82=81=E3=81=AE=E5=87=A6?= =?UTF-8?q?=E7=90=86=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/core/config.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/core/config.py b/app/core/config.py index 45f142d..afd8306 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -1,5 +1,10 @@ from pydantic import BaseSettings +class Settings(BaseSettings): + SECRET_KEY: str + ALGORITHM: str = "HS256" + ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 + class Settings(BaseSettings): # OpenAI API key openai_api_key: str @@ -14,4 +19,6 @@ class Settings(BaseSettings): database_url: str class Config: - env_file = ".env" \ No newline at end of file + env_file = ".env" + +settings = Settings() \ No newline at end of file From e3485fc063a7e6820a063a666fd4ff01b7e6e1a9 Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Sun, 27 Apr 2025 17:20:20 +0900 Subject: [PATCH 06/72] =?UTF-8?q?[add]=E5=90=84Router=E3=82=92=E3=81=BE?= =?UTF-8?q?=E3=81=A8=E3=82=81=E3=82=8B=E5=87=A6=E7=90=86=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/api.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/api/api.py b/app/api/api.py index e69de29..0422408 100644 --- a/app/api/api.py +++ b/app/api/api.py @@ -0,0 +1,12 @@ +from fastapi import APIRouter +from app.api.endpoints import auth, user, session, message, emotion, favorite, generated_media + +api_router = APIRouter() + +api_router.include_router(auth.router, tags=["Auth"]) +api_router.include_router(user.router, tags=["User"]) +api_router.include_router(session.router, tags=["Session"]) +api_router.include_router(message.router, tags=["Message"]) +api_router.include_router(emotion.router, tags=["Emotion"]) +api_router.include_router(favorite.router, tags=["Favorite"]) +api_router.include_router(generated_media.router, tags=["GeneratedMedia"]) \ No newline at end of file From d2381bbf1ae0c8ec657c97dec8cff5dfa14bbef4 Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Sun, 27 Apr 2025 17:22:33 +0900 Subject: [PATCH 07/72] =?UTF-8?q?[add]=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC?= =?UTF-8?q?=E7=99=BB=E9=8C=B2=E3=80=81=E3=83=AD=E3=82=B0=E3=82=A4=E3=83=B3?= =?UTF-8?q?=E3=80=81=E3=83=AD=E3=82=B0=E3=82=A2=E3=82=A6=E3=83=88=E6=A9=9F?= =?UTF-8?q?=E8=83=BD=E3=81=AE=E3=82=A8=E3=83=B3=E3=83=89=E3=83=9D=E3=82=A4?= =?UTF-8?q?=E3=83=B3=E3=83=88=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/endpoints/auth.py | 38 ++++++++++++++++++++++++++++++++ app/api/endpoints/chat.py | 0 app/api/endpoints/media.py | 0 app/api/endpoints/personality.py | 0 app/api/endpoints/poem.py | 0 5 files changed, 38 insertions(+) create mode 100644 app/api/endpoints/auth.py delete mode 100644 app/api/endpoints/chat.py delete mode 100644 app/api/endpoints/media.py delete mode 100644 app/api/endpoints/personality.py delete mode 100644 app/api/endpoints/poem.py diff --git a/app/api/endpoints/auth.py b/app/api/endpoints/auth.py new file mode 100644 index 0000000..f743c13 --- /dev/null +++ b/app/api/endpoints/auth.py @@ -0,0 +1,38 @@ +from fastapi import APIRouter, HTTPException, Depends, status +from sqlalchemy.orm import Session +from app.schemas.user import UserRegister, UserLogin, TokenResponse +from app.models import User +from app.core.database import get_db +from app.utils.auth import hash_password, verify_password, create_access_token, get_current_user + +router = APIRouter() + +@router.post("/register") +async def register(user_data: UserRegister, db: Session = Depends(get_db)): + + if db.query(User).filter_by(email=user_data.email).first(): + raise HTTPException(status_code=400, detail="このメールアドレスは既に登録されています。") + + new_user = User( + email=user_data.email, + password_hash=hash_password(user_data.password) + ) + db.add(new_user) + db.commit() + db.refresh(new_user) + + token = create_access_token({"sub": str(new_user.id)}) + return {"token": token} + +@router.post("/login") +async def login(user_data: UserLogin, db: Session = Depends(get_db)): + user = db.query(User).filter_by(email=user_data.email).first() + if not user: + raise HTTPException(status_code=400, detail="メールアドレスまたはパスワードが違います。") + + if not verify_password(user_data.password, user.password_hash): + raise HTTPException(status_code=400, detail="メールアドレスまたはパスワードが違います。") + +@router.post("/logout") +async def logout(current_user: User = Depends(get_current_user)): + return {"message": "ログアウトしました。"} \ No newline at end of file diff --git a/app/api/endpoints/chat.py b/app/api/endpoints/chat.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/api/endpoints/media.py b/app/api/endpoints/media.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/api/endpoints/personality.py b/app/api/endpoints/personality.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/api/endpoints/poem.py b/app/api/endpoints/poem.py deleted file mode 100644 index e69de29..0000000 From 63f16ba62ce604fcd478d6cbe8259d88dd3b166f Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Sun, 27 Apr 2025 17:22:48 +0900 Subject: [PATCH 08/72] =?UTF-8?q?[add]=E6=84=9F=E6=83=85=E3=81=AE=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0=E3=80=81=E5=8F=96=E5=BE=97=E6=A9=9F=E8=83=BD=E3=81=AE?= =?UTF-8?q?=E3=82=A8=E3=83=B3=E3=83=89=E3=83=9D=E3=82=A4=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/endpoints/emotion.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/app/api/endpoints/emotion.py b/app/api/endpoints/emotion.py index e69de29..2848f96 100644 --- a/app/api/endpoints/emotion.py +++ b/app/api/endpoints/emotion.py @@ -0,0 +1,17 @@ +from fastapi import APIRouter, HTTPException, Depends +from sqlalchemy.orm import Session +from app.models import Emotion +from app.schemas.emotion import EmotionCreate, EmotionResponse +from app.core.database import get_db + +@router.get("/emotions") +async def get_emotions(db: Session = Depends(get_db)): + emotions = db.query(Emotion).all() + return emotions + +@router.post("/emotions") +async def create_emotions(emotion_data: EmotionCreate, db: Session = Depends(get_db)): + new_emotion = Emotion(emotion=emotion_data.emotion) + db.add(new_emotion) + db.commit() + return new_emotion \ No newline at end of file From 92f49e11c50ae4ece8ff9cb29f5e121555980250 Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Sun, 27 Apr 2025 17:23:05 +0900 Subject: [PATCH 09/72] =?UTF-8?q?[add]=E3=83=A1=E3=83=83=E3=82=BB=E3=83=BC?= =?UTF-8?q?=E3=82=B8=E3=81=AE=E3=81=8A=E6=B0=97=E3=81=AB=E5=85=A5=E3=82=8A?= =?UTF-8?q?=E7=99=BB=E9=8C=B2=E3=80=81=E8=A7=A3=E9=99=A4=E6=A9=9F=E8=83=BD?= =?UTF-8?q?=E3=81=AE=E3=82=A8=E3=83=B3=E3=83=89=E3=83=9D=E3=82=A4=E3=83=B3?= =?UTF-8?q?=E3=83=88=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/endpoints/favorite.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/app/api/endpoints/favorite.py b/app/api/endpoints/favorite.py index e69de29..f628bcc 100644 --- a/app/api/endpoints/favorite.py +++ b/app/api/endpoints/favorite.py @@ -0,0 +1,20 @@ +from fastapi import APIRouter, HTTPException, Depends +from sqlalchemy.orm import Session +from app.models import Favorite, message +from app.schemas.favorite import FavoriteCreate, FavoriteResponse +from app.core.database import get_db + +@router.post("/favorites") +async def create_favorite(favorite_data: FavoriteCreate, db: Session = Depends(get_db)): + message = db.query(Message).filter(Message.id == favorite_data.message_id).first() + if not message: + raise HTTPException(status_code=404, detail="お気に入り登録が見つかりません。") + favorite = Favorite(message_id=favorite_data.message_id, user_id=favorite_data.user_id) + db.add(favorite) + db.commit() + return favorite + +@router.get("/favorites") +async def get_favorites(user_id: int, db: Session = Depends(get_db)): + favorites = db.query(Favorite).filter(Favorite.user_id == user_id).all() + return favorites \ No newline at end of file From f46dd08fa54ee56623696388cb99fd58c62bcfac Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Sun, 27 Apr 2025 17:23:26 +0900 Subject: [PATCH 10/72] =?UTF-8?q?[add]=E7=94=9F=E6=88=90=E3=81=95=E3=82=8C?= =?UTF-8?q?=E3=81=9F=E3=83=A1=E3=83=87=E3=82=A3=E3=82=A2=E3=81=AE=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0=E3=80=81=E5=8F=96=E5=BE=97=E6=A9=9F=E8=83=BD=E3=81=AE?= =?UTF-8?q?=E3=82=A8=E3=83=B3=E3=83=89=E3=83=9D=E3=82=A4=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/endpoints/generated_media.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/app/api/endpoints/generated_media.py b/app/api/endpoints/generated_media.py index e69de29..ba9393a 100644 --- a/app/api/endpoints/generated_media.py +++ b/app/api/endpoints/generated_media.py @@ -0,0 +1,17 @@ +from fastapi import APIRouter, HTTPException, Depends +from sqlalchemy.orm import GeneratedMedia +from app.models import GeneratedMedia +from app.schemas.generated_media import GeneratedMediaCreate, GeneratedMediaResponse +from app.core.database import get_db + +@router.post("/generated-media") +async def create_generated_media(media_data: GeneratedMediaCreate, db: Session = Depends(get_db)): + new_media = GeneratedMedia( + message_id=media_data.message_id, + emotion_id=media_data.emotion_id, + media_url=media_data.media_url, + media_type=media_data.media_type + ) + db.add(new_media) + db.commit() + return new_media \ No newline at end of file From 0cf7c9320bdce4c6b9b8c86505d9d4f9e24b5818 Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Sun, 27 Apr 2025 17:24:34 +0900 Subject: [PATCH 11/72] =?UTF-8?q?[add]=E3=83=81=E3=83=A3=E3=83=83=E3=83=88?= =?UTF-8?q?=E3=82=BB=E3=83=83=E3=82=B7=E3=83=A7=E3=83=B3=E3=81=AE=E4=BD=9C?= =?UTF-8?q?=E6=88=90=E3=80=81=E5=8F=96=E5=BE=97=E3=80=81=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E3=80=81=E5=89=8A=E9=99=A4=E6=A9=9F=E8=83=BD=E3=81=AE=E3=82=A8?= =?UTF-8?q?=E3=83=B3=E3=83=89=E3=83=9D=E3=82=A4=E3=83=B3=E3=83=88=E3=82=92?= =?UTF-8?q?=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/endpoints/session.py | 38 ++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 app/api/endpoints/session.py diff --git a/app/api/endpoints/session.py b/app/api/endpoints/session.py new file mode 100644 index 0000000..de4d83e --- /dev/null +++ b/app/api/endpoints/session.py @@ -0,0 +1,38 @@ +from fastapi import APIRouter, HTTPException, Depends +from sqlalchemy.orm import Session +from app.models import Session as ChatSession, Message +from app.schemas.session import SessionCreate, SessionUpdate, SessionResponse +from app.core.database import get_db +from app.utils.auth import get_current_user + +@router.post("/sessions") +async def create_session(session_data: SessionCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): + new_session = ChatSession(chat_title=session_data.chat_title, user_id=current_user.id) + db.add(new_session) + db.commit() + return new_session + +@router.get("/sessions") +async def get_session(id: int, db: Session = Depends(get_db)): + session = db.query(ChatSession).filter(ChatSession.id == id).first() + if not session: + raise HTTPException(status_code=404, detail="セッションが見つかりません。") + retuen session + +@router.patch("/sessions") +async def update_session(id: int, session_data: SessionUpdate, db:Session = Depends(get_db) ): + session = db.query(ChatSession).filter(ChatSession.id == id).first() + if not session: + raise HTTPException(status_code=404, detail="セッションが見つかりません。") + session.chat_title = session_data.chat_title + db.commit() + return session + +@router.delete("/sessions") +async def delete_session(id: int, db: Session = Depends(get_db)): + session = db.query(ChatSession).filter(ChatSession.id == id).first() + if not session: + raise HTTPException(status_code=404, detail="セッションが見つかりません。") + db.delete(session) + db.commit() + raise {"message": "セッションを削除しました。"} From 7501ff18edac0300a568a6cc7813b9e6857f57ea Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Sun, 27 Apr 2025 17:25:00 +0900 Subject: [PATCH 12/72] =?UTF-8?q?[add]=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC?= =?UTF-8?q?=E6=83=85=E5=A0=B1=E3=81=AE=E5=8F=96=E5=BE=97=E3=80=81=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E3=80=81=E5=89=8A=E9=99=A4=E6=A9=9F=E8=83=BD=E3=81=AE?= =?UTF-8?q?=E3=82=A8=E3=83=B3=E3=83=89=E3=83=9D=E3=82=A4=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/endpoints/user.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/app/api/endpoints/user.py b/app/api/endpoints/user.py index e69de29..28a46a7 100644 --- a/app/api/endpoints/user.py +++ b/app/api/endpoints/user.py @@ -0,0 +1,23 @@ +from fastapi import APIRouter, HTTPException, Depends, status +from sqlalchemy.orm import Session +from app.models import User +from app.schemas.user import UserRegister, UserLogin, TokenResponse, UserResponse +from app.core.database import get_db +from app.utils.auth import create_access_token, verify_password + +@router.get("/user") +async def get_user_info(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): + return current_user + +@router.patch("/user") +async def updated_user_info(user_data: UserUpdate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): + current_user.email = user_data.email + current_user.user_name = user_data.user_name + db.commit() + return current_user + +@router.delete("/user") +async def delete_user(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): + db:delete(current_user) + db.commit() + return {"message": "アカウントを削除しました。"} \ No newline at end of file From 469a6f7b88caf18c2ca6d00f29809a0c8248b079 Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Sun, 27 Apr 2025 17:25:24 +0900 Subject: [PATCH 13/72] =?UTF-8?q?[add]=E3=83=81=E3=83=A3=E3=83=83=E3=83=88?= =?UTF-8?q?=E3=83=A1=E3=83=83=E3=82=BB=E3=83=BC=E3=82=B8=E3=81=AE=E4=BD=9C?= =?UTF-8?q?=E6=88=90=E3=80=81=E6=9B=B4=E6=96=B0=E3=80=81=E5=89=8A=E9=99=A4?= =?UTF-8?q?=E3=80=81=E5=B1=A5=E6=AD=B4=E5=8F=96=E5=BE=97=E6=A9=9F=E8=83=BD?= =?UTF-8?q?=E3=81=AE=E3=82=A8=E3=83=B3=E3=83=89=E3=83=9D=E3=82=A4=E3=83=B3?= =?UTF-8?q?=E3=83=88=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/endpoints/message.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 app/api/endpoints/message.py diff --git a/app/api/endpoints/message.py b/app/api/endpoints/message.py new file mode 100644 index 0000000..4ec39c3 --- /dev/null +++ b/app/api/endpoints/message.py @@ -0,0 +1,24 @@ +from fastapi import APIRouter, HTTPException, Depends +from sqlalchemy.orm import Session +from app.models import Message, Session as ChatSession +from app.schemas.message import MessageCreate, MessageUpdate, MessageResponse +from app.core.database import get_db + +@router.post("/sessions/{session_id}/messages") +async def create_message(session_id: int, message_data: MessageCreate, db: Session = Depends(get_db)): + session = db.query(ChatSession).filter(ChatSession.id == session_id).first() + if not session: + raise HTTPException(status_code=404, detail="メッセージが見つかりません。") + message = Message(content=message_data.content, session_id=session_id) + db.add(message) + db.commit() + return message + +@router.put("/sessions/{session_id}/messages/{message_id}") +async def update_message(session_id: int, message_id: int, message_data: MessageUpdate, db: Session = Depends(get_db)): + message = db.query(Message).filter(Message.id == message_id).first() + if not message: + raise HTTPException(status_code=404, detail="メッセージが見つかりません。") + message.content = message_data.content + db.commit() + return message From faaf65d0a1e7d1331717d7553024474a173ef0c5 Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Sun, 27 Apr 2025 17:27:29 +0900 Subject: [PATCH 14/72] =?UTF-8?q?[add]JWT=E8=AA=8D=E8=A8=BC=E3=83=88?= =?UTF-8?q?=E3=83=BC=E3=82=AF=E3=83=B3=E3=81=AE=E7=94=9F=E6=88=90=E3=80=81?= =?UTF-8?q?=E3=83=91=E3=82=B9=E3=83=AF=E3=83=BC=E3=83=89=E3=81=AE=E3=83=8F?= =?UTF-8?q?=E3=83=83=E3=82=B7=E3=83=A5=E5=8C=96=E3=81=8A=E3=82=88=E3=81=B3?= =?UTF-8?q?=E6=A4=9C=E8=A8=BC=E3=80=81=E8=AA=8D=E8=A8=BC=E3=83=A6=E3=83=BC?= =?UTF-8?q?=E3=82=B6=E3=83=BC=E3=81=AE=E5=8F=96=E5=BE=97=E5=87=A6=E7=90=86?= =?UTF-8?q?=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/utils/auth.py | 60 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 app/utils/auth.py diff --git a/app/utils/auth.py b/app/utils/auth.py new file mode 100644 index 0000000..f309520 --- /dev/null +++ b/app/utils/auth.py @@ -0,0 +1,60 @@ +from datetime import datetime, timedelta +from jose import jwt, JWTError +from passlib.context import CryptContext +from fastapi import HTTPException, Depends +from app.models.user import User +from app.core.database import get_db +from fastapi.security import OAuth2PasswordBearer +from app.core.config import settings + +# OAuth2のアクセストークンをリクエストヘッダから自動的に取得するための依存関係 +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +# OAuth2のトークン受け取り用 +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login") + +# 秘密鍵とアルゴリズムの設定 +SECRET_KEY = settings.SECRET_KEY +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 60 + +# パスワードのハッシュ化 +def hash_password(password: str) -> str: + return pwd_context.hash(password) + +# ハッシュ化されたパスワードと入力されたパスワードの一致確認 +def verify_password(plain_password: str, hashed_password: str) -> bool: + + return pwd_context.verify(plain_password, hashed_password) + +# JWT アクセストークンの作成 +def create_access_token(data: dict, expires_delta: timedelta = None) -> str: + + to_encode = data.copy() + expire = datetime.utcnow() + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + +# トークンから現在のユーザー情報を取得する +def get_current_user(db: Session = Depends(get_db), token: str = Depends(oauth2_scheme)) -> User: + + credentials_exception = HTTPException( + status_code=401, + detail="認証情報が無効です", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + # トークンをデコードしてユーザーIDを取得 + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + user_id: str = payload.get("sub") + if user_id is None: + raise credentials_exception + except JWTError: + raise credentials_exception + + # ユーザーをデータベースから取得 + user = db.query(User).filter(User.id == int(user_id)).first() + if user is None: + raise credentials_exception + return user From 73f456540f141b66acb650b48975eb026fdd0b01 Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Sun, 27 Apr 2025 17:27:40 +0900 Subject: [PATCH 15/72] =?UTF-8?q?[add]=E3=83=87=E3=83=BC=E3=82=BF=E3=83=99?= =?UTF-8?q?=E3=83=BC=E3=82=B9=E6=8E=A5=E7=B6=9A=E3=81=AE=E3=82=BB=E3=83=83?= =?UTF-8?q?=E3=82=B7=E3=83=A7=E3=83=B3=E7=AE=A1=E7=90=86=E3=82=92=E8=A1=8C?= =?UTF-8?q?=E3=81=86=E5=87=A6=E7=90=86=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/core/database.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 app/core/database.py diff --git a/app/core/database.py b/app/core/database.py new file mode 100644 index 0000000..3fb41ad --- /dev/null +++ b/app/core/database.py @@ -0,0 +1,16 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, declarative_base +from app.core.config import settings + +DATABASE_URL = settings.DATABASE_URL + +engine = create_engine(DATABASE_URL, echo=True, future=True) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +Base = declarative_base() + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() \ No newline at end of file From dc53a97985fd4cf3a11ec983b80d130d93ba9140 Mon Sep 17 00:00:00 2001 From: sasakifuruta Date: Sun, 27 Apr 2025 20:57:39 +0900 Subject: [PATCH 16/72] =?UTF-8?q?=E3=83=81=E3=83=A3=E3=83=83=E3=83=88?= =?UTF-8?q?=E5=B1=A5=E6=AD=B4=E3=80=80=E5=89=8A=E9=99=A4=E6=A9=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/schemas/session.py | 2 -- app/servises/message.py | 17 ++++++++++------- app/servises/session.py | 39 +++++++++++++++++++++++++++++++-------- 3 files changed, 41 insertions(+), 17 deletions(-) diff --git a/app/schemas/session.py b/app/schemas/session.py index 96c7eea..bd649b4 100644 --- a/app/schemas/session.py +++ b/app/schemas/session.py @@ -10,8 +10,6 @@ class CharacterMode(str, Enum): ANGER_MOM = "anger_mom" class SessionQueryParams(BaseModel): - skip: int = 0 - limit: int = 20 favorite_only: bool = False keyword: Optional[str] = None diff --git a/app/servises/message.py b/app/servises/message.py index f58e1c6..1936a55 100644 --- a/app/servises/message.py +++ b/app/servises/message.py @@ -1,7 +1,8 @@ from fastapi import HTTPException from sqlalchemy.orm import Session +from typing import List from app.models.message import Message -from app.schemas.message import MessageCreate +from app.schemas.message import MessageCreate, MessageResponse def create_message(db: Session, message: MessageCreate) -> Message: @@ -12,12 +13,13 @@ def create_message(db: Session, message: MessageCreate) -> Message: return db_message -def get_message(db: Session, session_id: int) -> list[Message]: - return db.query(Message).filter(Message.session_id == session_id).order_by(Message.created_at).all() +def get_message(db: Session, session_id: int) -> List[MessageResponse]: + messages = db.query(Message).filter(Message.session_id == session_id).order_by(Message.created_at).all() + return [MessageResponse.from_orm(message) for message in messages] def update_message(db: Session, message_id: int, content: str) -> Message: - db_message = db.query(Message).filter(Message.id == message_id).first() + db_message = db.query(Message).filter(Message.id == message_id, Message.is_users == True).first() if db_message is None: raise HTTPException(status_code=404, detail="該当するメッセージが見つかりません") # もしくはreturn None db_message.content = content @@ -28,6 +30,7 @@ def update_message(db: Session, message_id: int, content: str) -> Message: def delete_message(db: Session, message_id: int) -> None: db_message = db.query(Message).filter(Message.id == message_id).first() - if db_message: - db.delete(db_message) - db.commit() + if db_message is None: + raise HTTPException(status_code=404, detail="該当するメッセージが見つかりません") + db.delete(db_message) + db.commit() diff --git a/app/servises/session.py b/app/servises/session.py index 8af3466..a6f2019 100644 --- a/app/servises/session.py +++ b/app/servises/session.py @@ -7,8 +7,6 @@ def get_sessions_with_first_message( db: Session, user_id: int, - skip: int = 0, - limit: int = 100, favorite_only: bool = False, keyword: str | None = None, ): @@ -19,8 +17,6 @@ def get_sessions_with_first_message( Args: db (Session): データベースセッション user_id (int): ユーザーID - skip (int, optional): 取得開始位置(デフォルト0) - limit (int, optional): 取得件数(デフォルト100) favorite_only (bool, optional): お気に入りメッセージが存在するセッションのみ取得する場合True(デフォルトFalse) keyword (str | None, optional): メッセージ本文に含まれるキーワードでフィルタリングする場合に指定(デフォルトNone) @@ -29,13 +25,16 @@ def get_sessions_with_first_message( """ query = db.query(SessionModel).filter(SessionModel.user_id == user_id) + if favorite_only or keyword: + query = query.join(SessionModel.messages) + if favorite_only: - query = query.join(SessionModel.messages).join(Message.favorites).distinct() + query = query.join(Message.favorites) if keyword: - query = query.join(SessionModel.messages).filter(Message.content.ilike(f"%{keyword}%")).distinct() + query = query.filter(Message.content.ilike(f"%{keyword}%")) - sessions = query.offset(skip).limit(limit).all() + sessions = query.distinct().all() result = [] for session in sessions: first_message = ( @@ -50,4 +49,28 @@ def get_sessions_with_first_message( first_message=first_message.content[:20] if first_message else "", created_at=session.created_at, )) - return result \ No newline at end of file + return result + + +def delete_session(db: Session, session_id: int, user_id: int): + """指定したセッションとそのメッセージを削除します。 + + Args: + db (Session): データベースセッション + session_id (int): 削除対象のセッションID + user_id (int): ユーザーID(本人確認用) + """ + session = db.query(SessionModel).filter( + SessionModel.id == session_id, + SessionModel.user_id == user_id + ).first() + + if not session: + return False + + db.query(Message).filter(Message.session_id == session_id).delete() + + db.delete(session) + db.commit() + + return True \ No newline at end of file From a75d9521445321f6d451f79f6b0441029984b8b9 Mon Sep 17 00:00:00 2001 From: sasakifuruta Date: Sun, 27 Apr 2025 21:22:20 +0900 Subject: [PATCH 17/72] =?UTF-8?q?models=E5=A4=89=E6=9B=B4=20=E3=83=9E?= =?UTF-8?q?=E3=82=A4=E3=82=B0=E3=83=AC=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3?= =?UTF-8?q?=E3=81=97=E7=9B=B4=E3=81=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...l_migration.py => 957084faf6ba_initial.py} | 46 +++++++++++-------- app/models/__init__.py | 1 - app/models/favorite.py | 4 +- app/models/message.py | 1 - app/models/session.py | 1 + 5 files changed, 30 insertions(+), 23 deletions(-) rename app/migrations/versions/{4e316c0822e0_initial_migration.py => 957084faf6ba_initial.py} (85%) diff --git a/app/migrations/versions/4e316c0822e0_initial_migration.py b/app/migrations/versions/957084faf6ba_initial.py similarity index 85% rename from app/migrations/versions/4e316c0822e0_initial_migration.py rename to app/migrations/versions/957084faf6ba_initial.py index 9339022..39e21f3 100644 --- a/app/migrations/versions/4e316c0822e0_initial_migration.py +++ b/app/migrations/versions/957084faf6ba_initial.py @@ -1,8 +1,8 @@ -"""initial migration +"""initial -Revision ID: 4e316c0822e0 +Revision ID: 957084faf6ba Revises: -Create Date: 2025-04-23 15:13:35.197369 +Create Date: 2025-04-27 12:20:34.661477 """ from typing import Sequence, Union @@ -12,7 +12,7 @@ # revision identifiers, used by Alembic. -revision: str = '4e316c0822e0' +revision: str = '957084faf6ba' down_revision: Union[str, None] = None branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None @@ -21,14 +21,20 @@ def upgrade() -> None: """Upgrade schema.""" # ### commands auto generated by Alembic - please adjust! ### + op.create_table('emotions', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('emotion', sa.String(length=255), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_emotions_id'), 'emotions', ['id'], unique=False) op.create_table('users', sa.Column('user_id', sa.Integer(), nullable=False), sa.Column('password_hash', sa.String(length=255), nullable=False), sa.Column('email', sa.String(length=255), nullable=False), sa.Column('user_name', sa.String(length=255), nullable=False), - sa.Column('poem_id', sa.Integer(), nullable=True), sa.Column('is_admin', sa.Boolean(), nullable=True), - sa.ForeignKeyConstraint(['poem_id'], ['poems.id'], ), sa.PrimaryKeyConstraint('user_id') ) op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True) @@ -41,29 +47,29 @@ def upgrade() -> None: sa.PrimaryKeyConstraint('id') ) op.create_index(op.f('ix_sessions_id'), 'sessions', ['id'], unique=False) - op.create_table('messages', + op.create_table('favorites', sa.Column('id', sa.Integer(), nullable=False), sa.Column('session_id', sa.Integer(), nullable=True), - sa.Column('is_users', sa.Boolean(), nullable=True), - sa.Column('response_type', sa.Enum('praise', 'insult', name='responsetypeenum'), nullable=True), - sa.Column('content', sa.Text(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=True), sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), sa.ForeignKeyConstraint(['session_id'], ['sessions.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['users.user_id'], ), sa.PrimaryKeyConstraint('id') ) - op.create_index(op.f('ix_messages_id'), 'messages', ['id'], unique=False) - op.create_table('favorites', + op.create_index(op.f('ix_favorites_id'), 'favorites', ['id'], unique=False) + op.create_table('messages', sa.Column('id', sa.Integer(), nullable=False), - sa.Column('message_id', sa.Integer(), nullable=True), - sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('session_id', sa.Integer(), nullable=True), + sa.Column('is_users', sa.Boolean(), nullable=True), + sa.Column('response_type', sa.Enum('praise', 'insult', name='responsetypeenum'), nullable=True), + sa.Column('content', sa.Text(), nullable=False), sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), - sa.ForeignKeyConstraint(['message_id'], ['messages.id'], ), - sa.ForeignKeyConstraint(['user_id'], ['users.user_id'], ), + sa.ForeignKeyConstraint(['session_id'], ['sessions.id'], ), sa.PrimaryKeyConstraint('id') ) - op.create_index(op.f('ix_favorites_id'), 'favorites', ['id'], unique=False) + op.create_index(op.f('ix_messages_id'), 'messages', ['id'], unique=False) op.create_table('generated_media', sa.Column('id', sa.Integer(), nullable=False), sa.Column('message_id', sa.Integer(), nullable=True), @@ -88,13 +94,15 @@ def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.drop_index(op.f('ix_generated_media_id'), table_name='generated_media') op.drop_table('generated_media') - op.drop_index(op.f('ix_favorites_id'), table_name='favorites') - op.drop_table('favorites') op.drop_index(op.f('ix_messages_id'), table_name='messages') op.drop_table('messages') + op.drop_index(op.f('ix_favorites_id'), table_name='favorites') + op.drop_table('favorites') op.drop_index(op.f('ix_sessions_id'), table_name='sessions') op.drop_table('sessions') op.drop_index(op.f('ix_users_user_id'), table_name='users') op.drop_index(op.f('ix_users_email'), table_name='users') op.drop_table('users') + op.drop_index(op.f('ix_emotions_id'), table_name='emotions') + op.drop_table('emotions') # ### end Alembic commands ### diff --git a/app/models/__init__.py b/app/models/__init__.py index 3d4562c..992de54 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -1,6 +1,5 @@ from .base import Base from .user import User -from .poem import Poem from .session import Session from .message import Message from .favorite import Favorite diff --git a/app/models/favorite.py b/app/models/favorite.py index 29fb64f..0c3a680 100644 --- a/app/models/favorite.py +++ b/app/models/favorite.py @@ -7,8 +7,8 @@ class Favorite(Base, TimestampMixin): __tablename__ = "favorites" id = Column(Integer, primary_key=True, index=True) - message_id = Column(Integer, ForeignKey("messages.id")) + session_id = Column(Integer, ForeignKey("sessions.id")) user_id = Column(Integer, ForeignKey("users.user_id")) - message = relationship("Message", back_populates="favorites") + session = relationship("Session", back_populates="favorites") user = relationship("User", back_populates="favorites") diff --git a/app/models/message.py b/app/models/message.py index fa18fcf..23f89f7 100644 --- a/app/models/message.py +++ b/app/models/message.py @@ -18,5 +18,4 @@ class Message(Base, TimestampMixin): content = Column(Text, nullable=False) session = relationship("Session", back_populates="messages") - favorites = relationship("Favorite", back_populates="message") generated_media = relationship("GeneratedMedia", back_populates="message") \ No newline at end of file diff --git a/app/models/session.py b/app/models/session.py index e978ec9..580e621 100644 --- a/app/models/session.py +++ b/app/models/session.py @@ -18,3 +18,4 @@ class Session(Base): user = relationship("User", back_populates="sessions") messages = relationship("Message", back_populates="session") + favorites = relationship("Favorite", back_populates="session") From bff8aff23f3969038118add948f75c1ac629ef73 Mon Sep 17 00:00:00 2001 From: sasakifuruta Date: Mon, 28 Apr 2025 23:56:15 +0900 Subject: [PATCH 18/72] =?UTF-8?q?=E3=83=A1=E3=83=83=E3=82=BB=E3=83=BC?= =?UTF-8?q?=E3=82=B8=E3=81=AECRUD=E5=87=A6=E7=90=86=E5=8B=95=E4=BD=9C?= =?UTF-8?q?=E7=A2=BA=E8=AA=8DOK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/deps.py | 7 +++++++ app/api/endpoints/chat.py | 0 app/api/endpoints/message.py | 25 +++++++++++++++++++++++++ app/db/session.py | 23 +++++++++++++++++++++++ app/main.py | 9 +++++++++ app/{servises => services}/__init__.py | 0 app/{servises => services}/message.py | 4 ++-- app/{servises => services}/session.py | 6 +++--- 8 files changed, 69 insertions(+), 5 deletions(-) delete mode 100644 app/api/endpoints/chat.py create mode 100644 app/api/endpoints/message.py create mode 100644 app/db/session.py rename app/{servises => services}/__init__.py (100%) rename app/{servises => services}/message.py (92%) rename app/{servises => services}/session.py (94%) diff --git a/app/api/deps.py b/app/api/deps.py index e69de29..23f63b0 100644 --- a/app/api/deps.py +++ b/app/api/deps.py @@ -0,0 +1,7 @@ +from sqlalchemy.orm import Session +from app.db.session import get_db +from fastapi import Depends + +# データベースセッションを依存関係として注入する +def get_db_dependency(db: Session = Depends(get_db)): + return db diff --git a/app/api/endpoints/chat.py b/app/api/endpoints/chat.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/api/endpoints/message.py b/app/api/endpoints/message.py new file mode 100644 index 0000000..e729b20 --- /dev/null +++ b/app/api/endpoints/message.py @@ -0,0 +1,25 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from services.message import create_message, get_message, update_message, delete_message +from schemas.message import MessageCreate, MessageResponse +from db.session import get_db +from typing import List + +router = APIRouter() + +@router.post("/messages/", response_model=MessageResponse) +def create_message_endpoint(message: MessageCreate, db: Session = Depends(get_db)): + return create_message(db=db, message=message) + +@router.get("/messages/{session_id}", response_model=List[MessageResponse]) +def get_message_endpoint(session_id: int, db: Session = Depends(get_db)): + return get_message(db=db, session_id=session_id) + +@router.put("/messages/{message_id}", response_model=MessageResponse) +def update_message_endpoint(message_id: int, content: str, db: Session = Depends(get_db)): + return update_message(db=db, message_id=message_id, content=content) + +@router.delete("/messages/{message_id}", status_code=204) +def delete_message_endpoint(message_id: int, db: Session = Depends(get_db)): + delete_message(db=db, message_id=message_id) + return {"detail": "Message deleted"} diff --git a/app/db/session.py b/app/db/session.py new file mode 100644 index 0000000..14dcb10 --- /dev/null +++ b/app/db/session.py @@ -0,0 +1,23 @@ +# db/session.py +import os +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, Session +from fastapi import Depends +from dotenv import load_dotenv +from models import Base + + +DATABASE_URL = os.getenv("DATABASE_URL") + + +# MySQL用:connect_argsなしでエンジン作成 +engine = create_engine(DATABASE_URL) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +# データベースの接続を管理する関数 +def get_db() -> Session: + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/app/main.py b/app/main.py index 731166c..789ab6f 100644 --- a/app/main.py +++ b/app/main.py @@ -3,8 +3,10 @@ import mysql.connector import os + app = FastAPI() + # ✅ CORS設定(Next.jsからアクセス可能にする) app.add_middleware( CORSMiddleware, @@ -34,3 +36,10 @@ def db_status(): return {"db_status": "not connected"} except Exception as e: return {"db_status": "error", "details": str(e)} + + + + +# テスト +from api.endpoints import message +app.include_router(message.router) \ No newline at end of file diff --git a/app/servises/__init__.py b/app/services/__init__.py similarity index 100% rename from app/servises/__init__.py rename to app/services/__init__.py diff --git a/app/servises/message.py b/app/services/message.py similarity index 92% rename from app/servises/message.py rename to app/services/message.py index 1936a55..d24dfad 100644 --- a/app/servises/message.py +++ b/app/services/message.py @@ -1,8 +1,8 @@ from fastapi import HTTPException from sqlalchemy.orm import Session from typing import List -from app.models.message import Message -from app.schemas.message import MessageCreate, MessageResponse +from models.message import Message +from schemas.message import MessageCreate, MessageResponse def create_message(db: Session, message: MessageCreate) -> Message: diff --git a/app/servises/session.py b/app/services/session.py similarity index 94% rename from app/servises/session.py rename to app/services/session.py index a6f2019..71498e5 100644 --- a/app/servises/session.py +++ b/app/services/session.py @@ -1,7 +1,7 @@ from sqlalchemy.orm import Session -from app.models.session import Session as SessionModel -from app.models.message import Message -from app.schemas.session import SessionSummaryResponse +from models.session import Session as SessionModel +from models.message import Message +from schemas.session import SessionSummaryResponse def get_sessions_with_first_message( From 214d9de6aec507e6f415f63c38937d2d964bc7d4 Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Tue, 29 Apr 2025 02:36:53 +0900 Subject: [PATCH 19/72] =?UTF-8?q?[add]=E7=AE=A1=E7=90=86=E8=80=85=E6=A9=9F?= =?UTF-8?q?=E8=83=BD=E3=81=AE=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC=E7=AE=A1?= =?UTF-8?q?=E7=90=86=EF=BC=88=E5=89=8A=E9=99=A4=E3=83=BB=E5=87=8D=E7=B5=90?= =?UTF-8?q?=EF=BC=89=E3=82=84=E8=A8=AD=E5=AE=9A=E5=A4=89=E6=9B=B4=E3=81=AB?= =?UTF-8?q?=E5=AF=BE=E5=BF=9C=E3=81=97=E3=81=9FAPI=E3=82=A8=E3=83=B3?= =?UTF-8?q?=E3=83=89=E3=83=9D=E3=82=A4=E3=83=B3=E3=83=88=E3=82=92=E5=AE=9F?= =?UTF-8?q?=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/endpoints/admin.py | 104 +++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/app/api/endpoints/admin.py b/app/api/endpoints/admin.py index e69de29..0b5e686 100644 --- a/app/api/endpoints/admin.py +++ b/app/api/endpoints/admin.py @@ -0,0 +1,104 @@ +from fastapi import APIRouter, HTTPException, Depends +from sqlalchemy.orm import Session +from app.schemas.admin import AdminBase, AdminLogin, AdminResponse +from app.models import Admin, User, Session as ChatSession, Message +from app.core.database import get_db +from app.utils.auth import hash_password, verify_password, create_access_token + +@router.post("/admin/register/") +async def register_admin(admin_data: AdminBase, db: Session = Depends(get_db)): + if db.query(Admin).filter_by(email=admin_data.email).first(): + raise HTTPException(status_code=400, detail="このメールアドレスは既に登録されています。") + + new_admin = Admin( + email=admin_data.email, + password_hash=hash_password(admin_data.password), + user_name=admin_data.user_name, + pin_code=str(admin_data.pin_code) + ) + db.add(new_admin) + db.commit() + db.refresh(new_admin) + + token = create_access_token({"sub": str(new_admin.id)}) + return {"token": token} + +@router.post("/admin/login") +async def login_admin(admin_data: AdminLogin, db: Session = Depends(get_db)): + admin = db.query(Admin).filter_by(email=admin_data.email).first() + if not admin: + raise HTTPException(status_code=400, detail="メールアドレスまたはパスワードが違います。") + + if not verify_password(admin_data.password, admin.password_hash): + raise HTTPException(status_code=400, detail="メールアドレスまたはパスワードが違います。") + + if admin_data.pin_code != admin.pin_code: + raise HTTPException(status_code=400, detail="PINコードが正しくありません。") + + token = create_access_token({"sub": str(new_admin.id)}) + return {"token": token} + +@router.get("/admin_info") +async def get_admin_info(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): + return current_admin + +@router.get("/users") +async def get_users(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): + users = db.query(User).filter_by(is_active=True).all() + return users + +@router.delete("/users/{user_id}") +async def delete_user(user_id: int, db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): + user = db.query(User).filter_by(id=user_id).first() + if not user: + raise HTTPException(status_code=404, detail="ユーザーが見つかりません。") + + db.delete(user) + db.commit() + return {"message": "ユーザーが削除されました。"} + +# ユーザーアカウントを凍結する処理 +@router.patch("/users/{user_id}/deactivate") +async def deactivate_user(user_id: int, db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): + user = db.query(User).filter_by(id=user_id).first() + if not user: + raise HTTPException(status_code=404, detail="ユーザーが見つかりません。") + + user.is_active = False + db.commit() + return {"message": "ユーザーが無効化されました。"} + +# ユーザーアカウントを有効化する処理 +@router.patch("/users/{user_id}/activate") +async def activate_user(user_id: int, db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): + user = db.query(User).filter_by(id=user_id).first() + if not user: + raise HTTPException(status_code=404, detail="ユーザーが見つかりません。") + + user.is_active = True + db.commit() + return {"message": "ユーザーが有効化されました。"} + +@router.patch("/users/{user_id}") +async def update_user(user_id: int, user_data: UserBase, db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): + user = db.query(User).filter_by(id=user_id).first() + if not user: + raise HTTPException(status_code=404, detail="ユーザーが見つかりません。") + + user.user_name = user_data.user_name + user.email = user_data.email + + db.commit() + return {"message": "ユーザー情報が更新されました。"} + +@router.get("/messages") +async def get_messages(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): + messages = db.query(Message).all() + return messages + +@router.patch("/settings") +async def update_settings(new_message: str, db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): + settings = db.query(Settings).first() + settings.support_message = new_message + db.commit() + return {"message": "設定が更新されました。"} From 208b098fed4e03a3ee0c8c1b46bc4f25197e57ba Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Tue, 29 Apr 2025 02:40:53 +0900 Subject: [PATCH 20/72] =?UTF-8?q?[add]Admin=E3=83=A2=E3=83=87=E3=83=AB?= =?UTF-8?q?=E3=81=ABpin=5Fcode=E3=83=95=E3=82=A3=E3=83=BC=E3=83=AB?= =?UTF-8?q?=E3=83=89=E3=80=81is=5Factive=E3=83=95=E3=83=A9=E3=82=B0?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/admin.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 app/models/admin.py diff --git a/app/models/admin.py b/app/models/admin.py new file mode 100644 index 0000000..507cc18 --- /dev/null +++ b/app/models/admin.py @@ -0,0 +1,15 @@ +from sqlalchemy import Column, Integer, String, Boolean +from sqlalchemy.orm import relationship +from .base import Base, TimestampMixin + +class Admin(Base, TimestampMixin): + __tablename__ = "admins" + + id = Column(Integer, primary_key=True, index=True) + user_name = Column(String(255), unique=True, index=True, nullable=False) + email = Column(String(255), unique=True, index=True, nullable=False) + password_hash = Column(string(255), nullable=False) + pin_code = Column(String(6), nullable=False) + is_active = Column(Boolean, default=True) + + user = relationship("User", back_populates="admin") \ No newline at end of file From d4f6e995b8c73cd6f17411c2f335a205d49bbc60 Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Tue, 29 Apr 2025 02:41:31 +0900 Subject: [PATCH 21/72] =?UTF-8?q?[add]=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC?= =?UTF-8?q?=E3=81=AEis=5Factive=E3=83=95=E3=83=A9=E3=82=B0=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=E3=81=97=E3=80=81=E3=83=A6=E3=83=BC=E3=82=B6?= =?UTF-8?q?=E3=83=BC=E3=81=AE=E7=8A=B6=E6=85=8B=E7=AE=A1=E7=90=86=E6=A9=9F?= =?UTF-8?q?=E8=83=BD=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/user.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/models/user.py b/app/models/user.py index 719a865..c7bd302 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -13,7 +13,11 @@ class User(Base): password_hash = Column(String(255), nullable=False) email = Column(String(255), unique=True, index=True, nullable=False) user_name = Column(String(255), nullable=False) + is_active = Column(Boolean, default=True) is_admin = Column(Boolean, default=False) + admin_is = Column(Integer, ForeignKey("admin.id")) + + admin = relationship("Admin", back_populates="user") sessions = relationship("Session", back_populates="user") favorites = relationship("Favorite", back_populates="user") From 8cac3f6cc756f9df01c5fff40a5a1ab09895cdeb Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Tue, 29 Apr 2025 02:41:57 +0900 Subject: [PATCH 22/72] =?UTF-8?q?[add]admin=E3=83=AB=E3=83=BC=E3=82=BF?= =?UTF-8?q?=E3=83=BC=E3=82=92API=E3=81=AB=E8=BF=BD=E5=8A=A0=E3=81=97?= =?UTF-8?q?=E3=80=81=E7=AE=A1=E7=90=86=E8=80=85=E6=A9=9F=E8=83=BD=E7=94=A8?= =?UTF-8?q?=E3=81=AE=E3=82=A8=E3=83=B3=E3=83=89=E3=83=9D=E3=82=A4=E3=83=B3?= =?UTF-8?q?=E3=83=88=E3=82=92=E3=83=AB=E3=83=BC=E3=83=86=E3=82=A3=E3=83=B3?= =?UTF-8?q?=E3=82=B0=E3=81=AB=E7=B5=84=E3=81=BF=E8=BE=BC=E3=81=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/api/api.py b/app/api/api.py index 0422408..66589fa 100644 --- a/app/api/api.py +++ b/app/api/api.py @@ -1,9 +1,10 @@ from fastapi import APIRouter -from app.api.endpoints import auth, user, session, message, emotion, favorite, generated_media +from app.api.endpoints import auth, admin, user, session, message, emotion, favorite, generated_media api_router = APIRouter() api_router.include_router(auth.router, tags=["Auth"]) +api_router.include_router(admin.router, tags=["admin"]) api_router.include_router(user.router, tags=["User"]) api_router.include_router(session.router, tags=["Session"]) api_router.include_router(message.router, tags=["Message"]) From 9db523e17fa09463f374fb52f426ae9244ecf3a1 Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Tue, 29 Apr 2025 02:42:28 +0900 Subject: [PATCH 23/72] =?UTF-8?q?[add]=20=E7=AE=A1=E7=90=86=E8=80=85?= =?UTF-8?q?=E3=81=AE=E7=99=BB=E9=8C=B2=E3=83=BB=E3=83=AD=E3=82=B0=E3=82=A4?= =?UTF-8?q?=E3=83=B3=E6=99=82=E3=81=AB=E4=BD=BF=E7=94=A8=E3=81=99=E3=82=8B?= =?UTF-8?q?Pydantic=E3=82=B9=E3=82=AD=E3=83=BC=E3=83=9E=E3=82=92=E5=AE=9A?= =?UTF-8?q?=E7=BE=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/schemas/admin.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 app/schemas/admin.py diff --git a/app/schemas/admin.py b/app/schemas/admin.py new file mode 100644 index 0000000..a59e8cf --- /dev/null +++ b/app/schemas/admin.py @@ -0,0 +1,24 @@ +from pydantic import BaseModel +from datetime import datetime +from typing import Optional + +class AdminBase(BaseModel): + email: EmailStr + password: str + user_name: str + pin_code: int + +class AdminLogin(BaseModel): + email: EmailStr + password: str + pin_code: int + +class AdminResponse(BaseModel): + id: int + email: EmailStr + user_name: str + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True From bced6dde6567c1b6369008042c1a944dd94ea731 Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Tue, 29 Apr 2025 02:44:27 +0900 Subject: [PATCH 24/72] =?UTF-8?q?[add]=E8=AA=8D=E8=A8=BC=E9=96=A2=E9=80=A3?= =?UTF-8?q?=E3=81=AE=E3=83=AD=E3=82=B8=E3=83=83=E3=82=AF=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/utils/auth.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/app/utils/auth.py b/app/utils/auth.py index f309520..3fa411c 100644 --- a/app/utils/auth.py +++ b/app/utils/auth.py @@ -1,3 +1,5 @@ +import os +from dotenv import load_dotenv from datetime import datetime, timedelta from jose import jwt, JWTError from passlib.context import CryptContext @@ -7,27 +9,23 @@ from fastapi.security import OAuth2PasswordBearer from app.core.config import settings -# OAuth2のアクセストークンをリクエストヘッダから自動的に取得するための依存関係 +load_dotenv() + pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") -# OAuth2のトークン受け取り用 oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login") -# 秘密鍵とアルゴリズムの設定 SECRET_KEY = settings.SECRET_KEY ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 60 -# パスワードのハッシュ化 def hash_password(password: str) -> str: return pwd_context.hash(password) -# ハッシュ化されたパスワードと入力されたパスワードの一致確認 def verify_password(plain_password: str, hashed_password: str) -> bool: return pwd_context.verify(plain_password, hashed_password) -# JWT アクセストークンの作成 def create_access_token(data: dict, expires_delta: timedelta = None) -> str: to_encode = data.copy() @@ -36,7 +34,6 @@ def create_access_token(data: dict, expires_delta: timedelta = None) -> str: encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt -# トークンから現在のユーザー情報を取得する def get_current_user(db: Session = Depends(get_db), token: str = Depends(oauth2_scheme)) -> User: credentials_exception = HTTPException( @@ -45,7 +42,6 @@ def get_current_user(db: Session = Depends(get_db), token: str = Depends(oauth2_ headers={"WWW-Authenticate": "Bearer"}, ) try: - # トークンをデコードしてユーザーIDを取得 payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) user_id: str = payload.get("sub") if user_id is None: @@ -53,7 +49,6 @@ def get_current_user(db: Session = Depends(get_db), token: str = Depends(oauth2_ except JWTError: raise credentials_exception - # ユーザーをデータベースから取得 user = db.query(User).filter(User.id == int(user_id)).first() if user is None: raise credentials_exception From a9d4619b5ec6578b3bf419e59975437704ab86c2 Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Wed, 30 Apr 2025 22:45:07 +0900 Subject: [PATCH 25/72] =?UTF-8?q?[fix]=E3=81=8A=E6=B0=97=E3=81=AB=E5=85=A5?= =?UTF-8?q?=E3=82=8A=E7=99=BB=E9=8C=B2=E5=87=A6=E7=90=86=E3=82=92session?= =?UTF-8?q?=5Fid=E3=83=99=E3=83=BC=E3=82=B9=E3=81=AB=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/endpoints/favorite.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/api/endpoints/favorite.py b/app/api/endpoints/favorite.py index f628bcc..e24783e 100644 --- a/app/api/endpoints/favorite.py +++ b/app/api/endpoints/favorite.py @@ -1,15 +1,15 @@ from fastapi import APIRouter, HTTPException, Depends from sqlalchemy.orm import Session -from app.models import Favorite, message +from app.models import Favorite, Session as DBSession from app.schemas.favorite import FavoriteCreate, FavoriteResponse from app.core.database import get_db @router.post("/favorites") async def create_favorite(favorite_data: FavoriteCreate, db: Session = Depends(get_db)): - message = db.query(Message).filter(Message.id == favorite_data.message_id).first() - if not message: + session = db.query(DBSession).filter(DBSession.id == favorite_data.session_id).first() + if not session: raise HTTPException(status_code=404, detail="お気に入り登録が見つかりません。") - favorite = Favorite(message_id=favorite_data.message_id, user_id=favorite_data.user_id) + favorite = Favorite(session_id=favorite_data.session_id, user_id=favorite_data.user_id) db.add(favorite) db.commit() return favorite From 81ad95a49249d1a9529fdcf0ea5098e838059608 Mon Sep 17 00:00:00 2001 From: sasakifuruta Date: Sat, 3 May 2025 16:50:39 +0900 Subject: [PATCH 26/72] =?UTF-8?q?=E8=AA=8D=E8=A8=BC=E6=A9=9F=E8=83=BDAPI?= =?UTF-8?q?=E7=A2=BA=E8=AA=8D=E5=AE=8C=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 4 +- Makefile | 12 +- app/api/endpoints/auth.py | 35 ++++-- app/api/endpoints/message.py | 24 ++-- app/api/endpoints/session.py | 106 ++++++++++++++++-- app/api/endpoints/user.py | 87 +++++++++++--- app/core/config.py | 8 +- app/core/database.py | 9 +- app/main.py | 4 +- ...6ba_initial.py => c1621488556f_initial.py} | 20 ++-- app/models/admin.py | 15 --- app/models/favorite.py | 4 +- app/models/generated_media.py | 2 +- app/models/message.py | 4 +- app/models/session.py | 8 +- app/models/user.py | 10 +- app/requirements.txt | 5 + app/schemas/admin.py | 24 ---- app/schemas/session.py | 23 +++- app/schemas/user.py | 20 ++++ app/services/session.py | 10 +- app/utils/auth.py | 16 +-- docker-compose.yml | 11 +- 23 files changed, 325 insertions(+), 136 deletions(-) rename app/migrations/versions/{957084faf6ba_initial.py => c1621488556f_initial.py} (85%) delete mode 100644 app/models/admin.py delete mode 100644 app/schemas/admin.py diff --git a/Dockerfile b/Dockerfile index f823951..0eb2d27 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,9 @@ COPY app/requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY app/ . +ENV PYTHONPATH=/app EXPOSE 8000 -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] diff --git a/Makefile b/Makefile index 110e0ae..d4a9e30 100644 --- a/Makefile +++ b/Makefile @@ -11,23 +11,23 @@ logs: docker compose logs exec-app: - docker exec -it fastapi-app bash + docker compose exec -it web bash exec-db: - docker exec -it mysql-db bash + docker compose exec -it db bash down: docker compose down migrate: - docker compose exec fastapi-app sh -c "cd /app && alembic upgrade head" + docker compose exec -it web bash -c "cd /app && alembic upgrade head" makemigration: @read -p "Migration name: " name; \ - docker compose exec fastapi-app sh -c "cd /app && alembic revision --autogenerate -m $$name" + docker compose exec -it web bash -c "cd /app && alembic revision --autogenerate -m $$name" show: - docker compose exec fastapi-app sh -c "cd /app && alembic history" + docker compose exec -it web bash -c "cd /app && alembic history" downgrade: - docker compose exec fastapi-app sh -c "cd /app && alembic downgrade -1" \ No newline at end of file + docker compose exec -it web bash -c "cd /app && alembic downgrade -1" \ No newline at end of file diff --git a/app/api/endpoints/auth.py b/app/api/endpoints/auth.py index f743c13..7eac2c6 100644 --- a/app/api/endpoints/auth.py +++ b/app/api/endpoints/auth.py @@ -1,13 +1,17 @@ from fastapi import APIRouter, HTTPException, Depends, status from sqlalchemy.orm import Session -from app.schemas.user import UserRegister, UserLogin, TokenResponse -from app.models import User -from app.core.database import get_db -from app.utils.auth import hash_password, verify_password, create_access_token, get_current_user + +from core.config import settings +from schemas.user import UserRegister, UserLogin, TokenResponse +from models import User +from core.database import get_db +from utils.auth import hash_password, verify_password, create_access_token, get_current_user + router = APIRouter() -@router.post("/register") +# アカウント登録 +@router.post("/register", response_model=TokenResponse) async def register(user_data: UserRegister, db: Session = Depends(get_db)): if db.query(User).filter_by(email=user_data.email).first(): @@ -15,24 +19,35 @@ async def register(user_data: UserRegister, db: Session = Depends(get_db)): new_user = User( email=user_data.email, + user_name=user_data.user_name, password_hash=hash_password(user_data.password) ) db.add(new_user) db.commit() db.refresh(new_user) - token = create_access_token({"sub": str(new_user.id)}) + token = create_access_token({"sub": str(new_user.user_id)}) return {"token": token} -@router.post("/login") + +# ログイン +@router.post("/login", response_model=TokenResponse) async def login(user_data: UserLogin, db: Session = Depends(get_db)): user = db.query(User).filter_by(email=user_data.email).first() - if not user: + if not user or not verify_password(user_data.password, user.password_hash): raise HTTPException(status_code=400, detail="メールアドレスまたはパスワードが違います。") - if not verify_password(user_data.password, user.password_hash): - raise HTTPException(status_code=400, detail="メールアドレスまたはパスワードが違います。") + if user.is_admin: + if user_data.pin_code is None: + raise HTTPException(status_code=400, detail="PINコードが必要です。") + if user_data.pin_code != settings.admin_pin_code: + raise HTTPException(status_code=400, detail="PINコードが違います。") + + token = create_access_token({"sub": str(user.user_id)}) + return {"token": token} + +# ログアウト @router.post("/logout") async def logout(current_user: User = Depends(get_current_user)): return {"message": "ログアウトしました。"} \ No newline at end of file diff --git a/app/api/endpoints/message.py b/app/api/endpoints/message.py index 4ec39c3..5085da7 100644 --- a/app/api/endpoints/message.py +++ b/app/api/endpoints/message.py @@ -1,8 +1,10 @@ from fastapi import APIRouter, HTTPException, Depends from sqlalchemy.orm import Session -from app.models import Message, Session as ChatSession -from app.schemas.message import MessageCreate, MessageUpdate, MessageResponse -from app.core.database import get_db +from models import Message, Session as ChatSession +from schemas.message import MessageCreate +from core.database import get_db + +router = APIRouter() @router.post("/sessions/{session_id}/messages") async def create_message(session_id: int, message_data: MessageCreate, db: Session = Depends(get_db)): @@ -14,11 +16,11 @@ async def create_message(session_id: int, message_data: MessageCreate, db: Sessi db.commit() return message -@router.put("/sessions/{session_id}/messages/{message_id}") -async def update_message(session_id: int, message_id: int, message_data: MessageUpdate, db: Session = Depends(get_db)): - message = db.query(Message).filter(Message.id == message_id).first() - if not message: - raise HTTPException(status_code=404, detail="メッセージが見つかりません。") - message.content = message_data.content - db.commit() - return message +# @router.put("/sessions/{session_id}/messages/{message_id}") +# async def update_message(session_id: int, message_id: int, message_data: MessageUpdate, db: Session = Depends(get_db)): +# message = db.query(Message).filter(Message.id == message_id).first() +# if not message: +# raise HTTPException(status_code=404, detail="メッセージが見つかりません。") +# message.content = message_data.content +# db.commit() +# return message diff --git a/app/api/endpoints/session.py b/app/api/endpoints/session.py index de4d83e..1feef8e 100644 --- a/app/api/endpoints/session.py +++ b/app/api/endpoints/session.py @@ -1,10 +1,15 @@ from fastapi import APIRouter, HTTPException, Depends from sqlalchemy.orm import Session -from app.models import Session as ChatSession, Message -from app.schemas.session import SessionCreate, SessionUpdate, SessionResponse +from app.models import Session as ChatSession +from app.models import Favorite, User +from app.schemas.session import SessionCreate, SessionUpdate, SessionResponse, SessionWithMessagesResponse, SessionSummaryResponse from app.core.database import get_db from app.utils.auth import get_current_user +from service.session import get_sessions_with_first_message +from typing import Optional + +# TODO::チャットの開始 @router.post("/sessions") async def create_session(session_data: SessionCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): new_session = ChatSession(chat_title=session_data.chat_title, user_id=current_user.id) @@ -12,27 +17,106 @@ async def create_session(session_data: SessionCreate, db: Session = Depends(get_ db.commit() return new_session -@router.get("/sessions") + +# チャット一覧取得 +@router.get("/api/sessions", response_model=list[SessionSummaryResponse]) +async def get_sessions( + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), + favorite_only: bool =False, + keyword: Optional[str] = None + ): + return get_sessions_with_first_message( + db = db, + user_id = current_user.user_id, + favorite_only = favorite_only, + keyword = keyword + ) + + +# 特定のチャットを取得 +@router.get("/api/sessions/{id}", response_model=SessionWithMessagesResponse) async def get_session(id: int, db: Session = Depends(get_db)): session = db.query(ChatSession).filter(ChatSession.id == id).first() if not session: raise HTTPException(status_code=404, detail="セッションが見つかりません。") - retuen session + return { + "chat_id" : session.id, + "messages" : [ + { + "message_id": m.id, + "message_text": m.content, + "sender_type": "user" if m.is_users else "ai" + }for m in session.messages + ], + "created_at": session.created_at, + "updated_at": session.updated_at, + } -@router.patch("/sessions") -async def update_session(id: int, session_data: SessionUpdate, db:Session = Depends(get_db) ): - session = db.query(ChatSession).filter(ChatSession.id == id).first() + +# 特定のチャットを変更 +@router.patch("/api/sessions/{id}", response_model= SessionResponse) +async def update_session( + id: int, + session_data: SessionUpdate, + db:Session = Depends(get_db) + ): + # TODO::tokenからuser_idを取得 + + session = db.query(Session).filter(ChatSession.id == id).first() if not session: - raise HTTPException(status_code=404, detail="セッションが見つかりません。") - session.chat_title = session_data.chat_title + raise HTTPException(status_code=404, detail="チャットが見つかりません") + + # 更新 + if session_data.character_mode: + session.character_mode = session_data.character_mode + db.commit() + db.refresh(session) return session -@router.delete("/sessions") + +# 特定のチャットを削除 +@router.delete("/api/sessions/{id}") async def delete_session(id: int, db: Session = Depends(get_db)): session = db.query(ChatSession).filter(ChatSession.id == id).first() if not session: raise HTTPException(status_code=404, detail="セッションが見つかりません。") db.delete(session) db.commit() - raise {"message": "セッションを削除しました。"} + raise {"message": "チャットを削除しました。"} + + +# お気に入りのトグル +@router.post("/api/favorites") +def toggle_favorite( + session_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + + # セッションの存在を確認 + session = db.query(ChatSession).filter(ChatSession.id == session_id).first() + if not session: + if not session: + raise HTTPException(status_code=404, detail= "") + + # お気に入りの状態チェック + favorite = db.query(Favorite).filter_by( + session_id = session_id, + user_id = current_user.id + ).first() + + if favorite: + db.delete(favorite) + db.commit() + return{"message": "お気に入りを解除しました", "is_favorite": False} + else: + new_fav = Favorite( + session_id = session_id, + user_id = current_user.id + ) + db.add(new_fav) + db.commit() + return {"message": "お気に入りを追加しました", "is_favorite": True} + diff --git a/app/api/endpoints/user.py b/app/api/endpoints/user.py index 28a46a7..8e29815 100644 --- a/app/api/endpoints/user.py +++ b/app/api/endpoints/user.py @@ -1,23 +1,82 @@ from fastapi import APIRouter, HTTPException, Depends, status from sqlalchemy.orm import Session -from app.models import User -from app.schemas.user import UserRegister, UserLogin, TokenResponse, UserResponse -from app.core.database import get_db -from app.utils.auth import create_access_token, verify_password +from models import User +from schemas.user import UserUpdate, PasswordUpdate, UserResponse +from core.database import get_db +from utils.auth import hash_password, verify_password, get_current_user +from utils.timestamp import now_jst -@router.get("/user") -async def get_user_info(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): + +router = APIRouter() + +# アカウント情報取得 +@router.get("/api/user", response_model=UserResponse) +async def get_my_account(current_user: User = Depends(get_current_user)): return current_user -@router.patch("/user") -async def updated_user_info(user_data: UserUpdate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): - current_user.email = user_data.email - current_user.user_name = user_data.user_name + +# アカウント情報の更新 +@router.patch("/api/user", response_model=UserResponse) +async def update_my_account( + user_update: UserUpdate, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + # メールアドレスの更新 + if user_update.email and user_update.email != current_user.email: + # 他のユーザと重複していないかチェック + if db.query(User).filter_by(email=user_update.email).first(): + raise HTTPException(status_code=400, detail="このメールアドレスはすでに使われています") + current_user.email = user_update.email + + if user_update.user_name: + current_user.user_name = user_update.user_name + db.commit() + db.refresh(current_user) return current_user -@router.delete("/user") -async def delete_user(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): - db:delete(current_user) + +# パスワード変更 +@router.put("/api/password") +async def change_password( + password_data: PasswordUpdate, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + if not verify_password(password_data.current_password, current_user.password_hash): + raise HTTPException(status_code=400, detail="現在のパスワードが間違っています") + + current_user.password_hash = hash_password(password_data.new_password) + db.commit() + + return {"message": "パスワードを変更しました"} + + +# アカウント凍結 +@router.delete("/api/user") +async def deactivate_account( + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + if current_user.is_admin: + raise HTTPException(status_code=403, detail="管理者アカウントは削除できません") + + current_user.is_active =False + current_user.deleted_at = now_jst() + db.commit() + return {"message": "アカウントを削除しました"} + + +# アカウント削除 +@router.delete("/api/user/delete") +async def delete_account( + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + if current_user.is_admin: + raise HTTPException(status_code=403, detail="管理者アカウントは削除できません") + + db.delete(current_user) db.commit() - return {"message": "アカウントを削除しました。"} \ No newline at end of file + return {"message": "アカウントを完全に削除しました"} \ No newline at end of file diff --git a/app/core/config.py b/app/core/config.py index afd8306..3cff794 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -1,15 +1,14 @@ -from pydantic import BaseSettings +from pydantic_settings import BaseSettings class Settings(BaseSettings): SECRET_KEY: str ALGORITHM: str = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 -class Settings(BaseSettings): # OpenAI API key - openai_api_key: str + # openai_api_key: str # OpenAI Model - openai_model: str ="gpt-4.1-nano" + # openai_model: str ="gpt-4.1-nano" # Database mysql_root_password: str @@ -17,6 +16,7 @@ class Settings(BaseSettings): mysql_user: str mysql_password: str database_url: str + admin_pin_code: int class Config: env_file = ".env" diff --git a/app/core/database.py b/app/core/database.py index 3fb41ad..74eef76 100644 --- a/app/core/database.py +++ b/app/core/database.py @@ -1,8 +1,13 @@ +from dotenv import load_dotenv +import os from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker, declarative_base -from app.core.config import settings +from core.config import settings -DATABASE_URL = settings.DATABASE_URL +# .envを読み込む +load_dotenv() + +DATABASE_URL = os.getenv("DATABASE_URL") engine = create_engine(DATABASE_URL, echo=True, future=True) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) diff --git a/app/main.py b/app/main.py index 789ab6f..23e740c 100644 --- a/app/main.py +++ b/app/main.py @@ -2,6 +2,7 @@ from fastapi.middleware.cors import CORSMiddleware import mysql.connector import os +from api.endpoints.auth import router as auth_router app = FastAPI() @@ -37,7 +38,8 @@ def db_status(): except Exception as e: return {"db_status": "error", "details": str(e)} - +# 認証用エンドポイント +app.include_router(auth_router, prefix="/api", tags=["auth"]) # テスト diff --git a/app/migrations/versions/957084faf6ba_initial.py b/app/migrations/versions/c1621488556f_initial.py similarity index 85% rename from app/migrations/versions/957084faf6ba_initial.py rename to app/migrations/versions/c1621488556f_initial.py index 39e21f3..3a19ac3 100644 --- a/app/migrations/versions/957084faf6ba_initial.py +++ b/app/migrations/versions/c1621488556f_initial.py @@ -1,8 +1,8 @@ """initial -Revision ID: 957084faf6ba +Revision ID: c1621488556f Revises: -Create Date: 2025-04-27 12:20:34.661477 +Create Date: 2025-05-03 07:44:52.951958 """ from typing import Sequence, Union @@ -12,7 +12,7 @@ # revision identifiers, used by Alembic. -revision: str = '957084faf6ba' +revision: str = 'c1621488556f' down_revision: Union[str, None] = None branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None @@ -34,6 +34,8 @@ def upgrade() -> None: sa.Column('password_hash', sa.String(length=255), nullable=False), sa.Column('email', sa.String(length=255), nullable=False), sa.Column('user_name', sa.String(length=255), nullable=False), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('deleted_at', sa.DateTime(), nullable=True), sa.Column('is_admin', sa.Boolean(), nullable=True), sa.PrimaryKeyConstraint('user_id') ) @@ -43,7 +45,9 @@ def upgrade() -> None: sa.Column('id', sa.Integer(), nullable=False), sa.Column('user_id', sa.Integer(), nullable=True), sa.Column('character_mode', sa.Enum('saburo', 'bijyo', 'anger_mom', name='charactermodeenum'), nullable=False), - sa.ForeignKeyConstraint(['user_id'], ['users.user_id'], ), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.user_id'], ondelete='CASCADE'), sa.PrimaryKeyConstraint('id') ) op.create_index(op.f('ix_sessions_id'), 'sessions', ['id'], unique=False) @@ -53,8 +57,8 @@ def upgrade() -> None: sa.Column('user_id', sa.Integer(), nullable=True), sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), - sa.ForeignKeyConstraint(['session_id'], ['sessions.id'], ), - sa.ForeignKeyConstraint(['user_id'], ['users.user_id'], ), + sa.ForeignKeyConstraint(['session_id'], ['sessions.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['users.user_id'], ondelete='CASCADE'), sa.PrimaryKeyConstraint('id') ) op.create_index(op.f('ix_favorites_id'), 'favorites', ['id'], unique=False) @@ -66,7 +70,7 @@ def upgrade() -> None: sa.Column('content', sa.Text(), nullable=False), sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), - sa.ForeignKeyConstraint(['session_id'], ['sessions.id'], ), + sa.ForeignKeyConstraint(['session_id'], ['sessions.id'], ondelete='CASCADE'), sa.PrimaryKeyConstraint('id') ) op.create_index(op.f('ix_messages_id'), 'messages', ['id'], unique=False) @@ -82,7 +86,7 @@ def upgrade() -> None: sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), sa.ForeignKeyConstraint(['emotion_id'], ['emotions.id'], ), - sa.ForeignKeyConstraint(['message_id'], ['messages.id'], ), + sa.ForeignKeyConstraint(['message_id'], ['messages.id'], ondelete='CASCADE'), sa.PrimaryKeyConstraint('id') ) op.create_index(op.f('ix_generated_media_id'), 'generated_media', ['id'], unique=False) diff --git a/app/models/admin.py b/app/models/admin.py deleted file mode 100644 index 507cc18..0000000 --- a/app/models/admin.py +++ /dev/null @@ -1,15 +0,0 @@ -from sqlalchemy import Column, Integer, String, Boolean -from sqlalchemy.orm import relationship -from .base import Base, TimestampMixin - -class Admin(Base, TimestampMixin): - __tablename__ = "admins" - - id = Column(Integer, primary_key=True, index=True) - user_name = Column(String(255), unique=True, index=True, nullable=False) - email = Column(String(255), unique=True, index=True, nullable=False) - password_hash = Column(string(255), nullable=False) - pin_code = Column(String(6), nullable=False) - is_active = Column(Boolean, default=True) - - user = relationship("User", back_populates="admin") \ No newline at end of file diff --git a/app/models/favorite.py b/app/models/favorite.py index 0c3a680..abb678e 100644 --- a/app/models/favorite.py +++ b/app/models/favorite.py @@ -7,8 +7,8 @@ class Favorite(Base, TimestampMixin): __tablename__ = "favorites" id = Column(Integer, primary_key=True, index=True) - session_id = Column(Integer, ForeignKey("sessions.id")) - user_id = Column(Integer, ForeignKey("users.user_id")) + session_id = Column(Integer, ForeignKey("sessions.id", ondelete="CASCADE")) + user_id = Column(Integer, ForeignKey("users.user_id", ondelete="CASCADE")) session = relationship("Session", back_populates="favorites") user = relationship("User", back_populates="favorites") diff --git a/app/models/generated_media.py b/app/models/generated_media.py index f5986cf..51fc505 100644 --- a/app/models/generated_media.py +++ b/app/models/generated_media.py @@ -11,7 +11,7 @@ class GeneratedMedia(Base, TimestampMixin): __tablename__ = "generated_media" id = Column(Integer, primary_key=True, index=True) - message_id = Column(Integer, ForeignKey("messages.id")) + message_id = Column(Integer, ForeignKey("messages.id", ondelete="CASCADE")) emotion_id = Column(Integer, ForeignKey("emotions.id")) media_type = Column(Enum(MediaTypeEnum), nullable=False) media_url = Column(String(255), nullable=False) diff --git a/app/models/message.py b/app/models/message.py index 23f89f7..1ac6524 100644 --- a/app/models/message.py +++ b/app/models/message.py @@ -12,10 +12,10 @@ class Message(Base, TimestampMixin): __tablename__ = "messages" id = Column(Integer, primary_key=True, index=True) - session_id = Column(Integer, ForeignKey("sessions.id")) + session_id = Column(Integer, ForeignKey("sessions.id", ondelete="CASCADE")) is_users = Column(Boolean, default=True) response_type = Column(Enum(ResponseTypeEnum)) content = Column(Text, nullable=False) session = relationship("Session", back_populates="messages") - generated_media = relationship("GeneratedMedia", back_populates="message") \ No newline at end of file + generated_media = relationship("GeneratedMedia", back_populates="message", cascade="all, delete-orphan") \ No newline at end of file diff --git a/app/models/session.py b/app/models/session.py index 580e621..1840c7c 100644 --- a/app/models/session.py +++ b/app/models/session.py @@ -9,13 +9,13 @@ class CharacterModeEnum(str, enum.Enum): bijyo = "bijyo" anger_mom = "anger_mom" -class Session(Base): +class Session(Base, TimestampMixin): __tablename__ = "sessions" id = Column(Integer, primary_key=True, index=True) - user_id = Column(Integer, ForeignKey("users.user_id")) + user_id = Column(Integer, ForeignKey("users.user_id", ondelete="CASCADE")) character_mode = Column(Enum(CharacterModeEnum), nullable=False) user = relationship("User", back_populates="sessions") - messages = relationship("Message", back_populates="session") - favorites = relationship("Favorite", back_populates="session") + messages = relationship("Message", back_populates="session", cascade="all, delete-orphan") + favorites = relationship("Favorite", back_populates="session", cascade="all, delete-orphan") diff --git a/app/models/user.py b/app/models/user.py index c7bd302..09b989b 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -14,10 +14,8 @@ class User(Base): email = Column(String(255), unique=True, index=True, nullable=False) user_name = Column(String(255), nullable=False) is_active = Column(Boolean, default=True) + deleted_at = Column(DateTime, nullable=True) # 論理削除日時 is_admin = Column(Boolean, default=False) - - admin_is = Column(Integer, ForeignKey("admin.id")) - - admin = relationship("Admin", back_populates="user") - sessions = relationship("Session", back_populates="user") - favorites = relationship("Favorite", back_populates="user") + + sessions = relationship("Session", back_populates="user", cascade="all, delete-orphan") + favorites = relationship("Favorite", back_populates="user", cascade="all, delete-orphan") diff --git a/app/requirements.txt b/app/requirements.txt index 623e696..e394e23 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -2,5 +2,10 @@ fastapi uvicorn[standard] mysql-connector-python pymysql +email-validator +pydantic-settings cryptography +python-jose +passlib[bcrypt] +bcrypt==3.2.0 alembic \ No newline at end of file diff --git a/app/schemas/admin.py b/app/schemas/admin.py deleted file mode 100644 index a59e8cf..0000000 --- a/app/schemas/admin.py +++ /dev/null @@ -1,24 +0,0 @@ -from pydantic import BaseModel -from datetime import datetime -from typing import Optional - -class AdminBase(BaseModel): - email: EmailStr - password: str - user_name: str - pin_code: int - -class AdminLogin(BaseModel): - email: EmailStr - password: str - pin_code: int - -class AdminResponse(BaseModel): - id: int - email: EmailStr - user_name: str - created_at: datetime - updated_at: datetime - - class Config: - from_attributes = True diff --git a/app/schemas/session.py b/app/schemas/session.py index bd649b4..fe57296 100644 --- a/app/schemas/session.py +++ b/app/schemas/session.py @@ -20,6 +20,13 @@ class SessionBase(BaseModel): class SessionCreate(SessionBase): pass +class SessionUpdate(BaseModel): + character_mode: Optional[CharacterMode] = None + token: str + + class Config: + from_attributes = True + class SessionResponse(SessionBase): id: int created_at: datetime @@ -33,6 +40,20 @@ class SessionSummaryResponse(BaseModel): character_mode: CharacterMode first_message: Optional[str] = "" created_at: datetime + is_favorite: bool = False class Config: - from_attributes = True \ No newline at end of file + from_attributes = True + + +class MessageSummary(BaseModel): + message_id :int + message_text: str + sender_type : str # "user" or "ai" + + +class SessionWIthMessagesResponse(SessionResponse): + chat_id: int + messages: list[MessageSummary] = [] + created_at: datetime + updated_at: datetime \ No newline at end of file diff --git a/app/schemas/user.py b/app/schemas/user.py index 0e3197f..ac9325f 100644 --- a/app/schemas/user.py +++ b/app/schemas/user.py @@ -2,18 +2,38 @@ from datetime import datetime from typing import Optional +# アカウント登録・リクエスト class UserRegister(BaseModel): email: EmailStr password: str user_name: str + +# ログイン・リクエスト class UserLogin(BaseModel): email: EmailStr password: str + pin_code: Optional[int] = None # 管理者ログイン時のみ使用 + + +# アカウント情報更新・リクエスト +class UserUpdate(BaseModel): + email: Optional[EmailStr] = None + user_name: Optional[str] = None + +# パスワード変更・リクエスト +class PasswordUpdate(BaseModel): + current_password: str + new_password: str + + +# アカウント登録・ログイン レスポンス class TokenResponse(BaseModel): token: str + +# アカウント情報取得・レスポンス class UserResponse(BaseModel): user_id: int email: EmailStr diff --git a/app/services/session.py b/app/services/session.py index 71498e5..8dbb9c6 100644 --- a/app/services/session.py +++ b/app/services/session.py @@ -1,6 +1,7 @@ from sqlalchemy.orm import Session from models.session import Session as SessionModel from models.message import Message +from models.favorite import Favorite from schemas.session import SessionSummaryResponse @@ -29,7 +30,7 @@ def get_sessions_with_first_message( query = query.join(SessionModel.messages) if favorite_only: - query = query.join(Message.favorites) + query = query.join(SessionModel.favorites) if keyword: query = query.filter(Message.content.ilike(f"%{keyword}%")) @@ -43,11 +44,18 @@ def get_sessions_with_first_message( .order_by(Message.created_at.asc()) .first() ) + + is_fav = db.query(Favorite).filter( + session_id = session.id, + user_id = user_id + ).first() is not None + result.append(SessionSummaryResponse( session_id=session.id, character_mode=session.character_mode, first_message=first_message.content[:20] if first_message else "", created_at=session.created_at, + is_favorite=is_fav )) return result diff --git a/app/utils/auth.py b/app/utils/auth.py index 3fa411c..ad678f1 100644 --- a/app/utils/auth.py +++ b/app/utils/auth.py @@ -1,19 +1,17 @@ -import os -from dotenv import load_dotenv +from sqlalchemy.orm import Session from datetime import datetime, timedelta from jose import jwt, JWTError from passlib.context import CryptContext from fastapi import HTTPException, Depends -from app.models.user import User -from app.core.database import get_db +from models.user import User +from core.database import get_db from fastapi.security import OAuth2PasswordBearer -from app.core.config import settings +from core.config import settings -load_dotenv() pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login") +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/login") SECRET_KEY = settings.SECRET_KEY ALGORITHM = "HS256" @@ -49,7 +47,9 @@ def get_current_user(db: Session = Depends(get_db), token: str = Depends(oauth2_ except JWTError: raise credentials_exception - user = db.query(User).filter(User.id == int(user_id)).first() + user = db.query(User).filter(User.user_id == int(user_id)).first() if user is None: raise credentials_exception + elif not user.is_active: + raise HTTPException(status_code=403, detail="このアカウントは無効です") return user diff --git a/docker-compose.yml b/docker-compose.yml index de3d0d3..73e176e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,10 +14,6 @@ services: ports: - "8000:8000" - # ホスト側の ./app ディレクトリをコンテナの /app にマウント(ソースコードの同期) - volumes: - - ./app:/app - # 環境変数を定義した .env ファイルを読み込む env_file: - .env @@ -29,6 +25,13 @@ services: # コンテナ起動時の実行コマンド(開発用:ホットリロードを有効にして uvicorn を起動) command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload + environment: + - PYTHONPATH=/app + + # ホスト側の ./app ディレクトリをコンテナの /app にマウント(ソースコードの同期) + volumes: + - ./app:/app + # MySQL データベースのサービス定義 db: # 使用する公式 MySQL イメージのバージョンを指定(8.0) From b2ac7a8cbe1d76f3946ac70f6991f469a5b7c262 Mon Sep 17 00:00:00 2001 From: sasakifuruta Date: Sat, 3 May 2025 21:43:14 +0900 Subject: [PATCH 27/72] =?UTF-8?q?=E5=85=A8=E4=BD=93=E3=81=AE=E3=82=B3?= =?UTF-8?q?=E3=83=BC=E3=83=89=E5=BE=AE=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/endpoints/admin.py | 104 --------------- app/api/endpoints/auth.py | 4 +- app/api/endpoints/emotion.py | 2 + app/api/endpoints/favorite.py | 20 --- app/api/endpoints/generated_media.py | 10 +- app/api/endpoints/message.py | 98 +++++++++++--- app/api/endpoints/session.py | 122 +++++++++--------- app/main.py | 18 ++- .../versions/c1621488556f_initial.py | 112 ---------------- app/models/favorite.py | 2 +- app/models/session.py | 2 +- app/models/user.py | 2 +- app/schemas/favorite.py | 2 +- app/schemas/message.py | 3 + app/schemas/session.py | 5 +- app/services/message.py | 25 ++-- app/services/session.py | 72 ++++++----- app/utils/auth.py | 2 +- 18 files changed, 237 insertions(+), 368 deletions(-) delete mode 100644 app/api/endpoints/admin.py delete mode 100644 app/api/endpoints/favorite.py delete mode 100644 app/migrations/versions/c1621488556f_initial.py diff --git a/app/api/endpoints/admin.py b/app/api/endpoints/admin.py deleted file mode 100644 index 0b5e686..0000000 --- a/app/api/endpoints/admin.py +++ /dev/null @@ -1,104 +0,0 @@ -from fastapi import APIRouter, HTTPException, Depends -from sqlalchemy.orm import Session -from app.schemas.admin import AdminBase, AdminLogin, AdminResponse -from app.models import Admin, User, Session as ChatSession, Message -from app.core.database import get_db -from app.utils.auth import hash_password, verify_password, create_access_token - -@router.post("/admin/register/") -async def register_admin(admin_data: AdminBase, db: Session = Depends(get_db)): - if db.query(Admin).filter_by(email=admin_data.email).first(): - raise HTTPException(status_code=400, detail="このメールアドレスは既に登録されています。") - - new_admin = Admin( - email=admin_data.email, - password_hash=hash_password(admin_data.password), - user_name=admin_data.user_name, - pin_code=str(admin_data.pin_code) - ) - db.add(new_admin) - db.commit() - db.refresh(new_admin) - - token = create_access_token({"sub": str(new_admin.id)}) - return {"token": token} - -@router.post("/admin/login") -async def login_admin(admin_data: AdminLogin, db: Session = Depends(get_db)): - admin = db.query(Admin).filter_by(email=admin_data.email).first() - if not admin: - raise HTTPException(status_code=400, detail="メールアドレスまたはパスワードが違います。") - - if not verify_password(admin_data.password, admin.password_hash): - raise HTTPException(status_code=400, detail="メールアドレスまたはパスワードが違います。") - - if admin_data.pin_code != admin.pin_code: - raise HTTPException(status_code=400, detail="PINコードが正しくありません。") - - token = create_access_token({"sub": str(new_admin.id)}) - return {"token": token} - -@router.get("/admin_info") -async def get_admin_info(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): - return current_admin - -@router.get("/users") -async def get_users(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): - users = db.query(User).filter_by(is_active=True).all() - return users - -@router.delete("/users/{user_id}") -async def delete_user(user_id: int, db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): - user = db.query(User).filter_by(id=user_id).first() - if not user: - raise HTTPException(status_code=404, detail="ユーザーが見つかりません。") - - db.delete(user) - db.commit() - return {"message": "ユーザーが削除されました。"} - -# ユーザーアカウントを凍結する処理 -@router.patch("/users/{user_id}/deactivate") -async def deactivate_user(user_id: int, db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): - user = db.query(User).filter_by(id=user_id).first() - if not user: - raise HTTPException(status_code=404, detail="ユーザーが見つかりません。") - - user.is_active = False - db.commit() - return {"message": "ユーザーが無効化されました。"} - -# ユーザーアカウントを有効化する処理 -@router.patch("/users/{user_id}/activate") -async def activate_user(user_id: int, db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): - user = db.query(User).filter_by(id=user_id).first() - if not user: - raise HTTPException(status_code=404, detail="ユーザーが見つかりません。") - - user.is_active = True - db.commit() - return {"message": "ユーザーが有効化されました。"} - -@router.patch("/users/{user_id}") -async def update_user(user_id: int, user_data: UserBase, db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): - user = db.query(User).filter_by(id=user_id).first() - if not user: - raise HTTPException(status_code=404, detail="ユーザーが見つかりません。") - - user.user_name = user_data.user_name - user.email = user_data.email - - db.commit() - return {"message": "ユーザー情報が更新されました。"} - -@router.get("/messages") -async def get_messages(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): - messages = db.query(Message).all() - return messages - -@router.patch("/settings") -async def update_settings(new_message: str, db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): - settings = db.query(Settings).first() - settings.support_message = new_message - db.commit() - return {"message": "設定が更新されました。"} diff --git a/app/api/endpoints/auth.py b/app/api/endpoints/auth.py index 7eac2c6..0c2eef7 100644 --- a/app/api/endpoints/auth.py +++ b/app/api/endpoints/auth.py @@ -26,7 +26,7 @@ async def register(user_data: UserRegister, db: Session = Depends(get_db)): db.commit() db.refresh(new_user) - token = create_access_token({"sub": str(new_user.user_id)}) + token = create_access_token({"sub": str(new_user.id)}) return {"token": token} @@ -43,7 +43,7 @@ async def login(user_data: UserLogin, db: Session = Depends(get_db)): if user_data.pin_code != settings.admin_pin_code: raise HTTPException(status_code=400, detail="PINコードが違います。") - token = create_access_token({"sub": str(user.user_id)}) + token = create_access_token({"sub": str(user.id)}) return {"token": token} diff --git a/app/api/endpoints/emotion.py b/app/api/endpoints/emotion.py index 2848f96..ff5e249 100644 --- a/app/api/endpoints/emotion.py +++ b/app/api/endpoints/emotion.py @@ -4,6 +4,8 @@ from app.schemas.emotion import EmotionCreate, EmotionResponse from app.core.database import get_db +router = APIRouter() + @router.get("/emotions") async def get_emotions(db: Session = Depends(get_db)): emotions = db.query(Emotion).all() diff --git a/app/api/endpoints/favorite.py b/app/api/endpoints/favorite.py deleted file mode 100644 index e24783e..0000000 --- a/app/api/endpoints/favorite.py +++ /dev/null @@ -1,20 +0,0 @@ -from fastapi import APIRouter, HTTPException, Depends -from sqlalchemy.orm import Session -from app.models import Favorite, Session as DBSession -from app.schemas.favorite import FavoriteCreate, FavoriteResponse -from app.core.database import get_db - -@router.post("/favorites") -async def create_favorite(favorite_data: FavoriteCreate, db: Session = Depends(get_db)): - session = db.query(DBSession).filter(DBSession.id == favorite_data.session_id).first() - if not session: - raise HTTPException(status_code=404, detail="お気に入り登録が見つかりません。") - favorite = Favorite(session_id=favorite_data.session_id, user_id=favorite_data.user_id) - db.add(favorite) - db.commit() - return favorite - -@router.get("/favorites") -async def get_favorites(user_id: int, db: Session = Depends(get_db)): - favorites = db.query(Favorite).filter(Favorite.user_id == user_id).all() - return favorites \ No newline at end of file diff --git a/app/api/endpoints/generated_media.py b/app/api/endpoints/generated_media.py index ba9393a..ca6c200 100644 --- a/app/api/endpoints/generated_media.py +++ b/app/api/endpoints/generated_media.py @@ -1,8 +1,10 @@ from fastapi import APIRouter, HTTPException, Depends -from sqlalchemy.orm import GeneratedMedia -from app.models import GeneratedMedia -from app.schemas.generated_media import GeneratedMediaCreate, GeneratedMediaResponse -from app.core.database import get_db +from sqlalchemy.orm import Session +from models import GeneratedMedia +from schemas.generated_media import GeneratedMediaCreate, GeneratedMediaResponse +from core.database import get_db + +router = APIRouter() @router.post("/generated-media") async def create_generated_media(media_data: GeneratedMediaCreate, db: Session = Depends(get_db)): diff --git a/app/api/endpoints/message.py b/app/api/endpoints/message.py index 5085da7..8664f8b 100644 --- a/app/api/endpoints/message.py +++ b/app/api/endpoints/message.py @@ -1,26 +1,88 @@ from fastapi import APIRouter, HTTPException, Depends from sqlalchemy.orm import Session from models import Message, Session as ChatSession -from schemas.message import MessageCreate +from models import User +from schemas.message import MessageCreate, MessageResponse, MessageUpdate from core.database import get_db +from utils.auth import get_current_user +from services.message import create_message, get_messages_by_session, update_user_message ,delete_message + router = APIRouter() -@router.post("/sessions/{session_id}/messages") -async def create_message(session_id: int, message_data: MessageCreate, db: Session = Depends(get_db)): - session = db.query(ChatSession).filter(ChatSession.id == session_id).first() + +# 日記の投稿またはキャラクターの返答を作成 +@router.post("/api/sessions/{session_id}/messages", response_model=MessageResponse) +async def create_message( + session_id: int, + message_data: MessageCreate, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + session = db.query(ChatSession).filter( + ChatSession.id == session_id, + ChatSession.user_id == current_user.id + ).first() + + if not session: + raise HTTPException(status_code=404, detail="チャットが見つかりません") + + return create_message(db, session_id, content=message_data.content) + + +# 特定のチャットの全メッセージを取得 +@router.get("/api/sessions/{session_id}/messages", response_model=list[MessageResponse]) +async def get_messages_endpoint( + session_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + session = db.query(ChatSession).filter( + ChatSession.id == session_id, + ChatSession.user_id == current_user.id + ).first() + + if not session: + raise HTTPException(status_code=404, detail="チャットが見つかりません") + + return get_messages_by_session(db, session_id) + + +# ユーザのメッセージを更新 +@router.put("/api/sessions/{session_id}/messages/{message_id}", response_model=MessageResponse) +async def update_message( + session_id: int, + message_id: int, + message_data: MessageUpdate, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + session = db.query(ChatSession).filter( + ChatSession.id == session_id, + ChatSession.user_id == current_user.id + ).first() + if not session: - raise HTTPException(status_code=404, detail="メッセージが見つかりません。") - message = Message(content=message_data.content, session_id=session_id) - db.add(message) - db.commit() - return message - -# @router.put("/sessions/{session_id}/messages/{message_id}") -# async def update_message(session_id: int, message_id: int, message_data: MessageUpdate, db: Session = Depends(get_db)): -# message = db.query(Message).filter(Message.id == message_id).first() -# if not message: -# raise HTTPException(status_code=404, detail="メッセージが見つかりません。") -# message.content = message_data.content -# db.commit() -# return message + raise HTTPException(status_code=404, detail="チャットが見つかりません") + + return update_user_message(db, message_id, message_data.content) + + +# 特定のメッセージを削除 +@router.delete("/api/sessions/{session_id}/messages/{message_id}") +async def delete_message_endpoint( + session_id: int, + message_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + session = db.query(ChatSession).filter( + ChatSession.id == session_id, + ChatSession.user_id == current_user.id + ).first() + + if not session: + raise HTTPException(status_code=404, detail="チャットが見つかりません。") + + delete_message(db, message_id) + return {"message": "メッセージを削除しました。"} \ No newline at end of file diff --git a/app/api/endpoints/session.py b/app/api/endpoints/session.py index 1feef8e..1ee11bb 100644 --- a/app/api/endpoints/session.py +++ b/app/api/endpoints/session.py @@ -1,20 +1,29 @@ from fastapi import APIRouter, HTTPException, Depends from sqlalchemy.orm import Session -from app.models import Session as ChatSession -from app.models import Favorite, User -from app.schemas.session import SessionCreate, SessionUpdate, SessionResponse, SessionWithMessagesResponse, SessionSummaryResponse -from app.core.database import get_db -from app.utils.auth import get_current_user -from service.session import get_sessions_with_first_message +from models import Session as ChatSession +from models import Favorite, User +from schemas.session import SessionCreate, SessionUpdate, SessionResponse, SessionWithMessagesResponse, SessionSummaryResponse +from core.database import get_db +from utils.auth import get_current_user +from services.session import get_sessions_with_first_message, toggle_favorite_session from typing import Optional +router = APIRouter() -# TODO::チャットの開始 -@router.post("/sessions") -async def create_session(session_data: SessionCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): - new_session = ChatSession(chat_title=session_data.chat_title, user_id=current_user.id) +# チャットの開始 +@router.post("/api/sessions", response_model=SessionResponse) +async def create_session( + session_data: SessionCreate, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) + ): + new_session = ChatSession( + character_mode=session_data.character_mode, + user_id=current_user.id + ) db.add(new_session) db.commit() + db.refresh(new_session) return new_session @@ -28,42 +37,56 @@ async def get_sessions( ): return get_sessions_with_first_message( db = db, - user_id = current_user.user_id, + user_id = current_user.id, favorite_only = favorite_only, keyword = keyword ) # 特定のチャットを取得 -@router.get("/api/sessions/{id}", response_model=SessionWithMessagesResponse) -async def get_session(id: int, db: Session = Depends(get_db)): - session = db.query(ChatSession).filter(ChatSession.id == id).first() +@router.get("/api/sessions/{session_id}", response_model=SessionWithMessagesResponse) +async def get_session( + session_id: int, + db: Session = Depends(get_db), + current_user = Depends(get_current_user) +): + session = db.query(ChatSession).filter( + ChatSession.id == session_id, + ChatSession.user_id == current_user.id + ).first() + if not session: raise HTTPException(status_code=404, detail="セッションが見つかりません。") + + messages =[{ + "message_id": m.id, + "message_text": m.content, + "sender_type": "user" if m.is_users else "ai" + } for m in session.messages + ] + return { "chat_id" : session.id, - "messages" : [ - { - "message_id": m.id, - "message_text": m.content, - "sender_type": "user" if m.is_users else "ai" - }for m in session.messages - ], + "messages" : messages, "created_at": session.created_at, "updated_at": session.updated_at, } # 特定のチャットを変更 -@router.patch("/api/sessions/{id}", response_model= SessionResponse) +@router.patch("/api/sessions/{session_id}", response_model= SessionResponse) async def update_session( id: int, session_data: SessionUpdate, - db:Session = Depends(get_db) + db: Session = Depends(get_db), + current_user : User= Depends(get_current_user) ): - # TODO::tokenからuser_idを取得 - - session = db.query(Session).filter(ChatSession.id == id).first() + + session = db.query(ChatSession).filter( + ChatSession.id == id, + ChatSession.user_id == current_user.id + ).first() + if not session: raise HTTPException(status_code=404, detail="チャットが見つかりません") @@ -77,46 +100,23 @@ async def update_session( # 特定のチャットを削除 -@router.delete("/api/sessions/{id}") -async def delete_session(id: int, db: Session = Depends(get_db)): - session = db.query(ChatSession).filter(ChatSession.id == id).first() - if not session: - raise HTTPException(status_code=404, detail="セッションが見つかりません。") - db.delete(session) - db.commit() - raise {"message": "チャットを削除しました。"} +@router.delete("/api/sessions/{session_id}") +async def delete_session( + session_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + success = delete_session(db, session_id, current_user.id) + if not success: + raise HTTPException(status_code=404, detail="チャットが見つかりません。") + return {"message": "チャットを削除しました。"} # お気に入りのトグル -@router.post("/api/favorites") +@router.post("/api/sessions/{session_id}/favorite") def toggle_favorite( session_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): - - # セッションの存在を確認 - session = db.query(ChatSession).filter(ChatSession.id == session_id).first() - if not session: - if not session: - raise HTTPException(status_code=404, detail= "") - - # お気に入りの状態チェック - favorite = db.query(Favorite).filter_by( - session_id = session_id, - user_id = current_user.id - ).first() - - if favorite: - db.delete(favorite) - db.commit() - return{"message": "お気に入りを解除しました", "is_favorite": False} - else: - new_fav = Favorite( - session_id = session_id, - user_id = current_user.id - ) - db.add(new_fav) - db.commit() - return {"message": "お気に入りを追加しました", "is_favorite": True} - + return toggle_favorite_session(db, session_id, current_user.id) diff --git a/app/main.py b/app/main.py index 23e740c..375a572 100644 --- a/app/main.py +++ b/app/main.py @@ -3,6 +3,10 @@ import mysql.connector import os from api.endpoints.auth import router as auth_router +from api.endpoints.user import router as user_router +from api.endpoints.session import router as session_router +from api.endpoints.message import router as message_router +from api.endpoints.generated_media import router as generated_media_router app = FastAPI() @@ -38,10 +42,18 @@ def db_status(): except Exception as e: return {"db_status": "error", "details": str(e)} + # 認証用エンドポイント app.include_router(auth_router, prefix="/api", tags=["auth"]) +# アカウント情報用エンドポイント +app.include_router(user_router, prefix="/api", tags=["user"]) + +# チャット用エンドポイント +app.include_router(session_router, prefix="/api", tags=["session"]) + +# メッセージ用エンドポイント +app.include_router(message_router, prefix="/api", tags=["message"]) -# テスト -from api.endpoints import message -app.include_router(message.router) \ No newline at end of file +# 生成用エンドポイント +app.include_router(generated_media_router, prefix="/api", tags=["generated_media"]) \ No newline at end of file diff --git a/app/migrations/versions/c1621488556f_initial.py b/app/migrations/versions/c1621488556f_initial.py deleted file mode 100644 index 3a19ac3..0000000 --- a/app/migrations/versions/c1621488556f_initial.py +++ /dev/null @@ -1,112 +0,0 @@ -"""initial - -Revision ID: c1621488556f -Revises: -Create Date: 2025-05-03 07:44:52.951958 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = 'c1621488556f' -down_revision: Union[str, None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('emotions', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('emotion', sa.String(length=255), nullable=False), - sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_emotions_id'), 'emotions', ['id'], unique=False) - op.create_table('users', - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('password_hash', sa.String(length=255), nullable=False), - sa.Column('email', sa.String(length=255), nullable=False), - sa.Column('user_name', sa.String(length=255), nullable=False), - sa.Column('is_active', sa.Boolean(), nullable=True), - sa.Column('deleted_at', sa.DateTime(), nullable=True), - sa.Column('is_admin', sa.Boolean(), nullable=True), - sa.PrimaryKeyConstraint('user_id') - ) - op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True) - op.create_index(op.f('ix_users_user_id'), 'users', ['user_id'], unique=False) - op.create_table('sessions', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('character_mode', sa.Enum('saburo', 'bijyo', 'anger_mom', name='charactermodeenum'), nullable=False), - sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['users.user_id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_sessions_id'), 'sessions', ['id'], unique=False) - op.create_table('favorites', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('session_id', sa.Integer(), nullable=True), - sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), - sa.ForeignKeyConstraint(['session_id'], ['sessions.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['user_id'], ['users.user_id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_favorites_id'), 'favorites', ['id'], unique=False) - op.create_table('messages', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('session_id', sa.Integer(), nullable=True), - sa.Column('is_users', sa.Boolean(), nullable=True), - sa.Column('response_type', sa.Enum('praise', 'insult', name='responsetypeenum'), nullable=True), - sa.Column('content', sa.Text(), nullable=False), - sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), - sa.ForeignKeyConstraint(['session_id'], ['sessions.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_messages_id'), 'messages', ['id'], unique=False) - op.create_table('generated_media', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('message_id', sa.Integer(), nullable=True), - sa.Column('emotion_id', sa.Integer(), nullable=True), - sa.Column('media_type', sa.Enum('IMAGE', 'BGM', name='mediatypeenum'), nullable=False), - sa.Column('media_url', sa.String(length=255), nullable=False), - sa.Column('image_prompt', sa.Text(), nullable=True), - sa.Column('bgm_prompt', sa.Text(), nullable=True), - sa.Column('bgm_duration', sa.Integer(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), - sa.ForeignKeyConstraint(['emotion_id'], ['emotions.id'], ), - sa.ForeignKeyConstraint(['message_id'], ['messages.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_generated_media_id'), 'generated_media', ['id'], unique=False) - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.drop_index(op.f('ix_generated_media_id'), table_name='generated_media') - op.drop_table('generated_media') - op.drop_index(op.f('ix_messages_id'), table_name='messages') - op.drop_table('messages') - op.drop_index(op.f('ix_favorites_id'), table_name='favorites') - op.drop_table('favorites') - op.drop_index(op.f('ix_sessions_id'), table_name='sessions') - op.drop_table('sessions') - op.drop_index(op.f('ix_users_user_id'), table_name='users') - op.drop_index(op.f('ix_users_email'), table_name='users') - op.drop_table('users') - op.drop_index(op.f('ix_emotions_id'), table_name='emotions') - op.drop_table('emotions') - # ### end Alembic commands ### diff --git a/app/models/favorite.py b/app/models/favorite.py index abb678e..f9ecf37 100644 --- a/app/models/favorite.py +++ b/app/models/favorite.py @@ -8,7 +8,7 @@ class Favorite(Base, TimestampMixin): id = Column(Integer, primary_key=True, index=True) session_id = Column(Integer, ForeignKey("sessions.id", ondelete="CASCADE")) - user_id = Column(Integer, ForeignKey("users.user_id", ondelete="CASCADE")) + user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE")) session = relationship("Session", back_populates="favorites") user = relationship("User", back_populates="favorites") diff --git a/app/models/session.py b/app/models/session.py index 1840c7c..9140669 100644 --- a/app/models/session.py +++ b/app/models/session.py @@ -13,7 +13,7 @@ class Session(Base, TimestampMixin): __tablename__ = "sessions" id = Column(Integer, primary_key=True, index=True) - user_id = Column(Integer, ForeignKey("users.user_id", ondelete="CASCADE")) + user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE")) character_mode = Column(Enum(CharacterModeEnum), nullable=False) user = relationship("User", back_populates="sessions") diff --git a/app/models/user.py b/app/models/user.py index 09b989b..aa92b05 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -9,7 +9,7 @@ class User(Base): __tablename__ = "users" - user_id = Column(Integer, primary_key=True, index=True) + id = Column(Integer, primary_key=True, index=True) password_hash = Column(String(255), nullable=False) email = Column(String(255), unique=True, index=True, nullable=False) user_name = Column(String(255), nullable=False) diff --git a/app/schemas/favorite.py b/app/schemas/favorite.py index 95cf16b..b4c1add 100644 --- a/app/schemas/favorite.py +++ b/app/schemas/favorite.py @@ -2,7 +2,7 @@ from datetime import datetime class FavoriteBase(BaseModel): - message_id: int + session_id: int user_id: int class FavoriteCreate(FavoriteBase): diff --git a/app/schemas/message.py b/app/schemas/message.py index 8333516..6116803 100644 --- a/app/schemas/message.py +++ b/app/schemas/message.py @@ -16,6 +16,9 @@ class MessageBase(BaseModel): class MessageCreate(MessageBase): pass +class MessageUpdate(BaseModel): + content: str + class MessageResponse(MessageBase): id: int created_at: datetime diff --git a/app/schemas/session.py b/app/schemas/session.py index fe57296..f5ff4cb 100644 --- a/app/schemas/session.py +++ b/app/schemas/session.py @@ -52,8 +52,9 @@ class MessageSummary(BaseModel): sender_type : str # "user" or "ai" -class SessionWIthMessagesResponse(SessionResponse): +class SessionWithMessagesResponse(SessionResponse): chat_id: int messages: list[MessageSummary] = [] created_at: datetime - updated_at: datetime \ No newline at end of file + updated_at: datetime + \ No newline at end of file diff --git a/app/services/message.py b/app/services/message.py index d24dfad..422a72f 100644 --- a/app/services/message.py +++ b/app/services/message.py @@ -5,29 +5,38 @@ from schemas.message import MessageCreate, MessageResponse -def create_message(db: Session, message: MessageCreate) -> Message: - db_message = Message(**message.dict()) +# メッセージ(日記またはキャラクターの返答)を作成 +def create_message( + db: Session, + session_id: int, + content: str, + is_users: bool = True +) -> MessageResponse: + db_message = Message(session_id=session_id, content=content, is_users=is_users) db.add(db_message) db.commit() db.refresh(db_message) - return db_message + return MessageResponse.from_orm(db_message) -def get_message(db: Session, session_id: int) -> List[MessageResponse]: +# チャット内の全メッセージを取得 +def get_messages_by_session(db: Session, session_id: int) -> List[MessageResponse]: messages = db.query(Message).filter(Message.session_id == session_id).order_by(Message.created_at).all() - return [MessageResponse.from_orm(message) for message in messages] + return [MessageResponse.from_orm(m) for m in messages] -def update_message(db: Session, message_id: int, content: str) -> Message: +# メッセージを更新 +def update_user_message(db: Session, message_id: int, new_content: str) -> MessageResponse: db_message = db.query(Message).filter(Message.id == message_id, Message.is_users == True).first() if db_message is None: raise HTTPException(status_code=404, detail="該当するメッセージが見つかりません") # もしくはreturn None - db_message.content = content + db_message.content = new_content db.commit() db.refresh(db_message) - return db_message + return MessageResponse.from_orm(db_message) +# メッセージ削除 def delete_message(db: Session, message_id: int) -> None: db_message = db.query(Message).filter(Message.id == message_id).first() if db_message is None: diff --git a/app/services/session.py b/app/services/session.py index 8dbb9c6..6714d0a 100644 --- a/app/services/session.py +++ b/app/services/session.py @@ -3,6 +3,7 @@ from models.message import Message from models.favorite import Favorite from schemas.session import SessionSummaryResponse +from fastapi import HTTPException def get_sessions_with_first_message( @@ -10,20 +11,7 @@ def get_sessions_with_first_message( user_id: int, favorite_only: bool = False, keyword: str | None = None, - ): - """ - 指定したユーザーIDに紐づくセッション一覧を取得します。 - セッションごとに、最初のメッセージの内容と作成日時をまとめて返します。 - - Args: - db (Session): データベースセッション - user_id (int): ユーザーID - favorite_only (bool, optional): お気に入りメッセージが存在するセッションのみ取得する場合True(デフォルトFalse) - keyword (str | None, optional): メッセージ本文に含まれるキーワードでフィルタリングする場合に指定(デフォルトNone) - - Returns: - セッション情報(session_id、character_mode、first_message、created_at)をまとめたリスト - """ + )->list[SessionSummaryResponse]: query = db.query(SessionModel).filter(SessionModel.user_id == user_id) if favorite_only or keyword: @@ -36,6 +24,7 @@ def get_sessions_with_first_message( query = query.filter(Message.content.ilike(f"%{keyword}%")) sessions = query.distinct().all() + result = [] for session in sessions: first_message = ( @@ -46,8 +35,8 @@ def get_sessions_with_first_message( ) is_fav = db.query(Favorite).filter( - session_id = session.id, - user_id = user_id + Favorite.session_id == session.id, + Favorite.user_id == user_id ).first() is not None result.append(SessionSummaryResponse( @@ -60,25 +49,50 @@ def get_sessions_with_first_message( return result -def delete_session(db: Session, session_id: int, user_id: int): - """指定したセッションとそのメッセージを削除します。 - - Args: - db (Session): データベースセッション - session_id (int): 削除対象のセッションID - user_id (int): ユーザーID(本人確認用) - """ +def delete_session( + db: Session, + session_id: int, + user_id: int +)-> bool: session = db.query(SessionModel).filter( SessionModel.id == session_id, SessionModel.user_id == user_id ).first() - if not session: + if not session: return False - db.query(Message).filter(Message.session_id == session_id).delete() - - db.delete(session) + db.delete(session) # cascade設定によりメッセージも削除される db.commit() + return True + + +def toggle_favorite_session( + db: Session, + session_id: int, + user_id: int +)->dict: + # セッションの存在を確認 + session = db.query(SessionModel).filter(SessionModel.id == session_id).first() + if not session: + raise HTTPException(status_code=404, detail= "チャットが見つかりません") + + # お気に入りの状態チェック + favorite = db.query(Favorite).filter_by( + session_id = session_id, + user_id = user_id + ).first() - return True \ No newline at end of file + if favorite: + db.delete(favorite) + db.commit() + return{"message": "お気に入りを解除しました", "is_favorite": False} + else: + new_fav = Favorite( + session_id = session_id, + user_id = user_id + ) + db.add(new_fav) + db.commit() + return {"message": "お気に入りを追加しました", "is_favorite": True} + \ No newline at end of file diff --git a/app/utils/auth.py b/app/utils/auth.py index ad678f1..2368a8c 100644 --- a/app/utils/auth.py +++ b/app/utils/auth.py @@ -47,7 +47,7 @@ def get_current_user(db: Session = Depends(get_db), token: str = Depends(oauth2_ except JWTError: raise credentials_exception - user = db.query(User).filter(User.user_id == int(user_id)).first() + user = db.query(User).filter(User.id == int(user_id)).first() if user is None: raise credentials_exception elif not user.is_active: From 9431692c46208d0e04387eb4c8205ddb3d5a4362 Mon Sep 17 00:00:00 2001 From: honda Date: Sat, 3 May 2025 22:45:06 +0900 Subject: [PATCH 28/72] =?UTF-8?q?alembic/versions=E3=83=87=E3=82=A3?= =?UTF-8?q?=E3=83=AC=E3=82=AF=E3=83=88=E3=83=AA=E3=81=AE=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 0 -> 6148 bytes .gitignore | 2 +- app/migrations/versions/38db158025e3_honda.py | 112 ++++++++++++++++++ 3 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 .DS_Store create mode 100644 app/migrations/versions/38db158025e3_honda.py diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..91f06533db1e8acfe914459c8becb3378ffa4139 GIT binary patch literal 6148 zcmeHKOHRWu5FL{&DzWJbA+hlZka~kqg%k7wP%0HfqhtYl?~r<(o`4Mp;3OP@_dEmC zC1QyXnvv``o{uMaQtXU~g!$uYCNdR~2?jAx84;RCT}Kv7XW$;2^=xs|cf-ggf7Qfi zujO3UawiYv^Y6~~eO=$S%T2$84LNCh|N3%K`d2^thub}FsyC!cCD$^@P*$y}k`s@ri1J1xF13Vuh2C*?riuvfk zlv@B`4RaRg>?N2J4I9Ix2oEG^Do|5{wHQIuVUHHq7$!wcmtf6Du*x6JOUTu+f6U<$ z8bzO-0cW6Opwi`(_x~k6nb9V{O!1X7;0*jT214B~+Bsgz?$&Rw$GbLSTwzccm#9FX l7e4`5@EkeKPW4CXh-(a!B6p!Zrvv>*AQR$~Gw=%xyaRcEKtuol literal 0 HcmV?d00001 diff --git a/.gitignore b/.gitignore index d308cd2..b3eaeaf 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ ### Alembic ### alembic/__pycache__/ -alembic/versions/__pycache__/ +# alembic/versions/__pycache__/ ### Python ### # Byte-compiled / optimized / DLL files diff --git a/app/migrations/versions/38db158025e3_honda.py b/app/migrations/versions/38db158025e3_honda.py new file mode 100644 index 0000000..9cd3524 --- /dev/null +++ b/app/migrations/versions/38db158025e3_honda.py @@ -0,0 +1,112 @@ +"""honda + +Revision ID: 38db158025e3 +Revises: +Create Date: 2025-05-03 13:42:30.792840 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '38db158025e3' +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('emotions', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('emotion', sa.String(length=255), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_emotions_id'), 'emotions', ['id'], unique=False) + op.create_table('users', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('password_hash', sa.String(length=255), nullable=False), + sa.Column('email', sa.String(length=255), nullable=False), + sa.Column('user_name', sa.String(length=255), nullable=False), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('deleted_at', sa.DateTime(), nullable=True), + sa.Column('is_admin', sa.Boolean(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True) + op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False) + op.create_table('sessions', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('character_mode', sa.Enum('saburo', 'bijyo', 'anger_mom', name='charactermodeenum'), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_sessions_id'), 'sessions', ['id'], unique=False) + op.create_table('favorites', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('session_id', sa.Integer(), nullable=True), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['session_id'], ['sessions.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_favorites_id'), 'favorites', ['id'], unique=False) + op.create_table('messages', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('session_id', sa.Integer(), nullable=True), + sa.Column('is_users', sa.Boolean(), nullable=True), + sa.Column('response_type', sa.Enum('praise', 'insult', name='responsetypeenum'), nullable=True), + sa.Column('content', sa.Text(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['session_id'], ['sessions.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_messages_id'), 'messages', ['id'], unique=False) + op.create_table('generated_media', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('message_id', sa.Integer(), nullable=True), + sa.Column('emotion_id', sa.Integer(), nullable=True), + sa.Column('media_type', sa.Enum('IMAGE', 'BGM', name='mediatypeenum'), nullable=False), + sa.Column('media_url', sa.String(length=255), nullable=False), + sa.Column('image_prompt', sa.Text(), nullable=True), + sa.Column('bgm_prompt', sa.Text(), nullable=True), + sa.Column('bgm_duration', sa.Integer(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['emotion_id'], ['emotions.id'], ), + sa.ForeignKeyConstraint(['message_id'], ['messages.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_generated_media_id'), 'generated_media', ['id'], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_generated_media_id'), table_name='generated_media') + op.drop_table('generated_media') + op.drop_index(op.f('ix_messages_id'), table_name='messages') + op.drop_table('messages') + op.drop_index(op.f('ix_favorites_id'), table_name='favorites') + op.drop_table('favorites') + op.drop_index(op.f('ix_sessions_id'), table_name='sessions') + op.drop_table('sessions') + op.drop_index(op.f('ix_users_id'), table_name='users') + op.drop_index(op.f('ix_users_email'), table_name='users') + op.drop_table('users') + op.drop_index(op.f('ix_emotions_id'), table_name='emotions') + op.drop_table('emotions') + # ### end Alembic commands ### From 8b8f12dd61cc074ba0d14c84af8909716b99c25d Mon Sep 17 00:00:00 2001 From: ponz <155151085+sasakifuruta@users.noreply.github.com> Date: Sat, 3 May 2025 23:14:24 +0900 Subject: [PATCH 29/72] Update Makefile --- Makefile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index d4a9e30..5858053 100644 --- a/Makefile +++ b/Makefile @@ -16,6 +16,9 @@ exec-app: exec-db: docker compose exec -it db bash +db-shell: + docker compose exec -it db bash -c "mysql -u root -p" + down: docker compose down @@ -30,4 +33,4 @@ show: docker compose exec -it web bash -c "cd /app && alembic history" downgrade: - docker compose exec -it web bash -c "cd /app && alembic downgrade -1" \ No newline at end of file + docker compose exec -it web bash -c "cd /app && alembic downgrade -1" From ecd61be5fb520ec01599a42eccf8885883257a01 Mon Sep 17 00:00:00 2001 From: sasakifuruta Date: Sun, 4 May 2025 01:09:50 +0900 Subject: [PATCH 30/72] =?UTF-8?q?=E7=AE=A1=E7=90=86=E8=80=85=E3=83=AD?= =?UTF-8?q?=E3=82=B0=E3=82=A4=E3=83=B3=E9=80=94=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 3 + app/api/api.py | 22 ++-- app/api/endpoints/admin.py | 110 +++++++++++++++++ app/api/endpoints/auth.py | 2 +- app/api/endpoints/emotion.py | 6 +- app/main.py | 26 +--- .../versions/e154e91b4d38_initial.py | 112 ++++++++++++++++++ app/schemas/user.py | 1 + 8 files changed, 247 insertions(+), 35 deletions(-) create mode 100644 app/api/endpoints/admin.py create mode 100644 app/migrations/versions/e154e91b4d38_initial.py diff --git a/Makefile b/Makefile index d4a9e30..b708a7a 100644 --- a/Makefile +++ b/Makefile @@ -16,6 +16,9 @@ exec-app: exec-db: docker compose exec -it db bash +db-shell: + docker compose exec -it db bash -c "mysql -u root -p" + down: docker compose down diff --git a/app/api/api.py b/app/api/api.py index 66589fa..119c0f3 100644 --- a/app/api/api.py +++ b/app/api/api.py @@ -1,13 +1,15 @@ from fastapi import APIRouter -from app.api.endpoints import auth, admin, user, session, message, emotion, favorite, generated_media +from api.endpoints import auth, user, session, message, generated_media -api_router = APIRouter() +router = APIRouter() -api_router.include_router(auth.router, tags=["Auth"]) -api_router.include_router(admin.router, tags=["admin"]) -api_router.include_router(user.router, tags=["User"]) -api_router.include_router(session.router, tags=["Session"]) -api_router.include_router(message.router, tags=["Message"]) -api_router.include_router(emotion.router, tags=["Emotion"]) -api_router.include_router(favorite.router, tags=["Favorite"]) -api_router.include_router(generated_media.router, tags=["GeneratedMedia"]) \ No newline at end of file +# 認証用エンドポイント +router.include_router(auth.router, prefix="/api", tags=["Auth"]) +# アカウント情報用エンドポイント +router.include_router(user.router, prefix="/api", tags=["User"]) +# チャット用エンドポイント +router.include_router(session.router, prefix="/api", tags=["Session"]) +# メッセージ用エンドポイント +router.include_router(message.router, prefix="/api", tags=["Message"]) +# 生成用エンドポイント +router.include_router(generated_media.router, prefix="/api", tags=["Generated_media"]) \ No newline at end of file diff --git a/app/api/endpoints/admin.py b/app/api/endpoints/admin.py new file mode 100644 index 0000000..55c675a --- /dev/null +++ b/app/api/endpoints/admin.py @@ -0,0 +1,110 @@ +# from fastapi import APIRouter, HTTPException, Depends +# from sqlalchemy.orm import Session +# from models import User, Session as ChatSession, Message +# from core.database import get_db +# from utils.auth import hash_password, verify_password, create_access_token + +# router = APIRouter() + +# @router.post("/admin/register/") +# async def register_admin(admin_data: User, db: Session = Depends(get_db)): +# if db.query(User).filter_by(email=admin_data.email).first(): +# raise HTTPException(status_code=400, detail="このメールアドレスは既に登録されています。") + +# new_admin = User( +# email=admin_data.email, +# password_hash=hash_password(admin_data.password), +# user_name=admin_data.user_name, +# pin_code=str(admin_data.pin_code) +# ) +# db.add(new_admin) +# db.commit() +# db.refresh(new_admin) + +# token = create_access_token({"sub": str(new_admin.id)}) +# return {"token": token} + +# @router.post("/admin/login") +# async def login_admin(admin_data: AdminLogin, db: Session = Depends(get_db)): +# admin = db.query(Admin).filter_by(email=admin_data.email).first() +# if not admin: +# raise HTTPException(status_code=400, detail="メールアドレスまたはパスワードが違います。") + +# if not verify_password(admin_data.password, admin.password_hash): +# raise HTTPException(status_code=400, detail="メールアドレスまたはパスワードが違います。") + +# if admin_data.pin_code != admin.pin_code: +# raise HTTPException(status_code=400, detail="PINコードが正しくありません。") + +# token = create_access_token({"sub": str(new_admin.id)}) +# return {"token": token} + + +# @router.get("/admin_info") +# async def get_admin_info(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): +# return current_admin + + +# @router.get("/users") +# async def get_users(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): +# users = db.query(User).filter_by(is_active=True).all() +# return users + + +# @router.delete("/users/{user_id}") +# async def delete_user(user_id: int, db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): +# user = db.query(User).filter_by(id=user_id).first() +# if not user: +# raise HTTPException(status_code=404, detail="ユーザーが見つかりません。") + +# db.delete(user) +# db.commit() +# return {"message": "ユーザーが削除されました。"} + +# # ユーザーアカウントを凍結する処理 +# @router.patch("/users/{user_id}/deactivate") +# async def deactivate_user(user_id: int, db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): +# user = db.query(User).filter_by(id=user_id).first() +# if not user: +# raise HTTPException(status_code=404, detail="ユーザーが見つかりません。") + +# user.is_active = False +# db.commit() +# return {"message": "ユーザーが無効化されました。"} + + +# # ユーザーアカウントを有効化する処理 +# @router.patch("/users/{user_id}/activate") +# async def activate_user(user_id: int, db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): +# user = db.query(User).filter_by(id=user_id).first() +# if not user: +# raise HTTPException(status_code=404, detail="ユーザーが見つかりません。") + +# user.is_active = True +# db.commit() +# return {"message": "ユーザーが有効化されました。"} + + +# @router.patch("/users/{user_id}") +# async def update_user(user_id: int, user_data: UserBase, db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): +# user = db.query(User).filter_by(id=user_id).first() +# if not user: +# raise HTTPException(status_code=404, detail="ユーザーが見つかりません。") + +# user.user_name = user_data.user_name +# user.email = user_data.email + +# db.commit() +# return {"message": "ユーザー情報が更新されました。"} + +# @router.get("/messages") +# async def get_messages(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): +# messages = db.query(Message).all() +# return messages + +# @router.patch("/settings") +# async def update_settings(new_message: str, db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): +# settings = db.query(Settings).first() +# settings.support_message = new_message +# db.commit() +# return {"message": "設定が更新されました。"} \ No newline at end of file diff --git a/app/api/endpoints/auth.py b/app/api/endpoints/auth.py index 0c2eef7..e23cee9 100644 --- a/app/api/endpoints/auth.py +++ b/app/api/endpoints/auth.py @@ -27,7 +27,7 @@ async def register(user_data: UserRegister, db: Session = Depends(get_db)): db.refresh(new_user) token = create_access_token({"sub": str(new_user.id)}) - return {"token": token} + return {"token": token, "is_admin": new_user.is_admin} # ログイン diff --git a/app/api/endpoints/emotion.py b/app/api/endpoints/emotion.py index ff5e249..245a2b2 100644 --- a/app/api/endpoints/emotion.py +++ b/app/api/endpoints/emotion.py @@ -1,8 +1,8 @@ from fastapi import APIRouter, HTTPException, Depends from sqlalchemy.orm import Session -from app.models import Emotion -from app.schemas.emotion import EmotionCreate, EmotionResponse -from app.core.database import get_db +from models import Emotion +from schemas.emotion import EmotionCreate, EmotionResponse +from core.database import get_db router = APIRouter() diff --git a/app/main.py b/app/main.py index 375a572..ca4395a 100644 --- a/app/main.py +++ b/app/main.py @@ -2,11 +2,8 @@ from fastapi.middleware.cors import CORSMiddleware import mysql.connector import os -from api.endpoints.auth import router as auth_router -from api.endpoints.user import router as user_router -from api.endpoints.session import router as session_router -from api.endpoints.message import router as message_router -from api.endpoints.generated_media import router as generated_media_router +from api.api import router as api_router + app = FastAPI() @@ -41,19 +38,6 @@ def db_status(): return {"db_status": "not connected"} except Exception as e: return {"db_status": "error", "details": str(e)} - - -# 認証用エンドポイント -app.include_router(auth_router, prefix="/api", tags=["auth"]) - -# アカウント情報用エンドポイント -app.include_router(user_router, prefix="/api", tags=["user"]) - -# チャット用エンドポイント -app.include_router(session_router, prefix="/api", tags=["session"]) - -# メッセージ用エンドポイント -app.include_router(message_router, prefix="/api", tags=["message"]) - -# 生成用エンドポイント -app.include_router(generated_media_router, prefix="/api", tags=["generated_media"]) \ No newline at end of file + + +app.include_router(api_router, prefix="/api") \ No newline at end of file diff --git a/app/migrations/versions/e154e91b4d38_initial.py b/app/migrations/versions/e154e91b4d38_initial.py new file mode 100644 index 0000000..0b023a1 --- /dev/null +++ b/app/migrations/versions/e154e91b4d38_initial.py @@ -0,0 +1,112 @@ +"""initial + +Revision ID: e154e91b4d38 +Revises: +Create Date: 2025-05-03 13:09:02.691077 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'e154e91b4d38' +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('emotions', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('emotion', sa.String(length=255), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_emotions_id'), 'emotions', ['id'], unique=False) + op.create_table('users', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('password_hash', sa.String(length=255), nullable=False), + sa.Column('email', sa.String(length=255), nullable=False), + sa.Column('user_name', sa.String(length=255), nullable=False), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('deleted_at', sa.DateTime(), nullable=True), + sa.Column('is_admin', sa.Boolean(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True) + op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False) + op.create_table('sessions', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('character_mode', sa.Enum('saburo', 'bijyo', 'anger_mom', name='charactermodeenum'), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_sessions_id'), 'sessions', ['id'], unique=False) + op.create_table('favorites', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('session_id', sa.Integer(), nullable=True), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['session_id'], ['sessions.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_favorites_id'), 'favorites', ['id'], unique=False) + op.create_table('messages', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('session_id', sa.Integer(), nullable=True), + sa.Column('is_users', sa.Boolean(), nullable=True), + sa.Column('response_type', sa.Enum('praise', 'insult', name='responsetypeenum'), nullable=True), + sa.Column('content', sa.Text(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['session_id'], ['sessions.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_messages_id'), 'messages', ['id'], unique=False) + op.create_table('generated_media', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('message_id', sa.Integer(), nullable=True), + sa.Column('emotion_id', sa.Integer(), nullable=True), + sa.Column('media_type', sa.Enum('IMAGE', 'BGM', name='mediatypeenum'), nullable=False), + sa.Column('media_url', sa.String(length=255), nullable=False), + sa.Column('image_prompt', sa.Text(), nullable=True), + sa.Column('bgm_prompt', sa.Text(), nullable=True), + sa.Column('bgm_duration', sa.Integer(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['emotion_id'], ['emotions.id'], ), + sa.ForeignKeyConstraint(['message_id'], ['messages.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_generated_media_id'), 'generated_media', ['id'], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_generated_media_id'), table_name='generated_media') + op.drop_table('generated_media') + op.drop_index(op.f('ix_messages_id'), table_name='messages') + op.drop_table('messages') + op.drop_index(op.f('ix_favorites_id'), table_name='favorites') + op.drop_table('favorites') + op.drop_index(op.f('ix_sessions_id'), table_name='sessions') + op.drop_table('sessions') + op.drop_index(op.f('ix_users_id'), table_name='users') + op.drop_index(op.f('ix_users_email'), table_name='users') + op.drop_table('users') + op.drop_index(op.f('ix_emotions_id'), table_name='emotions') + op.drop_table('emotions') + # ### end Alembic commands ### diff --git a/app/schemas/user.py b/app/schemas/user.py index ac9325f..b70b8b2 100644 --- a/app/schemas/user.py +++ b/app/schemas/user.py @@ -7,6 +7,7 @@ class UserRegister(BaseModel): email: EmailStr password: str user_name: str + is_admin: Optional[bool] = False # ログイン・リクエスト From e5a9c4ca35d32b7986876ba695554fffdfb5d20f Mon Sep 17 00:00:00 2001 From: sasakifuruta Date: Mon, 5 May 2025 00:39:24 +0900 Subject: [PATCH 31/72] =?UTF-8?q?AI=E3=82=AD=E3=83=A3=E3=83=A9=E3=81=AE?= =?UTF-8?q?=E8=BF=94=E7=AD=94=E7=94=9F=E6=88=90=E3=81=A8=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E8=80=85=E6=93=8D=E4=BD=9C=E3=81=AE=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/api.py | 10 +- app/api/endpoints/admin.py | 276 +++++++++++++++++++++------------- app/api/endpoints/message.py | 12 +- app/api/endpoints/session.py | 12 +- app/api/endpoints/user.py | 10 +- app/core/config.py | 7 +- app/models/user.py | 6 +- app/requirements.txt | 3 +- app/schemas/user.py | 4 +- app/services/ai/characters.py | 52 +++++++ app/services/ai/response.py | 43 ++++++ app/services/message.py | 51 +++++-- app/utils/auth.py | 12 +- 13 files changed, 355 insertions(+), 143 deletions(-) create mode 100644 app/services/ai/characters.py create mode 100644 app/services/ai/response.py diff --git a/app/api/api.py b/app/api/api.py index 119c0f3..ec17a7e 100644 --- a/app/api/api.py +++ b/app/api/api.py @@ -4,12 +4,12 @@ router = APIRouter() # 認証用エンドポイント -router.include_router(auth.router, prefix="/api", tags=["Auth"]) +router.include_router(auth.router, tags=["Auth"]) # アカウント情報用エンドポイント -router.include_router(user.router, prefix="/api", tags=["User"]) +router.include_router(user.router, tags=["User"]) # チャット用エンドポイント -router.include_router(session.router, prefix="/api", tags=["Session"]) +router.include_router(session.router, tags=["Session"]) # メッセージ用エンドポイント -router.include_router(message.router, prefix="/api", tags=["Message"]) +router.include_router(message.router, tags=["Message"]) # 生成用エンドポイント -router.include_router(generated_media.router, prefix="/api", tags=["Generated_media"]) \ No newline at end of file +router.include_router(generated_media.router, tags=["Generated_media"]) \ No newline at end of file diff --git a/app/api/endpoints/admin.py b/app/api/endpoints/admin.py index 55c675a..4b127ee 100644 --- a/app/api/endpoints/admin.py +++ b/app/api/endpoints/admin.py @@ -1,110 +1,182 @@ -# from fastapi import APIRouter, HTTPException, Depends -# from sqlalchemy.orm import Session -# from models import User, Session as ChatSession, Message -# from core.database import get_db -# from utils.auth import hash_password, verify_password, create_access_token - -# router = APIRouter() - -# @router.post("/admin/register/") -# async def register_admin(admin_data: User, db: Session = Depends(get_db)): -# if db.query(User).filter_by(email=admin_data.email).first(): -# raise HTTPException(status_code=400, detail="このメールアドレスは既に登録されています。") +from fastapi import APIRouter, HTTPException, Depends +from sqlalchemy.orm import Session +from datetime import timedelta +from models import User, Message +from schemas.user import UserRegister, UserLogin, UserUpdate, UserResponse, TokenResponse +from core.config import settings +from core.database import get_db +from utils.auth import hash_password, verify_password, create_access_token, get_current_admin_user +from utils.timestamp import now_jst + +router = APIRouter() + + +# 管理者アカウント登録 +@router.post("/admin/register/", response_model=TokenResponse) +async def register_admin( + admin_data: UserRegister, + db: Session = Depends(get_db) +): + if db.query(User).filter_by(email=admin_data.email).first(): + raise HTTPException(status_code=400, detail="このメールアドレスは既に登録されています。") -# new_admin = User( -# email=admin_data.email, -# password_hash=hash_password(admin_data.password), -# user_name=admin_data.user_name, -# pin_code=str(admin_data.pin_code) -# ) -# db.add(new_admin) -# db.commit() -# db.refresh(new_admin) - -# token = create_access_token({"sub": str(new_admin.id)}) -# return {"token": token} - -# @router.post("/admin/login") -# async def login_admin(admin_data: AdminLogin, db: Session = Depends(get_db)): -# admin = db.query(Admin).filter_by(email=admin_data.email).first() -# if not admin: -# raise HTTPException(status_code=400, detail="メールアドレスまたはパスワードが違います。") + new_admin = User( + email=admin_data.email, + password_hash=hash_password(admin_data.password), + user_name=admin_data.user_name, + is_admin=True + ) + db.add(new_admin) + db.commit() + db.refresh(new_admin) + + token = create_access_token({"sub": str(new_admin.id)}) + return {"token": token } + + +# 管理者ログイン +@router.post("/admin/login", response_model=TokenResponse) +async def login_admin( + admin_data: UserLogin, + db: Session = Depends(get_db) +): + admin = db.query(User).filter_by(email=admin_data.email, is_admin=True).first() + if not admin: + raise HTTPException(status_code=400, detail="メールアドレスまたはパスワードが違います") -# if not verify_password(admin_data.password, admin.password_hash): -# raise HTTPException(status_code=400, detail="メールアドレスまたはパスワードが違います。") + if not verify_password(admin_data.password, admin.password_hash): + raise HTTPException(status_code=400, detail="メールアドレスまたはパスワードが違います") -# if admin_data.pin_code != admin.pin_code: -# raise HTTPException(status_code=400, detail="PINコードが正しくありません。") + # PINコードは.envファイルから取得 + if str(admin_data.pin_code) != settings.ADMIN_PIN_CODE: + raise HTTPException(status_code=400, detail="PINコードが正しくありません") -# token = create_access_token({"sub": str(new_admin.id)}) -# return {"token": token} - - -# @router.get("/admin_info") -# async def get_admin_info(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): -# return current_admin - - -# @router.get("/users") -# async def get_users(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): -# users = db.query(User).filter_by(is_active=True).all() -# return users - - -# @router.delete("/users/{user_id}") -# async def delete_user(user_id: int, db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): -# user = db.query(User).filter_by(id=user_id).first() -# if not user: -# raise HTTPException(status_code=404, detail="ユーザーが見つかりません。") + token = create_access_token({"sub": str(admin_data.id)}) + return {"token": token, "is_admin": admin_data.is_admin} + + +# 管理者アカウント情報取得 +@router.get("/admin_info", response_model=UserResponse) +async def get_admin_info( + current_admin: User = Depends(get_current_admin_user) +): + return current_admin + + +# 一般ユーザ情報一覧取得 +@router.get("/users", response_model=list[UserResponse]) +async def get_all_users( + db: Session = Depends(get_db), + current_admin: User = Depends(get_current_admin_user) +): + users = db.query(User).filter(User.is_admin==False).all() + now = now_jst() + return [ + UserResponse( + user_id=user.id, + email=user.email, + user_name=user.user_name, + is_active=user.is_active, + can_be_delete=( + not user.is_active + and user.deleted_at + and (now - user.deleted_at >= timedelta(days=30)) + ) + )for user in users + ] + + +# ユーザーアカウントを削除する処理 +@router.delete("/users/{user_id}") +async def delete_user( + user_id: int, + db: Session = Depends(get_db), + current_admin: User = Depends(get_current_admin_user) +): + user = db.query(User).filter_by(id=user_id).first() + if not user: + raise HTTPException(status_code=404, detail="ユーザーが見つかりません") -# db.delete(user) -# db.commit() -# return {"message": "ユーザーが削除されました。"} - -# # ユーザーアカウントを凍結する処理 -# @router.patch("/users/{user_id}/deactivate") -# async def deactivate_user(user_id: int, db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): -# user = db.query(User).filter_by(id=user_id).first() -# if not user: -# raise HTTPException(status_code=404, detail="ユーザーが見つかりません。") + if user.is_active or not user.deleted_at: + raise HTTPException(status_code=400, detail="無効化されたアカウントのみ削除可能です") -# user.is_active = False -# db.commit() -# return {"message": "ユーザーが無効化されました。"} - - -# # ユーザーアカウントを有効化する処理 -# @router.patch("/users/{user_id}/activate") -# async def activate_user(user_id: int, db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): -# user = db.query(User).filter_by(id=user_id).first() -# if not user: -# raise HTTPException(status_code=404, detail="ユーザーが見つかりません。") + now = now_jst() + if user.deleted_at + timedelta(days=30) > now: + raise HTTPException(status_code=400, detail=f"{now - user.deleted_at}日後に削除可能です") -# user.is_active = True -# db.commit() -# return {"message": "ユーザーが有効化されました。"} - - -# @router.patch("/users/{user_id}") -# async def update_user(user_id: int, user_data: UserBase, db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): -# user = db.query(User).filter_by(id=user_id).first() -# if not user: -# raise HTTPException(status_code=404, detail="ユーザーが見つかりません。") + db.delete(user) + db.commit() + return {"message": "ユーザーが削除されました"} + + +# ユーザーアカウントを凍結する処理 +@router.patch("/users/{user_id}/deactivate") +async def deactivate_user( + user_id: int, + db: Session = Depends(get_db), + current_admin: User = Depends(get_current_admin_user) +): + user = db.query(User).filter_by(id=user_id).first() + if not user: + raise HTTPException(status_code=404, detail="ユーザーが見つかりません") + + user.is_active = False + user.deleted_at = now_jst() + db.commit() + return {"message": "ユーザーが無効化されました"} + + +# ユーザーアカウントを有効化する処理 +@router.patch("/users/{user_id}/activate") +async def activate_user( + user_id: int, + db: Session = Depends(get_db), + current_admin: User = Depends(get_current_admin_user) +): + user = db.query(User).filter_by(id=user_id).first() + if not user: + raise HTTPException(status_code=404, detail="ユーザーが見つかりません") + + user.is_active = True + user.deleted_at = None # 論理削除日時をリセット + db.commit() + return {"message": "ユーザーが有効化されました"} + + +# ユーザー情報更新 +@router.patch("/users/{user_id}") +async def update_user( + user_id: int, + user_data: UserUpdate, + db: Session = Depends(get_db), + current_admin: User = Depends(get_current_admin_user) +): + user = db.query(User).filter_by(id=user_id).first() + if not user: + raise HTTPException(status_code=404, detail="ユーザーが見つかりません") + + if user_data.user_name is None and user_data.email is None: + raise HTTPException(status_code=400, detail="更新内容がありません") -# user.user_name = user_data.user_name -# user.email = user_data.email - -# db.commit() -# return {"message": "ユーザー情報が更新されました。"} - -# @router.get("/messages") -# async def get_messages(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): -# messages = db.query(Message).all() -# return messages - -# @router.patch("/settings") -# async def update_settings(new_message: str, db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): -# settings = db.query(Settings).first() -# settings.support_message = new_message -# db.commit() -# return {"message": "設定が更新されました。"} \ No newline at end of file + if user_data.user_name is not None: + user.user_name = user_data.user_name + if user_data.email is not None: + user.email = user_data.email + + db.commit() + return {"message": "ユーザー情報が更新されました"} + + +# TODO:削除?:投稿一覧 +@router.get("/messages") +async def get_messages(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): + messages = db.query(Message).all() + return messages + +# TODO:設定変更 +@router.patch("/settings") +async def update_settings(new_message: str, db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): + settings = db.query(Settings).first() + settings.support_message = new_message + db.commit() + return {"message": "設定が更新されました。"} \ No newline at end of file diff --git a/app/api/endpoints/message.py b/app/api/endpoints/message.py index 8664f8b..7600e50 100644 --- a/app/api/endpoints/message.py +++ b/app/api/endpoints/message.py @@ -5,14 +5,14 @@ from schemas.message import MessageCreate, MessageResponse, MessageUpdate from core.database import get_db from utils.auth import get_current_user -from services.message import create_message, get_messages_by_session, update_user_message ,delete_message +from services.message import create_message_with_ai, get_messages_by_session, update_user_message ,delete_message router = APIRouter() # 日記の投稿またはキャラクターの返答を作成 -@router.post("/api/sessions/{session_id}/messages", response_model=MessageResponse) +@router.post("/sessions/{session_id}/messages", response_model=MessageResponse) async def create_message( session_id: int, message_data: MessageCreate, @@ -27,11 +27,11 @@ async def create_message( if not session: raise HTTPException(status_code=404, detail="チャットが見つかりません") - return create_message(db, session_id, content=message_data.content) + return create_message_with_ai(db, session_id, content=message_data.content) # 特定のチャットの全メッセージを取得 -@router.get("/api/sessions/{session_id}/messages", response_model=list[MessageResponse]) +@router.get("/sessions/{session_id}/messages", response_model=list[MessageResponse]) async def get_messages_endpoint( session_id: int, db: Session = Depends(get_db), @@ -49,7 +49,7 @@ async def get_messages_endpoint( # ユーザのメッセージを更新 -@router.put("/api/sessions/{session_id}/messages/{message_id}", response_model=MessageResponse) +@router.put("/sessions/{session_id}/messages/{message_id}", response_model=MessageResponse) async def update_message( session_id: int, message_id: int, @@ -69,7 +69,7 @@ async def update_message( # 特定のメッセージを削除 -@router.delete("/api/sessions/{session_id}/messages/{message_id}") +@router.delete("/sessions/{session_id}/messages/{message_id}") async def delete_message_endpoint( session_id: int, message_id: int, diff --git a/app/api/endpoints/session.py b/app/api/endpoints/session.py index 1ee11bb..374ab3d 100644 --- a/app/api/endpoints/session.py +++ b/app/api/endpoints/session.py @@ -11,7 +11,7 @@ router = APIRouter() # チャットの開始 -@router.post("/api/sessions", response_model=SessionResponse) +@router.post("/sessions", response_model=SessionResponse) async def create_session( session_data: SessionCreate, db: Session = Depends(get_db), @@ -28,7 +28,7 @@ async def create_session( # チャット一覧取得 -@router.get("/api/sessions", response_model=list[SessionSummaryResponse]) +@router.get("/sessions", response_model=list[SessionSummaryResponse]) async def get_sessions( db: Session = Depends(get_db), current_user: User = Depends(get_current_user), @@ -44,7 +44,7 @@ async def get_sessions( # 特定のチャットを取得 -@router.get("/api/sessions/{session_id}", response_model=SessionWithMessagesResponse) +@router.get("/sessions/{session_id}", response_model=SessionWithMessagesResponse) async def get_session( session_id: int, db: Session = Depends(get_db), @@ -74,7 +74,7 @@ async def get_session( # 特定のチャットを変更 -@router.patch("/api/sessions/{session_id}", response_model= SessionResponse) +@router.patch("/sessions/{session_id}", response_model= SessionResponse) async def update_session( id: int, session_data: SessionUpdate, @@ -100,7 +100,7 @@ async def update_session( # 特定のチャットを削除 -@router.delete("/api/sessions/{session_id}") +@router.delete("/sessions/{session_id}") async def delete_session( session_id: int, db: Session = Depends(get_db), @@ -113,7 +113,7 @@ async def delete_session( # お気に入りのトグル -@router.post("/api/sessions/{session_id}/favorite") +@router.post("/sessions/{session_id}/favorite") def toggle_favorite( session_id: int, db: Session = Depends(get_db), diff --git a/app/api/endpoints/user.py b/app/api/endpoints/user.py index 8e29815..c57e79b 100644 --- a/app/api/endpoints/user.py +++ b/app/api/endpoints/user.py @@ -10,13 +10,13 @@ router = APIRouter() # アカウント情報取得 -@router.get("/api/user", response_model=UserResponse) +@router.get("/user", response_model=UserResponse) async def get_my_account(current_user: User = Depends(get_current_user)): return current_user # アカウント情報の更新 -@router.patch("/api/user", response_model=UserResponse) +@router.patch("/user", response_model=UserResponse) async def update_my_account( user_update: UserUpdate, db: Session = Depends(get_db), @@ -38,7 +38,7 @@ async def update_my_account( # パスワード変更 -@router.put("/api/password") +@router.put("/password") async def change_password( password_data: PasswordUpdate, db: Session = Depends(get_db), @@ -54,7 +54,7 @@ async def change_password( # アカウント凍結 -@router.delete("/api/user") +@router.delete("/user") async def deactivate_account( db: Session = Depends(get_db), current_user: User = Depends(get_current_user) @@ -69,7 +69,7 @@ async def deactivate_account( # アカウント削除 -@router.delete("/api/user/delete") +@router.delete("/user/delete") async def delete_account( db: Session = Depends(get_db), current_user: User = Depends(get_current_user) diff --git a/app/core/config.py b/app/core/config.py index 3cff794..80cef6c 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -4,11 +4,14 @@ class Settings(BaseSettings): SECRET_KEY: str ALGORITHM: str = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 + + # PIN code + ADMIN_PIN_CODE: str # OpenAI API key - # openai_api_key: str + openai_api_key: str # OpenAI Model - # openai_model: str ="gpt-4.1-nano" + openai_model: str ="gpt-4.1-nano" # Database mysql_root_password: str diff --git a/app/models/user.py b/app/models/user.py index aa92b05..30ed04e 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -1,12 +1,8 @@ from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey from sqlalchemy.orm import relationship -from datetime import datetime -from zoneinfo import ZoneInfo from .base import Base, TimestampMixin -JST = ZoneInfo("Asia/Tokyo") - -class User(Base): +class User(Base, TimestampMixin): __tablename__ = "users" id = Column(Integer, primary_key=True, index=True) diff --git a/app/requirements.txt b/app/requirements.txt index e394e23..45dc1fa 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -8,4 +8,5 @@ cryptography python-jose passlib[bcrypt] bcrypt==3.2.0 -alembic \ No newline at end of file +alembic +openai \ No newline at end of file diff --git a/app/schemas/user.py b/app/schemas/user.py index b70b8b2..7875421 100644 --- a/app/schemas/user.py +++ b/app/schemas/user.py @@ -36,9 +36,11 @@ class TokenResponse(BaseModel): # アカウント情報取得・レスポンス class UserResponse(BaseModel): - user_id: int + id: int email: EmailStr user_name: str + is_active: bool + can_be_deleted: Optional[bool] = False # 管理画面の「削除可能」表示用 created_at: datetime updated_at: datetime diff --git a/app/services/ai/characters.py b/app/services/ai/characters.py new file mode 100644 index 0000000..4645596 --- /dev/null +++ b/app/services/ai/characters.py @@ -0,0 +1,52 @@ +# app/services/ai/characters.py + +CHARACTER_PROMPTS= { + "saburo": { + "name": "岩谷三郎", + "description":( + "岩谷三郎は、通称「さぶちゃん」として知られる54歳のおじさんです。\ + 彼はいろんな苦難を乗り越えてきた経験豊富な人物です。\ + 飄々としていますが、さっぱりとした親しみやすい人物で、よく若者の相談に乗っています。\ + 彼と話した若者は皆明るく前向きな気持ちになります。" + ), + "prompt": + ( + "あなたは岩谷三郎(さぶちゃん)というキャラクターとして振る舞ってください。" + "少し口が悪いが根は優しく、頼れる年上の人物です。" + "ユーザーの日記に対して、励ましやユーモアを交えてフランクにコメントしてください。" + "語尾は『~だな』『~だぞ』など、自然な口調にしてください。" + ) + }, + "bijyo": { + "name": "黒髪お姉さん", + "description":( + "黒髪お姉さんは、仕事ができて頭のいい姉御肌の28歳キャリアウーマンです。\ + いつも長くて綺麗な黒髪を靡かせて颯爽と歩き、周囲の目を引く魅力あふれる女性です。\ + キリッとしていますが優しく、褒めるのがとても上手です。\ + 彼女に褒められると皆、天にも昇るような気持ちで喜び、意欲を増します。" + ), + "prompt": + ( + "あなたは黒髪お姉さんというキャラクターとして振る舞ってください。" + "キリッとした雰囲気を持ちつつも、優しさと褒め上手な一面を持っています。" + "ユーザーの日記に対して、励ましやユーモアを交えてフランクにコメントしてください。" + "語尾は『〜ね』など、自然な口調にしてください。" + ) + }, + "anger-mom": { + "name": "鬼お母さん", + "description":( + "鬼お母さんは、いつもビシッと喝を入れてくれます。\ + 表情は怖いですが、的確な一言で目標を明確にさせてくれます。\ + 彼女は知りませんが、実は彼女に喝を入れてほしいファンは結構多いです。\ + 彼女の喝を受けた人は皆、目標に向かって頑張る気持ちになります。" + ), + "prompt": + ( + "あなたは鬼お母さんというキャラクターとして振る舞ってください。" + "表情は怖いですが、的確な一言で目標を明確にさせてくれます。" + "ユーザーの日記に対して、ビシッと喝を入れてください." + "語尾は『〜しな』『〜さい』など、自然な口調にしてください。" + ) + } +} \ No newline at end of file diff --git a/app/services/ai/response.py b/app/services/ai/response.py new file mode 100644 index 0000000..f1ef681 --- /dev/null +++ b/app/services/ai/response.py @@ -0,0 +1,43 @@ +import openai +import random +from core.config import settings +from services.ai.characters import CHARACTER_PROMPTS +from models.session import CharacterModeEnum +from models.message import ResponseTypeEnum + +openai.api_key = settings.openai_api_key + +# キャラモードに応じたAI返答を生成。 +def generate_ai_response( + character_mode: CharacterModeEnum, + user_input: str + )->tuple[str, ResponseTypeEnum]: + + if character_mode == CharacterModeEnum.saburo: + prompt_data = CHARACTER_PROMPTS["saburo"] + response_type = None + + elif character_mode == CharacterModeEnum.bijyo: + if random.randint(1, 5) == 1: + prompt_data = CHARACTER_PROMPTS["anger-mom"] + response_type = ResponseTypeEnum.insult + else: + prompt_data = CHARACTER_PROMPTS["bijyo"] + response_type = ResponseTypeEnum.praise + + else: + raise ValueError("無効なキャラクターモードです") + + system_prompt = prompt_data["description"] + "\n" + prompt_data["prompt"] + user_diary = f"ユーザーの日記: {user_input}" + + response = openai.ChatCompletion.create( + model = settings.openai_model, + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_diary} + ] + ) + ai_reply = response.choices[0].message.content.strip() + return ai_reply, response_type + diff --git a/app/services/message.py b/app/services/message.py index 422a72f..a69a4e2 100644 --- a/app/services/message.py +++ b/app/services/message.py @@ -1,22 +1,54 @@ from fastapi import HTTPException from sqlalchemy.orm import Session from typing import List -from models.message import Message -from schemas.message import MessageCreate, MessageResponse +from models.message import Message, ResponseTypeEnum +from models.session import Session as SessionModel, CharacterModeEnum +from schemas.message import MessageResponse +from services.ai.response import generate_ai_response -# メッセージ(日記またはキャラクターの返答)を作成 -def create_message( + +# 日記を保存してキャラクターの返答を生成 +def create_message_with_ai( db: Session, session_id: int, content: str, - is_users: bool = True ) -> MessageResponse: - db_message = Message(session_id=session_id, content=content, is_users=is_users) - db.add(db_message) + # ユーザーの日記を保存 + user_message = Message( + session_id=session_id, + content=content, + is_users=True + ) + db.add(user_message) db.commit() - db.refresh(db_message) - return MessageResponse.from_orm(db_message) + db.refresh(user_message) + + # セッションを取得してモードを確認 + session = db.query(SessionModel).filter(SessionModel.id == session_id).first() + if session is None: + raise HTTPException(status_code=404, detail="チャットが見つかりません") + character_mode = session.character_mode + + # AI返答を生成 + ai_reply, response_type = generate_ai_response( + character_mode=character_mode, + user_input=content + ) + + # AI返答を保存 + ai_message = Message( + session_id=session_id, + content=ai_reply, + is_users=False, + response_type=response_type + ) + db.add(ai_message) + db.commit() + db.refresh(ai_message) + + # AI返答を返す + return MessageResponse.from_orm(ai_message) # チャット内の全メッセージを取得 @@ -43,3 +75,4 @@ def delete_message(db: Session, message_id: int) -> None: raise HTTPException(status_code=404, detail="該当するメッセージが見つかりません") db.delete(db_message) db.commit() + diff --git a/app/utils/auth.py b/app/utils/auth.py index 2368a8c..cd364a9 100644 --- a/app/utils/auth.py +++ b/app/utils/auth.py @@ -32,8 +32,8 @@ def create_access_token(data: dict, expires_delta: timedelta = None) -> str: encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt + def get_current_user(db: Session = Depends(get_db), token: str = Depends(oauth2_scheme)) -> User: - credentials_exception = HTTPException( status_code=401, detail="認証情報が無効です", @@ -53,3 +53,13 @@ def get_current_user(db: Session = Depends(get_db), token: str = Depends(oauth2_ elif not user.is_active: raise HTTPException(status_code=403, detail="このアカウントは無効です") return user + + +def get_current_admin_user( + db: Session = Depends(get_db), + token: str = Depends(oauth2_scheme) +)-> User: + user = get_current_user(db, token) + if not user.is_admin: + raise HTTPException(status_code=400, detail="管理者アカウントではありません") + return user From b764ee511efe57b92e68c504fb5a8188d42f5658 Mon Sep 17 00:00:00 2001 From: sasakifuruta Date: Tue, 6 May 2025 12:15:13 +0900 Subject: [PATCH 32/72] =?UTF-8?q?=E6=97=A5=E8=A8=98=E6=8A=95=E7=A8=BF?= =?UTF-8?q?=E3=81=A8=E5=90=8C=E6=99=82=E3=81=ABSession=E3=83=87=E3=83=BC?= =?UTF-8?q?=E3=82=BF=E6=96=B0=E8=A6=8F=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/api.py | 6 +- app/api/endpoints/admin.py | 23 ++-- app/api/endpoints/emotion.py | 3 + app/api/endpoints/generated_media.py | 8 +- app/api/endpoints/message.py | 60 ++++++--- app/api/endpoints/session.py | 23 ++-- app/core/config.py | 7 + app/main.py | 10 ++ ...d38_initial.py => b7c203677009_initial.py} | 10 +- app/models/message.py | 2 +- app/requirements.txt | 5 +- app/schemas/generated_media.py | 2 +- app/schemas/message.py | 5 +- app/services/ai/generator.py | 126 ++++++++++++++++++ app/services/ai/{characters.py => prompts.py} | 9 ++ app/services/ai/response.py | 43 ------ app/services/message.py | 34 ++++- app/utils/s3.py | 28 ++++ 18 files changed, 305 insertions(+), 99 deletions(-) rename app/migrations/versions/{e154e91b4d38_initial.py => b7c203677009_initial.py} (94%) create mode 100644 app/services/ai/generator.py rename app/services/ai/{characters.py => prompts.py} (82%) delete mode 100644 app/services/ai/response.py create mode 100644 app/utils/s3.py diff --git a/app/api/api.py b/app/api/api.py index ec17a7e..b174992 100644 --- a/app/api/api.py +++ b/app/api/api.py @@ -1,10 +1,14 @@ from fastapi import APIRouter -from api.endpoints import auth, user, session, message, generated_media +from api.endpoints import auth, admin, user, session, message, generated_media + router = APIRouter() + # 認証用エンドポイント router.include_router(auth.router, tags=["Auth"]) +# 管理者用エンドポイント +router.include_router(admin.router, tags=["Admin"]) # アカウント情報用エンドポイント router.include_router(user.router, tags=["User"]) # チャット用エンドポイント diff --git a/app/api/endpoints/admin.py b/app/api/endpoints/admin.py index 4b127ee..9f7a5b0 100644 --- a/app/api/endpoints/admin.py +++ b/app/api/endpoints/admin.py @@ -168,15 +168,18 @@ async def update_user( # TODO:削除?:投稿一覧 -@router.get("/messages") -async def get_messages(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): - messages = db.query(Message).all() - return messages +# @router.get("/messages") +# async def get_messages(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): +# messages = db.query(Message).all() +# return messages # TODO:設定変更 -@router.patch("/settings") -async def update_settings(new_message: str, db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): - settings = db.query(Settings).first() - settings.support_message = new_message - db.commit() - return {"message": "設定が更新されました。"} \ No newline at end of file +# @router.patch("/settings") +# async def update_settings( +# new_message: str, +# db: Session = Depends(get_db), +# current_admin: User = Depends(get_current_admin_user)): +# settings = db.query().first() +# settings.support_message = new_message +# db.commit() +# return {"message": "設定が更新されました。"} \ No newline at end of file diff --git a/app/api/endpoints/emotion.py b/app/api/endpoints/emotion.py index 245a2b2..2b9e638 100644 --- a/app/api/endpoints/emotion.py +++ b/app/api/endpoints/emotion.py @@ -6,11 +6,14 @@ router = APIRouter() +# 感情一覧取得 @router.get("/emotions") async def get_emotions(db: Session = Depends(get_db)): emotions = db.query(Emotion).all() return emotions + +# 感情登録 @router.post("/emotions") async def create_emotions(emotion_data: EmotionCreate, db: Session = Depends(get_db)): new_emotion = Emotion(emotion=emotion_data.emotion) diff --git a/app/api/endpoints/generated_media.py b/app/api/endpoints/generated_media.py index ca6c200..e59c293 100644 --- a/app/api/endpoints/generated_media.py +++ b/app/api/endpoints/generated_media.py @@ -6,8 +6,11 @@ router = APIRouter() -@router.post("/generated-media") -async def create_generated_media(media_data: GeneratedMediaCreate, db: Session = Depends(get_db)): +@router.post("/generated-media", response_model=GeneratedMediaResponse) +async def create_generated_media( + media_data: GeneratedMediaCreate, + db: Session = Depends(get_db) +): new_media = GeneratedMedia( message_id=media_data.message_id, emotion_id=media_data.emotion_id, @@ -16,4 +19,5 @@ async def create_generated_media(media_data: GeneratedMediaCreate, db: Session = ) db.add(new_media) db.commit() + db.refresh(new_media) return new_media \ No newline at end of file diff --git a/app/api/endpoints/message.py b/app/api/endpoints/message.py index 7600e50..4cc3b3a 100644 --- a/app/api/endpoints/message.py +++ b/app/api/endpoints/message.py @@ -1,33 +1,55 @@ -from fastapi import APIRouter, HTTPException, Depends +from fastapi import Request, APIRouter, HTTPException, Depends from sqlalchemy.orm import Session -from models import Message, Session as ChatSession +from slowapi import Limiter +from slowapi.util import get_remote_address + +from models import Message, Session as SessionModel from models import User from schemas.message import MessageCreate, MessageResponse, MessageUpdate from core.database import get_db from utils.auth import get_current_user -from services.message import create_message_with_ai, get_messages_by_session, update_user_message ,delete_message +from services.message import create_message_with_ai, create_message as create_user_message, get_messages_by_session, update_user_message ,delete_message router = APIRouter() +# レートリミッター +limiter = Limiter(key_func=get_remote_address) # 日記の投稿またはキャラクターの返答を作成 -@router.post("/sessions/{session_id}/messages", response_model=MessageResponse) +@router.post("/sessions/{session_id}/messages") +@limiter.limit("2/minute") # IPごとに2回/分 async def create_message( + request: Request, session_id: int, message_data: MessageCreate, db: Session = Depends(get_db), - current_user: User = Depends(get_current_user) + # current_user: User = Depends(get_current_user) ): - session = db.query(ChatSession).filter( - ChatSession.id == session_id, - ChatSession.user_id == current_user.id + session = db.query(SessionModel).filter( + SessionModel.id == session_id, + # SessionModel.user_id == current_user.id + SessionModel.user_id == 1 # テスト ).first() if not session: - raise HTTPException(status_code=404, detail="チャットが見つかりません") + if message_data.character_mode is None: + raise HTTPException(status_code=400, detail="新規チャットの場合はcharacter_modeが必要です") + + # セッションを新規作成 + session = SessionModel( + character_mode = message_data.character_mode, + # user_id = current_user.id + user_id = 1 # テスト用 + ) + db.add(session) + db.commit() + db.refresh(session) + + return create_user_message(db, session_id, content=message_data.content) #テスト + - return create_message_with_ai(db, session_id, content=message_data.content) + # return create_message_with_ai(db, session_id, message_data.content) # 特定のチャットの全メッセージを取得 @@ -37,9 +59,9 @@ async def get_messages_endpoint( db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): - session = db.query(ChatSession).filter( - ChatSession.id == session_id, - ChatSession.user_id == current_user.id + session = db.query(SessionModel).filter( + SessionModel.id == session_id, + SessionModel.user_id == current_user.id ).first() if not session: @@ -57,9 +79,9 @@ async def update_message( db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): - session = db.query(ChatSession).filter( - ChatSession.id == session_id, - ChatSession.user_id == current_user.id + session = db.query(SessionModel).filter( + SessionModel.id == session_id, + SessionModel.user_id == current_user.id ).first() if not session: @@ -76,9 +98,9 @@ async def delete_message_endpoint( db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): - session = db.query(ChatSession).filter( - ChatSession.id == session_id, - ChatSession.user_id == current_user.id + session = db.query(SessionModel).filter( + SessionModel.id == session_id, + SessionModel.user_id == current_user.id ).first() if not session: diff --git a/app/api/endpoints/session.py b/app/api/endpoints/session.py index 374ab3d..5eb1438 100644 --- a/app/api/endpoints/session.py +++ b/app/api/endpoints/session.py @@ -1,6 +1,6 @@ from fastapi import APIRouter, HTTPException, Depends from sqlalchemy.orm import Session -from models import Session as ChatSession +from models import Session as SessionModel from models import Favorite, User from schemas.session import SessionCreate, SessionUpdate, SessionResponse, SessionWithMessagesResponse, SessionSummaryResponse from core.database import get_db @@ -15,11 +15,12 @@ async def create_session( session_data: SessionCreate, db: Session = Depends(get_db), - current_user: User = Depends(get_current_user) + # current_user: User = Depends(get_current_user) ): - new_session = ChatSession( + new_session = SessionModel( character_mode=session_data.character_mode, - user_id=current_user.id + # user_id=current_user.id + user_id=1 ) db.add(new_session) db.commit() @@ -50,9 +51,9 @@ async def get_session( db: Session = Depends(get_db), current_user = Depends(get_current_user) ): - session = db.query(ChatSession).filter( - ChatSession.id == session_id, - ChatSession.user_id == current_user.id + session = db.query(SessionModel).filter( + SessionModel.id == session_id, + SessionModel.user_id == current_user.id ).first() if not session: @@ -61,7 +62,7 @@ async def get_session( messages =[{ "message_id": m.id, "message_text": m.content, - "sender_type": "user" if m.is_users else "ai" + "sender_type": "user" if m.is_user else "ai" } for m in session.messages ] @@ -82,9 +83,9 @@ async def update_session( current_user : User= Depends(get_current_user) ): - session = db.query(ChatSession).filter( - ChatSession.id == id, - ChatSession.user_id == current_user.id + session = db.query(SessionModel).filter( + SessionModel.id == id, + SessionModel.user_id == current_user.id ).first() if not session: diff --git a/app/core/config.py b/app/core/config.py index 80cef6c..2c202c1 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -12,6 +12,13 @@ class Settings(BaseSettings): openai_api_key: str # OpenAI Model openai_model: str ="gpt-4.1-nano" + + # AWS S3 + aws_access_key_id: str + aws_secret_access_key: str + aws_s3_bucket_name: str + aws_region: str = "ap-northeast-1" + # Database mysql_root_password: str diff --git a/app/main.py b/app/main.py index ca4395a..88887b1 100644 --- a/app/main.py +++ b/app/main.py @@ -1,6 +1,11 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware import mysql.connector +from slowapi.errors import RateLimitExceeded +from slowapi.middleware import SlowAPIMiddleware +from slowapi import Limiter, _rate_limit_exceeded_handler +from slowapi.util import get_remote_address + import os from api.api import router as api_router @@ -8,6 +13,11 @@ app = FastAPI() +# レートリミットの設定 +limiter = Limiter(key_func=get_remote_address) +app.state.limiter = limiter +app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) +app.add_middleware(SlowAPIMiddleware) # ✅ CORS設定(Next.jsからアクセス可能にする) app.add_middleware( diff --git a/app/migrations/versions/e154e91b4d38_initial.py b/app/migrations/versions/b7c203677009_initial.py similarity index 94% rename from app/migrations/versions/e154e91b4d38_initial.py rename to app/migrations/versions/b7c203677009_initial.py index 0b023a1..db469d6 100644 --- a/app/migrations/versions/e154e91b4d38_initial.py +++ b/app/migrations/versions/b7c203677009_initial.py @@ -1,8 +1,8 @@ """initial -Revision ID: e154e91b4d38 +Revision ID: b7c203677009 Revises: -Create Date: 2025-05-03 13:09:02.691077 +Create Date: 2025-05-06 00:13:17.550204 """ from typing import Sequence, Union @@ -12,7 +12,7 @@ # revision identifiers, used by Alembic. -revision: str = 'e154e91b4d38' +revision: str = 'b7c203677009' down_revision: Union[str, None] = None branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None @@ -37,6 +37,8 @@ def upgrade() -> None: sa.Column('is_active', sa.Boolean(), nullable=True), sa.Column('deleted_at', sa.DateTime(), nullable=True), sa.Column('is_admin', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), sa.PrimaryKeyConstraint('id') ) op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True) @@ -65,7 +67,7 @@ def upgrade() -> None: op.create_table('messages', sa.Column('id', sa.Integer(), nullable=False), sa.Column('session_id', sa.Integer(), nullable=True), - sa.Column('is_users', sa.Boolean(), nullable=True), + sa.Column('is_user', sa.Boolean(), nullable=True), sa.Column('response_type', sa.Enum('praise', 'insult', name='responsetypeenum'), nullable=True), sa.Column('content', sa.Text(), nullable=False), sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), diff --git a/app/models/message.py b/app/models/message.py index 1ac6524..406820c 100644 --- a/app/models/message.py +++ b/app/models/message.py @@ -13,7 +13,7 @@ class Message(Base, TimestampMixin): id = Column(Integer, primary_key=True, index=True) session_id = Column(Integer, ForeignKey("sessions.id", ondelete="CASCADE")) - is_users = Column(Boolean, default=True) + is_user = Column(Boolean, default=True) response_type = Column(Enum(ResponseTypeEnum)) content = Column(Text, nullable=False) diff --git a/app/requirements.txt b/app/requirements.txt index 45dc1fa..5b8f28a 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -9,4 +9,7 @@ python-jose passlib[bcrypt] bcrypt==3.2.0 alembic -openai \ No newline at end of file +openai>=1.3.8,<2.0.0 +boto3 +slowapi +tenacity \ No newline at end of file diff --git a/app/schemas/generated_media.py b/app/schemas/generated_media.py index 63cc821..889055b 100644 --- a/app/schemas/generated_media.py +++ b/app/schemas/generated_media.py @@ -13,7 +13,7 @@ class GeneratedMediaBase(BaseModel): media_type: MediaType image_prompt: Optional[str] = None bgm_prompt: Optional[str] = None - bgm_duration: Optional[str] = None + bgm_duration: Optional[int] = None class GeneratedMediaCreate(GeneratedMediaBase): pass diff --git a/app/schemas/message.py b/app/schemas/message.py index 6116803..34657d4 100644 --- a/app/schemas/message.py +++ b/app/schemas/message.py @@ -9,12 +9,13 @@ class ResponseType(str, Enum): class MessageBase(BaseModel): session_id: int - is_users: bool + is_user: bool response_type: Optional[ResponseType] = None content: str class MessageCreate(MessageBase): - pass + content: str + character_mode: Optional[str] = None class MessageUpdate(BaseModel): content: str diff --git a/app/services/ai/generator.py b/app/services/ai/generator.py new file mode 100644 index 0000000..8dc2f00 --- /dev/null +++ b/app/services/ai/generator.py @@ -0,0 +1,126 @@ +from tenacity import retry, wait_fixed, stop_after_attempt, retry_if_exception_type +from openai import OpenAI, OpenAIError, APIError, RateLimitError, AuthenticationError +import base64 +import uuid +import random +import time +from fastapi import HTTPException +from sqlalchemy.orm import Session + +from core.config import settings +from services.ai.prompts import CHARACTER_PROMPTS, EMOTION_IMAGE_PROMPTS +from models.session import CharacterModeEnum +from models.message import ResponseTypeEnum +from models.emotion import Emotion +from utils.s3 import upload_to_s3 +from utils.timestamp import now_jst + +# OpenAIクライアント初期化 +client = OpenAI(api_key=settings.openai_api_key) + +# AI応答のOpenAI呼び出し(リトライ付き) +@retry( + wait=wait_fixed(10), # 10秒待つ + stop=stop_after_attempt(3), # 最大3回まで試す + retry=retry_if_exception_type(RateLimitError) +) +# キャラモードに応じたAI返答を生成 +def generate_ai_response( + character_mode: CharacterModeEnum, + user_input: str + )->tuple[str, ResponseTypeEnum]: + + if character_mode == CharacterModeEnum.saburo: + prompt_data = CHARACTER_PROMPTS["saburo"] + response_type = None + + elif character_mode == CharacterModeEnum.bijyo: + if random.randint(1, 5) == 1: + prompt_data = CHARACTER_PROMPTS["anger-mom"] + response_type = ResponseTypeEnum.insult + else: + prompt_data = CHARACTER_PROMPTS["bijyo"] + response_type = ResponseTypeEnum.praise + + else: + raise ValueError("無効なキャラクターモードです") + + system_prompt = prompt_data["description"] + "\n" + prompt_data["prompt"] + user_diary = f"ユーザーの日記: {user_input}" + + try: + response = client.chat.completions.create( + model = settings.openai_model, + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_diary} + ] + ) + ai_reply = response.choices[0].message.content.strip() + return ai_reply, response_type + + # except RateLimitError: + # raise HTTPException(status_code=429, detail="APIのレート制限を超えました。しばらくしてから再度お試しください") + except AuthenticationError: + raise HTTPException(status_code=401, detail="OpenAIの認証に失敗しました。APIキーを確認してください") + except APIError: + raise HTTPException(status_code=502, detail="OpenAIサーバでエラーが発生しました") + except Exception as e: + raise HTTPException(status_code=500, detail=f"AI応答の生成中にエラーが発生しました: {str(e)}") + + +# 感情に応じたプロンプトを取得 +def get_prompt_for_emotion( + emotion_id: int, + db: Session +)->str: + emotion = db.get(Emotion,emotion_id) + if not emotion: + raise ValueError("該当する選択肢がありません") + return EMOTION_IMAGE_PROMPTS.get(emotion.name.lower(), "A bright and happy illustration that encourages positivity") + + +# 画像を生成 +def generate_image_bytes(prompt:str)->bytes: + try: + response = client.images.create( + prompt=prompt, + n=1, + size="512x512", + response_format="b64_json" + ) + + return base64.b64decode(response.data[0].b64_json) + + except RateLimitError: + raise HTTPException(status_code=429, detail="APIのレート制限を超えました。しばらくしてから再度お試しください") + except AuthenticationError: + raise HTTPException(status_code=401, detail="OpenAIの認証に失敗しました。APIキーを確認してください") + except APIError: + raise HTTPException(status_code=502, detail="OpenAIサーバでエラーが発生しました") + except Exception as e: + raise HTTPException(status_code=500, detail=f"画像生成中にエラーが発生しました: {str(e)}") + + +# 画像生成してS3にアップロードする +def generate_and_upload_image(prompt: str)->str: + image_bytes = generate_image_bytes(prompt) + unique_filename = f"emotion_{now_jst().strftime('%Y%m%d%H%M%S')}_{uuid.uuid4().hex[:8]}.png" + + return upload_to_s3( + file_bytes=image_bytes, + filename=unique_filename, + content_type="image/png" + ) + + +# 画像のURLを作成 +def generate_emotion_image_url( + emotion_id: int, + db:Session +)->str: + prompt = get_prompt_for_emotion(emotion_id, db) + return generate_and_upload_image(prompt) + + + diff --git a/app/services/ai/characters.py b/app/services/ai/prompts.py similarity index 82% rename from app/services/ai/characters.py rename to app/services/ai/prompts.py index 4645596..fac4673 100644 --- a/app/services/ai/characters.py +++ b/app/services/ai/prompts.py @@ -49,4 +49,13 @@ "語尾は『〜しな』『〜さい』など、自然な口調にしてください。" ) } +} + + +EMOTION_IMAGE_PROMPTS = { + "疲れた": "A peaceful nature scene with soft sunlight and blooming flowers, evoking rest and hope, digital art, soft pastel colors", + "眠い": "A cozy bedroom with warm lighting, a cat sleeping on a bed, peaceful and dreamy atmosphere, digital painting, warm tones", + "イライラする": "A calm lake at sunset, soothing and peaceful atmosphere, realistic landscape, golden hour lighting", + "悲しい": "A beautiful field of sunflowers under a clear sky, uplifting and full of light, watercolor style", + "不安": "A safe cozy space with a cup of tea and soft cushions, calming and reassuring, studio lighting, concept art" } \ No newline at end of file diff --git a/app/services/ai/response.py b/app/services/ai/response.py deleted file mode 100644 index f1ef681..0000000 --- a/app/services/ai/response.py +++ /dev/null @@ -1,43 +0,0 @@ -import openai -import random -from core.config import settings -from services.ai.characters import CHARACTER_PROMPTS -from models.session import CharacterModeEnum -from models.message import ResponseTypeEnum - -openai.api_key = settings.openai_api_key - -# キャラモードに応じたAI返答を生成。 -def generate_ai_response( - character_mode: CharacterModeEnum, - user_input: str - )->tuple[str, ResponseTypeEnum]: - - if character_mode == CharacterModeEnum.saburo: - prompt_data = CHARACTER_PROMPTS["saburo"] - response_type = None - - elif character_mode == CharacterModeEnum.bijyo: - if random.randint(1, 5) == 1: - prompt_data = CHARACTER_PROMPTS["anger-mom"] - response_type = ResponseTypeEnum.insult - else: - prompt_data = CHARACTER_PROMPTS["bijyo"] - response_type = ResponseTypeEnum.praise - - else: - raise ValueError("無効なキャラクターモードです") - - system_prompt = prompt_data["description"] + "\n" + prompt_data["prompt"] - user_diary = f"ユーザーの日記: {user_input}" - - response = openai.ChatCompletion.create( - model = settings.openai_model, - messages = [ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": user_diary} - ] - ) - ai_reply = response.choices[0].message.content.strip() - return ai_reply, response_type - diff --git a/app/services/message.py b/app/services/message.py index a69a4e2..93e864f 100644 --- a/app/services/message.py +++ b/app/services/message.py @@ -4,7 +4,33 @@ from models.message import Message, ResponseTypeEnum from models.session import Session as SessionModel, CharacterModeEnum from schemas.message import MessageResponse -from services.ai.response import generate_ai_response +from services.ai.generator import generate_ai_response + + +# テスト用-日記を保存 +def create_message( + db: Session, + session_id: int, + content: str, +) -> MessageResponse: + # ユーザーの日記を保存 + user_message = Message( + session_id=session_id, + content=content, + is_user=True + ) + db.add(user_message) + db.commit() + db.refresh(user_message) + + # セッションを取得してモードを確認 + session = db.query(SessionModel).filter(SessionModel.id == session_id).first() + if session is None: + raise HTTPException(status_code=404, detail="チャットが見つかりません") + character_mode = session.character_mode + + return MessageResponse.from_orm(user_message), character_mode + @@ -18,7 +44,7 @@ def create_message_with_ai( user_message = Message( session_id=session_id, content=content, - is_users=True + is_user=True ) db.add(user_message) db.commit() @@ -40,7 +66,7 @@ def create_message_with_ai( ai_message = Message( session_id=session_id, content=ai_reply, - is_users=False, + is_user=False, response_type=response_type ) db.add(ai_message) @@ -59,7 +85,7 @@ def get_messages_by_session(db: Session, session_id: int) -> List[MessageRespons # メッセージを更新 def update_user_message(db: Session, message_id: int, new_content: str) -> MessageResponse: - db_message = db.query(Message).filter(Message.id == message_id, Message.is_users == True).first() + db_message = db.query(Message).filter(Message.id == message_id, Message.is_user == True).first() if db_message is None: raise HTTPException(status_code=404, detail="該当するメッセージが見つかりません") # もしくはreturn None db_message.content = new_content diff --git a/app/utils/s3.py b/app/utils/s3.py new file mode 100644 index 0000000..789605e --- /dev/null +++ b/app/utils/s3.py @@ -0,0 +1,28 @@ +# utils/s3.py +import boto3 +import uuid +from core.config import settings + + +# S3操作するための設定 +s3_client = boto3.client( + "s3", + aws_access_key_id=settings.aws_access_key_id, + aws_secret_access_key=settings.aws_secret_access_key, + region_name=settings.aws_region +) + +def upload_to_s3( + file_bytes: bytes, + filename: str, + content_type: str +)->str: + key = f"generated/{uuid.uuid4()}_{filename}" + s3_client.put_object( + Bucket=settings.aws_s3_bucket_name, + Key=key, + Body=file_bytes, + ContentType=content_type, + ACL="public-read" + ) + return f"https://{settings.aws_s3_bucket_name}.s3.{settings.aws_region}.amazonaws.com/{key}" \ No newline at end of file From 02d5e3ff2adf65445a7e70fbcf1dcc55a4f5be89 Mon Sep 17 00:00:00 2001 From: sasakifuruta Date: Tue, 6 May 2025 18:09:58 +0900 Subject: [PATCH 33/72] =?UTF-8?q?AI=E3=83=AC=E3=82=B9=E3=83=9D=E3=83=B3?= =?UTF-8?q?=E3=82=B9=E8=BF=94=E7=AD=94=E3=80=80=E5=AE=9F=E8=A3=85=E9=80=94?= =?UTF-8?q?=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/endpoints/message.py | 28 ++++++++++++---------------- app/api/endpoints/session.py | 4 ++-- app/core/config.py | 8 ++++---- app/schemas/message.py | 3 +-- app/services/ai/generator.py | 9 ++++++++- app/services/message.py | 6 ++++-- app/utils/s3.py | 12 ++++++------ 7 files changed, 37 insertions(+), 33 deletions(-) diff --git a/app/api/endpoints/message.py b/app/api/endpoints/message.py index 4cc3b3a..68407da 100644 --- a/app/api/endpoints/message.py +++ b/app/api/endpoints/message.py @@ -33,23 +33,19 @@ async def create_message( ).first() if not session: - if message_data.character_mode is None: - raise HTTPException(status_code=400, detail="新規チャットの場合はcharacter_modeが必要です") - - # セッションを新規作成 - session = SessionModel( - character_mode = message_data.character_mode, - # user_id = current_user.id - user_id = 1 # テスト用 - ) - db.add(session) - db.commit() - db.refresh(session) + raise HTTPException(status_code=404, detail="チャットが見つかりません") + print(f"session_id>>{session_id}") - return create_user_message(db, session_id, content=message_data.content) #テスト - - - # return create_message_with_ai(db, session_id, message_data.content) + # return create_user_message(db, session_id, content=message_data.content) #テスト + + try: + return await create_message_with_ai(db, session_id, message_data.content) + except HTTPException as e: + print(f"HTTPエラー: {e.detail}") + raise e + except Exception as e: + print(f"未処理エラー:{e}") + raise HTTPException(status_code=500, detail="サーバ内部エラー") # 特定のチャットの全メッセージを取得 diff --git a/app/api/endpoints/session.py b/app/api/endpoints/session.py index 5eb1438..708441c 100644 --- a/app/api/endpoints/session.py +++ b/app/api/endpoints/session.py @@ -15,11 +15,11 @@ async def create_session( session_data: SessionCreate, db: Session = Depends(get_db), - # current_user: User = Depends(get_current_user) + # current_user: User = Depends(get_current_user) # テスト ): new_session = SessionModel( character_mode=session_data.character_mode, - # user_id=current_user.id + # user_id=current_user.id # テスト user_id=1 ) db.add(new_session) diff --git a/app/core/config.py b/app/core/config.py index 2c202c1..bcc4d14 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -14,10 +14,10 @@ class Settings(BaseSettings): openai_model: str ="gpt-4.1-nano" # AWS S3 - aws_access_key_id: str - aws_secret_access_key: str - aws_s3_bucket_name: str - aws_region: str = "ap-northeast-1" + # aws_access_key_id: str + # aws_secret_access_key: str + # aws_s3_bucket_name: str + # aws_region: str = "ap-northeast-1" # Database diff --git a/app/schemas/message.py b/app/schemas/message.py index 34657d4..b06ca4d 100644 --- a/app/schemas/message.py +++ b/app/schemas/message.py @@ -14,8 +14,7 @@ class MessageBase(BaseModel): content: str class MessageCreate(MessageBase): - content: str - character_mode: Optional[str] = None + pass class MessageUpdate(BaseModel): content: str diff --git a/app/services/ai/generator.py b/app/services/ai/generator.py index 8dc2f00..5d02331 100644 --- a/app/services/ai/generator.py +++ b/app/services/ai/generator.py @@ -29,6 +29,11 @@ def generate_ai_response( character_mode: CharacterModeEnum, user_input: str )->tuple[str, ResponseTypeEnum]: + + # ===テスト + # models = client.models.list() + # print([m.id for m in models.data]) + # ====確認用 if character_mode == CharacterModeEnum.saburo: prompt_data = CHARACTER_PROMPTS["saburo"] @@ -63,9 +68,11 @@ def generate_ai_response( # raise HTTPException(status_code=429, detail="APIのレート制限を超えました。しばらくしてから再度お試しください") except AuthenticationError: raise HTTPException(status_code=401, detail="OpenAIの認証に失敗しました。APIキーを確認してください") - except APIError: + except APIError as e: + print(f"OpenAIサーバでエラー: {e}") raise HTTPException(status_code=502, detail="OpenAIサーバでエラーが発生しました") except Exception as e: + print(f"AI生成中のエラー: {e}") raise HTTPException(status_code=500, detail=f"AI応答の生成中にエラーが発生しました: {str(e)}") diff --git a/app/services/message.py b/app/services/message.py index 93e864f..e5aa3fa 100644 --- a/app/services/message.py +++ b/app/services/message.py @@ -1,5 +1,6 @@ from fastapi import HTTPException from sqlalchemy.orm import Session +from starlette.concurrency import run_in_threadpool from typing import List from models.message import Message, ResponseTypeEnum from models.session import Session as SessionModel, CharacterModeEnum @@ -35,7 +36,7 @@ def create_message( # 日記を保存してキャラクターの返答を生成 -def create_message_with_ai( +async def create_message_with_ai( db: Session, session_id: int, content: str, @@ -57,7 +58,8 @@ def create_message_with_ai( character_mode = session.character_mode # AI返答を生成 - ai_reply, response_type = generate_ai_response( + ai_reply, response_type = await run_in_threadpool( + generate_ai_response, character_mode=character_mode, user_input=content ) diff --git a/app/utils/s3.py b/app/utils/s3.py index 789605e..5fe5241 100644 --- a/app/utils/s3.py +++ b/app/utils/s3.py @@ -5,12 +5,12 @@ # S3操作するための設定 -s3_client = boto3.client( - "s3", - aws_access_key_id=settings.aws_access_key_id, - aws_secret_access_key=settings.aws_secret_access_key, - region_name=settings.aws_region -) +# s3_client = boto3.client( +# "s3", +# aws_access_key_id=settings.aws_access_key_id, +# aws_secret_access_key=settings.aws_secret_access_key, +# region_name=settings.aws_region +# ) def upload_to_s3( file_bytes: bytes, From 3768dc62f4d33fbca60431b51728df37ce1aa98e Mon Sep 17 00:00:00 2001 From: sasakifuruta Date: Fri, 9 May 2025 00:27:59 +0900 Subject: [PATCH 34/72] =?UTF-8?q?AWS=20Bedrock=E7=94=A8=E3=81=AEAI?= =?UTF-8?q?=E8=BF=94=E7=AD=94=E7=94=9F=E6=88=90=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/core/config.py | 7 +++ app/services/ai/generator.py | 115 ++++++++++++++++++++++++++++++++--- app/services/message.py | 10 ++- app/utils/s3.py | 1 + 4 files changed, 122 insertions(+), 11 deletions(-) diff --git a/app/core/config.py b/app/core/config.py index bcc4d14..12af55f 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -13,6 +13,13 @@ class Settings(BaseSettings): # OpenAI Model openai_model: str ="gpt-4.1-nano" + + # AWS Bedrock + MODEL_ID : str ="anthropic.claude-instant-v1" + REGION : str ="ap-northeast-1" + + + # AWS S3 # aws_access_key_id: str # aws_secret_access_key: str diff --git a/app/services/ai/generator.py b/app/services/ai/generator.py index 5d02331..382eb67 100644 --- a/app/services/ai/generator.py +++ b/app/services/ai/generator.py @@ -1,9 +1,12 @@ from tenacity import retry, wait_fixed, stop_after_attempt, retry_if_exception_type +from botocore.exceptions import NoCredentialsError, ClientError, EndpointConnectionError from openai import OpenAI, OpenAIError, APIError, RateLimitError, AuthenticationError import base64 import uuid import random -import time +import boto3 +import json +import re from fastapi import HTTPException from sqlalchemy.orm import Session @@ -15,8 +18,103 @@ from utils.s3 import upload_to_s3 from utils.timestamp import now_jst + +# ============================================ +# AWS Bedrock(Claude Instantモデル)版AI応答生成 +# =========================================== + +bedrock = boto3.client("bedrock-runtime", region_name=settings.REGION) + +# 起動時 or 初回リスエスト時に接続確認 +def verify_bedrock_connection(): + try: + mgmt_client = boto3.client("bedrock", region_name=settings.REGION) + response = mgmt_client.list_foundation_models() + print(f"✅ Bedrock接続確認:利用可能モデル数 = {len(response['modelSummaries'])}") + except NoCredentialsError: + raise RuntimeError("❌ AWS認証情報が見つかりません。") + except EndpointConnectionError: + raise RuntimeError("❌ Bedrock エンドポイントに接続できません。region_name=settings.REGION を確認してください。") + except ClientError as e: + raise RuntimeError(f"❌ Bedrockクライアントエラー: {e}") + except Exception as e: + raise RuntimeError(f"❌ Bedrock接続失敗: {e}") + + +# アプリ起動時にBedrock接続確認 +verify_bedrock_connection() + +@retry( # 関数失敗時にリトライ + wait=wait_fixed(10), # 10秒まつ + stop=stop_after_attempt(3) # 最大3回試行 + ) +def generate_ai_response_via_bedrock( + character_mode: CharacterModeEnum, + user_input: str + )->tuple[str, ResponseTypeEnum]: + + if character_mode == CharacterModeEnum.saburo: + prompt_data = CHARACTER_PROMPTS["saburo"] + response_type = None + + elif character_mode == CharacterModeEnum.bijyo: + if random.randint(1, 5) == 1: + prompt_data = CHARACTER_PROMPTS["anger-mom"] + response_type = ResponseTypeEnum.insult + else: + prompt_data = CHARACTER_PROMPTS["bijyo"] + response_type = ResponseTypeEnum.praise + + else: + raise ValueError("無効なキャラクターモードです") + + # Claude形式のプロンプトを作成 + prompt = f"Human: {prompt_data['description']}\n{prompt_data['prompt']}\nユーザーの日記:{user_input}\nAssistant:" + print(f"Bedrockへのプロンプト>>>{prompt}") + + try: + response = bedrock.invoke_model( + modelId = settings.MODEL_ID, + body = json.dumps({ + "prompt": prompt, + "max_tokens_to_sample": 128, # 最大出力トークン(1文字=約1.5トークン。50文字の出力を想定) + "temperature": 0.7, # 返答の自由さ(1に近いほど自由) + "stop_sequences": ["\n\n", "Human", "ユーザー:"] # AIの出力を終了する区切り + }), + contentType ="application/json", + accept = "application/json" + ) + response_body = json.loads(response["body"].read()) + bedrock_reply = response_body["completion"].strip() + + # 句点で切る + ai_reply = stop_generate_sentence(bedrock_reply) + + return ai_reply, response_type + + except bedrock.exceptions.AccessDeniedException: + raise HTTPException(status_code=403, detail="Bedrockへのアクセスが拒否されました") + except Exception as e: + print(f"Bedrockエラー: {e}") + raise HTTPException(status_code=500, detail=f"AI応答の生成中にエラーが発生しました: {str(e)}") + + +# Bedrockの出力を句点で切る +def stop_generate_sentence(text: str)-> str: + # (。!?)までを残す + match = re.search(r'[。!?](?!.*[。!?])', text) + if match: + return text[:match.end()] + return text + + + +# ========================= +# OpenAIキー版 AI応答生成 +# ========================= + # OpenAIクライアント初期化 -client = OpenAI(api_key=settings.openai_api_key) +# client = OpenAI(api_key=settings.openai_api_key) # AI応答のOpenAI呼び出し(リトライ付き) @retry( @@ -29,11 +127,6 @@ def generate_ai_response( character_mode: CharacterModeEnum, user_input: str )->tuple[str, ResponseTypeEnum]: - - # ===テスト - # models = client.models.list() - # print([m.id for m in models.data]) - # ====確認用 if character_mode == CharacterModeEnum.saburo: prompt_data = CHARACTER_PROMPTS["saburo"] @@ -64,8 +157,7 @@ def generate_ai_response( ai_reply = response.choices[0].message.content.strip() return ai_reply, response_type - # except RateLimitError: - # raise HTTPException(status_code=429, detail="APIのレート制限を超えました。しばらくしてから再度お試しください") + except AuthenticationError: raise HTTPException(status_code=401, detail="OpenAIの認証に失敗しました。APIキーを確認してください") except APIError as e: @@ -76,6 +168,11 @@ def generate_ai_response( raise HTTPException(status_code=500, detail=f"AI応答の生成中にエラーが発生しました: {str(e)}") + +# ============================= +# 追加機能ー画像生成(実現見込み薄い) +# ============================= + # 感情に応じたプロンプトを取得 def get_prompt_for_emotion( emotion_id: int, diff --git a/app/services/message.py b/app/services/message.py index e5aa3fa..603249c 100644 --- a/app/services/message.py +++ b/app/services/message.py @@ -5,7 +5,7 @@ from models.message import Message, ResponseTypeEnum from models.session import Session as SessionModel, CharacterModeEnum from schemas.message import MessageResponse -from services.ai.generator import generate_ai_response +from services.ai.generator import generate_ai_response, generate_ai_response_via_bedrock # テスト用-日記を保存 @@ -50,6 +50,7 @@ async def create_message_with_ai( db.add(user_message) db.commit() db.refresh(user_message) + print("ユーザーの日記を保存") # セッションを取得してモードを確認 session = db.query(SessionModel).filter(SessionModel.id == session_id).first() @@ -58,8 +59,13 @@ async def create_message_with_ai( character_mode = session.character_mode # AI返答を生成 + # ai_reply, response_type = await run_in_threadpool( + # # generate_ai_response, + # character_mode=character_mode, + # user_input=content + # ) ai_reply, response_type = await run_in_threadpool( - generate_ai_response, + generate_ai_response_via_bedrock, character_mode=character_mode, user_input=content ) diff --git a/app/utils/s3.py b/app/utils/s3.py index 5fe5241..a9fa9bc 100644 --- a/app/utils/s3.py +++ b/app/utils/s3.py @@ -1,3 +1,4 @@ +# TODO::削除予定 # utils/s3.py import boto3 import uuid From d388e3d4a495c4042e079341c74f8bff7f972995 Mon Sep 17 00:00:00 2001 From: free-honda <154935293+free-honda@users.noreply.github.com> Date: Sun, 11 May 2025 00:52:30 +0900 Subject: [PATCH 35/72] Rename Dockerfile.prd to Dockerfile.prod --- Dockerfile.prd => Dockerfile.prod | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Dockerfile.prd => Dockerfile.prod (100%) diff --git a/Dockerfile.prd b/Dockerfile.prod similarity index 100% rename from Dockerfile.prd rename to Dockerfile.prod From 075dfeebb1d4728340c2ca27683d56ffcb63b00a Mon Sep 17 00:00:00 2001 From: free-honda <154935293+free-honda@users.noreply.github.com> Date: Sun, 11 May 2025 00:56:21 +0900 Subject: [PATCH 36/72] Update Dockerfile.prod --- Dockerfile.prod | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile.prod b/Dockerfile.prod index c1eff8f..b877399 100644 --- a/Dockerfile.prod +++ b/Dockerfile.prod @@ -12,9 +12,10 @@ RUN pip install --no-cache-dir -r requirements.txt # アプリケーション本体をすべてコンテナにコピー(app ディレクトリの中身を /app にコピー) COPY app/ . +ENV PYTHONPATH=/app # アプリケーションで使用するポートを開放(FastAPI や uvicorn のデフォルトに近い 8000 番を使用) EXPOSE 8000 # コンテナ起動時に実行されるコマンドを指定(uvicorn で main.py 内の app を起動) -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--log-level", "info"] +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--log-level", "info"] From cddcc757e66b6d5c463ce847f40416899b7243f0 Mon Sep 17 00:00:00 2001 From: free-honda <154935293+free-honda@users.noreply.github.com> Date: Sun, 11 May 2025 01:03:18 +0900 Subject: [PATCH 37/72] Create dev.yml --- .github/workflows/dev.yml | 48 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .github/workflows/dev.yml diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml new file mode 100644 index 0000000..c0424d1 --- /dev/null +++ b/.github/workflows/dev.yml @@ -0,0 +1,48 @@ +name: Push to ECR + +on: + push: + branches: + - develop + workflow_dispatch: + +jobs: + build-and-push: + runs-on: ubuntu-latest + + steps: + # リポジトリのクローン + - name: Checkout code + uses: actions/checkout@v3 + + # AWS CLI のセットアップ + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v3 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_REGION }} + + # Dockerログイン + - name: Log in to Amazon ECR + run: | + aws ecr get-login-password --region ${{ secrets.AWS_REGION }} \ + | docker login --username AWS --password-stdin \ + $(aws sts get-caller-identity --query 'Account' --output text).dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com + + # Dockerイメージのビルド + - name: Build Docker image + run: | + docker build -t ${{ secrets.ECR_REPOSITORY }}:${{ secrets.IMAGE_TAG }} -f Dockerfile.prod . + + # Dockerイメージのタグ付け + - name: Tag Docker image + run: | + docker tag ${{ secrets.ECR_REPOSITORY }}:${{ secrets.IMAGE_TAG }} \ + $(aws sts get-caller-identity --query 'Account' --output text).dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com/${{ secrets.ECR_REPOSITORY }}:${{ secrets.IMAGE_TAG }} + + # Dockerイメージのプッシュ + - name: Push Docker image to Amazon ECR + run: | + docker push \ + $(aws sts get-caller-identity --query 'Account' --output text).dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com/${{ secrets.ECR_REPOSITORY }}:${{ secrets.IMAGE_TAG }} From 9f9c5a084dcf2183d88bd2d1ab77ca0ee6c07268 Mon Sep 17 00:00:00 2001 From: sasakifuruta Date: Sun, 11 May 2025 15:32:27 +0900 Subject: [PATCH 38/72] =?UTF-8?q?=E6=97=A5=E8=A8=98=E6=8A=95=E7=A8=BF?= =?UTF-8?q?=E3=82=921=E6=97=A5=EF=BC=91=E5=9B=9E=E3=81=AB=E5=88=B6?= =?UTF-8?q?=E9=99=90=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/endpoints/message.py | 2 +- app/api/endpoints/session.py | 33 ++++++++++++++++++++++++++++----- app/services/ai/generator.py | 1 - 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/app/api/endpoints/message.py b/app/api/endpoints/message.py index 68407da..6814168 100644 --- a/app/api/endpoints/message.py +++ b/app/api/endpoints/message.py @@ -36,7 +36,7 @@ async def create_message( raise HTTPException(status_code=404, detail="チャットが見つかりません") print(f"session_id>>{session_id}") - # return create_user_message(db, session_id, content=message_data.content) #テスト + return create_user_message(db, session_id, content=message_data.content) #テスト try: return await create_message_with_ai(db, session_id, message_data.content) diff --git a/app/api/endpoints/session.py b/app/api/endpoints/session.py index 708441c..c58eff0 100644 --- a/app/api/endpoints/session.py +++ b/app/api/endpoints/session.py @@ -1,12 +1,16 @@ +from datetime import datetime, timedelta +from typing import Optional + from fastapi import APIRouter, HTTPException, Depends from sqlalchemy.orm import Session from models import Session as SessionModel -from models import Favorite, User +from models import User from schemas.session import SessionCreate, SessionUpdate, SessionResponse, SessionWithMessagesResponse, SessionSummaryResponse from core.database import get_db from utils.auth import get_current_user +from utils.timestamp import now_jst from services.session import get_sessions_with_first_message, toggle_favorite_session -from typing import Optional + router = APIRouter() @@ -15,12 +19,31 @@ async def create_session( session_data: SessionCreate, db: Session = Depends(get_db), - # current_user: User = Depends(get_current_user) # テスト + # current_user: User = Depends(get_current_user) # テスト ): + + # user_id=current_user.id # テスト + user_id=1 + today = now_jst().date() + + # 当日のセッションを確認 + existing_session = db.query(SessionModel).filter( + SessionModel.user_id == user_id, + SessionModel.created_at >= datetime.combine(today, datetime.min.time()), + SessionModel.created_at <= datetime.combine(today, datetime.max.time()) + ).first() + + if existing_session: + # 1日1回制限:エラーメッセージで伝える + raise HTTPException( + status_code=403, + detail="今日はすでにチャットを開始しています。明日またご利用ください。" + ) + + new_session = SessionModel( character_mode=session_data.character_mode, - # user_id=current_user.id # テスト - user_id=1 + user_id=user_id ) db.add(new_session) db.commit() diff --git a/app/services/ai/generator.py b/app/services/ai/generator.py index 382eb67..4ec4747 100644 --- a/app/services/ai/generator.py +++ b/app/services/ai/generator.py @@ -70,7 +70,6 @@ def generate_ai_response_via_bedrock( # Claude形式のプロンプトを作成 prompt = f"Human: {prompt_data['description']}\n{prompt_data['prompt']}\nユーザーの日記:{user_input}\nAssistant:" - print(f"Bedrockへのプロンプト>>>{prompt}") try: response = bedrock.invoke_model( From dc69f2a59d5de0e346e1db88cf1e15f80926c71b Mon Sep 17 00:00:00 2001 From: sasakifuruta Date: Sun, 11 May 2025 15:38:22 +0900 Subject: [PATCH 39/72] =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E9=83=A8?= =?UTF-8?q?=E5=88=86=E3=82=92=E3=82=B3=E3=83=A1=E3=83=B3=E3=83=88=E3=82=A2?= =?UTF-8?q?=E3=82=A6=E3=83=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/endpoints/message.py | 8 ++++---- app/services/message.py | 8 ++------ 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/app/api/endpoints/message.py b/app/api/endpoints/message.py index 6814168..c2d7c66 100644 --- a/app/api/endpoints/message.py +++ b/app/api/endpoints/message.py @@ -24,19 +24,19 @@ async def create_message( session_id: int, message_data: MessageCreate, db: Session = Depends(get_db), - # current_user: User = Depends(get_current_user) + current_user: User = Depends(get_current_user) ): session = db.query(SessionModel).filter( SessionModel.id == session_id, - # SessionModel.user_id == current_user.id - SessionModel.user_id == 1 # テスト + SessionModel.user_id == current_user.id + # SessionModel.user_id == 1 # テスト ).first() if not session: raise HTTPException(status_code=404, detail="チャットが見つかりません") print(f"session_id>>{session_id}") - return create_user_message(db, session_id, content=message_data.content) #テスト + # return create_user_message(db, session_id, content=message_data.content) #テスト try: return await create_message_with_ai(db, session_id, message_data.content) diff --git a/app/services/message.py b/app/services/message.py index 603249c..4bb101b 100644 --- a/app/services/message.py +++ b/app/services/message.py @@ -59,13 +59,9 @@ async def create_message_with_ai( character_mode = session.character_mode # AI返答を生成 - # ai_reply, response_type = await run_in_threadpool( - # # generate_ai_response, - # character_mode=character_mode, - # user_input=content - # ) ai_reply, response_type = await run_in_threadpool( - generate_ai_response_via_bedrock, + # generate_ai_response, # Open API用 + # generate_ai_response_via_bedrock, # Amazon Bedrock用 character_mode=character_mode, user_input=content ) From 6177c52149fffbe7a67d9ce77f7b628fb9473fb4 Mon Sep 17 00:00:00 2001 From: honda Date: Sun, 11 May 2025 17:03:59 +0900 Subject: [PATCH 40/72] =?UTF-8?q?chore:=20=E7=92=B0=E5=A2=83=E5=A4=89?= =?UTF-8?q?=E6=95=B0=E9=96=A2=E9=80=A3=E3=81=AE=E8=A8=AD=E5=AE=9A=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/endpoints/favorite.py | 20 ++++++++++++++++++++ app/core/config.py | 33 ++++++++++++++------------------- app/core/database.py | 9 +++++---- app/db/session.py | 6 ++++-- app/main.py | 33 ++++++++++++++++----------------- app/migrations/env.py | 9 +++++---- 6 files changed, 64 insertions(+), 46 deletions(-) create mode 100644 app/api/endpoints/favorite.py diff --git a/app/api/endpoints/favorite.py b/app/api/endpoints/favorite.py new file mode 100644 index 0000000..e24783e --- /dev/null +++ b/app/api/endpoints/favorite.py @@ -0,0 +1,20 @@ +from fastapi import APIRouter, HTTPException, Depends +from sqlalchemy.orm import Session +from app.models import Favorite, Session as DBSession +from app.schemas.favorite import FavoriteCreate, FavoriteResponse +from app.core.database import get_db + +@router.post("/favorites") +async def create_favorite(favorite_data: FavoriteCreate, db: Session = Depends(get_db)): + session = db.query(DBSession).filter(DBSession.id == favorite_data.session_id).first() + if not session: + raise HTTPException(status_code=404, detail="お気に入り登録が見つかりません。") + favorite = Favorite(session_id=favorite_data.session_id, user_id=favorite_data.user_id) + db.add(favorite) + db.commit() + return favorite + +@router.get("/favorites") +async def get_favorites(user_id: int, db: Session = Depends(get_db)): + favorites = db.query(Favorite).filter(Favorite.user_id == user_id).all() + return favorites \ No newline at end of file diff --git a/app/core/config.py b/app/core/config.py index 12af55f..571a8aa 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -1,31 +1,26 @@ +import os from pydantic_settings import BaseSettings +# ローカル環境のみ .env を読み込む +if os.getenv("ENV") != "production": + from dotenv import load_dotenv + load_dotenv() + class Settings(BaseSettings): SECRET_KEY: str ALGORITHM: str = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 - + # PIN code ADMIN_PIN_CODE: str - # OpenAI API key + # OpenAI openai_api_key: str - # OpenAI Model - openai_model: str ="gpt-4.1-nano" - - - # AWS Bedrock - MODEL_ID : str ="anthropic.claude-instant-v1" - REGION : str ="ap-northeast-1" - - - - # AWS S3 - # aws_access_key_id: str - # aws_secret_access_key: str - # aws_s3_bucket_name: str - # aws_region: str = "ap-northeast-1" + openai_model: str = "gpt-4.1-nano" + # AWS Bedrock + MODEL_ID: str = "anthropic.claude-instant-v1" + REGION: str = "ap-northeast-1" # Database mysql_root_password: str @@ -36,6 +31,6 @@ class Settings(BaseSettings): admin_pin_code: int class Config: - env_file = ".env" + env_file = ".env" # ローカルで .env を参照するように設定 -settings = Settings() \ No newline at end of file +settings = Settings() diff --git a/app/core/database.py b/app/core/database.py index 74eef76..7229adf 100644 --- a/app/core/database.py +++ b/app/core/database.py @@ -1,11 +1,12 @@ -from dotenv import load_dotenv import os +from dotenv import load_dotenv from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker, declarative_base from core.config import settings -# .envを読み込む -load_dotenv() +# 本番以外のときだけ .env を読み込む +if os.getenv("ENV") != "production": + load_dotenv() DATABASE_URL = os.getenv("DATABASE_URL") @@ -18,4 +19,4 @@ def get_db(): try: yield db finally: - db.close() \ No newline at end of file + db.close() diff --git a/app/db/session.py b/app/db/session.py index 14dcb10..51fa802 100644 --- a/app/db/session.py +++ b/app/db/session.py @@ -3,13 +3,15 @@ from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker, Session from fastapi import Depends -from dotenv import load_dotenv from models import Base +# ローカル開発環境のみ .env を読み込む +if os.getenv("ENV") != "production": + from dotenv import load_dotenv + load_dotenv() DATABASE_URL = os.getenv("DATABASE_URL") - # MySQL用:connect_argsなしでエンジン作成 engine = create_engine(DATABASE_URL) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) diff --git a/app/main.py b/app/main.py index 88887b1..a71931f 100644 --- a/app/main.py +++ b/app/main.py @@ -32,22 +32,21 @@ def read_root(): return {"message": "Hello from FastAPI + MySQL in Docker!"} -@app.get("/db-status") -def db_status(): - try: - connection = mysql.connector.connect( - host="db", # ← Dockerコンテナ名 - user=os.getenv("MYSQL_USER"), - password=os.getenv("MYSQL_PASSWORD"), - database=os.getenv("MYSQL_DATABASE") - ) - if connection.is_connected(): - connection.close() # ✅ 接続が確認できたら明示的にクローズ - return {"db_status": "connected"} - else: - return {"db_status": "not connected"} - except Exception as e: - return {"db_status": "error", "details": str(e)} - +# @app.get("/db-status") +# def db_status(): +# try: +# connection = mysql.connector.connect( +# host="db", # ← Dockerコンテナ名 +# user=os.getenv("MYSQL_USER"), +# password=os.getenv("MYSQL_PASSWORD"), +# database=os.getenv("MYSQL_DATABASE") +# ) +# if connection.is_connected(): +# connection.close() # ✅ 接続が確認できたら明示的にクローズ +# return {"db_status": "connected"} +# else: +# return {"db_status": "not connected"} +# except Exception as e: +# return {"db_status": "error", "details": str(e)} app.include_router(api_router, prefix="/api") \ No newline at end of file diff --git a/app/migrations/env.py b/app/migrations/env.py index ca3e7b6..9897454 100644 --- a/app/migrations/env.py +++ b/app/migrations/env.py @@ -1,17 +1,18 @@ from logging.config import fileConfig import os , sys from pathlib import Path - from sqlalchemy import engine_from_config, pool - from alembic import context -from dotenv import load_dotenv # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config -load_dotenv() +# .envはローカル環境のみ読み込む +if os.getenv("ENV") != "production": + from dotenv import load_dotenv + load_dotenv() + database_url = os.getenv("DATABASE_URL") if database_url is None: raise ValueError("DATABASE_URLの環境変数が見つかりません。") From 1413743eedcf18f4639ca0c491a719fd8521acfb Mon Sep 17 00:00:00 2001 From: sasakifuruta Date: Thu, 15 May 2025 01:20:53 +0900 Subject: [PATCH 41/72] =?UTF-8?q?=E7=AE=A1=E7=90=86=E8=80=85=E7=94=BB?= =?UTF-8?q?=E9=9D=A2=E3=81=AE=E5=87=A6=E7=90=86OK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/endpoints/admin.py | 39 +++++++++++++------------------ app/api/endpoints/session.py | 6 ++--- app/api/endpoints/user.py | 14 ++++++++--- app/schemas/message.py | 15 ++++++++++-- app/schemas/user.py | 3 +-- app/services/session.py | 45 ++++++++++++++++++++++++++++++++---- 6 files changed, 85 insertions(+), 37 deletions(-) diff --git a/app/api/endpoints/admin.py b/app/api/endpoints/admin.py index 9f7a5b0..8361025 100644 --- a/app/api/endpoints/admin.py +++ b/app/api/endpoints/admin.py @@ -1,10 +1,12 @@ from fastapi import APIRouter, HTTPException, Depends -from sqlalchemy.orm import Session -from datetime import timedelta +from sqlalchemy.orm import Session, joinedload +from datetime import datetime, timedelta from models import User, Message from schemas.user import UserRegister, UserLogin, UserUpdate, UserResponse, TokenResponse +from schemas.message import AdminMessageResponse from core.config import settings from core.database import get_db +from services.session import get_all_sessions_with_first_message from utils.auth import hash_password, verify_password, create_access_token, get_current_admin_user from utils.timestamp import now_jst @@ -51,8 +53,8 @@ async def login_admin( if str(admin_data.pin_code) != settings.ADMIN_PIN_CODE: raise HTTPException(status_code=400, detail="PINコードが正しくありません") - token = create_access_token({"sub": str(admin_data.id)}) - return {"token": token, "is_admin": admin_data.is_admin} + token = create_access_token({"sub": str(admin.id)}) + return {"token": token, "is_admin": admin.is_admin} # 管理者アカウント情報取得 @@ -69,11 +71,11 @@ async def get_all_users( db: Session = Depends(get_db), current_admin: User = Depends(get_current_admin_user) ): - users = db.query(User).filter(User.is_admin==False).all() - now = now_jst() + users = db.query(User).filter(User.is_admin==False).all() + now = datetime.now() return [ UserResponse( - user_id=user.id, + id=user.id, email=user.email, user_name=user.user_name, is_active=user.is_active, @@ -167,19 +169,10 @@ async def update_user( return {"message": "ユーザー情報が更新されました"} -# TODO:削除?:投稿一覧 -# @router.get("/messages") -# async def get_messages(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)): -# messages = db.query(Message).all() -# return messages - -# TODO:設定変更 -# @router.patch("/settings") -# async def update_settings( -# new_message: str, -# db: Session = Depends(get_db), -# current_admin: User = Depends(get_current_admin_user)): -# settings = db.query().first() -# settings.support_message = new_message -# db.commit() -# return {"message": "設定が更新されました。"} \ No newline at end of file +# 投稿一覧 +@router.get("/messages", response_model=list[AdminMessageResponse]) +async def get_messages( + db: Session = Depends(get_db), + current_admin: User = Depends(get_current_admin_user) + ): + return get_all_sessions_with_first_message(db) \ No newline at end of file diff --git a/app/api/endpoints/session.py b/app/api/endpoints/session.py index c58eff0..a8a0b1d 100644 --- a/app/api/endpoints/session.py +++ b/app/api/endpoints/session.py @@ -19,11 +19,11 @@ async def create_session( session_data: SessionCreate, db: Session = Depends(get_db), - # current_user: User = Depends(get_current_user) # テスト + current_user: User = Depends(get_current_user) ): - # user_id=current_user.id # テスト - user_id=1 + user_id=current_user.id + # user_id=1 today = now_jst().date() # 当日のセッションを確認 diff --git a/app/api/endpoints/user.py b/app/api/endpoints/user.py index c57e79b..091908f 100644 --- a/app/api/endpoints/user.py +++ b/app/api/endpoints/user.py @@ -22,18 +22,26 @@ async def update_my_account( db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): + print(f"更新前current_user>>{current_user.user_name}") + print(f"リクエスト>>{user_update}") + # ユーザ名の更新 + if user_update.user_name and user_update.user_name != current_user.user_name: + current_user.user_name = user_update.user_name + # メールアドレスの更新 if user_update.email and user_update.email != current_user.email: # 他のユーザと重複していないかチェック if db.query(User).filter_by(email=user_update.email).first(): raise HTTPException(status_code=400, detail="このメールアドレスはすでに使われています") current_user.email = user_update.email - - if user_update.user_name: - current_user.user_name = user_update.user_name + + # パスワードの更新 + if user_update.password: + current_user.password_hash = hash_password(user_update.password) db.commit() db.refresh(current_user) + print(f"更新後current_user>>{current_user.user_name}") return current_user diff --git a/app/schemas/message.py b/app/schemas/message.py index b06ca4d..9730b1f 100644 --- a/app/schemas/message.py +++ b/app/schemas/message.py @@ -2,6 +2,8 @@ from pydantic import BaseModel from datetime import datetime from typing import Optional +from schemas.user import UserResponse + class ResponseType(str, Enum): PRAISE = "praise" @@ -21,8 +23,17 @@ class MessageUpdate(BaseModel): class MessageResponse(MessageBase): id: int - created_at: datetime - updated_at: datetime + created_at: Optional[datetime] + updated_at: Optional[datetime] + user: Optional[UserResponse] = None + + +# 管理者投稿一覧表示 +class AdminMessageResponse(BaseModel): + user_name: str + content: str + created_at: Optional[datetime] + class Config: from_attributes = True \ No newline at end of file diff --git a/app/schemas/user.py b/app/schemas/user.py index 7875421..2e74c4b 100644 --- a/app/schemas/user.py +++ b/app/schemas/user.py @@ -21,6 +21,7 @@ class UserLogin(BaseModel): class UserUpdate(BaseModel): email: Optional[EmailStr] = None user_name: Optional[str] = None + password: Optional[str] = None # パスワード変更・リクエスト @@ -41,8 +42,6 @@ class UserResponse(BaseModel): user_name: str is_active: bool can_be_deleted: Optional[bool] = False # 管理画面の「削除可能」表示用 - created_at: datetime - updated_at: datetime class Config: from_attributes = True \ No newline at end of file diff --git a/app/services/session.py b/app/services/session.py index 6714d0a..bb0effa 100644 --- a/app/services/session.py +++ b/app/services/session.py @@ -1,4 +1,5 @@ from sqlalchemy.orm import Session +from sqlalchemy import cast, String, or_ from models.session import Session as SessionModel from models.message import Message from models.favorite import Favorite @@ -6,6 +7,7 @@ from fastapi import HTTPException +# チャット履歴取得 def get_sessions_with_first_message( db: Session, user_id: int, @@ -16,14 +18,20 @@ def get_sessions_with_first_message( if favorite_only or keyword: query = query.join(SessionModel.messages) - + if favorite_only: query = query.join(SessionModel.favorites) + #  キーワード検索 if keyword: - query = query.filter(Message.content.ilike(f"%{keyword}%")) - - sessions = query.distinct().all() + query = query.join(SessionModel.messages).filter( + or_( + Message.content.ilike(f"%{keyword}%"), + cast(SessionModel.created_at, String).ilike(f"%{keyword}%") + ) + ) + + sessions = query.distinct().all() result = [] for session in sessions: @@ -49,6 +57,34 @@ def get_sessions_with_first_message( return result +# 管理者用投稿内容一覧 +def get_all_sessions_with_first_message( + db:Session, +)-> list[dict]: + sessions = db.query(SessionModel).join(SessionModel.user).all() + result = [] + for session in sessions: + first_user_message = ( + db.query(Message) + .filter( + Message.session_id == session.id, + Message.is_user == True + ) + .order_by(Message.created_at.asc()) + .first() + ) + + if first_user_message: + result.append({ + "user_name": session.user.user_name, + "content": first_user_message.content, + "created_at": first_user_message.created_at or session.created_at, + }) + return result + + + +# 履歴削除 def delete_session( db: Session, session_id: int, @@ -67,6 +103,7 @@ def delete_session( return True +# お気に入りのトグル def toggle_favorite_session( db: Session, session_id: int, From 19a9e6b4539c53f010c5360383288d1acd7e477b Mon Sep 17 00:00:00 2001 From: sasakifuruta Date: Fri, 16 May 2025 01:03:22 +0900 Subject: [PATCH 42/72] =?UTF-8?q?=E5=B1=A5=E6=AD=B4=E3=81=AE=E5=89=8A?= =?UTF-8?q?=E9=99=A4=E5=8B=95=E4=BD=9C=E7=A2=BA=E8=AA=8DOK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/endpoints/message.py | 1 - app/api/endpoints/session.py | 4 ++-- app/api/endpoints/user.py | 3 --- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/app/api/endpoints/message.py b/app/api/endpoints/message.py index c2d7c66..845e370 100644 --- a/app/api/endpoints/message.py +++ b/app/api/endpoints/message.py @@ -34,7 +34,6 @@ async def create_message( if not session: raise HTTPException(status_code=404, detail="チャットが見つかりません") - print(f"session_id>>{session_id}") # return create_user_message(db, session_id, content=message_data.content) #テスト diff --git a/app/api/endpoints/session.py b/app/api/endpoints/session.py index a8a0b1d..d079b12 100644 --- a/app/api/endpoints/session.py +++ b/app/api/endpoints/session.py @@ -9,7 +9,7 @@ from core.database import get_db from utils.auth import get_current_user from utils.timestamp import now_jst -from services.session import get_sessions_with_first_message, toggle_favorite_session +from services.session import get_sessions_with_first_message, toggle_favorite_session, delete_session router = APIRouter() @@ -125,7 +125,7 @@ async def update_session( # 特定のチャットを削除 @router.delete("/sessions/{session_id}") -async def delete_session( +async def delete_session_route( session_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) diff --git a/app/api/endpoints/user.py b/app/api/endpoints/user.py index 091908f..e3ee9ee 100644 --- a/app/api/endpoints/user.py +++ b/app/api/endpoints/user.py @@ -22,8 +22,6 @@ async def update_my_account( db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): - print(f"更新前current_user>>{current_user.user_name}") - print(f"リクエスト>>{user_update}") # ユーザ名の更新 if user_update.user_name and user_update.user_name != current_user.user_name: current_user.user_name = user_update.user_name @@ -41,7 +39,6 @@ async def update_my_account( db.commit() db.refresh(current_user) - print(f"更新後current_user>>{current_user.user_name}") return current_user From f97a5a488dce3c710fb5db2eb06b8ec63f96ff8a Mon Sep 17 00:00:00 2001 From: sasakifuruta Date: Fri, 16 May 2025 22:05:04 +0900 Subject: [PATCH 43/72] =?UTF-8?q?=E4=B8=8D=E8=A6=81=E3=81=AA=E9=96=A2?= =?UTF-8?q?=E6=95=B0=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/services/ai/generator.py | 69 ++---------------------------------- app/services/ai/prompts.py | 9 ----- app/utils/s3.py | 29 --------------- 3 files changed, 2 insertions(+), 105 deletions(-) delete mode 100644 app/utils/s3.py diff --git a/app/services/ai/generator.py b/app/services/ai/generator.py index 4ec4747..5cee1e4 100644 --- a/app/services/ai/generator.py +++ b/app/services/ai/generator.py @@ -11,12 +11,9 @@ from sqlalchemy.orm import Session from core.config import settings -from services.ai.prompts import CHARACTER_PROMPTS, EMOTION_IMAGE_PROMPTS +from services.ai.prompts import CHARACTER_PROMPTS from models.session import CharacterModeEnum from models.message import ResponseTypeEnum -from models.emotion import Emotion -from utils.s3 import upload_to_s3 -from utils.timestamp import now_jst # ============================================ @@ -164,66 +161,4 @@ def generate_ai_response( raise HTTPException(status_code=502, detail="OpenAIサーバでエラーが発生しました") except Exception as e: print(f"AI生成中のエラー: {e}") - raise HTTPException(status_code=500, detail=f"AI応答の生成中にエラーが発生しました: {str(e)}") - - - -# ============================= -# 追加機能ー画像生成(実現見込み薄い) -# ============================= - -# 感情に応じたプロンプトを取得 -def get_prompt_for_emotion( - emotion_id: int, - db: Session -)->str: - emotion = db.get(Emotion,emotion_id) - if not emotion: - raise ValueError("該当する選択肢がありません") - return EMOTION_IMAGE_PROMPTS.get(emotion.name.lower(), "A bright and happy illustration that encourages positivity") - - -# 画像を生成 -def generate_image_bytes(prompt:str)->bytes: - try: - response = client.images.create( - prompt=prompt, - n=1, - size="512x512", - response_format="b64_json" - ) - - return base64.b64decode(response.data[0].b64_json) - - except RateLimitError: - raise HTTPException(status_code=429, detail="APIのレート制限を超えました。しばらくしてから再度お試しください") - except AuthenticationError: - raise HTTPException(status_code=401, detail="OpenAIの認証に失敗しました。APIキーを確認してください") - except APIError: - raise HTTPException(status_code=502, detail="OpenAIサーバでエラーが発生しました") - except Exception as e: - raise HTTPException(status_code=500, detail=f"画像生成中にエラーが発生しました: {str(e)}") - - -# 画像生成してS3にアップロードする -def generate_and_upload_image(prompt: str)->str: - image_bytes = generate_image_bytes(prompt) - unique_filename = f"emotion_{now_jst().strftime('%Y%m%d%H%M%S')}_{uuid.uuid4().hex[:8]}.png" - - return upload_to_s3( - file_bytes=image_bytes, - filename=unique_filename, - content_type="image/png" - ) - - -# 画像のURLを作成 -def generate_emotion_image_url( - emotion_id: int, - db:Session -)->str: - prompt = get_prompt_for_emotion(emotion_id, db) - return generate_and_upload_image(prompt) - - - + raise HTTPException(status_code=500, detail=f"AI応答の生成中にエラーが発生しました: {str(e)}") \ No newline at end of file diff --git a/app/services/ai/prompts.py b/app/services/ai/prompts.py index fac4673..4645596 100644 --- a/app/services/ai/prompts.py +++ b/app/services/ai/prompts.py @@ -49,13 +49,4 @@ "語尾は『〜しな』『〜さい』など、自然な口調にしてください。" ) } -} - - -EMOTION_IMAGE_PROMPTS = { - "疲れた": "A peaceful nature scene with soft sunlight and blooming flowers, evoking rest and hope, digital art, soft pastel colors", - "眠い": "A cozy bedroom with warm lighting, a cat sleeping on a bed, peaceful and dreamy atmosphere, digital painting, warm tones", - "イライラする": "A calm lake at sunset, soothing and peaceful atmosphere, realistic landscape, golden hour lighting", - "悲しい": "A beautiful field of sunflowers under a clear sky, uplifting and full of light, watercolor style", - "不安": "A safe cozy space with a cup of tea and soft cushions, calming and reassuring, studio lighting, concept art" } \ No newline at end of file diff --git a/app/utils/s3.py b/app/utils/s3.py deleted file mode 100644 index a9fa9bc..0000000 --- a/app/utils/s3.py +++ /dev/null @@ -1,29 +0,0 @@ -# TODO::削除予定 -# utils/s3.py -import boto3 -import uuid -from core.config import settings - - -# S3操作するための設定 -# s3_client = boto3.client( -# "s3", -# aws_access_key_id=settings.aws_access_key_id, -# aws_secret_access_key=settings.aws_secret_access_key, -# region_name=settings.aws_region -# ) - -def upload_to_s3( - file_bytes: bytes, - filename: str, - content_type: str -)->str: - key = f"generated/{uuid.uuid4()}_{filename}" - s3_client.put_object( - Bucket=settings.aws_s3_bucket_name, - Key=key, - Body=file_bytes, - ContentType=content_type, - ACL="public-read" - ) - return f"https://{settings.aws_s3_bucket_name}.s3.{settings.aws_region}.amazonaws.com/{key}" \ No newline at end of file From 8f1d37058cc1469b381c9abe44b6fd67ce7796f3 Mon Sep 17 00:00:00 2001 From: sasakifuruta Date: Sat, 17 May 2025 18:41:12 +0900 Subject: [PATCH 44/72] =?UTF-8?q?Amazon=20Bedrock=E4=BB=A5=E5=A4=96?= =?UTF-8?q?=E3=81=AE=E5=8B=95=E4=BD=9C=E7=A2=BA=E8=AA=8D=E5=AE=8C=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/endpoints/admin.py | 6 +- app/api/endpoints/session.py | 15 ++- app/migrations/versions/38db158025e3_honda.py | 112 ------------------ app/schemas/message.py | 16 ++- app/schemas/session.py | 20 +++- app/services/ai/generator.py | 4 +- app/services/message.py | 2 +- app/utils/auth.py | 9 +- 8 files changed, 46 insertions(+), 138 deletions(-) delete mode 100644 app/migrations/versions/38db158025e3_honda.py diff --git a/app/api/endpoints/admin.py b/app/api/endpoints/admin.py index 8361025..2df6063 100644 --- a/app/api/endpoints/admin.py +++ b/app/api/endpoints/admin.py @@ -1,5 +1,7 @@ from fastapi import APIRouter, HTTPException, Depends from sqlalchemy.orm import Session, joinedload +from zoneinfo import ZoneInfo + from datetime import datetime, timedelta from models import User, Message from schemas.user import UserRegister, UserLogin, UserUpdate, UserResponse, TokenResponse @@ -102,10 +104,6 @@ async def delete_user( if user.is_active or not user.deleted_at: raise HTTPException(status_code=400, detail="無効化されたアカウントのみ削除可能です") - now = now_jst() - if user.deleted_at + timedelta(days=30) > now: - raise HTTPException(status_code=400, detail=f"{now - user.deleted_at}日後に削除可能です") - db.delete(user) db.commit() return {"message": "ユーザーが削除されました"} diff --git a/app/api/endpoints/session.py b/app/api/endpoints/session.py index d079b12..72c039e 100644 --- a/app/api/endpoints/session.py +++ b/app/api/endpoints/session.py @@ -23,7 +23,6 @@ async def create_session( ): user_id=current_user.id - # user_id=1 today = now_jst().date() # 当日のセッションを確認 @@ -33,12 +32,12 @@ async def create_session( SessionModel.created_at <= datetime.combine(today, datetime.max.time()) ).first() - if existing_session: - # 1日1回制限:エラーメッセージで伝える - raise HTTPException( - status_code=403, - detail="今日はすでにチャットを開始しています。明日またご利用ください。" - ) + # if existing_session: + # # 1日1回制限:エラーメッセージで伝える + # raise HTTPException( + # status_code=403, + # detail="今日はすでにチャットを開始しています。明日またご利用ください。" + # ) new_session = SessionModel( @@ -90,7 +89,7 @@ async def get_session( ] return { - "chat_id" : session.id, + "session_id" : session.id, "messages" : messages, "created_at": session.created_at, "updated_at": session.updated_at, diff --git a/app/migrations/versions/38db158025e3_honda.py b/app/migrations/versions/38db158025e3_honda.py deleted file mode 100644 index 9cd3524..0000000 --- a/app/migrations/versions/38db158025e3_honda.py +++ /dev/null @@ -1,112 +0,0 @@ -"""honda - -Revision ID: 38db158025e3 -Revises: -Create Date: 2025-05-03 13:42:30.792840 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '38db158025e3' -down_revision: Union[str, None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('emotions', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('emotion', sa.String(length=255), nullable=False), - sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_emotions_id'), 'emotions', ['id'], unique=False) - op.create_table('users', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('password_hash', sa.String(length=255), nullable=False), - sa.Column('email', sa.String(length=255), nullable=False), - sa.Column('user_name', sa.String(length=255), nullable=False), - sa.Column('is_active', sa.Boolean(), nullable=True), - sa.Column('deleted_at', sa.DateTime(), nullable=True), - sa.Column('is_admin', sa.Boolean(), nullable=True), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True) - op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False) - op.create_table('sessions', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('character_mode', sa.Enum('saburo', 'bijyo', 'anger_mom', name='charactermodeenum'), nullable=False), - sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_sessions_id'), 'sessions', ['id'], unique=False) - op.create_table('favorites', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('session_id', sa.Integer(), nullable=True), - sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), - sa.ForeignKeyConstraint(['session_id'], ['sessions.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_favorites_id'), 'favorites', ['id'], unique=False) - op.create_table('messages', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('session_id', sa.Integer(), nullable=True), - sa.Column('is_users', sa.Boolean(), nullable=True), - sa.Column('response_type', sa.Enum('praise', 'insult', name='responsetypeenum'), nullable=True), - sa.Column('content', sa.Text(), nullable=False), - sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), - sa.ForeignKeyConstraint(['session_id'], ['sessions.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_messages_id'), 'messages', ['id'], unique=False) - op.create_table('generated_media', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('message_id', sa.Integer(), nullable=True), - sa.Column('emotion_id', sa.Integer(), nullable=True), - sa.Column('media_type', sa.Enum('IMAGE', 'BGM', name='mediatypeenum'), nullable=False), - sa.Column('media_url', sa.String(length=255), nullable=False), - sa.Column('image_prompt', sa.Text(), nullable=True), - sa.Column('bgm_prompt', sa.Text(), nullable=True), - sa.Column('bgm_duration', sa.Integer(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), - sa.ForeignKeyConstraint(['emotion_id'], ['emotions.id'], ), - sa.ForeignKeyConstraint(['message_id'], ['messages.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_generated_media_id'), 'generated_media', ['id'], unique=False) - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.drop_index(op.f('ix_generated_media_id'), table_name='generated_media') - op.drop_table('generated_media') - op.drop_index(op.f('ix_messages_id'), table_name='messages') - op.drop_table('messages') - op.drop_index(op.f('ix_favorites_id'), table_name='favorites') - op.drop_table('favorites') - op.drop_index(op.f('ix_sessions_id'), table_name='sessions') - op.drop_table('sessions') - op.drop_index(op.f('ix_users_id'), table_name='users') - op.drop_index(op.f('ix_users_email'), table_name='users') - op.drop_table('users') - op.drop_index(op.f('ix_emotions_id'), table_name='emotions') - op.drop_table('emotions') - # ### end Alembic commands ### diff --git a/app/schemas/message.py b/app/schemas/message.py index 9730b1f..cce5d17 100644 --- a/app/schemas/message.py +++ b/app/schemas/message.py @@ -1,7 +1,7 @@ from enum import Enum from pydantic import BaseModel from datetime import datetime -from typing import Optional +from typing import Optional, Literal from schemas.user import UserResponse @@ -14,6 +14,9 @@ class MessageBase(BaseModel): is_user: bool response_type: Optional[ResponseType] = None content: str + + class Config: + from_attributes = True class MessageCreate(MessageBase): pass @@ -26,6 +29,17 @@ class MessageResponse(MessageBase): created_at: Optional[datetime] updated_at: Optional[datetime] user: Optional[UserResponse] = None + + class Config: + from_attributes = True + + +# 履歴の詳細表示用 +class MessageDetail(BaseModel): + message_id: int + message_text: str + sender_type: Literal["user", "ai"] + # 管理者投稿一覧表示 diff --git a/app/schemas/session.py b/app/schemas/session.py index f5ff4cb..f40102b 100644 --- a/app/schemas/session.py +++ b/app/schemas/session.py @@ -2,7 +2,9 @@ from enum import Enum from pydantic import BaseModel from datetime import datetime -from typing import Optional +from typing import Optional, List +from schemas.message import MessageDetail + class CharacterMode(str, Enum): SABURO = "saburo" @@ -14,7 +16,6 @@ class SessionQueryParams(BaseModel): keyword: Optional[str] = None class SessionBase(BaseModel): - user_id: int character_mode: CharacterMode class SessionCreate(SessionBase): @@ -29,6 +30,7 @@ class Config: class SessionResponse(SessionBase): id: int + user_id: int created_at: datetime updated_at: datetime @@ -52,9 +54,15 @@ class MessageSummary(BaseModel): sender_type : str # "user" or "ai" -class SessionWithMessagesResponse(SessionResponse): - chat_id: int - messages: list[MessageSummary] = [] +# class SessionWithMessagesResponse(SessionResponse): +# session_id: int +# messages: list[MessageSummary] = [] +# created_at: datetime +# updated_at: datetime + + +class SessionWithMessagesResponse(BaseModel): + session_id: int created_at: datetime updated_at: datetime - \ No newline at end of file + messages: List[MessageDetail] \ No newline at end of file diff --git a/app/services/ai/generator.py b/app/services/ai/generator.py index 5cee1e4..2a095c3 100644 --- a/app/services/ai/generator.py +++ b/app/services/ai/generator.py @@ -20,7 +20,7 @@ # AWS Bedrock(Claude Instantモデル)版AI応答生成 # =========================================== -bedrock = boto3.client("bedrock-runtime", region_name=settings.REGION) +# bedrock = boto3.client("bedrock-runtime", region_name=settings.REGION) # 起動時 or 初回リスエスト時に接続確認 def verify_bedrock_connection(): @@ -110,7 +110,7 @@ def stop_generate_sentence(text: str)-> str: # ========================= # OpenAIクライアント初期化 -# client = OpenAI(api_key=settings.openai_api_key) +client = OpenAI(api_key=settings.openai_api_key) # AI応答のOpenAI呼び出し(リトライ付き) @retry( diff --git a/app/services/message.py b/app/services/message.py index 4bb101b..8df1bb2 100644 --- a/app/services/message.py +++ b/app/services/message.py @@ -60,7 +60,7 @@ async def create_message_with_ai( # AI返答を生成 ai_reply, response_type = await run_in_threadpool( - # generate_ai_response, # Open API用 + generate_ai_response, # Open API用 # generate_ai_response_via_bedrock, # Amazon Bedrock用 character_mode=character_mode, user_input=content diff --git a/app/utils/auth.py b/app/utils/auth.py index cd364a9..eed73f1 100644 --- a/app/utils/auth.py +++ b/app/utils/auth.py @@ -11,7 +11,8 @@ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/login") +# oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/login") +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") SECRET_KEY = settings.SECRET_KEY ALGORITHM = "HS256" @@ -21,11 +22,9 @@ def hash_password(password: str) -> str: return pwd_context.hash(password) def verify_password(plain_password: str, hashed_password: str) -> bool: - return pwd_context.verify(plain_password, hashed_password) def create_access_token(data: dict, expires_delta: timedelta = None) -> str: - to_encode = data.copy() expire = datetime.utcnow() + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)) to_encode.update({"exp": expire}) @@ -41,10 +40,12 @@ def get_current_user(db: Session = Depends(get_db), token: str = Depends(oauth2_ ) try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + print("JWT payload:", payload) user_id: str = payload.get("sub") if user_id is None: raise credentials_exception - except JWTError: + except JWTError as e: + print("JWTError:", e) raise credentials_exception user = db.query(User).filter(User.id == int(user_id)).first() From 78b89b5cb64cc449a85d01a1ed45ef4049192e76 Mon Sep 17 00:00:00 2001 From: sasakifuruta Date: Sun, 18 May 2025 14:03:21 +0900 Subject: [PATCH 45/72] =?UTF-8?q?Amazon=20Bedrock=E3=81=AE=E5=8B=95?= =?UTF-8?q?=E4=BD=9C=E7=A2=BA=E8=AA=8D=E5=AE=8C=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/endpoints/message.py | 1 - app/schemas/session.py | 7 ------- app/services/ai/generator.py | 3 ++- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/app/api/endpoints/message.py b/app/api/endpoints/message.py index 845e370..3747b09 100644 --- a/app/api/endpoints/message.py +++ b/app/api/endpoints/message.py @@ -29,7 +29,6 @@ async def create_message( session = db.query(SessionModel).filter( SessionModel.id == session_id, SessionModel.user_id == current_user.id - # SessionModel.user_id == 1 # テスト ).first() if not session: diff --git a/app/schemas/session.py b/app/schemas/session.py index f40102b..770d47f 100644 --- a/app/schemas/session.py +++ b/app/schemas/session.py @@ -53,13 +53,6 @@ class MessageSummary(BaseModel): message_text: str sender_type : str # "user" or "ai" - -# class SessionWithMessagesResponse(SessionResponse): -# session_id: int -# messages: list[MessageSummary] = [] -# created_at: datetime -# updated_at: datetime - class SessionWithMessagesResponse(BaseModel): session_id: int diff --git a/app/services/ai/generator.py b/app/services/ai/generator.py index 2a095c3..94ac151 100644 --- a/app/services/ai/generator.py +++ b/app/services/ai/generator.py @@ -20,7 +20,7 @@ # AWS Bedrock(Claude Instantモデル)版AI応答生成 # =========================================== -# bedrock = boto3.client("bedrock-runtime", region_name=settings.REGION) +bedrock = boto3.client("bedrock-runtime", region_name=settings.REGION) # 起動時 or 初回リスエスト時に接続確認 def verify_bedrock_connection(): @@ -67,6 +67,7 @@ def generate_ai_response_via_bedrock( # Claude形式のプロンプトを作成 prompt = f"Human: {prompt_data['description']}\n{prompt_data['prompt']}\nユーザーの日記:{user_input}\nAssistant:" + print("Bedrockへのプロンプト>>>",prompt) try: response = bedrock.invoke_model( From 6d0acf329d6c4da8ff608823b11cfccffa0a5335 Mon Sep 17 00:00:00 2001 From: sasakifuruta Date: Sat, 24 May 2025 16:47:22 +0900 Subject: [PATCH 46/72] =?UTF-8?q?=E4=B8=8D=E8=A6=81=E3=81=AA=E3=82=B3?= =?UTF-8?q?=E3=83=BC=E3=83=89=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/api.py | 6 +-- app/api/endpoints/admin.py | 22 ++++++++-- app/api/endpoints/auth.py | 6 +++ app/api/endpoints/emotion.py | 22 ---------- app/api/endpoints/favorite.py | 20 ---------- app/api/endpoints/generated_media.py | 23 ----------- app/api/endpoints/message.py | 11 ++++- app/api/endpoints/session.py | 12 ++++++ app/api/endpoints/user.py | 10 +++++ app/main.py | 17 -------- ...7009_initial.py => 3e6918d322bf_deploy.py} | 36 ++--------------- app/models/__init__.py | 4 +- app/models/emotion.py | 11 ----- app/models/generated_media.py | 26 ------------ app/models/message.py | 3 +- app/schemas/emotion.py | 16 -------- app/schemas/favorite.py | 17 -------- app/schemas/generated_media.py | 27 ------------- app/schemas/message.py | 1 - app/schemas/session.py | 7 +++- app/schemas/user.py | 3 ++ app/services/ai/generator.py | 3 +- app/services/ai/prompts.py | 16 -------- app/services/message.py | 40 +++++-------------- app/services/session.py | 10 ++++- 25 files changed, 93 insertions(+), 276 deletions(-) delete mode 100644 app/api/endpoints/emotion.py delete mode 100644 app/api/endpoints/favorite.py delete mode 100644 app/api/endpoints/generated_media.py rename app/migrations/versions/{b7c203677009_initial.py => 3e6918d322bf_deploy.py} (68%) delete mode 100644 app/models/emotion.py delete mode 100644 app/models/generated_media.py delete mode 100644 app/schemas/emotion.py delete mode 100644 app/schemas/favorite.py delete mode 100644 app/schemas/generated_media.py diff --git a/app/api/api.py b/app/api/api.py index b174992..16c78fd 100644 --- a/app/api/api.py +++ b/app/api/api.py @@ -1,5 +1,5 @@ from fastapi import APIRouter -from api.endpoints import auth, admin, user, session, message, generated_media +from api.endpoints import auth, admin, user, session, message router = APIRouter() @@ -14,6 +14,4 @@ # チャット用エンドポイント router.include_router(session.router, tags=["Session"]) # メッセージ用エンドポイント -router.include_router(message.router, tags=["Message"]) -# 生成用エンドポイント -router.include_router(generated_media.router, tags=["Generated_media"]) \ No newline at end of file +router.include_router(message.router, tags=["Message"]) \ No newline at end of file diff --git a/app/api/endpoints/admin.py b/app/api/endpoints/admin.py index 2df6063..61fb8db 100644 --- a/app/api/endpoints/admin.py +++ b/app/api/endpoints/admin.py @@ -1,6 +1,5 @@ from fastapi import APIRouter, HTTPException, Depends -from sqlalchemy.orm import Session, joinedload -from zoneinfo import ZoneInfo +from sqlalchemy.orm import Session from datetime import datetime, timedelta from models import User, Message @@ -14,8 +13,9 @@ router = APIRouter() - +# ------------------ # 管理者アカウント登録 +# ------------------ @router.post("/admin/register/", response_model=TokenResponse) async def register_admin( admin_data: UserRegister, @@ -38,7 +38,9 @@ async def register_admin( return {"token": token } +# ------------------ # 管理者ログイン +# ------------------ @router.post("/admin/login", response_model=TokenResponse) async def login_admin( admin_data: UserLogin, @@ -59,7 +61,9 @@ async def login_admin( return {"token": token, "is_admin": admin.is_admin} +# ----------------------- # 管理者アカウント情報取得 +# ------------------ @router.get("/admin_info", response_model=UserResponse) async def get_admin_info( current_admin: User = Depends(get_current_admin_user) @@ -67,7 +71,9 @@ async def get_admin_info( return current_admin +# ------------------ # 一般ユーザ情報一覧取得 +# ------------------ @router.get("/users", response_model=list[UserResponse]) async def get_all_users( db: Session = Depends(get_db), @@ -90,7 +96,9 @@ async def get_all_users( ] +# ---------------------------- # ユーザーアカウントを削除する処理 +# ---------------------------- @router.delete("/users/{user_id}") async def delete_user( user_id: int, @@ -109,7 +117,9 @@ async def delete_user( return {"message": "ユーザーが削除されました"} +# ---------------------------- # ユーザーアカウントを凍結する処理 +# ---------------------------- @router.patch("/users/{user_id}/deactivate") async def deactivate_user( user_id: int, @@ -126,7 +136,9 @@ async def deactivate_user( return {"message": "ユーザーが無効化されました"} +# ---------------------------- # ユーザーアカウントを有効化する処理 +# ---------------------------- @router.patch("/users/{user_id}/activate") async def activate_user( user_id: int, @@ -143,7 +155,9 @@ async def activate_user( return {"message": "ユーザーが有効化されました"} +# --------------- # ユーザー情報更新 +# ---------------- @router.patch("/users/{user_id}") async def update_user( user_id: int, @@ -167,7 +181,9 @@ async def update_user( return {"message": "ユーザー情報が更新されました"} +# -------- # 投稿一覧 +# -------- @router.get("/messages", response_model=list[AdminMessageResponse]) async def get_messages( db: Session = Depends(get_db), diff --git a/app/api/endpoints/auth.py b/app/api/endpoints/auth.py index e23cee9..e4520e7 100644 --- a/app/api/endpoints/auth.py +++ b/app/api/endpoints/auth.py @@ -10,7 +10,9 @@ router = APIRouter() +# ------------- # アカウント登録 +# ------------- @router.post("/register", response_model=TokenResponse) async def register(user_data: UserRegister, db: Session = Depends(get_db)): @@ -30,7 +32,9 @@ async def register(user_data: UserRegister, db: Session = Depends(get_db)): return {"token": token, "is_admin": new_user.is_admin} +# -------- # ログイン +# --------- @router.post("/login", response_model=TokenResponse) async def login(user_data: UserLogin, db: Session = Depends(get_db)): user = db.query(User).filter_by(email=user_data.email).first() @@ -47,7 +51,9 @@ async def login(user_data: UserLogin, db: Session = Depends(get_db)): return {"token": token} +# ---------- # ログアウト +# ---------- @router.post("/logout") async def logout(current_user: User = Depends(get_current_user)): return {"message": "ログアウトしました。"} \ No newline at end of file diff --git a/app/api/endpoints/emotion.py b/app/api/endpoints/emotion.py deleted file mode 100644 index 2b9e638..0000000 --- a/app/api/endpoints/emotion.py +++ /dev/null @@ -1,22 +0,0 @@ -from fastapi import APIRouter, HTTPException, Depends -from sqlalchemy.orm import Session -from models import Emotion -from schemas.emotion import EmotionCreate, EmotionResponse -from core.database import get_db - -router = APIRouter() - -# 感情一覧取得 -@router.get("/emotions") -async def get_emotions(db: Session = Depends(get_db)): - emotions = db.query(Emotion).all() - return emotions - - -# 感情登録 -@router.post("/emotions") -async def create_emotions(emotion_data: EmotionCreate, db: Session = Depends(get_db)): - new_emotion = Emotion(emotion=emotion_data.emotion) - db.add(new_emotion) - db.commit() - return new_emotion \ No newline at end of file diff --git a/app/api/endpoints/favorite.py b/app/api/endpoints/favorite.py deleted file mode 100644 index e24783e..0000000 --- a/app/api/endpoints/favorite.py +++ /dev/null @@ -1,20 +0,0 @@ -from fastapi import APIRouter, HTTPException, Depends -from sqlalchemy.orm import Session -from app.models import Favorite, Session as DBSession -from app.schemas.favorite import FavoriteCreate, FavoriteResponse -from app.core.database import get_db - -@router.post("/favorites") -async def create_favorite(favorite_data: FavoriteCreate, db: Session = Depends(get_db)): - session = db.query(DBSession).filter(DBSession.id == favorite_data.session_id).first() - if not session: - raise HTTPException(status_code=404, detail="お気に入り登録が見つかりません。") - favorite = Favorite(session_id=favorite_data.session_id, user_id=favorite_data.user_id) - db.add(favorite) - db.commit() - return favorite - -@router.get("/favorites") -async def get_favorites(user_id: int, db: Session = Depends(get_db)): - favorites = db.query(Favorite).filter(Favorite.user_id == user_id).all() - return favorites \ No newline at end of file diff --git a/app/api/endpoints/generated_media.py b/app/api/endpoints/generated_media.py deleted file mode 100644 index e59c293..0000000 --- a/app/api/endpoints/generated_media.py +++ /dev/null @@ -1,23 +0,0 @@ -from fastapi import APIRouter, HTTPException, Depends -from sqlalchemy.orm import Session -from models import GeneratedMedia -from schemas.generated_media import GeneratedMediaCreate, GeneratedMediaResponse -from core.database import get_db - -router = APIRouter() - -@router.post("/generated-media", response_model=GeneratedMediaResponse) -async def create_generated_media( - media_data: GeneratedMediaCreate, - db: Session = Depends(get_db) -): - new_media = GeneratedMedia( - message_id=media_data.message_id, - emotion_id=media_data.emotion_id, - media_url=media_data.media_url, - media_type=media_data.media_type - ) - db.add(new_media) - db.commit() - db.refresh(new_media) - return new_media \ No newline at end of file diff --git a/app/api/endpoints/message.py b/app/api/endpoints/message.py index 3747b09..cf245a8 100644 --- a/app/api/endpoints/message.py +++ b/app/api/endpoints/message.py @@ -8,15 +8,16 @@ from schemas.message import MessageCreate, MessageResponse, MessageUpdate from core.database import get_db from utils.auth import get_current_user -from services.message import create_message_with_ai, create_message as create_user_message, get_messages_by_session, update_user_message ,delete_message +from services.message import create_message_with_ai, get_messages_by_session, update_user_message ,delete_message router = APIRouter() # レートリミッター limiter = Limiter(key_func=get_remote_address) - +# ---------------------------------- # 日記の投稿またはキャラクターの返答を作成 +# ---------------------------------- @router.post("/sessions/{session_id}/messages") @limiter.limit("2/minute") # IPごとに2回/分 async def create_message( @@ -46,7 +47,9 @@ async def create_message( raise HTTPException(status_code=500, detail="サーバ内部エラー") +# ---------------------------------- # 特定のチャットの全メッセージを取得 +# ---------------------------------- @router.get("/sessions/{session_id}/messages", response_model=list[MessageResponse]) async def get_messages_endpoint( session_id: int, @@ -64,7 +67,9 @@ async def get_messages_endpoint( return get_messages_by_session(db, session_id) +# ---------------------- # ユーザのメッセージを更新 +# ---------------------- @router.put("/sessions/{session_id}/messages/{message_id}", response_model=MessageResponse) async def update_message( session_id: int, @@ -84,7 +89,9 @@ async def update_message( return update_user_message(db, message_id, message_data.content) +# ---------------------- # 特定のメッセージを削除 +# ----------------------- @router.delete("/sessions/{session_id}/messages/{message_id}") async def delete_message_endpoint( session_id: int, diff --git a/app/api/endpoints/session.py b/app/api/endpoints/session.py index 72c039e..a9209f2 100644 --- a/app/api/endpoints/session.py +++ b/app/api/endpoints/session.py @@ -14,7 +14,9 @@ router = APIRouter() +# ----------------- # チャットの開始 +# ----------------- @router.post("/sessions", response_model=SessionResponse) async def create_session( session_data: SessionCreate, @@ -50,7 +52,9 @@ async def create_session( return new_session +# ---------------- # チャット一覧取得 +# ---------------- @router.get("/sessions", response_model=list[SessionSummaryResponse]) async def get_sessions( db: Session = Depends(get_db), @@ -66,7 +70,9 @@ async def get_sessions( ) +# ------------------- # 特定のチャットを取得 +# ------------------- @router.get("/sessions/{session_id}", response_model=SessionWithMessagesResponse) async def get_session( session_id: int, @@ -96,7 +102,9 @@ async def get_session( } +# ------------------- # 特定のチャットを変更 +# ------------------- @router.patch("/sessions/{session_id}", response_model= SessionResponse) async def update_session( id: int, @@ -122,7 +130,9 @@ async def update_session( return session +# ------------------ # 特定のチャットを削除 +# ------------------ @router.delete("/sessions/{session_id}") async def delete_session_route( session_id: int, @@ -135,7 +145,9 @@ async def delete_session_route( return {"message": "チャットを削除しました。"} +# ---------------- # お気に入りのトグル +# ---------------- @router.post("/sessions/{session_id}/favorite") def toggle_favorite( session_id: int, diff --git a/app/api/endpoints/user.py b/app/api/endpoints/user.py index e3ee9ee..b244d22 100644 --- a/app/api/endpoints/user.py +++ b/app/api/endpoints/user.py @@ -9,13 +9,17 @@ router = APIRouter() +# ----------------- # アカウント情報取得 +# ----------------- @router.get("/user", response_model=UserResponse) async def get_my_account(current_user: User = Depends(get_current_user)): return current_user +# ----------------- # アカウント情報の更新 +# ----------------- @router.patch("/user", response_model=UserResponse) async def update_my_account( user_update: UserUpdate, @@ -42,7 +46,9 @@ async def update_my_account( return current_user +# ----------------- # パスワード変更 +# ----------------- @router.put("/password") async def change_password( password_data: PasswordUpdate, @@ -58,7 +64,9 @@ async def change_password( return {"message": "パスワードを変更しました"} +# ----------------- # アカウント凍結 +# ----------------- @router.delete("/user") async def deactivate_account( db: Session = Depends(get_db), @@ -73,7 +81,9 @@ async def deactivate_account( return {"message": "アカウントを削除しました"} +# ------------- # アカウント削除 +# -------------- @router.delete("/user/delete") async def delete_account( db: Session = Depends(get_db), diff --git a/app/main.py b/app/main.py index a71931f..59a1b75 100644 --- a/app/main.py +++ b/app/main.py @@ -1,6 +1,5 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -import mysql.connector from slowapi.errors import RateLimitExceeded from slowapi.middleware import SlowAPIMiddleware from slowapi import Limiter, _rate_limit_exceeded_handler @@ -32,21 +31,5 @@ def read_root(): return {"message": "Hello from FastAPI + MySQL in Docker!"} -# @app.get("/db-status") -# def db_status(): -# try: -# connection = mysql.connector.connect( -# host="db", # ← Dockerコンテナ名 -# user=os.getenv("MYSQL_USER"), -# password=os.getenv("MYSQL_PASSWORD"), -# database=os.getenv("MYSQL_DATABASE") -# ) -# if connection.is_connected(): -# connection.close() # ✅ 接続が確認できたら明示的にクローズ -# return {"db_status": "connected"} -# else: -# return {"db_status": "not connected"} -# except Exception as e: -# return {"db_status": "error", "details": str(e)} app.include_router(api_router, prefix="/api") \ No newline at end of file diff --git a/app/migrations/versions/b7c203677009_initial.py b/app/migrations/versions/3e6918d322bf_deploy.py similarity index 68% rename from app/migrations/versions/b7c203677009_initial.py rename to app/migrations/versions/3e6918d322bf_deploy.py index db469d6..a425ef9 100644 --- a/app/migrations/versions/b7c203677009_initial.py +++ b/app/migrations/versions/3e6918d322bf_deploy.py @@ -1,8 +1,8 @@ -"""initial +"""deploy -Revision ID: b7c203677009 +Revision ID: 3e6918d322bf Revises: -Create Date: 2025-05-06 00:13:17.550204 +Create Date: 2025-05-24 07:32:24.105720 """ from typing import Sequence, Union @@ -12,7 +12,7 @@ # revision identifiers, used by Alembic. -revision: str = 'b7c203677009' +revision: str = '3e6918d322bf' down_revision: Union[str, None] = None branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None @@ -21,14 +21,6 @@ def upgrade() -> None: """Upgrade schema.""" # ### commands auto generated by Alembic - please adjust! ### - op.create_table('emotions', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('emotion', sa.String(length=255), nullable=False), - sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_emotions_id'), 'emotions', ['id'], unique=False) op.create_table('users', sa.Column('id', sa.Integer(), nullable=False), sa.Column('password_hash', sa.String(length=255), nullable=False), @@ -76,30 +68,12 @@ def upgrade() -> None: sa.PrimaryKeyConstraint('id') ) op.create_index(op.f('ix_messages_id'), 'messages', ['id'], unique=False) - op.create_table('generated_media', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('message_id', sa.Integer(), nullable=True), - sa.Column('emotion_id', sa.Integer(), nullable=True), - sa.Column('media_type', sa.Enum('IMAGE', 'BGM', name='mediatypeenum'), nullable=False), - sa.Column('media_url', sa.String(length=255), nullable=False), - sa.Column('image_prompt', sa.Text(), nullable=True), - sa.Column('bgm_prompt', sa.Text(), nullable=True), - sa.Column('bgm_duration', sa.Integer(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), - sa.ForeignKeyConstraint(['emotion_id'], ['emotions.id'], ), - sa.ForeignKeyConstraint(['message_id'], ['messages.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_generated_media_id'), 'generated_media', ['id'], unique=False) # ### end Alembic commands ### def downgrade() -> None: """Downgrade schema.""" # ### commands auto generated by Alembic - please adjust! ### - op.drop_index(op.f('ix_generated_media_id'), table_name='generated_media') - op.drop_table('generated_media') op.drop_index(op.f('ix_messages_id'), table_name='messages') op.drop_table('messages') op.drop_index(op.f('ix_favorites_id'), table_name='favorites') @@ -109,6 +83,4 @@ def downgrade() -> None: op.drop_index(op.f('ix_users_id'), table_name='users') op.drop_index(op.f('ix_users_email'), table_name='users') op.drop_table('users') - op.drop_index(op.f('ix_emotions_id'), table_name='emotions') - op.drop_table('emotions') # ### end Alembic commands ### diff --git a/app/models/__init__.py b/app/models/__init__.py index 992de54..84f1e40 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -2,6 +2,4 @@ from .user import User from .session import Session from .message import Message -from .favorite import Favorite -from .emotion import Emotion -from .generated_media import GeneratedMedia \ No newline at end of file +from .favorite import Favorite \ No newline at end of file diff --git a/app/models/emotion.py b/app/models/emotion.py deleted file mode 100644 index e3f237e..0000000 --- a/app/models/emotion.py +++ /dev/null @@ -1,11 +0,0 @@ -from sqlalchemy import Column, Integer, String, DateTime -from sqlalchemy.orm import relationship -from datetime import datetime -from .base import Base, TimestampMixin - -class Emotion(Base, TimestampMixin): - __tablename__ = "emotions" - - id = Column(Integer, primary_key=True, index=True) - emotion = Column(String(255), nullable=False) - generated_media = relationship("GeneratedMedia", back_populates="emotion") \ No newline at end of file diff --git a/app/models/generated_media.py b/app/models/generated_media.py deleted file mode 100644 index 51fc505..0000000 --- a/app/models/generated_media.py +++ /dev/null @@ -1,26 +0,0 @@ -from sqlalchemy import Column, Integer, String, Enum, Text, DateTime, ForeignKey -from sqlalchemy.orm import relationship -import enum -from .base import Base, TimestampMixin - -class MediaTypeEnum(str, enum.Enum): - IMAGE = "image" - BGM = "bgm" - -class GeneratedMedia(Base, TimestampMixin): - __tablename__ = "generated_media" - - id = Column(Integer, primary_key=True, index=True) - message_id = Column(Integer, ForeignKey("messages.id", ondelete="CASCADE")) - emotion_id = Column(Integer, ForeignKey("emotions.id")) - media_type = Column(Enum(MediaTypeEnum), nullable=False) - media_url = Column(String(255), nullable=False) - - image_prompt = Column(Text, nullable=True) - - bgm_prompt = Column(Text, nullable=True) - bgm_duration = Column(Integer, nullable=True) - - message = relationship("Message", back_populates="generated_media") - emotion = relationship("Emotion", back_populates="generated_media") - \ No newline at end of file diff --git a/app/models/message.py b/app/models/message.py index 406820c..86852b0 100644 --- a/app/models/message.py +++ b/app/models/message.py @@ -17,5 +17,4 @@ class Message(Base, TimestampMixin): response_type = Column(Enum(ResponseTypeEnum)) content = Column(Text, nullable=False) - session = relationship("Session", back_populates="messages") - generated_media = relationship("GeneratedMedia", back_populates="message", cascade="all, delete-orphan") \ No newline at end of file + session = relationship("Session", back_populates="messages") \ No newline at end of file diff --git a/app/schemas/emotion.py b/app/schemas/emotion.py deleted file mode 100644 index ab73d77..0000000 --- a/app/schemas/emotion.py +++ /dev/null @@ -1,16 +0,0 @@ -from pydantic import BaseModel -from datetime import datetime - -class EmotionBase(BaseModel): - emotion: str - -class EmotionCreate(EmotionBase): - pass - -class EmotionResponse(EmotionBase): - id: int - created_at: datetime - updated_at: datetime - - class Config: - from_attributes = True \ No newline at end of file diff --git a/app/schemas/favorite.py b/app/schemas/favorite.py deleted file mode 100644 index b4c1add..0000000 --- a/app/schemas/favorite.py +++ /dev/null @@ -1,17 +0,0 @@ -from pydantic import BaseModel -from datetime import datetime - -class FavoriteBase(BaseModel): - session_id: int - user_id: int - -class FavoriteCreate(FavoriteBase): - pass - -class FavoriteResponse(FavoriteBase): - id: int - created_at: datetime - updated_at: datetime - - class Config: - from_attributes = True \ No newline at end of file diff --git a/app/schemas/generated_media.py b/app/schemas/generated_media.py deleted file mode 100644 index 889055b..0000000 --- a/app/schemas/generated_media.py +++ /dev/null @@ -1,27 +0,0 @@ -from enum import Enum -from pydantic import BaseModel -from datetime import datetime -from typing import Optional - -class MediaType(str, Enum): - IMG = "img" - BGM = "bgm" - -class GeneratedMediaBase(BaseModel): - message_id: int - emotion_id: int - media_type: MediaType - image_prompt: Optional[str] = None - bgm_prompt: Optional[str] = None - bgm_duration: Optional[int] = None - -class GeneratedMediaCreate(GeneratedMediaBase): - pass - -class GeneratedMediaResponse(GeneratedMediaBase): - id: int - created_at: datetime - updated_at: datetime - - class Config: - from_attributes = True \ No newline at end of file diff --git a/app/schemas/message.py b/app/schemas/message.py index cce5d17..cdcb911 100644 --- a/app/schemas/message.py +++ b/app/schemas/message.py @@ -41,7 +41,6 @@ class MessageDetail(BaseModel): sender_type: Literal["user", "ai"] - # 管理者投稿一覧表示 class AdminMessageResponse(BaseModel): user_name: str diff --git a/app/schemas/session.py b/app/schemas/session.py index 770d47f..954797d 100644 --- a/app/schemas/session.py +++ b/app/schemas/session.py @@ -15,9 +15,11 @@ class SessionQueryParams(BaseModel): favorite_only: bool = False keyword: Optional[str] = None + class SessionBase(BaseModel): character_mode: CharacterMode + class SessionCreate(SessionBase): pass @@ -58,4 +60,7 @@ class SessionWithMessagesResponse(BaseModel): session_id: int created_at: datetime updated_at: datetime - messages: List[MessageDetail] \ No newline at end of file + messages: List[MessageDetail] + + class Config: + from_attributes = True \ No newline at end of file diff --git a/app/schemas/user.py b/app/schemas/user.py index 2e74c4b..c88ce9f 100644 --- a/app/schemas/user.py +++ b/app/schemas/user.py @@ -33,6 +33,9 @@ class PasswordUpdate(BaseModel): # アカウント登録・ログイン レスポンス class TokenResponse(BaseModel): token: str + + class Config: + from_attributes = True # アカウント情報取得・レスポンス diff --git a/app/services/ai/generator.py b/app/services/ai/generator.py index 94ac151..4c136a7 100644 --- a/app/services/ai/generator.py +++ b/app/services/ai/generator.py @@ -95,8 +95,9 @@ def generate_ai_response_via_bedrock( print(f"Bedrockエラー: {e}") raise HTTPException(status_code=500, detail=f"AI応答の生成中にエラーが発生しました: {str(e)}") - +# ------------------------ # Bedrockの出力を句点で切る +# ------------------------- def stop_generate_sentence(text: str)-> str: # (。!?)までを残す match = re.search(r'[。!?](?!.*[。!?])', text) diff --git a/app/services/ai/prompts.py b/app/services/ai/prompts.py index 4645596..8bf3aaf 100644 --- a/app/services/ai/prompts.py +++ b/app/services/ai/prompts.py @@ -33,20 +33,4 @@ "語尾は『〜ね』など、自然な口調にしてください。" ) }, - "anger-mom": { - "name": "鬼お母さん", - "description":( - "鬼お母さんは、いつもビシッと喝を入れてくれます。\ - 表情は怖いですが、的確な一言で目標を明確にさせてくれます。\ - 彼女は知りませんが、実は彼女に喝を入れてほしいファンは結構多いです。\ - 彼女の喝を受けた人は皆、目標に向かって頑張る気持ちになります。" - ), - "prompt": - ( - "あなたは鬼お母さんというキャラクターとして振る舞ってください。" - "表情は怖いですが、的確な一言で目標を明確にさせてくれます。" - "ユーザーの日記に対して、ビシッと喝を入れてください." - "語尾は『〜しな』『〜さい』など、自然な口調にしてください。" - ) - } } \ No newline at end of file diff --git a/app/services/message.py b/app/services/message.py index 8df1bb2..1e41c6f 100644 --- a/app/services/message.py +++ b/app/services/message.py @@ -2,40 +2,15 @@ from sqlalchemy.orm import Session from starlette.concurrency import run_in_threadpool from typing import List -from models.message import Message, ResponseTypeEnum -from models.session import Session as SessionModel, CharacterModeEnum +from models.message import Message +from models.session import Session as SessionModel from schemas.message import MessageResponse from services.ai.generator import generate_ai_response, generate_ai_response_via_bedrock -# テスト用-日記を保存 -def create_message( - db: Session, - session_id: int, - content: str, -) -> MessageResponse: - # ユーザーの日記を保存 - user_message = Message( - session_id=session_id, - content=content, - is_user=True - ) - db.add(user_message) - db.commit() - db.refresh(user_message) - - # セッションを取得してモードを確認 - session = db.query(SessionModel).filter(SessionModel.id == session_id).first() - if session is None: - raise HTTPException(status_code=404, detail="チャットが見つかりません") - character_mode = session.character_mode - - return MessageResponse.from_orm(user_message), character_mode - - - - +# -------------------------------- # 日記を保存してキャラクターの返答を生成 +# --------------------------------- async def create_message_with_ai( db: Session, session_id: int, @@ -80,14 +55,17 @@ async def create_message_with_ai( # AI返答を返す return MessageResponse.from_orm(ai_message) - +# -------------------------- # チャット内の全メッセージを取得 +# -------------------------- def get_messages_by_session(db: Session, session_id: int) -> List[MessageResponse]: messages = db.query(Message).filter(Message.session_id == session_id).order_by(Message.created_at).all() return [MessageResponse.from_orm(m) for m in messages] +# -------------- # メッセージを更新 +# -------------- def update_user_message(db: Session, message_id: int, new_content: str) -> MessageResponse: db_message = db.query(Message).filter(Message.id == message_id, Message.is_user == True).first() if db_message is None: @@ -98,7 +76,9 @@ def update_user_message(db: Session, message_id: int, new_content: str) -> Messa return MessageResponse.from_orm(db_message) +# ------------- # メッセージ削除 +# ------------- def delete_message(db: Session, message_id: int) -> None: db_message = db.query(Message).filter(Message.id == message_id).first() if db_message is None: diff --git a/app/services/session.py b/app/services/session.py index bb0effa..b2ee84e 100644 --- a/app/services/session.py +++ b/app/services/session.py @@ -7,7 +7,9 @@ from fastapi import HTTPException +# -------------- # チャット履歴取得 +# -------------- def get_sessions_with_first_message( db: Session, user_id: int, @@ -57,7 +59,9 @@ def get_sessions_with_first_message( return result +# ------------------ # 管理者用投稿内容一覧 +# ------------------ def get_all_sessions_with_first_message( db:Session, )-> list[dict]: @@ -83,8 +87,9 @@ def get_all_sessions_with_first_message( return result - +# -------- # 履歴削除 +# --------- def delete_session( db: Session, session_id: int, @@ -103,7 +108,9 @@ def delete_session( return True +# ---------------- # お気に入りのトグル +# ----------------- def toggle_favorite_session( db: Session, session_id: int, @@ -132,4 +139,3 @@ def toggle_favorite_session( db.add(new_fav) db.commit() return {"message": "お気に入りを追加しました", "is_favorite": True} - \ No newline at end of file From 0f9d0dcf71c25687e9dd56fba04a01fcc3759767 Mon Sep 17 00:00:00 2001 From: sasakifuruta Date: Sun, 25 May 2025 15:04:25 +0900 Subject: [PATCH 47/72] =?UTF-8?q?Amazon=20Bedrock=E4=BB=95=E6=A7=98?= =?UTF-8?q?=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/services/message.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/services/message.py b/app/services/message.py index 1e41c6f..e269540 100644 --- a/app/services/message.py +++ b/app/services/message.py @@ -35,8 +35,8 @@ async def create_message_with_ai( # AI返答を生成 ai_reply, response_type = await run_in_threadpool( - generate_ai_response, # Open API用 - # generate_ai_response_via_bedrock, # Amazon Bedrock用 + # generate_ai_response, # Open API用 + generate_ai_response_via_bedrock, # Amazon Bedrock用 character_mode=character_mode, user_input=content ) From 014b6acd8e800bffeaa48bbdb3dc2e2d0ba8e05e Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Sun, 25 May 2025 16:44:30 +0900 Subject: [PATCH 48/72] =?UTF-8?q?[fix]=E6=AD=A3=E3=81=97=E3=81=84=E3=83=91?= =?UTF-8?q?=E3=82=B9=E3=81=A7=E5=8B=95=E4=BD=9C=E3=81=99=E3=82=8B=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 586914b..db623ec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,14 +6,13 @@ on: - main - develop paths: - - 'backend/**' - - '.github/workflows/ci.yml' + - '**' pull_request: branches: - main - develop paths: - - 'backend/**' + - '**' workflow_dispatch: jobs: @@ -29,7 +28,7 @@ jobs: with: python-version: '3.11' cache: 'pip' - cache-dependency-path: 'backend/requirements.txt' + cache-dependency-path: 'requirements.txt' - name: Install dependencies working-directory: backend @@ -52,7 +51,7 @@ jobs: - name: Build API Docker image (local) uses: docker/build-push-action@v5 with: - context: ./backend/api + context: ./app/api push: false load: true tags: saburo-api:local-test From c3011f55f0c26d442f7f37bf52c2a6088e6af997 Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Sun, 25 May 2025 16:50:15 +0900 Subject: [PATCH 49/72] =?UTF-8?q?[fix]checkout=20=E3=83=91=E3=82=B9?= =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3=E3=81=97=E3=81=A6=20requirements.tx?= =?UTF-8?q?t=20=E3=82=92=E8=AA=8D=E8=AD=98=E5=87=BA=E6=9D=A5=E3=82=8B?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db623ec..e87e936 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + with: + path: . - name: Set up Python uses: actions/setup-python@v5 From 60d273dfaf02503ea0be224b9c2aba0110b49455 Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Sun, 25 May 2025 16:53:38 +0900 Subject: [PATCH 50/72] =?UTF-8?q?[fix]Set=20up=20Python=E3=81=AE=E3=83=91?= =?UTF-8?q?=E3=82=B9=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e87e936..a55822a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: with: python-version: '3.11' cache: 'pip' - cache-dependency-path: 'requirements.txt' + cache-dependency-path: 'backend/requirements.txt' - name: Install dependencies working-directory: backend From 00e9ba5cfd0bc566b72ce4efe2afe7ee7fa35eb7 Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Sun, 25 May 2025 16:57:02 +0900 Subject: [PATCH 51/72] =?UTF-8?q?[fix]checkout=20=E3=83=91=E3=82=B9?= =?UTF-8?q?=E3=82=92=20backend=20=E3=81=AB=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a55822a..fe0d2e1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,14 +5,10 @@ on: branches: - main - develop - paths: - - '**' pull_request: branches: - main - develop - paths: - - '**' workflow_dispatch: jobs: @@ -23,7 +19,7 @@ jobs: - name: Checkout code uses: actions/checkout@v4 with: - path: . + path: backend - name: Set up Python uses: actions/setup-python@v5 From 6c228b6328e5a76c22e53674e8eb923619c7a09e Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Sun, 25 May 2025 16:59:48 +0900 Subject: [PATCH 52/72] =?UTF-8?q?[fix]checkout=20path=20=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe0d2e1..8eaa95e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,8 +18,6 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - with: - path: backend - name: Set up Python uses: actions/setup-python@v5 From ce40e7bd0e5af471388cabb60a639d7268a9cdee Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Sun, 25 May 2025 17:03:17 +0900 Subject: [PATCH 53/72] =?UTF-8?q?[fix]cache-dependency=E3=81=A8=E3=83=87?= =?UTF-8?q?=E3=82=A3=E3=83=AC=E3=82=AF=E3=83=88=E3=83=AA=E6=A7=8B=E9=80=A0?= =?UTF-8?q?=E7=B5=B1=E4=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8eaa95e..58bf662 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,21 +24,18 @@ jobs: with: python-version: '3.11' cache: 'pip' - cache-dependency-path: 'backend/requirements.txt' + cache-dependency-path: 'requirements.txt' - name: Install dependencies - working-directory: backend run: | python -m pip install --upgrade pip pip install pytest flake8 pip install -r requirements.txt - name: Lint with flake8 - working-directory: backend run: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - name: Test with pytest - working-directory: backend run: pytest || echo "No tests configured" - name: Set up Docker Buildx @@ -51,13 +48,11 @@ jobs: push: false load: true tags: saburo-api:local-test - cache-from: type=gha - cache-to: type=gha,mode=max - name: Build Migration Docker image (local) uses: docker/build-push-action@v5 with: - context: ./backend/migration + context: ./app/migration push: false load: true tags: saburo-migration:local-test From f004e85158a803ffd4bc9b1a8380fc489b33fe02 Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Sun, 25 May 2025 17:16:07 +0900 Subject: [PATCH 54/72] =?UTF-8?q?[fix]requirements.txt=E3=81=AE=E3=83=91?= =?UTF-8?q?=E3=82=B9=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 58bf662..0724b67 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,18 +24,19 @@ jobs: with: python-version: '3.11' cache: 'pip' - cache-dependency-path: 'requirements.txt' + cache-dependency-path: 'app/requirements.txt' - name: Install dependencies run: | python -m pip install --upgrade pip pip install pytest flake8 - pip install -r requirements.txt + pip install -r app/requirements.txt - name: Lint with flake8 - run: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + run: flake8 app/ --count --select=E9,F63,F7,F82 --show-source --statistics - name: Test with pytest + working-directory: ./app run: pytest || echo "No tests configured" - name: Set up Docker Buildx From 7f47d4cac1a5862a915dff7047baa7df1dcf510f Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Sun, 25 May 2025 17:20:07 +0900 Subject: [PATCH 55/72] =?UTF-8?q?[fix]Docker=E3=83=93=E3=83=AB=E3=83=89?= =?UTF-8?q?=E3=81=AEcontext=E3=83=91=E3=82=B9=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0724b67..0813fd4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,18 +45,10 @@ jobs: - name: Build API Docker image (local) uses: docker/build-push-action@v5 with: - context: ./app/api + context: . push: false load: true tags: saburo-api:local-test - - - name: Build Migration Docker image (local) - uses: docker/build-push-action@v5 - with: - context: ./app/migration - push: false - load: true - tags: saburo-migration:local-test cache-from: type=gha cache-to: type=gha,mode=max From 4760d7808060cc26180f28aa2dfa884512590a01 Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Sun, 25 May 2025 17:33:09 +0900 Subject: [PATCH 56/72] =?UTF-8?q?[fix]artifact=20=E3=81=8C=E5=AD=98?= =?UTF-8?q?=E5=9C=A8=E3=81=97=E3=81=AA=E3=81=84=E3=82=A8=E3=83=A9=E3=83=BC?= =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd.yml | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index d7c58b4..7c2b51c 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -23,10 +23,14 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - + with: + ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }} + - name: Download commit info - if: github.event.inputs.commit_sha == '' + if: github.event.inputs.commit_sha == '' && github.event_name == 'workflow_run' uses: actions/download-artifact@v4 + continue-on-error: true + id: download-artifact with: name: backend-commit-info path: /tmp/artifacts @@ -62,21 +66,14 @@ jobs: - name: Build and push API image uses: docker/build-push-action@v5 with: - context: ./backend/api + context: ./backend + fiel: ./backend/Dockerfile + target: api push: true tags: ${{ steps.login-ecr.outputs.registry }}/saburo-api:${{ steps.set-sha.outputs.commit_sha }},${{ steps.login-ecr.outputs.registry }}/saburo-api:latest cache-from: type=gha cache-to: type=gha,mode=max - - name: Build and push Migration image - uses: docker/build-push-action@v5 - with: - context: ./backend/migration - push: true - tags: ${{ steps.login-ecr.outputs.registry }}/saburo-migration:${{ steps.set-sha.outputs.commit_sha }},${{ steps.login-ecr.outputs.registry }}/saburo-migration:latest - cache-from: type=gha - cache-to: type=gha,mode=max - - name: Update ECS API task definition id: task-def-api uses: aws-actions/amazon-ecs-render-task-definition@v1 From 9ec71ef1250f9a8ff3f88830423fdaaddf8072c3 Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Sun, 25 May 2025 23:15:45 +0900 Subject: [PATCH 57/72] =?UTF-8?q?[fix]=E3=82=BF=E3=83=BC=E3=82=B2=E3=83=83?= =?UTF-8?q?=E3=83=88=E5=90=8D=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 7c2b51c..33a76ca 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -20,6 +20,7 @@ jobs: runs-on: ubuntu-latest if: ${{ (github.event_name == 'workflow_dispatch') || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'main') }} environment: production + steps: - name: Checkout code uses: actions/checkout@v4 @@ -67,7 +68,7 @@ jobs: uses: docker/build-push-action@v5 with: context: ./backend - fiel: ./backend/Dockerfile + file: ./backend/Dockerfile target: api push: true tags: ${{ steps.login-ecr.outputs.registry }}/saburo-api:${{ steps.set-sha.outputs.commit_sha }},${{ steps.login-ecr.outputs.registry }}/saburo-api:latest @@ -86,14 +87,14 @@ jobs: uses: aws-actions/amazon-ecs-deploy-task-definition@v1 with: task-definition: ${{ steps.task-def-api.outputs.task-definition }} - service: saburo-api-service - cluster: saburo-cluster + service: prod-ecs-service + cluster: prod-ecs-cluster wait-for-service-stability: true - name: Run DB Migration run: | aws ecs run-task \ - --cluster saburo-cluster \ - --task-definition saburo-migration \ + --cluster prod-ecs-cluster \ + --task-definition prod-saburo-app-task \ --launch-type FARGATE \ --network-configuration "awsvpcConfiguration={subnets=[${{ secrets.ECS_SUBNETS }}],securityGroups=[${{ secrets.ECS_SECURITY_GROUPS }}],assignPublicIp=ENABLED}" \ No newline at end of file From 3053edfefbf6a225767b6e0da5b458f664cc6747 Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Mon, 26 May 2025 11:34:40 +0900 Subject: [PATCH 58/72] =?UTF-8?q?[fix]Terraform=20=E3=81=A7=E5=87=BA?= =?UTF-8?q?=E5=8A=9B=E3=81=95=E3=82=8C=E3=81=9F=20rds=5Fendpoint=20?= =?UTF-8?q?=E3=82=92=E3=82=82=E3=81=A8=E3=81=AB=20DATABASE=5FURL=20?= =?UTF-8?q?=E3=82=92=20Secrets=20Manager=20=E3=81=AB=E5=8B=95=E7=9A=84?= =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd.yml | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 33a76ca..3180732 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -35,8 +35,6 @@ jobs: with: name: backend-commit-info path: /tmp/artifacts - github-token: ${{ secrets.GITHUB_TOKEN }} - run-id: ${{ github.event.workflow_run.id }} - name: Set commit SHA id: set-sha @@ -90,6 +88,27 @@ jobs: service: prod-ecs-service cluster: prod-ecs-cluster wait-for-service-stability: true + + - name: Checkout infra-Terraform repository + uses: actions/checkout@v4 + with: + repository: 2025SpringTeamA/infra-Terraform + path: infra + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Get RDS endpoint from Terraform output + id: get-rds + run: | + cd infra + terraform init -backend=false + echo "RDS_ENDPOINT=$(terraform output -raw rds_endpoint)" >> $GITHUB_ENV + + - name: Update Secrets Manager with new DATABASE_URL + run: | + DB_URL="mysql+pymysql://fastapi_user:fastapi_pass@${RDS_ENDPOINT}/fastapi_db" + aws secretsmanager update-secret \ + --secret-id prod/saburo-fastapi/db-credentials \ + --secret-string "{\"MYSQL_DATABASE\":\"fastapi_db\",\"MYSQL_USER\":\"fastapi_user\",\"MYSQL_PASSWORD\":\"fastapi_pass\",\"MYSQL_ROOT_PASSWORD\":\"rootpassword\",\"DATABASE_URL\":\"$DB_URL\"}" - name: Run DB Migration run: | From 0349e7ae46f88502d27417f16f86c8f4093193c3 Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Mon, 26 May 2025 17:12:16 +0900 Subject: [PATCH 59/72] =?UTF-8?q?[fix]workflow=5Frun=E3=83=88=E3=83=AA?= =?UTF-8?q?=E3=82=AC=E3=83=BC=E3=81=A7=E3=81=AE=E3=82=A2=E3=83=BC=E3=83=86?= =?UTF-8?q?=E3=82=A3=E3=83=95=E3=82=A1=E3=82=AF=E3=83=88=E5=8F=96=E5=BE=97?= =?UTF-8?q?=E5=88=B6=E9=99=90=E3=81=AB=E3=82=88=E3=82=8B=E3=82=A8=E3=83=A9?= =?UTF-8?q?=E3=83=BC=E3=82=92=E5=9B=9E=E9=81=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd.yml | 9 --------- .github/workflows/ci.yml | 16 +--------------- 2 files changed, 1 insertion(+), 24 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 3180732..6c7ff8a 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -27,15 +27,6 @@ jobs: with: ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }} - - name: Download commit info - if: github.event.inputs.commit_sha == '' && github.event_name == 'workflow_run' - uses: actions/download-artifact@v4 - continue-on-error: true - id: download-artifact - with: - name: backend-commit-info - path: /tmp/artifacts - - name: Set commit SHA id: set-sha run: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0813fd4..ebe2943 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,18 +50,4 @@ jobs: load: true tags: saburo-api:local-test cache-from: type=gha - cache-to: type=gha,mode=max - - - name: Save commit info - if: github.event_name != 'pull_request' - run: | - mkdir -p /tmp/artifacts - echo "${{ github.sha }}" > /tmp/artifacts/commit-sha - - - name: Upload commit SHA - if: github.event_name != 'pull_request' - uses: actions/upload-artifact@v4 - with: - name: backend-commit-info - path: /tmp/artifacts/commit-sha - retention-days: 7 \ No newline at end of file + cache-to: type=gha,mode=max \ No newline at end of file From dc911e80239fd08e48f29935c74959488f7b119e Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Mon, 26 May 2025 17:51:26 +0900 Subject: [PATCH 60/72] =?UTF-8?q?[fix][fix]=20Docker=E3=83=93=E3=83=AB?= =?UTF-8?q?=E3=83=89=E6=99=82=E3=81=AEcontext=E3=83=91=E3=82=B9=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 6c7ff8a..cf0d5f7 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -56,8 +56,8 @@ jobs: - name: Build and push API image uses: docker/build-push-action@v5 with: - context: ./backend - file: ./backend/Dockerfile + context: . + file: ./Dockerfile target: api push: true tags: ${{ steps.login-ecr.outputs.registry }}/saburo-api:${{ steps.set-sha.outputs.commit_sha }},${{ steps.login-ecr.outputs.registry }}/saburo-api:latest From a741a6b65a974bf5ca230faee0c4180288042c90 Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Mon, 26 May 2025 19:43:03 +0900 Subject: [PATCH 61/72] =?UTF-8?q?[add]=E3=82=A8=E3=83=A9=E3=83=BC=E3=83=8F?= =?UTF-8?q?=E3=83=B3=E3=83=89=E3=83=AA=E3=83=B3=E3=82=B0=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd.yml | 61 ++++++++++++++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 11 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index cf0d5f7..10d29b2 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -30,11 +30,10 @@ jobs: - name: Set commit SHA id: set-sha run: | - if [ -n "${{ github.event.inputs.commit_sha }}" ]; then - echo "commit_sha=${{ github.event.inputs.commit_sha }}" >> "$GITHUB_OUTPUT" - elif [ -f "/tmp/artifacts/commit-sha" ]; then - SHA=$(cat /tmp/artifacts/commit-sha) - echo "commit_sha=$SHA" >> "$GITHUB_OUTPUT" + if [ "${{ github.event_name }}" == "workflow_dispatch" ] && [ -n "${{ inputs.commit_sha }}" ]; then + echo "commit_sha=${{ inputs.commit_sha }}" >> "$GITHUB_OUTPUT" + elif [ "${{ github.event_name }}" == "workflow_run" ]; then + echo "commit_sha=${{ github.event.workflow_run.head_sha }}" >> "$GITHUB_OUTPUT" else echo "commit_sha=${{ github.sha }}" >> "$GITHUB_OUTPUT" fi @@ -92,19 +91,59 @@ jobs: run: | cd infra terraform init -backend=false - echo "RDS_ENDPOINT=$(terraform output -raw rds_endpoint)" >> $GITHUB_ENV + if ! RDS_ENDPOINT=$(terraform output -raw rds_endpoint 2 >/dev/null); then + echo "Failed to get RDS endpoint from Terraform output" + exit 1 + fi + if [ -z "$RDS_ENDPOINT" ]; then + echo "RDS endpoint is empty" + exit 1 + fi + echo "rds=$RDS_ENDPOINT" >> $GITHUB_OUTPUT + - name: Update Secrets Manager with new DATABASE_URL run: | - DB_URL="mysql+pymysql://fastapi_user:fastapi_pass@${RDS_ENDPOINT}/fastapi_db" - aws secretsmanager update-secret \ + DB_URL="mysql+pymysql://fastapi_user:fastapi_pass@${{ steps.get-rds.outputs.rds }}/fastapi_db" + if ! aws secretsmanager update-secret \ --secret-id prod/saburo-fastapi/db-credentials \ - --secret-string "{\"MYSQL_DATABASE\":\"fastapi_db\",\"MYSQL_USER\":\"fastapi_user\",\"MYSQL_PASSWORD\":\"fastapi_pass\",\"MYSQL_ROOT_PASSWORD\":\"rootpassword\",\"DATABASE_URL\":\"$DB_URL\"}" + --secret-string "{\"MYSQL_DATABASE\":\"fastapi_db\",\"MYSQL_USER\":\"fastapi_user\",\"MYSQL_PASSWORD\":\"fastapi_pass\",\"MYSQL_ROOT_PASSWORD\":\"rootpassword\",\"DATABASE_URL\":\"$DB_URL\"}" + then + echo "Failed to update Secrets Manager" + exit 1 + fi - name: Run DB Migration run: | - aws ecs run-task \ + TASK_ARN=$(aws ecs run-task \ --cluster prod-ecs-cluster \ --task-definition prod-saburo-app-task \ --launch-type FARGATE \ - --network-configuration "awsvpcConfiguration={subnets=[${{ secrets.ECS_SUBNETS }}],securityGroups=[${{ secrets.ECS_SECURITY_GROUPS }}],assignPublicIp=ENABLED}" \ No newline at end of file + --network-configuration "awsvpcConfiguration={subnets=[${{ secrets.ECS_SUBNETS }}],securityGroups=[${{ secrets.ECS_SECURITY_GROUPS }}],assignPublicIp=ENABLED}" + --overrides '{"containerOverrides":[{"name":"api","command":["python","-m","alembic","upgrade","head"]}]}' \ + --query 'tasks[0].taskArn' \ + --output text) + + if [ -z "$TASK_ARN" ] || [ "$TASK_ARN" = "None" ]; then + echo "Failed to start migration task" + exit 1 + fi + + echo "Migration task started: $TASK_ARN" + + # タスクの完了を待機 + aws ecs wait tasks-stopped --cluster prod-ecs-cluster --tasks $TASK_ARN + + # タスクの終了コードを確認 + EXIT_CODE=$(aws ecs describe-tasks \ + --cluster prod-ecs-cluster \ + --tasks $TASK_ARN \ + --query 'tasks[0].containers[0].exitCode' \ + --output text) + + if [ "$EXIT_CODE" != "0" ]; then + echo "Migration task failed with exit code: $EXIT_CODE" + exit 1 + fi + + echo "Migration completed successfully" \ No newline at end of file From 5cea54e1ab132d9ba363bd598291070794e8686f Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Mon, 26 May 2025 19:58:00 +0900 Subject: [PATCH 62/72] =?UTF-8?q?[fix]Docker=20build=E3=81=AEtarget:=20api?= =?UTF-8?q?=E6=8C=87=E5=AE=9A=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 10d29b2..7f37eb6 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -57,7 +57,6 @@ jobs: with: context: . file: ./Dockerfile - target: api push: true tags: ${{ steps.login-ecr.outputs.registry }}/saburo-api:${{ steps.set-sha.outputs.commit_sha }},${{ steps.login-ecr.outputs.registry }}/saburo-api:latest cache-from: type=gha From 40e708c8c2baa16afc9a6e5302b76bf20d3aeef1 Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Mon, 26 May 2025 20:47:58 +0900 Subject: [PATCH 63/72] =?UTF-8?q?[fix]ECR=E3=83=AA=E3=83=9D=E3=82=B8?= =?UTF-8?q?=E3=83=88=E3=83=AA=E5=90=8D=E3=81=A8=E3=83=9E=E3=82=A4=E3=82=B0?= =?UTF-8?q?=E3=83=AC=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3=E7=94=A8=E3=82=B3?= =?UTF-8?q?=E3=83=B3=E3=83=86=E3=83=8A=E5=90=8D=E3=81=AE=E4=B8=8D=E4=B8=80?= =?UTF-8?q?=E8=87=B4=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 7f37eb6..f9d6a99 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -58,7 +58,7 @@ jobs: context: . file: ./Dockerfile push: true - tags: ${{ steps.login-ecr.outputs.registry }}/saburo-api:${{ steps.set-sha.outputs.commit_sha }},${{ steps.login-ecr.outputs.registry }}/saburo-api:latest + tags: ${{ steps.login-ecr.outputs.registry }}/saburo-fastapi:${{ steps.set-sha.outputs.commit_sha }},${{ steps.login-ecr.outputs.registry }}/saburo-fastapi:latest cache-from: type=gha cache-to: type=gha,mode=max @@ -67,8 +67,8 @@ jobs: uses: aws-actions/amazon-ecs-render-task-definition@v1 with: task-definition: backend/task-definitions/api.json - container-name: api - image: ${{ steps.login-ecr.outputs.registry }}/saburo-api:${{ steps.set-sha.outputs.commit_sha }} + container-name: fastapi-app + image: ${{ steps.login-ecr.outputs.registry }}/saburo-fastapi:${{ steps.set-sha.outputs.commit_sha }} - name: Deploy ECS API task definition uses: aws-actions/amazon-ecs-deploy-task-definition@v1 @@ -119,7 +119,7 @@ jobs: --task-definition prod-saburo-app-task \ --launch-type FARGATE \ --network-configuration "awsvpcConfiguration={subnets=[${{ secrets.ECS_SUBNETS }}],securityGroups=[${{ secrets.ECS_SECURITY_GROUPS }}],assignPublicIp=ENABLED}" - --overrides '{"containerOverrides":[{"name":"api","command":["python","-m","alembic","upgrade","head"]}]}' \ + --overrides '{"containerOverrides":[{"name":"db-migration","command":["python","-m","alembic","upgrade","head"]}]}' \ --query 'tasks[0].taskArn' \ --output text) From 971884d2c23ba29a00c4dfaeea92e4ecdf36adb1 Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Mon, 26 May 2025 21:07:15 +0900 Subject: [PATCH 64/72] =?UTF-8?q?[fix]ECS=E3=82=BF=E3=82=B9=E3=82=AF?= =?UTF-8?q?=E5=AE=9A=E7=BE=A9=E3=81=AE=E3=83=AC=E3=83=B3=E3=83=80=E3=83=AA?= =?UTF-8?q?=E3=83=B3=E3=82=B0=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd.yml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index f9d6a99..ad15fdf 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -62,18 +62,10 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max - - name: Update ECS API task definition - id: task-def-api - uses: aws-actions/amazon-ecs-render-task-definition@v1 - with: - task-definition: backend/task-definitions/api.json - container-name: fastapi-app - image: ${{ steps.login-ecr.outputs.registry }}/saburo-fastapi:${{ steps.set-sha.outputs.commit_sha }} - - name: Deploy ECS API task definition uses: aws-actions/amazon-ecs-deploy-task-definition@v1 with: - task-definition: ${{ steps.task-def-api.outputs.task-definition }} + task-definition: prod-saburo-app-task service: prod-ecs-service cluster: prod-ecs-cluster wait-for-service-stability: true From 694e6d1e2cdb342a67b79d3c1924f2e845f9ba69 Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Mon, 26 May 2025 21:22:44 +0900 Subject: [PATCH 65/72] =?UTF-8?q?[fix]ECR=20push=E3=80=81Secrets=20Manager?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E3=80=81DB=E3=83=9E=E3=82=A4=E3=82=B0?= =?UTF-8?q?=E3=83=AC=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3=E3=81=AE=E4=B8=8D?= =?UTF-8?q?=E5=85=B7=E5=90=88=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd.yml | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index ad15fdf..93bdae7 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -62,14 +62,13 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max - - name: Deploy ECS API task definition - uses: aws-actions/amazon-ecs-deploy-task-definition@v1 - with: - task-definition: prod-saburo-app-task - service: prod-ecs-service - cluster: prod-ecs-cluster - wait-for-service-stability: true - + - name: Force ECS Service New Deployment + run: | + aws ecs update-service \ + --cluster prod-ecs-cluster \ + --service prod-ecs-service \ + --force-new-deployment + - name: Checkout infra-Terraform repository uses: actions/checkout@v4 with: @@ -82,7 +81,7 @@ jobs: run: | cd infra terraform init -backend=false - if ! RDS_ENDPOINT=$(terraform output -raw rds_endpoint 2 >/dev/null); then + if ! RDS_ENDPOINT=$(terraform output -raw rds_endpoint 2>/dev/null); then echo "Failed to get RDS endpoint from Terraform output" exit 1 fi From 9d7d90a0d78836b2b2c63b0d87e39e2cd12dfb1b Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Mon, 26 May 2025 23:50:35 +0900 Subject: [PATCH 66/72] =?UTF-8?q?[fix]COPY=20app=20./app=E3=81=B8=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 0eb2d27..caaa45b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,10 +13,10 @@ RUN apt-get update && \ COPY app/requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -COPY app/ . +COPY app ./app ENV PYTHONPATH=/app EXPOSE 8000 -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] \ No newline at end of file From 702c1599aad70556b46a986633416d6fe9dc0c89 Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Tue, 27 May 2025 00:05:52 +0900 Subject: [PATCH 67/72] =?UTF-8?q?[fix]=E3=83=91=E3=82=B9=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index caaa45b..6ec9d1d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,10 +13,10 @@ RUN apt-get update && \ COPY app/requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -COPY app ./app +COPY app . ENV PYTHONPATH=/app EXPOSE 8000 -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] \ No newline at end of file +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] \ No newline at end of file From 43f5a7232fdc3cb7a6201ffe233e530961e36417 Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Tue, 27 May 2025 00:19:28 +0900 Subject: [PATCH 68/72] =?UTF-8?q?[fix]=E3=83=91=E3=82=B9=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6ec9d1d..caaa45b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,10 +13,10 @@ RUN apt-get update && \ COPY app/requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -COPY app . +COPY app ./app ENV PYTHONPATH=/app EXPOSE 8000 -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] \ No newline at end of file +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] \ No newline at end of file From cf059315cac5e36c74255c2d681f6e98cfd62574 Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Wed, 28 May 2025 20:37:58 +0900 Subject: [PATCH 69/72] =?UTF-8?q?[fix]RunTask=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd.yml | 50 ++++++++-------------------------------- 1 file changed, 10 insertions(+), 40 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 93bdae7..51940e0 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -56,19 +56,12 @@ jobs: uses: docker/build-push-action@v5 with: context: . - file: ./Dockerfile + file: ./Dockerfile.prod push: true tags: ${{ steps.login-ecr.outputs.registry }}/saburo-fastapi:${{ steps.set-sha.outputs.commit_sha }},${{ steps.login-ecr.outputs.registry }}/saburo-fastapi:latest cache-from: type=gha cache-to: type=gha,mode=max - - name: Force ECS Service New Deployment - run: | - aws ecs update-service \ - --cluster prod-ecs-cluster \ - --service prod-ecs-service \ - --force-new-deployment - - name: Checkout infra-Terraform repository uses: actions/checkout@v4 with: @@ -76,6 +69,11 @@ jobs: path: infra token: ${{ secrets.GITHUB_TOKEN }} + - name: Install Terraform CLI + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: 1.6.6 + - name: Get RDS endpoint from Terraform output id: get-rds run: | @@ -103,37 +101,9 @@ jobs: exit 1 fi - - name: Run DB Migration + - name: Force ECS Service New Deployment run: | - TASK_ARN=$(aws ecs run-task \ - --cluster prod-ecs-cluster \ - --task-definition prod-saburo-app-task \ - --launch-type FARGATE \ - --network-configuration "awsvpcConfiguration={subnets=[${{ secrets.ECS_SUBNETS }}],securityGroups=[${{ secrets.ECS_SECURITY_GROUPS }}],assignPublicIp=ENABLED}" - --overrides '{"containerOverrides":[{"name":"db-migration","command":["python","-m","alembic","upgrade","head"]}]}' \ - --query 'tasks[0].taskArn' \ - --output text) - - if [ -z "$TASK_ARN" ] || [ "$TASK_ARN" = "None" ]; then - echo "Failed to start migration task" - exit 1 - fi - - echo "Migration task started: $TASK_ARN" - - # タスクの完了を待機 - aws ecs wait tasks-stopped --cluster prod-ecs-cluster --tasks $TASK_ARN - - # タスクの終了コードを確認 - EXIT_CODE=$(aws ecs describe-tasks \ + aws ecs update-service \ --cluster prod-ecs-cluster \ - --tasks $TASK_ARN \ - --query 'tasks[0].containers[0].exitCode' \ - --output text) - - if [ "$EXIT_CODE" != "0" ]; then - echo "Migration task failed with exit code: $EXIT_CODE" - exit 1 - fi - - echo "Migration completed successfully" \ No newline at end of file + --service prod-ecs-service \ + --force-new-deployment \ No newline at end of file From 24251b24dcc2a985acf9e632169322632875440b Mon Sep 17 00:00:00 2001 From: ponz <155151085+sasakifuruta@users.noreply.github.com> Date: Sat, 31 May 2025 13:43:57 +0900 Subject: [PATCH 70/72] Update main.py --- app/main.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/app/main.py b/app/main.py index 59a1b75..a1ee196 100644 --- a/app/main.py +++ b/app/main.py @@ -27,9 +27,4 @@ allow_headers=["*"], ) -@app.get("/") -def read_root(): - return {"message": "Hello from FastAPI + MySQL in Docker!"} - - -app.include_router(api_router, prefix="/api") \ No newline at end of file +app.include_router(api_router, prefix="/api") From 37f4931aecbd3f7e13bf75b68f13faa741840804 Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Sun, 1 Jun 2025 14:56:04 +0900 Subject: [PATCH 71/72] =?UTF-8?q?[add]=E3=83=AB=E3=83=BC=E3=83=88=E3=82=A8?= =?UTF-8?q?=E3=83=B3=E3=83=89=E3=83=9D=E3=82=A4=E3=83=B3=E3=83=88=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=E3=81=97=E3=80=81404=E3=82=A8=E3=83=A9?= =?UTF-8?q?=E3=83=BC=E8=A7=A3=E6=B6=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/main.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/main.py b/app/main.py index a1ee196..5e6bf52 100644 --- a/app/main.py +++ b/app/main.py @@ -27,4 +27,12 @@ allow_headers=["*"], ) +@app.get("/") +async def root(): + return {"message": "Saburo FastAPI application is running", "status": "ok"} + +@app.get("/health") +async def health_check(): + return {"status": "healthy", "service": "saburo-fastapi"} + app.include_router(api_router, prefix="/api") From c6c8e40e4531de525800c63ea7b109fcc06a0e0b Mon Sep 17 00:00:00 2001 From: ARISA1115 Date: Sun, 1 Jun 2025 15:01:12 +0900 Subject: [PATCH 72/72] =?UTF-8?q?[fix]ECR=E3=83=97=E3=83=83=E3=82=B7?= =?UTF-8?q?=E3=83=A5=E3=81=AE=E3=81=BF=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd.yml | 95 +++++++--------------------------------- 1 file changed, 17 insertions(+), 78 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 51940e0..8bc2b42 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -1,4 +1,4 @@ -name: Deploy FastAPI to ECR and ECS +name: Deploy FastAPI to ECR on: workflow_run: @@ -8,15 +8,10 @@ on: branches: - main workflow_dispatch: - inputs: - commit_sha: - description: 'Commit SHA to deploy' - required: false - type: string jobs: - deploy-to-ecr-ecs: - name: Deploy to ECR and ECS + deploy-to-ecr: + name: Deploy to ECR runs-on: ubuntu-latest if: ${{ (github.event_name == 'workflow_dispatch') || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'main') }} environment: production @@ -24,86 +19,30 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - with: - ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }} - - - name: Set commit SHA - id: set-sha - run: | - if [ "${{ github.event_name }}" == "workflow_dispatch" ] && [ -n "${{ inputs.commit_sha }}" ]; then - echo "commit_sha=${{ inputs.commit_sha }}" >> "$GITHUB_OUTPUT" - elif [ "${{ github.event_name }}" == "workflow_run" ]; then - echo "commit_sha=${{ github.event.workflow_run.head_sha }}" >> "$GITHUB_OUTPUT" - else - echo "commit_sha=${{ github.sha }}" >> "$GITHUB_OUTPUT" - fi - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ap-northeast-1 - - - name: Login to Amazon ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build and push API image - uses: docker/build-push-action@v5 - with: - context: . - file: ./Dockerfile.prod - push: true - tags: ${{ steps.login-ecr.outputs.registry }}/saburo-fastapi:${{ steps.set-sha.outputs.commit_sha }},${{ steps.login-ecr.outputs.registry }}/saburo-fastapi:latest - cache-from: type=gha - cache-to: type=gha,mode=max + aws-region: ${{ secrets.AWS_REGION }} - - name: Checkout infra-Terraform repository - uses: actions/checkout@v4 - with: - repository: 2025SpringTeamA/infra-Terraform - path: infra - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Install Terraform CLI - uses: hashicorp/setup-terraform@v3 - with: - terraform_version: 1.6.6 - - - name: Get RDS endpoint from Terraform output - id: get-rds + - name: Log in to Amazon ECR run: | - cd infra - terraform init -backend=false - if ! RDS_ENDPOINT=$(terraform output -raw rds_endpoint 2>/dev/null); then - echo "Failed to get RDS endpoint from Terraform output" - exit 1 - fi - if [ -z "$RDS_ENDPOINT" ]; then - echo "RDS endpoint is empty" - exit 1 - fi - echo "rds=$RDS_ENDPOINT" >> $GITHUB_OUTPUT + aws ecr get-login-password --region ${{ secrets.AWS_REGION }} \ + | docker login --username AWS --password-stdin \ + $(aws sts get-caller-identity --query 'Account' --output text).dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com + - name: Build Docker image + run: | + docker build -t ${{ secrets.ECR_REPOSITORY }}:${{ secrets.IMAGE_TAG }} -f Dockerfile.prod . - - name: Update Secrets Manager with new DATABASE_URL + - name: Tag Docker image run: | - DB_URL="mysql+pymysql://fastapi_user:fastapi_pass@${{ steps.get-rds.outputs.rds }}/fastapi_db" - if ! aws secretsmanager update-secret \ - --secret-id prod/saburo-fastapi/db-credentials \ - --secret-string "{\"MYSQL_DATABASE\":\"fastapi_db\",\"MYSQL_USER\":\"fastapi_user\",\"MYSQL_PASSWORD\":\"fastapi_pass\",\"MYSQL_ROOT_PASSWORD\":\"rootpassword\",\"DATABASE_URL\":\"$DB_URL\"}" - then - echo "Failed to update Secrets Manager" - exit 1 - fi + docker tag ${{ secrets.ECR_REPOSITORY }}:${{ secrets.IMAGE_TAG }} \ + $(aws sts get-caller-identity --query 'Account' --output text).dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com/${{ secrets.ECR_REPOSITORY }}:${{ secrets.IMAGE_TAG }} - - name: Force ECS Service New Deployment + - name: Push Docker image to Amazon ECR run: | - aws ecs update-service \ - --cluster prod-ecs-cluster \ - --service prod-ecs-service \ - --force-new-deployment \ No newline at end of file + docker push \ + $(aws sts get-caller-identity --query 'Account' --output text).dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com/${{ secrets.ECR_REPOSITORY }}:${{ secrets.IMAGE_TAG }} \ No newline at end of file