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()