правки для деплоя через Coolify
This commit is contained in:
+4
-4
@@ -119,16 +119,16 @@ async def main():
|
||||
if "duplicate column" not in str(e).lower():
|
||||
logger.warning(f"Migration review.bables: {e}")
|
||||
|
||||
# Создаём настройки для хороших и плохих отзывов при первом запуске.
|
||||
# Существующие настройки (включая автоответы) не трогаем, чтобы они
|
||||
# переживали перезапуск и редеплой.
|
||||
for is_good in [True, False]:
|
||||
settings, _ = await Settings.get_or_create(
|
||||
await Settings.get_or_create(
|
||||
is_good=is_good,
|
||||
defaults={
|
||||
"analysis_enabled": not is_good,
|
||||
},
|
||||
)
|
||||
settings.auto_response_enabled = False
|
||||
settings.auto_response_empty_enabled = False
|
||||
await settings.save()
|
||||
|
||||
workers = asyncio.ensure_future(
|
||||
asyncio.gather(
|
||||
|
||||
+17
-7
@@ -20,11 +20,21 @@ PRODUCTS_WB_SHEET_LINK = env.str("PRODUCTS_WB_SHEET_LINK", default="")
|
||||
PRODUCTS_OZON_SHEET_LINK = env.str("PRODUCTS_OZON_SHEET_LINK", default="")
|
||||
PRODUCTS_CACHE_TTL = env.int("PRODUCTS_CACHE_TTL", default=60 * 5)
|
||||
|
||||
# Ключ сервисного аккаунта Google: либо содержимое JSON (или его base64)
|
||||
# в переменной окружения, либо путь к файлу.
|
||||
GOOGLE_SERVICE_ACCOUNT_JSON = env.str("GOOGLE_SERVICE_ACCOUNT_JSON", default="")
|
||||
GOOGLE_SERVICE_ACCOUNT_FILE = env.str(
|
||||
"GOOGLE_SERVICE_ACCOUNT_FILE", default="google-auth/key.json"
|
||||
)
|
||||
|
||||
OZON_API_KEY = env.str("OZON_API_KEY", default="")
|
||||
OZON_CLIENT_ID = env.str("OZON_CLIENT_ID", default="")
|
||||
WB_API_KEY = env.str("WB_API_KEY", default="")
|
||||
GPTUNNEL_API_KEY = env.str("GPTUNNEL_API_KEY", default="")
|
||||
|
||||
# Название бренда, от имени которого бот анализирует отзывы и отвечает на них
|
||||
BRAND_NAME = env.str("BRAND_NAME", default="Magic Stories")
|
||||
|
||||
OZON_BASE_URL = "https://api-seller.ozon.ru"
|
||||
WB_BASE_URL = "https://feedbacks-api.wildberries.ru"
|
||||
GPT_API_URL = "https://gptunnel.ru/v1/chat/completions"
|
||||
@@ -32,8 +42,8 @@ GPT_API_URL = "https://gptunnel.ru/v1/chat/completions"
|
||||
OZON_CHECK_INTERVAL = env.int("OZON_CHECK_INTERVAL", default=60)
|
||||
WB_CHECK_INTERVAL = env.int("WB_CHECK_INTERVAL", default=300)
|
||||
|
||||
ANALYSIS_PROMPT = """
|
||||
Вы - представитель бренда Magic Stories и анализируете отзывы клиентов.
|
||||
ANALYSIS_PROMPT = f"""
|
||||
Вы - представитель бренда {BRAND_NAME} и анализируете отзывы клиентов.
|
||||
Классифицируйте отзыв по следующим критериям:
|
||||
|
||||
1. Тональность отзыва (позитивная, нейтральная, негативная).
|
||||
@@ -49,26 +59,26 @@ ANALYSIS_PROMPT = """
|
||||
|
||||
Ответ должен быть СТРОГО в формате JSON:
|
||||
```json
|
||||
{
|
||||
{{
|
||||
"tone": "positive|negative|neutral",
|
||||
"reason": "причина тональности из списка или пустая строка для нейтральных отзывов",
|
||||
"is_critical": false // true если отзыв критичный
|
||||
}
|
||||
}}
|
||||
```
|
||||
|
||||
ОБЯЗАТЕЛЬНО используйте ТОЛЬКО указанные категории из списка. Никаких других категорий или вариантов.
|
||||
""".strip()
|
||||
|
||||
ANSWER_PROMPT = """
|
||||
ANSWER_PROMPT = f"""
|
||||
Вы — профессиональный работник службы поддержки с многолетним опытом и чутким пониманием психологии человека,
|
||||
официальный представитель бренда Magic Stories. Ваша задача — вежливо и профессионально отвечать на отзывы клиентов.
|
||||
официальный представитель бренда {BRAND_NAME}. Ваша задача — вежливо и профессионально отвечать на отзывы клиентов.
|
||||
Ответ должен быть коротким, информативным и соответствовать тональности отзыва:
|
||||
- Негативный отзыв: выразите сожаление за произошедшее, предложите решение или помощь, объясните, как можно исправить ситуацию.
|
||||
- Позитивный отзыв: искренне поблагодарите за выбор бренда и положительный опыт, подтвердив, что вам важна обратная связь.
|
||||
- Нейтральный отзыв: подтвердите получение обратной связи и предложите дополнительную помощь, если это нужно.
|
||||
Не забывайте, что лесть перед клиентом люди хорошо считывают, поэтому избегайте чрезмерных похвал и канцеляризмов.
|
||||
Опишите решение проблемы или благодарность за положительный отзыв простым и понятным языком, который будет одинаково понятен любому клиенту. Также не отправляй почту
|
||||
В конец отзыва всегда добавляйте "С уважением, команда Magic Stories." с новой строки.
|
||||
В конец отзыва всегда добавляйте "С уважением, команда {BRAND_NAME}." с новой строки.
|
||||
""".strip()
|
||||
|
||||
# Шаблоны автоответа на отзывы без текста (только оценка)
|
||||
|
||||
+44
-3
@@ -1,15 +1,56 @@
|
||||
import base64
|
||||
import binascii
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import gspread
|
||||
from gspread.utils import InsertDataOption, ValueInputOption
|
||||
|
||||
from app import config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
sheets_api = gspread.service_account(filename="google-auth/key.json")
|
||||
|
||||
# Таймаут HTTP-запросов (сек): защита от зависания при проблемах с сетью/Google API
|
||||
sheets_api.set_timeout(30)
|
||||
_SHEETS_HTTP_TIMEOUT = 30
|
||||
|
||||
|
||||
def _load_service_account_info(raw: str) -> dict[str, Any]:
|
||||
"""Разобрать ключ из переменной окружения: чистый JSON или его base64."""
|
||||
raw = raw.strip()
|
||||
if not raw.startswith("{"):
|
||||
try:
|
||||
raw = base64.b64decode(raw, validate=True).decode("utf-8")
|
||||
except (binascii.Error, UnicodeDecodeError) as e:
|
||||
raise ValueError(
|
||||
"GOOGLE_SERVICE_ACCOUNT_JSON: ожидается JSON ключа сервисного "
|
||||
"аккаунта или его base64"
|
||||
) from e
|
||||
return json.loads(raw)
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_client() -> gspread.Client:
|
||||
"""Клиент Google Sheets. Создаётся при первом обращении, не при импорте."""
|
||||
if config.GOOGLE_SERVICE_ACCOUNT_JSON:
|
||||
info = _load_service_account_info(config.GOOGLE_SERVICE_ACCOUNT_JSON)
|
||||
client = gspread.service_account_from_dict(info)
|
||||
logger.info("Google Sheets: ключ загружен из GOOGLE_SERVICE_ACCOUNT_JSON")
|
||||
else:
|
||||
key_path = Path(config.GOOGLE_SERVICE_ACCOUNT_FILE)
|
||||
if not key_path.is_file():
|
||||
raise FileNotFoundError(
|
||||
f"Ключ сервисного аккаунта Google не найден: {key_path}. "
|
||||
"Задайте GOOGLE_SERVICE_ACCOUNT_JSON или положите файл по пути "
|
||||
"GOOGLE_SERVICE_ACCOUNT_FILE"
|
||||
)
|
||||
client = gspread.service_account(filename=str(key_path))
|
||||
logger.info("Google Sheets: ключ загружен из файла %s", key_path)
|
||||
client.set_timeout(_SHEETS_HTTP_TIMEOUT)
|
||||
return client
|
||||
|
||||
# HTTP коды, при которых стоит сбросить кэш листа (устаревший объект)
|
||||
_CACHE_INVALIDATE_CODES = frozenset({401, 403, 404, 429})
|
||||
@@ -31,7 +72,7 @@ def parse_google_sheets_url(url: str) -> tuple[str, int | None]:
|
||||
def get_sheet(sheet_link: str) -> gspread.Worksheet:
|
||||
spreadsheet_id, worksheet_id = parse_google_sheets_url(sheet_link)
|
||||
|
||||
spreadsheet = sheets_api.open_by_key(spreadsheet_id)
|
||||
spreadsheet = get_client().open_by_key(spreadsheet_id)
|
||||
if worksheet_id is not None:
|
||||
return spreadsheet.get_worksheet_by_id(worksheet_id)
|
||||
return spreadsheet.sheet1
|
||||
|
||||
Reference in New Issue
Block a user