npcpy 1.3.5__tar.gz → 1.3.7__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.
- {npcpy-1.3.5/npcpy.egg-info → npcpy-1.3.7}/PKG-INFO +1 -1
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/gen/response.py +127 -12
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/llm_funcs.py +8 -3
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/memory/command_history.py +32 -11
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/memory/knowledge_graph.py +1 -1
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/npc_sysenv.py +27 -1
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/serve.py +227 -20
- {npcpy-1.3.5 → npcpy-1.3.7/npcpy.egg-info}/PKG-INFO +1 -1
- {npcpy-1.3.5 → npcpy-1.3.7}/setup.py +1 -1
- {npcpy-1.3.5 → npcpy-1.3.7}/LICENSE +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/MANIFEST.in +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/README.md +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/__init__.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/build_funcs.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/data/__init__.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/data/audio.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/data/data_models.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/data/image.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/data/load.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/data/text.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/data/video.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/data/web.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/ft/__init__.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/ft/diff.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/ft/ge.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/ft/memory_trainer.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/ft/model_ensembler.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/ft/rl.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/ft/sft.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/ft/usft.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/gen/__init__.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/gen/audio_gen.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/gen/embeddings.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/gen/image_gen.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/gen/ocr.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/gen/video_gen.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/gen/world_gen.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/main.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/memory/__init__.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/memory/kg_vis.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/memory/memory_processor.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/memory/search.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/mix/__init__.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/mix/debate.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/ml_funcs.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/npc_array.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/npc_compiler.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/npcs.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/sql/__init__.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/sql/ai_function_tools.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/sql/database_ai_adapters.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/sql/database_ai_functions.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/sql/model_runner.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/sql/npcsql.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/sql/sql_model_compiler.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/tools.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/work/__init__.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/work/browser.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/work/desktop.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/work/plan.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy/work/trigger.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy.egg-info/SOURCES.txt +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy.egg-info/dependency_links.txt +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy.egg-info/requires.txt +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/npcpy.egg-info/top_level.txt +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/setup.cfg +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/tests/test_audio.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/tests/test_command_history.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/tests/test_image.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/tests/test_llm_funcs.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/tests/test_load.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/tests/test_npc_array.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/tests/test_npc_compiler.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/tests/test_npcsql.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/tests/test_response.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/tests/test_serve.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/tests/test_text.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/tests/test_tools.py +0 -0
- {npcpy-1.3.5 → npcpy-1.3.7}/tests/test_web.py +0 -0
|
@@ -570,6 +570,106 @@ def get_ollama_response(
|
|
|
570
570
|
import time
|
|
571
571
|
|
|
572
572
|
|
|
573
|
+
def get_llamacpp_response(
|
|
574
|
+
prompt: str = None,
|
|
575
|
+
model: str = None,
|
|
576
|
+
images: List[str] = None,
|
|
577
|
+
tools: list = None,
|
|
578
|
+
tool_choice: Dict = None,
|
|
579
|
+
tool_map: Dict = None,
|
|
580
|
+
think=None,
|
|
581
|
+
format: Union[str, BaseModel] = None,
|
|
582
|
+
messages: List[Dict[str, str]] = None,
|
|
583
|
+
stream: bool = False,
|
|
584
|
+
attachments: List[str] = None,
|
|
585
|
+
auto_process_tool_calls: bool = False,
|
|
586
|
+
**kwargs,
|
|
587
|
+
) -> Dict[str, Any]:
|
|
588
|
+
"""
|
|
589
|
+
Generate response using llama-cpp-python for local GGUF/GGML files.
|
|
590
|
+
"""
|
|
591
|
+
try:
|
|
592
|
+
from llama_cpp import Llama
|
|
593
|
+
except ImportError:
|
|
594
|
+
return {
|
|
595
|
+
"response": "",
|
|
596
|
+
"messages": messages or [],
|
|
597
|
+
"error": "llama-cpp-python not installed. Install with: pip install llama-cpp-python"
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
result = {
|
|
601
|
+
"response": None,
|
|
602
|
+
"messages": messages.copy() if messages else [],
|
|
603
|
+
"raw_response": None,
|
|
604
|
+
"tool_calls": [],
|
|
605
|
+
"tool_results": []
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if prompt:
|
|
609
|
+
if messages and messages[-1]["role"] == "user":
|
|
610
|
+
messages[-1]["content"] = prompt
|
|
611
|
+
else:
|
|
612
|
+
if not messages:
|
|
613
|
+
messages = []
|
|
614
|
+
messages.append({"role": "user", "content": prompt})
|
|
615
|
+
|
|
616
|
+
try:
|
|
617
|
+
# Load model
|
|
618
|
+
n_ctx = kwargs.get("n_ctx", 4096)
|
|
619
|
+
n_gpu_layers = kwargs.get("n_gpu_layers", -1) # -1 = all layers on GPU if available
|
|
620
|
+
|
|
621
|
+
llm = Llama(
|
|
622
|
+
model_path=model,
|
|
623
|
+
n_ctx=n_ctx,
|
|
624
|
+
n_gpu_layers=n_gpu_layers,
|
|
625
|
+
verbose=False
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
# Build params
|
|
629
|
+
params = {
|
|
630
|
+
"messages": messages,
|
|
631
|
+
"stream": stream,
|
|
632
|
+
}
|
|
633
|
+
if kwargs.get("temperature"):
|
|
634
|
+
params["temperature"] = kwargs["temperature"]
|
|
635
|
+
if kwargs.get("max_tokens"):
|
|
636
|
+
params["max_tokens"] = kwargs["max_tokens"]
|
|
637
|
+
if kwargs.get("top_p"):
|
|
638
|
+
params["top_p"] = kwargs["top_p"]
|
|
639
|
+
if kwargs.get("stop"):
|
|
640
|
+
params["stop"] = kwargs["stop"]
|
|
641
|
+
|
|
642
|
+
if stream:
|
|
643
|
+
response = llm.create_chat_completion(**params)
|
|
644
|
+
|
|
645
|
+
def generate():
|
|
646
|
+
for chunk in response:
|
|
647
|
+
# Yield the full chunk dict for proper streaming handling
|
|
648
|
+
yield chunk
|
|
649
|
+
|
|
650
|
+
result["response"] = generate()
|
|
651
|
+
else:
|
|
652
|
+
response = llm.create_chat_completion(**params)
|
|
653
|
+
result["raw_response"] = response
|
|
654
|
+
|
|
655
|
+
if response.get("choices"):
|
|
656
|
+
content = response["choices"][0].get("message", {}).get("content", "")
|
|
657
|
+
result["response"] = content
|
|
658
|
+
result["messages"].append({"role": "assistant", "content": content})
|
|
659
|
+
|
|
660
|
+
if response.get("usage"):
|
|
661
|
+
result["usage"] = {
|
|
662
|
+
"input_tokens": response["usage"].get("prompt_tokens", 0),
|
|
663
|
+
"output_tokens": response["usage"].get("completion_tokens", 0),
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
except Exception as e:
|
|
667
|
+
result["error"] = f"llama.cpp error: {str(e)}"
|
|
668
|
+
result["response"] = ""
|
|
669
|
+
|
|
670
|
+
return result
|
|
671
|
+
|
|
672
|
+
|
|
573
673
|
def get_litellm_response(
|
|
574
674
|
prompt: str = None,
|
|
575
675
|
model: str = None,
|
|
@@ -614,22 +714,37 @@ def get_litellm_response(
|
|
|
614
714
|
)
|
|
615
715
|
elif provider=='transformers':
|
|
616
716
|
return get_transformers_response(
|
|
617
|
-
prompt,
|
|
618
|
-
model,
|
|
619
|
-
images=images,
|
|
620
|
-
tools=tools,
|
|
621
|
-
tool_choice=tool_choice,
|
|
717
|
+
prompt,
|
|
718
|
+
model,
|
|
719
|
+
images=images,
|
|
720
|
+
tools=tools,
|
|
721
|
+
tool_choice=tool_choice,
|
|
622
722
|
tool_map=tool_map,
|
|
623
723
|
think=think,
|
|
624
|
-
format=format,
|
|
625
|
-
messages=messages,
|
|
626
|
-
stream=stream,
|
|
627
|
-
attachments=attachments,
|
|
628
|
-
auto_process_tool_calls=auto_process_tool_calls,
|
|
724
|
+
format=format,
|
|
725
|
+
messages=messages,
|
|
726
|
+
stream=stream,
|
|
727
|
+
attachments=attachments,
|
|
728
|
+
auto_process_tool_calls=auto_process_tool_calls,
|
|
629
729
|
**kwargs
|
|
630
730
|
|
|
631
731
|
)
|
|
632
|
-
|
|
732
|
+
elif provider == 'llamacpp':
|
|
733
|
+
return get_llamacpp_response(
|
|
734
|
+
prompt,
|
|
735
|
+
model,
|
|
736
|
+
images=images,
|
|
737
|
+
tools=tools,
|
|
738
|
+
tool_choice=tool_choice,
|
|
739
|
+
tool_map=tool_map,
|
|
740
|
+
think=think,
|
|
741
|
+
format=format,
|
|
742
|
+
messages=messages,
|
|
743
|
+
stream=stream,
|
|
744
|
+
attachments=attachments,
|
|
745
|
+
auto_process_tool_calls=auto_process_tool_calls,
|
|
746
|
+
**kwargs
|
|
747
|
+
)
|
|
633
748
|
|
|
634
749
|
if attachments:
|
|
635
750
|
for attachment in attachments:
|
|
@@ -753,7 +868,7 @@ def get_litellm_response(
|
|
|
753
868
|
model = model.split('-npc')[0]
|
|
754
869
|
provider = "openai"
|
|
755
870
|
|
|
756
|
-
if isinstance(format, BaseModel):
|
|
871
|
+
if isinstance(format, type) and issubclass(format, BaseModel):
|
|
757
872
|
api_params["response_format"] = format
|
|
758
873
|
if model is None:
|
|
759
874
|
model = os.environ.get("NPCSH_CHAT_MODEL", "llama3.2")
|
|
@@ -776,18 +776,23 @@ Instructions:
|
|
|
776
776
|
|
|
777
777
|
if required_inputs:
|
|
778
778
|
# Get just the parameter names (handle both string and dict formats)
|
|
779
|
+
# String inputs are required, dict inputs have defaults and are optional
|
|
779
780
|
required_names = []
|
|
781
|
+
optional_names = []
|
|
780
782
|
for inp in required_inputs:
|
|
781
783
|
if isinstance(inp, str):
|
|
784
|
+
# String inputs have no default, so they're required
|
|
782
785
|
required_names.append(inp)
|
|
783
786
|
elif isinstance(inp, dict):
|
|
784
|
-
|
|
787
|
+
# Dict inputs have default values, so they're optional
|
|
788
|
+
optional_names.extend(inp.keys())
|
|
785
789
|
|
|
786
|
-
# Check which required params are missing
|
|
790
|
+
# Check which required params are missing (only string inputs, not dict inputs with defaults)
|
|
787
791
|
missing = [p for p in required_names if p not in inputs or not inputs.get(p)]
|
|
788
792
|
provided = list(inputs.keys())
|
|
789
793
|
if missing:
|
|
790
|
-
|
|
794
|
+
all_params = required_names + optional_names
|
|
795
|
+
context = f"Error: jinx '{jinx_name}' requires parameters {required_names} but got {provided}. Missing: {missing}. Optional params with defaults: {optional_names}. Please retry with correct parameter names."
|
|
791
796
|
logger.debug(f"[_react_fallback] Missing required params: {missing}")
|
|
792
797
|
print(f"[REACT-DEBUG] Missing params for {jinx_name}: {missing}, got: {provided}")
|
|
793
798
|
continue
|
|
@@ -30,7 +30,22 @@ except NameError as e:
|
|
|
30
30
|
chromadb = None
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
import logging
|
|
33
|
+
import logging
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def normalize_path_for_db(path_str):
|
|
37
|
+
"""
|
|
38
|
+
Normalize a path for consistent database storage.
|
|
39
|
+
Converts backslashes to forward slashes for cross-platform compatibility.
|
|
40
|
+
"""
|
|
41
|
+
if not path_str:
|
|
42
|
+
return path_str
|
|
43
|
+
# Convert backslashes to forward slashes
|
|
44
|
+
normalized = path_str.replace('\\', '/')
|
|
45
|
+
# Remove trailing slashes for consistency
|
|
46
|
+
normalized = normalized.rstrip('/')
|
|
47
|
+
return normalized
|
|
48
|
+
|
|
34
49
|
|
|
35
50
|
def flush_messages(n: int, messages: list) -> dict:
|
|
36
51
|
if n <= 0:
|
|
@@ -852,6 +867,9 @@ class CommandHistory:
|
|
|
852
867
|
if tool_results is not None and not isinstance(tool_results, str):
|
|
853
868
|
tool_results = json.dumps(tool_results, cls=CustomJSONEncoder)
|
|
854
869
|
|
|
870
|
+
# Normalize directory path for cross-platform compatibility
|
|
871
|
+
normalized_directory_path = normalize_path_for_db(directory_path)
|
|
872
|
+
|
|
855
873
|
stmt = """
|
|
856
874
|
INSERT INTO conversation_history
|
|
857
875
|
(message_id, timestamp, role, content, conversation_id, directory_path, model, provider, npc, team, reasoning_content, tool_calls, tool_results)
|
|
@@ -859,7 +877,7 @@ class CommandHistory:
|
|
|
859
877
|
"""
|
|
860
878
|
params = {
|
|
861
879
|
"message_id": message_id, "timestamp": timestamp, "role": role, "content": content,
|
|
862
|
-
"conversation_id": conversation_id, "directory_path":
|
|
880
|
+
"conversation_id": conversation_id, "directory_path": normalized_directory_path, "model": model,
|
|
863
881
|
"provider": provider, "npc": npc, "team": team, "reasoning_content": reasoning_content,
|
|
864
882
|
"tool_calls": tool_calls, "tool_results": tool_results
|
|
865
883
|
}
|
|
@@ -879,28 +897,31 @@ class CommandHistory:
|
|
|
879
897
|
|
|
880
898
|
return message_id
|
|
881
899
|
|
|
882
|
-
def add_memory_to_database(self, message_id: str, conversation_id: str, npc: str, team: str,
|
|
883
|
-
directory_path: str, initial_memory: str, status: str,
|
|
900
|
+
def add_memory_to_database(self, message_id: str, conversation_id: str, npc: str, team: str,
|
|
901
|
+
directory_path: str, initial_memory: str, status: str,
|
|
884
902
|
model: str = None, provider: str = None, final_memory: str = None):
|
|
885
903
|
"""Store a memory entry in the database"""
|
|
886
904
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
887
|
-
|
|
905
|
+
|
|
906
|
+
# Normalize directory path for cross-platform compatibility
|
|
907
|
+
normalized_directory_path = normalize_path_for_db(directory_path)
|
|
908
|
+
|
|
888
909
|
stmt = """
|
|
889
|
-
INSERT INTO memory_lifecycle
|
|
890
|
-
(message_id, conversation_id, npc, team, directory_path, timestamp,
|
|
910
|
+
INSERT INTO memory_lifecycle
|
|
911
|
+
(message_id, conversation_id, npc, team, directory_path, timestamp,
|
|
891
912
|
initial_memory, final_memory, status, model, provider)
|
|
892
|
-
VALUES (:message_id, :conversation_id, :npc, :team, :directory_path,
|
|
913
|
+
VALUES (:message_id, :conversation_id, :npc, :team, :directory_path,
|
|
893
914
|
:timestamp, :initial_memory, :final_memory, :status, :model, :provider)
|
|
894
915
|
"""
|
|
895
|
-
|
|
916
|
+
|
|
896
917
|
params = {
|
|
897
918
|
"message_id": message_id, "conversation_id": conversation_id,
|
|
898
|
-
"npc": npc, "team": team, "directory_path":
|
|
919
|
+
"npc": npc, "team": team, "directory_path": normalized_directory_path,
|
|
899
920
|
"timestamp": timestamp, "initial_memory": initial_memory,
|
|
900
921
|
"final_memory": final_memory, "status": status,
|
|
901
922
|
"model": model, "provider": provider
|
|
902
923
|
}
|
|
903
|
-
|
|
924
|
+
|
|
904
925
|
return self._execute_returning_id(stmt, params)
|
|
905
926
|
def get_memories_for_scope(
|
|
906
927
|
self,
|
|
@@ -594,7 +594,7 @@ def kg_dream_process(existing_kg,
|
|
|
594
594
|
return existing_kg, {}
|
|
595
595
|
print(f" - Generated Dream: '{dream_text[:150]}...'")
|
|
596
596
|
|
|
597
|
-
dream_kg, _ = kg_evolve_incremental(existing_kg, dream_text, model, provider, npc,
|
|
597
|
+
dream_kg, _ = kg_evolve_incremental(existing_kg, new_content_text=dream_text, model=model, provider=provider, npc=npc, context=context)
|
|
598
598
|
|
|
599
599
|
original_fact_stmts = {f['statement'] for f in existing_kg['facts']}
|
|
600
600
|
for fact in dream_kg['facts']:
|
|
@@ -305,7 +305,33 @@ def get_locally_available_models(project_directory, airplane_mode=False):
|
|
|
305
305
|
available_models[mod] = "ollama"
|
|
306
306
|
except (ImportError, concurrent.futures.TimeoutError, Exception) as e:
|
|
307
307
|
logging.info(f"Error loading Ollama models or timed out: {e}")
|
|
308
|
-
|
|
308
|
+
|
|
309
|
+
# Scan for local GGUF/GGML models
|
|
310
|
+
gguf_dirs = [
|
|
311
|
+
os.path.expanduser('~/.npcsh/models/gguf'),
|
|
312
|
+
os.path.expanduser('~/.npcsh/models'),
|
|
313
|
+
os.path.expanduser('~/models'),
|
|
314
|
+
os.path.expanduser('~/.cache/huggingface/hub'),
|
|
315
|
+
]
|
|
316
|
+
env_gguf_dir = os.environ.get('NPCSH_GGUF_DIR')
|
|
317
|
+
if env_gguf_dir:
|
|
318
|
+
gguf_dirs.insert(0, os.path.expanduser(env_gguf_dir))
|
|
319
|
+
|
|
320
|
+
seen_paths = set()
|
|
321
|
+
for scan_dir in gguf_dirs:
|
|
322
|
+
if not os.path.isdir(scan_dir):
|
|
323
|
+
continue
|
|
324
|
+
try:
|
|
325
|
+
for root, dirs, files in os.walk(scan_dir):
|
|
326
|
+
for f in files:
|
|
327
|
+
if f.endswith(('.gguf', '.ggml')) and not f.startswith('.'):
|
|
328
|
+
full_path = os.path.join(root, f)
|
|
329
|
+
if full_path not in seen_paths:
|
|
330
|
+
seen_paths.add(full_path)
|
|
331
|
+
available_models[full_path] = "llamacpp"
|
|
332
|
+
except Exception as e:
|
|
333
|
+
logging.info(f"Error scanning GGUF directory {scan_dir}: {e}")
|
|
334
|
+
|
|
309
335
|
return available_models
|
|
310
336
|
|
|
311
337
|
|
|
@@ -88,6 +88,21 @@ cancellation_flags = {}
|
|
|
88
88
|
cancellation_lock = threading.Lock()
|
|
89
89
|
|
|
90
90
|
|
|
91
|
+
def normalize_path_for_db(path_str):
|
|
92
|
+
"""
|
|
93
|
+
Normalize a path for consistent database storage/querying.
|
|
94
|
+
Converts backslashes to forward slashes for cross-platform compatibility.
|
|
95
|
+
This ensures Windows paths match Unix paths in the database.
|
|
96
|
+
"""
|
|
97
|
+
if not path_str:
|
|
98
|
+
return path_str
|
|
99
|
+
# Convert backslashes to forward slashes
|
|
100
|
+
normalized = path_str.replace('\\', '/')
|
|
101
|
+
# Remove trailing slashes for consistency
|
|
102
|
+
normalized = normalized.rstrip('/')
|
|
103
|
+
return normalized
|
|
104
|
+
|
|
105
|
+
|
|
91
106
|
# Minimal MCP client (inlined from npcsh corca to avoid corca import)
|
|
92
107
|
class MCPClientNPC:
|
|
93
108
|
def __init__(self, debug: bool = True):
|
|
@@ -1172,14 +1187,9 @@ def get_models():
|
|
|
1172
1187
|
)
|
|
1173
1188
|
|
|
1174
1189
|
display_model = m
|
|
1175
|
-
if
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
display_model = "claude-3.5-sonnet"
|
|
1179
|
-
elif "gemini-1.5-flash" in m:
|
|
1180
|
-
display_model = "gemini-1.5-flash"
|
|
1181
|
-
elif "gemini-2.0-flash-lite-preview-02-05" in m:
|
|
1182
|
-
display_model = "gemini-2.0-flash-lite-preview"
|
|
1190
|
+
if m.endswith(('.gguf', '.ggml')):
|
|
1191
|
+
# For local GGUF/GGML files, show just the filename
|
|
1192
|
+
display_model = os.path.basename(m)
|
|
1183
1193
|
|
|
1184
1194
|
display_name = f"{display_model} | {p} {text_only}".strip()
|
|
1185
1195
|
|
|
@@ -2120,16 +2130,18 @@ def get_last_used_model_and_npc_in_directory(directory_path):
|
|
|
2120
2130
|
engine = get_db_connection()
|
|
2121
2131
|
try:
|
|
2122
2132
|
with engine.connect() as conn:
|
|
2133
|
+
# Normalize path for cross-platform compatibility
|
|
2123
2134
|
query = text("""
|
|
2124
2135
|
SELECT model, npc
|
|
2125
2136
|
FROM conversation_history
|
|
2126
|
-
WHERE directory_path = :
|
|
2127
|
-
AND model IS NOT NULL AND npc IS NOT NULL
|
|
2137
|
+
WHERE REPLACE(RTRIM(directory_path, '/\\'), '\\', '/') = :normalized_path
|
|
2138
|
+
AND model IS NOT NULL AND npc IS NOT NULL
|
|
2128
2139
|
AND model != '' AND npc != ''
|
|
2129
2140
|
ORDER BY timestamp DESC, id DESC
|
|
2130
2141
|
LIMIT 1
|
|
2131
2142
|
""")
|
|
2132
|
-
|
|
2143
|
+
normalized_path = normalize_path_for_db(directory_path)
|
|
2144
|
+
result = conn.execute(query, {"normalized_path": normalized_path}).fetchone()
|
|
2133
2145
|
return {"model": result[0], "npc": result[1]} if result else {"model": None, "npc": None}
|
|
2134
2146
|
except Exception as e:
|
|
2135
2147
|
print(f"Error getting last used model/NPC for directory {directory_path}: {e}")
|
|
@@ -3359,7 +3371,7 @@ def stream():
|
|
|
3359
3371
|
provider = data.get("provider", None)
|
|
3360
3372
|
if provider is None:
|
|
3361
3373
|
provider = available_models.get(model)
|
|
3362
|
-
|
|
3374
|
+
|
|
3363
3375
|
npc_name = data.get("npc", None)
|
|
3364
3376
|
npc_source = data.get("npcSource", "global")
|
|
3365
3377
|
current_path = data.get("currentPath")
|
|
@@ -3986,7 +3998,27 @@ def stream():
|
|
|
3986
3998
|
|
|
3987
3999
|
print('.', end="", flush=True)
|
|
3988
4000
|
dot_count += 1
|
|
3989
|
-
if
|
|
4001
|
+
if provider == 'llamacpp':
|
|
4002
|
+
# llama-cpp-python returns OpenAI-format dicts
|
|
4003
|
+
chunk_content = ""
|
|
4004
|
+
reasoning_content = None
|
|
4005
|
+
if isinstance(response_chunk, dict) and response_chunk.get("choices"):
|
|
4006
|
+
delta = response_chunk["choices"][0].get("delta", {})
|
|
4007
|
+
chunk_content = delta.get("content", "") or ""
|
|
4008
|
+
reasoning_content = delta.get("reasoning_content")
|
|
4009
|
+
if chunk_content:
|
|
4010
|
+
complete_response.append(chunk_content)
|
|
4011
|
+
if reasoning_content:
|
|
4012
|
+
complete_reasoning.append(reasoning_content)
|
|
4013
|
+
chunk_data = {
|
|
4014
|
+
"id": response_chunk.get("id"),
|
|
4015
|
+
"object": response_chunk.get("object"),
|
|
4016
|
+
"created": response_chunk.get("created"),
|
|
4017
|
+
"model": response_chunk.get("model", model),
|
|
4018
|
+
"choices": [{"index": 0, "delta": {"content": chunk_content, "role": "assistant", "reasoning_content": reasoning_content}, "finish_reason": response_chunk.get("choices", [{}])[0].get("finish_reason")}]
|
|
4019
|
+
}
|
|
4020
|
+
yield f"data: {json.dumps(chunk_data)}\n\n"
|
|
4021
|
+
elif "hf.co" in model or provider == 'ollama' and 'gpt-oss' not in model:
|
|
3990
4022
|
# Ollama returns ChatResponse objects - support both attribute and dict access
|
|
3991
4023
|
msg = getattr(response_chunk, "message", None) or response_chunk.get("message", {}) if hasattr(response_chunk, "get") else {}
|
|
3992
4024
|
chunk_content = getattr(msg, "content", None) or (msg.get("content") if hasattr(msg, "get") else "") or ""
|
|
@@ -4235,24 +4267,24 @@ def get_conversations():
|
|
|
4235
4267
|
engine = get_db_connection()
|
|
4236
4268
|
try:
|
|
4237
4269
|
with engine.connect() as conn:
|
|
4270
|
+
# Use REPLACE to normalize paths in the query for cross-platform compatibility
|
|
4271
|
+
# This handles both forward slashes and backslashes stored in the database
|
|
4238
4272
|
query = text("""
|
|
4239
4273
|
SELECT DISTINCT conversation_id,
|
|
4240
4274
|
MIN(timestamp) as start_time,
|
|
4241
4275
|
MAX(timestamp) as last_message_timestamp,
|
|
4242
4276
|
GROUP_CONCAT(content) as preview
|
|
4243
4277
|
FROM conversation_history
|
|
4244
|
-
WHERE directory_path
|
|
4278
|
+
WHERE REPLACE(RTRIM(directory_path, '/\\'), '\\', '/') = :normalized_path
|
|
4245
4279
|
GROUP BY conversation_id
|
|
4246
4280
|
ORDER BY MAX(timestamp) DESC
|
|
4247
4281
|
""")
|
|
4248
4282
|
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4283
|
+
# Normalize the input path (convert backslashes to forward slashes, strip trailing slashes)
|
|
4284
|
+
normalized_path = normalize_path_for_db(path)
|
|
4285
|
+
|
|
4253
4286
|
result = conn.execute(query, {
|
|
4254
|
-
"
|
|
4255
|
-
"path_with_slash": path_with_slash
|
|
4287
|
+
"normalized_path": normalized_path
|
|
4256
4288
|
})
|
|
4257
4289
|
conversations = result.fetchall()
|
|
4258
4290
|
|
|
@@ -4746,6 +4778,181 @@ def openai_list_models():
|
|
|
4746
4778
|
})
|
|
4747
4779
|
|
|
4748
4780
|
|
|
4781
|
+
# ============== GGUF/GGML Model Scanning ==============
|
|
4782
|
+
@app.route('/api/models/gguf/scan', methods=['GET'])
|
|
4783
|
+
def scan_gguf_models():
|
|
4784
|
+
"""Scan for GGUF/GGML model files in specified or default directories."""
|
|
4785
|
+
directory = request.args.get('directory')
|
|
4786
|
+
|
|
4787
|
+
# Default directories to scan
|
|
4788
|
+
default_dirs = [
|
|
4789
|
+
os.path.expanduser('~/.npcsh/models/gguf'),
|
|
4790
|
+
os.path.expanduser('~/.npcsh/models'),
|
|
4791
|
+
os.path.expanduser('~/models'),
|
|
4792
|
+
os.path.expanduser('~/.cache/huggingface/hub'),
|
|
4793
|
+
]
|
|
4794
|
+
|
|
4795
|
+
# Add env var directory if set
|
|
4796
|
+
env_dir = os.environ.get('NPCSH_GGUF_DIR')
|
|
4797
|
+
if env_dir:
|
|
4798
|
+
default_dirs.insert(0, os.path.expanduser(env_dir))
|
|
4799
|
+
|
|
4800
|
+
dirs_to_scan = [os.path.expanduser(directory)] if directory else default_dirs
|
|
4801
|
+
|
|
4802
|
+
models = []
|
|
4803
|
+
seen_paths = set()
|
|
4804
|
+
|
|
4805
|
+
for scan_dir in dirs_to_scan:
|
|
4806
|
+
if not os.path.isdir(scan_dir):
|
|
4807
|
+
continue
|
|
4808
|
+
|
|
4809
|
+
for root, dirs, files in os.walk(scan_dir):
|
|
4810
|
+
for f in files:
|
|
4811
|
+
if f.endswith(('.gguf', '.ggml', '.bin')) and not f.startswith('.'):
|
|
4812
|
+
full_path = os.path.join(root, f)
|
|
4813
|
+
if full_path not in seen_paths:
|
|
4814
|
+
seen_paths.add(full_path)
|
|
4815
|
+
try:
|
|
4816
|
+
size = os.path.getsize(full_path)
|
|
4817
|
+
models.append({
|
|
4818
|
+
'name': f,
|
|
4819
|
+
'path': full_path,
|
|
4820
|
+
'size': size,
|
|
4821
|
+
'size_gb': round(size / (1024**3), 2)
|
|
4822
|
+
})
|
|
4823
|
+
except OSError:
|
|
4824
|
+
pass
|
|
4825
|
+
|
|
4826
|
+
return jsonify({'models': models, 'error': None})
|
|
4827
|
+
|
|
4828
|
+
|
|
4829
|
+
@app.route('/api/models/hf/download', methods=['POST'])
|
|
4830
|
+
def download_hf_model():
|
|
4831
|
+
"""Download a GGUF model from HuggingFace."""
|
|
4832
|
+
data = request.json
|
|
4833
|
+
url = data.get('url', '')
|
|
4834
|
+
target_dir = data.get('target_dir', '~/.npcsh/models/gguf')
|
|
4835
|
+
|
|
4836
|
+
target_dir = os.path.expanduser(target_dir)
|
|
4837
|
+
os.makedirs(target_dir, exist_ok=True)
|
|
4838
|
+
|
|
4839
|
+
try:
|
|
4840
|
+
# Parse HuggingFace URL or model ID
|
|
4841
|
+
# Formats:
|
|
4842
|
+
# - TheBloke/Llama-2-7B-GGUF
|
|
4843
|
+
# - https://huggingface.co/TheBloke/Llama-2-7B-GGUF/resolve/main/llama-2-7b.Q4_K_M.gguf
|
|
4844
|
+
|
|
4845
|
+
if url.startswith('http'):
|
|
4846
|
+
# Direct URL - download the file
|
|
4847
|
+
import requests
|
|
4848
|
+
filename = url.split('/')[-1].split('?')[0]
|
|
4849
|
+
target_path = os.path.join(target_dir, filename)
|
|
4850
|
+
|
|
4851
|
+
print(f"Downloading {url} to {target_path}")
|
|
4852
|
+
response = requests.get(url, stream=True)
|
|
4853
|
+
response.raise_for_status()
|
|
4854
|
+
|
|
4855
|
+
with open(target_path, 'wb') as f:
|
|
4856
|
+
for chunk in response.iter_content(chunk_size=8192):
|
|
4857
|
+
f.write(chunk)
|
|
4858
|
+
|
|
4859
|
+
return jsonify({'path': target_path, 'error': None})
|
|
4860
|
+
else:
|
|
4861
|
+
# Model ID - use huggingface_hub to download
|
|
4862
|
+
try:
|
|
4863
|
+
from huggingface_hub import hf_hub_download, list_repo_files
|
|
4864
|
+
|
|
4865
|
+
# List files in repo to find GGUF files
|
|
4866
|
+
files = list_repo_files(url)
|
|
4867
|
+
gguf_files = [f for f in files if f.endswith('.gguf')]
|
|
4868
|
+
|
|
4869
|
+
if not gguf_files:
|
|
4870
|
+
return jsonify({'error': 'No GGUF files found in repository'}), 400
|
|
4871
|
+
|
|
4872
|
+
# Download the first/smallest Q4 quantized version or first available
|
|
4873
|
+
q4_files = [f for f in gguf_files if 'Q4' in f or 'q4' in f]
|
|
4874
|
+
file_to_download = q4_files[0] if q4_files else gguf_files[0]
|
|
4875
|
+
|
|
4876
|
+
print(f"Downloading {file_to_download} from {url}")
|
|
4877
|
+
path = hf_hub_download(
|
|
4878
|
+
repo_id=url,
|
|
4879
|
+
filename=file_to_download,
|
|
4880
|
+
local_dir=target_dir,
|
|
4881
|
+
local_dir_use_symlinks=False
|
|
4882
|
+
)
|
|
4883
|
+
|
|
4884
|
+
return jsonify({'path': path, 'error': None})
|
|
4885
|
+
except ImportError:
|
|
4886
|
+
return jsonify({'error': 'huggingface_hub not installed. Run: pip install huggingface_hub'}), 500
|
|
4887
|
+
|
|
4888
|
+
except Exception as e:
|
|
4889
|
+
print(f"Error downloading HF model: {e}")
|
|
4890
|
+
return jsonify({'error': str(e)}), 500
|
|
4891
|
+
|
|
4892
|
+
|
|
4893
|
+
# ============== Local Model Provider Status ==============
|
|
4894
|
+
@app.route('/api/models/local/scan', methods=['GET'])
|
|
4895
|
+
def scan_local_models():
|
|
4896
|
+
"""Scan for models from local providers (LM Studio, llama.cpp)."""
|
|
4897
|
+
provider = request.args.get('provider', '')
|
|
4898
|
+
|
|
4899
|
+
if provider == 'lmstudio':
|
|
4900
|
+
# LM Studio typically runs on port 1234
|
|
4901
|
+
try:
|
|
4902
|
+
import requests
|
|
4903
|
+
response = requests.get('http://127.0.0.1:1234/v1/models', timeout=2)
|
|
4904
|
+
if response.ok:
|
|
4905
|
+
data = response.json()
|
|
4906
|
+
models = [{'name': m.get('id', m.get('name', 'unknown'))} for m in data.get('data', [])]
|
|
4907
|
+
return jsonify({'models': models, 'error': None})
|
|
4908
|
+
except:
|
|
4909
|
+
pass
|
|
4910
|
+
return jsonify({'models': [], 'error': 'LM Studio not running or not accessible'})
|
|
4911
|
+
|
|
4912
|
+
elif provider == 'llamacpp':
|
|
4913
|
+
# llama.cpp server typically runs on port 8080
|
|
4914
|
+
try:
|
|
4915
|
+
import requests
|
|
4916
|
+
response = requests.get('http://127.0.0.1:8080/v1/models', timeout=2)
|
|
4917
|
+
if response.ok:
|
|
4918
|
+
data = response.json()
|
|
4919
|
+
models = [{'name': m.get('id', m.get('name', 'unknown'))} for m in data.get('data', [])]
|
|
4920
|
+
return jsonify({'models': models, 'error': None})
|
|
4921
|
+
except:
|
|
4922
|
+
pass
|
|
4923
|
+
return jsonify({'models': [], 'error': 'llama.cpp server not running or not accessible'})
|
|
4924
|
+
|
|
4925
|
+
return jsonify({'models': [], 'error': f'Unknown provider: {provider}'})
|
|
4926
|
+
|
|
4927
|
+
|
|
4928
|
+
@app.route('/api/models/local/status', methods=['GET'])
|
|
4929
|
+
def get_local_model_status():
|
|
4930
|
+
"""Check if a local model provider is running."""
|
|
4931
|
+
provider = request.args.get('provider', '')
|
|
4932
|
+
|
|
4933
|
+
if provider == 'lmstudio':
|
|
4934
|
+
try:
|
|
4935
|
+
import requests
|
|
4936
|
+
response = requests.get('http://127.0.0.1:1234/v1/models', timeout=2)
|
|
4937
|
+
if response.ok:
|
|
4938
|
+
return jsonify({'status': 'running'})
|
|
4939
|
+
except:
|
|
4940
|
+
pass
|
|
4941
|
+
return jsonify({'status': 'not_running'})
|
|
4942
|
+
|
|
4943
|
+
elif provider == 'llamacpp':
|
|
4944
|
+
try:
|
|
4945
|
+
import requests
|
|
4946
|
+
response = requests.get('http://127.0.0.1:8080/v1/models', timeout=2)
|
|
4947
|
+
if response.ok:
|
|
4948
|
+
return jsonify({'status': 'running'})
|
|
4949
|
+
except:
|
|
4950
|
+
pass
|
|
4951
|
+
return jsonify({'status': 'not_running'})
|
|
4952
|
+
|
|
4953
|
+
return jsonify({'status': 'unknown', 'error': f'Unknown provider: {provider}'})
|
|
4954
|
+
|
|
4955
|
+
|
|
4749
4956
|
def start_flask_server(
|
|
4750
4957
|
port=5337,
|
|
4751
4958
|
cors_origins=None,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|