KennyOry commited on
Commit
9c09266
·
verified ·
1 Parent(s): 78a65ac

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +133 -85
app.py CHANGED
@@ -1,4 +1,4 @@
1
- from flask import Flask, render_template, request, Response, jsonify
2
  from mistralai import Mistral
3
  import logging
4
  import time
@@ -14,6 +14,7 @@ import random
14
 
15
  app = Flask(__name__)
16
  app.secret_key = 'super_secret_key'
 
17
 
18
  message_queue = queue.Queue()
19
 
@@ -21,12 +22,12 @@ message_queue = queue.Queue()
21
  MISTRAL_MODEL = "mistral-large-latest"
22
  N_CTX = 32768
23
  MAX_RESULTS = 5
24
- MAX_CONTENT_LENGTH = 10000 # Максимальная длина контента на источник
 
25
 
26
- # Новый клиент Mistral
27
  mistral_client = Mistral(api_key=os.getenv("MISTRAL_API_KEY"))
28
 
29
-
30
  SYSTEM_PROMPT = """
31
  Ты PrintMaster, сервисный инженер по печатной технике. Правила:
32
  1. Формат ответа строго:
@@ -48,6 +49,14 @@ SYSTEM_PROMPT = """
48
  6. Будь лаконичен, но технически точен
