lollms-client 0.29.0__py3-none-any.whl → 0.29.1__py3-none-any.whl
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.
Potentially problematic release.
This version of lollms-client might be problematic. Click here for more details.
- examples/text_gen.py +1 -1
- lollms_client/__init__.py +1 -1
- lollms_client/llm_bindings/llamacpp/__init__.py +1 -0
- lollms_client/llm_bindings/lollms/__init__.py +411 -267
- lollms_client/llm_bindings/lollms_webui/__init__.py +428 -0
- lollms_client/lollms_core.py +151 -124
- lollms_client/lollms_discussion.py +262 -38
- lollms_client/lollms_utilities.py +10 -2
- {lollms_client-0.29.0.dist-info → lollms_client-0.29.1.dist-info}/METADATA +248 -47
- {lollms_client-0.29.0.dist-info → lollms_client-0.29.1.dist-info}/RECORD +13 -13
- lollms_client/llm_bindings/lollms_chat/__init__.py +0 -571
- {lollms_client-0.29.0.dist-info → lollms_client-0.29.1.dist-info}/WHEEL +0 -0
- {lollms_client-0.29.0.dist-info → lollms_client-0.29.1.dist-info}/licenses/LICENSE +0 -0
- {lollms_client-0.29.0.dist-info → lollms_client-0.29.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
# bindings/lollms/binding.py
|
|
2
|
+
import requests
|
|
3
|
+
from lollms_client.lollms_llm_binding import LollmsLLMBinding
|
|
4
|
+
from lollms_client.lollms_types import MSG_TYPE
|
|
5
|
+
from lollms_client.lollms_utilities import encode_image
|
|
6
|
+
from lollms_client.lollms_types import ELF_COMPLETION_FORMAT
|
|
7
|
+
from lollms_client.lollms_discussion import LollmsDiscussion
|
|
8
|
+
from ascii_colors import ASCIIColors, trace_exception
|
|
9
|
+
from typing import Optional, Callable, List, Union
|
|
10
|
+
import json
|
|
11
|
+
|
|
12
|
+
BindingName = "LollmsWebuiLLMBinding"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LollmsWebuiLLMBinding(LollmsLLMBinding):
|
|
16
|
+
"""LOLLMS-specific binding implementation"""
|
|
17
|
+
|
|
18
|
+
DEFAULT_HOST_ADDRESS = "http://localhost:9600"
|
|
19
|
+
|
|
20
|
+
def __init__(self,
|
|
21
|
+
host_address: str = None,
|
|
22
|
+
model_name: str = "",
|
|
23
|
+
service_key: str = None,
|
|
24
|
+
verify_ssl_certificate: bool = True,
|
|
25
|
+
personality: Optional[int] = None,
|
|
26
|
+
**kwargs
|
|
27
|
+
):
|
|
28
|
+
"""
|
|
29
|
+
Initialize the LOLLMS binding.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
host_address (str): Host address for the LOLLMS service. Defaults to DEFAULT_HOST_ADDRESS.
|
|
33
|
+
model_name (str): Name of the model to use. Defaults to empty string.
|
|
34
|
+
service_key (str): Authentication key for the service. Defaults to None.
|
|
35
|
+
verify_ssl_certificate (bool): Whether to verify SSL certificates. Defaults to True.
|
|
36
|
+
personality (Optional[int]): Personality ID for generation. Defaults to None.
|
|
37
|
+
"""
|
|
38
|
+
super().__init__(
|
|
39
|
+
binding_name = "lollms"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
self.host_address=host_address if host_address is not None else self.DEFAULT_HOST_ADDRESS
|
|
43
|
+
self.model_name=model_name
|
|
44
|
+
self.service_key=service_key
|
|
45
|
+
self.verify_ssl_certificate=verify_ssl_certificate
|
|
46
|
+
self.default_completion_format=kwargs.get("default_completion_format",ELF_COMPLETION_FORMAT.Chat)
|
|
47
|
+
self.personality = personality
|
|
48
|
+
self.model = None
|
|
49
|
+
|
|
50
|
+
def generate_text(self,
|
|
51
|
+
prompt: str,
|
|
52
|
+
images: Optional[List[str]] = None,
|
|
53
|
+
system_prompt: str = "",
|
|
54
|
+
n_predict: Optional[int] = None,
|
|
55
|
+
stream: Optional[bool] = None,
|
|
56
|
+
temperature: Optional[float] = None,
|
|
57
|
+
top_k: Optional[int] = None,
|
|
58
|
+
top_p: Optional[float] = None,
|
|
59
|
+
repeat_penalty: Optional[float] = None,
|
|
60
|
+
repeat_last_n: Optional[int] = None,
|
|
61
|
+
seed: Optional[int] = None,
|
|
62
|
+
n_threads: Optional[int] = None,
|
|
63
|
+
ctx_size: int | None = None,
|
|
64
|
+
streaming_callback: Optional[Callable[[str, MSG_TYPE], None]] = None,
|
|
65
|
+
split:Optional[bool]=False, # put to true if the prompt is a discussion
|
|
66
|
+
user_keyword:Optional[str]="!@>user:",
|
|
67
|
+
ai_keyword:Optional[str]="!@>assistant:",
|
|
68
|
+
) -> Union[str, dict]:
|
|
69
|
+
"""
|
|
70
|
+
Generate text using the active LLM binding, using instance defaults if parameters are not provided.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
prompt (str): The input prompt for text generation.
|
|
74
|
+
images (Optional[List[str]]): List of image file paths for multimodal generation.
|
|
75
|
+
n_predict (Optional[int]): Maximum number of tokens to generate. Uses instance default if None.
|
|
76
|
+
stream (Optional[bool]): Whether to stream the output. Uses instance default if None.
|
|
77
|
+
temperature (Optional[float]): Sampling temperature. Uses instance default if None.
|
|
78
|
+
top_k (Optional[int]): Top-k sampling parameter. Uses instance default if None.
|
|
79
|
+
top_p (Optional[float]): Top-p sampling parameter. Uses instance default if None.
|
|
80
|
+
repeat_penalty (Optional[float]): Penalty for repeated tokens. Uses instance default if None.
|
|
81
|
+
repeat_last_n (Optional[int]): Number of previous tokens to consider for repeat penalty. Uses instance default if None.
|
|
82
|
+
seed (Optional[int]): Random seed for generation. Uses instance default if None.
|
|
83
|
+
n_threads (Optional[int]): Number of threads to use. Uses instance default if None.
|
|
84
|
+
ctx_size (int | None): Context size override for this generation.
|
|
85
|
+
streaming_callback (Optional[Callable[[str, str], None]]): Callback function for streaming output.
|
|
86
|
+
- First parameter (str): The chunk of text received.
|
|
87
|
+
- Second parameter (str): The message type (e.g., MSG_TYPE.MSG_TYPE_CHUNK).
|
|
88
|
+
split:Optional[bool]: put to true if the prompt is a discussion
|
|
89
|
+
user_keyword:Optional[str]: when splitting we use this to extract user prompt
|
|
90
|
+
ai_keyword:Optional[str]": when splitting we use this to extract ai prompt
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Union[str, dict]: Generated text or error dictionary if failed.
|
|
94
|
+
"""
|
|
95
|
+
# Determine endpoint based on presence of images
|
|
96
|
+
endpoint = "/lollms_generate_with_images" if images else "/lollms_generate"
|
|
97
|
+
url = f"{self.host_address}{endpoint}"
|
|
98
|
+
|
|
99
|
+
# Set headers
|
|
100
|
+
headers = {
|
|
101
|
+
'Content-Type': 'application/json',
|
|
102
|
+
}
|
|
103
|
+
if self.service_key:
|
|
104
|
+
headers['Authorization'] = f'Bearer {self.service_key}'
|
|
105
|
+
|
|
106
|
+
# Handle images if provided
|
|
107
|
+
image_data = []
|
|
108
|
+
if images:
|
|
109
|
+
for image_path in images:
|
|
110
|
+
try:
|
|
111
|
+
encoded_image = encode_image(image_path)
|
|
112
|
+
image_data.append(encoded_image)
|
|
113
|
+
except Exception as e:
|
|
114
|
+
return {"status": False, "error": f"Failed to process image {image_path}: {str(e)}"}
|
|
115
|
+
|
|
116
|
+
# Prepare request data
|
|
117
|
+
data = {
|
|
118
|
+
"prompt":"!@>system: "+system_prompt+"\n"+"!@>user: "+prompt if system_prompt else prompt,
|
|
119
|
+
"model_name": self.model_name,
|
|
120
|
+
"personality": self.personality,
|
|
121
|
+
"n_predict": n_predict,
|
|
122
|
+
"stream": stream,
|
|
123
|
+
"temperature": temperature,
|
|
124
|
+
"top_k": top_k,
|
|
125
|
+
"top_p": top_p,
|
|
126
|
+
"repeat_penalty": repeat_penalty,
|
|
127
|
+
"repeat_last_n": repeat_last_n,
|
|
128
|
+
"seed": seed,
|
|
129
|
+
"n_threads": n_threads
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if image_data:
|
|
133
|
+
data["images"] = image_data
|
|
134
|
+
|
|
135
|
+
# Make the request
|
|
136
|
+
response = requests.post(
|
|
137
|
+
url,
|
|
138
|
+
json=data,
|
|
139
|
+
headers=headers,
|
|
140
|
+
stream=stream,
|
|
141
|
+
verify=self.verify_ssl_certificate
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if not stream:
|
|
145
|
+
if response.status_code == 200:
|
|
146
|
+
try:
|
|
147
|
+
text = response.text.strip()
|
|
148
|
+
return text
|
|
149
|
+
except Exception as ex:
|
|
150
|
+
return {"status": False, "error": str(ex)}
|
|
151
|
+
else:
|
|
152
|
+
return {"status": False, "error": response.text}
|
|
153
|
+
else:
|
|
154
|
+
text = ""
|
|
155
|
+
if response.status_code == 200:
|
|
156
|
+
try:
|
|
157
|
+
for line in response.iter_lines():
|
|
158
|
+
chunk = line.decode("utf-8")
|
|
159
|
+
text += chunk
|
|
160
|
+
if streaming_callback:
|
|
161
|
+
streaming_callback(chunk, MSG_TYPE.MSG_TYPE_CHUNK)
|
|
162
|
+
# Handle potential quotes from streaming response
|
|
163
|
+
if text and text[0] == '"':
|
|
164
|
+
text = text[1:]
|
|
165
|
+
if text and text[-1] == '"':
|
|
166
|
+
text = text[:-1]
|
|
167
|
+
return text.rstrip('!')
|
|
168
|
+
except Exception as ex:
|
|
169
|
+
return {"status": False, "error": str(ex)}
|
|
170
|
+
else:
|
|
171
|
+
return {"status": False, "error": response.text}
|
|
172
|
+
def chat(self,
|
|
173
|
+
discussion: LollmsDiscussion,
|
|
174
|
+
branch_tip_id: Optional[str] = None,
|
|
175
|
+
n_predict: Optional[int] = None,
|
|
176
|
+
stream: Optional[bool] = None,
|
|
177
|
+
temperature: Optional[float] = None,
|
|
178
|
+
top_k: Optional[int] = None,
|
|
179
|
+
top_p: Optional[float] = None,
|
|
180
|
+
repeat_penalty: Optional[float] = None,
|
|
181
|
+
repeat_last_n: Optional[int] = None,
|
|
182
|
+
seed: Optional[int] = None,
|
|
183
|
+
n_threads: Optional[int] = None,
|
|
184
|
+
ctx_size: int | None = None,
|
|
185
|
+
streaming_callback: Optional[Callable[[str, MSG_TYPE], None]] = None
|
|
186
|
+
) -> Union[str, dict]:
|
|
187
|
+
"""
|
|
188
|
+
Conduct a chat session with a lollms-webui server using a LollmsDiscussion object.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
discussion (LollmsDiscussion): The discussion object containing the conversation history.
|
|
192
|
+
branch_tip_id (Optional[str]): The ID of the message to use as the tip of the conversation branch. Defaults to the active branch.
|
|
193
|
+
... (other parameters) ...
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Union[str, dict]: The generated text or an error dictionary.
|
|
197
|
+
"""
|
|
198
|
+
# 1. Export the discussion to the lollms-native text format
|
|
199
|
+
prompt_text = discussion.export("lollms_text", branch_tip_id)
|
|
200
|
+
|
|
201
|
+
# 2. Extract images from the LAST message of the branch
|
|
202
|
+
# lollms-webui's endpoint associates images with the final prompt
|
|
203
|
+
active_branch_id = branch_tip_id or discussion.active_branch_id
|
|
204
|
+
branch = discussion.get_branch(active_branch_id)
|
|
205
|
+
last_message = branch[-1] if branch else None
|
|
206
|
+
|
|
207
|
+
image_data = []
|
|
208
|
+
if last_message and last_message.images:
|
|
209
|
+
# The endpoint expects a list of base64 strings.
|
|
210
|
+
# We will only process images of type 'base64'. URL types are not supported by this endpoint.
|
|
211
|
+
for img in last_message.images:
|
|
212
|
+
if img['type'] == 'base64':
|
|
213
|
+
image_data.append(img['data'])
|
|
214
|
+
# Note: 'url' type images are ignored for this binding.
|
|
215
|
+
|
|
216
|
+
# 3. Determine endpoint and build payload
|
|
217
|
+
endpoint = "/lollms_generate_with_images" if image_data else "/lollms_generate"
|
|
218
|
+
url = f"{self.host_address}{endpoint}"
|
|
219
|
+
|
|
220
|
+
headers = {'Content-Type': 'application/json'}
|
|
221
|
+
if self.service_key:
|
|
222
|
+
headers['Authorization'] = f'Bearer {self.service_key}'
|
|
223
|
+
|
|
224
|
+
data = {
|
|
225
|
+
"prompt": prompt_text,
|
|
226
|
+
"model_name": self.model_name,
|
|
227
|
+
"personality": self.personality,
|
|
228
|
+
"n_predict": n_predict,
|
|
229
|
+
"stream": stream,
|
|
230
|
+
"temperature": temperature,
|
|
231
|
+
"top_k": top_k,
|
|
232
|
+
"top_p": top_p,
|
|
233
|
+
"repeat_penalty": repeat_penalty,
|
|
234
|
+
"repeat_last_n": repeat_last_n,
|
|
235
|
+
"seed": seed,
|
|
236
|
+
"n_threads": n_threads
|
|
237
|
+
}
|
|
238
|
+
if image_data:
|
|
239
|
+
data["images"] = image_data
|
|
240
|
+
|
|
241
|
+
# 4. Make the request (logic copied and adapted from generate_text)
|
|
242
|
+
try:
|
|
243
|
+
response = requests.post(
|
|
244
|
+
url,
|
|
245
|
+
json=data,
|
|
246
|
+
headers=headers,
|
|
247
|
+
stream=stream,
|
|
248
|
+
verify=self.verify_ssl_certificate
|
|
249
|
+
)
|
|
250
|
+
response.raise_for_status() # Raise an exception for bad status codes
|
|
251
|
+
|
|
252
|
+
if not stream:
|
|
253
|
+
return response.text.strip()
|
|
254
|
+
else:
|
|
255
|
+
full_response_text = ""
|
|
256
|
+
for line in response.iter_lines():
|
|
257
|
+
if line:
|
|
258
|
+
chunk = line.decode("utf-8")
|
|
259
|
+
full_response_text += chunk
|
|
260
|
+
if streaming_callback:
|
|
261
|
+
if not streaming_callback(chunk, MSG_TYPE.MSG_TYPE_CHUNK):
|
|
262
|
+
break
|
|
263
|
+
# Clean up potential quotes from some streaming formats
|
|
264
|
+
if full_response_text.startswith('"') and full_response_text.endswith('"'):
|
|
265
|
+
full_response_text = full_response_text[1:-1]
|
|
266
|
+
return full_response_text.rstrip('!')
|
|
267
|
+
|
|
268
|
+
except requests.exceptions.RequestException as e:
|
|
269
|
+
error_message = f"lollms-webui request error: {e}"
|
|
270
|
+
return {"status": "error", "message": error_message}
|
|
271
|
+
except Exception as ex:
|
|
272
|
+
error_message = f"lollms-webui generation error: {str(ex)}"
|
|
273
|
+
return {"status": "error", "message": error_message}
|
|
274
|
+
def tokenize(self, text: str) -> list:
|
|
275
|
+
"""
|
|
276
|
+
Tokenize the input text into a list of tokens using the /lollms_tokenize endpoint.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
text (str): The text to tokenize.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
list: List of tokens.
|
|
283
|
+
"""
|
|
284
|
+
response=None
|
|
285
|
+
try:
|
|
286
|
+
# Prepare the request payload
|
|
287
|
+
payload = {
|
|
288
|
+
"prompt": text,
|
|
289
|
+
"return_named": False # Set to True if you want named tokens
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
# Make the POST request to the /lollms_tokenize endpoint
|
|
293
|
+
response = requests.post(f"{self.host_address}/lollms_tokenize", json=payload)
|
|
294
|
+
|
|
295
|
+
# Check if the request was successful
|
|
296
|
+
if response.status_code == 200:
|
|
297
|
+
return response.json()
|
|
298
|
+
else:
|
|
299
|
+
raise Exception(f"Failed to tokenize text: {response.text}")
|
|
300
|
+
except Exception as ex:
|
|
301
|
+
trace_exception(ex)
|
|
302
|
+
raise Exception(f"Failed to tokenize text: {response.text}")
|
|
303
|
+
|
|
304
|
+
def detokenize(self, tokens: list) -> str:
|
|
305
|
+
"""
|
|
306
|
+
Convert a list of tokens back to text using the /lollms_detokenize endpoint.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
tokens (list): List of tokens to detokenize.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
str: Detokenized text.
|
|
313
|
+
"""
|
|
314
|
+
try:
|
|
315
|
+
# Prepare the request payload
|
|
316
|
+
payload = {
|
|
317
|
+
"tokens": tokens,
|
|
318
|
+
"return_named": False # Set to True if you want named tokens
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
# Make the POST request to the /lollms_detokenize endpoint
|
|
322
|
+
response = requests.post(f"{self.host_address}/lollms_detokenize", json=payload)
|
|
323
|
+
|
|
324
|
+
# Check if the request was successful
|
|
325
|
+
if response.status_code == 200:
|
|
326
|
+
return response.json()
|
|
327
|
+
else:
|
|
328
|
+
raise Exception(f"Failed to detokenize tokens: {response.text}")
|
|
329
|
+
except Exception as ex:
|
|
330
|
+
return {"status": False, "error": str(ex)}
|
|
331
|
+
|
|
332
|
+
def count_tokens(self, text: str) -> int:
|
|
333
|
+
"""
|
|
334
|
+
Count tokens from a text.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
tokens (list): List of tokens to detokenize.
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
int: Number of tokens in text.
|
|
341
|
+
"""
|
|
342
|
+
return len(self.tokenize(text))
|
|
343
|
+
|
|
344
|
+
def embed(self, text: str, **kwargs) -> list:
|
|
345
|
+
"""
|
|
346
|
+
Get embeddings for the input text using Ollama API
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
text (str or List[str]): Input text to embed
|
|
350
|
+
**kwargs: Additional arguments like model, truncate, options, keep_alive
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
dict: Response containing embeddings
|
|
354
|
+
"""
|
|
355
|
+
api_key = kwargs.pop("api_key", None)
|
|
356
|
+
headers = (
|
|
357
|
+
{"Content-Type": "application/json", "Authorization": api_key}
|
|
358
|
+
if api_key
|
|
359
|
+
else {"Content-Type": "application/json"}
|
|
360
|
+
)
|
|
361
|
+
embeddings = []
|
|
362
|
+
request_data = {"text": text}
|
|
363
|
+
response = requests.post(f"{self.host_address}/lollms_embed", json=request_data, headers=headers)
|
|
364
|
+
response.raise_for_status()
|
|
365
|
+
result = response.json()
|
|
366
|
+
return result["vector"]
|
|
367
|
+
|
|
368
|
+
def get_model_info(self) -> dict:
|
|
369
|
+
"""
|
|
370
|
+
Return information about the current LOLLMS model.
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
dict: Dictionary containing model name, version, host address, and personality.
|
|
374
|
+
"""
|
|
375
|
+
return {
|
|
376
|
+
"name": "lollms",
|
|
377
|
+
"version": "1.0",
|
|
378
|
+
"host_address": self.host_address,
|
|
379
|
+
"model_name": self.model_name,
|
|
380
|
+
"personality": self.personality
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def listModels(self) -> dict:
|
|
385
|
+
"""Lists models"""
|
|
386
|
+
url = f"{self.host_address}/list_models"
|
|
387
|
+
|
|
388
|
+
response = requests.get(url)
|
|
389
|
+
|
|
390
|
+
if response.status_code == 200:
|
|
391
|
+
try:
|
|
392
|
+
models = json.loads(response.content.decode("utf-8"))
|
|
393
|
+
return [{"model_name":m} for m in models]
|
|
394
|
+
except Exception as ex:
|
|
395
|
+
return {"status": False, "error": str(ex)}
|
|
396
|
+
else:
|
|
397
|
+
return {"status": False, "error": response.text}
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def load_model(self, model_name: str) -> bool:
|
|
401
|
+
"""
|
|
402
|
+
Load a specific model into the LOLLMS binding.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
model_name (str): Name of the model to load.
|
|
406
|
+
|
|
407
|
+
Returns:
|
|
408
|
+
bool: True if model loaded successfully.
|
|
409
|
+
"""
|
|
410
|
+
self.model = model_name
|
|
411
|
+
self.model_name = model_name
|
|
412
|
+
return True
|
|
413
|
+
|
|
414
|
+
# Lollms specific methods
|
|
415
|
+
def lollms_listMountedPersonalities(self, host_address:str=None):
|
|
416
|
+
host_address = host_address if host_address else self.host_address
|
|
417
|
+
url = f"{host_address}/list_mounted_personalities"
|
|
418
|
+
|
|
419
|
+
response = requests.get(url)
|
|
420
|
+
|
|
421
|
+
if response.status_code == 200:
|
|
422
|
+
try:
|
|
423
|
+
text = json.loads(response.content.decode("utf-8"))
|
|
424
|
+
return text
|
|
425
|
+
except Exception as ex:
|
|
426
|
+
return {"status": False, "error": str(ex)}
|
|
427
|
+
else:
|
|
428
|
+
return {"status": False, "error": response.text}
|