LLMlight 0.2.1__tar.gz → 0.4.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.
@@ -17,16 +17,27 @@ from transformers import AutoTokenizer
17
17
  import copy
18
18
  import re
19
19
  from tqdm import tqdm
20
+ import tempfile
21
+
22
+ from typing import List, Union
20
23
 
21
24
  from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer
22
25
  from sklearn.metrics.pairwise import cosine_similarity
23
26
  from sentence_transformers import SentenceTransformer
27
+ from distfit import distfit
28
+
29
+ from memvid import MemvidEncoder, MemvidRetriever
30
+ # from memvid.config import get_default_config as memvid_get_default_config
24
31
 
25
- from .RAG import RAG_with_RSE
26
- from . import utils
27
- # DEBUG
28
- # import utils
29
- # from RAG import RAG_with_RSE
32
+ try:
33
+ from . import RAG
34
+ from . import utils
35
+ from . import memory
36
+ except:
37
+ # DEBUG
38
+ import memory
39
+ import RAG
40
+ import utils
30
41
 
31
42
  logger = logging.getLogger(__name__)
32
43
 
@@ -42,32 +53,86 @@ class LLMlight:
42
53
  5. Enable "server on local network" if you need.
43
54
  6. Enable Running.
44
55
 
56
+ How LLMlight Works
57
+ -------------------
58
+ LLMlight processes text through several key stages to generate intelligent responses:
59
+
60
+ 1. Context strategy
61
+ ---------------------
62
+ The input context can be processed in different ways:
63
+ - No Context strategy: Uses the raw context directly
64
+ - Chunk-wise processing: Breaks down the context into manageable chunks, processes each chunk independently, and combines results
65
+ - Global reasoning: Creates a global summary of the context before processing
66
+
67
+ 2. Retrieval Method Stage
68
+ --------------------------
69
+ Three main approaches for retrieving relevant information:
70
+ - Naive RAG: Splits text into chunks and uses similarity scoring to find the most relevant sections
71
+ - RSE (Relevant Segment Extraction): Identifies and extracts complete relevant text segments
72
+ - No retrieval: Uses the entire context directly
73
+
74
+ 3. Embedding Stage
75
+ -------------------
76
+ Multiple embedding options for text representation:
77
+ - TF-IDF: Best for structured documents with matching query terms
78
+ - Bag of Words: Simple word frequency approach
79
+ - BERT: Advanced contextual embeddings for free-form text
80
+ - BGE-small: Efficient embedding model for general use
81
+
82
+ 4. Prompting Stage
83
+ -------------------
84
+ The system constructs prompts by combining:
85
+ - System message: Defines the AI's role and behavior
86
+ - Context: Processed and retrieved relevant information
87
+ - User query: The specific question or request
88
+ - Instructions: Additional guidance for response generation
89
+
90
+ 5. Response Generation
91
+ -----------------------
92
+ The system can be configured through various parameters to optimize for different use cases, from simple Q&A to complex document analysis.
93
+ The model generates responses using:
94
+ - Temperature control: Adjusts response randomness (0.7 default)
95
+ - Top-p sampling: Controls response diversity
96
+ - Context window management: Handles token limits efficiently
97
+
98
+ Processing Flow
99
+ ----------------
100
+ The system follows a sequential processing flow where each stage builds upon the previous one. First, the input context undergoes the context strategy, 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.
101
+ 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.
102
+ 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.
103
+ 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.
104
+
45
105
  Parameters
46
106
  ----------
47
- modelname : str
48
- 'hermes-3-llama-3.2-3b'
49
- 'mistral-7b-grok'
50
- 'openhermes-2.5-mistral-7b'
51
- 'gemma-2-9b-it'
107
+ model : str
108
+ 'mistralai/mistral-small-3.2'
109
+ 'qwen/qwen3-coder-30b'
110
+ 'openai/gpt-oss-20b'
52
111
  system : str
53
112
  String of the system message.
54
113
  "I am a helpfull assistant"
55
- preprocessing : str
56
- None: No pre-processing is performed. The original context is used in the pipeline of method, embedding and the response.
57
- 'chunk-wise': In case you have a very large document. The text 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.
58
- 'global-reasoning': In case you have a very large document. The text 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.
59
- method : str
60
- None: No processing is performed. The entire context is used for the query.
61
- 'naive_RAG': 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.
62
- 'RSE': Identify and extract entire segments of relevant text.
63
- embedding : str
64
- None
65
- 'tfidf': Best use when it is a structured documents and the words in the queries are matching.
66
- 'bow': Bag of words approach. Best use when you expect words in the document and queries to be matching.
67
- 'bert': Best use when document is more free text and the queries may not match exactly the words or sentences in the document.
114
+ retrieval_method : str (default: 'naive_rag')
115
+ None: No processing. The entire context is used for the query.
116
+ 'naive_rag': 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.
117
+ 'RSE': Context is processed using Navie RSE approach. Identify and extract entire segments of relevant text.
118
+ embedding : str, dict (default: 'automatic')
119
+ Specify the embedding. When using both video-memory and context, it can be specified with a dictionary: {'memory': 'memvid', 'naive_rag': 'bert'}
120
+ None: No embedding is performed.
121
+ 'automatic': {'memory': 'memvid', 'naive_rag': 'bert'}
122
+ 'memvid': This embedding can only be applied when using video-memory in the retrieval method.
123
+ 'tfidf': Best use when it is a structured documents and the words in the queries are matching.
124
+ 'bow': Bag of words approach. Best use when you expect words in the document and queries to be matching.
125
+ 'bert': Best use when document is more free text and the queries may not match exactly the words or sentences in the document.
68
126
  'bge-small':
127
+ context_strategy : str (default: None)
128
+ None: No pre-processing. The original context is used in the pipeline of retrieval_method, embedding and the response.
129
+ '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.
130
+ '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.
69
131
  temperature : float, optional
70
- Sampling temperature (default is 0.7).
132
+ Sampling temperature.
133
+ 0.7: (default)
134
+ 0: Deterministic
135
+ 1: Stochastic (max)
71
136
  top_p : float, optional
72
137
  Top-p (nucleus) sampling parameter (default is 1.0, no filtering).
73
138
  chunks: dict : {'method': 'chars', 'size': 1000, 'overlap': 250, 'top_chunks': 5}
@@ -79,122 +144,113 @@ class LLMlight:
79
144
  With a context window (n_ctx) of 4096 your can set size=1000 words with n chunks=5 and leave some space for instructions, system and the query.
80
145
  'overlap': overlap between chunks
81
146
  'top_chunks': Retrieval of the top N chunks when performing RAG analysis.
82
- endpoint : str
83
- Endpoint of the LLM API
84
- "http://localhost:1234/v1/chat/completions"
85
- './models/Hermes-3-Llama-3.2-3B.Q4_K_M.gguf'
86
- r'C:/Users/username/.lmstudio/models/lmstudio-community/gemma-2-9b-it-GGUF/gemma-2-9b-it-Q4_K_M.gguf'
87
147
  n_ctx : int, default: 4096
88
148
  The context window length is determined by the max tokens. A larger number of tokens will ask more cpu/gpu resources. Estimates: 1000 words or ~10.000 chars costs ~3000 tokens.
