npcpy 1.2.33__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 +257 -41
- npcpy/npc_compiler.py +102 -157
- npcpy/serve.py +1469 -739
- {npcpy-1.2.33.dist-info → npcpy-1.2.35.dist-info}/METADATA +1 -1
- {npcpy-1.2.33.dist-info → npcpy-1.2.35.dist-info}/RECORD +14 -13
- {npcpy-1.2.33.dist-info → npcpy-1.2.35.dist-info}/WHEEL +0 -0
- {npcpy-1.2.33.dist-info → npcpy-1.2.35.dist-info}/licenses/LICENSE +0 -0
- {npcpy-1.2.33.dist-info → npcpy-1.2.35.dist-info}/top_level.txt +0 -0
npcpy/serve.py
CHANGED
|
@@ -7,8 +7,10 @@ import uuid
|
|
|
7
7
|
import sys
|
|
8
8
|
import traceback
|
|
9
9
|
import glob
|
|
10
|
+
import re
|
|
11
|
+
import time
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
import io
|
|
12
14
|
from flask_cors import CORS
|
|
13
15
|
import os
|
|
14
16
|
import sqlite3
|
|
@@ -29,12 +31,27 @@ try:
|
|
|
29
31
|
import ollama
|
|
30
32
|
except:
|
|
31
33
|
pass
|
|
34
|
+
from jinja2 import Environment, FileSystemLoader, Template, Undefined, DictLoader
|
|
35
|
+
class SilentUndefined(Undefined):
|
|
36
|
+
def _fail_with_undefined_error(self, *args, **kwargs):
|
|
37
|
+
return ""
|
|
38
|
+
|
|
39
|
+
# Import ShellState and helper functions from npcsh
|
|
40
|
+
from npcsh._state import ShellState
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
from npcpy.memory.knowledge_graph import load_kg_from_db
|
|
44
|
+
from npcpy.memory.search import execute_rag_command, execute_brainblast_command
|
|
45
|
+
from npcpy.data.load import load_file_contents
|
|
46
|
+
from npcpy.data.web import search_web
|
|
47
|
+
|
|
48
|
+
from npcsh._state import get_relevant_memories, search_kg_facts
|
|
32
49
|
|
|
33
50
|
import base64
|
|
34
51
|
import shutil
|
|
35
52
|
import uuid
|
|
36
53
|
|
|
37
|
-
from npcpy.llm_funcs import gen_image
|
|
54
|
+
from npcpy.llm_funcs import gen_image, breathe
|
|
38
55
|
|
|
39
56
|
from sqlalchemy import create_engine, text
|
|
40
57
|
from sqlalchemy.orm import sessionmaker
|
|
@@ -69,6 +86,82 @@ cancellation_flags = {}
|
|
|
69
86
|
cancellation_lock = threading.Lock()
|
|
70
87
|
|
|
71
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
|
+
|
|
72
165
|
def get_project_npc_directory(current_path=None):
|
|
73
166
|
"""
|
|
74
167
|
Get the project NPC directory based on the current path
|
|
@@ -171,6 +264,34 @@ def get_db_session():
|
|
|
171
264
|
Session = sessionmaker(bind=engine)
|
|
172
265
|
return Session()
|
|
173
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
|
+
|
|
174
295
|
extension_map = {
|
|
175
296
|
"PNG": "images",
|
|
176
297
|
"JPG": "images",
|
|
@@ -426,8 +547,6 @@ def capture():
|
|
|
426
547
|
return None
|
|
427
548
|
|
|
428
549
|
return jsonify({"screenshot": screenshot})
|
|
429
|
-
|
|
430
|
-
|
|
431
550
|
@app.route("/api/settings/global", methods=["GET", "OPTIONS"])
|
|
432
551
|
def get_global_settings():
|
|
433
552
|
if request.method == "OPTIONS":
|
|
@@ -436,22 +555,22 @@ def get_global_settings():
|
|
|
436
555
|
try:
|
|
437
556
|
npcshrc_path = os.path.expanduser("~/.npcshrc")
|
|
438
557
|
|
|
439
|
-
|
|
440
558
|
global_settings = {
|
|
441
559
|
"model": "llama3.2",
|
|
442
560
|
"provider": "ollama",
|
|
443
561
|
"embedding_model": "nomic-embed-text",
|
|
444
562
|
"embedding_provider": "ollama",
|
|
445
563
|
"search_provider": "perplexity",
|
|
446
|
-
"NPC_STUDIO_LICENSE_KEY": "",
|
|
447
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
|
|
448
568
|
}
|
|
449
569
|
global_vars = {}
|
|
450
570
|
|
|
451
571
|
if os.path.exists(npcshrc_path):
|
|
452
572
|
with open(npcshrc_path, "r") as f:
|
|
453
573
|
for line in f:
|
|
454
|
-
|
|
455
574
|
line = line.split("#")[0].strip()
|
|
456
575
|
if not line:
|
|
457
576
|
continue
|
|
@@ -459,33 +578,35 @@ def get_global_settings():
|
|
|
459
578
|
if "=" not in line:
|
|
460
579
|
continue
|
|
461
580
|
|
|
462
|
-
|
|
463
581
|
key, value = line.split("=", 1)
|
|
464
582
|
key = key.strip()
|
|
465
583
|
if key.startswith("export "):
|
|
466
584
|
key = key[7:]
|
|
467
585
|
|
|
468
|
-
|
|
469
586
|
value = value.strip()
|
|
470
587
|
if value.startswith('"') and value.endswith('"'):
|
|
471
588
|
value = value[1:-1]
|
|
472
589
|
elif value.startswith("'") and value.endswith("'"):
|
|
473
590
|
value = value[1:-1]
|
|
474
591
|
|
|
475
|
-
|
|
476
592
|
key_mapping = {
|
|
477
593
|
"NPCSH_MODEL": "model",
|
|
478
594
|
"NPCSH_PROVIDER": "provider",
|
|
479
595
|
"NPCSH_EMBEDDING_MODEL": "embedding_model",
|
|
480
596
|
"NPCSH_EMBEDDING_PROVIDER": "embedding_provider",
|
|
481
597
|
"NPCSH_SEARCH_PROVIDER": "search_provider",
|
|
482
|
-
"NPC_STUDIO_LICENSE_KEY": "NPC_STUDIO_LICENSE_KEY",
|
|
483
598
|
"NPCSH_STREAM_OUTPUT": "NPCSH_STREAM_OUTPUT",
|
|
484
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
|
|
485
603
|
}
|
|
486
604
|
|
|
487
605
|
if key in key_mapping:
|
|
488
|
-
|
|
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
|
|
489
610
|
else:
|
|
490
611
|
global_vars[key] = value
|
|
491
612
|
|
|
@@ -502,6 +623,7 @@ def get_global_settings():
|
|
|
502
623
|
except Exception as e:
|
|
503
624
|
print(f"Error in get_global_settings: {str(e)}")
|
|
504
625
|
return jsonify({"error": str(e)}), 500
|
|
626
|
+
|
|
505
627
|
def _get_jinx_files_recursively(directory):
|
|
506
628
|
"""Helper to recursively find all .jinx file paths."""
|
|
507
629
|
jinx_paths = []
|
|
@@ -535,46 +657,12 @@ def get_available_jinxs():
|
|
|
535
657
|
traceback.print_exc()
|
|
536
658
|
return jsonify({'jinxs': [], 'error': str(e)}), 500
|
|
537
659
|
|
|
538
|
-
@app.route("/api/jinxs/global", methods=["GET"])
|
|
539
|
-
def get_global_jinxs():
|
|
540
|
-
jinxs_dir = os.path.join(os.path.expanduser("~"), ".npcsh", "npc_team", "jinxs")
|
|
541
|
-
jinx_paths = _get_jinx_files_recursively(jinxs_dir)
|
|
542
|
-
jinxs = []
|
|
543
|
-
for path in jinx_paths:
|
|
544
|
-
try:
|
|
545
|
-
with open(path, "r") as f:
|
|
546
|
-
jinx_data = yaml.safe_load(f)
|
|
547
|
-
jinxs.append(jinx_data)
|
|
548
|
-
except Exception as e:
|
|
549
|
-
print(f"Error loading global jinx {path}: {e}")
|
|
550
|
-
return jsonify({"jinxs": jinxs})
|
|
551
|
-
|
|
552
|
-
@app.route("/api/jinxs/project", methods=["GET"])
|
|
553
|
-
def get_project_jinxs():
|
|
554
|
-
current_path = request.args.get("currentPath")
|
|
555
|
-
if not current_path:
|
|
556
|
-
return jsonify({"jinxs": []})
|
|
557
|
-
|
|
558
|
-
if not current_path.endswith("npc_team"):
|
|
559
|
-
current_path = os.path.join(current_path, "npc_team")
|
|
560
|
-
|
|
561
|
-
jinxs_dir = os.path.join(current_path, "jinxs")
|
|
562
|
-
jinx_paths = _get_jinx_files_recursively(jinxs_dir)
|
|
563
|
-
jinxs = []
|
|
564
|
-
for path in jinx_paths:
|
|
565
|
-
try:
|
|
566
|
-
with open(path, "r") as f:
|
|
567
|
-
jinx_data = yaml.safe_load(f)
|
|
568
|
-
jinxs.append(jinx_data)
|
|
569
|
-
except Exception as e:
|
|
570
|
-
print(f"Error loading project jinx {path}: {e}")
|
|
571
|
-
return jsonify({"jinxs": jinxs})
|
|
572
660
|
|
|
573
661
|
@app.route("/api/jinx/execute", methods=["POST"])
|
|
574
662
|
def execute_jinx():
|
|
575
663
|
"""
|
|
576
664
|
Execute a specific jinx with provided arguments.
|
|
577
|
-
|
|
665
|
+
Returns the output as a JSON response.
|
|
578
666
|
"""
|
|
579
667
|
data = request.json
|
|
580
668
|
|
|
@@ -585,19 +673,18 @@ def execute_jinx():
|
|
|
585
673
|
with cancellation_lock:
|
|
586
674
|
cancellation_flags[stream_id] = False
|
|
587
675
|
|
|
588
|
-
print(f"--- Jinx Execution Request for streamId: {stream_id} ---")
|
|
589
|
-
print(f"Request Data: {json.dumps(data, indent=2)}")
|
|
676
|
+
print(f"--- Jinx Execution Request for streamId: {stream_id} ---", file=sys.stderr)
|
|
677
|
+
print(f"Request Data: {json.dumps(data, indent=2)}", file=sys.stderr)
|
|
590
678
|
|
|
591
679
|
jinx_name = data.get("jinxName")
|
|
592
680
|
jinx_args = data.get("jinxArgs", [])
|
|
593
|
-
print(f"Jinx Name: {jinx_name}, Jinx Args: {jinx_args}")
|
|
681
|
+
print(f"Jinx Name: {jinx_name}, Jinx Args: {jinx_args}", file=sys.stderr)
|
|
594
682
|
conversation_id = data.get("conversationId")
|
|
595
683
|
model = data.get("model")
|
|
596
684
|
provider = data.get("provider")
|
|
597
685
|
|
|
598
|
-
# --- IMPORTANT: Ensure conversation_id is present for context persistence ---
|
|
599
686
|
if not conversation_id:
|
|
600
|
-
print("ERROR: conversationId is required for Jinx execution with persistent variables")
|
|
687
|
+
print("ERROR: conversationId is required for Jinx execution with persistent variables", file=sys.stderr)
|
|
601
688
|
return jsonify({"error": "conversationId is required for Jinx execution with persistent variables"}), 400
|
|
602
689
|
|
|
603
690
|
npc_name = data.get("npc")
|
|
@@ -605,223 +692,193 @@ def execute_jinx():
|
|
|
605
692
|
current_path = data.get("currentPath")
|
|
606
693
|
|
|
607
694
|
if not jinx_name:
|
|
608
|
-
print("ERROR: jinxName is required")
|
|
695
|
+
print("ERROR: jinxName is required", file=sys.stderr)
|
|
609
696
|
return jsonify({"error": "jinxName is required"}), 400
|
|
610
697
|
|
|
611
|
-
# Load project environment if applicable
|
|
612
698
|
if current_path:
|
|
613
699
|
load_project_env(current_path)
|
|
614
700
|
|
|
615
|
-
|
|
616
|
-
|
|
701
|
+
jinx = None
|
|
702
|
+
|
|
617
703
|
if npc_name:
|
|
618
704
|
db_conn = get_db_connection()
|
|
619
705
|
npc_object = load_npc_by_name_and_source(npc_name, npc_source, db_conn, current_path)
|
|
620
706
|
if not npc_object and npc_source == 'project':
|
|
621
707
|
npc_object = load_npc_by_name_and_source(npc_name, 'global', db_conn)
|
|
708
|
+
else:
|
|
709
|
+
npc_object = None
|
|
622
710
|
|
|
623
|
-
# Try to find the jinx
|
|
624
|
-
jinx = None
|
|
625
|
-
|
|
626
|
-
# Check NPC's jinxs
|
|
627
711
|
if npc_object and hasattr(npc_object, 'jinxs_dict') and jinx_name in npc_object.jinxs_dict:
|
|
628
712
|
jinx = npc_object.jinxs_dict[jinx_name]
|
|
713
|
+
print(f"Found jinx in NPC's jinxs_dict", file=sys.stderr)
|
|
629
714
|
|
|
630
|
-
# Check team jinxs
|
|
631
715
|
if not jinx and current_path:
|
|
632
|
-
|
|
633
|
-
if os.path.exists(
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
716
|
+
project_jinxs_base = os.path.join(current_path, 'npc_team', 'jinxs')
|
|
717
|
+
if os.path.exists(project_jinxs_base):
|
|
718
|
+
for root, dirs, files in os.walk(project_jinxs_base):
|
|
719
|
+
if f'{jinx_name}.jinx' in files:
|
|
720
|
+
project_jinx_path = os.path.join(root, f'{jinx_name}.jinx')
|
|
721
|
+
jinx = Jinx(jinx_path=project_jinx_path)
|
|
722
|
+
print(f"Found jinx at: {project_jinx_path}", file=sys.stderr)
|
|
723
|
+
break
|
|
724
|
+
|
|
637
725
|
if not jinx:
|
|
638
|
-
|
|
639
|
-
if os.path.exists(
|
|
640
|
-
|
|
726
|
+
global_jinxs_base = os.path.expanduser('~/.npcsh/npc_team/jinxs')
|
|
727
|
+
if os.path.exists(global_jinxs_base):
|
|
728
|
+
for root, dirs, files in os.walk(global_jinxs_base):
|
|
729
|
+
if f'{jinx_name}.jinx' in files:
|
|
730
|
+
global_jinx_path = os.path.join(root, f'{jinx_name}.jinx')
|
|
731
|
+
jinx = Jinx(jinx_path=global_jinx_path)
|
|
732
|
+
print(f"Found jinx at: {global_jinx_path}", file=sys.stderr)
|
|
733
|
+
|
|
734
|
+
# Initialize jinx steps by calling render_first_pass
|
|
735
|
+
from jinja2 import Environment
|
|
736
|
+
temp_env = Environment()
|
|
737
|
+
jinx.render_first_pass(temp_env, {})
|
|
738
|
+
|
|
739
|
+
break
|
|
641
740
|
|
|
642
741
|
if not jinx:
|
|
643
|
-
print(f"ERROR: Jinx '{jinx_name}' not found")
|
|
742
|
+
print(f"ERROR: Jinx '{jinx_name}' not found", file=sys.stderr)
|
|
743
|
+
searched_paths = []
|
|
744
|
+
if npc_object:
|
|
745
|
+
searched_paths.append(f"NPC {npc_name} jinxs_dict")
|
|
746
|
+
if current_path:
|
|
747
|
+
searched_paths.append(f"Project jinxs at {os.path.join(current_path, 'npc_team', 'jinxs')}")
|
|
748
|
+
searched_paths.append(f"Global jinxs at {os.path.expanduser('~/.npcsh/npc_team/jinxs')}")
|
|
749
|
+
print(f"Searched in: {', '.join(searched_paths)}", file=sys.stderr)
|
|
644
750
|
return jsonify({"error": f"Jinx '{jinx_name}' not found"}), 404
|
|
645
751
|
|
|
646
|
-
# Extract inputs from args
|
|
647
752
|
from npcpy.npc_compiler import extract_jinx_inputs
|
|
648
753
|
|
|
649
|
-
# Re-assemble arguments that were incorrectly split by spaces.
|
|
650
754
|
fixed_args = []
|
|
651
755
|
i = 0
|
|
652
|
-
|
|
653
|
-
|
|
756
|
+
|
|
757
|
+
# Filter out None values from jinx_args before processing
|
|
758
|
+
cleaned_jinx_args = [arg for arg in jinx_args if arg is not None]
|
|
759
|
+
|
|
760
|
+
while i < len(cleaned_jinx_args):
|
|
761
|
+
arg = cleaned_jinx_args[i]
|
|
654
762
|
if arg.startswith('-'):
|
|
655
763
|
fixed_args.append(arg)
|
|
656
764
|
value_parts = []
|
|
657
765
|
i += 1
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
value_parts.append(jinx_args[i])
|
|
766
|
+
while i < len(cleaned_jinx_args) and not cleaned_jinx_args[i].startswith('-'):
|
|
767
|
+
value_parts.append(cleaned_jinx_args[i])
|
|
661
768
|
i += 1
|
|
662
769
|
|
|
663
770
|
if value_parts:
|
|
664
|
-
# Join the parts back into a single string.
|
|
665
771
|
full_value = " ".join(value_parts)
|
|
666
|
-
# Clean up the extraneous quotes that the initial bad split left behind.
|
|
667
772
|
if full_value.startswith("'") and full_value.endswith("'"):
|
|
668
773
|
full_value = full_value[1:-1]
|
|
669
774
|
elif full_value.startswith('"') and full_value.endswith('"'):
|
|
670
775
|
full_value = full_value[1:-1]
|
|
671
776
|
fixed_args.append(full_value)
|
|
672
|
-
# The 'i' counter is already advanced, so the loop continues from the next flag.
|
|
673
777
|
else:
|
|
674
|
-
# This handles positional arguments, just in case.
|
|
675
778
|
fixed_args.append(arg)
|
|
676
779
|
i += 1
|
|
677
780
|
|
|
678
|
-
# Now, use the corrected arguments to extract inputs.
|
|
679
781
|
input_values = extract_jinx_inputs(fixed_args, jinx)
|
|
680
782
|
|
|
681
|
-
print(f'Executing jinx with input_values: {input_values}')
|
|
682
|
-
|
|
783
|
+
print(f'Executing jinx with input_values: {input_values}', file=sys.stderr)
|
|
784
|
+
|
|
683
785
|
command_history = CommandHistory(app.config.get('DB_PATH'))
|
|
684
786
|
messages = fetch_messages_for_conversation(conversation_id)
|
|
685
787
|
|
|
686
|
-
# Prepare jinxs_dict for execution
|
|
687
788
|
all_jinxs = {}
|
|
688
789
|
if npc_object and hasattr(npc_object, 'jinxs_dict'):
|
|
689
790
|
all_jinxs.update(npc_object.jinxs_dict)
|
|
690
791
|
|
|
691
|
-
# --- IMPORTANT: Retrieve or initialize the persistent Jinx context for this conversation ---
|
|
692
792
|
if conversation_id not in app.jinx_conversation_contexts:
|
|
693
793
|
app.jinx_conversation_contexts[conversation_id] = {}
|
|
694
794
|
jinx_local_context = app.jinx_conversation_contexts[conversation_id]
|
|
695
795
|
|
|
696
|
-
print(f"--- CONTEXT STATE (conversationId: {conversation_id}) ---")
|
|
697
|
-
print(f"jinx_local_context BEFORE Jinx execution: {jinx_local_context}")
|
|
796
|
+
print(f"--- CONTEXT STATE (conversationId: {conversation_id}) ---", file=sys.stderr)
|
|
797
|
+
print(f"jinx_local_context BEFORE Jinx execution: {jinx_local_context}", file=sys.stderr)
|
|
698
798
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
# Get output (this still comes from the 'output' key in the result)
|
|
728
|
-
output = result.get('output', str(result))
|
|
729
|
-
messages_updated = result.get('messages', messages)
|
|
799
|
+
|
|
800
|
+
# Create state object
|
|
801
|
+
state = ShellState(
|
|
802
|
+
npc=npc_object,
|
|
803
|
+
team=None,
|
|
804
|
+
conversation_id=conversation_id,
|
|
805
|
+
chat_model=model or os.getenv('NPCSH_CHAT_MODEL', 'gemma3:4b'),
|
|
806
|
+
chat_provider=provider or os.getenv('NPCSH_CHAT_PROVIDER', 'ollama'),
|
|
807
|
+
current_path=current_path or os.getcwd(),
|
|
808
|
+
search_provider=os.getenv('NPCSH_SEARCH_PROVIDER', 'duckduckgo'),
|
|
809
|
+
embedding_model=os.getenv('NPCSH_EMBEDDING_MODEL', 'nomic-embed-text'),
|
|
810
|
+
embedding_provider=os.getenv('NPCSH_EMBEDDING_PROVIDER', 'ollama'),
|
|
811
|
+
)
|
|
812
|
+
|
|
813
|
+
# Build extra_globals with state and all necessary functions
|
|
814
|
+
extra_globals_for_jinx = {
|
|
815
|
+
**jinx_local_context,
|
|
816
|
+
'state': state,
|
|
817
|
+
'CommandHistory': CommandHistory,
|
|
818
|
+
'load_kg_from_db': load_kg_from_db,
|
|
819
|
+
'execute_rag_command': execute_rag_command,
|
|
820
|
+
'execute_brainblast_command': execute_brainblast_command,
|
|
821
|
+
'load_file_contents': load_file_contents,
|
|
822
|
+
'search_web': search_web,
|
|
823
|
+
'get_relevant_memories': get_relevant_memories,
|
|
824
|
+
'search_kg_facts': search_kg_facts,
|
|
825
|
+
}
|
|
730
826
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
# Stream the output in chunks for consistent UI experience
|
|
741
|
-
if isinstance(output, str):
|
|
742
|
-
chunk_size = 50 # Characters per chunk
|
|
743
|
-
for i in range(0, len(output), chunk_size):
|
|
744
|
-
chunk = output[i:i + chunk_size]
|
|
745
|
-
chunk_data = {
|
|
746
|
-
"id": None,
|
|
747
|
-
"object": None,
|
|
748
|
-
"created": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
|
749
|
-
"model": model,
|
|
750
|
-
"choices": [{
|
|
751
|
-
"index": 0,
|
|
752
|
-
"delta": {
|
|
753
|
-
"content": chunk,
|
|
754
|
-
"role": "assistant"
|
|
755
|
-
},
|
|
756
|
-
"finish_reason": None
|
|
757
|
-
}]
|
|
758
|
-
}
|
|
759
|
-
yield f"data: {json.dumps(chunk_data)}\n\n"
|
|
760
|
-
else:
|
|
761
|
-
# Non-string output, send as single chunk
|
|
762
|
-
chunk_data = {
|
|
763
|
-
"id": None,
|
|
764
|
-
"object": None,
|
|
765
|
-
"created": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
|
766
|
-
"model": model,
|
|
767
|
-
"choices": [{
|
|
768
|
-
"index": 0,
|
|
769
|
-
"delta": {
|
|
770
|
-
"content": str(output),
|
|
771
|
-
"role": "assistant"
|
|
772
|
-
},
|
|
773
|
-
"finish_reason": None
|
|
774
|
-
}]
|
|
775
|
-
}
|
|
776
|
-
yield f"data: {json.dumps(chunk_data)}\n\n"
|
|
777
|
-
|
|
778
|
-
# Send completion message
|
|
779
|
-
yield f"data: {json.dumps({'type': 'message_stop'})}\n\n"
|
|
780
|
-
|
|
781
|
-
# Save to conversation history
|
|
782
|
-
message_id = generate_message_id()
|
|
783
|
-
save_conversation_message(
|
|
784
|
-
command_history,
|
|
785
|
-
conversation_id,
|
|
786
|
-
"user",
|
|
787
|
-
f"/{jinx_name} {' '.join(jinx_args)}",
|
|
788
|
-
wd=current_path,
|
|
789
|
-
model=model,
|
|
790
|
-
provider=provider,
|
|
791
|
-
npc=npc_name,
|
|
792
|
-
message_id=message_id
|
|
793
|
-
)
|
|
794
|
-
|
|
795
|
-
message_id = generate_message_id()
|
|
796
|
-
save_conversation_message(
|
|
797
|
-
command_history,
|
|
798
|
-
conversation_id,
|
|
799
|
-
"assistant",
|
|
800
|
-
str(output),
|
|
801
|
-
wd=current_path,
|
|
802
|
-
model=model,
|
|
803
|
-
provider=provider,
|
|
804
|
-
npc=npc_name,
|
|
805
|
-
message_id=message_id
|
|
806
|
-
)
|
|
807
|
-
|
|
808
|
-
except Exception as e:
|
|
809
|
-
print(f"ERROR: Exception during jinx execution {jinx_name}: {str(e)}")
|
|
810
|
-
traceback.print_exc()
|
|
811
|
-
error_data = {
|
|
812
|
-
"type": "error",
|
|
813
|
-
"error": str(e)
|
|
814
|
-
}
|
|
815
|
-
yield f"data: {json.dumps(error_data)}\n\n"
|
|
816
|
-
|
|
817
|
-
finally:
|
|
818
|
-
with cancellation_lock:
|
|
819
|
-
if current_stream_id in cancellation_flags:
|
|
820
|
-
del cancellation_flags[current_stream_id]
|
|
821
|
-
print(f"--- Jinx Execution Finished for streamId: {stream_id} ---")
|
|
827
|
+
jinx_execution_result = jinx.execute(
|
|
828
|
+
input_values=input_values,
|
|
829
|
+
jinja_env=npc_object.jinja_env if npc_object else None,
|
|
830
|
+
npc=npc_object,
|
|
831
|
+
messages=messages,
|
|
832
|
+
extra_globals=extra_globals_for_jinx
|
|
833
|
+
)
|
|
834
|
+
|
|
835
|
+
output_from_jinx_result = jinx_execution_result.get('output')
|
|
822
836
|
|
|
823
|
-
|
|
837
|
+
final_output_string = str(output_from_jinx_result) if output_from_jinx_result is not None else ""
|
|
838
|
+
|
|
839
|
+
if isinstance(jinx_execution_result, dict):
|
|
840
|
+
for key, value in jinx_execution_result.items():
|
|
841
|
+
jinx_local_context[key] = value
|
|
842
|
+
|
|
843
|
+
print(f"jinx_local_context AFTER Jinx execution (final state): {jinx_local_context}", file=sys.stderr)
|
|
844
|
+
print(f"Jinx execution result output: {output_from_jinx_result}", file=sys.stderr)
|
|
845
|
+
|
|
846
|
+
user_message_id = generate_message_id()
|
|
847
|
+
|
|
848
|
+
# Use cleaned_jinx_args for logging the user message
|
|
849
|
+
user_command_log = f"/{jinx_name} {' '.join(cleaned_jinx_args)}"
|
|
850
|
+
save_conversation_message(
|
|
851
|
+
command_history,
|
|
852
|
+
conversation_id,
|
|
853
|
+
"user",
|
|
854
|
+
user_command_log,
|
|
855
|
+
wd=current_path,
|
|
856
|
+
model=model,
|
|
857
|
+
provider=provider,
|
|
858
|
+
npc=npc_name,
|
|
859
|
+
message_id=user_message_id
|
|
860
|
+
)
|
|
861
|
+
|
|
862
|
+
assistant_message_id = generate_message_id()
|
|
863
|
+
save_conversation_message(
|
|
864
|
+
command_history,
|
|
865
|
+
conversation_id,
|
|
866
|
+
"assistant",
|
|
867
|
+
final_output_string,
|
|
868
|
+
wd=current_path,
|
|
869
|
+
model=model,
|
|
870
|
+
provider=provider,
|
|
871
|
+
npc=npc_name,
|
|
872
|
+
message_id=assistant_message_id
|
|
873
|
+
)
|
|
824
874
|
|
|
875
|
+
# Determine mimetype based on content
|
|
876
|
+
is_html = bool(re.search(r'<[a-z][\s\S]*>', final_output_string, re.IGNORECASE))
|
|
877
|
+
|
|
878
|
+
if is_html:
|
|
879
|
+
return Response(final_output_string, mimetype="text/html")
|
|
880
|
+
else:
|
|
881
|
+
return Response(final_output_string, mimetype="text/plain")
|
|
825
882
|
@app.route("/api/settings/global", methods=["POST", "OPTIONS"])
|
|
826
883
|
def save_global_settings():
|
|
827
884
|
if request.method == "OPTIONS":
|
|
@@ -837,35 +894,41 @@ def save_global_settings():
|
|
|
837
894
|
"embedding_model": "NPCSH_EMBEDDING_MODEL",
|
|
838
895
|
"embedding_provider": "NPCSH_EMBEDDING_PROVIDER",
|
|
839
896
|
"search_provider": "NPCSH_SEARCH_PROVIDER",
|
|
840
|
-
"NPC_STUDIO_LICENSE_KEY": "NPC_STUDIO_LICENSE_KEY",
|
|
841
897
|
"NPCSH_STREAM_OUTPUT": "NPCSH_STREAM_OUTPUT",
|
|
842
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
|
|
843
902
|
}
|
|
844
903
|
|
|
845
904
|
os.makedirs(os.path.dirname(npcshrc_path), exist_ok=True)
|
|
846
905
|
print(data)
|
|
847
906
|
with open(npcshrc_path, "w") as f:
|
|
848
|
-
|
|
907
|
+
|
|
849
908
|
for key, value in data.get("global_settings", {}).items():
|
|
850
|
-
if key in key_mapping and value:
|
|
851
|
-
|
|
852
|
-
if
|
|
853
|
-
|
|
854
|
-
|
|
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")
|
|
855
918
|
|
|
856
|
-
|
|
857
919
|
for key, value in data.get("global_vars", {}).items():
|
|
858
|
-
if key and value:
|
|
920
|
+
if key and value is not None: # Check for None explicitly
|
|
859
921
|
if " " in str(value):
|
|
860
|
-
|
|
861
|
-
|
|
922
|
+
value_to_write = f'"{value}"'
|
|
923
|
+
else:
|
|
924
|
+
value_to_write = str(value)
|
|
925
|
+
f.write(f"export {key}={value_to_write}\n")
|
|
862
926
|
|
|
863
927
|
return jsonify({"message": "Global settings saved successfully", "error": None})
|
|
864
928
|
|
|
865
929
|
except Exception as e:
|
|
866
930
|
print(f"Error in save_global_settings: {str(e)}")
|
|
867
931
|
return jsonify({"error": str(e)}), 500
|
|
868
|
-
|
|
869
932
|
@app.route("/api/settings/project", methods=["GET", "OPTIONS"])
|
|
870
933
|
def get_project_settings():
|
|
871
934
|
if request.method == "OPTIONS":
|
|
@@ -1009,52 +1072,6 @@ def api_command(command):
|
|
|
1009
1072
|
return jsonify(result)
|
|
1010
1073
|
except Exception as e:
|
|
1011
1074
|
return jsonify({"error": str(e)})
|
|
1012
|
-
@app.route("/api/npc_team_global")
|
|
1013
|
-
def get_npc_team_global():
|
|
1014
|
-
try:
|
|
1015
|
-
db_conn = get_db_connection()
|
|
1016
|
-
global_npc_directory = os.path.expanduser("~/.npcsh/npc_team")
|
|
1017
|
-
|
|
1018
|
-
npc_data = []
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
for file in os.listdir(global_npc_directory):
|
|
1022
|
-
if file.endswith(".npc"):
|
|
1023
|
-
npc_path = os.path.join(global_npc_directory, file)
|
|
1024
|
-
npc = NPC(file=npc_path, db_conn=db_conn)
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
serialized_npc = {
|
|
1028
|
-
"name": npc.name,
|
|
1029
|
-
"primary_directive": npc.primary_directive,
|
|
1030
|
-
"model": npc.model,
|
|
1031
|
-
"provider": npc.provider,
|
|
1032
|
-
"api_url": npc.api_url,
|
|
1033
|
-
"use_global_jinxs": npc.use_global_jinxs,
|
|
1034
|
-
"jinxs": [
|
|
1035
|
-
{
|
|
1036
|
-
"jinx_name": jinx.jinx_name,
|
|
1037
|
-
"inputs": jinx.inputs,
|
|
1038
|
-
"steps": [
|
|
1039
|
-
{
|
|
1040
|
-
"name": step.get("name", f"step_{i}"),
|
|
1041
|
-
"engine": step.get("engine", "natural"),
|
|
1042
|
-
"code": step.get("code", "")
|
|
1043
|
-
}
|
|
1044
|
-
for i, step in enumerate(jinx.steps)
|
|
1045
|
-
]
|
|
1046
|
-
}
|
|
1047
|
-
for jinx in npc.jinxs
|
|
1048
|
-
],
|
|
1049
|
-
}
|
|
1050
|
-
npc_data.append(serialized_npc)
|
|
1051
|
-
|
|
1052
|
-
return jsonify({"npcs": npc_data, "error": None})
|
|
1053
|
-
|
|
1054
|
-
except Exception as e:
|
|
1055
|
-
print(f"Error loading global NPCs: {str(e)}")
|
|
1056
|
-
return jsonify({"npcs": [], "error": str(e)})
|
|
1057
|
-
|
|
1058
1075
|
|
|
1059
1076
|
@app.route("/api/jinxs/save", methods=["POST"])
|
|
1060
1077
|
def save_jinx():
|
|
@@ -1093,101 +1110,727 @@ def save_jinx():
|
|
|
1093
1110
|
return jsonify({"status": "success"})
|
|
1094
1111
|
except Exception as e:
|
|
1095
1112
|
return jsonify({"error": str(e)}), 500
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
current_path = data.get("currentPath")
|
|
1105
|
-
|
|
1106
|
-
if not npc_data or "name" not in npc_data:
|
|
1107
|
-
return jsonify({"error": "Invalid NPC data"}), 400
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
if is_global:
|
|
1111
|
-
npc_directory = os.path.expanduser("~/.npcsh/npc_team")
|
|
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)
|
|
1112
1121
|
else:
|
|
1113
|
-
|
|
1122
|
+
result.append(str(inp))
|
|
1123
|
+
return result
|
|
1114
1124
|
|
|
1115
|
-
|
|
1116
|
-
|
|
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
|
+
)
|
|
1117
1158
|
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
provider: {npc_data['provider']}
|
|
1123
|
-
api_url: {npc_data.get('api_url', '')}
|
|
1124
|
-
use_global_jinxs: {str(npc_data.get('use_global_jinxs', True)).lower()}
|
|
1125
|
-
"""
|
|
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
|
|
1126
1163
|
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
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}"
|
|
1131
1180
|
|
|
1132
|
-
|
|
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.
|
|
1133
1185
|
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
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
|
+
)
|
|
1137
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
|
|
1138
1208
|
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
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
|
+
)
|
|
1143
1213
|
|
|
1144
|
-
|
|
1145
|
-
if not project_npc_directory.endswith("npc_team"):
|
|
1146
|
-
project_npc_directory = os.path.join(project_npc_directory, "npc_team")
|
|
1214
|
+
from collections import defaultdict # ADD THIS LINE for collecting links if not already present
|
|
1147
1215
|
|
|
1148
|
-
|
|
1216
|
+
finetune_jobs = {}
|
|
1149
1217
|
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
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
|
|
1155
1343
|
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
"model": npc.model,
|
|
1161
|
-
"provider": npc.provider,
|
|
1162
|
-
"api_url": npc.api_url,
|
|
1163
|
-
"use_global_jinxs": npc.use_global_jinxs,
|
|
1164
|
-
"jinxs": [
|
|
1165
|
-
{
|
|
1166
|
-
"jinx_name": jinx.jinx_name,
|
|
1167
|
-
"inputs": jinx.inputs,
|
|
1168
|
-
"steps": [
|
|
1169
|
-
{
|
|
1170
|
-
"name": step.get("name", f"step_{i}"),
|
|
1171
|
-
"engine": step.get("engine", "natural"),
|
|
1172
|
-
"code": step.get("code", "")
|
|
1173
|
-
}
|
|
1174
|
-
for i, step in enumerate(jinx.steps)
|
|
1175
|
-
]
|
|
1176
|
-
}
|
|
1177
|
-
for jinx in npc.jinxs
|
|
1178
|
-
],
|
|
1179
|
-
}
|
|
1180
|
-
npc_data.append(serialized_npc)
|
|
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
|
|
1181
1348
|
|
|
1182
|
-
print(
|
|
1183
|
-
|
|
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
|
|
1184
1370
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
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})
|
|
1630
|
+
|
|
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
|
+
})
|
|
1649
|
+
@app.route("/api/save_npc", methods=["POST"])
|
|
1650
|
+
def save_npc():
|
|
1651
|
+
try:
|
|
1652
|
+
data = request.json
|
|
1653
|
+
npc_data = data.get("npc")
|
|
1654
|
+
is_global = data.get("isGlobal")
|
|
1655
|
+
current_path = data.get("currentPath")
|
|
1656
|
+
|
|
1657
|
+
if not npc_data or "name" not in npc_data:
|
|
1658
|
+
return jsonify({"error": "Invalid NPC data"}), 400
|
|
1659
|
+
|
|
1660
|
+
|
|
1661
|
+
if is_global:
|
|
1662
|
+
npc_directory = os.path.expanduser("~/.npcsh/npc_team")
|
|
1663
|
+
else:
|
|
1664
|
+
npc_directory = os.path.join(current_path, "npc_team")
|
|
1665
|
+
|
|
1666
|
+
|
|
1667
|
+
os.makedirs(npc_directory, exist_ok=True)
|
|
1668
|
+
|
|
1669
|
+
|
|
1670
|
+
yaml_content = f"""name: {npc_data['name']}
|
|
1671
|
+
primary_directive: "{npc_data['primary_directive']}"
|
|
1672
|
+
model: {npc_data['model']}
|
|
1673
|
+
provider: {npc_data['provider']}
|
|
1674
|
+
api_url: {npc_data.get('api_url', '')}
|
|
1675
|
+
use_global_jinxs: {str(npc_data.get('use_global_jinxs', True)).lower()}
|
|
1676
|
+
"""
|
|
1677
|
+
|
|
1678
|
+
|
|
1679
|
+
file_path = os.path.join(npc_directory, f"{npc_data['name']}.npc")
|
|
1680
|
+
with open(file_path, "w") as f:
|
|
1681
|
+
f.write(yaml_content)
|
|
1682
|
+
|
|
1683
|
+
return jsonify({"message": "NPC saved successfully", "error": None})
|
|
1684
|
+
|
|
1685
|
+
except Exception as e:
|
|
1686
|
+
print(f"Error saving NPC: {str(e)}")
|
|
1687
|
+
return jsonify({"error": str(e)}), 500
|
|
1688
|
+
|
|
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 = []
|
|
1693
|
+
|
|
1694
|
+
if not os.path.exists(global_jinx_directory):
|
|
1695
|
+
return jsonify({"jinxs": [], "error": None})
|
|
1696
|
+
|
|
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
|
+
})
|
|
1723
|
+
|
|
1724
|
+
return jsonify({"jinxs": jinx_data, "error": None})
|
|
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
|
|
1731
|
+
|
|
1732
|
+
if not project_dir.endswith("jinxs"):
|
|
1733
|
+
project_dir = os.path.join(project_dir, "jinxs")
|
|
1734
|
+
|
|
1735
|
+
jinx_data = []
|
|
1736
|
+
if not os.path.exists(project_dir):
|
|
1737
|
+
return jsonify({"jinxs": [], "error": None})
|
|
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})
|
|
1767
|
+
|
|
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 = []
|
|
1772
|
+
|
|
1773
|
+
if not os.path.exists(global_npc_directory):
|
|
1774
|
+
return jsonify({"npcs": [], "error": None})
|
|
1775
|
+
|
|
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
|
+
})
|
|
1791
|
+
|
|
1792
|
+
return jsonify({"npcs": npc_data, "error": None})
|
|
1793
|
+
|
|
1794
|
+
|
|
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
|
+
)
|
|
1806
|
+
|
|
1807
|
+
npc_data = []
|
|
1808
|
+
|
|
1809
|
+
if not os.path.exists(project_npc_directory):
|
|
1810
|
+
return jsonify({"npcs": [], "error": None})
|
|
1811
|
+
|
|
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)
|
|
1828
|
+
|
|
1829
|
+
return jsonify({"npcs": npc_data, "error": None})
|
|
1830
|
+
|
|
1831
|
+
def get_last_used_model_and_npc_in_directory(directory_path):
|
|
1832
|
+
"""
|
|
1833
|
+
Fetches the model and NPC from the most recent message in any conversation
|
|
1191
1834
|
within the given directory.
|
|
1192
1835
|
"""
|
|
1193
1836
|
engine = get_db_connection()
|
|
@@ -1503,11 +2146,62 @@ IMAGE_MODELS = {
|
|
|
1503
2146
|
{"value": "runwayml/stable-diffusion-v1-5", "display_name": "Stable Diffusion v1.5"},
|
|
1504
2147
|
],
|
|
1505
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
|
|
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
|
|
1506
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
|
|
1507
2201
|
def get_available_image_models(current_path=None):
|
|
1508
2202
|
"""
|
|
1509
2203
|
Retrieves available image generation models based on environment variables
|
|
1510
|
-
and predefined configurations.
|
|
2204
|
+
and predefined configurations, including locally fine-tuned Diffusers models.
|
|
1511
2205
|
"""
|
|
1512
2206
|
|
|
1513
2207
|
if current_path:
|
|
@@ -1515,7 +2209,7 @@ def get_available_image_models(current_path=None):
|
|
|
1515
2209
|
|
|
1516
2210
|
all_image_models = []
|
|
1517
2211
|
|
|
1518
|
-
|
|
2212
|
+
# Add models configured via environment variables
|
|
1519
2213
|
env_image_model = os.getenv("NPCSH_IMAGE_MODEL")
|
|
1520
2214
|
env_image_provider = os.getenv("NPCSH_IMAGE_PROVIDER")
|
|
1521
2215
|
|
|
@@ -1526,9 +2220,8 @@ def get_available_image_models(current_path=None):
|
|
|
1526
2220
|
"display_name": f"{env_image_model} | {env_image_provider} (Configured)"
|
|
1527
2221
|
})
|
|
1528
2222
|
|
|
1529
|
-
|
|
2223
|
+
# Add predefined models (OpenAI, Gemini, and standard Diffusers)
|
|
1530
2224
|
for provider_key, models_list in IMAGE_MODELS.items():
|
|
1531
|
-
|
|
1532
2225
|
if provider_key == "openai":
|
|
1533
2226
|
if os.environ.get("OPENAI_API_KEY"):
|
|
1534
2227
|
all_image_models.extend([
|
|
@@ -1541,16 +2234,25 @@ def get_available_image_models(current_path=None):
|
|
|
1541
2234
|
{**model, "provider": provider_key, "display_name": f"{model['display_name']} | {provider_key}"}
|
|
1542
2235
|
for model in models_list
|
|
1543
2236
|
])
|
|
1544
|
-
elif provider_key == "diffusers":
|
|
1545
|
-
|
|
1546
|
-
|
|
2237
|
+
elif provider_key == "diffusers": # This entry in IMAGE_MODELS is for standard diffusers
|
|
1547
2238
|
all_image_models.extend([
|
|
1548
2239
|
{**model, "provider": provider_key, "display_name": f"{model['display_name']} | {provider_key}"}
|
|
1549
2240
|
for model in models_list
|
|
1550
2241
|
])
|
|
1551
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}")
|
|
1552
2254
|
|
|
1553
|
-
|
|
2255
|
+
# Deduplicate models
|
|
1554
2256
|
seen_models = set()
|
|
1555
2257
|
unique_models = []
|
|
1556
2258
|
for model_entry in all_image_models:
|
|
@@ -1559,6 +2261,7 @@ def get_available_image_models(current_path=None):
|
|
|
1559
2261
|
seen_models.add(key)
|
|
1560
2262
|
unique_models.append(model_entry)
|
|
1561
2263
|
|
|
2264
|
+
# Return the combined, deduplicated list of models as a dictionary with a 'models' key
|
|
1562
2265
|
return unique_models
|
|
1563
2266
|
|
|
1564
2267
|
@app.route('/api/generative_fill', methods=['POST'])
|
|
@@ -1893,14 +2596,24 @@ def get_mcp_tools():
|
|
|
1893
2596
|
It will try to use an existing client from corca_states if available and matching,
|
|
1894
2597
|
otherwise it creates a temporary client.
|
|
1895
2598
|
"""
|
|
1896
|
-
|
|
2599
|
+
raw_server_path = request.args.get("mcpServerPath")
|
|
2600
|
+
current_path_arg = request.args.get("currentPath")
|
|
1897
2601
|
conversation_id = request.args.get("conversationId")
|
|
1898
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()]
|
|
1899
2605
|
|
|
1900
|
-
if not
|
|
2606
|
+
if not raw_server_path:
|
|
1901
2607
|
return jsonify({"error": "mcpServerPath parameter is required."}), 400
|
|
1902
2608
|
|
|
1903
|
-
|
|
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
|
+
|
|
1904
2617
|
try:
|
|
1905
2618
|
from npcsh.corca import MCPClientNPC
|
|
1906
2619
|
except ImportError:
|
|
@@ -1917,13 +2630,19 @@ def get_mcp_tools():
|
|
|
1917
2630
|
and existing_corca_state.mcp_client.server_script_path == server_path:
|
|
1918
2631
|
print(f"Using existing MCP client for {state_key} to fetch tools.")
|
|
1919
2632
|
temp_mcp_client = existing_corca_state.mcp_client
|
|
1920
|
-
|
|
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})
|
|
1921
2637
|
|
|
1922
2638
|
|
|
1923
2639
|
print(f"Creating a temporary MCP client to fetch tools for {server_path}.")
|
|
1924
2640
|
temp_mcp_client = MCPClientNPC()
|
|
1925
2641
|
if temp_mcp_client.connect_sync(server_path):
|
|
1926
|
-
|
|
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})
|
|
1927
2646
|
else:
|
|
1928
2647
|
return jsonify({"error": f"Failed to connect to MCP server at {server_path}."}), 500
|
|
1929
2648
|
except FileNotFoundError as e:
|
|
@@ -1942,6 +2661,64 @@ def get_mcp_tools():
|
|
|
1942
2661
|
temp_mcp_client.disconnect_sync()
|
|
1943
2662
|
|
|
1944
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
|
+
|
|
1945
2722
|
@app.route("/api/image_models", methods=["GET"])
|
|
1946
2723
|
def get_image_models_api():
|
|
1947
2724
|
"""
|
|
@@ -1950,6 +2727,7 @@ def get_image_models_api():
|
|
|
1950
2727
|
current_path = request.args.get("currentPath")
|
|
1951
2728
|
try:
|
|
1952
2729
|
image_models = get_available_image_models(current_path)
|
|
2730
|
+
print('image models', image_models)
|
|
1953
2731
|
return jsonify({"models": image_models, "error": None})
|
|
1954
2732
|
except Exception as e:
|
|
1955
2733
|
print(f"Error getting available image models: {str(e)}")
|
|
@@ -1961,34 +2739,224 @@ def get_image_models_api():
|
|
|
1961
2739
|
|
|
1962
2740
|
|
|
1963
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}!")
|
|
1964
2759
|
|
|
1965
|
-
|
|
1966
|
-
|
|
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():
|
|
1967
2825
|
data = request.json
|
|
1968
|
-
|
|
2826
|
+
|
|
1969
2827
|
stream_id = data.get("streamId")
|
|
1970
2828
|
if not stream_id:
|
|
1971
|
-
import uuid
|
|
1972
2829
|
stream_id = str(uuid.uuid4())
|
|
1973
2830
|
|
|
1974
2831
|
with cancellation_lock:
|
|
1975
2832
|
cancellation_flags[stream_id] = False
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
provider = available_models.get(model)
|
|
1984
|
-
|
|
1985
|
-
npc_name = data.get("npc", None)
|
|
1986
|
-
npc_source = data.get("npcSource", "global")
|
|
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))
|
|
1987
2840
|
current_path = data.get("currentPath")
|
|
1988
|
-
|
|
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
|
+
|
|
1989
2846
|
if current_path:
|
|
1990
|
-
|
|
1991
|
-
|
|
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")
|
|
2931
|
+
|
|
2932
|
+
@app.route("/api/stream", methods=["POST"])
|
|
2933
|
+
def stream():
|
|
2934
|
+
data = request.json
|
|
2935
|
+
|
|
2936
|
+
stream_id = data.get("streamId")
|
|
2937
|
+
if not stream_id:
|
|
2938
|
+
import uuid
|
|
2939
|
+
stream_id = str(uuid.uuid4())
|
|
2940
|
+
|
|
2941
|
+
with cancellation_lock:
|
|
2942
|
+
cancellation_flags[stream_id] = False
|
|
2943
|
+
print(f"Starting stream with ID: {stream_id}")
|
|
2944
|
+
|
|
2945
|
+
commandstr = data.get("commandstr")
|
|
2946
|
+
conversation_id = data.get("conversationId")
|
|
2947
|
+
model = data.get("model", None)
|
|
2948
|
+
provider = data.get("provider", None)
|
|
2949
|
+
if provider is None:
|
|
2950
|
+
provider = available_models.get(model)
|
|
2951
|
+
|
|
2952
|
+
npc_name = data.get("npc", None)
|
|
2953
|
+
npc_source = data.get("npcSource", "global")
|
|
2954
|
+
current_path = data.get("currentPath")
|
|
2955
|
+
is_resend = data.get("isResend", False) # ADD THIS LINE
|
|
2956
|
+
|
|
2957
|
+
if current_path:
|
|
2958
|
+
loaded_vars = load_project_env(current_path)
|
|
2959
|
+
print(f"Loaded project env variables for stream request: {list(loaded_vars.keys())}")
|
|
1992
2960
|
|
|
1993
2961
|
npc_object = None
|
|
1994
2962
|
team_object = None
|
|
@@ -2155,7 +3123,9 @@ def stream():
|
|
|
2155
3123
|
if 'tools' in tool_args and tool_args['tools']:
|
|
2156
3124
|
tool_args['tool_choice'] = {"type": "auto"}
|
|
2157
3125
|
|
|
2158
|
-
|
|
3126
|
+
# Default stream response so closures below always have a value
|
|
3127
|
+
stream_response = {"output": "", "messages": messages}
|
|
3128
|
+
|
|
2159
3129
|
exe_mode = data.get('executionMode','chat')
|
|
2160
3130
|
|
|
2161
3131
|
if exe_mode == 'chat':
|
|
@@ -2230,23 +3200,18 @@ def stream():
|
|
|
2230
3200
|
messages = state.messages
|
|
2231
3201
|
|
|
2232
3202
|
elif exe_mode == 'corca':
|
|
2233
|
-
|
|
2234
3203
|
try:
|
|
2235
3204
|
from npcsh.corca import execute_command_corca, create_corca_state_and_mcp_client, MCPClientNPC
|
|
2236
3205
|
from npcsh._state import initial_state as state
|
|
2237
3206
|
except ImportError:
|
|
2238
|
-
|
|
2239
3207
|
print("ERROR: npcsh.corca or MCPClientNPC not found. Corca mode is disabled.", file=sys.stderr)
|
|
2240
|
-
state = None
|
|
3208
|
+
state = None
|
|
2241
3209
|
stream_response = {"output": "Corca mode is not available due to missing dependencies.", "messages": messages}
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
if state is not None:
|
|
2245
|
-
|
|
3210
|
+
|
|
3211
|
+
if state is not None:
|
|
2246
3212
|
mcp_server_path_from_request = data.get("mcpServerPath")
|
|
2247
3213
|
selected_mcp_tools_from_request = data.get("selectedMcpTools", [])
|
|
2248
|
-
|
|
2249
|
-
|
|
3214
|
+
|
|
2250
3215
|
effective_mcp_server_path = mcp_server_path_from_request
|
|
2251
3216
|
if not effective_mcp_server_path and team_object and hasattr(team_object, 'team_ctx') and team_object.team_ctx:
|
|
2252
3217
|
mcp_servers_list = team_object.team_ctx.get('mcp_servers', [])
|
|
@@ -2254,18 +3219,19 @@ def stream():
|
|
|
2254
3219
|
first_server_obj = next((s for s in mcp_servers_list if isinstance(s, dict) and 'value' in s), None)
|
|
2255
3220
|
if first_server_obj:
|
|
2256
3221
|
effective_mcp_server_path = first_server_obj['value']
|
|
2257
|
-
elif isinstance(team_object.team_ctx.get('mcp_server'), str):
|
|
3222
|
+
elif isinstance(team_object.team_ctx.get('mcp_server'), str):
|
|
2258
3223
|
effective_mcp_server_path = team_object.team_ctx.get('mcp_server')
|
|
2259
3224
|
|
|
2260
|
-
|
|
3225
|
+
if effective_mcp_server_path:
|
|
3226
|
+
effective_mcp_server_path = os.path.abspath(os.path.expanduser(effective_mcp_server_path))
|
|
3227
|
+
|
|
2261
3228
|
if not hasattr(app, 'corca_states'):
|
|
2262
3229
|
app.corca_states = {}
|
|
2263
|
-
|
|
3230
|
+
|
|
2264
3231
|
state_key = f"{conversation_id}_{npc_name or 'default'}"
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
if
|
|
2268
|
-
|
|
3232
|
+
corca_state = app.corca_states.get(state_key)
|
|
3233
|
+
|
|
3234
|
+
if corca_state is None:
|
|
2269
3235
|
corca_state = create_corca_state_and_mcp_client(
|
|
2270
3236
|
conversation_id=conversation_id,
|
|
2271
3237
|
command_history=command_history,
|
|
@@ -2276,21 +3242,21 @@ def stream():
|
|
|
2276
3242
|
)
|
|
2277
3243
|
app.corca_states[state_key] = corca_state
|
|
2278
3244
|
else:
|
|
2279
|
-
corca_state = app.corca_states[state_key]
|
|
2280
3245
|
corca_state.npc = npc_object
|
|
2281
3246
|
corca_state.team = team_object
|
|
2282
3247
|
corca_state.current_path = current_path
|
|
2283
3248
|
corca_state.messages = messages
|
|
2284
3249
|
corca_state.command_history = command_history
|
|
2285
3250
|
|
|
2286
|
-
|
|
2287
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))
|
|
2288
3254
|
|
|
2289
3255
|
if effective_mcp_server_path != current_mcp_client_path:
|
|
2290
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'}.")
|
|
2291
3257
|
if corca_state.mcp_client and corca_state.mcp_client.session:
|
|
2292
3258
|
corca_state.mcp_client.disconnect_sync()
|
|
2293
|
-
corca_state.mcp_client = None
|
|
3259
|
+
corca_state.mcp_client = None
|
|
2294
3260
|
|
|
2295
3261
|
if effective_mcp_server_path:
|
|
2296
3262
|
new_mcp_client = MCPClientNPC()
|
|
@@ -2300,20 +3266,19 @@ def stream():
|
|
|
2300
3266
|
else:
|
|
2301
3267
|
print(f"Failed to reconnect MCP client for {state_key} to {effective_mcp_server_path}. Corca will have no tools.")
|
|
2302
3268
|
corca_state.mcp_client = None
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
3269
|
+
|
|
2306
3270
|
state, stream_response = execute_command_corca(
|
|
2307
3271
|
commandstr,
|
|
2308
3272
|
corca_state,
|
|
2309
3273
|
command_history,
|
|
2310
|
-
selected_mcp_tools_names=selected_mcp_tools_from_request
|
|
3274
|
+
selected_mcp_tools_names=selected_mcp_tools_from_request
|
|
2311
3275
|
)
|
|
2312
|
-
|
|
2313
|
-
|
|
3276
|
+
|
|
2314
3277
|
app.corca_states[state_key] = state
|
|
2315
|
-
messages = state.messages
|
|
3278
|
+
messages = state.messages
|
|
2316
3279
|
|
|
3280
|
+
else:
|
|
3281
|
+
stream_response = {"output": f"Unsupported execution mode: {exe_mode}", "messages": messages}
|
|
2317
3282
|
|
|
2318
3283
|
user_message_filled = ''
|
|
2319
3284
|
|
|
@@ -2321,20 +3286,25 @@ def stream():
|
|
|
2321
3286
|
for cont in messages[-1].get('content'):
|
|
2322
3287
|
txt = cont.get('text')
|
|
2323
3288
|
if txt is not None:
|
|
2324
|
-
user_message_filled +=txt
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
3289
|
+
user_message_filled += txt
|
|
3290
|
+
|
|
3291
|
+
# Only save user message if it's NOT a resend
|
|
3292
|
+
if not is_resend: # ADD THIS CONDITION
|
|
3293
|
+
save_conversation_message(
|
|
3294
|
+
command_history,
|
|
3295
|
+
conversation_id,
|
|
3296
|
+
"user",
|
|
3297
|
+
user_message_filled if len(user_message_filled) > 0 else commandstr,
|
|
3298
|
+
wd=current_path,
|
|
3299
|
+
model=model,
|
|
3300
|
+
provider=provider,
|
|
3301
|
+
npc=npc_name,
|
|
3302
|
+
team=team,
|
|
3303
|
+
attachments=attachments_for_db,
|
|
3304
|
+
message_id=message_id,
|
|
3305
|
+
)
|
|
3306
|
+
|
|
3307
|
+
|
|
2338
3308
|
|
|
2339
3309
|
|
|
2340
3310
|
message_id = generate_message_id()
|
|
@@ -2349,44 +3319,44 @@ def stream():
|
|
|
2349
3319
|
if isinstance(stream_response, str) :
|
|
2350
3320
|
print('stream a str and not a gen')
|
|
2351
3321
|
chunk_data = {
|
|
2352
|
-
"id": None,
|
|
2353
|
-
"object": None,
|
|
2354
|
-
"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'),
|
|
2355
3325
|
"model": model,
|
|
2356
3326
|
"choices": [
|
|
2357
3327
|
{
|
|
2358
|
-
"index": 0,
|
|
2359
|
-
"delta":
|
|
3328
|
+
"index": 0,
|
|
3329
|
+
"delta":
|
|
2360
3330
|
{
|
|
2361
3331
|
"content": stream_response,
|
|
2362
3332
|
"role": "assistant"
|
|
2363
|
-
},
|
|
3333
|
+
},
|
|
2364
3334
|
"finish_reason": 'done'
|
|
2365
3335
|
}
|
|
2366
3336
|
]
|
|
2367
3337
|
}
|
|
2368
|
-
yield f"data: {json.dumps(chunk_data)}"
|
|
3338
|
+
yield f"data: {json.dumps(chunk_data)}\n\n"
|
|
2369
3339
|
return
|
|
2370
3340
|
elif isinstance(stream_response, dict) and 'output' in stream_response and isinstance(stream_response.get('output'), str):
|
|
2371
|
-
print('stream a str and not a gen')
|
|
3341
|
+
print('stream a str and not a gen')
|
|
2372
3342
|
chunk_data = {
|
|
2373
|
-
"id": None,
|
|
2374
|
-
"object": None,
|
|
2375
|
-
"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'),
|
|
2376
3346
|
"model": model,
|
|
2377
3347
|
"choices": [
|
|
2378
3348
|
{
|
|
2379
|
-
"index": 0,
|
|
2380
|
-
"delta":
|
|
3349
|
+
"index": 0,
|
|
3350
|
+
"delta":
|
|
2381
3351
|
{
|
|
2382
3352
|
"content": stream_response.get('output') ,
|
|
2383
3353
|
"role": "assistant"
|
|
2384
|
-
},
|
|
3354
|
+
},
|
|
2385
3355
|
"finish_reason": 'done'
|
|
2386
3356
|
}
|
|
2387
3357
|
]
|
|
2388
3358
|
}
|
|
2389
|
-
yield f"data: {json.dumps(chunk_data)}"
|
|
3359
|
+
yield f"data: {json.dumps(chunk_data)}\n\n"
|
|
2390
3360
|
return
|
|
2391
3361
|
for response_chunk in stream_response.get('response', stream_response.get('output')):
|
|
2392
3362
|
with cancellation_lock:
|
|
@@ -2414,8 +3384,8 @@ def stream():
|
|
|
2414
3384
|
if chunk_content:
|
|
2415
3385
|
complete_response.append(chunk_content)
|
|
2416
3386
|
chunk_data = {
|
|
2417
|
-
"id": None, "object": None,
|
|
2418
|
-
"created": response_chunk["created_at"] or datetime.datetime.now(),
|
|
3387
|
+
"id": None, "object": None,
|
|
3388
|
+
"created": response_chunk["created_at"] or datetime.datetime.now(),
|
|
2419
3389
|
"model": response_chunk["model"],
|
|
2420
3390
|
"choices": [{"index": 0, "delta": {"content": chunk_content, "role": response_chunk["message"]["role"]}, "finish_reason": response_chunk.get("done_reason")}]
|
|
2421
3391
|
}
|
|
@@ -2449,36 +3419,85 @@ def stream():
|
|
|
2449
3419
|
print(f"\nAn exception occurred during streaming for {current_stream_id}: {e}")
|
|
2450
3420
|
traceback.print_exc()
|
|
2451
3421
|
interrupted = True
|
|
2452
|
-
|
|
3422
|
+
|
|
2453
3423
|
finally:
|
|
2454
3424
|
print(f"\nStream {current_stream_id} finished. Interrupted: {interrupted}")
|
|
2455
3425
|
print('\r' + ' ' * dot_count*2 + '\r', end="", flush=True)
|
|
2456
3426
|
|
|
2457
3427
|
final_response_text = ''.join(complete_response)
|
|
3428
|
+
|
|
3429
|
+
# Yield message_stop immediately so the client's stream ends quickly
|
|
2458
3430
|
yield f"data: {json.dumps({'type': 'message_stop'})}\n\n"
|
|
2459
|
-
|
|
3431
|
+
|
|
3432
|
+
# Save assistant message to the database
|
|
2460
3433
|
npc_name_to_save = npc_object.name if npc_object else ''
|
|
2461
3434
|
save_conversation_message(
|
|
2462
|
-
command_history,
|
|
2463
|
-
conversation_id,
|
|
2464
|
-
"assistant",
|
|
3435
|
+
command_history,
|
|
3436
|
+
conversation_id,
|
|
3437
|
+
"assistant",
|
|
2465
3438
|
final_response_text,
|
|
2466
|
-
wd=current_path,
|
|
2467
|
-
model=model,
|
|
3439
|
+
wd=current_path,
|
|
3440
|
+
model=model,
|
|
2468
3441
|
provider=provider,
|
|
2469
|
-
npc=npc_name_to_save,
|
|
2470
|
-
team=team,
|
|
3442
|
+
npc=npc_name_to_save,
|
|
3443
|
+
team=team,
|
|
2471
3444
|
message_id=message_id,
|
|
2472
3445
|
)
|
|
2473
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
|
+
|
|
2474
3468
|
with cancellation_lock:
|
|
2475
3469
|
if current_stream_id in cancellation_flags:
|
|
2476
3470
|
del cancellation_flags[current_stream_id]
|
|
2477
3471
|
print(f"Cleaned up cancellation flag for stream ID: {current_stream_id}")
|
|
2478
|
-
|
|
2479
3472
|
return Response(event_stream(stream_id), mimetype="text/event-stream")
|
|
2480
3473
|
|
|
2481
|
-
|
|
3474
|
+
@app.route('/api/delete_message', methods=['POST'])
|
|
3475
|
+
def delete_message():
|
|
3476
|
+
data = request.json
|
|
3477
|
+
conversation_id = data.get('conversationId')
|
|
3478
|
+
message_id = data.get('messageId')
|
|
3479
|
+
|
|
3480
|
+
if not conversation_id or not message_id:
|
|
3481
|
+
return jsonify({"error": "Missing conversationId or messageId"}), 400
|
|
3482
|
+
|
|
3483
|
+
try:
|
|
3484
|
+
command_history = CommandHistory(app.config.get('DB_PATH'))
|
|
3485
|
+
|
|
3486
|
+
# Delete the message from the database
|
|
3487
|
+
result = command_history.delete_message(conversation_id, message_id)
|
|
3488
|
+
|
|
3489
|
+
print(f"[DELETE_MESSAGE] Deleted message {message_id} from conversation {conversation_id}. Rows affected: {result}")
|
|
3490
|
+
|
|
3491
|
+
return jsonify({
|
|
3492
|
+
"success": True,
|
|
3493
|
+
"deletedMessageId": message_id,
|
|
3494
|
+
"rowsAffected": result
|
|
3495
|
+
}), 200
|
|
3496
|
+
|
|
3497
|
+
except Exception as e:
|
|
3498
|
+
print(f"[DELETE_MESSAGE] Error: {e}")
|
|
3499
|
+
traceback.print_exc()
|
|
3500
|
+
return jsonify({"error": str(e)}), 500
|
|
2482
3501
|
|
|
2483
3502
|
@app.route("/api/memory/approve", methods=["POST"])
|
|
2484
3503
|
def approve_memories():
|
|
@@ -2503,295 +3522,6 @@ def approve_memories():
|
|
|
2503
3522
|
|
|
2504
3523
|
|
|
2505
3524
|
|
|
2506
|
-
@app.route("/api/execute", methods=["POST"])
|
|
2507
|
-
def execute():
|
|
2508
|
-
data = request.json
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
stream_id = data.get("streamId")
|
|
2512
|
-
if not stream_id:
|
|
2513
|
-
import uuid
|
|
2514
|
-
stream_id = str(uuid.uuid4())
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
with cancellation_lock:
|
|
2518
|
-
cancellation_flags[stream_id] = False
|
|
2519
|
-
print(f"Starting execute stream with ID: {stream_id}")
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
commandstr = data.get("commandstr")
|
|
2523
|
-
conversation_id = data.get("conversationId")
|
|
2524
|
-
model = data.get("model", 'llama3.2')
|
|
2525
|
-
provider = data.get("provider", 'ollama')
|
|
2526
|
-
if provider is None:
|
|
2527
|
-
provider = available_models.get(model)
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
npc_name = data.get("npc", "sibiji")
|
|
2531
|
-
npc_source = data.get("npcSource", "global")
|
|
2532
|
-
team = data.get("team", None)
|
|
2533
|
-
current_path = data.get("currentPath")
|
|
2534
|
-
|
|
2535
|
-
if current_path:
|
|
2536
|
-
loaded_vars = load_project_env(current_path)
|
|
2537
|
-
print(f"Loaded project env variables for stream request: {list(loaded_vars.keys())}")
|
|
2538
|
-
|
|
2539
|
-
npc_object = None
|
|
2540
|
-
team_object = None
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
if team:
|
|
2544
|
-
print(team)
|
|
2545
|
-
if hasattr(app, 'registered_teams') and team in app.registered_teams:
|
|
2546
|
-
team_object = app.registered_teams[team]
|
|
2547
|
-
print(f"Using registered team: {team}")
|
|
2548
|
-
else:
|
|
2549
|
-
print(f"Warning: Team {team} not found in registered teams")
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
if npc_name:
|
|
2553
|
-
|
|
2554
|
-
if team and hasattr(app, 'registered_teams') and team in app.registered_teams:
|
|
2555
|
-
team_object = app.registered_teams[team]
|
|
2556
|
-
print('team', team_object)
|
|
2557
|
-
|
|
2558
|
-
if hasattr(team_object, 'npcs'):
|
|
2559
|
-
team_npcs = team_object.npcs
|
|
2560
|
-
if isinstance(team_npcs, dict):
|
|
2561
|
-
if npc_name in team_npcs:
|
|
2562
|
-
npc_object = team_npcs[npc_name]
|
|
2563
|
-
print(f"Found NPC {npc_name} in registered team {team}")
|
|
2564
|
-
elif isinstance(team_npcs, list):
|
|
2565
|
-
for npc in team_npcs:
|
|
2566
|
-
if hasattr(npc, 'name') and npc.name == npc_name:
|
|
2567
|
-
npc_object = npc
|
|
2568
|
-
print(f"Found NPC {npc_name} in registered team {team}")
|
|
2569
|
-
break
|
|
2570
|
-
|
|
2571
|
-
if not npc_object and hasattr(team_object, 'forenpc') and hasattr(team_object.forenpc, 'name'):
|
|
2572
|
-
if team_object.forenpc.name == npc_name:
|
|
2573
|
-
npc_object = team_object.forenpc
|
|
2574
|
-
print(f"Found NPC {npc_name} as forenpc in team {team}")
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
if not npc_object and hasattr(app, 'registered_npcs') and npc_name in app.registered_npcs:
|
|
2578
|
-
npc_object = app.registered_npcs[npc_name]
|
|
2579
|
-
print(f"Found NPC {npc_name} in registered NPCs")
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
if not npc_object:
|
|
2583
|
-
db_conn = get_db_connection()
|
|
2584
|
-
npc_object = load_npc_by_name_and_source(npc_name, npc_source, db_conn, current_path)
|
|
2585
|
-
|
|
2586
|
-
if not npc_object and npc_source == 'project':
|
|
2587
|
-
print(f"NPC {npc_name} not found in project directory, trying global...")
|
|
2588
|
-
npc_object = load_npc_by_name_and_source(npc_name, 'global', db_conn)
|
|
2589
|
-
|
|
2590
|
-
if npc_object:
|
|
2591
|
-
print(f"Successfully loaded NPC {npc_name} from {npc_source} directory")
|
|
2592
|
-
else:
|
|
2593
|
-
print(f"Warning: Could not load NPC {npc_name}")
|
|
2594
|
-
|
|
2595
|
-
attachments = data.get("attachments", [])
|
|
2596
|
-
command_history = CommandHistory(app.config.get('DB_PATH'))
|
|
2597
|
-
images = []
|
|
2598
|
-
attachments_loaded = []
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
if attachments:
|
|
2602
|
-
for attachment in attachments:
|
|
2603
|
-
extension = attachment["name"].split(".")[-1]
|
|
2604
|
-
extension_mapped = extension_map.get(extension.upper(), "others")
|
|
2605
|
-
file_path = os.path.expanduser("~/.npcsh/" + extension_mapped + "/" + attachment["name"])
|
|
2606
|
-
if extension_mapped == "images":
|
|
2607
|
-
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
|
2608
|
-
img = Image.open(attachment["path"])
|
|
2609
|
-
img_byte_arr = BytesIO()
|
|
2610
|
-
img.save(img_byte_arr, format="PNG")
|
|
2611
|
-
img_byte_arr.seek(0)
|
|
2612
|
-
img.save(file_path, optimize=True, quality=50)
|
|
2613
|
-
images.append(file_path)
|
|
2614
|
-
attachments_loaded.append({
|
|
2615
|
-
"name": attachment["name"], "type": extension_mapped,
|
|
2616
|
-
"data": img_byte_arr.read(), "size": os.path.getsize(file_path)
|
|
2617
|
-
})
|
|
2618
|
-
|
|
2619
|
-
messages = fetch_messages_for_conversation(conversation_id)
|
|
2620
|
-
if len(messages) == 0 and npc_object is not None:
|
|
2621
|
-
messages = [{'role': 'system', 'content': npc_object.get_system_prompt()}]
|
|
2622
|
-
elif len(messages)>0 and messages[0]['role'] != 'system' and npc_object is not None:
|
|
2623
|
-
messages.insert(0, {'role': 'system', 'content': npc_object.get_system_prompt()})
|
|
2624
|
-
elif len(messages) > 0 and npc_object is not None:
|
|
2625
|
-
messages[0]['content'] = npc_object.get_system_prompt()
|
|
2626
|
-
if npc_object is not None and messages and messages[0]['role'] == 'system':
|
|
2627
|
-
messages[0]['content'] = npc_object.get_system_prompt()
|
|
2628
|
-
|
|
2629
|
-
message_id = generate_message_id()
|
|
2630
|
-
save_conversation_message(
|
|
2631
|
-
command_history, conversation_id, "user", commandstr,
|
|
2632
|
-
wd=current_path, model=model, provider=provider, npc=npc_name,
|
|
2633
|
-
team=team, attachments=attachments_loaded, message_id=message_id,
|
|
2634
|
-
)
|
|
2635
|
-
response_gen = check_llm_command(
|
|
2636
|
-
commandstr, messages=messages, images=images, model=model,
|
|
2637
|
-
provider=provider, npc=npc_object, team=team_object, stream=True
|
|
2638
|
-
)
|
|
2639
|
-
print(response_gen)
|
|
2640
|
-
|
|
2641
|
-
message_id = generate_message_id()
|
|
2642
|
-
|
|
2643
|
-
def event_stream(current_stream_id):
|
|
2644
|
-
complete_response = []
|
|
2645
|
-
dot_count = 0
|
|
2646
|
-
interrupted = False
|
|
2647
|
-
tool_call_data = {"id": None, "function_name": None, "arguments": ""}
|
|
2648
|
-
memory_data = None
|
|
2649
|
-
|
|
2650
|
-
try:
|
|
2651
|
-
for response_chunk in stream_response.get('response', stream_response.get('output')):
|
|
2652
|
-
with cancellation_lock:
|
|
2653
|
-
if cancellation_flags.get(current_stream_id, False):
|
|
2654
|
-
print(f"Cancellation flag triggered for {current_stream_id}. Breaking loop.")
|
|
2655
|
-
interrupted = True
|
|
2656
|
-
break
|
|
2657
|
-
|
|
2658
|
-
print('.', end="", flush=True)
|
|
2659
|
-
dot_count += 1
|
|
2660
|
-
|
|
2661
|
-
if "hf.co" in model or provider == 'ollama':
|
|
2662
|
-
chunk_content = response_chunk["message"]["content"] if "message" in response_chunk and "content" in response_chunk["message"] else ""
|
|
2663
|
-
if "message" in response_chunk and "tool_calls" in response_chunk["message"]:
|
|
2664
|
-
for tool_call in response_chunk["message"]["tool_calls"]:
|
|
2665
|
-
if "id" in tool_call:
|
|
2666
|
-
tool_call_data["id"] = tool_call["id"]
|
|
2667
|
-
if "function" in tool_call:
|
|
2668
|
-
if "name" in tool_call["function"]:
|
|
2669
|
-
tool_call_data["function_name"] = tool_call["function"]["name"]
|
|
2670
|
-
if "arguments" in tool_call["function"]:
|
|
2671
|
-
arg_val = tool_call["function"]["arguments"]
|
|
2672
|
-
if isinstance(arg_val, dict):
|
|
2673
|
-
arg_val = json.dumps(arg_val)
|
|
2674
|
-
tool_call_data["arguments"] += arg_val
|
|
2675
|
-
if chunk_content:
|
|
2676
|
-
complete_response.append(chunk_content)
|
|
2677
|
-
chunk_data = {
|
|
2678
|
-
"id": None, "object": None, "created": response_chunk["created_at"], "model": response_chunk["model"],
|
|
2679
|
-
"choices": [{"index": 0, "delta": {"content": chunk_content, "role": response_chunk["message"]["role"]}, "finish_reason": response_chunk.get("done_reason")}]
|
|
2680
|
-
}
|
|
2681
|
-
yield f"data: {json.dumps(chunk_data)}\n\n"
|
|
2682
|
-
else:
|
|
2683
|
-
chunk_content = ""
|
|
2684
|
-
reasoning_content = ""
|
|
2685
|
-
for choice in response_chunk.choices:
|
|
2686
|
-
if hasattr(choice.delta, "tool_calls") and choice.delta.tool_calls:
|
|
2687
|
-
for tool_call in choice.delta.tool_calls:
|
|
2688
|
-
if tool_call.id:
|
|
2689
|
-
tool_call_data["id"] = tool_call.id
|
|
2690
|
-
if tool_call.function:
|
|
2691
|
-
if hasattr(tool_call.function, "name") and tool_call.function.name:
|
|
2692
|
-
tool_call_data["function_name"] = tool_call.function.name
|
|
2693
|
-
if hasattr(tool_call.function, "arguments") and tool_call.function.arguments:
|
|
2694
|
-
tool_call_data["arguments"] += tool_call.function.arguments
|
|
2695
|
-
for choice in response_chunk.choices:
|
|
2696
|
-
if hasattr(choice.delta, "reasoning_content"):
|
|
2697
|
-
reasoning_content += choice.delta.reasoning_content
|
|
2698
|
-
chunk_content = "".join(choice.delta.content for choice in response_chunk.choices if choice.delta.content is not None)
|
|
2699
|
-
if chunk_content:
|
|
2700
|
-
complete_response.append(chunk_content)
|
|
2701
|
-
chunk_data = {
|
|
2702
|
-
"id": response_chunk.id, "object": response_chunk.object, "created": response_chunk.created, "model": response_chunk.model,
|
|
2703
|
-
"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]
|
|
2704
|
-
}
|
|
2705
|
-
yield f"data: {json.dumps(chunk_data)}\n\n"
|
|
2706
|
-
|
|
2707
|
-
except Exception as e:
|
|
2708
|
-
print(f"\nAn exception occurred during streaming for {current_stream_id}: {e}")
|
|
2709
|
-
traceback.print_exc()
|
|
2710
|
-
interrupted = True
|
|
2711
|
-
|
|
2712
|
-
finally:
|
|
2713
|
-
print(f"\nStream {current_stream_id} finished. Interrupted: {interrupted}")
|
|
2714
|
-
print('\r' + ' ' * dot_count*2 + '\r', end="", flush=True)
|
|
2715
|
-
|
|
2716
|
-
final_response_text = ''.join(complete_response)
|
|
2717
|
-
|
|
2718
|
-
conversation_turn_text = f"User: {commandstr}\nAssistant: {final_response_text}"
|
|
2719
|
-
|
|
2720
|
-
try:
|
|
2721
|
-
memory_examples = command_history.get_memory_examples_for_context(
|
|
2722
|
-
npc=npc_name,
|
|
2723
|
-
team=team,
|
|
2724
|
-
directory_path=current_path
|
|
2725
|
-
)
|
|
2726
|
-
|
|
2727
|
-
memory_context = format_memory_context(memory_examples)
|
|
2728
|
-
|
|
2729
|
-
facts = get_facts(
|
|
2730
|
-
conversation_turn_text,
|
|
2731
|
-
model=npc_object.model if npc_object else model,
|
|
2732
|
-
provider=npc_object.provider if npc_object else provider,
|
|
2733
|
-
npc=npc_object,
|
|
2734
|
-
context=memory_context
|
|
2735
|
-
)
|
|
2736
|
-
|
|
2737
|
-
if facts:
|
|
2738
|
-
memories_for_approval = []
|
|
2739
|
-
for i, fact in enumerate(facts):
|
|
2740
|
-
memory_id = command_history.add_memory_to_database(
|
|
2741
|
-
message_id=f"{conversation_id}_{datetime.now().strftime('%H%M%S')}_{i}",
|
|
2742
|
-
conversation_id=conversation_id,
|
|
2743
|
-
npc=npc_name or "default",
|
|
2744
|
-
team=team or "default",
|
|
2745
|
-
directory_path=current_path or "/",
|
|
2746
|
-
initial_memory=fact['statement'],
|
|
2747
|
-
status="pending_approval",
|
|
2748
|
-
model=npc_object.model if npc_object else model,
|
|
2749
|
-
provider=npc_object.provider if npc_object else provider
|
|
2750
|
-
)
|
|
2751
|
-
|
|
2752
|
-
memories_for_approval.append({
|
|
2753
|
-
"memory_id": memory_id,
|
|
2754
|
-
"content": fact['statement'],
|
|
2755
|
-
"context": f"Type: {fact.get('type', 'unknown')}, Source: {fact.get('source_text', '')}",
|
|
2756
|
-
"npc": npc_name or "default"
|
|
2757
|
-
})
|
|
2758
|
-
|
|
2759
|
-
memory_data = {
|
|
2760
|
-
"type": "memory_approval",
|
|
2761
|
-
"memories": memories_for_approval,
|
|
2762
|
-
"conversation_id": conversation_id
|
|
2763
|
-
}
|
|
2764
|
-
|
|
2765
|
-
except Exception as e:
|
|
2766
|
-
print(f"Memory generation error: {e}")
|
|
2767
|
-
|
|
2768
|
-
if memory_data:
|
|
2769
|
-
yield f"data: {json.dumps(memory_data)}\n\n"
|
|
2770
|
-
|
|
2771
|
-
yield f"data: {json.dumps({'type': 'message_stop'})}\n\n"
|
|
2772
|
-
|
|
2773
|
-
npc_name_to_save = npc_object.name if npc_object else ''
|
|
2774
|
-
save_conversation_message(
|
|
2775
|
-
command_history,
|
|
2776
|
-
conversation_id,
|
|
2777
|
-
"assistant",
|
|
2778
|
-
final_response_text,
|
|
2779
|
-
wd=current_path,
|
|
2780
|
-
model=model,
|
|
2781
|
-
provider=provider,
|
|
2782
|
-
npc=npc_name_to_save,
|
|
2783
|
-
team=team,
|
|
2784
|
-
message_id=message_id,
|
|
2785
|
-
)
|
|
2786
|
-
|
|
2787
|
-
with cancellation_lock:
|
|
2788
|
-
if current_stream_id in cancellation_flags:
|
|
2789
|
-
del cancellation_flags[current_stream_id]
|
|
2790
|
-
print(f"Cleaned up cancellation flag for stream ID: {current_stream_id}")
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
return Response(event_stream(stream_id), mimetype="text/event-stream")
|
|
2795
3525
|
|
|
2796
3526
|
@app.route("/api/interrupt", methods=["POST"])
|
|
2797
3527
|
def interrupt_stream():
|