LLMlight 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,114 +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"
147
- base_url = self.endpoint.split("/chat")[0]
148
- logger.info('Collecting models in the API endpoint..')
149
-
150
- # Query available models
151
- response = requests.get(f"{base_url}/models")
152
- model_dict = {}
153
-
154
- try:
155
- response = requests.get("http://localhost:1234/v1/models", timeout=10)
156
- if response.status_code == 200:
157
- try:
158
- models = response.json()["data"]
159
- model_dict = {model["id"]: model for model in models}
160
- except (KeyError, ValueError) as e:
161
- logger.error("Error parsing model data:", e)
162
- else:
163
- logger.warning("Request failed with status code:", response.status_code)
164
- logger.warning("Response:", response.text)
165
-
166
- except requests.exceptions.RequestException as e:
167
- logger.error("Request error:", e)
199
+ # Set chunk parameters
200
+ if chunks is None: chunks = {}
201
+ self.chunks = {**{'method': 'chars', 'size': 1000, 'overlap': 250}, **chunks}
168
202
 
169
- # Check each model whether it returns a response
170
- if validate:
171
- logger.info("Validating models:")
172
- keys = copy.deepcopy(list(model_dict.keys()))
203
+ # Set Memory parameters.
204
+ self.memory_init(retrieval_method)
173
205
 
174
- for key in keys:
175
- from LLMlight import LLMlight
176
- llm = LLMlight(modelname=key)
177
- response = llm.prompt('What is the capital of France?', system="You are a helpful assistant.", return_type='string')
178
- response = response[0:30].replace('\n', ' ').replace('\r', ' ').lower()
179
- if 'error: 404' in response:
180
- logger.error(f"{llm.modelname}: {response}")
181
- model_dict.pop(key)
182
- else:
183
- logger.info(f"{llm.modelname}: {response}")
206
+ # Load local LLM gguf model
207
+ if os.path.isfile(self.endpoint):
208
+ self.llm = load_local_gguf_model(self.endpoint, n_ctx=self.n_ctx)
184
209
 
185
- 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"}')
186
215
 
187
216
  def prompt(self,
188
- query,
189
- instructions=None,
190
- system=None,
191
- context=None,
192
- response_format=None,
193
- temperature=None,
194
- top_p=None,
195
- stream=False,
196
- return_type='string',
197
- ):
198
- """
199
- Run the model with the provided parameters.
217
+ query: str,
218
+ instructions: str = None,
219
+ system: str = None,
220
+ context: str = None,
221
+ response_format=None,
222
+ temperature: (int, float) = None,
223
+ top_p: (int, float) = None,
224
+ stream: bool = False,
225
+ return_type: str = 'string',
226
+ verbose=None,
227
+ ):
228
+ """Run the model with the provided parameters.
229
+
200
230
  The final prompt is created based on the query, instructions, and the context
201
231
 
202
232
  Parameters
@@ -225,48 +255,70 @@ class LLMlight:
225
255
  'string': Return only the string answer (remove thinking strings using tags: <think> </think>).
226
256
  'string_with_thinking' Return the full response which includes the thinking proces (if available).
227
257
 
258
+ Examples
259
+ --------
260
+ >>> # Examples
261
+ >>> from LLMlight import LLMlight
262
+ >>> client = LLMlight()
263
+ >>> client.prompt('hello, who are you?')
264
+ >>> system_message = "You are a helpful assistant."
265
+ >>> response = client.prompt('What is the capital of France?', system=system_message, top_p=0.9)
266
+ >>> print(response)
267
+
228
268
  Returns
229
269
  -------
230
270
  str
231
271
  The model's response or an error message if the request fails.
