KennyOry commited on
Commit
f97a528
·
verified ·
1 Parent(s): 601e79d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +272 -162
app.py CHANGED
@@ -1,4 +1,4 @@
1
- from flask import Flask, render_template, request, Response, jsonify, session
2
  from mistralai import Mistral
3
  import logging
4
  import time
@@ -11,21 +11,65 @@ import os
11
  import trafilatura
12
  from bs4 import BeautifulSoup
13
  import random
 
14
 
15
  app = Flask(__name__)
16
  app.secret_key = 'super_secret_key'
17
- app.config['SESSION_TYPE'] = 'filesystem'
18
-
19
  message_queue = queue.Queue()
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  # Конфигурация Mistral
22
  MISTRAL_MODEL = "mistral-large-latest"
23
  N_CTX = 32768
24
  MAX_RESULTS = 5
25
- MAX_CONTENT_LENGTH = 10000
26
- MAX_HISTORY_LENGTH = 5 # Максимальное количество сообщений в истории
27
 
28
- # Клиент Mistral
29
  mistral_client = Mistral(api_key=os.getenv("MISTRAL_API_KEY"))
30
 
31
  SYSTEM_PROMPT = """
@@ -49,14 +93,6 @@ SYSTEM_PROMPT = """
49
  6. Будь лаконичен, но технически точен
50
  """
51
 
52
- SYSTEM_PROMPT_CONTINUE = """
53
- Ты PrintMaster, сервисный инженер. Продолжаешь диалог с пользователем. Правила:
54
- 1. Отвечай на основе предыдущего контекста диалога
55
- 2. Будь лаконичным и технически точным
56
- 3. Если вопрос не связан с предыдущей проблемой, вежливо предложи начать новый диалог
57
- 4. Сохраняй профессиональный стиль общения
58
- """
59
-
60
  BLACKLISTED_DOMAINS = [
61
  'reddit.com',
62
  'stackoverflow.com',
@@ -76,7 +112,7 @@ USER_AGENTS = [
76
  "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
77
  "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
78
  "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15",
79
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15"
80
  ]
81
 
82
  logging.basicConfig(
@@ -100,26 +136,29 @@ def get_random_headers():
100
  }
101
 
102
  def extract_main_content(html, url):
103
- """Извлекает основной контент страницы"""
104
  try:
 
105
  content = trafilatura.extract(html, include_links=False, include_tables=False)
106
  if content and len(content) > 500:
107
  return content[:MAX_CONTENT_LENGTH]
108
  except Exception as e:
109
  logging.error(f"Trafilatura error: {str(e)}")
110
 
 
111
  try:
112
  soup = BeautifulSoup(html, 'html.parser')
113
-
114
  for element in soup(['script', 'style', 'header', 'footer', 'nav', 'aside', 'form']):
115
  element.decompose()
116
 
 
117
  main_content = soup.find('main') or soup.find('article') or soup.find('div', class_=re.compile('content|main|article|post', re.I))
118
-
119
  if main_content:
120
  text = main_content.get_text(separator='\n', strip=True)
121
  return text[:MAX_CONTENT_LENGTH] if text else None
122
 
 
123
  return soup.body.get_text(separator='\n', strip=True)[:MAX_CONTENT_LENGTH]
124
  except Exception as e:
125
  logging.error(f"BeautifulSoup error: {str(e)}")
@@ -134,7 +173,6 @@ def generate_search_query(prompt: str) -> dict:
134
  - error_code: error code (if present)
135
  - problem_description: brief English problem description (1-2 sentences)
136
  - search_query: full English search query
137
-
138
  Important rules:
139
  1. All fields MUST be in English
140
  2. For brands use official English names
@@ -142,7 +180,6 @@ def generate_search_query(prompt: str) -> dict:
142
  4. If error code is specified - include it in search_query
143
  5. Problem description should be concise technical terms (max 7 words)
144
  """
145
-
146
  try:
147
  response = mistral_client.chat.complete(
148
  model=MISTRAL_MODEL,
@@ -154,9 +191,7 @@ def generate_search_query(prompt: str) -> dict:
154
  max_tokens=350,
155
  response_format={"type": "json_object"}
156
  )
157
-
158
  json_data = json.loads(response.choices[0].message.content)
159
-
160
  required_fields = ['brand', 'model', 'error_code', 'problem_description', 'search_query']
161
  for field in required_fields:
162
  if field not in json_data:
@@ -179,7 +214,6 @@ def generate_search_query(prompt: str) -> dict:
179
  json_data['search_query'] = " ".join(search_parts).strip()
180
 
181
  return json_data
182
-
183
  except Exception as e:
184
  error_msg = f"❌ Ошибка извлечения данных: {str(e)}"
185
  message_queue.put(('log', error_msg))
@@ -195,7 +229,6 @@ def web_search(query: str) -> tuple:
195
  try:
