Nikolay Ponomarev commited on
Commit
42bde7a
·
1 Parent(s): 9a0dcc6

Item Search

Browse files
Files changed (1) hide show
  1. app.py +227 -128
app.py CHANGED
@@ -1,16 +1,17 @@
 
1
  import os
2
  import re
3
  import gradio as gr
4
  import torch
5
- from transformers import pipeline
6
 
7
- # ----------------------------
8
- # Model config (3 Transformers)
9
- # ----------------------------
10
  # 1) Intent / zero-shot
11
  DEFAULT_INTENT_MODEL = os.getenv("INTENT_MODEL", "joeddav/xlm-roberta-large-xnli")
12
 
13
- # 2) Checklist generator
14
  DEFAULT_GEN_MODEL = os.getenv("GEN_MODEL", "Qwen/Qwen2.5-0.5B-Instruct")
15
 
16
  # 3) QA over checklist
@@ -20,28 +21,21 @@ DEVICE = 0 if torch.cuda.is_available() else -1
20
 
21
 
22
  def safe_make_pipeline(task: str, model_name: str, **kwargs):
23
- """
24
- Tries to load a pipeline; if fails, uses a smaller/safer fallback.
25
- This keeps the Space alive even if the preferred model name is unavailable.
26
- """
27
  try:
28
  return pipeline(task, model=model_name, device=DEVICE, **kwargs), model_name
29
- except Exception as e:
30
- # Fallbacks (kept simple)
31
  if task == "zero-shot-classification":
32
  fallback = "facebook/bart-large-mnli"
33
- elif task == "text2text-generation":
34
- fallback = "google/flan-t5-base"
35
  elif task == "question-answering":
36
  fallback = "distilbert-base-cased-distilled-squad"
37
  else:
38
- raise e
 
39
 
40
- pipe = pipeline(task, model=fallback, device=DEVICE, **kwargs)
41
- return pipe, fallback
42
 
