""" Автоматическая генерация фразы дня. Запускается через 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())