Compare commits

...

6 Commits

Author SHA1 Message Date
a649fb1192 Изменения:
-добавлены нетворки в докер композ
-исправлен рутинг (баг пайчарма)
-запросы к ии агентам не проходят из-за ссл сертификата (пробовали отключить, но пока не выходит, нужно доделать)
2025-12-19 00:57:13 +03:00
24f4ce118f Обновить new-planet-backend/requirements.txt 2025-12-18 17:18:04 +03:00
c256012a69 Merge pull request 'Refactored:' (#2) from feature/projectUpped into master
Reviewed-on: #2
2025-12-18 17:08:16 +03:00
4bbe086cec Refactored:
- пофикшен баг с авторизацией;
- поменен README.md, более подробно описан запуск проекта;
- починен .env для проекта.
Checked:
- docker-compose работает;
- auth работает;
- чат с нейросетью работает, но кидает 400 из за NEWPLANET-AI-AGENTS,нужно настроить подключение.
2025-12-18 17:07:33 +03:00
4cb6043931 Merge pull request 'Refactored:' (#1) from feature/projectUpped into master
Reviewed-on: #1
2025-12-18 14:33:08 +03:00
e04933b9c1 Refactored:
- пофикшен баг с авторизацией;
- поменен README.md, более подробно описан запуск проекта;
- починен .env для проекта.
Checked:
- docker-compose работает;
- auth работает;
- чат с нейросетью работает, но кидает 400 из за NEWPLANET-AI-AGENTS,нужно настроить подключение.
2025-12-18 14:14:04 +03:00
21 changed files with 1979 additions and 73 deletions

5
.idea/misc.xml generated
View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_23" default="true" project-jdk-name="openjdk-23" project-jdk-type="JavaSDK"> <component name="Black">
<output url="file://$PROJECT_DIR$/out" /> <option name="sdkName" value="Python 3.14 (New-planet-api)" />
</component> </component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.14 (New-planet-api)" project-jdk-type="Python SDK" />
</project> </project>

View File

@@ -2,8 +2,11 @@
<module type="JAVA_MODULE" version="4"> <module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true"> <component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output /> <exclude-output />
<content url="file://$MODULE_DIR$" /> <content url="file://$MODULE_DIR$">
<orderEntry type="inheritedJdk" /> <sourceFolder url="file://$MODULE_DIR$/new-planet-backend" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.14 (New-planet-api)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
</component> </component>
</module> </module>

View File

@@ -1,7 +1,31 @@
DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/newplanet 
REDIS_URL=redis://localhost:6379 GIGACHAT_CLIENT_ID=019966f4-1c5c-7382-9006-b84419fbe5d1
SECRET_KEY=your-secret-key-here GIGACHAT_CLIENT_SECRET=MDE5OTY2ZjQtMWM1Yy03MzgyLTkwMDYtYjg0NDE5ZmJlNWQxOjJjODBmOWE2LWU4YWMtNDE4YS1iOGVkLWE4NTE0YzVkNDAwNw==
GIGACHAT_API_KEY=your-gigachat-api-key GIGACHAT_AUTH_URL=https://ngw.devices.sberbank.ru:9443/api/v2/oauth
MINIO_ENDPOINT=localhost:9000 GIGACHAT_BASE_URL=https://gigachat.devices.sberbank.ru/api/v1
MINIO_ACCESS_KEY=minioadmin GIGACHAT_MODEL_CHAT=GigaChat-2-Lite
MINIO_SECRET_KEY=minioadmin GIGACHAT_MODEL_SCHEDULE=GigaChat-2-Pro
# Security
SECRET_KEY=3db8542397edddbd6162ad823157e36f8d47232aa646725d4799266229ba7aa4
# Database
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=newplanet
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_DB=0
# Storage
STORAGE_ENDPOINT=localhost:9000
STORAGE_ACCESS_KEY=minioadmin
STORAGE_SECRET_KEY=minioadmin
STORAGE_BUCKET=new-planet-images
STORAGE_USE_SSL=false
STORAGE_REGION=us-east-1

View File

@@ -23,30 +23,37 @@ Backend API для мобильного приложения **Новая Пла
### Установка ### Установка
1. Клонируйте репозиторий 1. Клонируйте репозиторий
2. Установите зависимости: 2. Установить окружение ("python -m venv venv")
3. Установите зависимости:
```bash ```bash
pip install -r requirements.txt pip install -r requirements.txt
``` ```
3. Настройте `.env`: 4. Настройте `.env`:
```bash ```bash
cp .env.example .env cp .env.example .env
#В целом вам нужно поменять GIGACHAT API секцию, JWT Secret key сгенерить, просто в поисковике генератор на 256 байт сделаете JWT
#Для гигачата логинетесь, дергаете от туда CLIENT_ID и SECRET KEY
# Отредактируйте .env с вашими настройками # Отредактируйте .env с вашими настройками
``` ```
4. Запустите инфраструктуру (Docker): 5. Запустите инфраструктуру (Docker):
```bash ```bash
docker-compose -f docker/docker-compose.yml up -d docker-compose -f docker/docker-compose.yml up -d
# или используйте вариант ниже,но лучше вариант выше для избежания непредвиденного
#также напоминаю что вам необходим сам запущенный докер чтобы тестировать локально
docker-compose up
``` ```
5. Примените миграции: 6. Примените миграции:
```bash ```bash
alembic upgrade head alembic upgrade head
``` ```
6. Запустите сервер: 7. Запустите сервер:
```bash ```bash
uvicorn app.main:app --reload uvicorn app.main:app --reload
# если не запустилось, проверяйте есть ли .venv(установлено ли окружение для питона), также попробуйте в венве uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
``` ```
API доступен на `http://localhost:8000` API доступен на `http://localhost:8000`
@@ -60,6 +67,9 @@ Swagger UI: `http://localhost:8000/docs`
- `POST /api/v1/auth/refresh` - Обновление токена - `POST /api/v1/auth/refresh` - Обновление токена
- `GET /api/v1/auth/me` - Текущий пользователь - `GET /api/v1/auth/me` - Текущий пользователь
### Для написания всех запросов ниже:
Не забывайте авторизоваться в сваггере, сверху кнопка Authorize
### Schedules ### Schedules
- `GET /api/v1/schedules` - Список расписаний - `GET /api/v1/schedules` - Список расписаний
- `POST /api/v1/schedules` - Создать расписание - `POST /api/v1/schedules` - Создать расписание
@@ -101,6 +111,6 @@ Swagger UI: `http://localhost:8000/docs`
## 🔗 Связанные репозитории ## 🔗 Связанные репозитории
- **Frontend (Android)** — [new-planet-android](https://github.com/your-org/new-planet-android) - **Frontend (Android)** — [new-planet-android](https://git.bro-js.ru/Glevel/New-planet-app.git)
- **AI Agents** — [new-planet-ai-agents](https://github.com/your-org/new-planet-ai-agents) - **AI Agents** — [new-planet-ai-agents](https://git.bro-js.ru/Glevel/New-planet-ai-agent.git)

View File

@@ -1,9 +1,8 @@
from logging.config import fileConfig from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool from sqlalchemy import pool
from alembic import context from alembic import context
import asyncio import asyncio
from sqlalchemy.ext.asyncio import AsyncEngine from sqlalchemy.ext.asyncio import create_async_engine
# this is the Alembic Config object # this is the Alembic Config object
config = context.config config = context.config
@@ -22,12 +21,12 @@ target_metadata = Base.metadata
def get_url(): def get_url():
"""Получить URL БД""" """Получить URL БД"""
return settings.database_url.replace("+asyncpg", "") return settings.database_url
def run_migrations_offline() -> None: def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.""" """Run migrations in 'offline' mode."""
url = get_url() url = get_url().replace("+asyncpg", "")
context.configure( context.configure(
url=url, url=url,
target_metadata=target_metadata, target_metadata=target_metadata,
@@ -48,15 +47,9 @@ def do_run_migrations(connection):
async def run_migrations_online() -> None: async def run_migrations_online() -> None:
"""Run migrations in 'online' mode.""" """Run migrations in 'online' mode."""
configuration = config.get_section(config.config_ini_section) connectable = create_async_engine(
configuration["sqlalchemy.url"] = get_url() get_url(),
connectable = AsyncEngine( poolclass=pool.NullPool,
engine_from_config(
configuration,
prefix="sqlalchemy.",
poolclass=pool.NullPool,
future=True,
)
) )
async with connectable.connect() as connection: async with connectable.connect() as connection:

View File

@@ -0,0 +1,121 @@
"""Initial migration
Revision ID: 001_initial
Revises:
Create Date: 2024-01-01 00:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '001_initial'
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
# Create users table (enum will be created automatically by SQLAlchemy)
op.create_table(
'users',
sa.Column('id', sa.String(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('email', sa.String(length=255), nullable=False),
sa.Column('hashed_password', sa.String(length=255), nullable=False),
sa.Column('role', postgresql.ENUM('CHILD', 'PARENT', 'EDUCATOR', name='userrole'), nullable=False),
sa.Column('full_name', sa.String(length=255), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index('ix_users_email', 'users', ['email'], unique=True)
# Create schedules table
op.create_table(
'schedules',
sa.Column('id', sa.String(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('user_id', sa.String(), nullable=False),
sa.Column('title', sa.String(length=255), nullable=False),
sa.Column('date', sa.Date(), nullable=False),
sa.Column('description', sa.String(length=1000), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_index('ix_schedules_user_id', 'schedules', ['user_id'])
op.create_index('ix_schedules_date', 'schedules', ['date'])
# Create tasks table
op.create_table(
'tasks',
sa.Column('id', sa.String(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('schedule_id', sa.String(), nullable=False),
sa.Column('title', sa.String(length=255), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('image_url', sa.String(length=500), nullable=True),
sa.Column('duration_minutes', sa.Integer(), nullable=False, server_default='30'),
sa.Column('completed', sa.Boolean(), nullable=False, server_default='false'),
sa.Column('order', sa.Integer(), nullable=False, server_default='0'),
sa.Column('category', sa.String(length=100), nullable=True),
sa.ForeignKeyConstraint(['schedule_id'], ['schedules.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_index('ix_tasks_schedule_id', 'tasks', ['schedule_id'])
# Create rewards table
op.create_table(
'rewards',
sa.Column('id', sa.String(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('user_id', sa.String(), nullable=False),
sa.Column('title', sa.String(length=255), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('image_url', sa.String(length=500), nullable=True),
sa.Column('points_required', sa.Integer(), nullable=False, server_default='1'),
sa.Column('is_claimed', sa.Boolean(), nullable=False, server_default='false'),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_index('ix_rewards_user_id', 'rewards', ['user_id'])
# Create ai_conversations table
op.create_table(
'ai_conversations',
sa.Column('id', sa.String(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('user_id', sa.String(), nullable=False),
sa.Column('conversation_id', sa.String(), nullable=False),
sa.Column('message', sa.Text(), nullable=False),
sa.Column('response', sa.Text(), nullable=False),
sa.Column('tokens_used', sa.Integer(), nullable=True),
sa.Column('model', sa.String(length=100), nullable=True),
sa.Column('context', postgresql.JSON(astext_type=sa.Text()), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('conversation_id')
)
op.create_index('ix_ai_conversations_user_id', 'ai_conversations', ['user_id'])
op.create_index('ix_ai_conversations_conversation_id', 'ai_conversations', ['conversation_id'])
def downgrade() -> None:
op.drop_index('ix_ai_conversations_conversation_id', table_name='ai_conversations')
op.drop_index('ix_ai_conversations_user_id', table_name='ai_conversations')
op.drop_table('ai_conversations')
op.drop_index('ix_rewards_user_id', table_name='rewards')
op.drop_table('rewards')
op.drop_index('ix_tasks_schedule_id', table_name='tasks')
op.drop_table('tasks')
op.drop_index('ix_schedules_date', table_name='schedules')
op.drop_index('ix_schedules_user_id', table_name='schedules')
op.drop_table('schedules')
op.drop_index('ix_users_email', table_name='users')
op.drop_table('users')
op.execute("DROP TYPE userrole")

View File

@@ -7,7 +7,7 @@ from app.crud import user as crud_user
from app.services.auth_service import auth_service from app.services.auth_service import auth_service
from app.models.user import User from app.models.user import User
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/v1/auth/login") oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
async def get_current_user( async def get_current_user(

View File

@@ -3,9 +3,27 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import get_db from app.db.session import get_db
from app.api.deps import get_current_active_user from app.api.deps import get_current_active_user
from app.models.user import User from app.models.user import User
from app.schemas.ai import ChatRequest, ChatResponse, ScheduleGenerateRequest, ScheduleGenerateResponse from app.models.ai_conversation import AIConversation
from app.schemas.ai import (
ChatRequest,
ChatResponse,
ScheduleGenerateRequest,
ScheduleGenerateResponse,
ConversationHistory,
ConversationListItem,
ScheduleUpdateRequest,
ScheduleUpdateResponse,
RecommendationRequest,
RecommendationResponse,
)
from app.services.chat_service import chat_service from app.services.chat_service import chat_service
from app.services.schedule_generator import schedule_generator from app.services.schedule_generator import schedule_generator
from app.services.cache_service import cache_service
from app.services.gigachat_service import gigachat_service
from app.crud import schedule as crud_schedule, task as crud_task
from app.schemas.task import TaskCreate
from app.schemas.schedule import ScheduleUpdate
from app.core.config import settings
router = APIRouter() router = APIRouter()
@@ -30,7 +48,6 @@ async def chat_with_ai(
detail=f"Chat error: {str(e)}" detail=f"Chat error: {str(e)}"
) )
@router.post("/schedule/generate", response_model=ScheduleGenerateResponse) @router.post("/schedule/generate", response_model=ScheduleGenerateResponse)
async def generate_schedule_ai( async def generate_schedule_ai(
request: ScheduleGenerateRequest, request: ScheduleGenerateRequest,
@@ -58,3 +75,4 @@ async def generate_schedule_ai(
detail=f"Failed to generate schedule: {str(e)}" detail=f"Failed to generate schedule: {str(e)}"
) )

View File

@@ -10,15 +10,15 @@ class Settings(BaseSettings):
DEBUG: bool = False DEBUG: bool = False
# Security # Security
SECRET_KEY: str SECRET_KEY: str = "3db8542397edddbd6162ad823157e36f8d47232aa646725d4799266229ba7aa4"
ALGORITHM: str = "HS256" ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
REFRESH_TOKEN_EXPIRE_DAYS: int = 7 REFRESH_TOKEN_EXPIRE_DAYS: int = 7
# Database # Database
POSTGRES_USER: str POSTGRES_USER: str = "postgres"
POSTGRES_PASSWORD: str POSTGRES_PASSWORD: str = "postgres"
POSTGRES_DB: str POSTGRES_DB: str = "newplanet"
POSTGRES_HOST: str = "localhost" POSTGRES_HOST: str = "localhost"
POSTGRES_PORT: int = 5432 POSTGRES_PORT: int = 5432
DATABASE_URL: Optional[str] = None DATABASE_URL: Optional[str] = None
@@ -43,15 +43,15 @@ class Settings(BaseSettings):
# Storage (MinIO/S3) # Storage (MinIO/S3)
STORAGE_ENDPOINT: str = "localhost:9000" STORAGE_ENDPOINT: str = "localhost:9000"
STORAGE_ACCESS_KEY: str STORAGE_ACCESS_KEY: str = "minioadmin"
STORAGE_SECRET_KEY: str STORAGE_SECRET_KEY: str = "minioadmin"
STORAGE_BUCKET: str = "new-planet-images" STORAGE_BUCKET: str = "new-planet-images"
STORAGE_USE_SSL: bool = False STORAGE_USE_SSL: bool = False
STORAGE_REGION: str = "us-east-1" STORAGE_REGION: str = "us-east-1"
# GigaChat # GigaChat
GIGACHAT_CLIENT_ID: str GIGACHAT_CLIENT_ID: str = "019966f4-1c5c-7382-9006-b84419fbe5d1"
GIGACHAT_CLIENT_SECRET: str GIGACHAT_CLIENT_SECRET: str = "MDE5OTY2ZjQtMWM1Yy03MzgyLTkwMDYtYjg0NDE5ZmJlNWQxOjJjODBmOWE2LWU4YWMtNDE4YS1iOGVkLWE4NTE0YzVkNDAwNw=="
GIGACHAT_AUTH_URL: str = "https://ngw.devices.sberbank.ru:9443/api/v2/oauth" GIGACHAT_AUTH_URL: str = "https://ngw.devices.sberbank.ru:9443/api/v2/oauth"
GIGACHAT_BASE_URL: str = "https://gigachat.devices.sberbank.ru/api/v1" GIGACHAT_BASE_URL: str = "https://gigachat.devices.sberbank.ru/api/v1"
GIGACHAT_MODEL_CHAT: str = "GigaChat-2-Lite" GIGACHAT_MODEL_CHAT: str = "GigaChat-2-Lite"

View File

@@ -1,20 +1,54 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Optional, Dict, Any from typing import Optional, Dict, Any
from jose import JWTError, jwt from jose import JWTError, jwt
from passlib.context import CryptContext import bcrypt
from app.core.config import settings from config import settings
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def verify_password(plain_password: str, hashed_password: str) -> bool: def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Проверка пароля""" """Проверка пароля"""
return pwd_context.verify(plain_password, hashed_password) # Используем bcrypt напрямую для проверки
try:
return bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password.encode('utf-8'))
except (ValueError, TypeError, AttributeError):
return False
def _truncate_password_to_72_bytes(password: str) -> str:
"""Обрезает пароль до 72 байт, корректно обрабатывая UTF-8"""
password_bytes = password.encode('utf-8')
if len(password_bytes) <= 72:
return password
# Обрезаем до 72 байт
password_bytes = password_bytes[:72]
# Удаляем неполные UTF-8 последовательности в конце
# (байты, которые начинаются с 10xxxxxx, но не являются началом символа)
while password_bytes and (password_bytes[-1] & 0xC0) == 0x80:
password_bytes = password_bytes[:-1]
return password_bytes.decode('utf-8', errors='replace')
def get_password_hash(password: str) -> str: def get_password_hash(password: str) -> str:
"""Хеширование пароля""" """Хеширование пароля"""
return pwd_context.hash(password) # bcrypt имеет ограничение в 72 байта
# Обрезаем пароль до 72 байт перед хешированием
password_bytes = password.encode('utf-8')
if len(password_bytes) > 72:
# Обрезаем до 72 байт
password_bytes = password_bytes[:72]
# Удаляем неполные UTF-8 последовательности в конце
while password_bytes and (password_bytes[-1] & 0xC0) == 0x80:
password_bytes = password_bytes[:-1]
password = password_bytes.decode('utf-8', errors='replace')
password_bytes = password.encode('utf-8')
# Используем bcrypt напрямую, чтобы избежать проблем с инициализацией passlib
salt = bcrypt.gensalt()
hashed = bcrypt.hashpw(password_bytes, salt)
return hashed.decode('utf-8')
def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str: def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str:

View File

@@ -1,6 +1,5 @@
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import declarative_base
from sqlalchemy import Column, DateTime, func from sqlalchemy import Column, DateTime, func, String
from sqlalchemy.dialects.postgresql import UUID
import uuid import uuid
Base = declarative_base() Base = declarative_base()
@@ -11,7 +10,7 @@ class BaseModel(Base):
__abstract__ = True __abstract__ = True
id = Column( id = Column(
UUID(as_uuid=False), String,
primary_key=True, primary_key=True,
default=lambda: str(uuid.uuid4()), default=lambda: str(uuid.uuid4()),
nullable=False nullable=False

View File

@@ -1,5 +1,5 @@
from app.db.base import Base from base import Base
from app.db.session import engine from session import engine
from app.models import user, schedule, task, reward, ai_conversation from app.models import user, schedule, task, reward, ai_conversation

View File

@@ -3,8 +3,7 @@ from app.core.config import settings
engine = create_async_engine( engine = create_async_engine(
settings.database_url, settings.database_url,
echo=settings.DEBUG, echo=settings.DEBUG
future=True
) )
AsyncSessionLocal = async_sessionmaker( AsyncSessionLocal = async_sessionmaker(

View File

@@ -1,3 +1,18 @@
import sys
from pathlib import Path
# Добавляем корневую директорию проекта в PYTHONPATH при прямом запуске
# Это нужно, чтобы Python мог найти модуль 'app'
# Проверяем, запускается ли файл напрямую, проверяя имя скрипта
if sys.argv and len(sys.argv) > 0:
script_path = Path(sys.argv[0]).resolve()
current_file = Path(__file__).resolve()
# Если скрипт запускается напрямую (не через модуль)
if script_path == current_file or script_path.name == current_file.name:
project_root = current_file.parent.parent # new-planet-backend
if str(project_root) not in sys.path:
sys.path.insert(0, str(project_root))
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException from starlette.exceptions import HTTPException as StarletteHTTPException
@@ -73,7 +88,7 @@ if __name__ == "__main__":
import uvicorn import uvicorn
uvicorn.run( uvicorn.run(
"app.main:app", "app.main:app",
host="0.0.0.0", host="127.0.0.1",
port=8000, port=8000,
reload=settings.DEBUG reload=settings.DEBUG
) )

View File

@@ -1,4 +1,4 @@
from sqlalchemy import Column, String, Integer, ForeignKey, Text from sqlalchemy import Column, String, Integer, ForeignKey, Text, Boolean
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from app.db.base import BaseModel from app.db.base import BaseModel

View File

@@ -38,3 +38,41 @@ class ConversationHistory(BaseModel):
class Config: class Config:
from_attributes = True from_attributes = True
class ConversationListItem(BaseModel):
"""Элемент списка разговоров"""
conversation_id: str
last_message: Optional[str] = None
created_at: datetime
updated_at: datetime
message_count: int = 0
class Config:
from_attributes = True
class ScheduleUpdateRequest(BaseModel):
"""Запрос на обновление расписания через ИИ"""
user_request: str = Field(..., min_length=1, max_length=1000, description="Описание желаемых изменений")
class ScheduleUpdateResponse(BaseModel):
"""Ответ после обновления расписания"""
schedule_id: str
title: str
tasks: List[Dict[str, Any]]
tokens_used: Optional[int] = None
class RecommendationRequest(BaseModel):
"""Запрос на получение рекомендаций"""
preferences: List[str] = Field(default_factory=list, description="Предпочтения пользователя")
category: Optional[str] = Field(None, description="Категория заданий")
completed_tasks: Optional[List[str]] = Field(default_factory=list, description="Уже выполненные задания")
top_k: int = Field(5, ge=1, le=20, description="Количество рекомендаций")
class RecommendationResponse(BaseModel):
"""Ответ с рекомендациями"""
recommendations: List[Dict[str, Any]]
total: int

View File

@@ -11,7 +11,7 @@ class UserBase(BaseModel):
class UserCreate(UserBase): class UserCreate(UserBase):
password: str = Field(..., min_length=8) password: str = Field(..., min_length=8, max_length=72, description="Password must be between 8 and 72 characters")
class UserUpdate(BaseModel): class UserUpdate(BaseModel):

View File

@@ -1,10 +1,17 @@
import os
import aiohttp import aiohttp
import base64 import base64
import uuid import uuid
import time import time
from urllib.parse import urlencode
from typing import Optional, List, Dict, Any from typing import Optional, List, Dict, Any
from dotenv import load_dotenv
from app.core.config import settings from app.core.config import settings
load_dotenv()
class GigaChatService: class GigaChatService:
def __init__(self): def __init__(self):
@@ -18,8 +25,19 @@ class GigaChatService:
if time.time() < (self.token_expires_at - 60): if time.time() < (self.token_expires_at - 60):
return self.access_token return self.access_token
credentials = f"{settings.GIGACHAT_CLIENT_ID}:{settings.GIGACHAT_CLIENT_SECRET}" # Проверяем наличие credentials
encoded_credentials = base64.b64encode(credentials.encode()).decode() client_id = os.getenv("GIGACHAT_CLIENT_ID")
client_secret = os.getenv("GIGACHAT_CLIENT_SECRET")
if not client_id or not client_secret:
raise Exception(
"GigaChat credentials not configured. "
"Please set GIGACHAT_CLIENT_ID and GIGACHAT_CLIENT_SECRET in .env file"
)
# Формируем credentials и кодируем в Base64 с явным указанием UTF-8
credentials = f"{client_id}:{client_secret}".strip().encode('utf-8') # Обрезаем лишние символы
encoded_credentials = base64.b64encode(credentials).decode('utf-8')
headers = { headers = {
"Authorization": f"Basic {encoded_credentials}", "Authorization": f"Basic {encoded_credentials}",
@@ -28,25 +46,61 @@ class GigaChatService:
"RqUID": str(uuid.uuid4()) "RqUID": str(uuid.uuid4())
} }
data = {"scope": "GIGACHAT_API_PERS"} # Правильно кодируем данные формы (как в рабочем примере)
form_data = {
"grant_type": "client_credentials",
"scope": "GIGACHAT_API_PERS"
}
async with aiohttp.ClientSession() as session: # Отключаем проверку SSL (только для разработки!)
# Используем ssl=False для полного отключения проверки сертификата
connector = aiohttp.TCPConnector(ssl=False)
async with aiohttp.ClientSession(connector=connector) as session:
async with session.post( async with session.post(
settings.GIGACHAT_AUTH_URL, os.getenv("GIGACHAT_BASE_URL"),
headers=headers, headers=headers,
data=data data=form_data
) as response: ) as response:
if response.status != 200: if response.status != 200:
raise Exception(f"Failed to get token: {response.status}") # Получаем детали ошибки из ответа
try:
error_body = await response.text()
# Пытаемся распарсить как JSON, если не получается - возвращаем текст
try:
error_json = await response.json()
error_detail = error_json.get("error_description") or error_json.get("error") or str(error_json)
except:
error_detail = error_body
except:
error_detail = "No error details available"
raise Exception(
f"Failed to get token: HTTP {response.status}. "
f"Error details: {error_detail}. "
f"Check your GIGACHAT_CLIENT_ID and GIGACHAT_CLIENT_SECRET_2 in .env file"
)
result = await response.json() result = await response.json()
self.access_token = result.get("access_token") self.access_token = result.get("access_token")
expires_in = result.get("expires_at", 1800) if not self.access_token:
# expires_at может быть timestamp или количество секунд raise Exception(f"Token not found in response: {result}")
if expires_in > 1000000000: # Это timestamp
self.token_expires_at = expires_in # Обрабатываем время истечения токена (может быть expires_at или expires_in)
else: # Это количество секунд expires_at = result.get("expires_at")
expires_in = result.get("expires_in")
if expires_at:
# expires_at может быть timestamp или количество секунд
if expires_at > 1000000000: # Это timestamp
self.token_expires_at = expires_at
else: # Это количество секунд
self.token_expires_at = time.time() + expires_at
elif expires_in:
# expires_in - это всегда количество секунд до истечения
self.token_expires_at = time.time() + expires_in self.token_expires_at = time.time() + expires_in
else:
# По умолчанию 30 минут (1800 секунд)
self.token_expires_at = time.time() + 1800
return self.access_token return self.access_token
@@ -58,7 +112,7 @@ class GigaChatService:
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Отправить сообщение в GigaChat""" """Отправить сообщение в GigaChat"""
token = await self._get_token() token = await self._get_token()
model = model or settings.GIGACHAT_MODEL_CHAT model = model or settings.GIGACHAT_MODEL_CHAT or "GigaChat"
messages = context or [] messages = context or []
messages.append({"role": "user", "content": message}) messages.append({"role": "user", "content": message})
@@ -75,7 +129,10 @@ class GigaChatService:
"max_tokens": 2000 "max_tokens": 2000
} }
async with aiohttp.ClientSession() as session: # Отключаем проверку SSL (только для разработки!)
# Используем ssl=False для полного отключения проверки сертификата
connector = aiohttp.TCPConnector(ssl=False)
async with aiohttp.ClientSession(connector=connector) as session:
async with session.post( async with session.post(
f"{settings.GIGACHAT_BASE_URL}/chat/completions", f"{settings.GIGACHAT_BASE_URL}/chat/completions",
headers=headers, headers=headers,

View File

@@ -17,6 +17,8 @@ services:
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
networks:
- new-planet-network
redis: redis:
image: redis:7-alpine image: redis:7-alpine
@@ -30,6 +32,8 @@ services:
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
networks:
- new-planet-network
minio: minio:
image: minio/minio:latest image: minio/minio:latest
@@ -48,9 +52,15 @@ services:
interval: 30s interval: 30s
timeout: 20s timeout: 20s
retries: 3 retries: 3
networks:
- new-planet-network
volumes: volumes:
postgres_data: postgres_data:
redis_data: redis_data:
minio_data: minio_data:
networks:
new-planet-network:
driver: bridge

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@ uvicorn[standard]
sqlalchemy>=2.0 sqlalchemy>=2.0
alembic alembic
asyncpg asyncpg
psycopg2-binary
redis redis
pydantic pydantic
pydantic-settings pydantic-settings
@@ -12,3 +13,4 @@ boto3
aiohttp aiohttp
websockets websockets
python-multipart python-multipart
email-validator