ana-solo commited on
Commit
3a0fb78
·
verified ·
1 Parent(s): d39e0df

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +243 -410
app.py CHANGED
@@ -4,27 +4,26 @@ from llama_index.llms.openrouter import OpenRouter
4
  from llama_index.core.llms import ChatMessage
5
  from llama_index.embeddings.huggingface import HuggingFaceEmbedding
6
  from llama_index.core import Settings, StorageContext, load_index_from_storage
7
-
8
  import nest_asyncio
 
9
  nest_asyncio.apply()
10
 
11
  # === Глобальная инициализация ===
12
  embed_model = HuggingFaceEmbedding(model_name='intfloat/multilingual-e5-large-instruct')
13
  Settings.embed_model = embed_model
14
  Settings.llm = OpenRouter(
15
- api_key=os.environ["OPENROUTER_API_KEY"],
16
- model="qwen/qwen3-4b:free",
17
  max_tokens=10000,
18
  context_window=20000,
19
  )
 
20
  system_prompt = """
21
  Ты — эксперт по адаптации к изменениям климата.
22
  У тебя есть база знаний с кейсами и нормативными документами.
23
  Пользователь вводит запрос, связанный с климатическим риском в регионе или отрасли.
24
-
25
  Твоя задача — на основе информации из базы знаний предложить 2–3 релевантных адаптационных мероприятия,
26
  которые помогут снизить климатический риск, о котором спрашивает пользователь.
27
-
28
  ### Требования к ответу:
29
  1. Представь результат **в виде Markdown-таблицы** с колонками:
30
  - Наименование мероприятий
@@ -32,476 +31,310 @@ system_prompt = """
32
  - Адаптационный эффект
33
  - Актуальность для региона (указать с учётом контекста запроса). Если регион не указан, считай, что задается вопрос по Тюменской области
34
  - Ответственная организация (из региона)
35
-
36
  2. Если источник данных, на которых ты основываешь ответ, известен (это URL и краткое название кейса),
37
  добавь их **ниже таблицы** в виде списка ссылок:
38
  `**Опорные источники:** [1] Наименовавание мероприятий - URL, [2] Наименовавание мероприятий - URL`
39
-
40
  3. Пиши кратко, по существу, с акцентом на реальные, практические меры.
41
  4. Если информация отсутствует — предложи логичные адаптационные меры на основе Приказа Минэкономразвития России от 13 мая 2021 г. № 267 «Об утверждении методических рекомендаций и показателей по вопросам адаптации к изменениям климата».
42
-
43
  Пример формата ответа:
44
-
45
  | Наименование мероприятий | Митигационный эффект | Адаптационный эффект | Актуальность для Тобольского района | Ответственная организация |
46
  |---------------------------|----------------------|----------------------|------------------------------------|----------------------------|
47
  | Развитие городского электротранспорта | снижение эмиссии | повышение устойчивости транспортной инфраструктуры | актуально | городские власти |
48
  | Перевод транспорта на газомоторное топливо | снижение эмиссии | рациональное использование ресурсов | реализуется частично | транспортные организации |
49
-
50
- **Опорные источники:** [1] Наименовавание мероприятий - https://example.com/case_12
51
  Приводи только те источники, которые используешь для формирования таблицы непосредственно. URL приводи строго такое же, как указано в базе знаний. Наименование мероприятий бери из базы знаний
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()
 
4
  from llama_index.core.llms import ChatMessage
5
  from llama_index.embeddings.huggingface import HuggingFaceEmbedding
6
  from llama_index.core import Settings, StorageContext, load_index_from_storage
 
7
  import nest_asyncio
8
+
9
  nest_asyncio.apply()
10
 
11
  # === Глобальная инициализация ===
12
  embed_model = HuggingFaceEmbedding(model_name='intfloat/multilingual-e5-large-instruct')
13
  Settings.embed_model = embed_model
14
  Settings.llm = OpenRouter(
15
+ api_key="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",
16
+ model="deepseek/deepseek-r1-0528-qwen3-8b:free",
17
  max_tokens=10000,
18
  context_window=20000,
19
  )
