-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhelper_functions.py
More file actions
304 lines (233 loc) · 9.25 KB
/
Copy pathhelper_functions.py
File metadata and controls
304 lines (233 loc) · 9.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
import asyncio
import random
import textwrap
from enum import Enum
from typing import List
import numpy as np
from langchain.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain.vectorstores import FAISS
from langchain_core.pydantic_v1 import BaseModel, Field
from langchain import PromptTemplate
from openai import RateLimitError
from rank_bm25 import BM25Okapi
import fitz
def replace_tabs_with_space(documents: List) -> List:
"""
Replaces all tab characters ('\t') with spaces in the page content of each document.
Args:
documents (List): A list of document objects with 'page_content' attributes.
Returns:
List: The modified list of documents with tab characters replaced by spaces.
"""
for doc in documents:
doc.page_content = doc.page_content.replace('\t', ' ') # Replace tabs with spaces
return documents
def wrap_text(text: str, width: int = 120) -> str:
"""
Wraps the input text to the specified width.
Args:
text (str): The input text to wrap.
width (int): The width at which to wrap the text.
Returns:
str: The wrapped text.
"""
return textwrap.fill(text, width=width)
def encode_pdf_to_vector_store(path: str, chunk_size: int = 1000, chunk_overlap: int = 200) -> FAISS:
"""
Encodes a PDF document into a vector store using OpenAI embeddings.
Args:
path (str): Path to the PDF file.
chunk_size (int): The size of each chunk of text.
chunk_overlap (int): The overlap between chunks.
Returns:
FAISS: A vector store containing the encoded content.
"""
loader = PyPDFLoader(path)
documents = loader.load()
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size, chunk_overlap=chunk_overlap, length_function=len
)
texts = text_splitter.split_documents(documents)
cleaned_texts = replace_tabs_with_space(texts)
embeddings = OpenAIEmbeddings()
return FAISS.from_documents(cleaned_texts, embeddings)
def encode_text_to_vector_store(content: str, chunk_size: int = 1000, chunk_overlap: int = 200) -> FAISS:
"""
Encodes a string into a vector store using OpenAI embeddings.
Args:
content (str): The text content to be encoded.
chunk_size (int): The size of each chunk of text.
chunk_overlap (int): The overlap between chunks.
Returns:
FAISS: A vector store containing the encoded content.
Raises:
ValueError: If the input content is not valid.
RuntimeError: If there is an error during encoding.
"""
if not isinstance(content, str) or not content.strip():
raise ValueError("Content must be a non-empty string.")
if not isinstance(chunk_size, int) or chunk_size <= 0:
raise ValueError("chunk_size must be a positive integer.")
if not isinstance(chunk_overlap, int) or chunk_overlap < 0:
raise ValueError("chunk_overlap must be a non-negative integer.")
try:
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
length_function=len,
is_separator_regex=False,
)
chunks = text_splitter.create_documents([content])
# Assign metadata to each chunk
for chunk in chunks:
chunk.metadata['relevance_score'] = 1.0
embeddings = OpenAIEmbeddings()
return FAISS.from_documents(chunks, embeddings)
except Exception as e:
raise RuntimeError(f"Error during encoding: {str(e)}")
def retrieve_context_for_question(question: str, query_retriever) -> List[str]:
"""
Retrieves relevant context for a given question using the provided query retriever.
Args:
question (str): The question for which to retrieve context.
Returns:
List[str]: A list of relevant document contents.
"""
docs = query_retriever.get_relevant_documents(question)
return [doc.page_content for doc in docs]
class QuestionAnswerContext(BaseModel):
"""
Model for generating answers based on the provided context.
Attributes:
answer_based_on_content (str): The generated answer.
"""
answer_based_on_content: str = Field(description="Answer based on the provided context.")
def create_question_answer_chain(llm) -> PromptTemplate:
"""
Creates a chain of reasoning for answering a question based on context.
Args:
llm: The language model to be used for generating answers.
Returns:
PromptTemplate: A chain combining the prompt template and language model.
"""
question_answer_prompt_template = """
For the question below, provide a concise but sufficient answer based ONLY on the provided context:
{context}
Question
{question}
"""
question_answer_prompt = PromptTemplate(
template=question_answer_prompt_template,
input_variables=["context", "question"],
)
return question_answer_prompt | llm.with_structured_output(QuestionAnswerContext)
def answer_question_from_context(
question: str, context: List[str], question_answer_chain: PromptTemplate
) -> dict:
"""
Answers a question using the provided context.
Args:
question (str): The question to be answered.
context (List[str]): The context to be used for answering the question.
question_answer_chain (PromptTemplate): The chain for answering the question.
Returns:
dict: A dictionary with the answer, context, and question.
"""
input_data = {"question": question, "context": context}
output = question_answer_chain.invoke(input_data)
return {"answer": output.answer_based_on_content, "context": context, "question": question}
def display_context(context: List[str]) -> None:
"""
Displays the provided context with headings.
Args:
context (List[str]): A list of context items to be displayed.
"""
for i, item in enumerate(context, 1):
print(f"Context {i}:")
print(item)
print("\n")
def read_pdf_content(path: str) -> str:
"""
Extracts the text content of a PDF file.
Args:
path (str): The file path to the PDF document.
Returns:
str: The concatenated text content of the PDF document.
"""
doc = fitz.open(path)
return "".join(page.get_text() for page in doc)
def bm25_retrieve(bm25: BM25Okapi, cleaned_texts: List[str], query: str, top_k: int = 5) -> List[str]:
"""
Retrieves the top k relevant text chunks using BM25 retrieval.
Args:
bm25 (BM25Okapi): The precomputed BM25 index.
cleaned_texts (List[str]): The list of cleaned text chunks.
query (str): The query string.
top_k (int): The number of top documents to retrieve.
Returns:
List[str]: The top k relevant text chunks.
"""
query_tokens = query.split()
bm25_scores = bm25.get_scores(query_tokens)
top_k_indices = np.argsort(bm25_scores)[::-1][:top_k]
return [cleaned_texts[i] for i in top_k_indices]
async def exponential_backoff(attempt: int) -> None:
"""
Implements exponential backoff with jitter for rate limiting.
Args:
attempt (int): The retry attempt number.
"""
wait_time = (2 ** attempt) + random.uniform(0, 1)
print(f"Rate limit hit. Retrying in {wait_time:.2f} seconds...")
await asyncio.sleep(wait_time)
async def retry_with_backoff(coroutine, max_retries: int = 5):
"""
Retries a coroutine with exponential backoff upon encountering a RateLimitError.
Args:
coroutine: The coroutine to retry.
max_retries (int): The maximum number of retries.
Returns:
The result of the coroutine if successful.
Raises:
The last exception if all retries fail.
"""
for attempt in range(max_retries):
try:
return await coroutine
except RateLimitError as e:
if attempt == max_retries - 1:
raise e
await exponential_backoff(attempt)
raise Exception("Max retries reached")
class EmbeddingProvider(Enum):
OPENAI = "openai"
COHERE = "cohere"
AMAZON_BEDROCK = "bedrock"
class ModelProvider(Enum):
OPENAI = "openai"
GROQ = "groq"
ANTHROPIC = "anthropic"
AMAZON_BEDROCK = "bedrock"
def get_embedding_provider(provider: EmbeddingProvider, model_id: str = None):
"""
Returns the embedding provider based on the specified provider and model ID.
Args:
provider (EmbeddingProvider): The embedding provider to use.
model_id (str, optional): The model ID to use for the provider.
Returns:
The appropriate embedding provider instance.
Raises:
ValueError: If the specified provider is unsupported.
"""
if provider == EmbeddingProvider.OPENAI:
return OpenAIEmbeddings()
elif provider == EmbeddingProvider.COHERE:
from langchain_cohere import CohereEmbeddings
return CohereEmbeddings()
elif provider == EmbeddingProvider.AMAZON_BEDROCK:
from langchain_community.embeddings import BedrockEmbeddings
return BedrockEmbeddings(model_id=model_id or "amazon.titan-embed-text-v2:0")
else:
raise ValueError(f"Unsupported embedding provider: {provider}")