49
  """
50
 
 
 
 
 
 
 
 
 
51
  BLACKLISTED_DOMAINS = [
52
  'reddit.com',
53
  'stackoverflow.com',
@@ -91,31 +100,26 @@ def get_random_headers():
91
  }
92
 
93
  def extract_main_content(html, url):
94
- """Извлекает основной контент страницы с помощью trafilatura или BeautifulSoup"""
95
  try:
96
- # Пробуем trafilatura
97
  content = trafilatura.extract(html, include_links=False, include_tables=False)
98
  if content and len(content) > 500:
99
  return content[:MAX_CONTENT_LENGTH]
100
  except Exception as e:
101
  logging.error(f"Trafilatura error: {str(e)}")
102
 
103
- # Fallback на BeautifulSoup
104
  try:
105
  soup = BeautifulSoup(html, 'html.parser')
106
 
107
- # Удаляем ненужные элементы
108
  for element in soup(['script', 'style', 'header', 'footer', 'nav', 'aside', 'form']):
109
  element.decompose()
110
 
111
- # Пытаемся найти основной контент
112
  main_content = soup.find('main') or soup.find('article') or soup.find('div', class_=re.compile('content|main|article|post', re.I))
113
 
114
  if main_content:
115
  text = main_content.get_text(separator='\n', strip=True)
116
  return text[:MAX_CONTENT_LENGTH] if text else None
117
 
118
- # Fallback: весь текст body
119
  return soup.body.get_text(separator='\n', strip=True)[:MAX_CONTENT_LENGTH]
120
  except Exception as e:
121
  logging.error(f"BeautifulSoup error: {str(e)}")
@@ -210,7 +214,6 @@ def web_search(query: str) -> tuple:
210
  sources = []
211
  full_contents = []
212
 
213
- # Обработка featured snippet
214
  featured_snippet = data.get("featured_snippet", {})
215
  if featured_snippet:
216
  snippet = featured_snippet.get("snippet", "")
@@ -222,9 +225,8 @@ def web_search(query: str) -> tuple:
222
  "content": snippet
223
  })
224
 
225
- # Обработка organic results
226
  organic_results = data.get("organic_results", [])
227
- for i, res in enumerate(organic_results[:5]): # Ограничиваемся топ-5
228
  title = res.get("title", "Без заголовка")
229
  link = res.get("link", "#")
230
  snippet = res.get("snippet", "") or ""
@@ -232,7 +234,6 @@ def web_search(query: str) -> tuple:
232
  if any(domain in link for domain in BLACKLISTED_DOMAINS):
233
  continue
234
 
235
- # Загрузка полного контента
236
  content = None
237
  try:
238
  headers = get_random_headers()
@@ -243,13 +244,11 @@ def web_search(query: str) -> tuple:
243
  logging.error(f"Ошибка загрузки {link}: {str(e)}")
244
 
245
  if not content:
246
- content = snippet # Fallback на сниппет
247
 
248
- # Форматирование контента
249
  cleaned_content = re.sub(r'\s+', ' ', content).strip()
250
  combined_content += f"[[Источник {i+1}]] {title}\n{cleaned_content}\n\n"
251
 
252
- # Сохранение источника
253
  source_data = {
254
  "title": title,
255
  "url": link,
@@ -269,39 +268,20 @@ def web_search(query: str) -> tuple:
269
 
270
 
271
  def clean_response(response: str, sources: list) -> str:
272
- # Удаление служебных тегов
273
  response = re.sub(r'</?assistant>|<\|system\|>|</s>', '', response, flags=re.IGNORECASE)
274
-
275
- # Удаление лишних разделителей
276
  response = re.sub(r'^-{3,}\s*', '', response)
277
-
278
- # Удаление дублирования разделов
279
  response = re.sub(r'(\*\*Проблема:\*\*.+?)(\*\*Проблема:\*\*)', r'\1', response, flags=re.DOTALL)
280
  response = re.sub(r'(\*\*Решение:\*\*.+?)(\*\*Решение:\*\*)', r'\1', response, flags=re.DOTALL)
281
-
282
- # Удаление лишних переносов
283
  response = re.sub(r'\n\s*\n', '\n\n', response)
284
  response = re.sub(r'[ \t]{2,}', ' ', response)
285
-
286
- # Удаление начальных фраз
287
  response = re.sub(r'^Вот исправленный ответ[^:]+:\s*', '', response)
288
-
289
- # Форматирование примечаний
290
  response = re.sub(r'^---\s*Примечания:\s*', '**Примечания:**\n', response)
291
-
292
- # Форматирование источников
293
  response = re.sub(r'^---\s*Источники:\s*', '**Источники информации:**\n', response)
294
-
295
- # Удаление лишних маркеров
296
  response = re.sub(r'^---\s*', '', response, flags=re.MULTILINE)
297
-
298
- # Очистка завершающих символов
299
  response = re.sub(r'\s*\.{3,}\s*$', '', response)
300
-
301
  return response.strip()
302
 
303
  def verify_with_sources(response: str, sources: list) -> str:
304
- """Проверяет соответствие ответа источникам с помощью LLM"""
305
  try:
306
  message_queue.put(('log', "🔍 Проверяю соответствие ответа источникам..."))
307
 
@@ -350,37 +330,59 @@ def process_query(prompt: str):
350
  try:
351
  start_time = time.time()
352
  message_queue.put(('log', f"👤 Запрос: {prompt}"))
353
- message_queue.put(('log', f"⚙️ Извлекаю параметры из входящего запроса"))
354
-
355
- norm_data = generate_search_query(prompt)
356
- message_queue.put(('log', f"⏏️ Извлечено: {json.dumps(norm_data, ensure_ascii=False)}"))
357
 
358
- search_query = norm_data['search_query']
359
- search_data, sources = web_search(search_query)
360
-
361
- message_queue.put(('log', f"📚 Собрано: {len(search_data)} символов в {len(sources)} источнике(-ах)"))
362
-
363
- message_queue.put(('log', f"⚙️ Определяю проблему"))
364
- problem_response = mistral_client.chat.complete(
365
- model=MISTRAL_MODEL,
366
- messages=[
367
- {"role": "system", "content": "Опиши СУТЬ проблемы в одном предложении. Только диагноз, без решений. Не более 12 слов. На русском."},
368
- {"role": "user", "content": f"Запрос пользователя: {prompt}\nПоисковые данные:\n{search_data}"}
369
- ],
370
- max_tokens=150,
371
- temperature=0.2
372
- )
373
- extracted_problem = problem_response.choices[0].message.content.strip()
374
-
375
- if not extracted_problem or len(extracted_problem) < 5:
376
- extracted_problem = f"Неисправность {norm_data['brand']} {norm_data['model']}"
377
-
378
- message_queue.put(('log', f"🧩 Определённая проблема: {extracted_problem}"))
379
-
380
- sources_text = "\n".join([f"[{i+1}] {s['title']} - {s['url']}" for i, s in enumerate(sources)])
381
-
382
- messages = [
383
- {"role": "system", "content": SYSTEM_PROMPT + f"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
  Контекст:
385
  Бренд: {norm_data['brand']}
386
  Модель: {norm_data['model']}
@@ -388,13 +390,26 @@ def process_query(prompt: str):
388
  Суть проблемы (на основе поиска): {extracted_problem}
389
  Данные поиска:
390
  {search_data}
391
- """},
392
- {"role": "user", "content": f"Проблема: {prompt}"}
393
- ]
394
-
 
 
 
 
 
 
 
 
 
 
 
 
 
