npcpy 1.2.34__py3-none-any.whl → 1.2.35__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/data/audio.py +35 -1
- npcpy/data/load.py +149 -7
- npcpy/data/video.py +72 -0
- npcpy/ft/diff.py +332 -71
- npcpy/gen/image_gen.py +120 -23
- npcpy/gen/ocr.py +187 -0
- npcpy/memory/command_history.py +231 -40
- npcpy/npc_compiler.py +14 -5
- npcpy/serve.py +1206 -547
- {npcpy-1.2.34.dist-info → npcpy-1.2.35.dist-info}/METADATA +1 -1
- {npcpy-1.2.34.dist-info → npcpy-1.2.35.dist-info}/RECORD +14 -13
- {npcpy-1.2.34.dist-info → npcpy-1.2.35.dist-info}/WHEEL +0 -0
- {npcpy-1.2.34.dist-info → npcpy-1.2.35.dist-info}/licenses/LICENSE +0 -0
- {npcpy-1.2.34.dist-info → npcpy-1.2.35.dist-info}/top_level.txt +0 -0
npcpy/serve.py
CHANGED
|
@@ -8,6 +8,7 @@ import sys
|
|
|
8
8
|
import traceback
|
|
9
9
|
import glob
|
|
10
10
|
import re
|
|
11
|
+
import time
|
|
11
12
|
|
|
12
13
|
import io
|
|
13
14
|
from flask_cors import CORS
|
|
@@ -43,13 +44,14 @@ from npcpy.memory.knowledge_graph import load_kg_from_db
|
|
|
43
44
|
from npcpy.memory.search import execute_rag_command, execute_brainblast_command
|
|
44
45
|
from npcpy.data.load import load_file_contents
|
|
45
46
|
from npcpy.data.web import search_web
|
|
47
|
+
|
|
46
48
|
from npcsh._state import get_relevant_memories, search_kg_facts
|
|
47
49
|
|
|
48
50
|
import base64
|
|
49
51
|
import shutil
|
|
50
52
|
import uuid
|
|
51
53
|
|
|
52
|
-
from npcpy.llm_funcs import gen_image
|
|
54
|
+
from npcpy.llm_funcs import gen_image, breathe
|
|
53
55
|
|
|
54
56
|
from sqlalchemy import create_engine, text
|
|
55
57
|
from sqlalchemy.orm import sessionmaker
|
|
@@ -84,6 +86,82 @@ cancellation_flags = {}
|
|
|
84
86
|
cancellation_lock = threading.Lock()
|
|
85
87
|
|
|
86
88
|
|
|
89
|
+
class MCPServerManager:
|
|
90
|
+
"""
|
|
91
|
+
Simple in-process tracker for launching/stopping MCP servers.
|
|
92
|
+
Currently uses subprocess.Popen to start a Python stdio MCP server script.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def __init__(self):
|
|
96
|
+
self._procs = {}
|
|
97
|
+
self._lock = threading.Lock()
|
|
98
|
+
|
|
99
|
+
def start(self, server_path: str):
|
|
100
|
+
server_path = os.path.expanduser(server_path)
|
|
101
|
+
abs_path = os.path.abspath(server_path)
|
|
102
|
+
if not os.path.exists(abs_path):
|
|
103
|
+
raise FileNotFoundError(f"MCP server script not found at {abs_path}")
|
|
104
|
+
|
|
105
|
+
with self._lock:
|
|
106
|
+
existing = self._procs.get(abs_path)
|
|
107
|
+
if existing and existing.poll() is None:
|
|
108
|
+
return {"status": "running", "pid": existing.pid, "serverPath": abs_path}
|
|
109
|
+
|
|
110
|
+
cmd = [sys.executable, abs_path]
|
|
111
|
+
proc = subprocess.Popen(
|
|
112
|
+
cmd,
|
|
113
|
+
cwd=os.path.dirname(abs_path) or ".",
|
|
114
|
+
stdout=subprocess.PIPE,
|
|
115
|
+
stderr=subprocess.PIPE,
|
|
116
|
+
)
|
|
117
|
+
self._procs[abs_path] = proc
|
|
118
|
+
return {"status": "started", "pid": proc.pid, "serverPath": abs_path}
|
|
119
|
+
|
|
120
|
+
def stop(self, server_path: str):
|
|
121
|
+
server_path = os.path.expanduser(server_path)
|
|
122
|
+
abs_path = os.path.abspath(server_path)
|
|
123
|
+
with self._lock:
|
|
124
|
+
proc = self._procs.get(abs_path)
|
|
125
|
+
if not proc:
|
|
126
|
+
return {"status": "not_found", "serverPath": abs_path}
|
|
127
|
+
if proc.poll() is None:
|
|
128
|
+
proc.terminate()
|
|
129
|
+
try:
|
|
130
|
+
proc.wait(timeout=5)
|
|
131
|
+
except subprocess.TimeoutExpired:
|
|
132
|
+
proc.kill()
|
|
133
|
+
del self._procs[abs_path]
|
|
134
|
+
return {"status": "stopped", "serverPath": abs_path}
|
|
135
|
+
|
|
136
|
+
def status(self, server_path: str):
|
|
137
|
+
server_path = os.path.expanduser(server_path)
|
|
138
|
+
abs_path = os.path.abspath(server_path)
|
|
139
|
+
with self._lock:
|
|
140
|
+
proc = self._procs.get(abs_path)
|
|
141
|
+
if not proc:
|
|
142
|
+
return {"status": "not_started", "serverPath": abs_path}
|
|
143
|
+
running = proc.poll() is None
|
|
144
|
+
return {
|
|
145
|
+
"status": "running" if running else "exited",
|
|
146
|
+
"serverPath": abs_path,
|
|
147
|
+
"pid": proc.pid,
|
|
148
|
+
"returncode": None if running else proc.returncode,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
def running(self):
|
|
152
|
+
with self._lock:
|
|
153
|
+
return {
|
|
154
|
+
path: {
|
|
155
|
+
"pid": proc.pid,
|
|
156
|
+
"status": "running" if proc.poll() is None else "exited",
|
|
157
|
+
"returncode": None if proc.poll() is None else proc.returncode,
|
|
158
|
+
}
|
|
159
|
+
for path, proc in self._procs.items()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
mcp_server_manager = MCPServerManager()
|
|
164
|
+
|
|
87
165
|
def get_project_npc_directory(current_path=None):
|
|
88
166
|
"""
|
|
89
167
|
Get the project NPC directory based on the current path
|
|
@@ -186,6 +264,34 @@ def get_db_session():
|
|
|
186
264
|
Session = sessionmaker(bind=engine)
|
|
187
265
|
return Session()
|
|
188
266
|
|
|
267
|
+
|
|
268
|
+
def resolve_mcp_server_path(current_path=None, explicit_path=None, force_global=False):
|
|
269
|
+
"""
|
|
270
|
+
Resolve an MCP server path using npcsh.corca's helper when available.
|
|
271
|
+
Falls back to ~/.npcsh/npc_team/mcp_server.py.
|
|
272
|
+
"""
|
|
273
|
+
if explicit_path:
|
|
274
|
+
abs_path = os.path.abspath(os.path.expanduser(explicit_path))
|
|
275
|
+
if os.path.exists(abs_path):
|
|
276
|
+
return abs_path
|
|
277
|
+
try:
|
|
278
|
+
from npcsh.corca import _resolve_and_copy_mcp_server_path
|
|
279
|
+
resolved = _resolve_and_copy_mcp_server_path(
|
|
280
|
+
explicit_path=explicit_path,
|
|
281
|
+
current_path=current_path,
|
|
282
|
+
team_ctx_mcp_servers=None,
|
|
283
|
+
interactive=False,
|
|
284
|
+
auto_copy_bypass=True,
|
|
285
|
+
force_global=force_global,
|
|
286
|
+
)
|
|
287
|
+
if resolved:
|
|
288
|
+
return os.path.abspath(resolved)
|
|
289
|
+
except Exception as e:
|
|
290
|
+
print(f"resolve_mcp_server_path: fallback path due to error: {e}")
|
|
291
|
+
|
|
292
|
+
fallback = os.path.expanduser("~/.npcsh/npc_team/mcp_server.py")
|
|
293
|
+
return fallback
|
|
294
|
+
|
|
189
295
|
extension_map = {
|
|
190
296
|
"PNG": "images",
|
|
191
297
|
"JPG": "images",
|
|
@@ -441,8 +547,6 @@ def capture():
|
|
|
441
547
|
return None
|
|
442
548
|
|
|
443
549
|
return jsonify({"screenshot": screenshot})
|
|
444
|
-
|
|
445
|
-
|
|
446
550
|
@app.route("/api/settings/global", methods=["GET", "OPTIONS"])
|
|
447
551
|
def get_global_settings():
|
|
448
552
|
if request.method == "OPTIONS":
|
|
@@ -451,22 +555,22 @@ def get_global_settings():
|
|
|
451
555
|
try:
|
|
452
556
|
npcshrc_path = os.path.expanduser("~/.npcshrc")
|
|
453
557
|
|
|
454
|
-
|
|
455
558
|
global_settings = {
|
|
456
559
|
"model": "llama3.2",
|
|
457
560
|
"provider": "ollama",
|
|
458
561
|
"embedding_model": "nomic-embed-text",
|
|
459
562
|
"embedding_provider": "ollama",
|
|
460
563
|
"search_provider": "perplexity",
|
|
461
|
-
"NPC_STUDIO_LICENSE_KEY": "",
|
|
462
564
|
"default_folder": os.path.expanduser("~/.npcsh/"),
|
|
565
|
+
"is_predictive_text_enabled": False, # Default value for the new setting
|
|
566
|
+
"predictive_text_model": "llama3.2", # Default predictive text model
|
|
567
|
+
"predictive_text_provider": "ollama", # Default predictive text provider
|
|
463
568
|
}
|
|
464
569
|
global_vars = {}
|
|
465
570
|
|
|
466
571
|
if os.path.exists(npcshrc_path):
|
|
467
572
|
with open(npcshrc_path, "r") as f:
|
|
468
573
|
for line in f:
|
|
469
|
-
|
|
470
574
|
line = line.split("#")[0].strip()
|
|
471
575
|
if not line:
|
|
472
576
|
continue
|
|
@@ -474,33 +578,35 @@ def get_global_settings():
|
|
|
474
578
|
if "=" not in line:
|
|
475
579
|
continue
|
|
476
580
|
|
|
477
|
-
|
|
478
581
|
key, value = line.split("=", 1)
|
|
479
582
|
key = key.strip()
|
|
480
583
|
if key.startswith("export "):
|
|
481
584
|
key = key[7:]
|
|
482
585
|
|
|
483
|
-
|
|
484
586
|
value = value.strip()
|
|
485
587
|
if value.startswith('"') and value.endswith('"'):
|
|
486
588
|
value = value[1:-1]
|
|
487
589
|
elif value.startswith("'") and value.endswith("'"):
|
|
488
590
|
value = value[1:-1]
|
|
489
591
|
|
|
490
|
-
|
|
491
592
|
key_mapping = {
|
|
492
593
|
"NPCSH_MODEL": "model",
|
|
493
594
|
"NPCSH_PROVIDER": "provider",
|
|
494
595
|
"NPCSH_EMBEDDING_MODEL": "embedding_model",
|
|
495
596
|
"NPCSH_EMBEDDING_PROVIDER": "embedding_provider",
|
|
496
597
|
"NPCSH_SEARCH_PROVIDER": "search_provider",
|
|
497
|
-
"NPC_STUDIO_LICENSE_KEY": "NPC_STUDIO_LICENSE_KEY",
|
|
498
598
|
"NPCSH_STREAM_OUTPUT": "NPCSH_STREAM_OUTPUT",
|
|
499
599
|
"NPC_STUDIO_DEFAULT_FOLDER": "default_folder",
|
|
600
|
+
"NPC_STUDIO_PREDICTIVE_TEXT_ENABLED": "is_predictive_text_enabled", # New mapping
|
|
601
|
+
"NPC_STUDIO_PREDICTIVE_TEXT_MODEL": "predictive_text_model", # New mapping
|
|
602
|
+
"NPC_STUDIO_PREDICTIVE_TEXT_PROVIDER": "predictive_text_provider", # New mapping
|
|
500
603
|
}
|
|
501
604
|
|
|
502
605
|
if key in key_mapping:
|
|
503
|
-
|
|
606
|
+
if key == "NPC_STUDIO_PREDICTIVE_TEXT_ENABLED":
|
|
607
|
+
global_settings[key_mapping[key]] = value.lower() == 'true'
|
|
608
|
+
else:
|
|
609
|
+
global_settings[key_mapping[key]] = value
|
|
504
610
|
else:
|
|
505
611
|
global_vars[key] = value
|
|
506
612
|
|
|
@@ -517,6 +623,7 @@ def get_global_settings():
|
|
|
517
623
|
except Exception as e:
|
|
518
624
|
print(f"Error in get_global_settings: {str(e)}")
|
|
519
625
|
return jsonify({"error": str(e)}), 500
|
|
626
|
+
|
|
520
627
|
def _get_jinx_files_recursively(directory):
|
|
521
628
|
"""Helper to recursively find all .jinx file paths."""
|
|
522
629
|
jinx_paths = []
|
|
@@ -550,58 +657,7 @@ def get_available_jinxs():
|
|
|
550
657
|
traceback.print_exc()
|
|
551
658
|
return jsonify({'jinxs': [], 'error': str(e)}), 500
|
|
552
659
|
|
|
553
|
-
|
|
554
|
-
def get_global_jinxs():
|
|
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
|
-
})
|
|
660
|
+
|
|
605
661
|
@app.route("/api/jinx/execute", methods=["POST"])
|
|
606
662
|
def execute_jinx():
|
|
607
663
|
"""
|
|
@@ -823,8 +879,6 @@ def execute_jinx():
|
|
|
823
879
|
return Response(final_output_string, mimetype="text/html")
|
|
824
880
|
else:
|
|
825
881
|
return Response(final_output_string, mimetype="text/plain")
|
|
826
|
-
|
|
827
|
-
|
|
828
882
|
@app.route("/api/settings/global", methods=["POST", "OPTIONS"])
|
|
829
883
|
def save_global_settings():
|
|
830
884
|
if request.method == "OPTIONS":
|
|
@@ -840,35 +894,41 @@ def save_global_settings():
|
|
|
840
894
|
"embedding_model": "NPCSH_EMBEDDING_MODEL",
|
|
841
895
|
"embedding_provider": "NPCSH_EMBEDDING_PROVIDER",
|
|
842
896
|
"search_provider": "NPCSH_SEARCH_PROVIDER",
|
|
843
|
-
"NPC_STUDIO_LICENSE_KEY": "NPC_STUDIO_LICENSE_KEY",
|
|
844
897
|
"NPCSH_STREAM_OUTPUT": "NPCSH_STREAM_OUTPUT",
|
|
845
898
|
"default_folder": "NPC_STUDIO_DEFAULT_FOLDER",
|
|
899
|
+
"is_predictive_text_enabled": "NPC_STUDIO_PREDICTIVE_TEXT_ENABLED", # New mapping
|
|
900
|
+
"predictive_text_model": "NPC_STUDIO_PREDICTIVE_TEXT_MODEL", # New mapping
|
|
901
|
+
"predictive_text_provider": "NPC_STUDIO_PREDICTIVE_TEXT_PROVIDER", # New mapping
|
|
846
902
|
}
|
|
847
903
|
|
|
848
904
|
os.makedirs(os.path.dirname(npcshrc_path), exist_ok=True)
|
|
849
905
|
print(data)
|
|
850
906
|
with open(npcshrc_path, "w") as f:
|
|
851
|
-
|
|
907
|
+
|
|
852
908
|
for key, value in data.get("global_settings", {}).items():
|
|
853
|
-
if key in key_mapping and value:
|
|
854
|
-
|
|
855
|
-
if
|
|
856
|
-
|
|
857
|
-
|
|
909
|
+
if key in key_mapping and value is not None: # Check for None explicitly
|
|
910
|
+
# Handle boolean conversion for saving
|
|
911
|
+
if key == "is_predictive_text_enabled":
|
|
912
|
+
value_to_write = str(value).upper()
|
|
913
|
+
elif " " in str(value):
|
|
914
|
+
value_to_write = f'"{value}"'
|
|
915
|
+
else:
|
|
916
|
+
value_to_write = str(value)
|
|
917
|
+
f.write(f"export {key_mapping[key]}={value_to_write}\n")
|
|
858
918
|
|
|
859
|
-
|
|
860
919
|
for key, value in data.get("global_vars", {}).items():
|
|
861
|
-
if key and value:
|
|
920
|
+
if key and value is not None: # Check for None explicitly
|
|
862
921
|
if " " in str(value):
|
|
863
|
-
|
|
864
|
-
|
|
922
|
+
value_to_write = f'"{value}"'
|
|
923
|
+
else:
|
|
924
|
+
value_to_write = str(value)
|
|
925
|
+
f.write(f"export {key}={value_to_write}\n")
|
|
865
926
|
|
|
866
927
|
return jsonify({"message": "Global settings saved successfully", "error": None})
|
|
867
928
|
|
|
868
929
|
except Exception as e:
|
|
869
930
|
print(f"Error in save_global_settings: {str(e)}")
|
|
870
931
|
return jsonify({"error": str(e)}), 500
|
|
871
|
-
|
|
872
932
|
@app.route("/api/settings/project", methods=["GET", "OPTIONS"])
|
|
873
933
|
def get_project_settings():
|
|
874
934
|
if request.method == "OPTIONS":
|
|
@@ -1050,8 +1110,542 @@ def save_jinx():
|
|
|
1050
1110
|
return jsonify({"status": "success"})
|
|
1051
1111
|
except Exception as e:
|
|
1052
1112
|
return jsonify({"error": str(e)}), 500
|
|
1113
|
+
def serialize_jinx_inputs(inputs):
|
|
1114
|
+
result = []
|
|
1115
|
+
for inp in inputs:
|
|
1116
|
+
if isinstance(inp, str):
|
|
1117
|
+
result.append(inp)
|
|
1118
|
+
elif isinstance(inp, dict):
|
|
1119
|
+
key = list(inp.keys())[0]
|
|
1120
|
+
result.append(key)
|
|
1121
|
+
else:
|
|
1122
|
+
result.append(str(inp))
|
|
1123
|
+
return result
|
|
1124
|
+
|
|
1125
|
+
@app.route("/api/jinx/test", methods=["POST"])
|
|
1126
|
+
def test_jinx():
|
|
1127
|
+
data = request.json
|
|
1128
|
+
jinx_data = data.get("jinx")
|
|
1129
|
+
test_inputs = data.get("inputs", {})
|
|
1130
|
+
current_path = data.get("currentPath")
|
|
1131
|
+
|
|
1132
|
+
if current_path:
|
|
1133
|
+
load_project_env(current_path)
|
|
1134
|
+
|
|
1135
|
+
jinx = Jinx(jinx_data=jinx_data)
|
|
1136
|
+
|
|
1137
|
+
from jinja2 import Environment
|
|
1138
|
+
temp_env = Environment()
|
|
1139
|
+
jinx.render_first_pass(temp_env, {})
|
|
1140
|
+
|
|
1141
|
+
conversation_id = f"jinx_test_{uuid.uuid4().hex[:8]}"
|
|
1142
|
+
command_history = CommandHistory(app.config.get('DB_PATH'))
|
|
1143
|
+
|
|
1144
|
+
# 1. Save user's test command to conversation_history to get a message_id
|
|
1145
|
+
user_test_command = f"Testing jinx /{jinx.jinx_name} with inputs: {test_inputs}"
|
|
1146
|
+
user_message_id = generate_message_id()
|
|
1147
|
+
save_conversation_message(
|
|
1148
|
+
command_history,
|
|
1149
|
+
conversation_id,
|
|
1150
|
+
"user",
|
|
1151
|
+
user_test_command,
|
|
1152
|
+
wd=current_path,
|
|
1153
|
+
model=None, # Or appropriate model/provider for the test context
|
|
1154
|
+
provider=None,
|
|
1155
|
+
npc=None,
|
|
1156
|
+
message_id=user_message_id
|
|
1157
|
+
)
|
|
1158
|
+
|
|
1159
|
+
# Jinx execution status and output are now part of the assistant's response
|
|
1160
|
+
jinx_execution_status = "success"
|
|
1161
|
+
jinx_error_message = None
|
|
1162
|
+
output = "Jinx execution did not complete." # Default output
|
|
1163
|
+
|
|
1164
|
+
try:
|
|
1165
|
+
result = jinx.execute(
|
|
1166
|
+
input_values=test_inputs,
|
|
1167
|
+
npc=None,
|
|
1168
|
+
messages=[],
|
|
1169
|
+
extra_globals={},
|
|
1170
|
+
jinja_env=temp_env
|
|
1171
|
+
)
|
|
1172
|
+
output = result.get('output', str(result))
|
|
1173
|
+
if result.get('error'): # Assuming jinx.execute might return an 'error' key
|
|
1174
|
+
jinx_execution_status = "failed"
|
|
1175
|
+
jinx_error_message = str(result.get('error'))
|
|
1176
|
+
except Exception as e:
|
|
1177
|
+
jinx_execution_status = "failed"
|
|
1178
|
+
jinx_error_message = str(e)
|
|
1179
|
+
output = f"Jinx execution failed: {e}"
|
|
1180
|
+
|
|
1181
|
+
# The jinx_executions table is populated by a trigger from conversation_history.
|
|
1182
|
+
# The details of the execution (inputs, output, status) are now expected to be
|
|
1183
|
+
# derived by analyzing the user's command and the subsequent assistant's response.
|
|
1184
|
+
# No explicit update to jinx_executions is needed here.
|
|
1185
|
+
|
|
1186
|
+
# 2. Save assistant's response to conversation_history
|
|
1187
|
+
assistant_response_message_id = generate_message_id() # ID for the assistant's response
|
|
1188
|
+
save_conversation_message(
|
|
1189
|
+
command_history,
|
|
1190
|
+
conversation_id,
|
|
1191
|
+
"assistant",
|
|
1192
|
+
output, # The jinx output is the assistant's response for the test
|
|
1193
|
+
wd=current_path,
|
|
1194
|
+
model=None,
|
|
1195
|
+
provider=None,
|
|
1196
|
+
npc=None,
|
|
1197
|
+
message_id=assistant_response_message_id
|
|
1198
|
+
)
|
|
1199
|
+
|
|
1200
|
+
return jsonify({
|
|
1201
|
+
"output": output,
|
|
1202
|
+
"conversation_id": conversation_id,
|
|
1203
|
+
"execution_id": user_message_id, # Return the user's message_id as the execution_id
|
|
1204
|
+
"error": jinx_error_message
|
|
1205
|
+
})
|
|
1206
|
+
from npcpy.ft.diff import train_diffusion, DiffusionConfig
|
|
1207
|
+
import threading
|
|
1208
|
+
|
|
1209
|
+
from npcpy.memory.knowledge_graph import (
|
|
1210
|
+
load_kg_from_db,
|
|
1211
|
+
save_kg_to_db # ADD THIS LINE to import the correct function
|
|
1212
|
+
)
|
|
1213
|
+
|
|
1214
|
+
from collections import defaultdict # ADD THIS LINE for collecting links if not already present
|
|
1215
|
+
|
|
1216
|
+
finetune_jobs = {}
|
|
1217
|
+
|
|
1218
|
+
def extract_and_store_memories(
|
|
1219
|
+
conversation_text,
|
|
1220
|
+
conversation_id,
|
|
1221
|
+
command_history,
|
|
1222
|
+
npc_name,
|
|
1223
|
+
team_name,
|
|
1224
|
+
current_path,
|
|
1225
|
+
model,
|
|
1226
|
+
provider,
|
|
1227
|
+
npc_object=None
|
|
1228
|
+
):
|
|
1229
|
+
from npcpy.llm_funcs import get_facts
|
|
1230
|
+
from npcpy.memory.command_history import format_memory_context
|
|
1231
|
+
# Your CommandHistory.get_memory_examples_for_context returns a dict with 'approved' and 'rejected'
|
|
1232
|
+
memory_examples_dict = command_history.get_memory_examples_for_context(
|
|
1233
|
+
npc=npc_name,
|
|
1234
|
+
team=team_name,
|
|
1235
|
+
directory_path=current_path
|
|
1236
|
+
)
|
|
1237
|
+
|
|
1238
|
+
memory_context = format_memory_context(memory_examples_dict)
|
|
1239
|
+
|
|
1240
|
+
facts = get_facts(
|
|
1241
|
+
conversation_text,
|
|
1242
|
+
model=npc_object.model if npc_object else model,
|
|
1243
|
+
provider=npc_object.provider if npc_object else provider,
|
|
1244
|
+
npc=npc_object,
|
|
1245
|
+
context=memory_context
|
|
1246
|
+
)
|
|
1247
|
+
|
|
1248
|
+
memories_for_approval = []
|
|
1249
|
+
|
|
1250
|
+
# Initialize structures to collect KG data for a single save_kg_to_db call
|
|
1251
|
+
kg_facts_to_save = []
|
|
1252
|
+
kg_concepts_to_save = []
|
|
1253
|
+
fact_to_concept_links_temp = defaultdict(list)
|
|
1254
|
+
|
|
1255
|
+
|
|
1256
|
+
if facts:
|
|
1257
|
+
for i, fact in enumerate(facts):
|
|
1258
|
+
# Store memory in memory_lifecycle table
|
|
1259
|
+
memory_id = command_history.add_memory_to_database(
|
|
1260
|
+
message_id=f"{conversation_id}_{datetime.datetime.now().strftime('%H%M%S')}_{i}",
|
|
1261
|
+
conversation_id=conversation_id,
|
|
1262
|
+
npc=npc_name or "default",
|
|
1263
|
+
team=team_name or "default",
|
|
1264
|
+
directory_path=current_path or "/",
|
|
1265
|
+
initial_memory=fact.get('statement', str(fact)),
|
|
1266
|
+
status="pending_approval",
|
|
1267
|
+
model=npc_object.model if npc_object else model,
|
|
1268
|
+
provider=npc_object.provider if npc_object else provider,
|
|
1269
|
+
final_memory=None # Explicitly None for pending memories
|
|
1270
|
+
)
|
|
1271
|
+
|
|
1272
|
+
memories_for_approval.append({
|
|
1273
|
+
"memory_id": memory_id,
|
|
1274
|
+
"content": fact.get('statement', str(fact)),
|
|
1275
|
+
"type": fact.get('type', 'unknown'),
|
|
1276
|
+
"context": fact.get('source_text', ''),
|
|
1277
|
+
"npc": npc_name or "default"
|
|
1278
|
+
})
|
|
1279
|
+
|
|
1280
|
+
# Collect facts and concepts for the Knowledge Graph
|
|
1281
|
+
#if fact.get('type') == 'concept':
|
|
1282
|
+
# kg_concepts_to_save.append({
|
|
1283
|
+
# "name": fact.get('statement'),
|
|
1284
|
+
# "generation": current_kg_generation,
|
|
1285
|
+
# "origin": "organic" # Assuming 'organic' for extracted facts
|
|
1286
|
+
# })
|
|
1287
|
+
#else: # It's a fact (or unknown type, treat as fact for KG)
|
|
1288
|
+
# kg_facts_to_save.append({
|
|
1289
|
+
# "statement": fact.get('statement'),
|
|
1290
|
+
# "source_text": fact.get('source_text', conversation_text), # Use source_text if available, else conversation_text
|
|
1291
|
+
# "type": fact.get('type', 'fact'), # Default to 'fact' if type is unknown
|
|
1292
|
+
# "generation": current_kg_generation,
|
|
1293
|
+
# "origin": "organic"
|
|
1294
|
+
# })
|
|
1295
|
+
# if fact.get('concepts'): # If this fact has related concepts
|
|
1296
|
+
# for concept_name in fact.get('concepts'):
|
|
1297
|
+
# fact_to_concept_links_temp[fact.get('statement')].append(concept_name)
|
|
1298
|
+
|
|
1299
|
+
# After processing all facts, save them to the KG database in one go
|
|
1300
|
+
if kg_facts_to_save or kg_concepts_to_save:
|
|
1301
|
+
temp_kg_data = {
|
|
1302
|
+
"facts": kg_facts_to_save,
|
|
1303
|
+
"concepts": kg_concepts_to_save,
|
|
1304
|
+
"generation": current_kg_generation,
|
|
1305
|
+
"fact_to_concept_links": fact_to_concept_links_temp,
|
|
1306
|
+
"concept_links": [], # Assuming no concept-to-concept links from direct extraction
|
|
1307
|
+
"fact_to_fact_links": [] # Assuming no fact-to-fact links from direct extraction
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
# Get the SQLAlchemy engine using your existing helper function
|
|
1311
|
+
db_engine = get_db_connection(app.config.get('DB_PATH'))
|
|
1312
|
+
|
|
1313
|
+
# Call the existing save_kg_to_db function
|
|
1314
|
+
save_kg_to_db(
|
|
1315
|
+
engine=db_engine,
|
|
1316
|
+
kg_data=temp_kg_data,
|
|
1317
|
+
team_name=team_name or "default",
|
|
1318
|
+
npc_name=npc_name or "default",
|
|
1319
|
+
directory_path=current_path or "/"
|
|
1320
|
+
)
|
|
1321
|
+
|
|
1322
|
+
return memories_for_approval
|
|
1323
|
+
@app.route('/api/finetuned_models', methods=['GET'])
|
|
1324
|
+
def get_finetuned_models():
|
|
1325
|
+
current_path = request.args.get("currentPath")
|
|
1326
|
+
|
|
1327
|
+
# Define a list of potential root directories where fine-tuned models might be saved.
|
|
1328
|
+
# We'll be very generous here, including both 'models' and 'images' directories
|
|
1329
|
+
# at both global and project levels, as the user's logs indicate saving to 'images'.
|
|
1330
|
+
potential_root_paths = [
|
|
1331
|
+
os.path.expanduser('~/.npcsh/models'), # Standard global models directory
|
|
1332
|
+
os.path.expanduser('~/.npcsh/images'), # Global images directory (where user's model was saved)
|
|
1333
|
+
]
|
|
1334
|
+
if current_path:
|
|
1335
|
+
# Add project-specific model directories if a current_path is provided
|
|
1336
|
+
project_models_path = os.path.join(current_path, 'models')
|
|
1337
|
+
project_images_path = os.path.join(current_path, 'images') # Also check project images directory
|
|
1338
|
+
potential_root_paths.extend([project_models_path, project_images_path])
|
|
1339
|
+
|
|
1340
|
+
finetuned_models = []
|
|
1341
|
+
|
|
1342
|
+
print(f"🌋 Searching for fine-tuned models in potential root paths: {set(potential_root_paths)}") # Use set for unique paths
|
|
1343
|
+
|
|
1344
|
+
for root_path in set(potential_root_paths): # Iterate through unique potential root paths
|
|
1345
|
+
if not os.path.exists(root_path) or not os.path.isdir(root_path):
|
|
1346
|
+
print(f"🌋 Skipping non-existent or non-directory root path: {root_path}")
|
|
1347
|
+
continue
|
|
1348
|
+
|
|
1349
|
+
print(f"🌋 Scanning root path: {root_path}")
|
|
1350
|
+
for model_dir_name in os.listdir(root_path):
|
|
1351
|
+
full_model_path = os.path.join(root_path, model_dir_name)
|
|
1352
|
+
|
|
1353
|
+
if not os.path.isdir(full_model_path):
|
|
1354
|
+
print(f"🌋 Skipping {full_model_path}: Not a directory.")
|
|
1355
|
+
continue
|
|
1356
|
+
|
|
1357
|
+
# NEW STRATEGY: Check for user's specific output files
|
|
1358
|
+
# Look for 'model_final.pt' or the 'checkpoints' directory
|
|
1359
|
+
has_model_final_pt = os.path.exists(os.path.join(full_model_path, 'model_final.pt'))
|
|
1360
|
+
has_checkpoints_dir = os.path.isdir(os.path.join(full_model_path, 'checkpoints'))
|
|
1361
|
+
|
|
1362
|
+
if has_model_final_pt or has_checkpoints_dir:
|
|
1363
|
+
print(f"🌋 Identified fine-tuned model: {model_dir_name} at {full_model_path} (found model_final.pt or checkpoints dir)")
|
|
1364
|
+
finetuned_models.append({
|
|
1365
|
+
"value": full_model_path, # This is the path to the directory containing the .pt files
|
|
1366
|
+
"provider": "diffusers", # Provider is still "diffusers"
|
|
1367
|
+
"display_name": f"{model_dir_name} | Fine-tuned Diffuser"
|
|
1368
|
+
})
|
|
1369
|
+
continue # Move to the next model_dir_name found in this root_path
|
|
1370
|
+
|
|
1371
|
+
print(f"🌋 Skipping {full_model_path}: No model_final.pt or checkpoints directory found at root.")
|
|
1372
|
+
|
|
1373
|
+
print(f"🌋 Finished scanning. Found {len(finetuned_models)} fine-tuned models.")
|
|
1374
|
+
return jsonify({"models": finetuned_models, "error": None})
|
|
1375
|
+
|
|
1376
|
+
@app.route('/api/finetune_diffusers', methods=['POST'])
|
|
1377
|
+
def finetune_diffusers():
|
|
1378
|
+
data = request.json
|
|
1379
|
+
images = data.get('images', [])
|
|
1380
|
+
captions = data.get('captions', [])
|
|
1381
|
+
output_name = data.get('outputName', 'my_diffusion_model')
|
|
1382
|
+
num_epochs = data.get('epochs', 100)
|
|
1383
|
+
batch_size = data.get('batchSize', 4)
|
|
1384
|
+
learning_rate = data.get('learningRate', 1e-4)
|
|
1385
|
+
output_path = data.get('outputPath', '~/.npcsh/models')
|
|
1386
|
+
|
|
1387
|
+
print(f"🌋 Finetune Diffusers Request Received!")
|
|
1388
|
+
print(f" Images: {len(images)} files")
|
|
1389
|
+
print(f" Output Name: {output_name}")
|
|
1390
|
+
print(f" Epochs: {num_epochs}, Batch Size: {batch_size}, Learning Rate: {learning_rate}")
|
|
1391
|
+
|
|
1392
|
+
if not images:
|
|
1393
|
+
print("🌋 Error: No images provided for finetuning.")
|
|
1394
|
+
return jsonify({'error': 'No images provided'}), 400
|
|
1395
|
+
|
|
1396
|
+
if not captions or len(captions) != len(images):
|
|
1397
|
+
print("🌋 Warning: Captions not provided or mismatching image count. Using empty captions.")
|
|
1398
|
+
captions = [''] * len(images)
|
|
1399
|
+
|
|
1400
|
+
expanded_images = [os.path.expanduser(p) for p in images]
|
|
1401
|
+
output_dir = os.path.expanduser(
|
|
1402
|
+
os.path.join(output_path, output_name)
|
|
1403
|
+
)
|
|
1404
|
+
|
|
1405
|
+
job_id = f"ft_{int(time.time())}"
|
|
1406
|
+
finetune_jobs[job_id] = {
|
|
1407
|
+
'status': 'running',
|
|
1408
|
+
'output_dir': output_dir,
|
|
1409
|
+
'epochs': num_epochs,
|
|
1410
|
+
'current_epoch': 0,
|
|
1411
|
+
'start_time': datetime.datetime.now().isoformat()
|
|
1412
|
+
}
|
|
1413
|
+
print(f"🌋 Finetuning job {job_id} initialized. Output directory: {output_dir}")
|
|
1414
|
+
|
|
1415
|
+
def run_training_async():
|
|
1416
|
+
print(f"🌋 Finetuning job {job_id}: Starting asynchronous training thread...")
|
|
1417
|
+
try:
|
|
1418
|
+
config = DiffusionConfig(
|
|
1419
|
+
num_epochs=num_epochs,
|
|
1420
|
+
batch_size=batch_size,
|
|
1421
|
+
learning_rate=learning_rate,
|
|
1422
|
+
output_model_path=output_dir
|
|
1423
|
+
)
|
|
1424
|
+
|
|
1425
|
+
print(f"🌋 Finetuning job {job_id}: Calling train_diffusion with config: {config}")
|
|
1426
|
+
# Assuming train_diffusion might print its own progress or allow callbacks
|
|
1427
|
+
# For more granular logging, you'd need to modify train_diffusion itself
|
|
1428
|
+
model_path = train_diffusion(
|
|
1429
|
+
expanded_images,
|
|
1430
|
+
captions,
|
|
1431
|
+
config=config
|
|
1432
|
+
)
|
|
1433
|
+
|
|
1434
|
+
finetune_jobs[job_id]['status'] = 'complete'
|
|
1435
|
+
finetune_jobs[job_id]['model_path'] = model_path
|
|
1436
|
+
finetune_jobs[job_id]['end_time'] = datetime.datetime.now().isoformat()
|
|
1437
|
+
print(f"🌋 Finetuning job {job_id}: Training complete! Model saved to: {model_path}")
|
|
1438
|
+
except Exception as e:
|
|
1439
|
+
finetune_jobs[job_id]['status'] = 'error'
|
|
1440
|
+
finetune_jobs[job_id]['error_msg'] = str(e)
|
|
1441
|
+
finetune_jobs[job_id]['end_time'] = datetime.datetime.now().isoformat()
|
|
1442
|
+
print(f"🌋 Finetuning job {job_id}: ERROR during training: {e}")
|
|
1443
|
+
traceback.print_exc()
|
|
1444
|
+
print(f"🌋 Finetuning job {job_id}: Asynchronous training thread finished.")
|
|
1445
|
+
|
|
1446
|
+
# Start the training in a separate thread
|
|
1447
|
+
thread = threading.Thread(target=run_training_async)
|
|
1448
|
+
thread.daemon = True # Allow the main program to exit even if this thread is still running
|
|
1449
|
+
thread.start()
|
|
1450
|
+
|
|
1451
|
+
print(f"🌋 Finetuning job {job_id} successfully launched in background. Returning initial status.")
|
|
1452
|
+
return jsonify({
|
|
1453
|
+
'status': 'started',
|
|
1454
|
+
'jobId': job_id,
|
|
1455
|
+
'message': f"Finetuning job '{job_id}' started. Check /api/finetune_status/{job_id} for updates."
|
|
1456
|
+
})
|
|
1457
|
+
|
|
1458
|
+
|
|
1459
|
+
@app.route('/api/finetune_status/<job_id>', methods=['GET'])
|
|
1460
|
+
def finetune_status(job_id):
|
|
1461
|
+
if job_id not in finetune_jobs:
|
|
1462
|
+
return jsonify({'error': 'Job not found'}), 404
|
|
1463
|
+
|
|
1464
|
+
job = finetune_jobs[job_id]
|
|
1465
|
+
|
|
1466
|
+
if job['status'] == 'complete':
|
|
1467
|
+
return jsonify({
|
|
1468
|
+
'complete': True,
|
|
1469
|
+
'outputPath': job.get('model_path', job['output_dir'])
|
|
1470
|
+
})
|
|
1471
|
+
elif job['status'] == 'error':
|
|
1472
|
+
return jsonify({'error': job.get('error_msg', 'Unknown error')})
|
|
1473
|
+
|
|
1474
|
+
return jsonify({
|
|
1475
|
+
'step': job.get('current_epoch', 0),
|
|
1476
|
+
'total': job['epochs'],
|
|
1477
|
+
'status': 'running'
|
|
1478
|
+
})
|
|
1479
|
+
|
|
1480
|
+
@app.route("/api/ml/train", methods=["POST"])
|
|
1481
|
+
def train_ml_model():
|
|
1482
|
+
import pickle
|
|
1483
|
+
import numpy as np
|
|
1484
|
+
from sklearn.linear_model import LinearRegression, LogisticRegression
|
|
1485
|
+
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
|
|
1486
|
+
from sklearn.tree import DecisionTreeRegressor
|
|
1487
|
+
from sklearn.cluster import KMeans
|
|
1488
|
+
from sklearn.model_selection import train_test_split
|
|
1489
|
+
from sklearn.metrics import mean_squared_error, r2_score, accuracy_score
|
|
1490
|
+
|
|
1491
|
+
data = request.json
|
|
1492
|
+
model_name = data.get("name")
|
|
1493
|
+
model_type = data.get("type")
|
|
1494
|
+
target = data.get("target")
|
|
1495
|
+
features = data.get("features")
|
|
1496
|
+
training_data = data.get("data")
|
|
1497
|
+
hyperparams = data.get("hyperparameters", {})
|
|
1498
|
+
|
|
1499
|
+
df = pd.DataFrame(training_data)
|
|
1500
|
+
X = df[features].values
|
|
1501
|
+
|
|
1502
|
+
metrics = {}
|
|
1503
|
+
model = None
|
|
1504
|
+
|
|
1505
|
+
if model_type == "linear_regression":
|
|
1506
|
+
y = df[target].values
|
|
1507
|
+
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
|
|
1508
|
+
model = LinearRegression()
|
|
1509
|
+
model.fit(X_train, y_train)
|
|
1510
|
+
y_pred = model.predict(X_test)
|
|
1511
|
+
metrics = {
|
|
1512
|
+
"r2_score": r2_score(y_test, y_pred),
|
|
1513
|
+
"rmse": np.sqrt(mean_squared_error(y_test, y_pred))
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
elif model_type == "logistic_regression":
|
|
1517
|
+
y = df[target].values
|
|
1518
|
+
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
|
|
1519
|
+
model = LogisticRegression(max_iter=1000)
|
|
1520
|
+
model.fit(X_train, y_train)
|
|
1521
|
+
y_pred = model.predict(X_test)
|
|
1522
|
+
metrics = {"accuracy": accuracy_score(y_test, y_pred)}
|
|
1523
|
+
|
|
1524
|
+
elif model_type == "random_forest":
|
|
1525
|
+
y = df[target].values
|
|
1526
|
+
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
|
|
1527
|
+
model = RandomForestRegressor(n_estimators=100)
|
|
1528
|
+
model.fit(X_train, y_train)
|
|
1529
|
+
y_pred = model.predict(X_test)
|
|
1530
|
+
metrics = {
|
|
1531
|
+
"r2_score": r2_score(y_test, y_pred),
|
|
1532
|
+
"rmse": np.sqrt(mean_squared_error(y_test, y_pred))
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
elif model_type == "clustering":
|
|
1536
|
+
n_clusters = hyperparams.get("n_clusters", 3)
|
|
1537
|
+
model = KMeans(n_clusters=n_clusters)
|
|
1538
|
+
labels = model.fit_predict(X)
|
|
1539
|
+
metrics = {"inertia": model.inertia_, "n_clusters": n_clusters}
|
|
1540
|
+
|
|
1541
|
+
elif model_type == "gradient_boost":
|
|
1542
|
+
y = df[target].values
|
|
1543
|
+
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
|
|
1544
|
+
model = GradientBoostingRegressor()
|
|
1545
|
+
model.fit(X_train, y_train)
|
|
1546
|
+
y_pred = model.predict(X_test)
|
|
1547
|
+
metrics = {
|
|
1548
|
+
"r2_score": r2_score(y_test, y_pred),
|
|
1549
|
+
"rmse": np.sqrt(mean_squared_error(y_test, y_pred))
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
model_id = f"{model_name}_{int(time.time())}"
|
|
1553
|
+
model_path = os.path.expanduser(f"~/.npcsh/models/{model_id}.pkl")
|
|
1554
|
+
os.makedirs(os.path.dirname(model_path), exist_ok=True)
|
|
1555
|
+
|
|
1556
|
+
with open(model_path, 'wb') as f:
|
|
1557
|
+
pickle.dump({
|
|
1558
|
+
"model": model,
|
|
1559
|
+
"features": features,
|
|
1560
|
+
"target": target,
|
|
1561
|
+
"type": model_type
|
|
1562
|
+
}, f)
|
|
1563
|
+
|
|
1564
|
+
return jsonify({
|
|
1565
|
+
"model_id": model_id,
|
|
1566
|
+
"metrics": metrics,
|
|
1567
|
+
"error": None
|
|
1568
|
+
})
|
|
1569
|
+
|
|
1570
|
+
|
|
1571
|
+
@app.route("/api/ml/predict", methods=["POST"])
|
|
1572
|
+
def ml_predict():
|
|
1573
|
+
import pickle
|
|
1574
|
+
|
|
1575
|
+
data = request.json
|
|
1576
|
+
model_name = data.get("model_name")
|
|
1577
|
+
input_data = data.get("input_data")
|
|
1578
|
+
|
|
1579
|
+
model_dir = os.path.expanduser("~/.npcsh/models/")
|
|
1580
|
+
model_files = [f for f in os.listdir(model_dir) if f.startswith(model_name)]
|
|
1581
|
+
|
|
1582
|
+
if not model_files:
|
|
1583
|
+
return jsonify({"error": f"Model {model_name} not found"})
|
|
1584
|
+
|
|
1585
|
+
model_path = os.path.join(model_dir, model_files[0])
|
|
1586
|
+
|
|
1587
|
+
with open(model_path, 'rb') as f:
|
|
1588
|
+
model_data = pickle.load(f)
|
|
1589
|
+
|
|
1590
|
+
model = model_data["model"]
|
|
1591
|
+
prediction = model.predict([input_data])
|
|
1592
|
+
|
|
1593
|
+
return jsonify({
|
|
1594
|
+
"prediction": prediction.tolist(),
|
|
1595
|
+
"error": None
|
|
1596
|
+
})
|
|
1597
|
+
@app.route("/api/jinx/executions/label", methods=["POST"])
|
|
1598
|
+
def label_jinx_execution():
|
|
1599
|
+
data = request.json
|
|
1600
|
+
execution_id = data.get("executionId")
|
|
1601
|
+
label = data.get("label")
|
|
1602
|
+
|
|
1603
|
+
command_history = CommandHistory(app.config.get('DB_PATH'))
|
|
1604
|
+
command_history.label_jinx_execution(execution_id, label)
|
|
1605
|
+
|
|
1606
|
+
return jsonify({"success": True, "error": None})
|
|
1607
|
+
|
|
1608
|
+
|
|
1609
|
+
@app.route("/api/npc/executions", methods=["GET"])
|
|
1610
|
+
def get_npc_executions():
|
|
1611
|
+
npc_name = request.args.get("npcName")
|
|
1612
|
+
|
|
1613
|
+
|
|
1614
|
+
command_history = CommandHistory(app.config.get('DB_PATH'))
|
|
1615
|
+
executions = command_history.get_npc_executions(npc_name)
|
|
1616
|
+
|
|
1617
|
+
return jsonify({"executions": executions, "error": None})
|
|
1618
|
+
|
|
1619
|
+
|
|
1620
|
+
@app.route("/api/npc/executions/label", methods=["POST"])
|
|
1621
|
+
def label_npc_execution():
|
|
1622
|
+
data = request.json
|
|
1623
|
+
execution_id = data.get("executionId")
|
|
1624
|
+
label = data.get("label")
|
|
1625
|
+
|
|
1626
|
+
command_history = CommandHistory(app.config.get('DB_PATH'))
|
|
1627
|
+
command_history.label_npc_execution(execution_id, label)
|
|
1628
|
+
|
|
1629
|
+
return jsonify({"success": True, "error": None})
|
|
1053
1630
|
|
|
1054
1631
|
|
|
1632
|
+
@app.route("/api/training/dataset", methods=["POST"])
|
|
1633
|
+
def build_training_dataset():
|
|
1634
|
+
data = request.json
|
|
1635
|
+
filters = data.get("filters", {})
|
|
1636
|
+
|
|
1637
|
+
command_history = CommandHistory(app.config.get('DB_PATH'))
|
|
1638
|
+
dataset = command_history.get_training_dataset(
|
|
1639
|
+
include_jinxs=filters.get("jinxs", True),
|
|
1640
|
+
include_npcs=filters.get("npcs", True),
|
|
1641
|
+
npc_names=filters.get("npc_names")
|
|
1642
|
+
)
|
|
1643
|
+
|
|
1644
|
+
return jsonify({
|
|
1645
|
+
"dataset": dataset,
|
|
1646
|
+
"count": len(dataset),
|
|
1647
|
+
"error": None
|
|
1648
|
+
})
|
|
1055
1649
|
@app.route("/api/save_npc", methods=["POST"])
|
|
1056
1650
|
def save_npc():
|
|
1057
1651
|
try:
|
|
@@ -1092,137 +1686,147 @@ use_global_jinxs: {str(npc_data.get('use_global_jinxs', True)).lower()}
|
|
|
1092
1686
|
print(f"Error saving NPC: {str(e)}")
|
|
1093
1687
|
return jsonify({"error": str(e)}), 500
|
|
1094
1688
|
|
|
1095
|
-
@app.route("/api/
|
|
1096
|
-
def
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
global_npc_directory = os.path.expanduser("~/.npcsh/npc_team")
|
|
1689
|
+
@app.route("/api/jinxs/global")
|
|
1690
|
+
def get_jinxs_global():
|
|
1691
|
+
global_jinx_directory = os.path.expanduser("~/.npcsh/npc_team/jinxs")
|
|
1692
|
+
jinx_data = []
|
|
1100
1693
|
|
|
1101
|
-
|
|
1694
|
+
if not os.path.exists(global_jinx_directory):
|
|
1695
|
+
return jsonify({"jinxs": [], "error": None})
|
|
1102
1696
|
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1697
|
+
for root, dirs, files in os.walk(global_jinx_directory):
|
|
1698
|
+
for file in files:
|
|
1699
|
+
if file.endswith(".jinx"):
|
|
1700
|
+
jinx_path = os.path.join(root, file)
|
|
1701
|
+
with open(jinx_path, 'r') as f:
|
|
1702
|
+
raw_data = yaml.safe_load(f)
|
|
1703
|
+
|
|
1704
|
+
inputs = []
|
|
1705
|
+
for inp in raw_data.get("inputs", []):
|
|
1706
|
+
if isinstance(inp, str):
|
|
1707
|
+
inputs.append(inp)
|
|
1708
|
+
elif isinstance(inp, dict):
|
|
1709
|
+
inputs.append(list(inp.keys())[0])
|
|
1710
|
+
else:
|
|
1711
|
+
inputs.append(str(inp))
|
|
1712
|
+
|
|
1713
|
+
rel_path = os.path.relpath(jinx_path, global_jinx_directory)
|
|
1714
|
+
path_without_ext = rel_path[:-5]
|
|
1715
|
+
|
|
1716
|
+
jinx_data.append({
|
|
1717
|
+
"jinx_name": raw_data.get("jinx_name", file[:-5]),
|
|
1718
|
+
"path": path_without_ext,
|
|
1719
|
+
"description": raw_data.get("description", ""),
|
|
1720
|
+
"inputs": inputs,
|
|
1721
|
+
"steps": raw_data.get("steps", [])
|
|
1722
|
+
})
|
|
1107
1723
|
|
|
1108
|
-
|
|
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)
|
|
1724
|
+
return jsonify({"jinxs": jinx_data, "error": None})
|
|
1147
1725
|
|
|
1726
|
+
@app.route("/api/jinxs/project", methods=["GET"])
|
|
1727
|
+
def get_jinxs_project():
|
|
1728
|
+
project_dir = request.args.get("currentPath")
|
|
1729
|
+
if not project_dir:
|
|
1730
|
+
return jsonify({"jinxs": [], "error": "currentPath required"}), 400
|
|
1148
1731
|
|
|
1149
|
-
|
|
1732
|
+
if not project_dir.endswith("jinxs"):
|
|
1733
|
+
project_dir = os.path.join(project_dir, "jinxs")
|
|
1150
1734
|
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
return jsonify({"npcs": [], "error": str(e)})
|
|
1735
|
+
jinx_data = []
|
|
1736
|
+
if not os.path.exists(project_dir):
|
|
1737
|
+
return jsonify({"jinxs": [], "error": None})
|
|
1155
1738
|
|
|
1739
|
+
for root, dirs, files in os.walk(project_dir):
|
|
1740
|
+
for file in files:
|
|
1741
|
+
if file.endswith(".jinx"):
|
|
1742
|
+
jinx_path = os.path.join(root, file)
|
|
1743
|
+
with open(jinx_path, 'r') as f:
|
|
1744
|
+
raw_data = yaml.safe_load(f)
|
|
1745
|
+
|
|
1746
|
+
inputs = []
|
|
1747
|
+
for inp in raw_data.get("inputs", []):
|
|
1748
|
+
if isinstance(inp, str):
|
|
1749
|
+
inputs.append(inp)
|
|
1750
|
+
elif isinstance(inp, dict):
|
|
1751
|
+
inputs.append(list(inp.keys())[0])
|
|
1752
|
+
else:
|
|
1753
|
+
inputs.append(str(inp))
|
|
1754
|
+
|
|
1755
|
+
rel_path = os.path.relpath(jinx_path, project_dir)
|
|
1756
|
+
path_without_ext = rel_path[:-5]
|
|
1757
|
+
|
|
1758
|
+
jinx_data.append({
|
|
1759
|
+
"jinx_name": raw_data.get("jinx_name", file[:-5]),
|
|
1760
|
+
"path": path_without_ext,
|
|
1761
|
+
"description": raw_data.get("description", ""),
|
|
1762
|
+
"inputs": inputs,
|
|
1763
|
+
"steps": raw_data.get("steps", [])
|
|
1764
|
+
})
|
|
1765
|
+
print(jinx_data)
|
|
1766
|
+
return jsonify({"jinxs": jinx_data, "error": None})
|
|
1156
1767
|
|
|
1157
|
-
@app.route("/api/
|
|
1158
|
-
def
|
|
1159
|
-
|
|
1160
|
-
|
|
1768
|
+
@app.route("/api/npc_team_global")
|
|
1769
|
+
def get_npc_team_global():
|
|
1770
|
+
global_npc_directory = os.path.expanduser("~/.npcsh/npc_team")
|
|
1771
|
+
npc_data = []
|
|
1161
1772
|
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
return jsonify({"npcs": [], "error": "currentPath is required for project NPCs"}), 400
|
|
1773
|
+
if not os.path.exists(global_npc_directory):
|
|
1774
|
+
return jsonify({"npcs": [], "error": None})
|
|
1165
1775
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1776
|
+
for file in os.listdir(global_npc_directory):
|
|
1777
|
+
if file.endswith(".npc"):
|
|
1778
|
+
npc_path = os.path.join(global_npc_directory, file)
|
|
1779
|
+
with open(npc_path, 'r') as f:
|
|
1780
|
+
raw_data = yaml.safe_load(f)
|
|
1781
|
+
|
|
1782
|
+
npc_data.append({
|
|
1783
|
+
"name": raw_data.get("name", file[:-4]),
|
|
1784
|
+
"primary_directive": raw_data.get("primary_directive", ""),
|
|
1785
|
+
"model": raw_data.get("model", ""),
|
|
1786
|
+
"provider": raw_data.get("provider", ""),
|
|
1787
|
+
"api_url": raw_data.get("api_url", ""),
|
|
1788
|
+
"use_global_jinxs": raw_data.get("use_global_jinxs", True),
|
|
1789
|
+
"jinxs": raw_data.get("jinxs", "*"),
|
|
1790
|
+
})
|
|
1168
1791
|
|
|
1169
|
-
|
|
1792
|
+
return jsonify({"npcs": npc_data, "error": None})
|
|
1170
1793
|
|
|
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
1794
|
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
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)
|
|
1795
|
+
@app.route("/api/npc_team_project", methods=["GET"])
|
|
1796
|
+
def get_npc_team_project():
|
|
1797
|
+
project_npc_directory = request.args.get("currentPath")
|
|
1798
|
+
if not project_npc_directory:
|
|
1799
|
+
return jsonify({"npcs": [], "error": "currentPath required"}), 400
|
|
1800
|
+
|
|
1801
|
+
if not project_npc_directory.endswith("npc_team"):
|
|
1802
|
+
project_npc_directory = os.path.join(
|
|
1803
|
+
project_npc_directory,
|
|
1804
|
+
"npc_team"
|
|
1805
|
+
)
|
|
1216
1806
|
|
|
1807
|
+
npc_data = []
|
|
1217
1808
|
|
|
1218
|
-
|
|
1219
|
-
return jsonify({"npcs":
|
|
1809
|
+
if not os.path.exists(project_npc_directory):
|
|
1810
|
+
return jsonify({"npcs": [], "error": None})
|
|
1220
1811
|
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1812
|
+
for file in os.listdir(project_npc_directory):
|
|
1813
|
+
if file.endswith(".npc"):
|
|
1814
|
+
npc_path = os.path.join(project_npc_directory, file)
|
|
1815
|
+
with open(npc_path, 'r') as f:
|
|
1816
|
+
raw_npc_data = yaml.safe_load(f)
|
|
1817
|
+
|
|
1818
|
+
serialized_npc = {
|
|
1819
|
+
"name": raw_npc_data.get("name", file[:-4]),
|
|
1820
|
+
"primary_directive": raw_npc_data.get("primary_directive", ""),
|
|
1821
|
+
"model": raw_npc_data.get("model", ""),
|
|
1822
|
+
"provider": raw_npc_data.get("provider", ""),
|
|
1823
|
+
"api_url": raw_npc_data.get("api_url", ""),
|
|
1824
|
+
"use_global_jinxs": raw_npc_data.get("use_global_jinxs", True),
|
|
1825
|
+
"jinxs": raw_npc_data.get("jinxs", "*"),
|
|
1826
|
+
}
|
|
1827
|
+
npc_data.append(serialized_npc)
|
|
1225
1828
|
|
|
1829
|
+
return jsonify({"npcs": npc_data, "error": None})
|
|
1226
1830
|
|
|
1227
1831
|
def get_last_used_model_and_npc_in_directory(directory_path):
|
|
1228
1832
|
"""
|
|
@@ -1542,11 +2146,62 @@ IMAGE_MODELS = {
|
|
|
1542
2146
|
{"value": "runwayml/stable-diffusion-v1-5", "display_name": "Stable Diffusion v1.5"},
|
|
1543
2147
|
],
|
|
1544
2148
|
}
|
|
2149
|
+
# In npcpy/serve.py, find the @app.route('/api/finetuned_models', methods=['GET'])
|
|
2150
|
+
# and replace the entire function with this:
|
|
2151
|
+
|
|
2152
|
+
# This is now an internal helper function, not a Flask route.
|
|
2153
|
+
def _get_finetuned_models_internal(current_path=None): # Renamed to indicate internal use
|
|
2154
|
+
|
|
2155
|
+
# Define a list of potential root directories where fine-tuned models might be saved.
|
|
2156
|
+
potential_root_paths = [
|
|
2157
|
+
os.path.expanduser('~/.npcsh/models'), # Standard global models directory
|
|
2158
|
+
os.path.expanduser('~/.npcsh/images'), # Global images directory (where user's model was saved)
|
|
2159
|
+
]
|
|
2160
|
+
if current_path:
|
|
2161
|
+
# Add project-specific model directories if a current_path is provided
|
|
2162
|
+
project_models_path = os.path.join(current_path, 'models')
|
|
2163
|
+
project_images_path = os.path.join(current_path, 'images') # Also check project images directory
|
|
2164
|
+
potential_root_paths.extend([project_models_path, project_images_path])
|
|
2165
|
+
|
|
2166
|
+
finetuned_models = []
|
|
2167
|
+
|
|
2168
|
+
print(f"🌋 (Internal) Searching for fine-tuned models in potential root paths: {set(potential_root_paths)}")
|
|
2169
|
+
|
|
2170
|
+
for root_path in set(potential_root_paths):
|
|
2171
|
+
if not os.path.exists(root_path) or not os.path.isdir(root_path):
|
|
2172
|
+
print(f"🌋 (Internal) Skipping non-existent or non-directory root path: {root_path}")
|
|
2173
|
+
continue
|
|
1545
2174
|
|
|
2175
|
+
print(f"🌋 (Internal) Scanning root path: {root_path}")
|
|
2176
|
+
for model_dir_name in os.listdir(root_path):
|
|
2177
|
+
full_model_path = os.path.join(root_path, model_dir_name)
|
|
2178
|
+
|
|
2179
|
+
if not os.path.isdir(full_model_path):
|
|
2180
|
+
print(f"🌋 (Internal) Skipping {full_model_path}: Not a directory.")
|
|
2181
|
+
continue
|
|
2182
|
+
|
|
2183
|
+
# Check for 'model_final.pt' or the 'checkpoints' directory
|
|
2184
|
+
has_model_final_pt = os.path.exists(os.path.join(full_model_path, 'model_final.pt'))
|
|
2185
|
+
has_checkpoints_dir = os.path.isdir(os.path.join(full_model_path, 'checkpoints'))
|
|
2186
|
+
|
|
2187
|
+
if has_model_final_pt or has_checkpoints_dir:
|
|
2188
|
+
print(f"🌋 (Internal) Identified fine-tuned model: {model_dir_name} at {full_model_path} (found model_final.pt or checkpoints dir)")
|
|
2189
|
+
finetuned_models.append({
|
|
2190
|
+
"value": full_model_path, # This is the path to the directory containing the .pt files
|
|
2191
|
+
"provider": "diffusers", # Provider is still "diffusers"
|
|
2192
|
+
"display_name": f"{model_dir_name} | Fine-tuned Diffuser"
|
|
2193
|
+
})
|
|
2194
|
+
continue
|
|
2195
|
+
|
|
2196
|
+
print(f"🌋 (Internal) Skipping {full_model_path}: No model_final.pt or checkpoints directory found at root.")
|
|
2197
|
+
|
|
2198
|
+
print(f"🌋 (Internal) Finished scanning. Found {len(finetuned_models)} fine-tuned models.")
|
|
2199
|
+
# <--- CRITICAL FIX: Directly return the list of models, not a Flask Response
|
|
2200
|
+
return {"models": finetuned_models, "error": None} # Return a dict for consistency
|
|
1546
2201
|
def get_available_image_models(current_path=None):
|
|
1547
2202
|
"""
|
|
1548
2203
|
Retrieves available image generation models based on environment variables
|
|
1549
|
-
and predefined configurations.
|
|
2204
|
+
and predefined configurations, including locally fine-tuned Diffusers models.
|
|
1550
2205
|
"""
|
|
1551
2206
|
|
|
1552
2207
|
if current_path:
|
|
@@ -1554,7 +2209,7 @@ def get_available_image_models(current_path=None):
|
|
|
1554
2209
|
|
|
1555
2210
|
all_image_models = []
|
|
1556
2211
|
|
|
1557
|
-
|
|
2212
|
+
# Add models configured via environment variables
|
|
1558
2213
|
env_image_model = os.getenv("NPCSH_IMAGE_MODEL")
|
|
1559
2214
|
env_image_provider = os.getenv("NPCSH_IMAGE_PROVIDER")
|
|
1560
2215
|
|
|
@@ -1565,9 +2220,8 @@ def get_available_image_models(current_path=None):
|
|
|
1565
2220
|
"display_name": f"{env_image_model} | {env_image_provider} (Configured)"
|
|
1566
2221
|
})
|
|
1567
2222
|
|
|
1568
|
-
|
|
2223
|
+
# Add predefined models (OpenAI, Gemini, and standard Diffusers)
|
|
1569
2224
|
for provider_key, models_list in IMAGE_MODELS.items():
|
|
1570
|
-
|
|
1571
2225
|
if provider_key == "openai":
|
|
1572
2226
|
if os.environ.get("OPENAI_API_KEY"):
|
|
1573
2227
|
all_image_models.extend([
|
|
@@ -1580,16 +2234,25 @@ def get_available_image_models(current_path=None):
|
|
|
1580
2234
|
{**model, "provider": provider_key, "display_name": f"{model['display_name']} | {provider_key}"}
|
|
1581
2235
|
for model in models_list
|
|
1582
2236
|
])
|
|
1583
|
-
elif provider_key == "diffusers":
|
|
1584
|
-
|
|
1585
|
-
|
|
2237
|
+
elif provider_key == "diffusers": # This entry in IMAGE_MODELS is for standard diffusers
|
|
1586
2238
|
all_image_models.extend([
|
|
1587
2239
|
{**model, "provider": provider_key, "display_name": f"{model['display_name']} | {provider_key}"}
|
|
1588
2240
|
for model in models_list
|
|
1589
2241
|
])
|
|
1590
2242
|
|
|
2243
|
+
# <--- CRITICAL FIX: Directly call the internal helper function for fine-tuned models
|
|
2244
|
+
try:
|
|
2245
|
+
finetuned_data_result = _get_finetuned_models_internal(current_path)
|
|
2246
|
+
if finetuned_data_result and finetuned_data_result.get("models"):
|
|
2247
|
+
all_image_models.extend(finetuned_data_result["models"])
|
|
2248
|
+
else:
|
|
2249
|
+
print(f"No fine-tuned models returned by internal helper or an error occurred internally.")
|
|
2250
|
+
if finetuned_data_result.get("error"):
|
|
2251
|
+
print(f"Internal error in _get_finetuned_models_internal: {finetuned_data_result['error']}")
|
|
2252
|
+
except Exception as e:
|
|
2253
|
+
print(f"Error calling _get_finetuned_models_internal: {e}")
|
|
1591
2254
|
|
|
1592
|
-
|
|
2255
|
+
# Deduplicate models
|
|
1593
2256
|
seen_models = set()
|
|
1594
2257
|
unique_models = []
|
|
1595
2258
|
for model_entry in all_image_models:
|
|
@@ -1598,6 +2261,7 @@ def get_available_image_models(current_path=None):
|
|
|
1598
2261
|
seen_models.add(key)
|
|
1599
2262
|
unique_models.append(model_entry)
|
|
1600
2263
|
|
|
2264
|
+
# Return the combined, deduplicated list of models as a dictionary with a 'models' key
|
|
1601
2265
|
return unique_models
|
|
1602
2266
|
|
|
1603
2267
|
@app.route('/api/generative_fill', methods=['POST'])
|
|
@@ -1932,14 +2596,24 @@ def get_mcp_tools():
|
|
|
1932
2596
|
It will try to use an existing client from corca_states if available and matching,
|
|
1933
2597
|
otherwise it creates a temporary client.
|
|
1934
2598
|
"""
|
|
1935
|
-
|
|
2599
|
+
raw_server_path = request.args.get("mcpServerPath")
|
|
2600
|
+
current_path_arg = request.args.get("currentPath")
|
|
1936
2601
|
conversation_id = request.args.get("conversationId")
|
|
1937
2602
|
npc_name = request.args.get("npc")
|
|
2603
|
+
selected_filter = request.args.get("selected", "")
|
|
2604
|
+
selected_names = [s.strip() for s in selected_filter.split(",") if s.strip()]
|
|
1938
2605
|
|
|
1939
|
-
if not
|
|
2606
|
+
if not raw_server_path:
|
|
1940
2607
|
return jsonify({"error": "mcpServerPath parameter is required."}), 400
|
|
1941
2608
|
|
|
1942
|
-
|
|
2609
|
+
# Normalize/expand the provided path so cwd/tilde don't break imports
|
|
2610
|
+
resolved_path = resolve_mcp_server_path(
|
|
2611
|
+
current_path=current_path_arg,
|
|
2612
|
+
explicit_path=raw_server_path,
|
|
2613
|
+
force_global=False
|
|
2614
|
+
)
|
|
2615
|
+
server_path = os.path.abspath(os.path.expanduser(resolved_path))
|
|
2616
|
+
|
|
1943
2617
|
try:
|
|
1944
2618
|
from npcsh.corca import MCPClientNPC
|
|
1945
2619
|
except ImportError:
|
|
@@ -1956,13 +2630,19 @@ def get_mcp_tools():
|
|
|
1956
2630
|
and existing_corca_state.mcp_client.server_script_path == server_path:
|
|
1957
2631
|
print(f"Using existing MCP client for {state_key} to fetch tools.")
|
|
1958
2632
|
temp_mcp_client = existing_corca_state.mcp_client
|
|
1959
|
-
|
|
2633
|
+
tools = temp_mcp_client.available_tools_llm
|
|
2634
|
+
if selected_names:
|
|
2635
|
+
tools = [t for t in tools if t.get("function", {}).get("name") in selected_names]
|
|
2636
|
+
return jsonify({"tools": tools, "error": None})
|
|
1960
2637
|
|
|
1961
2638
|
|
|
1962
2639
|
print(f"Creating a temporary MCP client to fetch tools for {server_path}.")
|
|
1963
2640
|
temp_mcp_client = MCPClientNPC()
|
|
1964
2641
|
if temp_mcp_client.connect_sync(server_path):
|
|
1965
|
-
|
|
2642
|
+
tools = temp_mcp_client.available_tools_llm
|
|
2643
|
+
if selected_names:
|
|
2644
|
+
tools = [t for t in tools if t.get("function", {}).get("name") in selected_names]
|
|
2645
|
+
return jsonify({"tools": tools, "error": None})
|
|
1966
2646
|
else:
|
|
1967
2647
|
return jsonify({"error": f"Failed to connect to MCP server at {server_path}."}), 500
|
|
1968
2648
|
except FileNotFoundError as e:
|
|
@@ -1981,6 +2661,64 @@ def get_mcp_tools():
|
|
|
1981
2661
|
temp_mcp_client.disconnect_sync()
|
|
1982
2662
|
|
|
1983
2663
|
|
|
2664
|
+
@app.route("/api/mcp/server/resolve", methods=["GET"])
|
|
2665
|
+
def api_mcp_resolve():
|
|
2666
|
+
current_path = request.args.get("currentPath")
|
|
2667
|
+
explicit = request.args.get("serverPath")
|
|
2668
|
+
try:
|
|
2669
|
+
resolved = resolve_mcp_server_path(current_path=current_path, explicit_path=explicit)
|
|
2670
|
+
return jsonify({"serverPath": resolved, "error": None})
|
|
2671
|
+
except Exception as e:
|
|
2672
|
+
return jsonify({"serverPath": None, "error": str(e)}), 500
|
|
2673
|
+
|
|
2674
|
+
|
|
2675
|
+
@app.route("/api/mcp/server/start", methods=["POST"])
|
|
2676
|
+
def api_mcp_start():
|
|
2677
|
+
data = request.get_json() or {}
|
|
2678
|
+
current_path = data.get("currentPath")
|
|
2679
|
+
explicit = data.get("serverPath")
|
|
2680
|
+
try:
|
|
2681
|
+
server_path = resolve_mcp_server_path(current_path=current_path, explicit_path=explicit)
|
|
2682
|
+
result = mcp_server_manager.start(server_path)
|
|
2683
|
+
return jsonify({**result, "error": None})
|
|
2684
|
+
except Exception as e:
|
|
2685
|
+
print(f"Error starting MCP server: {e}")
|
|
2686
|
+
traceback.print_exc()
|
|
2687
|
+
return jsonify({"error": str(e)}), 500
|
|
2688
|
+
|
|
2689
|
+
|
|
2690
|
+
@app.route("/api/mcp/server/stop", methods=["POST"])
|
|
2691
|
+
def api_mcp_stop():
|
|
2692
|
+
data = request.get_json() or {}
|
|
2693
|
+
explicit = data.get("serverPath")
|
|
2694
|
+
if not explicit:
|
|
2695
|
+
return jsonify({"error": "serverPath is required to stop a server."}), 400
|
|
2696
|
+
try:
|
|
2697
|
+
result = mcp_server_manager.stop(explicit)
|
|
2698
|
+
return jsonify({**result, "error": None})
|
|
2699
|
+
except Exception as e:
|
|
2700
|
+
print(f"Error stopping MCP server: {e}")
|
|
2701
|
+
traceback.print_exc()
|
|
2702
|
+
return jsonify({"error": str(e)}), 500
|
|
2703
|
+
|
|
2704
|
+
|
|
2705
|
+
@app.route("/api/mcp/server/status", methods=["GET"])
|
|
2706
|
+
def api_mcp_status():
|
|
2707
|
+
explicit = request.args.get("serverPath")
|
|
2708
|
+
current_path = request.args.get("currentPath")
|
|
2709
|
+
try:
|
|
2710
|
+
if explicit:
|
|
2711
|
+
result = mcp_server_manager.status(explicit)
|
|
2712
|
+
else:
|
|
2713
|
+
resolved = resolve_mcp_server_path(current_path=current_path, explicit_path=explicit)
|
|
2714
|
+
result = mcp_server_manager.status(resolved)
|
|
2715
|
+
return jsonify({**result, "running": result.get("status") == "running", "all": mcp_server_manager.running(), "error": None})
|
|
2716
|
+
except Exception as e:
|
|
2717
|
+
print(f"Error checking MCP server status: {e}")
|
|
2718
|
+
traceback.print_exc()
|
|
2719
|
+
return jsonify({"error": str(e)}), 500
|
|
2720
|
+
|
|
2721
|
+
|
|
1984
2722
|
@app.route("/api/image_models", methods=["GET"])
|
|
1985
2723
|
def get_image_models_api():
|
|
1986
2724
|
"""
|
|
@@ -1989,6 +2727,7 @@ def get_image_models_api():
|
|
|
1989
2727
|
current_path = request.args.get("currentPath")
|
|
1990
2728
|
try:
|
|
1991
2729
|
image_models = get_available_image_models(current_path)
|
|
2730
|
+
print('image models', image_models)
|
|
1992
2731
|
return jsonify({"models": image_models, "error": None})
|
|
1993
2732
|
except Exception as e:
|
|
1994
2733
|
print(f"Error getting available image models: {str(e)}")
|
|
@@ -2000,6 +2739,195 @@ def get_image_models_api():
|
|
|
2000
2739
|
|
|
2001
2740
|
|
|
2002
2741
|
|
|
2742
|
+
def _run_stream_post_processing(
|
|
2743
|
+
conversation_turn_text,
|
|
2744
|
+
conversation_id,
|
|
2745
|
+
command_history,
|
|
2746
|
+
npc_name,
|
|
2747
|
+
team_name,
|
|
2748
|
+
current_path,
|
|
2749
|
+
model,
|
|
2750
|
+
provider,
|
|
2751
|
+
npc_object,
|
|
2752
|
+
messages # For context compression
|
|
2753
|
+
):
|
|
2754
|
+
"""
|
|
2755
|
+
Runs memory extraction and context compression in a background thread.
|
|
2756
|
+
These operations will not block the main stream.
|
|
2757
|
+
"""
|
|
2758
|
+
print(f"🌋 Background task started for conversation {conversation_id}!")
|
|
2759
|
+
|
|
2760
|
+
# Memory extraction and KG fact insertion
|
|
2761
|
+
try:
|
|
2762
|
+
if len(conversation_turn_text) > 50: # Only extract memories if the turn is substantial
|
|
2763
|
+
memories_for_approval = extract_and_store_memories(
|
|
2764
|
+
conversation_turn_text,
|
|
2765
|
+
conversation_id,
|
|
2766
|
+
command_history,
|
|
2767
|
+
npc_name,
|
|
2768
|
+
team_name,
|
|
2769
|
+
current_path,
|
|
2770
|
+
model,
|
|
2771
|
+
provider,
|
|
2772
|
+
npc_object
|
|
2773
|
+
)
|
|
2774
|
+
if memories_for_approval:
|
|
2775
|
+
print(f"🔥 Background: Extracted {len(memories_for_approval)} memories for approval for conversation {conversation_id}. Stored as pending in the database (table: memory_lifecycle).")
|
|
2776
|
+
else:
|
|
2777
|
+
print(f"Background: Conversation turn too short ({len(conversation_turn_text)} chars) for memory extraction. Skipping.")
|
|
2778
|
+
except Exception as e:
|
|
2779
|
+
print(f"🌋 Background: Error during memory extraction and KG insertion for conversation {conversation_id}: {e}")
|
|
2780
|
+
traceback.print_exc()
|
|
2781
|
+
|
|
2782
|
+
# Context compression using breathe from llm_funcs
|
|
2783
|
+
try:
|
|
2784
|
+
if len(messages) > 30: # Use the threshold specified in your request
|
|
2785
|
+
# Directly call breathe for summarization
|
|
2786
|
+
breathe_result = breathe(
|
|
2787
|
+
messages=messages,
|
|
2788
|
+
model=model,
|
|
2789
|
+
provider=provider,
|
|
2790
|
+
npc=npc_object # Pass npc for context if available
|
|
2791
|
+
)
|
|
2792
|
+
compressed_output = breathe_result.get('output', '')
|
|
2793
|
+
|
|
2794
|
+
if compressed_output:
|
|
2795
|
+
# Save the compressed context as a new system message in conversation_history
|
|
2796
|
+
compressed_message_id = generate_message_id()
|
|
2797
|
+
save_conversation_message(
|
|
2798
|
+
command_history,
|
|
2799
|
+
conversation_id,
|
|
2800
|
+
"system", # Role for compressed context
|
|
2801
|
+
f"[AUTOMATIC CONTEXT COMPRESSION]: {compressed_output}",
|
|
2802
|
+
wd=current_path,
|
|
2803
|
+
model=model, # Use the same model/provider that generated the summary
|
|
2804
|
+
provider=provider,
|
|
2805
|
+
npc=npc_name, # Associate with the NPC
|
|
2806
|
+
team=team_name, # Associate with the team
|
|
2807
|
+
message_id=compressed_message_id
|
|
2808
|
+
)
|
|
2809
|
+
print(f"💨 Background: Compressed context for conversation {conversation_id} saved as new system message: {compressed_output[:100]}...")
|
|
2810
|
+
else:
|
|
2811
|
+
print(f"Background: Context compression returned no output for conversation {conversation_id}. Skipping saving.")
|
|
2812
|
+
else:
|
|
2813
|
+
print(f"Background: Conversation messages count ({len(messages)}) below threshold for context compression. Skipping.")
|
|
2814
|
+
except Exception as e:
|
|
2815
|
+
print(f"🌋 Background: Error during context compression with breathe for conversation {conversation_id}: {e}")
|
|
2816
|
+
traceback.print_exc()
|
|
2817
|
+
|
|
2818
|
+
print(f"🌋 Background task finished for conversation {conversation_id}!")
|
|
2819
|
+
|
|
2820
|
+
|
|
2821
|
+
|
|
2822
|
+
|
|
2823
|
+
@app.route("/api/text_predict", methods=["POST"])
|
|
2824
|
+
def text_predict():
|
|
2825
|
+
data = request.json
|
|
2826
|
+
|
|
2827
|
+
stream_id = data.get("streamId")
|
|
2828
|
+
if not stream_id:
|
|
2829
|
+
stream_id = str(uuid.uuid4())
|
|
2830
|
+
|
|
2831
|
+
with cancellation_lock:
|
|
2832
|
+
cancellation_flags[stream_id] = False
|
|
2833
|
+
|
|
2834
|
+
print(f"Starting text prediction stream with ID: {stream_id}")
|
|
2835
|
+
print('data')
|
|
2836
|
+
|
|
2837
|
+
|
|
2838
|
+
text_content = data.get("text_content", "")
|
|
2839
|
+
cursor_position = data.get("cursor_position", len(text_content))
|
|
2840
|
+
current_path = data.get("currentPath")
|
|
2841
|
+
model = data.get("model")
|
|
2842
|
+
provider = data.get("provider")
|
|
2843
|
+
context_type = data.get("context_type", "general") # e.g., 'code', 'chat', 'general'
|
|
2844
|
+
file_path = data.get("file_path") # Optional: for code context
|
|
2845
|
+
|
|
2846
|
+
if current_path:
|
|
2847
|
+
load_project_env(current_path)
|
|
2848
|
+
|
|
2849
|
+
text_before_cursor = text_content[:cursor_position]
|
|
2850
|
+
|
|
2851
|
+
|
|
2852
|
+
if context_type == 'code':
|
|
2853
|
+
prompt_for_llm = f"You are an AI code completion assistant. Your task is to complete the provided code snippet.\nYou MUST ONLY output the code that directly completes the snippet.\nDO NOT include any explanations, comments, or additional text.\nDO NOT wrap the completion in markdown code blocks.\n\nHere is the code context where the completion should occur (file: {file_path or 'unknown'}):\n\n{text_before_cursor}\n\nPlease provide the completion starting from the end of the last line shown.\n"
|
|
2854
|
+
system_prompt = "You are an AI code completion assistant. Only provide code. Do not add explanations or any other text."
|
|
2855
|
+
elif context_type == 'chat':
|
|
2856
|
+
prompt_for_llm = f"You are an AI chat assistant. Your task is to provide a natural and helpful completion to the user's ongoing message.\nYou MUST ONLY output the text that directly completes the message.\nDO NOT include any explanations or additional text.\n\nHere is the message context where the completion should occur:\n\n{text_before_cursor}\n\nPlease provide the completion starting from the end of the last line shown.\n"
|
|
2857
|
+
system_prompt = "You are an AI chat assistant. Only provide natural language completion. Do not add explanations or any other text."
|
|
2858
|
+
else: # general text prediction
|
|
2859
|
+
prompt_for_llm = f"You are an AI text completion assistant. Your task is to provide a natural and helpful completion to the user's ongoing text.\nYou MUST ONLY output the text that directly completes the snippet.\nDO NOT include any explanations or additional text.\n\nHere is the text context where the completion should occur:\n\n{text_before_cursor}\n\nPlease provide the completion starting from the end of the last line shown.\n"
|
|
2860
|
+
system_prompt = "You are an AI text completion assistant. Only provide natural language completion. Do not add explanations or any other text."
|
|
2861
|
+
|
|
2862
|
+
|
|
2863
|
+
npc_object = None # For prediction, we don't necessarily use a specific NPC
|
|
2864
|
+
|
|
2865
|
+
messages_for_llm = [
|
|
2866
|
+
{"role": "system", "content": system_prompt},
|
|
2867
|
+
{"role": "user", "content": prompt_for_llm}
|
|
2868
|
+
]
|
|
2869
|
+
|
|
2870
|
+
def event_stream_text_predict(current_stream_id):
|
|
2871
|
+
complete_prediction = []
|
|
2872
|
+
try:
|
|
2873
|
+
stream_response_generator = get_llm_response(
|
|
2874
|
+
prompt_for_llm,
|
|
2875
|
+
messages=messages_for_llm,
|
|
2876
|
+
model=model,
|
|
2877
|
+
provider=provider,
|
|
2878
|
+
npc=npc_object,
|
|
2879
|
+
stream=True,
|
|
2880
|
+
)
|
|
2881
|
+
|
|
2882
|
+
# get_llm_response returns a dict with 'response' as a generator when stream=True
|
|
2883
|
+
if isinstance(stream_response_generator, dict) and 'response' in stream_response_generator:
|
|
2884
|
+
stream_generator = stream_response_generator['response']
|
|
2885
|
+
else:
|
|
2886
|
+
# Fallback for non-streaming LLM responses or errors
|
|
2887
|
+
output_content = ""
|
|
2888
|
+
if isinstance(stream_response_generator, dict) and 'output' in stream_response_generator:
|
|
2889
|
+
output_content = stream_response_generator['output']
|
|
2890
|
+
elif isinstance(stream_response_generator, str):
|
|
2891
|
+
output_content = stream_response_generator
|
|
2892
|
+
|
|
2893
|
+
yield f"data: {json.dumps({'choices': [{'delta': {'content': output_content}}]})}\n\n"
|
|
2894
|
+
yield f"data: [DONE]\n\n"
|
|
2895
|
+
return
|
|
2896
|
+
|
|
2897
|
+
|
|
2898
|
+
for response_chunk in stream_generator:
|
|
2899
|
+
with cancellation_lock:
|
|
2900
|
+
if cancellation_flags.get(current_stream_id, False):
|
|
2901
|
+
print(f"Cancellation flag triggered for {current_stream_id}. Breaking loop.")
|
|
2902
|
+
break
|
|
2903
|
+
|
|
2904
|
+
chunk_content = ""
|
|
2905
|
+
# Handle different LLM API response formats
|
|
2906
|
+
if "hf.co" in model or (provider == 'ollama' and 'gpt-oss' not in model): # Heuristic for Ollama/HF models
|
|
2907
|
+
chunk_content = response_chunk["message"]["content"] if "message" in response_chunk and "content" in response_chunk["message"] else ""
|
|
2908
|
+
else: # Assume OpenAI-like streaming format
|
|
2909
|
+
chunk_content = "".join(choice.delta.content for choice in response_chunk.choices if choice.delta.content is not None)
|
|
2910
|
+
|
|
2911
|
+
print(chunk_content, end='')
|
|
2912
|
+
|
|
2913
|
+
if chunk_content:
|
|
2914
|
+
complete_prediction.append(chunk_content)
|
|
2915
|
+
yield f"data: {json.dumps({'choices': [{'delta': {'content': chunk_content}}]})}\n\n"
|
|
2916
|
+
|
|
2917
|
+
except Exception as e:
|
|
2918
|
+
print(f"\nAn exception occurred during text prediction streaming for {current_stream_id}: {e}")
|
|
2919
|
+
traceback.print_exc()
|
|
2920
|
+
yield f"data: {json.dumps({'error': str(e)})}\n\n"
|
|
2921
|
+
|
|
2922
|
+
finally:
|
|
2923
|
+
print(f"\nText prediction stream {current_stream_id} finished.")
|
|
2924
|
+
yield f"data: [DONE]\n\n" # Signal end of stream
|
|
2925
|
+
with cancellation_lock:
|
|
2926
|
+
if current_stream_id in cancellation_flags:
|
|
2927
|
+
del cancellation_flags[current_stream_id]
|
|
2928
|
+
print(f"Cleaned up cancellation flag for stream ID: {current_stream_id}")
|
|
2929
|
+
|
|
2930
|
+
return Response(event_stream_text_predict(stream_id), mimetype="text/event-stream")
|
|
2003
2931
|
|
|
2004
2932
|
@app.route("/api/stream", methods=["POST"])
|
|
2005
2933
|
def stream():
|
|
@@ -2195,7 +3123,9 @@ def stream():
|
|
|
2195
3123
|
if 'tools' in tool_args and tool_args['tools']:
|
|
2196
3124
|
tool_args['tool_choice'] = {"type": "auto"}
|
|
2197
3125
|
|
|
2198
|
-
|
|
3126
|
+
# Default stream response so closures below always have a value
|
|
3127
|
+
stream_response = {"output": "", "messages": messages}
|
|
3128
|
+
|
|
2199
3129
|
exe_mode = data.get('executionMode','chat')
|
|
2200
3130
|
|
|
2201
3131
|
if exe_mode == 'chat':
|
|
@@ -2270,23 +3200,18 @@ def stream():
|
|
|
2270
3200
|
messages = state.messages
|
|
2271
3201
|
|
|
2272
3202
|
elif exe_mode == 'corca':
|
|
2273
|
-
|
|
2274
3203
|
try:
|
|
2275
3204
|
from npcsh.corca import execute_command_corca, create_corca_state_and_mcp_client, MCPClientNPC
|
|
2276
3205
|
from npcsh._state import initial_state as state
|
|
2277
3206
|
except ImportError:
|
|
2278
|
-
|
|
2279
3207
|
print("ERROR: npcsh.corca or MCPClientNPC not found. Corca mode is disabled.", file=sys.stderr)
|
|
2280
|
-
state = None
|
|
3208
|
+
state = None
|
|
2281
3209
|
stream_response = {"output": "Corca mode is not available due to missing dependencies.", "messages": messages}
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
if state is not None:
|
|
2285
|
-
|
|
3210
|
+
|
|
3211
|
+
if state is not None:
|
|
2286
3212
|
mcp_server_path_from_request = data.get("mcpServerPath")
|
|
2287
3213
|
selected_mcp_tools_from_request = data.get("selectedMcpTools", [])
|
|
2288
|
-
|
|
2289
|
-
|
|
3214
|
+
|
|
2290
3215
|
effective_mcp_server_path = mcp_server_path_from_request
|
|
2291
3216
|
if not effective_mcp_server_path and team_object and hasattr(team_object, 'team_ctx') and team_object.team_ctx:
|
|
2292
3217
|
mcp_servers_list = team_object.team_ctx.get('mcp_servers', [])
|
|
@@ -2294,18 +3219,19 @@ def stream():
|
|
|
2294
3219
|
first_server_obj = next((s for s in mcp_servers_list if isinstance(s, dict) and 'value' in s), None)
|
|
2295
3220
|
if first_server_obj:
|
|
2296
3221
|
effective_mcp_server_path = first_server_obj['value']
|
|
2297
|
-
elif isinstance(team_object.team_ctx.get('mcp_server'), str):
|
|
3222
|
+
elif isinstance(team_object.team_ctx.get('mcp_server'), str):
|
|
2298
3223
|
effective_mcp_server_path = team_object.team_ctx.get('mcp_server')
|
|
2299
3224
|
|
|
2300
|
-
|
|
3225
|
+
if effective_mcp_server_path:
|
|
3226
|
+
effective_mcp_server_path = os.path.abspath(os.path.expanduser(effective_mcp_server_path))
|
|
3227
|
+
|
|
2301
3228
|
if not hasattr(app, 'corca_states'):
|
|
2302
3229
|
app.corca_states = {}
|
|
2303
|
-
|
|
3230
|
+
|
|
2304
3231
|
state_key = f"{conversation_id}_{npc_name or 'default'}"
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
if
|
|
2308
|
-
|
|
3232
|
+
corca_state = app.corca_states.get(state_key)
|
|
3233
|
+
|
|
3234
|
+
if corca_state is None:
|
|
2309
3235
|
corca_state = create_corca_state_and_mcp_client(
|
|
2310
3236
|
conversation_id=conversation_id,
|
|
2311
3237
|
command_history=command_history,
|
|
@@ -2316,21 +3242,21 @@ def stream():
|
|
|
2316
3242
|
)
|
|
2317
3243
|
app.corca_states[state_key] = corca_state
|
|
2318
3244
|
else:
|
|
2319
|
-
corca_state = app.corca_states[state_key]
|
|
2320
3245
|
corca_state.npc = npc_object
|
|
2321
3246
|
corca_state.team = team_object
|
|
2322
3247
|
corca_state.current_path = current_path
|
|
2323
3248
|
corca_state.messages = messages
|
|
2324
3249
|
corca_state.command_history = command_history
|
|
2325
3250
|
|
|
2326
|
-
|
|
2327
3251
|
current_mcp_client_path = getattr(corca_state.mcp_client, 'server_script_path', None)
|
|
3252
|
+
if current_mcp_client_path:
|
|
3253
|
+
current_mcp_client_path = os.path.abspath(os.path.expanduser(current_mcp_client_path))
|
|
2328
3254
|
|
|
2329
3255
|
if effective_mcp_server_path != current_mcp_client_path:
|
|
2330
3256
|
print(f"MCP server path changed/updated for {state_key}. Disconnecting old client (if any) and reconnecting to {effective_mcp_server_path or 'None'}.")
|
|
2331
3257
|
if corca_state.mcp_client and corca_state.mcp_client.session:
|
|
2332
3258
|
corca_state.mcp_client.disconnect_sync()
|
|
2333
|
-
corca_state.mcp_client = None
|
|
3259
|
+
corca_state.mcp_client = None
|
|
2334
3260
|
|
|
2335
3261
|
if effective_mcp_server_path:
|
|
2336
3262
|
new_mcp_client = MCPClientNPC()
|
|
@@ -2340,20 +3266,19 @@ def stream():
|
|
|
2340
3266
|
else:
|
|
2341
3267
|
print(f"Failed to reconnect MCP client for {state_key} to {effective_mcp_server_path}. Corca will have no tools.")
|
|
2342
3268
|
corca_state.mcp_client = None
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
3269
|
+
|
|
2346
3270
|
state, stream_response = execute_command_corca(
|
|
2347
3271
|
commandstr,
|
|
2348
3272
|
corca_state,
|
|
2349
3273
|
command_history,
|
|
2350
|
-
selected_mcp_tools_names=selected_mcp_tools_from_request
|
|
3274
|
+
selected_mcp_tools_names=selected_mcp_tools_from_request
|
|
2351
3275
|
)
|
|
2352
|
-
|
|
2353
|
-
|
|
3276
|
+
|
|
2354
3277
|
app.corca_states[state_key] = state
|
|
2355
|
-
messages = state.messages
|
|
3278
|
+
messages = state.messages
|
|
2356
3279
|
|
|
3280
|
+
else:
|
|
3281
|
+
stream_response = {"output": f"Unsupported execution mode: {exe_mode}", "messages": messages}
|
|
2357
3282
|
|
|
2358
3283
|
user_message_filled = ''
|
|
2359
3284
|
|
|
@@ -2394,44 +3319,44 @@ def stream():
|
|
|
2394
3319
|
if isinstance(stream_response, str) :
|
|
2395
3320
|
print('stream a str and not a gen')
|
|
2396
3321
|
chunk_data = {
|
|
2397
|
-
"id": None,
|
|
2398
|
-
"object": None,
|
|
2399
|
-
"created": datetime.datetime.now().strftime('YYYY-DD-MM-HHMMSS'),
|
|
3322
|
+
"id": None,
|
|
3323
|
+
"object": None,
|
|
3324
|
+
"created": datetime.datetime.now().strftime('YYYY-DD-MM-HHMMSS'),
|
|
2400
3325
|
"model": model,
|
|
2401
3326
|
"choices": [
|
|
2402
3327
|
{
|
|
2403
|
-
"index": 0,
|
|
2404
|
-
"delta":
|
|
3328
|
+
"index": 0,
|
|
3329
|
+
"delta":
|
|
2405
3330
|
{
|
|
2406
3331
|
"content": stream_response,
|
|
2407
3332
|
"role": "assistant"
|
|
2408
|
-
},
|
|
3333
|
+
},
|
|
2409
3334
|
"finish_reason": 'done'
|
|
2410
3335
|
}
|
|
2411
3336
|
]
|
|
2412
3337
|
}
|
|
2413
|
-
yield f"data: {json.dumps(chunk_data)}"
|
|
3338
|
+
yield f"data: {json.dumps(chunk_data)}\n\n"
|
|
2414
3339
|
return
|
|
2415
3340
|
elif isinstance(stream_response, dict) and 'output' in stream_response and isinstance(stream_response.get('output'), str):
|
|
2416
|
-
print('stream a str and not a gen')
|
|
3341
|
+
print('stream a str and not a gen')
|
|
2417
3342
|
chunk_data = {
|
|
2418
|
-
"id": None,
|
|
2419
|
-
"object": None,
|
|
2420
|
-
"created": datetime.datetime.now().strftime('YYYY-DD-MM-HHMMSS'),
|
|
3343
|
+
"id": None,
|
|
3344
|
+
"object": None,
|
|
3345
|
+
"created": datetime.datetime.now().strftime('YYYY-DD-MM-HHMMSS'),
|
|
2421
3346
|
"model": model,
|
|
2422
3347
|
"choices": [
|
|
2423
3348
|
{
|
|
2424
|
-
"index": 0,
|
|
2425
|
-
"delta":
|
|
3349
|
+
"index": 0,
|
|
3350
|
+
"delta":
|
|
2426
3351
|
{
|
|
2427
3352
|
"content": stream_response.get('output') ,
|
|
2428
3353
|
"role": "assistant"
|
|
2429
|
-
},
|
|
3354
|
+
},
|
|
2430
3355
|
"finish_reason": 'done'
|
|
2431
3356
|
}
|
|
2432
3357
|
]
|
|
2433
3358
|
}
|
|
2434
|
-
yield f"data: {json.dumps(chunk_data)}"
|
|
3359
|
+
yield f"data: {json.dumps(chunk_data)}\n\n"
|
|
2435
3360
|
return
|
|
2436
3361
|
for response_chunk in stream_response.get('response', stream_response.get('output')):
|
|
2437
3362
|
with cancellation_lock:
|
|
@@ -2459,8 +3384,8 @@ def stream():
|
|
|
2459
3384
|
if chunk_content:
|
|
2460
3385
|
complete_response.append(chunk_content)
|
|
2461
3386
|
chunk_data = {
|
|
2462
|
-
"id": None, "object": None,
|
|
2463
|
-
"created": response_chunk["created_at"] or datetime.datetime.now(),
|
|
3387
|
+
"id": None, "object": None,
|
|
3388
|
+
"created": response_chunk["created_at"] or datetime.datetime.now(),
|
|
2464
3389
|
"model": response_chunk["model"],
|
|
2465
3390
|
"choices": [{"index": 0, "delta": {"content": chunk_content, "role": response_chunk["message"]["role"]}, "finish_reason": response_chunk.get("done_reason")}]
|
|
2466
3391
|
}
|
|
@@ -2494,33 +3419,56 @@ def stream():
|
|
|
2494
3419
|
print(f"\nAn exception occurred during streaming for {current_stream_id}: {e}")
|
|
2495
3420
|
traceback.print_exc()
|
|
2496
3421
|
interrupted = True
|
|
2497
|
-
|
|
3422
|
+
|
|
2498
3423
|
finally:
|
|
2499
3424
|
print(f"\nStream {current_stream_id} finished. Interrupted: {interrupted}")
|
|
2500
3425
|
print('\r' + ' ' * dot_count*2 + '\r', end="", flush=True)
|
|
2501
3426
|
|
|
2502
3427
|
final_response_text = ''.join(complete_response)
|
|
3428
|
+
|
|
3429
|
+
# Yield message_stop immediately so the client's stream ends quickly
|
|
2503
3430
|
yield f"data: {json.dumps({'type': 'message_stop'})}\n\n"
|
|
2504
|
-
|
|
3431
|
+
|
|
3432
|
+
# Save assistant message to the database
|
|
2505
3433
|
npc_name_to_save = npc_object.name if npc_object else ''
|
|
2506
3434
|
save_conversation_message(
|
|
2507
|
-
command_history,
|
|
2508
|
-
conversation_id,
|
|
2509
|
-
"assistant",
|
|
3435
|
+
command_history,
|
|
3436
|
+
conversation_id,
|
|
3437
|
+
"assistant",
|
|
2510
3438
|
final_response_text,
|
|
2511
|
-
wd=current_path,
|
|
2512
|
-
model=model,
|
|
3439
|
+
wd=current_path,
|
|
3440
|
+
model=model,
|
|
2513
3441
|
provider=provider,
|
|
2514
|
-
npc=npc_name_to_save,
|
|
2515
|
-
team=team,
|
|
3442
|
+
npc=npc_name_to_save,
|
|
3443
|
+
team=team,
|
|
2516
3444
|
message_id=message_id,
|
|
2517
3445
|
)
|
|
2518
3446
|
|
|
3447
|
+
# Start background tasks for memory extraction and context compression
|
|
3448
|
+
# These will run without blocking the main response stream.
|
|
3449
|
+
conversation_turn_text = f"User: {commandstr}\nAssistant: {final_response_text}"
|
|
3450
|
+
background_thread = threading.Thread(
|
|
3451
|
+
target=_run_stream_post_processing,
|
|
3452
|
+
args=(
|
|
3453
|
+
conversation_turn_text,
|
|
3454
|
+
conversation_id,
|
|
3455
|
+
command_history,
|
|
3456
|
+
npc_name,
|
|
3457
|
+
team, # Pass the team variable from the outer scope
|
|
3458
|
+
current_path,
|
|
3459
|
+
model,
|
|
3460
|
+
provider,
|
|
3461
|
+
npc_object,
|
|
3462
|
+
messages # Pass messages for context compression
|
|
3463
|
+
)
|
|
3464
|
+
)
|
|
3465
|
+
background_thread.daemon = True # Allow the main program to exit even if this thread is still running
|
|
3466
|
+
background_thread.start()
|
|
3467
|
+
|
|
2519
3468
|
with cancellation_lock:
|
|
2520
3469
|
if current_stream_id in cancellation_flags:
|
|
2521
3470
|
del cancellation_flags[current_stream_id]
|
|
2522
3471
|
print(f"Cleaned up cancellation flag for stream ID: {current_stream_id}")
|
|
2523
|
-
|
|
2524
3472
|
return Response(event_stream(stream_id), mimetype="text/event-stream")
|
|
2525
3473
|
|
|
2526
3474
|
@app.route('/api/delete_message', methods=['POST'])
|
|
@@ -2574,295 +3522,6 @@ def approve_memories():
|
|
|
2574
3522
|
|
|
2575
3523
|
|
|
2576
3524
|
|
|
2577
|
-
@app.route("/api/execute", methods=["POST"])
|
|
2578
|
-
def execute():
|
|
2579
|
-
data = request.json
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
stream_id = data.get("streamId")
|
|
2583
|
-
if not stream_id:
|
|
2584
|
-
import uuid
|
|
2585
|
-
stream_id = str(uuid.uuid4())
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
with cancellation_lock:
|
|
2589
|
-
cancellation_flags[stream_id] = False
|
|
2590
|
-
print(f"Starting execute stream with ID: {stream_id}")
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
commandstr = data.get("commandstr")
|
|
2594
|
-
conversation_id = data.get("conversationId")
|
|
2595
|
-
model = data.get("model", 'llama3.2')
|
|
2596
|
-
provider = data.get("provider", 'ollama')
|
|
2597
|
-
if provider is None:
|
|
2598
|
-
provider = available_models.get(model)
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
npc_name = data.get("npc", "sibiji")
|
|
2602
|
-
npc_source = data.get("npcSource", "global")
|
|
2603
|
-
team = data.get("team", None)
|
|
2604
|
-
current_path = data.get("currentPath")
|
|
2605
|
-
|
|
2606
|
-
if current_path:
|
|
2607
|
-
loaded_vars = load_project_env(current_path)
|
|
2608
|
-
print(f"Loaded project env variables for stream request: {list(loaded_vars.keys())}")
|
|
2609
|
-
|
|
2610
|
-
npc_object = None
|
|
2611
|
-
team_object = None
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
if team:
|
|
2615
|
-
print(team)
|
|
2616
|
-
if hasattr(app, 'registered_teams') and team in app.registered_teams:
|
|
2617
|
-
team_object = app.registered_teams[team]
|
|
2618
|
-
print(f"Using registered team: {team}")
|
|
2619
|
-
else:
|
|
2620
|
-
print(f"Warning: Team {team} not found in registered teams")
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
if npc_name:
|
|
2624
|
-
|
|
2625
|
-
if team and hasattr(app, 'registered_teams') and team in app.registered_teams:
|
|
2626
|
-
team_object = app.registered_teams[team]
|
|
2627
|
-
print('team', team_object)
|
|
2628
|
-
|
|
2629
|
-
if hasattr(team_object, 'npcs'):
|
|
2630
|
-
team_npcs = team_object.npcs
|
|
2631
|
-
if isinstance(team_npcs, dict):
|
|
2632
|
-
if npc_name in team_npcs:
|
|
2633
|
-
npc_object = team_npcs[npc_name]
|
|
2634
|
-
print(f"Found NPC {npc_name} in registered team {team}")
|
|
2635
|
-
elif isinstance(team_npcs, list):
|
|
2636
|
-
for npc in team_npcs:
|
|
2637
|
-
if hasattr(npc, 'name') and npc.name == npc_name:
|
|
2638
|
-
npc_object = npc
|
|
2639
|
-
print(f"Found NPC {npc_name} in registered team {team}")
|
|
2640
|
-
break
|
|
2641
|
-
|
|
2642
|
-
if not npc_object and hasattr(team_object, 'forenpc') and hasattr(team_object.forenpc, 'name'):
|
|
2643
|
-
if team_object.forenpc.name == npc_name:
|
|
2644
|
-
npc_object = team_object.forenpc
|
|
2645
|
-
print(f"Found NPC {npc_name} as forenpc in team {team}")
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
if not npc_object and hasattr(app, 'registered_npcs') and npc_name in app.registered_npcs:
|
|
2649
|
-
npc_object = app.registered_npcs[npc_name]
|
|
2650
|
-
print(f"Found NPC {npc_name} in registered NPCs")
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
if not npc_object:
|
|
2654
|
-
db_conn = get_db_connection()
|
|
2655
|
-
npc_object = load_npc_by_name_and_source(npc_name, npc_source, db_conn, current_path)
|
|
2656
|
-
|
|
2657
|
-
if not npc_object and npc_source == 'project':
|
|
2658
|
-
print(f"NPC {npc_name} not found in project directory, trying global...")
|
|
2659
|
-
npc_object = load_npc_by_name_and_source(npc_name, 'global', db_conn)
|
|
2660
|
-
|
|
2661
|
-
if npc_object:
|
|
2662
|
-
print(f"Successfully loaded NPC {npc_name} from {npc_source} directory")
|
|
2663
|
-
else:
|
|
2664
|
-
print(f"Warning: Could not load NPC {npc_name}")
|
|
2665
|
-
|
|
2666
|
-
attachments = data.get("attachments", [])
|
|
2667
|
-
command_history = CommandHistory(app.config.get('DB_PATH'))
|
|
2668
|
-
images = []
|
|
2669
|
-
attachments_loaded = []
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
if attachments:
|
|
2673
|
-
for attachment in attachments:
|
|
2674
|
-
extension = attachment["name"].split(".")[-1]
|
|
2675
|
-
extension_mapped = extension_map.get(extension.upper(), "others")
|
|
2676
|
-
file_path = os.path.expanduser("~/.npcsh/" + extension_mapped + "/" + attachment["name"])
|
|
2677
|
-
if extension_mapped == "images":
|
|
2678
|
-
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
|
2679
|
-
img = Image.open(attachment["path"])
|
|
2680
|
-
img_byte_arr = BytesIO()
|
|
2681
|
-
img.save(img_byte_arr, format="PNG")
|
|
2682
|
-
img_byte_arr.seek(0)
|
|
2683
|
-
img.save(file_path, optimize=True, quality=50)
|
|
2684
|
-
images.append(file_path)
|
|
2685
|
-
attachments_loaded.append({
|
|
2686
|
-
"name": attachment["name"], "type": extension_mapped,
|
|
2687
|
-
"data": img_byte_arr.read(), "size": os.path.getsize(file_path)
|
|
2688
|
-
})
|
|
2689
|
-
|
|
2690
|
-
messages = fetch_messages_for_conversation(conversation_id)
|
|
2691
|
-
if len(messages) == 0 and npc_object is not None:
|
|
2692
|
-
messages = [{'role': 'system', 'content': npc_object.get_system_prompt()}]
|
|
2693
|
-
elif len(messages)>0 and messages[0]['role'] != 'system' and npc_object is not None:
|
|
2694
|
-
messages.insert(0, {'role': 'system', 'content': npc_object.get_system_prompt()})
|
|
2695
|
-
elif len(messages) > 0 and npc_object is not None:
|
|
2696
|
-
messages[0]['content'] = npc_object.get_system_prompt()
|
|
2697
|
-
if npc_object is not None and messages and messages[0]['role'] == 'system':
|
|
2698
|
-
messages[0]['content'] = npc_object.get_system_prompt()
|
|
2699
|
-
|
|
2700
|
-
message_id = generate_message_id()
|
|
2701
|
-
save_conversation_message(
|
|
2702
|
-
command_history, conversation_id, "user", commandstr,
|
|
2703
|
-
wd=current_path, model=model, provider=provider, npc=npc_name,
|
|
2704
|
-
team=team, attachments=attachments_loaded, message_id=message_id,
|
|
2705
|
-
)
|
|
2706
|
-
response_gen = check_llm_command(
|
|
2707
|
-
commandstr, messages=messages, images=images, model=model,
|
|
2708
|
-
provider=provider, npc=npc_object, team=team_object, stream=True
|
|
2709
|
-
)
|
|
2710
|
-
print(response_gen)
|
|
2711
|
-
|
|
2712
|
-
message_id = generate_message_id()
|
|
2713
|
-
|
|
2714
|
-
def event_stream(current_stream_id):
|
|
2715
|
-
complete_response = []
|
|
2716
|
-
dot_count = 0
|
|
2717
|
-
interrupted = False
|
|
2718
|
-
tool_call_data = {"id": None, "function_name": None, "arguments": ""}
|
|
2719
|
-
memory_data = None
|
|
2720
|
-
|
|
2721
|
-
try:
|
|
2722
|
-
for response_chunk in stream_response.get('response', stream_response.get('output')):
|
|
2723
|
-
with cancellation_lock:
|
|
2724
|
-
if cancellation_flags.get(current_stream_id, False):
|
|
2725
|
-
print(f"Cancellation flag triggered for {current_stream_id}. Breaking loop.")
|
|
2726
|
-
interrupted = True
|
|
2727
|
-
break
|
|
2728
|
-
|
|
2729
|
-
print('.', end="", flush=True)
|
|
2730
|
-
dot_count += 1
|
|
2731
|
-
|
|
2732
|
-
if "hf.co" in model or provider == 'ollama':
|
|
2733
|
-
chunk_content = response_chunk["message"]["content"] if "message" in response_chunk and "content" in response_chunk["message"] else ""
|
|
2734
|
-
if "message" in response_chunk and "tool_calls" in response_chunk["message"]:
|
|
2735
|
-
for tool_call in response_chunk["message"]["tool_calls"]:
|
|
2736
|
-
if "id" in tool_call:
|
|
2737
|
-
tool_call_data["id"] = tool_call["id"]
|
|
2738
|
-
if "function" in tool_call:
|
|
2739
|
-
if "name" in tool_call["function"]:
|
|
2740
|
-
tool_call_data["function_name"] = tool_call["function"]["name"]
|
|
2741
|
-
if "arguments" in tool_call["function"]:
|
|
2742
|
-
arg_val = tool_call["function"]["arguments"]
|
|
2743
|
-
if isinstance(arg_val, dict):
|
|
2744
|
-
arg_val = json.dumps(arg_val)
|
|
2745
|
-
tool_call_data["arguments"] += arg_val
|
|
2746
|
-
if chunk_content:
|
|
2747
|
-
complete_response.append(chunk_content)
|
|
2748
|
-
chunk_data = {
|
|
2749
|
-
"id": None, "object": None, "created": response_chunk["created_at"], "model": response_chunk["model"],
|
|
2750
|
-
"choices": [{"index": 0, "delta": {"content": chunk_content, "role": response_chunk["message"]["role"]}, "finish_reason": response_chunk.get("done_reason")}]
|
|
2751
|
-
}
|
|
2752
|
-
yield f"data: {json.dumps(chunk_data)}\n\n"
|
|
2753
|
-
else:
|
|
2754
|
-
chunk_content = ""
|
|
2755
|
-
reasoning_content = ""
|
|
2756
|
-
for choice in response_chunk.choices:
|
|
2757
|
-
if hasattr(choice.delta, "tool_calls") and choice.delta.tool_calls:
|
|
2758
|
-
for tool_call in choice.delta.tool_calls:
|
|
2759
|
-
if tool_call.id:
|
|
2760
|
-
tool_call_data["id"] = tool_call.id
|
|
2761
|
-
if tool_call.function:
|
|
2762
|
-
if hasattr(tool_call.function, "name") and tool_call.function.name:
|
|
2763
|
-
tool_call_data["function_name"] = tool_call.function.name
|
|
2764
|
-
if hasattr(tool_call.function, "arguments") and tool_call.function.arguments:
|
|
2765
|
-
tool_call_data["arguments"] += tool_call.function.arguments
|
|
2766
|
-
for choice in response_chunk.choices:
|
|
2767
|
-
if hasattr(choice.delta, "reasoning_content"):
|
|
2768
|
-
reasoning_content += choice.delta.reasoning_content
|
|
2769
|
-
chunk_content = "".join(choice.delta.content for choice in response_chunk.choices if choice.delta.content is not None)
|
|
2770
|
-
if chunk_content:
|
|
2771
|
-
complete_response.append(chunk_content)
|
|
2772
|
-
chunk_data = {
|
|
2773
|
-
"id": response_chunk.id, "object": response_chunk.object, "created": response_chunk.created, "model": response_chunk.model,
|
|
2774
|
-
"choices": [{"index": choice.index, "delta": {"content": choice.delta.content, "role": choice.delta.role, "reasoning_content": reasoning_content if hasattr(choice.delta, "reasoning_content") else None}, "finish_reason": choice.finish_reason} for choice in response_chunk.choices]
|
|
2775
|
-
}
|
|
2776
|
-
yield f"data: {json.dumps(chunk_data)}\n\n"
|
|
2777
|
-
|
|
2778
|
-
except Exception as e:
|
|
2779
|
-
print(f"\nAn exception occurred during streaming for {current_stream_id}: {e}")
|
|
2780
|
-
traceback.print_exc()
|
|
2781
|
-
interrupted = True
|
|
2782
|
-
|
|
2783
|
-
finally:
|
|
2784
|
-
print(f"\nStream {current_stream_id} finished. Interrupted: {interrupted}")
|
|
2785
|
-
print('\r' + ' ' * dot_count*2 + '\r', end="", flush=True)
|
|
2786
|
-
|
|
2787
|
-
final_response_text = ''.join(complete_response)
|
|
2788
|
-
|
|
2789
|
-
conversation_turn_text = f"User: {commandstr}\nAssistant: {final_response_text}"
|
|
2790
|
-
|
|
2791
|
-
try:
|
|
2792
|
-
memory_examples = command_history.get_memory_examples_for_context(
|
|
2793
|
-
npc=npc_name,
|
|
2794
|
-
team=team,
|
|
2795
|
-
directory_path=current_path
|
|
2796
|
-
)
|
|
2797
|
-
|
|
2798
|
-
memory_context = format_memory_context(memory_examples)
|
|
2799
|
-
|
|
2800
|
-
facts = get_facts(
|
|
2801
|
-
conversation_turn_text,
|
|
2802
|
-
model=npc_object.model if npc_object else model,
|
|
2803
|
-
provider=npc_object.provider if npc_object else provider,
|
|
2804
|
-
npc=npc_object,
|
|
2805
|
-
context=memory_context
|
|
2806
|
-
)
|
|
2807
|
-
|
|
2808
|
-
if facts:
|
|
2809
|
-
memories_for_approval = []
|
|
2810
|
-
for i, fact in enumerate(facts):
|
|
2811
|
-
memory_id = command_history.add_memory_to_database(
|
|
2812
|
-
message_id=f"{conversation_id}_{datetime.now().strftime('%H%M%S')}_{i}",
|
|
2813
|
-
conversation_id=conversation_id,
|
|
2814
|
-
npc=npc_name or "default",
|
|
2815
|
-
team=team or "default",
|
|
2816
|
-
directory_path=current_path or "/",
|
|
2817
|
-
initial_memory=fact['statement'],
|
|
2818
|
-
status="pending_approval",
|
|
2819
|
-
model=npc_object.model if npc_object else model,
|
|
2820
|
-
provider=npc_object.provider if npc_object else provider
|
|
2821
|
-
)
|
|
2822
|
-
|
|
2823
|
-
memories_for_approval.append({
|
|
2824
|
-
"memory_id": memory_id,
|
|
2825
|
-
"content": fact['statement'],
|
|
2826
|
-
"context": f"Type: {fact.get('type', 'unknown')}, Source: {fact.get('source_text', '')}",
|
|
2827
|
-
"npc": npc_name or "default"
|
|
2828
|
-
})
|
|
2829
|
-
|
|
2830
|
-
memory_data = {
|
|
2831
|
-
"type": "memory_approval",
|
|
2832
|
-
"memories": memories_for_approval,
|
|
2833
|
-
"conversation_id": conversation_id
|
|
2834
|
-
}
|
|
2835
|
-
|
|
2836
|
-
except Exception as e:
|
|
2837
|
-
print(f"Memory generation error: {e}")
|
|
2838
|
-
|
|
2839
|
-
if memory_data:
|
|
2840
|
-
yield f"data: {json.dumps(memory_data)}\n\n"
|
|
2841
|
-
|
|
2842
|
-
yield f"data: {json.dumps({'type': 'message_stop'})}\n\n"
|
|
2843
|
-
|
|
2844
|
-
npc_name_to_save = npc_object.name if npc_object else ''
|
|
2845
|
-
save_conversation_message(
|
|
2846
|
-
command_history,
|
|
2847
|
-
conversation_id,
|
|
2848
|
-
"assistant",
|
|
2849
|
-
final_response_text,
|
|
2850
|
-
wd=current_path,
|
|
2851
|
-
model=model,
|
|
2852
|
-
provider=provider,
|
|
2853
|
-
npc=npc_name_to_save,
|
|
2854
|
-
team=team,
|
|
2855
|
-
message_id=message_id,
|
|
2856
|
-
)
|
|
2857
|
-
|
|
2858
|
-
with cancellation_lock:
|
|
2859
|
-
if current_stream_id in cancellation_flags:
|
|
2860
|
-
del cancellation_flags[current_stream_id]
|
|
2861
|
-
print(f"Cleaned up cancellation flag for stream ID: {current_stream_id}")
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
return Response(event_stream(stream_id), mimetype="text/event-stream")
|
|
2866
3525
|
|
|
2867
3526
|
@app.route("/api/interrupt", methods=["POST"])
|
|
2868
3527
|
def interrupt_stream():
|