File size: 14,202 Bytes
c9548f6
 
 
 
 
 
 
 
 
 
 
39aa287
ecefdb3
 
 
 
 
240bf79
c9548f6
 
 
 
 
 
 
492bf81
c9548f6
 
 
 
 
 
 
 
7aa16a5
c9548f6
 
 
 
 
 
 
 
 
 
 
 
 
 
5e911ec
 
fdb0a88
c9548f6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7e529da
51ac7ff
 
ecefdb3
39aa287
51ac7ff
 
c9548f6
 
 
 
 
 
ff7d7bd
fdb0a88
 
c9548f6
 
 
 
 
 
4e68359
c9548f6
fdb0a88
c9548f6
 
 
fdb0a88
 
 
c9548f6
 
 
283442c
 
 
 
 
8a83884
283442c
 
 
 
 
fdb0a88
c9548f6
 
 
fdb0a88
 
 
 
c9548f6
 
 
 
 
fdb0a88
c9548f6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17b4618
c9548f6
 
5e911ec
 
 
 
c9548f6
 
 
 
 
 
 
 
 
 
 
 
 
fdb0a88
 
 
 
 
 
 
 
 
 
c9548f6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ecefdb3
 
 
 
 
 
fdb0a88
ecefdb3
 
 
 
 
c9548f6
 
 
 
 
 
 
7835daf
9568e40
7835daf
c9548f6
9568e40
c9548f6
 
 
5e911ec
fdb0a88
c9548f6
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
import gradio as gr
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
import time
import re
from typing import Tuple, Dict

# ------------------------------------------------------------
# Конфигурация
# ------------------------------------------------------------
MODEL_NAMES = [
    "Dilana/Llama-3.2-1B-Adaptive-RAG-v3",
    "LiquidAI/LFM2-1.2B-RAG",
    "HuggingFaceTB/SmolLM3-3B",
    "thelamapi/next2.5",
    "Qwen/Qwen3-4B-Instruct-2507",
    "utter-project/EuroLLM-1.7B-Instruct",
    "ai-sage/GigaChat3.1-10B-A1.8B"
]
DEFAULT_MODEL = MODEL_NAMES[0]

# Лимиты на длину ввода (в символах)
MAX_DOCUMENT_CHARS = 2000
MAX_QUESTION_CHARS = 1000
MAX_TOTAL_CHARS = MAX_DOCUMENT_CHARS + MAX_QUESTION_CHARS
MAX_PROMPT_TOKENS = 512

# Кэш для моделей и токенизаторов
model_cache: Dict[str, Tuple] = {}  # имя -> (tokenizer, model)

def load_model(model_name: str):
    """Загружает токенизатор и модель, если ещё не загружены."""
    if model_name not in model_cache:
        tokenizer = AutoTokenizer.from_pretrained(model_name)
        model = AutoModelForCausalLM.from_pretrained(model_name)
        model_cache[model_name] = (tokenizer, model)
    return model_cache[model_name]

def truncate_text(text: str, max_chars: int) -> str:
    """Обрезает текст до указанного количества символов (грубо, по символам)."""
    if len(text) > max_chars:
        return text[:max_chars] + "..."
    return text

