LLMlight 0.3.0__tar.gz → 0.5.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,25 +17,27 @@ from transformers import AutoTokenizer
17
17
  import copy
18
18
  import re
19
19
  from tqdm import tqdm
20
+ import tempfile
20
21
 
21
22
  from typing import List, Union
22
23
 
23
24
  from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer
24
25
  from sklearn.metrics.pairwise import cosine_similarity
25
26
  from sentence_transformers import SentenceTransformer
27
+ from distfit import distfit
26
28
 
27
29
  from memvid import MemvidEncoder, MemvidRetriever
28
30
  # from memvid.config import get_default_config as memvid_get_default_config
29
31
 
30
- # import memory
31
- # import RAG
32
- # import utils
33
- from . import RAG
34
- from . import utils
35
- from . import memory
36
- # DEBUG
37
- # import utils
38
- # 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
39
41
 
40
42
  logger = logging.getLogger(__name__)
41
43
 
@@ -52,25 +54,25 @@ class LLMlight:
52
54
  6. Enable Running.
53
55
 
54
56
  How LLMlight Works
55
- -----------------
57
+ -------------------
56
58
  LLMlight processes text through several key stages to generate intelligent responses:
57
59
 
58
- 1. Preprocessing Stage
60
+ 1. Context strategy
59
61
  ---------------------
60
62
  The input context can be processed in different ways:
61
- - No preprocessing: Uses the raw context directly
63
+ - No Context strategy: Uses the raw context directly
62
64
  - Chunk-wise processing: Breaks down the context into manageable chunks, processes each chunk independently, and combines results
63
65
  - Global reasoning: Creates a global summary of the context before processing
64
66
 
65
67
  2. Retrieval Method Stage
66
- ------------------------
68
+ --------------------------
67
69
  Three main approaches for retrieving relevant information:
68
70
  - Naive RAG: Splits text into chunks and uses similarity scoring to find the most relevant sections
69
71
  - RSE (Relevant Segment Extraction): Identifies and extracts complete relevant text segments
70
72
  - No retrieval: Uses the entire context directly
71
73
 
72
74
  3. Embedding Stage
73
- -----------------
75
+ -------------------
74
76
  Multiple embedding options for text representation:
75
77
  - TF-IDF: Best for structured documents with matching query terms
76
78
  - Bag of Words: Simple word frequency approach
@@ -78,7 +80,7 @@ class LLMlight:
78
80
  - BGE-small: Efficient embedding model for general use
79
81
 
80
82
  4. Prompting Stage
81
- -----------------
83
+ -------------------
82
84
  The system constructs prompts by combining:
83
85
  - System message: Defines the AI's role and behavior
84
86
  - Context: Processed and retrieved relevant information
@@ -86,50 +88,46 @@ class LLMlight:
86
88
  - Instructions: Additional guidance for response generation
87
89
 
88
90
  5. Response Generation
89
- ---------------------
91
+ -----------------------
92
+ The system can be configured through various parameters to optimize for different use cases, from simple Q&A to complex document analysis.
90
93
  The model generates responses using:
91
94
  - Temperature control: Adjusts response randomness (0.7 default)
92
95
  - Top-p sampling: Controls response diversity
93
96
  - Context window management: Handles token limits efficiently
94
97
 
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
98
  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.
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.
100
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.
101
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.
102
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.
103
104
 
104
-
105
105
  Parameters
106
106
  ----------
107
107
  model : str
108
- 'hermes-3-llama-3.2-3b'
109
- 'mistral-7b-grok'
110
- 'openhermes-2.5-mistral-7b'
111
- 'gemma-2-9b-it'
108
+ 'mistralai/mistral-small-3.2'
109
+ 'qwen/qwen3-coder-30b'
110
+ 'openai/gpt-oss-20b'
112
111
  system : str
113
112
  String of the system message.
114
113
  "I am a helpfull assistant"
115
- retrieval_method : str (default: 'RAG_basic')
114
+ retrieval_method : str (default: 'naive_rag')
116
115
  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.
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.
119
117
  'RSE': Context is processed using Navie RSE approach. Identify and extract entire segments of relevant text.
