init
This commit is contained in:
22
new-planet-backend/app/services/__init__.py
Normal file
22
new-planet-backend/app/services/__init__.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from app.services.auth_service import auth_service, AuthService
|
||||
from app.services.cache_service import cache_service, CacheService
|
||||
from app.services.storage_service import storage_service, StorageService
|
||||
from app.services.gigachat_service import gigachat_service, GigaChatService
|
||||
from app.services.chat_service import chat_service, ChatService
|
||||
from app.services.schedule_generator import schedule_generator, ScheduleGenerator
|
||||
|
||||
__all__ = [
|
||||
"auth_service",
|
||||
"AuthService",
|
||||
"cache_service",
|
||||
"CacheService",
|
||||
"storage_service",
|
||||
"StorageService",
|
||||
"gigachat_service",
|
||||
"GigaChatService",
|
||||
"chat_service",
|
||||
"ChatService",
|
||||
"schedule_generator",
|
||||
"ScheduleGenerator",
|
||||
]
|
||||
|
||||
73
new-planet-backend/app/services/auth_service.py
Normal file
73
new-planet-backend/app/services/auth_service.py
Normal file
@@ -0,0 +1,73 @@
|
||||
from typing import Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.crud import user as crud_user
|
||||
from app.core.security import verify_password, create_access_token, create_refresh_token, decode_token
|
||||
from app.schemas.user import UserCreate
|
||||
from app.schemas.token import Token
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
class AuthService:
|
||||
async def authenticate(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
email: str,
|
||||
password: str
|
||||
) -> Optional[Token]:
|
||||
"""Аутентификация пользователя"""
|
||||
db_user = await crud_user.get_by_email(db, email)
|
||||
if not db_user:
|
||||
return None
|
||||
|
||||
if not verify_password(password, db_user.hashed_password):
|
||||
return None
|
||||
|
||||
access_token = create_access_token(
|
||||
data={"sub": db_user.id, "email": db_user.email}
|
||||
)
|
||||
refresh_token = create_refresh_token(
|
||||
data={"sub": db_user.id, "email": db_user.email}
|
||||
)
|
||||
|
||||
return Token(
|
||||
access_token=access_token,
|
||||
refresh_token=refresh_token,
|
||||
token_type="bearer"
|
||||
)
|
||||
|
||||
async def register(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
user_in: UserCreate
|
||||
):
|
||||
"""Регистрация нового пользователя"""
|
||||
# Проверка существования пользователя
|
||||
existing_user = await crud_user.get_by_email(db, user_in.email)
|
||||
if existing_user:
|
||||
raise ValueError("User with this email already exists")
|
||||
|
||||
from app.core.security import get_password_hash
|
||||
hashed_password = get_password_hash(user_in.password)
|
||||
|
||||
db_user = await crud_user.create(db, user_in, hashed_password)
|
||||
return db_user
|
||||
|
||||
def verify_token(self, token: str) -> Optional[dict]:
|
||||
"""Проверка токена"""
|
||||
payload = decode_token(token)
|
||||
if payload and payload.get("type") == "access":
|
||||
return payload
|
||||
return None
|
||||
|
||||
def refresh_access_token(self, refresh_token: str) -> Optional[str]:
|
||||
"""Обновление access token"""
|
||||
payload = decode_token(refresh_token)
|
||||
if payload and payload.get("type") == "refresh":
|
||||
return create_access_token(
|
||||
data={"sub": payload.get("sub"), "email": payload.get("email")}
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
auth_service = AuthService()
|
||||
|
||||
70
new-planet-backend/app/services/cache_service.py
Normal file
70
new-planet-backend/app/services/cache_service.py
Normal file
@@ -0,0 +1,70 @@
|
||||
import json
|
||||
from typing import Optional, List, Dict, Any
|
||||
import redis.asyncio as redis
|
||||
from app.core.config import settings
|
||||
|
||||
|
||||
class CacheService:
|
||||
def __init__(self):
|
||||
self.redis_client: Optional[redis.Redis] = None
|
||||
|
||||
async def connect(self):
|
||||
"""Подключение к Redis"""
|
||||
self.redis_client = await redis.from_url(
|
||||
settings.redis_url,
|
||||
encoding="utf-8",
|
||||
decode_responses=True
|
||||
)
|
||||
|
||||
async def disconnect(self):
|
||||
"""Отключение от Redis"""
|
||||
if self.redis_client:
|
||||
await self.redis_client.close()
|
||||
|
||||
async def get(self, key: str) -> Optional[str]:
|
||||
"""Получить значение по ключу"""
|
||||
if not self.redis_client:
|
||||
await self.connect()
|
||||
return await self.redis_client.get(key)
|
||||
|
||||
async def set(self, key: str, value: str, expire: int = 3600):
|
||||
"""Установить значение с TTL"""
|
||||
if not self.redis_client:
|
||||
await self.connect()
|
||||
await self.redis_client.setex(key, expire, value)
|
||||
|
||||
async def delete(self, key: str):
|
||||
"""Удалить ключ"""
|
||||
if not self.redis_client:
|
||||
await self.connect()
|
||||
await self.redis_client.delete(key)
|
||||
|
||||
async def get_conversation_context(
|
||||
self,
|
||||
conversation_id: str
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Получить контекст разговора"""
|
||||
key = f"conversation:{conversation_id}"
|
||||
data = await self.get(key)
|
||||
if data:
|
||||
return json.loads(data)
|
||||
return []
|
||||
|
||||
async def save_conversation_context(
|
||||
self,
|
||||
conversation_id: str,
|
||||
context: List[Dict[str, Any]],
|
||||
expire: int = 86400 * 7 # 7 дней
|
||||
):
|
||||
"""Сохранить контекст разговора"""
|
||||
key = f"conversation:{conversation_id}"
|
||||
await self.set(key, json.dumps(context), expire=expire)
|
||||
|
||||
async def cache_token(self, token: str, expire: int = 1800):
|
||||
"""Кэшировать токен (для rate limiting)"""
|
||||
key = f"token:{token}"
|
||||
await self.set(key, "1", expire=expire)
|
||||
|
||||
|
||||
cache_service = CacheService()
|
||||
|
||||
107
new-planet-backend/app/services/chat_service.py
Normal file
107
new-planet-backend/app/services/chat_service.py
Normal file
@@ -0,0 +1,107 @@
|
||||
import uuid
|
||||
import json
|
||||
from typing import Optional, List, Dict, Any
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.services.gigachat_service import gigachat_service
|
||||
from app.services.cache_service import cache_service
|
||||
from app.models.ai_conversation import AIConversation
|
||||
from app.schemas.ai import ChatRequest, ChatResponse
|
||||
from app.core.config import settings
|
||||
|
||||
|
||||
# Персона "Планета Земля"
|
||||
EARTH_PERSONA = """Ты планета Земля - анимированный персонаж и друг детей с расстройством аутистического спектра (РАС).
|
||||
|
||||
Твоя личность:
|
||||
- Добрая, терпеливая, понимающая
|
||||
- Говоришь простым языком
|
||||
- Используешь эмодзи 🌍✨
|
||||
- Поощряешь любые достижения
|
||||
- Даешь четкие инструкции
|
||||
|
||||
Особенности общения:
|
||||
- Короткие предложения
|
||||
- Избегай сложных метафор
|
||||
- Подтверждай понимание
|
||||
- Задавай уточняющие вопросы
|
||||
- Будь позитивным и поддерживающим"""
|
||||
|
||||
|
||||
class ChatService:
|
||||
async def chat(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
user_id: str,
|
||||
request: ChatRequest
|
||||
) -> ChatResponse:
|
||||
"""Обработка чата с ИИ-агентом"""
|
||||
# Получить или создать conversation_id
|
||||
conversation_id = request.conversation_id or str(uuid.uuid4())
|
||||
|
||||
# Загрузить контекст из кэша
|
||||
context = await cache_service.get_conversation_context(conversation_id)
|
||||
|
||||
# Добавить персону в начало, если контекст пустой
|
||||
if not context:
|
||||
context.append({
|
||||
"role": "system",
|
||||
"content": EARTH_PERSONA
|
||||
})
|
||||
|
||||
# Добавить сообщение пользователя
|
||||
context.append({
|
||||
"role": "user",
|
||||
"content": request.message
|
||||
})
|
||||
|
||||
# Отправить запрос в GigaChat
|
||||
try:
|
||||
result = await gigachat_service.chat(
|
||||
message=request.message,
|
||||
context=context[:-1], # Без последнего сообщения (оно добавится автоматически)
|
||||
model=settings.GIGACHAT_MODEL_CHAT
|
||||
)
|
||||
|
||||
# Извлечь ответ
|
||||
choices = result.get("choices", [])
|
||||
if not choices:
|
||||
raise Exception("No response from GigaChat")
|
||||
|
||||
response_text = choices[0].get("message", {}).get("content", "")
|
||||
tokens_used = result.get("usage", {}).get("total_tokens")
|
||||
model_used = result.get("model")
|
||||
|
||||
# Добавить ответ в контекст
|
||||
context.append({
|
||||
"role": "assistant",
|
||||
"content": response_text
|
||||
})
|
||||
|
||||
# Сохранить контекст в кэш
|
||||
await cache_service.save_conversation_context(conversation_id, context)
|
||||
|
||||
# Сохранить в БД
|
||||
conversation = AIConversation(
|
||||
user_id=user_id,
|
||||
conversation_id=conversation_id,
|
||||
message=request.message,
|
||||
response=response_text,
|
||||
tokens_used=tokens_used,
|
||||
model=model_used,
|
||||
context=context
|
||||
)
|
||||
db.add(conversation)
|
||||
await db.commit()
|
||||
|
||||
return ChatResponse(
|
||||
response=response_text,
|
||||
conversation_id=conversation_id,
|
||||
tokens_used=tokens_used,
|
||||
model=model_used
|
||||
)
|
||||
except Exception as e:
|
||||
raise Exception(f"Chat service error: {str(e)}")
|
||||
|
||||
|
||||
chat_service = ChatService()
|
||||
|
||||
102
new-planet-backend/app/services/gigachat_service.py
Normal file
102
new-planet-backend/app/services/gigachat_service.py
Normal file
@@ -0,0 +1,102 @@
|
||||
import aiohttp
|
||||
import base64
|
||||
import uuid
|
||||
import time
|
||||
from typing import Optional, List, Dict, Any
|
||||
from app.core.config import settings
|
||||
|
||||
|
||||
class GigaChatService:
|
||||
def __init__(self):
|
||||
self.access_token: Optional[str] = None
|
||||
self.token_expires_at: Optional[float] = None
|
||||
|
||||
async def _get_token(self) -> str:
|
||||
"""Получить OAuth токен"""
|
||||
# Проверяем, не истек ли токен (оставляем запас 60 секунд)
|
||||
if self.access_token and self.token_expires_at:
|
||||
if time.time() < (self.token_expires_at - 60):
|
||||
return self.access_token
|
||||
|
||||
credentials = f"{settings.GIGACHAT_CLIENT_ID}:{settings.GIGACHAT_CLIENT_SECRET}"
|
||||
encoded_credentials = base64.b64encode(credentials.encode()).decode()
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Basic {encoded_credentials}",
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Accept": "application/json",
|
||||
"RqUID": str(uuid.uuid4())
|
||||
}
|
||||
|
||||
data = {"scope": "GIGACHAT_API_PERS"}
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(
|
||||
settings.GIGACHAT_AUTH_URL,
|
||||
headers=headers,
|
||||
data=data
|
||||
) as response:
|
||||
if response.status != 200:
|
||||
raise Exception(f"Failed to get token: {response.status}")
|
||||
|
||||
result = await response.json()
|
||||
self.access_token = result.get("access_token")
|
||||
expires_in = result.get("expires_at", 1800)
|
||||
# expires_at может быть timestamp или количество секунд
|
||||
if expires_in > 1000000000: # Это timestamp
|
||||
self.token_expires_at = expires_in
|
||||
else: # Это количество секунд
|
||||
self.token_expires_at = time.time() + expires_in
|
||||
|
||||
return self.access_token
|
||||
|
||||
async def chat(
|
||||
self,
|
||||
message: str,
|
||||
context: Optional[List[Dict[str, Any]]] = None,
|
||||
model: str = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Отправить сообщение в GigaChat"""
|
||||
token = await self._get_token()
|
||||
model = model or settings.GIGACHAT_MODEL_CHAT
|
||||
|
||||
messages = context or []
|
||||
messages.append({"role": "user", "content": message})
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
"temperature": 0.7,
|
||||
"max_tokens": 2000
|
||||
}
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(
|
||||
f"{settings.GIGACHAT_BASE_URL}/chat/completions",
|
||||
headers=headers,
|
||||
json=payload
|
||||
) as response:
|
||||
if response.status != 200:
|
||||
error_text = await response.text()
|
||||
raise Exception(f"GigaChat API error: {response.status} - {error_text}")
|
||||
|
||||
result = await response.json()
|
||||
return result
|
||||
|
||||
async def generate_text(
|
||||
self,
|
||||
prompt: str,
|
||||
model: str = None
|
||||
) -> str:
|
||||
"""Генерация текста по промпту"""
|
||||
result = await self.chat(prompt, model=model)
|
||||
return result.get("choices", [{}])[0].get("message", {}).get("content", "")
|
||||
|
||||
|
||||
gigachat_service = GigaChatService()
|
||||
|
||||
130
new-planet-backend/app/services/schedule_generator.py
Normal file
130
new-planet-backend/app/services/schedule_generator.py
Normal file
@@ -0,0 +1,130 @@
|
||||
import json
|
||||
from typing import List, Dict, Any, Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.services.gigachat_service import gigachat_service
|
||||
from app.core.config import settings
|
||||
from app.crud import schedule as crud_schedule, task as crud_task
|
||||
from app.schemas.schedule import ScheduleCreate
|
||||
from app.schemas.task import TaskCreate
|
||||
from datetime import date
|
||||
|
||||
|
||||
SCHEDULE_GENERATION_PROMPT = """Ты планета Земля, друг детей с расстройством аутистического спектра (РАС).
|
||||
|
||||
Создай расписание на {date} для ребенка {age} лет.
|
||||
Предпочтения ребенка: {preferences}
|
||||
|
||||
Важные правила:
|
||||
1. Задания должны быть простыми и понятными
|
||||
2. Каждое задание имеет четкие временные рамки
|
||||
3. Используй визуальные описания
|
||||
4. Избегай резких переходов между активностями
|
||||
5. Включи время на отдых
|
||||
6. Учитывай особенности РАС
|
||||
|
||||
Верни ТОЛЬКО валидный JSON формат без дополнительного текста:
|
||||
{{
|
||||
"title": "Название расписания",
|
||||
"description": "Краткое описание",
|
||||
"tasks": [
|
||||
{{
|
||||
"title": "Название задания",
|
||||
"description": "Подробное описание",
|
||||
"duration_minutes": 30,
|
||||
"category": "утренняя_рутина",
|
||||
"order": 0
|
||||
}}
|
||||
]
|
||||
}}"""
|
||||
|
||||
|
||||
class ScheduleGenerator:
|
||||
async def generate(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
user_id: str,
|
||||
child_age: int,
|
||||
preferences: List[str],
|
||||
schedule_date: str,
|
||||
description: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Генерация расписания через GigaChat"""
|
||||
# Формируем промпт
|
||||
prompt = SCHEDULE_GENERATION_PROMPT.format(
|
||||
date=schedule_date,
|
||||
age=child_age,
|
||||
preferences=", ".join(preferences) if preferences else "нет особых предпочтений"
|
||||
)
|
||||
|
||||
if description:
|
||||
prompt += f"\n\nДополнительная информация: {description}"
|
||||
|
||||
# Генерируем через GigaChat
|
||||
try:
|
||||
response_text = await gigachat_service.generate_text(
|
||||
prompt=prompt,
|
||||
model=settings.GIGACHAT_MODEL_SCHEDULE
|
||||
)
|
||||
|
||||
# Парсим JSON ответ
|
||||
# Убираем markdown код блоки если есть
|
||||
if "```json" in response_text:
|
||||
response_text = response_text.split("```json")[1].split("```")[0].strip()
|
||||
elif "```" in response_text:
|
||||
response_text = response_text.split("```")[1].split("```")[0].strip()
|
||||
|
||||
schedule_data = json.loads(response_text)
|
||||
|
||||
# Создаем расписание в БД
|
||||
schedule_create = ScheduleCreate(
|
||||
title=schedule_data.get("title", f"Расписание на {schedule_date}"),
|
||||
date=date.fromisoformat(schedule_date),
|
||||
description=schedule_data.get("description") or description
|
||||
)
|
||||
|
||||
db_schedule = await crud_schedule.create(
|
||||
db,
|
||||
{
|
||||
**schedule_create.model_dump(),
|
||||
"user_id": user_id
|
||||
}
|
||||
)
|
||||
|
||||
# Создаем задачи
|
||||
tasks_data = schedule_data.get("tasks", [])
|
||||
for task_data in tasks_data:
|
||||
task_create = TaskCreate(
|
||||
schedule_id=db_schedule.id,
|
||||
title=task_data.get("title"),
|
||||
description=task_data.get("description"),
|
||||
duration_minutes=task_data.get("duration_minutes", 30),
|
||||
category=task_data.get("category"),
|
||||
order=task_data.get("order", 0)
|
||||
)
|
||||
|
||||
await crud_task.create(db, task_create.model_dump())
|
||||
|
||||
await db.refresh(db_schedule)
|
||||
|
||||
return {
|
||||
"schedule_id": db_schedule.id,
|
||||
"title": db_schedule.title,
|
||||
"tasks": [
|
||||
{
|
||||
"title": task.title,
|
||||
"description": task.description,
|
||||
"duration_minutes": task.duration_minutes,
|
||||
"category": task.category,
|
||||
"order": task.order
|
||||
}
|
||||
for task in db_schedule.tasks
|
||||
]
|
||||
}
|
||||
except json.JSONDecodeError as e:
|
||||
raise Exception(f"Failed to parse GigaChat response as JSON: {str(e)}")
|
||||
except Exception as e:
|
||||
raise Exception(f"Schedule generation error: {str(e)}")
|
||||
|
||||
|
||||
schedule_generator = ScheduleGenerator()
|
||||
|
||||
80
new-planet-backend/app/services/storage_service.py
Normal file
80
new-planet-backend/app/services/storage_service.py
Normal file
@@ -0,0 +1,80 @@
|
||||
import uuid
|
||||
from typing import Optional, BinaryIO
|
||||
from botocore.exceptions import ClientError
|
||||
import boto3
|
||||
from app.core.config import settings
|
||||
|
||||
|
||||
class StorageService:
|
||||
def __init__(self):
|
||||
self.client = None
|
||||
self._initialize_client()
|
||||
|
||||
def _initialize_client(self):
|
||||
"""Инициализация S3/MinIO клиента"""
|
||||
self.client = boto3.client(
|
||||
"s3",
|
||||
endpoint_url=f"{'https' if settings.STORAGE_USE_SSL else 'http'}://{settings.STORAGE_ENDPOINT}",
|
||||
aws_access_key_id=settings.STORAGE_ACCESS_KEY,
|
||||
aws_secret_access_key=settings.STORAGE_SECRET_KEY,
|
||||
region_name=settings.STORAGE_REGION,
|
||||
use_ssl=settings.STORAGE_USE_SSL,
|
||||
verify=False # Для MinIO в dev
|
||||
)
|
||||
|
||||
async def upload_file(
|
||||
self,
|
||||
file_obj: BinaryIO,
|
||||
filename: str,
|
||||
content_type: str = "image/jpeg"
|
||||
) -> str:
|
||||
"""Загрузить файл в хранилище"""
|
||||
file_key = f"{uuid.uuid4()}_{filename}"
|
||||
|
||||
try:
|
||||
self.client.upload_fileobj(
|
||||
file_obj,
|
||||
settings.STORAGE_BUCKET,
|
||||
file_key,
|
||||
ExtraArgs={"ContentType": content_type}
|
||||
)
|
||||
|
||||
# Формируем URL
|
||||
url = f"{'https' if settings.STORAGE_USE_SSL else 'http'}://{settings.STORAGE_ENDPOINT}/{settings.STORAGE_BUCKET}/{file_key}"
|
||||
return url
|
||||
except ClientError as e:
|
||||
raise Exception(f"Failed to upload file: {str(e)}")
|
||||
|
||||
async def delete_file(self, file_key: str) -> bool:
|
||||
"""Удалить файл из хранилища"""
|
||||
try:
|
||||
# Извлекаем ключ из URL если передан полный URL
|
||||
if "/" in file_key:
|
||||
file_key = file_key.split("/")[-1]
|
||||
|
||||
self.client.delete_object(
|
||||
Bucket=settings.STORAGE_BUCKET,
|
||||
Key=file_key
|
||||
)
|
||||
return True
|
||||
except ClientError:
|
||||
return False
|
||||
|
||||
async def get_file_url(self, file_key: str) -> Optional[str]:
|
||||
"""Получить URL файла"""
|
||||
try:
|
||||
if "/" in file_key:
|
||||
file_key = file_key.split("/")[-1]
|
||||
|
||||
url = self.client.generate_presigned_url(
|
||||
"get_object",
|
||||
Params={"Bucket": settings.STORAGE_BUCKET, "Key": file_key},
|
||||
ExpiresIn=3600
|
||||
)
|
||||
return url
|
||||
except ClientError:
|
||||
return None
|
||||
|
||||
|
||||
storage_service = StorageService()
|
||||
|
||||
Reference in New Issue
Block a user