149
+ file_path : str
150
+ 'knowledge_base.mp4' Local or absolute path to your (video) memory file.
151
+ endpoint : str: Endpoint of the LLM API
152
+ "http://localhost:1234/v1/chat/completions"
89
153
 
90
154
  Examples
91
155
  --------
92
- >>> model = LLMlight()
93
- >>> model.prompt('hello, who are you?')
156
+ >>> # Examples
157
+ >>> from LLMlight import LLMlight
158
+ >>> client = LLMlight()
159
+ >>> client.prompt('hello, who are you?')
94
160
  >>> system_message = "You are a helpful assistant."
95
- >>> response = model.prompt('What is the capital of France?', system=system_message, top_p=0.9)
161
+ >>> response = client.prompt('What is the capital of France?', system=system_message, top_p=0.9)
162
+ >>> print(response)
96
163
 
97
164
  """
98
165
  def __init__(self,
99
- modelname="hermes-3-llama-3.2-3b",
100
- preprocessing=None,
101
- method='naive_RAG',
102
- embedding='bert',
103
- temperature=0.7,
104
- top_p=1.0,
105
- chunks={'method': 'chars', 'size': 1000, 'overlap': 250, 'top_chunks': 5},
106
- endpoint="http://localhost:1234/v1/chat/completions",
107
- n_ctx=4096,
108
- verbose='info',
166
+ model: str = None,
167
+ retrieval_method: (None, str) = 'naive_rag',
168
+ embedding: (str, dict) = {'memory': 'memvid', 'context': 'bert'},
169
+ context_strategy: str = None,
170
+ alpha: float = 0.05,
171
+ top_chunks: int = 5,
172
+ temperature: (int, float) = 0.7,
173
+ top_p: (int, float) = 1.0,
174
+ chunks: dict = {'method': 'chars', 'size': 1000, 'overlap': 200},
175
+ n_ctx: int = 4096,
176
+ file_path: str = None,
177
+ endpoint: str = "http://localhost:1234/v1/chat/completions",
178
+ verbose: (str, int) = 'info',
109
179
  ):
110
180
 
111
181
  # Set the logger
112
182
  set_logger(verbose)
183
+
113
184
  # Store data in self
114
- self.modelname = modelname
115
- self.preprocessing = preprocessing
116
- self.method = method
117
- self.embedding = embedding
185
+ self.model = model
186
+ self.context_strategy = context_strategy
187
+ self.retrieval_method = retrieval_method
188
+ self.alpha = alpha
189
+ self.top_chunks = top_chunks
118
190
  self.temperature = temperature
119
191
  self.top_p = top_p
120
192
  self.endpoint = endpoint
121
- if chunks is None: chunks = {}
122
- self.chunks = {**{'method': 'chars', 'size': 1000, 'overlap': 250, 'top_chunks': 5}, **chunks}
123
193
  self.n_ctx = n_ctx
124
194
  self.context = None
195
+ self.embedding = _set_embedding(embedding)
196
+ self.tempdir = os.path.join(tempfile.gettempdir(), 'temp_LLMlight')
197
+ self.file_path = self.get_full_path(file_path)
198
+
199
+ # Make checks
200
+ if model is None:
201
+ models = self.get_available_models(validate=False)
202
+ logger.error(f'Model must be set.')
203
+ logger.info(f'Available models: {models}')
204
+ raise AssertionError('Model must be set.')
205
+
206
+ # Create tempdir
207
+ if not os.path.isdir(self.tempdir):
208
+ os.makedirs(self.tempdir, exist_ok=True)
209
+
210
+ # Set chunk parameters
211
+ if chunks is None: chunks = {}
212
+ self.chunks = {**{'method': 'chars', 'size': 1000, 'overlap': 250}, **chunks}
125
213
 
126
- # Set the correct name for the model.
127
- if embedding == 'bert':
128
- self.embedding_model = SentenceTransformer('all-MiniLM-L6-v2')
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)
163
-
164
- except requests.exceptions.RequestException as e:
165
- logger.error("Request error:", e)
214
+ # Set Memory parameters.
215
+ if file_path:
216
+ self.memory_load(self.file_path)
217
+ # self.memory_save(overwrite=False)
166
218
 
167
- # Check each model whether it returns a response
168
- if validate:
169
- logger.info("Validating models:")
170
- keys = copy.deepcopy(list(model_dict.keys()))
171
219
 
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}")
220
+ # Load local LLM gguf model
221
+ if os.path.isfile(self.endpoint):
222
+ self.llm = load_local_gguf_model(self.endpoint, n_ctx=self.n_ctx)
182
223
 
183
- return list(model_dict.keys())
224
+ logger.info(f'Model: {self.model}')
225
+ logger.info(f'Context Strategy: {self.context_strategy or "disabled"}')
226
+ logger.info(f'Retrieval method: {self.retrieval_method or "disabled"}')
227
+ logger.info(f'Embedding: {self.embedding or "disabled"}')
228
+ logger.info('LLMlight is initialized!')
229
+
230
+ def get_full_path(self, filepath: str) -> str | None:
231
+ # If filepath is absolute, return as-is
232
+ if filepath is None:
233
+ return None
234
+ elif os.path.isabs(filepath):
235
+ return filepath
236
+
237
+ # If it's relative or just a filename, prepend tempdir
238
+ return os.path.join(self.tempdir, filepath)
184
239
 
185
240
  def prompt(self,
186
- query,
187
- instructions=None,
188
- system=None,
189
- context=None,
190
- response_format=None,
191
- temperature=None,
192
- top_p=None,
193
- stream=False,
194
- return_type='string',
195
- ):
196
- """
197
- Run the model with the provided parameters.
241
+ query: str,
242
+ instructions: str = None,
243
+ system: str = None,
244
+ context: str = None,
245
+ response_format=None,
246
+ temperature: (int, float) = None,
247
+ top_p: (int, float) = None,
248
+ stream: bool = False,
249
+ return_type: str = 'string',
250
+ verbose=None,
251
+ ):
252
+ """Run the model with the provided parameters.
253
+
198
254
  The final prompt is created based on the query, instructions, and the context
199
255
 
200
256
  Parameters
@@ -223,48 +279,71 @@ class LLMlight:
223
279
  'string': Return only the string answer (remove thinking strings using tags: <think> </think>).
224
280
  'string_with_thinking' Return the full response which includes the thinking proces (if available).
225
281
 
282
+ Examples
283
+ --------
284
+ >>> # Examples
285
+ >>> from LLMlight import LLMlight
286
+ >>> client = LLMlight()
287
+ >>> client.prompt('hello, who are you?')
288
+ >>> system_message = "You are a helpful assistant."
289
+ >>> response = client.prompt('What is the capital of France?', system=system_message, top_p=0.9)
290
+ >>> print(response)
291
+
226
292
  Returns
227
293
  -------
228
294
  str
229
295
  The model's response or an error message if the request fails.
230
296
  """
231
- logger.info(f'{self.modelname} is loaded..')
232
- headers = {"Content-Type": "application/json"}
297
+ if verbose is not None: set_logger(verbose)
298
+ logger.info(f'Creating response with {self.model}..')
233
299
 