120
118
  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'}
119
+ Specify the embedding. When using both video-memory and context, it can be specified with a dictionary: {'memory': 'memvid', 'naive_rag': 'bert'}
123
120
  None: No embedding is performed.
121
+ 'automatic': {'memory': 'memvid', 'naive_rag': 'bert'}
124
122
  'memvid': This embedding can only be applied when using video-memory in the retrieval method.
125
123
  'tfidf': Best use when it is a structured documents and the words in the queries are matching.
126
124
  'bow': Bag of words approach. Best use when you expect words in the document and queries to be matching.
127
125
  'bert': Best use when document is more free text and the queries may not match exactly the words or sentences in the document.
128
126
  'bge-small':
129
- preprocessing : str (default: None)
127
+ context_strategy : str (default: None)
130
128
  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
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.
133
131
  temperature : float, optional
134
132
  Sampling temperature.
135
133
  0.7: (default)
@@ -146,13 +144,12 @@ class LLMlight:
146
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.
147
145
  'overlap': overlap between chunks
148
146
  'top_chunks': Retrieval of the top N chunks when performing RAG analysis.
149
- endpoint : str
150
- Endpoint of the LLM API
151
- "http://localhost:1234/v1/chat/completions"
152
- './models/Hermes-3-Llama-3.2-3B.Q4_K_M.gguf'
153
- r'C:/Users/username/.lmstudio/models/lmstudio-community/gemma-2-9b-it-GGUF/gemma-2-9b-it-Q4_K_M.gguf'
154
147
  n_ctx : int, default: 4096
155
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"
156
153
 
157
154
  Examples
158
155
  --------
@@ -166,26 +163,27 @@ class LLMlight:
166
163
 
