This commit is contained in:
2025-12-13 14:39:50 +03:00
commit b666cdcb95
79 changed files with 3081 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
from app.api.v1.router import api_router
__all__ = ["api_router"]

View File

@@ -0,0 +1,60 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import get_db
from app.api.deps import get_current_active_user
from app.models.user import User
from app.schemas.ai import ChatRequest, ChatResponse, ScheduleGenerateRequest, ScheduleGenerateResponse
from app.services.chat_service import chat_service
from app.services.schedule_generator import schedule_generator
router = APIRouter()
@router.post("/chat", response_model=ChatResponse)
async def chat_with_ai(
request: ChatRequest,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db)
):
"""Чат с ИИ-агентом 'Планета Земля'"""
try:
response = await chat_service.chat(
db=db,
user_id=current_user.id,
request=request
)
return response
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Chat error: {str(e)}"
)
@router.post("/schedule/generate", response_model=ScheduleGenerateResponse)
async def generate_schedule_ai(
request: ScheduleGenerateRequest,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db)
):
"""Сгенерировать расписание через ИИ"""
try:
result = await schedule_generator.generate(
db=db,
user_id=current_user.id,
child_age=request.child_age,
preferences=request.preferences,
schedule_date=request.date,
description=request.description
)
return ScheduleGenerateResponse(
schedule_id=result["schedule_id"],
title=result["title"],
tasks=result["tasks"]
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to generate schedule: {str(e)}"
)

View File

@@ -0,0 +1,63 @@
from fastapi import APIRouter, Depends, HTTPException, status, Body
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import get_db
from app.schemas.user import UserCreate, User
from app.schemas.token import Token
from app.services.auth_service import auth_service
from app.api.deps import get_current_active_user
router = APIRouter()
@router.post("/register", response_model=User, status_code=status.HTTP_201_CREATED)
async def register(
user_in: UserCreate,
db: AsyncSession = Depends(get_db)
):
"""Регистрация нового пользователя"""
try:
user = await auth_service.register(db, user_in)
return user
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
@router.post("/login", response_model=Token)
async def login(
form_data: OAuth2PasswordRequestForm = Depends(),
db: AsyncSession = Depends(get_db)
):
"""Аутентификация пользователя"""
token = await auth_service.authenticate(db, form_data.username, form_data.password)
if not token:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
headers={"WWW-Authenticate": "Bearer"},
)
return token
@router.post("/refresh", response_model=Token)
async def refresh_token(
refresh_token: str = Body(..., embed=True)
):
"""Обновление access token"""
new_access_token = auth_service.refresh_access_token(refresh_token)
if not new_access_token:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid refresh token"
)
return Token(access_token=new_access_token, token_type="bearer")
@router.get("/me", response_model=User)
async def read_users_me(current_user: User = Depends(get_current_active_user)):
"""Получить информацию о текущем пользователе"""
return current_user

View File

@@ -0,0 +1,66 @@
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File
from app.api.deps import get_current_active_user
from app.models.user import User
from app.services.storage_service import storage_service
router = APIRouter()
@router.post("/upload")
async def upload_image(
file: UploadFile = File(...),
current_user: User = Depends(get_current_active_user)
):
"""Загрузить изображение"""
# Проверка типа файла
if not file.content_type or not file.content_type.startswith("image/"):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="File must be an image"
)
# Проверка размера (макс 10MB)
file_content = await file.read()
if len(file_content) > 10 * 1024 * 1024:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="File size must be less than 10MB"
)
try:
# Загружаем файл
from io import BytesIO
file_obj = BytesIO(file_content)
url = await storage_service.upload_file(
file_obj=file_obj,
filename=file.filename or "image.jpg",
content_type=file.content_type
)
return {"url": url, "filename": file.filename}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to upload image: {str(e)}"
)
@router.delete("/{file_key}")
async def delete_image(
file_key: str,
current_user: User = Depends(get_current_active_user)
):
"""Удалить изображение"""
try:
success = await storage_service.delete_file(file_key)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="File not found"
)
return {"message": "File deleted successfully"}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to delete image: {str(e)}"
)

View File

