Агент с RAG-памятью: client.py
This commit is contained in:
@@ -0,0 +1,143 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from langchain_ollama import ChatOllama
|
||||||
|
from langchain_qdrant import QdrantStore
|
||||||
|
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
||||||
|
from langchain_core.documents import Document
|
||||||
|
|
||||||
|
# Настройка логирования
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s [%(levelname)s] %(message)s",
|
||||||
|
handlers=[logging.StreamHandler(sys.stdout)],
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Конфигурация Ollama и Qdrant
|
||||||
|
OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "llama3")
|
||||||
|
EMBEDDING_MODEL = os.getenv("NOMIC_EMBED_TEXT_MODEL", "nomic-embed-text")
|
||||||
|
QDRANT_URL = os.getenv("QDRANT_URL", "http://localhost:6333")
|
||||||
|
COLLECTION_NAME = os.getenv("QDRANT_COLLECTION", "rag_collection")
|
||||||
|
|
||||||
|
# Инициализация Ollama и Qdrant
|
||||||
|
try:
|
||||||
|
llm = ChatOllama(model=OLLAMA_MODEL)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Не удалось подключиться к Ollama: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
qdrant_store = QdrantStore(
|
||||||
|
url=QDRANT_URL,
|
||||||
|
collection_name=COLLECTION_NAME,
|
||||||
|
embedding_function=lambda text: llm.embed_query(text),
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Не удалось подключиться к Qdrant: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Текстовый разделитель с гибкой схемой chunking
|
||||||
|
def split_text(text: str, chunk_size: int = 500, overlap: int = 50) -> List[Document]:
|
||||||
|
"""
|
||||||
|
Делит входной текст на чанки.
|
||||||
|
Параметры:
|
||||||
|
chunk_size: максимальная длина чанка в символах
|
||||||
|
overlap: количество пересекающихся символов между соседними чанками
|
||||||
|
Возвращает список Document объектов.
|
||||||
|
"""
|
||||||
|
splitter = RecursiveCharacterTextSplitter(
|
||||||
|
chunk_size=chunk_size,
|
||||||
|
chunk_overlap=overlap,
|
||||||
|
separators=["\n\n", "\n", " ", ""],
|
||||||
|
)
|
||||||
|
return [Document(page_content=c) for c in splitter.split_text(text)]
|
||||||
|
|
||||||
|
def add_document(file_path: str):
|
||||||
|
"""Добавляет документ из файла в базу Qdrant."""
|
||||||
|
if not os.path.isfile(file_path):
|
||||||
|
logger.warning(f"Файл не найден: {file_path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(file_path, "r", encoding="utf-8") as f:
|
||||||
|
content = f.read()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка чтения файла {file_path}: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
docs = split_text(content)
|
||||||
|
if not docs:
|
||||||
|
logger.warning("Документ пустой после разбиения на чанки.")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
qdrant_store.add_documents(docs)
|
||||||
|
logger.info(f"Добавлено {len(docs)} чанков из файла: {file_path}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при добавлении документов в Qdrant: {e}")
|
||||||
|
|
||||||
|
def search(query: str, k: int = 5):
|
||||||
|
"""Ищет релевантные документы и выводит ответы."""
|
||||||
|
try:
|
||||||
|
docs_and_scores = qdrant_store.similarity_search_with_score(query, k=k)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при поиске в Qdrant: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not docs_and_scores:
|
||||||
|
logger.info("Ничего не найдено.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Формируем контекст из найденных чанков
|
||||||
|
context = "\n\n".join([doc.page_content for doc, _ in docs_and_scores])
|
||||||
|
|
||||||
|
prompt = f"""
|
||||||
|
Ты эксперт по теме. На основе следующего контекста ответь на вопрос:
|
||||||
|
|
||||||
|
Контекст:
|
||||||
|
{context}
|
||||||
|
|
||||||
|
Вопрос: {query}
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = llm.invoke(prompt)
|
||||||
|
print("\nОтвет:\n" + response.content.strip())
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при генерации ответа Ollama: {e}")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
logger.info("Запущен CLI клиент RAG. Команды: /add <путь>, /search <запрос>, /quit")
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
user_input = input("\n> ").strip()
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
print("\nВыход.")
|
||||||
|
break
|
||||||
|
|
||||||
|
if not user_input:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if user_input.lower() == "/quit":
|
||||||
|
logger.info("Завершение работы.")
|
||||||
|
break
|
||||||
|
elif user_input.startswith("/add"):
|
||||||
|
parts = user_input.split(maxsplit=1)
|
||||||
|
if len(parts) != 2:
|
||||||
|
print("Использование: /add <путь_к_файлу>")
|
||||||
|
continue
|
||||||
|
add_document(parts[1])
|
||||||
|
elif user_input.startswith("/search"):
|
||||||
|
parts = user_input.split(maxsplit=1)
|
||||||
|
if len(parts) != 2:
|
||||||
|
print("Использование: /search <запрос>")
|
||||||
|
continue
|
||||||
|
search(parts[1])
|
||||||
|
else:
|
||||||
|
print("Неизвестная команда. Доступные команды: /add, /search, /quit")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user