232
272
  """
233
- logger.info(f'{self.modelname} is loaded..')
234
- headers = {"Content-Type": "application/json"}
273
+ if verbose is not None: set_logger(verbose)
274
+ logger.info(f'Creating response with {self.model}..')
235
275
 
276
+ if context is None: context = self.context
236
277
  if temperature is None: temperature = self.temperature
237
278
  if top_p is None: top_p = self.top_p
238
- if context is None: context = self.context
239
- # if embedding is not None: self.embedding = embedding
240
- if isinstance(context, dict): context = '\n\n'.join(context.values())
241
-
242
- # task (str): The use case for generation. (default: 'full')
243
- # 'summarization'
244
- # 'chat'
245
- # 'code'
246
- # 'longform'
247
- # 'full'
248
- # task = set_task(self.preprocessing, self.method)
249
279
  self.task = 'full'
250
280
 
251
281
  # Set system message
252
282
  system = set_system_message(system)
253
- # Preprocessing on the context
254
- 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
+
255
287
  # Extract relevant text using retrieval method
256
- 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
+
257
296
  # Set the prompt
258
- 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..')
259
302
 
260
303
  # Run model
261
304
  if os.path.isfile(self.endpoint):
262
305
  # Run LLM from gguf model
263
- response = self.requests_post_gguf(prompt, system, temperature=temperature, top_p=top_p, 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)
264
307
  else:
265
308
  # Run LLM with http model
266
- 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
+
267
311
  # Return
268
312
  return response
269
313
 
314
+ # def _get_context(self, context):
315
+ # # First get the context
316
+ # if context is None:
317
+ # context = self.context
318
+ # # if isinstance(context, dict):
319
+ # # context = '\n\n'.join(context.values())
320
+ # return context
321
+
270
322
  def requests_post_gguf(self, prompt, system, temperature=0.8, top_p=1, headers=None, task='full', stream=False, return_type='string'):
271
323
  # Note that it is better to use messages_prompt instead of a dict (messages_dict) because most GGUF-based models don't have a tokenizer/parser that can interpret the JSON-style message structure.
272
324
  # Prepare data for request.
@@ -274,7 +326,7 @@ class LLMlight:
274
326
  # Prepare messages
275
327
  messages = [{"role": "system", "content": system}, {"role": "user", "content": prompt}]
276
328
  # Convert messages to string prompt
277
- prompt = convert_prompt(messages, modelname=self.modelname)
329
+ prompt = convert_messages_to_model(messages, model=self.model)
278
330
  # Compute tokens
279
331
  used_tokens, max_tokens = compute_tokens(prompt, n_ctx=self.n_ctx, task=task)
280
332
 
@@ -297,19 +349,24 @@ class LLMlight:
297
349
  # Return
298
350
  return response
299
351
 
300
- def requests_post_http(self, prompt, system, temperature=0.8, top_p=1, headers=None, task='full', stream=False, return_type='string'):
352
+ def requests_post_http(self, prompt, system, temperature=0.8, top_p=1, headers=None, task='full', stream=False, return_type='string', max_tokens=None):
301
353
  # Prepare data for request.
302
354
  if headers is None: headers = {"Content-Type": "application/json"}
303
355
  # Prepare messages
304
356
  messages = [{"role": "system", "content": system}, {"role": "user", "content": prompt}]
357
+
358
+ # Convert messages to string prompt
359
+ prompt = convert_messages_to_model(messages, model=self.model)
360
+
305
361
  # Create full prompt
306
362
  prompt = messages[0]['content'] + messages[1]['content']
307
363
  # Compute tokens
308
- used_tokens, max_tokens = compute_tokens(prompt, n_ctx=self.n_ctx, task=task)
309
- 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}')
310
367
 
311
368
  data = {
312
- "model": self.modelname,
369
+ "model": self.model,
313
370
  "messages": messages,
314
371
  "temperature": temperature,
315
372
  "top_p": top_p,
@@ -350,6 +407,122 @@ class LLMlight:
350
407
  logger.error(f"{response.status_code} - {response}")
351
408
  return f"Error: {response.status_code} - {response}"
352
409
 
410
+ def memory_init(self, file_path: str = "llmlight_memory.mp4", config: dict = None, embedding=None):
411
+ """Build QR code video and index from chunks with unified codec handling.
412
+
413
+ Parameters
414
+ ----------
415
+ file_path : str
416
+ Path to output video memory file.
417
+ config : dict
418
+ Dictionary containing configuration parameters.
419
+
420
+ """
421
+ # Initialize video-memory
422
+ if not os.path.isfile(file_path): file_path = None
423
+ # Initialize
424
+ self.memory = memory.memvid_llm(file_path, config=config)
425
+
426
+ # Set the embedding
427
+ if embedding is not None:
428
+ self.embedding['memory'] = embedding
429
+ logger.info(f'Memory embedding is set: {self.embedding or "disabled"}')
430
+
431
+ def memory_load(self, file_path: str = None):
432
+ # Initialize if not yet initialized
433
+ if not hasattr(self.memory, 'encoder'):
434
+ self.memory_init(file_path)
435
+ # Load the retriever
436
+ if not hasattr(self.memory, 'retriever'):
437
+ # Load video-memory
438
+ self.memory.load()
439
+
440
+ def memory_save(self,
441
+ file_path: str = None,
442
+ codec: str = 'mp4v',
443
+ auto_build_docker: bool = True,
444
+ allow_fallback: bool = True,
445
+ overwrite: bool = False,
446
+ show_progress: bool = True,
447
+ ):
448
+ """Build QR code video and index from chunks with unified codec handling.
449
+
450
+ Parameters
451
+ ----------
452
+ file_path : str (default is the initialization memory-path)
453
+ Path to output video memory file.
454
+ codec : str, optional
455
+ Video codec ('mp4v', 'h265', 'h264', etc.)
456
+ 'mp4v': Default
457
+ auto_build_docker : bool (default: True)
458
+ Whether to auto-build Docker if needed.
459
+ allow_fallback : bool
460
+ Whether to fall back to MP4V if advanced codec fails.
461
+ show_progress : bool (default: True)
462
+
463
+ """
464
+ self.memory.save(file_path=file_path, codec=codec, auto_build_docker=auto_build_docker, allow_fallback=allow_fallback, overwrite=overwrite, show_progress=show_progress)
465
+
466
+ def memory_add(self,
467
+ text: Union[str, List[str]] = None,
468
+ input_files: Union[str, List[str]] = None,
469
+ dirpath: str = None,
470
+ filetypes: List[str] = ['.pdf', '.txt', '.epub', '.md', '.doc', '.docx', '.rtf', '.html', '.htm'],
471
+ chunk_size: int = 512,
472
+ chunk_overlap: int = 100,
473
+ overwrite=False):
474
+ """Add chunks to memory.
475
+
476
+ Parameters
477
+ ----------
478
+ input_files : (str, list)
479
+ Path to file(s).
480
+
481
+ """
482
+ self.memory.add(text=text, input_files=input_files, dirpath=dirpath, filetypes=filetypes, chunk_size=chunk_size, chunk_overlap=chunk_overlap, overwrite=overwrite)
483
+
484
+ def compute_probability(self, query, scores, embedding, n=5000):
485
+ if not hasattr(self.memory, 'retriever'):
486
+ logger.warning('No chunks to encode for null distribution. Use client.add_chunks() first')
487
+
488
+ logger.info('Creating random chunks of text with the same frequency distribution.')
489
+ if self.embedding['memory']=='memvid':
490
+ results = self.memory.retriever.index_manager.search(query, top_k=n)
491
+ random_scores = np.array(list(map(lambda x: x[1], results)))
492
+ bound = 'left'
493
+ else:
494
+ random_chunks = self.memory.get_random_chunks(n=n)
495
+ query_vector, chunk_vectors = self.fit_transform(query, random_chunks, embedding=embedding)
496
+ # Compute similarity
497
+ random_scores = cosine_similarity(query_vector, chunk_vectors)[0]
498
+ # Remove all scores with exactly value 0
499
+ random_scores = random_scores[random_scores!=0]
500
+ bound = 'right'
501
+
502
+ # Top indices
503
+ # top_indices = np.argsort(scores)[::-1]
504
+ # Join relevant chunks and send as prompt
505
+ # relevant_chunks = [random_chunks[i] for i in top_indices]
506
+ # relevant_scores = [scores[i] for i in top_indices]
507
+
508
+ from distfit import distfit
509
+ model = distfit(method='parametric', alpha=self.alpha, bound=bound, verbose='warning')
510
+ model.fit_transform(random_scores);
511
+
512
+ results = model.predict(scores, alpha=self.alpha, todf=False, multtest='fdr_bh')
513
+ # results['y_bool'] = results['P']<=self.alpha
514
+ # Store figure
515
+ fig, ax = model.plot(title=f'Retrieval method:{self.retrieval_method}, Embedding: {embedding}')
516
+
517
+ # Store
518
+ self.distfit = model
519
+ self.distfit.fig = fig
520
+ self.distfit.ax = ax
521
+ # Return
522
+ return results
523
+
524
+
525
+
353
526
  def task(self,
354
527
  query="Extract key insights while maintaining coherence of the previous summaries.",
355
528
  instructions="",
@@ -390,7 +563,7 @@ class LLMlight:
390
563
  logger.info(f'Working on text chunk {i}/{len(chunks)}')
391
564
 
392
565
  # Keep last N summaries for context (this needs to be within the context-window otherwise it will return an error.)
393
- previous_results = "\n---\n".join(response_list[-self.chunks['top_chunks']:])
566
+ previous_results = "\n---\n".join(response_list[-self.top_chunks:])
394
567
 
395
568
  prompt = (
396
569
  "### Context:\n"
@@ -416,7 +589,7 @@ class LLMlight:
416
589
  response_list.append(f"Results {i+1}:\n" + chunk_result)
417
590
 
418
591
  # Final summarization pass over all collected summaries
419
- results_total = "\n---\n".join(response_list[-self.chunks['top_chunks']:])
592
+ results_total = "\n---\n".join(response_list[-self.top_chunks:])
420
593
  final_prompt = f"""
