npcpy 1.3.16__tar.gz → 1.3.18__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.16/npcpy.egg-info → npcpy-1.3.18}/PKG-INFO +1 -1
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/data/web.py +0 -1
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/gen/response.py +160 -2
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/memory/command_history.py +14 -5
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/ml_funcs.py +61 -16
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/npc_array.py +149 -1
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/npc_compiler.py +23 -12
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/npc_sysenv.py +183 -8
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/serve.py +846 -73
- {npcpy-1.3.16 → npcpy-1.3.18/npcpy.egg-info}/PKG-INFO +1 -1
- {npcpy-1.3.16 → npcpy-1.3.18}/setup.py +1 -1
- {npcpy-1.3.16 → npcpy-1.3.18}/tests/test_ml_funcs.py +58 -16
- {npcpy-1.3.16 → npcpy-1.3.18}/tests/test_npc_array.py +111 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/tests/test_npc_compiler.py +208 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/tests/test_npc_sysenv.py +66 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/LICENSE +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/MANIFEST.in +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/README.md +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/__init__.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/build_funcs.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/data/__init__.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/data/audio.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/data/data_models.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/data/image.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/data/load.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/data/text.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/data/video.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/ft/__init__.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/ft/diff.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/ft/ge.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/ft/memory_trainer.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/ft/model_ensembler.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/ft/rl.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/ft/sft.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/ft/usft.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/gen/__init__.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/gen/audio_gen.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/gen/embeddings.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/gen/image_gen.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/gen/ocr.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/gen/video_gen.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/gen/world_gen.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/llm_funcs.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/main.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/memory/__init__.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/memory/kg_vis.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/memory/knowledge_graph.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/memory/memory_processor.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/memory/search.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/mix/__init__.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/mix/debate.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/npcs.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/sql/__init__.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/sql/ai_function_tools.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/sql/database_ai_adapters.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/sql/database_ai_functions.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/sql/model_runner.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/sql/npcsql.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/sql/sql_model_compiler.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/tools.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/work/__init__.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/work/browser.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/work/desktop.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/work/plan.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy/work/trigger.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy.egg-info/SOURCES.txt +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy.egg-info/dependency_links.txt +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy.egg-info/requires.txt +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/npcpy.egg-info/top_level.txt +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/setup.cfg +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/tests/test_audio.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/tests/test_browser.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/tests/test_build_funcs.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/tests/test_command_history.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/tests/test_data_models.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/tests/test_diff.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/tests/test_documentation_examples.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/tests/test_genetic_evolver.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/tests/test_image.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/tests/test_llm_funcs.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/tests/test_load.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/tests/test_memory_processor.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/tests/test_model_runner.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/tests/test_npcsql.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/tests/test_response.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/tests/test_serve.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/tests/test_sql_adapters.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/tests/test_sql_compiler.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/tests/test_sql_functions.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/tests/test_text.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/tests/test_tools.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/tests/test_video.py +0 -0
- {npcpy-1.3.16 → npcpy-1.3.18}/tests/test_web.py +0 -0
|
@@ -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,
|
|
@@ -611,7 +611,9 @@ class CommandHistory:
|
|
|
611
611
|
Column('reasoning_content', Text), # For thinking tokens / chain of thought
|
|
612
612
|
Column('tool_calls', Text), # JSON array of tool calls made by assistant
|
|
613
613
|
Column('tool_results', Text), # JSON array of tool call results
|
|
614
|
-
Column('parent_message_id', String(50)) # Links assistant response to parent user message for broadcast grouping
|
|
614
|
+
Column('parent_message_id', String(50)), # Links assistant response to parent user message for broadcast grouping
|
|
615
|
+
Column('device_id', String(255)), # UUID of the device that created this message
|
|
616
|
+
Column('device_name', String(255)) # Human-readable device name
|
|
615
617
|
)
|
|
616
618
|
|
|
617
619
|
Table('message_attachments', metadata,
|
|
@@ -867,6 +869,8 @@ class CommandHistory:
|
|
|
867
869
|
tool_calls=None,
|
|
868
870
|
tool_results=None,
|
|
869
871
|
parent_message_id=None,
|
|
872
|
+
device_id=None,
|
|
873
|
+
device_name=None,
|
|
870
874
|
):
|
|
871
875
|
if isinstance(content, (dict, list)):
|
|
872
876
|
content = json.dumps(content, cls=CustomJSONEncoder)
|
|
@@ -882,14 +886,15 @@ class CommandHistory:
|
|
|
882
886
|
|
|
883
887
|
stmt = """
|
|
884
888
|
INSERT INTO conversation_history
|
|
885
|
-
(message_id, timestamp, role, content, conversation_id, directory_path, model, provider, npc, team, reasoning_content, tool_calls, tool_results, parent_message_id)
|
|
886
|
-
VALUES (:message_id, :timestamp, :role, :content, :conversation_id, :directory_path, :model, :provider, :npc, :team, :reasoning_content, :tool_calls, :tool_results, :parent_message_id)
|
|
889
|
+
(message_id, timestamp, role, content, conversation_id, directory_path, model, provider, npc, team, reasoning_content, tool_calls, tool_results, parent_message_id, device_id, device_name)
|
|
890
|
+
VALUES (:message_id, :timestamp, :role, :content, :conversation_id, :directory_path, :model, :provider, :npc, :team, :reasoning_content, :tool_calls, :tool_results, :parent_message_id, :device_id, :device_name)
|
|
887
891
|
"""
|
|
888
892
|
params = {
|
|
889
893
|
"message_id": message_id, "timestamp": timestamp, "role": role, "content": content,
|
|
890
894
|
"conversation_id": conversation_id, "directory_path": normalized_directory_path, "model": model,
|
|
891
895
|
"provider": provider, "npc": npc, "team": team, "reasoning_content": reasoning_content,
|
|
892
|
-
"tool_calls": tool_calls, "tool_results": tool_results, "parent_message_id": parent_message_id
|
|
896
|
+
"tool_calls": tool_calls, "tool_results": tool_results, "parent_message_id": parent_message_id,
|
|
897
|
+
"device_id": device_id, "device_name": device_name
|
|
893
898
|
}
|
|
894
899
|
with self.engine.begin() as conn:
|
|
895
900
|
conn.execute(text(stmt), params)
|
|
@@ -1461,6 +1466,8 @@ def save_conversation_message(
|
|
|
1461
1466
|
tool_results: List[Dict] = None,
|
|
1462
1467
|
parent_message_id: str = None,
|
|
1463
1468
|
skip_if_exists: bool = True,
|
|
1469
|
+
device_id: str = None,
|
|
1470
|
+
device_name: str = None,
|
|
1464
1471
|
):
|
|
1465
1472
|
"""
|
|
1466
1473
|
Saves a conversation message linked to a conversation ID with optional attachments.
|
|
@@ -1495,7 +1502,9 @@ def save_conversation_message(
|
|
|
1495
1502
|
reasoning_content=reasoning_content,
|
|
1496
1503
|
tool_calls=tool_calls,
|
|
1497
1504
|
tool_results=tool_results,
|
|
1498
|
-
parent_message_id=parent_message_id
|
|
1505
|
+
parent_message_id=parent_message_id,
|
|
1506
|
+
device_id=device_id,
|
|
1507
|
+
device_name=device_name)
|
|
1499
1508
|
def retrieve_last_conversation(
|
|
1500
1509
|
command_history: CommandHistory, conversation_id: str
|
|
1501
1510
|
) -> str:
|
|
@@ -16,7 +16,6 @@ Same interface pattern as llm_funcs:
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
import copy
|
|
18
18
|
import itertools
|
|
19
|
-
import pickle
|
|
20
19
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
21
20
|
from dataclasses import dataclass, field
|
|
22
21
|
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
|
@@ -708,21 +707,67 @@ def cross_validate(
|
|
|
708
707
|
|
|
709
708
|
# ==================== Utility Functions ====================
|
|
710
709
|
|
|
711
|
-
def serialize_model(model: Any, path: str =
|
|
712
|
-
"""
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
710
|
+
def serialize_model(model: Any, path: str, format: str = "joblib") -> None:
|
|
711
|
+
"""
|
|
712
|
+
Serialize model to file using safe formats (no pickle).
|
|
713
|
+
|
|
714
|
+
Args:
|
|
715
|
+
model: The model to serialize
|
|
716
|
+
path: File path to write to (required)
|
|
717
|
+
format: Serialization format - "joblib" (default) or "safetensors"
|
|
718
|
+
|
|
719
|
+
Raises:
|
|
720
|
+
ImportError: If required library is not installed
|
|
721
|
+
ValueError: If format is not supported for the model type
|
|
722
|
+
"""
|
|
723
|
+
if format == "safetensors":
|
|
724
|
+
from safetensors.torch import save_file
|
|
725
|
+
if hasattr(model, 'state_dict'):
|
|
726
|
+
save_file(model.state_dict(), path)
|
|
727
|
+
else:
|
|
728
|
+
raise ValueError("safetensors format requires model with state_dict (PyTorch)")
|
|
729
|
+
elif format == "joblib":
|
|
730
|
+
import joblib
|
|
731
|
+
joblib.dump(model, path)
|
|
732
|
+
else:
|
|
733
|
+
raise ValueError(f"Unsupported format: {format}. Use 'joblib' or 'safetensors'.")
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
def deserialize_model(path: str, format: str = "auto") -> Any:
|
|
737
|
+
"""
|
|
738
|
+
Deserialize model from file using safe formats (no pickle).
|
|
739
|
+
|
|
740
|
+
Args:
|
|
741
|
+
path: File path to load from
|
|
742
|
+
format: "auto" (detect from extension), "joblib", or "safetensors"
|
|
743
|
+
|
|
744
|
+
Returns:
|
|
745
|
+
The deserialized model
|
|
746
|
+
|
|
747
|
+
Raises:
|
|
748
|
+
ImportError: If required library is not installed
|
|
749
|
+
ValueError: If format cannot be determined
|
|
750
|
+
"""
|
|
751
|
+
# Auto-detect format from extension
|
|
752
|
+
if format == "auto":
|
|
753
|
+
if path.endswith('.safetensors'):
|
|
754
|
+
format = "safetensors"
|
|
755
|
+
elif path.endswith('.joblib'):
|
|
756
|
+
format = "joblib"
|
|
757
|
+
else:
|
|
758
|
+
raise ValueError(
|
|
759
|
+
f"Cannot auto-detect format for {path}. "
|
|
760
|
+
"Use .joblib or .safetensors extension, or specify format explicitly."
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
if format == "safetensors":
|
|
764
|
+
from safetensors.torch import load_file
|
|
765
|
+
return load_file(path)
|
|
766
|
+
elif format == "joblib":
|
|
767
|
+
import joblib
|
|
768
|
+
return joblib.load(path)
|
|
769
|
+
else:
|
|
770
|
+
raise ValueError(f"Unsupported format: {format}. Use 'joblib' or 'safetensors'.")
|
|
726
771
|
|
|
727
772
|
|
|
728
773
|
def get_model_params(model: Any) -> Dict[str, Any]:
|
|
@@ -20,7 +20,6 @@ Example:
|
|
|
20
20
|
from __future__ import annotations
|
|
21
21
|
import copy
|
|
22
22
|
import itertools
|
|
23
|
-
import pickle
|
|
24
23
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
25
24
|
from dataclasses import dataclass, field
|
|
26
25
|
from typing import (
|
|
@@ -47,6 +46,7 @@ class OpType(Enum):
|
|
|
47
46
|
REDUCE = "reduce"
|
|
48
47
|
CHAIN = "chain"
|
|
49
48
|
EVOLVE = "evolve"
|
|
49
|
+
JINX = "jinx" # Execute a Jinx workflow across models
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
@dataclass
|
|
@@ -328,6 +328,61 @@ class NPCArray:
|
|
|
328
328
|
|
|
329
329
|
return cls(specs)
|
|
330
330
|
|
|
331
|
+
@classmethod
|
|
332
|
+
def from_matrix(
|
|
333
|
+
cls,
|
|
334
|
+
matrix: List[Dict[str, Any]]
|
|
335
|
+
) -> 'NPCArray':
|
|
336
|
+
"""
|
|
337
|
+
Create NPCArray from a matrix of model configurations.
|
|
338
|
+
|
|
339
|
+
This is particularly useful for defining model arrays in Jinx templates
|
|
340
|
+
where you want explicit control over each model configuration.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
matrix: List of model configuration dicts. Each dict should have:
|
|
344
|
+
- 'model': model name/reference (required)
|
|
345
|
+
- 'provider': provider name (optional)
|
|
346
|
+
- 'type': model type - 'llm', 'npc', 'sklearn', 'torch' (default: 'llm')
|
|
347
|
+
- Any additional config parameters
|
|
348
|
+
|
|
349
|
+
Example:
|
|
350
|
+
>>> # In a Jinx template, define a matrix of models:
|
|
351
|
+
>>> matrix = [
|
|
352
|
+
... {'model': 'gpt-4', 'provider': 'openai', 'temperature': 0.7},
|
|
353
|
+
... {'model': 'claude-3-opus', 'provider': 'anthropic', 'temperature': 0.5},
|
|
354
|
+
... {'model': 'llama3.2', 'provider': 'ollama', 'temperature': 0.8},
|
|
355
|
+
... ]
|
|
356
|
+
>>> arr = NPCArray.from_matrix(matrix)
|
|
357
|
+
|
|
358
|
+
>>> # Mixed model types:
|
|
359
|
+
>>> matrix = [
|
|
360
|
+
... {'model': 'gpt-4', 'type': 'llm', 'provider': 'openai'},
|
|
361
|
+
... {'model': my_npc, 'type': 'npc'},
|
|
362
|
+
... {'model': sklearn_model, 'type': 'sklearn'},
|
|
363
|
+
... ]
|
|
364
|
+
"""
|
|
365
|
+
specs = []
|
|
366
|
+
for config in matrix:
|
|
367
|
+
model_type = config.get('type', 'llm')
|
|
368
|
+
model_ref = config.get('model')
|
|
369
|
+
provider = config.get('provider')
|
|
370
|
+
|
|
371
|
+
# Extract config params (everything except type, model, provider)
|
|
372
|
+
extra_config = {
|
|
373
|
+
k: v for k, v in config.items()
|
|
374
|
+
if k not in ('type', 'model', 'provider')
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
specs.append(ModelSpec(
|
|
378
|
+
model_type=model_type,
|
|
379
|
+
model_ref=model_ref,
|
|
380
|
+
provider=provider,
|
|
381
|
+
config=extra_config
|
|
382
|
+
))
|
|
383
|
+
|
|
384
|
+
return cls(specs)
|
|
385
|
+
|
|
331
386
|
# ==================== Properties ====================
|
|
332
387
|
|
|
333
388
|
@property
|
|
@@ -490,6 +545,43 @@ class NPCArray:
|
|
|
490
545
|
|
|
491
546
|
return NPCArray(self._specs, new_node)
|
|
492
547
|
|
|
548
|
+
def jinx(
|
|
549
|
+
self,
|
|
550
|
+
jinx_name: str,
|
|
551
|
+
inputs: Optional[Dict[str, Any]] = None,
|
|
552
|
+
**kwargs
|
|
553
|
+
) -> 'LazyResult':
|
|
554
|
+
"""
|
|
555
|
+
Execute a Jinx workflow across all models in the array.
|
|
556
|
+
|
|
557
|
+
Each model in the array will be used as the 'npc' context for the jinx,
|
|
558
|
+
allowing you to run the same workflow template with different models.
|
|
559
|
+
|
|
560
|
+
Args:
|
|
561
|
+
jinx_name: Name of the jinx workflow to execute (e.g., 'analyze', 'summarize')
|
|
562
|
+
inputs: Input values for the jinx template variables
|
|
563
|
+
**kwargs: Additional execution parameters
|
|
564
|
+
|
|
565
|
+
Returns:
|
|
566
|
+
LazyResult with workflow outputs from each model
|
|
567
|
+
|
|
568
|
+
Example:
|
|
569
|
+
>>> models = NPCArray.from_llms(['gpt-4', 'claude-3'])
|
|
570
|
+
>>> results = models.jinx('analyze', inputs={'topic': 'AI safety'}).collect()
|
|
571
|
+
"""
|
|
572
|
+
new_node = GraphNode(
|
|
573
|
+
op_type=OpType.JINX,
|
|
574
|
+
params={
|
|
575
|
+
"jinx_name": jinx_name,
|
|
576
|
+
"inputs": inputs or {},
|
|
577
|
+
**kwargs
|
|
578
|
+
},
|
|
579
|
+
parents=[self._graph],
|
|
580
|
+
shape=(len(self._specs),)
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
return LazyResult(self._specs, new_node)
|
|
584
|
+
|
|
493
585
|
|
|
494
586
|
class LazyResult:
|
|
495
587
|
"""
|
|
@@ -792,6 +884,7 @@ class GraphExecutor:
|
|
|
792
884
|
OpType.REDUCE: self._exec_reduce,
|
|
793
885
|
OpType.CHAIN: self._exec_chain,
|
|
794
886
|
OpType.EVOLVE: self._exec_evolve,
|
|
887
|
+
OpType.JINX: self._exec_jinx,
|
|
795
888
|
}
|
|
796
889
|
|
|
797
890
|
handler = handlers.get(node.op_type)
|
|
@@ -1136,6 +1229,61 @@ class GraphExecutor:
|
|
|
1136
1229
|
metadata={"operation": "evolve", "generation": 1}
|
|
1137
1230
|
)
|
|
1138
1231
|
|
|
1232
|
+
def _exec_jinx(self, node, specs, prompts, parents) -> ResponseTensor:
|
|
1233
|
+
"""Execute a Jinx workflow across models"""
|
|
1234
|
+
from npcpy.npc_compiler import NPC, Jinx
|
|
1235
|
+
|
|
1236
|
+
jinx_name = node.params.get("jinx_name")
|
|
1237
|
+
inputs = node.params.get("inputs", {})
|
|
1238
|
+
extra_kwargs = {k: v for k, v in node.params.items()
|
|
1239
|
+
if k not in ("jinx_name", "inputs")}
|
|
1240
|
+
|
|
1241
|
+
results = []
|
|
1242
|
+
|
|
1243
|
+
def run_jinx_single(spec: ModelSpec) -> str:
|
|
1244
|
+
"""Run jinx for a single model spec"""
|
|
1245
|
+
try:
|
|
1246
|
+
if spec.model_type == "npc":
|
|
1247
|
+
# Use the NPC directly
|
|
1248
|
+
npc = spec.model_ref
|
|
1249
|
+
else:
|
|
1250
|
+
# Create a temporary NPC with the model
|
|
1251
|
+
npc = NPC(
|
|
1252
|
+
name=f"array_npc_{spec.model_ref}",
|
|
1253
|
+
model=spec.model_ref,
|
|
1254
|
+
provider=spec.provider
|
|
1255
|
+
)
|
|
1256
|
+
|
|
1257
|
+
# Execute the jinx
|
|
1258
|
+
result = npc.execute_jinx(
|
|
1259
|
+
jinx_name=jinx_name,
|
|
1260
|
+
input_values=inputs,
|
|
1261
|
+
**extra_kwargs
|
|
1262
|
+
)
|
|
1263
|
+
return result.get("output", str(result))
|
|
1264
|
+
except Exception as e:
|
|
1265
|
+
return f"Error: {e}"
|
|
1266
|
+
|
|
1267
|
+
if self.parallel and len(specs) > 1:
|
|
1268
|
+
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
|
1269
|
+
futures = {executor.submit(run_jinx_single, spec): i
|
|
1270
|
+
for i, spec in enumerate(specs)}
|
|
1271
|
+
results = [None] * len(specs)
|
|
1272
|
+
for future in as_completed(futures):
|
|
1273
|
+
idx = futures[future]
|
|
1274
|
+
try:
|
|
1275
|
+
results[idx] = future.result()
|
|
1276
|
+
except Exception as e:
|
|
1277
|
+
results[idx] = f"Error: {e}"
|
|
1278
|
+
else:
|
|
1279
|
+
results = [run_jinx_single(spec) for spec in specs]
|
|
1280
|
+
|
|
1281
|
+
return ResponseTensor(
|
|
1282
|
+
data=np.array(results, dtype=object),
|
|
1283
|
+
model_specs=specs,
|
|
1284
|
+
metadata={"operation": "jinx", "jinx_name": jinx_name, **inputs}
|
|
1285
|
+
)
|
|
1286
|
+
|
|
1139
1287
|
|
|
1140
1288
|
def _compute_response_variance(responses: List[str]) -> float:
|
|
1141
1289
|
"""Compute semantic variance across responses"""
|
|
@@ -52,6 +52,7 @@ import fnmatch
|
|
|
52
52
|
import subprocess
|
|
53
53
|
from typing import Any, Dict, List, Optional, Union, Callable, Tuple
|
|
54
54
|
from jinja2 import Environment, FileSystemLoader, Template, Undefined, DictLoader
|
|
55
|
+
from jinja2.sandbox import SandboxedEnvironment
|
|
55
56
|
from sqlalchemy import create_engine, text
|
|
56
57
|
import npcpy as npy
|
|
57
58
|
from npcpy.tools import auto_tools
|
|
@@ -231,7 +232,8 @@ def load_yaml_file(file_path):
|
|
|
231
232
|
|
|
232
233
|
# First pass: render Jinja2 templates to produce valid YAML
|
|
233
234
|
# This allows {% if %} and other control structures to work
|
|
234
|
-
|
|
235
|
+
# Use SandboxedEnvironment to prevent template injection attacks
|
|
236
|
+
jinja_env = SandboxedEnvironment(undefined=SilentUndefined)
|
|
235
237
|
# Configure tojson filter to handle SilentUndefined
|
|
236
238
|
jinja_env.policies['json.dumps_function'] = _json_dumps_with_undefined
|
|
237
239
|
template = jinja_env.from_string(content)
|
|
@@ -694,7 +696,8 @@ class Jinx:
|
|
|
694
696
|
jinja_env: Optional[Environment] = None):
|
|
695
697
|
|
|
696
698
|
if jinja_env is None:
|
|
697
|
-
|
|
699
|
+
# Use SandboxedEnvironment to prevent template injection attacks
|
|
700
|
+
jinja_env = SandboxedEnvironment(
|
|
698
701
|
loader=DictLoader({}),
|
|
699
702
|
undefined=SilentUndefined,
|
|
700
703
|
)
|
|
@@ -771,21 +774,24 @@ class Jinx:
|
|
|
771
774
|
|
|
772
775
|
self._log_debug(f"DEBUG: Executing step '{step_name}' with rendered code: {rendered_code}")
|
|
773
776
|
|
|
777
|
+
# Import NPCArray for array operations in jinx
|
|
778
|
+
from npcpy.npc_array import NPCArray, infer_matrix, ensemble_vote
|
|
779
|
+
|
|
774
780
|
exec_globals = {
|
|
775
781
|
"__builtins__": __builtins__,
|
|
776
782
|
"npc": active_npc,
|
|
777
783
|
"context": context, # Pass context by reference
|
|
778
|
-
"math": math,
|
|
779
|
-
"random": random,
|
|
784
|
+
"math": math,
|
|
785
|
+
"random": random,
|
|
780
786
|
"datetime": datetime,
|
|
781
787
|
"Image": Image,
|
|
782
788
|
"pd": pd,
|
|
783
789
|
"plt": plt,
|
|
784
|
-
"sys": sys,
|
|
790
|
+
"sys": sys,
|
|
785
791
|
"subprocess": subprocess,
|
|
786
792
|
"np": np,
|
|
787
793
|
"os": os,
|
|
788
|
-
're': re,
|
|
794
|
+
're': re,
|
|
789
795
|
"json": json,
|
|
790
796
|
"Path": pathlib.Path,
|
|
791
797
|
"fnmatch": fnmatch,
|
|
@@ -793,6 +799,10 @@ class Jinx:
|
|
|
793
799
|
"subprocess": subprocess,
|
|
794
800
|
"get_llm_response": npy.llm_funcs.get_llm_response,
|
|
795
801
|
"CommandHistory": CommandHistory,
|
|
802
|
+
# NPCArray support for compute graph operations in jinx
|
|
803
|
+
"NPCArray": NPCArray,
|
|
804
|
+
"infer_matrix": infer_matrix,
|
|
805
|
+
"ensemble_vote": ensemble_vote,
|
|
796
806
|
}
|
|
797
807
|
|
|
798
808
|
if extra_globals:
|
|
@@ -1261,7 +1271,8 @@ class NPC:
|
|
|
1261
1271
|
dirs.append(self.jinxs_directory)
|
|
1262
1272
|
|
|
1263
1273
|
# This jinja_env is for the *second pass* (runtime variable resolution in Jinx.execute)
|
|
1264
|
-
|
|
1274
|
+
# Use SandboxedEnvironment to prevent template injection attacks
|
|
1275
|
+
self.jinja_env = SandboxedEnvironment(
|
|
1265
1276
|
loader=FileSystemLoader([
|
|
1266
1277
|
os.path.expanduser(d) for d in dirs
|
|
1267
1278
|
]),
|
|
@@ -1389,13 +1400,13 @@ class NPC:
|
|
|
1389
1400
|
|
|
1390
1401
|
combined_raw_jinxs_dict = {j.jinx_name: j for j in all_available_raw_jinxs}
|
|
1391
1402
|
|
|
1392
|
-
npc_first_pass_jinja_env =
|
|
1393
|
-
|
|
1403
|
+
npc_first_pass_jinja_env = SandboxedEnvironment(undefined=SilentUndefined)
|
|
1404
|
+
|
|
1394
1405
|
jinx_macro_globals = {}
|
|
1395
1406
|
for raw_jinx in combined_raw_jinxs_dict.values():
|
|
1396
1407
|
def create_jinx_callable(jinx_obj_in_closure):
|
|
1397
1408
|
def callable_jinx(**kwargs):
|
|
1398
|
-
temp_jinja_env =
|
|
1409
|
+
temp_jinja_env = SandboxedEnvironment(undefined=SilentUndefined)
|
|
1399
1410
|
rendered_target_steps = []
|
|
1400
1411
|
for target_step in jinx_obj_in_closure._raw_steps:
|
|
1401
1412
|
temp_rendered_step = {}
|
|
@@ -2506,7 +2517,7 @@ class Team:
|
|
|
2506
2517
|
self._raw_jinxs_list: List['Jinx'] = [] # Temporary storage for raw Team-level Jinx objects
|
|
2507
2518
|
self.jinx_tool_catalog: Dict[str, Dict[str, Any]] = {} # Jinx-derived tool defs ready for MCP/LLM
|
|
2508
2519
|
|
|
2509
|
-
self.jinja_env_for_first_pass =
|
|
2520
|
+
self.jinja_env_for_first_pass = SandboxedEnvironment(undefined=SilentUndefined) # Env for macro expansion
|
|
2510
2521
|
|
|
2511
2522
|
self.db_conn = db_conn
|
|
2512
2523
|
self.team_path = os.path.expanduser(team_path) if team_path else None
|
|
@@ -2700,7 +2711,7 @@ class Team:
|
|
|
2700
2711
|
def callable_jinx(**kwargs):
|
|
2701
2712
|
# This callable will be invoked by the Jinja renderer during the first pass.
|
|
2702
2713
|
# It needs to render the target Jinx's *raw* steps with the provided kwargs.
|
|
2703
|
-
temp_jinja_env =
|
|
2714
|
+
temp_jinja_env = SandboxedEnvironment(undefined=SilentUndefined)
|
|
2704
2715
|
|
|
2705
2716
|
rendered_target_steps = []
|
|
2706
2717
|
for target_step in jinx_obj_in_closure._raw_steps:
|