ana-solo commited on
Commit
d218894
·
verified ·
1 Parent(s): 7da0fa8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +434 -30
app.py CHANGED
@@ -52,52 +52,456 @@ system_prompt = """
52
  Ответственную организацию в таблице указывай актуальную для региона, который пользователь указал в запросе
53
  """
54
 
55
- # === Функция получения ответа ===
56
  def get_facts(user_question: str) -> str:
 
 
 
 
57
  try:
58
- if not user_question.strip():
59
  return "Ошибка: пожалуйста, введите ваш запрос."
60
 
 
 
 
 
 
 
 
61
  storage_context = StorageContext.from_defaults(persist_dir="./storage")
62
  index = load_index_from_storage(storage_context)
63
  retriever = index.as_retriever(similarity_top_k=4)
64
  nodes = retriever.retrieve(user_question)
65
 
66
  context = "\n\n".join([node.get_content() for node in nodes]) if nodes else "Не найдено релевантных документов."
67
- full_system_prompt = system_prompt + f"\n\nКонтекст:\n{context}"
68
 
69
  messages = [
70
  ChatMessage(role="system", content=full_system_prompt),
71
  ChatMessage(role="user", content="Пользовательский запрос: " + user_question)
72
  ]
73
-
74
  response = Settings.llm.chat(messages)
75
  return response.message.content
76
 
77
  except Exception as e:
78
- return f"Ошибка:\n{str(e)}"
79
-
80
- response = Settings.llm.chat(messages)
81
- return response.message.content
82
-
83
- with gr.Blocks(theme=gr.themes.Soft()) as demo:
84
- gr.Markdown("# Информационная система для помощи в адаптации к климатическим рискам")
85
-
86
- user_input = gr.Textbox(
87
- label="Ваш запрос",
88
- lines=4,
89
- max_lines = 4,
90
- placeholder="Введите свой запрос"
91
- )
92
-
93
- gr.Markdown("# Ответ")
94
- answer_output = gr.Markdown(height=300)
95
- send_button = gr.Button("Получить ответ")
96
-
97
- send_button.click(
98
- fn=get_facts,
99
- inputs=user_input,
100
- outputs=answer_output
101
- )
102
-
103
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  Ответственную организацию в таблице указывай актуальную для региона, который пользователь указал в запросе
53
  """
54
 
 
55
  def get_facts(user_question: str) -> str:
56
+ """
57
+ Возвращает ответ (строку) по запросу пользователя.
58
+ Пытается использовать llama_index, если доступен; иначе возвращает понятное сообщение.
59
+ """
60
  try:
61
+ if not user_question or not user_question.strip():
62
  return "Ошибка: пожалуйста, введите ваш запрос."
63
 
64
+ if not LLM_AVAILABLE:
65
+ return ("Ошибка при обработке запроса: библиотека llama_index или зависимости не установлены "
66
+ "в окружении сервера. Интерфейс запущен, но LLM-вызовы недоступны.\n\n"
67
+ "Чтобы включить LLM, установите необходимые библиотеки и наполните ./storage индексом. "
68
+ "Подробности в README вашего проекта.")
69
+
70
+ # Если мы здесь — llama_index доступен
71
  storage_context = StorageContext.from_defaults(persist_dir="./storage")
72
  index = load_index_from_storage(storage_context)
73
  retriever = index.as_retriever(similarity_top_k=4)
74
  nodes = retriever.retrieve(user_question)
75
 
76
  context = "\n\n".join([node.get_content() for node in nodes]) if nodes else "Не найдено релевантных документов."
77
+ full_system_prompt = SYSTEM_PROMPT + f"\n\nКонтекст:\n{context}"
78
 
79
  messages = [
80
  ChatMessage(role="system", content=full_system_prompt),
81
  ChatMessage(role="user", content="Пользовательский запрос: " + user_question)
82
  ]
83
+
84
  response = Settings.llm.chat(messages)
85
  return response.message.content
86
 
87
  except Exception as e:
88
+ return f"Ошибка при попытке обработать запрос (проверьте зависимости и наличие индекса в ./storage):\n{str(e)}"
89
+
90
+ # === Gradio UI ===
91
+
92
+ # Custom CSS adapted from your template to 'skin' Gradio
93
+ CUSTOM_CSS = r"""
94
+ /* Reset Gradio containers to use full layout similar to your template */
95
+ .gradio-container {
96
+ padding: 0 !important;
97
+ margin: 0 !important;
98
+ height: 100vh;
99
+ display: flex;
100
+ flex-direction: row;
101
+ background: #ffffff;
102
+ font-family: Arial, Helvetica, sans-serif;
103
+ }
104
+
105
+ /* Sidebar */
106
+ .sidebar {
107
+ width: 280px;
108
+ background: #f8f9fb;
109
+ border-right: 1px solid #ececec;
110
+ padding: 20px;
111
+ box-sizing: border-box;
112
+ overflow: auto;
113
+ }
114
+ .aside-controls { display:flex; justify-content:space-between; align-items:center; margin-bottom:12px; }
115
+ .aside_img { width:22px; height:22px; opacity:0.9; }
116
+ .search { width:100%; padding:8px; border-radius:6px; border:1px solid #ddd; box-sizing:border-box; margin-bottom:12px; }
117
+ .scroll_container { margin-top: 6px; }
118
+
119
+ /* Main area */
120
+ .main {
121
+ flex: 1;
122
+ padding: 20px;
123
+ position: relative;
124
+ overflow: auto;
125
+ }
126
+ .header {
127
+ display:flex;
128
+ justify-content:space-between;
129
+ align-items:center;
130
+ margin-bottom: 6px;
131
+ }
132
+ .logo { height:36px; }
133
+ .header_img { width:28px; opacity:0.85; }
134
+
135
+ /* Chat area — keep centered and relatively positioned */
136
+ .chat_area {
137
+ width: 100%;
138
+ max-width: 1100px;
139
+ margin: 12px auto;
140
+ position: relative;
141
+ }
142
+
143
+ /* Bubble styles */
144
+ .chat_bubble {
145
+ display: block;
146
+ padding: 12px 16px;
147
+ border-radius: 12px;
148
+ margin: 8px 0;
149
+ max-width: 70%;
150
+ box-shadow: 0 1px 4px rgba(0,0,0,0.04);
151
+ word-break: break-word;
152
+ white-space: pre-wrap;
153
+ }
154
+
155
+ /* Assistant (left) */
156
+ .chat_bubble.assistant {
157
+ background: #f1f5f9;
158
+ margin-right: auto;
159
+ margin-left: 0;
160
+ border-top-left-radius: 6px;
161
+ }
162
+
163
+ /* User (right) */
164
+ .chat_bubble.user {
165
+ background: #dbeafe;
166
+ margin-left: auto;
167
+ margin-right: 0;
168
+ border-top-right-radius: 6px;
169
+ text-align: right;
170
+ }
171
+
172
+ /* Input area sticky to bottom-ish */
173
+ .input_area {
174
+ position: sticky;
175
+ bottom: 10px;
176
+ display: flex;
177
+ justify-content: flex-end;
178
+ align-items: center;
179
+ gap: 8px;
180
+ margin-top: 12px;
181
+ }
182
+
183
+ /* Textarea like your template */
184
+ #prompt_field {
185
+ width: 60%;
186
+ min-height: 100px;
187
+ resize: vertical;
188
+ border-radius: 8px;
189
+ border: 1px solid #e6e6e6;
190
+ padding: 12px;
191
+ box-sizing: border-box;
192
+ font-size: 14px;
193
+ }
194
+
195
+ /* Submit button image-like */
196
+ #submit_prompt {
197
+ background: transparent;
198
+ border: none;
199
+ cursor: pointer;
200
+ width: 44px;
201
+ height: 44px;
202
+ display:flex;
203
+ align-items:center;
204
+ justify-content:center;
205
+ }
206
+ #submit_prompt img { width: 28px; height:28px; }
207
+
208
+ /* Small responsive tweaks */
209
+ @media (max-width: 900px) {
210
+ .sidebar { display: none; }
211
+ #prompt_field { width: 80%; }
212
+ .chat_bubble { max-width: 88%; }
213
+ }
214
+ """
215
+
216
+ # Inline simple SVG icons as data-urls to avoid external assets (small placeholders)
217
+ ICON_SUBMIT_SVG = """data:image/svg+xml;utf8,
218
+ <svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='%230066ff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'>
219
+ <path d='M22 2L11 13'></path><path d='M22 2l-7 20-4-9-9-4 20-7z'></path>
220
+ </svg>
221
+ """
222
+
223
+ ICON_MENU_SVG = """data:image/svg+xml;utf8,
224
+ <svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'>
225
+ <path d='M3 6h18M3 12h18M3 18h18'/>
226
+ </svg>
227
+ """
228
+
229
+ ICON_LOGO_SVG = """data:image/svg+xml;utf8,
230
+ <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 30'>
231
+ <rect width='100' height='30' rx='4' fill='%230066ff'/>
232
+ <text x='50' y='20' font-family='Arial' font-size='12' fill='white' text-anchor='middle'>КП</text>
233
+ </svg>
234
+ """
235
+
236
+ # === Build the Gradio Blocks UI ===
237
+ with gr.Blocks(css=CUSTOM_CSS, title="Карбоновые Полигоны — Адаптация") as demo:
238
+ # top-level grid: two columns -> sidebar + main
239
+ with gr.Row(elem_classes="root_row", equal_height=False):
240
+ with gr.Column(scale=1, min_width=280):
241
+ # Sidebar (HTML structure based on your blade)
242
+ sidebar_html = f"""
243
+ <div class="sidebar">
244
+ <div class="aside-controls">
245
+ <img class="aside_img" src="{ICON_MENU_SVG}" alt="menu">
246
+ <div style="display:flex; gap:8px;">
247
+ <img class="aside_img" src="{ICON_MENU_SVG}" alt="bin">
248
+ <img class="aside_img" src="{ICON_MENU_SVG}" alt="chat">
249
+ </div>
250
+ </div>
251
+
252
+ <input type="search" class="search" placeholder="Поиск..." />
253
+
254
+ <div class="scroll_container">
255
+ <h4 style="margin:8px 0 4px 0; color:#333;">17 сентября</h4>
256
+ <div class="fade_text"><p class="m-0">План мероприятий для предотвращения пожаров</p></div>
257
+
258
+ <h4 style="margin:12px 0 4px 0; color:#333;">15 сентября</h4>
259
+ <div class="fade_text"><p class="m-0">Оценка опасности подъема уровня выбросов</p></div>
260
+
261
+ <h4 style="margin:12px 0 4px 0; color:#333;">7 сентября</h4>
262
+ <div class="fade_text"><p class="m-0">Построение адаптивных мероприятий для павод</p></div>
263
+ </div>
264
+
265
+ <div style="margin-top:20px; display:flex; justify-content:center;">
266
+ <button style="padding:10px 20px; border-radius:8px; border:1px solid #e6e6e6; background:#fff;">Помощь</button>
267
+ </div>
268
+ </div>
269
+ """
270
+ gr.HTML(sidebar_html)
271
+
272
+ with gr.Column(scale=3):
273
+ # Header
274
+ header_html = f"""
275
+ <div class="header">
276
+ <img class="logo" src="{ICON_LOGO_SVG}" alt="Лого">
277
+ <div style="display:flex; gap:12px; align-items:center;">
278
+ <img class="header_img" src="{ICON_MENU_SVG}" alt="menu">
279
+ <img class="header_img" src="{ICON_MENU_SVG}" alt="account">
280
+ </div>
281
+ </div>
282
+ """
283
+ gr.HTML(header_html)
284
+
285
+ # Chat area (we will manage history with gr.State and render as HTML)
286
+ chat_container = gr.HTML("<div class='chat_area' id='chat_area'></div>")
287
+
288
+ # Hidden state to store history: list of (role, text)
289
+ history_state = gr.State([])
290
+
291
+ # Input area: we render as HTML but use Gradio components for programmatic submit
292
+ # We'll use a hidden Textbox 'hidden_input' to trigger server function,
293
+ # but visually the user interacts with textarea in HTML and JS.
294
+ hidden_input = gr.Textbox(visible=False)
295
+ hidden_output = gr.Textbox(visible=False) # will carry the assistant markdown/answer
296
+
297
+ # Render input HTML (textarea + send button). Use IDs for JS to hook into.
298
+ input_html = f"""
299
+ <div class="input_area" style="width:100%; margin-top:12px;">
300
+ <textarea id="prompt_field" placeholder="Введите запрос" ></textarea>
301
+ <button id="submit_prompt" title="Отправить"><img src="{ICON_SUBMIT_SVG}" alt="send"></button>
302
+ </div>
303
+ """
304
+ gr.HTML(input_html)
305
+
306
+ # Invisible "status" area
307
+ status = gr.HTML("<div id='status' style='display:none'></div>")
308
+
309
+ # Define function to be called by Gradio when hidden_input changes:
310
+ def server_handle(prompt, history):
311
+ """
312
+ This function is invoked on submit; it receives the latest prompt (string)
313
+ and history (list of [role, text]) from gr.State, calls get_facts, and returns
314
+ new history plus assistant's reply.
315
+ """
316
+ prompt = (prompt or "").strip()
317
+ if not prompt:
318
+ return history, "Ошибка: пустой запрос."
319
+
320
+ # Append user message to history
321
+ history = list(history) # make a copy
322
+ history.append(("user", prompt))
323
+
324
+ # Call get_facts (synchronous)
325
+ answer = get_facts(prompt)
326
+
327
+ # Append assistant reply
328
+ history.append(("assistant", answer))
329
+
330
+ # Return updated history and assistant text
331
+ return history, answer
332
+
333
+ # Wire up the hidden_input -> server_handle outputs:
334
+ # We'll connect hidden_input (value) and history_state to server_handle and update history_state and hidden_output
335
+ hidden_input.change(fn=server_handle, inputs=[hidden_input, history_state], outputs=[history_state, hidden_output])
336
+
337
+ # Client-side JS to:
338
+ # - read textarea
339
+ # - push text to chat_area as user bubble
340
+ # - set hidden_input.value and trigger change (this causes Gradio to call server_handle)
341
+ # - when hidden_output updates, append assistant bubble
342
+ # The script will listen for Events from Gradio to know when hidden_output changes.
343
+ client_js = """
344
+ <script>
345
+ (function() {
346
+ // helper to escape HTML
347
+ function escapeHtml(unsafe) {
348
+ return unsafe
349
+ .replace(/&/g, "&amp;")
350
+ .replace(/</g, "&lt;")
351
+ .replace(/>/g, "&gt;")
352
+ .replace(/"/g, "&quot;")
353
+ .replace(/'/g, "&#039;");
354
+ }
355
+
356
+ const promptField = document.getElementById('prompt_field');
357
+ const submitBtn = document.getElementById('submit_prompt');
358
+ const chatArea = document.getElementById('chat_area');
359
+
360
+ // Gradio inputs we will use
361
+ const hiddenInput = document.querySelector('textarea[id^="component-"][style*="display: none"]')
362
+ || document.querySelector('textarea[aria-hidden="true"]')
363
+ || document.querySelector('textarea[style*="display:none"]');
364
+
365
+ // Better approach: find the hidden textbox by checking gradio components exposed in window
366
+ // We'll fallback to locating by name later.
367
+ function findHiddenInput() {
368
+ const textareas = document.querySelectorAll('textarea');
369
+ for (const t of textareas) {
370
+ // hidden ones often have style display:none or aria-hidden true or small size
371
+ if (t.style.display === 'none' || t.getAttribute('aria-hidden') === 'true' || t.offsetParent === null) {
372
+ return t;
373
+ }
374
+ }
375
+ // fallback: choose last textarea on page that is not prompt_field
376
+ const alt = Array.from(textareas).filter(t => t.id !== 'prompt_field').pop();
377
+ return alt;
378
+ }
379
+
380
+ // Find gradio hidden input and hidden output by probing known component order
381
+ // We'll search for textboxes that are not our prompt_field and not visible
382
+ let hiddenInputs = Array.from(document.querySelectorAll('textarea')).filter(t => t.id !== 'prompt_field');
383
+ let hiddenInputEl = null;
384
+ for (const t of hiddenInputs) {
385
+ if (t.style.display === 'none' || t.getAttribute('aria-hidden') === 'true' || t.offsetParent === null) {
386
+ hiddenInputEl = t; break;
387
+ }
388
+ }
389
+ if (!hiddenInputEl) {
390
+ hiddenInputEl = findHiddenInput();
391
+ }
392
+
393
+ // For hidden output (assistant answer), we look for input elements that change after submit
394
+ // We'll find all inputs and rely on Gradio change events to reflect server output.
395
+
396
+ // Helper to append bubble
397
+ function appendBubble(role, text) {
398
+ const bubble = document.createElement('div');
399
+ bubble.classList.add('chat_bubble');
400
+ bubble.classList.add(role === 'user' ? 'user' : 'assistant');
401
+ bubble.innerText = text;
402
+ chatArea.appendChild(bubble);
403
+ // scroll into view
404
+ bubble.scrollIntoView({behavior: 'smooth', block: 'end'});
405
+ }
406
+
407
+ // When user clicks send:
408
+ submitBtn.addEventListener('click', async function(e) {
409
+ e.preventDefault();
410
+ const value = promptField.value.trim();
411
+ if (!value) return;
412
+ // Add user bubble immediately
413
+ appendBubble('user', value);
414
+
415
+ // disable input while waiting
416
+ submitBtn.disabled = true;
417
+ promptField.disabled = true;
418
+
419
+ // Now set the hidden gradio textbox value and dispatch 'input' event to trigger change
420
+ try {
421
+ // best-effort set value on hidden component gradio uses
422
+ if (!hiddenInputEl) {
423
+ hiddenInputEl = findHiddenInput();
424
+ }
425
+ if (hiddenInputEl) {
426
+ hiddenInputEl.value = value;
427
+ hiddenInputEl.dispatchEvent(new Event('input', { bubbles: true }));
428
+ hiddenInputEl.dispatchEvent(new Event('change', { bubbles: true }));
429
+ } else {
430
+ console.warn('Hidden Gradio input not found; attempting fetch fallback.');
431
+ // fallback: call the server endpoint /run/predict directly via fetch to Gradio internals
432
+ // But direct fetch to Gradio internals is version-dependent; so we avoid that here.
433
+ }
434
+ } catch (err) {
435
+ console.error('Failed to set hidden input:', err);
436
+ }
437
+
438
+ // Now wait for the hidden_output to be updated by Gradio.
439
+ // We'll listen for DOMSubtreeModified events on document and look for changes in textareas (the hidden output one).
440
+ // Simpler approach: poll for changes in history via Gradio state container: find any visible textbox that receives assistant text.
441
+ // We'll poll last textareas for changed value; timeout after 30s.
442
+
443
+ let assistantText = null;
444
+ const start = Date.now();
445
+ const timeoutMs = 30000;
446
+
447
+ while (Date.now() - start < timeoutMs) {
448
+ // Look for any textarea that is not prompt_field and has some content (assistant)
449
+ const candidates = Array.from(document.querySelectorAll('textarea')).filter(t => t.id !== 'prompt_field');
450
+ let val = null;
451
+ for (const c of candidates.reverse()) {
452
+ if (c.value && c.value.trim().length > 0) {
453
+ val = c.value;
454
+ break;
455
+ }
456
+ }
457
+ if (val && val !== value) { // not the same as user prompt
458
+ assistantText = val;
459
+ break;
460
+ }
461
+ // also check for inputs (type=text)
462
+ const inputs = Array.from(document.querySelectorAll('input[type="text"]'));
463
+ for (const inp of inputs.reverse()) {
464
+ if (inp.value && inp.value.trim().length > 0 && inp.value.trim() !== value) {
465
+ assistantText = inp.value;
466
+ break;
467
+ }
468
+ }
469
+ if (assistantText) break;
470
+ await new Promise(r => setTimeout(r, 300)); // sleep 300ms
471
+ }
472
+
473
+ if (!assistantText) {
474
+ // If we didn't catch assistant text via polling, show a fallback message
475
+ appendBubble('assistant', 'Ответ не получен (возможны проблемы с сервером LLM или с интеграцией скрытого поля).');
476
+ } else {
477
+ appendBubble('assistant', assistantText);
478
+ // Clear the hidden textareas so future polling won't pick them up
479
+ try {
480
+ const used = Array.from(document.querySelectorAll('textarea')).filter(t => t.id !== 'prompt_field');
481
+ for (const u of used) {
482
+ u.value = '';
483
+ u.dispatchEvent(new Event('input', { bubbles: true }));
484
+ }
485
+ } catch(e){}
486
+ }
487
+
488
+ // re-enable
489
+ submitBtn.disabled = false;
490
+ promptField.disabled = false;
491
+ promptField.value = '';
492
+ });
493
+
494
+ // Submit on Ctrl+Enter
495
+ promptField.addEventListener('keydown', function(e) {
496
+ if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
497
+ submitBtn.click();
498
+ }
499
+ });
500
+
501
+ })();
502
+ </script>
503
+ """
504
+ gr.HTML(client_js)
505
+
506
+ # Launch with server_name and debug off by default
507
+ demo.launch(server_name="127.0.0.1", server_port=7860, share=False)