Compare commits

...

4 Commits

Author SHA1 Message Date
a868025a9e add minimal fix 2025-08-03 08:30:12 +00:00
9e86008ba0 add fetures change models in web 2025-08-03 08:03:19 +00:00
669bb5830a update readme and add install.sh 2025-08-03 07:37:00 +00:00
eed3a00efb Connection between the web interface and the logic 2025-08-03 07:22:11 +00:00
6 changed files with 448 additions and 232 deletions

View File

@@ -28,7 +28,7 @@ RUN pip install --upgrade pip && \
WORKDIR /app WORKDIR /app
# Копируем только requirements.txt для кэширования слоя # Копируем только requirements.txt для кэширования слоя
COPY offline_packages/test.txt /app/requirements.txt COPY offline_packages/requirements.txt /app/requirements.txt
# Установка зависимостей (включая GPU-версии) # Установка зависимостей (включая GPU-версии)
RUN pip install --no-cache-dir -r /app/requirements.txt RUN pip install --no-cache-dir -r /app/requirements.txt

101
README.md
View File

@@ -68,12 +68,7 @@ docker run --rm --runtime=nvidia --gpus all ubuntu nvidia-smi
--- ---
## 3. Установка системных зависимостей ## 3. Проверка наличия драйверов видеокарты
### Установка заголовков ядра Linux
```bash
apt install linux-headers-$(uname -r)
```
### Проверка видеодрайверов ### Проверка видеодрайверов
```bash ```bash
@@ -133,8 +128,10 @@ git lfs install
> ✅ Успешное клонирование: > ✅ Успешное клонирование:
```bash ```bash
git clone https://huggingface.co/Qwen/Qwen3-8B mkdir models/
cd Qwen3-8B/ cd models
git clone https://huggingface.co/Qwen/Qwen3-4B
git clone https://huggingface.co/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2
``` ```
--- ---
@@ -244,6 +241,94 @@ history > history.md
--- ---
## Структура проекта
```bash
├── Dockerfile
├── README.md
├── app
│   ├── __pycache__
│   │   └── config.cpython-310.pyc
│   ├── config.py
│   ├── data
│   │   ├── chroma_db
│   │   │   ├── 55652433-5130-4082-b593-3dcf3ad2bc92
│   │   │   └── chroma.sqlite3
│   │   ├── docs
│   │   │   ├── REALITY.md
│   │   │   ├── VLESS-GRPC.md
│   │   │   ├── XTLS-Vision.md
│   │   │   └── marzban_ru.md
│   │   ├── generate_json_files.py
│   │   ├── generated_docs_db.py
│   │   └── parsed_json
│   │   ├── REALITY.json
│   │   ├── VLESS-GRPC.json
│   │   ├── XTLS-Vision.json
│   │   └── marzban_ru.json
│   ├── main.py
│   └── old_without_db.py
├── backup_docker_image.md
├── cache
│   ├── cache
│   │   ├── chroma
│   │   │   └── telemetry_user_id
│   │   └── pip
│   │   ├── http-v2
│   │   └── selfcheck
│   └── site-packages
├── docker-compose.yml
├── history.md
├── models
│   ├── Qwen3-4B
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── config.json
│   │   ├── generation_config.json
│   │   ├── merges.txt
│   │   ├── model-00001-of-00003.safetensors
│   │   ├── model-00002-of-00003.safetensors
│   │   ├── model-00003-of-00003.safetensors
│   │   ├── model.safetensors.index.json
│   │   ├── tokenizer.json
│   │   ├── tokenizer_config.json
│   │   └── vocab.json
│   └── paraphrase-multilingual-MiniLM-L12-v2
│   ├── 1_Pooling
│   │   └── config.json
│   ├── README.md
│   ├── config.json
│   ├── config_sentence_transformers.json
│   ├── model.safetensors
│   ├── modules.json
│   ├── onnx
│   │   ├── model.onnx
│   │   ├── model_O1.onnx
│   │   ├── model_O2.onnx
│   │   ├── model_O3.onnx
│   │   ├── model_O4.onnx
│   │   ├── model_qint8_arm64.onnx
│   │   ├── model_qint8_avx512.onnx
│   │   ├── model_qint8_avx512_vnni.onnx
│   │   └── model_quint8_avx2.onnx
│   ├── openvino
│   │   ├── openvino_model.bin
│   │   ├── openvino_model.xml
│   │   ├── openvino_model_qint8_quantized.bin
│   │   └── openvino_model_qint8_quantized.xml
│   ├── pytorch_model.bin
│   ├── sentence_bert_config.json
│   ├── sentencepiece.bpe.model
│   ├── special_tokens_map.json
│   ├── tf_model.h5
│   ├── tokenizer.json
│   ├── tokenizer_config.json
│   └── unigram.json
└── offline_packages
└── requirements.txt
```
## Выводы ## Выводы
Был успешно выполнен полный цикл: Был успешно выполнен полный цикл:

View File

@@ -1,77 +1,250 @@
from typing import Any, Dict, List, Optional
import gradio as gr import gradio as gr
from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
import torch import torch
import threading import threading
from loguru import logger
from langchain_chroma import Chroma
from langchain_huggingface import HuggingFaceEmbeddings
from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
# Пути к модели # Предполагается, что у тебя есть config.py с settings
model_name = "/models/Qwen3-8B" from config import settings
# Загрузка токенизатора и модели (один раз при старте)
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.float16,
device_map="cuda",
trust_remote_code=True
)
# Отключим кэширование в истории, чтобы каждый запрос был независимым class ChatWithAI:
def generate_response(message, history): def __init__(self, model_name: str):
# Форматируем диалог: используем только текущую историю self.model_name = model_name
prompt = "" self.embeddings = HuggingFaceEmbeddings(
for human, assistant in history: model_name=settings.LM_MODEL_NAME,
prompt += f"<|im_start|>user\n{human}<|im_end|>\n<|im_start|>assistant\n{assistant}<|im_end|>\n" model_kwargs={"device": "cuda"},
prompt += f"<|im_start|>user\n{message}<|im_end|>\n<|im_start|>assistant\n" encode_kwargs={"normalize_embeddings": True},
)
# Токенизация self.chroma_db = Chroma(
inputs = tokenizer(prompt, return_tensors="pt").to(model.device) persist_directory=settings.DOCS_CHROMA_PATH,
embedding_function=self.embeddings,
collection_name=settings.DOCS_COLLECTION_NAME,
)
# Создаём уникальный streamer для каждого запроса self.tokenizer = None
streamer = TextIteratorStreamer( self.model = None
tokenizer, self.streamer = None
skip_prompt=True,
skip_special_tokens=True
)
# Параметры генерации self.load_model(model_name)
generation_kwargs = {
"input_ids": inputs["input_ids"], def load_model(self, model_name: str):
"max_new_tokens": 1024, """Загружает модель и токенизатор, освобождает предыдущие ресурсы."""
"temperature": 0.6, if hasattr(self, "model") and self.model is not None:
"top_p": 0.9, del self.model
"do_sample": True, torch.cuda.empty_cache()
"pad_token_id": tokenizer.eos_token_id, logger.info("Предыдущая модель удалена.")
"streamer": streamer,
logger.info(f"Загрузка модели: {model_name}")
try:
self.tokenizer = AutoTokenizer.from_pretrained(model_name)
self.model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.float32,
device_map="cuda",
)
logger.info(f"Модель {model_name} успешно загружена.")
except Exception as e:
logger.error(f"Ошибка при загрузке модели {model_name}: {e}")
raise
def get_relevant_context(self, query: str, k: int = 3) -> List[Dict[str, Any]]:
"""Получение релевантного контекста из базы данных."""
try:
results = self.chroma_db.similarity_search(query, k=k)
return [
{
"text": doc.page_content,
"metadata": doc.metadata,
}
for doc in results
]
except Exception as e:
logger.error(f"Ошибка при получении контекста: {e}")
return []
def format_context(self, context: List[Dict[str, Any]]) -> str:
"""Форматирование контекста для промпта."""
formatted_context = []
for item in context:
metadata_str = "\n".join(f"{k}: {v}" for k, v in item["metadata"].items())
formatted_context.append(
f"Текст: {item['text']}\nМетаданные:\n{metadata_str}\n"
)
return "\n---\n".join(formatted_context)
def generate_response_stream(self, query: str):
"""Генерация ответа с потоковой передачей токенов."""
try:
context = self.get_relevant_context(query)
if not context:
yield "Извините, не удалось найти релевантный контекст для ответа."
return
formatted_context = self.format_context(context)
messages = [
{
"role": "system",
"content": """Ты — внутренний менеджер компании Amvera Cloud. Отвечаешь по делу без лишних вступлений.
Правила:
1. Сразу переходи к сути, без фраз типа "На основе контекста"
2. Используй только факты. Если точных данных нет — отвечай общими фразами об Marzban, но не придумывай конкретику
3. Используй обычный текст без форматирования
4. Включай ссылки только если они есть в контексте
5. Говори от первого лица множественного числа: "Мы предоставляем", "У нас есть"
6. При упоминании файлов делай это естественно, например: "Я прикреплю инструкцию, где подробно описаны шаги"
7. На приветствия отвечай доброжелательно, на негатив — с легким юмором
8. Можешь при ответах использовать общую информацию из открытых источников по Marzban, но опирайся на контекст
9. Если пользователь спрашивает о ценах, планах или технических характеристиках — давай конкретные ответы из контекста
10. При технических вопросах предлагай практические решения
Персонализируй ответы, упоминая имя клиента если оно есть в контексте. Будь краток, информативен и полезен.""",
},
{
"role": "user",
"content": f"Вопрос: {query}\nКонтекст: {formatted_context}",
},
]
# Применяем шаблон чата
prompt = self.tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True,
enable_thinking=False
)
# Подготавливаем вход
inputs = self.tokenizer(prompt, return_tensors="pt").to("cuda")
# Создаём новый streamer
self.streamer = TextIteratorStreamer(
self.tokenizer,
skip_prompt=True,
skip_special_tokens=True
)
# Параметры генерации
generate_kwargs = {
"input_ids": inputs["input_ids"],
"max_new_tokens": 512,
"temperature": 0.7,
"do_sample": True,
"top_p": 0.9,
"pad_token_id": self.tokenizer.eos_token_id,
"streamer": self.streamer,
}
# Запускаем генерацию в отдельном потоке
thread = threading.Thread(target=self.model.generate, kwargs=generate_kwargs)
thread.start()
# Потоковая передача токенов
buffer = ""
for token in self.streamer:
buffer += token
yield buffer
except Exception as e:
logger.error(f"Ошибка при генерации ответа: {e}")
yield "Произошла ошибка при генерации ответа."
# === Gradio интерфейс с выбором модели ===
def main():
# Список доступных моделей (можно вынести в settings)
MODEL_OPTIONS = {
"Qwen3-4B (локальная)": getattr(settings, "LOCAL_LLM_NAME", "/models/Qwen3-4B"),
"Alibaba/Qwen2.5-1.8B-Chat": "Alibaba/Qwen2.5-1.8B-Chat",
"HuggingFaceH4/zephyr-7b-beta": "HuggingFaceH4/zephyr-7b-beta",
# Добавь другие модели по желанию
} }
# Запускаем генерацию в отдельном потоке chat = None # Будет инициализирована при выборе модели
thread = threading.Thread(target=model.generate, kwargs=generation_kwargs)
thread.start()
# Постепенно возвращаем результат def initialize_model(selected_model_key):
buffer = "" """Создаёт или перезагружает экземпляр ChatWithAI с выбранной моделью."""
for new_text in streamer: nonlocal chat
buffer += new_text if chat is not None:
yield buffer.strip() logger.info(f"Смена модели")
# Удаляем экземпляр
del chat
torch.cuda.empty_cache()
model_path = MODEL_OPTIONS[selected_model_key]
try:
chat = ChatWithAI(model_path)
return gr.update(interactive=True, placeholder="Введите свой вопрос..."), \
gr.update(visible=True), \
f"✅ Модель загружена: {selected_model_key}"
except Exception as e:
logger.error(f"Не удалось загрузить модель {selected_model_key}: {e}")
return gr.update(interactive=False), \
gr.update(visible=False), \
f"❌ Ошибка загрузки модели: {str(e)}"
def respond(message, history):
# Генерируем ответ по частям
for token in chat.generate_response_stream(message):
yield token
with gr.Blocks(title="Amvera Cloud Assistant") as demo:
gr.Markdown("# 🤖 Amvera Cloud Assistant")
gr.Markdown("Выберите модель и задайте вопрос — получите ответ с контекстом.")
with gr.Row():
with gr.Column(scale=1):
model_dropdown = gr.Dropdown(
choices=list(MODEL_OPTIONS.keys()),
value=list(MODEL_OPTIONS.keys())[0],
label="Выберите модель",
interactive=True
)
load_button = gr.Button("Загрузить модель", variant="primary")
status_text = gr.Textbox(
label="Состояние",
value="Выберите модель и нажмите 'Загрузить модель'",
interactive=False
)
with gr.Column(scale=3):
chatbot = gr.Chatbot(
label="Чат",
height=600,
bubble_full_width=False,
)
msg = gr.Textbox(
label="Сообщение",
placeholder="Введите ваш вопрос...",
interactive=False
)
clear = gr.Button("Очистить чат")
# Логика выбора модели
load_button.click(
initialize_model,
inputs=[model_dropdown],
outputs=[msg, chatbot, status_text]
)
# Отправка сообщения
msg.submit(
respond,
inputs=[msg, chatbot],
outputs=[chatbot]
)
# Очистка чата
clear.click(lambda: None, None, chatbot, queue=False)
demo.launch(server_name="0.0.0.0", server_port=8080, share=False)
# Создаем интерфейс
demo = gr.ChatInterface(
fn=generate_response,
title="Qwen3-4B-Base Chat",
description="Общайтесь с моделью Qwen3-4B-Base в режиме реального времени с потоковой генерацией",
examples=[
"Объясни, как работает квантование AWQ?",
"Напиши стихотворение про ИИ",
"Какие преимущества у Qwen3 перед предыдущими версиями?"
],
theme="soft",
)
if __name__ == "__main__": if __name__ == "__main__":
# ВАЖНО: используем .queue() для поддержки асинхронной обработки main()
demo.queue(max_size=20, default_concurrency_limit=10).launch(
server_port=8080,
server_name="0.0.0.0",
share=False,
# Можно добавить: max_batch_size=1, concurrency_count=4
)