20
+
21
  system_prompt = """
22
  Ты — эксперт по адаптации к изменениям климата.
23
  У тебя есть база знаний с кейсами и нормативными документами.
24
  Пользователь вводит запрос, связанный с климатическим риском в регионе или отрасли.
 
25
  Твоя задача — на основе информации из базы знаний предложить 2–3 релевантных адаптационных мероприятия,
26
  которые помогут снизить климатический риск, о котором спрашивает пользователь.
 
27
  ### Требования к ответу:
28
  1. Представь результат **в виде Markdown-таблицы** с колонками:
29
  - Наименование мероприятий
 
31
  - Адаптационный эффект
32
  - Актуальность для региона (указать с учётом контекста запроса). Если регион не указан, считай, что задается вопрос по Тюменской области
33
  - Ответственная организация (из региона)
 
34
  2. Если источник данных, на которых ты основываешь ответ, известен (это URL и краткое название кейса),
35
  добавь их **ниже таблицы** в виде списка ссылок:
36
  `**Опорные источники:** [1] Наименовавание мероприятий - URL, [2] Наименовавание мероприятий - URL`
 
37
  3. Пиши кратко, по существу, с акцентом на реальные, практические меры.
38
  4. Если информация отсутствует — предложи логичные адаптационные меры на основе Приказа Минэкономразвития России от 13 мая 2021 г. № 267 «Об утверждении методических рекомендаций и показателей по вопросам адаптации к изменениям климата».
 
39
  Пример формата ответа:
 
40
  | Наименование мероприятий | Митигационный эффект | Адаптационный эффект | Актуальность для Тобольского района | Ответственная организация |
41
  |---------------------------|----------------------|----------------------|------------------------------------|----------------------------|
42
  | Развитие городского электротранспорта | снижение эмиссии | повышение устойчивости транспортной инфраструктуры | актуально | городские власти |
43
  | Перевод транспорта на газомоторное топливо | снижение эмиссии | рациональное использование ресурсов | реализуется частично | транспортные организации |
44
+ **Опорные источники:** [1] Наименовавание мероприятий - https://example.com/case_12
 
45
  Приводи только те источники, которые используешь для формирования таблицы непосредственно. URL приводи строго такое же, как указано в базе знаний. Наименование мероприятий бери из базы знаний
46
  Ответственную организацию в таблице указывай актуальную для региона, который пользователь указал в запросе
47
  """
48
 
49
  def get_facts(user_question: str) -> str:
 
 
 
 
50
  try:
51
+ if not user_question.strip():
52
  return "Ошибка: пожалуйста, введите ваш запрос."
53
 
 
 
 
 
 
 
 
54
  storage_context = StorageContext.from_defaults(persist_dir="./storage")
55
  index = load_index_from_storage(storage_context)
56
  retriever = index.as_retriever(similarity_top_k=4)
57
  nodes = retriever.retrieve(user_question)
58
 
59
  context = "\n\n".join([node.get_content() for node in nodes]) if nodes else "Не найдено релевантных документов."
60
+ full_system_prompt = system_prompt + f"\n\nКонтекст:\n{context}"
61
 
62
  messages = [
63
  ChatMessage(role="system", content=full_system_prompt),
64
  ChatMessage(role="user", content="Пользовательский запрос: " + user_question)
65
  ]
66
+
67
  response = Settings.llm.chat(messages)
68
  return response.message.content
69
 
70
  except Exception as e:
71
+ return f"Ошибка:\n{str(e)}"
72
 
