# app.py import os import re import gradio as gr import torch from transformers import pipeline, AutoTokenizer # ============================ # 3 Transformers (pipelines) # ============================ # 1) Intent / zero-shot DEFAULT_INTENT_MODEL = os.getenv("INTENT_MODEL", "joeddav/xlm-roberta-large-xnli") # 2) Checklist generator (instruct) DEFAULT_GEN_MODEL = os.getenv("GEN_MODEL", "Qwen/Qwen2.5-1.5B-Instruct") # If your Space can handle it, this is often better structured: # DEFAULT_GEN_MODEL = os.getenv("GEN_MODEL", "Qwen/Qwen2.5-1.5B-Instruct") # 3) QA over checklist DEFAULT_QA_MODEL=os.getenv("QA_MODEL", "MilyaShams/rubert-russian-qa-sberquad") DEVICE = 0 if torch.cuda.is_available() else -1 def safe_make_pipeline(task: str, model_name: str, **kwargs): try: return pipeline(task, model=model_name, device=DEVICE, **kwargs), model_name except Exception: if task == "zero-shot-classification": fallback = "facebook/bart-large-mnli" elif task == "text-generation": fallback = "TinyLlama/TinyLlama-1.1B-Chat-v1.0" elif task == "question-answering": fallback = "distilbert-base-cased-distilled-squad" else: raise return pipeline(task, model=fallback, device=DEVICE, **kwargs), fallback intent_pipe, intent_model_used = safe_make_pipeline( "zero-shot-classification", DEFAULT_INTENT_MODEL, ) gen_pipe, gen_model_used = safe_make_pipeline( "text-generation", DEFAULT_GEN_MODEL, ) qa_pipe, qa_model_used = safe_make_pipeline( "question-answering", DEFAULT_QA_MODEL, ) # For Qwen chat formatting (works for many instruct LMs too) try: gen_tokenizer = AutoTokenizer.from_pretrained(gen_model_used, use_fast=True) except Exception: gen_tokenizer = None DEFAULT_LABELS = [ "обучение", "переезд", "релокация/иммиграция", "путешествие", "карьера/поиск работы", "финансы/покупка", "здоровье/фитнес", "ремонт/быт", ] CATEGORY_CHOICES = ["Авто (определить по тексту)"] + DEFAULT_LABELS def normalize_text(s: str) -> str: s = (s or "").strip() s = re.sub(r"\s+", " ", s) return s def infer_intent(user_goal: str, labels: list[str]): if not user_goal: return "не задано", 0.0, "Нет входного текста." result = intent_pipe(user_goal, candidate_labels=labels, multi_label=False) top_label = result["labels"][0] top_score = float(result["scores"][0]) lines = ["Распознавание намерения (zero-shot):"] for lab, sc in zip(result["labels"], result["scores"]): lines.append(f"- {lab}: {sc:.3f}") return top_label, top_score, "\n".join(lines) def build_checklist_prompt(user_goal: str, theme: str | None, style: str, constraints: str) -> str: theme_part = f"Тема: {theme}\n" if theme else "" constraints_part = f"Контекст: {constraints}\n" if constraints else "" style_hint = { "кратко": "короткие пункты без лишних слов", "подробно": "чуть более подробные пункты + подпункты где уместно", "с акцентом на риски": "сильный акцент на предотвращение ошибок и рисков", "с акцентом на сроки": "добавляй дедлайны/временные окна там, где уместно", }.get(style, "короткие пункты") return ( "Составь практичный чек-лист на русском языке.\n" "Верни ТОЛЬКО чек-лист без вступлений.\n" "Запрещено:\n" "- любые вступления/комментарии (например: 'Конечно!', 'Вот исправленный текст', 'не меняю смысл')\n" "- плейсхолдеры и шаблоны: никаких '[секунды]', '[какой-то текст]', '{...}', '<...>'\n" "- таблицы, 'Расчёт', 'Банк', 'Сообщение', поля для заполнения\n\n" "- любые сокращения и метки кроме (P0), (P1), (P2)\n" "- нельзя писать '(пос.)', '(пи)', '(n1)' и любые другие скобочные пометки\n" "Формат строго:\n" "- [ ] (P0) пункт\n" " - подпункт (если нужно)\n" "Где P0 = срочно/критично, P1 = важно, P2 = можно позже.\n\n" "Требования:\n" f"- стиль: {style_hint}\n" "- 12–18 пунктов\n" "- без нумерации (никаких '1.'), только '- [ ]'\n" "- КАЖДЫЙ пункт должен начинаться с (P0) или (P1) или (P2)\n" "- Сначала выведи все P0, потом P1, потом P2\n" "- пункты конкретные и выполнимые\n" "- для пунктов про документы/банки/счета/визы/страховку ОБЯЗАТЕЛЬНО добавь 2–5 подпунктов\n" "- подпункты начинай с ' - '\n" "- если тема релокации/иммиграции: обязательно охвати документы, финансы, связь, жильё, медицину, язык\n" "- в конце 2 блока:\n" "- после приоритета НЕ добавляй другие метки в скобках\n" "- пример: '- [ ] (P1) Открыть банковский счёт'\n" "Проверка готовности:\n" "- ... (3–5 вопросов)\n" "Риски и как снизить:\n" "- ... (3–6 пунктов)\n\n" f"{theme_part}" f"{constraints_part}" f"Цель: {user_goal}\n\n" "Чек-лист:\n" ) def apply_chat_template_if_available(user_prompt: str) -> str: if gen_tokenizer is None or not hasattr(gen_tokenizer, "apply_chat_template"): return user_prompt messages = [ {"role": "system", "content": "Ты пишешь грамотно по-русски и строго соблюдаешь формат чек-листа."}, {"role": "user", "content": user_prompt}, ] return gen_tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) def clean_checklist(text: str) -> str: text = (text or "").strip() text = re.sub(r"", "", text).strip() cleaned = [] current_group = "OTHER" for ln in text.splitlines(): s = ln.rstrip() if not s.strip(): continue low = s.strip().lower() bad_prefixes = ( "конечно", "вот исправленный", "вот исправленный текст", "исправленный текст", "ничего не добавляя", "не меняю смысл", "я не меняю смысл", "готово", "итог", "результат", ) if low.startswith(bad_prefixes): continue if low.startswith(("расч", "банк", "сообщение", "время ", "план поездки", "расход")): continue s2 = re.sub(r"\[(?!\s*\])([^\]]+)\]", "", s).strip() s2 = re.sub(r"^\-\s*\[\s*\]\s*\[(.+)\]\s*$", r"- [ ] \1", s2).strip() s2 = s2.replace("•", "-").replace("–", "-") s2 = re.sub(r"\s+", " ", s2) if s2.startswith("- [ ]") or s2.startswith(" -") or s2.startswith("Проверка готовности") or s2.startswith("Риски"): cleaned.append(s2) out = "\n".join(cleaned).strip() if "- [ ]" not in out: raw = [l.strip() for l in text.splitlines() if l.strip()] raw = [re.sub(r"^\d+[\).\s]+", "", r).strip(" -•\t") for r in raw] raw = [r for r in raw if r] raw = [f"- [ ] (P1) {r}" for r in raw[:18]] out = "\n".join(raw).strip() return out def split_tail_sections(text: str): lines = text.splitlines() idx = None for i, ln in enumerate(lines): if ln.strip().startswith("Проверка готовности"): idx = i break if idx is None: return text, "" return "\n".join(lines[:idx]).strip(), "\n".join(lines[idx:]).strip() def sort_by_priority(text: str) -> str: body, tail = split_tail_sections(text) if not body: return text groups = {"P0": [], "P1": [], "P2": [], "OTHER": []} current = "OTHER" for ln in body.splitlines(): s = ln.strip() if s.startswith("- [ ]"): if "(P0)" in s: current = "P0" elif "(P1)" in s: current = "P1" elif "(P2)" in s: current = "P2" else: current = "OTHER" groups[current].append(ln) else: groups[current].append(ln) out_lines = [] for k in ["P0", "P1", "P2", "OTHER"]: if groups[k]: out_lines.extend(groups[k]) out = "\n".join(out_lines).strip() if tail: out = (out + "\n\n" + tail).strip() return out def dedupe_checklist(text: str) -> str: body, tail = split_tail_sections(text) lines = body.splitlines() blocks = [] i = 0 while i < len(lines): if lines[i].startswith("- [ ]"): item = lines[i] subs = [] i += 1 while i < len(lines) and lines[i].startswith(" -"): subs.append(lines[i]) i += 1 blocks.append((item, subs)) else: i += 1 def norm(s: str) -> str: s = s.lower() s = re.sub(r"\(p[0-2]\)", "", s) s = re.sub(r"[^a-zа-я0-9 ]+", " ", s) s = re.sub(r"\s+", " ", s).strip() return s seen_items = set() out = [] for item, subs in blocks: k = norm(item) if k in seen_items: continue seen_items.add(k) out.append(item) seen_subs = set() for sub in subs: sk = norm(sub) if sk in seen_subs: continue seen_subs.add(sk) out.append(sub) out_text = "\n".join(out).strip() if tail: out_text = (out_text + "\n\n" + tail).strip() return out_text def trim_incomplete_last_line(text: str) -> str: lines = text.splitlines() if not lines: return text last = lines[-1].strip() if (last.startswith("- [ ]") or last.startswith(" -")) and ( re.search(r"[,:;—–-]$", last) or len(last.split()) < 3 ): lines = lines[:-1] return "\n".join(lines).strip() def ensure_doc_finance_subpoints(text: str) -> str: lines = text.splitlines() out = [] added_docs = False added_fin = False def has_any_subpoints_with_keywords(all_lines, keywords): low_all = "\n".join(all_lines).lower() return any(k in low_all for k in keywords) docs_kw_present = has_any_subpoints_with_keywords(lines, ["загранпаспорт", "виза", "внж", "копии", "облако", "приглашение"]) fin_kw_present = has_any_subpoints_with_keywords(lines, ["комисси", "лимит", "вторая карта", "2fa", "двухфактор", "уведомлен"]) i = 0 while i < len(lines): ln = lines[i] out.append(ln) if ln.startswith("- [ ]"): low = ln.lower() j = i + 1 has_sub = (j < len(lines) and lines[j].startswith(" -")) if (not added_docs) and (not docs_kw_present) and (not has_sub) and any(k in low for k in ["документ", "виза", "внж", "страхов", "паспорт"]): out.extend([ " - Проверь срок действия загранпаспорта и требования к остаточному сроку", " - Составь список нужных документов и сделай копии (бумага + облако)", " - Уточни требования по визе/ВНЖ и собери подтверждения (финансы, жильё, приглашение)", ]) added_docs = True if (not added_fin) and (not fin_kw_present) and (not has_sub) and any(k in low for k in ["счёт", "счет", "банк", "карта", "финанс"]): out.extend([ " - Проверь лимиты, комиссии и возможность работы карт за границей", " - Подготовь резервный доступ к деньгам (вторая карта/наличные/перевод)", " - Включи уведомления и двухфакторную защиту в банковских приложениях", ]) added_fin = True i += 1 return "\n".join(out).strip() def polish_russian_same_model(checklist_text: str) -> str: if not checklist_text or checklist_text.count("- [ ]") < 6: return checklist_text polish_prompt = ( "Отредактируй текст чек-листа: исправь грамматику и стиль русского языка.\n" "ВАЖНО:\n" "- Верни ТОЛЬКО чек-лист.\n" "- Никаких вступлений (например: 'Конечно', 'Вот исправленный текст').\n" "- Никаких комментариев (например: 'не меняю смысл').\n" "- Ничего не добавляй и не удаляй по смыслу.\n" "- Сохрани формат:\n" " - [ ] (P0/P1/P2) пункт\n" " - подпункт\n" "- Сохрани блоки 'Проверка готовности' и 'Риски и как снизить'.\n\n" "- не меняй формулировки на канцелярит, пиши простым естественным русским\n" f"{checklist_text}\n" ) polish_prompt = apply_chat_template_if_available(polish_prompt) outp = gen_pipe( polish_prompt, max_new_tokens=260, do_sample=False, temperature=0.0, # если поддерживается return_full_text=False, ) text2 = (outp[0].get("generated_text") or "").strip() text2 = clean_checklist(text2) text2 = sort_by_priority(text2) if text2.count("- [ ]") >= checklist_text.count("- [ ]"): return text2 return checklist_text def generate_checklist(user_goal: str, category: str, style: str, constraints: str): user_goal = normalize_text(user_goal) constraints = normalize_text(constraints) if not user_goal: return ( "", "Введите описание цели.", "", f"intent={intent_model_used}\ngen={gen_model_used}\nqa={qa_model_used}", None, None, ) inferred_label, inferred_score, intent_debug = infer_intent(user_goal, DEFAULT_LABELS) if category and category != "Авто (определить по тексту)": chosen_theme = category else: chosen_theme = inferred_label if inferred_score >= 0.30 else None base_prompt = build_checklist_prompt( user_goal=user_goal, theme=chosen_theme, style=style, constraints=constraints, ) prompt = apply_chat_template_if_available(base_prompt) out = gen_pipe( prompt, max_new_tokens=900, do_sample=True, temperature=0.55, top_p=0.9, repetition_penalty=1.15, no_repeat_ngram_size=4, return_full_text=False, ) text = (out[0].get("generated_text") or "").strip() text = clean_checklist(text) text = sort_by_priority(text) text = ensure_doc_finance_subpoints(text) text = dedupe_checklist(text) text = trim_incomplete_last_line(text) text = polish_russian_same_model(text) text = dedupe_checklist(text) text = trim_incomplete_last_line(text) if text.count("- [ ]") < 10: retry_prompt = ( "Сделай чек-лист строго в формате '- [ ] (P0/P1/P2) ...' (12–18 пунктов) на русском.\n" "Сначала P0, затем P1, затем P2.\n" "Без вступлений. Без комментариев. Без плейсхолдеров в квадратных скобках.\n" "Для документов/банков/счётов/визы/страховки добавь подпункты (2–5).\n" "В конце добавь:\n" "Проверка готовности: (3–5 вопросов)\n" "Риски и как снизить: (3–6 пунктов)\n\n" f"Цель: {user_goal}\n" f"Контекст: {constraints}\n" f"Тема: {chosen_theme}\n\n" "Чек-лист:\n" ) retry_prompt = apply_chat_template_if_available(retry_prompt) out2 = gen_pipe( retry_prompt, max_new_tokens=750, do_sample=True, temperature=0.75, top_p=0.93, repetition_penalty=1.12, return_full_text=False, ) text2 = (out2[0].get("generated_text") or "").strip() text2 = clean_checklist(text2) text2 = sort_by_priority(text2) if text2.count("- [ ]") > text.count("- [ ]"): text = text2 text = ensure_doc_finance_subpoints(text) text = polish_russian_same_model(text) if text.count("- [ ]") < 8: text = ( "- [ ] (P0) Не удалось корректно сформировать чек-лист.\n" "- [ ] (P0) Попробуйте уточнить цель (страна/город, статус визы, бюджет, сроки) и нажать ещё раз.\n" ) meta = { "goal": user_goal, "theme": chosen_theme, "intent_label": inferred_label, "intent_score": inferred_score, "intent_model": intent_model_used, "gen_model": gen_model_used, "qa_model": qa_model_used, } models_info = f"intent={intent_model_used}\ngen={gen_model_used}\nqa={qa_model_used}" theme_info = f"{chosen_theme or '(не определилась)'}" return text, intent_debug, theme_info, models_info, text, meta def answer_question(question: str, checklist_state: str, meta_state: dict | None): question = normalize_text(question) if not checklist_state: return "Сначала сгенерируйте чек-лист на первой вкладке.", "" if not question: return "Введите вопрос.", "" qa_res = qa_pipe(question=question, context=checklist_state) answer = (qa_res.get("answer") or "").strip() score = float(qa_res.get("score") or 0.0) evidence = f"QA score: {score:.3f}\n" if answer: evidence += f"Span: {answer}\n" if (not answer) or score < 0.20 or len(answer) < 3: goal = (meta_state or {}).get("goal", "") theme = (meta_state or {}).get("theme", "") user_prompt = ( "Ответь на вопрос по чек-листу. Пиши по-русски, кратко и практично.\n" "Если ответа нет в чек-листе — предложи 3–6 дополнительных пунктов (с приоритетом P0/P1/P2).\n" "Не используй вступления и комментарии.\n" "Не используй плейсхолдеры в квадратных скобках.\n\n" f"Цель: {goal}\n" f"Тема: {theme}\n\n" f"Чек-лист:\n{checklist_state}\n\n" f"Вопрос: {question}\n" "Ответ:\n" ) prompt = apply_chat_template_if_available(user_prompt) gen_out = gen_pipe( prompt, max_new_tokens=320, do_sample=False, return_full_text=False, )[0]["generated_text"].strip() gen_out = re.sub(r"", "", gen_out).strip() return gen_out, evidence + "Fallback: generator used (QA confidence low)." return f"{answer}\n\n_(Найдено в чек-листе; уверенность: {score:.2f})_", evidence with gr.Blocks(title="Умный чек-лист (3 Transformers)") as demo: gr.Markdown( "# ✅ Умный чек-лист (3 Transformers)\n" "1) распознаём намерение → 2) генерируем чек-лист → 3) отвечаем на вопросы по чек-листу\n" ) checklist_state = gr.State(value=None) meta_state = gr.State(value=None) with gr.Tab("1) Создать чек-лист"): with gr.Row(): with gr.Column(scale=3): user_goal = gr.Textbox( label="Опишите, что вы хотите сделать", placeholder="Напр.: Хочу переехать в Китай на 2 года. Бюджет 200 000 ₽. Есть загранпаспорт.", lines=4, ) category = gr.Dropdown( label="Категория (необязательно)", choices=CATEGORY_CHOICES, value="Авто (определить по тексту)", ) style = gr.Dropdown( label="Стиль чек-листа", choices=["кратко", "подробно", "с акцентом на риски", "с акцентом на сроки"], value="кратко", ) constraints = gr.Textbox( label="Контекст/ограничения (необязательно)", placeholder="Напр.: бюджет, срок, город/страна, семья/дети, удалённая работа, уровень языка...", lines=3, ) gen_btn = gr.Button("Сгенерировать чек-лист", variant="primary") with gr.Column(scale=2): theme_box = gr.Textbox( label="Выбранная/распознанная тема", lines=2, interactive=False, ) models_box = gr.Textbox( label="Модели", lines=3, interactive=False, ) intent_debug = gr.Textbox( label="Диагностика намерения", lines=10, interactive=False, ) checklist_out = gr.Code(label="Чек-лист", language="markdown") gen_btn.click( fn=generate_checklist, inputs=[user_goal, category, style, constraints], outputs=[checklist_out, intent_debug, theme_box, models_box, checklist_state, meta_state], ) with gr.Tab("2) Уточняющие вопросы по чек-листу"): with gr.Row(): with gr.Column(scale=3): gr.Markdown("Задайте вопрос по уже сгенерированному чек-листу.") question = gr.Textbox( label="Ваш вопрос", placeholder="Напр.: Какие документы подготовить? Как снизить расходы? Что сделать первым делом?", lines=2, ) ask_btn = gr.Button("Ответить", variant="primary") answer_out = gr.Markdown(label="Ответ") with gr.Column(scale=2): evidence_out = gr.Textbox(label="Тех. детали", lines=10) ask_btn.click( fn=answer_question, inputs=[question, checklist_state, meta_state], outputs=[answer_out, evidence_out], ) if __name__ == "__main__": demo.launch()