@@ -0,0 +1,135 @@
from typing import List
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import get_db
from app.api.deps import get_current_active_user
from app.models.user import User
from app.schemas.reward import Reward, RewardCreate, RewardUpdate
from app.crud.base import CRUDBase
from app.models.reward import Reward as RewardModel
router = APIRouter()
reward_crud = CRUDBase(RewardModel)
@router.get("", response_model=List[Reward])
async def get_rewards(
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=100),
is_claimed: bool = Query(None),
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db)
):
"""Получить список наград пользователя"""
filters = {"user_id": current_user.id}
if is_claimed is not None:
filters["is_claimed"] = is_claimed
rewards = await reward_crud.get_multi(db, skip=skip, limit=limit, filters=filters)
return rewards
@router.get("/{reward_id}", response_model=Reward)
async def get_reward(
reward_id: str,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db)
):
"""Получить награду по ID"""
reward = await reward_crud.get(db, reward_id)
if not reward:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Reward not found"
)
if reward.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions"
)
return reward
@router.post("", response_model=Reward, status_code=status.HTTP_201_CREATED)
async def create_reward(
reward_in: RewardCreate,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db)
):
"""Создать новую награду"""
reward_data = reward_in.model_dump()
reward_data["user_id"] = current_user.id
reward = await reward_crud.create(db, reward_data)
return reward
@router.put("/{reward_id}", response_model=Reward)
async def update_reward(
reward_id: str,
reward_in: RewardUpdate,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db)
):
"""Обновить награду"""
reward = await reward_crud.get(db, reward_id)
if not reward:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Reward not found"
)
if reward.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions"
)
update_data = reward_in.model_dump(exclude_unset=True)
reward = await reward_crud.update(db, reward, update_data)
return reward
@router.delete("/{reward_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_reward(
reward_id: str,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db)
):
"""Удалить награду"""
reward = await reward_crud.get(db, reward_id)
if not reward:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Reward not found"
)
if reward.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions"
)
await reward_crud.delete(db, reward_id)
return None
@router.patch("/{reward_id}/claim", response_model=Reward)
async def claim_reward(
reward_id: str,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db)
):
"""Получить награду (отметить как полученную)"""
reward = await reward_crud.get(db, reward_id)
if not reward:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Reward not found"
)
if reward.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions"
)
reward = await reward_crud.update(db, reward, {"is_claimed": True})
return reward

View File

@@ -0,0 +1,13 @@
from fastapi import APIRouter
from app.api.v1 import auth, schedules, tasks, rewards, images, ai, websocket
api_router = APIRouter()
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
api_router.include_router(schedules.router, prefix="/schedules", tags=["schedules"])
api_router.include_router(tasks.router, prefix="/tasks", tags=["tasks"])
api_router.include_router(rewards.router, prefix="/rewards", tags=["rewards"])
api_router.include_router(images.router, prefix="/images", tags=["images"])
api_router.include_router(ai.router, prefix="/ai", tags=["ai"])
api_router.include_router(websocket.router, prefix="/ws", tags=["websocket"])

View File

@@ -0,0 +1,141 @@
from typing import List
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.ext.asyncio import AsyncSession
from datetime import date
from app.db.session import get_db
from app.api.deps import get_current_active_user
from app.models.user import User
from app.schemas.schedule import Schedule, ScheduleCreate, ScheduleUpdate
from app.crud import schedule as crud_schedule
from app.services.schedule_generator import schedule_generator
from app.schemas.ai import ScheduleGenerateRequest, ScheduleGenerateResponse
router = APIRouter()
@router.get("", response_model=List[Schedule])
async def get_schedules(
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=100),
schedule_date: date = Query(None),
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db)
):
"""Получить список расписаний пользователя"""
if schedule_date:
schedule = await crud_schedule.get_by_date(db, current_user.id, schedule_date)
return [schedule] if schedule else []
else:
schedules = await crud_schedule.get_by_user(db, current_user.id, skip, limit)
return schedules
@router.get("/{schedule_id}", response_model=Schedule)
async def get_schedule(
schedule_id: str,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db)
):
"""Получить расписание по ID"""
schedule = await crud_schedule.get_with_tasks(db, schedule_id)
if not schedule:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Schedule not found"
)
if schedule.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions"
)
return schedule
@router.post("", response_model=Schedule, status_code=status.HTTP_201_CREATED)
async def create_schedule(
schedule_in: ScheduleCreate,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db)
):
"""Создать новое расписание"""
schedule_data = schedule_in.model_dump()
schedule_data["user_id"] = current_user.id
schedule = await crud_schedule.create(db, schedule_data)
return schedule
@router.put("/{schedule_id}", response_model=Schedule)
async def update_schedule(
schedule_id: str,
schedule_in: ScheduleUpdate,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db)
):
"""Обновить расписание"""
schedule = await crud_schedule.get(db, schedule_id)
if not schedule:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Schedule not found"
)
if schedule.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions"
)
update_data = schedule_in.model_dump(exclude_unset=True)
schedule = await crud_schedule.update(db, schedule, update_data)
return schedule
@router.delete("/{schedule_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_schedule(
schedule_id: str,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db)
):
"""Удалить расписание"""
schedule = await crud_schedule.get(db, schedule_id)
if not schedule:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Schedule not found"
)
if schedule.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions"
)
await crud_schedule.delete(db, schedule_id)
return None
@router.post("/generate", response_model=ScheduleGenerateResponse)
async def generate_schedule(
request: ScheduleGenerateRequest,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db)
):
"""Сгенерировать расписание через ИИ"""
try:
result = await schedule_generator.generate(
db=db,
user_id=current_user.id,
child_age=request.child_age,
preferences=request.preferences,
schedule_date=request.date,
description=request.description
)
return ScheduleGenerateResponse(
schedule_id=result["schedule_id"],
title=result["title"],
tasks=result["tasks"]
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to generate schedule: {str(e)}"
)

View File

