LLMlight 0.2.0__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.0 → llmlight-0.3.0}/LLMlight/LLMlight.py +569 -224
- {llmlight-0.2.0 → 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.0 → llmlight-0.3.0}/LLMlight/utils.py +5 -5
- {llmlight-0.2.0 → llmlight-0.3.0}/LLMlight.egg-info/PKG-INFO +116 -29
- {llmlight-0.2.0 → llmlight-0.3.0}/LLMlight.egg-info/SOURCES.txt +2 -0
- {llmlight-0.2.0 → llmlight-0.3.0}/LLMlight.egg-info/requires.txt +2 -0
- {llmlight-0.2.0 → llmlight-0.3.0}/PKG-INFO +116 -29
- llmlight-0.3.0/README.md +250 -0
- {llmlight-0.2.0 → llmlight-0.3.0}/pyproject.toml +2 -0
- llmlight-0.2.0/LLMlight/__init__.py +0 -48
- llmlight-0.2.0/LLMlight/examples.py +0 -277
- llmlight-0.2.0/README.md +0 -165
- {llmlight-0.2.0 → llmlight-0.3.0}/LICENSE +0 -0
- {llmlight-0.2.0 → llmlight-0.3.0}/LLMlight.egg-info/dependency_links.txt +0 -0
- {llmlight-0.2.0 → llmlight-0.3.0}/LLMlight.egg-info/top_level.txt +0 -0
- {llmlight-0.2.0 → llmlight-0.3.0}/MANIFEST.in +0 -0
- {llmlight-0.2.0 → 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,114 +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"
|
|
147
|
-
base_url = self.endpoint.split("/chat")[0]
|
|
148
|
-
logger.info('Collecting models in the API endpoint..')
|
|
149
|
-
|
|
150
|
-
# Query available models
|
|
151
|
-
response = requests.get(f"{base_url}/models")
|
|
152
|
-
model_dict = {}
|
|
153
|
-
|
|
154
|
-
try:
|
|
155
|
-
response = requests.get("http://localhost:1234/v1/models", timeout=10)
|
|
156
|
-
if response.status_code == 200:
|
|
157
|
-
try:
|
|
158
|
-
models = response.json()["data"]
|
|
159
|
-
model_dict = {model["id"]: model for model in models}
|
|
160
|
-
except (KeyError, ValueError) as e:
|
|
161
|
-
logger.error("Error parsing model data:", e)
|
|
162
|
-
else:
|
|
163
|
-
logger.warning("Request failed with status code:", response.status_code)
|
|
164
|
-
logger.warning("Response:", response.text)
|
|
165
|
-
|
|
166
|
-
except requests.exceptions.RequestException as e:
|
|
167
|
-
logger.error("Request error:", e)
|
|
199
|
+
# Set chunk parameters
|
|
200
|
+
if chunks is None: chunks = {}
|
|
201
|
+
self.chunks = {**{'method': 'chars', 'size': 1000, 'overlap': 250}, **chunks}
|
|
168
202
|
|
|
169
|
-
#
|
|
170
|
-
|
|
171
|
-
logger.info("Validating models:")
|
|
172
|
-
keys = copy.deepcopy(list(model_dict.keys()))
|
|
203
|
+
# Set Memory parameters.
|
|
204
|
+
self.memory_init(retrieval_method)
|
|
173
205
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
response = llm.prompt('What is the capital of France?', system="You are a helpful assistant.", return_type='string')
|
|
178
|
-
response = response[0:30].replace('\n', ' ').replace('\r', ' ').lower()
|
|
179
|
-
if 'error: 404' in response:
|
|
180
|
-
logger.error(f"{llm.modelname}: {response}")
|
|
181
|
-
model_dict.pop(key)
|
|
182
|
-
else:
|
|
183
|
-
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)
|
|
184
209
|
|
|
185
|
-
|
|
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"}')
|
|
186
215
|
|
|
187
216
|
def prompt(self,
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
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
|
+
|
|
200
230
|
The final prompt is created based on the query, instructions, and the context
|
|
201
231
|
|
|
202
232
|
Parameters
|
|
@@ -225,48 +255,70 @@ class LLMlight:
|
|
|
225
255
|
'string': Return only the string answer (remove thinking strings using tags: <think> </think>).
|
|
226
256
|
'string_with_thinking' Return the full response which includes the thinking proces (if available).
|
|
227
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
|
+
|
|
228
268
|
Returns
|
|
229
269
|
-------
|
|
230
270
|
str
|
|
231
271
|
The model's response or an error message if the request fails.
|
|
232
272
|
"""
|
|
233
|
-
|
|
234
|
-
|
|
273
|
+
if verbose is not None: set_logger(verbose)
|
|
274
|
+
logger.info(f'Creating response with {self.model}..')
|
|
235
275
|
|
|
276
|
+
if context is None: context = self.context
|
|
236
277
|
if temperature is None: temperature = self.temperature
|
|
237
278
|
if top_p is None: top_p = self.top_p
|
|
238
|
-
if context is None: context = self.context
|
|
239
|
-
# if embedding is not None: self.embedding = embedding
|
|
240
|
-
if isinstance(context, dict): context = '\n\n'.join(context.values())
|
|
241
|
-
|
|
242
|
-
# task (str): The use case for generation. (default: 'full')
|
|
243
|
-
# 'summarization'
|
|
244
|
-
# 'chat'
|
|
245
|
-
# 'code'
|
|
246
|
-
# 'longform'
|
|
247
|
-
# 'full'
|
|
248
|
-
# task = set_task(self.preprocessing, self.method)
|
|
249
279
|
self.task = 'full'
|
|
250
280
|
|
|
251
281
|
# Set system message
|
|
252
282
|
system = set_system_message(system)
|
|
253
|
-
|
|
254
|
-
|
|
283
|
+
|
|
284
|
+
# Extract relevant text for video memory
|
|
285
|
+
relevant_memory = self.relevant_memory_retrieval(query, return_type='list')
|
|
286
|
+
|
|
255
287
|
# Extract relevant text using retrieval method
|
|
256
|
-
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
|
+
|
|
257
296
|
# Set the prompt
|
|
258
|
-
|
|
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..')
|
|
259
302
|
|
|
260
303
|
# Run model
|
|
261
304
|
if os.path.isfile(self.endpoint):
|
|
262
305
|
# Run LLM from gguf model
|
|
263
|
-
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)
|
|
264
307
|
else:
|
|
265
308
|
# Run LLM with http model
|
|
266
|
-
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
|
+
|
|
267
311
|
# Return
|
|
268
312
|
return response
|
|
269
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
|
+
|
|
270
322
|
def requests_post_gguf(self, prompt, system, temperature=0.8, top_p=1, headers=None, task='full', stream=False, return_type='string'):
|
|
271
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.
|
|
272
324
|
# Prepare data for request.
|
|
@@ -274,7 +326,7 @@ class LLMlight:
|
|
|
274
326
|
# Prepare messages
|
|
275
327
|
messages = [{"role": "system", "content": system}, {"role": "user", "content": prompt}]
|
|
276
328
|
# Convert messages to string prompt
|
|
277
|
-
prompt =
|
|
329
|
+
prompt = convert_messages_to_model(messages, model=self.model)
|
|
278
330
|
# Compute tokens
|
|
279
331
|
used_tokens, max_tokens = compute_tokens(prompt, n_ctx=self.n_ctx, task=task)
|
|
280
332
|
|
|
@@ -297,19 +349,24 @@ class LLMlight:
|
|
|
297
349
|
# Return
|
|
298
350
|
return response
|
|
299
351
|
|
|
300
|
-
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):
|
|
301
353
|
# Prepare data for request.
|
|
302
354
|
if headers is None: headers = {"Content-Type": "application/json"}
|
|
303
355
|
# Prepare messages
|
|
304
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
|
+
|
|
305
361
|
# Create full prompt
|
|
306
362
|
prompt = messages[0]['content'] + messages[1]['content']
|
|
307
363
|
# Compute tokens
|
|
308
|
-
|
|
309
|
-
|
|
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}')
|
|
310
367
|
|
|
311
368
|
data = {
|
|
312
|
-
"model": self.
|
|
369
|
+
"model": self.model,
|
|
313
370
|
"messages": messages,
|
|
314
371
|
"temperature": temperature,
|
|
315
372
|
"top_p": top_p,
|
|
@@ -350,6 +407,122 @@ class LLMlight:
|
|
|
350
407
|
logger.error(f"{response.status_code} - {response}")
|
|
351
408
|
return f"Error: {response.status_code} - {response}"
|
|
352
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
|
+
|
|
353
526
|
def task(self,
|
|
354
527
|
query="Extract key insights while maintaining coherence of the previous summaries.",
|
|
355
528
|
instructions="",
|
|
@@ -390,7 +563,7 @@ class LLMlight:
|
|
|
390
563
|
logger.info(f'Working on text chunk {i}/{len(chunks)}')
|
|
391
564
|
|
|
392
565
|
# Keep last N summaries for context (this needs to be within the context-window otherwise it will return an error.)
|
|
393
|
-
previous_results = "\n---\n".join(response_list[-self.
|
|
566
|
+
previous_results = "\n---\n".join(response_list[-self.top_chunks:])
|
|
394
567
|
|
|
395
568
|
prompt = (
|
|
396
569
|
"### Context:\n"
|
|
@@ -416,7 +589,7 @@ class LLMlight:
|
|
|
416
589
|
response_list.append(f"Results {i+1}:\n" + chunk_result)
|
|
417
590
|
|
|
418
591
|
# Final summarization pass over all collected summaries
|
|
419
|
-
results_total = "\n---\n".join(response_list[-self.
|
|
592
|
+
results_total = "\n---\n".join(response_list[-self.top_chunks:])
|
|
420
593
|
final_prompt = f"""
|
|
421
594
|
### Context:
|
|
422
595
|
{results_total}
|
|
@@ -443,32 +616,6 @@ class LLMlight:
|
|
|
443
616
|
return final_result
|
|
444
617
|
# return {'summary': final_result, 'summary_per_chunk': results_total}
|
|
445
618
|
|
|
446
|
-
# def query_llm(self, prompt, system=None, task='full', stream=False, return_type='string'):
|
|
447
|
-
# """Calls the LLM and returns the response."""
|
|
448
|
-
# # Set defaults
|
|
449
|
-
# headers = {"Content-Type": "application/json"}
|
|
450
|
-
# # System
|
|
451
|
-
# if system is None: system = "You are a helpful assistant."
|
|
452
|
-
# # Messages
|
|
453
|
-
# messages = [{"role": "system", "content": system}, {"role": "user", "content": prompt}]
|
|
454
|
-
# # Compute tokens
|
|
455
|
-
# used_tokens, max_tokens = compute_tokens(prompt, n_ctx=self.n_ctx, task=task)
|
|
456
|
-
|
|
457
|
-
# data = {
|
|
458
|
-
# "model": self.modelname,
|
|
459
|
-
# "messages": messages,
|
|
460
|
-
# "temperature": self.temperature,
|
|
461
|
-
# "top_p": self.top_p,
|
|
462
|
-
# "stream": stream,
|
|
463
|
-
# "max_tokens": max_tokens,
|
|
464
|
-
# }
|
|
465
|
-
|
|
466
|
-
# # Send POST request
|
|
467
|
-
# response = self.requests_post(headers, data, return_type=return_type)
|
|
468
|
-
|
|
469
|
-
# # Return
|
|
470
|
-
# return response
|
|
471
|
-
|
|
472
619
|
def global_reasoning(self, query, context, instructions, system, return_per_chunk=False, rewrite_query=False, stream=False):
|
|
473
620
|
"""Global Reasoning.
|
|
474
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."
|
|
@@ -490,19 +637,24 @@ class LLMlight:
|
|
|
490
637
|
Only return the new question with no other information.
|
|
491
638
|
"""
|
|
492
639
|
# Initialize model for question refinement and summarization
|
|
493
|
-
qmodel = LLMlight(
|
|
640
|
+
qmodel = LLMlight(model=self.model, temperature=0.7, endpoint=self.endpoint)
|
|
494
641
|
# Create new query
|
|
495
642
|
new_query = qmodel.prompt(query=query, instructions=instructions)
|
|
496
643
|
else:
|
|
497
644
|
new_query = query
|
|
498
645
|
|
|
499
646
|
# Create chunks with overlapping parts to make sure we do not miss out
|
|
500
|
-
|
|
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.')
|
|
501
653
|
|
|
502
654
|
# Now summaries for the chunks
|
|
503
655
|
summaries = []
|
|
504
|
-
for i, chunk in enumerate(tqdm(chunks, desc="Processing
|
|
505
|
-
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)}')
|
|
506
658
|
|
|
507
659
|
prompt = f"""
|
|
508
660
|
### Context (Chunk {i+1} of {len(chunks)} from a larger document):
|
|
@@ -524,7 +676,7 @@ class LLMlight:
|
|
|
524
676
|
"""
|
|
525
677
|
|
|
526
678
|
# Summarize
|
|
527
|
-
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)
|
|
528
680
|
# Append
|
|
529
681
|
summaries.append(response)
|
|
530
682
|
# Show
|
|
@@ -580,11 +732,16 @@ class LLMlight:
|
|
|
580
732
|
|
|
581
733
|
"""
|
|
582
734
|
# Create chunks with overlapping parts to make sure we do not miss out
|
|
583
|
-
|
|
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.')
|
|
584
741
|
|
|
585
742
|
# Build a structured prompt that includes all previous summaries
|
|
586
743
|
response_list = []
|
|
587
|
-
for i, chunk in enumerate(tqdm(chunks, desc="Processing
|
|
744
|
+
for i, chunk in enumerate(tqdm(chunks, desc="Processing chunk", unit="chunk")):
|
|
588
745
|
logger.info(f'Working on text chunk {i+1}/{len(chunks)}')
|
|
589
746
|
|
|
590
747
|
if top_chunks > 0:
|
|
@@ -703,21 +860,22 @@ class LLMlight:
|
|
|
703
860
|
return final_response
|
|
704
861
|
# return {'response': final_response, 'response_per_chunk': response_total}
|
|
705
862
|
|
|
706
|
-
def
|
|
863
|
+
def search(self, query: str, chunks: list, return_type: str = 'string', top_chunks: int = None, embedding: str = None):
|
|
707
864
|
"""Splits large text into chunks and finds the most relevant ones."""
|
|
708
|
-
# Create chunks
|
|
709
|
-
chunks = utils.chunk_text(context, method=self.chunks['method'], chunk_size=self.chunks['size'], overlap=self.chunks['overlap'])
|
|
710
865
|
# Embedding
|
|
711
|
-
query_vector, chunk_vectors = self.fit_transform(query, chunks)
|
|
866
|
+
query_vector, chunk_vectors = self.fit_transform(query, chunks, embedding=embedding)
|
|
712
867
|
# Compute similarity
|
|
713
|
-
|
|
868
|
+
D = cosine_similarity(query_vector, chunk_vectors)[0]
|
|
714
869
|
# Get top scoring chunks
|
|
715
|
-
if
|
|
716
|
-
|
|
870
|
+
if top_chunks is None:
|
|
871
|
+
top_chunks = len(D)
|
|
872
|
+
logger.info('Number of top chunks selected is set to: {top_chunks}')
|
|
717
873
|
|
|
874
|
+
# Top indices
|
|
875
|
+
top_indices = np.argsort(D)[-top_chunks:][::-1]
|
|
718
876
|
# Join relevant chunks and send as prompt
|
|
719
877
|
relevant_chunks = [chunks[i] for i in top_indices]
|
|
720
|
-
relevant_scores = [
|
|
878
|
+
relevant_scores = [D[i] for i in top_indices]
|
|
721
879
|
|
|
722
880
|
# Set the return type
|
|
723
881
|
if return_type == 'score':
|
|
@@ -729,26 +887,40 @@ class LLMlight:
|
|
|
729
887
|
else:
|
|
730
888
|
return "\n---------\n".join(relevant_chunks)
|
|
731
889
|
|
|
732
|
-
def fit_transform(self, query, chunks):
|
|
890
|
+
def fit_transform(self, query, chunks, embedding=None):
|
|
733
891
|
"""Converts context chunks and query into vector space representations based on the selected embedding method."""
|
|
734
|
-
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':
|
|
735
903
|
vectorizer = TfidfVectorizer()
|
|
736
904
|
chunk_vectors = vectorizer.fit_transform(chunks)
|
|
737
905
|
# dense_matrix = chunk_vectors.toarray() # Converts to a NumPy array
|
|
738
906
|
query_vector = vectorizer.transform([query])
|
|
739
|
-
elif
|
|
907
|
+
elif embedding == 'bow':
|
|
740
908
|
vectorizer = CountVectorizer()
|
|
741
909
|
chunk_vectors = vectorizer.fit_transform(chunks)
|
|
742
910
|
query_vector = vectorizer.transform([query])
|
|
743
|
-
# elif
|
|
744
|
-
elif
|
|
745
|
-
chunk_vectors = np.vstack([
|
|
746
|
-
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])
|
|
747
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.')
|
|
748
918
|
else:
|
|
749
|
-
|
|
919
|
+
logger.error(f'Available embedding methods: {get_embeddings()}')
|
|
920
|
+
raise ValueError(f"Unsupported embedding method: {self.embedding}")
|
|
921
|
+
# Return
|
|
750
922
|
return query_vector, chunk_vectors
|
|
751
|
-
|
|
923
|
+
|
|
752
924
|
def compute_preprocessing(self, query, context, instructions, system):
|
|
753
925
|
# Create advanced prompt using relevant chunks of text, the input query and instructions
|
|
754
926
|
if context is not None:
|
|
@@ -759,7 +931,7 @@ class LLMlight:
|
|
|
759
931
|
# Analyze per chunk
|
|
760
932
|
relevant_context = self.chunk_wise(query, context, instructions, system, top_chunks=0, return_per_chunk=True)
|
|
761
933
|
else:
|
|
762
|
-
logger.info(f'No method is applied
|
|
934
|
+
logger.info(f'No preprocessing method is applied.')
|
|
763
935
|
relevant_context = context
|
|
764
936
|
else:
|
|
765
937
|
# Default
|
|
@@ -768,40 +940,107 @@ class LLMlight:
|
|
|
768
940
|
# Return
|
|
769
941
|
return relevant_context
|
|
770
942
|
|
|
771
|
-
def
|
|
772
|
-
|
|
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
|
+
|
|
773
985
|
if context is not None:
|
|
774
|
-
|
|
986
|
+
# Get relevant context using RAG and embedding
|
|
987
|
+
if self.retrieval_method == 'RAG_basic' and self.embedding['context'] in get_embeddings():
|
|
775
988
|
# Find the best matching parts using simple retrieval method approach.
|
|
776
|
-
logger.info(f
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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)
|
|
781
1002
|
else:
|
|
782
|
-
logger.info(f'No method is applied
|
|
1003
|
+
logger.info(f'No retrieval method is applied.')
|
|
783
1004
|
relevant_context = context
|
|
784
1005
|
else:
|
|
785
|
-
# Default
|
|
786
1006
|
relevant_context = context
|
|
787
1007
|
|
|
788
1008
|
# Return
|
|
789
1009
|
return relevant_context
|
|
790
1010
|
|
|
791
|
-
|
|
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):
|
|
792
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
|
+
|
|
793
1032
|
prompt = (
|
|
794
1033
|
("Context:\n" + context + "\n\n" if context else "")
|
|
795
|
-
+ (
|
|
796
|
-
+ (
|
|
797
|
-
+
|
|
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"
|
|
798
1037
|
+ query
|
|
799
|
-
|
|
1038
|
+
)
|
|
800
1039
|
|
|
801
1040
|
# Return
|
|
802
1041
|
return prompt
|
|
803
1042
|
|
|
804
|
-
def read_pdf(self,
|
|
1043
|
+
def read_pdf(self, file_path, title_pages=[1, 2], body_pages=[], reference_pages=[-1], return_type='dict'):
|
|
805
1044
|
"""
|
|
806
1045
|
Reads a PDF file and extracts its text content as a string.
|
|
807
1046
|
|
|
@@ -812,26 +1051,127 @@ class LLMlight:
|
|
|
812
1051
|
str: Extracted text from the PDF.
|
|
813
1052
|
|
|
814
1053
|
"""
|
|
815
|
-
if os.path.isfile(
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
if return_type=='dict':
|
|
821
|
-
|
|
822
|
-
|
|
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']
|
|
823
1062
|
else:
|
|
824
|
-
logger.
|
|
825
|
-
|
|
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.
|
|
826
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.
|
|
827
1070
|
|
|
828
|
-
|
|
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}
|
|
1156
|
+
else:
|
|
1157
|
+
embedding = {'memory': 'memvid', 'context': 'bert'}
|
|
1158
|
+
|
|
1159
|
+
if embedding.get('context') == 'memvid':
|
|
1160
|
+
embedding['context'] = 'tfidf'
|
|
1161
|
+
|
|
1162
|
+
embedding = {**{'memory': 'memvid', 'context': 'tfidf'}, **embedding}
|
|
1163
|
+
|
|
1164
|
+
# Return
|
|
1165
|
+
return embedding
|
|
1166
|
+
|
|
1167
|
+
#%%
|
|
1168
|
+
def convert_messages_to_model(messages, model='llama', add_assistant_start=True):
|
|
829
1169
|
"""
|
|
830
1170
|
Builds a prompt in the appropriate format for different models (LLaMA, Grok, Mistral).
|
|
831
1171
|
|
|
832
1172
|
Args:
|
|
833
1173
|
messages (list of dict): Each dict must have 'role' ('system', 'user', 'assistant') and 'content'.
|
|
834
|
-
|
|
1174
|
+
model (str): The type of model to generate the prompt for ('llama', 'grok', or 'mistral').
|
|
835
1175
|
add_assistant_start (bool): Whether to add the assistant start (default True).
|
|
836
1176
|
add_bos_token (bool): Helps models know it's a fresh conversation. Useful for llama/mistral/hermes-style models
|
|
837
1177
|
|
|
@@ -843,31 +1183,31 @@ def convert_prompt(messages, modelname='llama', add_assistant_start=True):
|
|
|
843
1183
|
... {"role": "system", "content": "You are a helpful assistant."},
|
|
844
1184
|
... {"role": "user", "content": "What is the capital of France?"}
|
|
845
1185
|
... ]
|
|
846
|
-
>>> prompt =
|
|
1186
|
+
>>> prompt = convert_messages_to_model(messages, model='llama')
|
|
847
1187
|
>>> print(prompt)
|
|
848
1188
|
|
|
849
1189
|
"""
|
|
850
1190
|
prompt = ""
|
|
851
1191
|
|
|
852
|
-
# if add_bos_token and ('llama' in
|
|
1192
|
+
# if add_bos_token and ('llama' in model or 'mistral' in model):
|
|
853
1193
|
# prompt += "<|begin_of_text|>\n"
|
|
854
1194
|
|
|
855
1195
|
for msg in messages:
|
|
856
1196
|
role = msg["role"]
|
|
857
1197
|
content = msg["content"].strip()
|
|
858
1198
|
|
|
859
|
-
if 'llama' in
|
|
1199
|
+
if 'llama' in model or 'mistral' in model:
|
|
860
1200
|
prompt += f"<|im_start|>{role}\n{content}\n<|im_end|>\n"
|
|
861
|
-
elif 'grok' in
|
|
1201
|
+
elif 'grok' in model:
|
|
862
1202
|
prompt += f"<start_of_turn>{role}\n{content}<end_of_turn>\n"
|
|
863
1203
|
else:
|
|
864
1204
|
# Default to ChatML format if model not recognized
|
|
865
1205
|
prompt += f"<|im_start|>{role}\n{content}\n<|im_end|>\n"
|
|
866
1206
|
|
|
867
1207
|
if add_assistant_start:
|
|
868
|
-
if 'llama' in
|
|
1208
|
+
if 'llama' in model or 'mistral' in model:
|
|
869
1209
|
prompt += "<|im_start|>assistant\n"
|
|
870
|
-
elif 'grok' in
|
|
1210
|
+
elif 'grok' in model:
|
|
871
1211
|
prompt += "<start_of_turn>assistant\n"
|
|
872
1212
|
|
|
873
1213
|
return prompt
|
|
@@ -923,7 +1263,9 @@ def compute_tokens(string, n_ctx=4096, task='full'):
|
|
|
923
1263
|
used_tokens = len(tokens)
|
|
924
1264
|
# Determine how many tokens are available for the model to generate
|
|
925
1265
|
max_tokens = compute_max_tokens(used_tokens, n_ctx=n_ctx, task=task)
|
|
926
|
-
|
|
1266
|
+
# Show message
|
|
1267
|
+
logger.debug(f"Used_tokens={used_tokens}, max_tokens={max_tokens}, context_limit={n_ctx}")
|
|
1268
|
+
# Return
|
|
927
1269
|
return used_tokens, max_tokens
|
|
928
1270
|
|
|
929
1271
|
|
|
@@ -962,8 +1304,19 @@ def compute_max_tokens(used_tokens, n_ctx=4096, task="full"):
|
|
|
962
1304
|
|
|
963
1305
|
|
|
964
1306
|
def set_system_message(system):
|
|
965
|
-
|
|
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
|
|
966
1316
|
|
|
1317
|
+
The context will be provided with each query based on semantic similarity to the user's question."""
|
|
1318
|
+
|
|
1319
|
+
return system
|
|
967
1320
|
|
|
968
1321
|
|
|
969
1322
|
# %%
|
|
@@ -1049,11 +1402,3 @@ def disable_tqdm():
|
|
|
1049
1402
|
"""Set the logger for verbosity messages."""
|
|
1050
1403
|
return (True if (logger.getEffectiveLevel()>=30) else False)
|
|
1051
1404
|
|
|
1052
|
-
|
|
1053
|
-
def check_logger(verbose: [str, int] = 'info'):
|
|
1054
|
-
"""Check the logger."""
|
|
1055
|
-
set_logger(verbose)
|
|
1056
|
-
logger.debug('DEBUG')
|
|
1057
|
-
logger.info('INFO')
|
|
1058
|
-
logger.warning('WARNING')
|
|
1059
|
-
logger.critical('CRITICAL')
|