77
app/old_without_db.py Normal file
View File

@@ -0,0 +1,77 @@
import gradio as gr
from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
import torch
import threading
# Пути к модели
model_name = "/models/Qwen3-8B"
# Загрузка токенизатора и модели (один раз при старте)
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.float16,
device_map="cuda",
trust_remote_code=True
)
# Отключим кэширование в истории, чтобы каждый запрос был независимым
def generate_response(message, history):
# Форматируем диалог: используем только текущую историю
prompt = ""
for human, assistant in history:
prompt += f"<|im_start|>user\n{human}<|im_end|>\n<|im_start|>assistant\n{assistant}<|im_end|>\n"
prompt += f"<|im_start|>user\n{message}<|im_end|>\n<|im_start|>assistant\n"
# Токенизация
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
# Создаём уникальный streamer для каждого запроса
streamer = TextIteratorStreamer(
tokenizer,
skip_prompt=True,
skip_special_tokens=True
)
# Параметры генерации
generation_kwargs = {
"input_ids": inputs["input_ids"],
"max_new_tokens": 1024,
"temperature": 0.6,
"top_p": 0.9,
"do_sample": True,
"pad_token_id": tokenizer.eos_token_id,
"streamer": streamer,
}
# Запускаем генерацию в отдельном потоке
thread = threading.Thread(target=model.generate, kwargs=generation_kwargs)
thread.start()
# Постепенно возвращаем результат
buffer = ""
for new_text in streamer:
buffer += new_text
yield buffer.strip()
# Создаем интерфейс
demo = gr.ChatInterface(
fn=generate_response,
title="Qwen3-4B-Base Chat",
description="Общайтесь с моделью Qwen3-4B-Base в режиме реального времени с потоковой генерацией",
examples=[
"Объясни, как работает квантование AWQ?",
"Напиши стихотворение про ИИ",
"Какие преимущества у Qwen3 перед предыдущими версиями?"
],
theme="soft",
)
if __name__ == "__main__":
# ВАЖНО: используем .queue() для поддержки асинхронной обработки
demo.queue(max_size=20, default_concurrency_limit=10).launch(
server_port=8080,
server_name="0.0.0.0",
share=False,
# Можно добавить: max_batch_size=1, concurrency_count=4
)

