Nikolay Ponomarev commited on
Commit
e521542
·
1 Parent(s): 8fe4785

Item Search

Browse files
Files changed (2) hide show
  1. app.py +266 -380
  2. requirements.txt +4 -6
app.py CHANGED
@@ -1,419 +1,305 @@
 
1
  import re
2
- from typing import Dict, List, Tuple, Any, Optional
3
-
4
  import gradio as gr
5
- import numpy as np
6
-
7
- from sentence_transformers import SentenceTransformer
8
- from transformers import pipeline, AutoTokenizer
9
-
10
-
11
- # =========================
12
- # Models (3 transformers)
13
- # =========================
14
- EMB_MODEL_NAME = "intfloat/multilingual-e5-small" # embeddings retrieval
15
- ZSHOT_MODEL_NAME = "MoritzLaurer/multilingual-MiniLMv2-L6-mnli-xnli" # zero-shot domain classifier
16
- QA_MODEL_NAME = "timpal0l/mdeberta-v3-base-squad2" # extractive QA
17
-
18
-
19
- _emb_model: Optional[SentenceTransformer] = None
20
- _zshot_pipe = None
21
- _zshot_tok: Optional[AutoTokenizer] = None
22
- _qa_pipe = None
23
-
24
-
25
- def get_emb_model() -> SentenceTransformer:
26
- global _emb_model
27
- if _emb_model is None:
28
- _emb_model = SentenceTransformer(EMB_MODEL_NAME)
29
- return _emb_model
30
-
31
-
32
- def get_zshot():
33
- global _zshot_pipe, _zshot_tok
34
- if _zshot_pipe is None:
35
- _zshot_pipe = pipeline("zero-shot-classification", model=ZSHOT_MODEL_NAME)
36
- _zshot_tok = AutoTokenizer.from_pretrained(ZSHOT_MODEL_NAME)
37
- return _zshot_pipe, _zshot_tok
38
-
39
-
40
- def get_qa():
41
- global _qa_pipe
42
- if _qa_pipe is None:
43
- _qa_pipe = pipeline("question-answering", model=QA_MODEL_NAME, tokenizer=QA_MODEL_NAME)
44
- return _qa_pipe
45
-
46
-
47
- # =========================
48
- # Knowledge base (small built-in)
49
- # Each entry is a "tip card" retrievable by embeddings.
50
- # =========================
51
- KB: List[Dict[str, str]] = [
52
- # Moving
53
- {"domain": "Переезд", "phase": "Подготовка", "title": "Инвентаризация вещей",
54
- "text": "Раздели вещи на: оставить / продать / отдать / выбросить. Упаковывай по комнатам, подписывай коробки, сделай список коробок и фото содержимого."},
55
- {"domain": "Переезд", "phase": "Подготовка", "title": "Коммуникации и адрес",
56
- "text": "Заранее обнови адрес доставки, банков, подписок. Проверь интернет на новом месте и запланируй подключение на день переезда."},
57
- {"domain": "Переезд", "phase": "День Х", "title": "Логистика и контроль",
58
- "text": "Собери сумку 'первый день': документы, зарядки, лекарства, вода, перекус, ключи, туалетные принадлежности. Сфотографируй состояние квартиры при выезде/въезде."},
59
-
60
- # Buying (electronics/general)
61
- {"domain": "Покупка", "phase": "Выбор", "title": "Критерии и сценарии",
62
- "text": "Запиши 3–5 сценариев использования (учёба, игры, работа, поездки). На каждый — приоритеты (вес, автономность, экран, производительность, шум)."},
63
- {"domain": "Покупка", "phase": "Проверка", "title": "Проверка перед покупкой",
64
- "text": "Сверь гарантию и условия возврата. Проверь комплектацию. Для техники — осмотр на дефекты, серийный номер, чек/инвойс."},
65
- {"domain": "Покупка", "phase": "После", "title": "Первые 48 часов",
66
- "text": "Сразу протестируй ключевые функции, обнови прошивку/ПО, сохрани упаковку до конца периода возврата."},
67
-
68
- # Study
69
- {"domain": "Учёба", "phase": "Подготовка", "title": "План на неделю",
70
- "text": "Разбей тему на блоки по 25–45 минут. На каждый блок: цель, краткий конспект, 3 вопроса для самопроверки. Запланируй повторение через 1 и 3 дня."},
71
- {"domain": "Учёба", "phase": "Во время", "title": "Активное вспоминание",
72
- "text": "Вместо перечитывания делай: тест, пересказ, карточки, задачи. Ошибки — отдельным списком, к ним возвращайся чаще."},
73
-
74
- # Event
75
- {"domain": "Мероприятие", "phase": "Подготовка", "title": "Список гостей и бюджет",
76
- "text": "Определи: формат (дом/кафе/парк), количество гостей, бюджет на человека, ограничения по еде. Сразу выдели 10–15% на непредвиденное."},
77
- {"domain": "Мероприятие", "phase": "Подготовка", "title": "Тайминг и роли",
78
- "text": "Составь тайминг по слотам (встреча, еда, активность, торт/финал). Назначь ответственных: музыка, фото, закупки, встреча гостей."},
79
-
80
- # Travel
81
- {"domain": "Путешествие", "phase": "Подготовка", "title": "Документы и безопасность",
82
- "text": "Проверь документы, страховку, резервные копии (сканы). Запиши экстренные контакты. Продумай связь и оплату (карта/наличные)."},
83
-
84
- # Home / Repair
85
- {"domain": "Дом/Ремонт", "phase": "Подготовка", "title": "Материалы и замеры",
86
- "text": "Сделай точные замеры, фото, и список материалов с запасом 5–10%. Договорись о вывозе мусора и защите мебели/пола."},
87
- {"domain": "Дом/Ремонт", "phase": "Контроль", "title": "Контроль работ",
88
- "text": "Фиксируй договорённости письменно, согласуй этапы приёмки, снимай фото прогресса. Оплата — по этапам после проверки качества."},
89
-
90
- # Finance (non-med)
91
- {"domain": "Финансы", "phase": "Подготовка", "title": "Разбор расходов",
92
- "text": "Раздели траты на обязательные и переменные. Найди 3 быстрых оптимизации (подписки, доставка, импульсные покупки). Поставь лимит на категории."},
93
- ]
94
-
95
- DOMAIN_LABELS = [
96
- "Переезд", "Покупка", "Учёба", "Мероприятие", "Путешествие", "Дом/Ремонт", "Финансы",
97
- "Работа/Проекты", "Документы/Бюрократия"
98
  ]