167
164
  """
168
165
  def __init__(self,
169
- model: str = "hermes-3-llama-3.2-3b",
170
- retrieval_method: str = 'RAG_basic',
166
+ model: str = None,
167
+ retrieval_method: (None, str) = 'naive_rag',
171
168
  embedding: (str, dict) = {'memory': 'memvid', 'context': 'bert'},
172
- preprocessing: str = None,
169
+ context_strategy: str = None,
173
170
  alpha: float = 0.05,
174
171
  top_chunks: int = 5,
175
172
  temperature: (int, float) = 0.7,
176
173
  top_p: (int, float) = 1.0,
177
- chunks: dict = {'method': 'chars', 'size': 1024, 'overlap': 200},
178
- endpoint: str = "http://localhost:1234/v1/chat/completions",
174
+ chunks: dict = {'method': 'chars', 'size': 1000, 'overlap': 200},
179
175
  n_ctx: int = 4096,
176
+ file_path: str = None,
177
+ endpoint: str = "http://localhost:1234/v1/chat/completions",
180
178
  verbose: (str, int) = 'info',
181
179
  ):
182
180
 
183
181
  # Set the logger
184
182
  set_logger(verbose)
185
-
183
+
186
184
  # Store data in self
187
185
  self.model = model
188
- self.preprocessing = preprocessing
186
+ self.context_strategy = context_strategy
189
187
  self.retrieval_method = retrieval_method
190
188
  self.alpha = alpha
191
189
  self.top_chunks = top_chunks
@@ -195,23 +193,49 @@ class LLMlight:
195
193
  self.n_ctx = n_ctx
196
194
  self.context = None
197
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)
198
209
 
199
210
  # Set chunk parameters
200
211
  if chunks is None: chunks = {}
201
212
  self.chunks = {**{'method': 'chars', 'size': 1000, 'overlap': 250}, **chunks}
202
213
 
203
214
  # Set Memory parameters.
204
- self.memory_init(retrieval_method)
215
+ if file_path:
216
+ self.memory_load(self.file_path)
217
+ # self.memory_save(overwrite=False)
218
+
205
219
 
206
220
  # Load local LLM gguf model
207
221
  if os.path.isfile(self.endpoint):
208
222
  self.llm = load_local_gguf_model(self.endpoint, n_ctx=self.n_ctx)
209
223
 
210
- logger.info('LLMlight is initialized!')
211
224
  logger.info(f'Model: {self.model}')
212
- logger.info(f'Preprocessing: {self.preprocessing or "disabled"}')
225
+ logger.info(f'Context Strategy: {self.context_strategy or "disabled"}')
213
226
  logger.info(f'Retrieval method: {self.retrieval_method or "disabled"}')
214
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)
215
239
 
216
240
  def prompt(self,
217
241
  query: str,
@@ -250,7 +274,7 @@ class LLMlight:
250
274
  Whether to enable streaming (default is False).
251
275
  return_type: bool, optional
252
276
  Return dictionary in case the output is a json
253
- 'full': Output the full json
277
+ 'max': Output the full json
254
278
  'dict': Convert json into dictionary.
255
279
  'string': Return only the string answer (remove thinking strings using tags: <think> </think>).
256
280
  'string_with_thinking' Return the full response which includes the thinking proces (if available).
@@ -276,7 +300,7 @@ class LLMlight:
276
300
  if context is None: context = self.context
277
301
  if temperature is None: temperature = self.temperature
278
302
  if top_p is None: top_p = self.top_p
279
- self.task = 'full'
303
+ self.task = 'max'
280
304
 
281
305
  # Set system message
282
306
  system = set_system_message(system)
@@ -288,17 +312,18 @@ class LLMlight:
288
312
  relevant_context = self.relevant_context_retrieval(query, context, return_type='list')
289
313
 
290
314
  # Append the relevant chunks of texts
315
+ if isinstance(relevant_context, str): relevant_context = [relevant_context]
291
316
  total_context = (relevant_memory or []) + (relevant_context or [])
292
317
 
293
- # Preprocessing on the context
294
- processed_context = self.compute_preprocessing(query, total_context, instructions, system)
318
+ # Context Strategu on the context
319
+ processed_context = self.compute_context_strategy(query, total_context, instructions, system)
295
320
 
296
321
  # Set the prompt
297
322
  logger.debug(processed_context)
298
323
 
299
324
  # Make the prompt
300
325
  prompt = self.set_prompt(query, instructions, processed_context, response_format=response_format)
301
- logger.info('Running model now..')
326
+ logger.info(f'Running model: {self.model} ')
302
327
 
303
328
  # Run model
304
329
  if os.path.isfile(self.endpoint):
@@ -319,7 +344,7 @@ class LLMlight:
319
344
  # # context = '\n\n'.join(context.values())
320
345
  # return context
321
346
 
322
- def requests_post_gguf(self, prompt, system, temperature=0.8, top_p=1, headers=None, task='full', stream=False, return_type='string'):
347
+ def requests_post_gguf(self, prompt, system, temperature=0.8, top_p=1, headers=None, task='max', stream=False, return_type='string'):
323
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.
324
349
  # Prepare data for request.
325
350
  if headers is None: headers = {"Content-Type": "application/json"}
@@ -349,7 +374,7 @@ class LLMlight:
349
374
  # Return
350
375
  return response
351
376
 
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):
377
+ def requests_post_http(self, prompt, system, temperature=0.8, top_p=1, headers=None, task='max', stream=False, return_type='string', max_tokens=None):
353
378
  # Prepare data for request.
354
379
  if headers is None: headers = {"Content-Type": "application/json"}
355
380
  # Prepare messages
@@ -407,7 +432,7 @@ class LLMlight:
407
432
  logger.error(f"{response.status_code} - {response}")
408
433
  return f"Error: {response.status_code} - {response}"
409
434
 
410
- def memory_init(self, file_path: str = "llmlight_memory.mp4", config: dict = None, embedding=None):
435
+ def memory_init(self, file_path: str = None, config: dict = None, embedding=None):
411
436
  """Build QR code video and index from chunks with unified codec handling.
412
437
 
413
438
  Parameters
@@ -418,31 +443,48 @@ class LLMlight:
418
443
  Dictionary containing configuration parameters.
419
444
 