300
+ if context is None: context = self.context
234
301
  if temperature is None: temperature = self.temperature
235
302
  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
303
  self.task = 'full'
248
304
 
249
305
  # Set system message
250
306
  system = set_system_message(system)
251
- # Preprocessing on the context
252
- processed_context = self.compute_preprocessing(query, context, instructions, system)
307
+
308
+ # Extract relevant text for video memory
309
+ relevant_memory = self.relevant_memory_retrieval(query, return_type='list')
310
+
253
311
  # Extract relevant text using retrieval method
254
- relevant_context = self.relevant_text_retrieval(query, processed_context, instructions, system)
312
+ relevant_context = self.relevant_context_retrieval(query, context, return_type='list')
313
+
314
+ # Append the relevant chunks of texts
315
+ if isinstance(relevant_context, str): relevant_context = [relevant_context]
316
+ total_context = (relevant_memory or []) + (relevant_context or [])
317
+
318
+ # Context Strategu on the context
319
+ processed_context = self.compute_context_strategy(query, total_context, instructions, system)
320
+
255
321
  # Set the prompt
256
- prompt = self.set_prompt(query, instructions, relevant_context, response_format=response_format)
322
+ logger.debug(processed_context)
323
+
324
+ # Make the prompt
325
+ prompt = self.set_prompt(query, instructions, processed_context, response_format=response_format)
326
+ logger.info(f'Running model: {self.model} ')
257
327
 
258
328
  # Run model
259
329
  if os.path.isfile(self.endpoint):
260
330
  # Run LLM from gguf model
261
- response = self.requests_post_gguf(prompt, system, temperature=temperature, top_p=top_p, headers=headers, task=self.task, stream=stream, return_type=return_type)
331
+ response = self.requests_post_gguf(prompt, system, temperature=temperature, top_p=top_p, task=self.task, stream=stream, return_type=return_type)
262
332
  else:
263
333
  # Run LLM with http model
264
- response = self.requests_post_http(prompt, system, temperature=temperature, top_p=top_p, headers=headers, task=self.task, stream=stream, return_type=return_type)
334
+ response = self.requests_post_http(prompt, system, temperature=temperature, top_p=top_p, task=self.task, stream=stream, return_type=return_type)
335
+
265
336
  # Return
266
337
  return response
267
338
 
339
+ # def _get_context(self, context):
340
+ # # First get the context
341
+ # if context is None:
342
+ # context = self.context
343
+ # # if isinstance(context, dict):
344
+ # # context = '\n\n'.join(context.values())
345
+ # return context
346
+
268
347
  def requests_post_gguf(self, prompt, system, temperature=0.8, top_p=1, headers=None, task='full', stream=False, return_type='string'):
269
348
  # 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
349
  # Prepare data for request.
@@ -272,7 +351,7 @@ class LLMlight:
272
351
  # Prepare messages
273
352
  messages = [{"role": "system", "content": system}, {"role": "user", "content": prompt}]
274
353
  # Convert messages to string prompt
275
- prompt = convert_prompt(messages, modelname=self.modelname)
354
+ prompt = convert_messages_to_model(messages, model=self.model)
276
355
  # Compute tokens
277
356
  used_tokens, max_tokens = compute_tokens(prompt, n_ctx=self.n_ctx, task=task)
278
357
 
@@ -295,19 +374,24 @@ class LLMlight:
295
374
  # Return
296
375
  return response
297
376
 
298
- def requests_post_http(self, prompt, system, temperature=0.8, top_p=1, headers=None, task='full', stream=False, return_type='string'):
377
+ 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
378
  # Prepare data for request.
300
379
  if headers is None: headers = {"Content-Type": "application/json"}
301
380
  # Prepare messages
302
381
  messages = [{"role": "system", "content": system}, {"role": "user", "content": prompt}]
382
+
383
+ # Convert messages to string prompt
384
+ prompt = convert_messages_to_model(messages, model=self.model)
385
+
303
386
  # Create full prompt
304
387
  prompt = messages[0]['content'] + messages[1]['content']
305
388
  # Compute tokens
306
- used_tokens, max_tokens = compute_tokens(prompt, n_ctx=self.n_ctx, task=task)
307
- logger.info(f'Running {self.modelname} with max tokens: {max_tokens}')
389
+ if max_tokens is None:
390
+ used_tokens, max_tokens = compute_tokens(prompt, n_ctx=self.n_ctx, task=task)
391
+ # logger.info(f'Generating response with {self.model}')
308
392
 