def generate_response(
    document: str,
    question: str,
    model_name: str,
    max_new_tokens: int,
    temperature: float,
    repetition_penalty: float
) -> Tuple[str, float, float, float]:
    """
    Генерирует ответ модели на основе документа и вопроса.
    Возвращает (ответ, время_генерации_сек).
    """
    # Проверка на пустые входные данные
    if not document.strip():
        return "Ошибка: документ не может быть пустым.", 0.0
    if not question.strip():
        return "Ошибка: вопрос не может быть пустым.", 0.0

    # Обрезка по длине
    document = truncate_text(document, MAX_DOCUMENT_CHARS)
    question = truncate_text(question, MAX_QUESTION_CHARS)

    # Формирование промпта (простая инструкция)
    prompt = f"Ты бот, который должен чётко и точно ответить на вопрос пользователя или сообщить, что требуемой информации не обнаружено, по документу:<document>{document}</document>\nВопрос пользователя:<question>{question}</question>\nОтвет на вопрос:<answer>\n"
    
    messages = [
        {"role": "system", "content": f"Ты бот, который дает короткий и чёткий ответ пользователю на русском языке строго по данным из документа не выдумывая ничего лишнего. Задача выполняется на 1000\10. Данные из документа: <document>{document}</document>"},
        {"role": "user", "content": f"Вопрос по документу: {question}"}
    ]
    
    # Загрузка модели
    try:
        tokenizer, model = load_model(model_name)
    except Exception as e:
        return f"Ошибка загрузки модели: {type(e).__name__}: {e}", 0.0

    prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)

    start_tok = time.time()
    # Токенизация с учётом максимальной длины модели
    try:
        inputs = tokenizer(
            prompt,
            return_tensors="pt",
            truncation=True,
            max_length=MAX_PROMPT_TOKENS
        )
        tok_time = time.time() - start_tok
    except Exception as e:
        return f"Ошибка токенизации: {type(e).__name__}: {e}", 0.0

    char_count = len(prompt)
    tok_time_per_char = tok_time / char_count if char_count > 0 else 0.0
    
    # Генерация
    start_time = time.time()
    try:
        with torch.no_grad():
            outputs = model.generate(
                inputs.input_ids,
                max_new_tokens=max_new_tokens,
                temperature=temperature if temperature > 0 else None,
                do_sample=False,
                top_p=0.95,
                repetition_penalty=repetition_penalty,      # штраф за повторяющиеся токены
                early_stopping=True,          # остановка при достижении eos_token
                pad_token_id=tokenizer.eos_token_id
            )
        gen_time = time.time() - start_time
    except Exception as e:
        return f"Ошибка генерации: {type(e).__name__}: {e}", time.time() - start_time

    # Количество сгенерированных токенов
    generated_tokens = outputs[0].shape[0] - inputs.input_ids.shape[1]
    gen_time_per_token = gen_time / generated_tokens if generated_tokens > 0 else 0.0
    
    # Декодирование ответа
    response = tokenizer.decode(outputs[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)
    if not response.strip():
        response = "[модель не дала ответа]"

    return response.strip(), gen_time, tok_time_per_char, gen_time_per_token

# ------------------------------------------------------------
# Интерфейс Gradio
# ------------------------------------------------------------
with gr.Blocks(title="Мини-чат по документу (русский язык)") as demo:
    gr.Markdown("""
    ## Чат с моделью на основе одного документа
    Задайте вопрос по предоставленному тексту. Модель ответит, используя только информацию из документа.
    """)

    with gr.Row():
        with gr.Column(scale=2):
            document_input = gr.Textbox(
                label="Документ (контекст)",
                lines=6,
                placeholder="Вставьте текст документа здесь..."
            )
            question_input = gr.Textbox(
                label="Ваш вопрос",
                lines=2,
                placeholder="Например: О чём говорится в документе?"
            )
            with gr.Row():
                model_selector = gr.Dropdown(
                    choices=MODEL_NAMES,
                    value=DEFAULT_MODEL,
                    label="Модель"
                )
                max_tokens_slider = gr.Slider(
                    10, 200, value=50, step=5,
                    label="Макс. новых токенов"
                )
                temperature_slider = gr.Slider(
                    0.0, 2.0, value=0.7, step=0.1,
                    label="Температура"
                )
                repetition_penalty_slider = gr.Slider(
                    0.1, 2.0, value=1.0, step=0.1,
                    label="Штраф за повторение"
                )
            submit_btn = gr.Button("Спросить", variant="primary")

        with gr.Column(scale=1):
            answer_output = gr.Textbox(
                label="Ответ модели",
                lines=6,
                interactive=False
            )
            latency_output = gr.Textbox(
                label="Время генерации (сек)",
                lines=1,
                interactive=False
            )
            tok_time_output = gr.Textbox(
                label="Ср. время токенизации на символ (сек)",
                lines=1,
                interactive=False
            )
            gen_time_output = gr.Textbox(
                label="Ср. время генерации на токен (сек)",
                lines=1,
                interactive=False
            )

    # Примеры (заполняют документ и вопрос, остальные параметры остаются текущими)
    gr.Examples(
        examples=[
            [
                "Кофе эспрессо готовится путём пропускания горячей воды под давлением через молотые зёрна. Температура воды 90-96°C, давление 9 бар. Выход напитка 25-35 мл.",
                "Как приготовить эспрессо?"
            ],
            [
                "Солнечная система состоит из Солнца и планет: Меркурий, Венера, Земля, Марс, Юпитер, Сатурн, Уран, Нептун. Земля — третья планета от Солнца, единственная известная планета с жизнью.",
                "Какая планета третья от Солнца?"
            ],
            [
                "Для сборки стола необходимо: столешница, 4 ножки, 8 шурупов, отвёртка. Сначала прикрутить ножки к столешнице, затянув шурупы крест-накрест.",
                "Какие инструменты нужны для сборки стола?"
            ],
            [
                "Исследования последних лет показывают, что регулярное употребление зелёного чая может снижать риск сердечно-сосудистых заболеваний. В состав зелёного чая входят катехины – антиоксиданты, которые нейтрализуют свободные радикалы и уменьшают окислительный стресс. Кроме того, зелёный чай содержит L-теанин – аминокислоту, способствующую расслаблению и улучшению когнитивных функций. Однако важно помнить, что чрезмерное потребление (более 5 чашек в день) может привести к негативным эффектам из-за кофеина. Врачи рекомендуют употреблять 2–3 чашки качественного зелёного чая в день для поддержания здоровья.",
                "Какие полезные вещества содержатся в зелёном чае и как они влияют на организм?"
            ],
            [
                "Для установки программы «Калькулятор v2.0» скачайте установочный файл с официального сайта. Запустите скачанный файл и следуйте инструкциям мастера установки. На первом этапе выберите язык интерфейса (русский или английский). Затем укажите папку для установки (по умолчанию C:\\Program Files\\Calculator). После завершения установки на рабочем столе появится ярлык. Для запуска программы дважды кликните по ярлыку. В главном окне доступны базовые арифметические операции: сложение, вычитание, умножение, деление. Для продвинутых вычислений откройте меню «Вид» и выберите «Инженерный режим».",
                "Как переключить программу в инженерный режим?"
            ],
            [
                "Старик сажал яблони. Ему говорили: «Зачем тебе эти яблони? Долго ждать с них плода, и ты не съешь с них яблочка». Старик ответил: «Я не съем – другие съедят, мне спасибо скажут». Эта притча отражает идею бескорыстной заботы о будущем. Многие философы считали, что смысл жизни заключается не только в личном благополучии, но и в том, чтобы оставить след, помочь следующим поколениям. В современном мире этот принцип можно увидеть в экологических движениях, посадке деревьев, создании общественных парков.",
                "Какой смысл вложен в притчу о старике, сажающем яблони?"
            ]
        ],
        inputs=[document_input, question_input],
        label="Примеры запросов"
    )

    # Функция обработки
    def process(document, question, model_name, max_tokens, temperature, repetition_penalty):
        answer, gen_time, tok_time_char, gen_time_token = generate_response(
            document, question, model_name, max_tokens, temperature, repetition_penalty
        )
        return answer, f"{gen_time:.3f}", f"{tok_time_char:.6f}", f"{gen_time_token:.6f}"

    submit_btn.click(
        fn=process,
        inputs=[document_input, question_input, model_selector, max_tokens_slider, temperature_slider, repetition_penalty_slider],
        outputs=[answer_output, latency_output, tok_time_output, gen_time_output]
    )

demo.launch()