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