309
393
  data = {
310
- "model": self.modelname,
394
+ "model": self.model,
311
395
  "messages": messages,
312
396
  "temperature": temperature,
313
397
  "top_p": top_p,
@@ -348,6 +432,180 @@ class LLMlight:
348
432
  logger.error(f"{response.status_code} - {response}")
349
433
  return f"Error: {response.status_code} - {response}"
350
434
 
435
+ def memory_init(self, file_path: str = None, config: dict = None, embedding=None):
436
+ """Build QR code video and index from chunks with unified codec handling.
437
+
438
+ Parameters
439
+ ----------
440
+ file_path : str
441
+ Path to output video memory file.
442
+ config : dict
443
+ Dictionary containing configuration parameters.
444
+
445
+ """
446
+ if file_path: self.file_path = file_path
447
+ # Make check whether already loaded
448
+ if hasattr(self, 'memory') and self.memory.file_path == self.file_path:
449
+ logger.info(f'Memory already initialized: [{self.file_path}] <return>')
450
+ return
451
+
452
+ # Initialize
453
+ self.memory = memory.memvid_llm(file_path, config=config)
454
+ # Update file path
455
+ self.file_path = self.memory.file_path
456
+
457
+ # Set the embedding
458
+ if embedding is not None:
459
+ self.embedding['memory'] = embedding
460
+ logger.info(f'Memory embedding is set: {self.embedding or "disabled"}')
461
+
462
+ def memory_load(self, file_path: str = None, config: dict = None):
463
+ # Load
464
+ if file_path: self.file_path = file_path
465
+ # if not hasattr(self.memory, 'encoder'):
466
+ if hasattr(self, 'memory') and config is None and hasattr(self.memory, 'config'):
467
+ config = self.memory.config
468
+
469
+ if not hasattr(self, 'memory'):
470
+ self.memory = memory.memvid_llm(self.file_path, config=config)
471
+ self.file_path = self.memory.file_path
472
+ # Update file path
473
+ if not hasattr(self.memory, 'retriever'):
474
+ self.memory.load()
475
+ return
476
+
477
+ # Load the retriever
478
+ # if not hasattr(self.memory, 'retriever'):
479
+ # Load video-memory retriever
480
+ self.memory.load()
481
+
482
+ def memory_save(self,
483
+ file_path: str = None,
484
+ codec: str = 'mp4v',
485
+ auto_build_docker: bool = False,
486
+ allow_fallback: bool = True,
487
+ overwrite: bool = True,
488
+ show_progress: bool = True,
489
+ ):
490
+ """Build QR code video and index from chunks with unified codec handling.
491
+
492
+ Parameters
493
+ ----------
494
+ file_path : str (default is the initialization memory-path)
495
+ Path to output video memory file.
496
+ codec : str, optional
497
+ Video codec ('mp4v', 'h265', 'h264', etc.)
498
+ 'mp4v': Default
499
+ auto_build_docker : bool (default: True)
500
+ Whether to auto-build Docker if needed.
501
+ allow_fallback : bool
502
+ Whether to fall back to MP4V if advanced codec fails.
503
+ show_progress : bool (default: True)
504
+
505
+ """
506
+ if file_path is not None: self.file_path = file_path
507
+ self.memory.save(file_path=self.file_path, codec=codec, auto_build_docker=auto_build_docker, allow_fallback=allow_fallback, overwrite=overwrite, show_progress=show_progress)
508
+ self.memory.load()
509
+
510
+ def memory_add(self,
511
+ text: Union[str, List[str]] = None,
512
+ files: Union[str, List[str]] = None,
513
+ dirpath: str = None,
514
+ filetypes: List[str] = ['.pdf', '.txt', '.epub', '.md', '.doc', '.docx', '.rtf', '.html', '.htm'],
515
+ chunk_size: int = 512,
516
+ chunk_overlap: int = 100,
517
+ overwrite=True):
518
+ """Add chunks to memory.
519
+
520
+ Parameters
521
+ ----------
522
+ files : (str, list)
523
+ Path to file(s).
524
+
525
+ """
526
+ self.memory.add(text=text, input_files=files, dirpath=dirpath, filetypes=filetypes, chunk_size=chunk_size, chunk_overlap=chunk_overlap, overwrite=overwrite, tempdir=self.tempdir)
527
+
528
+ def memory_chunks(self, n=10, return_type='disk'):
529
+ """Return the top n memory stack.
530
+
531
+ Parameters
532
+ ----------
533
+ n : int, optional
534
+ Top n chunks to be returned. The default is 5.
535
+ return_type : str, optional
536
+ Retrieve chunks from memory or disk. The default is 'disk'.
537
+ 'memory'
538
+ 'disk'
539
+
540
+ Returns
541
+ -------
542
+ chunks : list
543
+ Top n returned chunks.
544
+
545
+ """
546
+
547
+ if not hasattr(self, 'memory'):
548
+ logger.warning('Memory is not initialized. Hint: client.memory_init()')
549
+ return
550
+ if not hasattr(self.memory, 'retriever'):
551
+ logger.warning('Memory is empty. Use client.memory_add() to add text and then client.memory_save() to store to disk.')
552
+ return
553
+
554
+ if len(self.memory.encoder.chunks) > 0:
555
+ logger.warning(f'Encoder contains {len(self.memory.encoder.chunks)} chunks that are not saved yet. Save to disk with: client.memory_save()')
556
+ # logger.warning(f'Total chunks on disk: {len(self.memory.retriever.index_manager.metadata)}')
557
+ # logger.warning(f'Total chunks on memory: {len(self.memory.encoder.chunks)}')
558
+
559
+ logger.info(f'Retrieving the first top {n} chunks from {return_type}.')
560
+ if return_type == 'disk':
561
+ chunks = list(map(lambda x: x.get('text'), self.memory.retriever.index_manager.metadata))[0:n]
562
+ else:
563
+ chunks = self.memory.encoder.chunks[0:n]
564
+
565
+ return chunks
566
+
567
+ def compute_probability(self, query, scores, embedding, n=5000):
568
+ if not hasattr(self, 'memory') or not hasattr(self.memory, 'retriever'):
569
+ logger.debug('No chunks to encode for null distribution. Use client.add_chunks() first.')
570
+ return
571
+
572
+ logger.info('Creating null distribution -> For the detect of chunks with significant scores.')
573
+ if self.embedding['memory']=='memvid':
574
+ results = self.memory.retriever.index_manager.search(query, top_k=n)
575
+ random_scores = np.array(list(map(lambda x: x[1], results)))
576
+ bound = 'left'
577
+ else:
578
+ random_chunks = self.memory.get_random_chunks(n=n)
579
+ query_vector, chunk_vectors = self.fit_transform(query, random_chunks, embedding=embedding)
580
+ # Compute similarity
581
+ random_scores = cosine_similarity(query_vector, chunk_vectors)[0]
582
+ # Remove all scores with exactly value 0
583
+ random_scores = random_scores[random_scores!=0]
584
+ bound = 'right'
585
+
586
+ # Top indices
587
+ # top_indices = np.argsort(scores)[::-1]
588
+ # Join relevant chunks and send as prompt
589
+ # relevant_chunks = [random_chunks[i] for i in top_indices]
590
+ # relevant_scores = [scores[i] for i in top_indices]
591
+
592
+ model = distfit(method='parametric', alpha=self.alpha, bound=bound, verbose='warning')
593
+ _ = model.fit_transform(random_scores)
594
+
595
+ results = model.predict(scores, alpha=self.alpha, todf=False, multtest='fdr_bh')
596
+ # results['y_bool'] = results['P']<=self.alpha
597
+ # Store figure
598
+ fig, ax = model.plot(title=f'Retrieval method:{self.retrieval_method}, Embedding: {embedding}')
599
+
600
+ # Store
601
+ self.distfit = model
602
+ self.distfit.fig = fig
603
+ self.distfit.ax = ax
604
+ # Return
605
+ return results
606
+
607
+
608
+
351
609
  def task(self,
352
610
  query="Extract key insights while maintaining coherence of the previous summaries.",
353
611
  instructions="",
@@ -388,7 +646,7 @@ class LLMlight:
388
646
  logger.info(f'Working on text chunk {i}/{len(chunks)}')
389
647
 
390
648
  # 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.chunks['top_chunks']:])
649
+ previous_results = "\n---\n".join(response_list[-self.top_chunks:])
392
650
 
393
651
  prompt = (
394
652
  "### Context:\n"
@@ -414,7 +672,7 @@ class LLMlight:
414
672
  response_list.append(f"Results {i+1}:\n" + chunk_result)
415
673
 
416
674
  # Final summarization pass over all collected summaries
417
- results_total = "\n---\n".join(response_list[-self.chunks['top_chunks']:])
675
+ results_total = "\n---\n".join(response_list[-self.top_chunks:])
418
676
  final_prompt = f"""
419
677
  ### Context:
420
678
  {results_total}
@@ -441,32 +699,6 @@ class LLMlight:
441
699
  return final_result
442
700
  # return {'summary': final_result, 'summary_per_chunk': results_total}
443
701
 
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
702
  def global_reasoning(self, query, context, instructions, system, return_per_chunk=False, rewrite_query=False, stream=False):
471
703
  """Global Reasoning.
472
704
  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 +720,24 @@ class LLMlight:
488
720
  Only return the new question with no other information.
489
721
  """
490
722
  # Initialize model for question refinement and summarization
491
- qmodel = LLMlight(modelname=self.modelname, temperature=0.7, endpoint=self.endpoint)
723
+ qmodel = LLMlight(model=self.model, temperature=0.7, endpoint=self.endpoint)
492
724
  # Create new query
493
725
  new_query = qmodel.prompt(query=query, instructions=instructions)
494
726
  else:
495
727
  new_query = query
496
728
 
497
729
  # Create chunks with overlapping parts to make sure we do not miss out
498
- chunks = utils.chunk_text(context, method=self.chunks['method'], chunk_size=self.chunks['size'], overlap=self.chunks['overlap'])
730
+ if isinstance(context, str):
731
+ chunks = utils.chunk_text(context, method=self.chunks['method'], chunk_size=self.chunks['size'], overlap=self.chunks['overlap'])
732
+ else:
733
+ chunks = context
734
+
735
+ logger.info(f'Global-reasoning on {len(chunks)} chunks of text.')
499
736
 
500
737
  # Now summaries for the chunks
501
738
  summaries = []
502
- for i, chunk in enumerate(tqdm(chunks, desc="Processing chunks", unit="chunk")):
503
- logger.info(f'Working on text chunk {i+1}/{len(chunks)}')
739
+ for i, chunk in enumerate(tqdm(chunks, desc="Processing chunk", unit="chunk")):
740
+ # logger.info(f'Working on text chunk {i+1}/{len(chunks)}')
504
741
 
505
742
  prompt = f"""
506
743
  ### Context (Chunk {i+1} of {len(chunks)} from a larger document):
@@ -522,7 +759,7 @@ class LLMlight:
522
759
  """
523
760
 
524
761
  # Summarize
525
- response = self.requests_post_http(prompt, system, temperature=self.temperature, top_p=self.top_p, task='summarization', stream=stream, return_type='string')
762
+ response = self.requests_post_http(prompt, system, temperature=self.temperature, top_p=self.top_p, task='summarization', stream=stream)
526
763
  # Append
527
764
  summaries.append(response)
528
765
  # Show
@@ -578,11 +815,16 @@ class LLMlight:
578
815
 
579
816
  """
580
817
  # Create chunks with overlapping parts to make sure we do not miss out
581
- chunks = utils.chunk_text(context, method=self.chunks['method'], chunk_size=self.chunks['size'], overlap=self.chunks['overlap'])
818
+ if isinstance(context, str):
819
+ chunks = utils.chunk_text(context, method=self.chunks['method'], chunk_size=self.chunks['size'], overlap=self.chunks['overlap'], return_type='list')
820
+ else:
821
+ chunks = context
822
+
823
+ logger.info(f'Chunk wise analysis on {len(chunks)} chunks of text.')
582
824
 
583
825
  # Build a structured prompt that includes all previous summaries
584
826
  response_list = []
585
- for i, chunk in enumerate(tqdm(chunks, desc="Processing chunks", unit="chunk")):
827
+ for i, chunk in enumerate(tqdm(chunks, desc="Processing chunk", unit="chunk")):
586
828
  logger.info(f'Working on text chunk {i+1}/{len(chunks)}')
587
829
 
588
830
  if top_chunks > 0:
@@ -701,21 +943,22 @@ class LLMlight:
701
943
  return final_response
702
944
  # return {'response': final_response, 'response_per_chunk': response_total}
703
945
 
704
- def parse_large_document(self, query, context, return_type='string'):
946
+ def search(self, query: str, chunks: list, return_type: str = 'string', top_chunks: int = None, embedding: str = None):
705
947
  """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
948
  # Embedding
709
- query_vector, chunk_vectors = self.fit_transform(query, chunks)
949
+ query_vector, chunk_vectors = self.fit_transform(query, chunks, embedding=embedding)
710
950
  # Compute similarity
711
- similarities = cosine_similarity(query_vector, chunk_vectors)[0]
951
+ D = cosine_similarity(query_vector, chunk_vectors)[0]
712
952
  # Get top scoring chunks
713
- if self.chunks['top_chunks'] is None: top_chunks = len(similarities)
714
- top_indices = np.argsort(similarities)[-top_chunks:][::-1]
953
+ if top_chunks is None:
954
+ top_chunks = len(D)
955
+ logger.info('Number of top chunks selected is set to: {top_chunks}')
715
956
 
957
+ # Top indices
958
+ top_indices = np.argsort(D)[-top_chunks:][::-1]
716
959
  # Join relevant chunks and send as prompt
717
960
  relevant_chunks = [chunks[i] for i in top_indices]
718
- relevant_scores = [similarities[i] for i in top_indices]
961
+ relevant_scores = [D[i] for i in top_indices]
719
962
 
720
963
  # Set the return type
721
964
  if return_type == 'score':
@@ -727,37 +970,51 @@ class LLMlight:
727
970
  else:
728
971
  return "\n---------\n".join(relevant_chunks)
729
972
 
730
- def fit_transform(self, query, chunks):
973
+ def fit_transform(self, query, chunks, embedding=None):
731
974
  """Converts context chunks and query into vector space representations based on the selected embedding method."""
732
- if self.embedding == 'tfidf':
975
+ if embedding is None: embedding = self.embedding
976
+
977
+ # Set embedding model parameters.
978
+ if self.embedding['context'] == 'bert':
979
+ embedding_model = SentenceTransformer('all-MiniLM-L6-v2')
980
+ elif self.embedding['context'] == 'bge-small':
981
+ embedding_model = SentenceTransformer('BAAI/bge-small-en')
982
+ else:
983
+ embedding_model = None
984
+
985
+ if embedding == 'tfidf':
733
986
  vectorizer = TfidfVectorizer()
734
987
  chunk_vectors = vectorizer.fit_transform(chunks)
735
988
  # dense_matrix = chunk_vectors.toarray() # Converts to a NumPy array
736
989
  query_vector = vectorizer.transform([query])
737
- elif self.embedding == 'bow':
990
+ elif embedding == 'bow':
738
991
  vectorizer = CountVectorizer()
739
992
  chunk_vectors = vectorizer.fit_transform(chunks)
740
993
  query_vector = vectorizer.transform([query])
741
- # elif self.embedding_model is not None:
742
- elif self.embedding == 'bert' or self.embedding == 'bge-small':
743
- chunk_vectors = np.vstack([self.embedding_model.encode(chunk) for chunk in chunks])
744
- query_vector = self.embedding_model.encode([query])
994
+ # elif embedding_model is not None:
995
+ elif embedding == 'bert' or embedding == 'bge-small':
996
+ chunk_vectors = np.vstack([embedding_model.encode(chunk) for chunk in chunks])
997
+ query_vector = embedding_model.encode([query])
745
998
  query_vector = query_vector.reshape(1, -1)
999
+ elif embedding == 'memvid':
1000
+ logger.warning(f'Embedding method [{embedding}] can only be applied when retrieval method is set to memory-video path.')
746
1001
  else:
747
- raise ValueError("Unsupported embedding method. Choose a supported embedding method.")
1002
+ logger.error(f'Available embedding methods: {get_embeddings()}')
1003
+ raise ValueError(f"Unsupported embedding method: {self.embedding}")
1004
+ # Return
748
1005
  return query_vector, chunk_vectors
749
-
750
- def compute_preprocessing(self, query, context, instructions, system):
1006
+
1007
+ def compute_context_strategy(self, query, context, instructions, system):
751
1008
  # Create advanced prompt using relevant chunks of text, the input query and instructions
752
1009
  if context is not None:
753
- if self.preprocessing=='global-reasoning':
1010
+ if self.context_strategy=='global-reasoning':
754
1011
  # Global Reasoning
755
1012
  relevant_context = self.global_reasoning(query, context, instructions, system, rewrite_query=False, return_per_chunk=True)
756
- elif self.preprocessing=='chunk-wise':
1013
+ elif self.context_strategy=='chunk-wise':
757
1014
  # Analyze per chunk
758
1015
  relevant_context = self.chunk_wise(query, context, instructions, system, top_chunks=0, return_per_chunk=True)
759
1016
  else:
760
- logger.info(f'No method is applied: The entire context is used.')
1017
+ logger.info(f'No Context Strategy method is applied.')
761
1018
  relevant_context = context
762
1019
  else:
763
1020
  # Default
@@ -766,40 +1023,111 @@ class LLMlight:
766
1023
  # Return
767
1024
  return relevant_context
768
1025
 
769
- def relevant_text_retrieval(self, query, context, instructions, system):
770
- # Create advanced prompt using relevant chunks of text, the input query and instructions
1026
+ def relevant_memory_retrieval(self, query: str, return_type='list'):
1027
+ relevant_context = None
1028
+
1029
+ # Show warning if chunks are not processed yet
1030
+ if hasattr(self, 'encoder') and len(self.encoder.chunks) > 0:
1031
+ logger.warning('Documents are stored in the encoder but not saved into video memory! Use save first: client.memory_save() to include the information.')
1032
+
1033
+ # Retrieve context from video memory
1034
+ if self.file_path and os.path.isfile(self.file_path):
1035
+ # and os.path.isfile(self.memory.index_path)
1036
+ logger.info(f"Initialize retrieval from memory.. Collect [{self.top_chunks}] chunks from video-memory using {self.embedding['memory']}.")
1037
+ # Initialize retriever
1038
+ # retriever = self.memory.MemvidRetriever(video_file=self.memory.file_path, index_file=self.memory['index_path'], config=self.memory.config)
1039
+ if not hasattr(self, 'memory'):
1040
+ self.memory_load()
1041
+
1042
+ # Retrieval based on embedding
1043
+ if self.embedding['memory']=='memvid':
1044
+ # Use the memvid search retriever
1045
+ # relevant_context = self.memory.retriever.search(query, top_k=self.top_chunks)
1046
+ results = self.memory.retriever.index_manager.search(query, top_k=self.top_chunks)
1047
+ scores = np.array(list(map(lambda x: x[1], results)))
1048
+ relevant_context = list(map(lambda x: x[2]['text'], results))
1049
+ elif self.embedding['memory'] in get_embeddings():
1050
+ # Use the classic retrievers
1051
+ chunks = list(map(lambda x: x.get('text'), self.memory.retriever.index_manager.metadata))
1052
+ # Compute distances and get top k chunks
1053
+ results = self.search(query, chunks, top_chunks=self.top_chunks, embedding=self.embedding['memory'], return_type='score')
1054
+ scores, relevant_context = map(list, zip(*results))
1055
+
1056
+ # Filter on Probability
1057
+ relevant_context = self._filter_proba(query, scores, relevant_context)
1058
+
1059
+ if return_type=='string':
1060
+ # Join the chunks in context
1061
+ relevant_context = "\n\n---\n\n".join([f"### Chunk {i+1}:\n{s}" for i, s in enumerate(relevant_context)])
1062
+
1063
+ # Return
1064
+ return relevant_context
1065
+
1066
+ def relevant_context_retrieval(self, query, context: str, return_type='list'):
1067
+ # Get context
1068
+ # context = self._get_context(context)
1069
+
771
1070
  if context is not None:
772
- if self.method == 'naive_RAG' and np.isin(self.embedding, ['tfidf', 'bow', 'bert', 'bge-small']):
1071
+ # Get relevant context using RAG and embedding
1072
+ if self.retrieval_method == 'naive_rag' and self.embedding['context'] in get_embeddings():
773
1073
  # Find the best matching parts using simple retrieval method approach.
774
- logger.info(f'[{self.method}] approach is applied with [{self.embedding}] embedding.')
775
- relevant_context = self.parse_large_document(query, context, return_type='string')
776
- elif self.method == 'RSE' and np.isin(self.embedding, ['bert', 'bge-small']):
777
- logger.info(f'RAG approach [{self.method}] is applied.')
778
- relevant_context = RAG_with_RSE(context, query, label=None, chunk_size=self.chunks['size'], irrelevant_chunk_penalty=0, embedding=self.embedding, device='cpu', batch_size=32)
1074
+ logger.info(f"Initialize retrieval from context.. Collect [{self.top_chunks}] chunks from context using {self.embedding['context']}.")
1075
+ # Create chunks
1076
+ chunks = utils.chunk_text(context, method=self.chunks['method'], chunk_size=self.chunks['size'], overlap=self.chunks['overlap'])
1077
+ # Compute distances and get top k chunks
1078
+ results = self.search(query, chunks, top_chunks=self.top_chunks, embedding=self.embedding['context'], return_type='score')
1079
+ # Unzip scores and context
1080
+ scores, relevant_context = map(list, zip(*results))
1081
+ # Filter on Probability
1082
+ relevant_context = self._filter_proba(query, scores, relevant_context)
1083
+
1084
+ elif self.retrieval_method == 'RSE' and np.isin(self.embedding['context'], ['bert', 'bge-small']):
1085
+ logger.info(f'RAG approach [{self.retrieval_method}] is applied.')
1086
+ 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
1087
  else:
780
- logger.info(f'No method is applied: The entire context is used.')
1088
+ logger.info(f'No retrieval method is applied.')
781
1089
  relevant_context = context
782
1090
  else:
783
- # Default
784
1091
  relevant_context = context
785
1092
 
786
1093
  # Return
787
1094
  return relevant_context
788
1095
 
789
- def set_prompt(self, query, instructions, context, response_format=''):
1096
+ # Compute probability
1097
+ def _filter_proba(self, query, scores, relevant_context):
1098
+ # Filter on significance
1099
+ if self.alpha is not None:
1100
+ logger.info(f'Computing probability distribution..')
1101
+ # Compute probability
1102
+ out = self.compute_probability(query, scores, embedding=self.embedding['memory'], n=1000)
1103
+
1104
+ if out is not None:
1105
+ # Only keep significant chunks
1106
+ relevant_context = list(np.array(relevant_context)[out.get('y_bool', '')])
1107
+ logger.info(f'{len(relevant_context)} significant chunks found with alpha<={self.alpha}')
1108
+
1109
+ # Return relevant context
1110
+ return relevant_context
1111
+
1112
+ def set_prompt(self, query: str, instructions: str, context: (str, list), response_format: str = None):
790
1113
  # Default and update when context and instructions are available.
1114
+ if isinstance(context, list):
1115
+ context = "\n\n---\n\n".join([f"### Chunk {i+1}:\n{s}" for i, s in enumerate(context)])
1116
+ if context=='':
1117
+ logger.info('No context is provided into the prompt.')
1118
+
791
1119
  prompt = (
792
1120
  ("Context:\n" + context + "\n\n" if context else "")
793
- + (f"Instructions:\n{instructions}\n\n" if instructions != '' else "")
794
- + (f"Response format:\n{response_format}\n\n" if (response_format != '') and (response_format is not None) else "")
795
- + f"User question:\n"
1121
+ + ("Instructions:\n" + instructions + "\n\n" if instructions not in ("", None) else "")
1122
+ + ("Response format:\n" + response_format + "\n\n" if response_format not in ("", None) else "")
1123
+ + "User question:\n"
796
1124
  + query
797
- )
1125
+ )
798
1126
 
799
1127
  # Return
800
1128
  return prompt
801
1129
 
802
- def read_pdf(self, filepath, title_pages=[1, 2], body_pages=[], reference_pages=[-1], return_type='dict'):
1130
+ def read_pdf(self, file_path, title_pages=[1, 2], body_pages=[], reference_pages=[-1], return_type='str'):
803
1131
  """
804
1132
  Reads a PDF file and extracts its text content as a string.
805
1133
 
@@ -808,28 +1136,139 @@ class LLMlight:
808
1136
 
809
1137
  Returns:
810
1138
  str: Extracted text from the PDF.
1139
+ dict: dictionary
811
1140
 
812
1141
  """
813
- if os.path.isfile(filepath):
814
- self.context = utils.read_pdf(filepath, title_pages=title_pages, body_pages=body_pages, reference_pages=reference_pages, return_type=return_type)
815
- if self.context is None:
816
- logger.error('No input text gathered. <return>')
817
- return
818
- if return_type=='dict':
819
- counts = utils.count_words(self.context['body'])
820
- self.context['body'] = self.context['body'] + f"\n---\nThe exact word count in this document is: {counts}"
1142
+ context = ''
1143
+
1144
+ if 'http' in file_path[0:5]:
1145
+ logger.info('Downloading file from url..')
1146
+ url = file_path
1147
+ filename = wget.filename_from_url(url)
1148
+ file_path = os.path.join(self.tempdir, filename)
1149
+ context = wget.download(url, file_path)
1150
+
1151
+ if os.path.isfile(file_path):
1152
+ # Read pdf
1153
+ context = utils.read_pdf(file_path, title_pages=title_pages, body_pages=body_pages, reference_pages=reference_pages, return_type=return_type)
1154
+ # if return_type=='dict':
1155
+ # counts = utils.count_words(self.context['body'])
1156
+ # self.context['body'] = self.context['body']
1157
+ else:
1158
+ logger.error(f'file_path does not exist: {file_path}')
1159
+
1160
+ # Return
1161
+ return context
1162
+
1163
+ def get_available_models(self, validate=False):
1164
+ """Retrieve available models from the configured API endpoint.
1165
+
1166
+ Optionally validates each model by sending a test prompt and filtering out
1167
+ models that return a 404 error or similar failure response.
1168
+
1169
+ Parameters
1170
+ ----------
1171
+ validate : bool, optional
1172
+ If True, each model is tested with a prompt to ensure it can respond correctly.
1173
+ Models that fail validation (e.g., return a 404 error) are excluded from the result.
1174
+
1175
+ Returns
1176
+ -------
1177
+ list of str
1178
+ A list of model identifiers (e.g., `"llama3"`, `"gpt-4"`) that are available and valid.
1179
+
1180
+ Examples
1181
+ --------
1182
+ >>> # Import library
1183
+ >>> from LLMlight import LLMlight
1184
+ <<< # Initialize
1185
+ >>> client = LLMlight(endpoint='http://localhost:1234/v1/chat/completions')
1186
+ >>> # Get models
1187
+ >>> models = client.get_available_models(validate=False)
1188
+ >>> # Print
1189
+ >>> print(models)
1190
+ >>> ['llama3', 'mistral-7b']
1191
+
1192
+ Notes
1193
+ -----
1194
+ - Requires an accessible endpoint and valid API response.
1195
+ - Relies on the `LLMlight` class for validation (must be importable).
1196
+ """
1197
+ base_url = '/'.join(self.endpoint.split('/')[:3]) + '/'
1198
+ logger.info(f'Collecting models in the API endpoint: {self.endpoint}')
1199
+
1200
+ try:
1201
+ model_url = base_url.rstrip('/') + '/v1/models'
1202
+ response = requests.get(model_url, timeout=10)
1203
+ if response.status_code == 200:
1204
+ try:
1205
+ models = response.json()["data"]
1206
+ model_dict = {model["id"]: model for model in models}
1207
+ except (KeyError, ValueError) as e:
1208
+ logger.error("Error parsing model data:", e)
1209
+ else:
1210
+ logger.warning("Request failed with status code:", response.status_code)
1211
+ logger.warning("Response:", response.text)
1212
+
1213
+ except requests.exceptions.RequestException as e:
1214
+ logger.error("Request error:", e)
1215
+
1216
+ # Check each model whether it returns a response
1217
+ if validate:
1218
+ logger.info("Validating the working of each available model. Be patient.")
1219
+ keys = copy.deepcopy(list(model_dict.keys()))
1220
+
1221
+ for key in keys:
1222
+ # logger.info(f'Checking: {key}')
1223
+ from LLMlight import LLMlight
1224
+ llm = LLMlight(model=key)
1225
+ response = llm.prompt('What is the capital of France?', instructions="You are only allowed to return one word.", return_type='string')
1226
+ response = response[0:30].replace('\n', ' ').replace('\r', ' ').lower()
1227
+ if 'error: 404' in response:
1228
+ logger.error(f"{llm.model}: {response}")
1229
+ model_dict.pop(key)
1230
+ else:
1231
+ logger.debug(f"{llm.model}: {response}")
1232
+
1233
+ return list(model_dict.keys())
1234
+
1235
+ def check_logger(self):
1236
+ """Check the verbosity."""
1237
+ logger.debug('DEBUG')
1238
+ logger.info('INFO')
1239
+ logger.warning('WARNING')
1240
+ logger.critical('CRITICAL')
1241
+
1242
+ #%%
1243
+ def get_embeddings():
1244
+ return ['tfidf', 'bow', 'bert', 'bge-small', 'memvid']
1245
+
1246
+ def _set_embedding(embedding):
1247
+ if embedding is None: embedding = {}
1248
+ if isinstance(embedding, str):
1249
+ if embedding == 'automatic':
1250
+ embedding = {'memory': 'memvid', 'context': 'bert'}
1251
+ elif embedding in get_embeddings():
1252
+ embedding = {'memory': embedding, 'context': embedding}
821
1253
  else:
822
- logger.warning(f'{filepath} does not exist.')
823
- self.context = None
1254
+ embedding = {'memory': 'memvid', 'context': 'bert'}
1255
+
1256
+ if embedding.get('context') == 'memvid':
1257
+ embedding['context'] = 'tfidf'
824
1258
 
1259
+ embedding = {**{'memory': 'memvid', 'context': 'tfidf'}, **embedding}
825
1260
 
826
- def convert_prompt(messages, modelname='llama', add_assistant_start=True):
1261
+ # Return
1262
+ return embedding
1263
+
1264
+ #%%
1265
+ def convert_messages_to_model(messages, model='llama', add_assistant_start=True):
827
1266
  """
828
1267
  Builds a prompt in the appropriate format for different models (LLaMA, Grok, Mistral).
829
1268
 
830
1269
  Args:
831
1270
  messages (list of dict): Each dict must have 'role' ('system', 'user', 'assistant') and 'content'.
832
- modelname (str): The type of model to generate the prompt for ('llama', 'grok', or 'mistral').
1271
+ model (str): The type of model to generate the prompt for ('llama', 'grok', or 'mistral').
833
1272
  add_assistant_start (bool): Whether to add the assistant start (default True).
834
1273
  add_bos_token (bool): Helps models know it's a fresh conversation. Useful for llama/mistral/hermes-style models
835
1274
 
@@ -841,31 +1280,31 @@ def convert_prompt(messages, modelname='llama', add_assistant_start=True):
841
1280
  ... {"role": "system", "content": "You are a helpful assistant."},
842
1281
  ... {"role": "user", "content": "What is the capital of France?"}
843
1282
  ... ]
844
- >>> prompt = convert_prompt(messages, modelname='llama')
1283
+ >>> prompt = convert_messages_to_model(messages, model='llama')
845
1284
  >>> print(prompt)
846
1285
 
847
1286
  """
848
1287
  prompt = ""
849
1288
 
850
- # if add_bos_token and ('llama' in modelname or 'mistral' in modelname):
1289
+ # if add_bos_token and ('llama' in model or 'mistral' in model):
851
1290
  # prompt += "<|begin_of_text|>\n"
852
1291
 
853
1292
  for msg in messages:
854
1293
  role = msg["role"]
855
1294
  content = msg["content"].strip()
856
1295
 
857
- if 'llama' in modelname or 'mistral' in modelname:
1296
+ if 'llama' in model or 'mistral' in model:
858
1297
  prompt += f"<|im_start|>{role}\n{content}\n<|im_end|>\n"
859
- elif 'grok' in modelname:
1298
+ elif 'grok' in model:
860
1299
  prompt += f"<start_of_turn>{role}\n{content}<end_of_turn>\n"
861
1300
  else:
862
1301
  # Default to ChatML format if model not recognized
863
1302
  prompt += f"<|im_start|>{role}\n{content}\n<|im_end|>\n"
864
1303
 
865
1304
  if add_assistant_start:
866
- if 'llama' in modelname or 'mistral' in modelname:
1305
+ if 'llama' in model or 'mistral' in model:
867
1306
  prompt += "<|im_start|>assistant\n"
868
- elif 'grok' in modelname:
1307
+ elif 'grok' in model:
869
1308
  prompt += "<start_of_turn>assistant\n"
870
1309
 
871
1310
  return prompt
@@ -921,22 +1360,38 @@ def compute_tokens(string, n_ctx=4096, task='full'):
921
1360
  used_tokens = len(tokens)
922
1361
  # Determine how many tokens are available for the model to generate
923
1362
  max_tokens = compute_max_tokens(used_tokens, n_ctx=n_ctx, task=task)
924
- logger.info(f"Used_tokens={used_tokens}, max_tokens={max_tokens}, context_limit={n_ctx}")
1363
+ # Show message
1364
+ logger.debug(f"Used_tokens={used_tokens}, max_tokens={max_tokens}, context_limit={n_ctx}")
1365
+ # Return
925
1366
  return used_tokens, max_tokens
926
1367
 
927
1368
 
928
1369
  def compute_max_tokens(used_tokens, n_ctx=4096, task="full"):
929
1370
  """
930
- Compute the number of tokens that can be generated based on the task type.
1371
+ Compute the maximum number of tokens that can be generated for a given task,
1372
+ taking into account the number of tokens already used and the model's context window.
931
1373
 
932
- Parameters:
933
- - used_tokens (int): Tokens already used in the input prompt.
934
- - n_ctx (int): Total context window size of the model.
935
- - task (str): The use case for generation. Options:
936
- 'summarization', 'chat', 'code', 'longform', 'full'
1374
+ Parameters
1375
+ ----------
1376
+ used_tokens : int
1377
+ Number of tokens already consumed in the current context.
1378
+ n_ctx : int, optional
1379
+ Total context window size of the model (default is 4096 tokens).
1380
+ task : str, optional
1381
+ Type of generation task. Determines the proportion of the remaining tokens to use.
1382
+ Options are:
1383
+ - "summarization": Use up to 50% of the context window, minimum 128 tokens.
1384
+ - "chat": Use up to 60% of the context window, minimum 128 tokens.
1385
+ - "code": Use up to 75% of the context window, minimum 128 tokens.
1386
+ - "longform": Use up to 90% of the context window, minimum 256 tokens.
1387
+ - "full": Use all remaining tokens.
1388
+ Any unrecognized task defaults to a safe fallback using 50% of the context window.
937
1389
 
938
- Returns:
939
- - max_tokens (int): Tokens allowed for generation.
1390
+ Returns
1391
+ -------
1392
+ max_tokens : int
1393
+ Maximum number of tokens that can be generated for the specified task,
1394
+ ensuring at least a minimum number of tokens as defined per task type.
940
1395
  """
941
1396
 
942
1397
  available_tokens = max(n_ctx - used_tokens, 1) # Ensure at least 1
@@ -960,8 +1415,19 @@ def compute_max_tokens(used_tokens, n_ctx=4096, task="full"):
960
1415
 
961
1416
 
962
1417
  def set_system_message(system):
963
- return "You are a helpful assistant." if system is None else system
1418
+ if system is None:
1419
+ system = """You are a helpful AI assistant with access to a knowledge base.
1420
+
1421
+ When answering questions:
1422
+ 1. Use the provided context from the knowledge base when relevant
1423
+ 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.
1424
+ 3. Be clear about what information comes from the knowledge base vs. your general knowledge
1425
+ 4. If the context doesn't contain enough information, say so clearly
1426
+ 5. Provide helpful, accurate, and concise responses
1427
+
1428
+ The context will be provided with each query based on semantic similarity to the user's question."""
964
1429
 
1430
+ return system
965
1431
 
966
1432
 
967
1433
  # %%
@@ -1047,11 +1513,32 @@ def disable_tqdm():
1047
1513
  """Set the logger for verbosity messages."""
1048
1514
  return (True if (logger.getEffectiveLevel()>=30) else False)
1049
1515
 
1516
+ # %% Retrieve files files.
1517
+ class wget:
1518
+ """Retrieve file from url."""
1519
+
1520
+ def filename_from_url(url, ext=True):
1521
+ """Return filename."""
1522
+ urlname = os.path.basename(url)
1523
+ if not ext: _, ext = os.path.splitext(urlname)
1524
+ return urlname
1525
+
1526
+ def download(url, writepath):
1527
+ """Download.
1528
+
1529
+ Parameters
1530
+ ----------
1531
+ url : str.
1532
+ Internet source.
1533
+ writepath : str.
1534
+ Directory to write the file.
1535
+
1536
+ Returns
1537
+ -------
1538
+ None.
1050
1539
 
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')
1540
+ """
1541
+ r = requests.get(url, stream=True)
1542
+ with open(writepath, "wb") as fd:
1543
+ for chunk in r.iter_content(chunk_size=1024):
1544
+ fd.write(chunk)