421
594
  ### Context:
422
595
  {results_total}
@@ -443,32 +616,6 @@ class LLMlight:
443
616
  return final_result
444
617
  # return {'summary': final_result, 'summary_per_chunk': results_total}
445
618
 
446
- # def query_llm(self, prompt, system=None, task='full', stream=False, return_type='string'):
447
- # """Calls the LLM and returns the response."""
448
- # # Set defaults
449
- # headers = {"Content-Type": "application/json"}
450
- # # System
451
- # if system is None: system = "You are a helpful assistant."
452
- # # Messages
453
- # messages = [{"role": "system", "content": system}, {"role": "user", "content": prompt}]
454
- # # Compute tokens
455
- # used_tokens, max_tokens = compute_tokens(prompt, n_ctx=self.n_ctx, task=task)
456
-
457
- # data = {
458
- # "model": self.modelname,
459
- # "messages": messages,
460
- # "temperature": self.temperature,
461
- # "top_p": self.top_p,
462
- # "stream": stream,
463
- # "max_tokens": max_tokens,
464
- # }
465
-
466
- # # Send POST request
467
- # response = self.requests_post(headers, data, return_type=return_type)
468
-
469
- # # Return
470
- # return response
471
-
472
619
  def global_reasoning(self, query, context, instructions, system, return_per_chunk=False, rewrite_query=False, stream=False):