43
-
44
- # Create 3 pipelines (3 transformers)
45
  intent_pipe, intent_model_used = safe_make_pipeline(
46
  "zero-shot-classification",
47
  DEFAULT_INTENT_MODEL,
@@ -55,19 +49,24 @@ qa_pipe, qa_model_used = safe_make_pipeline(
55
  DEFAULT_QA_MODEL,
56
  )
57
 
 
 
 
 
 
58
 
59
- # ----------------------------
60
- # App logic
61
- # ----------------------------
62
  DEFAULT_LABELS = [
63
  "обучение",
64
  "переезд",
 
65
  "путешествие",
66
  "карьера/поиск работы",
67
  "финансы/покупка",
68
  "здоровье/фитнес",
69
  "ремонт/быт",
70
- "личный проект",
71
  ]
72
 
73
  CATEGORY_CHOICES = ["Авто (определить по тексту)"] + DEFAULT_LABELS
@@ -80,15 +79,10 @@ def normalize_text(s: str) -> str:
80
 
81
 
82
  def infer_intent(user_goal: str, labels: list[str]):
83
- """
84
- Returns (top_label, score, all_labels_scores_as_text).
85
- """
86
  if not user_goal:
87
  return "не задано", 0.0, "Нет входного текста."
88
 
89
- # zero-shot expects candidate_labels
90
  result = intent_pipe(user_goal, candidate_labels=labels, multi_label=False)
91
- # result: {'sequence': ..., 'labels': [...], 'scores': [...]}
92
  top_label = result["labels"][0]
93
  top_score = float(result["scores"][0])
94
 
@@ -98,23 +92,36 @@ def infer_intent(user_goal: str, labels: list[str]):
98
  return top_label, top_score, "\n".join(lines)
99
 
100
 
101
- def build_checklist_prompt(user_goal: str, theme: str | None, style: str, constraints: str):
102
  theme_part = f"Тема: {theme}\n" if theme else ""
103
  constraints_part = f"Контекст: {constraints}\n" if constraints else ""
104
 
 
 
 
 
 
 
 
105
  return (
106
- "Ты помощник, который делает практичные чек-листы.\n"
107
  "Верни ТОЛЬКО чек-лист без вступлений.\n"
108
- "Формат:\n"
 
 
 
109
  "- [ ] пункт\n"
110
  " - подпункт (если нужно)\n"
111
  "Требования:\n"
112
- "- 10–15 пунктов\n"
113
- "- конкретно и измеримо\n"
114
- "- в конце добавь разделы:\n"
115
- "Проверка готовности: (3–5 вопросов)\n"
116
- "Риски и как снизить: (3–6 пунктов)\n"
117
- "Пиши по-русски.\n\n"
 
 
 
118
  f"{theme_part}"
119
  f"{constraints_part}"
120
  f"Цель: {user_goal}\n\n"
@@ -122,90 +129,168 @@ def build_checklist_prompt(user_goal: str, theme: str | None, style: str, constr
122
  )
123
 
124
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  def generate_checklist(user_goal: str, category: str, style: str, constraints: str):
126
  user_goal = normalize_text(user_goal)
127
  constraints = normalize_text(constraints)
128
 
129
  if not user_goal:
130
  return (
131
- "Введите описание цели (например: 'Хочу переехать в другой город за 2 месяца').",
132
  "",
 
133
  "",
 
134
  None,
135
  None,
136
  )
137
 
138
- # Decide labels for intent detection (we keep it to 8)
139
- labels = DEFAULT_LABELS
140
-
141
- inferred_label, inferred_score, intent_debug = infer_intent(user_goal, labels)
142
 
143
- chosen_theme = None
144
  if category and category != "Авто (определить по тексту)":
145
  chosen_theme = category
146
  else:
147
- # Use inferred label only if confidence is decent; otherwise keep theme=None
148
- chosen_theme = inferred_label if inferred_score >= 0.35 else None
149
 
150
- prompt = build_checklist_prompt(
151
  user_goal=user_goal,
152
  theme=chosen_theme,
153
  style=style,
154
  constraints=constraints,
155
  )
 
156
 
157
- # Generation parameters: conservative to avoid rambling
158
  out = gen_pipe(
159
  prompt,
160
- max_new_tokens=450,
161
  do_sample=True,
162
- temperature=0.7,
163
  top_p=0.9,
164
- repetition_penalty=1.05,
165
- return_full_text=False, # критично, чтобы не возвращался промпт целиком
166
  )
167
  text = (out[0].get("generated_text") or "").strip()
168
-
169
- text = re.sub(r"<extra_id_\d+>", "", text).strip()
170
-
171
- # Если модель вдруг не вывела чекбоксы — принудительно форматируем строки
172
- if "- [ ]" not in text:
173
- lines = [ln.strip("-• ").strip() for ln in text.splitlines() if ln.strip()]
174
- lines = [f"- [ ] {ln}" for ln in lines[:15]]
175
- text = "\n".join(lines)
176
-
177
- # Если модель вернула пусто/слишком коротко — повторим с более "толкающими" параметрами
178
- if len(text) < 80:
179
- short_prompt = (
180
- "Сделай практичный чек-лист на русском.\n"
181
- "Формат строго:\n"
182
- "- [ ] пункт\n"
183
- "Подпункты: ' - ...'\n"
184
- "В конце: 'Проверка готовности' (3–5 вопросов) и 'Риски и как снизить'.\n\n"
185
  f"Цель: {user_goal}\n"
186
  f"Контекст: {constraints}\n"
187
  f"Тема: {chosen_theme}\n\n"
188
  "Чек-лист:\n"
189
  )
 
190
  out2 = gen_pipe(
191
- short_prompt,
192
- max_new_tokens=500,
193
  do_sample=True,
194
- temperature=0.9,
195
- top_p=0.9,
 
 
196
  )
197
  text2 = (out2[0].get("generated_text") or "").strip()
198
- if len(text2) > len(text):
 
199
  text = text2
200
 
201
- # Если всё равно пусто — покажем явное сообщение (а не “как будто ничего нет”)
202
- if len(text) < 20:
 
 
 
203
  text = (
204
- "- [ ] Не удалось сгенерировать чек-лист этой моделью.\n"
205
- "- [ ] Попробуйте сменить GEN_MODEL (см. ниже) или сократить описание цели.\n"
206
  )
207
 
208
- # Store in state: checklist text + theme + original goal
209
  meta = {
210
  "goal": user_goal,
211
  "theme": chosen_theme,
@@ -216,16 +301,10 @@ def generate_checklist(user_goal: str, category: str, style: str, constraints: s
216
  "qa_model": qa_model_used,
217
  }
218
 
219
- # A small header for UX
220
- header = []
221
- header.append(f"**Цель:** {user_goal}")
222
- if chosen_theme:
223
- header.append(f"**Тема:** {chosen_theme}")
224
- header.append(f"**Модели:** intent=`{intent_model_used}`, gen=`{gen_model_used}`, qa=`{qa_model_used}`")
225
- header.append("")
226
- checklist_md = "\n".join(header) + text
227
 
228
- return checklist_md, intent_debug, chosen_theme or "", checklist_md, meta
229
 
230
 
231
  def answer_question(question: str, checklist_state: str, meta_state: dict | None):
@@ -234,61 +313,66 @@ def answer_question(question: str, checklist_state: str, meta_state: dict | None
234
  return "Сначала сгенерируйте чек-лист на первой вкладке.", ""
235
 
236
  if not question:
237
- return "Введите вопрос (например: 'Какие документы подготовить?').", ""
238
 
239
- # Use extractive QA first
240
- context = checklist_state
241
- qa_res = qa_pipe(question=question, context=context)
242
  answer = (qa_res.get("answer") or "").strip()
243
  score = float(qa_res.get("score") or 0.0)
244
 
245
  evidence = f"QA score: {score:.3f}\n"
246
  if answer:
247
- evidence += f"Extracted span: {answer}\n"
248
 
249
- # If QA is weak or empty -> fallback to generator (still transformer #2, already loaded)
250
  if (not answer) or score < 0.20 or len(answer) < 3:
251
  goal = (meta_state or {}).get("goal", "")
252
  theme = (meta_state or {}).get("theme", "")
253
 
254
- prompt = (
255
- "Ты помощник по уточняющим вопросам к чек-листу.\n"
256
- "Ответь кратко и практично. Ссылайся на пункты чек-листа (если можно).\n"
257
- "Если в чек-листе этого нет — предложи, какими 2–5 пунктами его дополнить.\n"
258
- "Пиши по-русски.\n\n"
259
  f"Цель: {goal}\n"
260
  f"Тема: {theme}\n\n"
261
  f"Чек-лист:\n{checklist_state}\n\n"
262
  f"Вопрос: {question}\n"
263
  "Ответ:\n"
264
  )
265
- gen_out = gen_pipe(prompt, max_new_tokens=220, do_sample=False)[0]["generated_text"].strip()
 
 
 
 
 
 
 
266
  return gen_out, evidence + "Fallback: generator used (QA confidence low)."
267
 
268
- # Otherwise return extracted answer with a bit of framing
269
- final = f"{answer}\n\n_(Найдено в чек-листе; уверенность: {score:.2f})_"
270
- return final, evidence
271
 
272
 
273
- # ----------------------------
274
- # Gradio UI
275
- # ----------------------------
 
276
  with gr.Blocks(title="Умный чек-лист (3 Transformers)") as demo:
277
  gr.Markdown(
278
  "# ✅ Умный чек-лист (3 Transformers)\n"
279
- "1) Распознаём намерение (zero-shot) → 2) Генерируем чек-лист → 3) Отвечаем на вопросы по чек-листу\n"
280
  )
281
 
282
- checklist_state = gr.State(value=None) # stores checklist markdown
283
- meta_state = gr.State(value=None) # stores dict
284
 
285
  with gr.Tab("1) Создать чек-лист"):
 
286
  with gr.Row():
287
- with gr.Column(scale=2):
288
  user_goal = gr.Textbox(
289
  label="Опишите, что вы хотите сделать",
290
- placeholder="Например: 'Хочу переехать в другой город за 2 месяца и не забыть важное'",
291
- lines=3,
292
  )
293
 
294
  category = gr.Dropdown(
@@ -305,30 +389,51 @@ with gr.Blocks(title="Умный чек-лист (3 Transformers)") as demo:
305
 
306
  constraints = gr.Textbox(
307
  label="Контекст/ограничения (необязательно)",
308
- placeholder="Напр.: бюджет, срок, страна/город, семейное положение, уровень опыта...",
309
- lines=2,
310
  )
311
 
312
  gen_btn = gr.Button("Сгенерировать чек-лист", variant="primary")
313
 
314
- with gr.Column(scale=3):
315
- checklist_out = gr.Code(label="Чек-лист", language="markdown")
316
- intent_debug = gr.Textbox(label="Диагностика распознавания намерения", lines=10)
 
 
 
 
 
 
 
 
 
 
 
 
 
317
 
318
- theme_out = gr.Textbox(label="Выбранная/распознанная тема (если определилась)", interactive=False)
 
319
 
320
  gen_btn.click(
321
  fn=generate_checklist,
322
  inputs=[user_goal, category, style, constraints],
323
- outputs=[checklist_out, intent_debug, theme_out, checklist_state, meta_state],
324
  )
325
 
326
  with gr.Tab("2) Уточняющие вопросы по чек-листу"):
327
- gr.Markdown("Задайте вопрос по уже сгенерированному чек-листу (например: *'Какие документы подготовить?'*).")
328
- question = gr.Textbox(label="Ваш вопрос", placeholder="Введите вопрос...", lines=2)
329
- ask_btn = gr.Button("Ответить", variant="primary")
330
- answer_out = gr.Markdown(label="Ответ")
331
- evidence_out = gr.Textbox(label="Тех. детали (score и режим ответа)", lines=6)
 
 
 
 
 
 
 
332
 
333
  ask_btn.click(
334
  fn=answer_question,
@@ -336,11 +441,5 @@ with gr.Blocks(title="Умный чек-лист (3 Transformers)") as demo:
336
  outputs=[answer_out, evidence_out],
337
  )
338
 
339
- gr.Markdown(
340
- "### Примечания\n"
341
- "- Режим **QA** сначала пытается извлечь ответ прямо из чек-листа.\n"
342
- "- Если уверенность низкая, включается генератор и предлагает уточнение/дополнение чек-листа.\n"
343
- )
344
-
345
  if __name__ == "__main__":
346
  demo.launch()
 
1
+ # app.py
2
  import os
3
  import re
4
  import gradio as gr
5
  import torch
6
+ from transformers import pipeline, AutoTokenizer
7
 
8
+ # ============================
9
+ # 3 Transformers (pipelines)
10
+ # ============================
11
  # 1) Intent / zero-shot
12
  DEFAULT_INTENT_MODEL = os.getenv("INTENT_MODEL", "joeddav/xlm-roberta-large-xnli")
13
 
14
+ # 2) Checklist generator (instruct)
15
  DEFAULT_GEN_MODEL = os.getenv("GEN_MODEL", "Qwen/Qwen2.5-0.5B-Instruct")
16
 
17
  # 3) QA over checklist
 
21
 
22
 
23
  def safe_make_pipeline(task: str, model_name: str, **kwargs):
24
+ """Load pipeline with small fallbacks if needed."""
 
 
 
25
  try:
26
  return pipeline(task, model=model_name, device=DEVICE, **kwargs), model_name
27
+ except Exception:
 
28
  if task == "zero-shot-classification":
29
  fallback = "facebook/bart-large-mnli"
30
+ elif task == "text-generation":
31
+ fallback = "TinyLlama/TinyLlama-1.1B-Chat-v1.0"
32
  elif task == "question-answering":
33
  fallback = "distilbert-base-cased-distilled-squad"
34
  else:
35
+ raise
36
+ return pipeline(task, model=fallback, device=DEVICE, **kwargs), fallback
37
 
 
 
38
 
 
 
39
  intent_pipe, intent_model_used = safe_make_pipeline(
40
  "zero-shot-classification",
41
  DEFAULT_INTENT_MODEL,
 
49
  DEFAULT_QA_MODEL,
50
  )
51
 
52
+ # For Qwen chat formatting (works for many instruct LMs too)
53
+ try:
54
+ gen_tokenizer = AutoTokenizer.from_pretrained(gen_model_used, use_fast=True)
55
+ except Exception:
56
+ gen_tokenizer = None
57
 
58
+ # ============================
59
+ # Labels / UI choices
60
+ # ============================
61
  DEFAULT_LABELS = [
62
  "обучение",
63
  "переезд",
64
+ "релокация/иммиграция",
65
  "путешествие",
66
  "карьера/поиск работы",
67
  "финансы/покупка",
68
  "здоровье/фитнес",
69
  "ремонт/быт",
 
70
  ]
71
 
72
  CATEGORY_CHOICES = ["Авто (определить по тексту)"] + DEFAULT_LABELS
 
79
 
80
 
81
  def infer_intent(user_goal: str, labels: list[str]):
 
 
 
82
  if not user_goal:
83
  return "не задано", 0.0, "Нет входного текста."
84
 
 
85
  result = intent_pipe(user_goal, candidate_labels=labels, multi_label=False)
 
86
  top_label = result["labels"][0]
87
  top_score = float(result["scores"][0])
88
 
 
92
  return top_label, top_score, "\n".join(lines)
93
 
94
 
95
+ def build_checklist_prompt(user_goal: str, theme: str | None, style: str, constraints: str) -> str:
96
  theme_part = f"Тема: {theme}\n" if theme else ""
97
  constraints_part = f"Контекст: {constraints}\n" if constraints else ""
98
 
99
+ style_hint = {
100
+ "кратко": "короткие пункты без лишних слов",
101
+ "подробно": "чуть более подробные пункты + подпункты где уместно",
102
+ "с акцентом на риски": "сильный акцент на предотвращение ошибок и рисков",
103
+ "с акцентом на сроки": "добавляй дедлайны/временные окна там, где уместно",
104
+ }.get(style, "короткие пункты")
105
+
106
  return (
107
+ "Составь практичный чек-лист на русском языке.\n"
108
  "Верни ТОЛЬКО чек-лист без вступлений.\n"
109
+ "Запрещено:\n"
110
+ "- плейсхолдеры и шаблоны: никаких '[секунды]', '[какой-то текст]', '{...}', '<...>'\n"
111
+ "- таблицы, 'Расчёт', 'Банк', 'Сообщение', поля для заполнения\n"
112
+ "Формат ��трого:\n"
113
  "- [ ] пункт\n"
114
  " - подпункт (если нужно)\n"
115
  "Требования:\n"
116
+ f"- стиль: {style_hint}\n"
117
+ "- 12–18 пунктов\n"
118
+ "- без нумерации (никаких '1.')\n"
119
+ "- пункты конкретные и выполнимые\n"
120
+ "- в конце 2 блока:\n"
121
+ "Проверка готовности:\n"
122
+ "- ... (3–5 вопросов)\n"
123
+ "Риски и как снизить:\n"
124
+ "- ... (3–6 пунктов)\n\n"
125
  f"{theme_part}"
126
  f"{constraints_part}"
127
  f"Цель: {user_goal}\n\n"
 
129
  )
130
 
131
 
132
+ def apply_chat_template_if_available(user_prompt: str) -> str:
133
+ """Wrap prompt into chat template if tokenizer supports it (Qwen works best with this)."""
134
+ if gen_tokenizer is None or not hasattr(gen_tokenizer, "apply_chat_template"):
135
+ return user_prompt
136
+ messages = [
137
+ {"role": "system", "content": "Ты пишешь грамотно по-русски и строго соблюдаешь формат чек-листа."},
138
+ {"role": "user", "content": user_prompt},
139
+ ]
140
+ return gen_tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
141
+
142
+
143
+ def clean_checklist(text: str) -> str:
144
+ text = (text or "").strip()
145
+ # Remove T5-style tokens (just in case)
146
+ text = re.sub(r"<extra_id_\d+>", "", text).strip()
147
+
148
+ cleaned = []
149
+ for ln in text.splitlines():
150
+ s = ln.rstrip()
151
+
152
+ if not s.strip():
153
+ continue
154
+
155
+ # Cut obvious "form-like" garbage blocks
156
+ low = s.strip().lower()
157
+ if low.startswith(("расч", "банк", "сообщение", "время ", "план поездки", "расход")):
158
+ continue
159
+
160
+ # Remove square-bracket placeholders but keep "[ ]" checkbox
161
+ s2 = re.sub(r"\[(?!\s*\])([^\]]+)\]", "", s).strip()
162
+
163
+ # Normalize bullets
164
+ s2 = s2.replace("•", "-").replace("–", "-")
165
+
166
+ # Keep only checklist or the two required blocks
167
+ if s2.startswith("- [ ]") or s2.startswith(" -") or s2.startswith("Проверка готовности") or s2.startswith("Риски"):
168
+ cleaned.append(s2)
169
+
170
+ out = "\n".join(cleaned).strip()
171
+
172
+ # If model didn't follow checkbox format, force it
173
+ if "- [ ]" not in out:
174
+ raw = [l.strip() for l in text.splitlines() if l.strip()]
175
+ raw = [re.sub(r"^\d+[\).\s]+", "", r).strip(" -•\t") for r in raw]
176
+ raw = [r for r in raw if r]
177
+ raw = [f"- [ ] {r}" for r in raw[:18]]
178
+ out = "\n".join(raw).strip()
179
+
180
+ return out
181
+
182
+
183
+ def polish_russian_same_model(checklist_text: str) -> str:
184
+ """Second pass with SAME generator (still transformer #2) to polish grammar."""
185
+ if not checklist_text or "- [ ]" not in checklist_text:
186
+ return checklist_text
187
+
188
+ polish_prompt = (
189
+ "Исправь орфографию, грамматику и стилистику русского языка.\n"
190
+ "НИЧЕГО НЕ ДОБАВЛЯЙ и не меняй смысл.\n"
191
+ "Сохрани формат чек-листа:\n"
192
+ "- [ ] пункт\n"
193
+ " - подпункт\n"
194
+ "А также блоки 'Проверка готовности' и 'Риски и как снизить'.\n\n"
195
+ f"{checklist_text}\n"
196
+ )
197
+ polish_prompt = apply_chat_template_if_available(polish_prompt)
198
+
199
+ outp = gen_pipe(
200
+ polish_prompt,
201
+ max_new_tokens=260,
202
+ do_sample=False,
203
+ return_full_text=False,
204
+ )
205
+ text2 = (outp[0].get("generated_text") or "").strip()
206
+ text2 = clean_checklist(text2)
207
+
208
+ # keep only if not worse
209
+ if text2.count("- [ ]") >= checklist_text.count("- [ ]"):
210
+ return text2
211
+ return checklist_text
212
+
213
+
214
  def generate_checklist(user_goal: str, category: str, style: str, constraints: str):
215
  user_goal = normalize_text(user_goal)
216
  constraints = normalize_text(constraints)
217
 
218
  if not user_goal:
219
  return (
 
220
  "",
221
+ "Введите описание цели.",
222
  "",
223
+ f"intent={intent_model_used}\ngen={gen_model_used}\nqa={qa_model_used}",
224
  None,
225
  None,
226
  )
227
 
228
+ inferred_label, inferred_score, intent_debug = infer_intent(user_goal, DEFAULT_LABELS)
 
 
 
229
 
 
230
  if category and category != "Авто (определить по тексту)":
231
  chosen_theme = category
232
  else:
233
+ chosen_theme = inferred_label if inferred_score >= 0.30 else None
 
234
 
235
+ base_prompt = build_checklist_prompt(
236
  user_goal=user_goal,
237
  theme=chosen_theme,
238
  style=style,
239
  constraints=constraints,
240
  )
241
+ prompt = apply_chat_template_if_available(base_prompt)
242
 
243
+ # Generation (primary)
244
  out = gen_pipe(
245
  prompt,
246
+ max_new_tokens=520,
247
  do_sample=True,
248
+ temperature=0.6,
249
  top_p=0.9,
250
+ repetition_penalty=1.08,
251
+ return_full_text=False,
252
  )
253
  text = (out[0].get("generated_text") or "").strip()
254
+ text = clean_checklist(text)
255
+
256
+ # Retry if too short/poor format
257
+ if text.count("- [ ]") < 8:
258
+ retry_prompt = (
259
+ "Сделай чек-лист строго в формате '- [ ] ...' (12–18 пунктов) на русском.\n"
260
+ "Без плейсхолдеров в квадратных скобках. Без таблиц. Без 'Расчёт/Банк/Сообщение'.\n"
261
+ конце добавь:\n"
262
+ "Проверка готовности: (3–5 вопросов)\n"
263
+ "Риски и как снизить: (3–6 пунктов)\n\n"
 
 
 
 
 
 
 
264
  f"Цель: {user_goal}\n"
265
  f"Контекст: {constraints}\n"
266
  f"Тема: {chosen_theme}\n\n"
267
  "Чек-лист:\n"
268
  )
269
+ retry_prompt = apply_chat_template_if_available(retry_prompt)
270
  out2 = gen_pipe(
271
+ retry_prompt,
272
+ max_new_tokens=620,
273
  do_sample=True,
274
+ temperature=0.75,
275
+ top_p=0.92,
276
+ repetition_penalty=1.10,
277
+ return_full_text=False,
278
  )
279
  text2 = (out2[0].get("generated_text") or "").strip()
280
+ text2 = clean_checklist(text2)
281
+ if text2.count("- [ ]") > text.count("- [ ]"):
282
  text = text2
283
 
284
+ # Polish Russian (still same generator = transformer #2)
285
+ text = polish_russian_same_model(text)
286
+
287
+ # Final guard
288
+ if text.count("- [ ]") < 6:
289
  text = (
290
+ "- [ ] Не удалось корректно сформировать чек-лист.\n"
291
+ "- [ ] Попробуйте уточнить цель (страна/срок/статус визы/бюджет) и нажать ещё раз.\n"
292
  )
293
 
 
294
  meta = {
295
  "goal": user_goal,
296
  "theme": chosen_theme,
 
301
  "qa_model": qa_model_used,
302
  }
303
 
304
+ models_info = f"intent={intent_model_used}\ngen={gen_model_used}\nqa={qa_model_used}"
305
+ theme_info = f"{chosen_theme or '(не определилась)'}"
 
 
 
 
 
 
306
 
307
+ return text, intent_debug, theme_info, models_info, text, meta
308
 
309
 
310
  def answer_question(question: str, checklist_state: str, meta_state: dict | None):
 
313
  return "Сначала сгенерируйте чек-лист на первой вкладке.", ""
314
 
315
  if not question:
316
+ return "Введите вопрос.", ""
317
 
318
+ # Extractive QA first
319
+ qa_res = qa_pipe(question=question, context=checklist_state)
 
320
  answer = (qa_res.get("answer") or "").strip()
321
  score = float(qa_res.get("score") or 0.0)
322
 
323
  evidence = f"QA score: {score:.3f}\n"
324
  if answer:
325
+ evidence += f"Span: {answer}\n"
326
 
327
+ # If QA weak -> use generator (still transformer #2)
328
  if (not answer) or score < 0.20 or len(answer) < 3:
329
  goal = (meta_state or {}).get("goal", "")
330
  theme = (meta_state or {}).get("theme", "")
331
 
332
+ user_prompt = (
333
+ "Ответь на вопрос по чек-листу. Пиши по-русски, кратко и практично.\n"
334
+ "Если ответа нет в чек-листе предложи 3–6 дополнительных пунктов, которые стоит добавить.\n"
335
+ "Не используй плейсхолдеры в квадратных скобках.\n\n"
 
336
  f"Цель: {goal}\n"
337
  f"Тема: {theme}\n\n"
338
  f"Чек-лист:\n{checklist_state}\n\n"
339
  f"Вопрос: {question}\n"
340
  "Ответ:\n"
341
  )
342
+ prompt = apply_chat_template_if_available(user_prompt)
343
+ gen_out = gen_pipe(
344
+ prompt,
345
+ max_new_tokens=260,
346
+ do_sample=False,
347
+ return_full_text=False,
348
+ )[0]["generated_text"].strip()
349
+ gen_out = re.sub(r"<extra_id_\d+>", "", gen_out).strip()
350
  return gen_out, evidence + "Fallback: generator used (QA confidence low)."
351
 
352
+ return f"{answer}\n\n_(Найдено в чек-листе; уверенность: {score:.2f})_", evidence
 
 
353
 
354
 
355
+ # ============================
356
+ # UI: Checklist big at bottom,
357
+ # helper info on the right
358
+ # ============================
359
  with gr.Blocks(title="Умный чек-лист (3 Transformers)") as demo:
360
  gr.Markdown(
361
  "# ✅ Умный чек-лист (3 Transformers)\n"
362
+ "1) распознаём намерение → 2) генерируем чек-лист → 3) отвечаем на вопросы по чек-листу\n"
363
  )
364
 
365
+ checklist_state = gr.State(value=None)
366
+ meta_state = gr.State(value=None)
367
 
368
  with gr.Tab("1) Создать чек-лист"):
369
+ # Top row: inputs left, helper mini-blocks right
370
  with gr.Row():
371
+ with gr.Column(scale=3):
372
  user_goal = gr.Textbox(
373
  label="Опишите, что вы хотите сделать",
374
+ placeholder="Напр.: Хочу переехать в Китай на 2 года. Бюджет 200 000 ₽. Есть загранпаспорт.",
375
+ lines=4,
376
  )
377
 
378
  category = gr.Dropdown(
 
389
 
390
  constraints = gr.Textbox(
391
  label="Контекст/ограничения (необязательно)",
392
+ placeholder="Напр.: бюджет, срок, город/страна, семья/дети, удалённая работа, уровень языка...",
393
+ lines=3,
394
  )
395
 
396
  gen_btn = gr.Button("Сгенерировать чек-лист", variant="primary")
397
 
398
+ with gr.Column(scale=2):
399
+ theme_box = gr.Textbox(
400
+ label="Выбранная/распознанная тема",
401
+ lines=2,
402
+ interactive=False,
403
+ )
404
+ models_box = gr.Textbox(
405
+ label="Модели",
406
+ lines=3,
407
+ interactive=False,
408
+ )
409
+ intent_debug = gr.Textbox(
410
+ label="Диагностика намерения",
411
+ lines=10,
412
+ interactive=False,
413
+ )
414
 
415
+ # Big checklist output BELOW (full width)
416
+ checklist_out = gr.Code(label="Чек-лист", language="markdown")
417
 
418
  gen_btn.click(
419
  fn=generate_checklist,
420
  inputs=[user_goal, category, style, constraints],
421
+ outputs=[checklist_out, intent_debug, theme_box, models_box, checklist_state, meta_state],
422
  )
423
 
424
  with gr.Tab("2) Уточняющие вопросы по чек-листу"):
425
+ with gr.Row():
426
+ with gr.Column(scale=3):
427
+ gr.Markdown("Задайте вопрос по уже сгенерированному чек-листу.")
428
+ question = gr.Textbox(
429
+ label="Ваш вопрос",
430
+ placeholder="Напр.: Какие документы подготовить? Как снизить расходы? Что сделать первым делом?",
431
+ lines=2,
432
+ )
433
+ ask_btn = gr.Button("Ответить", variant="primary")
434
+ answer_out = gr.Markdown(label="Ответ")
435
+ with gr.Column(scale=2):
436
+ evidence_out = gr.Textbox(label="Тех. детали", lines=10)
437
 
438
  ask_btn.click(
439
  fn=answer_question,
 
441
  outputs=[answer_out, evidence_out],
442
  )
443
 
 
 
 
 
 
 
444
  if __name__ == "__main__":
445
  demo.launch()