LLMlight 0.1.0__tar.gz → 0.2.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.0/LLMlight/LLMlight.py +1059 -0
- llmlight-0.2.0/LLMlight/__init__.py +48 -0
- {llmlight-0.1.0 → llmlight-0.2.0}/LLMlight/examples.py +106 -4
- {llmlight-0.1.0 → llmlight-0.2.0}/LLMlight/utils.py +2 -2
- {llmlight-0.1.0 → llmlight-0.2.0}/LLMlight.egg-info/PKG-INFO +44 -6
- {llmlight-0.1.0 → llmlight-0.2.0}/LLMlight.egg-info/requires.txt +1 -0
- {llmlight-0.1.0 → llmlight-0.2.0}/PKG-INFO +44 -6
- {llmlight-0.1.0 → llmlight-0.2.0}/README.md +42 -5
- {llmlight-0.1.0 → llmlight-0.2.0}/pyproject.toml +1 -0
- llmlight-0.1.0/LLMlight/LLMlight.py +0 -786
- llmlight-0.1.0/LLMlight/__init__.py +0 -37
- {llmlight-0.1.0 → llmlight-0.2.0}/LICENSE +0 -0
- {llmlight-0.1.0 → llmlight-0.2.0}/LLMlight/RAG.py +0 -0
- {llmlight-0.1.0 → llmlight-0.2.0}/LLMlight.egg-info/SOURCES.txt +0 -0
- {llmlight-0.1.0 → llmlight-0.2.0}/LLMlight.egg-info/dependency_links.txt +0 -0
- {llmlight-0.1.0 → llmlight-0.2.0}/LLMlight.egg-info/top_level.txt +0 -0
- {llmlight-0.1.0 → llmlight-0.2.0}/MANIFEST.in +0 -0
- {llmlight-0.1.0 → llmlight-0.2.0}/setup.cfg +0 -0
|
@@ -0,0 +1,1059 @@
|
|
|
1
|
+
"""LLMlight.
|
|
2
|
+
|
|
3
|
+
Name : LLMlight.py
|
|
4
|
+
Author : E.Taskesen
|
|
5
|
+
Contact : erdogant@gmail.com
|
|
6
|
+
github : https://github.com/erdogant/LLMlight
|
|
7
|
+
Licence : See licences
|
|
8
|
+
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
import numpy as np
|
|
15
|
+
from llama_cpp import Llama
|
|
16
|
+
from transformers import AutoTokenizer
|
|
17
|
+
import copy
|
|
18
|
+
import re
|
|
19
|
+
from tqdm import tqdm
|
|
20
|
+
|
|
21
|
+
from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer
|
|
22
|
+
from sklearn.metrics.pairwise import cosine_similarity
|
|
23
|
+
from sentence_transformers import SentenceTransformer
|
|
24
|
+
|
|
25
|
+
from .RAG import RAG_with_RSE
|
|
26
|
+
from . import utils
|
|
27
|
+
# DEBUG
|
|
28
|
+
# import utils
|
|
29
|
+
# from RAG import RAG_with_RSE
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
# %%
|
|
34
|
+
class LLMlight:
|
|
35
|
+
"""Large Language Model Light.
|
|
36
|
+
|
|
37
|
+
Run your LLM models local and with minimum dependencies.
|
|
38
|
+
1. Go to LM-studio.
|
|
39
|
+
2. Go to left panel and select developers mode.
|
|
40
|
+
3. On top select your model of interest.
|
|
41
|
+
4. Then go to settings in the top bar.
|
|
42
|
+
5. Enable "server on local network" if you need.
|
|
43
|
+
6. Enable Running.
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
modelname : str
|
|
48
|
+
'hermes-3-llama-3.2-3b'
|
|
49
|
+
'mistral-7b-grok'
|
|
50
|
+
'openhermes-2.5-mistral-7b'
|
|
51
|
+
'gemma-2-9b-it'
|
|
52
|
+
system : str
|
|
53
|
+
String of the system message.
|
|
54
|
+
"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.
|
|
68
|
+
'bge-small':
|
|
69
|
+
temperature : float, optional
|
|
70
|
+
Sampling temperature (default is 0.7).
|
|
71
|
+
top_p : float, optional
|
|
72
|
+
Top-p (nucleus) sampling parameter (default is 1.0, no filtering).
|
|
73
|
+
chunks: dict : {'method': 'chars', 'size': 1000, 'overlap': 250, 'top_chunks': 5}
|
|
74
|
+
type : str
|
|
75
|
+
'chars' or 'words': Chunks are created using chars or words.
|
|
76
|
+
'size': Chunk length in chars or words.
|
|
77
|
+
The accuracy increases with smaller chunk sizes. But it also reduces the input context for the LLM.
|
|
78
|
+
Estimates: 1000 words or ~10.000 chars costs ~3000 tokens.
|
|
79
|
+
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
|
+
'overlap': overlap between chunks
|
|
81
|
+
'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
|
+
n_ctx : int, default: 4096
|
|
88
|
+
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.
|
|
89
|
+
|
|
90
|
+
Examples
|
|
91
|
+
--------
|
|
92
|
+
>>> model = LLMlight()
|
|
93
|
+
>>> model.prompt('hello, who are you?')
|
|
94
|
+
>>> system_message = "You are a helpful assistant."
|
|
95
|
+
>>> response = model.prompt('What is the capital of France?', system=system_message, top_p=0.9)
|
|
96
|
+
|
|
97
|
+
"""
|
|
98
|
+
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',
|
|
109
|
+
):
|
|
110
|
+
|
|
111
|
+
# Set the logger
|
|
112
|
+
set_logger(verbose)
|
|
113
|
+
# Store data in self
|
|
114
|
+
self.modelname = modelname
|
|
115
|
+
self.preprocessing = preprocessing
|
|
116
|
+
self.method = method
|
|
117
|
+
self.embedding = embedding
|
|
118
|
+
self.temperature = temperature
|
|
119
|
+
self.top_p = top_p
|
|
120
|
+
self.endpoint = endpoint
|
|
121
|
+
if chunks is None: chunks = {}
|
|
122
|
+
self.chunks = {**{'method': 'chars', 'size': 1000, 'overlap': 250, 'top_chunks': 5}, **chunks}
|
|
123
|
+
self.n_ctx = n_ctx
|
|
124
|
+
self.context = None
|
|
125
|
+
|
|
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)
|
|
168
|
+
|
|
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()))
|
|
173
|
+
|
|
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}")
|
|
184
|
+
|
|
185
|
+
return list(model_dict.keys())
|
|
186
|
+
|
|
187
|
+
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.
|
|
200
|
+
The final prompt is created based on the query, instructions, and the context
|
|
201
|
+
|
|
202
|
+
Parameters
|
|
203
|
+
----------
|
|
204
|
+
query : str
|
|
205
|
+
The question or query.
|
|
206
|
+
"What is the capital for France?"
|
|
207
|
+
context : str
|
|
208
|
+
Large text string that will be chunked, and embedded. The answer for the query is based on the chunks.
|
|
209
|
+
instructions : str
|
|
210
|
+
Set your instructions.
|
|
211
|
+
"Answer the question strictly based on the provided context."
|
|
212
|
+
system : str, optional
|
|
213
|
+
Optional system message to set context for the AI (default is None).
|
|
214
|
+
"You are helpfull assistant."
|
|
215
|
+
temperature : float, optional
|
|
216
|
+
Sampling temperature (default is 0.7).
|
|
217
|
+
top_p : float, optional
|
|
218
|
+
Top-p (nucleus) sampling parameter (default is 1.0, no filtering).
|
|
219
|
+
stream : bool, optional
|
|
220
|
+
Whether to enable streaming (default is False).
|
|
221
|
+
return_type: bool, optional
|
|
222
|
+
Return dictionary in case the output is a json
|
|
223
|
+
'full': Output the full json
|
|
224
|
+
'dict': Convert json into dictionary.
|
|
225
|
+
'string': Return only the string answer (remove thinking strings using tags: <think> </think>).
|
|
226
|
+
'string_with_thinking' Return the full response which includes the thinking proces (if available).
|
|
227
|
+
|
|
228
|
+
Returns
|
|
229
|
+
-------
|
|
230
|
+
str
|
|
231
|
+
The model's response or an error message if the request fails.
|
|
232
|
+
"""
|
|
233
|
+
logger.info(f'{self.modelname} is loaded..')
|
|
234
|
+
headers = {"Content-Type": "application/json"}
|
|
235
|
+
|
|
236
|
+
if temperature is None: temperature = self.temperature
|
|
237
|
+
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
|
+
self.task = 'full'
|
|
250
|
+
|
|
251
|
+
# Set system message
|
|
252
|
+
system = set_system_message(system)
|
|
253
|
+
# Preprocessing on the context
|
|
254
|
+
processed_context = self.compute_preprocessing(query, context, instructions, system)
|
|
255
|
+
# Extract relevant text using retrieval method
|
|
256
|
+
relevant_context = self.relevant_text_retrieval(query, processed_context, instructions, system)
|
|
257
|
+
# Set the prompt
|
|
258
|
+
prompt = self.set_prompt(query, instructions, relevant_context, response_format=response_format)
|
|
259
|
+
|
|
260
|
+
# Run model
|
|
261
|
+
if os.path.isfile(self.endpoint):
|
|
262
|
+
# 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)
|
|
264
|
+
else:
|
|
265
|
+
# 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)
|
|
267
|
+
# Return
|
|
268
|
+
return response
|
|
269
|
+
|
|
270
|
+
def requests_post_gguf(self, prompt, system, temperature=0.8, top_p=1, headers=None, task='full', stream=False, return_type='string'):
|
|
271
|
+
# 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
|
+
# Prepare data for request.
|
|
273
|
+
if headers is None: headers = {"Content-Type": "application/json"}
|
|
274
|
+
# Prepare messages
|
|
275
|
+
messages = [{"role": "system", "content": system}, {"role": "user", "content": prompt}]
|
|
276
|
+
# Convert messages to string prompt
|
|
277
|
+
prompt = convert_prompt(messages, modelname=self.modelname)
|
|
278
|
+
# Compute tokens
|
|
279
|
+
used_tokens, max_tokens = compute_tokens(prompt, n_ctx=self.n_ctx, task=task)
|
|
280
|
+
|
|
281
|
+
# Send post request to local GGUF model
|
|
282
|
+
response = self.llm(
|
|
283
|
+
prompt=prompt,
|
|
284
|
+
temperature=temperature,
|
|
285
|
+
top_p=top_p,
|
|
286
|
+
stream=stream,
|
|
287
|
+
max_tokens=max_tokens,
|
|
288
|
+
stop=["<end_of_turn>", "<|im_end|>"] # common stop tokens for chat formats
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# Take only the output
|
|
292
|
+
if 'string' in return_type:
|
|
293
|
+
response = response.get('choices', [{}])[0].get('text', "No response")
|
|
294
|
+
if return_type == 'string':
|
|
295
|
+
# Remove thinking
|
|
296
|
+
response = re.sub(r'<think>.*?</think>', '', response, flags=re.DOTALL).strip()
|
|
297
|
+
# Return
|
|
298
|
+
return response
|
|
299
|
+
|
|
300
|
+
def requests_post_http(self, prompt, system, temperature=0.8, top_p=1, headers=None, task='full', stream=False, return_type='string'):
|
|
301
|
+
# Prepare data for request.
|
|
302
|
+
if headers is None: headers = {"Content-Type": "application/json"}
|
|
303
|
+
# Prepare messages
|
|
304
|
+
messages = [{"role": "system", "content": system}, {"role": "user", "content": prompt}]
|
|
305
|
+
# Create full prompt
|
|
306
|
+
prompt = messages[0]['content'] + messages[1]['content']
|
|
307
|
+
# 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}')
|
|
310
|
+
|
|
311
|
+
data = {
|
|
312
|
+
"model": self.modelname,
|
|
313
|
+
"messages": messages,
|
|
314
|
+
"temperature": temperature,
|
|
315
|
+
"top_p": top_p,
|
|
316
|
+
"stream": stream,
|
|
317
|
+
"max_tokens": max_tokens,
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
# Send POST request
|
|
321
|
+
response = self.requests_post(headers, data, stream=stream, return_type=return_type)
|
|
322
|
+
|
|
323
|
+
# Return
|
|
324
|
+
return response
|
|
325
|
+
|
|
326
|
+
def requests_post(self, headers, data, stream=False, return_type='string'):
|
|
327
|
+
"""Create the request to the LLM."""
|
|
328
|
+
# Get response
|
|
329
|
+
response = requests.post(self.endpoint, headers=headers, json=data, stream=stream)
|
|
330
|
+
|
|
331
|
+
# Handle the response
|
|
332
|
+
if response.status_code == 200:
|
|
333
|
+
try:
|
|
334
|
+
# Create dictionary in case json
|
|
335
|
+
response_text = response.json().get('choices', [{}])[0].get('message', {}).get('content', "No response")
|
|
336
|
+
|
|
337
|
+
if return_type == 'dict':
|
|
338
|
+
response_text = utils.is_valid_json(response_text)
|
|
339
|
+
return response_text
|
|
340
|
+
elif return_type == 'string_with_thinking':
|
|
341
|
+
return response_text
|
|
342
|
+
elif return_type == 'string':
|
|
343
|
+
response_text = re.sub(r'<think>.*?</think>', '', response_text, flags=re.DOTALL).strip()
|
|
344
|
+
return response_text
|
|
345
|
+
else:
|
|
346
|
+
return response.json()
|
|
347
|
+
except:
|
|
348
|
+
return response
|
|
349
|
+
else:
|
|
350
|
+
logger.error(f"{response.status_code} - {response}")
|
|
351
|
+
return f"Error: {response.status_code} - {response}"
|
|
352
|
+
|
|
353
|
+
def task(self,
|
|
354
|
+
query="Extract key insights while maintaining coherence of the previous summaries.",
|
|
355
|
+
instructions="",
|
|
356
|
+
system="You are a helpfull assistant.",
|
|
357
|
+
response_format="**comprehensive, structured document covering all key insights**",
|
|
358
|
+
task='question',
|
|
359
|
+
context=None,
|
|
360
|
+
return_type='string',
|
|
361
|
+
):
|
|
362
|
+
"""
|
|
363
|
+
Analyze the large text in an iterative, coherent manner.
|
|
364
|
+
- Each chunk is processed while keeping track of previous summaries.
|
|
365
|
+
- After all chunks are processed, a final coherent text is made.
|
|
366
|
+
- The query can for example be to summarize the text or to extract key insights.
|
|
367
|
+
|
|
368
|
+
"""
|
|
369
|
+
if system is None:
|
|
370
|
+
logger.error('system can not be None. <return>')
|
|
371
|
+
return
|
|
372
|
+
if (context is None) and (not hasattr(self, 'text') or self.context is None):
|
|
373
|
+
logger.error('No input text found. Use context or <model.read_pdf("here comes your file path to the pdf")> first. <return>')
|
|
374
|
+
return
|
|
375
|
+
|
|
376
|
+
if context is None:
|
|
377
|
+
if isinstance(self.context, dict):
|
|
378
|
+
context = self.context['body'] + '\n---\n' + self.context['references']
|
|
379
|
+
else:
|
|
380
|
+
context = self.context
|
|
381
|
+
|
|
382
|
+
logger.info(f'Processing the document for the given task..')
|
|
383
|
+
|
|
384
|
+
# Create chunks based on words
|
|
385
|
+
chunks = utils.chunk_text(context, method=self.chunks['method'], chunk_size=self.chunks['size'], overlap=self.chunks['overlap'])
|
|
386
|
+
|
|
387
|
+
# Build a structured prompt that includes all previous summaries
|
|
388
|
+
response_list = []
|
|
389
|
+
for i, chunk in enumerate(chunks):
|
|
390
|
+
logger.info(f'Working on text chunk {i}/{len(chunks)}')
|
|
391
|
+
|
|
392
|
+
# 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']:])
|
|
394
|
+
|
|
395
|
+
prompt = (
|
|
396
|
+
"### Context:\n"
|
|
397
|
+
+ (f"Previous results:\n{previous_results}\n" if len(response_list) > 0 else "")
|
|
398
|
+
|
|
399
|
+
+ "\n---\nNew text chunk (Part of a larger document, maintain context):\n"
|
|
400
|
+
+ f"{chunk}\n\n"
|
|
401
|
+
|
|
402
|
+
"### Instructions:\n"
|
|
403
|
+
+ "- Extract key insights from the **new text chunk** while maintaining coherence with **Previous summaries**.\n"
|
|
404
|
+
+ f"{instructions}\n\n"
|
|
405
|
+
|
|
406
|
+
f"### {task}:\n"
|
|
407
|
+
f"{query}\n\n"
|
|
408
|
+
|
|
409
|
+
"### Improved Results:\n"
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
# Get the summary for the current chunk
|
|
413
|
+
# chunk_result = self.query_llm(prompt, system=system)
|
|
414
|
+
chunk_result= self.requests_post_http(prompt, system, temperature=self.temperature, top_p=self.top_p, task='full', stream=False, return_type='string')
|
|
415
|
+
|
|
416
|
+
response_list.append(f"Results {i+1}:\n" + chunk_result)
|
|
417
|
+
|
|
418
|
+
# Final summarization pass over all collected summaries
|
|
419
|
+
results_total = "\n---\n".join(response_list[-self.chunks['top_chunks']:])
|
|
420
|
+
final_prompt = f"""
|
|
421
|
+
### Context:
|
|
422
|
+
{results_total}
|
|
423
|
+
|
|
424
|
+
### Task:
|
|
425
|
+
Connect the result parts in context into a **coherent, well-structured document**.
|
|
426
|
+
|
|
427
|
+
### Instructions:
|
|
428
|
+
- Maintain as much as possible the key insights but ensure logical flow.
|
|
429
|
+
- Connect insights smoothly while keeping essential details intact.
|
|
430
|
+
|
|
431
|
+
{response_format}
|
|
432
|
+
|
|
433
|
+
f"### {task}:\n"
|
|
434
|
+
{query}\n\n
|
|
435
|
+
|
|
436
|
+
Begin your response below:
|
|
437
|
+
"""
|
|
438
|
+
logger.info('Combining all information to create a single coherent output..')
|
|
439
|
+
# Create the final summary.
|
|
440
|
+
# final_result = self.query_llm(final_prompt, system=system, return_type=return_type)
|
|
441
|
+
final_result = self.requests_post_http(final_prompt, system, temperature=self.temperature, top_p=self.top_p, task='full', stream=False, return_type=return_type)
|
|
442
|
+
# Return
|
|
443
|
+
return final_result
|
|
444
|
+
# return {'summary': final_result, 'summary_per_chunk': results_total}
|
|
445
|
+
|
|
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
|
+
def global_reasoning(self, query, context, instructions, system, return_per_chunk=False, rewrite_query=False, stream=False):
|
|
473
|
+
"""Global Reasoning.
|
|
474
|
+
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."
|
|
475
|
+
2. Break the document into manageable chunks with overlapping parts to make sure we do not miss out.
|
|
476
|
+
3. Create a global reasoning question based on the input user question.
|
|
477
|
+
4. Take the summarized outputs and aggregate them.
|
|
478
|
+
|
|
479
|
+
prompt = "Is the proposal well thought out?"
|
|
480
|
+
instructions = "Your task is to rewrite questions for global reasoning. As an example, if there is a question like: 'Does this document section explain the societal relevance of the research?', the desired output would be: 'Does this document section explain the societal relevance of the research? If so, summarize it. If not, return 'No societal relevance found.''"
|
|
481
|
+
response = model.llm.prompt(query=prompt, instructions=instructions, task='Task')
|
|
482
|
+
|
|
483
|
+
"""
|
|
484
|
+
|
|
485
|
+
if rewrite_query:
|
|
486
|
+
# 1. Rewrite user question in global reasoning question.
|
|
487
|
+
logger.info('Rewriting user question for global reasoning..')
|
|
488
|
+
instructions = """In the context are chunks of text from a document.
|
|
489
|
+
Rewrite the user question in such a way that relevant information can be captured by a Large language model for summarization for the chunks of text in the context.
|
|
490
|
+
Only return the new question with no other information.
|
|
491
|
+
"""
|
|
492
|
+
# Initialize model for question refinement and summarization
|
|
493
|
+
qmodel = LLMlight(modelname=self.modelname, temperature=0.7, endpoint=self.endpoint)
|
|
494
|
+
# Create new query
|
|
495
|
+
new_query = qmodel.prompt(query=query, instructions=instructions)
|
|
496
|
+
else:
|
|
497
|
+
new_query = query
|
|
498
|
+
|
|
499
|
+
# 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'])
|
|
501
|
+
|
|
502
|
+
# Now summaries for the chunks
|
|
503
|
+
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)}')
|
|
506
|
+
|
|
507
|
+
prompt = f"""
|
|
508
|
+
### Context (Chunk {i+1} of {len(chunks)} from a larger document):
|
|
509
|
+
{chunk}
|
|
510
|
+
|
|
511
|
+
### Instructions:
|
|
512
|
+
You are an expert summarizer. For the given chunk of text:
|
|
513
|
+
- Extract all **key points, decisions, facts, and actions**.
|
|
514
|
+
- Ensure your analysis captures important ideas, implications, or patterns.
|
|
515
|
+
- Preserve the **logical flow** and **chronological order**.
|
|
516
|
+
- **Avoid repetition** or superficial statements.
|
|
517
|
+
- Focus on **explicit and implicit information** that could be relevant in the full document.
|
|
518
|
+
- Keep the summary **clear, precise**, and suitable for combining with other chunk summaries later.
|
|
519
|
+
|
|
520
|
+
### User Task:
|
|
521
|
+
Summarize this chunk comprehensively and professionally.
|
|
522
|
+
{query}
|
|
523
|
+
|
|
524
|
+
"""
|
|
525
|
+
|
|
526
|
+
# Summarize
|
|
527
|
+
response = self.requests_post_http(prompt, system, temperature=self.temperature, top_p=self.top_p, task='summarization', stream=stream, return_type='string')
|
|
528
|
+
# Append
|
|
529
|
+
summaries.append(response)
|
|
530
|
+
# Show
|
|
531
|
+
logger.debug(response)
|
|
532
|
+
|
|
533
|
+
# Filter out "N/A" summaries
|
|
534
|
+
summaries = [s for s in summaries if s.strip() != "N/A" and not any(err in s.strip()[:30] for err in ("400", "404"))]
|
|
535
|
+
# Final summarization pass over all collected summaries
|
|
536
|
+
summaries_final = "\n\n---\n\n".join([f"### Summary {i+1}:\n{s}" for i, s in enumerate(summaries)])
|
|
537
|
+
# Return
|
|
538
|
+
if return_per_chunk:
|
|
539
|
+
return summaries_final
|
|
540
|
+
|
|
541
|
+
# Create final prompt
|
|
542
|
+
prompt_final = f"""### Context:
|
|
543
|
+
Below are the individual summaries generated from multiple sequential chunks of a larger document. They are presented in order:
|
|
544
|
+
{summaries_final}
|
|
545
|
+
|
|
546
|
+
---
|
|
547
|
+
|
|
548
|
+
### Instructions:
|
|
549
|
+
{instructions}
|
|
550
|
+
|
|
551
|
+
### User Task:
|
|
552
|
+
You are an expert editor. Your goal is to synthesize the above summaries into **one complete, well-structured, and logically coherent document**. Ensure:
|
|
553
|
+
- Smooth transitions between sections.
|
|
554
|
+
- Elimination of redundancies and overlaps.
|
|
555
|
+
- Consistent tone, clarity, and structure.
|
|
556
|
+
- That all essential information from the summaries is preserved.
|
|
557
|
+
- The final result aligns with the given instructions.
|
|
558
|
+
|
|
559
|
+
Produce the final, polished document below:
|
|
560
|
+
"""
|
|
561
|
+
|
|
562
|
+
system_summaries = (
|
|
563
|
+
"You are a helpful and detail-oriented assistant. "
|
|
564
|
+
"Your task is to compile and structure summaries into a single coherent and well-formatted document. "
|
|
565
|
+
"Follow all instructions precisely."
|
|
566
|
+
"Preserve important details, maintain logical flow, and respect any formatting requirements, such as using headings or bullet points when relevant.",
|
|
567
|
+
"Output the final results in the same language as the instructions.",
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
final_response = self.requests_post_http(prompt_final, system_summaries, temperature=self.temperature, top_p=self.top_p, task='summarization', stream=False, return_type='string')
|
|
571
|
+
|
|
572
|
+
# Return
|
|
573
|
+
return final_response
|
|
574
|
+
|
|
575
|
+
def chunk_wise(self, query, context, instructions, system, top_chunks=0, return_per_chunk=False, stream=False):
|
|
576
|
+
"""Chunk-wise.
|
|
577
|
+
1. Break the document into chunks with overlapping parts to make sure we do not miss out.
|
|
578
|
+
2. Include the last two results in the prompt as context.
|
|
579
|
+
3. Analyze each chunk seperately following the instructions and system messages and jointly with the last 2 results.
|
|
580
|
+
|
|
581
|
+
"""
|
|
582
|
+
# 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'])
|
|
584
|
+
|
|
585
|
+
# Build a structured prompt that includes all previous summaries
|
|
586
|
+
response_list = []
|
|
587
|
+
for i, chunk in enumerate(tqdm(chunks, desc="Processing chunks", unit="chunk")):
|
|
588
|
+
logger.info(f'Working on text chunk {i+1}/{len(chunks)}')
|
|
589
|
+
|
|
590
|
+
if top_chunks > 0:
|
|
591
|
+
previous_results = '\n\n---\n\n'.join(response_list[-top_chunks:])
|
|
592
|
+
prompt = f"""### Context:
|
|
593
|
+
Previous Results:\n{previous_results}" if response_list else "Previous Results: No results because this is the initial chunk."
|
|
594
|
+
|
|
595
|
+
---
|
|
596
|
+
New Text Chunk (Part of a larger document, maintain continuity and coherence):
|
|
597
|
+
{chunk}
|
|
598
|
+
|
|
599
|
+
### Instructions:
|
|
600
|
+
- Apply the instructions to the new chunk **in the context** of the previous results.
|
|
601
|
+
- Preserve logical structure and clarity.
|
|
602
|
+
- Maintain coherence and avoid repetition with prior content.
|
|
603
|
+
- Focus on extracting structured and relevant information.
|
|
604
|
+
|
|
605
|
+
{instructions}
|
|
606
|
+
|
|
607
|
+
### User Question:
|
|
608
|
+
{query}
|
|
609
|
+
|
|
610
|
+
### Final Improved Results:
|
|
611
|
+
"""
|
|
612
|
+
else:
|
|
613
|
+
prompt = f"""
|
|
614
|
+
### Context (Chunk {i+1} of {len(chunks)} — part of a larger document):
|
|
615
|
+
{chunk}
|
|
616
|
+
|
|
617
|
+
---
|
|
618
|
+
|
|
619
|
+
### Instructions:
|
|
620
|
+
Carefully analyze the above chunk in isolation while considering that it is part of a broader document. Apply the following instructions to this specific chunk:
|
|
621
|
+
{instructions}
|
|
622
|
+
- Avoid repetition and irrelevant details.
|
|
623
|
+
- Be clear and concise so this output can later be integrated with others.
|
|
624
|
+
|
|
625
|
+
---
|
|
626
|
+
|
|
627
|
+
### User Question:
|
|
628
|
+
{query}
|
|
629
|
+
|
|
630
|
+
---
|
|
631
|
+
|
|
632
|
+
### Output:
|
|
633
|
+
Provide your detailed, coherent analysis of this chunk below.
|
|
634
|
+
"""
|
|
635
|
+
# Get the summary for the current chunk
|
|
636
|
+
chunk_result = self.requests_post_http(prompt, system, temperature=self.temperature, top_p=self.top_p, task='summarization', stream=stream, return_type='string')
|
|
637
|
+
response_list.append(chunk_result)
|
|
638
|
+
|
|
639
|
+
# Filter out "N/A" summaries
|
|
640
|
+
response_list = [s for s in response_list if s.strip() != "N/A" and not any(err in s.strip()[:30] for err in ("400", "404"))]
|
|
641
|
+
# Combine all results
|
|
642
|
+
response_total = "\n\n---\n\n".join([f"### Chunk {i+1}:\n{s}" for i, s in enumerate(response_list)])
|
|
643
|
+
# Return all chunk information
|
|
644
|
+
if return_per_chunk:
|
|
645
|
+
return response_total
|
|
646
|
+
|
|
647
|
+
if top_chunks > 0:
|
|
648
|
+
prompt_final = f"""### Context (results based on {len(chunks)} chunk of text):
|
|
649
|
+
{response_total}
|
|
650
|
+
|
|
651
|
+
---
|
|
652
|
+
|
|
653
|
+
### Task:
|
|
654
|
+
The context that is given to you contains the output of {len(chunks)} seperate text chunks.
|
|
655
|
+
Your task is to connect all the parts and make one output that is **coherent** and well-structured.
|
|
656
|
+
|
|
657
|
+
### Instructions:
|
|
658
|
+
- Maintain as much as possible the key insights but ensure logical flow.
|
|
659
|
+
- Connect insights smoothly while keeping essential details intact.
|
|
660
|
+
- If repetitions are detected across the parts, combine it.
|
|
661
|
+
{instructions}
|
|
662
|
+
|
|
663
|
+
Begin your response below:
|
|
664
|
+
"""
|
|
665
|
+
|
|
666
|
+
system_chunk_analysis = """
|
|
667
|
+
You are a meticulous and structured AI assistant that performs detailed analyses of long documents, broken into smaller chunks.
|
|
668
|
+
Your task is to analyze each chunk individually and extract relevant insights, observations, or structured responses based on specific user instructions.
|
|
669
|
+
|
|
670
|
+
- Always follow the given instructions precisely.
|
|
671
|
+
- If formatting is implied (e.g., headers, lists, bullet points), apply it clearly.
|
|
672
|
+
- Do not add summaries or conclusions beyond the current chunk.
|
|
673
|
+
- Avoid introducing outside knowledge or assumptions beyond what is present in the text.
|
|
674
|
+
- Your analysis should be standalone, yet written clearly enough to be compiled later with other parts.
|
|
675
|
+
"""
|
|
676
|
+
|
|
677
|
+
logger.info('Combining all information to create a single coherent output.')
|
|
678
|
+
# Create the final summary.
|
|
679
|
+
final_response = self.requests_post_http(prompt_final, system_chunk_analysis, temperature=self.temperature, top_p=self.top_p, task='summarization', stream=False, return_type='string')
|
|
680
|
+
else:
|
|
681
|
+
prompt_final = f"""### Context:
|
|
682
|
+
{response_total}
|
|
683
|
+
|
|
684
|
+
### Task:
|
|
685
|
+
Given to you is a text that is compiled after analyzing multiple seperate chunks of text.
|
|
686
|
+
Your task is to restructure the text so that it complies with the instructions.
|
|
687
|
+
|
|
688
|
+
### Instructions:
|
|
689
|
+
- Maintain as much as possible the key insights but ensure logical flow.
|
|
690
|
+
- Connect insights smoothly while keeping essential details intact.
|
|
691
|
+
- If repetitions are detected across the parts, combine it.
|
|
692
|
+
- If there are vagues expressions, rewrite it to improve the quality.
|
|
693
|
+
{instructions}
|
|
694
|
+
|
|
695
|
+
Begin your response below:
|
|
696
|
+
"""
|
|
697
|
+
|
|
698
|
+
system = "You are a helpfull assistant specialized in combining multiple results that belong together. You are permitted to make assumptions if it improves the results."
|
|
699
|
+
# Create the final summary.
|
|
700
|
+
final_response = self.requests_post_http(prompt_final, system, temperature=self.temperature, top_p=self.top_p, task='summarization', stream=False, return_type='string')
|
|
701
|
+
|
|
702
|
+
# Return
|
|
703
|
+
return final_response
|
|
704
|
+
# return {'response': final_response, 'response_per_chunk': response_total}
|
|
705
|
+
|
|
706
|
+
def parse_large_document(self, query, context, return_type='string'):
|
|
707
|
+
"""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
|
+
# Embedding
|
|
711
|
+
query_vector, chunk_vectors = self.fit_transform(query, chunks)
|
|
712
|
+
# Compute similarity
|
|
713
|
+
similarities = cosine_similarity(query_vector, chunk_vectors)[0]
|
|
714
|
+
# 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]
|
|
717
|
+
|
|
718
|
+
# Join relevant chunks and send as prompt
|
|
719
|
+
relevant_chunks = [chunks[i] for i in top_indices]
|
|
720
|
+
relevant_scores = [similarities[i] for i in top_indices]
|
|
721
|
+
|
|
722
|
+
# Set the return type
|
|
723
|
+
if return_type == 'score':
|
|
724
|
+
return list(zip(relevant_scores, relevant_chunks))
|
|
725
|
+
elif return_type == 'list':
|
|
726
|
+
return relevant_chunks
|
|
727
|
+
elif return_type == 'string_flat':
|
|
728
|
+
return " ".join(relevant_chunks)
|
|
729
|
+
else:
|
|
730
|
+
return "\n---------\n".join(relevant_chunks)
|
|
731
|
+
|
|
732
|
+
def fit_transform(self, query, chunks):
|
|
733
|
+
"""Converts context chunks and query into vector space representations based on the selected embedding method."""
|
|
734
|
+
if self.embedding == 'tfidf':
|
|
735
|
+
vectorizer = TfidfVectorizer()
|
|
736
|
+
chunk_vectors = vectorizer.fit_transform(chunks)
|
|
737
|
+
# dense_matrix = chunk_vectors.toarray() # Converts to a NumPy array
|
|
738
|
+
query_vector = vectorizer.transform([query])
|
|
739
|
+
elif self.embedding == 'bow':
|
|
740
|
+
vectorizer = CountVectorizer()
|
|
741
|
+
chunk_vectors = vectorizer.fit_transform(chunks)
|
|
742
|
+
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])
|
|
747
|
+
query_vector = query_vector.reshape(1, -1)
|
|
748
|
+
else:
|
|
749
|
+
raise ValueError("Unsupported embedding method. Choose a supported embedding method.")
|
|
750
|
+
return query_vector, chunk_vectors
|
|
751
|
+
|
|
752
|
+
def compute_preprocessing(self, query, context, instructions, system):
|
|
753
|
+
# Create advanced prompt using relevant chunks of text, the input query and instructions
|
|
754
|
+
if context is not None:
|
|
755
|
+
if self.preprocessing=='global-reasoning':
|
|
756
|
+
# Global Reasoning
|
|
757
|
+
relevant_context = self.global_reasoning(query, context, instructions, system, rewrite_query=False, return_per_chunk=True)
|
|
758
|
+
elif self.preprocessing=='chunk-wise':
|
|
759
|
+
# Analyze per chunk
|
|
760
|
+
relevant_context = self.chunk_wise(query, context, instructions, system, top_chunks=0, return_per_chunk=True)
|
|
761
|
+
else:
|
|
762
|
+
logger.info(f'No method is applied: The entire context is used.')
|
|
763
|
+
relevant_context = context
|
|
764
|
+
else:
|
|
765
|
+
# Default
|
|
766
|
+
relevant_context = context
|
|
767
|
+
|
|
768
|
+
# Return
|
|
769
|
+
return relevant_context
|
|
770
|
+
|
|
771
|
+
def relevant_text_retrieval(self, query, context, instructions, system):
|
|
772
|
+
# Create advanced prompt using relevant chunks of text, the input query and instructions
|
|
773
|
+
if context is not None:
|
|
774
|
+
if self.method == 'naive_RAG' and np.isin(self.embedding, ['tfidf', 'bow', 'bert', 'bge-small']):
|
|
775
|
+
# 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)
|
|
781
|
+
else:
|
|
782
|
+
logger.info(f'No method is applied: The entire context is used.')
|
|
783
|
+
relevant_context = context
|
|
784
|
+
else:
|
|
785
|
+
# Default
|
|
786
|
+
relevant_context = context
|
|
787
|
+
|
|
788
|
+
# Return
|
|
789
|
+
return relevant_context
|
|
790
|
+
|
|
791
|
+
def set_prompt(self, query, instructions, context, response_format=''):
|
|
792
|
+
# Default and update when context and instructions are available.
|
|
793
|
+
prompt = (
|
|
794
|
+
("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"
|
|
798
|
+
+ query
|
|
799
|
+
)
|
|
800
|
+
|
|
801
|
+
# Return
|
|
802
|
+
return prompt
|
|
803
|
+
|
|
804
|
+
def read_pdf(self, filepath, title_pages=[1, 2], body_pages=[], reference_pages=[-1], return_type='dict'):
|
|
805
|
+
"""
|
|
806
|
+
Reads a PDF file and extracts its text content as a string.
|
|
807
|
+
|
|
808
|
+
Args:
|
|
809
|
+
pdf_path (str): Path to the PDF file.
|
|
810
|
+
|
|
811
|
+
Returns:
|
|
812
|
+
str: Extracted text from the PDF.
|
|
813
|
+
|
|
814
|
+
"""
|
|
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}"
|
|
823
|
+
else:
|
|
824
|
+
logger.warning(f'{filepath} does not exist.')
|
|
825
|
+
self.context = None
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
def convert_prompt(messages, modelname='llama', add_assistant_start=True):
|
|
829
|
+
"""
|
|
830
|
+
Builds a prompt in the appropriate format for different models (LLaMA, Grok, Mistral).
|
|
831
|
+
|
|
832
|
+
Args:
|
|
833
|
+
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').
|
|
835
|
+
add_assistant_start (bool): Whether to add the assistant start (default True).
|
|
836
|
+
add_bos_token (bool): Helps models know it's a fresh conversation. Useful for llama/mistral/hermes-style models
|
|
837
|
+
|
|
838
|
+
Returns:
|
|
839
|
+
str: The final prompt string in the correct format for the given model.
|
|
840
|
+
|
|
841
|
+
Example:
|
|
842
|
+
>>> messages = [
|
|
843
|
+
... {"role": "system", "content": "You are a helpful assistant."},
|
|
844
|
+
... {"role": "user", "content": "What is the capital of France?"}
|
|
845
|
+
... ]
|
|
846
|
+
>>> prompt = convert_prompt(messages, modelname='llama')
|
|
847
|
+
>>> print(prompt)
|
|
848
|
+
|
|
849
|
+
"""
|
|
850
|
+
prompt = ""
|
|
851
|
+
|
|
852
|
+
# if add_bos_token and ('llama' in modelname or 'mistral' in modelname):
|
|
853
|
+
# prompt += "<|begin_of_text|>\n"
|
|
854
|
+
|
|
855
|
+
for msg in messages:
|
|
856
|
+
role = msg["role"]
|
|
857
|
+
content = msg["content"].strip()
|
|
858
|
+
|
|
859
|
+
if 'llama' in modelname or 'mistral' in modelname:
|
|
860
|
+
prompt += f"<|im_start|>{role}\n{content}\n<|im_end|>\n"
|
|
861
|
+
elif 'grok' in modelname:
|
|
862
|
+
prompt += f"<start_of_turn>{role}\n{content}<end_of_turn>\n"
|
|
863
|
+
else:
|
|
864
|
+
# Default to ChatML format if model not recognized
|
|
865
|
+
prompt += f"<|im_start|>{role}\n{content}\n<|im_end|>\n"
|
|
866
|
+
|
|
867
|
+
if add_assistant_start:
|
|
868
|
+
if 'llama' in modelname or 'mistral' in modelname:
|
|
869
|
+
prompt += "<|im_start|>assistant\n"
|
|
870
|
+
elif 'grok' in modelname:
|
|
871
|
+
prompt += "<start_of_turn>assistant\n"
|
|
872
|
+
|
|
873
|
+
return prompt
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
def load_local_gguf_model(model_path: str, n_ctx: int=4096, n_threads: int=8, n_gpu_layers: int=0, verbose: bool=True) -> Llama:
|
|
878
|
+
"""
|
|
879
|
+
Loads a local GGUF model using llama-cpp-python.
|
|
880
|
+
|
|
881
|
+
Args:
|
|
882
|
+
model_path (str): Path to the .gguf model file.
|
|
883
|
+
n_ctx (int): Maximum context length. Default is 4096.
|
|
884
|
+
n_threads (int): Number of CPU threads to use. Default is 8.
|
|
885
|
+
n_gpu_layers (int): Number of layers to offload to GPU (if available). Default is 20.
|
|
886
|
+
verbose (bool): Whether to print status info.
|
|
887
|
+
|
|
888
|
+
Returns:
|
|
889
|
+
Llama: The loaded Llama model object.
|
|
890
|
+
|
|
891
|
+
Example:
|
|
892
|
+
>>> model_path = r'C://Users//beeld//.lmstudio//models//NousResearch//Hermes-3-Llama-3.2-3B-GGUF//Hermes-3-Llama-3.2-3B.Q4_K_M.gguf'
|
|
893
|
+
>>> llm = load_local_gguf_model(model_path, verbose=True)
|
|
894
|
+
>>> prompt = "<start_of_turn>user\\nWhat is 2 + 2?\\n<end_of_turn>\\n<start_of_turn>model\\n"
|
|
895
|
+
>>> response = llm(prompt=prompt, max_tokens=20, stop=["<end_of_turn>"])
|
|
896
|
+
>>> print(response["choices"][0]["text"].strip())
|
|
897
|
+
'4'
|
|
898
|
+
|
|
899
|
+
"""
|
|
900
|
+
if not os.path.exists(model_path):
|
|
901
|
+
raise FileNotFoundError(f"Model file not found at: {model_path}")
|
|
902
|
+
|
|
903
|
+
logger.info(f"Loading model from {model_path}")
|
|
904
|
+
logger.info(f"Context length: {n_ctx}, Threads: {n_threads}, GPU layers: {n_gpu_layers}")
|
|
905
|
+
|
|
906
|
+
llm = Llama(
|
|
907
|
+
model_path=model_path,
|
|
908
|
+
n_ctx=n_ctx,
|
|
909
|
+
n_threads=n_threads,
|
|
910
|
+
n_gpu_layers=n_gpu_layers,
|
|
911
|
+
verbose=verbose
|
|
912
|
+
)
|
|
913
|
+
|
|
914
|
+
logger.info("Model loaded successfully!")
|
|
915
|
+
# Return
|
|
916
|
+
return llm
|
|
917
|
+
|
|
918
|
+
def compute_tokens(string, n_ctx=4096, task='full'):
|
|
919
|
+
tokenizer = AutoTokenizer.from_pretrained("gpt2")
|
|
920
|
+
# Tokenize the input string
|
|
921
|
+
tokens = tokenizer.encode(string, truncation=True, max_length=n_ctx)
|
|
922
|
+
# Get the number of tokens
|
|
923
|
+
used_tokens = len(tokens)
|
|
924
|
+
# Determine how many tokens are available for the model to generate
|
|
925
|
+
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}")
|
|
927
|
+
return used_tokens, max_tokens
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
def compute_max_tokens(used_tokens, n_ctx=4096, task="full"):
|
|
931
|
+
"""
|
|
932
|
+
Compute the number of tokens that can be generated based on the task type.
|
|
933
|
+
|
|
934
|
+
Parameters:
|
|
935
|
+
- used_tokens (int): Tokens already used in the input prompt.
|
|
936
|
+
- n_ctx (int): Total context window size of the model.
|
|
937
|
+
- task (str): The use case for generation. Options:
|
|
938
|
+
'summarization', 'chat', 'code', 'longform', 'full'
|
|
939
|
+
|
|
940
|
+
Returns:
|
|
941
|
+
- max_tokens (int): Tokens allowed for generation.
|
|
942
|
+
"""
|
|
943
|
+
|
|
944
|
+
available_tokens = max(n_ctx - used_tokens, 1) # Ensure at least 1
|
|
945
|
+
|
|
946
|
+
task = task.lower()
|
|
947
|
+
if task == "summarization":
|
|
948
|
+
max_tokens = max(min(available_tokens, int(n_ctx * 0.5)), 128)
|
|
949
|
+
elif task == "chat":
|
|
950
|
+
max_tokens = max(min(available_tokens, int(n_ctx * 0.6)), 128)
|
|
951
|
+
elif task == "code":
|
|
952
|
+
max_tokens = max(min(available_tokens, int(n_ctx * 0.75)), 128)
|
|
953
|
+
elif task == "longform":
|
|
954
|
+
max_tokens = max(min(available_tokens, int(n_ctx * 0.9)), 256)
|
|
955
|
+
elif task == "full":
|
|
956
|
+
max_tokens = available_tokens
|
|
957
|
+
else:
|
|
958
|
+
# Default to safe fallback
|
|
959
|
+
max_tokens = max(min(available_tokens, int(n_ctx * 0.5)), 128)
|
|
960
|
+
|
|
961
|
+
return max_tokens
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
def set_system_message(system):
|
|
965
|
+
return "You are a helpful assistant." if system is None else system
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
# %%
|
|
970
|
+
def convert_verbose_to_new(verbose):
|
|
971
|
+
"""Convert old verbosity to the new."""
|
|
972
|
+
# In case the new verbosity is used, convert to the old one.
|
|
973
|
+
if verbose is None: verbose=0
|
|
974
|
+
if not isinstance(verbose, str) and verbose<10:
|
|
975
|
+
status_map = {
|
|
976
|
+
'None': 'silent',
|
|
977
|
+
0: 'silent',
|
|
978
|
+
6: 'silent',
|
|
979
|
+
1: 'critical',
|
|
980
|
+
2: 'warning',
|
|
981
|
+
3: 'info',
|
|
982
|
+
4: 'debug',
|
|
983
|
+
5: 'debug'}
|
|
984
|
+
if verbose>=2: print('[LLMlight] WARNING use the standardized verbose status. The status [1-6] will be deprecated in future versions.')
|
|
985
|
+
return status_map.get(verbose, 0)
|
|
986
|
+
else:
|
|
987
|
+
return verbose
|
|
988
|
+
|
|
989
|
+
def get_logger():
|
|
990
|
+
return logger.getEffectiveLevel()
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
def set_logger(verbose: [str, int] = 'info'):
|
|
994
|
+
"""Set the logger for verbosity messages.
|
|
995
|
+
|
|
996
|
+
Parameters
|
|
997
|
+
----------
|
|
998
|
+
verbose : [str, int], default is 'info' or 20
|
|
999
|
+
Set the verbose messages using string or integer values.
|
|
1000
|
+
* [0, 60, None, 'silent', 'off', 'no']: No message.
|
|
1001
|
+
* [10, 'debug']: Messages from debug level and higher.
|
|
1002
|
+
* [20, 'info']: Messages from info level and higher.
|
|
1003
|
+
* [30, 'warning']: Messages from warning level and higher.
|
|
1004
|
+
* [50, 'critical', 'error']: Messages from critical level and higher.
|
|
1005
|
+
|
|
1006
|
+
Returns
|
|
1007
|
+
-------
|
|
1008
|
+
None.
|
|
1009
|
+
|
|
1010
|
+
> # Set the logger to warning
|
|
1011
|
+
> set_logger(verbose='warning')
|
|
1012
|
+
> # Test with different messages
|
|
1013
|
+
> logger.debug("Hello debug")
|
|
1014
|
+
> logger.info("Hello info")
|
|
1015
|
+
> logger.warning("Hello warning")
|
|
1016
|
+
> logger.critical("Hello critical")
|
|
1017
|
+
|
|
1018
|
+
"""
|
|
1019
|
+
# Convert verbose to new
|
|
1020
|
+
verbose = convert_verbose_to_new(verbose)
|
|
1021
|
+
# Set 0 and None as no messages.
|
|
1022
|
+
if (verbose==0) or (verbose is None):
|
|
1023
|
+
verbose=60
|
|
1024
|
+
# Convert str to levels
|
|
1025
|
+
if isinstance(verbose, str):
|
|
1026
|
+
levels = {'silent': 60,
|
|
1027
|
+
'off': 60,
|
|
1028
|
+
'no': 60,
|
|
1029
|
+
'debug': 10,
|
|
1030
|
+
'info': 20,
|
|
1031
|
+
'warning': 30,
|
|
1032
|
+
'error': 50,
|
|
1033
|
+
'critical': 50}
|
|
1034
|
+
verbose = levels[verbose]
|
|
1035
|
+
|
|
1036
|
+
# Configure root logger if no handlers exist
|
|
1037
|
+
# if not logger.handlers:
|
|
1038
|
+
# handler = logging.StreamHandler()
|
|
1039
|
+
# fmt = '[{asctime}] [{name}] [{levelname}] {msg}'
|
|
1040
|
+
# formatter = logging.Formatter(fmt=fmt, style='{', datefmt='%d-%m-%Y %H:%M:%S')
|
|
1041
|
+
# handler.setFormatter(formatter)
|
|
1042
|
+
# logger.addHandler(handler)
|
|
1043
|
+
|
|
1044
|
+
# Set the level
|
|
1045
|
+
logger.setLevel(verbose)
|
|
1046
|
+
|
|
1047
|
+
|
|
1048
|
+
def disable_tqdm():
|
|
1049
|
+
"""Set the logger for verbosity messages."""
|
|
1050
|
+
return (True if (logger.getEffectiveLevel()>=30) else False)
|
|
1051
|
+
|
|
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')
|