showcase / app.py
ana-solo's picture
Update app.py
1d39f94 verified
import os
import gradio as gr
from llama_index.llms.openrouter import OpenRouter
from llama_index.core.llms import ChatMessage
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.core import Settings, StorageContext, load_index_from_storage
import nest_asyncio
nest_asyncio.apply()
# === Глобальная инициализация ===
embed_model = HuggingFaceEmbedding(model_name='intfloat/multilingual-e5-large-instruct')
Settings.embed_model = embed_model
Settings.llm = OpenRouter(
api_key="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",
model="deepseek/deepseek-r1-0528-qwen3-8b:free",
max_tokens=10000,
context_window=20000,
)
system_prompt = """
Ты — эксперт по адаптации к изменениям климата.
У тебя есть база знаний с кейсами и нормативными документами.
Пользователь вводит запрос, связанный с климатическим риском в регионе или отрасли.
Твоя задача — на основе информации из базы знаний предложить 2–3 релевантных адаптационных мероприятия,
которые помогут снизить климатический риск, о котором спрашивает пользователь.
### Требования к ответу:
1. Представь результат **в виде Markdown-таблицы** с колонками:
- Наименование мероприятий
- Митигационный эффект
- Адаптационный эффект
- Актуальность для региона (указать с учётом контекста запроса). Если регион не указан, считай, что задается вопрос по Тюменской области
- Ответственная организация (из региона)
2. Если источник данных, на которых ты основываешь ответ, известен (это URL и краткое название кейса),
добавь их **ниже таблицы** в виде списка ссылок:
`**Опорные источники:** [1] Наименовавание мероприятий - URL, [2] Наименовавание мероприятий - URL`
3. Пиши кратко, по существу, с акцентом на реальные, практические меры.
4. Если информация отсутствует — предложи логичные адаптационные меры на основе Приказа Минэкономразвития России от 13 мая 2021 г. № 267 «Об утверждении методических рекомендаций и показателей по вопросам адаптации к изменениям климата».
Пример формата ответа:
| Наименование мероприятий | Митигационный эффект | Адаптационный эффект | Актуальность для Тобольского района | Ответственная организация |
|---------------------------|----------------------|----------------------|------------------------------------|----------------------------|
| Развитие городского электротранспорта | снижение эмиссии | повышение устойчивости транспортной инфраструктуры | актуально | городские власти |
| Перевод транспорта на газомоторное топливо | снижение эмиссии | рациональное использование ресурсов | реализуется частично | транспортные организации |
**Опорные источники:** [1] Наименовавание мероприятий - https://example.com/case_12
Приводи только те источники, которые используешь для формирования таблицы непосредственно. URL приводи строго такое же, как указано в базе знаний. Наименование мероприятий бери из базы знаний
Ответственную организацию в таблице указывай актуальную для региона, который пользователь указал в запросе
"""
def get_facts(user_question: str) -> str:
try:
if not user_question.strip():
return "Ошибка: пожалуйста, введите ваш запрос."
storage_context = StorageContext.from_defaults(persist_dir="./storage")
index = load_index_from_storage(storage_context)
retriever = index.as_retriever(similarity_top_k=4)
nodes = retriever.retrieve(user_question)
context = "\n\n".join([node.get_content() for node in nodes]) if nodes else "Не найдено релевантных документов."
full_system_prompt = system_prompt + f"\n\nКонтекст:\n{context}"
messages = [
ChatMessage(role="system", content=full_system_prompt),
ChatMessage(role="user", content="Пользовательский запрос: " + user_question)
]
response = Settings.llm.chat(messages)
return response.message.content
except Exception as e:
return f"Ошибка:\n{str(e)}"
# === Кастомный CSS из style.css ===
custom_css = """
/* Импорт шрифта Inter */
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap");
body {
font-family: "Inter", sans-serif;
margin: 0 !important;
padding: 0 !important;
display: flex !important;
flex-direction: row !important;
justify-content: space-between !important;
height: 100vh !important;
}
.sidebar {
width: 210px;
height: 100vh;
background-color: #f9f9f9;
box-sizing: border-box;
padding: 20px 16px !important;
display: flex !important;
flex-direction: column !important;
justify-content: space-between !important;
}
.main {
width: calc(100% - 210px) !important;
height: 100vh !important;
box-sizing: border-box;
padding: 16px !important;
position: relative !important;
}
.aside_img {
height: 20px;
cursor: pointer;
}
.logo {
height: 40px;
}
.header_img {
height: 30px;
cursor: pointer;
}
.search {
height: 40px;
border: none;
border-radius: 15px;
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.25);
box-sizing: border-box;
font-size: 16px;
width: 100% !important;
padding-left: 10px !important;
}
.search_icon {
position: absolute !important;
right: 15px !important;
bottom: 10px !important;
height: 20px !important;
cursor: pointer !important;
}
.scroll_container {
overflow-y: auto;
height: 70vh;
margin-top: 12px;
}
.scroll_header {
font-size: 18px;
font-weight: 500;
color: #333333;
margin-top: 12px !important;
margin-bottom: 0 !important;
}
.fade_text {
white-space: nowrap;
overflow: hidden;
font-size: 16px;
font-weight: 300;
position: relative;
width: 100% !important;
}
.fade_text::after {
content: "";
position: absolute;
top: 0;
right: 0;
width: 50px;
height: 100%;
background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, #f9f9f9 100%);
}
.input_button {
height: 56px;
background-color: #fff;
border: none;
border-radius: 15px;
box-shadow: 0px 2px 2px 0px rgba(0, 0, 0, 0.25);
display: flex !important;
justify-content: center !important;
align-items: center !important;
gap: 16px !important;
}
.input_button button {
background-color: #fff !important;
border: none !important;
font-size: 16px !important;
font-weight: bold !important;
color: #6e6e6e !important;
padding: 0 !important;
margin: 0 !important;
}
.welcome {
width: 80%;
max-width: 930px;
margin: auto;
position: absolute !important;
top: 59%;
left: 50%;
transform: translate(-50%, -50%);
}
#prompt_field textarea {
font-family: "Inter", sans-serif !important;
height: 180px !important;
max-height: 180px !important;
width: 90% !important;
border: none !important;
border-radius: 15px !important;
box-shadow: 0px 2px 7px 0px rgba(0, 0, 0, 0.25) !important;
font-size: 16px !important;
resize: none !important;
padding: 16px !important;
padding-bottom: 70px !important;
}
#prompt_field textarea:focus {
outline: none !important;
}
.prompt_buttons {
width: 87% !important;
background-color: white !important;
border-radius: 0 !important;
position: absolute !important;
bottom: 0 !important;
padding: 8px 8px !important;
display: flex !important;
justify-content: flex-end !important;
}
.prompt_buttons::before {
content: "";
position: absolute;
top: -10px;
left: 0;
width: 100%;
height: 15px;
background: linear-gradient(to top, rgb(255, 255, 255), rgba(255, 255, 255, 0.6));
pointer-events: none;
}
.prompt_buttons button {
height: 40px !important;
width: 40px !important;
background: none !important;
border: none !important;
cursor: pointer !important;
padding: 0 !important;
}
.user_prompt {
background-color: #efefef;
border-radius: 15px;
padding: 12px !important;
margin-bottom: 16px !important;
width: 75% !important;
margin-left: auto !important;
}
.answer {
border: none;
border-radius: 15px;
box-shadow: 0px 2px 7px 0px rgba(0, 0, 0, 0.25);
padding: 12px !important;
width: 75% !important;
margin-right: auto !important;
}
.prompt_chat {
font-size: 20px !important;
margin: 0 !important;
}
/* Скрыть стандартные элементы Gradio */
#component-0, #component-1, #component-2 {
display: none !important;
}
/* Скрыть заголовки и кнопки по умолчанию */
.gradio-container {
background: white !important;
}
#submit_prompt {
background: none !important;
border: none !important;
font-size: 0 !important;
width: 40px !important;
height: 40px !important;
cursor: pointer !important;
position: relative !important;
}
#submit_prompt::after {
content: "➤";
font-size: 24px !important;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: black;
}
"""
# === Gradio UI ===
with gr.Blocks(css=custom_css, theme=gr.themes.Default()) as demo:
# Сайдбар
with gr.Column(elem_classes="sidebar"):
with gr.Row():
gr.Image(value="icons/arrow.png", elem_classes="aside_img", interactive=False)
with gr.Row():
gr.Image(value="icons/bin.png", elem_classes="aside_img", interactive=False)
gr.Image(value="icons/chat.png", elem_classes="aside_img ms-2", interactive=False)
with gr.Row():
gr.Textbox(placeholder="Поиск", elem_classes="search")
gr.Image(value="icons/search.png", elem_classes="search_icon", interactive=False)
with gr.Column(elem_classes="scroll_container"):
gr.Markdown("### 17 сентября", elem_classes="scroll_header")
gr.Markdown("План мероприятий для предовтращения пожаров", elem_classes="fade_text")
gr.Markdown("### 15 сентября", elem_classes="scroll_header")
gr.Markdown("Оценка опасности подъема уровня выбросов", elem_classes="fade_text")
gr.Markdown("### 7 сентября", elem_classes="scroll_header")
gr.Markdown("Построение адаптивных мероприятий для павод", elem_classes="fade_text")
with gr.Row(elem_classes="input_button"):
gr.Image(value="icons/question.png", elem_classes="input_img", interactive=False)
gr.Button("Помощь", elem_id="help-btn")
# Основная область
with gr.Column(elem_classes="main"):
with gr.Row():
gr.Image(value="icons/logo.png", elem_classes="logo", interactive=False)
with gr.Row():
gr.Image(value="icons/menu.png", elem_classes="header_img", interactive=False)
gr.Image(value="icons/account.png", elem_classes="header_img", interactive=False)
with gr.Column(elem_classes="welcome"):
user_input = gr.Textbox(
label="",
lines=5,
max_lines=5,
placeholder="Введите запрос",
elem_id="prompt_field"
)
with gr.Row(elem_classes="prompt_buttons"):
submit_btn = gr.Button("➤", elem_id="submit_prompt")
answer_output = gr.Markdown(elem_classes="answer", value="")
# Обработка
submit_btn.click(fn=get_facts, inputs=user_input, outputs=answer_output)
demo.launch()