Spaces:
Runtime error
Runtime error
jiarongqiu commited on
Commit ·
3fdc088
1
Parent(s): e68950c
add docs
Browse files- service/api.py +13 -2
- service/crawler.py +100 -0
- service/vector_store.py +50 -6
- util/schema.py +79 -1
service/api.py
CHANGED
|
@@ -15,10 +15,22 @@ class API():
|
|
| 15 |
def __init__(self) -> None:
|
| 16 |
pass
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
def get_suggestion(self,query):
|
| 19 |
if not query:
|
| 20 |
return []
|
| 21 |
-
docs = vector_store.marginal_search(query)
|
| 22 |
res = []
|
| 23 |
for doc in docs:
|
| 24 |
name = doc.metadata.get('title_llm','')
|
|
@@ -28,7 +40,6 @@ class API():
|
|
| 28 |
res.append({"name":name,"url":url})
|
| 29 |
# logger.write_log({"query":query,"api":'auto_complete',"result":res})
|
| 30 |
return res
|
| 31 |
-
|
| 32 |
|
| 33 |
def get_answer(self,query):
|
| 34 |
docs = vector_store.search(query)
|
|
|
|
| 15 |
def __init__(self) -> None:
|
| 16 |
pass
|
| 17 |
|
| 18 |
+
def search(self,query,**kwargs):
|
| 19 |
+
if not query:
|
| 20 |
+
return []
|
| 21 |
+
docs = vector_store.search(query,**kwargs)
|
| 22 |
+
return docs
|
| 23 |
+
|
| 24 |
+
def marginal_search(self,query,**kwargs):
|
| 25 |
+
if not query:
|
| 26 |
+
return []
|
| 27 |
+
docs = vector_store.marginal_search(query,**kwargs)
|
| 28 |
+
return docs
|
| 29 |
+
|
| 30 |
def get_suggestion(self,query):
|
| 31 |
if not query:
|
| 32 |
return []
|
| 33 |
+
docs = vector_store.marginal_search(query,lambda_mult=0.6)
|
| 34 |
res = []
|
| 35 |
for doc in docs:
|
| 36 |
name = doc.metadata.get('title_llm','')
|
|
|
|
| 40 |
res.append({"name":name,"url":url})
|
| 41 |
# logger.write_log({"query":query,"api":'auto_complete',"result":res})
|
| 42 |
return res
|
|
|
|
| 43 |
|
| 44 |
def get_answer(self,query):
|
| 45 |
docs = vector_store.search(query)
|
service/crawler.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import re
|
| 3 |
+
from tqdm import tqdm
|
| 4 |
+
from bs4 import BeautifulSoup
|
| 5 |
+
from langchain.document_loaders.recursive_url_loader import RecursiveUrlLoader
|
| 6 |
+
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
| 7 |
+
from typing import List
|
| 8 |
+
from service.llm import Bot
|
| 9 |
+
from util.schema import MyDoc
|
| 10 |
+
from typing import List
|
| 11 |
+
|
| 12 |
+
class Crawler(RecursiveUrlLoader):
|
| 13 |
+
|
| 14 |
+
HEADERS = {
|
| 15 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.76 Safari/537.36'
|
| 16 |
+
}
|
| 17 |
+
CHUNK_SIZE = 2000
|
| 18 |
+
TEMPLATE = """
|
| 19 |
+
You are an expert in refining and summarization based on the title, description of the document to make it more concise and informative. Please generate a refined title, description, and keywords for the document in json format.
|
| 20 |
+
Remember, the generated keywords should be less than 3 words, the generated title should be less than 10 words, and the generated description should be less than 100 words.
|
| 21 |
+
Here is a legal example of the json format:
|
| 22 |
+
{{
|
| 23 |
+
"title": "The Title of the Document",
|
| 24 |
+
"description": "The Description of the Document",
|
| 25 |
+
"keywords": ["keyword1", "keyword2", "keyword3"]
|
| 26 |
+
}}
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
def __init__(self,keywords=[]):
|
| 30 |
+
self.keywords = keywords
|
| 31 |
+
self.splitter = RecursiveCharacterTextSplitter(chunk_size=self.CHUNK_SIZE, chunk_overlap=200)
|
| 32 |
+
self.bot = Bot(template=self.TEMPLATE)
|
| 33 |
+
super().__init__(
|
| 34 |
+
url="",
|
| 35 |
+
extractor=self._extractor,
|
| 36 |
+
timeout=600,
|
| 37 |
+
headers=self.HEADERS,
|
| 38 |
+
exclude_dirs=[],
|
| 39 |
+
prevent_outside=True
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
def _extractor(self,html: str) -> str:
|
| 43 |
+
text = BeautifulSoup(html, "lxml").get_text(" ")
|
| 44 |
+
text = re.sub(r"\n\n+", "\n\n", text)
|
| 45 |
+
text = re.sub(r"\s\s+", " ", text).strip()
|
| 46 |
+
if self.check_keywords(text):
|
| 47 |
+
return text
|
| 48 |
+
return
|
| 49 |
+
|
| 50 |
+
def check_keywords(self,text):
|
| 51 |
+
if not self.keywords:
|
| 52 |
+
return True
|
| 53 |
+
for key in self.keywords:
|
| 54 |
+
if key in text:
|
| 55 |
+
return True
|
| 56 |
+
return False
|
| 57 |
+
|
| 58 |
+
def __call__(self,url,visited=set(),max_depth=2,prevent_outside=True):
|
| 59 |
+
self.url = url
|
| 60 |
+
self.max_depth = max_depth
|
| 61 |
+
self.prevent_outside = prevent_outside
|
| 62 |
+
docs = list(self._get_child_links_recursive(url, visited))
|
| 63 |
+
return docs
|
| 64 |
+
|
| 65 |
+
def from_docs(self,docs):
|
| 66 |
+
return MyDoc.from_docs(docs)
|
| 67 |
+
|
| 68 |
+
def chunk(self,docs):
|
| 69 |
+
res = []
|
| 70 |
+
for doc in docs:
|
| 71 |
+
res += self.splitter.split_documents([doc])
|
| 72 |
+
return res
|
| 73 |
+
|
| 74 |
+
def llm_augment(self,docs:List[MyDoc]):
|
| 75 |
+
question_template = """
|
| 76 |
+
Please generate a refined title, description, and keywords for the document
|
| 77 |
+
1. title:{title}
|
| 78 |
+
2. desription:{description}
|
| 79 |
+
3. content:{content}
|
| 80 |
+
"""
|
| 81 |
+
res =[]
|
| 82 |
+
for doc in tqdm(docs):
|
| 83 |
+
question = question_template.format(
|
| 84 |
+
title=doc.title,
|
| 85 |
+
description=doc.description,
|
| 86 |
+
content=doc.page_content
|
| 87 |
+
)
|
| 88 |
+
response = self.bot.custom_call(question=question)
|
| 89 |
+
try:
|
| 90 |
+
response = response.strip('`').strip('json\n')
|
| 91 |
+
data = json.loads(response)
|
| 92 |
+
doc.keywords = data['keywords']
|
| 93 |
+
doc.description_llm = data['description']
|
| 94 |
+
doc.title_llm = data['title']
|
| 95 |
+
res.append(doc)
|
| 96 |
+
except Exception as e:
|
| 97 |
+
print(response)
|
| 98 |
+
return res
|
| 99 |
+
|
| 100 |
+
crawler = Crawler()
|
service/vector_store.py
CHANGED
|
@@ -38,13 +38,57 @@ class VectorStore(Pinecone):
|
|
| 38 |
pinecone.create_index(name=self.INDEX_NAME, metric="cosine", dimension=self.dims)
|
| 39 |
Pinecone.from_documents(docs, self.embeddings, index_name=self.INDEX_NAME)
|
| 40 |
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
def jsonfy(self,docs):
|
| 50 |
docs = [doc.dict() for doc in docs]
|
|
|
|
| 38 |
pinecone.create_index(name=self.INDEX_NAME, metric="cosine", dimension=self.dims)
|
| 39 |
Pinecone.from_documents(docs, self.embeddings, index_name=self.INDEX_NAME)
|
| 40 |
|
| 41 |
+
def search(self,
|
| 42 |
+
query: str,
|
| 43 |
+
k: int = 4,
|
| 44 |
+
filter: Optional[dict] = None,
|
| 45 |
+
namespace: Optional[str] = None,
|
| 46 |
+
**kwargs: Any,
|
| 47 |
+
) -> List[Document]:
|
| 48 |
+
"""Return pinecone documents most similar to query.
|
| 49 |
|
| 50 |
+
Args:
|
| 51 |
+
query: Text to look up documents similar to.
|
| 52 |
+
k: Number of Documents to return. Defaults to 4.
|
| 53 |
+
filter: Dictionary of argument(s) to filter on metadata
|
| 54 |
+
namespace: Namespace to search in. Default will search in '' namespace.
|
| 55 |
+
|
| 56 |
+
Returns:
|
| 57 |
+
List of Documents most similar to the query and score for each
|
| 58 |
+
"""
|
| 59 |
+
docs_and_scores = self.similarity_search_with_score(
|
| 60 |
+
query, k=k, filter=filter, namespace=namespace, **kwargs
|
| 61 |
+
)
|
| 62 |
+
return [doc for doc, _ in docs_and_scores]
|
| 63 |
+
|
| 64 |
+
def marginal_search(self,
|
| 65 |
+
query: str,
|
| 66 |
+
k: int = 4,
|
| 67 |
+
fetch_k: int = 20,
|
| 68 |
+
lambda_mult: float = 0.5,
|
| 69 |
+
filter: Optional[dict] = None,
|
| 70 |
+
namespace: Optional[str] = None,
|
| 71 |
+
**kwargs: Any,
|
| 72 |
+
) -> List[Document]:
|
| 73 |
+
"""Return docs selected using the maximal marginal relevance.
|
| 74 |
+
|
| 75 |
+
Maximal marginal relevance optimizes for similarity to query AND diversity
|
| 76 |
+
among selected documents.
|
| 77 |
+
|
| 78 |
+
Args:
|
| 79 |
+
query: Text to look up documents similar to.
|
| 80 |
+
k: Number of Documents to return. Defaults to 4.
|
| 81 |
+
fetch_k: Number of Documents to fetch to pass to MMR algorithm.
|
| 82 |
+
lambda_mult: Number between 0 and 1 that determines the degree of diversity among the results with 0 corresponding
|
| 83 |
+
to maximum diversity and 1 to minimum diversity.
|
| 84 |
+
Defaults to 0.5.
|
| 85 |
+
Returns:
|
| 86 |
+
List of Documents selected by maximal marginal relevance.
|
| 87 |
+
"""
|
| 88 |
+
embedding = self._embed_query(query)
|
| 89 |
+
return self.max_marginal_relevance_search_by_vector(
|
| 90 |
+
embedding, k, fetch_k, lambda_mult, filter, namespace
|
| 91 |
+
)
|
| 92 |
|
| 93 |
def jsonfy(self,docs):
|
| 94 |
docs = [doc.dict() for doc in docs]
|
util/schema.py
CHANGED
|
@@ -39,4 +39,82 @@ class Card:
|
|
| 39 |
"title": self.title,
|
| 40 |
"description": self.description,
|
| 41 |
"url": self.url
|
| 42 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
"title": self.title,
|
| 40 |
"description": self.description,
|
| 41 |
"url": self.url
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class MyDoc:
|
| 47 |
+
|
| 48 |
+
def __init__(self, content, metadata):
|
| 49 |
+
self._content = content
|
| 50 |
+
self._metadata = {k:v for k,v in metadata.items() if v is not None and v.strip()!= ''}
|
| 51 |
+
|
| 52 |
+
@staticmethod
|
| 53 |
+
def from_docs(docs):
|
| 54 |
+
res = []
|
| 55 |
+
for doc in docs:
|
| 56 |
+
res.append(MyDoc(doc.page_content, doc.metadata))
|
| 57 |
+
return res
|
| 58 |
+
|
| 59 |
+
@property
|
| 60 |
+
def idx(self):
|
| 61 |
+
return self.metadata.get('idx', -1)
|
| 62 |
+
|
| 63 |
+
@property
|
| 64 |
+
def page_content(self):
|
| 65 |
+
return self._content
|
| 66 |
+
|
| 67 |
+
@property
|
| 68 |
+
def metadata(self):
|
| 69 |
+
return self._metadata
|
| 70 |
+
|
| 71 |
+
@page_content.setter
|
| 72 |
+
def page_content(self, value):
|
| 73 |
+
self._page_content = value
|
| 74 |
+
|
| 75 |
+
@metadata.setter
|
| 76 |
+
def metadata(self, value):
|
| 77 |
+
self._metadata = value
|
| 78 |
+
|
| 79 |
+
@property
|
| 80 |
+
def title(self):
|
| 81 |
+
return self.metadata.get('title', '')
|
| 82 |
+
|
| 83 |
+
@property
|
| 84 |
+
def source(self):
|
| 85 |
+
return self.metadata.get('source', '')
|
| 86 |
+
|
| 87 |
+
@property
|
| 88 |
+
def description(self):
|
| 89 |
+
return self.metadata.get('description', '')
|
| 90 |
+
|
| 91 |
+
@property
|
| 92 |
+
def language(self):
|
| 93 |
+
return self.metadata.get('language', '')
|
| 94 |
+
|
| 95 |
+
@property
|
| 96 |
+
def keywords(self):
|
| 97 |
+
return self.metadata.get('keywords', [])
|
| 98 |
+
|
| 99 |
+
@property
|
| 100 |
+
def description_llm(self):
|
| 101 |
+
return self.metadata.get('description_llm', '')
|
| 102 |
+
|
| 103 |
+
@property
|
| 104 |
+
def title_llm(self):
|
| 105 |
+
return self.metadata.get('title_llm', '')
|
| 106 |
+
|
| 107 |
+
@description_llm.setter
|
| 108 |
+
def description_llm(self, value):
|
| 109 |
+
self.metadata['description_llm'] = value
|
| 110 |
+
|
| 111 |
+
@title_llm.setter
|
| 112 |
+
def title_llm(self, value):
|
| 113 |
+
self.metadata['title_llm'] = value
|
| 114 |
+
|
| 115 |
+
@keywords.setter
|
| 116 |
+
def keywords(self, value):
|
| 117 |
+
self.metadata['keywords'] = value
|
| 118 |
+
|
| 119 |
+
def __str__(self):
|
| 120 |
+
return f"Source: {self.source}"
|