diff --git a/.idea/misc.xml b/.idea/misc.xml index 3c99962..ced1df3 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,7 @@ - - + + + \ No newline at end of file diff --git a/.idea/new-planet-backend.iml b/.idea/new-planet-backend.iml index d6ebd48..14d3ec3 100644 --- a/.idea/new-planet-backend.iml +++ b/.idea/new-planet-backend.iml @@ -2,8 +2,11 @@ - - + + + + + \ No newline at end of file diff --git a/new-planet-backend/.env b/new-planet-backend/.env index e817043..1ed5ccf 100644 --- a/new-planet-backend/.env +++ b/new-planet-backend/.env @@ -1,21 +1,27 @@ -# Database + +GIGACHAT_CLIENT_ID=019966f4-1c5c-7382-9006-b84419fbe5d1 +GIGACHAT_CLIENT_SECRET=MDE5OTY2ZjQtMWM1Yy03MzgyLTkwMDYtYjg0NDE5ZmJlNWQxOjJjODBmOWE2LWU4YWMtNDE4YS1iOGVkLWE4NTE0YzVkNDAwNw== +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,3 @@ 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" diff --git a/new-planet-backend/README.md b/new-planet-backend/README.md index 7313f07..1e62c21 100644 --- a/new-planet-backend/README.md +++ b/new-planet-backend/README.md @@ -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 diff --git a/new-planet-backend/app/api/deps.py b/new-planet-backend/app/api/deps.py index bd73c8d..e924143 100644 --- a/new-planet-backend/app/api/deps.py +++ b/new-planet-backend/app/api/deps.py @@ -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( diff --git a/new-planet-backend/app/api/v1/ai.py b/new-planet-backend/app/api/v1/ai.py index 4025fe6..72e4d0b 100644 --- a/new-planet-backend/app/api/v1/ai.py +++ b/new-planet-backend/app/api/v1/ai.py @@ -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)}" ) + diff --git a/new-planet-backend/app/core/config.py b/new-planet-backend/app/core/config.py index 4a71bd6..7bf8087 100644 --- a/new-planet-backend/app/core/config.py +++ b/new-planet-backend/app/core/config.py @@ -10,15 +10,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 +43,15 @@ 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 + 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" diff --git a/new-planet-backend/app/core/security.py b/new-planet-backend/app/core/security.py index a4eb2c3..d24198e 100644 --- a/new-planet-backend/app/core/security.py +++ b/new-planet-backend/app/core/security.py @@ -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: diff --git a/new-planet-backend/app/db/base.py b/new-planet-backend/app/db/base.py index 0435fb7..4a3d0e7 100644 --- a/new-planet-backend/app/db/base.py +++ b/new-planet-backend/app/db/base.py @@ -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 diff --git a/new-planet-backend/app/db/init_db.py b/new-planet-backend/app/db/init_db.py index b973e2c..e802165 100644 --- a/new-planet-backend/app/db/init_db.py +++ b/new-planet-backend/app/db/init_db.py @@ -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 diff --git a/new-planet-backend/app/db/session.py b/new-planet-backend/app/db/session.py index 04c4b72..e4632e5 100644 --- a/new-planet-backend/app/db/session.py +++ b/new-planet-backend/app/db/session.py @@ -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( diff --git a/new-planet-backend/app/main.py b/new-planet-backend/app/main.py index 80e524f..8404054 100644 --- a/new-planet-backend/app/main.py +++ b/new-planet-backend/app/main.py @@ -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 ) diff --git a/new-planet-backend/app/schemas/ai.py b/new-planet-backend/app/schemas/ai.py index 3f17ed9..4c5e9e3 100644 --- a/new-planet-backend/app/schemas/ai.py +++ b/new-planet-backend/app/schemas/ai.py @@ -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 \ No newline at end of file diff --git a/new-planet-backend/app/services/gigachat_service.py b/new-planet-backend/app/services/gigachat_service.py index cd3ed52..e7e73b8 100644 --- a/new-planet-backend/app/services/gigachat_service.py +++ b/new-planet-backend/app/services/gigachat_service.py @@ -1,11 +1,17 @@ +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 dotenv import load_dotenv + from app.core.config import settings +load_dotenv() class GigaChatService: def __init__(self): @@ -19,8 +25,19 @@ class GigaChatService: 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 +46,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 @@ -65,7 +112,7 @@ class GigaChatService: ) -> Dict[str, Any]: """Отправить сообщение в GigaChat""" 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.append({"role": "user", "content": message}) @@ -82,12 +129,9 @@ class GigaChatService: "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) + # Отключаем проверку SSL (только для разработки!) + # Используем ssl=False для полного отключения проверки сертификата + connector = aiohttp.TCPConnector(ssl=False) async with aiohttp.ClientSession(connector=connector) as session: async with session.post( f"{settings.GIGACHAT_BASE_URL}/chat/completions", diff --git a/new-planet-backend/docker/docker-compose.yml b/new-planet-backend/docker/docker-compose.yml index ff323b9..c75e92b 100644 --- a/new-planet-backend/docker/docker-compose.yml +++ b/new-planet-backend/docker/docker-compose.yml @@ -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,15 @@ services: interval: 30s timeout: 20s retries: 3 + networks: + - new-planet-network volumes: postgres_data: redis_data: minio_data: +networks: + new-planet-network: + driver: bridge + diff --git a/new-planet-backend/logs/app.log b/new-planet-backend/logs/app.log index d1f111d..fb61425 100644 --- a/new-planet-backend/logs/app.log +++ b/new-planet-backend/logs/app.log @@ -1341,3 +1341,242 @@ 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...