200 lines
7.7 KiB
Python
200 lines
7.7 KiB
Python
"""
|
||
Автоматическая генерация фразы дня.
|
||
Запускается через cron в 03:00 UTC (= 06:00 Минск).
|
||
|
||
Что делает:
|
||
1. Проверяет, есть ли фраза на сегодня в daily_phrases
|
||
2. Если нет — генерирует через AI (MiniMax-M3)
|
||
3. Пишет в daily_phrases с phrase_date = CURRENT_DATE
|
||
|
||
Fallback: если AI упал — пишет запасную фразу.
|
||
"""
|
||
import asyncio
|
||
import asyncpg
|
||
import httpx
|
||
import json
|
||
import logging
|
||
import random
|
||
from datetime import datetime, timezone
|
||
from config import DB_CONFIG, TOGETHER_API_KEY
|
||
|
||
logging.basicConfig(
|
||
level=logging.INFO,
|
||
format='%(asctime)s - %(levelname)s - %(message)s'
|
||
)
|
||
logger = logging.getLogger(__name__)
|
||
|
||
TOGETHER_API_URL = "https://api.together.xyz/v1/chat/completions"
|
||
TEACHER_MODEL = "MiniMaxAI/MiniMax-M3"
|
||
|
||
# Список тем для фразы дня
|
||
TOPICS_LIST = [
|
||
"приветствие и знакомство",
|
||
"семья и друзья",
|
||
"еда и напитки",
|
||
"покупки и магазины",
|
||
"транспорт и путешествия",
|
||
"работа и профессии",
|
||
"погода и времена года",
|
||
"дом и квартира",
|
||
"хобби и свободное время",
|
||
"здоровье и самочувствие",
|
||
"город и местность",
|
||
"ежедневная рутина",
|
||
]
|
||
|
||
# Fallback фраза, если AI упал
|
||
FALLBACK_PHRASE = "Lepiej późno niż wcale."
|
||
FALLBACK_TRANSLATIONS = {
|
||
"ru": "Лучше поздно, чем никогда.",
|
||
"uk": "Краще пізно, ніж ніколи.",
|
||
"en": "Better late than never."
|
||
}
|
||
FALLBACK_GRAMMAR = {
|
||
"pl": "Lepiej - przysłówek w stopniu wyższym. Późno - przysłówek.",
|
||
"ru": "Lepiej - наречие в сравнительной степени. Późno - наречие.",
|
||
"uk": "Lepiej - прислівник у вищому ступені. Późno - прислівник.",
|
||
"en": "Lepiej - adverb in comparative degree. Późno - adverb."
|
||
}
|
||
|
||
|
||
async def call_ai(system_prompt: str, user_prompt: str, max_tokens: int = 1000) -> str:
|
||
"""Вызывает AI и возвращает ответ."""
|
||
if not TOGETHER_API_KEY:
|
||
logger.error("TOGETHER_API_KEY not configured")
|
||
return ""
|
||
|
||
try:
|
||
async with httpx.AsyncClient(timeout=90.0) as client:
|
||
response = await client.post(
|
||
TOGETHER_API_URL,
|
||
headers={
|
||
"Authorization": f"Bearer {TOGETHER_API_KEY}",
|
||
"Content-Type": "application/json",
|
||
},
|
||
json={
|
||
"model": TEACHER_MODEL,
|
||
"messages": [
|
||
{"role": "system", "content": system_prompt},
|
||
{"role": "user", "content": user_prompt},
|
||
],
|
||
"max_tokens": max_tokens,
|
||
"temperature": 0.7,
|
||
},
|
||
)
|
||
|
||
if response.status_code != 200:
|
||
logger.error(f"AI API error: {response.status_code}, body={response.text[:200]}")
|
||
return ""
|
||
|
||
data = response.json()
|
||
return data["choices"][0]["message"]["content"].strip()
|
||
|
||
except Exception as e:
|
||
logger.error(f"AI call error: {e}")
|
||
return ""
|
||
|
||
|
||
async def generate_phrase() -> tuple:
|
||
"""
|
||
Генерирует фразу дня через AI.
|
||
Возвращает (phrase_pl, translations_dict, grammar_dict).
|
||
"""
|
||
selected_topic = random.choice(TOPICS_LIST)
|
||
|
||
system_prompt = f"""Ты — учитель польского языка. Создай фразу дня для изучения польского.
|
||
Тема фразы: {selected_topic}.
|
||
Фраза должна быть уровня A2-B1, полезной в повседневной жизни.
|
||
|
||
Верни ответ строго в таком формате (без markdown, просто текст):
|
||
|
||
PHRASE: [фраза на польском]
|
||
RU: [перевод на русский]
|
||
UK: [перевод на украинский]
|
||
EN: [перевод на английский]
|
||
PL_GRAMMAR: [грамматический разбор на польском, 1-2 предложения]
|
||
RU_GRAMMAR: [грамматический разбор на русском, 1-2 предложения]
|
||
UK_GRAMMAR: [грамматический разбор на украинском, 1-2 предложения]
|
||
EN_GRAMMAR: [грамматический разбор на английском, 1-2 предложения]"""
|
||
|
||
user_prompt = f"Создай фразу дня на тему: {selected_topic}."
|
||
|
||
ai_response = await call_ai(system_prompt, user_prompt, max_tokens=1000)
|
||
|
||
if not ai_response:
|
||
logger.warning("AI returned empty response, using fallback")
|
||
return FALLBACK_PHRASE, FALLBACK_TRANSLATIONS, FALLBACK_GRAMMAR
|
||
|
||
# Парсим ответ
|
||
phrase_pl = FALLBACK_PHRASE
|
||
translations = FALLBACK_TRANSLATIONS.copy()
|
||
grammar_breakdowns = FALLBACK_GRAMMAR.copy()
|
||
|
||
for line in ai_response.strip().split('\n'):
|
||
line = line.strip()
|
||
if line.startswith('PHRASE:'):
|
||
phrase_pl = line.replace('PHRASE:', '').strip()
|
||
elif line.startswith('RU:'):
|
||
translations["ru"] = line.replace('RU:', '').strip()
|
||
elif line.startswith('UK:'):
|
||
translations["uk"] = line.replace('UK:', '').strip()
|
||
elif line.startswith('EN:'):
|
||
translations["en"] = line.replace('EN:', '').strip()
|
||
elif line.startswith('PL_GRAMMAR:'):
|
||
grammar_breakdowns["pl"] = line.replace('PL_GRAMMAR:', '').strip()
|
||
elif line.startswith('RU_GRAMMAR:'):
|
||
grammar_breakdowns["ru"] = line.replace('RU_GRAMMAR:', '').strip()
|
||
elif line.startswith('UK_GRAMMAR:'):
|
||
grammar_breakdowns["uk"] = line.replace('UK_GRAMMAR:', '').strip()
|
||
elif line.startswith('EN_GRAMMAR:'):
|
||
grammar_breakdowns["en"] = line.replace('EN_GRAMMAR:', '').strip()
|
||
|
||
logger.info(f"Generated phrase for topic '{selected_topic}': {phrase_pl[:60]}...")
|
||
return phrase_pl, translations, grammar_breakdowns
|
||
|
||
|
||
async def main():
|
||
logger.info("=== Начало генерации фразы дня ===")
|
||
|
||
conn = await asyncpg.connect(**DB_CONFIG)
|
||
|
||
try:
|
||
today = datetime.now(timezone.utc).date()
|
||
|
||
# Проверяем, есть ли уже фраза на сегодня
|
||
existing = await conn.fetchrow("""
|
||
SELECT id FROM daily_phrases WHERE phrase_date = $1
|
||
""", today)
|
||
|
||
if existing:
|
||
logger.info(f"Phrase for {today} already exists, skipping")
|
||
return
|
||
|
||
# Генерируем новую фразу
|
||
logger.info(f"Generating phrase for {today}...")
|
||
phrase_pl, translations, grammar_breakdowns = await generate_phrase()
|
||
|
||
# Пишем в БД
|
||
await conn.execute("""
|
||
INSERT INTO daily_phrases (phrase_date, phrase_pl, translations, grammar_breakdowns)
|
||
VALUES ($1, $2, $3::jsonb, $4::jsonb)
|
||
ON CONFLICT (phrase_date) DO UPDATE SET
|
||
phrase_pl = EXCLUDED.phrase_pl,
|
||
translations = EXCLUDED.translations,
|
||
grammar_breakdowns = EXCLUDED.grammar_breakdowns
|
||
""", today, phrase_pl,
|
||
json.dumps(translations, ensure_ascii=False),
|
||
json.dumps(grammar_breakdowns, ensure_ascii=False))
|
||
|
||
logger.info(f"✅ Phrase for {today} saved: {phrase_pl}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error: {e}")
|
||
finally:
|
||
await conn.close()
|
||
|
||
logger.info("=== Готово ===")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
asyncio.run(main()) |