99
 
 
100
 
101
- # =========================
102
- # Helpers
103
- # =========================
104
- def norm(s: str) -> str:
105
- s = (s or "").replace("\x00", "")
106
- s = re.sub(r"[ \t]+", " ", s)
107
- s = re.sub(r"\n{3,}", "\n\n", s)
108
- return s.strip()
109
 
 
 
 
 
110
 
111
- def truncate_for_zshot(text: str, max_tokens: int = 320) -> str:
112
- _, tok = get_zshot()
113
- assert tok is not None
114
- enc = tok(text, truncation=True, max_length=max_tokens, add_special_tokens=False, return_tensors=None)
115
- return tok.decode(enc["input_ids"], skip_special_tokens=True)
116
 
 
 
 
 
 
 
117
 
118
- def cosine(a: np.ndarray, b: np.ndarray) -> float:
119
- a = a.astype(np.float32)
120
- b = b.astype(np.float32)
121
- na = float(np.linalg.norm(a) + 1e-9)
122
- nb = float(np.linalg.norm(b) + 1e-9)
123
- return float(np.dot(a / na, b / nb))
124
 
 
 
 
 
125
 
126
- def classify_domain(task_text: str) -> Tuple[str, float]:
127
- zshot, _ = get_zshot()
128
- t = truncate_for_zshot(task_text, max_tokens=320)
129
- res = zshot(
130
- t,
131
- candidate_labels=DOMAIN_LABELS,
132
- hypothesis_template="Эта задача относится к категории {}.",
133
- multi_label=False,
134
- )
135
- return res["labels"][0], float(res["scores"][0])
136
-
137
-
138
- def build_kb_index() -> Tuple[List[str], np.ndarray]:
139
- """Return (kb_texts, kb_embs)."""
140
- emb_model = get_emb_model()
141
- kb_texts = []
142
- for e in KB:
143
- kb_texts.append(f"{e['domain']} | {e['phase']} | {e['title']}. {e['text']}")
144
- kb_embs = emb_model.encode(["passage: " + t for t in kb_texts], show_progress_bar=False)
145
- return kb_texts, kb_embs
146
-
147
-
148
- _KB_TEXTS: Optional[List[str]] = None
149
- _KB_EMBS: Optional[np.ndarray] = None
150
-
151
-
152
- def get_kb_cache() -> Tuple[List[str], np.ndarray]:
153
- global _KB_TEXTS, _KB_EMBS
154
- if _KB_TEXTS is None or _KB_EMBS is None:
155
- _KB_TEXTS, _KB_EMBS = build_kb_index()
156
- return _KB_TEXTS, _KB_EMBS
157
-
158
-
159
- def retrieve_kb(task_text: str, domain_hint: str, topk: int = 10) -> List[Tuple[int, float]]:
160
- kb_texts, kb_embs = get_kb_cache()
161
- emb_model = get_emb_model()
162
- q = f"{domain_hint}. {task_text}".strip()
163
- q_emb = emb_model.encode(["query: " + q], show_progress_bar=False)[0]
164
- sims = [(i, cosine(q_emb, kb_embs[i])) for i in range(len(kb_texts))]
165
- sims.sort(key=lambda x: x[1], reverse=True)
166
- return sims[:topk]
167
-
168
-
169
- def missing_info(budget: str, deadline: str, location: str, people: str) -> List[str]:
170
- out = []
171
- if not norm(budget):
172
- out.append("Бюджет (пример: 300€, 15000₽, 'до 1000').")
173
- if not norm(deadline):
174
- out.append("Сроки/дедлайн (пример: 'за 2 недели', 'до 10 января').")
175
- if not norm(location):
176
- out.append("Город/контекст (если влияет: доставка, услуги, путешествия).")
177
- if not norm(people):
178
- out.append("Кто участвует (один/семья/дети/команда) и сколько людей.")
179
- return out
180
-
181
-
182
- def format_constraints(budget: str, deadline: str, location: str, people: str) -> str:
183
- parts = []
184
- if norm(budget): parts.append(f"- **Бюджет:** {norm(budget)}")
185
- if norm(deadline): parts.append(f"- **Сроки:** {norm(deadline)}")
186
- if norm(location): parts.append(f"- **Локация:** {norm(location)}")
187
- if norm(people): parts.append(f"- **Участники:** {norm(people)}")
188
- return "\n".join(parts) if parts else "_Ограничения не указаны._"
189
-
190
-
191
- def make_checklist_markdown(
192
- task_text: str,
193
- domain: str,
194
- domain_conf: float,
195
- budget: str,
196
- deadline: str,
197
- location: str,
198
- people: str,
199
- topk: int,
200
- ) -> Tuple[str, Dict[str, Any]]:
201
- task_text = norm(task_text)
202
- if not task_text:
203
- return "❗ Опишите задачу одним абзацем.", {}
204
-
205
- # retrieve KB tips
206
- picks = retrieve_kb(task_text, domain, topk=topk)
207
-
208
- # Group by phase (based on KB entry order/metadata)
209
- by_phase: Dict[str, List[Dict[str, str]]] = {}
210
- for idx, sim in picks:
211
- e = KB[idx]
212
- item = {
213
- "title": e["title"],
214
- "text": e["text"],
215
- "domain": e["domain"],
216
- "phase": e["phase"],
217
- "sim": f"{sim:.3f}",
218
- }
219
- by_phase.setdefault(e["phase"], []).append(item)
220
-
221
- # Ensure stable phase order
222
- phase_order = ["Подготовка", "Выбор", "Проверка", "Контроль", "Во время", "День Х", "После"]
223
- phases = sorted(by_phase.keys(), key=lambda p: phase_order.index(p) if p in phase_order else 999)
224
-
225
- miss = missing_info(budget, deadline, location, people)
226
-
227
- md = []
228
- md.append("## Умный чек-лист по задаче")
229
- md.append(f"**Задача:** {task_text}")
230
- md.append("")
231
- md.append(f"**Определённый домен:** `{domain}` (conf `{domain_conf:.3f}`)")
232
- md.append("")
233
- md.append("### Ограничения")
234
- md.append(format_constraints(budget, deadline, location, people))
235
- md.append("")
236
-
237
- if miss:
238
- md.append("### Что уточнить (быстро улучшит план)")
239
- for m in miss:
240
- md.append(f"- {m}")
241
- md.append("")
242
-
243
- md.append("### Чек-лист")
244
- if not phases:
245
- md.append("_Не удалось подобрать пункты. Попробуйте переформулировать задачу._")
246
- else:
247
- for ph in phases:
248
- md.append(f"#### {ph}")
249
- for j, it in enumerate(by_phase[ph], 1):
250
- md.append(f"**{j}. {it['title']}**")
251
- md.append(f"- {it['text']}")
252
- md.append(f"- _(релевантность: {it['sim']})_")
253
- md.append("")
254
-
255
- md.append("---")
256
- md.append("**Модели:**")
257
- md.append(f"- Zero-shot: `{ZSHOT_MODEL_NAME}`")
258
- md.append(f"- Embeddings: `{EMB_MODEL_NAME}`")
259
- md.append(f"- QA: `{QA_MODEL_NAME}`")
260
-
261
- state = {
262
- "task": task_text,
263
- "domain": domain,
264
- "domain_conf": domain_conf,
265
- "constraints": {
266
- "budget": norm(budget),
267
- "deadline": norm(deadline),
268
- "location": norm(location),
269
- "people": norm(people),
270
- },
271
- "kb_picks": picks, # indices and sims
272
- "plan_md": "\n".join(md).strip()
273
- }
274
- return "\n".join(md).strip(), state
275
 
 
 
 
 
 
 