196
  message_queue.put(('log', f"🔍 Провожу поиск по запросу: {query}"))
197
  start_time = time.time()
198
-
199
  params = {
200
  "api_key": os.getenv("SERPAPI_KEY"),
201
  "engine": "google",
@@ -205,35 +238,36 @@ def web_search(query: str) -> tuple:
205
  "num": 10,
206
  "safe": "off",
207
  }
208
-
209
  response = requests.get("https://serpapi.com/search", params=params, timeout=15)
210
  response.raise_for_status()
211
  data = response.json()
212
-
213
  combined_content = ""
214
  sources = []
215
  full_contents = []
216
-
 
217
  featured_snippet = data.get("featured_snippet", {})
218
  if featured_snippet:
219
  snippet = featured_snippet.get("snippet", "")
220
  if snippet:
221
- combined_content += f"[Автоответ Google]\n{snippet}\n\n"
222
  sources.insert(0, {
223
  "title": "Google — автоматический ответ",
224
  "url": f"https://www.google.com/search?q={requests.utils.quote(query)}",
225
  "content": snippet
226
  })
227
-
 
228
  organic_results = data.get("organic_results", [])
229
- for i, res in enumerate(organic_results[:5]):
230
  title = res.get("title", "Без заголовка")
231
  link = res.get("link", "#")
232
  snippet = res.get("snippet", "") or ""
233
-
234
  if any(domain in link for domain in BLACKLISTED_DOMAINS):
235
  continue
236
-
 
237
  content = None
238
  try:
239
  headers = get_random_headers()
@@ -244,11 +278,13 @@ def web_search(query: str) -> tuple:
244
  logging.error(f"Ошибка загрузки {link}: {str(e)}")
245
 
246
  if not content:
247
- content = snippet
248
 
 
249
  cleaned_content = re.sub(r'\s+', ' ', content).strip()
250
- combined_content += f"[[Источник {i+1}]] {title}\n{cleaned_content}\n\n"
251
 
 
252
  source_data = {
253
  "title": title,
254
  "url": link,
@@ -256,46 +292,51 @@ def web_search(query: str) -> tuple:
256
  }
257
  sources.append(source_data)
258
  full_contents.append(cleaned_content[:MAX_CONTENT_LENGTH])
259
-
260
  elapsed = time.time() - start_time
261
  message_queue.put(('log', f"✅ Поиск был произведен за {elapsed:.2f}с. Найдено {len(sources)} источников."))
262
  return combined_content[:20000], sources
263
-
264
  except Exception as e:
265
  error_msg = f"❌ SerpAPI ошибка: {str(e)}"
266
  message_queue.put(('log', error_msg))
267
  return f"Поиск недоступен: {str(e)}", []
268
 
269
-
270
  def clean_response(response: str, sources: list) -> str:
 
271
  response = re.sub(r'</?assistant>|<\|system\|>|</s>', '', response, flags=re.IGNORECASE)
 
272
  response = re.sub(r'^-{3,}\s*', '', response)
 
273
  response = re.sub(r'(\*\*Проблема:\*\*.+?)(\*\*Проблема:\*\*)', r'\1', response, flags=re.DOTALL)
274
  response = re.sub(r'(\*\*Решение:\*\*.+?)(\*\*Решение:\*\*)', r'\1', response, flags=re.DOTALL)
275
- response = re.sub(r'\n\s*\n', '\n\n', response)
 
276
  response = re.sub(r'[ \t]{2,}', ' ', response)
 
277
  response = re.sub(r'^Вот исправленный ответ[^:]+:\s*', '', response)
 
278
  response = re.sub(r'^---\s*Примечания:\s*', '**Примечания:**\n', response)
279
- response = re.sub(r'^---\s*Источники:\s*', '**Источники информации:**\n', response)
 
 
280
  response = re.sub(r'^---\s*', '', response, flags=re.MULTILINE)
 
281
  response = re.sub(r'\s*\.{3,}\s*$', '', response)
282
  return response.strip()
283
 
284
  def verify_with_sources(response: str, sources: list) -> str:
 
285
  try:
286
  message_queue.put(('log', "🔍 Проверяю соответствие ответа источникам..."))
287
-
288
- sources_text = "\n\n".join([
289
  f"Источник {i+1} ({source['title']}):\n{source['content'][:1500]}"
290
  for i, source in enumerate(sources)
291
  ])
292
 