@@ -0,0 +1,165 @@
from typing import List
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import get_db
from app.api.deps import get_current_active_user
from app.models.user import User
from app.schemas.task import Task, TaskCreate, TaskUpdate
from app.crud import task as crud_task, schedule as crud_schedule
router = APIRouter()
@router.get("/schedule/{schedule_id}", response_model=List[Task])
async def get_tasks(
schedule_id: str,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db)
):
"""Получить все задачи расписания"""
# Проверка прав доступа
schedule = await crud_schedule.get(db, schedule_id)
if not schedule:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Schedule not found"
)
if schedule.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions"
)
tasks = await crud_task.get_by_schedule(db, schedule_id)
return tasks
@router.get("/{task_id}", response_model=Task)
async def get_task(
task_id: str,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db)
):
"""Получить задачу по ID"""
task = await crud_task.get(db, task_id)
if not task:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Task not found"
)
# Проверка прав доступа через расписание
schedule = await crud_schedule.get(db, task.schedule_id)
if schedule.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions"
)
return task
@router.post("", response_model=Task, status_code=status.HTTP_201_CREATED)
async def create_task(
task_in: TaskCreate,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db)
):
"""Создать новую задачу"""
# Проверка прав доступа
schedule = await crud_schedule.get(db, task_in.schedule_id)
if not schedule:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Schedule not found"
)
if schedule.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions"
)
task = await crud_task.create(db, task_in.model_dump())
return task
@router.put("/{task_id}", response_model=Task)
async def update_task(
task_id: str,
task_in: TaskUpdate,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db)
):
"""Обновить задачу"""
task = await crud_task.get(db, task_id)
if not task:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Task not found"
)
# Проверка прав доступа
schedule = await crud_schedule.get(db, task.schedule_id)
if schedule.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions"
)
update_data = task_in.model_dump(exclude_unset=True)
task = await crud_task.update(db, task, update_data)
return task
@router.patch("/{task_id}/complete", response_model=Task)
async def complete_task(
task_id: str,
completed: bool = True,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db)
):
"""Отметить задачу как выполненную/невыполненную"""
task = await crud_task.get(db, task_id)
if not task:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Task not found"
)
# Проверка прав доступа
schedule = await crud_schedule.get(db, task.schedule_id)
if schedule.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions"
)
task = await crud_task.update_completion(db, task_id, completed)
return task
@router.delete("/{task_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_task(
task_id: str,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db)
):
"""Удалить задачу"""
task = await crud_task.get(db, task_id)
if not task:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Task not found"
)
# Проверка прав доступа
schedule = await crud_schedule.get(db, task.schedule_id)
if schedule.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions"
)
await crud_task.delete(db, task_id)
return None

View File

@@ -0,0 +1,109 @@
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Depends
from typing import Dict, Set, Optional
import json
from app.services.chat_service import chat_service
from app.db.session import AsyncSessionLocal
from app.api.deps import get_current_user
from app.core.security import decode_token
router = APIRouter()
# Хранилище активных соединений
active_connections: Dict[str, Set[WebSocket]] = {}
class ConnectionManager:
def __init__(self):
self.active_connections: Dict[str, Set[WebSocket]] = {}
async def connect(self, websocket: WebSocket, user_id: str):
await websocket.accept()
if user_id not in self.active_connections:
self.active_connections[user_id] = set()
self.active_connections[user_id].add(websocket)
def disconnect(self, websocket: WebSocket, user_id: str):
if user_id in self.active_connections:
self.active_connections[user_id].discard(websocket)
if not self.active_connections[user_id]:
del self.active_connections[user_id]
async def send_personal_message(self, message: str, websocket: WebSocket):
await websocket.send_text(message)
async def broadcast_to_user(self, user_id: str, message: str):
if user_id in self.active_connections:
for connection in self.active_connections[user_id]:
await connection.send_text(message)
manager = ConnectionManager()
async def get_user_from_token(token: str) -> Optional[str]:
"""Получить user_id из токена"""
payload = decode_token(token)
if payload:
return payload.get("sub")
return None
@router.websocket("/ws/chat")
async def websocket_chat(websocket: WebSocket):
"""WebSocket endpoint для чата с ИИ"""
# Получаем токен из query параметров
token = websocket.query_params.get("token")
if not token:
await websocket.close(code=1008, reason="Token required")
return
# Проверка токена
user_id = await get_user_from_token(token)
if not user_id:
await websocket.close(code=1008, reason="Unauthorized")
return
await manager.connect(websocket, user_id)
try:
async with AsyncSessionLocal() as db:
while True:
data = await websocket.receive_text()
message_data = json.loads(data)
# Создаем запрос для chat_service
from app.schemas.ai import ChatRequest
request = ChatRequest(
message=message_data.get("message", ""),
conversation_id=message_data.get("conversation_id")
)
# Получаем ответ от ИИ
try:
response = await chat_service.chat(
db=db,
user_id=user_id,
request=request
)
# Отправляем ответ клиенту
await manager.send_personal_message(
json.dumps({
"type": "message",
"response": response.response,
"conversation_id": response.conversation_id,
"tokens_used": response.tokens_used
}),
websocket
)
except Exception as e:
await manager.send_personal_message(
json.dumps({
"type": "error",
"message": str(e)
}),
websocket
)
except WebSocketDisconnect:
manager.disconnect(websocket, user_id)