npcpy 1.2.32__py3-none-any.whl → 1.2.34__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.
- npcpy/llm_funcs.py +57 -37
- npcpy/memory/command_history.py +26 -1
- npcpy/npc_compiler.py +591 -456
- npcpy/serve.py +362 -291
- {npcpy-1.2.32.dist-info → npcpy-1.2.34.dist-info}/METADATA +97 -34
- {npcpy-1.2.32.dist-info → npcpy-1.2.34.dist-info}/RECORD +9 -9
- {npcpy-1.2.32.dist-info → npcpy-1.2.34.dist-info}/WHEEL +0 -0
- {npcpy-1.2.32.dist-info → npcpy-1.2.34.dist-info}/licenses/LICENSE +0 -0
- {npcpy-1.2.32.dist-info → npcpy-1.2.34.dist-info}/top_level.txt +0 -0
npcpy/serve.py
CHANGED
|
@@ -7,8 +7,9 @@ import uuid
|
|
|
7
7
|
import sys
|
|
8
8
|
import traceback
|
|
9
9
|
import glob
|
|
10
|
+
import re
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
import io
|
|
12
13
|
from flask_cors import CORS
|
|
13
14
|
import os
|
|
14
15
|
import sqlite3
|
|
@@ -29,6 +30,20 @@ try:
|
|
|
29
30
|
import ollama
|
|
30
31
|
except:
|
|
31
32
|
pass
|
|
33
|
+
from jinja2 import Environment, FileSystemLoader, Template, Undefined, DictLoader
|
|
34
|
+
class SilentUndefined(Undefined):
|
|
35
|
+
def _fail_with_undefined_error(self, *args, **kwargs):
|
|
36
|
+
return ""
|
|
37
|
+
|
|
38
|
+
# Import ShellState and helper functions from npcsh
|
|
39
|
+
from npcsh._state import ShellState
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
from npcpy.memory.knowledge_graph import load_kg_from_db
|
|
43
|
+
from npcpy.memory.search import execute_rag_command, execute_brainblast_command
|
|
44
|
+
from npcpy.data.load import load_file_contents
|
|
45
|
+
from npcpy.data.web import search_web
|
|
46
|
+
from npcsh._state import get_relevant_memories, search_kg_facts
|
|
32
47
|
|
|
33
48
|
import base64
|
|
34
49
|
import shutil
|
|
@@ -535,46 +550,63 @@ def get_available_jinxs():
|
|
|
535
550
|
traceback.print_exc()
|
|
536
551
|
return jsonify({'jinxs': [], 'error': str(e)}), 500
|
|
537
552
|
|
|
538
|
-
@app.route(
|
|
553
|
+
@app.route('/api/jinxs/global', methods=['GET'])
|
|
539
554
|
def get_global_jinxs():
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
555
|
+
global_jinxs_dir = os.path.expanduser('~/.npcsh/npc_team/jinxs')
|
|
556
|
+
|
|
557
|
+
# Directories to exclude entirely
|
|
558
|
+
excluded_dirs = ['core', 'npc_studio']
|
|
559
|
+
|
|
560
|
+
code_jinxs = []
|
|
561
|
+
mode_jinxs = []
|
|
562
|
+
util_jinxs = []
|
|
563
|
+
|
|
564
|
+
if os.path.exists(global_jinxs_dir):
|
|
565
|
+
for root, dirs, files in os.walk(global_jinxs_dir):
|
|
566
|
+
# Filter out excluded directories
|
|
567
|
+
dirs[:] = [d for d in dirs if d not in excluded_dirs]
|
|
568
|
+
|
|
569
|
+
for filename in files:
|
|
570
|
+
if filename.endswith('.jinx'):
|
|
571
|
+
try:
|
|
572
|
+
jinx_path = os.path.join(root, filename)
|
|
573
|
+
with open(jinx_path, 'r') as f:
|
|
574
|
+
jinx_data = yaml.safe_load(f)
|
|
575
|
+
|
|
576
|
+
if jinx_data:
|
|
577
|
+
jinx_name = jinx_data.get('jinx_name', filename[:-5])
|
|
578
|
+
|
|
579
|
+
jinx_obj = {
|
|
580
|
+
'name': jinx_name,
|
|
581
|
+
'display_name': jinx_data.get('description', jinx_name),
|
|
582
|
+
'description': jinx_data.get('description', ''),
|
|
583
|
+
'inputs': jinx_data.get('inputs', []),
|
|
584
|
+
'path': jinx_path
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
# Categorize based on directory
|
|
588
|
+
rel_path = os.path.relpath(root, global_jinxs_dir)
|
|
589
|
+
|
|
590
|
+
if rel_path.startswith('code'):
|
|
591
|
+
code_jinxs.append(jinx_obj)
|
|
592
|
+
elif rel_path.startswith('modes'):
|
|
593
|
+
mode_jinxs.append(jinx_obj)
|
|
594
|
+
elif rel_path.startswith('utils'):
|
|
595
|
+
util_jinxs.append(jinx_obj)
|
|
596
|
+
|
|
597
|
+
except Exception as e:
|
|
598
|
+
print(f"Error loading jinx {filename}: {e}")
|
|
599
|
+
|
|
600
|
+
return jsonify({
|
|
601
|
+
'code': code_jinxs,
|
|
602
|
+
'modes': mode_jinxs,
|
|
603
|
+
'utils': util_jinxs
|
|
604
|
+
})
|
|
573
605
|
@app.route("/api/jinx/execute", methods=["POST"])
|
|
574
606
|
def execute_jinx():
|
|
575
607
|
"""
|
|
576
608
|
Execute a specific jinx with provided arguments.
|
|
577
|
-
|
|
609
|
+
Returns the output as a JSON response.
|
|
578
610
|
"""
|
|
579
611
|
data = request.json
|
|
580
612
|
|
|
@@ -585,19 +617,18 @@ def execute_jinx():
|
|
|
585
617
|
with cancellation_lock:
|
|
586
618
|
cancellation_flags[stream_id] = False
|
|
587
619
|
|
|
588
|
-
print(f"--- Jinx Execution Request for streamId: {stream_id} ---")
|
|
589
|
-
print(f"Request Data: {json.dumps(data, indent=2)}")
|
|
620
|
+
print(f"--- Jinx Execution Request for streamId: {stream_id} ---", file=sys.stderr)
|
|
621
|
+
print(f"Request Data: {json.dumps(data, indent=2)}", file=sys.stderr)
|
|
590
622
|
|
|
591
623
|
jinx_name = data.get("jinxName")
|
|
592
624
|
jinx_args = data.get("jinxArgs", [])
|
|
593
|
-
print(f"Jinx Name: {jinx_name}, Jinx Args: {jinx_args}")
|
|
625
|
+
print(f"Jinx Name: {jinx_name}, Jinx Args: {jinx_args}", file=sys.stderr)
|
|
594
626
|
conversation_id = data.get("conversationId")
|
|
595
627
|
model = data.get("model")
|
|
596
628
|
provider = data.get("provider")
|
|
597
629
|
|
|
598
|
-
# --- IMPORTANT: Ensure conversation_id is present for context persistence ---
|
|
599
630
|
if not conversation_id:
|
|
600
|
-
print("ERROR: conversationId is required for Jinx execution with persistent variables")
|
|
631
|
+
print("ERROR: conversationId is required for Jinx execution with persistent variables", file=sys.stderr)
|
|
601
632
|
return jsonify({"error": "conversationId is required for Jinx execution with persistent variables"}), 400
|
|
602
633
|
|
|
603
634
|
npc_name = data.get("npc")
|
|
@@ -605,222 +636,194 @@ def execute_jinx():
|
|
|
605
636
|
current_path = data.get("currentPath")
|
|
606
637
|
|
|
607
638
|
if not jinx_name:
|
|
608
|
-
print("ERROR: jinxName is required")
|
|
639
|
+
print("ERROR: jinxName is required", file=sys.stderr)
|
|
609
640
|
return jsonify({"error": "jinxName is required"}), 400
|
|
610
641
|
|
|
611
|
-
# Load project environment if applicable
|
|
612
642
|
if current_path:
|
|
613
643
|
load_project_env(current_path)
|
|
614
644
|
|
|
615
|
-
|
|
616
|
-
|
|
645
|
+
jinx = None
|
|
646
|
+
|
|
617
647
|
if npc_name:
|
|
618
648
|
db_conn = get_db_connection()
|
|
619
649
|
npc_object = load_npc_by_name_and_source(npc_name, npc_source, db_conn, current_path)
|
|
620
650
|
if not npc_object and npc_source == 'project':
|
|
621
651
|
npc_object = load_npc_by_name_and_source(npc_name, 'global', db_conn)
|
|
652
|
+
else:
|
|
653
|
+
npc_object = None
|
|
622
654
|
|
|
623
|
-
# Try to find the jinx
|
|
624
|
-
jinx = None
|
|
625
|
-
|
|
626
|
-
# Check NPC's jinxs
|
|
627
655
|
if npc_object and hasattr(npc_object, 'jinxs_dict') and jinx_name in npc_object.jinxs_dict:
|
|
628
656
|
jinx = npc_object.jinxs_dict[jinx_name]
|
|
657
|
+
print(f"Found jinx in NPC's jinxs_dict", file=sys.stderr)
|
|
629
658
|
|
|
630
|
-
# Check team jinxs
|
|
631
659
|
if not jinx and current_path:
|
|
632
|
-
|
|
633
|
-
if os.path.exists(
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
660
|
+
project_jinxs_base = os.path.join(current_path, 'npc_team', 'jinxs')
|
|
661
|
+
if os.path.exists(project_jinxs_base):
|
|
662
|
+
for root, dirs, files in os.walk(project_jinxs_base):
|
|
663
|
+
if f'{jinx_name}.jinx' in files:
|
|
664
|
+
project_jinx_path = os.path.join(root, f'{jinx_name}.jinx')
|
|
665
|
+
jinx = Jinx(jinx_path=project_jinx_path)
|
|
666
|
+
print(f"Found jinx at: {project_jinx_path}", file=sys.stderr)
|
|
667
|
+
break
|
|
668
|
+
|
|
637
669
|
if not jinx:
|
|
638
|
-
|
|
639
|
-
if os.path.exists(
|
|
640
|
-
|
|
670
|
+
global_jinxs_base = os.path.expanduser('~/.npcsh/npc_team/jinxs')
|
|
671
|
+
if os.path.exists(global_jinxs_base):
|
|
672
|
+
for root, dirs, files in os.walk(global_jinxs_base):
|
|
673
|
+
if f'{jinx_name}.jinx' in files:
|
|
674
|
+
global_jinx_path = os.path.join(root, f'{jinx_name}.jinx')
|
|
675
|
+
jinx = Jinx(jinx_path=global_jinx_path)
|
|
676
|
+
print(f"Found jinx at: {global_jinx_path}", file=sys.stderr)
|
|
677
|
+
|
|
678
|
+
# Initialize jinx steps by calling render_first_pass
|
|
679
|
+
from jinja2 import Environment
|
|
680
|
+
temp_env = Environment()
|
|
681
|
+
jinx.render_first_pass(temp_env, {})
|
|
682
|
+
|
|
683
|
+
break
|
|
641
684
|
|
|
642
685
|
if not jinx:
|
|
643
|
-
print(f"ERROR: Jinx '{jinx_name}' not found")
|
|
686
|
+
print(f"ERROR: Jinx '{jinx_name}' not found", file=sys.stderr)
|
|
687
|
+
searched_paths = []
|
|
688
|
+
if npc_object:
|
|
689
|
+
searched_paths.append(f"NPC {npc_name} jinxs_dict")
|
|
690
|
+
if current_path:
|
|
691
|
+
searched_paths.append(f"Project jinxs at {os.path.join(current_path, 'npc_team', 'jinxs')}")
|
|
692
|
+
searched_paths.append(f"Global jinxs at {os.path.expanduser('~/.npcsh/npc_team/jinxs')}")
|
|
693
|
+
print(f"Searched in: {', '.join(searched_paths)}", file=sys.stderr)
|
|
644
694
|
return jsonify({"error": f"Jinx '{jinx_name}' not found"}), 404
|
|
645
695
|
|
|
646
|
-
# Extract inputs from args
|
|
647
696
|
from npcpy.npc_compiler import extract_jinx_inputs
|
|
648
697
|
|
|
649
|
-
# Re-assemble arguments that were incorrectly split by spaces.
|
|
650
698
|
fixed_args = []
|
|
651
699
|
i = 0
|
|
652
|
-
|
|
653
|
-
|
|
700
|
+
|
|
701
|
+
# Filter out None values from jinx_args before processing
|
|
702
|
+
cleaned_jinx_args = [arg for arg in jinx_args if arg is not None]
|
|
703
|
+
|
|
704
|
+
while i < len(cleaned_jinx_args):
|
|
705
|
+
arg = cleaned_jinx_args[i]
|
|
654
706
|
if arg.startswith('-'):
|
|
655
707
|
fixed_args.append(arg)
|
|
656
708
|
value_parts = []
|
|
657
709
|
i += 1
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
value_parts.append(jinx_args[i])
|
|
710
|
+
while i < len(cleaned_jinx_args) and not cleaned_jinx_args[i].startswith('-'):
|
|
711
|
+
value_parts.append(cleaned_jinx_args[i])
|
|
661
712
|
i += 1
|
|
662
713
|
|
|
663
714
|
if value_parts:
|
|
664
|
-
# Join the parts back into a single string.
|
|
665
715
|
full_value = " ".join(value_parts)
|
|
666
|
-
# Clean up the extraneous quotes that the initial bad split left behind.
|
|
667
716
|
if full_value.startswith("'") and full_value.endswith("'"):
|
|
668
717
|
full_value = full_value[1:-1]
|
|
669
718
|
elif full_value.startswith('"') and full_value.endswith('"'):
|
|
670
719
|
full_value = full_value[1:-1]
|
|
671
720
|
fixed_args.append(full_value)
|
|
672
|
-
# The 'i' counter is already advanced, so the loop continues from the next flag.
|
|
673
721
|
else:
|
|
674
|
-
# This handles positional arguments, just in case.
|
|
675
722
|
fixed_args.append(arg)
|
|
676
723
|
i += 1
|
|
677
724
|
|
|
678
|
-
# Now, use the corrected arguments to extract inputs.
|
|
679
725
|
input_values = extract_jinx_inputs(fixed_args, jinx)
|
|
680
726
|
|
|
681
|
-
print(f'Executing jinx with input_values: {input_values}')
|
|
682
|
-
|
|
727
|
+
print(f'Executing jinx with input_values: {input_values}', file=sys.stderr)
|
|
728
|
+
|
|
683
729
|
command_history = CommandHistory(app.config.get('DB_PATH'))
|
|
684
730
|
messages = fetch_messages_for_conversation(conversation_id)
|
|
685
731
|
|
|
686
|
-
# Prepare jinxs_dict for execution
|
|
687
732
|
all_jinxs = {}
|
|
688
733
|
if npc_object and hasattr(npc_object, 'jinxs_dict'):
|
|
689
734
|
all_jinxs.update(npc_object.jinxs_dict)
|
|
690
735
|
|
|
691
|
-
# --- IMPORTANT: Retrieve or initialize the persistent Jinx context for this conversation ---
|
|
692
736
|
if conversation_id not in app.jinx_conversation_contexts:
|
|
693
737
|
app.jinx_conversation_contexts[conversation_id] = {}
|
|
694
738
|
jinx_local_context = app.jinx_conversation_contexts[conversation_id]
|
|
695
739
|
|
|
696
|
-
print(f"--- CONTEXT STATE (conversationId: {conversation_id}) ---")
|
|
697
|
-
print(f"jinx_local_context BEFORE Jinx execution: {jinx_local_context}")
|
|
740
|
+
print(f"--- CONTEXT STATE (conversationId: {conversation_id}) ---", file=sys.stderr)
|
|
741
|
+
print(f"jinx_local_context BEFORE Jinx execution: {jinx_local_context}", file=sys.stderr)
|
|
698
742
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
# Get output (this still comes from the 'output' key in the result)
|
|
728
|
-
output = result.get('output', str(result))
|
|
729
|
-
messages_updated = result.get('messages', messages)
|
|
743
|
+
|
|
744
|
+
# Create state object
|
|
745
|
+
state = ShellState(
|
|
746
|
+
npc=npc_object,
|
|
747
|
+
team=None,
|
|
748
|
+
conversation_id=conversation_id,
|
|
749
|
+
chat_model=model or os.getenv('NPCSH_CHAT_MODEL', 'gemma3:4b'),
|
|
750
|
+
chat_provider=provider or os.getenv('NPCSH_CHAT_PROVIDER', 'ollama'),
|
|
751
|
+
current_path=current_path or os.getcwd(),
|
|
752
|
+
search_provider=os.getenv('NPCSH_SEARCH_PROVIDER', 'duckduckgo'),
|
|
753
|
+
embedding_model=os.getenv('NPCSH_EMBEDDING_MODEL', 'nomic-embed-text'),
|
|
754
|
+
embedding_provider=os.getenv('NPCSH_EMBEDDING_PROVIDER', 'ollama'),
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
# Build extra_globals with state and all necessary functions
|
|
758
|
+
extra_globals_for_jinx = {
|
|
759
|
+
**jinx_local_context,
|
|
760
|
+
'state': state,
|
|
761
|
+
'CommandHistory': CommandHistory,
|
|
762
|
+
'load_kg_from_db': load_kg_from_db,
|
|
763
|
+
'execute_rag_command': execute_rag_command,
|
|
764
|
+
'execute_brainblast_command': execute_brainblast_command,
|
|
765
|
+
'load_file_contents': load_file_contents,
|
|
766
|
+
'search_web': search_web,
|
|
767
|
+
'get_relevant_memories': get_relevant_memories,
|
|
768
|
+
'search_kg_facts': search_kg_facts,
|
|
769
|
+
}
|
|
730
770
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
"user",
|
|
787
|
-
f"/{jinx_name} {' '.join(jinx_args)}",
|
|
788
|
-
wd=current_path,
|
|
789
|
-
model=model,
|
|
790
|
-
provider=provider,
|
|
791
|
-
npc=npc_name,
|
|
792
|
-
message_id=message_id
|
|
793
|
-
)
|
|
794
|
-
|
|
795
|
-
message_id = generate_message_id()
|
|
796
|
-
save_conversation_message(
|
|
797
|
-
command_history,
|
|
798
|
-
conversation_id,
|
|
799
|
-
"assistant",
|
|
800
|
-
str(output),
|
|
801
|
-
wd=current_path,
|
|
802
|
-
model=model,
|
|
803
|
-
provider=provider,
|
|
804
|
-
npc=npc_name,
|
|
805
|
-
message_id=message_id
|
|
806
|
-
)
|
|
807
|
-
|
|
808
|
-
except Exception as e:
|
|
809
|
-
print(f"ERROR: Exception during jinx execution {jinx_name}: {str(e)}")
|
|
810
|
-
traceback.print_exc()
|
|
811
|
-
error_data = {
|
|
812
|
-
"type": "error",
|
|
813
|
-
"error": str(e)
|
|
814
|
-
}
|
|
815
|
-
yield f"data: {json.dumps(error_data)}\n\n"
|
|
816
|
-
|
|
817
|
-
finally:
|
|
818
|
-
with cancellation_lock:
|
|
819
|
-
if current_stream_id in cancellation_flags:
|
|
820
|
-
del cancellation_flags[current_stream_id]
|
|
821
|
-
print(f"--- Jinx Execution Finished for streamId: {stream_id} ---")
|
|
771
|
+
jinx_execution_result = jinx.execute(
|
|
772
|
+
input_values=input_values,
|
|
773
|
+
jinja_env=npc_object.jinja_env if npc_object else None,
|
|
774
|
+
npc=npc_object,
|
|
775
|
+
messages=messages,
|
|
776
|
+
extra_globals=extra_globals_for_jinx
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
output_from_jinx_result = jinx_execution_result.get('output')
|
|
780
|
+
|
|
781
|
+
final_output_string = str(output_from_jinx_result) if output_from_jinx_result is not None else ""
|
|
782
|
+
|
|
783
|
+
if isinstance(jinx_execution_result, dict):
|
|
784
|
+
for key, value in jinx_execution_result.items():
|
|
785
|
+
jinx_local_context[key] = value
|
|
786
|
+
|
|
787
|
+
print(f"jinx_local_context AFTER Jinx execution (final state): {jinx_local_context}", file=sys.stderr)
|
|
788
|
+
print(f"Jinx execution result output: {output_from_jinx_result}", file=sys.stderr)
|
|
789
|
+
|
|
790
|
+
user_message_id = generate_message_id()
|
|
791
|
+
|
|
792
|
+
# Use cleaned_jinx_args for logging the user message
|
|
793
|
+
user_command_log = f"/{jinx_name} {' '.join(cleaned_jinx_args)}"
|
|
794
|
+
save_conversation_message(
|
|
795
|
+
command_history,
|
|
796
|
+
conversation_id,
|
|
797
|
+
"user",
|
|
798
|
+
user_command_log,
|
|
799
|
+
wd=current_path,
|
|
800
|
+
model=model,
|
|
801
|
+
provider=provider,
|
|
802
|
+
npc=npc_name,
|
|
803
|
+
message_id=user_message_id
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
assistant_message_id = generate_message_id()
|
|
807
|
+
save_conversation_message(
|
|
808
|
+
command_history,
|
|
809
|
+
conversation_id,
|
|
810
|
+
"assistant",
|
|
811
|
+
final_output_string,
|
|
812
|
+
wd=current_path,
|
|
813
|
+
model=model,
|
|
814
|
+
provider=provider,
|
|
815
|
+
npc=npc_name,
|
|
816
|
+
message_id=assistant_message_id
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
# Determine mimetype based on content
|
|
820
|
+
is_html = bool(re.search(r'<[a-z][\s\S]*>', final_output_string, re.IGNORECASE))
|
|
821
|
+
|
|
822
|
+
if is_html:
|
|
823
|
+
return Response(final_output_string, mimetype="text/html")
|
|
824
|
+
else:
|
|
825
|
+
return Response(final_output_string, mimetype="text/plain")
|
|
822
826
|
|
|
823
|
-
return Response(event_stream(stream_id), mimetype="text/event-stream")
|
|
824
827
|
|
|
825
828
|
@app.route("/api/settings/global", methods=["POST", "OPTIONS"])
|
|
826
829
|
def save_global_settings():
|
|
@@ -1009,52 +1012,6 @@ def api_command(command):
|
|
|
1009
1012
|
return jsonify(result)
|
|
1010
1013
|
except Exception as e:
|
|
1011
1014
|
return jsonify({"error": str(e)})
|
|
1012
|
-
@app.route("/api/npc_team_global")
|
|
1013
|
-
def get_npc_team_global():
|
|
1014
|
-
try:
|
|
1015
|
-
db_conn = get_db_connection()
|
|
1016
|
-
global_npc_directory = os.path.expanduser("~/.npcsh/npc_team")
|
|
1017
|
-
|
|
1018
|
-
npc_data = []
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
for file in os.listdir(global_npc_directory):
|
|
1022
|
-
if file.endswith(".npc"):
|
|
1023
|
-
npc_path = os.path.join(global_npc_directory, file)
|
|
1024
|
-
npc = NPC(file=npc_path, db_conn=db_conn)
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
serialized_npc = {
|
|
1028
|
-
"name": npc.name,
|
|
1029
|
-
"primary_directive": npc.primary_directive,
|
|
1030
|
-
"model": npc.model,
|
|
1031
|
-
"provider": npc.provider,
|
|
1032
|
-
"api_url": npc.api_url,
|
|
1033
|
-
"use_global_jinxs": npc.use_global_jinxs,
|
|
1034
|
-
"jinxs": [
|
|
1035
|
-
{
|
|
1036
|
-
"jinx_name": jinx.jinx_name,
|
|
1037
|
-
"inputs": jinx.inputs,
|
|
1038
|
-
"steps": [
|
|
1039
|
-
{
|
|
1040
|
-
"name": step.get("name", f"step_{i}"),
|
|
1041
|
-
"engine": step.get("engine", "natural"),
|
|
1042
|
-
"code": step.get("code", "")
|
|
1043
|
-
}
|
|
1044
|
-
for i, step in enumerate(jinx.steps)
|
|
1045
|
-
]
|
|
1046
|
-
}
|
|
1047
|
-
for jinx in npc.jinxs
|
|
1048
|
-
],
|
|
1049
|
-
}
|
|
1050
|
-
npc_data.append(serialized_npc)
|
|
1051
|
-
|
|
1052
|
-
return jsonify({"npcs": npc_data, "error": None})
|
|
1053
|
-
|
|
1054
|
-
except Exception as e:
|
|
1055
|
-
print(f"Error loading global NPCs: {str(e)}")
|
|
1056
|
-
return jsonify({"npcs": [], "error": str(e)})
|
|
1057
|
-
|
|
1058
1015
|
|
|
1059
1016
|
@app.route("/api/jinxs/save", methods=["POST"])
|
|
1060
1017
|
def save_jinx():
|
|
@@ -1135,6 +1092,67 @@ use_global_jinxs: {str(npc_data.get('use_global_jinxs', True)).lower()}
|
|
|
1135
1092
|
print(f"Error saving NPC: {str(e)}")
|
|
1136
1093
|
return jsonify({"error": str(e)}), 500
|
|
1137
1094
|
|
|
1095
|
+
@app.route("/api/npc_team_global")
|
|
1096
|
+
def get_npc_team_global():
|
|
1097
|
+
try:
|
|
1098
|
+
db_conn = get_db_connection()
|
|
1099
|
+
global_npc_directory = os.path.expanduser("~/.npcsh/npc_team")
|
|
1100
|
+
|
|
1101
|
+
npc_data = []
|
|
1102
|
+
|
|
1103
|
+
# Ensure the directory exists before listing
|
|
1104
|
+
if not os.path.exists(global_npc_directory):
|
|
1105
|
+
print(f"Global NPC directory not found: {global_npc_directory}", file=sys.stderr)
|
|
1106
|
+
return jsonify({"npcs": [], "error": f"Global NPC directory not found: {global_npc_directory}"})
|
|
1107
|
+
|
|
1108
|
+
for file in os.listdir(global_npc_directory):
|
|
1109
|
+
if file.endswith(".npc"):
|
|
1110
|
+
npc_path = os.path.join(global_npc_directory, file)
|
|
1111
|
+
try:
|
|
1112
|
+
npc = NPC(file=npc_path, db_conn=db_conn)
|
|
1113
|
+
|
|
1114
|
+
# Ensure jinxs are initialized after NPC creation if not already
|
|
1115
|
+
# This is crucial for populating npc.jinxs_dict
|
|
1116
|
+
if not npc.jinxs_dict and hasattr(npc, 'initialize_jinxs'):
|
|
1117
|
+
npc.initialize_jinxs()
|
|
1118
|
+
|
|
1119
|
+
serialized_npc = {
|
|
1120
|
+
"name": npc.name,
|
|
1121
|
+
"primary_directive": npc.primary_directive,
|
|
1122
|
+
"model": npc.model,
|
|
1123
|
+
"provider": npc.provider,
|
|
1124
|
+
"api_url": npc.api_url,
|
|
1125
|
+
"use_global_jinxs": npc.use_global_jinxs,
|
|
1126
|
+
# CRITICAL FIX: Iterate over npc.jinxs_dict.values() which contains Jinx objects
|
|
1127
|
+
"jinxs": [
|
|
1128
|
+
{
|
|
1129
|
+
"jinx_name": jinx.jinx_name,
|
|
1130
|
+
"inputs": jinx.inputs,
|
|
1131
|
+
"steps": [
|
|
1132
|
+
{
|
|
1133
|
+
"name": step.get("name", f"step_{i}"),
|
|
1134
|
+
"engine": step.get("engine", "natural"),
|
|
1135
|
+
"code": step.get("code", "")
|
|
1136
|
+
}
|
|
1137
|
+
for i, step in enumerate(jinx.steps)
|
|
1138
|
+
]
|
|
1139
|
+
}
|
|
1140
|
+
for jinx in npc.jinxs_dict.values() # Use jinxs_dict here
|
|
1141
|
+
] if hasattr(npc, 'jinxs_dict') else [], # Defensive check
|
|
1142
|
+
}
|
|
1143
|
+
npc_data.append(serialized_npc)
|
|
1144
|
+
except Exception as e:
|
|
1145
|
+
print(f"Error loading or serializing NPC {file}: {str(e)}", file=sys.stderr)
|
|
1146
|
+
traceback.print_exc(file=sys.stderr)
|
|
1147
|
+
|
|
1148
|
+
|
|
1149
|
+
return jsonify({"npcs": npc_data, "error": None})
|
|
1150
|
+
|
|
1151
|
+
except Exception as e:
|
|
1152
|
+
print(f"Error fetching global NPC team: {str(e)}", file=sys.stderr)
|
|
1153
|
+
traceback.print_exc(file=sys.stderr)
|
|
1154
|
+
return jsonify({"npcs": [], "error": str(e)})
|
|
1155
|
+
|
|
1138
1156
|
|
|
1139
1157
|
@app.route("/api/npc_team_project", methods=["GET"])
|
|
1140
1158
|
def get_npc_team_project():
|
|
@@ -1142,49 +1160,70 @@ def get_npc_team_project():
|
|
|
1142
1160
|
db_conn = get_db_connection()
|
|
1143
1161
|
|
|
1144
1162
|
project_npc_directory = request.args.get("currentPath")
|
|
1163
|
+
if not project_npc_directory:
|
|
1164
|
+
return jsonify({"npcs": [], "error": "currentPath is required for project NPCs"}), 400
|
|
1165
|
+
|
|
1145
1166
|
if not project_npc_directory.endswith("npc_team"):
|
|
1146
1167
|
project_npc_directory = os.path.join(project_npc_directory, "npc_team")
|
|
1147
1168
|
|
|
1148
1169
|
npc_data = []
|
|
1149
1170
|
|
|
1171
|
+
# Ensure the directory exists before listing
|
|
1172
|
+
if not os.path.exists(project_npc_directory):
|
|
1173
|
+
print(f"Project NPC directory not found: {project_npc_directory}", file=sys.stderr)
|
|
1174
|
+
return jsonify({"npcs": [], "error": f"Project NPC directory not found: {project_npc_directory}"})
|
|
1175
|
+
|
|
1150
1176
|
for file in os.listdir(project_npc_directory):
|
|
1151
|
-
print(file)
|
|
1177
|
+
print(f"Processing project NPC file: {file}", file=sys.stderr) # Diagnostic print
|
|
1152
1178
|
if file.endswith(".npc"):
|
|
1153
1179
|
npc_path = os.path.join(project_npc_directory, file)
|
|
1154
|
-
|
|
1180
|
+
try:
|
|
1181
|
+
npc = NPC(file=npc_path, db_conn=db_conn)
|
|
1182
|
+
|
|
1183
|
+
# Ensure jinxs are initialized after NPC creation if not already
|
|
1184
|
+
# This is crucial for populating npc.jinxs_dict
|
|
1185
|
+
if not npc.jinxs_dict and hasattr(npc, 'initialize_jinxs'):
|
|
1186
|
+
npc.initialize_jinxs()
|
|
1187
|
+
|
|
1188
|
+
serialized_npc = {
|
|
1189
|
+
"name": npc.name,
|
|
1190
|
+
"primary_directive": npc.primary_directive,
|
|
1191
|
+
"model": npc.model,
|
|
1192
|
+
"provider": npc.provider,
|
|
1193
|
+
"api_url": npc.api_url,
|
|
1194
|
+
"use_global_jinxs": npc.use_global_jinxs,
|
|
1195
|
+
# CRITICAL FIX: Iterate over npc.jinxs_dict.values() which contains Jinx objects
|
|
1196
|
+
"jinxs": [
|
|
1197
|
+
{
|
|
1198
|
+
"jinx_name": jinx.jinx_name,
|
|
1199
|
+
"inputs": jinx.inputs,
|
|
1200
|
+
"steps": [
|
|
1201
|
+
{
|
|
1202
|
+
"name": step.get("name", f"step_{i}"),
|
|
1203
|
+
"engine": step.get("engine", "natural"),
|
|
1204
|
+
"code": step.get("code", "")
|
|
1205
|
+
}
|
|
1206
|
+
for i, step in enumerate(jinx.steps)
|
|
1207
|
+
]
|
|
1208
|
+
}
|
|
1209
|
+
for jinx in npc.jinxs_dict.values() # Use jinxs_dict here
|
|
1210
|
+
] if hasattr(npc, 'jinxs_dict') else [], # Defensive check
|
|
1211
|
+
}
|
|
1212
|
+
npc_data.append(serialized_npc)
|
|
1213
|
+
except Exception as e:
|
|
1214
|
+
print(f"Error loading or serializing NPC {file}: {str(e)}", file=sys.stderr)
|
|
1215
|
+
traceback.print_exc(file=sys.stderr)
|
|
1155
1216
|
|
|
1156
|
-
|
|
1157
|
-
serialized_npc = {
|
|
1158
|
-
"name": npc.name,
|
|
1159
|
-
"primary_directive": npc.primary_directive,
|
|
1160
|
-
"model": npc.model,
|
|
1161
|
-
"provider": npc.provider,
|
|
1162
|
-
"api_url": npc.api_url,
|
|
1163
|
-
"use_global_jinxs": npc.use_global_jinxs,
|
|
1164
|
-
"jinxs": [
|
|
1165
|
-
{
|
|
1166
|
-
"jinx_name": jinx.jinx_name,
|
|
1167
|
-
"inputs": jinx.inputs,
|
|
1168
|
-
"steps": [
|
|
1169
|
-
{
|
|
1170
|
-
"name": step.get("name", f"step_{i}"),
|
|
1171
|
-
"engine": step.get("engine", "natural"),
|
|
1172
|
-
"code": step.get("code", "")
|
|
1173
|
-
}
|
|
1174
|
-
for i, step in enumerate(jinx.steps)
|
|
1175
|
-
]
|
|
1176
|
-
}
|
|
1177
|
-
for jinx in npc.jinxs
|
|
1178
|
-
],
|
|
1179
|
-
}
|
|
1180
|
-
npc_data.append(serialized_npc)
|
|
1181
1217
|
|
|
1182
|
-
print(npc_data)
|
|
1218
|
+
print(f"Project NPC data: {npc_data}", file=sys.stderr) # Diagnostic print
|
|
1183
1219
|
return jsonify({"npcs": npc_data, "error": None})
|
|
1184
1220
|
|
|
1185
1221
|
except Exception as e:
|
|
1186
|
-
print(f"Error fetching NPC team: {str(e)}")
|
|
1222
|
+
print(f"Error fetching NPC team: {str(e)}", file=sys.stderr)
|
|
1223
|
+
traceback.print_exc(file=sys.stderr)
|
|
1187
1224
|
return jsonify({"npcs": [], "error": str(e)})
|
|
1225
|
+
|
|
1226
|
+
|
|
1188
1227
|
def get_last_used_model_and_npc_in_directory(directory_path):
|
|
1189
1228
|
"""
|
|
1190
1229
|
Fetches the model and NPC from the most recent message in any conversation
|
|
@@ -1985,7 +2024,8 @@ def stream():
|
|
|
1985
2024
|
npc_name = data.get("npc", None)
|
|
1986
2025
|
npc_source = data.get("npcSource", "global")
|
|
1987
2026
|
current_path = data.get("currentPath")
|
|
1988
|
-
|
|
2027
|
+
is_resend = data.get("isResend", False) # ADD THIS LINE
|
|
2028
|
+
|
|
1989
2029
|
if current_path:
|
|
1990
2030
|
loaded_vars = load_project_env(current_path)
|
|
1991
2031
|
print(f"Loaded project env variables for stream request: {list(loaded_vars.keys())}")
|
|
@@ -2321,20 +2361,25 @@ def stream():
|
|
|
2321
2361
|
for cont in messages[-1].get('content'):
|
|
2322
2362
|
txt = cont.get('text')
|
|
2323
2363
|
if txt is not None:
|
|
2324
|
-
user_message_filled +=txt
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2364
|
+
user_message_filled += txt
|
|
2365
|
+
|
|
2366
|
+
# Only save user message if it's NOT a resend
|
|
2367
|
+
if not is_resend: # ADD THIS CONDITION
|
|
2368
|
+
save_conversation_message(
|
|
2369
|
+
command_history,
|
|
2370
|
+
conversation_id,
|
|
2371
|
+
"user",
|
|
2372
|
+
user_message_filled if len(user_message_filled) > 0 else commandstr,
|
|
2373
|
+
wd=current_path,
|
|
2374
|
+
model=model,
|
|
2375
|
+
provider=provider,
|
|
2376
|
+
npc=npc_name,
|
|
2377
|
+
team=team,
|
|
2378
|
+
attachments=attachments_for_db,
|
|
2379
|
+
message_id=message_id,
|
|
2380
|
+
)
|
|
2381
|
+
|
|
2382
|
+
|
|
2338
2383
|
|
|
2339
2384
|
|
|
2340
2385
|
message_id = generate_message_id()
|
|
@@ -2478,7 +2523,33 @@ def stream():
|
|
|
2478
2523
|
|
|
2479
2524
|
return Response(event_stream(stream_id), mimetype="text/event-stream")
|
|
2480
2525
|
|
|
2481
|
-
|
|
2526
|
+
@app.route('/api/delete_message', methods=['POST'])
|
|
2527
|
+
def delete_message():
|
|
2528
|
+
data = request.json
|
|
2529
|
+
conversation_id = data.get('conversationId')
|
|
2530
|
+
message_id = data.get('messageId')
|
|
2531
|
+
|
|
2532
|
+
if not conversation_id or not message_id:
|
|
2533
|
+
return jsonify({"error": "Missing conversationId or messageId"}), 400
|
|
2534
|
+
|
|
2535
|
+
try:
|
|
2536
|
+
command_history = CommandHistory(app.config.get('DB_PATH'))
|
|
2537
|
+
|
|
2538
|
+
# Delete the message from the database
|
|
2539
|
+
result = command_history.delete_message(conversation_id, message_id)
|
|
2540
|
+
|
|
2541
|
+
print(f"[DELETE_MESSAGE] Deleted message {message_id} from conversation {conversation_id}. Rows affected: {result}")
|
|
2542
|
+
|
|
2543
|
+
return jsonify({
|
|
2544
|
+
"success": True,
|
|
2545
|
+
"deletedMessageId": message_id,
|
|
2546
|
+
"rowsAffected": result
|
|
2547
|
+
}), 200
|
|
2548
|
+
|
|
2549
|
+
except Exception as e:
|
|
2550
|
+
print(f"[DELETE_MESSAGE] Error: {e}")
|
|
2551
|
+
traceback.print_exc()
|
|
2552
|
+
return jsonify({"error": str(e)}), 500
|
|
2482
2553
|
|
|
2483
2554
|
@app.route("/api/memory/approve", methods=["POST"])
|
|
2484
2555
|
def approve_memories():
|