LLMlight 0.2.1__tar.gz → 0.3.0__tar.gz
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-0.2.1 → llmlight-0.3.0}/LLMlight/LLMlight.py +569 -222
- {llmlight-0.2.1 → llmlight-0.3.0}/LLMlight/RAG.py +1 -1
- llmlight-0.3.0/LLMlight/__init__.py +129 -0
- llmlight-0.3.0/LLMlight/examples.py +561 -0
- llmlight-0.3.0/LLMlight/memory.py +334 -0
- llmlight-0.3.0/LLMlight/read_outlook.py +337 -0
- {llmlight-0.2.1 → llmlight-0.3.0}/LLMlight/utils.py +5 -5
- {llmlight-0.2.1 → llmlight-0.3.0}/LLMlight.egg-info/PKG-INFO +115 -29
- {llmlight-0.2.1 → llmlight-0.3.0}/LLMlight.egg-info/SOURCES.txt +2 -0
- {llmlight-0.2.1 → llmlight-0.3.0}/LLMlight.egg-info/requires.txt +1 -0
- {llmlight-0.2.1 → llmlight-0.3.0}/PKG-INFO +115 -29
- llmlight-0.3.0/README.md +250 -0
- {llmlight-0.2.1 → llmlight-0.3.0}/pyproject.toml +1 -0
- llmlight-0.2.1/LLMlight/__init__.py +0 -48
- llmlight-0.2.1/LLMlight/examples.py +0 -277
- llmlight-0.2.1/README.md +0 -165
- {llmlight-0.2.1 → llmlight-0.3.0}/LICENSE +0 -0
- {llmlight-0.2.1 → llmlight-0.3.0}/LLMlight.egg-info/dependency_links.txt +0 -0
- {llmlight-0.2.1 → llmlight-0.3.0}/LLMlight.egg-info/top_level.txt +0 -0
- {llmlight-0.2.1 → llmlight-0.3.0}/MANIFEST.in +0 -0
- {llmlight-0.2.1 → llmlight-0.3.0}/setup.cfg +0 -0
|
@@ -18,12 +18,21 @@ import copy
|
|
|
18
18
|
import re
|
|
19
19
|
from tqdm import tqdm
|
|
20
20
|
|
|
21
|
+
from typing import List, Union
|
|
22
|
+
|
|
21
23
|
from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer
|
|
22
24
|
from sklearn.metrics.pairwise import cosine_similarity
|
|
23
25
|
from sentence_transformers import SentenceTransformer
|
|
24
26
|
|
|
25
|
-
from
|
|
27
|
+
from memvid import MemvidEncoder, MemvidRetriever
|
|
28
|
+
# from memvid.config import get_default_config as memvid_get_default_config
|
|
29
|
+
|
|
30
|
+
# import memory
|
|
31
|
+
# import RAG
|
|
32
|
+
# import utils
|
|
33
|
+
from . import RAG
|
|
26
34
|
from . import utils
|
|
35
|
+
from . import memory
|
|
27
36
|
# DEBUG
|
|
28
37
|
# import utils
|
|
29
38
|
# from RAG import RAG_with_RSE
|
|
@@ -42,9 +51,60 @@ class LLMlight:
|
|
|
42
51
|
5. Enable "server on local network" if you need.
|
|
43
52
|
6. Enable Running.
|
|
44
53
|
|
|
54
|
+
How LLMlight Works
|
|
55
|
+
-----------------
|
|
56
|
+
LLMlight processes text through several key stages to generate intelligent responses:
|
|
57
|
+
|
|
58
|
+
1. Preprocessing Stage
|
|
59
|
+
---------------------
|
|
60
|
+
The input context can be processed in different ways:
|
|
61
|
+
- No preprocessing: Uses the raw context directly
|
|
62
|
+
- Chunk-wise processing: Breaks down the context into manageable chunks, processes each chunk independently, and combines results
|
|
63
|
+
- Global reasoning: Creates a global summary of the context before processing
|
|
64
|
+
|
|
65
|
+
2. Retrieval Method Stage
|
|
66
|
+
------------------------
|
|
67
|
+
Three main approaches for retrieving relevant information:
|
|
68
|
+
- Naive RAG: Splits text into chunks and uses similarity scoring to find the most relevant sections
|
|
69
|
+
- RSE (Relevant Segment Extraction): Identifies and extracts complete relevant text segments
|
|
70
|
+
- No retrieval: Uses the entire context directly
|
|
71
|
+
|
|
72
|
+
3. Embedding Stage
|
|
73
|
+
-----------------
|
|
74
|
+
Multiple embedding options for text representation:
|
|
75
|
+
- TF-IDF: Best for structured documents with matching query terms
|
|
76
|
+
- Bag of Words: Simple word frequency approach
|
|
77
|
+
- BERT: Advanced contextual embeddings for free-form text
|
|
78
|
+
- BGE-small: Efficient embedding model for general use
|
|
79
|
+
|
|
80
|
+
4. Prompting Stage
|
|
81
|
+
-----------------
|
|
82
|
+
The system constructs prompts by combining:
|
|
83
|
+
- System message: Defines the AI's role and behavior
|
|
84
|
+
- Context: Processed and retrieved relevant information
|
|
85
|
+
- User query: The specific question or request
|
|
86
|
+
- Instructions: Additional guidance for response generation
|
|
87
|
+
|
|
88
|
+
5. Response Generation
|
|
89
|
+
---------------------
|
|
90
|
+
The model generates responses using:
|
|
91
|
+
- Temperature control: Adjusts response randomness (0.7 default)
|
|
92
|
+
- Top-p sampling: Controls response diversity
|
|
93
|
+
- Context window management: Handles token limits efficiently
|
|
94
|
+
|
|
95
|
+
The system can be configured through various parameters to optimize for different use cases, from simple Q&A to complex document analysis.
|
|
96
|
+
|
|
97
|
+
Processing Flow
|
|
98
|
+
--------------
|
|
99
|
+
The system follows a sequential processing flow where each stage builds upon the previous one. First, the input context undergoes preprocessing, where it can be either used as-is or transformed into chunks for more manageable processing. These chunks are then passed through the retrieval method stage, which determines how relevant information is extracted and organized.
|
|
100
|
+
During the embedding stage, the text is converted into numerical representations that capture its semantic meaning. This is crucial for the system to understand and process the content effectively. The embedding method chosen can significantly impact the system's ability to match queries with relevant content.
|
|
101
|
+
The prompting stage brings together all the processed information, combining it with the user's query and any specific instructions. This creates a comprehensive prompt that guides the model in generating an appropriate response. The final response generation stage uses this prompt to create a coherent and relevant output, with parameters like temperature and top-p sampling helping to control the response's characteristics.
|
|
102
|
+
Throughout this process, the system maintains flexibility through various configuration options, allowing it to adapt to different types of queries and contexts. This modular approach enables the system to handle everything from simple questions to complex document analysis tasks efficiently.
|
|
103
|
+
|
|
104
|
+
|
|
45
105
|
Parameters
|
|
46
106
|
----------
|
|
47
|
-
|
|
107
|
+
model : str
|
|
48
108
|
'hermes-3-llama-3.2-3b'
|
|
49
109
|
'mistral-7b-grok'
|
|
50
110
|
'openhermes-2.5-mistral-7b'
|
|
@@ -52,22 +112,29 @@ class LLMlight:
|
|
|
52
112
|
system : str
|
|
53
113
|
String of the system message.
|
|
54
114
|
"I am a helpfull assistant"
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
'
|
|
58
|
-
'
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
'
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
'tfidf':
|
|
66
|
-
'bow':
|
|
67
|
-
'bert':
|
|
115
|
+
retrieval_method : str (default: 'RAG_basic')
|
|
116
|
+
None: No processing. The entire context is used for the query.
|
|
117
|
+
'knowledge_base.mp4' Local or absolute path to your (video) memory file.
|
|
118
|
+
'RAG_basic': Context is processed using Navie RAG approach. Ideal for chats and when you need to answer specfic questions: Chunk of text are created. Use cosine similarity to for ranking. The top scoring chunks will be combined (n chunks) and used as input with the prompt.
|
|
119
|
+
'RSE': Context is processed using Navie RSE approach. Identify and extract entire segments of relevant text.
|
|
120
|
+
embedding : str, dict (default: 'automatic')
|
|
121
|
+
Specify the embedding. When using both video-memory and context, it can be specified with a dictionary: {'memory': 'memvid', 'RAG_basic': 'bert'}
|
|
122
|
+
'automatic': {'memory': 'memvid', 'RAG_basic': 'bert'}
|
|
123
|
+
None: No embedding is performed.
|
|
124
|
+
'memvid': This embedding can only be applied when using video-memory in the retrieval method.
|
|
125
|
+
'tfidf': Best use when it is a structured documents and the words in the queries are matching.
|
|
126
|
+
'bow': Bag of words approach. Best use when you expect words in the document and queries to be matching.
|
|
127
|
+
'bert': Best use when document is more free text and the queries may not match exactly the words or sentences in the document.
|
|
68
128
|
'bge-small':
|
|
129
|
+
preprocessing : str (default: None)
|
|
130
|
+
None: No pre-processing. The original context is used in the pipeline of retrieval_method, embedding and the response.
|
|
131
|
+
'global-reasoning': The input context will be summarized per chunk globally. The total set of summarized context is then returned. The normal pipeline proceeds for the query, instructions, system etc.
|
|
132
|
+
'chunk-wise': The input context will be analyze chunkwise based on the query, instructions and system. The total set of answered-chunks is then returned. The normal pipeline proceeds for the query, instructions, system etc.
|
|
69
133
|
temperature : float, optional
|
|
70
|
-
Sampling temperature
|
|
134
|
+
Sampling temperature.
|
|
135
|
+
0.7: (default)
|
|
136
|
+
0: Deterministic
|
|
137
|
+
1: Stochastic (max)
|
|
71
138
|
top_p : float, optional
|
|
72
139
|
Top-p (nucleus) sampling parameter (default is 1.0, no filtering).
|
|
73
140
|
chunks: dict : {'method': 'chars', 'size': 1000, 'overlap': 250, 'top_chunks': 5}
|
|
@@ -89,112 +156,77 @@ class LLMlight:
|
|
|
89
156
|
|
|
90
157
|
Examples
|
|
91
158
|
--------
|
|
92
|
-
>>>
|
|
93
|
-
>>>
|
|
159
|
+
>>> # Examples
|
|
160
|
+
>>> from LLMlight import LLMlight
|
|
161
|
+
>>> client = LLMlight()
|
|
162
|
+
>>> client.prompt('hello, who are you?')
|
|
94
163
|
>>> system_message = "You are a helpful assistant."
|
|
95
|
-
>>> response =
|
|
164
|
+
>>> response = client.prompt('What is the capital of France?', system=system_message, top_p=0.9)
|
|
165
|
+
>>> print(response)
|
|
96
166
|
|
|
97
167
|
"""
|
|
98
168
|
def __init__(self,
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
169
|
+
model: str = "hermes-3-llama-3.2-3b",
|
|
170
|
+
retrieval_method: str = 'RAG_basic',
|
|
171
|
+
embedding: (str, dict) = {'memory': 'memvid', 'context': 'bert'},
|
|
172
|
+
preprocessing: str = None,
|
|
173
|
+
alpha: float = 0.05,
|
|
174
|
+
top_chunks: int = 5,
|
|
175
|
+
temperature: (int, float) = 0.7,
|
|
176
|
+
top_p: (int, float) = 1.0,
|
|
177
|
+
chunks: dict = {'method': 'chars', 'size': 1024, 'overlap': 200},
|
|
178
|
+
endpoint: str = "http://localhost:1234/v1/chat/completions",
|
|
179
|
+
n_ctx: int = 4096,
|
|
180
|
+
verbose: (str, int) = 'info',
|
|
109
181
|
):
|
|
110
182
|
|
|
111
183
|
# Set the logger
|
|
112
184
|
set_logger(verbose)
|
|
185
|
+
|
|
113
186
|
# Store data in self
|
|
114
|
-
self.
|
|
187
|
+
self.model = model
|
|
115
188
|
self.preprocessing = preprocessing
|
|
116
|
-
self.
|
|
117
|
-
self.
|
|
189
|
+
self.retrieval_method = retrieval_method
|
|
190
|
+
self.alpha = alpha
|
|
191
|
+
self.top_chunks = top_chunks
|
|
118
192
|
self.temperature = temperature
|
|
119
193
|
self.top_p = top_p
|
|
120
194
|
self.endpoint = endpoint
|
|
121
|
-
if chunks is None: chunks = {}
|
|
122
|
-
self.chunks = {**{'method': 'chars', 'size': 1000, 'overlap': 250, 'top_chunks': 5}, **chunks}
|
|
123
195
|
self.n_ctx = n_ctx
|
|
124
196
|
self.context = None
|
|
197
|
+
self.embedding = _set_embedding(embedding)
|
|
125
198
|
|
|
126
|
-
# Set
|
|
127
|
-
if
|
|
128
|
-
|
|
129
|
-
elif embedding == 'bge-small':
|
|
130
|
-
self.embedding_model = SentenceTransformer('BAAI/bge-small-en')
|
|
131
|
-
else:
|
|
132
|
-
self.embedding_model = None
|
|
133
|
-
# Load local model
|
|
134
|
-
if os.path.isfile(self.endpoint):
|
|
135
|
-
self.llm = load_local_gguf_model(self.endpoint, n_ctx=self.n_ctx)
|
|
136
|
-
|
|
137
|
-
def check_logger(self):
|
|
138
|
-
"""Check the verbosity."""
|
|
139
|
-
logger.debug('DEBUG')
|
|
140
|
-
logger.info('INFO')
|
|
141
|
-
logger.warning('WARNING')
|
|
142
|
-
logger.critical('CRITICAL')
|
|
143
|
-
|
|
144
|
-
def get_available_models(self, validate=False):
|
|
145
|
-
# Set your local API base URL
|
|
146
|
-
# base_url = 'http://localhost:1234/v1/chat/completions'
|
|
147
|
-
base_url = '/'.join(self.endpoint.split('/')[:3]) + '/'
|
|
148
|
-
logger.info('Collecting models in the API endpoint..')
|
|
149
|
-
|
|
150
|
-
try:
|
|
151
|
-
model_url = base_url.rstrip('/') + '/v1/models'
|
|
152
|
-
logger.info(f'Looking up models: {model_url}')
|
|
153
|
-
response = requests.get(model_url, timeout=10)
|
|
154
|
-
if response.status_code == 200:
|
|
155
|
-
try:
|
|
156
|
-
models = response.json()["data"]
|
|
157
|
-
model_dict = {model["id"]: model for model in models}
|
|
158
|
-
except (KeyError, ValueError) as e:
|
|
159
|
-
logger.error("Error parsing model data:", e)
|
|
160
|
-
else:
|
|
161
|
-
logger.warning("Request failed with status code:", response.status_code)
|
|
162
|
-
logger.warning("Response:", response.text)
|
|
199
|
+
# Set chunk parameters
|
|
200
|
+
if chunks is None: chunks = {}
|
|
201
|
+
self.chunks = {**{'method': 'chars', 'size': 1000, 'overlap': 250}, **chunks}
|
|
163
202
|
|
|
164
|
-
|
|
165
|
-
|
|
203
|
+
# Set Memory parameters.
|
|
204
|
+
self.memory_init(retrieval_method)
|
|
166
205
|
|
|
167
|
-
#
|
|
168
|
-
if
|
|
169
|
-
|
|
170
|
-
keys = copy.deepcopy(list(model_dict.keys()))
|
|
171
|
-
|
|
172
|
-
for key in keys:
|
|
173
|
-
from LLMlight import LLMlight
|
|
174
|
-
llm = LLMlight(modelname=key)
|
|
175
|
-
response = llm.prompt('What is the capital of France?', system="You are a helpful assistant.", return_type='string')
|
|
176
|
-
response = response[0:30].replace('\n', ' ').replace('\r', ' ').lower()
|
|
177
|
-
if 'error: 404' in response:
|
|
178
|
-
logger.error(f"{llm.modelname}: {response}")
|
|
179
|
-
model_dict.pop(key)
|
|
180
|
-
else:
|
|
181
|
-
logger.info(f"{llm.modelname}: {response}")
|
|
206
|
+
# Load local LLM gguf model
|
|
207
|
+
if os.path.isfile(self.endpoint):
|
|
208
|
+
self.llm = load_local_gguf_model(self.endpoint, n_ctx=self.n_ctx)
|
|
182
209
|
|
|
183
|
-
|
|
210
|
+
logger.info('LLMlight is initialized!')
|
|
211
|
+
logger.info(f'Model: {self.model}')
|
|
212
|
+
logger.info(f'Preprocessing: {self.preprocessing or "disabled"}')
|
|
213
|
+
logger.info(f'Retrieval method: {self.retrieval_method or "disabled"}')
|
|
214
|
+
logger.info(f'Embedding: {self.embedding or "disabled"}')
|
|
184
215
|
|
|
185
216
|
def prompt(self,
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
Run the model with the provided parameters.
|
|
217
|
+
query: str,
|
|
218
|
+
instructions: str = None,
|
|
219
|
+
system: str = None,
|
|
220
|
+
context: str = None,
|
|
221
|
+
response_format=None,
|
|
222
|
+
temperature: (int, float) = None,
|
|
223
|
+
top_p: (int, float) = None,
|
|
224
|
+
stream: bool = False,
|
|
225
|
+
return_type: str = 'string',
|
|
226
|
+
verbose=None,
|
|
227
|
+
):
|
|
228
|
+
"""Run the model with the provided parameters.
|
|
229
|
+
|
|
198
230
|
The final prompt is created based on the query, instructions, and the context
|
|
199
231
|
|
|
200
232
|
Parameters
|
|
@@ -223,48 +255,70 @@ class LLMlight:
|
|
|
223
255
|
'string': Return only the string answer (remove thinking strings using tags: <think> </think>).
|
|
224
256
|
'string_with_thinking' Return the full response which includes the thinking proces (if available).
|
|
225
257
|
|
|
258
|
+
Examples
|
|
259
|
+
--------
|
|
260
|
+
>>> # Examples
|
|
261
|
+
>>> from LLMlight import LLMlight
|
|
262
|
+
>>> client = LLMlight()
|
|
263
|
+
>>> client.prompt('hello, who are you?')
|
|
264
|
+
>>> system_message = "You are a helpful assistant."
|
|
265
|
+
>>> response = client.prompt('What is the capital of France?', system=system_message, top_p=0.9)
|
|
266
|
+
>>> print(response)
|
|
267
|
+
|
|
226
268
|
Returns
|
|
227
269
|
-------
|
|
228
270
|
str
|
|
229
271
|
The model's response or an error message if the request fails.
|
|
230
272
|
"""
|
|
231
|
-
|
|
232
|
-
|
|
273
|
+
if verbose is not None: set_logger(verbose)
|
|
274
|
+
logger.info(f'Creating response with {self.model}..')
|
|
233
275
|
|
|
276
|
+
if context is None: context = self.context
|
|
234
277
|
if temperature is None: temperature = self.temperature
|
|
235
278
|
if top_p is None: top_p = self.top_p
|
|
236
|
-
if context is None: context = self.context
|
|
237
|
-
# if embedding is not None: self.embedding = embedding
|
|
238
|
-
if isinstance(context, dict): context = '\n\n'.join(context.values())
|
|
239
|
-
|
|
240
|
-
# task (str): The use case for generation. (default: 'full')
|
|
241
|
-
# 'summarization'
|
|
242
|
-
# 'chat'
|
|
243
|
-
# 'code'
|
|
244
|
-
# 'longform'
|
|
245
|
-
# 'full'
|
|
246
|
-
# task = set_task(self.preprocessing, self.method)
|
|
247
279
|
self.task = 'full'
|
|
248
280
|
|
|
249
281
|
# Set system message
|
|
250
282
|
system = set_system_message(system)
|
|
251
|
-
|
|
252
|
-
|
|
283
|
+
|
|
284
|
+
# Extract relevant text for video memory
|
|
285
|
+
relevant_memory = self.relevant_memory_retrieval(query, return_type='list')
|
|
286
|
+
|
|
253
287
|
# Extract relevant text using retrieval method
|
|
254
|
-
relevant_context = self.
|
|
288
|
+
relevant_context = self.relevant_context_retrieval(query, context, return_type='list')
|
|
289
|
+
|
|
290
|
+
# Append the relevant chunks of texts
|
|
291
|
+
total_context = (relevant_memory or []) + (relevant_context or [])
|
|
292
|
+
|
|
293
|
+
# Preprocessing on the context
|
|
294
|
+
processed_context = self.compute_preprocessing(query, total_context, instructions, system)
|
|
295
|
+
|
|
255
296
|
# Set the prompt
|
|
256
|
-
|
|
297
|
+
logger.debug(processed_context)
|
|
298
|
+
|
|
299
|
+
# Make the prompt
|
|
300
|
+
prompt = self.set_prompt(query, instructions, processed_context, response_format=response_format)
|
|
301
|
+
logger.info('Running model now..')
|
|
257
302
|
|
|
258
303
|
# Run model
|
|
259
304
|
if os.path.isfile(self.endpoint):
|
|
260
305
|
# Run LLM from gguf model
|
|
261
|
-
response = self.requests_post_gguf(prompt, system, temperature=temperature, top_p=top_p,
|
|
306
|
+
response = self.requests_post_gguf(prompt, system, temperature=temperature, top_p=top_p, task=self.task, stream=stream, return_type=return_type)
|
|
262
307
|
else:
|
|
263
308
|
# Run LLM with http model
|
|
264
|
-
response = self.requests_post_http(prompt, system, temperature=temperature, top_p=top_p,
|
|
309
|
+
response = self.requests_post_http(prompt, system, temperature=temperature, top_p=top_p, task=self.task, stream=stream, return_type=return_type)
|
|
310
|
+
|
|
265
311
|
# Return
|
|
266
312
|
return response
|
|
267
313
|
|
|
314
|
+
# def _get_context(self, context):
|
|
315
|
+
# # First get the context
|
|
316
|
+
# if context is None:
|
|
317
|
+
# context = self.context
|
|
318
|
+
# # if isinstance(context, dict):
|
|
319
|
+
# # context = '\n\n'.join(context.values())
|
|
320
|
+
# return context
|
|
321
|
+
|
|
268
322
|
def requests_post_gguf(self, prompt, system, temperature=0.8, top_p=1, headers=None, task='full', stream=False, return_type='string'):
|
|
269
323
|
# Note that it is better to use messages_prompt instead of a dict (messages_dict) because most GGUF-based models don't have a tokenizer/parser that can interpret the JSON-style message structure.
|
|
270
324
|
# Prepare data for request.
|
|
@@ -272,7 +326,7 @@ class LLMlight:
|
|
|
272
326
|
# Prepare messages
|
|
273
327
|
messages = [{"role": "system", "content": system}, {"role": "user", "content": prompt}]
|
|
274
328
|
# Convert messages to string prompt
|
|
275
|
-
prompt =
|
|
329
|
+
prompt = convert_messages_to_model(messages, model=self.model)
|
|
276
330
|
# Compute tokens
|
|
277
331
|
used_tokens, max_tokens = compute_tokens(prompt, n_ctx=self.n_ctx, task=task)
|
|
278
332
|
|
|
@@ -295,19 +349,24 @@ class LLMlight:
|
|
|
295
349
|
# Return
|
|
296
350
|
return response
|
|
297
351
|
|
|
298
|
-
def requests_post_http(self, prompt, system, temperature=0.8, top_p=1, headers=None, task='full', stream=False, return_type='string'):
|
|
352
|
+
def requests_post_http(self, prompt, system, temperature=0.8, top_p=1, headers=None, task='full', stream=False, return_type='string', max_tokens=None):
|
|
299
353
|
# Prepare data for request.
|
|
300
354
|
if headers is None: headers = {"Content-Type": "application/json"}
|
|
301
355
|
# Prepare messages
|
|
302
356
|
messages = [{"role": "system", "content": system}, {"role": "user", "content": prompt}]
|
|
357
|
+
|
|
358
|
+
# Convert messages to string prompt
|
|
359
|
+
prompt = convert_messages_to_model(messages, model=self.model)
|
|
360
|
+
|
|
303
361
|
# Create full prompt
|
|
304
362
|
prompt = messages[0]['content'] + messages[1]['content']
|
|
305
363
|
# Compute tokens
|
|
306
|
-
|
|
307
|
-
|
|
364
|
+
if max_tokens is None:
|
|
365
|
+
used_tokens, max_tokens = compute_tokens(prompt, n_ctx=self.n_ctx, task=task)
|
|
366
|
+
# logger.info(f'Generating response with {self.model}')
|
|
308
367
|
|
|
309
368
|
data = {
|
|
310
|
-
"model": self.
|
|
369
|
+
"model": self.model,
|
|
311
370
|
"messages": messages,
|
|
312
371
|
"temperature": temperature,
|
|
313
372
|
"top_p": top_p,
|
|
@@ -348,6 +407,122 @@ class LLMlight:
|
|
|
348
407
|
logger.error(f"{response.status_code} - {response}")
|
|
349
408
|
return f"Error: {response.status_code} - {response}"
|
|
350
409
|
|
|
410
|
+
def memory_init(self, file_path: str = "llmlight_memory.mp4", config: dict = None, embedding=None):
|
|
411
|
+
"""Build QR code video and index from chunks with unified codec handling.
|
|
412
|
+
|
|
413
|
+
Parameters
|
|
414
|
+
----------
|
|
415
|
+
file_path : str
|
|
416
|
+
Path to output video memory file.
|
|
417
|
+
config : dict
|
|
418
|
+
Dictionary containing configuration parameters.
|
|
419
|
+
|
|
420
|
+
"""
|
|
421
|
+
# Initialize video-memory
|
|
422
|
+
if not os.path.isfile(file_path): file_path = None
|
|
423
|
+
# Initialize
|
|
424
|
+
self.memory = memory.memvid_llm(file_path, config=config)
|
|
425
|
+
|
|
426
|
+
# Set the embedding
|
|
427
|
+
if embedding is not None:
|
|
428
|
+
self.embedding['memory'] = embedding
|
|
429
|
+
logger.info(f'Memory embedding is set: {self.embedding or "disabled"}')
|
|
430
|
+
|
|
431
|
+
def memory_load(self, file_path: str = None):
|
|
432
|
+
# Initialize if not yet initialized
|
|
433
|
+
if not hasattr(self.memory, 'encoder'):
|
|
434
|
+
self.memory_init(file_path)
|
|
435
|
+
# Load the retriever
|
|
436
|
+
if not hasattr(self.memory, 'retriever'):
|
|
437
|
+
# Load video-memory
|
|
438
|
+
self.memory.load()
|
|
439
|
+
|
|
440
|
+
def memory_save(self,
|
|
441
|
+
file_path: str = None,
|
|
442
|
+
codec: str = 'mp4v',
|
|
443
|
+
auto_build_docker: bool = True,
|
|
444
|
+
allow_fallback: bool = True,
|
|
445
|
+
overwrite: bool = False,
|
|
446
|
+
show_progress: bool = True,
|
|
447
|
+
):
|
|
448
|
+
"""Build QR code video and index from chunks with unified codec handling.
|
|
449
|
+
|
|
450
|
+
Parameters
|
|
451
|
+
----------
|
|
452
|
+
file_path : str (default is the initialization memory-path)
|
|
453
|
+
Path to output video memory file.
|
|
454
|
+
codec : str, optional
|
|
455
|
+
Video codec ('mp4v', 'h265', 'h264', etc.)
|
|
456
|
+
'mp4v': Default
|
|
457
|
+
auto_build_docker : bool (default: True)
|
|
458
|
+
Whether to auto-build Docker if needed.
|
|
459
|
+
allow_fallback : bool
|
|
460
|
+
Whether to fall back to MP4V if advanced codec fails.
|
|
461
|
+
show_progress : bool (default: True)
|
|
462
|
+
|
|
463
|
+
"""
|
|
464
|
+
self.memory.save(file_path=file_path, codec=codec, auto_build_docker=auto_build_docker, allow_fallback=allow_fallback, overwrite=overwrite, show_progress=show_progress)
|
|
465
|
+
|
|
466
|
+
def memory_add(self,
|
|
467
|
+
text: Union[str, List[str]] = None,
|
|
468
|
+
input_files: Union[str, List[str]] = None,
|
|
469
|
+
dirpath: str = None,
|
|
470
|
+
filetypes: List[str] = ['.pdf', '.txt', '.epub', '.md', '.doc', '.docx', '.rtf', '.html', '.htm'],
|
|
471
|
+
chunk_size: int = 512,
|
|
472
|
+
chunk_overlap: int = 100,
|
|
473
|
+
overwrite=False):
|
|
474
|
+
"""Add chunks to memory.
|
|
475
|
+
|
|
476
|
+
Parameters
|
|
477
|
+
----------
|
|
478
|
+
input_files : (str, list)
|
|
479
|
+
Path to file(s).
|
|
480
|
+
|
|
481
|
+
"""
|
|
482
|
+
self.memory.add(text=text, input_files=input_files, dirpath=dirpath, filetypes=filetypes, chunk_size=chunk_size, chunk_overlap=chunk_overlap, overwrite=overwrite)
|
|
483
|
+
|
|
484
|
+
def compute_probability(self, query, scores, embedding, n=5000):
|
|
485
|
+
if not hasattr(self.memory, 'retriever'):
|
|
486
|
+
logger.warning('No chunks to encode for null distribution. Use client.add_chunks() first')
|
|
487
|
+
|
|
488
|
+
logger.info('Creating random chunks of text with the same frequency distribution.')
|
|
489
|
+
if self.embedding['memory']=='memvid':
|
|
490
|
+
results = self.memory.retriever.index_manager.search(query, top_k=n)
|
|
491
|
+
random_scores = np.array(list(map(lambda x: x[1], results)))
|
|
492
|
+
bound = 'left'
|
|
493
|
+
else:
|
|
494
|
+
random_chunks = self.memory.get_random_chunks(n=n)
|
|
495
|
+
query_vector, chunk_vectors = self.fit_transform(query, random_chunks, embedding=embedding)
|
|
496
|
+
# Compute similarity
|
|
497
|
+
random_scores = cosine_similarity(query_vector, chunk_vectors)[0]
|
|
498
|
+
# Remove all scores with exactly value 0
|
|
499
|
+
random_scores = random_scores[random_scores!=0]
|
|
500
|
+
bound = 'right'
|
|
501
|
+
|
|
502
|
+
# Top indices
|
|
503
|
+
# top_indices = np.argsort(scores)[::-1]
|
|
504
|
+
# Join relevant chunks and send as prompt
|
|
505
|
+
# relevant_chunks = [random_chunks[i] for i in top_indices]
|
|
506
|
+
# relevant_scores = [scores[i] for i in top_indices]
|
|
507
|
+
|
|
508
|
+
from distfit import distfit
|
|
509
|
+
model = distfit(method='parametric', alpha=self.alpha, bound=bound, verbose='warning')
|
|
510
|
+
model.fit_transform(random_scores);
|
|
511
|
+
|
|
512
|
+
results = model.predict(scores, alpha=self.alpha, todf=False, multtest='fdr_bh')
|
|
513
|
+
# results['y_bool'] = results['P']<=self.alpha
|
|
514
|
+
# Store figure
|
|
515
|
+
fig, ax = model.plot(title=f'Retrieval method:{self.retrieval_method}, Embedding: {embedding}')
|
|
516
|
+
|
|
517
|
+
# Store
|
|
518
|
+
self.distfit = model
|
|
519
|
+
self.distfit.fig = fig
|
|
520
|
+
self.distfit.ax = ax
|
|
521
|
+
# Return
|
|
522
|
+
return results
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
|
|
351
526
|
def task(self,
|
|
352
527
|
query="Extract key insights while maintaining coherence of the previous summaries.",
|
|
353
528
|
instructions="",
|
|
@@ -388,7 +563,7 @@ class LLMlight:
|
|
|
388
563
|
logger.info(f'Working on text chunk {i}/{len(chunks)}')
|
|
389
564
|
|
|
390
565
|
# Keep last N summaries for context (this needs to be within the context-window otherwise it will return an error.)
|
|
391
|
-
previous_results = "\n---\n".join(response_list[-self.
|
|
566
|
+
previous_results = "\n---\n".join(response_list[-self.top_chunks:])
|
|
392
567
|
|
|
393
568
|
prompt = (
|
|
394
569
|
"### Context:\n"
|
|
@@ -414,7 +589,7 @@ class LLMlight:
|
|
|
414
589
|
response_list.append(f"Results {i+1}:\n" + chunk_result)
|
|
415
590
|
|
|
416
591
|
# Final summarization pass over all collected summaries
|
|
417
|
-
results_total = "\n---\n".join(response_list[-self.
|
|
592
|
+
results_total = "\n---\n".join(response_list[-self.top_chunks:])
|
|
418
593
|
final_prompt = f"""
|
|
419
594
|
### Context:
|
|
420
595
|
{results_total}
|
|
@@ -441,32 +616,6 @@ class LLMlight:
|
|
|
441
616
|
return final_result
|
|
442
617
|
# return {'summary': final_result, 'summary_per_chunk': results_total}
|
|
443
618
|
|
|
444
|
-
# def query_llm(self, prompt, system=None, task='full', stream=False, return_type='string'):
|
|
445
|
-
# """Calls the LLM and returns the response."""
|
|
446
|
-
# # Set defaults
|
|
447
|
-
# headers = {"Content-Type": "application/json"}
|
|
448
|
-
# # System
|
|
449
|
-
# if system is None: system = "You are a helpful assistant."
|
|
450
|
-
# # Messages
|
|
451
|
-
# messages = [{"role": "system", "content": system}, {"role": "user", "content": prompt}]
|
|
452
|
-
# # Compute tokens
|
|
453
|
-
# used_tokens, max_tokens = compute_tokens(prompt, n_ctx=self.n_ctx, task=task)
|
|
454
|
-
|
|
455
|
-
# data = {
|
|
456
|
-
# "model": self.modelname,
|
|
457
|
-
# "messages": messages,
|
|
458
|
-
# "temperature": self.temperature,
|
|
459
|
-
# "top_p": self.top_p,
|
|
460
|
-
# "stream": stream,
|
|
461
|
-
# "max_tokens": max_tokens,
|
|
462
|
-
# }
|
|
463
|
-
|
|
464
|
-
# # Send POST request
|
|
465
|
-
# response = self.requests_post(headers, data, return_type=return_type)
|
|
466
|
-
|
|
467
|
-
# # Return
|
|
468
|
-
# return response
|
|
469
|
-
|
|
470
619
|
def global_reasoning(self, query, context, instructions, system, return_per_chunk=False, rewrite_query=False, stream=False):
|
|
471
620
|
"""Global Reasoning.
|
|
472
621
|
1. Rewrite the input user question into something like: "Based on the extracted summaries, does the document explain the societal relevance of the research? Justify your answer."
|
|
@@ -488,19 +637,24 @@ class LLMlight:
|
|
|
488
637
|
Only return the new question with no other information.
|
|
489
638
|
"""
|
|
490
639
|
# Initialize model for question refinement and summarization
|
|
491
|
-
qmodel = LLMlight(
|
|
640
|
+
qmodel = LLMlight(model=self.model, temperature=0.7, endpoint=self.endpoint)
|
|
492
641
|
# Create new query
|
|
493
642
|
new_query = qmodel.prompt(query=query, instructions=instructions)
|
|
494
643
|
else:
|
|
495
644
|
new_query = query
|
|
496
645
|
|
|
497
646
|
# Create chunks with overlapping parts to make sure we do not miss out
|
|
498
|
-
|
|
647
|
+
if isinstance(context, str):
|
|
648
|
+
chunks = utils.chunk_text(context, method=self.chunks['method'], chunk_size=self.chunks['size'], overlap=self.chunks['overlap'])
|
|
649
|
+
else:
|
|
650
|
+
chunks = context
|
|
651
|
+
|
|
652
|
+
logger.info(f'Global-reasoning on {len(chunks)} chunks of text.')
|
|
499
653
|
|
|
500
654
|
# Now summaries for the chunks
|
|
501
655
|
summaries = []
|
|
502
|
-
for i, chunk in enumerate(tqdm(chunks, desc="Processing
|
|
503
|
-
logger.info(f'Working on text chunk {i+1}/{len(chunks)}')
|
|
656
|
+
for i, chunk in enumerate(tqdm(chunks, desc="Processing chunk", unit="chunk")):
|
|
657
|
+
# logger.info(f'Working on text chunk {i+1}/{len(chunks)}')
|
|
504
658
|
|
|
505
659
|
prompt = f"""
|
|
506
660
|
### Context (Chunk {i+1} of {len(chunks)} from a larger document):
|
|
@@ -522,7 +676,7 @@ class LLMlight:
|
|
|
522
676
|
"""
|
|
523
677
|
|
|
524
678
|
# Summarize
|
|
525
|
-
response = self.requests_post_http(prompt, system, temperature=self.temperature, top_p=self.top_p, task='summarization', stream=stream
|
|
679
|
+
response = self.requests_post_http(prompt, system, temperature=self.temperature, top_p=self.top_p, task='summarization', stream=stream)
|
|
526
680
|
# Append
|
|
527
681
|
summaries.append(response)
|
|
528
682
|
# Show
|
|
@@ -578,11 +732,16 @@ class LLMlight:
|
|
|
578
732
|
|
|
579
733
|
"""
|
|
580
734
|
# Create chunks with overlapping parts to make sure we do not miss out
|
|
581
|
-
|
|
735
|
+
if isinstance(context, str):
|
|
736
|
+
chunks = utils.chunk_text(context, method=self.chunks['method'], chunk_size=self.chunks['size'], overlap=self.chunks['overlap'], return_type='list')
|
|
737
|
+
else:
|
|
738
|
+
chunks = context
|
|
739
|
+
|
|
740
|
+
logger.info(f'Chunk wise analysis on {len(chunks)} chunks of text.')
|
|
582
741
|
|
|
583
742
|
# Build a structured prompt that includes all previous summaries
|
|
584
743
|
response_list = []
|
|
585
|
-
for i, chunk in enumerate(tqdm(chunks, desc="Processing
|
|
744
|
+
for i, chunk in enumerate(tqdm(chunks, desc="Processing chunk", unit="chunk")):
|
|
586
745
|
logger.info(f'Working on text chunk {i+1}/{len(chunks)}')
|
|
587
746
|
|
|
588
747
|
if top_chunks > 0:
|
|
@@ -701,21 +860,22 @@ class LLMlight:
|
|
|
701
860
|
return final_response
|
|
702
861
|
# return {'response': final_response, 'response_per_chunk': response_total}
|
|
703
862
|
|
|
704
|
-
def
|
|
863
|
+
def search(self, query: str, chunks: list, return_type: str = 'string', top_chunks: int = None, embedding: str = None):
|
|
705
864
|
"""Splits large text into chunks and finds the most relevant ones."""
|
|
706
|
-
# Create chunks
|
|
707
|
-
chunks = utils.chunk_text(context, method=self.chunks['method'], chunk_size=self.chunks['size'], overlap=self.chunks['overlap'])
|
|
708
865
|
# Embedding
|
|
709
|
-
query_vector, chunk_vectors = self.fit_transform(query, chunks)
|
|
866
|
+
query_vector, chunk_vectors = self.fit_transform(query, chunks, embedding=embedding)
|
|
710
867
|
# Compute similarity
|
|
711
|
-
|
|
868
|
+
D = cosine_similarity(query_vector, chunk_vectors)[0]
|
|
712
869
|
# Get top scoring chunks
|
|
713
|
-
if
|
|
714
|
-
|
|
870
|
+
if top_chunks is None:
|
|
871
|
+
top_chunks = len(D)
|
|
872
|
+
logger.info('Number of top chunks selected is set to: {top_chunks}')
|
|
715
873
|
|
|
874
|
+
# Top indices
|
|
875
|
+
top_indices = np.argsort(D)[-top_chunks:][::-1]
|
|
716
876
|
# Join relevant chunks and send as prompt
|
|
717
877
|
relevant_chunks = [chunks[i] for i in top_indices]
|
|
718
|
-
relevant_scores = [
|
|
878
|
+
relevant_scores = [D[i] for i in top_indices]
|
|
719
879
|
|
|
720
880
|
# Set the return type
|
|
721
881
|
if return_type == 'score':
|
|
@@ -727,26 +887,40 @@ class LLMlight:
|
|
|
727
887
|
else:
|
|
728
888
|
return "\n---------\n".join(relevant_chunks)
|
|
729
889
|
|
|
730
|
-
def fit_transform(self, query, chunks):
|
|
890
|
+
def fit_transform(self, query, chunks, embedding=None):
|
|
731
891
|
"""Converts context chunks and query into vector space representations based on the selected embedding method."""
|
|
732
|
-
if
|
|
892
|
+
if embedding is None: embedding = self.embedding
|
|
893
|
+
|
|
894
|
+
# Set embedding model parameters.
|
|
895
|
+
if self.embedding['context'] == 'bert':
|
|
896
|
+
embedding_model = SentenceTransformer('all-MiniLM-L6-v2')
|
|
897
|
+
elif self.embedding['context'] == 'bge-small':
|
|
898
|
+
embedding_model = SentenceTransformer('BAAI/bge-small-en')
|
|
899
|
+
else:
|
|
900
|
+
embedding_model = None
|
|
901
|
+
|
|
902
|
+
if embedding == 'tfidf':
|
|
733
903
|
vectorizer = TfidfVectorizer()
|
|
734
904
|
chunk_vectors = vectorizer.fit_transform(chunks)
|
|
735
905
|
# dense_matrix = chunk_vectors.toarray() # Converts to a NumPy array
|
|
736
906
|
query_vector = vectorizer.transform([query])
|
|
737
|
-
elif
|
|
907
|
+
elif embedding == 'bow':
|
|
738
908
|
vectorizer = CountVectorizer()
|
|
739
909
|
chunk_vectors = vectorizer.fit_transform(chunks)
|
|
740
910
|
query_vector = vectorizer.transform([query])
|
|
741
|
-
# elif
|
|
742
|
-
elif
|
|
743
|
-
chunk_vectors = np.vstack([
|
|
744
|
-
query_vector =
|
|
911
|
+
# elif embedding_model is not None:
|
|
912
|
+
elif embedding == 'bert' or embedding == 'bge-small':
|
|
913
|
+
chunk_vectors = np.vstack([embedding_model.encode(chunk) for chunk in chunks])
|
|
914
|
+
query_vector = embedding_model.encode([query])
|
|
745
915
|
query_vector = query_vector.reshape(1, -1)
|
|
916
|
+
elif embedding == 'memvid':
|
|
917
|
+
logger.warning(f'Embedding method [{embedding}] can only be applied when retrieval method is set to memory-video path.')
|
|
746
918
|
else:
|
|
747
|
-
|
|
919
|
+
logger.error(f'Available embedding methods: {get_embeddings()}')
|
|
920
|
+
raise ValueError(f"Unsupported embedding method: {self.embedding}")
|
|
921
|
+
# Return
|
|
748
922
|
return query_vector, chunk_vectors
|
|
749
|
-
|
|
923
|
+
|
|
750
924
|
def compute_preprocessing(self, query, context, instructions, system):
|
|
751
925
|
# Create advanced prompt using relevant chunks of text, the input query and instructions
|
|
752
926
|
if context is not None:
|
|
@@ -757,7 +931,7 @@ class LLMlight:
|
|
|
757
931
|
# Analyze per chunk
|
|
758
932
|
relevant_context = self.chunk_wise(query, context, instructions, system, top_chunks=0, return_per_chunk=True)
|
|
759
933
|
else:
|
|
760
|
-
logger.info(f'No method is applied
|
|
934
|
+
logger.info(f'No preprocessing method is applied.')
|
|
761
935
|
relevant_context = context
|
|
762
936
|
else:
|
|
763
937
|
# Default
|
|
@@ -766,40 +940,107 @@ class LLMlight:
|
|
|
766
940
|
# Return
|
|
767
941
|
return relevant_context
|
|
768
942
|
|
|
769
|
-
def
|
|
770
|
-
|
|
943
|
+
def relevant_memory_retrieval(self, query: str, return_type='list'):
|
|
944
|
+
relevant_context = None
|
|
945
|
+
|
|
946
|
+
# Show warning if chunks are not processed yet
|
|
947
|
+
if hasattr(self, 'encoder') and len(self.encoder.chunks) > 0:
|
|
948
|
+
logger.warning('Documents are stored in the encoder but not saved into video memory! Use save first: client.memory_save() to include the information.')
|
|
949
|
+
|
|
950
|
+
# Retrieve context from video memory
|
|
951
|
+
if (self.memory.file_path is not None) and os.path.isfile(self.memory.file_path) and os.path.isfile(self.memory.index_path):
|
|
952
|
+
logger.info(f"Initialize retrieval from memory.. Collect [{self.top_chunks}] chunks from video-memory using {self.embedding['memory']}.")
|
|
953
|
+
# Initialize retriever
|
|
954
|
+
# retriever = self.memory.MemvidRetriever(video_file=self.memory.file_path, index_file=self.memory['index_path'], config=self.memory.config)
|
|
955
|
+
self.memory_load()
|
|
956
|
+
|
|
957
|
+
# Retrieval based on embedding
|
|
958
|
+
if self.embedding['memory']=='memvid':
|
|
959
|
+
# Use the memvid search retriever
|
|
960
|
+
# relevant_context = self.memory.retriever.search(query, top_k=self.top_chunks)
|
|
961
|
+
results = self.memory.retriever.index_manager.search(query, top_k=self.top_chunks)
|
|
962
|
+
scores = np.array(list(map(lambda x: x[1], results)))
|
|
963
|
+
relevant_context = list(map(lambda x: x[2]['text'], results))
|
|
964
|
+
elif self.embedding['memory'] in get_embeddings():
|
|
965
|
+
# Use the classic retrievers
|
|
966
|
+
chunks = list(map(lambda x: x.get('text'), self.memory.retriever.index_manager.metadata))
|
|
967
|
+
# Compute distances and get top k chunks
|
|
968
|
+
results = self.search(query, chunks, top_chunks=self.top_chunks, embedding=self.embedding['memory'], return_type='score')
|
|
969
|
+
scores, relevant_context = map(list, zip(*results))
|
|
970
|
+
|
|
971
|
+
# Filter on Probability
|
|
972
|
+
relevant_context = self._filter_proba(query, scores, relevant_context)
|
|
973
|
+
|
|
974
|
+
if return_type=='string':
|
|
975
|
+
# Join the chunks in context
|
|
976
|
+
relevant_context = "\n\n---\n\n".join([f"### Chunk {i+1}:\n{s}" for i, s in enumerate(relevant_context)])
|
|
977
|
+
|
|
978
|
+
# Return
|
|
979
|
+
return relevant_context
|
|
980
|
+
|
|
981
|
+
def relevant_context_retrieval(self, query, context: str, return_type='list'):
|
|
982
|
+
# Get context
|
|
983
|
+
# context = self._get_context(context)
|
|
984
|
+
|
|
771
985
|
if context is not None:
|
|
772
|
-
|
|
986
|
+
# Get relevant context using RAG and embedding
|
|
987
|
+
if self.retrieval_method == 'RAG_basic' and self.embedding['context'] in get_embeddings():
|
|
773
988
|
# Find the best matching parts using simple retrieval method approach.
|
|
774
|
-
logger.info(f
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
989
|
+
logger.info(f"Initialize retrieval from context.. Collect [{self.top_chunks}] chunks from context using {self.embedding['context']}.")
|
|
990
|
+
# Create chunks
|
|
991
|
+
chunks = utils.chunk_text(context, method=self.chunks['method'], chunk_size=self.chunks['size'], overlap=self.chunks['overlap'])
|
|
992
|
+
# Compute distances and get top k chunks
|
|
993
|
+
results = self.search(query, chunks, top_chunks=self.top_chunks, embedding=self.embedding['context'], return_type='score')
|
|
994
|
+
# Unzip scores and context
|
|
995
|
+
scores, relevant_context = map(list, zip(*results))
|
|
996
|
+
# Filter on Probability
|
|
997
|
+
relevant_context = self._filter_proba(query, scores, relevant_context)
|
|
998
|
+
|
|
999
|
+
elif self.retrieval_method == 'RSE' and np.isin(self.embedding['context'], ['bert', 'bge-small']):
|
|
1000
|
+
logger.info(f'RAG approach [{self.retrieval_method}] is applied.')
|
|
1001
|
+
relevant_context = RAG.RSE(context, query, label=None, chunk_size=self.chunks['size'], irrelevant_chunk_penalty=0, embedding=self.embedding['context'], device='cpu', batch_size=32)
|
|
779
1002
|
else:
|
|
780
|
-
logger.info(f'No method is applied
|
|
1003
|
+
logger.info(f'No retrieval method is applied.')
|
|
781
1004
|
relevant_context = context
|
|
782
1005
|
else:
|
|
783
|
-
# Default
|
|
784
1006
|
relevant_context = context
|
|
785
1007
|
|
|
786
1008
|
# Return
|
|
787
1009
|
return relevant_context
|
|
788
1010
|
|
|
789
|
-
|
|
1011
|
+
# Compute probability
|
|
1012
|
+
def _filter_proba(self, query, scores, relevant_context):
|
|
1013
|
+
# Filter on significance
|
|
1014
|
+
if self.alpha is not None:
|
|
1015
|
+
logger.info(f'Computing probability distribution..')
|
|
1016
|
+
# Compute probability
|
|
1017
|
+
out = self.compute_probability(query, scores, embedding=self.embedding['memory'], n=1000)
|
|
1018
|
+
# Only keep significant chunks
|
|
1019
|
+
relevant_context = list(np.array(relevant_context)[out['y_bool']])
|
|
1020
|
+
logger.info(f'{len(relevant_context)} significant chunks found with alpha<={self.alpha}')
|
|
1021
|
+
|
|
1022
|
+
# Return relevant context
|
|
1023
|
+
return relevant_context
|
|
1024
|
+
|
|
1025
|
+
def set_prompt(self, query: str, instructions: str, context: (str, list), response_format: str = None):
|
|
790
1026
|
# Default and update when context and instructions are available.
|
|
1027
|
+
if isinstance(context, list):
|
|
1028
|
+
context = "\n\n---\n\n".join([f"### Chunk {i+1}:\n{s}" for i, s in enumerate(context)])
|
|
1029
|
+
if context=='':
|
|
1030
|
+
logger.warning('No context is provided into the prompt.')
|
|
1031
|
+
|
|
791
1032
|
prompt = (
|
|
792
1033
|
("Context:\n" + context + "\n\n" if context else "")
|
|
793
|
-
+ (
|
|
794
|
-
+ (
|
|
795
|
-
+
|
|
1034
|
+
+ ("Instructions:\n" + instructions + "\n\n" if instructions not in ("", None) else "")
|
|
1035
|
+
+ ("Response format:\n" + response_format + "\n\n" if response_format not in ("", None) else "")
|
|
1036
|
+
+ "User question:\n"
|
|
796
1037
|
+ query
|
|
797
|
-
|
|
1038
|
+
)
|
|
798
1039
|
|
|
799
1040
|
# Return
|
|
800
1041
|
return prompt
|
|
801
1042
|
|
|
802
|
-
def read_pdf(self,
|
|
1043
|
+
def read_pdf(self, file_path, title_pages=[1, 2], body_pages=[], reference_pages=[-1], return_type='dict'):
|
|
803
1044
|
"""
|
|
804
1045
|
Reads a PDF file and extracts its text content as a string.
|
|
805
1046
|
|
|
@@ -810,26 +1051,127 @@ class LLMlight:
|
|
|
810
1051
|
str: Extracted text from the PDF.
|
|
811
1052
|
|
|
812
1053
|
"""
|
|
813
|
-
if os.path.isfile(
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
if return_type=='dict':
|
|
819
|
-
|
|
820
|
-
|
|
1054
|
+
if os.path.isfile(file_path):
|
|
1055
|
+
# Read pdf
|
|
1056
|
+
context = utils.read_pdf(file_path, title_pages=title_pages, body_pages=body_pages, reference_pages=reference_pages, return_type=return_type)
|
|
1057
|
+
# Return
|
|
1058
|
+
return context
|
|
1059
|
+
# if return_type=='dict':
|
|
1060
|
+
# counts = utils.count_words(self.context['body'])
|
|
1061
|
+
# self.context['body'] = self.context['body']
|
|
1062
|
+
else:
|
|
1063
|
+
logger.error(f'file_path does not exist: {file_path}')
|
|
1064
|
+
|
|
1065
|
+
def get_available_models(self, validate=False):
|
|
1066
|
+
"""Retrieve available models from the configured API endpoint.
|
|
1067
|
+
|
|
1068
|
+
Optionally validates each model by sending a test prompt and filtering out
|
|
1069
|
+
models that return a 404 error or similar failure response.
|
|
1070
|
+
|
|
1071
|
+
Parameters
|
|
1072
|
+
----------
|
|
1073
|
+
validate : bool, optional
|
|
1074
|
+
If True, each model is tested with a prompt to ensure it can respond correctly.
|
|
1075
|
+
Models that fail validation (e.g., return a 404 error) are excluded from the result.
|
|
1076
|
+
|
|
1077
|
+
Returns
|
|
1078
|
+
-------
|
|
1079
|
+
list of str
|
|
1080
|
+
A list of model identifiers (e.g., `"llama3"`, `"gpt-4"`) that are available and valid.
|
|
1081
|
+
|
|
1082
|
+
Examples
|
|
1083
|
+
--------
|
|
1084
|
+
>>> # Import library
|
|
1085
|
+
>>> from LLMlight import LLMlight
|
|
1086
|
+
<<< # Initialize
|
|
1087
|
+
>>> client = LLMlight(endpoint='http://localhost:1234/v1/chat/completions', verbose='info')
|
|
1088
|
+
>>> # Get models
|
|
1089
|
+
>>> models = client.get_available_models(validate=False)
|
|
1090
|
+
>>> # Print
|
|
1091
|
+
>>> print(models)
|
|
1092
|
+
>>> ['llama3', 'mistral-7b']
|
|
1093
|
+
|
|
1094
|
+
Notes
|
|
1095
|
+
-----
|
|
1096
|
+
- Requires an accessible endpoint and valid API response.
|
|
1097
|
+
- Relies on the `LLMlight` class for validation (must be importable).
|
|
1098
|
+
"""
|
|
1099
|
+
base_url = '/'.join(self.endpoint.split('/')[:3]) + '/'
|
|
1100
|
+
logger.info('Collecting models in the API endpoint..')
|
|
1101
|
+
|
|
1102
|
+
try:
|
|
1103
|
+
model_url = base_url.rstrip('/') + '/v1/models'
|
|
1104
|
+
logger.info(f'Looking up models: {model_url}')
|
|
1105
|
+
response = requests.get(model_url, timeout=10)
|
|
1106
|
+
if response.status_code == 200:
|
|
1107
|
+
try:
|
|
1108
|
+
models = response.json()["data"]
|
|
1109
|
+
model_dict = {model["id"]: model for model in models}
|
|
1110
|
+
except (KeyError, ValueError) as e:
|
|
1111
|
+
logger.error("Error parsing model data:", e)
|
|
1112
|
+
else:
|
|
1113
|
+
logger.warning("Request failed with status code:", response.status_code)
|
|
1114
|
+
logger.warning("Response:", response.text)
|
|
1115
|
+
|
|
1116
|
+
except requests.exceptions.RequestException as e:
|
|
1117
|
+
logger.error("Request error:", e)
|
|
1118
|
+
|
|
1119
|
+
# Check each model whether it returns a response
|
|
1120
|
+
if validate:
|
|
1121
|
+
logger.info("Validating the working of each available model. Be patient.")
|
|
1122
|
+
keys = copy.deepcopy(list(model_dict.keys()))
|
|
1123
|
+
|
|
1124
|
+
for key in keys:
|
|
1125
|
+
# logger.info(f'Checking: {key}')
|
|
1126
|
+
from LLMlight import LLMlight
|
|
1127
|
+
llm = LLMlight(model=key)
|
|
1128
|
+
response = llm.prompt('What is the capital of France?', instructions="You are only allowed to return one word.", return_type='string')
|
|
1129
|
+
response = response[0:30].replace('\n', ' ').replace('\r', ' ').lower()
|
|
1130
|
+
if 'error: 404' in response:
|
|
1131
|
+
logger.error(f"{llm.model}: {response}")
|
|
1132
|
+
model_dict.pop(key)
|
|
1133
|
+
else:
|
|
1134
|
+
logger.debug(f"{llm.model}: {response}")
|
|
1135
|
+
|
|
1136
|
+
return list(model_dict.keys())
|
|
1137
|
+
|
|
1138
|
+
def check_logger(self):
|
|
1139
|
+
"""Check the verbosity."""
|
|
1140
|
+
logger.debug('DEBUG')
|
|
1141
|
+
logger.info('INFO')
|
|
1142
|
+
logger.warning('WARNING')
|
|
1143
|
+
logger.critical('CRITICAL')
|
|
1144
|
+
|
|
1145
|
+
#%%
|
|
1146
|
+
def get_embeddings():
|
|
1147
|
+
return ['tfidf', 'bow', 'bert', 'bge-small', 'memvid']
|
|
1148
|
+
|
|
1149
|
+
def _set_embedding(embedding):
|
|
1150
|
+
if embedding is None: embedding = {}
|
|
1151
|
+
if isinstance(embedding, str):
|
|
1152
|
+
if embedding == 'automatic':
|
|
1153
|
+
embedding = {'memory': 'memvid', 'context': 'bert'}
|
|
1154
|
+
elif embedding in get_embeddings():
|
|
1155
|
+
embedding = {'memory': embedding, 'context': embedding}
|
|
821
1156
|
else:
|
|
822
|
-
|
|
823
|
-
|
|
1157
|
+
embedding = {'memory': 'memvid', 'context': 'bert'}
|
|
1158
|
+
|
|
1159
|
+
if embedding.get('context') == 'memvid':
|
|
1160
|
+
embedding['context'] = 'tfidf'
|
|
824
1161
|
|
|
1162
|
+
embedding = {**{'memory': 'memvid', 'context': 'tfidf'}, **embedding}
|
|
825
1163
|
|
|
826
|
-
|
|
1164
|
+
# Return
|
|
1165
|
+
return embedding
|
|
1166
|
+
|
|
1167
|
+
#%%
|
|
1168
|
+
def convert_messages_to_model(messages, model='llama', add_assistant_start=True):
|
|
827
1169
|
"""
|
|
828
1170
|
Builds a prompt in the appropriate format for different models (LLaMA, Grok, Mistral).
|
|
829
1171
|
|
|
830
1172
|
Args:
|
|
831
1173
|
messages (list of dict): Each dict must have 'role' ('system', 'user', 'assistant') and 'content'.
|
|
832
|
-
|
|
1174
|
+
model (str): The type of model to generate the prompt for ('llama', 'grok', or 'mistral').
|
|
833
1175
|
add_assistant_start (bool): Whether to add the assistant start (default True).
|
|
834
1176
|
add_bos_token (bool): Helps models know it's a fresh conversation. Useful for llama/mistral/hermes-style models
|
|
835
1177
|
|
|
@@ -841,31 +1183,31 @@ def convert_prompt(messages, modelname='llama', add_assistant_start=True):
|
|
|
841
1183
|
... {"role": "system", "content": "You are a helpful assistant."},
|
|
842
1184
|
... {"role": "user", "content": "What is the capital of France?"}
|
|
843
1185
|
... ]
|
|
844
|
-
>>> prompt =
|
|
1186
|
+
>>> prompt = convert_messages_to_model(messages, model='llama')
|
|
845
1187
|
>>> print(prompt)
|
|
846
1188
|
|
|
847
1189
|
"""
|
|
848
1190
|
prompt = ""
|
|
849
1191
|
|
|
850
|
-
# if add_bos_token and ('llama' in
|
|
1192
|
+
# if add_bos_token and ('llama' in model or 'mistral' in model):
|
|
851
1193
|
# prompt += "<|begin_of_text|>\n"
|
|
852
1194
|
|
|
853
1195
|
for msg in messages:
|
|
854
1196
|
role = msg["role"]
|
|
855
1197
|
content = msg["content"].strip()
|
|
856
1198
|
|
|
857
|
-
if 'llama' in
|
|
1199
|
+
if 'llama' in model or 'mistral' in model:
|
|
858
1200
|
prompt += f"<|im_start|>{role}\n{content}\n<|im_end|>\n"
|
|
859
|
-
elif 'grok' in
|
|
1201
|
+
elif 'grok' in model:
|
|
860
1202
|
prompt += f"<start_of_turn>{role}\n{content}<end_of_turn>\n"
|
|
861
1203
|
else:
|
|
862
1204
|
# Default to ChatML format if model not recognized
|
|
863
1205
|
prompt += f"<|im_start|>{role}\n{content}\n<|im_end|>\n"
|
|
864
1206
|
|
|
865
1207
|
if add_assistant_start:
|
|
866
|
-
if 'llama' in
|
|
1208
|
+
if 'llama' in model or 'mistral' in model:
|
|
867
1209
|
prompt += "<|im_start|>assistant\n"
|
|
868
|
-
elif 'grok' in
|
|
1210
|
+
elif 'grok' in model:
|
|
869
1211
|
prompt += "<start_of_turn>assistant\n"
|
|
870
1212
|
|
|
871
1213
|
return prompt
|
|
@@ -921,7 +1263,9 @@ def compute_tokens(string, n_ctx=4096, task='full'):
|
|
|
921
1263
|
used_tokens = len(tokens)
|
|
922
1264
|
# Determine how many tokens are available for the model to generate
|
|
923
1265
|
max_tokens = compute_max_tokens(used_tokens, n_ctx=n_ctx, task=task)
|
|
924
|
-
|
|
1266
|
+
# Show message
|
|
1267
|
+
logger.debug(f"Used_tokens={used_tokens}, max_tokens={max_tokens}, context_limit={n_ctx}")
|
|
1268
|
+
# Return
|
|
925
1269
|
return used_tokens, max_tokens
|
|
926
1270
|
|
|
927
1271
|
|
|
@@ -960,8 +1304,19 @@ def compute_max_tokens(used_tokens, n_ctx=4096, task="full"):
|
|
|
960
1304
|
|
|
961
1305
|
|
|
962
1306
|
def set_system_message(system):
|
|
963
|
-
|
|
1307
|
+
if system is None:
|
|
1308
|
+
system = """You are a helpful AI assistant with access to a knowledge base.
|
|
1309
|
+
|
|
1310
|
+
When answering questions:
|
|
1311
|
+
1. Use the provided context from the knowledge base when relevant
|
|
1312
|
+
2. When multiple sections are in the context; ### chunk 1:, ### chunk 2: or ### summary 1:, ### summary 2: etc, then the higher ranked chunks contain more relevant information.
|
|
1313
|
+
3. Be clear about what information comes from the knowledge base vs. your general knowledge
|
|
1314
|
+
4. If the context doesn't contain enough information, say so clearly
|
|
1315
|
+
5. Provide helpful, accurate, and concise responses
|
|
964
1316
|
|
|
1317
|
+
The context will be provided with each query based on semantic similarity to the user's question."""
|
|
1318
|
+
|
|
1319
|
+
return system
|
|
965
1320
|
|
|
966
1321
|
|
|
967
1322
|
# %%
|
|
@@ -1047,11 +1402,3 @@ def disable_tqdm():
|
|
|
1047
1402
|
"""Set the logger for verbosity messages."""
|
|
1048
1403
|
return (True if (logger.getEffectiveLevel()>=30) else False)
|
|
1049
1404
|
|
|
1050
|
-
|
|
1051
|
-
def check_logger(verbose: [str, int] = 'info'):
|
|
1052
|
-
"""Check the logger."""
|
|
1053
|
-
set_logger(verbose)
|
|
1054
|
-
logger.debug('DEBUG')
|
|
1055
|
-
logger.info('INFO')
|
|
1056
|
-
logger.warning('WARNING')
|
|
1057
|
-
logger.critical('CRITICAL')
|