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 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.31.0" # Updated version
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 # Store the name/path user gave
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
- if not self.load_model(self.user_provided_model_name):
298
- ASCIIColors.error(f"Initial model load for '{self.user_provided_model_name}' failed. Binding may not be functional.")
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(): return model_p
319
- else: raise FileNotFoundError(f"Absolute model path specified but not found: {model_p}")
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}"); return path_in_models_dir
324
-
325
- raise FileNotFoundError(f"Model '{model_name_or_path}' not found as absolute path or within '{self.models_path}'.")
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 listModels(self) -> List[Dict[str, str]]:
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
- unique_models = set()
811
- if self.models_path.exists() and self.models_path.is_dir():
812
- for model_file in self.models_path.rglob("*.gguf"):
813
- if model_file.is_file() and model_file.name not in unique_models:
814
- models_found.append({
815
- 'model_name': model_file.name,
816
- 'path_hint': str(model_file.relative_to(self.models_path.parent) if model_file.is_relative_to(self.models_path.parent) else model_file),
817
- 'size_gb': f"{model_file.stat().st_size / (1024**3):.2f} GB"
818
- })
819
- unique_models.add(model_file.name)
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=model_name_str, models_path=str(models_path), config=binding_config
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=model_name_str, models_path=str(models_path), config=binding_config # Same model and config
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: ASCIIColors.green(f"Found {len(listed_models)} GGUF files. First 5: {listed_models[:5]}")
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, use_chat_format_override=True)
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, Completion API, binding2) ---")
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, use_chat_format_override=False)
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 full path for clarity in test
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 full path for clip
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, use_chat_format_override=True
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(model_name_str) # Reload original 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.")