Files
AI_NovelGenerator/novel_generator/vectorstore_utils.py
T
2025-03-19 13:02:51 +08:00

245 lines
8.6 KiB
Python

#novel_generator/vectorstore_utils.py
# -*- coding: utf-8 -*-
"""
向量库相关操作(初始化、更新、检索、清空、文本切分等)
"""
import os
import logging
import traceback
import nltk
import numpy as np
import re
import ssl
import requests
import warnings
from langchain_chroma import Chroma
# 禁用特定的Torch警告
warnings.filterwarnings('ignore', message='.*Torch was not compiled with flash attention.*')
os.environ["TOKENIZERS_PARALLELISM"] = "false" # 禁用tokenizer并行警告
from chromadb.config import Settings
from langchain.docstore.document import Document
from sklearn.metrics.pairwise import cosine_similarity
from .common import call_with_retry
def get_vectorstore_dir(filepath: str) -> str:
"""获取 vectorstore 路径"""
return os.path.join(filepath, "vectorstore")
def clear_vector_store(filepath: str) -> bool:
"""清空 清空向量库"""
import shutil
store_dir = get_vectorstore_dir(filepath)
if not os.path.exists(store_dir):
logging.info("No vector store found to clear.")
return False
try:
shutil.rmtree(store_dir)
logging.info(f"Vector store directory '{store_dir}' removed.")
return True
except Exception as e:
logging.error(f"无法删除向量库文件夹,请关闭程序后手动删除 {store_dir}\n {str(e)}")
traceback.print_exc()
return False
def init_vector_store(embedding_adapter, texts, filepath: str):
"""
在 filepath 下创建/加载一个 Chroma 向量库并插入 texts。
如果Embedding失败,则返回 None,不中断任务。
"""
from langchain.embeddings.base import Embeddings as LCEmbeddings
store_dir = get_vectorstore_dir(filepath)
os.makedirs(store_dir, exist_ok=True)
documents = [Document(page_content=str(t)) for t in texts]
try:
class LCEmbeddingWrapper(LCEmbeddings):
def embed_documents(self, texts):
return call_with_retry(
func=embedding_adapter.embed_documents,
max_retries=3,
fallback_return=[],
texts=texts
)
def embed_query(self, query: str):
res = call_with_retry(
func=embedding_adapter.embed_query,
max_retries=3,
fallback_return=[],
query=query
)
return res
chroma_embedding = LCEmbeddingWrapper()
vectorstore = Chroma.from_documents(
documents,
embedding=chroma_embedding,
persist_directory=store_dir,
client_settings=Settings(anonymized_telemetry=False),
collection_name="novel_collection"
)
return vectorstore
except Exception as e:
logging.warning(f"Init vector store failed: {e}")
traceback.print_exc()
return None
def load_vector_store(embedding_adapter, filepath: str):
"""
读取已存在的 Chroma 向量库。若不存在则返回 None。
如果加载失败(embedding 或IO问题),则返回 None。
"""
from langchain.embeddings.base import Embeddings as LCEmbeddings
store_dir = get_vectorstore_dir(filepath)
if not os.path.exists(store_dir):
logging.info("Vector store not found. Will return None.")
return None
try:
class LCEmbeddingWrapper(LCEmbeddings):
def embed_documents(self, texts):
return call_with_retry(
func=embedding_adapter.embed_documents,
max_retries=3,
fallback_return=[],
texts=texts
)
def embed_query(self, query: str):
res = call_with_retry(
func=embedding_adapter.embed_query,
max_retries=3,
fallback_return=[],
query=query
)
return res
chroma_embedding = LCEmbeddingWrapper()
return Chroma(
persist_directory=store_dir,
embedding_function=chroma_embedding,
client_settings=Settings(anonymized_telemetry=False),
collection_name="novel_collection"
)
except Exception as e:
logging.warning(f"Failed to load vector store: {e}")
traceback.print_exc()
return None
def split_by_length(text: str, max_length: int = 500):
"""按照 max_length 切分文本"""
segments = []
start_idx = 0
while start_idx < len(text):
end_idx = min(start_idx + max_length, len(text))
segment = text[start_idx:end_idx]
segments.append(segment.strip())
start_idx = end_idx
return segments
def split_text_for_vectorstore(chapter_text: str, max_length: int = 500, similarity_threshold: float = 0.7):
"""
对新的章节文本进行分段后,再用于存入向量库。
使用 embedding 进行文本相似度计算。
"""
if not chapter_text.strip():
return []
nltk.download('punkt', quiet=True)
nltk.download('punkt_tab', quiet=True)
sentences = nltk.sent_tokenize(chapter_text)
if not sentences:
return []
# 直接按长度分段,不做相似度合并
final_segments = []
current_segment = []
current_length = 0
for sentence in sentences:
sentence_length = len(sentence)
if current_length + sentence_length > max_length:
if current_segment:
final_segments.append(" ".join(current_segment))
current_segment = [sentence]
current_length = sentence_length
else:
current_segment.append(sentence)
current_length += sentence_length
if current_segment:
final_segments.append(" ".join(current_segment))
return final_segments
def update_vector_store(embedding_adapter, new_chapter: str, filepath: str):
"""
将最新章节文本插入到向量库中。
若库不存在则初始化;若初始化/更新失败,则跳过。
"""
from utils import read_file, clear_file_content, save_string_to_txt
splitted_texts = split_text_for_vectorstore(new_chapter)
if not splitted_texts:
logging.warning("No valid text to insert into vector store. Skipping.")
return
store = load_vector_store(embedding_adapter, filepath)
if not store:
logging.info("Vector store does not exist or failed to load. Initializing a new one for new chapter...")
store = init_vector_store(embedding_adapter, splitted_texts, filepath)
if not store:
logging.warning("Init vector store failed, skip embedding.")
else:
logging.info("New vector store created successfully.")
return
try:
docs = [Document(page_content=str(t)) for t in splitted_texts]
store.add_documents(docs)
logging.info("Vector store updated with the new chapter splitted segments.")
except Exception as e:
logging.warning(f"Failed to update vector store: {e}")
traceback.print_exc()
def get_relevant_context_from_vector_store(embedding_adapter, query: str, filepath: str, k: int = 2) -> str:
"""
从向量库中检索与 query 最相关的 k 条文本,拼接后返回。
如果向量库加载/检索失败,则返回空字符串。
最终只返回最多2000字符的检索片段。
"""
store = load_vector_store(embedding_adapter, filepath)
if not store:
logging.info("No vector store found or load failed. Returning empty context.")
return ""
try:
docs = store.similarity_search(query, k=k)
if not docs:
logging.info(f"No relevant documents found for query '{query}'. Returning empty context.")
return ""
combined = "\n".join([d.page_content for d in docs])
if len(combined) > 2000:
combined = combined[:2000]
return combined
except Exception as e:
logging.warning(f"Similarity search failed: {e}")
traceback.print_exc()
return ""
def _get_sentence_transformer(model_name: str = 'paraphrase-MiniLM-L6-v2'):
"""获取sentence transformer模型,处理SSL问题"""
try:
# 设置torch环境变量
os.environ["TORCH_ALLOW_TF32_CUBLAS_OVERRIDE"] = "0"
os.environ["TORCH_CUDNN_V8_API_ENABLED"] = "0"
# 禁用SSL验证
ssl._create_default_https_context = ssl._create_unverified_context
# ...existing code...
except Exception as e:
logging.error(f"Failed to load sentence transformer model: {e}")
traceback.print_exc()
return None