73
+ # === Кастомный CSS из style.css ===
74
+ custom_css = """
75
+ /* Импорт шрифта Inter */
76
+ @import url("https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap");
77
 
78
+ body {
79
+ font-family: "Inter", sans-serif;
 
 
 
80
  margin: 0 !important;
81
+ padding: 0 !important;
82
+ display: flex !important;
83
+ flex-direction: row !important;
84
+ justify-content: space-between !important;
85
+ height: 100vh !important;
86
  }
87
 
 
88
  .sidebar {
89
+ width: 210px;
90
+ height: 100vh;
91
+ background-color: #f9f9f9;
 
92
  box-sizing: border-box;
93
+ padding: 20px 16px !important;
94
+ display: flex !important;
95
+ flex-direction: column !important;
96
+ justify-content: space-between !important;
97
  }
 
 
 
 
98
 
 
99
  .main {
100
+ width: calc(100% - 210px) !important;
101
+ height: 100vh !important;
102
+ box-sizing: border-box;
103
+ padding: 16px !important;
104
+ position: relative !important;
105
  }
106
+
107
+ .aside_img {
108
+ height: 20px;
109
+ cursor: pointer;
 
110
  }
 
 
111
 
112
+ .logo {
113
+ height: 40px;
 
 
 
 
114
  }
115
 
116
+ .header_img {
117
+ height: 30px;
118
+ cursor: pointer;
 
 
 
 
 
 
 
119
  }
120
 
121
+ .search {
122
+ height: 40px;
123
+ border: none;
124
+ border-radius: 15px;
125
+ box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.25);
126
+ box-sizing: border-box;
127
+ font-size: 16px;
128
+ width: 100% !important;
129
+ padding-left: 10px !important;
130
  }
131
 
132
+ .search_icon {
133
+ position: absolute !important;
134
+ right: 15px !important;
135
+ bottom: 10px !important;
136
+ height: 20px !important;
137
+ cursor: pointer !important;
 
138
  }
139
 
140
+ .scroll_container {
141
+ overflow-y: auto;
142
+ height: 70vh;
 
 
 
 
 
143
  margin-top: 12px;
144
  }
145
 
146
+ .scroll_header {
147
+ font-size: 18px;
148
+ font-weight: 500;
149
+ color: #333333;
150
+ margin-top: 12px !important;
151
+ margin-bottom: 0 !important;
152
+ }
153
+
154
+ .fade_text {
155
+ white-space: nowrap;
156
+ overflow: hidden;
157
+ font-size: 16px;
158
+ font-weight: 300;
159
+ position: relative;
160
+ width: 100% !important;
161
+ }
162
+
163
+ .fade_text::after {
164
+ content: "";
165
+ position: absolute;
166
+ top: 0;
167
+ right: 0;
168
+ width: 50px;
169
+ height: 100%;
170
+ background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, #f9f9f9 100%);
171
  }
172
 
173
+ .input_button {
174
+ height: 56px;
175
+ background-color: #fff;
176
  border: none;
177
+ border-radius: 15px;
178
+ box-shadow: 0px 2px 2px 0px rgba(0, 0, 0, 0.25);
179
+ display: flex !important;
180
+ justify-content: center !important;
181
+ align-items: center !important;
182
+ gap: 16px !important;
183
  }
 
184
 
185
+ .input_button button {
186
+ background-color: #fff !important;
187
+ border: none !important;
188
+ font-size: 16px !important;
189
+ font-weight: bold !important;
190
+ color: #6e6e6e !important;
191
+ padding: 0 !important;
192
+ margin: 0 !important;
193
  }
 
194
 
195
+ .welcome {
196
+ width: 80%;
197
+ max-width: 930px;
198
+ margin: auto;
199
+ position: absolute !important;
200
+ top: 59%;
201
+ left: 50%;
202
+ transform: translate(-50%, -50%);
203
+ }
204
 
205
+ #prompt_field textarea {
206
+ font-family: "Inter", sans-serif !important;
207
+ height: 180px !important;
208
+ max-height: 180px !important;
209
+ width: 90% !important;
210
+ border: none !important;
211
+ border-radius: 15px !important;
212
+ box-shadow: 0px 2px 7px 0px rgba(0, 0, 0, 0.25) !important;
213
+ font-size: 16px !important;
214
+ resize: none !important;
215
+ padding: 16px !important;
216
+ padding-bottom: 70px !important;
217
+ }
218
+
219
+ #prompt_field textarea:focus {
220
+ outline: none !important;
221
+ }
222
+
223
+ .prompt_buttons {
224
+ width: 87% !important;
225
+ background-color: white !important;
226
+ border-radius: 0 !important;
227
+ position: absolute !important;
228
+ bottom: 0 !important;
229
+ padding: 8px 8px !important;
230
+ display: flex !important;
231
+ justify-content: flex-end !important;
232
+ }
233
+
234
+ .prompt_buttons::before {
235
+ content: "";
236
+ position: absolute;
237
+ top: -10px;
238
+ left: 0;
239
+ width: 100%;
240
+ height: 15px;
241
+ background: linear-gradient(to top, rgb(255, 255, 255), rgba(255, 255, 255, 0.6));
242
+ pointer-events: none;
243
+ }
244
 
245
+ .prompt_buttons button {
246
+ height: 40px !important;
247
+ width: 40px !important;
248
+ background: none !important;
249
+ border: none !important;
250
+ cursor: pointer !important;
251
+ padding: 0 !important;
252
+ }
253
+
254
+ .user_prompt {
255
+ background-color: #efefef;
256
+ border-radius: 15px;
257
+ padding: 12px !important;
258
+ margin-bottom: 16px !important;
259
+ width: 75% !important;
260
+ margin-left: auto !important;
261
+ }
262
+
263
+ .answer {
264
+ border: none;
265
+ border-radius: 15px;
266
+ box-shadow: 0px 2px 7px 0px rgba(0, 0, 0, 0.25);
267
+ padding: 12px !important;
268
+ width: 75% !important;
269
+ margin-right: auto !important;
270
+ }
271
+
272
+ .prompt_chat {
273
+ font-size: 20px !important;
274
+ margin: 0 !important;
275
+ }
276
+
277
+ /* Скрыть стандартные элементы Gradio */
278
+ #component-0, #component-1, #component-2 {
279
+ display: none !important;
280
+ }
281
+
282
+ /* Скрыть заголовки и кнопки по умолчанию */
283
+ .gradio-container {
284
+ background: white !important;
285
+ }
286
  """
