LLMlight 0.2.1__tar.gz → 0.3.0__tar.gz

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