293
  verification_prompt = f"""
294
  Проверь соответствие решения источникам:
295
-
296
  ### Ответ бота:
297
  {response}
298
-
299
  ### Источники:
300
  {sources_text}
301
 
@@ -319,50 +360,60 @@ def verify_with_sources(response: str, sources: list) -> str:
319
 
320
  verified_response = verification.choices[0].message.content
321
  return verified_response.strip()
322
-
323
  except Exception as e:
324
  error_msg = f"❌ Ошибка верификации: {str(e)}"
325
  message_queue.put(('log', error_msg))
326
  return response
327
 
328
-
329
- def process_query(prompt: str):
330
  try:
 
 
 
 
 
 
 
 
 
 
331
  start_time = time.time()
332
- message_queue.put(('log', f"👤 Запрос: {prompt}"))
333
 
334
- # Проверяем тип запроса
335
- if prompt.strip().lower() == "/repeat":
336
- message_queue.put(('log', "🔄 Пользователь запросил повтор последнего ответа"))
337
- if 'last_response' in session:
338
- message_queue.put(('response', session['last_response']))
339
- message_queue.put(('sources', session.get('last_sources', '')))
340
- message_queue.put(('done', ''))
341
- return
342
- else:
343
- message_queue.put(('response', " Нет предыдущего ответа для повтора"))
344
- message_queue.put(('done', ''))
345
- return
 
 
 
 
 
 
346
 
347
- # Инициализируем историю диалога
348
- if 'history' not in session:
349
- session['history'] = []
350
- new_dialog = True
351
- else:
352
- new_dialog = False
353
-
354
- # Определяем, нужен ли поиск
355
- need_search = new_dialog or not any(msg['role'] == 'assistant' for msg in session['history'])
356
 
357
- # Для новых диалогов или первого сообщения - делаем поиск
358
- if need_search:
359
  message_queue.put(('log', "⚙️ Извлекаю параметры из входящего запроса"))
360
  norm_data = generate_search_query(prompt)
361
  message_queue.put(('log', f"⏏️ Извлечено: {json.dumps(norm_data, ensure_ascii=False)}"))
362
 
363
  search_query = norm_data['search_query']
 
 
364
  search_data, sources = web_search(search_query)
365
- message_queue.put(('log', f"📚 Собрано: {len(search_data)} символов в {len(sources)} источнике(-ах)"))
366
 
367
  message_queue.put(('log', f"⚙️ Определяю проблему"))
368
  problem_response = mistral_client.chat.complete(
@@ -375,120 +426,180 @@ def process_query(prompt: str):
375
  temperature=0.2
376
  )
377
  extracted_problem = problem_response.choices[0].message.content.strip()
378
-
379
  if not extracted_problem or len(extracted_problem) < 5:
380
  extracted_problem = f"Неисправность {norm_data['brand']} {norm_data['model']}"
381
-
382
  message_queue.put(('log', f"🧩 Определённая проблема: {extracted_problem}"))
383
-
384
- # Формируем системный промпт с результатами поиска
385
- system_prompt = SYSTEM_PROMPT + f"""
386
- Контекст:
387
- Бренд: {norm_data['brand']}
388
- Модель: {norm_data['model']}
389
- Ошибка: {norm_data['error_code']}
390
- Суть проблемы (на основе поиска): {extracted_problem}
391
- Данные поиска:
392
- {search_data}
393
- """
394
- else:
395
- # Для продолжения диалога используем упрощенный системный промпт
396
- system_prompt = SYSTEM_PROMPT_CONTINUE
397
- sources = []
398
- message_queue.put(('log', "💬 Продолжаю диалог на основе истории"))
399
-
400
- # Собираем все сообщения для модели
401
- messages = [{"role": "system", "content": system_prompt}]
402
-
403
- # Добавляем историю диалога (ограниченную по длине)
404
- history = session['history'][-MAX_HISTORY_LENGTH*2:] # Берем последние N пар сообщений
405
- messages.extend(history)
406
-
407
- # Добавляем текущий запрос пользователя
408
- messages.append({"role": "user", "content": prompt})
409
-
410
- message_queue.put(('log', "🧠 На основе полученных данных генерирую ответ..."))
411
-
412
- # Генерация ответа
413
- full_response = ""
414
- for chunk in mistral_client.chat.stream(
415
- model=MISTRAL_MODEL,
416
- messages=messages,
417
- max_tokens=2048,
418
- temperature=0.3
419
- ):
420
- if chunk.data.choices[0].delta.content is not None:
421
- chunk_text = chunk.data.choices[0].delta.content
422
- full_response += chunk_text
423
-
424
- # Для новых запросов делаем верификацию
425
- if need_search and sources:
426
  verified_response = verify_with_sources(full_response, sources)
 
427
  final_response = clean_response(verified_response, sources)
428
- else:
429
- final_response = clean_response(full_response, [])
430
-
431
- # Обновляем историю диалога
432
- session['history'].append({"role": "user", "content": prompt})
433
- session['history'].append({"role": "assistant", "content": final_response})
434
-
435
- # Сохраняем последний ответ для возможного повтора
436
- session['last_response'] = final_response
437
- if sources:
438
- session['last_sources'] = json.dumps(sources)
439
-
440
- # Отправляем ответ
441
- message_queue.put(('response', final_response))
442
- if sources:
443
  message_queue.put(('sources', json.dumps(sources)))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
444
 
 
445
  total_time = time.time() - start_time
446
- message_queue.put(('log', f"💡 Ответ сгенерирован"))
447
- message_queue.put(('log', f"⏱ Время обработки: {total_time:.1f}с"))
448
  message_queue.put(('done', ''))
449
-
450
  except Exception as e:
451
- error_msg = f"❌ Ошибка обработки: {str(e)}"
452
  message_queue.put(('log', error_msg))
453
- message_queue.put(('response', "\n⚠️ Произошла ошибка при обработке запроса"))
454
  message_queue.put(('done', ''))
455
 
456
-
457
  @app.route('/')
458
  def index():
459
- # Инициализируем сессию при первом заходе
460
- if 'history' not in session:
461
- session['history'] = []
462
- return render_template('index.html')
463
-
 
 
 
464
 
465
  @app.route('/ask', methods=['POST'])
466
  def ask():
467
  user_input = request.form['message']
468
- thread = threading.Thread(target=process_query, args=(user_input,))
 
 
 
 
 
469
  thread.daemon = True
470
  thread.start()
471
- return jsonify({'status': 'processing'})
472
-
 
 
 
473
 
474
- @app.route('/repeat', methods=['POST'])
475
  def repeat_last():
476
- """Повтор последнего ответа"""
477
- thread = threading.Thread(target=process_query, args=("/repeat",))
478
- thread.daemon = True
479
- thread.start()
480
- return jsonify({'status': 'repeating'})
481
-
482
-
483
- @app.route('/new', methods=['POST'])
484
- def new_dialog():
485
- """Начало нового диалога"""
486
- session.pop('history', None)
487
- session.pop('last_response', None)
488
- session.pop('last_sources', None)
489
- session['history'] = []
490
- return jsonify({'status': 'new_dialog_created'})
 
 
 
 
 
 
491
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
492
 
493
  @app.route('/stream')
494
  def stream():
@@ -502,6 +613,5 @@ def stream():
502
  time.sleep(0.1)
503
  return Response(generate(), mimetype='text/event-stream')
504
 
505
-
506
  if __name__ == '__main__':
507
  app.run(host='0.0.0.0', port=7860, debug=False)
 
1
+ from flask import Flask, render_template, request, Response, jsonify, make_response
2
  from mistralai import Mistral
3
  import logging
4
  import time
 
11
  import trafilatura
12
  from bs4 import BeautifulSoup
13
  import random
14
+ from uuid import uuid4
15
 
16
  app = Flask(__name__)
17
  app.secret_key = 'super_secret_key'
 
 
18
  message_queue = queue.Queue()
19
 
20
+ # Добавляем хранилище для диалогов
21
+ conversations = {}
22
+ MAX_CONVERSATION_LENGTH = 10 # Максимальная длина истории диалога
23
+
24
+ class Conversation:
25
+ def __init__(self):
26
+ self.history = []
27
+ self.last_sources = []
28
+ self.last_search_query = ""
29
+ self.last_problem = ""
30
+ self.last_norm_data = {}
31
+ self.last_full_response = ""
32
+ self.timestamp = time.time()
33
+
34
+ def add_exchange(self, user_message, bot_response, sources=None,
35
+ search_query=None, problem=None, norm_data=None, full_response=None):
36
+ self.history.append({
37
+ "user": user_message,
38
+ "bot": bot_response
39
+ })
40
+ self.timestamp = time.time()
41
+
42
+ if sources is not None:
43
+ self.last_sources = sources
44
+ if search_query is not None:
45
+ self.last_search_query = search_query
46
+ if problem is not None:
47
+ self.last_problem = problem
48
+ if norm_data is not None:
49
+ self.last_norm_data = norm_data
50
+ if full_response is not None:
51
+ self.last_full_response = full_response
52
+
53
+ # Ограничиваем длину истории
54
+ if len(self.history) > MAX_CONVERSATION_LENGTH:
55
+ self.history.pop(0)
56
+
57
+ def get_context(self):
58
+ """Возвращает контекст предыдущего диалога в читаемом формате"""
59
+ context = f"Предыдущая проблема: {self.last_problem}\n\n"
60
+ context += f"Последний ответ системы:\n{self.last_full_response}\n\n"
61
+ context += "Источники информации:\n"
62
+ for i, source in enumerate(self.last_sources[:3]): # Ограничиваем количество источников в контексте
63
+ context += f"[{i+1}] {source['title'][:50]}... - {source['url']}\n"
64
+ return context
65
+
66
  # Конфигурация Mistral
67
  MISTRAL_MODEL = "mistral-large-latest"
68
  N_CTX = 32768
69
  MAX_RESULTS = 5
70
+ MAX_CONTENT_LENGTH = 10000 # Максимальная длина контента на источник
 
71
 
72
+ # Новый клиент Mistral
73
  mistral_client = Mistral(api_key=os.getenv("MISTRAL_API_KEY"))
74
 
75
  SYSTEM_PROMPT = """
 