395
  message_queue.put(('log', "🧠 На основе полученных данных генерирую ответ..."))
396
- message_queue.put(('response_start', ""))
397
-
398
  full_response = ""
399
  for chunk in mistral_client.chat.stream(
400
  model=MISTRAL_MODEL,
@@ -405,31 +420,45 @@ def process_query(prompt: str):
405
  if chunk.data.choices[0].delta.content is not None:
406
  chunk_text = chunk.data.choices[0].delta.content
407
  full_response += chunk_text
408
- message_queue.put(('response_chunk', chunk_text))
409
 
410
- # Проверка соответствия источникам
411
- verified_response = verify_with_sources(full_response, sources)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
412
 
413
- # Очистка и форматирование ответа
414
- final_response = clean_response(verified_response, sources)
415
-
416
- message_queue.put(('response_end', final_response))
417
- message_queue.put(('sources', json.dumps(sources)))
418
-
419
  total_time = time.time() - start_time
420
- message_queue.put(('log', f"💡 Ответ: {final_response[:200]}..."))
421
- message_queue.put(('log', f"⏱ Время: {total_time:.1f}с"))
422
  message_queue.put(('done', ''))
423
 
424
  except Exception as e:
425
- error_msg = f"❌ Ошибка: {str(e)}"
426
  message_queue.put(('log', error_msg))
427
- message_queue.put(('response', "\n⚠️ Ошибка обработки запроса"))
428
  message_queue.put(('done', ''))
429
 
430
 
431
  @app.route('/')
432
  def index():
 
 
 
433
  return render_template('index.html')
434
 
435
 
@@ -442,6 +471,25 @@ def ask():
442
  return jsonify({'status': 'processing'})
443
 
444
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
445
  @app.route('/stream')
446
  def stream():
447
  def generate():
 
1
+ from flask import Flask, render_template, request, Response, jsonify, session
2
  from mistralai import Mistral
3
  import logging
4
  import time
 
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
 
 
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 = """
32
  Ты PrintMaster, сервисный инженер по печатной технике. Правила:
33
  1. Формат ответа строго:
 
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',
 
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)}")
 
214
  sources = []
215
  full_contents = []
216
 
 
217
  featured_snippet = data.get("featured_snippet", {})
218
  if featured_snippet:
219
  snippet = featured_snippet.get("snippet", "")
 
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 ""
 
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
  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,
 
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
 
 
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(
369
+ model=MISTRAL_MODEL,
370
+ messages=[
371
+ {"role": "system", "content": "Опиши СУТЬ проблемы в одном предложении. Только диагноз, без решений. Не более 12 слов. На русском."},
372
+ {"role": "user", "content": f"Запрос пользователя: {prompt}\nПоисковые данные:\n{search_data}"}
373
+ ],
374
+ max_tokens=150,
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']}
 
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,
 
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
 
 
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():
495
  def generate():