473
620
  """Global Reasoning.
474
621
  1. Rewrite the input user question into something like: "Based on the extracted summaries, does the document explain the societal relevance of the research? Justify your answer."
@@ -490,19 +637,24 @@ class LLMlight:
490
637
  Only return the new question with no other information.
491
638
  """
492
639
  # Initialize model for question refinement and summarization
493
- qmodel = LLMlight(modelname=self.modelname, temperature=0.7, endpoint=self.endpoint)
640
+ qmodel = LLMlight(model=self.model, temperature=0.7, endpoint=self.endpoint)
494
641
  # Create new query
495
642
  new_query = qmodel.prompt(query=query, instructions=instructions)
496
643
  else:
497
644
  new_query = query
498
645
 
499
646
  # Create chunks with overlapping parts to make sure we do not miss out
500
- 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.')
501
653
 
502
654
  # Now summaries for the chunks
503
655
  summaries = []
504
- for i, chunk in enumerate(tqdm(chunks, desc="Processing chunks", unit="chunk")):
505
- logger.info(f'Working on text chunk {i+1}/{len(chunks)}')
656
+ for i, chunk in enumerate(tqdm(chunks, desc="Processing chunk", unit="chunk")):
657
+ # logger.info(f'Working on text chunk {i+1}/{len(chunks)}')
506
658
 
