Compare commits

..

4 Commits

Author SHA1 Message Date
95f87c253c Merge pull request 'Обновлены настройки для интеграции с AI-agent сервисом. Изменены переменные окружения в .env, добавлен путь к .env в конфигурации, обновлен GigaChatSe…' (#3) from backendFixes into master
Reviewed-on: #3
2025-12-24 02:00:29 +03:00
32bfec2074 Обновлены настройки для интеграции с AI-agent сервисом. Изменены переменные окружения в .env, добавлен путь к .env в конфигурации, обновлен GigaChatService для работы через AI-agent. Также исправлены запросы в ScheduleGenerator для корректной загрузки задач. Обновлен docker-compose для подключения к AI-agent сервису. 2025-12-24 01:59:39 +03:00
a649fb1192 Изменения:
-добавлены нетворки в докер композ
-исправлен рутинг (баг пайчарма)
-запросы к ии агентам не проходят из-за ссл сертификата (пробовали отключить, но пока не выходит, нужно доделать)
2025-12-19 00:57:13 +03:00
24f4ce118f Обновить new-planet-backend/requirements.txt 2025-12-18 17:18:04 +03:00
73 changed files with 691 additions and 102 deletions

5
.idea/misc.xml generated
View File

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

View File

@@ -2,8 +2,11 @@
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<content url="file://$MODULE_DIR$">
<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" />
</component>
</module>

36
logs/app.log Normal file
View File

@@ -0,0 +1,36 @@
2025-12-23 21:37:30 - root - INFO - Starting up...
2025-12-23 21:37:30 - root - INFO - Starting up...
2025-12-23 21:39:37 - root - INFO - Shutting down...
2025-12-23 21:39:37 - root - INFO - Shutting down...
2025-12-23 21:39:42 - root - INFO - Starting up...
2025-12-23 21:39:42 - root - INFO - Starting up...
2025-12-23 22:07:40 - root - INFO - Shutting down...
2025-12-23 22:07:40 - root - INFO - Shutting down...
2025-12-23 22:07:45 - root - INFO - Starting up...
2025-12-23 22:07:45 - root - INFO - Starting up...
2025-12-23 22:12:42 - root - INFO - Shutting down...
2025-12-23 22:12:42 - root - INFO - Shutting down...
2025-12-23 22:12:47 - root - INFO - Starting up...
2025-12-23 22:12:47 - root - INFO - Starting up...
2025-12-23 22:23:22 - root - INFO - Shutting down...
2025-12-23 22:23:22 - root - INFO - Shutting down...
2025-12-23 22:23:27 - root - INFO - Starting up...
2025-12-23 22:23:27 - root - INFO - Starting up...
2025-12-23 22:52:47 - root - INFO - Shutting down...
2025-12-23 22:52:47 - root - INFO - Shutting down...
2025-12-23 22:52:54 - root - INFO - Starting up...
2025-12-23 22:52:54 - root - INFO - Starting up...
2025-12-23 23:12:01 - root - INFO - Shutting down...
2025-12-23 23:12:01 - root - INFO - Shutting down...
2025-12-23 23:12:07 - root - INFO - Starting up...
2025-12-23 23:12:07 - root - INFO - Starting up...
2025-12-23 23:15:05 - root - INFO - Shutting down...
2025-12-23 23:15:05 - root - INFO - Shutting down...
2025-12-23 23:15:10 - root - INFO - Starting up...
2025-12-23 23:15:10 - root - INFO - Starting up...
2025-12-24 00:01:39 - root - INFO - Shutting down...
2025-12-24 00:01:39 - root - INFO - Shutting down...
2025-12-24 00:01:45 - root - INFO - Starting up...
2025-12-24 00:01:45 - root - INFO - Starting up...
2025-12-24 01:38:58 - root - INFO - Shutting down...
2025-12-24 01:38:58 - root - INFO - Shutting down...

View File

@@ -1,21 +1,27 @@
# Database

GIGACHAT_CLIENT_ID=019966f0-5781-76e6-a84f-ec7de158188a
GIGACHAT_CLIENT_SECRET=MDE5OTY2ZjAtNTc4MS03NmU2LWE4NGYtZWM3ZGUxNTgxODhhOjI3MDMxZjIxLWY3NWYtNGI4NS05MzM1LTI4ZDYyOWM3MmM0MA==
GIGACHAT_AUTH_URL=https://ngw.devices.sberbank.ru:9443/api/v2/oauth
GIGACHAT_BASE_URL=https://gigachat.devices.sberbank.ru/api/v1
GIGACHAT_MODEL_CHAT=GigaChat-2-Lite
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
DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/newplanet
# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_DB=0
REDIS_URL=redis://localhost:6379
# Security
SECRET_KEY=jwt-secret-key
# Storage (MinIO/S3)
# Storage
STORAGE_ENDPOINT=localhost:9000
STORAGE_ACCESS_KEY=minioadmin
STORAGE_SECRET_KEY=minioadmin
@@ -23,6 +29,5 @@ STORAGE_BUCKET=new-planet-images
STORAGE_USE_SSL=false
STORAGE_REGION=us-east-1
# GigaChat API
GIGACHAT_CLIENT_ID=gigachat-client-id
GIGACHAT_CLIENT_SECRET="gigachat-token-here"
# Agents
AI_AGENT_BASE_URL=http://localhost:8001

View File

@@ -23,12 +23,13 @@ Backend API для мобильного приложения **Новая Пла
### Установка
1. Клонируйте репозиторий
2. Установите зависимости:
2. Установить окружение ("python -m venv venv")
3. Установите зависимости:
```bash
pip install -r requirements.txt
```
3. Настройте `.env`:
4. Настройте `.env`:
```bash
cp .env.example .env
#В целом вам нужно поменять GIGACHAT API секцию, JWT Secret key сгенерить, просто в поисковике генератор на 256 байт сделаете JWT
@@ -36,7 +37,7 @@ cp .env.example .env
# Отредактируйте .env с вашими настройками
```
4. Запустите инфраструктуру (Docker):
5. Запустите инфраструктуру (Docker):
```bash
docker-compose -f docker/docker-compose.yml up -d
# или используйте вариант ниже,но лучше вариант выше для избежания непредвиденного
@@ -44,12 +45,12 @@ docker-compose -f docker/docker-compose.yml up -d
docker-compose up
```
5. Примените миграции:
6. Примените миграции:
```bash
alembic upgrade head
```
6. Запустите сервер:
7. Запустите сервер:
```bash
uvicorn app.main:app --reload
# если не запустилось, проверяйте есть ли .venv(установлено ли окружение для питона), также попробуйте в венве uvicorn app.main:app --reload --host 0.0.0.0 --port 8000

View File

@@ -7,7 +7,7 @@ from app.crud import user as crud_user
from app.services.auth_service import auth_service
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(

View File

@@ -3,9 +3,27 @@ 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.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.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()
@@ -30,7 +48,6 @@ async def chat_with_ai(
detail=f"Chat error: {str(e)}"
)
@router.post("/schedule/generate", response_model=ScheduleGenerateResponse)
async def generate_schedule_ai(
request: ScheduleGenerateRequest,
@@ -58,3 +75,4 @@ async def generate_schedule_ai(
detail=f"Failed to generate schedule: {str(e)}"
)

View File

@@ -1,5 +1,13 @@
from pydantic_settings import BaseSettings
from typing import Optional
from dotenv import load_dotenv
from pathlib import Path
# Загружаем .env файл перед созданием Settings
# Ищем .env в корне проекта (на уровень выше от app/)
env_path = Path(__file__).parent.parent.parent / ".env"
# override=True гарантирует, что переменные из .env перезапишут существующие
load_dotenv(dotenv_path=env_path, override=True)
class Settings(BaseSettings):
@@ -10,15 +18,15 @@ class Settings(BaseSettings):
DEBUG: bool = False
# Security
SECRET_KEY: str
SECRET_KEY: str = "3db8542397edddbd6162ad823157e36f8d47232aa646725d4799266229ba7aa4"
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
REFRESH_TOKEN_EXPIRE_DAYS: int = 7
# Database
POSTGRES_USER: str
POSTGRES_PASSWORD: str
POSTGRES_DB: str
POSTGRES_USER: str = "postgres"
POSTGRES_PASSWORD: str = "postgres"
POSTGRES_DB: str = "newplanet"
POSTGRES_HOST: str = "localhost"
POSTGRES_PORT: int = 5432
DATABASE_URL: Optional[str] = None
@@ -43,15 +51,22 @@ class Settings(BaseSettings):
# Storage (MinIO/S3)
STORAGE_ENDPOINT: str = "localhost:9000"
STORAGE_ACCESS_KEY: str
STORAGE_SECRET_KEY: str
STORAGE_ACCESS_KEY: str = "minioadmin"
STORAGE_SECRET_KEY: str = "minioadmin"
STORAGE_BUCKET: str = "new-planet-images"
STORAGE_USE_SSL: bool = False
STORAGE_REGION: str = "us-east-1"
# GigaChat
GIGACHAT_CLIENT_ID: str
GIGACHAT_CLIENT_SECRET: str
# AI Agent Service (внешний сервис для работы с GigaChat)
# URL можно переопределить через переменную окружения AI_AGENT_BASE_URL
# Для Docker сети используйте: http://ai-agent:8000 (или имя сервиса из docker-compose)
# Для локальной разработки используйте: http://localhost:8000
AI_AGENT_BASE_URL: str = "http://ai-agent:8000"
AI_AGENT_TIMEOUT: int = 120 # Таймаут в секундах
# GigaChat (оставлено для обратной совместимости, но используется через AI-agent сервис)
GIGACHAT_CLIENT_ID: str = "019966f4-1c5c-7382-9006-b84419fbe5d1"
GIGACHAT_CLIENT_SECRET: str = "MDE5OTY2ZjQtMWM1Yy03MzgyLTkwMDYtYjg0NDE5ZmJlNWQxOjJjODBmOWE2LWU4YWMtNDE4YS1iOGVkLWE4NTE0YzVkNDAwNw=="
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_MODEL_CHAT: str = "GigaChat-2-Lite"
@@ -65,7 +80,9 @@ class Settings(BaseSettings):
RATE_LIMIT_PER_MINUTE: int = 60
class Config:
env_file = ".env"
# Путь к .env файлу относительно корня проекта
env_file = str(env_path) if env_path.exists() else ".env"
env_file_encoding = "utf-8"
case_sensitive = True

View File

@@ -2,7 +2,7 @@ from datetime import datetime, timedelta
from typing import Optional, Dict, Any
from jose import JWTError, jwt
import bcrypt
from app.core.config import settings
from config import settings
def verify_password(plain_password: str, hashed_password: str) -> bool:

View File

@@ -1,4 +1,4 @@
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import declarative_base
from sqlalchemy import Column, DateTime, func, String
import uuid

View File

@@ -1,5 +1,5 @@
from app.db.base import Base
from app.db.session import engine
from base import Base
from session import engine
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(
settings.database_url,
echo=settings.DEBUG,
future=True
echo=settings.DEBUG
)
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.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
@@ -73,7 +88,7 @@ if __name__ == "__main__":
import uvicorn
uvicorn.run(
"app.main:app",
host="0.0.0.0",
host="127.0.0.1",
port=8000,
reload=settings.DEBUG
)

View File

@@ -38,3 +38,41 @@ class ConversationHistory(BaseModel):
class Config:
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

@@ -4,6 +4,7 @@ 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
from app.services.ai_agent_client import ai_agent_client, AIAgentClient
__all__ = [
"auth_service",
@@ -18,5 +19,7 @@ __all__ = [
"ChatService",
"schedule_generator",
"ScheduleGenerator",
"ai_agent_client",
"AIAgentClient",
]

View File

@@ -0,0 +1,117 @@
import aiohttp
from typing import Optional, List, Dict, Any
from app.core.config import settings
class AIAgentClient:
"""
Клиент для взаимодействия с внешним AI-agent сервисом.
Сервис должен быть доступен в Docker сети и предоставлять следующие endpoints:
- POST /api/v1/chat - для чата с ИИ
- POST /api/v1/schedule/generate - для генерации расписаний
Примечание: Структура API endpoints может отличаться в зависимости от реализации
внешнего сервиса. При необходимости измените пути в методах этого класса.
"""
def __init__(self, base_url: Optional[str] = None):
self.base_url = base_url or settings.AI_AGENT_BASE_URL
if not self.base_url.endswith('/'):
self.base_url = self.base_url.rstrip('/')
async def chat(
self,
message: str,
conversation_id: Optional[str] = None,
context: Optional[List[Dict[str, Any]]] = None
) -> Dict[str, Any]:
"""
Отправить сообщение в чат через AI-agent сервис.
Ожидаемый формат ответа от сервиса:
{
"response": "текст ответа",
"conversation_id": "id беседы",
"tokens_used": 100,
"model": "модель"
}
или формат GigaChat API (с полем choices).
"""
url = f"{self.base_url}/api/v1/chat"
payload = {
"message": message,
}
if conversation_id:
payload["conversation_id"] = conversation_id
if context:
payload["context"] = context
async with aiohttp.ClientSession() as session:
async with session.post(url, json=payload, timeout=aiohttp.ClientTimeout(total=120)) as response:
if response.status != 200:
error_text = await response.text()
raise Exception(
f"AI-agent service error: HTTP {response.status} - {error_text}"
)
result = await response.json()
return result
async def generate_schedule(
self,
child_age: int,
preferences: List[str],
date: str,
description: Optional[str] = None
) -> Dict[str, Any]:
"""Сгенерировать расписание через AI-agent сервис"""
url = f"{self.base_url}/api/v1/schedule/generate"
payload = {
"child_age": child_age,
"preferences": preferences,
"date": date
}
if description:
payload["description"] = description
async with aiohttp.ClientSession() as session:
async with session.post(url, json=payload, timeout=aiohttp.ClientTimeout(total=120)) as response:
if response.status != 200:
error_text = await response.text()
raise Exception(
f"AI-agent service error: HTTP {response.status} - {error_text}"
)
result = await response.json()
return result
async def generate_text(
self,
prompt: str,
model: Optional[str] = None
) -> str:
"""Генерация текста по промпту через AI-agent сервис"""
# Для совместимости с текущим интерфейсом используем chat endpoint
result = await self.chat(message=prompt)
# Извлекаем текст ответа
# Предполагаем, что ответ имеет структуру ChatResponse
response_text = result.get("response", "")
if not response_text:
# Если структура другая, пытаемся извлечь из choices (как в GigaChat формате)
choices = result.get("choices", [])
if choices:
response_text = choices[0].get("message", {}).get("content", "")
return response_text
# Создаем экземпляр клиента
ai_agent_client = AIAgentClient()

View File

@@ -1,26 +1,50 @@
import os
import aiohttp
import ssl
import base64
import uuid
import time
from urllib.parse import urlencode
from typing import Optional, List, Dict, Any
from app.core.config import settings
from app.core.config import settings
from app.services.ai_agent_client import ai_agent_client
class GigaChatService:
"""
Сервис для работы с GigaChat через внешний AI-agent сервис.
Все запросы к GigaChat теперь проходят через внешний сервис.
"""
def __init__(self):
self.access_token: Optional[str] = None
self.token_expires_at: Optional[float] = None
async def _get_token(self) -> str:
"""Получить OAuth токен"""
"""
Получить OAuth токен.
ВНИМАНИЕ: Этот метод больше не используется, так как все запросы
к GigaChat теперь проходят через внешний AI-agent сервис.
Метод оставлен для возможной обратной совместимости.
"""
# Проверяем, не истек ли токен (оставляем запас 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()
# Проверяем наличие credentials
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 = {
"Authorization": f"Basic {encoded_credentials}",
@@ -29,31 +53,61 @@ class GigaChatService:
"RqUID": str(uuid.uuid4())
}
data = {"scope": "GIGACHAT_API_PERS"}
# Правильно кодируем данные формы (как в рабочем примере)
form_data = {
"grant_type": "client_credentials",
"scope": "GIGACHAT_API_PERS"
}
# Создаем SSL контекст без проверки сертификата (только для разработки!)
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
connector = aiohttp.TCPConnector(ssl=ssl_context)
# Отключаем проверку SSL (только для разработки!)
# Используем ssl=False для полного отключения проверки сертификата
connector = aiohttp.TCPConnector(ssl=False)
async with aiohttp.ClientSession(connector=connector) as session:
async with session.post(
settings.GIGACHAT_AUTH_URL,
os.getenv("GIGACHAT_BASE_URL"),
headers=headers,
data=data
data=form_data
) as response:
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()
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: # Это количество секунд
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 или количество секунд
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
else:
# По умолчанию 30 минут (1800 секунд)
self.token_expires_at = time.time() + 1800
return self.access_token
@@ -63,52 +117,59 @@ class GigaChatService:
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
"""
Отправить сообщение в GigaChat через внешний AI-agent сервис.
Сохраняет обратную совместимость с форматом ответа GigaChat API.
"""
try:
# Используем внешний AI-agent сервис
result = await ai_agent_client.chat(
message=message,
conversation_id=None, # Если нужен conversation_id, его нужно передавать отдельно
context=context
)
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
}
# Создаем SSL контекст без проверки сертификата (только для разработки!)
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
connector = aiohttp.TCPConnector(ssl=ssl_context)
async with aiohttp.ClientSession(connector=connector) 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()
# Преобразуем ответ AI-agent сервиса в формат, совместимый с GigaChat API
# Предполагаем, что ai_agent_client возвращает структуру ChatResponse или аналогичную
if "response" in result:
# Если ответ в формате ChatResponse, преобразуем в формат GigaChat
return {
"model": result.get("model", model or settings.GIGACHAT_MODEL_CHAT or "GigaChat"),
"choices": [{
"message": {
"role": "assistant",
"content": result["response"]
},
"finish_reason": "stop"
}],
"usage": {
"total_tokens": result.get("tokens_used", 0),
"prompt_tokens": 0,
"completion_tokens": result.get("tokens_used", 0)
}
}
else:
# Если ответ уже в формате GigaChat, возвращаем как есть
return result
except Exception as e:
# Если внешний сервис недоступен, пробрасываем ошибку
raise Exception(f"AI-agent service error: {str(e)}")
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", "")
"""
Генерация текста по промпту через внешний AI-agent сервис.
"""
try:
# Используем метод generate_text из ai_agent_client
response_text = await ai_agent_client.generate_text(prompt=prompt, model=model)
return response_text
except Exception as e:
# Если произошла ошибка, пробрасываем её
raise Exception(f"AI-agent service error: {str(e)}")
gigachat_service = GigaChatService()

View File

@@ -1,11 +1,14 @@
import json
from typing import List, Dict, Any, Optional
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from sqlalchemy.orm import selectinload
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 app.models.schedule import Schedule
from datetime import date
@@ -104,11 +107,17 @@ class ScheduleGenerator:
await crud_task.create(db, task_create.model_dump())
await db.refresh(db_schedule)
# Загружаем расписание с задачами через selectinload для async корректной работы
result = await db.execute(
select(Schedule)
.where(Schedule.id == db_schedule.id)
.options(selectinload(Schedule.tasks))
)
db_schedule_with_tasks = result.scalar_one()
return {
"schedule_id": db_schedule.id,
"title": db_schedule.title,
"schedule_id": db_schedule_with_tasks.id,
"title": db_schedule_with_tasks.title,
"tasks": [
{
"title": task.title,
@@ -117,7 +126,7 @@ class ScheduleGenerator:
"category": task.category,
"order": task.order
}
for task in db_schedule.tasks
for task in db_schedule_with_tasks.tasks
]
}
except json.JSONDecodeError as e:

View File

@@ -17,6 +17,8 @@ services:
interval: 10s
timeout: 5s
retries: 5
networks:
- new-planet-network
redis:
image: redis:7-alpine
@@ -30,6 +32,8 @@ services:
interval: 10s
timeout: 5s
retries: 5
networks:
- new-planet-network
minio:
image: minio/minio:latest
@@ -48,9 +52,19 @@ services:
interval: 30s
timeout: 20s
retries: 3
networks:
- new-planet-network
volumes:
postgres_data:
redis_data:
minio_data:
networks:
new-planet-network:
driver: bridge
# ВАЖНО: Внешний AI-agent сервис (https://git.bro-js.ru/Glevel/New-planet-ai-agent.git)
# должен быть запущен в этой же сети для доступа к GigaChat.
# Убедитесь, что сервис ai-agent доступен по имени 'ai-agent' в сети new-planet-network.
external: false

View File

@@ -1341,3 +1341,254 @@ WHERE users.id = $1::UUID]
2025-12-18 13:49:33 - root - INFO - Shutting down...
2025-12-18 13:49:38 - root - INFO - Starting up...
2025-12-18 13:51:42 - root - INFO - Shutting down...
2025-12-18 18:44:49 - root - INFO - Starting up...
2025-12-18 19:39:26 - root - INFO - Starting up...
2025-12-18 19:40:16 - app.middleware.error_handler - ERROR - Unhandled exception: Error 111 connecting to localhost:6379. Connection refused.
+ Exception Group Traceback (most recent call last):
| File "/usr/local/lib/python3.11/site-packages/starlette/_utils.py", line 79, in collapse_excgroups
| yield
| File "/usr/local/lib/python3.11/site-packages/starlette/middleware/base.py", line 192, in __call__
| async with anyio.create_task_group() as task_group:
| File "/usr/local/lib/python3.11/site-packages/anyio/_backends/_asyncio.py", line 783, in __aexit__
| raise BaseExceptionGroup(
| ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
+-+---------------- 1 ----------------
| Traceback (most recent call last):
| File "/usr/local/lib/python3.11/site-packages/starlette/middleware/errors.py", line 164, in __call__
| await self.app(scope, receive, _send)
| File "/usr/local/lib/python3.11/site-packages/starlette/middleware/base.py", line 191, in __call__
| with recv_stream, send_stream, collapse_excgroups():
| File "/usr/local/lib/python3.11/contextlib.py", line 158, in __exit__
| self.gen.throw(typ, value, traceback)
| File "/usr/local/lib/python3.11/site-packages/starlette/_utils.py", line 85, in collapse_excgroups
| raise exc
| File "/usr/local/lib/python3.11/site-packages/starlette/middleware/base.py", line 193, in __call__
| response = await self.dispatch_func(request, call_next)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| File "/app/app/middleware/rate_limiter.py", line 20, in dispatch
| current_requests = await cache_service.get(cache_key)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| File "/app/app/services/cache_service.py", line 28, in get
| return await self.redis_client.get(key)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| File "/usr/local/lib/python3.11/site-packages/redis/asyncio/client.py", line 720, in execute_command
| conn = self.connection or await pool.get_connection()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
| File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 1198, in get_connection
| await self.ensure_connection(connection)
| File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 1231, in ensure_connection
| await connection.connect()
| File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 298, in connect
| await self.connect_check_health(check_health=True)
| File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 317, in connect_check_health
| raise ConnectionError(self._error_message(e))
| redis.exceptions.ConnectionError: Error 111 connecting to localhost:6379. Connection refused.
+------------------------------------
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/usr/local/lib/python3.11/site-packages/starlette/middleware/errors.py", line 164, in __call__
await self.app(scope, receive, _send)
File "/usr/local/lib/python3.11/site-packages/starlette/middleware/base.py", line 191, in __call__
with recv_stream, send_stream, collapse_excgroups():
File "/usr/local/lib/python3.11/contextlib.py", line 158, in __exit__
self.gen.throw(typ, value, traceback)
File "/usr/local/lib/python3.11/site-packages/starlette/_utils.py", line 85, in collapse_excgroups
raise exc
File "/usr/local/lib/python3.11/site-packages/starlette/middleware/base.py", line 193, in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/app/app/middleware/rate_limiter.py", line 20, in dispatch
current_requests = await cache_service.get(cache_key)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/app/app/services/cache_service.py", line 28, in get
return await self.redis_client.get(key)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/redis/asyncio/client.py", line 720, in execute_command
conn = self.connection or await pool.get_connection()
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 1198, in get_connection
await self.ensure_connection(connection)
File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 1231, in ensure_connection
await connection.connect()
File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 298, in connect
await self.connect_check_health(check_health=True)
File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 317, in connect_check_health
raise ConnectionError(self._error_message(e))
redis.exceptions.ConnectionError: Error 111 connecting to localhost:6379. Connection refused.
2025-12-18 19:40:16 - app.middleware.error_handler - ERROR - Unhandled exception: Error 111 connecting to localhost:6379. Connection refused.
+ Exception Group Traceback (most recent call last):
| File "/usr/local/lib/python3.11/site-packages/starlette/_utils.py", line 79, in collapse_excgroups
| yield
| File "/usr/local/lib/python3.11/site-packages/starlette/middleware/base.py", line 192, in __call__
| async with anyio.create_task_group() as task_group:
| File "/usr/local/lib/python3.11/site-packages/anyio/_backends/_asyncio.py", line 783, in __aexit__
| raise BaseExceptionGroup(
| ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
+-+---------------- 1 ----------------
| Traceback (most recent call last):
| File "/usr/local/lib/python3.11/site-packages/starlette/middleware/errors.py", line 164, in __call__
| await self.app(scope, receive, _send)
| File "/usr/local/lib/python3.11/site-packages/starlette/middleware/base.py", line 191, in __call__
| with recv_stream, send_stream, collapse_excgroups():
| File "/usr/local/lib/python3.11/contextlib.py", line 158, in __exit__
| self.gen.throw(typ, value, traceback)
| File "/usr/local/lib/python3.11/site-packages/starlette/_utils.py", line 85, in collapse_excgroups
| raise exc
| File "/usr/local/lib/python3.11/site-packages/starlette/middleware/base.py", line 193, in __call__
| response = await self.dispatch_func(request, call_next)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| File "/app/app/middleware/rate_limiter.py", line 20, in dispatch
| current_requests = await cache_service.get(cache_key)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| File "/app/app/services/cache_service.py", line 28, in get
| return await self.redis_client.get(key)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| File "/usr/local/lib/python3.11/site-packages/redis/asyncio/client.py", line 720, in execute_command
| conn = self.connection or await pool.get_connection()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
| File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 1198, in get_connection
| await self.ensure_connection(connection)
| File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 1231, in ensure_connection
| await connection.connect()
| File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 298, in connect
| await self.connect_check_health(check_health=True)
| File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 317, in connect_check_health
| raise ConnectionError(self._error_message(e))
| redis.exceptions.ConnectionError: Error 111 connecting to localhost:6379. Connection refused.
+------------------------------------
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/usr/local/lib/python3.11/site-packages/starlette/middleware/errors.py", line 164, in __call__
await self.app(scope, receive, _send)
File "/usr/local/lib/python3.11/site-packages/starlette/middleware/base.py", line 191, in __call__
with recv_stream, send_stream, collapse_excgroups():
File "/usr/local/lib/python3.11/contextlib.py", line 158, in __exit__
self.gen.throw(typ, value, traceback)
File "/usr/local/lib/python3.11/site-packages/starlette/_utils.py", line 85, in collapse_excgroups
raise exc
File "/usr/local/lib/python3.11/site-packages/starlette/middleware/base.py", line 193, in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/app/app/middleware/rate_limiter.py", line 20, in dispatch
current_requests = await cache_service.get(cache_key)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/app/app/services/cache_service.py", line 28, in get
return await self.redis_client.get(key)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/redis/asyncio/client.py", line 720, in execute_command
conn = self.connection or await pool.get_connection()
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 1198, in get_connection
await self.ensure_connection(connection)
File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 1231, in ensure_connection
await connection.connect()
File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 298, in connect
await self.connect_check_health(check_health=True)
File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 317, in connect_check_health
raise ConnectionError(self._error_message(e))
redis.exceptions.ConnectionError: Error 111 connecting to localhost:6379. Connection refused.
2025-12-18 19:40:16 - app.middleware.error_handler - ERROR - Unhandled exception: Error 111 connecting to localhost:6379. Connection refused.
+ Exception Group Traceback (most recent call last):
| File "/usr/local/lib/python3.11/site-packages/starlette/_utils.py", line 79, in collapse_excgroups
| yield
| File "/usr/local/lib/python3.11/site-packages/starlette/middleware/base.py", line 192, in __call__
| async with anyio.create_task_group() as task_group:
| File "/usr/local/lib/python3.11/site-packages/anyio/_backends/_asyncio.py", line 783, in __aexit__
| raise BaseExceptionGroup(
| ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
+-+---------------- 1 ----------------
| Traceback (most recent call last):
| File "/usr/local/lib/python3.11/site-packages/starlette/middleware/errors.py", line 164, in __call__
| await self.app(scope, receive, _send)
| File "/usr/local/lib/python3.11/site-packages/starlette/middleware/base.py", line 191, in __call__
| with recv_stream, send_stream, collapse_excgroups():
| File "/usr/local/lib/python3.11/contextlib.py", line 158, in __exit__
| self.gen.throw(typ, value, traceback)
| File "/usr/local/lib/python3.11/site-packages/starlette/_utils.py", line 85, in collapse_excgroups
| raise exc
| File "/usr/local/lib/python3.11/site-packages/starlette/middleware/base.py", line 193, in __call__
| response = await self.dispatch_func(request, call_next)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| File "/app/app/middleware/rate_limiter.py", line 20, in dispatch
| current_requests = await cache_service.get(cache_key)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| File "/app/app/services/cache_service.py", line 28, in get
| return await self.redis_client.get(key)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| File "/usr/local/lib/python3.11/site-packages/redis/asyncio/client.py", line 720, in execute_command
| conn = self.connection or await pool.get_connection()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
| File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 1198, in get_connection
| await self.ensure_connection(connection)
| File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 1231, in ensure_connection
| await connection.connect()
| File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 298, in connect
| await self.connect_check_health(check_health=True)
| File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 317, in connect_check_health
| raise ConnectionError(self._error_message(e))
| redis.exceptions.ConnectionError: Error 111 connecting to localhost:6379. Connection refused.
+------------------------------------
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/usr/local/lib/python3.11/site-packages/starlette/middleware/errors.py", line 164, in __call__
await self.app(scope, receive, _send)
File "/usr/local/lib/python3.11/site-packages/starlette/middleware/base.py", line 191, in __call__
with recv_stream, send_stream, collapse_excgroups():
File "/usr/local/lib/python3.11/contextlib.py", line 158, in __exit__
self.gen.throw(typ, value, traceback)
File "/usr/local/lib/python3.11/site-packages/starlette/_utils.py", line 85, in collapse_excgroups
raise exc
File "/usr/local/lib/python3.11/site-packages/starlette/middleware/base.py", line 193, in __call__
response = await self.dispatch_func(request, call_next)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/app/app/middleware/rate_limiter.py", line 20, in dispatch
current_requests = await cache_service.get(cache_key)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/app/app/services/cache_service.py", line 28, in get
return await self.redis_client.get(key)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/redis/asyncio/client.py", line 720, in execute_command
conn = self.connection or await pool.get_connection()
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 1198, in get_connection
await self.ensure_connection(connection)
File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 1231, in ensure_connection
await connection.connect()
File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 298, in connect
await self.connect_check_health(check_health=True)
File "/usr/local/lib/python3.11/site-packages/redis/asyncio/connection.py", line 317, in connect_check_health
raise ConnectionError(self._error_message(e))
redis.exceptions.ConnectionError: Error 111 connecting to localhost:6379. Connection refused.
2025-12-18 22:55:17 - root - INFO - Starting up...
2025-12-18 22:55:17 - root - INFO - Starting up...
2025-12-18 22:56:56 - root - INFO - Starting up...
2025-12-18 22:56:56 - root - INFO - Shutting down...
2025-12-18 22:59:02 - root - INFO - Starting up...
2025-12-18 22:59:02 - root - INFO - Shutting down...
2025-12-18 22:59:53 - root - INFO - Starting up...
2025-12-18 22:59:53 - root - INFO - Starting up...
2025-12-18 22:59:53 - root - INFO - Shutting down...
2025-12-18 22:59:53 - root - INFO - Shutting down...
2025-12-18 20:00:09 - root - INFO - Shutting down...
2025-12-18 23:00:15 - root - INFO - Starting up...
2025-12-18 23:00:15 - root - INFO - Starting up...
2025-12-18 23:00:15 - root - INFO - Shutting down...
2025-12-18 23:00:15 - root - INFO - Shutting down...
2025-12-23 19:00:39 - root - INFO - Starting up...
2025-12-23 19:00:39 - root - INFO - Starting up...
2025-12-23 19:52:34 - root - INFO - Shutting down...
2025-12-23 19:52:34 - root - INFO - Shutting down...
2025-12-23 19:52:58 - root - INFO - Starting up...
2025-12-23 19:52:58 - root - INFO - Starting up...
2025-12-23 19:54:27 - root - INFO - Shutting down...
2025-12-23 19:54:27 - root - INFO - Shutting down...
2025-12-23 19:54:34 - root - INFO - Starting up...
2025-12-23 19:54:34 - root - INFO - Starting up...
2025-12-23 20:00:23 - root - INFO - Shutting down...
2025-12-23 20:00:23 - root - INFO - Shutting down...

View File

@@ -13,3 +13,4 @@ boto3
aiohttp
websockets
python-multipart
email-validator