Скрипты генерации rag
This commit is contained in:
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"localaipilot.standalone.ollamaCodeModel": "deepseek-coder:6.7b-base"
|
||||
}
|
||||
10
@rag/.gitignore
vendored
Normal file
10
@rag/.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
/input_html/*
|
||||
/output_md/*
|
||||
/ready_rag/*
|
||||
/venv
|
||||
|
||||
*.html
|
||||
*.md
|
||||
*.sqlite*
|
||||
|
||||
!.gitkeep
|
||||
49
@rag/1_download_page.sh
Executable file
49
@rag/1_download_page.sh
Executable file
@@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
DELAY=1
|
||||
|
||||
# 1. Указать реквизиты доступа к confluence
|
||||
USERNAME=""
|
||||
PASSWORD=""
|
||||
CONFLUENCE_URL=""
|
||||
|
||||
# 2. Вызвать: ./1_download_page.sh <pageId>
|
||||
|
||||
##################################################################
|
||||
|
||||
if [ $# -lt 1 ]; then
|
||||
echo "Usage: $0 <pageId>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
command -v curl >/dev/null 2>&1 || { echo >&2 "Error: curl is required but not installed."; exit 1; }
|
||||
command -v jq >/dev/null 2>&1 || { echo >&2 "Error: jq is required but not installed."; exit 1; }
|
||||
|
||||
PAGE_ID="$1"
|
||||
API_ENDPOINT="${CONFLUENCE_URL}/rest/api/content/${PAGE_ID}?expand=body.storage,children.page"
|
||||
|
||||
echo
|
||||
echo "Downloading: $API_ENDPOINT"
|
||||
|
||||
response=$(curl -s -u "$USERNAME:$PASSWORD" -H "Accept: application/json" "${API_ENDPOINT}")
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Failed to retrieve article"
|
||||
fi
|
||||
|
||||
error_message=$(echo "$response" | jq -r '.message' 2>/dev/null)
|
||||
if [ -n "$error_message" ] && [ "$error_message" != "null" ]; then
|
||||
echo "API Error: $error_message"
|
||||
else
|
||||
output_path="./input_html/"
|
||||
title=$(echo "$response" | jq -r .title)
|
||||
content=$(echo "$response" | jq -r .body.storage.value)
|
||||
[ ! -d "$output_path" ] && mkdir -p "$output_path"
|
||||
echo "<html><body>$content</body></html>" > "$output_path/$title.html"
|
||||
echo "Saved as: $output_path/$title.html"
|
||||
|
||||
child_ids=$(echo "$response" | jq -r '.children.page.results[]?.id' 2>/dev/null)
|
||||
for child_id in $child_ids; do
|
||||
echo "Downloading child page ID: $child_id"
|
||||
sleep $DELAY
|
||||
./confluence_get_article.sh "$child_id"
|
||||
done
|
||||
fi
|
||||
325
@rag/2_html_to_md.py
Normal file
325
@rag/2_html_to_md.py
Normal file
@@ -0,0 +1,325 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
RAG System for Local Ollama
|
||||
Конвертирует html-файлы в markdown, очищая от лишней разметки
|
||||
Скрипт сгенерирован claude-sonnet-4
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
from bs4 import BeautifulSoup
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def clean_confluence_html(soup):
|
||||
"""
|
||||
Удаляет Confluence-специфичные элементы и очищает HTML.
|
||||
"""
|
||||
# Удаляем Confluence макросы (structured-macro)
|
||||
for macro in soup.find_all('ac:structured-macro'):
|
||||
macro_name = macro.get('ac:name', '')
|
||||
|
||||
# Сохраняем содержимое некоторых макросов
|
||||
if macro_name == 'note':
|
||||
# Преобразуем заметки в блоки внимания
|
||||
rich_text = macro.find('ac:rich-text-body')
|
||||
if rich_text:
|
||||
note_content = rich_text.get_text(strip=True)
|
||||
note_tag = soup.new_tag('div', class_='note')
|
||||
note_tag.string = f"📝 **Примечание:** {note_content}"
|
||||
macro.replace_with(note_tag)
|
||||
else:
|
||||
macro.decompose()
|
||||
elif macro_name == 'toc':
|
||||
# Заменяем TOC на текст
|
||||
toc_tag = soup.new_tag('div')
|
||||
toc_tag.string = "**Содержание** (автогенерируется)"
|
||||
macro.replace_with(toc_tag)
|
||||
elif macro_name == 'drawio':
|
||||
# Заменяем диаграммы на заглушку
|
||||
diagram_name = macro.find('ac:parameter', {'ac:name': 'diagramName'})
|
||||
if diagram_name:
|
||||
diagram_text = diagram_name.get_text()
|
||||
else:
|
||||
diagram_text = 'Диаграмма'
|
||||
diagram_tag = soup.new_tag('div')
|
||||
diagram_tag.string = f"🖼️ **Диаграмма:** {diagram_text}"
|
||||
macro.replace_with(diagram_tag)
|
||||
else:
|
||||
# Удаляем остальные макросы
|
||||
macro.decompose()
|
||||
|
||||
# Удаляем другие Confluence элементы
|
||||
for element in soup.find_all(True):
|
||||
if element.name and element.name.startswith('ac:'):
|
||||
element.decompose()
|
||||
|
||||
return soup
|
||||
|
||||
|
||||
def convert_table_to_markdown(table):
|
||||
"""
|
||||
Конвертирует HTML таблицу в Markdown формат.
|
||||
"""
|
||||
rows = table.find_all('tr')
|
||||
if not rows:
|
||||
return ""
|
||||
|
||||
markdown_lines = []
|
||||
|
||||
# Обработка первой строки как заголовка
|
||||
first_row = rows[0]
|
||||
header_cells = first_row.find_all(['th', 'td'])
|
||||
|
||||
if not header_cells:
|
||||
return ""
|
||||
|
||||
# Заголовок таблицы
|
||||
header_line = "|"
|
||||
separator_line = "|"
|
||||
|
||||
for cell in header_cells:
|
||||
# Получаем текст и очищаем его
|
||||
cell_text = cell.get_text(separator=' ', strip=True)
|
||||
cell_text = re.sub(r'\s+', ' ', cell_text) # Заменяем множественные пробелы
|
||||
cell_text = cell_text.replace('|', '\\|') # Экранируем pipe символы
|
||||
|
||||
header_line += f" {cell_text} |"
|
||||
separator_line += " --- |"
|
||||
|
||||
markdown_lines.append(header_line)
|
||||
markdown_lines.append(separator_line)
|
||||
|
||||
# Обработка остальных строк
|
||||
for row in rows[1:]:
|
||||
data_cells = row.find_all(['td', 'th'])
|
||||
if not data_cells:
|
||||
continue
|
||||
|
||||
data_line = "|"
|
||||
for i, cell in enumerate(data_cells):
|
||||
if i >= len(header_cells): # Не больше столбцов чем в заголовке
|
||||
break
|
||||
|
||||
cell_text = cell.get_text(separator=' ', strip=True)
|
||||
cell_text = re.sub(r'\s+', ' ', cell_text)
|
||||
cell_text = cell_text.replace('|', '\\|')
|
||||
|
||||
data_line += f" {cell_text} |"
|
||||
|
||||
# Дополняем недостающие столбцы
|
||||
missing_cols = len(header_cells) - len(data_cells)
|
||||
for _ in range(missing_cols):
|
||||
data_line += " |"
|
||||
|
||||
markdown_lines.append(data_line)
|
||||
|
||||
return "\n".join(markdown_lines)
|
||||
|
||||
|
||||
def extract_json_blocks(soup):
|
||||
"""
|
||||
Извлекает и форматирует JSON блоки.
|
||||
"""
|
||||
json_blocks = []
|
||||
|
||||
# Ищем потенциальные JSON блоки в pre, code и script тегах
|
||||
for element in soup.find_all(['pre', 'code', 'script']):
|
||||
text_content = element.get_text(strip=True)
|
||||
|
||||
# Простая проверка на JSON
|
||||
if text_content and (
|
||||
(text_content.startswith('{') and text_content.endswith('}')) or
|
||||
(text_content.startswith('[') and text_content.endswith(']'))
|
||||
):
|
||||
try:
|
||||
# Пытаемся парсить как JSON
|
||||
json_data = json.loads(text_content)
|
||||
formatted_json = json.dumps(json_data, indent=2, ensure_ascii=False)
|
||||
|
||||
# Заменяем элемент на форматированный JSON блок
|
||||
json_tag = soup.new_tag('pre')
|
||||
json_tag.string = f"```json\n{formatted_json}\n```"
|
||||
element.replace_with(json_tag)
|
||||
json_blocks.append(formatted_json)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
# Если не JSON, оставляем как code block
|
||||
if element.name in ['pre', 'code']:
|
||||
element.string = f"```\n{text_content}\n```"
|
||||
|
||||
return json_blocks
|
||||
|
||||
|
||||
def html_to_markdown(html_content):
|
||||
"""
|
||||
Основная функция конвертации HTML в Markdown.
|
||||
"""
|
||||
soup = BeautifulSoup(html_content, 'html.parser')
|
||||
|
||||
# Удаляем скрипты и стили
|
||||
for element in soup(['script', 'style']):
|
||||
element.decompose()
|
||||
|
||||
# Очищаем Confluence элементы
|
||||
soup = clean_confluence_html(soup)
|
||||
|
||||
# Извлекаем JSON блоки
|
||||
json_blocks = extract_json_blocks(soup)
|
||||
|
||||
# Конвертируем таблицы
|
||||
for table in soup.find_all('table'):
|
||||
markdown_table = convert_table_to_markdown(table)
|
||||
if markdown_table:
|
||||
# Заменяем таблицу на Markdown
|
||||
table_div = soup.new_tag('div', class_='markdown-table')
|
||||
table_div.string = f"\n{markdown_table}\n"
|
||||
table.replace_with(table_div)
|
||||
|
||||
# Обработка заголовков
|
||||
for level in range(1, 7):
|
||||
for header in soup.find_all(f'h{level}'):
|
||||
header_text = header.get_text(strip=True)
|
||||
markdown_header = '#' * level + ' ' + header_text
|
||||
header.string = markdown_header
|
||||
|
||||
# Обработка списков
|
||||
for ul in soup.find_all('ul'):
|
||||
list_items = ul.find_all('li', recursive=False)
|
||||
if list_items:
|
||||
markdown_list = []
|
||||
for li in list_items:
|
||||
item_text = li.get_text(strip=True)
|
||||
markdown_list.append(f"- {item_text}")
|
||||
ul.string = '\n'.join(markdown_list)
|
||||
|
||||
for ol in soup.find_all('ol'):
|
||||
list_items = ol.find_all('li', recursive=False)
|
||||
if list_items:
|
||||
markdown_list = []
|
||||
for i, li in enumerate(list_items, 1):
|
||||
item_text = li.get_text(strip=True)
|
||||
markdown_list.append(f"{i}. {item_text}")
|
||||
ol.string = '\n'.join(markdown_list)
|
||||
|
||||
# Обработка жирного и курсивного текста
|
||||
for strong in soup.find_all(['strong', 'b']):
|
||||
text = strong.get_text()
|
||||
strong.string = f"**{text}**"
|
||||
|
||||
for em in soup.find_all(['em', 'i']):
|
||||
text = em.get_text()
|
||||
em.string = f"*{text}*"
|
||||
|
||||
# Получаем финальный текст
|
||||
text = soup.get_text(separator='\n', strip=True)
|
||||
|
||||
# Постобработка
|
||||
lines = []
|
||||
for line in text.split('\n'):
|
||||
line = line.strip()
|
||||
if line:
|
||||
lines.append(line)
|
||||
|
||||
# Убираем лишние пустые строки
|
||||
result_lines = []
|
||||
prev_empty = False
|
||||
|
||||
for line in lines:
|
||||
if not line:
|
||||
if not prev_empty:
|
||||
result_lines.append('')
|
||||
prev_empty = True
|
||||
else:
|
||||
result_lines.append(line)
|
||||
prev_empty = False
|
||||
|
||||
return '\n'.join(result_lines)
|
||||
|
||||
|
||||
def process_files(input_dir, output_dir):
|
||||
"""
|
||||
Обрабатывает все HTML-файлы в директории.
|
||||
"""
|
||||
input_path = Path(input_dir)
|
||||
output_path = Path(output_dir)
|
||||
|
||||
if not input_path.exists():
|
||||
print(f"❌ Директория {input_dir} не найдена")
|
||||
return
|
||||
|
||||
# Создаем выходную директорию
|
||||
output_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
html_files = list(input_path.glob('*.html'))
|
||||
|
||||
if not html_files:
|
||||
print(f"❌ HTML файлы не найдены в {input_dir}")
|
||||
return
|
||||
|
||||
print(f"📁 Найдено {len(html_files)} HTML файлов")
|
||||
|
||||
successful = 0
|
||||
failed = 0
|
||||
failed_files = []
|
||||
|
||||
for html_file in html_files:
|
||||
print(f"🔄 Обработка: {html_file.name}")
|
||||
|
||||
try:
|
||||
# Читаем HTML файл
|
||||
with open(html_file, 'r', encoding='utf-8') as f:
|
||||
html_content = f.read()
|
||||
|
||||
# Проверяем, что файл не пустой
|
||||
if not html_content.strip():
|
||||
print(f"⚠️ Пропущен: {html_file.name} (пустой файл)")
|
||||
continue
|
||||
|
||||
# Конвертируем в Markdown
|
||||
markdown_content = html_to_markdown(html_content)
|
||||
|
||||
# Проверяем результат конвертации
|
||||
if not markdown_content.strip():
|
||||
print(f"⚠️ Предупреждение: {html_file.name} - результат конвертации пустой")
|
||||
|
||||
# Сохраняем результат
|
||||
md_filename = html_file.stem + '.md'
|
||||
md_filepath = output_path / md_filename
|
||||
|
||||
with open(md_filepath, 'w', encoding='utf-8') as f:
|
||||
f.write(markdown_content)
|
||||
|
||||
print(f"✅ Сохранено: {md_filename}")
|
||||
successful += 1
|
||||
|
||||
except UnicodeDecodeError as e:
|
||||
print(f"❌ Ошибка кодировки в {html_file.name}: {str(e)}")
|
||||
failed += 1
|
||||
failed_files.append((html_file.name, f"Ошибка кодировки: {str(e)}"))
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка при обработке {html_file.name}: {str(e)}")
|
||||
failed += 1
|
||||
failed_files.append((html_file.name, str(e)))
|
||||
|
||||
print(f"\n📊 Результат:")
|
||||
print(f"✅ Успешно обработано: {successful}")
|
||||
print(f"❌ Ошибок: {failed}")
|
||||
|
||||
if failed_files:
|
||||
print(f"\n📋 Список файлов с ошибками:")
|
||||
for filename, error in failed_files:
|
||||
print(f" • {filename}: {error}")
|
||||
|
||||
print(f"📂 Результаты сохранены в: {output_dir}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
input_directory = "./input_html"
|
||||
output_directory = "./output_md"
|
||||
|
||||
print("🚀 Запуск конвертера HTML → Markdown")
|
||||
print("=" * 50)
|
||||
|
||||
process_files(input_directory, output_directory)
|
||||
529
@rag/3_rag.py
Normal file
529
@rag/3_rag.py
Normal file
@@ -0,0 +1,529 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
RAG System for Local Ollama
|
||||
Создает и использует RAG на основе markdown файлов для работы с локальной Ollama
|
||||
Скрипт сгенерирован claude-sonnet-4
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import hashlib
|
||||
import pickle
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Tuple, Any
|
||||
import requests
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
import re
|
||||
|
||||
try:
|
||||
import numpy as np
|
||||
import chromadb
|
||||
from chromadb.config import Settings
|
||||
except ImportError:
|
||||
print("Устанавливаем необходимые зависимости...")
|
||||
os.system("pip install chromadb numpy requests")
|
||||
import numpy as np
|
||||
import chromadb
|
||||
from chromadb.config import Settings
|
||||
|
||||
|
||||
class LocalRAGSystem:
|
||||
def __init__(self,
|
||||
md_folder: str = "output_md",
|
||||
db_path: str = "ready_rag",
|
||||
ollama_url: str = "http://localhost:11434",
|
||||
embed_model: str = "nomic-embed-text",
|
||||
chat_model: str = "qwen2.5:7b"):
|
||||
|
||||
self.md_folder = Path(md_folder)
|
||||
self.db_path = Path(db_path)
|
||||
self.ollama_url = ollama_url
|
||||
self.embed_model = embed_model
|
||||
self.chat_model = chat_model
|
||||
|
||||
# Создаем папку для базы данных
|
||||
self.db_path.mkdir(exist_ok=True)
|
||||
|
||||
# Инициализируем ChromaDB
|
||||
self.chroma_client = chromadb.PersistentClient(path=str(self.db_path))
|
||||
self.collection = self.chroma_client.get_or_create_collection(
|
||||
name="md_documents",
|
||||
metadata={"description": "RAG collection for markdown documents"}
|
||||
)
|
||||
|
||||
print(f"RAG система инициализирована:")
|
||||
print(f"- Папка с MD файлами: {self.md_folder}")
|
||||
print(f"- База данных: {self.db_path}")
|
||||
print(f"- Ollama URL: {self.ollama_url}")
|
||||
print(f"- Модель эмбеддингов: {self.embed_model}")
|
||||
print(f"- Модель чата: {self.chat_model}")
|
||||
|
||||
def check_ollama_connection(self) -> bool:
|
||||
"""Проверяем подключение к Ollama"""
|
||||
try:
|
||||
response = requests.get(f"{self.ollama_url}/api/tags")
|
||||
return response.status_code == 200
|
||||
except:
|
||||
return False
|
||||
|
||||
def get_ollama_models(self) -> List[str]:
|
||||
"""Получаем список доступных моделей в Ollama"""
|
||||
try:
|
||||
response = requests.get(f"{self.ollama_url}/api/tags")
|
||||
if response.status_code == 200:
|
||||
models = response.json().get('models', [])
|
||||
return [model['name'] for model in models]
|
||||
return []
|
||||
except:
|
||||
return []
|
||||
|
||||
def find_model(self, model_name: str, available_models: List[str]) -> str:
|
||||
"""Найти модель по имени, учитывая суффикс :latest"""
|
||||
# Сначала ищем точное совпадение
|
||||
if model_name in available_models:
|
||||
return model_name
|
||||
|
||||
# Затем ищем с суффиксом :latest
|
||||
if f"{model_name}:latest" in available_models:
|
||||
return f"{model_name}:latest"
|
||||
|
||||
# Если модель содержит :latest, пробуем без него
|
||||
if model_name.endswith(":latest"):
|
||||
base_name = model_name[:-7] # убираем ":latest"
|
||||
if base_name in available_models:
|
||||
return base_name
|
||||
|
||||
return None
|
||||
|
||||
def chunk_text(self, text: str, chunk_size: int = 1000, overlap: int = 200) -> List[str]:
|
||||
"""Разбиваем текст на чанки с перекрытием"""
|
||||
chunks = []
|
||||
start = 0
|
||||
|
||||
while start < len(text):
|
||||
end = start + chunk_size
|
||||
chunk = text[start:end]
|
||||
|
||||
# Попытаемся разбить по предложениям
|
||||
if end < len(text):
|
||||
last_period = chunk.rfind('.')
|
||||
last_newline = chunk.rfind('\n')
|
||||
split_point = max(last_period, last_newline)
|
||||
|
||||
if split_point > start + chunk_size // 2:
|
||||
chunk = text[start:split_point + 1]
|
||||
end = split_point + 1
|
||||
|
||||
chunks.append(chunk.strip())
|
||||
start = max(start + chunk_size - overlap, end - overlap)
|
||||
|
||||
if start >= len(text):
|
||||
break
|
||||
|
||||
return [chunk for chunk in chunks if len(chunk.strip()) > 50]
|
||||
|
||||
def extract_metadata(self, text: str, filename: str) -> Dict[str, Any]:
|
||||
"""Извлекаем метаданные из markdown файла"""
|
||||
metadata = {
|
||||
'filename': filename,
|
||||
'length': len(text),
|
||||
'created_at': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
# Ищем заголовки
|
||||
headers = re.findall(r'^#{1,6}\s+(.+)$', text, re.MULTILINE)
|
||||
if headers:
|
||||
metadata['title'] = headers[0]
|
||||
# Конвертируем список заголовков в строку (ChromaDB не принимает списки)
|
||||
metadata['headers_text'] = '; '.join(headers[:5])
|
||||
metadata['headers_count'] = len(headers)
|
||||
|
||||
# Ищем специальные секции
|
||||
if '# Краткое описание' in text:
|
||||
desc_match = re.search(r'# Краткое описание\n(.*?)(?=\n#|\n$)', text, re.DOTALL)
|
||||
if desc_match:
|
||||
metadata['description'] = desc_match.group(1).strip()[:500]
|
||||
|
||||
# Ищем требования
|
||||
if '# Требования' in text:
|
||||
metadata['has_requirements'] = True
|
||||
|
||||
# Ищем нормативные документы
|
||||
if '# Нормативная документация' in text:
|
||||
metadata['has_regulations'] = True
|
||||
|
||||
return metadata
|
||||
|
||||
def get_embedding(self, text: str) -> List[float]:
|
||||
"""Получаем эмбеддинг через Ollama"""
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{self.ollama_url}/api/embeddings",
|
||||
json={
|
||||
"model": self.embed_model,
|
||||
"prompt": text
|
||||
},
|
||||
timeout=600
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()["embedding"]
|
||||
else:
|
||||
print(f"Ошибка получения эмбеддинга: {response.status_code}")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"Ошибка при получении эмбеддинга: {e}")
|
||||
return None
|
||||
|
||||
def process_markdown_files(self) -> int:
|
||||
"""Обрабатываем все markdown файлы и добавляем их в векторную базу"""
|
||||
if not self.md_folder.exists():
|
||||
print(f"Папка {self.md_folder} не найдена!")
|
||||
return 0
|
||||
|
||||
md_files = list(self.md_folder.glob("*.md"))
|
||||
if not md_files:
|
||||
print(f"Markdown файлы не найдены в {self.md_folder}")
|
||||
return 0
|
||||
|
||||
print(f"Найдено {len(md_files)} markdown файлов")
|
||||
|
||||
# Проверяем подключение к Ollama
|
||||
if not self.check_ollama_connection():
|
||||
print(f"Не удается подключиться к Ollama по адресу {self.ollama_url}")
|
||||
print("Убедитесь, что Ollama запущена и доступна")
|
||||
return 0
|
||||
|
||||
# Проверяем наличие модели эмбеддингов
|
||||
available_models = self.get_ollama_models()
|
||||
embed_model_name = self.find_model(self.embed_model, available_models)
|
||||
|
||||
if not embed_model_name:
|
||||
print(f"Модель эмбеддингов {self.embed_model} не найдена в Ollama")
|
||||
print(f"Доступные модели: {available_models}")
|
||||
print(f"Загружаем модель {self.embed_model}...")
|
||||
|
||||
# Пытаемся загрузить модель
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{self.ollama_url}/api/pull",
|
||||
json={"name": self.embed_model},
|
||||
timeout=300
|
||||
)
|
||||
if response.status_code != 200:
|
||||
print(f"Не удается загрузить модель {self.embed_model}")
|
||||
return 0
|
||||
else:
|
||||
# После загрузки обновляем список моделей
|
||||
available_models = self.get_ollama_models()
|
||||
embed_model_name = self.find_model(self.embed_model, available_models)
|
||||
except Exception as e:
|
||||
print(f"Ошибка при загрузке модели: {e}")
|
||||
return 0
|
||||
|
||||
# Обновляем имя модели эмбеддингов
|
||||
if embed_model_name:
|
||||
self.embed_model = embed_model_name
|
||||
print(f"Используем модель эмбеддингов: {self.embed_model}")
|
||||
|
||||
processed_count = 0
|
||||
total_chunks = 0
|
||||
|
||||
for md_file in md_files:
|
||||
print(f"\nОбрабатываем: {md_file.name}")
|
||||
|
||||
try:
|
||||
with open(md_file, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Создаем чанки
|
||||
chunks = self.chunk_text(content)
|
||||
print(f" Создано чанков: {len(chunks)}")
|
||||
|
||||
# Извлекаем метаданные
|
||||
base_metadata = self.extract_metadata(content, md_file.name)
|
||||
|
||||
# Обрабатываем каждый чанк
|
||||
for i, chunk in enumerate(chunks):
|
||||
# Создаем уникальный ID для чанка
|
||||
chunk_id = hashlib.md5(f"{md_file.name}_{i}_{chunk[:100]}".encode()).hexdigest()
|
||||
|
||||
# Получаем эмбеддинг
|
||||
embedding = self.get_embedding(chunk)
|
||||
if embedding is None:
|
||||
print(f" Пропускаем чанк {i} - не удалось получить эмбеддинг")
|
||||
continue
|
||||
|
||||
# Подготавливаем метаданные для чанка
|
||||
chunk_metadata = {}
|
||||
# Копируем только допустимые типы метаданных
|
||||
for key, value in base_metadata.items():
|
||||
if isinstance(value, (str, int, float, bool, type(None))):
|
||||
chunk_metadata[key] = value
|
||||
elif isinstance(value, list):
|
||||
# Конвертируем списки в строки
|
||||
chunk_metadata[key] = '; '.join(map(str, value))
|
||||
else:
|
||||
# Конвертируем другие типы в строки
|
||||
chunk_metadata[key] = str(value)
|
||||
|
||||
chunk_metadata.update({
|
||||
'chunk_id': i,
|
||||
'chunk_size': len(chunk),
|
||||
'source_file': str(md_file)
|
||||
})
|
||||
|
||||
# Добавляем в коллекцию
|
||||
self.collection.add(
|
||||
embeddings=[embedding],
|
||||
documents=[chunk],
|
||||
metadatas=[chunk_metadata],
|
||||
ids=[chunk_id]
|
||||
)
|
||||
|
||||
total_chunks += 1
|
||||
|
||||
processed_count += 1
|
||||
print(f" Успешно обработан файл {md_file.name}")
|
||||
|
||||
except Exception as e:
|
||||
print(f" Ошибка при обработке {md_file.name}: {e}")
|
||||
continue
|
||||
|
||||
print(f"\nОбработка завершена:")
|
||||
print(f"- Обработано файлов: {processed_count}")
|
||||
print(f"- Создано чанков: {total_chunks}")
|
||||
|
||||
return processed_count
|
||||
|
||||
def search(self, query: str, n_results: int = 5) -> List[Dict]:
|
||||
"""Поиск релевантных документов"""
|
||||
if self.collection.count() == 0:
|
||||
return []
|
||||
|
||||
# Получаем эмбеддинг для запроса
|
||||
query_embedding = self.get_embedding(query)
|
||||
if query_embedding is None:
|
||||
print("Не удалось получить эмбеддинг для запроса")
|
||||
return []
|
||||
|
||||
# Ищем похожие документы
|
||||
results = self.collection.query(
|
||||
query_embeddings=[query_embedding],
|
||||
n_results=n_results
|
||||
)
|
||||
|
||||
# Форматируем результаты
|
||||
formatted_results = []
|
||||
for i in range(len(results['documents'][0])):
|
||||
formatted_results.append({
|
||||
'document': results['documents'][0][i],
|
||||
'metadata': results['metadatas'][0][i],
|
||||
'distance': results['distances'][0][i] if 'distances' in results else None
|
||||
})
|
||||
|
||||
return formatted_results
|
||||
|
||||
def generate_response(self, query: str, context_docs: List[Dict]) -> str:
|
||||
"""Генерируем ответ используя контекст и Ollama"""
|
||||
# Формируем контекст из найденных документов
|
||||
context = ""
|
||||
for i, doc in enumerate(context_docs, 1):
|
||||
context += f"\n--- Документ {i} (файл: {doc['metadata'].get('filename', 'unknown')}) ---\n"
|
||||
context += doc['document'][:1000] + ("..." if len(doc['document']) > 1000 else "")
|
||||
context += "\n"
|
||||
|
||||
# Формируем промпт
|
||||
prompt = f"""На основе предоставленного контекста ответь на вопрос на русском языке. Если ответа нет в контексте, скажи об этом.
|
||||
|
||||
Контекст:
|
||||
{context}
|
||||
|
||||
Вопрос: {query}
|
||||
|
||||
Ответ:"""
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{self.ollama_url}/api/generate",
|
||||
json={
|
||||
"model": self.chat_model,
|
||||
"prompt": prompt,
|
||||
"stream": false
|
||||
},
|
||||
timeout=600
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()["response"]
|
||||
else:
|
||||
return f"Ошибка генерации ответа: {response.status_code}"
|
||||
|
||||
except Exception as e:
|
||||
return f"Ошибка при обращении к Ollama: {e}"
|
||||
|
||||
def query(self, question: str, n_results: int = 5) -> Dict:
|
||||
"""Полный цикл RAG: поиск + генерация ответа"""
|
||||
print(f"\nВопрос: {question}")
|
||||
|
||||
# Проверяем доступность моделей
|
||||
available_models = self.get_ollama_models()
|
||||
|
||||
# Находим правильные имена моделей
|
||||
embed_model_name = self.find_model(self.embed_model, available_models)
|
||||
if not embed_model_name:
|
||||
return {
|
||||
"question": question,
|
||||
"answer": f"Модель эмбеддингов {self.embed_model} не найдена в Ollama",
|
||||
"sources": []
|
||||
}
|
||||
|
||||
chat_model_name = self.find_model(self.chat_model, available_models)
|
||||
if not chat_model_name:
|
||||
return {
|
||||
"question": question,
|
||||
"answer": f"Модель чата {self.chat_model} не найдена в Ollama",
|
||||
"sources": []
|
||||
}
|
||||
|
||||
# Обновляем имена моделей
|
||||
self.embed_model = embed_model_name
|
||||
self.chat_model = chat_model_name
|
||||
|
||||
print("Ищем релевантные документы...")
|
||||
|
||||
# Поиск документов
|
||||
search_results = self.search(question, n_results)
|
||||
|
||||
if not search_results:
|
||||
return {
|
||||
"question": question,
|
||||
"answer": "Не найдено релевантных документов для ответа на ваш вопрос.",
|
||||
"sources": []
|
||||
}
|
||||
|
||||
print(f"Найдено {len(search_results)} релевантных документов")
|
||||
|
||||
# Генерация ответа
|
||||
print("Генерируем ответ...")
|
||||
answer = self.generate_response(question, search_results)
|
||||
|
||||
# Формируем источники
|
||||
sources = []
|
||||
for doc in search_results:
|
||||
sources.append({
|
||||
"filename": doc['metadata'].get('filename', 'unknown'),
|
||||
"title": doc['metadata'].get('title', ''),
|
||||
"distance": doc.get('distance', 0)
|
||||
})
|
||||
|
||||
return {
|
||||
"question": question,
|
||||
"answer": answer,
|
||||
"sources": sources,
|
||||
"context_docs": len(search_results)
|
||||
}
|
||||
|
||||
def get_stats(self) -> Dict:
|
||||
"""Получаем статистику RAG системы"""
|
||||
return {
|
||||
"total_documents": self.collection.count(),
|
||||
"embedding_model": self.embed_model,
|
||||
"chat_model": self.chat_model,
|
||||
"database_path": str(self.db_path),
|
||||
"source_folder": str(self.md_folder)
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="RAG System for Local Ollama")
|
||||
parser.add_argument("--action", choices=["build", "query", "interactive", "stats"],
|
||||
default="interactive", help="Действие для выполнения")
|
||||
parser.add_argument("--question", type=str, help="Вопрос для поиска")
|
||||
parser.add_argument("--md-folder", default="output_md", help="Папка с markdown файлами")
|
||||
parser.add_argument("--embed-model", default="nomic-embed-text", help="Модель для эмбеддингов")
|
||||
parser.add_argument("--chat-model", default="llama3.2:3b", help="Модель для чата")
|
||||
parser.add_argument("--results", type=int, default=5, help="Количество результатов поиска")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Создаем RAG систему
|
||||
rag = LocalRAGSystem(
|
||||
md_folder=args.md_folder,
|
||||
embed_model=args.embed_model,
|
||||
chat_model=args.chat_model
|
||||
)
|
||||
|
||||
if args.action == "build":
|
||||
print("Строим RAG базу данных...")
|
||||
count = rag.process_markdown_files()
|
||||
print(f"Обработано {count} файлов")
|
||||
|
||||
elif args.action == "query":
|
||||
if not args.question:
|
||||
print("Укажите вопрос с помощью --question")
|
||||
return
|
||||
|
||||
result = rag.query(args.question, args.results)
|
||||
print(f"\nВопрос: {result['question']}")
|
||||
print(f"\nОтвет:\n{result['answer']}")
|
||||
print(f"\nИсточники:")
|
||||
for source in result['sources']:
|
||||
print(f"- {source['filename']}: {source['title']}")
|
||||
|
||||
elif args.action == "stats":
|
||||
stats = rag.get_stats()
|
||||
print("Статистика RAG системы:")
|
||||
for key, value in stats.items():
|
||||
print(f"- {key}: {value}")
|
||||
|
||||
elif args.action == "interactive":
|
||||
print("\n=== Интерактивный режим RAG системы ===")
|
||||
print("Введите 'exit' для выхода, 'stats' для статистики")
|
||||
|
||||
# Проверяем, есть ли данные в базе
|
||||
if rag.collection.count() == 0:
|
||||
print("\nБаза данных пуста. Строим RAG...")
|
||||
count = rag.process_markdown_files()
|
||||
if count == 0:
|
||||
print("Не удалось построить базу данных. Завершение работы.")
|
||||
return
|
||||
|
||||
stats = rag.get_stats()
|
||||
print(f"\nДоступно документов: {stats['total_documents']}")
|
||||
print(f"Модель эмбеддингов: {stats['embedding_model']}")
|
||||
print(f"Модель чата: {stats['chat_model']}\n")
|
||||
|
||||
while True:
|
||||
try:
|
||||
question = input("\nВаш вопрос: ").strip()
|
||||
|
||||
if question.lower() == 'exit':
|
||||
break
|
||||
elif question.lower() == 'stats':
|
||||
stats = rag.get_stats()
|
||||
for key, value in stats.items():
|
||||
print(f"- {key}: {value}")
|
||||
continue
|
||||
elif not question:
|
||||
continue
|
||||
|
||||
result = rag.query(question, args.results)
|
||||
print(f"\nОтвет:\n{result['answer']}")
|
||||
|
||||
print(f"\nИсточники ({len(result['sources'])}):")
|
||||
for i, source in enumerate(result['sources'], 1):
|
||||
print(f"{i}. {source['filename']}")
|
||||
if source['title']:
|
||||
print(f" Заголовок: {source['title']}")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\nЗавершение работы...")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"Ошибка: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
0
@rag/input_html/.gitkeep
Normal file
0
@rag/input_html/.gitkeep
Normal file
0
@rag/output_md/.gitkeep
Normal file
0
@rag/output_md/.gitkeep
Normal file
0
@rag/ready_rag/.gitkeep
Normal file
0
@rag/ready_rag/.gitkeep
Normal file
96
@rag/requirements.txt
Normal file
96
@rag/requirements.txt
Normal file
@@ -0,0 +1,96 @@
|
||||
apt-xapian-index==0.49
|
||||
aptdaemon==2.0.2
|
||||
argcomplete==3.5.3
|
||||
asn1crypto==1.5.1
|
||||
attrs==25.1.0
|
||||
autocommand==2.2.2
|
||||
bcc==0.30.0
|
||||
bcrypt==4.2.0
|
||||
blinker==1.9.0
|
||||
Brlapi==0.8.6
|
||||
certifi==2025.1.31
|
||||
chardet==5.2.0
|
||||
click==8.1.8
|
||||
command-not-found==0.3
|
||||
cryptography==43.0.0
|
||||
cupshelpers==1.0
|
||||
dbus-python==1.3.2
|
||||
defer==1.0.6
|
||||
distro==1.9.0
|
||||
distro-info==1.13
|
||||
docker==7.1.0
|
||||
docker-compose==1.29.2
|
||||
dockerpty==0.4.1
|
||||
docopt==0.6.2
|
||||
fuse-python==1.0.9
|
||||
html5lib-modern==1.2
|
||||
httplib2==0.22.0
|
||||
idna==3.10
|
||||
importlib_metadata==8.6.1
|
||||
inflect==7.3.1
|
||||
jaraco.context==6.0.1
|
||||
jaraco.functools==4.1.0
|
||||
jsonpointer==2.4
|
||||
jsonschema==4.19.2
|
||||
jsonschema-specifications==2023.12.1
|
||||
language-selector==0.1
|
||||
launchpadlib==2.1.0
|
||||
lazr.restfulclient==0.14.6
|
||||
lazr.uri==1.0.6
|
||||
louis==3.32.0
|
||||
markdown-it-py==3.0.0
|
||||
mdurl==0.1.2
|
||||
mechanize==0.4.10
|
||||
more-itertools==10.6.0
|
||||
netaddr==1.3.0
|
||||
netifaces==0.11.0
|
||||
oauthlib==3.2.2
|
||||
packaging==24.2
|
||||
pipx==1.7.1
|
||||
platformdirs==4.3.6
|
||||
psutil==5.9.8
|
||||
pycairo==1.27.0
|
||||
pycups==2.0.4
|
||||
Pygments==2.18.0
|
||||
PyGObject==3.50.0
|
||||
PyJWT==2.10.1
|
||||
pylibacl==0.7.2
|
||||
pyparsing==3.1.2
|
||||
PyQt5==5.15.11
|
||||
PyQt5_sip==12.17.0
|
||||
PyQt6==6.8.1
|
||||
PyQt6_sip==13.10.0
|
||||
python-apt==3.0.0
|
||||
python-dateutil==2.9.0
|
||||
python-debian==1.0.1+ubuntu1
|
||||
python-dotenv==1.0.1
|
||||
python-magic==0.4.27
|
||||
pyxattr==0.8.1
|
||||
pyxdg==0.28
|
||||
PyYAML==6.0.2
|
||||
referencing==0.35.1
|
||||
requests==2.32.3
|
||||
rich==13.9.4
|
||||
rpds-py==0.21.0
|
||||
s3cmd==2.4.0
|
||||
sentry-sdk==2.18.0
|
||||
ssh-import-id==5.11
|
||||
systemd-python==235
|
||||
texttable==1.7.0
|
||||
tornado==6.4.2
|
||||
typeguard==4.4.2
|
||||
typing_extensions==4.12.2
|
||||
ubuntu-drivers-common==0.0.0
|
||||
ubuntu-pro-client==8001
|
||||
ufw==0.36.2
|
||||
unattended-upgrades==0.1
|
||||
urllib3==2.3.0
|
||||
usb-creator==0.3.16
|
||||
userpath==1.9.2
|
||||
wadllib==2.0.0
|
||||
webencodings==0.5.1
|
||||
websocket-client==1.8.0
|
||||
wheel==0.45.1
|
||||
xdg==5
|
||||
xkit==0.0.0
|
||||
zipp==3.21.0
|
||||
30
README.md
30
README.md
@@ -36,7 +36,35 @@
|
||||
|
||||
Плагины должны соединиться с `localhost:11434` и подгрузить доступные модели из контейнера.
|
||||
|
||||
Есть веб-морда по адресу [localhost:9999](http://localhost:9999).
|
||||
## Использование RAG системы
|
||||
|
||||
RAG (Retrieval-Augmented Generation) система позволяет задавать вопросы по содержимому документации.
|
||||
|
||||
Для работы RAG системы необходимо:
|
||||
|
||||
1. Установить необходимые системные зависимости (требуется только один раз):
|
||||
```bash
|
||||
sudo apt install -y python3-pip python3.13-venv
|
||||
```
|
||||
|
||||
2. Создать виртуальное окружение и установить Python-зависимости:
|
||||
```bash
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install requests numpy scikit-learn
|
||||
```
|
||||
|
||||
3. Запустить сервер Ollama (если еще не запущен):
|
||||
```bash
|
||||
./run.sh
|
||||
```
|
||||
|
||||
4. Запустить RAG систему:
|
||||
```bash
|
||||
./run_rag.sh
|
||||
```
|
||||
|
||||
После запуска система задаст пример вопроса и выведет ответ.
|
||||
|
||||
## Дополнительные материалы
|
||||
|
||||
|
||||
@@ -3,8 +3,5 @@
|
||||
{
|
||||
"path": "."
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"localaipilot.standalone.ollamaCodeModel": "deepseek-coder:6.7b-base"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user