420
445
  """
421
- # Initialize video-memory
422
- if not os.path.isfile(file_path): file_path = None
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
+
423
452
  # Initialize
424
453
  self.memory = memory.memvid_llm(file_path, config=config)
454
+ # Update file path
455
+ self.file_path = self.memory.file_path
425
456
 
426
457
  # Set the embedding
427
458
  if embedding is not None:
428
459
  self.embedding['memory'] = embedding
429
460
  logger.info(f'Memory embedding is set: {self.embedding or "disabled"}')
430
461
 
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)
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
+
435
477
  # Load the retriever
436
- if not hasattr(self.memory, 'retriever'):
437
- # Load video-memory
438
- self.memory.load()
478
+ # if not hasattr(self.memory, 'retriever'):
479
+ # Load video-memory retriever
480
+ self.memory.load()
439
481
 
440
482
  def memory_save(self,
441
483
  file_path: str = None,
442
484
  codec: str = 'mp4v',
443
- auto_build_docker: bool = True,
485
+ auto_build_docker: bool = False,
444
486
  allow_fallback: bool = True,
445
- overwrite: bool = False,
487
+ overwrite: bool = True,
446
488
  show_progress: bool = True,
447
489
  ):
448
490
  """Build QR code video and index from chunks with unified codec handling.
@@ -461,31 +503,73 @@ class LLMlight:
461
503
  show_progress : bool (default: True)
462
504
 
463
505
  """
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)
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()
465
509
 
466
510
  def memory_add(self,
467
511
  text: Union[str, List[str]] = None,
468
- input_files: Union[str, List[str]] = None,
512
+ files: Union[str, List[str]] = None,
469
513
  dirpath: str = None,
470
514
  filetypes: List[str] = ['.pdf', '.txt', '.epub', '.md', '.doc', '.docx', '.rtf', '.html', '.htm'],
471
515
  chunk_size: int = 512,
472
516
  chunk_overlap: int = 100,
473
- overwrite=False):
517
+ overwrite=True):
474
518
  """Add chunks to memory.
475
519
 
476
520
  Parameters
477
521
  ----------
478
- input_files : (str, list)
522
+ files : (str, list)
479
523
  Path to file(s).
480
524
 
481
525
  """
482
- self.memory.add(text=text, input_files=input_files, dirpath=dirpath, filetypes=filetypes, chunk_size=chunk_size, chunk_overlap=chunk_overlap, overwrite=overwrite)
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)
483
527
 
484
- def compute_probability(self, query, scores, embedding, n=5000):
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
485
550
  if not hasattr(self.memory, 'retriever'):
486
- logger.warning('No chunks to encode for null distribution. Use client.add_chunks() first')
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
487
571
 
488
- logger.info('Creating random chunks of text with the same frequency distribution.')
572
+ logger.info('Creating null distribution -> For the detect of chunks with significant scores.')
489
573
  if self.embedding['memory']=='memvid':
490
574
  results = self.memory.retriever.index_manager.search(query, top_k=n)
491
575
  random_scores = np.array(list(map(lambda x: x[1], results)))
@@ -505,9 +589,8 @@ class LLMlight:
505
589
  # relevant_chunks = [random_chunks[i] for i in top_indices]
506
590
  # relevant_scores = [scores[i] for i in top_indices]
507
591
 
508
- from distfit import distfit
509
592
  model = distfit(method='parametric', alpha=self.alpha, bound=bound, verbose='warning')
510
- model.fit_transform(random_scores);
593
+ _ = model.fit_transform(random_scores)
511
594
 
512
595
  results = model.predict(scores, alpha=self.alpha, todf=False, multtest='fdr_bh')
513
596
  # results['y_bool'] = results['P']<=self.alpha
@@ -521,22 +604,41 @@ class LLMlight:
521
604
  # Return
522
605
  return results
523
606
 
524
-
525
-
526
- def task(self,
607
+ def summarize(self,
527
608
  query="Extract key insights while maintaining coherence of the previous summaries.",
528
- instructions="",
529
- system="You are a helpfull assistant.",
530
- response_format="**comprehensive, structured document covering all key insights**",
531
- task='question',
609
+ instructions="Extract key insights from the **new text chunk** while maintaining coherence with **Previous summaries",
610
+ system="You are a professional summarizer with over two decades of experience. Your strength is that you know how to deal with partial and incomplete texts but you do not make up new stuff. Keep the focus on the original input.",
611
+ response_format="**Make a comprehensive, structured document covering all key insights**",
532
612
  context=None,
533
613
  return_type='string',
534
614
  ):
535
615
  """
