Compare commits
6 Commits
b666cdcb95
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| a649fb1192 | |||
| 24f4ce118f | |||
| c256012a69 | |||
| 4bbe086cec | |||
| 4cb6043931 | |||
| e04933b9c1 |
5
.idea/misc.xml
generated
5
.idea/misc.xml
generated
@@ -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>
|
||||||
7
.idea/new-planet-backend.iml
generated
7
.idea/new-planet-backend.iml
generated
@@ -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>
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
|
||||||
engine_from_config(
|
|
||||||
configuration,
|
|
||||||
prefix="sqlalchemy.",
|
|
||||||
poolclass=pool.NullPool,
|
poolclass=pool.NullPool,
|
||||||
future=True,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async with connectable.connect() as connection:
|
async with connectable.connect() as connection:
|
||||||
|
|||||||
121
new-planet-backend/alembic/versions/001_initial_migration.py
Normal file
121
new-planet-backend/alembic/versions/001_initial_migration.py
Normal 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")
|
||||||
|
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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):
|
||||||
|
|||||||
@@ -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:
|
||||||
|
raise Exception(f"Token not found in response: {result}")
|
||||||
|
|
||||||
|
# Обрабатываем время истечения токена (может быть expires_at или expires_in)
|
||||||
|
expires_at = result.get("expires_at")
|
||||||
|
expires_in = result.get("expires_in")
|
||||||
|
|
||||||
|
if expires_at:
|
||||||
# expires_at может быть timestamp или количество секунд
|
# expires_at может быть timestamp или количество секунд
|
||||||
if expires_in > 1000000000: # Это timestamp
|
if expires_at > 1000000000: # Это timestamp
|
||||||
self.token_expires_at = expires_in
|
self.token_expires_at = expires_at
|
||||||
else: # Это количество секунд
|
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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
1582
new-planet-backend/logs/app.log
Normal file
1582
new-planet-backend/logs/app.log
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
Reference in New Issue
Block a user