507
659
  prompt = f"""
508
660
  ### Context (Chunk {i+1} of {len(chunks)} from a larger document):
@@ -524,7 +676,7 @@ class LLMlight:
524
676
  """
525
677
 
526
678
  # Summarize
527
- response = self.requests_post_http(prompt, system, temperature=self.temperature, top_p=self.top_p, task='summarization', stream=stream, return_type='string')
679
+ response = self.requests_post_http(prompt, system, temperature=self.temperature, top_p=self.top_p, task='summarization', stream=stream)
528
680
  # Append
529
681
  summaries.append(response)
530
682
  # Show
@@ -580,11 +732,16 @@ class LLMlight:
580
732
 
581
733
  """
582
734
  # Create chunks with overlapping parts to make sure we do not miss out
583
- 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.')
584
741
 
585
742
  # Build a structured prompt that includes all previous summaries
586
743
  response_list = []
587
- for i, chunk in enumerate(tqdm(chunks, desc="Processing chunks", unit="chunk")):
744
+ for i, chunk in enumerate(tqdm(chunks, desc="Processing chunk", unit="chunk")):
588
745
  logger.info(f'Working on text chunk {i+1}/{len(chunks)}')
589
746
 
590
747
  if top_chunks > 0:
@@ -703,21 +860,22 @@ class LLMlight:
703
860
  return final_response
704
861
  # return {'response': final_response, 'response_per_chunk': response_total}
705
862
 
706
- def 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):
707
864
  """Splits large text into chunks and finds the most relevant ones."""
708
- # Create chunks
709
- chunks = utils.chunk_text(context, method=self.chunks['method'], chunk_size=self.chunks['size'], overlap=self.chunks['overlap'])
710
865
  # Embedding
711
- query_vector, chunk_vectors = self.fit_transform(query, chunks)
866
+ query_vector, chunk_vectors = self.fit_transform(query, chunks, embedding=embedding)
712
867
  # Compute similarity
713
- similarities = cosine_similarity(query_vector, chunk_vectors)[0]
868
+ D = cosine_similarity(query_vector, chunk_vectors)[0]
714
869
  # Get top scoring chunks
715
- if self.chunks['top_chunks'] is None: top_chunks = len(similarities)
716
- 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}')
717
873
 
874
+ # Top indices
875
+ top_indices = np.argsort(D)[-top_chunks:][::-1]
718
876
  # Join relevant chunks and send as prompt
719
877
  relevant_chunks = [chunks[i] for i in top_indices]
720
- relevant_scores = [similarities[i] for i in top_indices]
878
+ relevant_scores = [D[i] for i in top_indices]
721
879
 
722
880
  # Set the return type
723
881
  if return_type == 'score':
@@ -729,26 +887,40 @@ class LLMlight:
729
887
  else:
730
888
  return "\n---------\n".join(relevant_chunks)
731
889
 
732
- def fit_transform(self, query, chunks):
890
+ def fit_transform(self, query, chunks, embedding=None):
733
891
  """Converts context chunks and query into vector space representations based on the selected embedding method."""
734
- if 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':
735
903
  vectorizer = TfidfVectorizer()
736
904
  chunk_vectors = vectorizer.fit_transform(chunks)
737
905
  # dense_matrix = chunk_vectors.toarray() # Converts to a NumPy array
738
906
  query_vector = vectorizer.transform([query])
739
- elif self.embedding == 'bow':
907
+ elif embedding == 'bow':
740
908
  vectorizer = CountVectorizer()
741
909
  chunk_vectors = vectorizer.fit_transform(chunks)
742
910
  query_vector = vectorizer.transform([query])
743
- # elif self.embedding_model is not None:
744
- elif self.embedding == 'bert' or self.embedding == 'bge-small':
745
- chunk_vectors = np.vstack([self.embedding_model.encode(chunk) for chunk in chunks])
746
- 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])
747
915
  query_vector = query_vector.reshape(1, -1)
916
+ elif embedding == 'memvid':
917
+ logger.warning(f'Embedding method [{embedding}] can only be applied when retrieval method is set to memory-video path.')
748
918
  else:
749
- 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
750
922
  return query_vector, chunk_vectors
751
-
923
+
752
924
  def compute_preprocessing(self, query, context, instructions, system):
753
925
  # Create advanced prompt using relevant chunks of text, the input query and instructions
754
926
  if context is not None:
@@ -759,7 +931,7 @@ class LLMlight:
759
931
  # Analyze per chunk
760
932
  relevant_context = self.chunk_wise(query, context, instructions, system, top_chunks=0, return_per_chunk=True)
761
933
  else:
762
- logger.info(f'No method is applied: The entire context is used.')
934
+ logger.info(f'No preprocessing method is applied.')
763
935
  relevant_context = context
764
936
  else:
765
937
  # Default
@@ -768,40 +940,107 @@ class LLMlight:
768
940
  # Return
769
941
  return relevant_context
770
942
 
771
- def relevant_text_retrieval(self, query, context, instructions, system):
772
- # 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
+
773
985
  if context is not None:
774
- 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():
775
988
  # Find the best matching parts using simple retrieval method approach.
776
- logger.info(f'[{self.method}] approach is applied with [{self.embedding}] embedding.')
777
- relevant_context = self.parse_large_document(query, context, return_type='string')
778
- elif self.method == 'RSE' and np.isin(self.embedding, ['bert', 'bge-small']):
779
- logger.info(f'RAG approach [{self.method}] is applied.')
780
- 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)
781
1002
  else:
782
- logger.info(f'No method is applied: The entire context is used.')
1003
+ logger.info(f'No retrieval method is applied.')
783
1004
  relevant_context = context
784
1005
  else:
785
- # Default
786
1006
  relevant_context = context
787
1007
 
788
1008
  # Return
789
1009
  return relevant_context
790
1010
 
791
- 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):
792
1026
  # Default and update when context and instructions are available.
1027
+ if isinstance(context, list):
1028
+ context = "\n\n---\n\n".join([f"### Chunk {i+1}:\n{s}" for i, s in enumerate(context)])
1029
+ if context=='':
1030
+ logger.warning('No context is provided into the prompt.')
1031
+
793
1032
  prompt = (
794
1033
  ("Context:\n" + context + "\n\n" if context else "")
795
- + (f"Instructions:\n{instructions}\n\n" if instructions != '' else "")
796
- + (f"Response format:\n{response_format}\n\n" if (response_format != '') and (response_format is not None) else "")
797
- + 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"
798
1037
  + query
799
- )
1038
+ )
800
1039
 
801
1040
  # Return
802
1041
  return prompt
803
1042
 
804
- 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'):
805
1044
  """
