File size: 10,692 Bytes
106cde4
6c363d4
9700948
42fa16e
 
 
c6c602e
1080c74
c6c602e
 
 
 
42fa16e
 
929276c
 
 
 
 
 
 
 
 
 
 
 
106cde4
929276c
e8fef7f
929276c
 
 
 
 
9700948
 
 
047143f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e8fef7f
106cde4
047143f
106cde4
 
 
 
b41467e
c6c602e
 
 
 
 
b41467e
047143f
106cde4
6c363d4
 
1080c74
6c363d4
 
1080c74
 
6c363d4
c6c602e
1080c74
047143f
1080c74
 
 
 
 
 
 
6c363d4
 
c6c602e
6c363d4
 
106cde4
e8fef7f
106cde4
 
047143f
106cde4
8db6a28
b41467e
 
6c363d4
b41467e
 
8db6a28
 
 
 
 
 
 
 
 
b41467e
 
8db6a28
b41467e
8db6a28
 
 
 
 
 
d833d70
 
 
b41467e
 
 
6c363d4
8db6a28
 
6c363d4
9700948
b41467e
c6c602e
1080c74
b41467e
1080c74
f314e13
b41467e
 
e8fef7f
b41467e
 
f314e13
b41467e
 
f314e13
1080c74
b41467e
 
 
1080c74
 
 
b41467e
8db6a28
106cde4
f314e13
b41467e
 
1080c74
c6c602e
106cde4
047143f
106cde4
c6c602e
 
106cde4
 
e8fef7f
 
 
106cde4
e8fef7f
106cde4
e8fef7f
106cde4
 
047143f
106cde4
 
 
 
 
047143f
106cde4
 
 
 
 
 
 
6c363d4
 
1080c74
6c363d4
 
 
 
 
 
 
 
 
 
 
 
 
f314e13
 
106cde4
6c363d4
106cde4
c6c602e
106cde4
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
import os
import re

import gradio as gr

from src.core.engine import engine
from src.utils.helpers import (
    extract_search_query,
    extract_text_from_file,
    get_clean_text,
    web_search,
)


def user_input(user_message, history):
    if not user_message:
        return None, history
    history = history or []
    history.append({"role": "user", "content": str(user_message)})
    return "", history


def set_interactive(is_interactive):
    return (
        gr.update(
            interactive=is_interactive,
            placeholder="Ожидайте, Агент думает..."
            if not is_interactive
            else "Напиши сообщение...",
        ),
        gr.update(interactive=is_interactive),
    )


