AnatoliiG commited on
Commit
b41467e
·
1 Parent(s): 9db46db

remake web search tool

Browse files
Files changed (2) hide show
  1. src/ui/callbacks.py +118 -66
  2. src/ui/styles.py +2 -2
src/ui/callbacks.py CHANGED
@@ -1,11 +1,8 @@
1
- # src/ui/callbacks.py
2
-
3
  import os
4
- import re
5
- import urllib.parse
6
 
7
  import gradio as gr
8
- import httpx
 
9
 
10
  from src.core.engine import engine
11
  from src.utils.helpers import get_clean_text
@@ -32,41 +29,27 @@ def set_interactive(is_interactive):
32
 
33
 
34
  def web_search(query: str, max_results: int = 3) -> list:
35
- headers = {
36
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
37
- }
38
- url = f"https://html.duckduckgo.com/html/?q={urllib.parse.quote(query)}"
39
  try:
40
- response = httpx.get(url, headers=headers, timeout=5.0)
41
- if response.status_code != 200:
42
- return []
43
- html = response.text
44
- titles = re.findall(r'<a class="result__a" href="([^"]+)">([^<]+)</a>', html)
45
- snippets = re.findall(r'<td class="result-snippet">([\s\S]*?)</td>', html)
46
- results = []
47
- for i in range(min(len(titles), len(snippets), max_results)):
48
- url_match = re.search(r"uddg=([^&]+)", titles[i][0])
49
- raw_url = (
50
- urllib.parse.unquote(url_match.group(1)) if url_match else titles[i][0]
51
- )
52
- snippet_clean = (
53
- re.sub(r"<[^>]+>", "", snippets[i])
54
- .strip()
55
- .replace("&amp;", "&")
56
- .replace("&quot;", '"')
57
- .replace("&lt;", "<")
58
- .replace("&gt;", ">")
59
- )
60
- results.append(
61
- {
62
- "title": titles[i][1].strip(),
63
- "url": raw_url,
64
- "snippet": snippet_clean,
65
- }
66
- )
67
- return results
68
  except Exception as e:
69
- print(f"Ошибка поиска: {e}")
70
  return []
71
 
72
 
