Compare commits
2 Commits
3b15a6a19e
...
f3672e6ffd
| Author | SHA1 | Date | |
|---|---|---|---|
|
f3672e6ffd
|
|||
|
3f2491db27
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,7 +1,8 @@
|
|||||||
/.data/*
|
/.data/*
|
||||||
/rag/input_html/*
|
/rag/input_html/*
|
||||||
/rag/input_md/*
|
/rag/data/*
|
||||||
/rag/sys_prompt.txt
|
/rag/sys_prompt.txt
|
||||||
|
/rag/chats/*.md
|
||||||
|
|
||||||
.old/
|
.old/
|
||||||
.venv/
|
.venv/
|
||||||
|
|||||||
3
.idea/.gitignore
generated
vendored
Normal file
3
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
||||||
7
.idea/misc.xml
generated
Normal file
7
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Black">
|
||||||
|
<option name="sdkName" value="Python 3.13" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13 (ollama)" project-jdk-type="Python SDK" />
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/ollama.iml" filepath="$PROJECT_DIR$/.idea/ollama.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
17
.idea/ollama.iml
generated
Normal file
17
.idea/ollama.iml
generated
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="jdk" jdkName="Python 3.13 (ollama)" jdkType="Python SDK" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
<component name="PyDocumentationSettings">
|
||||||
|
<option name="format" value="GOOGLE" />
|
||||||
|
<option name="myDocStringFormat" value="Google" />
|
||||||
|
</component>
|
||||||
|
<component name="TestRunnerService">
|
||||||
|
<option name="PROJECT_TEST_RUNNER" value="py.test" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
* python, venv, pip
|
* python, venv, pip
|
||||||
* [docker](https://docker.com)
|
* [docker](https://docker.com)
|
||||||
* [ollama](https://ollama.com)
|
* [ollama](https://ollama.com)
|
||||||
* [qdrant](https://qdrant.tech)
|
* [qdrant](https://qdrant.tech/documentation/quickstart/)
|
||||||
* [open-webui](https://docs.openwebui.com)
|
* [open-webui](https://docs.openwebui.com)
|
||||||
|
|
||||||
## Как использовать
|
## Как использовать
|
||||||
|
|||||||
@@ -37,9 +37,10 @@ RAG (Retrieval-Augmented Generation) — это архитектура, кото
|
|||||||
```
|
```
|
||||||
rag/
|
rag/
|
||||||
├── input_html/ # Входные файлы HTML, загруженные из Confluence
|
├── input_html/ # Входные файлы HTML, загруженные из Confluence
|
||||||
├── input_md/ # Входные (конвертированные) файлы Markdown
|
├── data/ # Входные (конвертированные) Markdown и прочие текстовые файлы
|
||||||
├── download.sh # Скрипт для загрузки страниц из Confluence
|
├── chats/ # Директория для сохранения чатов
|
||||||
├── convert.py # Скрипт конвертации HTML в Markdown
|
├── download.sh # Скрипт для загрузки страниц из Confluence
|
||||||
|
├── convert.py # Скрипт конвертации HTML в Markdown
|
||||||
├── vectorize.py # Скрипт векторизации Markdown
|
├── vectorize.py # Скрипт векторизации Markdown
|
||||||
├── rag.py # Основной скрипт RAG системы
|
├── rag.py # Основной скрипт RAG системы
|
||||||
├── clear.sh # Скрипт очистки html/md файлов
|
├── clear.sh # Скрипт очистки html/md файлов
|
||||||
@@ -120,7 +121,7 @@ https://conf.company.ltd/pages/viewpreviousversions.action?pageId=987654321
|
|||||||
python3 convert.py
|
python3 convert.py
|
||||||
```
|
```
|
||||||
|
|
||||||
В результате все html-файлы будут сохранены в директорию `./input_md/`.
|
В результате все html-файлы будут сохранены в директорию `./data/`.
|
||||||
Файлы будут названы по заголовкам страниц, внутри также сохранится ссылка на исходную страницу `@@...@@`.
|
Файлы будут названы по заголовкам страниц, внутри также сохранится ссылка на исходную страницу `@@...@@`.
|
||||||
|
|
||||||
Для получения справки по скрипту выполни команду:
|
Для получения справки по скрипту выполни команду:
|
||||||
@@ -131,9 +132,9 @@ python3 convert.py --help
|
|||||||
|
|
||||||
### 3. Векторизация (индексирование)
|
### 3. Векторизация (индексирование)
|
||||||
|
|
||||||
Файлы `./input_md/*.md` должны быть проиндексированы.
|
Файлы `./data/*` должны быть проиндексированы.
|
||||||
|
|
||||||
Для того, чтобы проиндексировать Markdown-документы, выполнить команду:
|
Для того, чтобы проиндексировать документы, выполнить команду:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 vectorize.py
|
python3 vectorize.py
|
||||||
@@ -184,6 +185,9 @@ python3 rag.py --query "твой запрос здесь"
|
|||||||
python3 rag.py --help
|
python3 rag.py --help
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> У скрипта очень довольно аргументов для гибкой настройки.
|
||||||
|
|
||||||
### Кастомный системный промпт
|
### Кастомный системный промпт
|
||||||
|
|
||||||
Если хочется уточнить роль генеративной модели, можно создать файл `sys_prompt.txt` и прописать туда всё необходимое, учитывая следующие правила:
|
Если хочется уточнить роль генеративной модели, можно создать файл `sys_prompt.txt` и прописать туда всё необходимое, учитывая следующие правила:
|
||||||
@@ -212,10 +216,15 @@ python3 rag.py --help
|
|||||||
Для генерации ответов:
|
Для генерации ответов:
|
||||||
|
|
||||||
- `qwen2.5:3b` (по умолчанию)
|
- `qwen2.5:3b` (по умолчанию)
|
||||||
|
- `qwen3:8b`
|
||||||
- `gemma3n:e2b`
|
- `gemma3n:e2b`
|
||||||
- `phi4-mini:3.8b`
|
- `phi4-mini:3.8b`
|
||||||
- `qwen2.5:1.5b`
|
- `qwen2.5:1.5b`
|
||||||
- ...
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Чем меньше млрд параметров (b, billion), тем меньше вероятности получить корректный ответ на не-английском языке.
|
||||||
|
> Такие модели работают быстро, но качество ответов низкое.
|
||||||
|
> Чем больше параметров, тем лучше и медленее ответы.
|
||||||
|
|
||||||
## Дисклеймер
|
## Дисклеймер
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
* [ ] Описать подготовку знаний в Open WebUI
|
* [ ] Описать подготовку знаний в Open WebUI
|
||||||
* [ ] Обработка pdf, json, ...
|
* [ ] Обработка pdf, json, ...
|
||||||
* [ ] Ранжировние результатов
|
* [ ] Ранжировние результатов
|
||||||
|
* [ ] Конвертирование таблиц в списки
|
||||||
* [ ] Режим диалога (запоминание запросов и ответов)
|
* [ ] Режим диалога (запоминание запросов и ответов)
|
||||||
* [ ] API
|
* [ ] API
|
||||||
* [ ] Telegram-бот
|
* [ ] Telegram-бот
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
rm -rf ./input_html/*.html
|
rm -rf ./input_html/*.html
|
||||||
rm -rf ./input_md/*.md
|
rm -rf ./data/*
|
||||||
|
touch ./data/.gitkeep
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ def convert_html_to_md(input_dir, output_dir):
|
|||||||
md_content = markdownify.markdownify(html_content, heading_style="ATX")
|
md_content = markdownify.markdownify(html_content, heading_style="ATX")
|
||||||
with open(output_path, "w", encoding="utf-8") as f:
|
with open(output_path, "w", encoding="utf-8") as f:
|
||||||
f.write(md_content)
|
f.write(md_content)
|
||||||
print(f"Converted {input_path} to {output_path}")
|
print(f"Готово: {output_path}")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(description="Конвертер HTML-файлов в Markdown")
|
parser = argparse.ArgumentParser(description="Конвертер HTML-файлов в Markdown")
|
||||||
parser.add_argument("--input-dir", type=str, default="input_html", help="Директория с HTML-файлами для конвертации")
|
parser.add_argument("--input-dir", type=str, default="input_html", help="Директория с HTML-файлами для конвертации")
|
||||||
parser.add_argument("--output-dir", type=str, default="input_md", help="Директория для сохранения Markdown-файлов")
|
parser.add_argument("--output-dir", type=str, default="data", help="Директория для сохранения Markdown-файлов")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
convert_html_to_md(args.input_dir, args.output_dir)
|
convert_html_to_md(args.input_dir, args.output_dir)
|
||||||
|
|||||||
0
rag/data/.gitkeep
Normal file
0
rag/data/.gitkeep
Normal file
@@ -21,7 +21,7 @@ OUTPUT_PATH="./input_html"
|
|||||||
[ ! -d "$OUTPUT_PATH" ] && mkdir -p "$OUTPUT_PATH"
|
[ ! -d "$OUTPUT_PATH" ] && mkdir -p "$OUTPUT_PATH"
|
||||||
|
|
||||||
for PAGE_ID in "$@"; do
|
for PAGE_ID in "$@"; do
|
||||||
API_ENDPOINT="${CONF_URL}/rest/api/content/${PAGE_ID}?expand=body.view,children.page"
|
API_ENDPOINT="${CONF_URL}/rest/api/content/${PAGE_ID}?expand=body.view,children.page,version"
|
||||||
echo
|
echo
|
||||||
echo "Загрузка: $API_ENDPOINT"
|
echo "Загрузка: $API_ENDPOINT"
|
||||||
|
|
||||||
@@ -37,25 +37,40 @@ for PAGE_ID in "$@"; do
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
TITLE=$(echo "$RESPONSE" | jq -r .title)
|
|
||||||
CONTENT=$(echo "$RESPONSE" | jq -r .body.view.value)
|
CONTENT=$(echo "$RESPONSE" | jq -r .body.view.value)
|
||||||
|
|
||||||
if [ -z "$CONTENT" ]; then
|
if [ -z "$CONTENT" ]; then
|
||||||
echo "Пустая страница, пропущено"
|
echo "Пустая страница, пропущено"
|
||||||
exit
|
exit
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
TITLE=$(echo "$RESPONSE" | jq -r .title)
|
||||||
|
VERSION_NUM=$(echo "$RESPONSE" | jq -r .version.number)
|
||||||
|
VERSION_WHEN=$(date -d "$(echo "$RESPONSE" | jq -r .version.when)" +'%d.%m.%Y %H:%M:%S %Z')
|
||||||
|
VERSION_BY=$(echo "$RESPONSE" | jq -r .version.by.username)
|
||||||
|
|
||||||
TITLE_ESC="${TITLE//\//_}"
|
TITLE_ESC="${TITLE//\//_}"
|
||||||
FILENAME="$TITLE_ESC.html"
|
FILENAME="$TITLE_ESC.html"
|
||||||
if [ "$(echo "$FILENAME" | wc -c)" -gt 255 ]; then # измерение по байтам, а не длине
|
if [ "$(echo "$FILENAME" | wc -c)" -gt 255 ]; then # измерение по байтам, а не длине
|
||||||
FILENAME="${TITLE_ESC:0:120}.html"
|
FILENAME="${TITLE_ESC:0:120}.html"
|
||||||
fi
|
fi
|
||||||
HTML_FILEPATH="$OUTPUT_PATH/$FILENAME"
|
HTML_FILEPATH="$OUTPUT_PATH/$FILENAME"
|
||||||
CONTENT=${CONTENT//href=\"\//href=\"$CONF_URL}
|
CONTENT=${CONTENT//href=\"\//href=\"$CONF_URL/}
|
||||||
CONTENT=${CONTENT//src=\"\//src=\"$CONF_URL}
|
CONTENT=${CONTENT//src=\"\//src=\"$CONF_URL/}
|
||||||
|
|
||||||
URL="$CONF_URL/pages/viewpage.action?pageId=$PAGE_ID"
|
URL="$CONF_URL/pages/viewpage.action?pageId=$PAGE_ID"
|
||||||
echo -e "@@$URL@@\n<br><html><body>Исходная страница: <a href=$URL>$URL</a><br><br><h1>$TITLE</h1>$CONTENT</body></html>" > "$HTML_FILEPATH"
|
|
||||||
|
cat > "$HTML_FILEPATH" <<EOF
|
||||||
|
<html><body>
|
||||||
|
<!-- разметка для эмбеддинга -->
|
||||||
|
@@ $URL @@
|
||||||
|
^^ $VERSION_NUM ^^
|
||||||
|
%% $VERSION_BY %%
|
||||||
|
== $VERSION_WHEN ==
|
||||||
|
<!-- / разметка для эмбеддинга -->
|
||||||
|
<h1>$TITLE</h1>
|
||||||
|
$CONTENT
|
||||||
|
</body></html>
|
||||||
|
EOF
|
||||||
echo "Сохранено: $OUTPUT_PATH/$TITLE.html"
|
echo "Сохранено: $OUTPUT_PATH/$TITLE.html"
|
||||||
|
|
||||||
CHILD_IDS=$(echo "$RESPONSE" | jq -r '.children.page.results[]?.id' 2>/dev/null)
|
CHILD_IDS=$(echo "$RESPONSE" | jq -r '.children.page.results[]?.id' 2>/dev/null)
|
||||||
|
|||||||
347
rag/rag.py
347
rag/rag.py
@@ -1,36 +1,40 @@
|
|||||||
import argparse
|
|
||||||
import os
|
import os
|
||||||
import hashlib
|
|
||||||
import requests
|
import requests
|
||||||
|
import json
|
||||||
|
import time
|
||||||
from sentence_transformers import SentenceTransformer
|
from sentence_transformers import SentenceTransformer
|
||||||
|
|
||||||
class LocalRAGSystem:
|
class RagSystem:
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
md_folder: str = "input_md",
|
md_folder: str = "data",
|
||||||
ollama_url: str = "http://localhost:11434",
|
ollama_url: str = "http://localhost:11434",
|
||||||
qdrant_host: str = "localhost",
|
qdrant_host: str = "localhost",
|
||||||
qdrant_port: int = 6333,
|
qdrant_port: int = 6333,
|
||||||
embed_model: str = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2",
|
embed_model: str = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2",
|
||||||
chat_model: str = "qwen2.5:3b"):
|
chat_model: str = "phi4-mini:3.8b"):
|
||||||
self.md_folder = md_folder
|
self.md_folder = md_folder
|
||||||
self.ollama_url = ollama_url
|
self.ollama_url = ollama_url
|
||||||
self.qdrant_host = qdrant_host
|
self.qdrant_host = qdrant_host
|
||||||
self.qdrant_port = qdrant_port
|
self.qdrant_port = qdrant_port
|
||||||
self.embed_model = embed_model
|
|
||||||
self.chat_model = chat_model
|
self.chat_model = chat_model
|
||||||
self.emb_model = SentenceTransformer(embed_model)
|
self.emb_model = SentenceTransformer(embed_model)
|
||||||
self.prompt = ""
|
self.prompt = ""
|
||||||
|
self.conversation_history = []
|
||||||
|
self.load_chat_model()
|
||||||
|
|
||||||
def get_embedding(self, text: str):
|
def load_chat_model(self):
|
||||||
return self.emb_model.encode(text, show_progress_bar=False).tolist()
|
url = f"{self.ollama_url}/api/generate"
|
||||||
|
body = {"model": self.chat_model}
|
||||||
|
requests.post(url, json=body, timeout=600)
|
||||||
|
|
||||||
def search_qdrant(self, query: str, top_k: int = 6):
|
def search_qdrant(self, query: str, top_k: int = 6, qdrant_collection="rag"):
|
||||||
query_vec = self.get_embedding(query)
|
query_vec = self.emb_model.encode(query, show_progress_bar=False).tolist()
|
||||||
url = f"http://{self.qdrant_host}:{self.qdrant_port}/collections/rag_collection/points/search"
|
url = f"http://{self.qdrant_host}:{self.qdrant_port}/collections/{qdrant_collection}/points/search"
|
||||||
payload = {
|
payload = {
|
||||||
"vector": query_vec,
|
"vector": query_vec,
|
||||||
"top": top_k,
|
"top": top_k,
|
||||||
"with_payload": True
|
"with_payload": True,
|
||||||
|
# "score_threshold": 0.6
|
||||||
}
|
}
|
||||||
resp = requests.post(url, json=payload)
|
resp = requests.post(url, json=payload)
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
@@ -38,69 +42,178 @@ class LocalRAGSystem:
|
|||||||
results = resp.json().get("result", [])
|
results = resp.json().get("result", [])
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def generate_answer(self, prompt: str):
|
|
||||||
url = f"{self.ollama_url}/api/generate"
|
|
||||||
body = {
|
|
||||||
"model": self.chat_model,
|
|
||||||
"prompt": prompt,
|
|
||||||
"stream": False
|
|
||||||
}
|
|
||||||
resp = requests.post(url, json=body, timeout=600)
|
|
||||||
if resp.status_code != 200:
|
|
||||||
return f"Ошибка генерации ответа: {resp.status_code} {resp.text}"
|
|
||||||
return resp.json().get("response", "").strip()
|
|
||||||
|
|
||||||
def prepare_sources(self, context_docs: list):
|
def prepare_sources(self, context_docs: list):
|
||||||
sources = ""
|
sources = ""
|
||||||
for idx, doc in enumerate(context_docs, start=1):
|
for idx, doc in enumerate(context_docs, start=1):
|
||||||
text = doc['payload'].get("text", "").strip()
|
text = doc['payload'].get("text", "").strip()
|
||||||
sources = f"{sources}\n--- Source [{idx}] ---\n{text}\n"
|
sources = f"{sources}\n<source id=\"{idx}\">\n{text}\n</source>\n"
|
||||||
return sources.strip()
|
return sources
|
||||||
|
|
||||||
def prepare_prompt(self, query: str, context_docs: list):
|
def prepare_prompt(self, query: str, context_docs: list):
|
||||||
sources = self.prepare_sources(context_docs)
|
sources = self.prepare_sources(context_docs)
|
||||||
if os.path.exists('sys_prompt.txt'):
|
if os.path.exists('sys_prompt.txt'):
|
||||||
with open('sys_prompt.txt', 'r') as fp:
|
with open('sys_prompt.txt', 'r') as fp:
|
||||||
return fp.read().replace("{{sources}}", sources).replace("{{query}}", query)
|
prompt_template = fp.read()
|
||||||
|
return prompt_template.replace("{{sources}}", sources).replace("{{query}}", query)
|
||||||
else:
|
else:
|
||||||
return f"""
|
return f"""### Your role
|
||||||
Please provide an answer based solely on the provided sources.
|
You are a helpful assistant that can answer questions based on the provided sources.
|
||||||
It is prohibited to generate an answer based on your pretrained data.
|
|
||||||
If uncertain, ask the user for clarification.
|
|
||||||
Respond in the same language as the user's query.
|
|
||||||
If there are no sources in context, clearly state that.
|
|
||||||
If the context is unreadable or of poor quality, inform the user and provide the best possible answer.
|
|
||||||
When referencing information from a source, cite the appropriate source(s) using their corresponding numbers.
|
|
||||||
Every answer should include at least one source citation.
|
|
||||||
Only cite a source when you are explicitly referencing it.
|
|
||||||
|
|
||||||
If none of the sources are helpful, you should indicate that.
|
### Your user
|
||||||
For example:
|
User is a human who is asking a question related to the provided sources.
|
||||||
|
|
||||||
--- Source 1 ---
|
### Your task
|
||||||
The sky is red in the evening and blue in the morning.
|
Please provide an answer based solely on the provided sources and the conversation history.
|
||||||
|
|
||||||
--- Source 2 ---
|
### Rules
|
||||||
Water is wet when the sky is red.
|
- You **MUST** respond in the SAME language as the user's query.
|
||||||
|
- If uncertain, you **MUST** the user for clarification.
|
||||||
|
- If there are no sources in context, you **MUST** clearly state that.
|
||||||
|
- If none of the sources are helpful, you **MUST** clearly state that.
|
||||||
|
- If you are unsure about the answer, you **MUST** clearly state that.
|
||||||
|
- If the context is unreadable or of poor quality, you **MUST** inform the user and provide the best possible answer.
|
||||||
|
- When referencing information from a source, you **MUST** cite the appropriate source(s) using their corresponding numbers.
|
||||||
|
- **Only include inline citations using [id] (e.g., [1], [2]) when the <source> tag includes an id attribute.**
|
||||||
|
- You NEVER MUST NOT add <source> or any XML/HTML tags in your response.
|
||||||
|
- You NEVER MUST NOT cite if the <source> tag does not contain an id attribute.
|
||||||
|
- Every answer MAY include at least one source citation.
|
||||||
|
- Only cite a source when you are explicitly referencing it.
|
||||||
|
- You may also cite multiple sources if they are all relevant to the question.
|
||||||
|
- Ensure citations are concise and directly related to the information provided.
|
||||||
|
- You CAN format your responses using Markdown.
|
||||||
|
|
||||||
Query: When is water wet?
|
### Example of sources list:
|
||||||
Answer: Water will be wet when the sky is red [2], which occurs in the evening [1].
|
|
||||||
|
|
||||||
Now it's your turn. Below are several numbered sources of information:
|
```
|
||||||
{context}
|
<source id="1">The sky is red in the evening and blue in the morning.</source>
|
||||||
|
<source id="2">Water is wet when the sky is red.</source>
|
||||||
|
<query>When is water wet?</query>
|
||||||
|
```
|
||||||
|
Response:
|
||||||
|
```
|
||||||
|
Water will be wet when the sky is red [2], which occurs in the evening [1].
|
||||||
|
```
|
||||||
|
|
||||||
User query: {query}
|
### Now let's start!
|
||||||
Your answer:
|
|
||||||
"""
|
```
|
||||||
|
{sources}
|
||||||
|
<query>{query}</query>
|
||||||
|
```
|
||||||
|
|
||||||
|
Respond."""
|
||||||
|
|
||||||
|
def generate_answer(self, prompt: str):
|
||||||
|
url = f"{self.ollama_url}/api/generate"
|
||||||
|
body = {
|
||||||
|
"model": self.chat_model,
|
||||||
|
"prompt": prompt,
|
||||||
|
"messages": self.conversation_history,
|
||||||
|
"stream": False,
|
||||||
|
# "options": {
|
||||||
|
# "temperature": 0.4,
|
||||||
|
# "top_p": 0.1,
|
||||||
|
# },
|
||||||
|
}
|
||||||
|
self.response = requests.post(url, json=body, timeout=900)
|
||||||
|
if self.response.status_code != 200:
|
||||||
|
return f"Ошибка генерации ответа: {self.response.status_code} {self.response.text}"
|
||||||
|
return self.response.json().get("response", "").strip()
|
||||||
|
|
||||||
|
def generate_answer_stream(self, prompt: str):
|
||||||
|
url = f"{self.ollama_url}/api/generate"
|
||||||
|
body = {
|
||||||
|
"model": self.chat_model,
|
||||||
|
"prompt": prompt,
|
||||||
|
"messages": self.conversation_history,
|
||||||
|
"stream": True
|
||||||
|
}
|
||||||
|
resp = requests.post(url, json=body, stream=True, timeout=900)
|
||||||
|
if resp.status_code != 200:
|
||||||
|
raise RuntimeError(f"Ошибка генерации ответа: {resp.status_code} {resp.text}")
|
||||||
|
full_answer = ""
|
||||||
|
for chunk in resp.iter_lines():
|
||||||
|
if chunk:
|
||||||
|
try:
|
||||||
|
decoded_chunk = chunk.decode('utf-8')
|
||||||
|
data = json.loads(decoded_chunk)
|
||||||
|
if "response" in data:
|
||||||
|
yield data["response"]
|
||||||
|
full_answer += data["response"]
|
||||||
|
elif "error" in data:
|
||||||
|
print(f"Stream error: {data['error']}")
|
||||||
|
break
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print(f"Could not decode JSON from chunk: {chunk.decode('utf-8')}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing chunk: {e}")
|
||||||
|
|
||||||
|
def get_prompt_eval_count(self):
|
||||||
|
if not self.response:
|
||||||
|
return 0
|
||||||
|
return self.response.json().get("prompt_eval_count", 0)
|
||||||
|
|
||||||
|
def get_prompt_eval_duration(self):
|
||||||
|
if not self.response:
|
||||||
|
return 0
|
||||||
|
return self.response.json().get("prompt_eval_duration", 0) / (10 ** 9)
|
||||||
|
|
||||||
|
def get_eval_count(self):
|
||||||
|
if not self.response:
|
||||||
|
return 0
|
||||||
|
return self.response.json().get("eval_count", 0)
|
||||||
|
|
||||||
|
def get_eval_duration(self):
|
||||||
|
if not self.response:
|
||||||
|
return 0
|
||||||
|
return self.response.json().get("eval_duration", 0) / (10 ** 9)
|
||||||
|
|
||||||
|
def get_total_duration(self):
|
||||||
|
if not self.response:
|
||||||
|
return 0
|
||||||
|
return self.response.json().get("total_duration", 0) / (10 ** 9)
|
||||||
|
|
||||||
|
def get_tps(self):
|
||||||
|
eval_count = self.get_eval_count()
|
||||||
|
eval_duration = self.get_eval_duration()
|
||||||
|
if eval_count == 0 or eval_duration == 0:
|
||||||
|
return 0
|
||||||
|
return eval_count / eval_duration
|
||||||
|
|
||||||
def print_sources(context_docs: list):
|
def print_sources(context_docs: list):
|
||||||
|
print("\n\nИсточники:")
|
||||||
for idx, doc in enumerate(context_docs, start=1):
|
for idx, doc in enumerate(context_docs, start=1):
|
||||||
filename = doc['payload'].get("filename", None)
|
title = doc['payload'].get("filename", None)
|
||||||
url = doc['payload'].get("url", None)
|
url = doc['payload'].get("url", None)
|
||||||
title = filename
|
date = doc['payload'].get("date", None)
|
||||||
|
version = doc['payload'].get("version", None)
|
||||||
|
author = doc['payload'].get("author", None)
|
||||||
|
|
||||||
if url is None:
|
if url is None:
|
||||||
url = "(нет веб-ссылки)"
|
url = "(нет веб-ссылки)"
|
||||||
print(f"{idx}. {title}\n {url}")
|
if date is None:
|
||||||
|
date = "(неизвестно)"
|
||||||
|
if version is None:
|
||||||
|
version = "0"
|
||||||
|
if author is None:
|
||||||
|
author = "(неизвестен)"
|
||||||
|
|
||||||
|
print(f"{idx}. {title}")
|
||||||
|
print(f" {url} (v{version} {author})")
|
||||||
|
print(f" актуальность на {date}")
|
||||||
|
|
||||||
|
def print_v(text: str, is_verbose: bool):
|
||||||
|
if is_verbose:
|
||||||
|
print(text)
|
||||||
|
|
||||||
|
def print_stats(rag: RagSystem):
|
||||||
|
print("\n\nСтатистика:")
|
||||||
|
print(f"* Time: {rag.get_total_duration()}s")
|
||||||
|
print(f"* TPS: {rag.get_tps()}")
|
||||||
|
print(f"* PEC: {rag.get_prompt_eval_count()}")
|
||||||
|
print(f"* PED: {rag.get_prompt_eval_duration()}s")
|
||||||
|
print(f"* EC: {rag.get_eval_count()}")
|
||||||
|
print(f"* ED: {rag.get_eval_duration()}s\n")
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
import sys
|
import sys
|
||||||
@@ -112,43 +225,47 @@ def main():
|
|||||||
parser.add_argument("--show-prompt", default=False, action=argparse.BooleanOptionalAction, help="Показать полный промпт перед обработкой запроса")
|
parser.add_argument("--show-prompt", default=False, action=argparse.BooleanOptionalAction, help="Показать полный промпт перед обработкой запроса")
|
||||||
parser.add_argument("--qdrant-host", default="localhost", help="Qdrant host")
|
parser.add_argument("--qdrant-host", default="localhost", help="Qdrant host")
|
||||||
parser.add_argument("--qdrant-port", type=int, default=6333, help="Qdrant port")
|
parser.add_argument("--qdrant-port", type=int, default=6333, help="Qdrant port")
|
||||||
|
parser.add_argument("--qdrant-collection", type=str, default="rag", help="Название коллекции для поиска документов")
|
||||||
parser.add_argument("--ollama-url", default="http://localhost:11434", help="Ollama API URL")
|
parser.add_argument("--ollama-url", default="http://localhost:11434", help="Ollama API URL")
|
||||||
parser.add_argument("--emb-model", default="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2", help="Модель эмбеддинга")
|
parser.add_argument("--emb-model", default="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2", help="Модель эмбеддинга")
|
||||||
parser.add_argument("--chat-model", default="qwen2.5:3b", help="Модель генерации Ollama")
|
parser.add_argument("--chat-model", default="phi4-mini:3.8b", help="Модель генерации Ollama")
|
||||||
parser.add_argument("--topk", type=int, default=6, help="Количество документов для поиска")
|
parser.add_argument("--topk", type=int, default=6, help="Количество документов для поиска")
|
||||||
|
parser.add_argument("--verbose", default=False, action=argparse.BooleanOptionalAction, help="Выводить промежуточные служебные сообщения")
|
||||||
|
parser.add_argument("--show-stats", default=False, action=argparse.BooleanOptionalAction, help="Выводить статистику об ответе (не работает с --stream)")
|
||||||
|
parser.add_argument("--stream", default=False, action=argparse.BooleanOptionalAction, help="Выводить статистику об ответе")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if not args.query and not args.interactive:
|
if not args.query and not args.interactive:
|
||||||
print("Ошибка: укажите запрос (--query) и/или используйте интерактивный режим (--interactive)")
|
print("Ошибка: укажите запрос (--query) и/или используйте интерактивный режим (--interactive)")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
print(f"Адрес ollama: {args.ollama_url}")
|
print_v(f"Адрес ollama: {args.ollama_url}", args.verbose)
|
||||||
print(f"Адрес qdrant: {args.qdrant_host}:{args.qdrant_port}")
|
print_v(f"Адрес qdrant: {args.qdrant_host}:{args.qdrant_port}", args.verbose)
|
||||||
print(f"Модель эмбеддинга: {args.emb_model}")
|
print_v(f"Модель эмбеддинга: {args.emb_model}", args.verbose)
|
||||||
print(f"Модель чата: {args.chat_model}")
|
print_v(f"Модель чата: {args.chat_model}", args.verbose)
|
||||||
print(f"Документов для поиска: {args.topk}")
|
print_v(f"Документов для поиска: {args.topk}", args.verbose)
|
||||||
|
print_v(f"Коллекция для поиска: {args.qdrant_collection}", args.verbose)
|
||||||
if os.path.exists('sys_prompt.txt'):
|
if os.path.exists('sys_prompt.txt'):
|
||||||
print("Будет использоваться sys_prompt.txt!")
|
print_v("Будет использоваться sys_prompt.txt!", args.verbose)
|
||||||
|
|
||||||
print("\nПервая инициализация моделей...")
|
print_v("\nПервая инициализация моделей...", args.verbose)
|
||||||
rag = LocalRAGSystem(
|
rag = RagSystem(
|
||||||
ollama_url=args.ollama_url,
|
ollama_url=args.ollama_url,
|
||||||
qdrant_host=args.qdrant_host,
|
qdrant_host=args.qdrant_host,
|
||||||
qdrant_port=args.qdrant_port,
|
qdrant_port=args.qdrant_port,
|
||||||
embed_model=args.emb_model,
|
embed_model=args.emb_model,
|
||||||
chat_model=args.chat_model
|
chat_model=args.chat_model
|
||||||
)
|
)
|
||||||
print(f"Модели загружены. Если ответ плохой, переформулируйте запрос, укажите --chat-model или улучшите исходные данные RAG")
|
print_v(f"Модели загружены. Если ответ плохой, переформулируйте запрос, укажите --chat-model или улучшите исходные данные RAG", args.verbose)
|
||||||
|
|
||||||
|
query = None
|
||||||
if args.interactive:
|
if args.interactive:
|
||||||
print("\nИНТЕРАКТИВНЫЙ РЕЖИМ")
|
print_v("\nИНТЕРАКТИВНЫЙ РЕЖИМ", args.verbose)
|
||||||
print("Можете вводить запрос (или 'exit' для выхода)\n")
|
print_v("Можете вводить запрос (или 'exit' для выхода)\n", args.verbose)
|
||||||
|
|
||||||
if args.query:
|
if args.query:
|
||||||
query = args.query.strip()
|
query = args.query.strip()
|
||||||
print(f">>> {query}")
|
print(f">>> {query}")
|
||||||
else:
|
|
||||||
query = input(">>> ").strip()
|
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
@@ -158,34 +275,104 @@ def main():
|
|||||||
if not query or query == "":
|
if not query or query == "":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if query.lower() == "exit":
|
if query.lower() == "help":
|
||||||
print("\n*** Завершение работы")
|
print("<<< Команды итерактивного режима:")
|
||||||
|
print("save -- сохранить диалог в файл")
|
||||||
|
print("stats -- вывести статистику последнего ответа")
|
||||||
|
print("exit -- выход\n")
|
||||||
|
query = None
|
||||||
|
continue
|
||||||
|
|
||||||
|
if query.strip().lower() == "save":
|
||||||
|
import datetime
|
||||||
|
timestamp = int(time.time())
|
||||||
|
dt = datetime.datetime.fromtimestamp(timestamp).strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||||
|
filename = f"chats/chat-{timestamp}.md"
|
||||||
|
|
||||||
|
markdown_content = f"# История диалога от {dt}\n\n"
|
||||||
|
markdown_content += f"## Параметры диалога\n"
|
||||||
|
markdown_content += f"```\nargs = {args}\n```\n"
|
||||||
|
markdown_content += f"```\nemb_model = {rag.emb_model}\n```\n"
|
||||||
|
|
||||||
|
for entry in rag.conversation_history:
|
||||||
|
if entry['role'] == 'user':
|
||||||
|
markdown_content += f"## Пользователь\n\n"
|
||||||
|
elif entry['role'] == 'assistant':
|
||||||
|
markdown_content += f"## Модель\n\n"
|
||||||
|
docs = rag.prepare_sources(entry['docs']).replace("```", "")
|
||||||
|
markdown_content += f"```\n{docs}\n```\n\n"
|
||||||
|
markdown_content += f"{entry['content']}\n\n"
|
||||||
|
|
||||||
|
os.makedirs('chats', exist_ok=True)
|
||||||
|
with open(filename, 'w') as fp:
|
||||||
|
fp.write(markdown_content)
|
||||||
|
|
||||||
|
print(f"<<< Диалог сохранён в файл: {filename}\n")
|
||||||
|
query = None
|
||||||
|
continue
|
||||||
|
|
||||||
|
if query.strip().lower() == "exit":
|
||||||
|
print_v("\n*** Завершение работы", args.verbose)
|
||||||
break
|
break
|
||||||
|
|
||||||
print("\nПоиск релевантных документов...")
|
print_v("\nПоиск релевантных документов...", args.verbose)
|
||||||
context_docs = rag.search_qdrant(query, top_k=args.topk)
|
context_docs = rag.search_qdrant(query, top_k=args.topk, qdrant_collection=args.qdrant_collection)
|
||||||
if not context_docs:
|
if not context_docs:
|
||||||
print("Релевантные документы не найдены.")
|
print("<<< Релевантные документы не найдены")
|
||||||
if args.interactive:
|
if args.interactive:
|
||||||
|
query = None
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
|
||||||
print(f"Найдено {len(context_docs)} релевантных документов:")
|
print_v(f"Найдено {len(context_docs)} релевантных документов", args.verbose)
|
||||||
print_sources(context_docs)
|
# print_sources(context_docs)
|
||||||
|
|
||||||
prompt = rag.prepare_prompt(query=query, context_docs=context_docs)
|
prompt = rag.prepare_prompt(query=query, context_docs=context_docs)
|
||||||
if args.show_prompt:
|
if args.show_prompt:
|
||||||
print("\nПолный системный промпт: --------------------------\n")
|
print("\nПолный системный промпт: --------------------------")
|
||||||
print(f"{prompt}\n---------------------------------------------------\n")
|
print(f"{prompt}\n---------------------------------------------------")
|
||||||
|
|
||||||
|
print_v("\nГенерация ответа...\n", args.verbose)
|
||||||
|
|
||||||
|
if args.stream:
|
||||||
|
answer = "\n<<< "
|
||||||
|
print(answer, end='', flush=True)
|
||||||
|
try:
|
||||||
|
for message_part in rag.generate_answer_stream(prompt):
|
||||||
|
answer += message_part
|
||||||
|
print(message_part, end='', flush=True)
|
||||||
|
except RuntimeError as e:
|
||||||
|
answer = str(e)
|
||||||
|
print(f"\n{answer}\n===================================================\n")
|
||||||
|
else:
|
||||||
|
answer = rag.generate_answer(prompt)
|
||||||
|
print(f"<<< {answer}\n")
|
||||||
|
|
||||||
|
print_sources(context_docs)
|
||||||
|
if args.show_stats and not args.stream:
|
||||||
|
print_stats(rag)
|
||||||
|
|
||||||
|
rag.conversation_history.append({
|
||||||
|
"role": "user",
|
||||||
|
"content": query,
|
||||||
|
})
|
||||||
|
|
||||||
|
rag.conversation_history.append({
|
||||||
|
"role": "assistant",
|
||||||
|
"docs": context_docs,
|
||||||
|
"content": answer,
|
||||||
|
})
|
||||||
|
|
||||||
|
if args.interactive:
|
||||||
|
query = None
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
print("Генерация ответа...")
|
|
||||||
answer = rag.generate_answer(prompt)
|
|
||||||
print(f"\n<<< {answer}\n===================================================\n")
|
|
||||||
query = None
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\n*** Завершение работы")
|
print("\n*** Завершение работы")
|
||||||
break
|
break
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Ошибка: {e}")
|
print(f"Ошибка: {e}")
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -15,12 +15,39 @@ def load_markdown_files(input_dir):
|
|||||||
content = f.read()
|
content = f.read()
|
||||||
lines = content.splitlines()
|
lines = content.splitlines()
|
||||||
url = None
|
url = None
|
||||||
|
version = None
|
||||||
|
author = None
|
||||||
|
date = None
|
||||||
|
|
||||||
if lines:
|
if lines:
|
||||||
first_line = lines[0].strip()
|
# Проверка первой строки на URL
|
||||||
if first_line.startswith("@@") and first_line.endswith("@@") and len(first_line) > 4:
|
if lines[0].strip().startswith("@@") and lines[0].strip().endswith("@@") and len(lines[0].strip()) > 4:
|
||||||
url = first_line[2:-2].strip()
|
url = lines[0].strip()[2:-2].strip()
|
||||||
content = "\n".join(lines[1:]) # Remove the first line from content
|
lines = lines[1:]
|
||||||
documents.append({"id": filename, "text": content, "url": url})
|
|
||||||
|
# Проверка оставшихся строк на метаданные
|
||||||
|
i = 0
|
||||||
|
while i < len(lines):
|
||||||
|
line = lines[i].strip()
|
||||||
|
if line.startswith("^^") and line.endswith("^^") and len(line) > 4:
|
||||||
|
version = line[2:-2].strip()
|
||||||
|
lines.pop(i)
|
||||||
|
elif line.startswith("%%") and line.endswith("%%") and len(line) > 4:
|
||||||
|
author = line[2:-2].strip()
|
||||||
|
lines.pop(i)
|
||||||
|
elif line.startswith("==") and line.endswith("==") and len(line) > 4:
|
||||||
|
date = line[2:-2].strip()
|
||||||
|
lines.pop(i)
|
||||||
|
else:
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
doc_metadata = {"id": filename, "text": "\n".join(lines)}
|
||||||
|
if url: doc_metadata["url"] = url
|
||||||
|
if version: doc_metadata["version"] = version
|
||||||
|
if author: doc_metadata["author"] = author
|
||||||
|
if date: doc_metadata["date"] = date
|
||||||
|
|
||||||
|
documents.append(doc_metadata)
|
||||||
return documents
|
return documents
|
||||||
|
|
||||||
def chunk_text(texts, chunk_size, chunk_overlap):
|
def chunk_text(texts, chunk_size, chunk_overlap):
|
||||||
@@ -36,12 +63,15 @@ def chunk_text(texts, chunk_size, chunk_overlap):
|
|||||||
for i, chunk in enumerate(doc_chunks):
|
for i, chunk in enumerate(doc_chunks):
|
||||||
chunk_id = f"{doc['id']}_chunk{i}"
|
chunk_id = f"{doc['id']}_chunk{i}"
|
||||||
chunk_dict = {"id": chunk_id, "text": chunk}
|
chunk_dict = {"id": chunk_id, "text": chunk}
|
||||||
if "url" in doc and doc["url"] is not None:
|
|
||||||
chunk_dict["url"] = doc["url"]
|
# Перенос всех доступных метаданных
|
||||||
|
for key in ["url", "version", "author", "date"]:
|
||||||
|
if key in doc and doc[key] is not None:
|
||||||
|
chunk_dict[key] = doc[key]
|
||||||
chunks.append(chunk_dict)
|
chunks.append(chunk_dict)
|
||||||
return chunks
|
return chunks
|
||||||
|
|
||||||
def embed_and_upload(chunks, embedding_model_name, qdrant_host="localhost", qdrant_port=6333):
|
def embed_and_upload(chunks, embedding_model_name, qdrant_host="localhost", qdrant_port=6333, qdrant_collection="rag"):
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
print(f"Инициализация модели {args.embedding_model}")
|
print(f"Инициализация модели {args.embedding_model}")
|
||||||
@@ -49,13 +79,12 @@ def embed_and_upload(chunks, embedding_model_name, qdrant_host="localhost", qdra
|
|||||||
|
|
||||||
print(f"Подключение к qdrant ({qdrant_host}:{qdrant_port})")
|
print(f"Подключение к qdrant ({qdrant_host}:{qdrant_port})")
|
||||||
client = QdrantClient(host=qdrant_host, port=qdrant_port)
|
client = QdrantClient(host=qdrant_host, port=qdrant_port)
|
||||||
collection_name = "rag_collection"
|
|
||||||
|
|
||||||
if client.collection_exists(collection_name):
|
if client.collection_exists(qdrant_collection):
|
||||||
client.delete_collection(collection_name)
|
client.delete_collection(qdrant_collection)
|
||||||
|
|
||||||
client.create_collection(
|
client.create_collection(
|
||||||
collection_name=collection_name,
|
collection_name=qdrant_collection,
|
||||||
vectors_config=models.VectorParams(size=embedder.get_sentence_embedding_dimension(), distance=models.Distance.COSINE),
|
vectors_config=models.VectorParams(size=embedder.get_sentence_embedding_dimension(), distance=models.Distance.COSINE),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -72,7 +101,10 @@ def embed_and_upload(chunks, embedding_model_name, qdrant_host="localhost", qdra
|
|||||||
payload={
|
payload={
|
||||||
"text": chunk["text"],
|
"text": chunk["text"],
|
||||||
"filename": chunk["id"].rsplit(".md_chunk", 1)[0],
|
"filename": chunk["id"].rsplit(".md_chunk", 1)[0],
|
||||||
"url": chunk.get("url", None)
|
"url": chunk.get("url", None),
|
||||||
|
"version": chunk.get("version", None),
|
||||||
|
"author": chunk.get("author", None),
|
||||||
|
"date": chunk.get("date", None)
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
print(f"[{idx}/{total_chunks}] Подготовлен чанк: {chunk['id']} -> ID: {id_hash}")
|
print(f"[{idx}/{total_chunks}] Подготовлен чанк: {chunk['id']} -> ID: {id_hash}")
|
||||||
@@ -80,20 +112,20 @@ def embed_and_upload(chunks, embedding_model_name, qdrant_host="localhost", qdra
|
|||||||
batch_size = 100
|
batch_size = 100
|
||||||
for i in range(0, total_chunks, batch_size):
|
for i in range(0, total_chunks, batch_size):
|
||||||
batch = points[i : i + batch_size]
|
batch = points[i : i + batch_size]
|
||||||
client.upsert(collection_name=collection_name, points=batch)
|
client.upsert(collection_name=qdrant_collection, points=batch)
|
||||||
print(f"Записан батч {(i // batch_size) + 1}, содержащий {len(batch)} точек, всего записано: {min(i + batch_size, total_chunks)}/{total_chunks}")
|
print(f"Записан батч {(i // batch_size) + 1}, содержащий {len(batch)} точек, всего записано: {min(i + batch_size, total_chunks)}/{total_chunks}")
|
||||||
|
|
||||||
print(f"Завершена запись всех {total_chunks} чанков в коллекцию '{collection_name}'.")
|
print(f"Завершена запись всех {total_chunks} чанков в коллекцию '{qdrant_collection}'.")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
print(f"Инициализация...")
|
|
||||||
parser = argparse.ArgumentParser(description="Скрипт векторизаци данных для Qdrant")
|
parser = argparse.ArgumentParser(description="Скрипт векторизаци данных для Qdrant")
|
||||||
parser.add_argument("--input_dir", type=str, default="input_md", help="Директория с Markdown-файлами для чтения")
|
parser.add_argument("--input-dir", type=str, default="data", help="Директория с Markdown-файлами для чтения")
|
||||||
parser.add_argument("--chunk_size", type=int, default=500, help="Размер чанка")
|
parser.add_argument("--chunk-size", type=int, default=500, help="Размер чанка")
|
||||||
parser.add_argument("--chunk_overlap", type=int, default=100, help="Размер перекрытия")
|
parser.add_argument("--chunk-overlap", type=int, default=100, help="Размер перекрытия")
|
||||||
parser.add_argument("--embedding_model", type=str, default="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2", help="Модель эмбеддинга")
|
parser.add_argument("--embedding-model", type=str, default="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2", help="Модель эмбеддинга")
|
||||||
parser.add_argument("--qdrant_host", type=str, default="localhost", help="Адрес хоста Qdrant")
|
parser.add_argument("--qdrant-host", type=str, default="localhost", help="Адрес хоста Qdrant")
|
||||||
parser.add_argument("--qdrant_port", type=int, default=6333, help="Номер порта Qdrant")
|
parser.add_argument("--qdrant-port", type=int, default=6333, help="Номер порта Qdrant")
|
||||||
|
parser.add_argument("--qdrant-collection", type=str, default="rag", help="Название коллекции для сохранения документов")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
documents = load_markdown_files(args.input_dir)
|
documents = load_markdown_files(args.input_dir)
|
||||||
@@ -103,4 +135,4 @@ if __name__ == "__main__":
|
|||||||
chunks = chunk_text(documents, args.chunk_size, args.chunk_overlap)
|
chunks = chunk_text(documents, args.chunk_size, args.chunk_overlap)
|
||||||
print(f"Создано чанков: {len(chunks)} ({args.chunk_size}/{args.chunk_overlap})")
|
print(f"Создано чанков: {len(chunks)} ({args.chunk_size}/{args.chunk_overlap})")
|
||||||
|
|
||||||
embed_and_upload(chunks, args.embedding_model, args.qdrant_host, args.qdrant_port)
|
embed_and_upload(chunks, args.embedding_model, args.qdrant_host, args.qdrant_port, args.qdrant_collection)
|
||||||
|
|||||||
1
up
1
up
@@ -7,4 +7,5 @@ docker compose up -d --build --remove-orphans
|
|||||||
echo "* Ollama доступен по адресу: localhost:$OLLAMA_PORT"
|
echo "* Ollama доступен по адресу: localhost:$OLLAMA_PORT"
|
||||||
echo "* Open WebUI доступен по адресу: http://localhost:$QDRANT_PORT/"
|
echo "* Open WebUI доступен по адресу: http://localhost:$QDRANT_PORT/"
|
||||||
echo "* Qdrant доступен по адресу: localhost:$OWEBUI_PORT"
|
echo "* Qdrant доступен по адресу: localhost:$OWEBUI_PORT"
|
||||||
|
echo "* Qdrant UI доступен по адресу: http://localhost:$OWEBUI_PORT/dashboard"
|
||||||
echo "Для остановки контейнеров выполните ./down"
|
echo "Для остановки контейнеров выполните ./down"
|
||||||
|
|||||||
Reference in New Issue
Block a user