276
 
277
- def qa_on_plan(question: str, plan_state: Dict[str, Any], extra_topk: int = 6) -> str:
278
- q = norm(question)
279
- if not q:
280
- return " Введите вопрос."
281
- if not plan_state or not plan_state.get("task"):
282
- return " Сначала сгенерируйте чек-лист (вкладка Generate)."
 
 
 
 
 
 
 
 
 
 
 
283
 
284
- # Build context from: plan markdown + top KB cards (for evidence)
285
- domain = plan_state.get("domain", "")
286
- task = plan_state.get("task", "")
287
 
288
- # Retrieve a few extra KB items directly for the question
289
- picks = retrieve_kb(f"{task}\nВопрос: {q}", domain, topk=extra_topk)
 
290
 
291
- context_parts = []
292
- context_parts.append("=== PLAN ===\n" + (plan_state.get("plan_md", "")[:3200]))
 
 
 
 
 
 
293
 
294
- context_parts.append("\n=== RELEVANT TIPS ===")
295
- for idx, sim in picks:
296
- e = KB[idx]
297
- context_parts.append(f"[{e['domain']} | {e['phase']} | {e['title']} | sim {sim:.3f}] {e['text']}")
298
 
299
- context = "\n".join(context_parts)
300
- context = context[:5200]
301
 