287
 
288
+ # === Gradio UI ===
289
+ with gr.Blocks(css=custom_css, theme=gr.themes.Default()) as demo:
290
+ # Сайдбар
291
+ with gr.Column(elem_classes="sidebar"):
292
+ with gr.Row():
293
+ gr.Image(value="icons/arrow.png", elem_classes="aside_img", interactive=False)
294
+ with gr.Row():
295
+ gr.Image(value="icons/bin.png", elem_classes="aside_img", interactive=False)
296
+ gr.Image(value="icons/chat.png", elem_classes="aside_img ms-2", interactive=False)
297
+
298
+ with gr.Row():
299
+ gr.Textbox(placeholder="Поиск", elem_classes="search")
300
+ gr.Image(value="icons/search.png", elem_classes="search_icon", interactive=False)
301
+
302
+ with gr.Column(elem_classes="scroll_container"):
303
+ gr.Markdown("### 17 сентября", elem_classes="scroll_header")
304
+ gr.Markdown("План мероприятий для предовтращения пожаров", elem_classes="fade_text")
305
+ gr.Markdown("### 15 сентября", elem_classes="scroll_header")
306
+ gr.Markdown("Оценка опасности подъема уровня выбросов", elem_classes="fade_text")
307
+ gr.Markdown("### 7 сентября", elem_classes="scroll_header")
308
+ gr.Markdown("Построение адаптивных мероприятий для павод", elem_classes="fade_text")
309
+
310
+ with gr.Row(elem_classes="input_button"):
311
+ gr.Image(value="icons/question.png", elem_classes="input_img", interactive=False)
312
+ gr.Button("Помощь", elem_id="help-btn")
313
+
314
+ # Основная область
315
+ with gr.Column(elem_classes="main"):
316
+ with gr.Row():
317
+ gr.Image(value="icons/logo.png", elem_classes="logo", interactive=False)
318
+ with gr.Row():
319
+ gr.Image(value="icons/menu.png", elem_classes="header_img", interactive=False)
320
+ gr.Image(value="icons/account.png", elem_classes="header_img", interactive=False)
321
+
322
+ with gr.Column(elem_classes="welcome"):
323
+ user_input = gr.Textbox(
324
+ label="",
325
+ lines=5,
326
+ max_lines=5,
327
+ placeholder="Введите запрос",
328
+ elem_id="prompt_field"
329
+ )
330
+ with gr.Row(elem_classes="prompt_buttons"):
331
+ submit_btn = gr.Button("", elem_id="submit_prompt")
332
+ # Иконка отправки (замените на реальный путь, если нужно)
333
+ submit_btn.style(full_width=False)
334
+
335
+ answer_output = gr.Markdown(elem_classes="answer", value="")
336
+
337
+ # Обработка
338
+ submit_btn.click(fn=get_facts, inputs=user_input, outputs=answer_output)
339
+
340
+ demo.launch()