lollms-client 0.31.0__py3-none-any.whl → 0.32.0__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.
- lollms_client/__init__.py +1 -1
- lollms_client/llm_bindings/llamacpp/__init__.py +139 -35
- lollms_client/llm_bindings/openai/__init__.py +372 -294
- lollms_client/llm_bindings/pythonllamacpp/__init__.py +153 -139
- lollms_client/lollms_core.py +27 -15
- lollms_client/lollms_discussion.py +2 -0
- lollms_client/lollms_llm_binding.py +1 -1
- lollms_client/lollms_mcp_security.py +84 -0
- {lollms_client-0.31.0.dist-info → lollms_client-0.32.0.dist-info}/METADATA +1 -1
- {lollms_client-0.31.0.dist-info → lollms_client-0.32.0.dist-info}/RECORD +13 -12
- {lollms_client-0.31.0.dist-info → lollms_client-0.32.0.dist-info}/WHEEL +0 -0
- {lollms_client-0.31.0.dist-info → lollms_client-0.32.0.dist-info}/licenses/LICENSE +0 -0
- {lollms_client-0.31.0.dist-info → lollms_client-0.32.0.dist-info}/top_level.txt +0 -0
lollms_client/__init__.py
CHANGED
|
@@ -8,7 +8,7 @@ from lollms_client.lollms_utilities import PromptReshaper # Keep general utiliti
|
|
|
8
8
|
from lollms_client.lollms_mcp_binding import LollmsMCPBinding, LollmsMCPBindingManager
|
|
9
9
|
from lollms_client.lollms_llm_binding import LollmsLLMBindingManager
|
|
10
10
|
|
|
11
|
-
__version__ = "0.
|
|
11
|
+
__version__ = "0.32.0" # Updated version
|
|
12
12
|
|
|
13
13
|
# Optionally, you could define __all__ if you want to be explicit about exports
|
|
14
14
|
__all__ = [
|
|
@@ -272,8 +272,22 @@ class LlamaCppServerBinding(LollmsLLMBinding):
|
|
|
272
272
|
if llama_cpp_binaries is None: raise ImportError("llama-cpp-binaries package is required but not found.")
|
|
273
273
|
|
|
274
274
|
self.models_path = Path(models_path)
|
|
275
|
-
self.user_provided_model_name = model_name
|
|
276
|
-
|
|
275
|
+
self.user_provided_model_name = model_name # Store the name/path user gave
|
|
276
|
+
self._model_path_map: Dict[str, Path] = {} # Maps unique name to full Path
|
|
277
|
+
|
|
278
|
+
# Initial scan for available models
|
|
279
|
+
self._scan_models()
|
|
280
|
+
|
|
281
|
+
# Determine the model to load
|
|
282
|
+
effective_model_to_load = model_name
|
|
283
|
+
if not effective_model_to_load and self._model_path_map:
|
|
284
|
+
# If no model was specified and we have models, pick the first one
|
|
285
|
+
# Sorting ensures a deterministic choice
|
|
286
|
+
first_model_name = sorted(self._model_path_map.keys())[0]
|
|
287
|
+
effective_model_to_load = first_model_name
|
|
288
|
+
ASCIIColors.info(f"No model was specified. Automatically selecting the first available model: '{effective_model_to_load}'")
|
|
289
|
+
self.user_provided_model_name = effective_model_to_load # Update for get_model_info etc.
|
|
290
|
+
|
|
277
291
|
# Initial hint for clip_model_path, resolved fully in load_model
|
|
278
292
|
self.clip_model_path: Optional[Path] = None
|
|
279
293
|
if clip_model_name:
|
|
@@ -294,8 +308,12 @@ class LlamaCppServerBinding(LollmsLLMBinding):
|
|
|
294
308
|
self.port: Optional[int] = None
|
|
295
309
|
self.server_key: Optional[tuple] = None
|
|
296
310
|
|
|
297
|
-
|
|
298
|
-
|
|
311
|
+
# Now, attempt to load the selected model
|
|
312
|
+
if effective_model_to_load:
|
|
313
|
+
if not self.load_model(effective_model_to_load):
|
|
314
|
+
ASCIIColors.error(f"Initial model load for '{effective_model_to_load}' failed. Binding may not be functional.")
|
|
315
|
+
else:
|
|
316
|
+
ASCIIColors.warning("No models found in the models path. The binding will be idle until a model is loaded.")
|
|
299
317
|
|
|
300
318
|
def _get_server_binary_path(self) -> Path:
|
|
301
319
|
custom_path_str = self.server_args.get("llama_server_binary_path")
|
|
@@ -313,16 +331,41 @@ class LlamaCppServerBinding(LollmsLLMBinding):
|
|
|
313
331
|
raise FileNotFoundError("Llama.cpp server binary not found. Ensure 'llama-cpp-binaries' or 'llama-cpp-python[server]' is installed or provide 'llama_server_binary_path'.")
|
|
314
332
|
|
|
315
333
|
def _resolve_model_path(self, model_name_or_path: str) -> Path:
|
|
334
|
+
"""
|
|
335
|
+
Resolves a model name or path to a full Path object.
|
|
336
|
+
It prioritizes the internal map, then checks for absolute/relative paths,
|
|
337
|
+
and rescans the models directory as a fallback.
|
|
338
|
+
"""
|
|
339
|
+
# 1. Check if the provided name is a key in our map
|
|
340
|
+
if model_name_or_path in self._model_path_map:
|
|
341
|
+
resolved_path = self._model_path_map[model_name_or_path]
|
|
342
|
+
ASCIIColors.info(f"Resolved model name '{model_name_or_path}' to path: {resolved_path}")
|
|
343
|
+
return resolved_path
|
|
344
|
+
|
|
345
|
+
# 2. If not in map, treat it as a potential path (absolute or relative to models_path)
|
|
316
346
|
model_p = Path(model_name_or_path)
|
|
317
347
|
if model_p.is_absolute():
|
|
318
|
-
if model_p.exists()
|
|
319
|
-
|
|
320
|
-
|
|
348
|
+
if model_p.exists() and model_p.is_file():
|
|
349
|
+
return model_p
|
|
350
|
+
|
|
321
351
|
path_in_models_dir = self.models_path / model_name_or_path
|
|
322
352
|
if path_in_models_dir.exists() and path_in_models_dir.is_file():
|
|
323
|
-
ASCIIColors.info(f"Found model at: {path_in_models_dir}")
|
|
324
|
-
|
|
325
|
-
|
|
353
|
+
ASCIIColors.info(f"Found model at relative path: {path_in_models_dir}")
|
|
354
|
+
return path_in_models_dir
|
|
355
|
+
|
|
356
|
+
# 3. As a fallback, rescan the models directory in case the file was just added
|
|
357
|
+
ASCIIColors.info("Model not found in cache, rescanning directory...")
|
|
358
|
+
self._scan_models()
|
|
359
|
+
if model_name_or_path in self._model_path_map:
|
|
360
|
+
resolved_path = self._model_path_map[model_name_or_path]
|
|
361
|
+
ASCIIColors.info(f"Found model '{model_name_or_path}' after rescan: {resolved_path}")
|
|
362
|
+
return resolved_path
|
|
363
|
+
|
|
364
|
+
# Final check for absolute path after rescan
|
|
365
|
+
if model_p.is_absolute() and model_p.exists() and model_p.is_file():
|
|
366
|
+
return model_p
|
|
367
|
+
|
|
368
|
+
raise FileNotFoundError(f"Model '{model_name_or_path}' not found in the map, as an absolute path, or within '{self.models_path}'.")
|
|
326
369
|
|
|
327
370
|
def _find_available_port(self) -> int:
|
|
328
371
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
@@ -352,6 +395,7 @@ class LlamaCppServerBinding(LollmsLLMBinding):
|
|
|
352
395
|
|
|
353
396
|
|
|
354
397
|
def load_model(self, model_name_or_path: str) -> bool:
|
|
398
|
+
self.user_provided_model_name = model_name_or_path # Keep track of the selected model name
|
|
355
399
|
try:
|
|
356
400
|
resolved_model_path = self._resolve_model_path(model_name_or_path)
|
|
357
401
|
except Exception as ex:
|
|
@@ -805,19 +849,54 @@ class LlamaCppServerBinding(LollmsLLMBinding):
|
|
|
805
849
|
info["supports_structured_output"] = self.server_args.get("grammar_string") is not None
|
|
806
850
|
return info
|
|
807
851
|
|
|
808
|
-
def
|
|
852
|
+
def _scan_models(self):
|
|
853
|
+
"""
|
|
854
|
+
Scans the models_path for GGUF files and populates the model map.
|
|
855
|
+
Handles duplicate filenames by prefixing them with their parent directory path.
|
|
856
|
+
"""
|
|
857
|
+
self._model_path_map = {}
|
|
858
|
+
if not self.models_path.exists() or not self.models_path.is_dir():
|
|
859
|
+
ASCIIColors.warning(f"Models path does not exist or is not a directory: {self.models_path}")
|
|
860
|
+
return
|
|
861
|
+
|
|
862
|
+
all_paths = list(self.models_path.rglob("*.gguf"))
|
|
863
|
+
filenames_count = {}
|
|
864
|
+
for path in all_paths:
|
|
865
|
+
if path.is_file():
|
|
866
|
+
filenames_count[path.name] = filenames_count.get(path.name, 0) + 1
|
|
867
|
+
|
|
868
|
+
for model_file in all_paths:
|
|
869
|
+
if model_file.is_file():
|
|
870
|
+
# On Windows, path separators can be tricky. Convert to generic format.
|
|
871
|
+
relative_path_str = str(model_file.relative_to(self.models_path).as_posix())
|
|
872
|
+
if filenames_count[model_file.name] > 1:
|
|
873
|
+
# Duplicate filename, use relative path as the unique name
|
|
874
|
+
unique_name = relative_path_str
|
|
875
|
+
else:
|
|
876
|
+
# Unique filename, use the name itself
|
|
877
|
+
unique_name = model_file.name
|
|
878
|
+
|
|
879
|
+
self._model_path_map[unique_name] = model_file
|
|
880
|
+
|
|
881
|
+
ASCIIColors.info(f"Scanned {len(self._model_path_map)} models from {self.models_path}.")
|
|
882
|
+
|
|
883
|
+
def listModels(self) -> List[Dict[str, Any]]:
|
|
884
|
+
"""
|
|
885
|
+
Lists all available GGUF models, rescanning the directory first.
|
|
886
|
+
"""
|
|
887
|
+
self._scan_models() # Always rescan when asked for the list
|
|
888
|
+
|
|
809
889
|
models_found = []
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
return models_found
|
|
890
|
+
for unique_name, model_path in self._model_path_map.items():
|
|
891
|
+
models_found.append({
|
|
892
|
+
'name': unique_name, # The unique name for selection
|
|
893
|
+
'model_name': model_path.name, # The original filename for display
|
|
894
|
+
'path': str(model_path), # The full path
|
|
895
|
+
'size': model_path.stat().st_size
|
|
896
|
+
})
|
|
897
|
+
|
|
898
|
+
# Sort the list alphabetically by the unique name for consistent ordering
|
|
899
|
+
return sorted(models_found, key=lambda x: x['name'])
|
|
821
900
|
|
|
822
901
|
def __del__(self):
|
|
823
902
|
self.unload_model()
|
|
@@ -872,17 +951,21 @@ if __name__ == '__main__':
|
|
|
872
951
|
try:
|
|
873
952
|
if primary_model_available:
|
|
874
953
|
ASCIIColors.cyan("\n--- Initializing First LlamaCppServerBinding Instance ---")
|
|
954
|
+
# Test default model selection by passing model_name=None
|
|
955
|
+
ASCIIColors.info("Testing default model selection (model_name=None)")
|
|
875
956
|
active_binding1 = LlamaCppServerBinding(
|
|
876
|
-
model_name=
|
|
957
|
+
model_name=None, models_path=str(models_path), config=binding_config
|
|
877
958
|
)
|
|
878
959
|
if not active_binding1.server_process or not active_binding1.server_process.is_healthy:
|
|
879
960
|
raise RuntimeError("Server for binding1 failed to start or become healthy.")
|
|
880
|
-
ASCIIColors.green(f"Binding1 initialized. Server for '{active_binding1.current_model_path.name}' running on port {active_binding1.port}.")
|
|
961
|
+
ASCIIColors.green(f"Binding1 initialized with default model. Server for '{active_binding1.current_model_path.name}' running on port {active_binding1.port}.")
|
|
881
962
|
ASCIIColors.info(f"Binding1 Model Info: {json.dumps(active_binding1.get_model_info(), indent=2)}")
|
|
882
963
|
|
|
883
|
-
ASCIIColors.cyan("\n--- Initializing Second LlamaCppServerBinding Instance (Same Model) ---")
|
|
964
|
+
ASCIIColors.cyan("\n--- Initializing Second LlamaCppServerBinding Instance (Same Model, explicit name) ---")
|
|
965
|
+
# Load the same model explicitly now
|
|
966
|
+
model_to_load_explicitly = active_binding1.user_provided_model_name
|
|
884
967
|
active_binding2 = LlamaCppServerBinding(
|
|
885
|
-
model_name=
|
|
968
|
+
model_name=model_to_load_explicitly, models_path=str(models_path), config=binding_config
|
|
886
969
|
)
|
|
887
970
|
if not active_binding2.server_process or not active_binding2.server_process.is_healthy:
|
|
888
971
|
raise RuntimeError("Server for binding2 failed to start or become healthy (should reuse).")
|
|
@@ -896,9 +979,30 @@ if __name__ == '__main__':
|
|
|
896
979
|
|
|
897
980
|
# --- List Models (scans configured directories) ---
|
|
898
981
|
ASCIIColors.cyan("\n--- Listing Models (from search paths, using binding1) ---")
|
|
982
|
+
# Create a dummy duplicate model to test unique naming
|
|
983
|
+
duplicate_folder = models_path / "subdir"
|
|
984
|
+
duplicate_folder.mkdir(exist_ok=True)
|
|
985
|
+
duplicate_model_path = duplicate_folder / test_model_path.name
|
|
986
|
+
import shutil
|
|
987
|
+
shutil.copy(test_model_path, duplicate_model_path)
|
|
988
|
+
ASCIIColors.info(f"Created a duplicate model for testing: {duplicate_model_path}")
|
|
989
|
+
|
|
899
990
|
listed_models = active_binding1.listModels()
|
|
900
|
-
if listed_models:
|
|
991
|
+
if listed_models:
|
|
992
|
+
ASCIIColors.green(f"Found {len(listed_models)} GGUF files.")
|
|
993
|
+
pprint.pprint(listed_models)
|
|
994
|
+
# Check if the duplicate was handled
|
|
995
|
+
names = [m['name'] for m in listed_models]
|
|
996
|
+
if test_model_path.name in names and f"subdir/{test_model_path.name}" in names:
|
|
997
|
+
ASCIIColors.green("SUCCESS: Duplicate model names were correctly handled.")
|
|
998
|
+
else:
|
|
999
|
+
ASCIIColors.error("FAILURE: Duplicate model names were not handled correctly.")
|
|
901
1000
|
else: ASCIIColors.warning("No GGUF models found in search paths.")
|
|
1001
|
+
|
|
1002
|
+
# Clean up dummy duplicate
|
|
1003
|
+
duplicate_model_path.unlink()
|
|
1004
|
+
duplicate_folder.rmdir()
|
|
1005
|
+
|
|
902
1006
|
|
|
903
1007
|
# --- Tokenize/Detokenize ---
|
|
904
1008
|
ASCIIColors.cyan("\n--- Tokenize/Detokenize (using binding1) ---")
|
|
@@ -913,16 +1017,16 @@ if __name__ == '__main__':
|
|
|
913
1017
|
# --- Text Generation (Non-Streaming, Chat API, binding1) ---
|
|
914
1018
|
ASCIIColors.cyan("\n--- Text Generation (Non-Streaming, Chat API, binding1) ---")
|
|
915
1019
|
prompt_text = "What is the capital of Germany?"
|
|
916
|
-
generated_text = active_binding1.generate_text(prompt_text, system_prompt="Concise expert.", n_predict=20, stream=False
|
|
1020
|
+
generated_text = active_binding1.generate_text(prompt_text, system_prompt="Concise expert.", n_predict=20, stream=False)
|
|
917
1021
|
if isinstance(generated_text, str): ASCIIColors.green(f"Generated text (binding1): {generated_text}")
|
|
918
1022
|
else: ASCIIColors.error(f"Generation failed (binding1): {generated_text}")
|
|
919
1023
|
|
|
920
1024
|
# --- Text Generation (Streaming, Completion API, binding2) ---
|
|
921
|
-
ASCIIColors.cyan("\n--- Text Generation (Streaming,
|
|
1025
|
+
ASCIIColors.cyan("\n--- Text Generation (Streaming, Chat API, binding2) ---")
|
|
922
1026
|
full_streamed_text = "" # Reset global
|
|
923
1027
|
def stream_callback(chunk: str, msg_type: int): global full_streamed_text; ASCIIColors.green(f"{chunk}", end="", flush=True); full_streamed_text += chunk; return True
|
|
924
1028
|
|
|
925
|
-
result_b2 = active_binding2.generate_text(prompt_text, system_prompt="Concise expert.", n_predict=30, stream=True, streaming_callback=stream_callback
|
|
1029
|
+
result_b2 = active_binding2.generate_text(prompt_text, system_prompt="Concise expert.", n_predict=30, stream=True, streaming_callback=stream_callback)
|
|
926
1030
|
print("\n--- End of Stream (binding2) ---")
|
|
927
1031
|
if isinstance(result_b2, str): ASCIIColors.green(f"Full streamed text (binding2): {result_b2}")
|
|
928
1032
|
else: ASCIIColors.error(f"Streaming generation failed (binding2): {result_b2}")
|
|
@@ -957,9 +1061,9 @@ if __name__ == '__main__':
|
|
|
957
1061
|
# llava_binding_config["chat_template"] = "llava-1.5"
|
|
958
1062
|
|
|
959
1063
|
active_binding_llava = LlamaCppServerBinding(
|
|
960
|
-
model_name=str(llava_model_path), # Pass
|
|
1064
|
+
model_name=str(llava_model_path.name), # Pass filename, let it resolve
|
|
961
1065
|
models_path=str(models_path),
|
|
962
|
-
clip_model_name=str(llava_clip_path_actual), # Pass
|
|
1066
|
+
clip_model_name=str(llava_clip_path_actual.name), # Pass filename for clip
|
|
963
1067
|
config=llava_binding_config
|
|
964
1068
|
)
|
|
965
1069
|
if not active_binding_llava.server_process or not active_binding_llava.server_process.is_healthy:
|
|
@@ -970,7 +1074,7 @@ if __name__ == '__main__':
|
|
|
970
1074
|
|
|
971
1075
|
llava_prompt = "Describe this image."
|
|
972
1076
|
llava_response = active_binding_llava.generate_text(
|
|
973
|
-
prompt=llava_prompt, images=[str(dummy_image_path)], n_predict=40, stream=False
|
|
1077
|
+
prompt=llava_prompt, images=[str(dummy_image_path)], n_predict=40, stream=False
|
|
974
1078
|
)
|
|
975
1079
|
if isinstance(llava_response, str): ASCIIColors.green(f"LLaVA response: {llava_response}")
|
|
976
1080
|
else: ASCIIColors.error(f"LLaVA generation failed: {llava_response}")
|
|
@@ -986,7 +1090,7 @@ if __name__ == '__main__':
|
|
|
986
1090
|
# --- Test changing model (using binding1 to load a different or same model) ---
|
|
987
1091
|
ASCIIColors.cyan("\n--- Testing Model Change (binding1 reloads its model) ---")
|
|
988
1092
|
# For a real change, use a different model name if available. Here, we reload the same.
|
|
989
|
-
reload_success = active_binding1.load_model(
|
|
1093
|
+
reload_success = active_binding1.load_model(active_binding1.user_provided_model_name) # Reload original model
|
|
990
1094
|
if reload_success and active_binding1.server_process and active_binding1.server_process.is_healthy:
|
|
991
1095
|
ASCIIColors.green(f"Model reloaded/re-confirmed successfully by binding1. Server on port {active_binding1.port}.")
|
|
992
1096
|
reloaded_gen = active_binding1.generate_text("Ping", n_predict=5, stream=False)
|
|
@@ -1023,4 +1127,4 @@ if __name__ == '__main__':
|
|
|
1023
1127
|
else:
|
|
1024
1128
|
ASCIIColors.green("All servers shut down correctly.")
|
|
1025
1129
|
|
|
1026
|
-
ASCIIColors.yellow("\nLlamaCppServerBinding test finished.")
|
|
1130
|
+
ASCIIColors.yellow("\nLlamaCppServerBinding test finished.")
|