536
- Analyze the large text in an iterative, coherent manner.
537
- - Each chunk is processed while keeping track of previous summaries.
538
- - After all chunks are processed, a final coherent text is made.
539
- - The query can for example be to summarize the text or to extract key insights.
616
+ Summarize large documents iteratively while maintaining coherence across text chunks.
617
+
618
+ This function splits the input text into smaller chunks and processes each part in sequence.
619
+ For every chunk, it generates a partial summary while incorporating the context of the
620
+ previous summaries. After all chunks have been processed, the function combines the partial
621
+ results into a final, coherent, and structured summary.
622
+
623
+ Parameters
624
+ ----------
625
+ query : str, optional
626
+ The guiding task or question for summarization (default extracts key insights).
627
+ instructions : str, optional
628
+ Additional instructions for the summarizer, tailored to each chunk.
629
+ system : str
630
+ System message that sets the role and behavior of the summarizer.
631
+ response_format : str, optional
632
+ Defines the format of the final output (default is a structured document).
633
+ context : str or dict, optional
634
+ Input text or structured content to be summarized. If None, uses `self.context`.
635
+ return_type : str, optional
636
+ Format of the returned result (default "string").
637
+
638
+ Returns
639
+ -------
640
+ str
641
+ A comprehensive, coherent summary that integrates insights across all chunks.
540
642
 
541
643
  """
542
644
  if system is None:
@@ -552,11 +654,11 @@ class LLMlight:
552
654
  else:
553
655
  context = self.context
554
656
 
555
- logger.info(f'Processing the document for the given task..')
556
-
557
657
  # Create chunks based on words
558
658
  chunks = utils.chunk_text(context, method=self.chunks['method'], chunk_size=self.chunks['size'], overlap=self.chunks['overlap'])
559
659
 
660
+ logger.info(f'Processing the document using {len(chunks)} for the given task..')
661
+
560
662
  # Build a structured prompt that includes all previous summaries
561
663
  response_list = []
562
664
  for i, chunk in enumerate(chunks):
@@ -573,10 +675,9 @@ class LLMlight:
573
675
  + f"{chunk}\n\n"
574
676
 
575
677
  "### Instructions:\n"
576
- + "- Extract key insights from the **new text chunk** while maintaining coherence with **Previous summaries**.\n"
577
- + f"{instructions}\n\n"
678
+ + f"{instructions}**.\n\n"
578
679
 
579
- f"### {task}:\n"
680
+ f"### Question:\n"
580
681
  f"{query}\n\n"
581
682
 
582
683
  "### Improved Results:\n"
@@ -584,7 +685,7 @@ class LLMlight:
584
685
 
585
686
  # Get the summary for the current chunk
586
687
  # chunk_result = self.query_llm(prompt, system=system)
587
- chunk_result= self.requests_post_http(prompt, system, temperature=self.temperature, top_p=self.top_p, task='full', stream=False, return_type='string')
688
+ chunk_result = self.requests_post_http(prompt, system, temperature=self.temperature, top_p=self.top_p, task='max', stream=False, return_type='string')
588
689
 
589
690
  response_list.append(f"Results {i+1}:\n" + chunk_result)
590
691
 
@@ -595,23 +696,20 @@ class LLMlight:
595
696
  {results_total}
596
697
 
597
698
  ### Task:
598
- Connect the result parts in context into a **coherent, well-structured document**.
699
+ Your task is to connect all the parts into a **coherent, well-structured document**. Make sure it becomes is a very good summary.
599
700
 
600
701
  ### Instructions:
601
702
  - Maintain as much as possible the key insights but ensure logical flow.
602
703
  - Connect insights smoothly while keeping essential details intact.
603
-
604
- {response_format}
605
-
606
- f"### {task}:\n"
607
- {query}\n\n
704
+ - Only use bulletpoints when really needed.
705
+ - {response_format}
608
706
 
609
707
  Begin your response below:
610
708
  """