View File

@@ -1,161 +0,0 @@
from typing import Any, Dict, List, Literal, Optional
import torch
from config import settings
from langchain_chroma import Chroma
from langchain_huggingface import HuggingFaceEmbeddings
from loguru import logger
from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
class ChatWithAI:
def __init__(self, provider: "qwen3"):
self.provider = provider
self.embeddings = HuggingFaceEmbeddings(
model_name=settings.LM_MODEL_NAME,
model_kwargs={"device": "cuda"},
encode_kwargs={"normalize_embeddings": True},
)
if provider == "qwen3":
model_name = getattr(settings, "LOCAL_LLM_NAME", "/models/Qwen3-4B")
logger.info(f"Загрузка локальной модели: {model_name}")
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.float32,
device_map="cuda",
)
#Создаём text-generation pipeline
self.llm = pipeline(
"text-generation",
model=model,
tokenizer=tokenizer,
device_map="cuda", # 0 = GPU, -1 = CPU
temperature=0.7,
max_new_tokens=512,
do_sample=True,
pad_token_id=tokenizer.eos_token_id,
)
else:
raise ValueError(f"Неподдерживаемый провайдер: {provider}")
self.chroma_db = Chroma(
persist_directory=settings.DOCS_CHROMA_PATH,
embedding_function=self.embeddings,
collection_name=settings.DOCS_COLLECTION_NAME,
)
def get_relevant_context(self, query: str, k: int = 3) -> List[Dict[str, Any]]:
"""Получение релевантного контекста из базы данных."""
try:
results = self.chroma_db.similarity_search(query, k=k)
return [
{
"text": doc.page_content,
"metadata": doc.metadata,
}
for doc in results
]
except Exception as e:
logger.error(f"Ошибка при получении контекста: {e}")
return []
def format_context(self, context: List[Dict[str, Any]]) -> str:
"""Форматирование контекста для промпта."""
formatted_context = []
for item in context:
metadata_str = "\n".join(f"{k}: {v}" for k, v in item["metadata"].items())
formatted_context.append(
f"Текст: {item['text']}\nМетаданные:\n{metadata_str}\n"
)
return "\n---\n".join(formatted_context)
def generate_response(self, query: str) -> Optional[str]:
"""Генерация ответа на основе запроса и контекста."""
try:
context = self.get_relevant_context(query)
if not context:
return "Извините, не удалось найти релевантный контекст для ответа."
formatted_context = self.format_context(context)
messages = [
{
"role": "system",
"content": """Ты — внутренний менеджер компании Amvera Cloud. Отвечаешь по делу без лишних вступлений.
Правила:
1. Сразу переходи к сути, без фраз типа "На основе контекста"
2. Используй только факты. Если точных данных нет — отвечай общими фразами об Marzban, но не придумывай конкретику
3. Используй обычный текст без форматирования
4. Включай ссылки только если они есть в контексте
5. Говори от первого лица множественного числа: "Мы предоставляем", "У нас есть"
6. При упоминании файлов делай это естественно, например: "Я прикреплю инструкцию, где подробно описаны шаги"
7. На приветствия отвечай доброжелательно, на негатив — с легким юмором
8. Можешь при ответах использовать общую информацию из открытых источников по Marzban, но опирайся на контекст
9. Если пользователь спрашивает о ценах, планах или технических характеристиках — давай конкретные ответы из контекста
10. При технических вопросах предлагай практические решения
Персонализируй ответы, упоминая имя клиента если оно есть в контексте. Будь краток, информативен и полезен.""",
},
{
"role": "user",
"content": f"Вопрос: {query}\nКонтекст: {formatted_context}",
},
]
# Генерация через transformers
if self.provider == "qwen3":
# Используем токенизатор модели для форматирования чата
tokenizer = self.llm.tokenizer
model = self.llm.model
# Применяем chat template (поддерживается в современных моделях: Zephyr, Llama3, Qwen и т.д.)
prompt = tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True, enable_thinking=False
)
# Генерация
outputs = self.llm(
prompt,
max_new_tokens=512,
temperature=0.7,
do_sample=True,
top_p=0.9,
pad_token_id=tokenizer.eos_token_id,
)
response_text = outputs[0]["generated_text"]
# Убираем входной промпт, оставляем только ответ
if prompt in response_text:
response_text = response_text[len(prompt):].strip()
return response_text
else:
# Остальные провайдеры (deepseek, openai) используют langchain
response = self.llm.invoke(messages)
if hasattr(response, "content"):
return str(response.content)
return str(response).strip()
except Exception as e:
logger.error(f"Ошибка при генерации ответа: {e}")
return "Произошла ошибка при генерации ответа."
if __name__ == "__main__":
chat = ChatWithAI(provider="qwen3")
print("\n=== Чат с ИИ ===\n")
while True:
query = input("Вы: ")
if query.lower() == "выход":
print("\nДо свидания!")
break
print("\nИИ печатает...", end="\r")
response = chat.generate_response(query)
print(" " * 20, end="\r") # Очищаем "ИИ печатает..."
print(f"ИИ: {response}\n")

42
install.sh Normal file
View File

@@ -0,0 +1,42 @@
#!/bin/bash
apt-get update
apt-get install ca-certificates curl
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null
echo \
'{ \
"runtimes": { \
"nvidia": { \
"path": "nvidia-container-runtime", \
"runtimeArgs": [] \
} \
}, \
"default-runtime": "nvidia" \
}' > /etc/docker/daemon.json
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg && \
curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
apt-get update
export NVIDIA_CONTAINER_TOOLKIT_VERSION=1.17.8-1
apt-get install -y \
nvidia-container-toolkit=${NVIDIA_CONTAINER_TOOLKIT_VERSION} \
nvidia-container-toolkit-base=${NVIDIA_CONTAINER_TOOLKIT_VERSION} \
libnvidia-container-tools=${NVIDIA_CONTAINER_TOOLKIT_VERSION} \
libnvidia-container1=${NVIDIA_CONTAINER_TOOLKIT_VERSION} \
nvidia-container-runtime \
docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin \
git git-lfs
git lfs install
lspci -k | grep -EA2 'VGA|3D'
nvidia-smi