806
1045
  Reads a PDF file and extracts its text content as a string.
807
1046
 
@@ -812,26 +1051,127 @@ class LLMlight:
812
1051
  str: Extracted text from the PDF.
813
1052
 
814
1053
  """
815
- if os.path.isfile(filepath):
816
- self.context = utils.read_pdf(filepath, title_pages=title_pages, body_pages=body_pages, reference_pages=reference_pages, return_type=return_type)
817
- if self.context is None:
818
- logger.error('No input text gathered. <return>')
819
- return
820
- if return_type=='dict':
821
- counts = utils.count_words(self.context['body'])
822
- 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']
823
1062
  else:
824
- logger.warning(f'{filepath} does not exist.')
825
- self.context = None
1063
+ logger.error(f'file_path does not exist: {file_path}')
1064
+
1065
+ def get_available_models(self, validate=False):
1066
+ """Retrieve available models from the configured API endpoint.
826
1067
 
1068
+ Optionally validates each model by sending a test prompt and filtering out
1069
+ models that return a 404 error or similar failure response.
827
1070
 
828
- def convert_prompt(messages, modelname='llama', add_assistant_start=True):
1071
+ Parameters
1072
+ ----------
1073
+ validate : bool, optional
1074
+ If True, each model is tested with a prompt to ensure it can respond correctly.
1075
+ Models that fail validation (e.g., return a 404 error) are excluded from the result.
1076
+
1077
+ Returns
1078
+ -------
1079
+ list of str
1080
+ A list of model identifiers (e.g., `"llama3"`, `"gpt-4"`) that are available and valid.
1081
+
1082
+ Examples
1083
+ --------
1084
+ >>> # Import library
1085
+ >>> from LLMlight import LLMlight
1086
+ <<< # Initialize
1087
+ >>> client = LLMlight(endpoint='http://localhost:1234/v1/chat/completions', verbose='info')
1088
+ >>> # Get models
1089
+ >>> models = client.get_available_models(validate=False)
1090
+ >>> # Print
1091
+ >>> print(models)
1092
+ >>> ['llama3', 'mistral-7b']
1093
+
1094
+ Notes
1095
+ -----
1096
+ - Requires an accessible endpoint and valid API response.
1097
+ - Relies on the `LLMlight` class for validation (must be importable).
1098
+ """
1099
+ base_url = '/'.join(self.endpoint.split('/')[:3]) + '/'
1100
+ logger.info('Collecting models in the API endpoint..')
1101
+
1102
+ try:
1103
+ model_url = base_url.rstrip('/') + '/v1/models'
1104
+ logger.info(f'Looking up models: {model_url}')
1105
+ response = requests.get(model_url, timeout=10)
1106
+ if response.status_code == 200:
1107
+ try:
1108
+ models = response.json()["data"]
1109
+ model_dict = {model["id"]: model for model in models}
1110
+ except (KeyError, ValueError) as e:
1111
+ logger.error("Error parsing model data:", e)
1112
+ else:
1113
+ logger.warning("Request failed with status code:", response.status_code)
1114
+ logger.warning("Response:", response.text)
1115
+
1116
+ except requests.exceptions.RequestException as e:
1117
+ logger.error("Request error:", e)
1118
+
1119
+ # Check each model whether it returns a response
1120
+ if validate:
1121
+ logger.info("Validating the working of each available model. Be patient.")
1122
+ keys = copy.deepcopy(list(model_dict.keys()))
1123
+
1124
+ for key in keys:
1125
+ # logger.info(f'Checking: {key}')
1126
+ from LLMlight import LLMlight
1127
+ llm = LLMlight(model=key)
1128
+ response = llm.prompt('What is the capital of France?', instructions="You are only allowed to return one word.", return_type='string')
1129
+ response = response[0:30].replace('\n', ' ').replace('\r', ' ').lower()
1130
+ if 'error: 404' in response:
1131
+ logger.error(f"{llm.model}: {response}")
1132
+ model_dict.pop(key)
1133
+ else:
1134
+ logger.debug(f"{llm.model}: {response}")
1135
+
1136
+ return list(model_dict.keys())
1137
+
1138
+ def check_logger(self):
1139
+ """Check the verbosity."""
1140
+ logger.debug('DEBUG')
1141
+ logger.info('INFO')
1142
+ logger.warning('WARNING')
1143
+ logger.critical('CRITICAL')
1144
+
1145
+ #%%
1146
+ def get_embeddings():
1147
+ return ['tfidf', 'bow', 'bert', 'bge-small', 'memvid']
1148
+
1149
+ def _set_embedding(embedding):
1150
+ if embedding is None: embedding = {}
1151
+ if isinstance(embedding, str):
1152
+ if embedding == 'automatic':
1153
+ embedding = {'memory': 'memvid', 'context': 'bert'}
1154
+ elif embedding in get_embeddings():
1155
+ embedding = {'memory': embedding, 'context': embedding}
1156
+ else:
1157
+ embedding = {'memory': 'memvid', 'context': 'bert'}
1158
+
1159
+ if embedding.get('context') == 'memvid':
1160
+ embedding['context'] = 'tfidf'
1161
+
1162
+ embedding = {**{'memory': 'memvid', 'context': 'tfidf'}, **embedding}
1163
+
1164
+ # Return
1165
+ return embedding
1166
+
1167
+ #%%
1168
+ def convert_messages_to_model(messages, model='llama', add_assistant_start=True):
829
1169
  """