611
709
  logger.info('Combining all information to create a single coherent output..')
612
710
  # Create the final summary.
613
711
  # final_result = self.query_llm(final_prompt, system=system, return_type=return_type)
614
- final_result = self.requests_post_http(final_prompt, system, temperature=self.temperature, top_p=self.top_p, task='full', stream=False, return_type=return_type)
712
+ final_result = self.requests_post_http(final_prompt, system, temperature=self.temperature, top_p=self.top_p, task='max', stream=False, return_type=return_type)
615
713
  # Return
616
714
  return final_result
617
715
  # return {'summary': final_result, 'summary_per_chunk': results_total}
@@ -921,17 +1019,17 @@ class LLMlight:
921
1019
  # Return
922
1020
  return query_vector, chunk_vectors
923
1021
 
924
- def compute_preprocessing(self, query, context, instructions, system):
1022
+ def compute_context_strategy(self, query, context, instructions, system):
925
1023
  # Create advanced prompt using relevant chunks of text, the input query and instructions
926
1024
  if context is not None:
927
- if self.preprocessing=='global-reasoning':
1025
+ if self.context_strategy=='global-reasoning':
928
1026
  # Global Reasoning
929
1027
  relevant_context = self.global_reasoning(query, context, instructions, system, rewrite_query=False, return_per_chunk=True)
930
- elif self.preprocessing=='chunk-wise':
1028
+ elif self.context_strategy=='chunk-wise':
931
1029
  # Analyze per chunk
932
1030
  relevant_context = self.chunk_wise(query, context, instructions, system, top_chunks=0, return_per_chunk=True)
933
1031
  else:
934
- logger.info(f'No preprocessing method is applied.')
1032
+ logger.info(f'No Context Strategy method is applied.')
935
1033
  relevant_context = context
936
1034
  else:
937
1035
  # Default
@@ -948,11 +1046,13 @@ class LLMlight:
948
1046
  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
1047
 
950
1048
  # 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):
1049
+ if self.file_path and os.path.isfile(self.file_path):
1050
+ # and os.path.isfile(self.memory.index_path)
952
1051
  logger.info(f"Initialize retrieval from memory.. Collect [{self.top_chunks}] chunks from video-memory using {self.embedding['memory']}.")
953
1052
  # Initialize retriever
954
1053
  # retriever = self.memory.MemvidRetriever(video_file=self.memory.file_path, index_file=self.memory['index_path'], config=self.memory.config)
955
- self.memory_load()
1054
+ if not hasattr(self, 'memory'):
1055
+ self.memory_load()
956
1056
 
957
1057
  # Retrieval based on embedding
958
1058
  if self.embedding['memory']=='memvid':
@@ -984,7 +1084,7 @@ class LLMlight:
984
1084
 
985
1085
  if context is not None:
986
1086
  # Get relevant context using RAG and embedding
987
- if self.retrieval_method == 'RAG_basic' and self.embedding['context'] in get_embeddings():
1087
+ if self.retrieval_method == 'naive_rag' and self.embedding['context'] in get_embeddings():
988
1088
  # Find the best matching parts using simple retrieval method approach.
989
1089
  logger.info(f"Initialize retrieval from context.. Collect [{self.top_chunks}] chunks from context using {self.embedding['context']}.")
990
1090
  # Create chunks
@@ -1015,9 +1115,11 @@ class LLMlight:
1015
1115
  logger.info(f'Computing probability distribution..')
1016
1116
  # Compute probability
1017
1117
  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}')
1118
+
1119
+ if out is not None:
1120
+ # Only keep significant chunks
1121
+ relevant_context = list(np.array(relevant_context)[out.get('y_bool', '')])
1122
+ logger.info(f'{len(relevant_context)} significant chunks found with alpha<={self.alpha}')
1021
1123
 
