""" Teacher Router — гибридный чат-учитель. Контент из БД (бесплатно), AI только для реакций и пробелов. Типы сообщений: 1. Объяснение — из learning_lessons 2. Вопрос recognition — кнопки A/B/C/D 3. Вопрос production — поле ввода 4. Реакция — шаблон + wrong_because_explanations (без AI) 5. AI-сообщение — только по явному триггеру Слэш-команды: - /start — начать урок - /resume — восстановить сессию - /answer — проверить ответ - /explain — AI-объяснение - /examples — 5 примеров по теме (после теста темы) - /free-message — свободное сообщение - /diagnostic-test — тест на определение уровня - /translate-to-polish — перевод на польский """ import logging import json import httpx import re import unicodedata from datetime import datetime, timezone from fastapi import APIRouter, Request, HTTPException from auth import require_auth from database import get_pool from config import TOGETHER_API_KEY, ADMIN_IDS from .orchestrator import ( get_next_lesson, get_lesson_by_id, get_lesson_questions, get_test_questions, get_random_reaction, get_wrong_because_explanation, ) logger = logging.getLogger(__name__) router = APIRouter(prefix="/teacher", tags=["teacher"]) TOGETHER_API_URL = "https://api.together.xyz/v1/chat/completions" TEACHER_MODEL = "MiniMaxAI/MiniMax-M3" TEACHER_AI_DAILY_LIMIT = 5 def get_ai_limit_message(lang: str) -> str: """Возвращает локализованное сообщение о исчерпании AI-лимита.""" messages = { "pl": "Dziś wykorzystałeś już limit AI-pomocy. Wróć jutro!", "ru": "Сегодня ты использовал лимит AI-помощи. Возвращайся завтра!", "uk": "Сьогодні ти використав ліміт AI-допомоги. Повертайся завтра!", "en": "You've used your AI help limit today. Come back tomorrow!", } return messages.get(lang, messages["en"]) def normalize_answer(text: str) -> str: """Нормализует ответ: нижний регистр, убирает диакритику и пунктуацию.""" if not text: return "" text = text.strip().lower() text = re.sub(r'[.,!?;:()\[\]{}"\'\-—–…]', '', text) text = unicodedata.normalize('NFKD', text) text = ''.join(c for c in text if not unicodedata.combining(c)) text = ' '.join(text.split()) return text async def get_user_lang(conn, user_id: int) -> str: """Получает язык интерфейса пользователя.""" lang = await conn.fetchval(""" SELECT app_language FROM users WHERE telegram_id = $1 """, user_id) return lang if lang in ["pl", "ru", "uk", "en"] else "pl" async def get_teacher_ai_usage(conn, user_id: int) -> int: """Получает количество AI-команд учителя за сегодня.""" count = await conn.fetchval(""" SELECT COUNT(*) FROM teacher_ai_usage WHERE user_id = $1 AND usage_date = CURRENT_DATE """, user_id) return count or 0 async def increment_teacher_ai_usage(conn, user_id: int) -> None: """Увеличивает счётчик AI-команд учителя за сегодня.""" await conn.execute(""" INSERT INTO teacher_ai_usage (user_id, usage_date, command_count) VALUES ($1, CURRENT_DATE, 1) ON CONFLICT (user_id, usage_date) DO UPDATE SET command_count = teacher_ai_usage.command_count + 1 """, user_id) async def check_ai_limit(conn, user_id: int) -> bool: """Проверяет, может ли пользователь использовать AI-команду.""" if user_id in ADMIN_IDS: return True usage = await get_teacher_ai_usage(conn, user_id) return usage < TEACHER_AI_DAILY_LIMIT async def start_learning_session(conn, user_id: int, level_code: str, topic_id: str, session_type: str) -> int: """Создаёт сессию обучения и возвращает session_id.""" row = await conn.fetchrow(""" INSERT INTO learning_sessions (user_id, level_code, topic_id, session_type) VALUES ($1, $2, $3, $4) RETURNING id """, user_id, level_code, topic_id, session_type) return row["id"] async def save_answer_to_session(conn, user_id: int, session_id: int, question_data: dict, selected_answer: str, correct: bool, time_taken_ms: int = 0, hint_used: bool = False) -> None: """Сохраняет ответ в user_learning_answers.""" q = question_data["question"] wrong_because = None if not correct: for opt in question_data.get("options", []): if opt.get("option_key") == selected_answer and opt.get("wrong_because"): wrong_because = opt["wrong_because"] break await conn.execute(""" INSERT INTO user_learning_answers ( user_id, session_id, level_code, topic_id, lesson_id, question_id, selected_answer, correct, skill_tag, question_type, context_type, difficulty, time_taken_ms, hint_used, wrong_because, is_final_answer ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, TRUE) """, user_id, session_id, q.get("level_code", "A1"), q.get("topic_id", ""), q.get("lesson_id"), q["question_id"], selected_answer, correct, q["skill_tag"], q["question_type"], q["context_type"], q["difficulty"], time_taken_ms, hint_used, wrong_because) async def update_mastery_after_lesson(conn, user_id: int, session_id: int) -> None: """Обновляет mastery и прогресс урока после завершения.""" from .learning import update_skill_mastery, update_topic_progress, update_ai_context skill_tags = await conn.fetch(""" SELECT DISTINCT skill_tag FROM user_learning_answers WHERE session_id = $1 """, session_id) for st in skill_tags: await update_skill_mastery(conn, user_id, st["skill_tag"]) session = await conn.fetchrow("SELECT * FROM learning_sessions WHERE id = $1", session_id) if session and session["session_type"] == "teacher_lesson": lesson_row = await conn.fetchrow(""" SELECT DISTINCT lesson_id FROM user_learning_answers WHERE session_id = $1 AND lesson_id IS NOT NULL """, session_id) if lesson_row and lesson_row["lesson_id"]: lesson_id = lesson_row["lesson_id"] stats = await conn.fetchrow(""" SELECT COUNT(*) as total, COALESCE(SUM(CASE WHEN correct THEN 1 ELSE 0 END), 0) as correct_count FROM user_learning_answers WHERE session_id = $1 AND is_final_answer = TRUE """, session_id) total = stats["total"] if stats["total"] else 0 correct_count = stats["correct_count"] if stats["correct_count"] else 0 score = round((correct_count / total * 100), 2) if total > 0 else 0 await conn.execute(""" INSERT INTO user_lesson_progress (user_id, lesson_id, started_at, completed_at, attempts_count, correct_count, best_score, completed) VALUES ($1, $2, NOW(), NOW(), 1, $3, $4, TRUE) ON CONFLICT (user_id, lesson_id) DO UPDATE SET completed_at = NOW(), attempts_count = user_lesson_progress.attempts_count + 1, correct_count = user_lesson_progress.correct_count + $3, best_score = GREATEST(user_lesson_progress.best_score, $4), completed = TRUE """, user_id, lesson_id, correct_count, score) if session and session["topic_id"]: if session["session_type"] == "teacher_test": stats = await conn.fetchrow(""" SELECT COUNT(*) as total, COALESCE(SUM(CASE WHEN correct THEN 1 ELSE 0 END), 0) as correct_count FROM user_learning_answers WHERE session_id = $1 AND is_final_answer = TRUE """, session_id) total = stats["total"] if stats["total"] else 0 correct_count = stats["correct_count"] if stats["correct_count"] else 0 test_score = round((correct_count / total * 100), 2) if total > 0 else 0 await conn.execute(""" INSERT INTO user_topic_progress (user_id, topic_id, test_started, test_completed, test_score, completed_at) VALUES ($1, $2, TRUE, TRUE, $3, NOW()) ON CONFLICT (user_id, topic_id) DO UPDATE SET test_completed = TRUE, test_score = $3, completed_at = NOW() """, user_id, session["topic_id"], test_score) await update_topic_progress(conn, user_id, session["topic_id"]) await update_ai_context(conn, user_id) async def call_ai(prompt: str, system_prompt: str, max_tokens: int = 500) -> str: """Вызывает AI и возвращает ответ.""" if not TOGETHER_API_KEY: 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": 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() ai_response = data["choices"][0]["message"]["content"].strip() ai_response = re.sub(r'\s*\([a-z_]+_[a-z_]+\)', '', ai_response) return ai_response except Exception as e: logger.error(f"AI error: {str(e)}") return "" async def get_wrong_answer_ai_explanation(telegram_id: int, lang: str, prompt_text: str, selected_answer: str, correct_answer_text: str) -> str: """ Получает AI-объяснение для неправильного ответа. Проверяет лимит и инкрементирует счётчик. Если лимит исчерпан — возвращает локализованное сообщение. """ pool = await get_pool() async with pool.acquire() as conn: if not await check_ai_limit(conn, telegram_id): return get_ai_limit_message(lang) ai_prompt = f"""Вопрос: {prompt_text} Ответ ученика: {selected_answer} Правильный ответ: {correct_answer_text} Объясни кратко (1-2 предложения), почему правильный ответ именно такой.""" system_prompt = f"""Ты — Марек, учитель польского языка. Объясняй ошибки кратко и понятно. Отвечай на языке: {lang}. Не используй markdown.""" ai_response = await call_ai(ai_prompt, system_prompt, max_tokens=200) if ai_response: async with pool.acquire() as conn: await increment_teacher_ai_usage(conn, telegram_id) else: ai_response = get_ai_limit_message(lang) if not await _check_ai_limit_for_fallback(telegram_id, lang) else "" return ai_response async def _check_ai_limit_for_fallback(telegram_id: int, lang: str) -> bool: """Проверяет, был ли лимит исчерпан (для fallback-сообщения).""" pool = await get_pool() async with pool.acquire() as conn: return await check_ai_limit(conn, telegram_id) # ============ ФРАЗА ДНЯ ============ @router.get("/phrase-of-day") async def get_phrase_of_day(request: Request): """ Возвращает фразу дня. Фраза кэшируется для всех пользователей на 24 часа. Не тратит AI-лимит пользователя. """ telegram_id, _ = await require_auth(request) pool = await get_pool() async with pool.acquire() as conn: lang = await get_user_lang(conn, telegram_id) await conn.execute(""" CREATE TABLE IF NOT EXISTS daily_phrases ( id SERIAL PRIMARY KEY, phrase_date DATE UNIQUE NOT NULL, phrase_pl TEXT NOT NULL, translations JSONB NOT NULL DEFAULT '{}', grammar_breakdowns JSONB NOT NULL DEFAULT '{}', created_at TIMESTAMP DEFAULT NOW() ) """) today = datetime.now(timezone.utc).date() today_phrase = await conn.fetchrow(""" SELECT * FROM daily_phrases WHERE phrase_date = $1 """, today) if not today_phrase: # Список тем для фразы дня topics_list = [ "приветствие и знакомство", "семья и друзья", "еда и напитки", "покупки и магазины", "транспорт и путешествия", "работа и профессии", "погода и времена года", "дом и квартира", "хобби и свободное время", "здоровье и самочувствие", "город и местность", "ежедневная рутина" ] # Выбираем случайную тему import random 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(user_prompt, system_prompt, max_tokens=1000) # Fallback значения phrase_pl = "Lepiej późno niż wcale." translations = { "ru": "Лучше поздно, чем никогда.", "uk": "Краще пізно, ніж ніколи.", "en": "Better late than never." } grammar_breakdowns = { "pl": "Lepiej - przyslowek w stopniu wyzszym. Pozno - przyslowek.", "ru": "Lepiej - наречие в сравнительной степени. Pozno - наречие.", "uk": "Lepiej - прислівник у вищому ступені. Pozno - прислівник.", "en": "Lepiej - adverb in comparative degree. Pozno - adverb." } if ai_response: # Парсим текстовый ответ построчно lines = ai_response.strip().split('\n') for line in lines: 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() 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)) today_phrase = await conn.fetchrow(""" SELECT * FROM daily_phrases WHERE phrase_date = $1 """, today) translations = today_phrase["translations"] grammar_breakdowns = today_phrase["grammar_breakdowns"] if isinstance(translations, str): translations = json.loads(translations) if isinstance(grammar_breakdowns, str): grammar_breakdowns = json.loads(grammar_breakdowns) result = { "phrase_pl": today_phrase["phrase_pl"], "date": today_phrase["phrase_date"].isoformat() if today_phrase["phrase_date"] else None, } if lang != "pl": result["translation"] = translations.get(lang, translations.get("en", "")) result["grammar_breakdown"] = grammar_breakdowns.get(lang, grammar_breakdowns.get("en", "")) return result @router.post("/resume") async def teacher_resume(request: Request): """ Восстанавливает активную сессию. Проверяет, есть ли незавершённая сессия и возвращает текущий вопрос. """ telegram_id, _ = await require_auth(request) pool = await get_pool() async with pool.acquire() as conn: lang = await get_user_lang(conn, telegram_id) session = await conn.fetchrow(""" SELECT * FROM learning_sessions WHERE user_id = $1 AND is_completed = FALSE ORDER BY id DESC LIMIT 1 """, telegram_id) if not session: return {"has_active_session": False} last_answer = await conn.fetchrow(""" SELECT question_id FROM user_learning_answers WHERE session_id = $1 ORDER BY id DESC LIMIT 1 """, session["id"]) if not last_answer: if session["session_type"] == "teacher_test": questions = await get_test_questions(conn, session["topic_id"]) else: lesson_id = await conn.fetchval(""" SELECT lesson_id FROM learning_sessions WHERE id = $1 """, session["id"]) if lesson_id: questions = await get_lesson_questions(conn, lesson_id) else: questions = [] if questions: return { "has_active_session": True, "session_id": session["id"], "session_type": session["session_type"], "topic_id": session["topic_id"], "question": questions[0], "question_index": 0, "total_questions": len(questions), } return {"has_active_session": False} if session["session_type"] == "teacher_test": questions = await get_test_questions(conn, session["topic_id"]) else: lesson_id = await conn.fetchval(""" SELECT lesson_id FROM user_learning_answers WHERE session_id = $1 AND lesson_id IS NOT NULL LIMIT 1 """, session["id"]) if lesson_id: questions = await get_lesson_questions(conn, lesson_id) else: questions = [] last_question_id = last_answer["question_id"] last_index = 0 for i, q in enumerate(questions): if q["question"]["question_id"] == last_question_id: last_index = i break next_index = last_index + 1 if next_index >= len(questions): await conn.execute(""" UPDATE learning_sessions SET is_completed = TRUE, completed_at = NOW() WHERE id = $1 """, session["id"]) await update_mastery_after_lesson(conn, telegram_id, session["id"]) return {"has_active_session": False, "session_completed": True} next_question = questions[next_index] return { "has_active_session": True, "session_id": session["id"], "session_type": session["session_type"], "topic_id": session["topic_id"], "question": next_question, "question_index": next_index, "total_questions": len(questions), } @router.post("/start") async def teacher_start(request: Request): """ Начинает или продолжает урок. Возвращает первое сообщение от учителя. """ telegram_id, username = await require_auth(request) pool = await get_pool() async with pool.acquire() as conn: lang = await get_user_lang(conn, telegram_id) first_name = username or "uczniu" next_lesson = await get_next_lesson(conn, telegram_id) if next_lesson["type"] == "completed": return { "message_type": "completed", "text": "Wszystkie lekcje ukończone! Świetna robota!" if lang == "pl" else "Все уроки пройдены! Отличная работа!" if lang == "ru" else "Всі уроки пройдені! Чудова робота!" if lang == "uk" else "All lessons completed! Great job!", } if next_lesson["type"] == "test": topic_id = next_lesson.get("topic_id") test_questions = await get_test_questions(conn, topic_id) if not test_questions: return { "message_type": "error", "text": "No test questions found.", } session_id = await start_learning_session(conn, telegram_id, "A1", topic_id, "teacher_test") first_question = test_questions[0] return { "message_type": "test_start", "session_id": session_id, "topic_id": topic_id, "topic_name": "", "text": "Czas na test!" if lang == "pl" else "Время теста!" if lang == "ru" else "Час тесту!" if lang == "uk" else "Test time!", "question": first_question, "question_index": 0, "total_questions": len(test_questions), "has_translate_command": True, } lesson_id = next_lesson["lesson"]["lesson_id"] lesson_data = await get_lesson_by_id(conn, lesson_id) if not lesson_data: return { "message_type": "error", "text": "Lesson not found.", } session_id = await start_learning_session( conn, telegram_id, "A1", lesson_data["lesson"]["topic_id"], "teacher_lesson" ) lesson = lesson_data["lesson"] examples = lesson_data["examples"] explanation_field = f"explanation_{lang}" title_field = f"title_{lang}" explanation_text = lesson.get(explanation_field) or lesson.get("explanation_en") or "" title_text = lesson.get(title_field) or lesson.get("title_en") or "" examples_text = "" for ex in examples: ex_field = f"text_{lang}" ex_translation = ex.get(ex_field) or ex.get("text_en") or "" examples_text += f"🇵🇱 {ex['text_pl']} → {ex_translation}\n" full_text = f"{title_text}\n\n{explanation_text}" if examples_text: full_text += f"\n\nPrzykłady:\n{examples_text}" questions = lesson_data["questions"] first_question = questions[0] if questions else None if not first_question: return { "message_type": "lesson_completed", "session_id": session_id, "text": "Lekcja zakończona!" if lang == "pl" else "Урок завершён!" if lang == "ru" else "Урок завершено!" if lang == "uk" else "Lesson completed!", } return { "message_type": "lesson_explanation", "session_id": session_id, "lesson_id": lesson_id, "topic_id": lesson_data["lesson"]["topic_id"], "topic_name": title_text, "text": full_text, "question": first_question, "question_index": 0, "total_questions": len(questions), "has_explain_command": True, "has_translate_command": True, } @router.post("/answer") async def teacher_answer(request: Request): """ Проверяет ответ пользователя и возвращает реакцию + следующий вопрос. """ telegram_id, _ = await require_auth(request) body = await request.json() session_id = body.get("session_id") question_id = body.get("question_id") selected_answer = body.get("answer", "") time_taken_ms = body.get("time_taken_ms", 0) if not session_id or not question_id: raise HTTPException(400, "session_id and question_id required") pool = await get_pool() async with pool.acquire() as conn: lang = await get_user_lang(conn, telegram_id) question_row = await conn.fetchrow(""" SELECT * FROM learning_questions WHERE question_id = $1 """, question_id) if not question_row: raise HTTPException(404, "Question not found") options = await conn.fetch(""" SELECT * FROM question_options WHERE question_id = $1 """, question_id) correct_answer = question_row["correct_answer"] or "" is_correct = normalize_answer(selected_answer) == normalize_answer(correct_answer) wrong_because = None for opt in options: if opt["option_key"] == selected_answer and not is_correct and opt.get("wrong_because"): wrong_because = opt["wrong_because"] break question_data = { "question": dict(question_row), "options": [dict(o) for o in options], } await save_answer_to_session( conn, telegram_id, session_id, question_data, selected_answer, is_correct, time_taken_ms ) if is_correct: reaction_type = "correct" else: reaction_type = "wrong" if wrong_because else "almost" reaction_text = await get_random_reaction(conn, reaction_type, lang) db_explanation = "" if not is_correct and wrong_because: db_explanation = await get_wrong_because_explanation(conn, wrong_because, lang) session = await conn.fetchrow("SELECT * FROM learning_sessions WHERE id = $1", session_id) if not session: raise HTTPException(404, "Session not found") if session["session_type"] == "teacher_test": questions = await get_test_questions(conn, session["topic_id"]) else: lesson_id = question_row["lesson_id"] if lesson_id: questions = await get_lesson_questions(conn, lesson_id) else: questions = [] current_index = 0 for i, q in enumerate(questions): if q["question"]["question_id"] == question_id: current_index = i break next_index = current_index + 1 options_language = question_row.get("options_language") or "pl" correct_answer_text = "" for opt in options: if opt["is_correct"]: if options_language == "user": correct_answer_text = opt.get(f"text_{lang}", "") or opt.get("text_pl", "") or "" else: correct_answer_text = opt.get("text_pl", "") or opt.get("text_ru", "") or "" break prompt_text = question_row.get(f"prompt_{lang}") or question_row.get("prompt_en") or "" needs_ai_explanation = not is_correct and not db_explanation session_completed = next_index >= len(questions) next_question_data = None if session_completed else questions[next_index] if session_completed: stats = await conn.fetchrow(""" SELECT COUNT(*) as total, COALESCE(SUM(CASE WHEN correct THEN 1 ELSE 0 END), 0) as correct_count FROM user_learning_answers WHERE session_id = $1 AND is_final_answer = TRUE """, session_id) session_stats = { "total": stats["total"] if stats["total"] else 0, "correct_count": stats["correct_count"] if stats["correct_count"] else 0, } session_type = session["session_type"] await conn.execute(""" UPDATE learning_sessions SET is_completed = TRUE, completed_at = NOW() WHERE id = $1 """, session_id) await update_mastery_after_lesson(conn, telegram_id, session_id) explanation_text = db_explanation if needs_ai_explanation: explanation_text = await get_wrong_answer_ai_explanation( telegram_id, lang, prompt_text, selected_answer, correct_answer_text ) if session_completed: return { "message_type": "lesson_completed", "reaction": reaction_text, "explanation": explanation_text, "is_correct": is_correct, "correct_answer": correct_answer_text, "wrong_because": wrong_because or "", "session_completed": True, "session_stats": session_stats, "session_type": session_type, } return { "message_type": "next_question", "reaction": reaction_text, "explanation": explanation_text, "is_correct": is_correct, "correct_answer": correct_answer_text, "wrong_because": wrong_because or "", "question": next_question_data, "question_index": next_index, "total_questions": len(questions), "has_explain_command": not is_correct, } @router.post("/explain") async def teacher_explain(request: Request): """ AI-объяснение текущего урока или ошибки. """ telegram_id, _ = await require_auth(request) body = await request.json() lesson_id = body.get("lesson_id", "") question_id = body.get("question_id", "") topic = body.get("topic", "") pool = await get_pool() async with pool.acquire() as conn: lang = await get_user_lang(conn, telegram_id) if not await check_ai_limit(conn, telegram_id): return { "response": "", "limit_exceeded": True, "message": get_ai_limit_message(lang) } content = "" if lesson_id: lesson = await conn.fetchrow(""" SELECT * FROM learning_lessons WHERE lesson_id = $1 """, lesson_id) if lesson: content = lesson.get(f"explanation_{lang}") or lesson.get("explanation_en") or "" elif question_id: question = await conn.fetchrow(""" SELECT * FROM learning_questions WHERE question_id = $1 """, question_id) if question: content = question.get(f"prompt_{lang}") or question.get("prompt_en") or "" else: content = topic if not content: return {"response": "", "error": "No content to explain"} system_prompt = f"""Ты — учитель польского языка. Объясни материал простыми словами. Отвечай на языке: {lang}. Не используй markdown.""" ai_response = await call_ai(content, system_prompt) if ai_response: async with pool.acquire() as conn: await increment_teacher_ai_usage(conn, telegram_id) return {"response": ai_response} @router.post("/examples") async def teacher_examples(request: Request): """ Даёт 5 примеров предложений по текущей теме. Формат: 🇵🇱 польская фраза → перевод на язык пользователя. AI-команда, лимит 5/день. """ telegram_id, _ = await require_auth(request) body = await request.json() topic = body.get("topic", "") lesson_id = body.get("lesson_id", "") topic_id = body.get("topic_id", "") pool = await get_pool() async with pool.acquire() as conn: lang = await get_user_lang(conn, telegram_id) if not await check_ai_limit(conn, telegram_id): return { "response": "", "limit_exceeded": True, "message": get_ai_limit_message(lang) } # Если не передан topic — берём по lesson_id if lesson_id and not topic: lesson = await conn.fetchrow(""" SELECT title_pl, title_ru, title_uk, title_en FROM learning_lessons WHERE lesson_id = $1 """, lesson_id) if lesson: topic = lesson.get(f"title_{lang}") or lesson.get("title_en") or "" # Если не передан topic и lesson_id — берём по topic_id if topic_id and not topic: topic_row = await conn.fetchrow(""" SELECT name_pl, name_ru, name_uk, name_en FROM learning_topics WHERE topic_id = $1 """, topic_id) if topic_row: topic = topic_row.get(f"name_{lang}") or topic_row.get("name_en") or "" if not topic: topic = "polish language" lang_names = { "pl": "polskiego", "ru": "русский", "uk": "украинский", "en": "English", } lang_name = lang_names.get(lang, "English") system_prompt = f"""Ты — учитель польского языка. Дай ровно 5 примеров предложений по теме: {topic}. Каждый пример — простое предложение уровня A2, полезное в повседневной жизни. Формат ответа (без markdown, каждый пример с новой строки): 🇵🇱 [польское предложение] → [перевод на {lang_name}] Пример: 🇵🇱 Jestem nauczycielem. → Я учитель. 🇵🇱 Pracuję w biurze. → Я работаю в офисе. Верни только 5 примеров, без вступлений и пояснений.""" ai_response = await call_ai(topic, system_prompt, max_tokens=500) if ai_response: async with pool.acquire() as conn: await increment_teacher_ai_usage(conn, telegram_id) return {"response": ai_response} @router.post("/free-message") async def teacher_free_message(request: Request): """ Обрабатывает свободное сообщение пользователя. """ telegram_id, _ = await require_auth(request) body = await request.json() user_message = body.get("message", "").strip() lesson_id = body.get("lesson_id", "") topic_id = body.get("topic_id", "") topic_name = body.get("topic_name", "") if not user_message: raise HTTPException(400, "message is required") if len(user_message) > 300: raise HTTPException(400, "Message too long (max 300 characters)") pool = await get_pool() async with pool.acquire() as conn: lang = await get_user_lang(conn, telegram_id) if not await check_ai_limit(conn, telegram_id): return { "response": "", "limit_exceeded": True, "message": get_ai_limit_message(lang) } context_parts = [] if topic_name: context_parts.append(f"Текущая тема урока: {topic_name}") if lesson_id: lesson = await conn.fetchrow(""" SELECT title_pl, title_ru, title_uk, title_en FROM learning_lessons WHERE lesson_id = $1 """, lesson_id) if lesson: lesson_title = lesson.get(f"title_{lang}") or lesson.get("title_en") or "" context_parts.append(f"Текущий урок: {lesson_title}") context_text = "\n".join(context_parts) if context_parts else "" system_prompt = f"""Ты — Марек, учитель польского языка в приложении Filwords PL. Ты дружелюбный, поддерживающий и краткий. Отвечай на языке: {lang}. Не используй markdown. {context_text} Ученик может спросить что угодно: объяснить тему, перевести фразу, дать пример. Отвечай кратко (1-3 предложения) и по делу.""" ai_response = await call_ai(user_message, system_prompt, max_tokens=300) if ai_response: async with pool.acquire() as conn: await increment_teacher_ai_usage(conn, telegram_id) return {"response": ai_response} @router.post("/diagnostic-test") async def teacher_diagnostic_test(request: Request): """ Тест на определение уровня языка. Берёт случайные вопросы разных тем и уровней. """ telegram_id, _ = await require_auth(request) pool = await get_pool() async with pool.acquire() as conn: lang = await get_user_lang(conn, telegram_id) questions = await conn.fetch(""" SELECT * FROM learning_questions WHERE is_test_question = TRUE ORDER BY RANDOM() LIMIT 15 """) if not questions: return {"error": "No questions available"} session_id = await start_learning_session(conn, telegram_id, "A1", "", "diagnostic_test") questions_data = [] for q in questions: options = await conn.fetch(""" SELECT * FROM question_options WHERE question_id = $1 """, q["question_id"]) questions_data.append({ "question": dict(q), "options": [dict(o) for o in options], }) return { "session_id": session_id, "questions": questions_data, "total_questions": len(questions_data), } @router.post("/translate-to-polish") async def teacher_translate_to_polish(request: Request): """ Переводит текст на польский язык. AI-команда, лимит 5/день. """ telegram_id, _ = await require_auth(request) body = await request.json() text = body.get("text", "").strip() if not text: raise HTTPException(400, "text is required") if len(text) > 300: raise HTTPException(400, "Text too long (max 300 characters)") pool = await get_pool() async with pool.acquire() as conn: lang = await get_user_lang(conn, telegram_id) if not await check_ai_limit(conn, telegram_id): return { "response": "", "limit_exceeded": True, "message": get_ai_limit_message(lang) } system_prompt = f"""Ты — переводчик польского языка. Переведи текст на польский. Отвечай только переводом без пояснений. Не используй markdown.""" ai_response = await call_ai(text, system_prompt, max_tokens=300) if ai_response: async with pool.acquire() as conn: await increment_teacher_ai_usage(conn, telegram_id) return {"response": ai_response} # ============ ПРОГРЕСС ПО УРОВНЯМ ============ @router.get("/progress") async def teacher_progress(request: Request): """ Возвращает прогресс пользователя по всем уровням (A1-C2). Для каждого уровня: сколько тем пройдено из общего количества. """ telegram_id, _ = await require_auth(request) pool = await get_pool() async with pool.acquire() as conn: levels = await conn.fetch(""" SELECT code, name_pl, name_ru, name_uk, name_en FROM learning_levels WHERE is_active = TRUE ORDER BY sort_order """) result = [] for level in levels: code = level["code"] topics_total = await conn.fetchval(""" SELECT COUNT(*) FROM learning_topics WHERE level_code = $1 AND is_active = TRUE """, code) or 0 topics_completed = await conn.fetchval(""" SELECT COUNT(*) FROM user_topic_progress WHERE user_id = $1 AND topic_id IN ( SELECT topic_id FROM learning_topics WHERE level_code = $2 AND is_active = TRUE ) AND test_completed = TRUE """, telegram_id, code) or 0 result.append({ "code": code, "name_pl": level["name_pl"], "name_ru": level["name_ru"], "name_uk": level["name_uk"], "name_en": level["name_en"], "topics_total": topics_total, "topics_completed": topics_completed, }) return {"levels": result} @router.post("/history/save") async def save_teacher_history(request: Request): """Сохраняет историю чата учителя на сервере.""" telegram_id, _ = await require_auth(request) body = await request.json() messages = body.get("messages", []) if not isinstance(messages, list): raise HTTPException(400, "messages must be a list") pool = await get_pool() async with pool.acquire() as conn: await conn.execute(""" INSERT INTO teacher_chat_history (user_id, messages, updated_at) VALUES ($1, $2::jsonb, NOW()) ON CONFLICT (user_id) DO UPDATE SET messages = $2::jsonb, updated_at = NOW() """, telegram_id, json.dumps(messages, ensure_ascii=False)) return {"status": "ok"} @router.get("/history/load") async def load_teacher_history(request: Request): """Загружает историю чата учителя с сервера.""" telegram_id, _ = await require_auth(request) pool = await get_pool() async with pool.acquire() as conn: row = await conn.fetchrow(""" SELECT messages FROM teacher_chat_history WHERE user_id = $1 """, telegram_id) if row and row["messages"]: messages = row["messages"] if isinstance(messages, str): messages = json.loads(messages) return {"messages": messages} return {"messages": []} @router.post("/history/clear") async def clear_teacher_history(request: Request): """Очищает историю чата учителя на сервере.""" telegram_id, _ = await require_auth(request) pool = await get_pool() async with pool.acquire() as conn: await conn.execute(""" DELETE FROM teacher_chat_history WHERE user_id = $1 """, telegram_id) return {"status": "ok"}