npcpy 1.3.16__tar.gz → 1.3.17__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.
Files changed (93) hide show
  1. {npcpy-1.3.16/npcpy.egg-info → npcpy-1.3.17}/PKG-INFO +1 -1
  2. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/gen/response.py +160 -2
  3. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/npc_sysenv.py +27 -2
  4. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/serve.py +806 -35
  5. {npcpy-1.3.16 → npcpy-1.3.17/npcpy.egg-info}/PKG-INFO +1 -1
  6. {npcpy-1.3.16 → npcpy-1.3.17}/setup.py +1 -1
  7. {npcpy-1.3.16 → npcpy-1.3.17}/LICENSE +0 -0
  8. {npcpy-1.3.16 → npcpy-1.3.17}/MANIFEST.in +0 -0
  9. {npcpy-1.3.16 → npcpy-1.3.17}/README.md +0 -0
  10. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/__init__.py +0 -0
  11. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/build_funcs.py +0 -0
  12. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/data/__init__.py +0 -0
  13. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/data/audio.py +0 -0
  14. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/data/data_models.py +0 -0
  15. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/data/image.py +0 -0
  16. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/data/load.py +0 -0
  17. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/data/text.py +0 -0
  18. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/data/video.py +0 -0
  19. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/data/web.py +0 -0
  20. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/ft/__init__.py +0 -0
  21. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/ft/diff.py +0 -0
  22. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/ft/ge.py +0 -0
  23. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/ft/memory_trainer.py +0 -0
  24. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/ft/model_ensembler.py +0 -0
  25. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/ft/rl.py +0 -0
  26. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/ft/sft.py +0 -0
  27. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/ft/usft.py +0 -0
  28. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/gen/__init__.py +0 -0
  29. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/gen/audio_gen.py +0 -0
  30. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/gen/embeddings.py +0 -0
  31. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/gen/image_gen.py +0 -0
  32. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/gen/ocr.py +0 -0
  33. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/gen/video_gen.py +0 -0
  34. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/gen/world_gen.py +0 -0
  35. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/llm_funcs.py +0 -0
  36. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/main.py +0 -0
  37. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/memory/__init__.py +0 -0
  38. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/memory/command_history.py +0 -0
  39. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/memory/kg_vis.py +0 -0
  40. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/memory/knowledge_graph.py +0 -0
  41. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/memory/memory_processor.py +0 -0
  42. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/memory/search.py +0 -0
  43. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/mix/__init__.py +0 -0
  44. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/mix/debate.py +0 -0
  45. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/ml_funcs.py +0 -0
  46. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/npc_array.py +0 -0
  47. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/npc_compiler.py +0 -0
  48. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/npcs.py +0 -0
  49. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/sql/__init__.py +0 -0
  50. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/sql/ai_function_tools.py +0 -0
  51. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/sql/database_ai_adapters.py +0 -0
  52. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/sql/database_ai_functions.py +0 -0
  53. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/sql/model_runner.py +0 -0
  54. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/sql/npcsql.py +0 -0
  55. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/sql/sql_model_compiler.py +0 -0
  56. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/tools.py +0 -0
  57. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/work/__init__.py +0 -0
  58. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/work/browser.py +0 -0
  59. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/work/desktop.py +0 -0
  60. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/work/plan.py +0 -0
  61. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy/work/trigger.py +0 -0
  62. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy.egg-info/SOURCES.txt +0 -0
  63. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy.egg-info/dependency_links.txt +0 -0
  64. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy.egg-info/requires.txt +0 -0
  65. {npcpy-1.3.16 → npcpy-1.3.17}/npcpy.egg-info/top_level.txt +0 -0
  66. {npcpy-1.3.16 → npcpy-1.3.17}/setup.cfg +0 -0
  67. {npcpy-1.3.16 → npcpy-1.3.17}/tests/test_audio.py +0 -0
  68. {npcpy-1.3.16 → npcpy-1.3.17}/tests/test_browser.py +0 -0
  69. {npcpy-1.3.16 → npcpy-1.3.17}/tests/test_build_funcs.py +0 -0
  70. {npcpy-1.3.16 → npcpy-1.3.17}/tests/test_command_history.py +0 -0
  71. {npcpy-1.3.16 → npcpy-1.3.17}/tests/test_data_models.py +0 -0
  72. {npcpy-1.3.16 → npcpy-1.3.17}/tests/test_diff.py +0 -0
  73. {npcpy-1.3.16 → npcpy-1.3.17}/tests/test_documentation_examples.py +0 -0
  74. {npcpy-1.3.16 → npcpy-1.3.17}/tests/test_genetic_evolver.py +0 -0
  75. {npcpy-1.3.16 → npcpy-1.3.17}/tests/test_image.py +0 -0
  76. {npcpy-1.3.16 → npcpy-1.3.17}/tests/test_llm_funcs.py +0 -0
  77. {npcpy-1.3.16 → npcpy-1.3.17}/tests/test_load.py +0 -0
  78. {npcpy-1.3.16 → npcpy-1.3.17}/tests/test_memory_processor.py +0 -0
  79. {npcpy-1.3.16 → npcpy-1.3.17}/tests/test_ml_funcs.py +0 -0
  80. {npcpy-1.3.16 → npcpy-1.3.17}/tests/test_model_runner.py +0 -0
  81. {npcpy-1.3.16 → npcpy-1.3.17}/tests/test_npc_array.py +0 -0
  82. {npcpy-1.3.16 → npcpy-1.3.17}/tests/test_npc_compiler.py +0 -0
  83. {npcpy-1.3.16 → npcpy-1.3.17}/tests/test_npc_sysenv.py +0 -0
  84. {npcpy-1.3.16 → npcpy-1.3.17}/tests/test_npcsql.py +0 -0
  85. {npcpy-1.3.16 → npcpy-1.3.17}/tests/test_response.py +0 -0
  86. {npcpy-1.3.16 → npcpy-1.3.17}/tests/test_serve.py +0 -0
  87. {npcpy-1.3.16 → npcpy-1.3.17}/tests/test_sql_adapters.py +0 -0
  88. {npcpy-1.3.16 → npcpy-1.3.17}/tests/test_sql_compiler.py +0 -0
  89. {npcpy-1.3.16 → npcpy-1.3.17}/tests/test_sql_functions.py +0 -0
  90. {npcpy-1.3.16 → npcpy-1.3.17}/tests/test_text.py +0 -0
  91. {npcpy-1.3.16 → npcpy-1.3.17}/tests/test_tools.py +0 -0
  92. {npcpy-1.3.16 → npcpy-1.3.17}/tests/test_video.py +0 -0
  93. {npcpy-1.3.16 → npcpy-1.3.17}/tests/test_web.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: npcpy