@@ -76,20 +59,36 @@ def bot_response(
76
  messages = [{"role": "system", "content": system_prompt}]
77
  file_info, file_content = "", ""
78
 
79
- # 1. Чтение файла
 
 
80
  if uploaded_file and os.path.exists(uploaded_file):
81
  filename = os.path.basename(uploaded_file)
82
  size_kb = os.path.getsize(uploaded_file) / 1024
83
  file_info = f"📎 **Файл прочитан:** `{filename}` ({size_kb:.1f} KB)"
 
84
  try:
85
- with open(uploaded_file, "r", encoding="utf-8", errors="ignore") as f:
86
- file_content = f.read(40000)
87
- if len(file_content) >= 40000:
88
- file_info += " *(загружен первый сегмент)*"
 
 
 
 
 
 
 
 
 
 
 
89
  except Exception as e:
90
  file_info = f"❌ **Ошибка файла:** `{filename}` ({e})"
91
 
92
- # 2. Формирование контекста диалога
 
 
93
  for msg in history[-7:]:
94
  messages.append(
95
  {"role": msg["role"], "content": get_clean_text(msg["content"])}
@@ -98,33 +97,82 @@ def bot_response(
98
  history.append({"role": "assistant", "content": "⏳ Инициализация..."})
99
  yield history
100
 
101
- # 3. Веб-поиск
 
 
102
  search_info = ""
103
  if use_search:
104
- user_query = next(
105
- (msg["content"] for msg in reversed(messages) if msg["role"] == "user"), ""
106
- )
107
- if user_query:
108
- search_info = f'🌐 Ищем: *"{user_query}"*...'
109
- history[-1]["content"] = (
110
- f"{file_info + '\n' if file_info else ''}{search_info}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  )
112
- yield history
113
-
114
- search_results = web_search(user_query)
115
- if search_results:
116
- search_info = f"🌐 Найдено {len(search_results)} результатов."
117
- search_context = "Результаты поиска:\n" + "".join(
118
- [
119
- f"{i}. {r['title']} ({r['url']})\n{r['snippet']}\n\n"
120
- for i, r in enumerate(search_results, 1)
121
- ]
 
122
  )
123
- messages.append({"role": "system", "content": search_context})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  else:
125
- search_info = "🌐 Ничего не найдено."
 
 
 
 
126
 
127
- # Инжект файла
 
 
128
  if file_content:
129
  messages.append(
130
  {
@@ -133,6 +181,7 @@ def bot_response(
133
  }
134
  )
135
 
 
136
  status_header = (file_info + "\n" if file_info else "") + (
137
  search_info + "\n" if search_info else ""
138
  )
@@ -142,7 +191,9 @@ def bot_response(
142
  history[-1]["content"] = status_header + "⏳ Генерация ответа..."
143
  yield history
144
 
145
- # 4. Стриминг ответа от LLM
 
 
146
  try:
147
  stream = engine.generate(
148
  messages=messages,
@@ -155,6 +206,7 @@ def bot_response(
155
  delta = chunk["choices"][0].get("delta", {})
156
  if delta.get("content"):
157
  partial_text += delta["content"]
 
158
  history[-1]["content"] = status_header + partial_text
159
  yield history
160
  except Exception as e:
 
 
 
1
  import os
 
 
2
 
3
  import gradio as gr
4
+ import PyPDF2 # Для нормального чтения PDF
5
+ from duckduckgo_search import DDGS # Официальная библиотека поиска
6
 
7
  from src.core.engine import engine
8
  from src.utils.helpers import get_clean_text
 
29
 
30
 
31
  def web_search(query: str, max_results: int = 3) -> list:
32
+ """
33
+ Чистый и надежный поиск через API duckduckgo-search.
34
+ Никаких костылей с регулярками и парсингом HTML.
35
+ """
36
  try:
37
+ with DDGS() as ddgs:
38
+ # DDGS возвращает генератор, конвертируем в список
39
+ results = list(ddgs.text(query, max_results=max_results))
40
+
41
+ formatted_results = []
42
+ for r in results:
43
+ formatted_results.append(
44
+ {
45
+ "title": r.get("title", ""),
46
+ "url": r.get("href", ""),
47
+ "snippet": r.get("body", ""),
48
+ }
49
+ )
50
+ return formatted_results
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  except Exception as e:
52
+ print(f"Ошибка DDGS: {e}")
53
  return []
54
 
55
 
 
59
  messages = [{"role": "system", "content": system_prompt}]
60
  file_info, file_content = "", ""
61
 
62
+ # ==========================================
63
+ # 1. УМНОЕ ЧТЕНИЕ ФАЙЛОВ (Исправлен баг с PDF)
64
+ # ==========================================
65
  if uploaded_file and os.path.exists(uploaded_file):
66
  filename = os.path.basename(uploaded_file)
67
  size_kb = os.path.getsize(uploaded_file) / 1024
68
  file_info = f"📎 **Файл прочитан:** `{filename}` ({size_kb:.1f} KB)"
69
+
70
  try:
71
+ # Если это PDF - используем PyPDF2
72
+ if filename.lower().endswith(".pdf"):
73
+ with open(uploaded_file, "rb") as f:
74
+ pdf_reader = PyPDF2.PdfReader(f)
75
+ text_parts = [
76
+ page.extract_text() or "" for page in pdf_reader.pages
77
+ ]
78
+ file_content = "\n".join(text_parts)[:40000]
79
+ # Иначе читаем как обычный текст
80
+ else:
81
+ with open(uploaded_file, "r", encoding="utf-8", errors="ignore") as f:
82
+ file_content = f.read(40000)
83
+
84
+ if len(file_content) >= 40000:
85
+ file_info += " *(загружен первый сегмент)*"
86
  except Exception as e:
87
  file_info = f"❌ **Ошибка файла:** `{filename}` ({e})"
88
 
89
+ # ==========================================
90
+ # 2. ФОРМИРОВАНИЕ ИСТОРИИ
91
+ # ==========================================
92
  for msg in history[-7:]:
93
  messages.append(
94
  {"role": msg["role"], "content": get_clean_text(msg["content"])}
 
97
  history.append({"role": "assistant", "content": "⏳ Инициализация..."})
98
  yield history
99
 
100
+ # ==========================================
101
+ # 3. АГЕНТСКИЙ ВЕБ-ПОИСК (LLM ДУМАЕТ САМА)
102
+ # ==========================================
103
  search_info = ""
104
  if use_search:
105
+ history[-1]["content"] = (
106
+ file_info + "\n" if file_info else ""
107
+ ) + "🤔 Агент анализирует необходимость поиска..."
108
+ yield history
109
+
110
+ # Скрытый "внутренний диалог" LLM: просим ее саму написать запрос
111
+ agent_messages = [
112
+ {
113
+ "role": "system",
114
+ "content": (
115
+ "Ты AI-агент. Оцени, нужен ли поиск в интернете для ответа на последний вопрос пользователя. "
116
+ "Если вопрос требует свежих новостей, фактов или данных извне — напиши ТОЛЬКО поисковый запрос (2-5 слов, без кавычек и лишних слов). "
117
+ "Если поиск не нужен (это простое общение, приветствие, или ответ есть в тексте файла) — ответь ровно одним словом: NO_SEARCH."
118
+ ),
119
+ }
120
+ ]
121
+
122
+ # Даем агенту последние 3 сообщения, чтобы он понял контекст разговора
123
+ for msg in history[-3:]:
124
+ if msg["role"] == "user":
125
+ agent_messages.append({"role": "user", "content": msg["content"]})
126
+
127
+ try:
128
+ # Делаем быстрый, скрытый запрос к модели (stream=False)
129
+ eval_response = engine.generate(
130
+ messages=agent_messages,
131
+ max_tokens=20,
132
+ temperature=0.1, # Низкая температура, чтобы Агент не фантазировал, а был точным
133
+ stream=False,
134
  )
135
+
136
+ generated_query = eval_response["choices"][0]["message"]["content"].strip()
137
+ generated_query = generated_query.replace('"', "").replace(
138
+ "'", ""
139
+ ) # Очищаем запрос от случайных кавычек
140
+
141
+ # Если Агент решил, что поиск нужен
142
+ if generated_query and "NO_SEARCH" not in generated_query.upper():
143
+ search_info = f'🌐 Агент ищет: *"{generated_query}"*...'
144
+ history[-1]["content"] = (
145
+ f"{file_info + '\n' if file_info else ''}{search_info}"
146
  )
147
+ yield history
148
+
149
+ search_results = web_search(generated_query)
150
+
151
+ if search_results:
152
+ search_info = f'🌐 Найдено {len(search_results)} результатов по запросу *"{generated_query}"*'
153
+
154
+ search_context = (
155
+ "СВЕЖИЕ РЕЗУЛЬТАТЫ ПОИСКА ИЗ ИНТЕРНЕТА ДЛЯ ТВОЕГО ОТВЕТА:\n\n"
156
+ )
157
+ for i, r in enumerate(search_results, 1):
158
+ search_context += f"{i}. {r['title']} ({r['url']})\nСниппет: {r['snippet']}\n\n"
159
+
160
+ # Незаметно подсовываем результаты в системный промпт основной модели
161
+ messages.append({"role": "system", "content": search_context})
162
+ else:
163
+ search_info = (
164
+ f'🌐 Поиск по запросу *"{generated_query}"* не дал результатов.'
165
+ )
166
  else:
167
+ search_info = "⚡ Агент решил отвечать на основе собственных знаний (поиск не потребовался)."
168
+
169
+ except Exception as e:
170
+ print(f"Ошибка при логике Агента: {e}")
171
+ pass # Если скрытый запрос упал, просто продолжаем стандартную генерацию без поиска
172
 
173
+ # ==========================================
174
+ # 4. ИНЖЕКТ ФАЙЛА В КОНТЕКСТ
175
+ # ==========================================
176
  if file_content:
177
  messages.append(
178
  {
 
181
  }
182
  )
183
 
184
+ # Формируем красивую плашку статуса перед финальным ответом
185
  status_header = (file_info + "\n" if file_info else "") + (
186
  search_info + "\n" if search_info else ""
187
  )
 
191
  history[-1]["content"] = status_header + "⏳ Генерация ответа..."
192
  yield history
193
 
194
+ # ==========================================
195
+ # 5. СТРИМИНГ ФИНАЛЬНОГО ОТВЕТА
196
+ # ==========================================
197
  try:
198
  stream = engine.generate(
199
  messages=messages,
 
206
  delta = chunk["choices"][0].get("delta", {})
207
  if delta.get("content"):
208
  partial_text += delta["content"]
209
+ # Показываем текст под плашкой статусов
210
  history[-1]["content"] = status_header + partial_text
211
  yield history
212
  except Exception as e:
src/ui/styles.py CHANGED
@@ -30,7 +30,7 @@ body, .gradio-container {
30
  textarea {
31
  background-color: #2f2f2f !important;
32
  border: 1px solid #424242 !important;
33
- border-radius: 24px !important;
34
  color: #ffffff !important;
35
  padding: 12px 18px !important;
36
  }
@@ -40,7 +40,7 @@ textarea:focus {
40
 
41
  /* Кнопка отправки */
42
  .gr-button-primary {
43
- border-radius: 24px !important;
44
  background-color: #ececec !important;
45
  color: #171717 !important;
46
  font-weight: bold !important;
 
30
  textarea {
31
  background-color: #2f2f2f !important;
32
  border: 1px solid #424242 !important;
33
+ border-radius: 1.5rem !important;
34
  color: #ffffff !important;
35
  padding: 12px 18px !important;
36
  }
 
40
 
41
  /* Кнопка отправки */
42
  .gr-button-primary {
43
+ border-radius: 1.5rem !important;
44
  background-color: #ececec !important;
45
  color: #171717 !important;
46
  font-weight: bold !important;