93
  6. Будь лаконичен, но технически точен
94
  """
95
 
 
 
 
 
 
 
 
 
96
  BLACKLISTED_DOMAINS = [
97
  'reddit.com',
98
  'stackoverflow.com',
 
112
  "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
113
  "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
114
  "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15",
115
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15"
116
  ]
117
 
118
  logging.basicConfig(
 
136
  }
137
 
138
  def extract_main_content(html, url):
139
+ """Извлекает основной контент страницы с помощью trafilatura или BeautifulSoup"""
140
  try:
141
+ # Пробуем trafilatura
142
  content = trafilatura.extract(html, include_links=False, include_tables=False)
143
  if content and len(content) > 500:
144
  return content[:MAX_CONTENT_LENGTH]
145
  except Exception as e:
146
  logging.error(f"Trafilatura error: {str(e)}")
147
 
148
+ # Fallback на BeautifulSoup
149
  try:
150
  soup = BeautifulSoup(html, 'html.parser')
151
+ # Удаляем ненужные элементы
152
  for element in soup(['script', 'style', 'header', 'footer', 'nav', 'aside', 'form']):
153
  element.decompose()
154
 
155
+ # Пытаемся найти основной контент
156
  main_content = soup.find('main') or soup.find('article') or soup.find('div', class_=re.compile('content|main|article|post', re.I))
 
157
  if main_content:
158
  text = main_content.get_text(separator='\n', strip=True)
159
  return text[:MAX_CONTENT_LENGTH] if text else None
160
 
161
+ # Fallback: весь текст body
162
  return soup.body.get_text(separator='\n', strip=True)[:MAX_CONTENT_LENGTH]
163
  except Exception as e:
164
  logging.error(f"BeautifulSoup error: {str(e)}")
 
173
  - error_code: error code (if present)
174
  - problem_description: brief English problem description (1-2 sentences)
175
  - search_query: full English search query
 
176
  Important rules:
177
  1. All fields MUST be in English
178
  2. For brands use official English names
 
180
  4. If error code is specified - include it in search_query
181
  5. Problem description should be concise technical terms (max 7 words)
182
  """
 
