initial commit (backend)
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
"""
|
||||
Автоматическая генерация фразы дня.
|
||||
Запускается через 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())
|
||||
Reference in New Issue
Block a user