def bot_response(
    history, system_prompt, temperature, max_tokens, use_search, uploaded_file
):
    # --- 1. ДИНАМИЧЕСКИЙ ПРОМПТ И ПРЕДОХРАНИТЕЛЬ ---
    actual_sys_prompt = system_prompt

    if use_search:
        system_guard = (
            "\n\n[ВАЖНО]: Отвечай пользователю напрямую обычным текстом. "
            "КАТЕГОРИЧЕСКИ ЗАПРЕЩАЕТСЯ выводить технические команды, массивы или JSON-вызовы (например [{'type': 'search'}]). "
            "Если тебе нужно рассуждать перед ответом, оборачивай свои мысли СТРОГО в теги <think>твои мысли</think>."
        )
    else:
        # Если галочка поиска выключена, удаляем из системного промпта призыв искать
        actual_sys_prompt = actual_sys_prompt.replace(
            "Если нужно узнать свежую информацию, используйте поиск.", ""
        )

        system_guard = (
            "\n\n[СИСТЕМНОЕ УВЕДОМЛЕНИЕ]: ВНИМАНИЕ! Функция поиска в интернете сейчас ОТКЛЮЧЕНА. "
            "Ты ДОЛЖЕН ответить, используя только свои внутренние знания. "
            "НИ В КОЕМ СЛУЧАЕ не пытайся генерировать команды поиска (например [{'type': 'search'}]). "
            "Просто дай лучший ответ, который можешь, основываясь на своей памяти. "
            "Если тебе нужно рассуждать, используй теги <think>мысли</think>."
        )

    messages = [{"role": "system", "content": actual_sys_prompt + system_guard}]
    file_info, file_content = "", ""

    # --- 2. ОБРАБОТКА ФАЙЛА ---
    if uploaded_file and os.path.exists(uploaded_file):
        filename = os.path.basename(uploaded_file)
        size_kb = os.path.getsize(uploaded_file) / 1024
        file_info = f"📎 **Файл прочитан:** `{filename}` ({size_kb:.1f} KB)"

        file_content = extract_text_from_file(uploaded_file)[:40000]
        if len(file_content) >= 40000:
            file_info += " *(загружен первый сегмент)*"
        if "[Ошибка" in file_content:
            file_info = f"❌ **Ошибка файла:** `{filename}`"

    # --- 3. ОЧИСТКА И ФОРМИРОВАНИЕ ИСТОРИИ ---
    for msg in history[-7:]:
        content = str(msg["content"])
        if msg["role"] == "assistant":
            # Убираем плашки UI
            if "---\n\n" in content:
                content = content.split("---\n\n", 1)[-1]

            # Тотально вырезаем теги размышлений и HTML спойлеры
            content = re.sub(r"<details.*?>.*?</details>", "", content, flags=re.DOTALL)
            content = re.sub(r"<think>.*?</think>", "", content, flags=re.DOTALL)

            # Вырезаем технические массивы поиска, если они проскочили в прошлых ответах
            content = re.sub(
                r"\[\s*\{.*?['\"]type['\"]\s*:\s*['\"]search['\"].*?\}\s*\]",
                "",
                content,
                flags=re.DOTALL,
            )

            content = content.strip()
            if not content:
                content = "*(ответ скрыт)*"

        messages.append({"role": msg["role"], "content": get_clean_text(content)})

    history.append({"role": "assistant", "content": "⏳ Инициализация..."})
    yield history

    # --- 4. АГЕНТ ПОИСКА (РОУТЕР) ---
    if use_search:
        search_info = ""
        history[-1]["content"] = (
            file_info + "\n" if file_info else ""
        ) + "🤔 Агент анализирует необходимость поиска..."
        yield history

        # Находим последний вопрос пользователя
        last_user_msg = ""
        for msg in reversed(history):
            if msg["role"] == "user":
                last_user_msg = msg["content"]
                break

        # Промпт Роутера: кладем инструкцию прямо в USER, чтобы модель не могла её проигнорировать.
        # Используем XML теги, LLM справляются с ними лучше, чем с JSON.
        agent_messages = [
            {
                "role": "user",
                "content": (
                    f"Пользователь спросил: «{last_user_msg}»\n\n"
                    "Определи, нужен ли поиск в интернете для ответа на этот запрос.\n"
                    "Любые вопросы про недавние события, атаки (например, на НПЗ), новости и точные факты ТРЕБУЮТ поиска.\n\n"
                    "ОТВЕТЬ СТРОГО ОДНИМ ИЗ ВАРИАНТОВ (без лишних слов):\n"
                    "Вариант 1 (поиск нужен): <search>твой поисковый запрос</search>\n"
                    "Вариант 2 (поиск не нужен): <no_search>"
                ),
            }
        ]

        try:
            eval_response = engine.generate(
                messages=agent_messages,
                max_tokens=400,  # <--- Увеличили, чтобы модель успела дописать тег поиска, если задумается
                temperature=0.0,
                stream=False,
            )

            raw_response = eval_response["choices"][0]["message"]["content"]
            clean_query = extract_search_query(raw_response)

            if clean_query:
                search_info = f'🌐 Ищем: *"{clean_query}"*...'
                history[-1]["content"] = (
                    f"{file_info + '\n' if file_info else ''}{search_info}"
                )
                yield history

                search_results = web_search(clean_query)

                if search_results:
                    search_info = f'🌐 Найдено {len(search_results)} результатов по запросу *"{clean_query}"*'
                    search_context = "СВЕЖИЕ РЕЗУЛЬТАТЫ ПОИСКА ИЗ ИНТЕРНЕТА:\n\n"
                    for i, r in enumerate(search_results, 1):
                        search_context += f"{i}. {r['title']} ({r['url']})\nСниппет: {r['snippet']}\n\n"

                    messages[0]["content"] += (
                        f"\n\n{search_context}\n\n[УВЕДОМЛЕНИЕ]: Поиск выполнен. Сформулируй ответ на основе контекста выше."
                    )
                else:
                    search_info = f'🌐 Поиск по запросу *"{clean_query}"* не дал результатов (возможно сработал лимит DuckDuckGo).'
            else:
                search_info = "⚡ Агент ответит из своих знаний (поиск не нужен)."

        except Exception as e:
            print(f"Ошибка Роутера: {e}")
            search_info = "⚡ Ошибка роутера. Отвечаю из базы знаний."

    # Добавляем контент файла в начало (чтобы модель опиралась на него при ответе)
    if file_content:
        messages[0]["content"] += (
            f"\n\nСодержимое файла '{os.path.basename(uploaded_file)}':\n\n{file_content}"
        )

    status_header = (file_info + "\n" if file_info else "") + (
        search_info + "\n" if search_info else ""
    )
    if status_header:
        status_header += "---\n\n"

    history[-1]["content"] = status_header + "⏳ Генерация ответа..."
    yield history

    # --- 5. СТРИМИНГ И УМНЫЙ UI ---
    try:
        stream = engine.generate(
            messages=messages,
            max_tokens=max_tokens,
            temperature=temperature,
            repeat_penalty=1.15,  # Штраф за зацикливание текста
            stream=True,
        )
        partial_text = ""
        for chunk in stream:
            delta = chunk["choices"][0].get("delta", {})
            if delta.get("content"):
                partial_text += delta["content"]
                display_text = partial_text

                # Умный UI для тегов <think>
                if "<think>" in partial_text and "</think>" not in partial_text:
                    display_text = (
                        partial_text.replace(
                            "<think>",
                            "<details open><summary><i>🧠 Идет анализ (в процессе)...</i></summary>\n\n",
                        )
                        + "\n\n</details>"
                    )
                elif "</think>" in partial_text:
                    display_text = partial_text.replace(
                        "<think>",
                        "<details><summary><i>🧠 Анализ завершен (нажмите, чтобы раскрыть)</i></summary>\n\n",
                    ).replace("</think>", "\n\n</details>\n\n")

                history[-1]["content"] = status_header + display_text
                yield history

    except Exception as e:
        history[-1]["content"] = status_header + f"\n\n❌ Ошибка генерации: {str(e)}"
        yield history