3
- Version: 1.3.16
3
+ Version: 1.3.17
4
4
  Summary: npcpy is the premier open-source library for integrating LLMs and Agents into python systems.
5
5
  Home-page: https://github.com/NPC-Worldwide/npcpy
6
6
  Author: Christopher Agostino
@@ -588,6 +588,148 @@ def get_ollama_response(
588
588
  import time
589
589
 
590
590
 
591
+ def get_lora_response(
592
+ prompt: str = None,
593
+ model: str = None,
594
+ tools: list = None,
595
+ tool_map: Dict = None,
596
+ format: str = None,
597
+ messages: List[Dict[str, str]] = None,
598
+ stream: bool = False,
599
+ auto_process_tool_calls: bool = False,
600
+ **kwargs,
601
+ ) -> Dict[str, Any]:
602
+ """
603
+ Generate response using a LoRA adapter on top of a base model.
604
+ The adapter path should contain adapter_config.json with base_model_name_or_path.
605
+ """
606
+ print(f"🎯 get_lora_response called with model={model}, prompt={prompt[:50] if prompt else 'None'}...")
607
+
608
+ result = {
609
+ "response": None,
610
+ "messages": messages.copy() if messages else [],
611
+ "raw_response": None,
612
+ "tool_calls": [],
613
+ "tool_results": []
614
+ }
615
+
616
+ try:
617
+ import torch
618
+ from transformers import AutoTokenizer, AutoModelForCausalLM
619
+ from peft import PeftModel
620
+ print("🎯 Successfully imported torch, transformers, peft")
621
+ except ImportError as e:
622
+ print(f"🎯 Import error: {e}")
623
+ return {
624
+ "response": "",
625
+ "messages": messages or [],
626
+ "error": f"Missing dependencies for LoRA. Install with: pip install transformers peft torch. Error: {e}"
627
+ }
628
+
629
+ adapter_path = os.path.expanduser(model)
630
+ adapter_config_path = os.path.join(adapter_path, 'adapter_config.json')
631
+
632
+ if not os.path.exists(adapter_config_path):
633
+ return {
634
+ "response": "",
635
+ "messages": messages or [],
636
+ "error": f"No adapter_config.json found at {adapter_path}"
637
+ }
638
+
639
+ # Read base model from adapter config
640
+ try:
641
+ with open(adapter_config_path, 'r') as f:
642
+ adapter_config = json.load(f)
643
+ base_model_id = adapter_config.get('base_model_name_or_path')
644
+ if not base_model_id:
645
+ return {
646
+ "response": "",
647
+ "messages": messages or [],
648
+ "error": "adapter_config.json missing base_model_name_or_path"
649
+ }
650
+ except Exception as e:
651
+ return {
652
+ "response": "",
653
+ "messages": messages or [],
654
+ "error": f"Failed to read adapter config: {e}"
655
+ }
656
+
657
+ if prompt:
658
+ if result['messages'] and result['messages'][-1]["role"] == "user":
659
+ result['messages'][-1]["content"] = prompt
660
+ else:
661
+ result['messages'].append({"role": "user", "content": prompt})
662
+
663
+ if format == "json":
664
+ json_instruction = """If you are returning a json object, begin directly with the opening {.
665
+ Do not include any additional markdown formatting or leading ```json tags in your response."""
666
+ if result["messages"] and result["messages"][-1]["role"] == "user":
667
+ result["messages"][-1]["content"] += "\n" + json_instruction
668
+
669
+ try:
670
+ logger.info(f"Loading base model: {base_model_id}")
671
+ tokenizer = AutoTokenizer.from_pretrained(base_model_id, trust_remote_code=True)
672
+ base_model = AutoModelForCausalLM.from_pretrained(
673
+ base_model_id,
674
+ torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
675
+ device_map="auto" if torch.cuda.is_available() else None,
676
+ trust_remote_code=True
677
+ )
678
+
679
+ if tokenizer.pad_token is None:
680
+ tokenizer.pad_token = tokenizer.eos_token
681
+
682
+ logger.info(f"Loading LoRA adapter: {adapter_path}")
683
+ model_with_adapter = PeftModel.from_pretrained(base_model, adapter_path)
684
+
685
+ # Apply chat template
686
+ chat_text = tokenizer.apply_chat_template(
687
+ result["messages"],
688
+ tokenize=False,
689
+ add_generation_prompt=True
690
+ )
691
+ device = next(model_with_adapter.parameters()).device
692
+ inputs = tokenizer(chat_text, return_tensors="pt", padding=True, truncation=True)
693
+ inputs = {k: v.to(device) for k, v in inputs.items()}
694
+
695
+ max_new_tokens = kwargs.get("max_tokens", 512)
696
+ temperature = kwargs.get("temperature", 0.7)
697
+
698
+ with torch.no_grad():
699
+ outputs = model_with_adapter.generate(
700
+ **inputs,
701
+ max_new_tokens=max_new_tokens,
702
+ temperature=temperature,
703
+ do_sample=True,
704
+ pad_token_id=tokenizer.eos_token_id,
705
+ )
706
+
707
+ response_content = tokenizer.decode(
708
+ outputs[0][inputs['input_ids'].shape[1]:],
709
+ skip_special_tokens=True
710
+ ).strip()
711
+
712
+ result["response"] = response_content
713
+ result["raw_response"] = response_content
714
+ result["messages"].append({"role": "assistant", "content": response_content})
715
+
716
+ if format == "json":
717
+ try:
718
+ if response_content.startswith("```json"):
719
+ response_content = response_content.replace("```json", "").replace("```", "").strip()
720
+ parsed_response = json.loads(response_content)
721
+ result["response"] = parsed_response
722
+ except json.JSONDecodeError:
723
+ result["error"] = f"Invalid JSON response: {response_content}"
724
+
725
+ except Exception as e:
726
+ logger.error(f"LoRA inference error: {e}")
727
+ result["error"] = f"LoRA inference error: {str(e)}"
728
+ result["response"] = ""
729
+
730
+ return result
731
+
732
+
591
733
  def get_llamacpp_response(
592
734
  prompt: str = None,
593
735
  model: str = None,
@@ -730,7 +872,7 @@ def get_litellm_response(
730
872
  auto_process_tool_calls=auto_process_tool_calls,
731
873
  **kwargs
732
874
  )
733
- elif provider=='transformers':
875
+ elif provider == 'transformers':
734
876
  return get_transformers_response(
735
877
  prompt,
736
878
  model,
@@ -745,8 +887,24 @@ def get_litellm_response(
745
887
  attachments=attachments,
746
888
  auto_process_tool_calls=auto_process_tool_calls,
747
889
  **kwargs
748
-
749
890
  )
891
+ elif provider == 'lora':
892
+ print(f"🔧 LoRA provider detected, calling get_lora_response with model: {model}")
893
+ result = get_lora_response(
894
+ prompt=prompt,
895
+ model=model,
896
+ tools=tools,
897
+ tool_map=tool_map,
898
+ format=format,
899
+ messages=messages,
900
+ stream=stream,
901
+ auto_process_tool_calls=auto_process_tool_calls,
902
+ **kwargs
903
+ )
904
+ print(f"🔧 LoRA response: {result.get('response', 'NO RESPONSE')[:200] if result.get('response') else 'EMPTY'}")
905
+ if result.get('error'):
906
+ print(f"🔧 LoRA error: {result.get('error')}")
907
+ return result
750
908
  elif provider == 'llamacpp':
751
909
  return get_llamacpp_response(
752
910
  prompt,
@@ -358,6 +358,25 @@ def get_locally_available_models(project_directory, airplane_mode=False):
358
358
  except Exception as e:
359
359
  logging.debug(f"llama.cpp server not available: {e}")
360
360
 
361
+ # Scan for LoRA adapters (fine-tuned models with adapter_config.json)
362
+ lora_dirs = [
363
+ os.path.expanduser('~/.npcsh/models'),
364
+ ]
365
+ for scan_dir in lora_dirs:
366
+ if not os.path.isdir(scan_dir):
367
+ continue
368
+ try:
369
+ for item in os.listdir(scan_dir):
370
+ item_path = os.path.join(scan_dir, item)
371
+ if os.path.isdir(item_path):
372
+ adapter_config = os.path.join(item_path, 'adapter_config.json')
373
+ if os.path.exists(adapter_config):
374
+ # This is a LoRA adapter
375
+ available_models[item_path] = "lora"
376
+ logging.debug(f"Found LoRA adapter: {item_path}")
377
+ except Exception as e:
378
+ logging.debug(f"Error scanning LoRA directory {scan_dir}: {e}")
379
+
361
380
  return available_models
362
381
 
363
382
 
@@ -959,13 +978,19 @@ def lookup_provider(model: str) -> str:
959
978
  """
960
979
  Determine the provider based on the model name.
961
980
  Checks custom providers first, then falls back to known providers.
962
-
981
+
963
982
  Args:
964
983
  model: The model name
965
-
984
+
966
985
  Returns:
967
986
  The provider name or None if not found
968
987
  """
988
+ # Check if model is a LoRA adapter path
989
+ if model and os.path.isdir(os.path.expanduser(model)):
990
+ adapter_config = os.path.join(os.path.expanduser(model), 'adapter_config.json')
991
+ if os.path.exists(adapter_config):
992
+ return "lora"
993
+
969
994
  custom_providers = load_custom_providers()
970
995
 
971
996
  for provider_name, config in custom_providers.items():