diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 1bb2b85..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "localaipilot.standalone.ollamaCodeModel": "deepseek-coder:6.7b-base" -} diff --git a/@rag/.gitignore b/@rag/.gitignore new file mode 100644 index 0000000..72c9a9e --- /dev/null +++ b/@rag/.gitignore @@ -0,0 +1,10 @@ +/input_html/* +/output_md/* +/ready_rag/* +/venv + +*.html +*.md +*.sqlite* + +!.gitkeep diff --git a/@rag/1_download_page.sh b/@rag/1_download_page.sh new file mode 100755 index 0000000..e45e16e --- /dev/null +++ b/@rag/1_download_page.sh @@ -0,0 +1,49 @@ +#!/bin/bash +DELAY=1 + +# 1. Указать реквизиты доступа к confluence +USERNAME="" +PASSWORD="" +CONFLUENCE_URL="" + +# 2. Вызвать: ./1_download_page.sh + +################################################################## + +if [ $# -lt 1 ]; then + echo "Usage: $0 " + 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 "$content" > "$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 diff --git a/@rag/2_html_to_md.py b/@rag/2_html_to_md.py new file mode 100644 index 0000000..ae1bb30 --- /dev/null +++ b/@rag/2_html_to_md.py @@ -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) diff --git a/@rag/3_rag.py b/@rag/3_rag.py new file mode 100644 index 0000000..56eff45 --- /dev/null +++ b/@rag/3_rag.py @@ -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() diff --git a/@rag/input_html/.gitkeep b/@rag/input_html/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/@rag/output_md/.gitkeep b/@rag/output_md/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/@rag/ready_rag/.gitkeep b/@rag/ready_rag/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/@rag/requirements.txt b/@rag/requirements.txt new file mode 100644 index 0000000..88863ca --- /dev/null +++ b/@rag/requirements.txt @@ -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 diff --git a/README.md b/README.md index f77da9e..52a7f08 100644 --- a/README.md +++ b/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 + ``` + +После запуска система задаст пример вопроса и выведет ответ. ## Дополнительные материалы diff --git a/ollama.code-workspace b/ollama.code-workspace index b068ec1..8f4183e 100644 --- a/ollama.code-workspace +++ b/ollama.code-workspace @@ -3,8 +3,5 @@ { "path": "." } - ], - "settings": { - "localaipilot.standalone.ollamaCodeModel": "deepseek-coder:6.7b-base" - } + ] } diff --git a/run.sh b/run.sh index 03cbfee..d7604cc 100755 --- a/run.sh +++ b/run.sh @@ -20,7 +20,5 @@ docker run \ ghcr.io/open-webui/open-webui:main echo -echo "Ready, opening http://localhost:9999/" +echo "Open WebUI acessible on address http://localhost:9999/" echo - -open http://localhost:9999/