initial commit (backend)

This commit is contained in:
2026-09-20 11:45:58 +00:00
commit 033adf023e
65 changed files with 28292 additions and 0 deletions
+200
View File
@@ -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())