183
  try:
184
  response = mistral_client.chat.complete(
185
  model=MISTRAL_MODEL,
 
191
  max_tokens=350,
192
  response_format={"type": "json_object"}
193
  )
 
194
  json_data = json.loads(response.choices[0].message.content)
 
195
  required_fields = ['brand', 'model', 'error_code', 'problem_description', 'search_query']
196
  for field in required_fields:
197
  if field not in json_data:
 
214
  json_data['search_query'] = " ".join(search_parts).strip()
215
 
216
  return json_data
 
217
  except Exception as e:
218
  error_msg = f"❌ Ошибка извлечения данных: {str(e)}"
219
  message_queue.put(('log', error_msg))
 
229
  try:
230
  message_queue.put(('log', f"🔍 Провожу поиск по запросу: {query}"))
231
  start_time = time.time()
 
232
  params = {
233
  "api_key": os.getenv("SERPAPI_KEY"),
234
  "engine": "google",
 
238
  "num": 10,
239
  "safe": "off",
240
  }
 
241
  response = requests.get("https://serpapi.com/search", params=params, timeout=15)
242
  response.raise_for_status()
243
  data = response.json()
 
244
  combined_content = ""
245
  sources = []
246
  full_contents = []
247
+
248
+ # Обработка featured snippet
249
  featured_snippet = data.get("featured_snippet", {})
250
  if featured_snippet:
251
  snippet = featured_snippet.get("snippet", "")
252
  if snippet:
253
+ combined_content += f"[Автоответ Google]\n{snippet}\n"
254
  sources.insert(0, {
255
  "title": "Google — автоматический ответ",
256
  "url": f"https://www.google.com/search?q={requests.utils.quote(query)}",
257
  "content": snippet
258
  })
259
+
260
+ # Обработка organic results
261
  organic_results = data.get("organic_results", [])
262
+ for i, res in enumerate(organic_results[:5]): # Ограничиваемся топ-5
263
  title = res.get("title", "Без заголовка")
264
  link = res.get("link", "#")
265
  snippet = res.get("snippet", "") or ""
266
+
267
  if any(domain in link for domain in BLACKLISTED_DOMAINS):
268
  continue
269
+
270
+ # Загрузка полного контента
271
  content = None
272
  try:
273
  headers = get_random_headers()
 