1022
1124
  # Return relevant context
1023
1125
  return relevant_context
@@ -1027,7 +1129,7 @@ class LLMlight:
1027
1129
  if isinstance(context, list):
1028
1130
  context = "\n\n---\n\n".join([f"### Chunk {i+1}:\n{s}" for i, s in enumerate(context)])
1029
1131
  if context=='':
1030
- logger.warning('No context is provided into the prompt.')
1132
+ logger.info('No context is provided into the prompt.')
1031
1133
 
1032
1134
  prompt = (
1033
1135
  ("Context:\n" + context + "\n\n" if context else "")
@@ -1040,7 +1142,7 @@ class LLMlight:
1040
1142
  # Return
1041
1143
  return prompt
1042
1144
 
1043
- def read_pdf(self, file_path, title_pages=[1, 2], body_pages=[], reference_pages=[-1], return_type='dict'):
1145
+ def read_pdf(self, file_path, title_pages=[1, 2], body_pages=[], reference_pages=[-1], return_type='str'):
1044
1146
  """
1045
1147
  Reads a PDF file and extracts its text content as a string.
1046
1148
 
@@ -1049,19 +1151,30 @@ class LLMlight:
1049
1151
 
1050
1152
  Returns:
1051
1153
  str: Extracted text from the PDF.
1154
+ dict: dictionary
1052
1155
 
1053
1156
  """
1157
+ context = ''
1158
+
1159
+ if 'http' in file_path[0:5]:
1160
+ logger.info('Downloading file from url..')
1161
+ url = file_path
1162
+ filename = wget.filename_from_url(url)
1163
+ file_path = os.path.join(self.tempdir, filename)
1164
+ context = wget.download(url, file_path)
1165
+
1054
1166
  if os.path.isfile(file_path):
1055
1167
  # Read pdf
1056
1168
  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
1169
  # if return_type=='dict':
1060
1170
  # counts = utils.count_words(self.context['body'])
1061
1171
  # self.context['body'] = self.context['body']
1062
1172
  else:
1063
1173
  logger.error(f'file_path does not exist: {file_path}')
1064
1174
 
1175
+ # Return
1176
+ return context
1177
+
1065
1178
  def get_available_models(self, validate=False):
1066
1179
  """Retrieve available models from the configured API endpoint.
1067
1180
 
@@ -1084,7 +1197,7 @@ class LLMlight:
1084
1197
  >>> # Import library
1085
1198
  >>> from LLMlight import LLMlight
1086
1199
  <<< # Initialize
1087
- >>> client = LLMlight(endpoint='http://localhost:1234/v1/chat/completions', verbose='info')
1200
+ >>> client = LLMlight(endpoint='http://localhost:1234/v1/chat/completions')
1088
1201
  >>> # Get models
1089
1202
  >>> models = client.get_available_models(validate=False)
1090
1203
  >>> # Print
@@ -1097,11 +1210,10 @@ class LLMlight:
1097
1210
  - Relies on the `LLMlight` class for validation (must be importable).
1098
1211
  """
1099
1212
  base_url = '/'.join(self.endpoint.split('/')[:3]) + '/'
1100
- logger.info('Collecting models in the API endpoint..')
1213
+ logger.info(f'Collecting models in the API endpoint: {self.endpoint}')
1101
1214
 
1102
1215
  try:
1103
1216
  model_url = base_url.rstrip('/') + '/v1/models'
1104
- logger.info(f'Looking up models: {model_url}')
1105
1217
  response = requests.get(model_url, timeout=10)
1106
1218
  if response.status_code == 200:
1107
1219
  try:
@@ -1255,7 +1367,7 @@ def load_local_gguf_model(model_path: str, n_ctx: int=4096, n_threads: int=8, n_
1255
1367
  # Return
1256
1368
  return llm
1257
1369
 
1258
- def compute_tokens(string, n_ctx=4096, task='full'):
1370
+ def compute_tokens(string, n_ctx=4096, task='max'):
1259
1371
  tokenizer = AutoTokenizer.from_pretrained("gpt2")
1260
1372
  # Tokenize the input string
1261
1373
  tokens = tokenizer.encode(string, truncation=True, max_length=n_ctx)
@@ -1269,18 +1381,32 @@ def compute_tokens(string, n_ctx=4096, task='full'):
1269
1381
  return used_tokens, max_tokens
1270
1382
 
1271
1383
 
1272
- def compute_max_tokens(used_tokens, n_ctx=4096, task="full"):
1384
+ def compute_max_tokens(used_tokens, n_ctx=4096, task="max"):
1273
1385
  """
1274
- Compute the number of tokens that can be generated based on the task type.
1386
+ Compute the maximum number of tokens that can be generated for a given task,
1387
+ taking into account the number of tokens already used and the model's context window.
1275
1388
 
1276
- Parameters:
1277
- - used_tokens (int): Tokens already used in the input prompt.
1278
- - n_ctx (int): Total context window size of the model.
1279
- - task (str): The use case for generation. Options:
1280
- 'summarization', 'chat', 'code', 'longform', 'full'
1389
+ Parameters
1390
+ ----------
1391
+ used_tokens : int
1392
+ Number of tokens already consumed in the current context.
1393
+ n_ctx : int, optional
1394
+ Total context window size of the model (default is 4096 tokens).
1395
+ task : str, optional
1396
+ Type of generation task. Determines the proportion of the remaining tokens to use.
1397
+ Options are:
1398
+ - "summarization": Use up to 50% of the context window, minimum 128 tokens.
1399
+ - "chat": Use up to 60% of the context window, minimum 128 tokens.
1400
+ - "code": Use up to 75% of the context window, minimum 128 tokens.
1401
+ - "longform": Use up to 90% of the context window, minimum 256 tokens.
1402
+ - "max": Use all remaining tokens.
1403
+ Any unrecognized task defaults to a safe fallback using 50% of the context window.
1281
1404
 
1282
- Returns:
1283
- - max_tokens (int): Tokens allowed for generation.
1405
+ Returns
1406
+ -------
1407
+ max_tokens : int
1408
+ Maximum number of tokens that can be generated for the specified task,
1409
+ ensuring at least a minimum number of tokens as defined per task type.
1284
1410
  """
1285
1411
 
1286
1412
  available_tokens = max(n_ctx - used_tokens, 1) # Ensure at least 1
@@ -1294,7 +1420,7 @@ def compute_max_tokens(used_tokens, n_ctx=4096, task="full"):
1294
1420
  max_tokens = max(min(available_tokens, int(n_ctx * 0.75)), 128)
1295
1421
  elif task == "longform":
1296
1422
  max_tokens = max(min(available_tokens, int(n_ctx * 0.9)), 256)
1297
- elif task == "full":
1423
+ elif task == "max":
1298
1424
  max_tokens = available_tokens
1299
1425
  else:
1300
1426
  # Default to safe fallback
@@ -1402,3 +1528,32 @@ def disable_tqdm():
1402
1528
  """Set the logger for verbosity messages."""
1403
1529
  return (True if (logger.getEffectiveLevel()>=30) else False)
1404
1530
 
1531
+ # %% Retrieve files files.
1532
+ class wget:
1533
+ """Retrieve file from url."""
1534
+
1535
+ def filename_from_url(url, ext=True):
1536
+ """Return filename."""
1537
+ urlname = os.path.basename(url)
1538
+ if not ext: _, ext = os.path.splitext(urlname)
1539
+ return urlname
1540
+
1541
+ def download(url, writepath):
1542
+ """Download.
1543
+
1544
+ Parameters
1545
+ ----------
1546
+ url : str.
1547
+ Internet source.
1548
+ writepath : str.
1549
+ Directory to write the file.
1550
+
1551
+ Returns
1552
+ -------
1553
+ None.
1554
+
1555
+ """
1556
+ r = requests.get(url, stream=True)
1557
+ with open(writepath, "wb") as fd:
1558
+ for chunk in r.iter_content(chunk_size=1024):
1559
+ fd.write(chunk)