302
- qa = get_qa()
303
- res = qa(question=q, context=context, topk=5, handle_impossible_answer=True)
304
- cands = res if isinstance(res, list) else [res]
305
- cands.sort(key=lambda r: float(r.get("score", 0.0)), reverse=True)
306
- best = cands[0]
 
 
 
 
 
 
 
 
307
 
308
- ans = (best.get("answer") or "").strip()
309
- score = float(best.get("score") or 0.0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
310
 
311
- if not ans:
312
- return (
313
- "## Q&A\n"
314
- "Ответ не найден в контексте плана/подсказок.\n\n"
315
- "Попробуйте переформулировать вопрос или уточнить ограничения (бюджет/сроки/локация)."
316
- ).strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
 
318
- evidence = context.replace("\n", " ")
319
- if len(evidence) > 900:
320
- evidence = evidence[:900] + "…"
321
 
322
- return (
323
- "## Q&A\n"
324
- f"- **Вопрос:** {q}\n"
325
- f"- **Уверенность:** `{score:.3f}`\n\n"
326
- "### Ответ\n"
327
- f"{ans}\n\n"
328
- "### Evidence (snippet)\n"
329
- f"{evidence}"
330
- ).strip()
331
-
332
-
333
- # =========================
334
- # UI
335
- # =========================
336
- TITLE_HTML = """
337
- <h2>Умный чек-лист по задаче (3 Transformers)</h2>
338
- <p style="color:#6b7280;margin-top:-6px">
339
- Zero-shot → определяем категорию · Embeddings → подбираем пункты · QA → отвечаем на вопросы по плану
340
- </p>
341
- """
342
-
343
- EXAMPLE_TASK = "Хочу организовать день рождения дома для 8 человек, чтобы было весело и без хаоса."
344
- EXAMPLE_BUDGET = "до 150€"
345
- EXAMPLE_DEADLINE = "через 10 дней"
346
- EXAMPLE_LOCATION = "Амстердам"
347
- EXAMPLE_PEOPLE = "8 взрослых, без детей"
348
-
349
- with gr.Blocks() as demo:
350
- gr.HTML(TITLE_HTML)
351
-
352
- plan_state = gr.State({})
353
-
354
- with gr.Tab("Generate"):
355
- task_text = gr.Textbox(
356
- label="Опишите задачу (1–5 предложений)",
357
- lines=4,
358
- value=EXAMPLE_TASK,
359
- placeholder="Например: Хочу переехать в новую квартиру за 2 недели, бюджет ограничен..."
360
- )
361
 
362
- with gr.Row():
363
- budget = gr.Textbox(label="Бюджет (опционально)", value=EXAMPLE_BUDGET, placeholder="Например: до 500€")
364
- deadline = gr.Textbox(label="Сроки/дедлайн (опционально)", value=EXAMPLE_DEADLINE, placeholder="Например: до 10 января")
 
 
 
 
 
365
 
366
- with gr.Row():
367
- location = gr.Textbox(label="Локация (опционально)", value=EXAMPLE_LOCATION, placeholder="Город/страна")
368
- people = gr.Textbox(label="Кто участвует (опционально)", value=EXAMPLE_PEOPLE, placeholder="Один/семья/команда...")
369
 
 
370
  with gr.Row():
371
- topk = gr.Slider(6, 16, value=10, step=1, label="Сколько пунктов подбирать из базы")
372
- gen_btn = gr.Button("Сгенерировать чек-лист", variant="primary")
373
-
374
- report = gr.Markdown()
375
-
376
- def generate(task, budget_, deadline_, location_, people_, topk_):
377
- t = norm(task)
378
- if not t:
379
- return " Опишите задачу.", {}
380
- domain, conf = classify_domain(t)
381
- md, st = make_checklist_markdown(
382
- task_text=t,
383
- domain=domain,
384
- domain_conf=conf,
385
- budget=budget_,
386
- deadline=deadline_,
387
- location=location_,
388
- people=people_,
389
- topk=int(topk_),
390
- )
391
- return md, st
 
 
 
 
 
 
 
 
 
 
 
392
 
393
  gen_btn.click(
394
- generate,
395
- inputs=[task_text, budget, deadline, location, people, topk],
396
- outputs=[report, plan_state],
397
  )
398
 
399
- with gr.Tab("Ask (Q&A)"):
400
- gr.Markdown(
401
- "Задайте вопрос по плану. Примеры:\n"
402
- "- «С чего начать прямо сегодня?»\n"
403
- "- «Какие риски самые вероятные?»\n"
404
- "- «Как уложиться в бюджет?»\n"
405
- "- «Что можно упростить, если мало времени?»"
406
- )
407
- question = gr.Textbox(label="Ваш вопрос", lines=2, placeholder="Например: Как сократить расходы и не потерять качество?")
408
  ask_btn = gr.Button("Ответить", variant="primary")
409
- answer_md = gr.Markdown()
 
410
 
411
- ask_btn.click(qa_on_plan, inputs=[question, plan_state], outputs=[answer_md])
 
 
 
 
412
 
413
  gr.Markdown(
414
- "_Примечание: это инструмент-помощник. Он может ошибаться — используйте как основу и уточняйте детали._"
 
 
415
  )
416
 
417
  if __name__ == "__main__":
418
- demo.queue()
419
  demo.launch()
 
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", "google/mt5-small")
15
+
16
+ # 3) QA over checklist
17
+ DEFAULT_QA_MODEL = os.getenv("QA_MODEL", "deepset/xlm-roberta-base-squad2")
18
+
19
+ 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,
48
+ )
49
+ gen_pipe, gen_model_used = safe_make_pipeline(
50
+ "text2text-generation",
51
+ DEFAULT_GEN_MODEL,
52
+ )
53
+ qa_pipe, qa_model_used = safe_make_pipeline(
54
+ "question-answering",
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
74
 
 
 
 
 
 
 
 
 
75
 
76
+ def normalize_text(s: str) -> str:
77
+ s = (s or "").strip()
78
+ s = re.sub(r"\s+", " ", s)
79
+ return s
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
 
95
+ lines = ["Распознавание намерения (zero-shot):"]
96
+ for lab, sc in zip(result["labels"], result["scores"]):
97
+ lines.append(f"- {lab}: {sc:.3f}")
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
+ """
103
+ Prompt for generator model.
104
+ """
105
+ theme_part = f"Тема (если помогает): {theme}\n" if theme else ""
106
+ constraints_part = f"Ограничения/контекст: {constraints}\n" if constraints else ""
107
 
108
+ # Works for mt5/flan-t5 style models; they respond better to clear structure.
109
+ return (
110
+ "Ты помощник, который делает практичные чек-листы.\n"
111
+ "Сформируй чек-лист так, чтобы обычный пользователь мог выполнить задачу.\n"
112
+ "Требования:\n"
113
+ "- Выведи 8–15 пунктов максимум.\n"
114
+ "- Каждый пункт в формате: '- [ ] ...'\n"
115
+ "- Где уместно, добавляй краткие подпункты (через ' - ...').\n"
116
+ "- Делай пункты измеримыми и конкретными.\n"
117
+ "- В конце добавь блок 'Проверка готовности' (3–5 вопросов) и блок 'Риски и как снизить'.\n"
118
+ "- Пиши по-русски.\n\n"
119
+ f"Стиль: {style}\n"
120
+ f"{theme_part}"
121
+ f"{constraints_part}"
122
+ f"Задача пользователя: {user_goal}\n\n"
123
+ "Чек-лист:\n"
124
+ )
125
 
 
 
 
126
 
127
+ def generate_checklist(user_goal: str, category: str, style: str, constraints: str):
128
+ user_goal = normalize_text(user_goal)
129
+ constraints = normalize_text(constraints)
130
 
131
+ if not user_goal:
132
+ return (
133
+ "Введите описание цели (например: 'Хочу переехать в другой город за 2 месяца').",
134
+ "",
135
+ "",
136
+ None,
137
+ None,
138
+ )
139
 
140
+ # Decide labels for intent detection (we keep it to 8)
141
+ labels = DEFAULT_LABELS
 
 
142
 
143
+ inferred_label, inferred_score, intent_debug = infer_intent(user_goal, labels)
 
144
 
145
+ chosen_theme = None
146
+ if category and category != "Авто (определить по тексту)":
147
+ chosen_theme = category
148
+ else:
149
+ # Use inferred label only if confidence is decent; otherwise keep theme=None
150
+ chosen_theme = inferred_label if inferred_score >= 0.35 else None
151
+
152
+ prompt = build_checklist_prompt(
153
+ user_goal=user_goal,
154
+ theme=chosen_theme,
155
+ style=style,
156
+ constraints=constraints,
157
+ )
158
 
159
+ # Generation parameters: conservative to avoid rambling
160
+ out = gen_pipe(
161
+ prompt,
162
+ max_new_tokens=450,
163
+ do_sample=False,
164
+ )
165
+ text = out[0]["generated_text"].strip()
166
+
167
+ # Store in state: checklist text + theme + original goal
168
+ meta = {
169
+ "goal": user_goal,
170
+ "theme": chosen_theme,
171
+ "intent_label": inferred_label,
172
+ "intent_score": inferred_score,
173
+ "intent_model": intent_model_used,
174
+ "gen_model": gen_model_used,
175
+ "qa_model": qa_model_used,
176
+ }
177
 
178
+ # A small header for UX
179
+ header = []
180
+ header.append(f"**Цель:** {user_goal}")
181
+ if chosen_theme:
182
+ header.append(f"**Тема:** {chosen_theme}")
183
+ header.append(f"**Модели:** intent=`{intent_model_used}`, gen=`{gen_model_used}`, qa=`{qa_model_used}`")
184
+ header.append("")
185
+ checklist_md = "\n".join(header) + text
186
+
187
+ return checklist_md, intent_debug, chosen_theme or "", checklist_md, meta
188
+
189
+
190
+ def answer_question(question: str, checklist_state: str, meta_state: dict | None):
191
+ question = normalize_text(question)
192
+ if not checklist_state:
193
+ return "Сначала сгенерируйте чек-лист на первой вкладке.", ""
194
+
195
+ if not question:
196
+ return "Введите вопрос (например: 'Какие документы подготовить?').", ""
197
+
198
+ # Use extractive QA first
199
+ context = checklist_state
200
+ qa_res = qa_pipe(question=question, context=context)
201
+ answer = (qa_res.get("answer") or "").strip()
202
+ score = float(qa_res.get("score") or 0.0)
203
+
204
+ evidence = f"QA score: {score:.3f}\n"
205
+ if answer:
206
+ evidence += f"Extracted span: {answer}\n"
207
+
208
+ # If QA is weak or empty -> fallback to generator (still transformer #2, already loaded)
209
+ if (not answer) or score < 0.20 or len(answer) < 3:
210
+ goal = (meta_state or {}).get("goal", "")
211
+ theme = (meta_state or {}).get("theme", "")
212
+
213
+ prompt = (
214
+ "Ты — помощник по уточняющим вопросам к чек-листу.\n"
215
+ "Ответь кратко и практично. Ссылайся на пункты чек-листа (если можно).\n"
216
+ "Если в чек-листе этого нет — предложи, какими 2–5 пунктами его дополнить.\n"
217
+ "Пиши по-русски.\n\n"
218
+ f"Цель: {goal}\n"
219
+ f"Тема: {theme}\n\n"
220
+ f"Чек-лист:\n{checklist_state}\n\n"
221
+ f"Вопрос: {question}\n"
222
+ "Ответ:\n"
223
+ )
224
+ gen_out = gen_pipe(prompt, max_new_tokens=220, do_sample=False)[0]["generated_text"].strip()
225
+ return gen_out, evidence + "Fallback: generator used (QA confidence low)."
226
 
227
+ # Otherwise return extracted answer with a bit of framing
228
+ final = f"{answer}\n\n_(Найдено в чек-листе; уверенность: {score:.2f})_"
229
+ return final, evidence
230
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
 
232
+ # ----------------------------
233
+ # Gradio UI
234
+ # ----------------------------
235
+ with gr.Blocks(title="Умный чек-лист (3 Transformers)") as demo:
236
+ gr.Markdown(
237
+ "# ✅ Умный чек-лист (3 Transformers)\n"
238
+ "1) Распознаём намерение (zero-shot) → 2) Генерируем чек-лист → 3) Отвечаем на вопросы по чек-листу\n"
239
+ )
240
 
241
+ checklist_state = gr.State(value=None) # stores checklist markdown
242
+ meta_state = gr.State(value=None) # stores dict
 
243
 
244
+ with gr.Tab("1) Создать чек-лист"):
245
  with gr.Row():
246
+ with gr.Column(scale=2):
247
+ user_goal = gr.Textbox(
248
+ label="Опишите, что вы хотите сделать",
249
+ placeholder="Например: 'Хочу переехать в другой город за 2 месяца и не забыть важное'",
250
+ lines=3,
251
+ )
252
+
253
+ category = gr.Dropdown(
254
+ label="Категория (необязательно)",
255
+ choices=CATEGORY_CHOICES,
256
+ value="Авто (определить по тексту)",
257
+ )
258
+
259
+ style = gr.Dropdown(
260
+ label="Стиль чек-листа",
261
+ choices=["кратко", "подробно", "с акцентом на риски", "с акцентом на сроки"],
262
+ value="кратко",
263
+ )
264
+
265
+ constraints = gr.Textbox(
266
+ label="Контекст/ограничения (необязательно)",
267
+ placeholder="Напр.: бюджет, срок, страна/город, семейное положение, уровень опыта...",
268
+ lines=2,
269
+ )
270
+
271
+ gen_btn = gr.Button("Сгенерировать чек-лист", variant="primary")
272
+
273
+ with gr.Column(scale=3):
274
+ checklist_out = gr.Markdown(label="Чек-лист")
275
+ intent_debug = gr.Textbox(label="Диагностика распознавания намерения", lines=10)
276
+
277
+ theme_out = gr.Textbox(label="Выбранная/распознанная тема (если определилась)", interactive=False)
278
 
279
  gen_btn.click(
280
+ fn=generate_checklist,
281
+ inputs=[user_goal, category, style, constraints],
282
+ outputs=[checklist_out, intent_debug, theme_out, checklist_state, meta_state],
283
  )
284
 
285
+ with gr.Tab("2) Уточняющие вопросы по чек-листу"):
286
+ gr.Markdown("Задайте вопрос по уже сгенерированному чек-листу (например: *'Какие документы подготовить?'*).")
287
+ question = gr.Textbox(label="Ваш вопрос", placeholder="Введите вопрос...", lines=2)
 
 
 
 
 
 
288
  ask_btn = gr.Button("Ответить", variant="primary")
289
+ answer_out = gr.Markdown(label="Ответ")
290
+ evidence_out = gr.Textbox(label="Тех. детали (score и режим ответа)", lines=6)
291
 
292
+ ask_btn.click(
293
+ fn=answer_question,
294
+ inputs=[question, checklist_state, meta_state],
295
+ outputs=[answer_out, evidence_out],
296
+ )
297
 
298
  gr.Markdown(
299
+ "### Примечания\n"
300
+ "- Режим **QA** сначала пытается извлечь ответ прямо из чек-листа.\n"
301
+ "- Если уверенность низкая, включается генератор и предлагает уточнение/дополнение чек-листа.\n"
302
  )
303
 
304
  if __name__ == "__main__":
 
305
  demo.launch()
requirements.txt CHANGED
@@ -1,7 +1,5 @@
1
- gradio>=4.44.0
2
- transformers>=4.43.0
3
- sentence-transformers>=3.0.0
4
  torch
5
- numpy
6
- pandas
7
- scikit-learn
 
1
+ gradio>=4.0.0
2
+ transformers>=4.40.0
 
3
  torch
4
+ accelerate
5
+ sentencepiece