278
  logging.error(f"Ошибка загрузки {link}: {str(e)}")
279
 
280
  if not content:
281
+ content = snippet # Fallback на сниппет
282
 
283
+ # Форматирование контента
284
  cleaned_content = re.sub(r'\s+', ' ', content).strip()
285
+ combined_content += f"[[Источник {i+1}]] {title}\n{cleaned_content}\n"
286
 
287
+ # Сохранение источника
288
  source_data = {
289
  "title": title,
290
  "url": link,
 
292
  }
293
  sources.append(source_data)
294
  full_contents.append(cleaned_content[:MAX_CONTENT_LENGTH])
295
+
296
  elapsed = time.time() - start_time
297
  message_queue.put(('log', f"✅ Поиск был произведен за {elapsed:.2f}с. Найдено {len(sources)} источников."))
298
  return combined_content[:20000], sources
 
299
  except Exception as e:
300
  error_msg = f"❌ SerpAPI ошибка: {str(e)}"
301
  message_queue.put(('log', error_msg))
302
  return f"Поиск недоступен: {str(e)}", []
303
 
 
304
  def clean_response(response: str, sources: list) -> str:
305
+ # Удаление служебных тегов
306
  response = re.sub(r'</?assistant>|<\|system\|>|</s>', '', response, flags=re.IGNORECASE)
307
+ # Удаление лишних разделителей
308
  response = re.sub(r'^-{3,}\s*', '', response)
309
+ # Удаление дублирования разделов
310
  response = re.sub(r'(\*\*Проблема:\*\*.+?)(\*\*Проблема:\*\*)', r'\1', response, flags=re.DOTALL)
311
  response = re.sub(r'(\*\*Решение:\*\*.+?)(\*\*Решение:\*\*)', r'\1', response, flags=re.DOTALL)
312
+ # Удаление лишних переносов
313
+ response = re.sub(r'\n\s*\n', '\n', response)
314
  response = re.sub(r'[ \t]{2,}', ' ', response)
315
+ # Удаление начальных фраз
316
  response = re.sub(r'^Вот исправленный ответ[^:]+:\s*', '', response)
317
+ # Форматирование примечаний
318
  response = re.sub(r'^---\s*Примечания:\s*', '**Примечания:**\n', response)
319
+ # Форматирование источников
320
+ response = re.sub(r'^---\s*Источники:\s*', '**Источники:**\n', response)
321
+ # Удаление лишних маркеров
322
  response = re.sub(r'^---\s*', '', response, flags=re.MULTILINE)
323
+ # Очистка завершающих символов
324
  response = re.sub(r'\s*\.{3,}\s*$', '', response)
325
  return response.strip()
326
 
327
  def verify_with_sources(response: str, sources: list) -> str:
328
+ """Проверяет соответствие ответа источникам с помощью LLM"""
329
  try:
330
  message_queue.put(('log', "🔍 Проверяю соответствие ответа источникам..."))
331
+ sources_text = "\n".join([
 
332
  f"Источник {i+1} ({source['title']}):\n{source['content'][:1500]}"
333
  for i, source in enumerate(sources)
334
  ])
335
 
