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.

@@ -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}