830
1170
  Builds a prompt in the appropriate format for different models (LLaMA, Grok, Mistral).
831
1171
 
832
1172
  Args:
833
1173
  messages (list of dict): Each dict must have 'role' ('system', 'user', 'assistant') and 'content'.
834
- 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').
835
1175
  add_assistant_start (bool): Whether to add the assistant start (default True).
836
1176
  add_bos_token (bool): Helps models know it's a fresh conversation. Useful for llama/mistral/hermes-style models
837
1177
 
@@ -843,31 +1183,31 @@ def convert_prompt(messages, modelname='llama', add_assistant_start=True):
843
1183
  ... {"role": "system", "content": "You are a helpful assistant."},
844
1184
  ... {"role": "user", "content": "What is the capital of France?"}
845
1185
  ... ]
846
- >>> prompt = convert_prompt(messages, modelname='llama')
1186
+ >>> prompt = convert_messages_to_model(messages, model='llama')
847
1187
  >>> print(prompt)
848
1188
 
849
1189
  """
850
1190
  prompt = ""
851
1191
 
852
- # if add_bos_token and ('llama' in modelname or 'mistral' in modelname):
1192
+ # if add_bos_token and ('llama' in model or 'mistral' in model):
853
1193
  # prompt += "<|begin_of_text|>\n"
854
1194
 
855
1195
  for msg in messages:
856
1196
  role = msg["role"]
857
1197
  content = msg["content"].strip()
858
1198
 
859
- if 'llama' in modelname or 'mistral' in modelname:
1199
+ if 'llama' in model or 'mistral' in model:
860
1200
  prompt += f"<|im_start|>{role}\n{content}\n<|im_end|>\n"
861
- elif 'grok' in modelname:
1201
+ elif 'grok' in model:
862
1202
  prompt += f"<start_of_turn>{role}\n{content}<end_of_turn>\n"
863
1203
  else:
864
1204
  # Default to ChatML format if model not recognized
865
1205
  prompt += f"<|im_start|>{role}\n{content}\n<|im_end|>\n"
866
1206
 
867
1207
  if add_assistant_start:
868
- if 'llama' in modelname or 'mistral' in modelname:
1208
+ if 'llama' in model or 'mistral' in model:
869
1209
  prompt += "<|im_start|>assistant\n"
870
- elif 'grok' in modelname:
1210
+ elif 'grok' in model:
871
1211
  prompt += "<start_of_turn>assistant\n"
872
1212
 
873
1213
  return prompt
@@ -923,7 +1263,9 @@ def compute_tokens(string, n_ctx=4096, task='full'):
923
1263
  used_tokens = len(tokens)
924
1264
  # Determine how many tokens are available for the model to generate
925
1265
  max_tokens = compute_max_tokens(used_tokens, n_ctx=n_ctx, task=task)
926
- 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
927
1269
  return used_tokens, max_tokens
928
1270
 
929
1271
 
@@ -962,8 +1304,19 @@ def compute_max_tokens(used_tokens, n_ctx=4096, task="full"):
962
1304
 
963
1305
 
964
1306
  def set_system_message(system):
965
- 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
966
1316
 
1317
+ The context will be provided with each query based on semantic similarity to the user's question."""
1318
+
1319
+ return system
967
1320
 
968
1321
 
969
1322
  # %%
@@ -1049,11 +1402,3 @@ def disable_tqdm():
1049
1402
  """Set the logger for verbosity messages."""
1050
1403
  return (True if (logger.getEffectiveLevel()>=30) else False)
1051
1404
 
1052
-
1053
- def check_logger(verbose: [str, int] = 'info'):
1054
- """Check the logger."""
1055
- set_logger(verbose)
1056
- logger.debug('DEBUG')
1057
- logger.info('INFO')
1058
- logger.warning('WARNING')
1059
- logger.critical('CRITICAL')