336
  verification_prompt = f"""
337
  Проверь соответствие решения источникам:
 
338
  ### Ответ бота:
339
  {response}
 
340
  ### Источники:
341
  {sources_text}
342
 
 
360
 
361
  verified_response = verification.choices[0].message.content
362
  return verified_response.strip()
 
363
  except Exception as e:
364
  error_msg = f"❌ Ошибка верификации: {str(e)}"
365
  message_queue.put(('log', error_msg))
366
  return response
367
 
368
+ def process_query(prompt: str, conversation_id: str):
 
369
  try:
370
+ conversation = conversations.get(conversation_id)
371
+ is_new_conversation = False
372
+
373
+ # Создаем новый диалог, если его нет
374
+ if not conversation:
375
+ conversation = Conversation()
376
+ conversations[conversation_id] = conversation
377
+ is_new_conversation = True
378
+ message_queue.put(('log', "🔄 Начат новый диалог"))
379
+
380
  start_time = time.time()
381
+ message_queue.put(('log', f"👤 Получен запрос: {prompt}"))
382
 
383
+ # Проверяем, является ли запрос продолжение�� диалога
384
+ is_follow_up = False
385
+ if not is_new_conversation and conversation.last_problem:
386
+ message_queue.put(('log', "🔍 Анализирую, является ли запрос продолжением диалога..."))
387
+
388
+ # Запрашиваем у ИИ анализ контекста
389
+ follow_up_check = mistral_client.chat.complete(
390
+ model=MISTRAL_MODEL,
391
+ messages=[
392
+ {"role": "system", "content": "Определи, является ли следующий запрос продолжением предыдущего диалога о конкретной проблеме с принтером и требует ли он нового поиска информации. Ответь только 'да' или 'нет'."},
393
+ {"role": "user", "content": f"Предыдущая проблема: {conversation.last_problem}\nТекущий запрос: {prompt}"}
394
+ ],
395
+ max_tokens=10,
396
+ temperature=0.1
397
+ )
398
+
399
+ is_follow_up = 'да' in follow_up_check.choices[0].message.content.lower()
400
+ message_queue.put(('log', f"🤖 Анализ показал, что запрос {'является продолжением' if is_follow_up else 'требует нового поиска'}"))
401
 
402
+ full_response = ""
403
+ final_response = ""
404
+ sources = []
 
 
 
 
 
 
405
 
406
+ if not is_follow_up or is_new_conversation:
407
+ # Полная обработка с поиском (как в оригинале)
408
  message_queue.put(('log', "⚙️ Извлекаю параметры из входящего запроса"))
409
  norm_data = generate_search_query(prompt)
410
  message_queue.put(('log', f"⏏️ Извлечено: {json.dumps(norm_data, ensure_ascii=False)}"))
411
 
412
  search_query = norm_data['search_query']
413
+ message_queue.put(('log', f"🌐 Формирую поисковый запрос: {search_query}"))
414
+
415
  search_data, sources = web_search(search_query)
416
+ message_queue.put(('log', f"📚 Собрано: {len(search_data)} символов в {len(sources)} источниках"))
417
 
418
  message_queue.put(('log', f"⚙️ Определяю проблему"))
419
  problem_response = mistral_client.chat.complete(
 
426
  temperature=0.2
427
  )
428
  extracted_problem = problem_response.choices[0].message.content.strip()
 
429
  if not extracted_problem or len(extracted_problem) < 5:
430
  extracted_problem = f"Неисправность {norm_data['brand']} {norm_data['model']}"
 
431
  message_queue.put(('log', f"🧩 Определённая проблема: {extracted_problem}"))
432
+
433
+ sources_text = "\n".join([f"[{i+1}] {s['title']} - {s['url']}" for i, s in enumerate(sources)])
434
+
435
+ messages = [
436
+ {"role": "system", "content": SYSTEM_PROMPT + f"""
437
+ Контекст:
438
+ Бренд: {norm_data['brand']}
439
+ Модель: {norm_data['model']}
440
+ Ошибка: {norm_data['error_code']}
441
+ Суть проблемы (на основе поиска): {extracted_problem}
442
+ Данные поиска:
443
+ {search_data}
444
+ """},
445
+ {"role": "user", "content": f"Проблема: {prompt}"}
446
+ ]
447
+
448
+ message_queue.put(('log', "🧠 На основе полученных данных генерирую ответ..."))
449
+ message_queue.put(('response_start', ""))
450
+
451
+ for chunk in mistral_client.chat.stream(
452
+ model=MISTRAL_MODEL,
453
+ messages=messages,
454
+ max_tokens=2048,
455
+ temperature=0.3
456
+ ):
457
+ if chunk.data.choices[0].delta.content is not None:
458
+ chunk_text = chunk.data.choices[0].delta.content
459
+ full_response += chunk_text
460
+ message_queue.put(('response_chunk', chunk_text))
461
+
462
+ # Проверка соответствия источникам
 
 
 
 
 
 
 
 
 
 
 
 
463
  verified_response = verify_with_sources(full_response, sources)
464
+ # Очистка и форматирование ответа
465
  final_response = clean_response(verified_response, sources)
466
+
467
+ # Сохраняем контекст
468
+ conversation.add_exchange(
469
+ prompt,
470
+ final_response,
471
+ sources=sources,
472
+ search_query=search_query,
473
+ problem=extracted_problem,
474
+ norm_data=norm_data,
475
+ full_response=full_response
476
+ )
477
+
478
+ message_queue.put(('response_end', final_response))
 
 
479
  message_queue.put(('sources', json.dumps(sources)))
480
+ else:
481
+ # Обработка в контексте предыдущего диалога
482
+ message_queue.put(('log', "🔄 Обрабатываю запрос в контексте предыдущего диалога"))
483
+
484
+ # Создаем контекст из предыдущего диалога
485
+ context = conversation.get_context()
486
+
487
+ # Генерируем ответ на основе контекста
488
+ messages = [
489
+ {"role": "system", "content": SYSTEM_PROMPT + """
490
+ Вы отвечаете в контексте предыдущего диалога.
491
+ Используйте предоставленную информацию вместо поиска в интернете.
492
+ Формат ответа должен быть таким же, как и при первом запросе.
493
+ """},
494
+ {"role": "user", "content": f"Контекст предыдущего диалога:\n{context}\n\nНовый запрос: {prompt}"}
495
+ ]
496
+
497
+ message_queue.put(('log', "🧠 Генерирую ответ на основе контекста..."))
498
+ message_queue.put(('response_start', ""))
499
+
500
+ for chunk in mistral_client.chat.stream(
501
+ model=MISTRAL_MODEL,
502
+ messages=messages,
503
+ max_tokens=2048,
504
+ temperature=0.3
505
+ ):
506
+ if chunk.data.choices[0].delta.content is not None:
507
+ chunk_text = chunk.data.choices[0].delta.content
508
+ full_response += chunk_text
509
+ message_queue.put(('response_chunk', chunk_text))
510
+
511
+ # Очистка и форматирование ответа
512
+ final_response = clean_response(full_response, conversation.last_sources)
513
+
514
+ # Сохраняем в историю
515
+ conversation.add_exchange(prompt, final_response)
516
+
517
+ # Отправляем ответ и источники
518
+ message_queue.put(('response_end', final_response))
519
+ message_queue.put(('sources', json.dumps(conversation.last_sources)))
520
 
521
+ message_queue.put(('log', f"💡 Ответ: {final_response[:200]}..."))
522
  total_time = time.time() - start_time
523
+ message_queue.put(('log', f" Обработка завершена за {total_time:.1f}с"))
 
524
  message_queue.put(('done', ''))
 
525
  except Exception as e:
526
+ error_msg = f"❌ Ошибка: {str(e)}"
527
  message_queue.put(('log', error_msg))
528
+ message_queue.put(('response', "\n⚠️ Ошибка обработки запроса"))
529
  message_queue.put(('done', ''))
530
 
 
531
  @app.route('/')
532
  def index():
533
+ # Генерируем или получаем ID диалога
534
+ conversation_id = request.cookies.get('conversation_id')
535
+ if not conversation_id:
536
+ conversation_id = str(uuid4())
537
+
538
+ response = make_response(render_template('index.html', conversation_id=conversation_id))
539
+ response.set_cookie('conversation_id', conversation_id)
540
+ return response
541
 
542
  @app.route('/ask', methods=['POST'])
543
  def ask():
544
  user_input = request.form['message']
545
+ conversation_id = request.form.get('conversation_id', 'default')
546
+
547
+ thread = threading.Thread(
548
+ target=process_query,
549
+ args=(user_input, conversation_id)
550
+ )
551
  thread.daemon = True
552
  thread.start()
553
+
554
+ return jsonify({
555
+ 'status': 'processing',
556
+ 'conversation_id': conversation_id
557
+ })
558
 
559
+ @app.route('/repeat_last', methods=['POST'])
560
  def repeat_last():
561
+ conversation_id = request.form.get('conversation_id', 'default')
562
+ conversation = conversations.get(conversation_id)
563
+
564
+ if conversation and conversation.history:
565
+ last_exchange = conversation.history[-1]
566
+ message_queue.put(('log', "🔄 Повторяю последний ответ"))
567
+ message_queue.put(('response_start', ""))
568
+
569
+ # Отправляем ответ по частям для анимации
570
+ chunk_size = 50
571
+ for i in range(0, len(last_exchange['bot']), chunk_size):
572
+ chunk = last_exchange['bot'][i:i+chunk_size]
573
+ message_queue.put(('response_chunk', chunk))
574
+ time.sleep(0.05) # Небольшая задержка для эффекта печати
575
+
576
+ message_queue.put(('response_end', last_exchange['bot']))
577
+ message_queue.put(('sources', json.dumps(conversation.last_sources)))
578
+ message_queue.put(('done', ''))
579
+
580
+ return jsonify({'status': 'success'})
581
+ return jsonify({'status': 'error', 'message': 'No previous response to repeat'}), 404
582
 
583
+ @app.route('/new_conversation', methods=['POST'])
584
+ def new_conversation():
585
+ old_id = request.form.get('conversation_id', 'default')
586
+
587
+ # Создаем новый ID
588
+ new_id = str(uuid4())
589
+
590
+ # Перемещаем данные диалога (если нужно сохранить историю)
591
+ if old_id in conversations:
592
+ conversations[new_id] = conversations[old_id]
593
+ # Сбрасываем историю для нового диалога
594
+ conversations[new_id].history = []
595
+ conversations[new_id].last_sources = []
596
+ conversations[new_id].last_search_query = ""
597
+ conversations[new_id].last_problem = ""
598
+ conversations[new_id].last_norm_data = {}
599
+ del conversations[old_id]
600
+
601
+ message_queue.put(('log', "🔄 Начат новый диалог"))
602
+ return jsonify({'status': 'success', 'new_conversation_id': new_id})
603
 
604
  @app.route('/stream')
605
  def stream():
 
613
  time.sleep(0.1)
614
  return Response(generate(), mimetype='text/event-stream')
615
 
 
616
  if __name__ == '__main__':
617
  app.run(host='0.0.0.0', port=7860, debug=False)