LLMlight 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- LLMlight/LLMlight.py +786 -0
- LLMlight/RAG.py +441 -0
- LLMlight/__init__.py +37 -0
- LLMlight/examples.py +175 -0
- LLMlight/utils.py +150 -0
- llmlight-0.1.0.dist-info/METADATA +177 -0
- llmlight-0.1.0.dist-info/RECORD +10 -0
- llmlight-0.1.0.dist-info/WHEEL +5 -0
- llmlight-0.1.0.dist-info/licenses/LICENSE +21 -0
- llmlight-0.1.0.dist-info/top_level.txt +1 -0
LLMlight/RAG.py
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import pymupdf
|
|
2
|
+
import os
|
|
3
|
+
import numpy as np
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
from sentence_transformers import SentenceTransformer
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
from . import utils
|
|
10
|
+
# import utils
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
class SimpleVectorStore:
|
|
15
|
+
"""
|
|
16
|
+
A lightweight vector store implementation using NumPy.
|
|
17
|
+
"""
|
|
18
|
+
def __init__(self, dimension=1536):
|
|
19
|
+
"""
|
|
20
|
+
Initialize the vector store.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
dimension (int): Dimension of embeddings
|
|
24
|
+
"""
|
|
25
|
+
self.dimension = dimension
|
|
26
|
+
self.vectors = []
|
|
27
|
+
self.documents = []
|
|
28
|
+
self.metadata = []
|
|
29
|
+
|
|
30
|
+
def add_documents(self, documents, vectors=None, metadata=None):
|
|
31
|
+
"""
|
|
32
|
+
Add documents to the vector store.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
documents (List[str]): List of document chunks
|
|
36
|
+
vectors (List[List[float]], optional): List of embedding vectors
|
|
37
|
+
metadata (List[Dict], optional): List of metadata dictionaries
|
|
38
|
+
"""
|
|
39
|
+
if vectors is None:
|
|
40
|
+
vectors = [None] * len(documents)
|
|
41
|
+
|
|
42
|
+
if metadata is None:
|
|
43
|
+
metadata = [{} for _ in range(len(documents))]
|
|
44
|
+
|
|
45
|
+
for doc, vec, meta in zip(documents, vectors, metadata):
|
|
46
|
+
self.documents.append(doc)
|
|
47
|
+
self.vectors.append(vec)
|
|
48
|
+
self.metadata.append(meta)
|
|
49
|
+
|
|
50
|
+
def search(self, query_vector, top_k=5):
|
|
51
|
+
"""
|
|
52
|
+
Search for most similar documents.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
query_vector (List[float]): Query embedding vector
|
|
56
|
+
top_k (int): Number of results to return
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
List[Dict]: List of results with documents, scores, and metadata
|
|
60
|
+
"""
|
|
61
|
+
if not self.vectors or not self.documents:
|
|
62
|
+
return []
|
|
63
|
+
|
|
64
|
+
# Convert query vector to numpy array
|
|
65
|
+
query_array = np.array(query_vector)
|
|
66
|
+
|
|
67
|
+
# Compute similarity
|
|
68
|
+
# similarities = cosine_similarity(query_vector, chunk_vectors)[0]
|
|
69
|
+
# # Get top scoring chunks
|
|
70
|
+
# if top_chunks is None: top_chunks = len(similarities)
|
|
71
|
+
# top_indices = np.argsort(similarities)[-top_chunks:][::-1]
|
|
72
|
+
# # Join relevant chunks and send as prompt
|
|
73
|
+
# relevant_chunks = [chunks[i] for i in top_indices]
|
|
74
|
+
# relevant_scores = [similarities[i] for i in top_indices]
|
|
75
|
+
|
|
76
|
+
# Calculate similarities
|
|
77
|
+
similarities = []
|
|
78
|
+
for i, vector in enumerate(self.vectors):
|
|
79
|
+
if vector is not None:
|
|
80
|
+
# Compute cosine similarity
|
|
81
|
+
similarity = np.dot(query_array, vector) / (
|
|
82
|
+
np.linalg.norm(query_array) * np.linalg.norm(vector)
|
|
83
|
+
)
|
|
84
|
+
similarities.append((i, similarity))
|
|
85
|
+
|
|
86
|
+
# Sort by similarity (descending)
|
|
87
|
+
similarities.sort(key=lambda x: x[1], reverse=True)
|
|
88
|
+
|
|
89
|
+
# Get top-k results
|
|
90
|
+
results = []
|
|
91
|
+
for i, score in similarities[:top_k]:
|
|
92
|
+
results.append({
|
|
93
|
+
"document": self.documents[i],
|
|
94
|
+
"score": float(score),
|
|
95
|
+
"metadata": self.metadata[i]
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
return results
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def RAG_with_RSE(context, query, label=None, chunk_size=800, irrelevant_chunk_penalty=0.2, embedding_method='bert', device='cpu', batch_size=16):
|
|
102
|
+
"""
|
|
103
|
+
Complete RAG pipeline with Relevant Segment Extraction.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
text (str):
|
|
107
|
+
Text (context)
|
|
108
|
+
Path to the PDF document
|
|
109
|
+
query (str): User query
|
|
110
|
+
chunk_size (int): Size of chunks
|
|
111
|
+
irrelevant_chunk_penalty (float): Penalty for irrelevant chunks
|
|
112
|
+
embedding_method:
|
|
113
|
+
"bge": The large retrieval-optimized embedding model for search or ranking
|
|
114
|
+
"bge-small": The small retrieval-optimized embedding model for search or ranking
|
|
115
|
+
"bert": General-purpose sentence embedding model
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Dict: Result with query, segments, and response
|
|
119
|
+
"""
|
|
120
|
+
print("\n=== STARTING RAG WITH RELEVANT SEGMENT EXTRACTION ===")
|
|
121
|
+
print(f"Query: {query}")
|
|
122
|
+
|
|
123
|
+
if embedding_method == 'bge':
|
|
124
|
+
embedding_method = "BAAI/bge-en-icl"
|
|
125
|
+
elif embedding_method == 'bge-small':
|
|
126
|
+
embedding_method = "BAAI/bge-small-en"
|
|
127
|
+
elif embedding_method == 'bert':
|
|
128
|
+
embedding_method = "all-MiniLM-L6-v2"
|
|
129
|
+
else:
|
|
130
|
+
raise Exception(f'The embedding method [{embedding_method}] is not valid.')
|
|
131
|
+
|
|
132
|
+
# Process the document to extract text, chunk it, and create embeddings
|
|
133
|
+
chunks, vector_store, doc_info = process_document(context, label=label, chunk_size=chunk_size, embedding_method=embedding_method, device=device, batch_size=batch_size)
|
|
134
|
+
|
|
135
|
+
# Calculate relevance scores and chunk values based on the query
|
|
136
|
+
print("\nCalculating relevance scores and chunk values...")
|
|
137
|
+
chunk_values = calculate_chunk_values(query, chunks, vector_store, irrelevant_chunk_penalty)
|
|
138
|
+
|
|
139
|
+
# Find the best segments of text based on chunk values
|
|
140
|
+
best_segments, scores = find_best_segments(chunk_values, max_segment_length=20, total_max_length=30, min_segment_value=0.2)
|
|
141
|
+
|
|
142
|
+
# Reconstruct text segments from the best chunks
|
|
143
|
+
print("\nReconstructing text segments from chunks...")
|
|
144
|
+
segments = reconstruct_segments(chunks, best_segments)
|
|
145
|
+
|
|
146
|
+
# Format the segments into a context string for the language model
|
|
147
|
+
context = format_segments_for_context(segments)
|
|
148
|
+
|
|
149
|
+
return context
|
|
150
|
+
|
|
151
|
+
# # Generate a response from the language model using the context
|
|
152
|
+
# response = generate_response(query, context)
|
|
153
|
+
# # Compile the result into a dictionary
|
|
154
|
+
# result = {
|
|
155
|
+
# "query": query,
|
|
156
|
+
# "segments": segments,
|
|
157
|
+
# "response": response
|
|
158
|
+
# }
|
|
159
|
+
# print("\n=== FINAL RESPONSE ===")
|
|
160
|
+
# print(response)
|
|
161
|
+
# return result
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def create_embeddings(texts, model_path="BAAI/bge-small-en", batch_size=32, device="cpu"):
|
|
165
|
+
"""
|
|
166
|
+
Generate embeddings using a local model.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
texts (List[str]): List of texts to embed
|
|
170
|
+
model_path (str): Path or name of the local embedding model
|
|
171
|
+
batch_size: int : 16 when you run out of memory. Default: 32
|
|
172
|
+
embedding_method:
|
|
173
|
+
"BAAI/bge-en-icl": The large version of retrieval-optimized embedding model for search or ranking.
|
|
174
|
+
"BAAI/bge-small-en": The smaller version of retrieval-optimized embedding model for search or ranking.
|
|
175
|
+
"all-MiniLM-L6-v2": general-purpose sentence embedding model
|
|
176
|
+
device: str: "cuda" or 'cpu'
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
List[List[float]]: List of embedding vectors
|
|
180
|
+
"""
|
|
181
|
+
if not texts:
|
|
182
|
+
return []
|
|
183
|
+
|
|
184
|
+
# Load the model once and reuse it
|
|
185
|
+
model = SentenceTransformer(model_path, device=device)
|
|
186
|
+
|
|
187
|
+
# Compute embeddings
|
|
188
|
+
embeddings = model.encode(texts, batch_size=batch_size, convert_to_list=True)
|
|
189
|
+
|
|
190
|
+
# Return the list of all embeddings
|
|
191
|
+
return embeddings
|
|
192
|
+
|
|
193
|
+
# def create_embeddings(texts, model="BAAI/bge-en-icl"):
|
|
194
|
+
# """
|
|
195
|
+
# Generate embeddings for texts.
|
|
196
|
+
|
|
197
|
+
# Args:
|
|
198
|
+
# texts (List[str]): List of texts to embed
|
|
199
|
+
# model (str): Embedding model to use
|
|
200
|
+
|
|
201
|
+
# Returns:
|
|
202
|
+
# List[List[float]]: List of embedding vectors
|
|
203
|
+
# """
|
|
204
|
+
# if not texts:
|
|
205
|
+
# return [] # Return an empty list if no texts are provided
|
|
206
|
+
|
|
207
|
+
# # Process in batches if the list is long
|
|
208
|
+
# batch_size = 100 # Adjust based on your API limits
|
|
209
|
+
# all_embeddings = [] # Initialize a list to store all embeddings
|
|
210
|
+
|
|
211
|
+
# for i in range(0, len(texts), batch_size):
|
|
212
|
+
# batch = texts[i:i + batch_size] # Get the current batch of texts
|
|
213
|
+
|
|
214
|
+
# # Create embeddings for the current batch using the specified model
|
|
215
|
+
# response = client.embeddings.create(
|
|
216
|
+
# input=batch,
|
|
217
|
+
# model=model
|
|
218
|
+
# )
|
|
219
|
+
|
|
220
|
+
# # Extract embeddings from the response
|
|
221
|
+
# batch_embeddings = [item.embedding for item in response.data]
|
|
222
|
+
# all_embeddings.extend(batch_embeddings) # Add the batch embeddings to the list
|
|
223
|
+
|
|
224
|
+
# return all_embeddings # Return the list of all embeddings
|
|
225
|
+
|
|
226
|
+
def extract_text_from_pdf(pdf_path):
|
|
227
|
+
"""
|
|
228
|
+
Extracts text from a PDF file and prints the first `num_chars` characters.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
pdf_path (str): Path to the PDF file.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
str: Extracted text from the PDF.
|
|
235
|
+
"""
|
|
236
|
+
# Open the PDF file
|
|
237
|
+
mypdf = pymupdf.open(pdf_path)
|
|
238
|
+
all_text = "" # Initialize an empty string to store the extracted text
|
|
239
|
+
|
|
240
|
+
# Iterate through each page in the PDF
|
|
241
|
+
for page_num in range(mypdf.page_count):
|
|
242
|
+
page = mypdf[page_num] # Get the page
|
|
243
|
+
text = page.get_text("text") # Extract text from the page
|
|
244
|
+
all_text += text # Append the extracted text to the all_text string
|
|
245
|
+
|
|
246
|
+
return all_text # Return the extracted text
|
|
247
|
+
|
|
248
|
+
def process_document(context, label=None, chunk_size=800, embedding_method="all-MiniLM-L6-v2", batch_size=16, device='cpu'):
|
|
249
|
+
"""
|
|
250
|
+
Process a document for use with RSE.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
context (str):
|
|
254
|
+
Text (context)
|
|
255
|
+
The Path to the PDF document
|
|
256
|
+
label (str):
|
|
257
|
+
Add label to identify where the specific text originates from.
|
|
258
|
+
chunk_size (int): Size of each chunk in characters
|
|
259
|
+
embedding_method:
|
|
260
|
+
"BAAI/bge-en-icl": The large version of retrieval-optimized embedding model for search or ranking.
|
|
261
|
+
"BAAI/bge-small-en": The smaller version of retrieval-optimized embedding model for search or ranking.
|
|
262
|
+
"all-MiniLM-L6-v2": bert: general-purpose sentence embedding model
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
Tuple[List[str], SimpleVectorStore, Dict]: Chunks, vector store, and document info
|
|
266
|
+
"""
|
|
267
|
+
print("Extracting context from document...")
|
|
268
|
+
# Extract text from the PDF file
|
|
269
|
+
if os.path.isfile(context):
|
|
270
|
+
label = context
|
|
271
|
+
context = extract_text_from_pdf(context)
|
|
272
|
+
# text = utils.read_pdf(pdf_path, return_type='string')
|
|
273
|
+
|
|
274
|
+
print("Chunking context into non-overlapping segments...")
|
|
275
|
+
# Chunk the extracted text into non-overlapping segments
|
|
276
|
+
chunks = utils.chunk_text(context, chunk_size=chunk_size, overlap=0, method='chars')
|
|
277
|
+
print(f"Created {len(chunks)} chunks")
|
|
278
|
+
|
|
279
|
+
print("Generating embeddings for chunks...")
|
|
280
|
+
# Generate embeddings for the context chunks
|
|
281
|
+
chunk_embeddings = create_embeddings(chunks, model_path=embedding_method, batch_size=batch_size, device=device)
|
|
282
|
+
|
|
283
|
+
# Create an instance of the SimpleVectorStore
|
|
284
|
+
vector_store = SimpleVectorStore()
|
|
285
|
+
|
|
286
|
+
# Add documents with metadata (including chunk index for later reconstruction)
|
|
287
|
+
metadata = [{"chunk_index": i, "source": label} for i in range(len(chunks))]
|
|
288
|
+
vector_store.add_documents(chunks, chunk_embeddings, metadata)
|
|
289
|
+
|
|
290
|
+
# Track original document structure for segment reconstruction
|
|
291
|
+
doc_info = {
|
|
292
|
+
"chunks": chunks,
|
|
293
|
+
"source": label,
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return chunks, vector_store, doc_info
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def calculate_chunk_values(query, chunks, vector_store, irrelevant_chunk_penalty=0.2):
|
|
300
|
+
"""
|
|
301
|
+
Calculate chunk values by combining relevance and position.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
query (str): Query text
|
|
305
|
+
chunks (List[str]): List of document chunks
|
|
306
|
+
vector_store (SimpleVectorStore): Vector store containing the chunks
|
|
307
|
+
irrelevant_chunk_penalty (float): Penalty for irrelevant chunks
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
List[float]: List of chunk values
|
|
311
|
+
"""
|
|
312
|
+
# Create query embedding
|
|
313
|
+
query_embedding = create_embeddings([query])[0]
|
|
314
|
+
|
|
315
|
+
# Get all chunks with similarity scores
|
|
316
|
+
num_chunks = len(chunks)
|
|
317
|
+
results = vector_store.search(query_embedding, top_k=num_chunks)
|
|
318
|
+
|
|
319
|
+
# Create a mapping of chunk_index to relevance score
|
|
320
|
+
relevance_scores = {result["metadata"]["chunk_index"]: result["score"] for result in results}
|
|
321
|
+
|
|
322
|
+
# Calculate chunk values (relevance score minus penalty)
|
|
323
|
+
chunk_values = []
|
|
324
|
+
for i in range(num_chunks):
|
|
325
|
+
# Get relevance score or default to 0 if not in results
|
|
326
|
+
score = relevance_scores.get(i, 0.0)
|
|
327
|
+
# Apply penalty to convert to a value where irrelevant chunks have negative value
|
|
328
|
+
value = score - irrelevant_chunk_penalty
|
|
329
|
+
chunk_values.append(value)
|
|
330
|
+
|
|
331
|
+
return chunk_values
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def find_best_segments(chunk_values, max_segment_length=20, total_max_length=30, min_segment_value=0.2):
|
|
335
|
+
"""
|
|
336
|
+
Find the best segments using a variant of the maximum sum subarray algorithm.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
chunk_values (List[float]): Values for each chunk
|
|
340
|
+
max_segment_length (int): Maximum length of a single segment
|
|
341
|
+
total_max_length (int): Maximum total length across all segments
|
|
342
|
+
min_segment_value (float): Minimum value for a segment to be considered
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
List[Tuple[int, int]]: List of (start, end) indices for best segments
|
|
346
|
+
"""
|
|
347
|
+
print("Finding optimal continuous text segments...")
|
|
348
|
+
|
|
349
|
+
best_segments = []
|
|
350
|
+
segment_scores = []
|
|
351
|
+
total_included_chunks = 0
|
|
352
|
+
|
|
353
|
+
# Keep finding segments until we hit our limits
|
|
354
|
+
while total_included_chunks < total_max_length:
|
|
355
|
+
best_score = min_segment_value # Minimum threshold for a segment
|
|
356
|
+
best_segment = None
|
|
357
|
+
|
|
358
|
+
# Try each possible starting position
|
|
359
|
+
for start in range(len(chunk_values)):
|
|
360
|
+
# Skip if this start position is already in a selected segment
|
|
361
|
+
if any(start >= s[0] and start < s[1] for s in best_segments):
|
|
362
|
+
continue
|
|
363
|
+
|
|
364
|
+
# Try each possible segment length
|
|
365
|
+
for length in range(1, min(max_segment_length, len(chunk_values) - start) + 1):
|
|
366
|
+
end = start + length
|
|
367
|
+
|
|
368
|
+
# Skip if end position is already in a selected segment
|
|
369
|
+
if any(end > s[0] and end <= s[1] for s in best_segments):
|
|
370
|
+
continue
|
|
371
|
+
|
|
372
|
+
# Calculate segment value as sum of chunk values
|
|
373
|
+
segment_value = sum(chunk_values[start:end])
|
|
374
|
+
|
|
375
|
+
# Update best segment if this one is better
|
|
376
|
+
if segment_value > best_score:
|
|
377
|
+
best_score = segment_value
|
|
378
|
+
best_segment = (start, end)
|
|
379
|
+
|
|
380
|
+
# If we found a good segment, add it
|
|
381
|
+
if best_segment:
|
|
382
|
+
best_segments.append(best_segment)
|
|
383
|
+
segment_scores.append(best_score)
|
|
384
|
+
total_included_chunks += best_segment[1] - best_segment[0]
|
|
385
|
+
print(f"Found segment {best_segment} with score {best_score:.4f}")
|
|
386
|
+
else:
|
|
387
|
+
# No more good segments to find
|
|
388
|
+
break
|
|
389
|
+
|
|
390
|
+
# Sort segments by their starting position for readability
|
|
391
|
+
best_segments = sorted(best_segments, key=lambda x: x[0])
|
|
392
|
+
|
|
393
|
+
return best_segments, segment_scores
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def reconstruct_segments(chunks, best_segments):
|
|
397
|
+
"""
|
|
398
|
+
Reconstruct text segments based on chunk indices.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
chunks (List[str]): List of all document chunks
|
|
402
|
+
best_segments (List[Tuple[int, int]]): List of (start, end) indices for segments
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
List[str]: List of reconstructed text segments
|
|
406
|
+
"""
|
|
407
|
+
reconstructed_segments = [] # Initialize an empty list to store the reconstructed segments
|
|
408
|
+
|
|
409
|
+
for start, end in best_segments:
|
|
410
|
+
# Join the chunks in this segment to form the complete segment text
|
|
411
|
+
segment_text = " ".join(chunks[start:end])
|
|
412
|
+
# Append the segment text and its range to the reconstructed_segments list
|
|
413
|
+
reconstructed_segments.append({
|
|
414
|
+
"text": segment_text,
|
|
415
|
+
"segment_range": (start, end),
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
return reconstructed_segments # Return the list of reconstructed text segments
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def format_segments_for_context(segments):
|
|
422
|
+
"""
|
|
423
|
+
Format segments into a context string for the LLM.
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
segments (List[Dict]): List of segment dictionaries
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
str: Formatted context text
|
|
430
|
+
"""
|
|
431
|
+
context = [] # Initialize an empty list to store the formatted context
|
|
432
|
+
|
|
433
|
+
for i, segment in enumerate(segments):
|
|
434
|
+
# Create a header for each segment with its index and chunk range
|
|
435
|
+
segment_header = f"SEGMENT {i+1} (Chunks {segment['segment_range'][0]}-{segment['segment_range'][1]-1}):"
|
|
436
|
+
context.append(segment_header) # Add the segment header to the context list
|
|
437
|
+
context.append(segment['text']) # Add the segment text to the context list
|
|
438
|
+
context.append("-" * 80) # Add a separator line for readability
|
|
439
|
+
|
|
440
|
+
# Join all elements in the context list with double newlines and return the result
|
|
441
|
+
return "\n\n".join(context)
|
LLMlight/__init__.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from LLMlight.LLMlight import LLMlight
|
|
3
|
+
|
|
4
|
+
__author__ = 'Erdogan Tasksen'
|
|
5
|
+
__email__ = 'erdogant@gmail.com'
|
|
6
|
+
__version__ = '0.1.0'
|
|
7
|
+
|
|
8
|
+
# Setup root logger
|
|
9
|
+
_logger = logging.getLogger('LLMlight')
|
|
10
|
+
_log_handler = logging.StreamHandler()
|
|
11
|
+
_fmt = '[{asctime}] [{name}] [{levelname}] {msg}'
|
|
12
|
+
_formatter = logging.Formatter(fmt=_fmt, style='{', datefmt='%d-%m-%Y %H:%M:%S')
|
|
13
|
+
_log_handler.setFormatter(_formatter)
|
|
14
|
+
_logger.addHandler(_log_handler)
|
|
15
|
+
_log_handler.setLevel(logging.DEBUG)
|
|
16
|
+
_logger.propagate = False
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# module level doc-string
|
|
21
|
+
__doc__ = """
|
|
22
|
+
LLMlight
|
|
23
|
+
=====================================================================
|
|
24
|
+
|
|
25
|
+
LLMlight is for...
|
|
26
|
+
|
|
27
|
+
Example
|
|
28
|
+
-------
|
|
29
|
+
>>> import LLMlight as LLMlight
|
|
30
|
+
>>> model = LLMlight.fit_transform(X)
|
|
31
|
+
>>> fig,ax = LLMlight.plot(model)
|
|
32
|
+
|
|
33
|
+
References
|
|
34
|
+
----------
|
|
35
|
+
https://github.com/erdogant/LLMlight
|
|
36
|
+
|
|
37
|
+
"""
|
LLMlight/examples.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# %%
|
|
2
|
+
# import LLMlight as llm
|
|
3
|
+
# print(dir(llm))
|
|
4
|
+
# print(llm.__version__)
|
|
5
|
+
|
|
6
|
+
# %%
|
|
7
|
+
from LLMlight import LLMlight
|
|
8
|
+
model = LLMlight(verbose='debug')
|
|
9
|
+
model.check_logger()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
#%% Available models
|
|
13
|
+
from LLMlight import LLMlight
|
|
14
|
+
model = LLMlight(verbose='info')
|
|
15
|
+
modelnames = model.get_available_models(validate=True)
|
|
16
|
+
|
|
17
|
+
# %%
|
|
18
|
+
for modelname in modelnames:
|
|
19
|
+
from LLMlight import LLMlight
|
|
20
|
+
llm = LLMlight(modelname=modelname)
|
|
21
|
+
print(llm.modelname)
|
|
22
|
+
|
|
23
|
+
system_message = "You are a helpful assistant."
|
|
24
|
+
response = llm.run('What is the capital of France?', system=system_message)
|
|
25
|
+
print(response)
|
|
26
|
+
|
|
27
|
+
# %%
|
|
28
|
+
from LLMlight import LLMlight
|
|
29
|
+
model_path = r'C:/Users\beeld/.lmstudio/models/NousResearch/Hermes-3-Llama-3.2-3B-GGUF\Hermes-3-Llama-3.2-3B.Q4_K_M.gguf'
|
|
30
|
+
model = LLMlight(endpoint=model_path, top_p=0.9)
|
|
31
|
+
# model.run('hello, who are you?')
|
|
32
|
+
system_message = "You are a helpful assistant."
|
|
33
|
+
response = model.run('What is the capital of France?', system=system_message)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
#%%
|
|
40
|
+
import llama_cpp
|
|
41
|
+
print(llama_cpp.__version__)
|
|
42
|
+
print(llama_cpp.llama_cpp_version()) # Might crash if incompatible
|
|
43
|
+
|
|
44
|
+
# Check your GGUF model's metadata
|
|
45
|
+
model_path = r'C:/Users\beeld/.lmstudio/models/NousResearch/Hermes-3-Llama-3.2-3B-GGUF\Hermes-3-Llama-3.2-3B.Q4_K_M.gguf'
|
|
46
|
+
with open(model_path, 'rb') as f:
|
|
47
|
+
header = f.read(128)
|
|
48
|
+
print(header)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
#%%
|
|
52
|
+
import os
|
|
53
|
+
import logging
|
|
54
|
+
import requests
|
|
55
|
+
from tqdm import tqdm
|
|
56
|
+
from llama_cpp import Llama
|
|
57
|
+
|
|
58
|
+
logger = logging.getLogger(__name__)
|
|
59
|
+
logging.basicConfig(level=logging.INFO)
|
|
60
|
+
|
|
61
|
+
def download_and_load_gguf_model(
|
|
62
|
+
url: str,
|
|
63
|
+
model_name: str,
|
|
64
|
+
cache_dir: str = "local_models",
|
|
65
|
+
n_ctx: int = 4096,
|
|
66
|
+
n_threads: int = 8,
|
|
67
|
+
n_gpu_layers: int = 0,
|
|
68
|
+
verbose: bool = True
|
|
69
|
+
) -> Llama:
|
|
70
|
+
"""
|
|
71
|
+
Downloads a GGUF model from a URL (if not already cached) and loads it with llama-cpp-python.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
url (str): Direct URL to the .gguf model file.
|
|
75
|
+
model_name (str): Filename to use for local caching (e.g. 'Hermes.gguf').
|
|
76
|
+
cache_dir (str): Directory to store the model. Default is 'local_models'.
|
|
77
|
+
n_ctx (int): Context window size.
|
|
78
|
+
n_threads (int): CPU threads to use.
|
|
79
|
+
n_gpu_layers (int): GPU layers to offload. Use 0 for CPU-only.
|
|
80
|
+
verbose (bool): Print logs during loading.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Llama: Loaded model ready for inference.
|
|
84
|
+
"""
|
|
85
|
+
os.makedirs(cache_dir, exist_ok=True)
|
|
86
|
+
model_path = os.path.join(cache_dir, model_name)
|
|
87
|
+
|
|
88
|
+
if not os.path.exists(model_path):
|
|
89
|
+
logger.info(f"Model not found locally. Downloading from:\n{url}")
|
|
90
|
+
try:
|
|
91
|
+
with requests.get(url, stream=True) as r:
|
|
92
|
+
r.raise_for_status()
|
|
93
|
+
total_size = int(r.headers.get("content-length", 0))
|
|
94
|
+
with open(model_path, "wb") as f, tqdm(
|
|
95
|
+
total=total_size, unit='B', unit_scale=True, desc=model_name
|
|
96
|
+
) as bar:
|
|
97
|
+
for chunk in r.iter_content(chunk_size=8192):
|
|
98
|
+
f.write(chunk)
|
|
99
|
+
bar.update(len(chunk))
|
|
100
|
+
except Exception as e:
|
|
101
|
+
raise RuntimeError(f"Failed to download model: {e}")
|
|
102
|
+
|
|
103
|
+
else:
|
|
104
|
+
logger.info(f"Using cached model at: {model_path}")
|
|
105
|
+
|
|
106
|
+
# Load with llama-cpp
|
|
107
|
+
llm = Llama(
|
|
108
|
+
model_path=model_path,
|
|
109
|
+
n_ctx=n_ctx,
|
|
110
|
+
n_threads=n_threads,
|
|
111
|
+
n_gpu_layers=n_gpu_layers,
|
|
112
|
+
verbose=verbose
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
logger.info("Model loaded successfully.")
|
|
116
|
+
return llm
|
|
117
|
+
|
|
118
|
+
#%%
|
|
119
|
+
url = "https://huggingface.co/TheBloke/Hermes-2-Pro-Llama-3-GGUF/resolve/main/hermes-2-pro-llama-3.Q4_K_M.gguf"
|
|
120
|
+
|
|
121
|
+
model_name = "hermes-2-pro-llama-3.Q4_K_M.gguf"
|
|
122
|
+
|
|
123
|
+
llm = download_and_load_gguf_model(url, model_name)
|
|
124
|
+
|
|
125
|
+
prompt = "<|begin_of_text|><|start_header_id|>user<|end_header_id|>\nWhat is the capital of France?\n<|start_header_id|>assistant<|end_header_id|>\n"
|
|
126
|
+
response = llm(prompt=prompt, max_tokens=20, stop=["<|end_of_text|>"])
|
|
127
|
+
print(response["choices"][0]["text"].strip())
|
|
128
|
+
|
|
129
|
+
#%%
|
|
130
|
+
# Code to inference Hermes with HF Transformers
|
|
131
|
+
# Requires pytorch, transformers, bitsandbytes, sentencepiece, protobuf, and flash-attn packages
|
|
132
|
+
|
|
133
|
+
import torch
|
|
134
|
+
from transformers import AutoTokenizer, AutoModelForCausalLM, LlamaForCausalLM
|
|
135
|
+
import bitsandbytes, flash_attn
|
|
136
|
+
|
|
137
|
+
tokenizer = AutoTokenizer.from_pretrained('NousResearch/Hermes-3-Llama-3.1-8B', trust_remote_code=True)
|
|
138
|
+
model = LlamaForCausalLM.from_pretrained(
|
|
139
|
+
"NousResearch/Hermes-3-Llama-3.1-8B",
|
|
140
|
+
torch_dtype=torch.float16,
|
|
141
|
+
device_map="auto",
|
|
142
|
+
load_in_8bit=False,
|
|
143
|
+
load_in_4bit=True,
|
|
144
|
+
use_flash_attention_2=True
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
prompts = [
|
|
148
|
+
"""<|im_start|>system
|
|
149
|
+
You are a sentient, superintelligent artificial general intelligence, here to teach and assist me.<|im_end|>
|
|
150
|
+
<|im_start|>user
|
|
151
|
+
Write a short story about Goku discovering kirby has teamed up with Majin Buu to destroy the world.<|im_end|>
|
|
152
|
+
<|im_start|>assistant""",
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
for chat in prompts:
|
|
156
|
+
print(chat)
|
|
157
|
+
input_ids = tokenizer(chat, return_tensors="pt").input_ids.to("cuda")
|
|
158
|
+
generated_ids = model.generate(input_ids, max_new_tokens=750, temperature=0.8, repetition_penalty=1.1, do_sample=True, eos_token_id=tokenizer.eos_token_id)
|
|
159
|
+
response = tokenizer.decode(generated_ids[0][input_ids.shape[-1]:], skip_special_tokens=True, clean_up_tokenization_space=True)
|
|
160
|
+
print(f"Response: {response}")
|
|
161
|
+
|
|
162
|
+
#%%
|
|
163
|
+
url = "https://huggingface.co/TheBloke/Hermes-2-Pro-Llama-3-GGUF/resolve/main/hermes-2-pro-llama-3.Q4_K_M.gguf"
|
|
164
|
+
model_name = "hermes-2-pro-llama-3.Q4_K_M.gguf"
|
|
165
|
+
|
|
166
|
+
# Already avoids bitsandbytes
|
|
167
|
+
llm = download_and_load_gguf_model(url, model_name, n_gpu_layers=0)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
|
172
|
+
|
|
173
|
+
model_id = "meta-llama/Llama-2-7b-hf" # for example
|
|
174
|
+
|
|
175
|
+
model = AutoModelForCausalLM.from_pretrained(model_name, device_map="auto") # will crash without CUDA if quantized
|